求助:使用C#将H264裸流转为HLS TS分段的实现方案
H264转HLS TS分段实现方案
问题1:GetSegment方法读取H264数据的正确方式
必须按帧(NALU单元)读取,且每个TS分段必须以SPS/PPS + I帧开头,原因如下:
- HLS要求每个TS分段是可独立解码的单元,播放器拿到分段就能直接初始化解码器播放,不需要依赖其他分段的数据。
- 固定字节块读取会大概率截断NALU(H264帧的基本单元),导致播放器无法解析帧数据,出现花屏或无法播放的情况。
- SPS/PPS是解码器初始化必需的参数,但仅读取SPS/PPS块没有意义,必须搭配后续的I/P/B帧,且每个分段开头都要携带最新的SPS/PPS(如果流中有更新的话)。
读取逻辑要点:
- 先识别H264裸流中的起始码(
0x000001或0x00000001),分割出单个NALU单元。 - 标记NALU类型:SPS(类型7)、PPS(类型8)、I帧(类型5)、P帧(类型1)等。
- 缓存最新的SPS/PPS,每次开启新分段时,先写入这两个NALU,再写入后续的帧,直到达到目标分段时长(或下一个I帧前结束)。
问题2:TS分段的生成算法
TS分段生成核心是将H264的NALU封装为符合MPEG-TS标准的数据包,步骤如下:
1. 基础准备
- TS包固定为188字节,其中4字节是TS头部(同步字节
0x47+ 包标识符PID + 控制字段等),剩余184字节为负载。 - 每个TS分段对应一个独立的TS文件,时长建议设置为2-10秒(HLS标准推荐,平衡延迟和兼容性)。
2. 封装流程
- 步骤1:封装NALU为PES包
PES(分组基本流)是TS包的上层容器,用于携带媒体数据。需要:- 为视频流分配固定PID(比如
0x1000)。 - 计算PTS/DTS时间戳:H264的时间戳基于90kHz时钟,直接转换为TS的PTS/DTS格式(将数值拆分为3个32位字段的组合)。
- 将NALU的起始码替换为4字节的NALU长度字段(因为TS封装不需要起始码,用长度标识帧边界),然后作为PES负载。
- 为视频流分配固定PID(比如
- 步骤2:拆分PES包为TS包
- 如果PES负载长度 ≤ 184字节,直接放入单个TS包,设置头部的有效负载单元起始指示符。
- 如果负载过长,拆分为多个TS包:第一个TS包携带完整的PES头部,后续TS包仅携带负载分片,通过头部的连续性计数器保证包顺序。
- 步骤3:生成完整TS分段
- 从I帧开始收集帧,写入SPS/PPS → I帧 → P/B帧,直到累计时长达到目标值。
- 将所有封装好的TS包写入文件,完成一个分段。
- 步骤4:更新M3U8播放列表
在播放列表中添加该分段的URL、时长,更新EXT-X-TARGETDURATION为所有分段的最大时长,保证播放器正确识别。
C#实现方案
纯C#手动实现
核心逻辑示例(伪代码):
// 解析H264 NALU List<byte[]> ParseNalus(byte[] h264Data) { var nalus = new List<byte[]>(); int pos = 0; while (pos < h264Data.Length) { // 查找起始码 int startCodeLen = 0; if (h264Data[pos] == 0 && h264Data[pos+1] == 0 && h264Data[pos+2] == 1) startCodeLen = 3; else if (h264Data[pos] == 0 && h264Data[pos+1] == 0 && h264Data[pos+2] == 0 && h264Data[pos+3] == 1) startCodeLen = 4; else { pos++; continue; } pos += startCodeLen; // 查找下一个起始码位置 int nextStart = FindNextStartCode(h264Data, pos); int naluLen = nextStart - pos; nalus.Add(h264Data.AsSpan(pos, naluLen).ToArray()); pos = nextStart; } return nalus; } // 封装NALU为TS包 byte[] WrapNaluToTs(byte[] nalu, ushort pid, long pts) { // 构造PES头部 var pesHeader = new byte[14]; pesHeader[0] = 0x00; pesHeader[1] = 0x00; pesHeader[2] = 0x01; // PES起始码 pesHeader[3] = 0xE0; // 视频流 // 填充PES长度、PTS等字段... // 构造TS头部 var tsHeader = new byte[4]; tsHeader[0] = 0x47; // 同步字节 tsHeader[1] = (byte)((pid >> 8) & 0x1F); tsHeader[2] = (byte)(pid & 0xFF); tsHeader[3] = 0x10; // 有效负载单元起始 // 合并头部和负载,拆分TS包... return tsPackets; }
使用FFmpeg.AutoGen实现(推荐)
利用FFmpeg的复用器直接生成TS流,避免手动处理复杂的TS封装逻辑:
using FFmpeg.AutoGen; // 初始化FFmpeg ffmpeg.av_register_all(); ffmpeg.avformat_network_init(); // 打开H264裸流 var inputFormatContext = ffmpeg.avformat_alloc_context(); ffmpeg.avformat_open_input(ref inputFormatContext, "input.h264", ffmpeg.av_find_input_format("h264"), null); ffmpeg.avformat_find_stream_info(inputFormatContext, null); // 初始化TS复用器 var outputFormat = ffmpeg.av_guess_format("mpegts", null, null); var outputFormatContext = ffmpeg.avformat_alloc_context(); outputFormatContext.oformat = outputFormat; // 创建输出流 var videoStream = ffmpeg.av_stream_new(outputFormatContext, null); ffmpeg.avcodec_parameters_copy(videoStream.codecpar, inputFormatContext.streams[0].codecpar); // 打开输出文件 ffmpeg.avio_open(ref outputFormatContext.pb, "segment_001.ts", ffmpeg.AVIO_FLAG_WRITE); ffmpeg.avformat_write_header(outputFormatContext, null); // 读取帧并写入TS var packet = ffmpeg.av_packet_alloc(); long currentDuration = 0; const long targetDuration = 5 * ffmpeg.AV_TIME_BASE; // 5秒 while (ffmpeg.av_read_frame(inputFormatContext, packet) >= 0) { // 转换时间戳 ffmpeg.av_packet_rescale_ts(packet, inputFormatContext.streams[packet.stream_index].time_base, videoStream.time_base); currentDuration += packet.duration; ffmpeg.av_interleaved_write_frame(outputFormatContext, packet); // 达到目标时长,切换分段 if (currentDuration >= targetDuration) { ffmpeg.av_write_trailer(outputFormatContext); ffmpeg.avio_close(outputFormatContext.pb); // 创建新的分段文件 currentDuration = 0; ffmpeg.avio_open(ref outputFormatContext.pb, "segment_002.ts", ffmpeg.AVIO_FLAG_WRITE); ffmpeg.avformat_write_header(outputFormatContext, null); } ffmpeg.av_packet_unref(packet); } // 清理资源 ffmpeg.av_write_trailer(outputFormatContext); ffmpeg.avio_close(outputFormatContext.pb); ffmpeg.avformat_close_input(ref inputFormatContext); ffmpeg.avformat_free_context(outputFormatContext); ffmpeg.av_packet_free(ref packet);
微软MediaFoundation方案
通过MediaFoundation的COM接口实现H264解码+TS复用,适合Windows平台:
- 使用
SourceReader读取H264裸流,解码为未压缩帧。 - 使用
SinkWriter配置MPEG-TS输出格式,将编码后的帧写入TS文件。 - 注意处理时间戳同步和分段切换逻辑,需要熟悉MediaFoundation的媒体类型配置。
内容的提问来源于stack exchange,提问作者Fatih
相关产品推荐
相关产品推荐

