ZLMediaKit 接收/处理PS流分析

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.cpp
  • src/Rtp/RtpProcess.cpp,
  • src/Rtp/Decoder.cpp
  • src/Rtp/PSDecoder.cpp
  • src/Common/MediaSink.cpp(核心逻辑所在)
  • 3rdpart/media-server/libmpeg/source/mpeg-ps-dec.c、mpeg-psm.c
  • ext-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_ms1000音频声明后无数据 → 忽略
general.wait_track_ready_ms10000全局 track 就绪兜底超时
general.wait_add_track_ms3000单 track 时再等多 track
general.unready_frame_cache100未就绪 track 缓存帧上限
protocol.enable_audio1全局开关音频
protocol.add_mute_audio1是否补静音 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。
  1. Track 已创建但永不就绪
    G711Track::ready() 虽恒为 true,但 MediaSink 的 onTrackReady 触发条件是 「收到过帧 && ready()」。音频一帧未到 → gotFrame=false → 其 ready 回调一直留在 _track_ready_callbackemitAllTrackReady 被阻塞,流暂时无法注册。
  2. 音频无数据超时(核心)
    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 声明有音频但实际没有」而设计的。

  1. 流以纯视频上线:音频被踢后 _track_map.size()==_max_track_size==1 且回调已空 → emitAllTrackReady() → onAllTrackReady() → MediaSource 注册,播放器可拉流;缓存的视频帧随后一次性输出。
  2. 静音 AAC 补轨(可选):若 protocol.add_mute_audio=1(默认开)且此刻 _track_map 中已无音频,addMuteAudioTrack() 会补一条静音 AAC track,由视频帧驱动 MuteAudioMaker 生成,使下游 RTMP/FLV/HLS 仍是完整 A/V。
  3. 兜底:若上述路径未触发,general.wait_track_ready_ms(默认 10000ms)到点强制 emitAllTrackReady(),移除所有 !gotFrame||!ready() 的 track;若最终零有效 track,抛 SockException(Err_shutdown, "no vaild track data") 断开。
  4. 可配置:若该路 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

(0)

相关推荐