mirror of
https://github.com/langhuihui/monibuca.git
synced 2025-12-24 13:48:04 +08:00
- 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>
252 lines
7.0 KiB
Go
252 lines
7.0 KiB
Go
package flv
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
"m7s.live/v5"
|
|
"m7s.live/v5/pkg"
|
|
"m7s.live/v5/pkg/config"
|
|
"m7s.live/v5/pkg/task"
|
|
rtmp "m7s.live/v5/plugin/rtmp/pkg"
|
|
)
|
|
|
|
type WriteFlvMetaTagQueueTask struct {
|
|
task.Work
|
|
}
|
|
|
|
var writeMetaTagQueueTask WriteFlvMetaTagQueueTask
|
|
|
|
func init() {
|
|
m7s.Servers.AddTask(&writeMetaTagQueueTask)
|
|
}
|
|
|
|
type writeMetaTagTask struct {
|
|
task.Task
|
|
file *os.File
|
|
writer *FlvWriter
|
|
flags byte
|
|
metaData []byte
|
|
}
|
|
|
|
func (task *writeMetaTagTask) Start() (err error) {
|
|
defer func() {
|
|
err = task.file.Close()
|
|
if info, err := task.file.Stat(); err == nil && info.Size() == 0 {
|
|
err = os.Remove(info.Name())
|
|
}
|
|
}()
|
|
var tempFile *os.File
|
|
if tempFile, err = os.CreateTemp("", "*.flv"); err != nil {
|
|
task.Error("create temp file failed", "err", err)
|
|
return
|
|
} else {
|
|
defer func() {
|
|
err = tempFile.Close()
|
|
err = os.Remove(tempFile.Name())
|
|
task.Info("writeMetaData success")
|
|
}()
|
|
_, err = tempFile.Write([]byte{'F', 'L', 'V', 0x01, task.flags, 0, 0, 0, 9, 0, 0, 0, 0})
|
|
if err != nil {
|
|
task.Error(err.Error())
|
|
return
|
|
}
|
|
task.writer = NewFlvWriter(tempFile)
|
|
err = task.writer.WriteTag(FLV_TAG_TYPE_SCRIPT, 0, uint32(len(task.metaData)), task.metaData)
|
|
_, err = task.file.Seek(13, io.SeekStart)
|
|
if err != nil {
|
|
task.Error("writeMetaData Seek failed", "err", err)
|
|
return
|
|
}
|
|
_, err = io.Copy(tempFile, task.file)
|
|
if err != nil {
|
|
task.Error("writeMetaData Copy failed", "err", err)
|
|
return
|
|
}
|
|
_, err = tempFile.Seek(0, io.SeekStart)
|
|
_, err = task.file.Seek(0, io.SeekStart)
|
|
_, err = io.Copy(task.file, tempFile)
|
|
if err != nil {
|
|
task.Error("writeMetaData Copy failed", "err", err)
|
|
}
|
|
return
|
|
}
|
|
}
|
|
|
|
func writeMetaTag(file *os.File, suber *m7s.Subscriber, filepositions []uint64, times []float64, duration *int64) {
|
|
ar, vr := suber.AudioReader, suber.VideoReader
|
|
hasAudio, hasVideo := ar != nil, vr != nil
|
|
var amf rtmp.AMF
|
|
metaData := rtmp.EcmaArray{
|
|
"MetaDataCreator": "m7s/" + m7s.Version,
|
|
"hasVideo": hasVideo,
|
|
"hasAudio": hasAudio,
|
|
"hasMatadata": true,
|
|
"canSeekToEnd": true,
|
|
"duration": float64(*duration) / 1000,
|
|
"hasKeyFrames": len(filepositions) > 0,
|
|
"filesize": 0,
|
|
}
|
|
var flags byte
|
|
if hasAudio {
|
|
ctx := ar.Track.ICodecCtx.GetBase().(pkg.IAudioCodecCtx)
|
|
flags |= (1 << 2)
|
|
metaData["audiocodecid"] = int(rtmp.ParseAudioCodec(ctx.FourCC()))
|
|
metaData["audiosamplerate"] = ctx.GetSampleRate()
|
|
metaData["audiosamplesize"] = ctx.GetSampleSize()
|
|
metaData["stereo"] = ctx.GetChannels() == 2
|
|
}
|
|
if hasVideo {
|
|
ctx := vr.Track.ICodecCtx.GetBase().(pkg.IVideoCodecCtx)
|
|
flags |= 1
|
|
metaData["videocodecid"] = int(rtmp.ParseVideoCodec(ctx.FourCC()))
|
|
metaData["width"] = ctx.Width()
|
|
metaData["height"] = ctx.Height()
|
|
metaData["framerate"] = vr.Track.FPS
|
|
metaData["videodatarate"] = vr.Track.BPS
|
|
metaData["keyframes"] = map[string]any{
|
|
"filepositions": filepositions,
|
|
"times": times,
|
|
}
|
|
}
|
|
amf.Marshals("onMetaData", metaData)
|
|
offset := amf.GetBuffer().Len() + 13 + 15
|
|
if keyframesCount := len(filepositions); keyframesCount > 0 {
|
|
metaData["filesize"] = uint64(offset) + filepositions[keyframesCount-1]
|
|
for i := range filepositions {
|
|
filepositions[i] += uint64(offset)
|
|
}
|
|
metaData["keyframes"] = map[string]any{
|
|
"filepositions": filepositions,
|
|
"times": times,
|
|
}
|
|
}
|
|
amf.GetBuffer().Reset()
|
|
marshals := amf.Marshals("onMetaData", metaData)
|
|
task := &writeMetaTagTask{
|
|
file: file,
|
|
flags: flags,
|
|
metaData: marshals,
|
|
}
|
|
task.Logger = suber.Logger.With("file", file.Name())
|
|
writeMetaTagQueueTask.AddTask(task)
|
|
}
|
|
|
|
func NewRecorder(conf config.Record) m7s.IRecorder {
|
|
return &Recorder{}
|
|
}
|
|
|
|
type Recorder struct {
|
|
m7s.DefaultRecorder
|
|
writer *FlvWriter
|
|
file *os.File
|
|
}
|
|
|
|
var CustomFileName = func(job *m7s.RecordJob) string {
|
|
if job.RecConf.Fragment == 0 || job.RecConf.Append {
|
|
return fmt.Sprintf("%s.flv", job.RecConf.FilePath)
|
|
}
|
|
return filepath.Join(job.RecConf.FilePath, fmt.Sprintf("%d.flv", time.Now().Unix()))
|
|
}
|
|
|
|
func (r *Recorder) createStream(start time.Time) (err error) {
|
|
r.RecordJob.RecConf.Type = "flv"
|
|
err = r.CreateStream(start, CustomFileName)
|
|
if err != nil {
|
|
return
|
|
}
|
|
if r.file, err = os.OpenFile(r.Event.FilePath, os.O_CREATE|os.O_RDWR, 0666); err != nil {
|
|
return
|
|
}
|
|
_, err = r.file.Write(FLVHead)
|
|
r.writer = NewFlvWriter(r.file)
|
|
if err != nil {
|
|
return
|
|
}
|
|
return
|
|
}
|
|
|
|
func (r *Recorder) writeTailer(end time.Time) {
|
|
if r.Event.EndTime.After(r.Event.StartTime) {
|
|
return
|
|
}
|
|
r.Event.EndTime = end
|
|
if r.RecordJob.Plugin.DB != nil {
|
|
if r.RecordJob.Event != nil {
|
|
r.RecordJob.Plugin.DB.Save(&r.Event)
|
|
} else {
|
|
r.RecordJob.Plugin.DB.Save(&r.Event.RecordStream)
|
|
}
|
|
writeMetaTagQueueTask.AddTask(m7s.NewEventRecordCheck(r.Event.Type, r.Event.StreamPath, r.RecordJob.Plugin.DB))
|
|
}
|
|
}
|
|
|
|
func (r *Recorder) Dispose() {
|
|
r.writeTailer(time.Now())
|
|
}
|
|
|
|
func (r *Recorder) Run() (err error) {
|
|
var filepositions []uint64
|
|
var times []float64
|
|
var offset int64
|
|
var duration int64
|
|
ctx := &r.RecordJob
|
|
suber := ctx.Subscriber
|
|
noFragment := ctx.RecConf.Fragment == 0 || ctx.RecConf.Append
|
|
checkFragment := func(absTime uint32, writeTime time.Time) {
|
|
if duration = int64(absTime); time.Duration(duration)*time.Millisecond >= ctx.RecConf.Fragment {
|
|
writeMetaTag(r.file, suber, filepositions, times, &duration)
|
|
r.writeTailer(writeTime)
|
|
filepositions = []uint64{0}
|
|
times = []float64{0}
|
|
offset = 0
|
|
if err = r.createStream(writeTime); err != nil {
|
|
return
|
|
}
|
|
if vr := suber.VideoReader; vr != nil {
|
|
vr.ResetAbsTime()
|
|
seq := vr.Track.ICodecCtx.(pkg.ISequenceCodecCtx[*rtmp.VideoFrame]).GetSequenceFrame()
|
|
err = r.writer.WriteTag(FLV_TAG_TYPE_VIDEO, 0, uint32(seq.Size), seq.Buffers...)
|
|
offset = int64(seq.Size + 15)
|
|
}
|
|
if ar := suber.AudioReader; ar != nil {
|
|
ar.ResetAbsTime()
|
|
if seqCtx, ok := ar.Track.ICodecCtx.(pkg.ISequenceCodecCtx[*rtmp.AudioFrame]); ok {
|
|
seq := seqCtx.GetSequenceFrame()
|
|
err = r.writer.WriteTag(FLV_TAG_TYPE_AUDIO, 0, uint32(seq.Size), seq.Buffers...)
|
|
offset += int64(seq.Size + 15)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return m7s.PlayBlock(ctx.Subscriber, func(audio *rtmp.AudioFrame) (err error) {
|
|
if suber.VideoReader == nil && !noFragment {
|
|
checkFragment(suber.AudioReader.AbsTime, suber.AudioReader.Value.WriteTime)
|
|
}
|
|
err = r.writer.WriteTag(FLV_TAG_TYPE_AUDIO, suber.AudioReader.AbsTime, uint32(audio.Size), audio.Buffers...)
|
|
offset += int64(audio.Size + 15)
|
|
return
|
|
}, func(video *rtmp.VideoFrame) (err error) {
|
|
if r.Event.StartTime.IsZero() {
|
|
err = r.createStream(suber.VideoReader.Value.WriteTime)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if suber.VideoReader.Value.IDR {
|
|
filepositions = append(filepositions, uint64(offset))
|
|
times = append(times, float64(suber.VideoReader.AbsTime)/1000)
|
|
if !noFragment {
|
|
checkFragment(suber.VideoReader.AbsTime, suber.VideoReader.Value.WriteTime)
|
|
}
|
|
}
|
|
err = r.writer.WriteTag(FLV_TAG_TYPE_VIDEO, suber.VideoReader.AbsTime, uint32(video.Size), video.Buffers...)
|
|
offset += int64(video.Size + 15)
|
|
return
|
|
})
|
|
}
|