01 背景
最近使用AI开发了一个安卓GB28181推流APP,采集手机音视频进行编码压缩然后封装为PS流,再打包RTP发送给ZLMediaKit,但是每次通过接口/index/api/getMediaList查询推流信息,都需要大约十秒左右的时间才能看到推流被注册到ZLMediaKit 。
由于自己对安卓开发一窍不通,并且app推流涉及摄像头视频采集,硬件编码,PS头封装,RTP打包等众多环节,总以为是AI写的代码有问题,然后开始排查APP的代码,调试各种参数,排查防火墙,甚至在app代码里加了很多时间打印,排查到底是哪个环节耗时十秒,但始终找不到问题的原因,
随后,登录到服务器看了下ZLMediaKit 的日志,看到:
2026-06-22 14:09:36.814 D [zlmedia-server] [159682-event poller 0] WebApi.cpp:266 http api debug | GET /index/api/openRtpServer?secret=18kj72&port=0&enable_tcp=1&stream_id=34020000001320000011
2026-06-22 14:09:37.577 I [zlmedia-server] [159682-event poller 1] GB28181Process.cpp:182 onRtpDecode | 34020000001320000011 judged to be PS
2026-06-22 14:09:47.607 W [zlmedia-server] [159682-event poller 1] MediaSink.cpp:169 emitAllTrackReady | Track not ready for a long time, ignored: PCMA
2026-06-22 14:09:47.607 I [zlmedia-server] [159682-event poller 1] MediaSource.cpp:517 emitEvent | 媒体注册:fmp4://__defaultVhost__/rtp/34020000001320000011
可以看到APP在收到invite播放请求之后,马上就调用ZLMediaKit的API请求开放接收码流的端口,然后ZLMediaKit 在2026-06-22 14:09:37.547接收到了app推送的PS流, 但是在2026-06-22 14:09:47.607也就是十秒之后,ZLMediaKit 才将推流注册完成。
Track not ready for a long time, ignored: PCMA
看来这个日志信息是重点。
根本问题是:ZLMediaKit 在 PS 流里检测到了 PCMA 音频 track 的声明(PSM/System Header 里有 stream_id=0xC0),但客户端从来不发音频数据,于是服务端一直等音频 track ready,直到 10 秒超时才忽略音频并注册媒体流。
那么我们详细分析一下,如果ZLMediaKit 在 PS 流里检测到了 PCMA 音频 track 的声明(PSM/System Header 里有 stream_id=0xC0),但客户端从来不发音频数据,会如何处理?
这里的「PSM/System Header 里有 stream_id=0xC0」需要区分两处:
- System Header(
00 00 01 BB)只声明 stream_id 的存在与 buffer_bound,不携带 codec 类型。 - PSM(Program Stream Map,
00 00 01 BC)才是真正声明 codec 的地方。每个 elementary stream 条目是s
tream_type(1B) + elementary_stream_id(1B) + es_info_length(2B) + descriptors。
- 对 PCMA:
stream_type = 0x90 ( PSI_STREAM_AUDIO_G711A ),
sid = 0xC0 (音频流号范围0xC0~0xDF)。
- 所以「PCMA 声明」的来源是 PSM,下面分析即以此为准。
02 整体调用链
RtpSession::onRtpPacket
└─ RtpProcess::inputRtp (src/Rtp/RtpProcess.cpp:102)
└─ GB28181Process::inputRtp (src/Rtp/GB28181Process.cpp:71)
└─ CommonRtpDecoder 解 RTP → PS 帧
└─ onRtpDecode → DecoderImp(decoder_ps) + PSDecoder (Decoder.cpp:194)
└─ ps_demuxer_input (3rdpart/media-server/libmpeg)
├─ case PES_SID_PSM → psm_read → ps_demuxer_notify → onstream 回调
└─ case 0xC0~0xDF → psm_fetch → pes_read_header → PES 负载
└─ DecoderImp::onStream / onDecode
└─ _sink->addTrack / addTrackCompleted / inputFrame
└─ RtpProcess → MultiMediaSourceMuxer(MediaSink)
关键文件:
src/Rtp/GB28181Process.cppsrc/Rtp/RtpProcess.cpp,src/Rtp/Decoder.cpp、src/Rtp/PSDecoder.cppsrc/Common/MediaSink.cpp(核心逻辑所在)3rdpart/media-server/libmpeg/source/mpeg-ps-dec.c、mpeg-psm.cext-codec/G711.cpp/.h
03 PSM 解析与 Track 创建
1. PSM 解析(mpeg-psm.cpsm_read)
遍历 PSM 里的每个 elementary stream 条目,写入 psm->streams[i]:
cid = mpeg_bits_read8(reader); // 0x90 (G711A)
sid = mpeg_bits_read8(reader); // 0xC0
stream = psm_fetch(psm, sid);
stream->codecid = cid; stream->sid = sid;
psm->streams[16],最多 16 路。
2. PSM 变更触发 notify(mpeg-ps-dec.c)
case PES_SID_PSM:
n = ps->psm.stream_count;
r = psm_read(&ps->psm, reader);
if (n != ps->psm.stream_count || ps->ver != ps->psm.ver)
ps_demuxer_notify(ps); // 遍历所有 stream,逐个回调 onstream,最后一条 finish=1
3. ZLMediaKit 侧创建 Track(Decoder.cpponStream)
auto codec = getCodecByMpegId(codecid); // 0x90 → CodecG711A
auto track = Factory::getTrackByCodecId(codec); // → G711Track
onTrack(stream, track); // _sink->addTrack(track)
if (finish && _have_video) {
_finished = true;
_sink->addTrackCompleted(); // 关键:把 max_track_size 锁定为当前 track 数(=2)
}
4. G711Track 的 ready() 恒为 true(ext-codec/G711.h + Track.h)
class G711Track : public AudioTrackImp { ... }; // 8000/1/16 写死
// AudioTrackImp:
bool ready() const override { return true; } // G711 无需任何带内元数据
这一点很关键:PCMA track 一被创建 ready() 就返回 true。但「ready」不等于「会被当成有效 track」,见下文。
04 MediaSink 的 Track 就绪/超时机制
src/Common/MediaSink.cpp MediaSink 内部维护:
unordered_map<int, pair<Track::Ptr, bool /*got frame*/>> _track_map;
unordered_map<int, function<void()>> _track_ready_callback; // 未触发 onTrackReady 的
size_t _max_track_size = 2; // addTrackCompleted 后被设为 2
Ticker _ticker; // 每次 addTrack 都 resetTime()
每来一帧 → inputFrame → checkTrackIfReady(),逻辑分三段:
① onTrackReady 触发条件:收到过帧 && ready()
for (auto &pr : _track_map) {
if (pr.second.second /*gotFrame*/ && pr.second.first->ready()) {
// 触发 onTrackReady,并 erase 掉该 callback
}
}
视频:收帧后拿到 sps/pps → ready()=true → callback 被清掉 PCMA:ready()=true,但 gotFrame=false(从不发音频)→ callback 一直留在 map 里,阻塞 all-track-ready
GET_CONFIG(uint32_t, kWaitAudioTrackDataMS, General::kWaitAudioTrackDataMS); // 默认 1000ms
if (_max_track_size > 1) {
for (auto it = _track_map.begin(); it != _track_map.end();) {
if (it->second.first->getTrackType() == TrackAudio
&& _ticker.elapsedTime() > kWaitAudioTrackDataMS
&& !it->second.second /*从未收到音频帧*/) {
WarnL << ”Audio track index ” << index << ” codec ”
<< it->second.first->getCodecName()
<< ” receive no data for long ” << _ticker.elapsedTime()
<< ”ms. Ignore it!”;
it = _track_map.erase(it); // ① 把 PCMA track 直接踢掉
_max_track_size -= 1; // ② 2 → 1,不再等音频
_track_ready_callback.erase(index); // ③ 清掉它的 ready 回调
} else ++it;
}
}
而且 config.h 里这条配置的注释写明了就是为这个场景设计的:
// 最多等待音频Track收到数据时间,单位毫秒,超时且完全没收到音频数据,忽略音频Track
// 加快某些带封装的流metadata说明有音频,但是实际上没有的流ready时间(比如很多厂商的GB28181 PS)
extern const std::string kWaitAudioTrackDataMS; // 默认 1000
③ 触发 emitAllTrackReady
音频被踢掉后:
_track_map.size()==1、_max_track_size==1、_track_ready_callback 已空 →
if (_track_map.size() == _max_track_size) {
emitAllTrackReady(); // 流以「纯视频」上线
return;
}
emitAllTrackReady() 里:
- 清空剩余 callback,移除任何
!gotFrame || !ready()的 track; onAllTrackReady_l():若protocol.add_mute_audio=1(默认开)且当前_track_map里没有音频 →addMuteAudioTrack()补一条静音 AAC track(index=0xFFFF,由视频帧驱动MuteAudioMaker产生),保证下游 RTMP/FLV/HLS 拿到完整 A/V;onAllTrackReady()→ MediaSource 注册,播放器可拉流;- 回放之前缓存在
_frame_unread的视频帧(未就绪期最多缓存general.unready_frame_cache=100帧,超出清空防 OOM)。
④ 兜底超时
即便 ①②③ 没命中,general.wait_track_ready_ms(默认 10000ms)到点也会强制 emitAllTrackReady(),移除所有未就绪 track。如果一个有效 track 都没有,抛 SockException(Err_shutdown, "no vaild track data")。
05 背景
相关配置默认值(config.cpp)
| 配置项 | 默认值 | 作用 |
|---|---|---|
general.wait_audio_track_data_ms | 1000 | 音频声明后无数据 → 忽略 |
general.wait_track_ready_ms | 10000 | 全局 track 就绪兜底超时 |
general.wait_add_track_ms | 3000 | 单 track 时再等多 track |
general.unready_frame_cache | 100 | 未就绪 track 缓存帧上限 |
protocol.enable_audio | 1 | 全局开关音频 |
protocol.add_mute_audio | 1 | 是否补静音 AAC |
06 结论
PS 流里 PSM 检测到 PCMA(stream_id=0xC0,stream_type=0x90)的声明,但客户端从来不发音频数据,ZLMediaKit 会如何处理?
PSM 解析阶段:
psm_read 把
sid=0xC0 / codecid=0x90
写入 psm->streams[];
ps_demuxer_notify 逐路上报,
DecoderImp::onStream 经getCodecByMpegId(0x90)=CodecG711A
创建 G711Track 并 _sink->addTrack()。
因视频先就绪,
finish && _have_video 成立,
addTrackCompleted() 把 _max_track_size 锁定为 2。
- Track 已创建但永不就绪:
G711Track::ready()虽恒为true,但MediaSink的onTrackReady触发条件是 「收到过帧 && ready()」。音频一帧未到 →gotFrame=false→ 其 ready 回调一直留在_track_ready_callback,emitAllTrackReady被阻塞,流暂时无法注册。 - 音频无数据超时(核心):
MediaSink::checkTrackIfReady()每收一帧视频都会检查——若某音频 track 在general.wait_audio_track_data_ms(默认1000ms)内gotFrame仍为 false,则:
- 打印
WarnL << "Audio track index ... receive no data for long ...ms. Ignore it!"
- 从
_track_map中 erase 掉 PCMA track; _max_track_size 由 2 减为 1;- 清除其 ready 回调。
这条机制正是 config.h 注释里专门为「很多厂商的 GB28181 PS:metadata 声明有音频但实际没有」而设计的。
- 流以纯视频上线:音频被踢后
_track_map.size()==_max_track_size==1且回调已空 →emitAllTrackReady()→onAllTrackReady()→ MediaSource 注册,播放器可拉流;缓存的视频帧随后一次性输出。 - 静音 AAC 补轨(可选):若
protocol.add_mute_audio=1(默认开)且此刻_track_map中已无音频,addMuteAudioTrack()会补一条静音 AAC track,由视频帧驱动MuteAudioMaker生成,使下游 RTMP/FLV/HLS 仍是完整 A/V。 - 兜底:若上述路径未触发,
general.wait_track_ready_ms(默认 10000ms)到点强制emitAllTrackReady(),移除所有!gotFrame||!ready()的 track;若最终零有效 track,抛SockException(Err_shutdown, "no vaild track data")断开。 - 可配置:若该路 GB28181 通过
only_track=kOnlyVideo推流(RtpProcess::emitOnPublish调muxer->enableAudio(false)),则 PCMA 在MediaSink::addTrack阶段就被直接拒绝(return false),连 1 秒等待都省了——适合已知设备无音频的场景,可加快首屏。
一句话总结:
ZLMediaKit 不会因为 PSM 声明了 PCMA 就死等音频。
它会先用 PSM 声明把 _max_track_size 设成 2 并把 PCMA track 挂进 MediaSink,但只要 1 秒内没收到任何音频帧,就在 checkTrackIfReady 里把该音频 track 直接丢弃、_max_track_size 减 1,
随后以纯视频(必要时补静音 AAC)完成 onAllTrackReady 让流注册上线;
10 秒是最终兜底。
本文来自作者投稿,版权归原作者所有。如需转载,请注明出处:https://www.nxrte.com/jishu/yinshipin/68453.html