Go语言中如何正确组装HLS流片段为可跳转的MP4文件
解决方案:纯Go实现HLS音频流转可Seek的MP4(边下载边处理)
问题根源
直接追加TS片段生成的文件只是后缀改为MP4的TS流,缺少MP4格式必需的moov元数据盒子——这个盒子包含了音频的时间戳索引、编码参数等关键信息,没有它播放器无法计算跳转位置,只能顺序播放。
核心实现思路
要生成可Seek的MP4,必须完成两个关键步骤:
- 解复用TS片段:从每个TS流中提取原始音频帧(ES流)及对应的PTS/DTS时间戳
- 标准MP4封装:将音频帧按时间顺序写入MP4文件,同时生成并写入完整的moov元数据,支持边下载边追加处理
可行代码实现(基于可靠Go库)
使用go-astits处理TS解复用,mp4ff处理MP4封装,修正ChatGPT示例中的错误用法,实现边下载边处理:
package main import ( "bufio" "fmt" "io" "net/http" "os" astits "github.com/asticode/go-astits" "github.com/edgeware/mp4ff/mp4" ) func main() { // 替换为你的HLS片段URL列表 segmentURLs := []string{ "https://stream.xyz/segment1.ts", "https://stream.xyz/segment2.ts", // ... 更多片段 } outFile, err := os.Create("output.mp4") if err != nil { fmt.Printf("创建输出文件失败: %v\n", err) return } defer outFile.Close() // 初始化MP4 writer(使用mp4ff的正确API) mp4Writer, err := mp4.CreateWriter(outFile, mp4.WithWriteFragmented()) if err != nil { fmt.Printf("初始化MP4 writer失败: %v\n", err) return } defer func() { if err := mp4Writer.Close(); err != nil { fmt.Printf("关闭MP4 writer失败: %v\n", err) } }() var audioTrack *mp4.Track var audioTimeScale uint32 for _, url := range segmentURLs { fmt.Printf("处理片段: %s\n", url) resp, err := http.Get(url) if err != nil { fmt.Printf("下载片段失败: %v\n", err) continue } defer resp.Body.Close() // 将HTTP响应流包装为bufio.Reader,供解复用器读取 bufReader := bufio.NewReader(resp.Body) demuxer := astits.NewDemuxer(bufReader) for { data, err := demuxer.NextData() if err != nil { if err == io.EOF { break } fmt.Printf("解复用TS失败: %v\n", err) break } if data == nil { break } // 处理音频PES包 if data.PES != nil && data.PES.Data != nil && data.PES.Header.StreamID == astits.StreamIDAudio { // 首次处理时,获取音频参数并创建MP4轨道 if audioTrack == nil { // 从PMT中提取音频编码信息(这里以AAC为例) for _, s := range demuxer.PMT().Streams { if s.StreamType == astits.StreamTypeAAC { // 获取AAC的采样率(从ADTS头或PMT中提取,这里简化为从流信息获取) audioTimeScale = uint32(s.ElementaryStreamDescriptor.AudioSamplingFrequency) if audioTimeScale == 0 { audioTimeScale = 44100 // 默认值 } break } } var err error audioTrack, err = mp4Writer.AddAudioTrack(audioTimeScale, "mp4a.40.2") // AAC-LC编码 if err != nil { fmt.Printf("创建音频轨道失败: %v\n", err) return } } // 转换PTS/DTS为MP4时间刻度(PTS单位是90kHz,需转成音频的time scale) pts := data.PES.Header.OptionalHeader.PTS dts := data.PES.Header.OptionalHeader.DTS if dts == nil { dts = pts // 音频通常DTS=PTS } // 计算样本的时间戳(转成MP4的time scale) mp4PTS := uint64(pts.Value) * uint64(audioTimeScale) / 90000 mp4DTS := uint64(dts.Value) * uint64(audioTimeScale) / 90000 // 写入音频样本到MP4 err := audioTrack.WriteSample(data.PES.Data, mp4DTS, mp4PTS, true) if err != nil { fmt.Printf("写入音频样本失败: %v\n", err) return } } } } fmt.Println("MP4文件生成完成,支持Seek操作") }
关键细节说明
- TS解复用:
go-astits会自动处理TS的同步字节(0x47),无需手动跳过,直接传入HTTP响应流即可 - MP4元数据:
mp4ff的CreateWriter配合AddAudioTrack会自动维护moov盒子的信息,最终关闭writer时会将完整的元数据写入文件 - 时间戳转换:TS的PTS/DTS基于90kHz时钟,需要转换为音频采样率对应的时间刻度,确保MP4的时间轴正确
- 边下载边处理:直接读取HTTP响应流,无需缓存整个TS片段到本地,符合你“边下载边追加”的需求
验证方法
生成文件后,可用ffprobe检查MP4结构:
ffprobe output.mp4
如果输出中包含moov和mdat盒子,且音频轨道信息完整,即可确认文件支持Seek。
内容的提问来源于stack exchange,提问作者mrfree
相关产品推荐
相关产品推荐

