尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
H264裸流转RTMP:SPS/PPS、avcC与时间戳封装实践
简介这份压缩包面向希望打通 H.264 到直播推流链路的中初级音视频开发者聚焦如何借助 FFmpeg API 在内存中完成 H.264 NAL 单元到 FLV 封装的转换并通过 RTMP 协议推送至流媒体服务器可作为理解直播推流核心流程的轻量参考。包内共 1 个 C 源文件压缩包约 3KB体量虽小却集中呈现了网络库初始化、FLV 封装、RTMP 连接创建、帧数据持续写入以及连接关闭与资源释放等关键环节便于读者对照梳理时间戳处理、内存 IO 与推流接口之间的衔接方式。这份源码已有 964 人学习适合用来拆解 H.264 到 FLV 再到 RTMP 的调用关系理解封装与推流的边界并积累实时音视频应用开发中的接口使用经验和排错思路。1. 从一路裸 H264 到可播放的 RTMP 流这份源码补的正是中间那层摄像头模组、硬件编码器、录屏 SDK 吐出来的通常是一段段 Annex-B 裸 H264每个 NALU 前面顶着00 00 00 01的起始码没有容器、没有时间戳、没有帧类型标记。而播放器和 CDN 要的是一条 RTMP 连接里一个个带毫秒时间戳的 FLV Tag。两头对不上中间就缺一层封装。这份 H264tortmp 源码做的就是这层切 NALU、抽 SPS/PPS 生成 AVCDecoderConfigurationRecord、按 FLV 规矩打上帧类型和 AVCPacketType再经 ffmpeg 的 libavformat 从rtmp://推出去。它不做编码也不做解码纯搬运和重组适合手上有裸流、又不想在服务端多转一道的开发者——IPC、录屏推流、图传、内网低延迟直播这类场景用得上。2. 工程骨架拆解解析、封装、推流三层怎么分工拿到压缩包解压后第一眼会有点意外代码量不大核心逻辑集中在一只手数得过来的几个.c文件里。这其实是好事说明作者没把东西搅在一起。整个工程按职责切成三层层与层之间只靠结构体和环形队列通信任何一层想换实现都不牵动另外两层。2.1 目录结构与各模块职责先看目录心里有个地图再动手编译能省掉一半的迷茫。h264_rtmp/ ├── src/ │ ├── main.c 入口解析命令行、拉起推流线程、信号处理 │ ├── h264_parser.c/.h Annex-B 切分、SPS/PPS 提取、NAL 类型判定 │ ├── flv_muxer.c/.h 组 avcC、填 AVPacket、写 FLV Tag │ ├── rtmp_sender.c/.h URL 拼接、连接、断线重连、发送循环 │ └── ring_buffer.c/.h 定长环形队列解耦收流和发送 ├── include/ │ └── config.h 码率、GOP、超时、重连间隔等宏 ├── third_party/ffmpeg/ 预编译头文件与静态库有些包只留了 include ├── conf/push.conf 推流地址与参数方便改配置不用重编 └── build.shh264_parser.c只认字节流输入一段 buffer输出若干{nal_ptr, nal_len, nal_type}它不知道 FLV 是什么也不知道 RTMP 是什么。flv_muxer.c拿到 NAL 之后负责两件事首帧到来时把 SPS/PPS 塞进extradata并写 sequence header tag之后每个 NAL 加 4 字节长度前缀、包成 AVPacket 交给 libavformat。rtmp_sender.c是唯一碰网络的一层负责avio_open、写包、出错重连。ring_buffer.c存在的意义是把「读多快」和「发多快」解耦——上游是文件时读得飞快上游是摄像头时又可能被阻塞中间没有缓冲时间戳一定乱。这套分层带来的直接好处是想换成 UDP 收流只改main.c的数据源想换成 HTTP-FLV 输出只改rtmp_sender.c里的 URL 和 muxer 名其余代码一行不动。2.2 一条 NALU 从读入到发出的完整链路把主循环摊开看整条链路一共六个阶段每个阶段的输入输出都很明确阶段输入输出负责模块读原始字节文件/管道/UDP字节 buffermain.c切 NAL字节 bufferNAL 数组h264_parser.c判类型NAL headerSPS/PPS/IDR/普通帧h264_parser.c组 extradataSPS PPSavcC 字节串flv_muxer.c打包NAL ptsAVPacketflv_muxer.c发送AVPacketRTMP chunkrtmp_sender.c主循环的骨架大致是这个样子看懂了它后面所有细节都只是往里填空/* 推流主循环读 → 切 → 打包 → 发失败就退避重连 */ for (;;) { int n source_read(src, buf, sizeof(buf)); /* 阻塞读文件源返回 0 表示 EOF */ if (n 0) break; nal_t nals[MAX_NAL_PER_AU]; int cnt h264_split_nals(buf, n, nals, MAX_NAL_PER_AU); /* 按起始码切开 */ if (cnt 0) continue; for (int i 0; i cnt; i) { if (nals[i].type 7) { save_sps(nals[i]); continue; } /* SPS 不进画面 */ if (nals[i].type 8) { save_pps(nals[i]); continue; } /* PPS 同上 */ if (!muxer-header_written) { muxer_write_sequence_header(muxer); /* 第一个非 SPS/PPS 到达前必须先写 avcC */ } int64_t pts next_pts(src); /* 见 4.3按帧率或源时间戳推 */ if (muxer_write_frame(muxer, nals[i], pts) 0) { sender_reconnect(sender); /* 发不出去就地重连不丢循环 */ } } }这段代码里有两个容易看漏的点。一是 SPS/PPS 必须continue掉不能当普通帧写进去——它们会被flv_muxer单独塞进 extradata再当画面帧写一遍播放器解析 avcC 时就会拿到重复的 parameter set行为不可预期。二是header_written的判断位置必须放在第一个真正的画面帧之前不能放在连接建立之后RTMP 连上了但还没写 avcC 就发帧服务端会直接吐一个 Missing sequence header 把连接掐了。2.3 选型对比libavformat 一把梭 vs 手写 RTMP工程里用的是 libavformat 走完整封装路径。有人会问既然只是把 NAL 塞进 FLV 再走 RTMP为什么不干脆手写做个对比就清楚了维度libavformat手写 RTMP FLV代码量约 300 行2000 行起握手与 chunk 分片库内部处理自己做 C0/C1/C2、S0/S1/S2时间戳管理有 time_base 体系全靠自己算排错手段AV_LOG_DEBUG一把梭只能加 printf依赖体积静态链接后 23 MB几乎为零可控性中遇到怪问题要看库源码高每一字节都是自己的结论其实很朴素除非目标环境对二进制体积有硬性限制或者需要对接某些非标准 RTMP 变种否则没有理由手写。这个项目选 libavformat代价是包里必须带 ffmpeg 的头和库好处是协议层的坑别人已经替你踩过一遍了。注意third_party/ffmpeg在很多流传的包里只保留了include库文件是空的。编译前先ls一下lib/目录空的就去用系统 ffmpeg 的pkg-config路径别对着链接错误干瞪眼。3. 编译与跑通从解压到 ffplay 看到画面这一章按我自己的习惯顺序走先确认依赖再编译再对参数最后用三处证据确认推流真的成功了。整个过程在一台普通开发机上十分钟能走完前提是别跳步骤。3.1 依赖版本与编译参数ffmpeg 的 API 在 4.x 到 6.x 之间有过几轮清理av_register_all这类函数早没了。这个工程的调用方式偏新用avformat_alloc_output_context2而不是手工av_alloc_format_context所以拿 ffmpeg 4.2 以上的版本比较稳妥。先确认系统里有什么# 看系统 ffmpeg 版本和三件套是否齐全 pkg-config --modversion libavformat libavcodec libavutil # 编译注意 -lpthread工程里用了独立推流线程 gcc -O2 -g -Wall -Wextra \ -Iinclude -Ithird_party/ffmpeg/include \ src/*.c -o h264_rtmp \ -Lthird_party/ffmpeg/lib \ -lavformat -lavcodec -lavutil -lpthread如果包里的库文件缺失把-I和-L换成pkg-config --cflags --libs libavformat libavcodec libavutil的输出即可。想自己裁一份干净的 ffmpeg只保留必需组件configure 长这样./configure \ --disable-everything \ --enable-protocolfile,rtmp \ --enable-demuxerh264,flv \ --enable-muxerflv \ --enable-parserh264 \ --enable-avformat --enable-avcodec --enable-avutil \ --disable-shared --enable-static \ --prefix/opt/ffmpeg-min--disable-everything会关掉所有组件后面再逐个打开。这样编出来的库通常只有几百 KB静态链接进去整个可执行文件也就 2 MB 出头塞进嵌入式设备很舒服。代价是以后想临时加个 mp4 输出得回头重编 ffmpeg——所以项目还在验证阶段时我一般不这么干等形态定了再裁剪。3.2 配置项与命令行参数工程支持命令行和conf/push.conf两种配置方式后者优先级更高。参数不多但每一个都影响最终能不能播参数默认值说明-i无输入源可以是裸 h264 文件、udp://收流地址、-表示 stdin-o无输出格式rtmp://host:1935/app/stream-r25帧率源没有时间戳时按帧数硬推 PTS-g50关键帧间隔帧数影响服务端 GOP 缓存和首屏速度-reconnect1断线是否自动重连0 关闭-timeout5000avio_open超时毫秒数内网可以调小配置文件长这样改完不用重编# conf/push.conf input /data/test.h264 output rtmp://127.0.0.1:1935/live/test fps 25 keyint 50 reconnect 1 timeout_ms 5000一个常见的误区是把-g当成编码参数调。这工程不做编码-g只是告诉 muxer「每多少帧应该出现一个关键帧」用来在推流侧打标签、在丢包时决定从哪儿重新对齐。真正的 GOP 是编码器决定的如果上游摄像头输出的是 100 帧一个 I 帧你把-g设成 25 也不会让关键帧变多。3.3 三段验证ffplay、服务端日志、抓包推流程序返回 0 不代表推成功了它只代表进程没崩。我一般按三步验证任何一步没对上就停在那里查。第一步本机拉流。用 ffplay加上低延迟参数否则默认缓冲会让你误以为延迟很大# 启动推流 ./h264_rtmp -i test.h264 -o rtmp://127.0.0.1:1935/live/test -r 25 -g 50 # 另一终端拉流关掉缓冲看真实延迟 ffplay -fflags nobuffer -flags low_delay -framedrop \ -probesize 32 -analyzeduration 0 \ rtmp://127.0.0.1:1935/live/test第二步看服务端日志。不管用的是哪个流媒体服务publish成功时一定会打一行类似publish streamtest的记录并且随后的bytes_in会持续增长。如果只有connect没有publish说明 RTMP 握手过了但推流名不对多半是 URL 里app/stream拼错。第三步抓包。前两步都正常但画面还是有问题时抓包是唯一能看清真相的手段# 抓 RTMP 端口存下来用 wireshark 看握手和 chunk sudo tcpdump -i any -s0 -w rtmp.pcap port 1935 # 或者直接看 ffmpeg 自己的协议日志 ./h264_rtmp -i test.h264 -o rtmp://127.0.0.1:1935/live/test 21 | grep -i rtmp在 wireshark 里过滤rtmp重点看两处connect之后服务端返回的_result里 code 是不是NetConnection.Connect.Success以及第一个 video tag 的 AVCPacketType 是不是 0。这两处对上了剩下的问题基本都在时间戳和帧类型上。4. 核心代码逐段拆解SPS/PPS、AVCC 转换与时间戳真正决定能不能播、延迟大不大的就是下面这四段。它们加起来不到两百行但每一行都对应一个具体的坑。4.1 从裸流里抠出 SPS/PPS 并生成 avcCH264 的 NAL header 只有 1 个字节低 5 位是类型7 是 SPS8 是 PPS5 是 IDR 帧1 是非 IDR 帧。先写一个在字节流里按类型找 NAL 的函数/* 在 Annex-B 字节流里找指定类型的 NAL成功返回 0out 指向 NAL 载荷不含起始码 */ static int find_nal(const uint8_t *buf, int len, int want_type, const uint8_t **out, int *out_len) { for (int i 0; i 4 len; ) { int sc 0; if (buf[i] 0 buf[i1] 0 buf[i2] 1) sc 3; else if (buf[i] 0 buf[i1] 0 buf[i2] 0 buf[i3] 1) sc 4; if (!sc) { i; continue; } /* 不是起始码往后挪一字节 */ const uint8_t *nal buf i sc; int type nal[0] 0x1F; /* 低 5 位才是 NAL 类型 */ int j i sc 1; while (j 2 len !(buf[j] 0 buf[j1] 0 buf[j2] 1)) j; int n j - (i sc); while (n 1 nal[n-1] 0) n--; /* 4 字节起始码多带一个 0回退掉 */ if (type want_type) { *out nal; *out_len n; return 0; } i j; /* 跳到下一个起始码别逐字节扫 */ } return -1; }i j那一句看着不起眼其实是性能关键。如果写成i一段 2 MB 的 IDR 帧会被从头到尾扫一遍找起始码在嵌入式设备上直接卡住。跳到下一个起始码位置复杂度从 O(帧长×NAL数) 降到 O(帧长)。拿到 SPS 和 PPS 之后拼 avcC。这段字节序列是 FLV 里 AVCPacketType0 那个 tag 的载荷也是 ffmpeg 里AVCodecParameters::extradata的内容/* 按 ISO/IEC 14496-15 拼 AVCDecoderConfigurationRecord返回写入字节数 */ static int build_avcc(const uint8_t *sps, int sps_len, const uint8_t *pps, int pps_len, uint8_t *out) { int p 0; out[p] 0x01; /* configurationVersion规范写死 1 */ out[p] sps[1]; /* AVCProfileIndication来自 SPS 第 2 字节 */ out[p] sps[2]; /* profile_compatibility */ out[p] sps[3]; /* AVCLevelIndication常见 0x1F/0x28/0x32 */ out[p] 0xFC | 0x03; /* 高 6 位保留全 1低 2 位 lengthSizeMinusOne3即 4 字节长度前缀 */ out[p] 0xE0 | 0x01; /* 高 3 位保留全 1低 5 位 numOfSPS1 */ out[p] (sps_len 8) 0xFF; out[p] sps_len 0xFF; memcpy(out p, sps, sps_len); p sps_len; out[p] 0x01; /* numOfPPS1 */ out[p] (pps_len 8) 0xFF; out[p] pps_len 0xFF; memcpy(out p, pps, pps_len); p pps_len; return p; /* 一般是 3050 字节取决于 SPS 长度 */ }参数说明一下lengthSizeMinusOne这个字段决定了后面每个 NAL 前面挂几个字节的长度。填 3 表示 4 字节大端长度这是绝大多数编码器的默认值如果上游用的是 2 字节长度少见这里得改成 1同时 4.2 那段也要跟着改两处必须一致否则播放器按错误的位宽读长度画面立刻花掉。4.2 Annex-B 转 AVCC起始码换成 4 字节长度前缀裸流里 NAL 之间用起始码分隔FLV 里用长度前缀分隔这一步就是格式转换的全部内容/* 一个 NAL 载荷 → [4 字节大端长度][NAL 载荷] */ static int nal_to_avcc(const uint8_t *nal, int nal_len, uint8_t *dst, int dst_cap) { if (nal_len 0 || nal_len 4 dst_cap) return -1; dst[0] (nal_len 24) 0xFF; /* 大端高位在前 */ dst[1] (nal_len 16) 0xFF; dst[2] (nal_len 8) 0xFF; dst[3] nal_len 0xFF; memcpy(dst 4, nal, nal_len); return nal_len 4; }看起来简单但有两个细节必须注意。第一长度是大端x86 上直接memcpy(len, dst, 4)会得到小端结果画面会花成一片这个错我见过不止一次。第二nal_len传进来的必须是去掉起始码之后的长度多算一个00就会让播放器把这 1 字节当成下一个 NAL 的长度高位整个帧就废了。4.1 里那个while (n 1 nal[n-1] 0) n--就是干这个的。还有一个容易忽略的问题一帧access unit里往往不止一个 NAL。低分辨率编码器一般一帧一个 NAL上了 1080p 之后一个 I 帧常被切成 4 个 slice也就是 4 个 NAL。这 4 个 NAL 要拼在同一个 AVPacket 里发出去不能拆成 4 个包——拆了之后时间戳相同、帧数翻倍播放器会认为是 4 帧帧率直接乱掉。正确做法是先算总长再一次性写进同一个 buffer。4.3 时间戳换算与首帧归零时间戳是最容易出玄学问题的地方。FLV 的时间戳单位是毫秒AVStream::time_base是1/1000而摄像头和 MPEG-TS 常见的是 90 kHz。两套体系之间换算错了表现是播放器进度条乱跳或者干脆不播。/* src_tb 常见 1/90000TS或 1/1000毫秒级采集base 是首帧原始时间戳 */ static int64_t ts_to_ms(int64_t ts, int num, int den, int64_t base) { int64_t ms (ts - base) * 1000 * num / den; /* 先减基准再换算避免大数溢出 */ return ms 0 ? 0 : ms; }base这个基准值是血泪经验。很多采集卡上电后时间戳从几百万甚至几十亿开始直接换算成毫秒第一个包的时间戳就是几十万毫秒服务端会认为你推了一个已经播了很久的流GOP 缓存直接失效首屏要么很慢要么黑屏。正确做法是记住第一帧的原始时间戳当基准后面全部减掉它让流从 0 开始。填 AVPacket 的时候dts和pts的关系也要明确pkt-pts pkt-dts ts_to_ms(raw_ts, tb_num, tb_den, base); pkt-stream_index st-index; pkt-flags is_key ? AV_PKT_FLAG_KEY : 0; /* 关键帧必须打上否则服务端没法做 GOP 缓存 */ pkt-duration 1000 / fps; /* 毫秒按帧率均分 */ /* 如果内部 time_base 不是 1/1000还要再转一次 */ av_packet_rescale_ts(pkt, (AVRational){1, 1000}, st-time_base);duration很多人不填短时间看不出问题跑久了时钟漂移会累积。服务端做 GOP 缓存和切片时依赖这个字段缺失的话拉流端可能出现音画不同步或者切片时长不准。裸 H264 没有 B 帧pts恒等于dts如果哪天把输入换成了带 B 帧的码流这里就得单独算 dts否则 muxer 会直接报 non-monotonous dts。4.4 推流循环与断线重连发送层最怕的不是连不上是连上了又断、断了没清干净。RTMP 走 TCP网络抖一下就会断重连逻辑写不好跑一晚上文件描述符就耗光了。for (;;) { if (!ctx-connected) { if (rtmp_connect(ctx) 0) { /* 内部做 alloc_output_context2 avio_open */ av_log(NULL, AV_LOG_WARNING, connect failed, retry in %d ms, ctx-retry_ms); av_usleep(ctx-retry_ms * 1000); /* 指数退避封顶 8 秒避免服务端重启时被疯狂重连打满 */ ctx-retry_ms ctx-retry_ms * 2 8000 ? 8000 : ctx-retry_ms * 2; continue; } ctx-retry_ms 500; /* 连上就复位 */ } int n ring_pop(ctx-rb, ctx-buf, sizeof(ctx-buf)); if (n 0) { av_usleep(2000); continue; } /* 队列空让出 CPU别忙等 */ if (push_one_frame(ctx, ctx-buf, n) 0) { rtmp_close(ctx); /* 关键avio_closep avformat_free_context 都要调 */ ctx-connected 0; } }rtmp_close里必须把三样东西都释放掉avio_closep(oc-pb)、avformat_free_context(oc)、以及oc-pb背后的 TCP socket。少放一样重连一百次就泄漏一百个 fd进程迟早挂在Too many open files。指数退避也很必要——服务端重启那几十秒如果按 10 ms 间隔重连几千次握手请求会把服务端的 accept 队列打爆反而谁都连不上。提示重连成功后第一件事是重写 sequence header。新连接在服务端看来是一个全新的会话不会继承上一次的 extradata只发画面帧过去播放器依然黑屏。5. 常见问题排查花屏、延迟、连不上、内存涨下面这五条基本覆盖了这个工程 90% 的现场问题每条都按现象、原因、解决三段写。遇到问题时按顺序对号入座比漫无目的地翻代码快得多。5.1 播放器黑屏日志一直刷 no extradata现象ffplay 能连上窗口出来了但一直是黑的控制台反复打印缺少 extradata 或者无法确定编码格式。原因sequence header 没写出去或者写的时间点不对。常见两种情况一是代码里把 SPS/PPS 当普通帧发了、extradata 一直是空的二是avformat_write_header调用了但AVCodecParameters::extradata在调用之后才赋值头信息已经发完了后面的修改不生效。解决确保extradata在avformat_write_header之前就av_malloc好、build_avcc填好、长度写进extradata_size。如果输入源是「先来一段 IDRSPS/PPS 夹在中间」这种非标准顺序得在读完第一帧后回退去扫 SPS/PPS别指望它一定在前面。5.2 画面花屏、绿块I 帧之后恢复正常现象每隔一两个 GOP 就出现一次花屏持续几百毫秒下一个 I 帧到来后自动恢复如此循环。原因丢包。RTMP 走 TCP 理论上不丢但服务端在缓冲溢出时会主动丢弃非关键帧切片或者上游编码器切了 slice 而代码只发了第一个 slice。解决先确认是不是 slice 问题——打日志统计每个 access unit 里的 NAL 数量1080p 下正常应该有多个。如果确实只发了一个就是 4.2 里说的「一帧多 NAL 要拼进同一个包」没做对。如果 NAL 数量正常就去看服务端的丢帧日志通常是客户端推得太快、服务端消费不过来把ring_buffer的容量调大或者降码率能缓解。5.3 端到端延迟三秒以上越推越慢现象刚推上去延迟还行跑十几分钟后延迟慢慢涨到三五秒重启推流进程立刻恢复。原因发送队列只进不出。push_one_frame是阻塞的但生产端没有背压队列越堆越长延迟就是这么攒出来的。另一种可能是avio_open时没设置缓冲区大小默认缓冲在弱网下会吃掉几百毫秒。解决给环形队列设上限满了就丢最老的非关键帧关键帧永远保留——丢关键帧的代价比丢普通帧大得多。同时在avio_open之后把 socket 的TCP_NODELAY打开禁用 Nagle 算法能省掉几十毫秒的粘连延迟。这两招在局域网里效果立竿见影。5.4 推流十几小时后进程被 OOM kill现象跑一晚上第二天发现进程没了dmesg里能看到 oom-killer 的记录内存曲线是缓慢爬升的锯齿形。原因重连路径漏释放。最常见的是AVDictionary用完没av_dict_free以及avformat_alloc_output_context2在重连时又 alloc 了一次旧的 context 没 free。锯齿形说明每次重连泄漏一点累积到阈值就爆。解决把重连的释放逻辑收进一个函数rtmp_close()里集中处理avio_closep、avformat_free_context、av_dict_free三件事别分散在多个if分支里。有条件的话用 valgrind 跑一遍模拟断网泄漏点一目了然。5.5 连不上 RTMP握手直接断现象avio_open返回失败或者连上之后马上被服务端断开日志里出现NetConnection.Connect.Rejected。原因三种可能。URL 拼错app和stream之间少了个斜杠服务端需要鉴权参数而 URL 里没带tcUrl和实际 URL 不一致某些服务端会校验这个字段。解决先把 URL 拿去用 ffmpeg 命令行推一遍ffmpeg -re -i test.h264 -c copy -f flv rtmp://...命令行能推成功说明 URL 和鉴权没问题问题在代码里命令行也推不进去就是服务端配置或者网络策略的事跟这个工程无关。这一步能把排查范围砍掉一半。6. 二次开发把它接进自己的业务链路这份代码最值钱的地方不是它能推流而是三层切得干净接进现有业务几乎不用动核心。我的做法是先在外面包一层接口把main.c里的命令行逻辑剥掉让业务侧只关心「喂一帧、发一帧」/* 对业务暴露的最小接口隐藏 ffmpeg 和线程细节 */ typedef struct { void (*on_connect)(void *user, const char *url); void (*on_disconnect)(void *user, int err); void (*on_stats)(void *user, int64_t bytes, int64_t frames); void *user; } pusher_ops_t; int pusher_start(const char *url, const pusher_ops_t *ops); int pusher_send(pusher_handle_t h, const uint8_t *nal, int len, int64_t pts_ms, int key); void pusher_stop(pusher_handle_t h);包好之后几个常见改造方向就都很顺了改造点改哪里注意接摄像头实时裸流main.c换成 V4L2 读线程PTS 用采集时间戳别用帧计数推 PTS采集抖动会累积成漂移多路并发推流每路一个pusher_handle_t共享一个 ffmpeg 初始化avformat_network_init全局只调一次加 SEI 透传业务数据flv_muxer.c里 type6 的 NAL 原样保留别自己拼 SEI直接透传编码器打好的输出改 HTTP-FLVrtmp_sender.c里 muxer 名换成flvURL 换成 http需要服务端支持 chunked 输出按需启停pusher_stop里 join 推流线程别用pthread_cancel资源释放会漏有一个改造我踩过坑想加一路「推流的同时本地存一份」最省事的写法是在flv_muxer里复制一份 AVPacket 往第二个 context 写。结果是两个 muxer 共享同一个AVPacket的buf一个av_packet_unref之后另一个读到野指针画面随机崩。正确做法是av_packet_ref出独立引用再交给第二个 context两个 muxer 各管各的引用计数。这种引用计数的问题在单路推流时永远暴露不出来一上多路就原形毕露。从那以后我每次给这类封装层加旁路输出都强制先跑一遍「单路推流 本地存盘」的对照测试两边的时间戳和帧数必须完全对齐才敢往上叠功能。裸流封装这活儿真正难的不是把字节塞进容器而是把时间戳、引用计数、重连释放这三件事在所有分支上都摆平。希望帮到你。本文还有配套的精品资源点击获取
RELATED

相关推荐

PyTorch图像分类模型实战:从数据准备到部署优化

PyTorch图像分类模型实战:从数据准备到部署优化

1. 网络分类模型训练全流程实战指南在计算机视觉和机器学习领域,图像分类任务一直是最基础也最考验基本功的项目类型。最近在GitHub上看到一个star数很高的分类网络训练项目,但代码仓库只有基础框架,缺乏详细的实现说明。这让我想起自己刚入门…

📅 2026/10/26 6:29:27
具身智能:AI从虚拟到物理世界的关键技术

具身智能:AI从虚拟到物理世界的关键技术

1. 具身智能:AI进化的必经之路当英伟达CEO黄仁勋亲自撰文强调"具身智能"的重要性时,这个原本晦涩的专业术语突然成为了AI领域的热门话题。作为在机器人领域深耕多年的从业者,我深刻理解为什么老黄会将具身智能称为AI全栈必须跨越的…

📅 2026/10/26 6:29:27
Hugging Face模型下载原理与企业级实践指南

Hugging Face模型下载原理与企业级实践指南

1. 为什么“下载模型”这件事,比想象中更值得深挖Hugging Face模型下载方法小结——这标题看起来平平无奇,甚至有点“基础得不该写成博文”。但过去三年里,我帮超过40个不同背景的团队(从某高校自然语言处理课程助教,到…

📅 2026/10/26 6:29:27
MORE NEWS

更多资讯

📰

skyRL-Agent:基于强化学习的智能体系统开发实践

1. skyRL-Agent项目概述skyRL-Agent是一个基于强化学习(Reinforcement Learning)框架开发的智能体系统,从名称中的"RL"可以明确看出其技术基础。这类系统通常用于构建能够自主学习和决策的AI代理,在游戏AI、自动化控制、…

📰

Mhvtl虚拟带库搭建与TSM 6.3备份恢复实战指南

简介:面向Linux运维、备份管理员与系统架构师的TSM 6.3 Mhvtl Oracle DB2备份恢复测试文档,基于CentOS 6.5环境,完整覆盖虚拟带库搭建到数据库异机恢复的测试链路。资源为单个docx文档,共53页,压缩包大小1.83MB&…

📰

Apollo配置中心包管理安装与生产调优实践

Apollo 这套配置中心在不少团队里都是"上线前必须过的一道坎",而真正让人卡住的地方往往不是代码怎么接,而是第一步——怎么把它稳稳当当装到机器上。我前后装过源码版、容器版、包管理版,折腾下来发现生产环境最省心的还是包管理安…

📰

Go内存逃逸分析:原理、优化与实践指南

1. Go 内存逃逸分析核心概念解析内存逃逸分析是 Go 编译器在编译阶段执行的关键优化技术,它决定了变量应该分配在栈(stack)还是堆(heap)上。当变量在函数执行结束后仍然可能被访问时,就会发生"逃逸"现象,此时变量必须分配在堆上。G…

📰

Golang即时通信系统架构设计与可靠性实践

做后端这一行,基于Golang的即时通信系统这个题目我前后被问过不下十次,问的人里有刚把语法啃完想找个项目练手的,也有手上业务真到了要自己搭一套消息通道的。这两拨人的诉求完全不一样:前者想看到一个能跑起来的骨架,…

📰

Miniforge环境管理:Python开发高效工具指南

1. Miniforge开发环境概述Miniforge作为轻量级的conda替代方案,已经成为Python开发者的新宠。它继承了conda强大的环境管理能力,同时避免了Anaconda的商业许可问题。我在多个机器学习项目中切换使用Miniforge后,发现其启动速度和包解析效率确…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬