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

Go语言中如何正确组装HLS流片段为可跳转的MP4文件

解决方案:纯Go实现HLS音频流转可Seek的MP4(边下载边处理)

问题根源

直接追加TS片段生成的文件只是后缀改为MP4的TS流,缺少MP4格式必需的moov元数据盒子——这个盒子包含了音频的时间戳索引、编码参数等关键信息,没有它播放器无法计算跳转位置,只能顺序播放。

核心实现思路

要生成可Seek的MP4,必须完成两个关键步骤:

  1. 解复用TS片段:从每个TS流中提取原始音频帧(ES流)及对应的PTS/DTS时间戳
  2. 标准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操作")
}

关键细节说明

  1. TS解复用:go-astits会自动处理TS的同步字节(0x47),无需手动跳过,直接传入HTTP响应流即可
  2. MP4元数据:mp4ff的CreateWriter配合AddAudioTrack会自动维护moov盒子的信息,最终关闭writer时会将完整的元数据写入文件
  3. 时间戳转换:TS的PTS/DTS基于90kHz时钟,需要转换为音频采样率对应的时间刻度,确保MP4的时间轴正确
  4. 边下载边处理:直接读取HTTP响应流,无需缓存整个TS片段到本地,符合你“边下载边追加”的需求

验证方法

生成文件后,可用ffprobe检查MP4结构:

ffprobe output.mp4

如果输出中包含moov和mdat盒子,且音频轨道信息完整,即可确认文件支持Seek。

内容的提问来源于stack exchange,提问作者mrfree

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.09 04:02:44