mirror of
https://github.com/langhuihui/monibuca.git
synced 2025-12-24 13:48:04 +08:00
405 lines
10 KiB
Go
405 lines
10 KiB
Go
package mp4
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
task "github.com/langhuihui/gotask"
|
|
m7s "m7s.live/v5"
|
|
"m7s.live/v5/pkg"
|
|
"m7s.live/v5/pkg/codec"
|
|
"m7s.live/v5/pkg/config"
|
|
"m7s.live/v5/pkg/storage"
|
|
"m7s.live/v5/pkg/util"
|
|
"m7s.live/v5/plugin/mp4/pkg/box"
|
|
|
|
"github.com/langhuihui/gomem"
|
|
)
|
|
|
|
type WriteTrailerQueueTask struct {
|
|
task.Work
|
|
}
|
|
|
|
var writeTrailerQueueTask WriteTrailerQueueTask
|
|
|
|
type writeTrailerTask struct {
|
|
task.Task
|
|
muxer *Muxer
|
|
file storage.File
|
|
filePath string
|
|
}
|
|
|
|
func (task *writeTrailerTask) Start() (err error) {
|
|
err = task.muxer.WriteTrailer(task.file)
|
|
if err != nil {
|
|
task.Error("write trailer", "err", err)
|
|
if task.file != nil {
|
|
if errClose := task.file.Close(); errClose != nil {
|
|
return errClose
|
|
}
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
const BeforeMdatData = 16 // free box + mdat box header or big mdat box header
|
|
// 将 moov 从末尾移动到前方
|
|
// 将 ftyp + free(optional) + moov + mdat 写入临时文件, 然后替换原文件
|
|
func (t *writeTrailerTask) Run() (err error) {
|
|
t.Info("write trailer")
|
|
var temp *os.File
|
|
temp, err = os.CreateTemp("", "*.mp4")
|
|
if err != nil {
|
|
t.Error("create temp file", "err", err)
|
|
return
|
|
}
|
|
|
|
defer os.Remove(temp.Name())
|
|
|
|
_, err = t.file.Seek(0, io.SeekStart)
|
|
if err != nil {
|
|
t.Error("seek file", "err", err)
|
|
return
|
|
}
|
|
// 复制 mdat box之前的内容
|
|
_, err = io.CopyN(temp, t.file, int64(t.muxer.mdatOffset)-BeforeMdatData)
|
|
if err != nil {
|
|
t.Error("copy file", "err", err)
|
|
return
|
|
}
|
|
for _, track := range t.muxer.Tracks {
|
|
for i := range len(track.Samplelist) {
|
|
track.Samplelist[i].Offset += int64(t.muxer.moov.Size())
|
|
}
|
|
}
|
|
err = t.muxer.WriteMoov(temp)
|
|
if err != nil {
|
|
t.Error("rewrite with moov", "err", err)
|
|
return
|
|
}
|
|
// 复制 mdat box
|
|
_, err = io.CopyN(temp, t.file, int64(t.muxer.mdatSize)+BeforeMdatData)
|
|
|
|
if err != nil {
|
|
if err == pkg.ErrSkip {
|
|
return task.ErrTaskComplete
|
|
}
|
|
t.Error("rewrite with moov", "err", err)
|
|
return
|
|
}
|
|
if _, err = t.file.Seek(0, io.SeekStart); err != nil {
|
|
t.Error("seek file", "err", err)
|
|
return
|
|
}
|
|
if _, err = temp.Seek(0, io.SeekStart); err != nil {
|
|
t.Error("seek temp file", "err", err)
|
|
return
|
|
}
|
|
if _, err = io.Copy(t.file, temp); err != nil {
|
|
t.Error("copy file", "err", err)
|
|
return
|
|
}
|
|
if err = t.file.Close(); err != nil {
|
|
t.Error("close file", "err", err)
|
|
return
|
|
}
|
|
if err = temp.Close(); err != nil {
|
|
t.Error("close temp file", "err", err)
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func init() {
|
|
m7s.Servers.AddTask(&writeTrailerQueueTask)
|
|
}
|
|
|
|
func NewRecorder(conf config.Record) m7s.IRecorder {
|
|
return &Recorder{}
|
|
}
|
|
|
|
type Recorder struct {
|
|
m7s.DefaultRecorder
|
|
muxer *Muxer
|
|
file storage.File
|
|
firstVideoFrame bool // 标记是否是第一个视频帧
|
|
}
|
|
|
|
func (r *Recorder) writeTailer(end time.Time) {
|
|
r.WriteTail(end, &writeTrailerQueueTask)
|
|
writeTrailerQueueTask.AddTask(&writeTrailerTask{
|
|
muxer: r.muxer,
|
|
file: r.file,
|
|
filePath: r.Event.FilePath,
|
|
}, r.Logger)
|
|
}
|
|
|
|
var CustomFileName = func(job *m7s.RecordJob) string {
|
|
now := time.Now()
|
|
return filepath.Join(job.RecConf.FilePath, fmt.Sprintf("%s_%09d.mp4", time.Now().Local().Format("2006-01-02-15-04-05"), now.Nanosecond()))
|
|
}
|
|
|
|
func (r *Recorder) createStream(start time.Time) (err error) {
|
|
if r.RecordJob.RecConf.Type == "" {
|
|
r.RecordJob.RecConf.Type = "mp4"
|
|
}
|
|
err = r.CreateStream(start, CustomFileName)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
// 注意: 不要在这里关闭旧文件,因为它已经被传递给 writeTrailerTask
|
|
// writeTrailerTask 会负责关闭旧文件
|
|
// 直接创建新文件并覆盖 r.file
|
|
|
|
// 获取存储实例
|
|
storage := r.RecordJob.GetStorage()
|
|
|
|
if storage != nil {
|
|
// 使用存储抽象层
|
|
r.file, err = storage.CreateFile(context.Background(), r.Event.FilePath)
|
|
if err != nil {
|
|
return
|
|
}
|
|
} else {
|
|
// 默认本地文件行为
|
|
// 使用 OpenFile 以读写模式打开,因为 writeTrailerTask.Run() 需要读取文件内容
|
|
r.file, err = os.OpenFile(r.Event.FilePath, os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0644)
|
|
if err != nil {
|
|
return
|
|
}
|
|
}
|
|
|
|
if r.Event.Type == "fmp4" {
|
|
r.muxer = NewMuxerWithStreamPath(FLAG_FRAGMENT, r.Event.StreamPath)
|
|
} else {
|
|
r.muxer = NewMuxerWithStreamPath(0, r.Event.StreamPath)
|
|
}
|
|
|
|
r.firstVideoFrame = true // 重置第一个视频帧标志
|
|
return r.muxer.WriteInitSegment(r.file)
|
|
}
|
|
|
|
func (r *Recorder) Dispose() {
|
|
if r.muxer != nil {
|
|
r.writeTailer(time.Now())
|
|
// 注意: 文件的关闭由 writeTrailerTask.Run() 负责
|
|
// 不在这里关闭,避免在异步任务执行前文件被关闭
|
|
} else {
|
|
// 如果没有 muxer,需要在这里关闭文件
|
|
if r.file != nil {
|
|
r.file.Close()
|
|
}
|
|
}
|
|
}
|
|
|
|
func (r *Recorder) Run() (err error) {
|
|
recordJob := &r.RecordJob
|
|
sub := recordJob.Subscriber
|
|
var audioTrack, videoTrack *Track
|
|
var at, vt *pkg.AVTrack
|
|
checkEventRecordStop := func(absTime uint32) (err error) {
|
|
if absTime >= recordJob.Event.AfterDuration+recordJob.Event.BeforeDuration {
|
|
r.RecordJob.Stop(task.ErrStopByUser)
|
|
}
|
|
return
|
|
}
|
|
|
|
checkFragment := func(reader *pkg.AVRingReader) (err error) {
|
|
if duration := int64(reader.AbsTime); time.Duration(duration)*time.Millisecond >= recordJob.RecConf.Fragment {
|
|
r.writeTailer(reader.Value.WriteTime)
|
|
err = r.createStream(reader.Value.WriteTime)
|
|
if err != nil {
|
|
return
|
|
}
|
|
at, vt = nil, nil
|
|
if vr := sub.VideoReader; vr != nil {
|
|
vr.ResetAbsTime()
|
|
}
|
|
if ar := sub.AudioReader; ar != nil {
|
|
ar.ResetAbsTime()
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
return m7s.PlayBlock(sub, func(audio *AudioFrame) error {
|
|
if r.Event.StartTime.IsZero() {
|
|
err = r.createStream(sub.AudioReader.Value.WriteTime)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
r.Event.Duration = sub.AudioReader.AbsTime
|
|
if sub.VideoReader == nil {
|
|
if recordJob.Event != nil {
|
|
err = checkEventRecordStop(sub.VideoReader.AbsTime)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if recordJob.RecConf.Fragment != 0 {
|
|
err = checkFragment(sub.AudioReader)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
if at == nil {
|
|
at = sub.AudioReader.Track
|
|
switch at.ICodecCtx.GetBase().(type) {
|
|
case *codec.AACCtx:
|
|
track := r.muxer.AddTrack(box.MP4_CODEC_AAC)
|
|
audioTrack = track
|
|
track.ICodecCtx = at.ICodecCtx
|
|
case *codec.PCMACtx:
|
|
track := r.muxer.AddTrack(box.MP4_CODEC_G711A)
|
|
audioTrack = track
|
|
track.ICodecCtx = at.ICodecCtx
|
|
case *codec.PCMUCtx:
|
|
track := r.muxer.AddTrack(box.MP4_CODEC_G711U)
|
|
audioTrack = track
|
|
track.ICodecCtx = at.ICodecCtx
|
|
}
|
|
}
|
|
sample := box.Sample{
|
|
Timestamp: sub.AudioReader.AbsTime,
|
|
Memory: audio.Memory,
|
|
}
|
|
return r.muxer.WriteSample(r.file, audioTrack, sample)
|
|
}, func(video *VideoFrame) error {
|
|
if r.Event.StartTime.IsZero() {
|
|
err = r.createStream(sub.VideoReader.Value.WriteTime)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
r.Event.Duration = sub.VideoReader.AbsTime
|
|
if sub.VideoReader.Value.IDR {
|
|
if recordJob.Event != nil {
|
|
err = checkEventRecordStop(sub.VideoReader.AbsTime)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if recordJob.RecConf.Fragment != 0 {
|
|
err = checkFragment(sub.VideoReader)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
if vt == nil {
|
|
vt = sub.VideoReader.Track
|
|
switch video.ICodecCtx.GetBase().(type) {
|
|
case *codec.H264Ctx:
|
|
track := r.muxer.AddTrack(box.MP4_CODEC_H264)
|
|
videoTrack = track
|
|
track.ICodecCtx = video.ICodecCtx
|
|
case *codec.H265Ctx:
|
|
track := r.muxer.AddTrack(box.MP4_CODEC_H265)
|
|
videoTrack = track
|
|
track.ICodecCtx = video.ICodecCtx
|
|
}
|
|
}
|
|
//ctx := video.ICodecCtx.(pkg.IVideoCodecCtx)
|
|
//if videoTrackCtx, ok := videoTrack.ICodecCtx.(pkg.IVideoCodecCtx); ok && videoTrackCtx != ctx {
|
|
// width, height := uint32(ctx.Width()), uint32(ctx.Height())
|
|
// oldWidth, oldHeight := uint32(videoTrackCtx.Width()), uint32(videoTrackCtx.Height())
|
|
// r.Info("ctx changed, restarting recording",
|
|
// "old", fmt.Sprintf("%dx%d", oldWidth, oldHeight),
|
|
// "new", fmt.Sprintf("%dx%d", width, height))
|
|
// r.writeTailer(sub.VideoReader.Value.WriteTime)
|
|
// err = r.createStream(sub.VideoReader.Value.WriteTime)
|
|
// if err != nil {
|
|
// return nil
|
|
// }
|
|
// at, vt = nil, nil
|
|
// if vr := sub.VideoReader; vr != nil {
|
|
// vr.ResetAbsTime()
|
|
// vt = vr.Track
|
|
// switch video.ICodecCtx.GetBase().(type) {
|
|
// case *codec.H264Ctx:
|
|
// track := r.muxer.AddTrack(box.MP4_CODEC_H264)
|
|
// videoTrack = track
|
|
// track.ICodecCtx = video.ICodecCtx
|
|
// case *codec.H265Ctx:
|
|
// track := r.muxer.AddTrack(box.MP4_CODEC_H265)
|
|
// videoTrack = track
|
|
// track.ICodecCtx = video.ICodecCtx
|
|
// }
|
|
// }
|
|
// if ar := sub.AudioReader; ar != nil {
|
|
// ar.ResetAbsTime()
|
|
// }
|
|
//}
|
|
sample := box.Sample{
|
|
Timestamp: sub.VideoReader.AbsTime,
|
|
KeyFrame: video.IDR,
|
|
CTS: video.GetCTS32(),
|
|
Memory: video.Memory,
|
|
}
|
|
// 如果是第一个视频 I 帧,将参数集放在 I 帧前面一起写入
|
|
//if r.firstVideoFrame && video.IDR {
|
|
if video.IDR {
|
|
// 创建包含参数集的 Memory
|
|
var combinedMemory gomem.Memory
|
|
var naluSizeLen int = 4
|
|
var sps, pps, vps []byte
|
|
|
|
switch ctx := video.ICodecCtx.GetBase().(type) {
|
|
case *codec.H264Ctx:
|
|
naluSizeLen = int(ctx.RecordInfo.LengthSizeMinusOne) + 1
|
|
sps = ctx.SPS()
|
|
pps = ctx.PPS()
|
|
if len(sps) > 0 && len(pps) > 0 {
|
|
// 写入 SPS
|
|
sizeBuf := make([]byte, naluSizeLen)
|
|
util.PutBE(sizeBuf, uint32(len(sps)))
|
|
combinedMemory.Push(sizeBuf)
|
|
combinedMemory.Push(sps)
|
|
// 写入 PPS
|
|
sizeBuf = make([]byte, naluSizeLen)
|
|
util.PutBE(sizeBuf, uint32(len(pps)))
|
|
combinedMemory.Push(sizeBuf)
|
|
combinedMemory.Push(pps)
|
|
}
|
|
case *codec.H265Ctx:
|
|
naluSizeLen = int(ctx.RecordInfo.LengthSizeMinusOne) + 1
|
|
vps = ctx.VPS()
|
|
sps = ctx.SPS()
|
|
pps = ctx.PPS()
|
|
if len(vps) > 0 && len(sps) > 0 && len(pps) > 0 {
|
|
// 写入 VPS
|
|
sizeBuf := make([]byte, naluSizeLen)
|
|
util.PutBE(sizeBuf, uint32(len(vps)))
|
|
combinedMemory.Push(sizeBuf)
|
|
combinedMemory.Push(vps)
|
|
// 写入 SPS
|
|
sizeBuf = make([]byte, naluSizeLen)
|
|
util.PutBE(sizeBuf, uint32(len(sps)))
|
|
combinedMemory.Push(sizeBuf)
|
|
combinedMemory.Push(sps)
|
|
// 写入 PPS
|
|
sizeBuf = make([]byte, naluSizeLen)
|
|
util.PutBE(sizeBuf, uint32(len(pps)))
|
|
combinedMemory.Push(sizeBuf)
|
|
combinedMemory.Push(pps)
|
|
}
|
|
}
|
|
// 将原始视频帧数据追加到参数集后面
|
|
combinedMemory.Push(video.Memory.Buffers...)
|
|
sample.Memory = combinedMemory
|
|
r.firstVideoFrame = false
|
|
} else if r.firstVideoFrame {
|
|
r.firstVideoFrame = false
|
|
}
|
|
return r.muxer.WriteSample(r.file, videoTrack, sample)
|
|
})
|
|
}
|