Qt/C++ 手撸 RTMP 服务端:从握手到协议桥接的完整实现
前言前面写了 RTMP 推流和拉流客户端这篇补齐服务端。服务端是整条链路的枢纽——它同时收 RTMP 推流、RTSP 推流然后通过 Muxer 把同一路流分发给不同协议的拉流客户端。本文围绕RtmpSession服务端单个 RTMP 会话展开每个协议步骤都给出真实十六进制字节 中文逐字段注释。读完你能掌握RTMP 服务端三次握手的字节交换connect / createStream / play / publish 的处理推流方向如何解析 avcC/ASC 并构建 Track拉流方向如何生成 AVCDecoderConfigurationRecordRTMP 推 → RTSP 拉的协议桥接机制一、RtmpSession 定位RtmpSession是一个 TCP 连接对应一个实例同时支持两个方向方向触发数据流拉流客户端发playMediaSource → RtmpMuxer → 客户端推流客户端发publish客户端 → 解析 → MediaSource一个类共用两个方向通过_playing/_publishing两个标志区分。整体数据流RTMP 客户端推流 ──► RtmpSession::handlePublish │ ├─ 解析 avcC → VideoTrackextradata AnnexB SPSPPS ├─ 解析 ASC → AudioTrackextradata ASC └─ MediaSource::writeFrame(frame) │ ▼ RingBuffer │ 分发 ┌───────────┴────────────┐ ▼ ▼ RtspMuxer RtmpMuxer │ │ ▼ ▼ RTSP 客户端拉流 RTMP 客户端拉流二、三次握手2.1 客户端 → 服务器C0 C1字节序列┌──────────────────────────────────────────────────────────────────┐ │ 03 ← C0: 版本号 0x03 │ ├──────────────────────────────────────────────────────────────────┤ │ 00 00 00 00 ← C1 前 4 字节客户端时间戳ms │ │ 00 00 00 00 ← C1 第 5~8 字节保留零 │ │ 8D 4C B6 2E 91 0F 5A C3 ... ← C1 第 9~1536 字节随机 1528 字节 │ │ ...共 1536 字节 │ └──────────────────────────────────────────────────────────────────┘中文说明字段大小含义C0.version1B固定 0x03C1.timestamp4B客户端启动时间戳毫秒C1.zero4B固定 0x00000000C1.random1528B随机字节2.2 服务器 → 客户端S0 S1 S2QByteArray makeServerS0S1S2(const QByteArray c1) { QByteArray out; out.append(char(0x03)); // S0 QByteArray s1(1536, \0); for (int i 8; i 1536; i) s1[i] char(i 0xFF); out.append(s1); // S1服务器数据 out.append(c1); // S2 C1 回显简单握手 return out; }字节序列03 ← S0: 版本号 0x03 00 00 00 00 ← S1 前 4 字节服务器时间戳 00 00 00 00 ← S1 第 5~8 字节保留零 08 09 0A 0B 0C 0D 0E 0F ... ← S1 第 9~1536 字节随机数据 ...共 1536 字节 C1 的完整 1536 字节回显 ← S2 客户端的 C1总长度1 1536 1536 3073 字节2.3 客户端 → 服务器C2C1 之后的 S1 拷贝1536 字节服务器不校验 C2握手完成。2.4 服务端处理代码void RtmpSession::processHandshake() { constexpr int c0c1Size 1 1536; // C0(1) C1(1536) if (_hsBuf.size() c0c1Size) return; // 数据不足等下次 if (uint8_t(_hsBuf[0]) ! 0x03) { // 版本号必须 0x03 _sock-abort(); // 非法直接断开 return; } QByteArray c1 _hsBuf.mid(1, 1536); // 提取 C1 _sock-write(makeServerS0S1S2(c1)); // 回 S0S1S2 _hsBuf.remove(0, c0c1Size); // 消费 C0C1 _hsDone true; // 标记完成 if (_hsBuf.size() 1536) // 若 C2 也到了 _hsBuf.remove(0, 1536); // 一并消费 }三、connect 命令3.1 客户端 → 服务器RTMP 消息结构03 ← Basic Header: fmt0, csid3 00 00 00 ← timestamp 0 00 00 C0 ← length 192 (0xC0) 14 ← typeId 20 (MSG_CMD) 00 00 00 00 ← streamId 0 (小端) ├──────────────────────────────────────────────────────────┤ │ 02 00 07 63 6F 6E 6E 65 63 74 │ String connect │ 00 3F F0 00 00 00 00 00 00 │ Number 1.0 (txn) │ 03 │ Object 开始 │ 00 03 61 70 70 02 00 04 6C 69 76 65 │ key app → String live │ 00 05 74 63 55 72 6C │ key tcUrl │ 02 00 1D 72 74 6D 70 3A 2F 2F ... │ → rtmp://127.0.0.1:1935/live │ 00 08 66 6C 61 73 68 56 65 72 │ key flashVer │ 02 00 0C 46 4D 4C 45 2F 33 2E 30 │ → FMLE/3.0 │ 00 0B 61 75 64 69 6F 43 6F 64 65 63 73 │ key audioCodecs │ 00 40 B0 00 00 00 00 00 00 │ → Number 1024 (AAC) │ 00 0B 76 69 64 65 6F 43 6F 64 65 63 73 │ key videoCodecs │ 00 40 80 00 00 00 00 00 00 │ → Number 128 (H.264) │ 00 00 09 │ Object 结束 └──────────────────────────────────────────────────────────┘3.2 服务端解析void RtmpSession::handleConnect(double txn, AmfDecoder dec) { _connectTxn txn; // 保存事务号 AmfValue obj dec.readValue(); // 读参数对象 _app obj.value(app).asString(); // 取出 app live sendConnectResult(txn); // 回复 _result }3.3 服务器 → 客户端_result字节序列03 00 00 00 00 00 B8 14 00 00 00 00 ← Chunk Msg 头 (length184) 02 00 07 5F 72 65 73 75 6C 74 ← String _result 00 3F F0 00 00 00 00 00 00 ← Number 1.0 (txn) 05 ← Null 03 ← Object 1: 服务器信息 00 06 66 6D 73 56 65 72 ← key fmsVer 02 00 0F 46 4D 53 2F 33 2C 30 2C 31 2C 31 32 33 ← FMS/3,0,1,123 00 0C 63 61 70 61 62 69 6C 69 74 69 65 73 ← key capabilities 00 40 3F 80 00 00 00 00 00 ← Number 31 00 00 09 ← Object 1 结束 03 ← Object 2: 结果信息 00 05 6C 65 76 65 6C ← key level 02 00 06 73 74 61 74 75 73 ← status 00 04 63 6F 64 65 ← key code 02 00 1F 4E 65 74 43 6F 6E 6E 65 63 74 69 6F 6E 2E 43 6F 6E 6E 65 63 74 2E 53 75 63 63 65 73 73 ← NetConnection.Connect.Success 00 0B 64 65 73 63 72 69 70 74 69 6F 6E ← key description 02 00 15 43 6F 6E 6E 65 63 74 69 6F 6E 20 73 75 63 63 65 65 64 65 64 2E ← Connection succeeded. 00 00 09 ← Object 2 结束中文说明字段值含义_result命令名响应1.0事务号与 connect 对应Object 1fmsVer / capabilities服务器信息Object 2levelstatus /codeNetConnection.Connect.Success★ 成功标志Object 2description描述3.4 服务端代码void RtmpSession::sendConnectResult(double txn) { AmfEncoder enc; enc.writeString(_result); // 命令名 enc.writeNumber(txn); // 事务号 AmfValue cmdObj AmfValue::makeObject(); cmdObj.set(fmsVer, AmfValue::makeString(FMS/3,0,1,123)); cmdObj.set(capabilities, AmfValue::makeNumber(31)); enc.writeObject(cmdObj); AmfValue info AmfValue::makeObject(); info.set(level, AmfValue::makeString(status)); info.set(code, AmfValue::makeString(NetConnection.Connect.Success)); info.set(description, AmfValue::makeString(Connection succeeded.)); info.set(objectEncoding, AmfValue::makeNumber(0)); enc.writeObject(info); writeRtmpMessage(MSG_CMD, 0, CHUNK_SYSTEM, enc.data()); // 顺带发 Window Ack Size 5MB uint8_t buf[4]; ByteOrder::set_be32(buf, 5000000); writeRtmpMessage(MSG_WIN_SIZE, 0, CHUNK_NETWORK, QByteArray(reinterpret_castconst char*(buf), 4)); }四、createStream 命令4.1 客户端 → 服务器03 00 00 00 00 00 19 14 00 00 00 00 ← Chunk Msg 头 (length25) 02 00 0C 63 72 65 61 74 65 53 74 72 65 61 6D ← String createStream 00 40 00 00 00 00 00 00 00 ← Number 2.0 (txn) 05 ← Null4.2 服务器 → 客户端03 00 00 00 00 00 15 14 00 00 00 00 ← Chunk Msg 头 (length21) 02 00 07 5F 72 65 73 75 6C 74 ← _result 00 40 00 00 00 00 00 00 00 ← txn 2.0 05 ← Null 00 3F F0 00 00 00 00 00 00 ← ★ streamId 1.04.3 服务端代码void RtmpSession::handleCreateStream(double txn) { _createStreamTxn txn; sendCreateStreamResult(txn); } void RtmpSession::sendCreateStreamResult(double txn) { AmfEncoder enc; enc.writeString(_result); enc.writeNumber(txn); enc.writeNull(); enc.writeNumber(1); // ★ streamId 1简化版写死 _streamId 1; writeRtmpMessage(MSG_CMD, 0, CHUNK_SYSTEM, enc.data()); }⚠️ 写死 streamId 的隐患同一连接多次createStream会返回相同 ID。生产环境应改成自增static std::atomicuint32_t gNextStreamId{1}; _streamId gNextStreamId;五、拉流方向play5.1 客户端 → 服务器03 00 00 00 00 00 20 14 01 00 00 00 ← ★ streamId 1小端 02 00 04 70 6C 61 79 ← String play 00 40 08 00 00 00 00 00 00 ← txn 3.0 05 ← Null 02 00 04 74 65 73 74 ← String test流名 00 C0 9F 40 00 00 00 00 00 ← Number -2000.0负数live5.2 服务端处理流程void RtmpSession::handlePlay(double txn, const QString name) { QString streamName extractStreamName(name); // test MediaTuple tuple; tuple.schema rtmp; // 先按 rtmp 找 tuple.vhost 127.0.0.1; tuple.app _app; // live tuple.stream streamName; // test // ★ 协议桥接先按 rtmp 找找不到换 rtsp _playSource MediaSourceManager::instance().find(tuple); if (!_playSource) { MediaTuple rtspTuple tuple; rtspTuple.schema rtsp; _playSource MediaSourceManager::instance().find(rtspTuple); } if (!_playSource) { sendPlayOnStatus(NetStream.Play.StreamNotFound, Stream not found); return; } // ★ 从 StreamManager 拿共享 Muxer没有就现场创建 auto muxer std::dynamic_pointer_castRtmpMuxer( StreamManager::instance().getRtmpMuxer(_playSource)); if (!muxer) { muxer std::make_sharedRtmpMuxer(_playSource); muxer-start(); StreamManager::instance().registerRtmpMuxer(_playSource, muxer); } _rtmpMuxer muxer; _rtmpMuxer-addSession(this); // ★ 加入 Muxer 分发列表 // 回复三段 onStatus sendPlayOnStatus(NetStream.Play.Start, Started playing.); sendPlayOnStatus(NetStream.Data.Start, ); sendPlayOnStatus(NetStream.Play.Reset, ); // 发 onMetaData config sendOnMetaData(_playSource); sendVideoConfig(_playSource); sendAudioConfig(_playSource); _playing true; }5.3 服务器 → 客户端三段 onStatusonStatus (Play.Reset)03 00 00 00 00 00 7A 14 01 00 00 00 ← streamId 1 02 00 08 6F 6E 53 74 61 74 75 73 ← onStatus 00 00 00 00 00 00 00 00 00 ← txn 0 05 ← Null 03 ... 00 00 09 ← Object level status code NetStream.Play.Reset description Playing and resetting.onStatus (Play.Start)code NetStream.Play.Start ← ★ 播放开始标志 description Started playing.onStatus (Data.Start)code NetStream.Data.Start description 5.4 服务器 → 客户端onMetaData字节序列03 00 00 00 00 01 06 12 01 00 00 00 ← Chunk Msg 头 (length262) 02 00 0D 40 73 65 74 44 61 74 61 46 72 61 6D 65 ← String setDataFrame 02 00 0A 6F 6E 4D 65 74 61 44 61 74 61 ← String onMetaData 08 ← ECMA Array 00 00 00 07 ← count 7 00 08 64 75 72 61 74 69 6F 6E ← key duration 00 00 00 00 00 00 00 00 ← 0 00 05 77 69 64 74 68 ← key width 00 40 88 00 00 00 00 00 00 ← 1920 00 06 68 65 69 67 68 74 ← key height 00 40 87 00 00 00 00 00 00 ← 1080 00 09 66 72 61 6D 65 72 61 74 65 ← key framerate 00 40 39 00 00 00 00 00 00 ← 25 00 0C 76 69 64 65 6F 63 6F 64 65 63 69 64 ← key videocodecid 00 40 1C 00 00 00 00 00 00 ← 7 (H.264) 00 0C 61 75 64 69 6F 63 6F 64 65 63 69 64 ← key audiocodecid 00 40 24 00 00 00 00 00 00 ← 10 (AAC) 00 00 09 ← 结束5.5 服务器 → 客户端Video Sequence Header这一步最关键——服务端要把 MediaSource 里存的内存的 AnnexB extradata 转成AVCDecoderConfigurationRecord再发出去。字节序列05 00 00 00 00 00 2A 09 01 00 00 00 ← streamId 1 17 ← Video Tag Header (key AVC) 00 ← AVCPacketType 0 (Sequence Header) 00 00 00 ← CompositionTime 01 ← ★ configurationVersion 1 42 C0 1E ← profile / compat / level FF ← reserved(6) | lengthSizeMinusOne3 E1 ← reserved(3) | numOfSPS1 00 19 ← SPS 长度 25 67 42 C0 1E D9 00 50 05 BB 01 10 00 00 03 00 10 00 00 03 03 C8 F1 22 6A 2C ← SPS 数据 01 ← numOfPPS 1 00 05 ← PPS 长度 5 68 CE 3C 80 ← PPS 数据服务端代码void RtmpSession::sendVideoConfig(const MediaSource::Ptr src) { auto vt std::dynamic_pointer_castVideoTrack(src-findVideoTrack()); QByteArray extra vt-extradata(); // AnnexB SPSPPS // 从 AnnexB 提取 SPS/PPS QListQByteArray spsList, ppsList; const uint8_t* d reinterpret_castconst uint8_t*(extra.constData()); int size extra.size(), pos 0, start -1; while (pos size) { bool atEnd (pos size); bool sc3 (!atEnd pos3 size d[pos]0 d[pos1]0 d[pos2]1); bool sc4 (!atEnd pos4 size d[pos]0 d[pos1]0 d[pos2]0 d[pos3]1); if (atEnd || sc3 || sc4) { if (start 0 pos start) { QByteArray nalu(reinterpret_castconst char*(d start), pos - start); uint8_t t uint8_t(nalu[0]) 0x1F; if (t 7) spsList.append(nalu); else if (t 8) ppsList.append(nalu); } if (atEnd) break; pos sc4 ? 4 : 3; start pos; } else pos; } // 拼 AVCDecoderConfigurationRecord QByteArray cfg; cfg.append(char(1)); // configurationVersion cfg.append(sps[1]); cfg.append(sps[2]); cfg.append(sps[3]); cfg.append(char(0xFF)); // lengthSizeMinusOne3 cfg.append(char(0xE1)); // numOfSPS1 uint8_t lb[2]; ByteOrder::set_be16(lb, uint16_t(sps.size())); cfg.append(reinterpret_castconst char*(lb), 2); cfg.append(sps); cfg.append(char(0x01)); // numOfPPS1 ByteOrder::set_be16(lb, uint16_t(pps.size())); cfg.append(reinterpret_castconst char*(lb), 2); cfg.append(pps); // 拼 RTMP video tag QByteArray body; body.append(char(0x17)); // key AVC body.append(char(0x00)); // Sequence Header body.append(3, \0); // CompositionTime body.append(cfg); writeRtmpMessage(MSG_VIDEO, _streamId, CHUNK_VIDEO, body, 0); }5.6 服务器 → 客户端Audio Sequence Header字节序列04 00 00 00 00 00 04 08 01 00 00 00 AF ← Audio Tag Header (AAC/44k/16/stereo) 00 ← AAC sequence header 12 10 ← AudioSpecificConfigASC 解析12 100001 0010 0001 0000audioObjectType 2 (AAC-LC) samplingFreqIdx 4 (44100 Hz) channelConfig 2 (stereo)5.7 服务器 → 客户端媒体帧由RtmpMuxer从 MediaSource 拿到 Frame 后转发bool RtmpMuxer::onTrackFrame(const Frame::Ptr frame) { if (frame-isVideo()) { QByteArray body makeVideoBody(frame); // AnnexB → AVCC dispatch(MSG_VIDEO, body, videoTs(frame)); } // ... }视频帧字节05 00 03 E8 00 0A B6 09 01 00 00 00 ← timestamp 1000ms 17 ← key AVC 01 ← NALU 00 00 00 ← CompositionTime 00 00 0A B2 ← 第一个 NALU 长度 65 88 84 ... ← IDR NALU ...六、推流方向publish6.1 客户端 → 服务器03 00 00 00 00 00 20 14 01 00 00 00 ← streamId 1 02 00 07 70 75 62 6C 69 73 68 ← String publish 00 40 08 00 00 00 00 00 00 ← txn 3.0 05 ← Null 02 00 04 74 65 73 74 ← String test流名 02 00 04 6C 69 76 65 ← String live推流类型6.2 服务端处理void RtmpSession::handlePublish(double txn, const QString name, const QString type) { _publishTxn txn; _publishType type; // live _streamName extractStreamName(name); MediaTuple tuple; tuple.schema rtmp; tuple.vhost 127.0.0.1; tuple.app _app; tuple.stream _streamName; _publishSource std::make_sharedMediaSource(tuple); // 创建 MediaSource _publishRegistered false; _pubVideoTrack.reset(); _pubAudioTrack.reset(); _videoConfigReady false; _audioConfigReady false; _pubBaseDts -1; // 音视频共享 DTS 基准 sendPublishOnStatus(NetStream.Publish.Start, Started publishing.); _publishing true; }6.3 服务器响应03 00 00 00 00 00 XX 14 01 00 00 00 02 00 08 6F 6E 53 74 61 74 75 73 00 00 00 00 00 00 00 00 00 05 03 ... 00 00 09 level status code NetStream.Publish.Start description Started publishing.6.4 客户端 → 服务器onMetaData和拉流方向一样但是客户端发给服务端03 00 00 00 00 01 06 12 01 00 00 00 02 00 0D setDataFrame 02 00 0A onMetaData 08 00 00 00 07 00 08 duration 00 00 00 00 00 00 00 00 00 05 width 00 40 88 00 00 00 00 00 00 ← 1920 00 06 height 00 40 87 00 00 00 00 00 00 ← 1080 00 09 framerate 00 40 39 00 00 00 00 00 00 ← 25 00 0C videocodecid 00 40 1C 00 00 00 00 00 00 ← 7 00 0C audiocodecid 00 40 24 00 00 00 00 00 00 ← 10 00 00 09服务端缓存元数据void RtmpSession::handleData(const RtmpPacket::Ptr pkt) { AmfDecoder dec(Buffer(pkt-body)); QString first dec.readString(); // setDataFrame QString metaName first; if (first setDataFrame) { metaName dec.readString(); // onMetaData } if (metaName ! onMetaData) return; AmfValue meta dec.readValue(); _meta.width meta.value(width).asInt(); _meta.height meta.value(height).asInt(); _meta.fps meta.value(framerate).asInt(); _meta.sampleRate meta.value(audiosamplerate).asInt(); _meta.channels meta.value(stereo).asBool() ? 2 : 1; _meta.valid true; }6.5 客户端 → 服务器Video Sequence Header05 00 00 00 00 00 2A 09 01 00 00 00 17 ← key AVC 00 ← Sequence Header 00 00 00 ← CompositionTime 01 ← configurationVersion 42 C0 1E FF E1 00 19 ← 头 67 42 C0 1E D9 ... 25 字节 SPS ← SPS 01 00 05 68 CE 3C 80 ← PPS服务端处理void RtmpSession::handlePublishVideo(const RtmpPacket::Ptr pkt) { const uint8_t* p (const uint8_t*)pkt-body.constData(); uint8_t frameType (p[0] 4) 0x0F; uint8_t codecNum p[0] 0x0F; // 7 AVC uint8_t avcType p[1]; // 0 Sequence Header if (codecNum ! 7) return; // 建立 DTS 基准 if (_pubBaseDts 0) _pubBaseDts int64_t(pkt-timeStamp); uint64_t dts pkt-timeStamp - _pubBaseDts; if (avcType 0) { QByteArray avcCfg pkt-body.mid(5); // ★ avcC → AnnexB SPSPPS int naluLenSize 4; QByteArray annexBExtra avcConfigToAnnexB(avcCfg, naluLenSize); if (!_pubVideoTrack) { _pubVideoTrack std::make_sharedVideoTrack(CodecId::H264, 0); _pubVideoTrack-setWidth(_meta.width); _pubVideoTrack-setHeight(_meta.height); _pubVideoTrack-setFps(_meta.fps); _pubVideoTrack-setNaluLengthSize(naluLenSize); _pubVideoTrack-setExtradata(annexBExtra); // AnnexB 存入 extradata _publishSource-addTrack(_pubVideoTrack); } _videoConfigReady true; // config 帧写入 MediaSource关键帧标记供 RingBuffer GOP 使用 auto frame FrameFactory::create( CodecId::H264, annexBExtra, 0, 0, false, true, 0); _publishSource-writeFrame(frame, false); finalizePublishSource(); // 检查所有 track 是否 ready return; } }avcC → AnnexB 代码static QByteArray avcConfigToAnnexB(const QByteArray cfg, int naluLenSize) { // cfg 结构[version][profile][compat][level][FF][E1][spsLen][sps][01][ppsLen][pps] naluLenSize (p[4] 0x03) 1; // 从 [4] 读长度字段宽度 int off 5; int numSps p[off] 0x1F; // numOfSPS QByteArray out; // 遍历 SPS for (int i 0; i numSps; i) { int len (p[off] 8) | p[off1]; off 2; out.append(\x00\x00\x00\x01, 4); // 起始码 out.append((const char*)(p off), len); // SPS 数据 off len; } // 遍历 PPS int numPps p[off]; for (int i 0; i numPps; i) { int len (p[off] 8) | p[off1]; off 2; out.append(\x00\x00\x00\x01, 4); out.append((const char*)(p off), len); off len; } return out; }输出00 00 00 01 67 42 C0 1E ...SPS 00 00 00 01 68 CE 3C 80 PPS6.6 客户端 → 服务器Video FrameAVCC05 00 03 E8 00 0A B6 09 01 00 00 00 17 ← key AVC 01 ← NALU 00 00 00 ← CompositionTime 00 00 0A B2 65 88 84 ... ← AVCC 数据长度前缀服务端处理if (avcType 1) { QByteArray avccData pkt-body.mid(5); // ★ AVCC → AnnexB QByteArray annexB avccToAnnexB(avccData, _pubVideoTrack-naluLengthSize()); bool key (frameType 1); auto frame FrameFactory::create( CodecId::H264, annexB, dts, dts, key, false, 0); _publishSource-writeFrame(frame, key); // ★ 写入 MediaSource }6.7 客户端 → 服务器Audio Sequence Header04 00 00 00 00 00 04 08 01 00 00 00 AF ← Audio Tag Header 00 ← AAC sequence header 12 10 ← AudioSpecificConfig服务端处理void RtmpSession::handlePublishAudio(const RtmpPacket::Ptr pkt) { const uint8_t* p (const uint8_t*)pkt-body.constData(); uint8_t soundFormat (p[0] 4) 0x0F; // 10 AAC uint8_t aacType p[1]; // 0 Sequence Header if (soundFormat ! 10) return; if (_pubBaseDts 0) _pubBaseDts int64_t(pkt-timeStamp); uint64_t dts pkt-timeStamp - _pubBaseDts; if (aacType 0) { QByteArray asc pkt-body.mid(2); if (!_pubAudioTrack) { _pubAudioTrack std::make_sharedAudioTrack(CodecId::AAC, 1); // ★ 从 ASC 解析采样率/声道 auto info AdtsUtils::parseAudioSpecificConfig(asc); _pubAudioTrack-setSampleRate(info.sampleRate); _pubAudioTrack-setChannels(info.channelConfig); _pubAudioTrack-setExtradata(asc); _publishSource-addTrack(_pubAudioTrack); } auto frame FrameFactory::create( CodecId::AAC, asc, 0, 0, true, true, 1); _publishSource-writeFrame(frame, false); finalizePublishSource(); return; } }6.8 客户端 → 服务器裸 AAC04 00 03 E8 00 01 84 08 01 00 00 00 AF ← Audio Tag Header 01 ← raw AAC 21 09 0E 2C ... ← 裸 AAC 数据服务端处理if (aacType 1) { QByteArray rawAac pkt-body.mid(2); auto frame FrameFactory::create( CodecId::AAC, rawAac, dts, dts, true, false, 1); _publishSource-writeFrame(frame, true); // ★ 写入 MediaSource }6.9 注册 MediaSource关键void RtmpSession::finalizePublishSource() { auto tracks _publishSource-tracks(); for (auto t : tracks) { if (!t-ready()) return; // 有 track 未就绪等 } if (_publishRegistered) return; _publishSource-regist(); // ★ 注册到 MediaSourceManager _publishRegistered true; qDebug() [RTMP] MediaSource registered: _publishSource-url(); }注册后StreamManager::onTick()每 500ms → 发现新流 rtmp://127.0.0.1/live/test → 创建 RtspMuxer RtmpMuxer → muxer-start() 订阅 RingBuffer → 之后任何协议拉流都能拿到帧七、★ 协议桥接核心机制7.1 桥接原理关键MediaSource的 key 是vhost/app/stream不含 schema。MediaTuple::url()cppQString url() const { return QString(%1://%2/%3/%4).arg(schema).arg(vhost).arg(app).arg(stream); }但MediaSourceManager查找时的 key 只按完整 URL所以// RtspSession 拉流时客户端访问 rtsp://127.0.0.1/live/test MediaTuple rtspTuple { rtsp, 127.0.0.1, live, test }; // 查找失败 // 再换 rtmp 前缀找 MediaTuple rtmpTuple { rtmp, 127.0.0.1, live, test }; // 找到因为推流时注册的就是这个 URL7.2 跨协议查找代码RtmpSession 里的跨协议查找拉流时找 RTSP 推的流_playSource MediaSourceManager::instance().find(tuple); // 先按 rtmp 找 if (!_playSource) { MediaTuple rtspTuple tuple; rtspTuple.schema rtsp; // 换 rtsp 前缀 _playSource MediaSourceManager::instance().find(rtspTuple); }RtspSession 里同理拉流时找 RTMP 推的流_liveSource MediaSourceManager::instance().find(tuple); // 先按 rtsp 找 if (!_liveSource) { MediaTuple rtmpTuple tuple; rtmpTuple.schema rtmp; // 换 rtmp 前缀 _liveSource MediaSourceManager::instance().find(rtmpTuple); }7.3 四种组合都能通推流拉流命令RTMPRTMPffplay rtmp://127.0.0.1:1935/live/testRTMPRTSPffplay -rtsp_transport tcp rtsp://127.0.0.1:554/live/testRTSPRTMPffplay rtmp://127.0.0.1:1935/live/testRTSPRTSPffplay -rtsp_transport tcp rtsp://127.0.0.1:554/live/test这就是一个完整的流媒体服务器——推送协议和拉流协议完全解耦。八、完整时序图8.1 推流方向客户端 服务器 │ │ │──── TCP 连接 ───────────────────────►│ │──── C0C1 (11536B) ────────────────►│ │◄─── S0S1S2 (3073B) ─────────────────│ │──── C2 ( S1, 1536B) ───────────────►│ │ │ │──── connect live ─────────────────►│ RtmpSession::handleConnect │◄─── _result (Success) ────────────────│ → sendConnectResult │◄─── Window Ack Size (5000000) ────────│ │ │ │──── createStream ───────────────────►│ handleCreateStream │◄─── _result (streamId1) ────────────│ │ │ │──── publish test live ──────────►│ handlePublish │◄─── onStatus (Publish.Start) ─────────│ → _publishSource new MediaSource │ │ │──── setDataFrame/onMetaData ───────►│ handleData缓存元数据 │──── Video Sequence Header ──────────►│ handlePublishVideo(avcType0) │ │ → avcConfigToAnnexB → VideoTrack │──── Audio Sequence Header ──────────►│ handlePublishAudio(aacType0) │ │ → parseASC → AudioTrack │ │ → finalizePublishSource → regist() │ │ → StreamManager 创建 RtspMuxer │ │ │──── Video Frame (AVCC) ─────────────►│ handlePublishVideo(avcType1) │ │ → avccToAnnexB → Frame │──── Audio Frame (raw AAC) ──────────►│ → writeFrame → RingBuffer │──── ... 持续 ... │ → RtspMuxer 分发8.2 拉流方向客户端 服务器 │ │ │──── C0C1 → S0S1S2 → C2 ──────────►│ │──── connect ────────────────────────►│ │◄─── _result ──────────────────────────│ │──── createStream ───────────────────►│ │◄─── _result (streamId1) ────────────│ │ │ │──── play test -2000 ──────────────►│ handlePlay │ │ → 跨协议 find MediaSource │ │ → 拿/建 RtmpMuxer │ │ → addSession │◄─── onStatus (Play.Reset) ────────────│ │◄─── onStatus (Play.Start) ────────────│ │◄─── onStatus (Data.Start) ────────────│ │◄─── onMetaData ──────────────────────│ → sendOnMetaData │◄─── Video Sequence Header (avcC) ────│ → sendVideoConfig │◄─── Audio Sequence Header (ASC) ─────│ → sendAudioConfig │ │ │◄─── Video Frame (AVCC) ──────────────│ ← RtmpMuxer 从 RingBuffer 拿帧 │◄─── Audio Frame (raw AAC) ───────────│ ← 转 RTMP 格式发给客户端 │◄─── ... 持续 ... │九、常见问题速查现象原因修复握手后立即断开C0 版本号不是 0x03客户端要用 0x03_result收到但无法继续txn不匹配检查_connectTxn保存是否正确play 后无数据跨协议查找失败检查 URL 的 app/stream 是否一致publish 后拉流 404MediaSource 未 regist检查所有 track 是否 ready推流成功但花屏avcC 解析错检查avcConfigToAnnexB的字段偏移音频无声ASC 解析错从 ASC 解析采样率不要用 tag bit拉流断流Muxer 未正确 addSession检查_rtmpMuxer-addSession(this)时间戳跳变未做 DTS 基准归一化用_pubBaseDts归一化到 0十、小结RTMP 服务端的核心就四件事1. 握手接收方视角收到 C0C1回 S0S1S2消费 C2不校验关键S2 C1 回显2. 命令处理connect→ 保存 app回_resultcreateStream→ 分配 streamId生产要自增play→ 跨协议查找 Muxer 分发publish→ 创建 MediaSource3. 推流方向AVCC ↔ AnnexB收到avcC→ 转AnnexB→ 存入Track::extradata收到AVCC→ 转AnnexB→ 写入MediaSource收到ASC→ 解析采样率/声道4. 拉流方向AnnexB → AVCCTrack::extradataAnnexB→ 拼AVCDecoderConfigurationRecord→ 发送每一帧AnnexB→ 转AVCC→ 拼 RTMP video tag → 发送三条核心边界边界接口协议 ↔ 数据MediaSource::writeFrame()/regist()推流 ↔ 拉流MediaSourceManager::find()跨协议查Muxer ↔ 客户端Muxer::addSession()/onTrackFrame()把这三条理清一个支持 RTSP/RTMP 双向桥接的流媒体服务器就立起来了。任何协议推上来的流都能被任何协议拉走——这就是协议桥接的价值。rtsp,rtmp

相关新闻

【Bug已解决】Why does PyTorch not find my NVDIA drivers for CUDA support? 解决方案

【Bug已解决】Why does PyTorch not find my NVDIA drivers for CUDA support? 解决方案

【Bug已解决】Why does PyTorch not find my NVDIA drivers for CUDA support? 解决方案 问题描述 PyTorch 是目前最流行的深度学习框架之一,其强大的 GPU 加速能力依赖于 NVIDIA 的 CUDA 平台。然而,许多开发者在安装或使用 PyTorch 时会遇到一个令人…

2026/10/13 3:24:49 阅读更多 →
VSCode 推代码到 GitHub/Gitee,到底图个啥?

VSCode 推代码到 GitHub/Gitee,到底图个啥?

前言 很多人用 VSCode 写完代码,直接 CtrlS 就完事了。 但老手总会多做一步:把代码推送到 GitHub 或 Gitee。 这步到底有什么用?为什么非做不可? 一句话:给代码上“云备份 时光机 协作平台”。 一、推送到远程仓库&a…

2026/10/13 3:24:49 阅读更多 →
退出码 0、日志干净,但一件事也没干成:自动化脚本的 9 种静默失败

退出码 0、日志干净,但一件事也没干成:自动化脚本的 9 种静默失败

自动化脚本的 9 种"静默失败":不报错、不崩溃、就是不干活脚本跑完退出码 0,日志干干净净,一件事也没干成。 这种失败最难查,因为它不留下任何线索。下面 9 种是我实际踩过的,每一种都给出现象、真因、一条自…

2026/10/13 3:24:49 阅读更多 →

最新新闻

Vue+Spring Boot构建门诊电子病历与药品进销存系统实践

Vue+Spring Boot构建门诊电子病历与药品进销存系统实践

1. 一套门诊管理系统,先把三类人的痛说清楚我第一次接触到“vue springboot门诊电子病历处方药品进销存管理系统”这个需求,是某门诊部的负责人拿着一摞单据来找我。那天下午分诊台上堆着三样东西:病历本、处方笺、药房的出库单。护士要把同一…

2026/10/13 22:55:13 阅读更多 →
ACM竞赛中自定义cmp函数的黄金法则与避坑指南

ACM竞赛中自定义cmp函数的黄金法则与避坑指南

1. 项目概述:为什么排序和cmp函数是ACM新手绕不开的第一道真题关卡刚接触ACM/ICPC竞赛的新人,常会陷入一个认知误区:以为算法题就是堆砌高级数据结构、套用复杂模板。我带过几届校队,几乎每届都有同学在模拟赛里卡在一道“给100个…

2026/10/13 22:55:13 阅读更多 →
图表设计器开发实战:Canvas渲染、数据模型与撤销重做实现

图表设计器开发实战:Canvas渲染、数据模型与撤销重做实现

1. 项目定位:先弄清楚 diagram-design 到底要做成什么1.1 为什么叫 diagram-design,而不是 chart-library很多人第一次看到 layout 类项目的名字,第一反应是“这不就是个画图工具吗”。实际把 diagram-design 这类项目从零跑通之后&#xff0…

2026/10/13 22:55:13 阅读更多 →
GEO生成式引擎优化实战:从AI引用原理到企业落地全链路

GEO生成式引擎优化实战:从AI引用原理到企业落地全链路

去年年底,我在给某内容平台做案例分析时,第一次注意到生成式引擎优化这个词的搜索热度曲线,增速比当年搜索引擎优化最火的时候还快。那之后我陆续接触过几位做GEO优化的人,但多数要么只讲概念,要么只做工具&#xff0c…

2026/10/13 22:55:13 阅读更多 →
WifiForge 架构全解:从 TUI 到 Mininet-wifi 再到 TMUX 的完整调用链剖析

WifiForge 架构全解:从 TUI 到 Mininet-wifi 再到 TMUX 的完整调用链剖析

【免费下载链接】WifiForge WifiForge is a tool developed by Black Hills InfoSec to help train Pentesters on different Wi-Fi attack vectors and Wireless capabilities. 项目地址: https://gitcode.com/gh_mirrors/wi/WifiForge 点击查看 免费下载 WifiFor…

2026/10/13 22:55:13 阅读更多 →
NPOI+Unity:Word模板占位符替换与docx处理完整实战

NPOI+Unity:Word模板占位符替换与docx处理完整实战

接了个内部工具需求,用户要在不装Office的Windows机器上,用Unity客户端打开一份Word模板,把模板里的【项目名称】【客户单位】【编制人】【日期】这类占位符替换成实际内容,然后另存为新文档,供后续打印和归档。起初我…

2026/10/13 22:54:13 阅读更多 →

日新闻

物联网系统集成避坑指南:从选型到验收的关键要点

物联网系统集成避坑指南:从选型到验收的关键要点

做了这么多年物联网系统集成的项目,我最大的感受是,大部分坑不是出在技术前沿,而是出在一些看起来很平常的地方。设备选了便宜的、协议没问清楚、现场没摸透、验收标准写得含糊——这些细枝末节,最后都会变成延期、超支和扯皮的大…

2026/10/13 0:00:57 阅读更多 →
ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

如果你玩ESP32有一阵子了,很难不碰到舵机控制这道坎。做智能小车要转方向,做机械臂要抬关节,做云台要偏航俯仰,背后都是同一个东西在干活:舵机。而我见过太多人卡在“代码能编译、舵机就是不听话”这个阶段&#xff0c…

2026/10/13 0:00:57 阅读更多 →
CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

1. 为什么CAN总线至今仍是工业现场的“硬通货”?——从汽车底盘到智能温室的真实需求你有没有拆过一辆老款燃油车的中控台?或者调试过某高校实验室里那台跑着Modbus协议却频频丢帧的PLC控制柜?又或者,在某个农业物联网项目里&…

2026/10/13 0:00:57 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/13 2:54:56 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/13 16:22:04 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/13 1:58:08 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/13 22:01:27 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/13 14:57:39 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 18:11:17 阅读更多 →