You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

求助:使用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负载。
  • 步骤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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.20 15:57:01