Files
monibuca/plugin/mp4/api_extract.go
langhuihui 8a9fffb987 refactor: frame converter and mp4 track improvements
- Refactor frame converter implementation
- Update mp4 track to use ICodex
- General refactoring and code improvements

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-28 19:55:37 +08:00

847 lines
22 KiB
Go
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* @file 文件名.h
* @brief MP4 文件查询提取功能,GOP提取新的MP4片段提取图片等已验证测试H264,H265
* @author erroot
* @date 250614
* @version 1.0.0
*/
package plugin_mp4
import (
"bytes"
"fmt"
"io"
"net"
"net/http"
"os"
"strconv"
"strings"
"time"
m7s "m7s.live/v5"
"m7s.live/v5/pkg"
"m7s.live/v5/pkg/codec"
"m7s.live/v5/pkg/util"
mp4 "m7s.live/v5/plugin/mp4/pkg"
"m7s.live/v5/plugin/mp4/pkg/box"
)
// bytes2hexStr 将字节数组前n个字节转为16进制字符串
// data: 原始字节数组
// length: 需要转换的字节数(超过实际长度时自动截断)
func Bytes2HexStr(data []byte, length int) string {
if length > len(data) {
length = len(data)
}
var builder strings.Builder
for i := 0; i < length; i++ {
if i > 0 {
builder.WriteString(" ")
}
builder.WriteString(fmt.Sprintf("%02X", data[i]))
}
return builder.String()
}
/*
提取压缩视频(快放视频)
njtv/glgc.mp4?
start=1748620153000&
end=1748620453000&
outputPath=/opt/njtv/1748620153000.mp4
gopSeconds=1&
gopInterval=1&
FLAG_FRAGMENT 暂时不支持没有调试
假设原生帧率25fps GOP = 50 frame
时间范围: endTime-startTime = 300s = 7500 frame = 150 GOP
gopSeconds=0.2 6 frame
gopInterval=10
提取结果15 gop, 90 frame , 90/25 = 3.6 s
反过推算 要求 5范围分钟 压缩到15s 播放完
当gopSeconds=0.1 推算 gopInterval=1
当gopSeconds=0.2 推算 gopInterval=2
*/
func (p *MP4Plugin) extractCompressedVideo(streamPath string, startTime, endTime time.Time, writer io.Writer, gopSeconds float64, gopInterval int) error {
if p.DB == nil {
return pkg.ErrNoDB
}
var flag mp4.Flag
if strings.HasSuffix(streamPath, ".fmp4") {
flag = mp4.FLAG_FRAGMENT
streamPath = strings.TrimSuffix(streamPath, ".fmp4")
} else {
streamPath = strings.TrimSuffix(streamPath, ".mp4")
}
// 查询数据库获取符合条件的片段
queryRecord := m7s.RecordStream{
Type: "mp4",
}
var streams []m7s.RecordStream
p.DB.Where(&queryRecord).Find(&streams, "end_time>? AND start_time<? AND stream_path=?", startTime, endTime, streamPath)
if len(streams) == 0 {
return fmt.Errorf("no matching MP4 segments found")
}
// 创建输出文件
outputFile := writer
p.Info("extracting compressed video", "streamPath", streamPath, "start", startTime, "end", endTime,
"gopSeconds", gopSeconds, "gopInterval", gopInterval)
muxer := mp4.NewMuxer(flag)
ftyp := muxer.CreateFTYPBox()
n := ftyp.Size()
muxer.CurrentOffset = int64(n)
var videoTrack *mp4.Track
sampleOffset := muxer.CurrentOffset + mp4.BeforeMdatData
mdatOffset := sampleOffset
//var audioTrack *mp4.Track
var extraData []byte
// 压缩相关变量
currentGOPCount := -1
inGOP := false
targetFrameInterval := 40 // 25fps对应的毫秒间隔 (1000/25=40ms)
var filteredSamples []box.Sample
//var lastVideoTimestamp uint32
var timescale uint32 = 1000 // 默认时间刻度为1000 (毫秒)
var currentGopStartTime int64 = -1
// 仅处理视频轨道
for i, stream := range streams {
file, err := os.Open(stream.FilePath)
if err != nil {
return fmt.Errorf("failed to open file %s: %v", stream.FilePath, err)
}
defer file.Close()
p.Info("processing segment", "file", file.Name())
demuxer := mp4.NewDemuxer(file)
err = demuxer.Demux()
if err != nil {
p.Warn("demux error, skipping segment", "error", err, "file", stream.FilePath)
continue
}
// 确保有视频轨道
var hasVideo bool
for _, track := range demuxer.Tracks {
if track.Cid.IsVideo() {
hasVideo = true
// 只在第一个片段或关键帧变化时更新extraData
trackExtraData := track.GetRecord()
if extraData == nil || !bytes.Equal(extraData, trackExtraData) {
extraData = trackExtraData
if videoTrack == nil {
videoTrack = muxer.AddTrack(track.Cid)
videoTrack.ICodecCtx = track.ICodecCtx
}
}
break
}
}
if !hasVideo {
p.Warn("no video track found in segment", "file", stream.FilePath)
continue
}
// 处理起始时间边界
var tsOffset int64
if i == 0 {
startTimestamp := startTime.Sub(stream.StartTime).Milliseconds()
if startTimestamp < 0 {
startTimestamp = 0
}
startSample, err := demuxer.SeekTime(uint64(startTimestamp))
if err == nil {
tsOffset = -int64(startSample.Timestamp)
}
}
// 处理样本
for track, sample := range demuxer.RangeSample {
if !track.Cid.IsVideo() {
continue
}
//for _, sample := range samples {
adjustedTimestamp := sample.Timestamp + uint32(tsOffset)
// 处理GOP逻辑
if sample.KeyFrame {
currentGOPCount++
inGOP = false
if currentGOPCount%gopInterval == 0 {
currentGopStartTime = int64(sample.Timestamp)
inGOP = true
}
}
// 跳过不在当前GOP的帧
if !inGOP {
currentGopStartTime = -1
continue
}
// 如果不在有效的GOP中跳过
if currentGopStartTime == -1 {
continue
}
// 检查是否超过gopSeconds限制
currentTime := int64(sample.Timestamp)
gopElapsed := float64(currentTime-currentGopStartTime) / float64(timescale)
if gopSeconds > 0 && gopElapsed > gopSeconds {
continue
}
// 处理结束时间边界
if i == len(streams)-1 && int64(adjustedTimestamp) > endTime.Sub(streams[0].StartTime).Milliseconds() {
continue
}
// 确保样本数据有效
if sample.Size <= 0 || sample.Size > 10*1024*1024 { // 10MB限制
p.Warn("invalid sample size", "size", sample.Size, "timestamp", sample.Timestamp)
continue
}
// 读取样本数据
if _, err := file.Seek(sample.Offset, io.SeekStart); err != nil {
p.Warn("seek error", "error", err, "offset", sample.Offset)
continue
}
data := make([]byte, sample.Size)
if _, err := io.ReadFull(file, data); err != nil {
p.Warn("read sample error", "error", err, "size", sample.Size)
continue
}
// 创建新的样本
newSample := box.Sample{
KeyFrame: sample.KeyFrame,
Timestamp: adjustedTimestamp,
Offset: sampleOffset,
Duration: sample.Duration,
}
newSample.PushOne(data)
// p.Info("Compressed", "KeyFrame", newSample.KeyFrame,
// "CTS", newSample.CTS,
// "Timestamp", newSample.Timestamp,
// "Offset", newSample.Offset,
// "Size", newSample.Size,
// "Duration", newSample.Duration,
// "Data", Bytes2HexStr(newSample.Data, 16))
sampleOffset += int64(newSample.Size)
filteredSamples = append(filteredSamples, newSample)
}
}
if len(filteredSamples) == 0 {
return fmt.Errorf("no valid video samples found")
}
// 按25fps重新计算时间戳
for i := range filteredSamples {
filteredSamples[i].Timestamp = uint32(i * targetFrameInterval)
}
// 添加样本到轨道
for _, sample := range filteredSamples {
videoTrack.AddSampleEntry(sample)
}
// 计算视频时长
videoDuration := uint32(len(filteredSamples) * targetFrameInterval)
// 写入输出文件
if flag == 0 {
// 非分片MP4处理
moovSize := muxer.MakeMoov().Size()
dataSize := uint64(sampleOffset - mdatOffset)
// 调整sample偏移量
for _, track := range muxer.Tracks {
for i := range track.Samplelist {
track.Samplelist[i].Offset += int64(moovSize)
}
}
// 创建MDAT盒子 (添加8字节头)
mdatHeaderSize := uint64(8)
mdatBox := box.CreateBaseBox(box.TypeMDAT, dataSize+mdatHeaderSize)
var freeBox *box.FreeBox
if mdatBox.HeaderSize() == box.BasicBoxLen {
freeBox = box.CreateFreeBox(nil)
}
// 写入文件头
_, err := box.WriteTo(outputFile, ftyp, muxer.MakeMoov(), freeBox, mdatBox)
if err != nil {
return fmt.Errorf("failed to write header: %v", err)
}
for _, track := range muxer.Tracks {
for i := range track.Samplelist {
track.Samplelist[i].Offset += int64(moovSize)
if _, err := outputFile.Write(track.Samplelist[i].Buffers[0]); err != nil {
return err
}
}
}
} else {
// 分片MP4处理
var children []box.IBox
moov := muxer.MakeMoov()
children = append(children, ftyp, moov)
// 创建分片
for _, sample := range filteredSamples {
moof, mdat := muxer.CreateFlagment(videoTrack, sample)
children = append(children, moof, mdat)
}
_, err := box.WriteTo(outputFile, children...)
if err != nil {
return fmt.Errorf("failed to write fragmented MP4: %v", err)
}
}
p.Info("compressed video saved",
"originalDuration", (endTime.Sub(startTime)).Milliseconds(),
"compressedDuration", videoDuration,
"frameCount", len(filteredSamples),
"fps", 25)
return nil
}
/*
根据时间范围提取视频片段
njtv/glgc.mp4?
timest=1748620153000&
outputPath=/opt/njtv/gop_tmp_1748620153000.mp4
原理根据时间戳找到最近的mp4文件再从mp4 文件中找到最近gop 生成mp4 文件
*/
func (p *MP4Plugin) extractGopVideo(streamPath string, targetTime time.Time, writer io.Writer) (float64, error) {
if p.DB == nil {
return 0, pkg.ErrNoDB
}
var flag mp4.Flag
if strings.HasSuffix(streamPath, ".fmp4") {
flag = mp4.FLAG_FRAGMENT
streamPath = strings.TrimSuffix(streamPath, ".fmp4")
} else {
streamPath = strings.TrimSuffix(streamPath, ".mp4")
}
// 查询数据库获取符合条件的片段
queryRecord := m7s.RecordStream{
Type: "mp4",
}
var streams []m7s.RecordStream
p.DB.Where(&queryRecord).Find(&streams, "end_time>=? AND start_time<=? AND stream_path=?", targetTime, targetTime, streamPath)
if len(streams) == 0 {
return 0, fmt.Errorf("no matching MP4 segments found")
}
// 创建输出文件
outputFile := writer
p.Info("extracting compressed video", "streamPath", streamPath, "targetTime", targetTime)
muxer := mp4.NewMuxer(flag)
ftyp := muxer.CreateFTYPBox()
n := ftyp.Size()
muxer.CurrentOffset = int64(n)
var videoTrack *mp4.Track
sampleOffset := muxer.CurrentOffset + mp4.BeforeMdatData
mdatOffset := sampleOffset
//var audioTrack *mp4.Track
var extraData []byte
// 压缩相关变量
findGOP := false
targetFrameInterval := 40 // 25fps对应的毫秒间隔 (1000/25=40ms)
var filteredSamples []box.Sample
//var lastVideoTimestamp uint32
var timescale uint32 = 1000 // 默认时间刻度为1000 (毫秒)
var currentGopStartTime int64 = -1
var gopElapsed float64 = 0
// 仅处理视频轨道
for _, stream := range streams {
file, err := os.Open(stream.FilePath)
if err != nil {
return 0, fmt.Errorf("failed to open file %s: %v", stream.FilePath, err)
}
defer file.Close()
p.Info("processing segment", "file", file.Name())
demuxer := mp4.NewDemuxer(file)
err = demuxer.Demux()
if err != nil {
p.Warn("demux error, skipping segment", "error", err, "file", stream.FilePath)
continue
}
// 确保有视频轨道
var hasVideo bool
for _, track := range demuxer.Tracks {
if track.Cid.IsVideo() {
hasVideo = true
// 只在第一个片段或关键帧变化时更新extraData
trackExtraData := track.GetRecord()
if extraData == nil || !bytes.Equal(extraData, trackExtraData) {
extraData = trackExtraData
if videoTrack == nil {
videoTrack = muxer.AddTrack(track.Cid)
videoTrack.ICodecCtx = track.ICodecCtx
}
}
break
}
}
if !hasVideo {
p.Warn("no video track found in segment", "file", stream.FilePath)
continue
}
// 处理起始时间边界
var tsOffset int64
startTimestamp := targetTime.Sub(stream.StartTime).Milliseconds()
// p.Info("extractGop", "targetTime", targetTime,
// "stream.StartTime", stream.StartTime,
// "startTimestamp", startTimestamp)
if startTimestamp < 0 {
startTimestamp = 0
}
//通过时间戳定位到最近的关键帧如视频IDR帧返回的startSample是该关键帧对应的样本
startSample, err := demuxer.SeekTime(uint64(startTimestamp))
if err == nil {
tsOffset = -int64(startSample.Timestamp)
}
//p.Info("extractGop", "startSample", startSample)
// 处理样本
//RangeSample迭代的是当前时间范围内的所有样本可能包含非关键帧顺序取决于MP4文件中样本的物理存储顺序
for track, sample := range demuxer.RangeSample {
if !track.Cid.IsVideo() {
continue
}
if sample.Timestamp < startSample.Timestamp {
continue
}
//for _, sample := range samples {
adjustedTimestamp := sample.Timestamp + uint32(tsOffset)
// 处理GOP逻辑,已经处理完上一个gop
if sample.KeyFrame && findGOP {
break
}
// 处理GOP逻辑
if sample.KeyFrame && !findGOP {
findGOP = true
currentGopStartTime = int64(sample.Timestamp)
}
// 跳过不在当前GOP的帧
if !findGOP {
currentGopStartTime = -1
continue
}
// 检查是否超过gopSeconds限制
currentTime := int64(sample.Timestamp)
gopElapsed = float64(currentTime-currentGopStartTime) / float64(timescale)
// 确保样本数据有效
if sample.Size <= 0 || sample.Size > 10*1024*1024 { // 10MB限制
p.Warn("invalid sample size", "size", sample.Size, "timestamp", sample.Timestamp)
continue
}
// 读取样本数据
if _, err := file.Seek(sample.Offset, io.SeekStart); err != nil {
p.Warn("seek error", "error", err, "offset", sample.Offset)
continue
}
data := make([]byte, sample.Size)
if _, err := io.ReadFull(file, data); err != nil {
p.Warn("read sample error", "error", err, "size", sample.Size)
continue
}
// 创建新的样本
newSample := box.Sample{
KeyFrame: sample.KeyFrame,
Timestamp: adjustedTimestamp,
Offset: sampleOffset,
Duration: sample.Duration,
}
newSample.PushOne(data)
// p.Info("extractGop", "KeyFrame", newSample.KeyFrame,
// "CTS", newSample.CTS,
// "Timestamp", newSample.Timestamp,
// "Offset", newSample.Offset,
// "Size", newSample.Size,
// "Duration", newSample.Duration,
// "Data", Bytes2HexStr(newSample.Data, 16))
sampleOffset += int64(newSample.Size)
filteredSamples = append(filteredSamples, newSample)
}
}
if len(filteredSamples) == 0 {
return 0, fmt.Errorf("no valid video samples found")
}
// 按25fps重新计算时间戳
for i := range filteredSamples {
filteredSamples[i].Timestamp = uint32(i * targetFrameInterval)
}
// 添加样本到轨道
for _, sample := range filteredSamples {
videoTrack.AddSampleEntry(sample)
}
// 计算视频时长
videoDuration := uint32(len(filteredSamples) * targetFrameInterval)
// 写入输出文件
if flag == 0 {
// 非分片MP4处理
moovSize := muxer.MakeMoov().Size()
dataSize := uint64(sampleOffset - mdatOffset)
// 调整sample偏移量
for _, track := range muxer.Tracks {
for i := range track.Samplelist {
track.Samplelist[i].Offset += int64(moovSize)
}
}
// 创建MDAT盒子 (添加8字节头)
mdatHeaderSize := uint64(8)
mdatBox := box.CreateBaseBox(box.TypeMDAT, dataSize+mdatHeaderSize)
var freeBox *box.FreeBox
if mdatBox.HeaderSize() == box.BasicBoxLen {
freeBox = box.CreateFreeBox(nil)
}
// 写入文件头
_, err := box.WriteTo(outputFile, ftyp, muxer.MakeMoov(), freeBox, mdatBox)
if err != nil {
return 0, fmt.Errorf("failed to write header: %v", err)
}
for _, track := range muxer.Tracks {
for i := range track.Samplelist {
track.Samplelist[i].Offset += int64(moovSize)
if _, err := outputFile.Write(track.Samplelist[i].Buffers[0]); err != nil {
return 0, err
}
}
}
} else {
// 分片MP4处理
var children []box.IBox
moov := muxer.MakeMoov()
children = append(children, ftyp, moov)
// 创建分片
for _, sample := range filteredSamples {
moof, mdat := muxer.CreateFlagment(videoTrack, sample)
children = append(children, moof, mdat)
}
_, err := box.WriteTo(outputFile, children...)
if err != nil {
return 0, fmt.Errorf("failed to write fragmented MP4: %v", err)
}
}
p.Info("extract gop video saved",
"targetTime", targetTime,
"compressedDuration", videoDuration,
"gopElapsed", gopElapsed,
"frameCount", len(filteredSamples),
"fps", 25)
return gopElapsed, nil
}
/*
提取压缩视频
GET http://192.168.0.238:8080/mp4/extract/compressed/
njtv/glgc.mp4?
start=1748620153000&
end=1748620453000&
outputPath=/opt/njtv/1748620153000.mp4
gopSeconds=1&
gopInterval=1&
*/
func (p *MP4Plugin) extractCompressedVideoHandel(w http.ResponseWriter, r *http.Request) {
streamPath := r.PathValue("streamPath")
query := r.URL.Query()
// 合并多个 mp4
startTime, endTime, err := util.TimeRangeQueryParse(query)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
p.Info("extractCompressedVideoHandel", "streamPath", streamPath, "start", startTime, "end", endTime)
gopSeconds, _ := strconv.ParseFloat(query.Get("gopSeconds"), 64)
gopInterval, _ := strconv.Atoi(query.Get("gopInterval"))
if gopSeconds == 0 {
gopSeconds = 1
}
if gopInterval == 0 {
gopInterval = 1
}
// 设置响应头
w.Header().Set("Content-Type", "video/mp4")
w.Header().Set("Content-Disposition", "attachment; filename=\"compressed_video.mp4\"")
err = p.extractCompressedVideo(streamPath, startTime, endTime, w, gopSeconds, gopInterval)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
func (p *MP4Plugin) extractGopVideoHandel(w http.ResponseWriter, r *http.Request) {
streamPath := r.PathValue("streamPath")
query := r.URL.Query()
targetTimeString := query.Get("targetTime")
// 合并多个 mp4
targetTime, err := util.UnixTimeQueryParse(targetTimeString)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
p.Info("extractGopVideoHandel", "streamPath", streamPath, "targetTime", targetTime)
// 设置响应头
w.Header().Set("Content-Type", "video/mp4")
w.Header().Set("Content-Disposition", "attachment; filename=\"gop_video.mp4\"")
_, err = p.extractGopVideo(streamPath, targetTime, w)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
func (p *MP4Plugin) snapHandel(w http.ResponseWriter, r *http.Request) {
streamPath := r.PathValue("streamPath")
query := r.URL.Query()
targetTimeString := query.Get("targetTime")
// 合并多个 mp4
targetTime, err := util.UnixTimeQueryParse(targetTimeString)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
p.Info("snapHandel", "streamPath", streamPath, "targetTime", targetTime)
// 设置响应头
w.Header().Set("Content-Type", "image/jpeg")
w.Header().Set("Content-Disposition", "attachment; filename=\"snapshot.jpg\"")
err = p.snapToWriter(streamPath, targetTime, w)
if err != nil {
p.Info("snapHandel", "err", err)
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
}
func (p *MP4Plugin) snapToWriter(streamPath string, targetTime time.Time, writer io.Writer) error {
if p.DB == nil {
return pkg.ErrNoDB
}
var flag mp4.Flag
if strings.HasSuffix(streamPath, ".fmp4") {
flag = mp4.FLAG_FRAGMENT
streamPath = strings.TrimSuffix(streamPath, ".fmp4")
} else {
streamPath = strings.TrimSuffix(streamPath, ".mp4")
}
// 查询数据库获取符合条件的片段
queryRecord := m7s.RecordStream{
Type: "mp4",
}
var streams []m7s.RecordStream
p.DB.Where(&queryRecord).Find(&streams, "end_time>=? AND start_time<=? AND stream_path=?", targetTime, targetTime, streamPath)
if len(streams) == 0 {
return fmt.Errorf("no matching MP4 segments found")
}
muxer := mp4.NewMuxer(flag)
ftyp := muxer.CreateFTYPBox()
n := ftyp.Size()
muxer.CurrentOffset = int64(n)
var videoTrack *mp4.Track
sampleOffset := muxer.CurrentOffset + mp4.BeforeMdatData
//var audioTrack *mp4.Track
var extraData []byte
// 压缩相关变量
findGOP := false
var filteredSamples net.Buffers
var sampleIdx = 0
// 仅处理视频轨道
for _, stream := range streams {
file, err := os.Open(stream.FilePath)
if err != nil {
return fmt.Errorf("failed to open file %s: %v", stream.FilePath, err)
}
defer file.Close()
p.Info("processing segment", "file", file.Name())
demuxer := mp4.NewDemuxer(file)
err = demuxer.Demux()
if err != nil {
p.Warn("demux error, skipping segment", "error", err, "file", stream.FilePath)
continue
}
// 确保有视频轨道
var hasVideo bool
for _, track := range demuxer.Tracks {
if track.Cid.IsVideo() {
hasVideo = true
// 只在第一个片段或关键帧变化时更新extraData
trackExtraData := track.GetRecord()
if extraData == nil || !bytes.Equal(extraData, trackExtraData) {
extraData = trackExtraData
if videoTrack == nil {
videoTrack = muxer.AddTrack(track.Cid)
videoTrack.ICodecCtx = track.ICodecCtx
}
}
break
}
}
if !hasVideo {
p.Warn("no video track found in segment", "file", stream.FilePath)
continue
}
startTimestamp := targetTime.Sub(stream.StartTime).Milliseconds()
if startTimestamp < 0 {
startTimestamp = 0
}
//通过时间戳定位到最近的关键帧如视频IDR帧返回的startSample是该关键帧对应的样本
startSample, err := demuxer.SeekTime(uint64(startTimestamp))
if err == nil {
}
// 处理样本
//RangeSample迭代的是当前时间范围内的所有样本可能包含非关键帧顺序取决于MP4文件中样本的物理存储顺序
for track, sample := range demuxer.RangeSample {
if !track.Cid.IsVideo() {
continue
}
if sample.Timestamp < startSample.Timestamp {
continue
}
//记录GOP内帧的序号没有考虑B帧的情况
if sample.Timestamp < uint32(startTimestamp) {
sampleIdx++
}
// 处理GOP逻辑,已经处理完上一个gop
if sample.KeyFrame && findGOP {
break
}
// 处理GOP逻辑
if sample.KeyFrame && !findGOP {
findGOP = true
}
// 跳过不在当前GOP的帧
if !findGOP {
continue
}
// 确保样本数据有效
if sample.Size <= 0 || sample.Size > 10*1024*1024 { // 10MB限制
p.Warn("invalid sample size", "size", sample.Size, "timestamp", sample.Timestamp)
continue
}
// 读取样本数据
if _, err := file.Seek(sample.Offset, io.SeekStart); err != nil {
p.Warn("seek error", "error", err, "offset", sample.Offset)
continue
}
data := make([]byte, sample.Size)
if _, err := io.ReadFull(file, data); err != nil {
p.Warn("read sample error", "error", err, "size", sample.Size)
continue
}
for offset := 0; offset < sample.Size; {
nalusSize := util.BigEndian.Uint32(data[offset:])
filteredSamples = append(filteredSamples, codec.NALU_Delimiter2[:], data[offset+4:offset+4+int(nalusSize)])
offset += int(nalusSize) + 4
}
sampleOffset += int64(sample.Size)
}
}
if len(filteredSamples) == 0 {
return fmt.Errorf("no valid video samples found")
}
err := ProcessWithFFmpeg(filteredSamples, sampleIdx, writer)
if err != nil {
return err
}
p.Info("extract gop and snap saved",
"targetTime", targetTime,
"frameCount", len(filteredSamples))
return nil
}