mirror of
https://github.com/Monibuca/plugin-record.git
synced 2025-10-16 13:50:58 +08:00
181 lines
4.5 KiB
Go
181 lines
4.5 KiB
Go
package record
|
|
|
|
import (
|
|
"path/filepath"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/edgeware/mp4ff/aac"
|
|
"github.com/edgeware/mp4ff/mp4"
|
|
. "m7s.live/engine/v4"
|
|
"m7s.live/engine/v4/codec"
|
|
"m7s.live/engine/v4/track"
|
|
"m7s.live/engine/v4/util"
|
|
)
|
|
|
|
var defaultFtyp = mp4.NewFtyp("isom", 0x200, []string{
|
|
"isom", "iso2", "avc1", "mp41",
|
|
})
|
|
|
|
type mediaContext struct {
|
|
trackId uint32
|
|
fragment *mp4.Fragment
|
|
ts uint32 // 每个小片段起始时间戳
|
|
abs uint32 // 绝对起始时间戳
|
|
absSet bool // 是否设置过abs
|
|
}
|
|
|
|
func (m *mediaContext) push(recoder *MP4Recorder, dt uint32, dur uint32, data []byte, flags uint32) {
|
|
if !m.absSet {
|
|
m.abs = dt
|
|
m.absSet = true
|
|
}
|
|
dt -= m.abs
|
|
if m.fragment != nil && dt-m.ts > 5000 {
|
|
m.fragment.Encode(recoder)
|
|
m.fragment = nil
|
|
}
|
|
if m.fragment == nil {
|
|
recoder.seqNumber++
|
|
m.fragment, _ = mp4.CreateFragment(recoder.seqNumber, m.trackId)
|
|
m.ts = dt
|
|
}
|
|
m.fragment.AddFullSample(mp4.FullSample{
|
|
Data: data,
|
|
DecodeTime: uint64(dt),
|
|
Sample: mp4.Sample{
|
|
Flags: flags,
|
|
Dur: dur,
|
|
Size: uint32(len(data)),
|
|
},
|
|
})
|
|
}
|
|
|
|
type MP4Recorder struct {
|
|
Recorder
|
|
*mp4.InitSegment `json:"-"`
|
|
video mediaContext
|
|
audio mediaContext
|
|
seqNumber uint32
|
|
}
|
|
|
|
func NewMP4Recorder() *MP4Recorder {
|
|
r := &MP4Recorder{
|
|
InitSegment: mp4.CreateEmptyInit(),
|
|
}
|
|
r.Moov.Mvhd.NextTrackID = 1
|
|
return r
|
|
}
|
|
|
|
func (r *MP4Recorder) Start(streamPath string) (err error) {
|
|
r.Record = &recordConfig.Mp4
|
|
r.ID = streamPath + "/mp4"
|
|
return plugin.Subscribe(streamPath, r)
|
|
}
|
|
|
|
func (r *MP4Recorder) Close() error {
|
|
if r.Writer != nil {
|
|
if r.video.fragment != nil {
|
|
r.video.fragment.Encode(r.Writer)
|
|
r.video.fragment = nil
|
|
}
|
|
if r.audio.fragment != nil {
|
|
r.audio.fragment.Encode(r.Writer)
|
|
r.audio.fragment = nil
|
|
}
|
|
r.Closer.Close()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *MP4Recorder) OnEvent(event any) {
|
|
r.Recorder.OnEvent(event)
|
|
switch v := event.(type) {
|
|
case *track.Video:
|
|
moov := r.Moov
|
|
trackID := moov.Mvhd.NextTrackID
|
|
moov.Mvhd.NextTrackID++
|
|
newTrak := mp4.CreateEmptyTrak(trackID, 1000, "video", "chi")
|
|
moov.AddChild(newTrak)
|
|
moov.Mvex.AddChild(mp4.CreateTrex(trackID))
|
|
r.video.trackId = trackID
|
|
switch v.CodecID {
|
|
case codec.CodecID_H264:
|
|
newTrak.SetAVCDescriptor("avc1", v.DecoderConfiguration.Raw[0:1], v.DecoderConfiguration.Raw[1:2], true)
|
|
case codec.CodecID_H265:
|
|
newTrak.SetHEVCDescriptor("hev1", v.DecoderConfiguration.Raw[0:1], v.DecoderConfiguration.Raw[1:2], v.DecoderConfiguration.Raw[2:3], true)
|
|
}
|
|
r.AddTrack(v)
|
|
case *track.Audio:
|
|
moov := r.Moov
|
|
trackID := moov.Mvhd.NextTrackID
|
|
moov.Mvhd.NextTrackID++
|
|
newTrak := mp4.CreateEmptyTrak(trackID, 1000, "audio", "chi")
|
|
moov.AddChild(newTrak)
|
|
moov.Mvex.AddChild(mp4.CreateTrex(trackID))
|
|
r.audio.trackId = trackID
|
|
switch v.CodecID {
|
|
case codec.CodecID_AAC:
|
|
switch v.Profile {
|
|
case 0:
|
|
newTrak.SetAACDescriptor(aac.HEAACv1, int(v.SampleRate))
|
|
case 1:
|
|
newTrak.SetAACDescriptor(aac.AAClc, int(v.SampleRate))
|
|
case 2:
|
|
newTrak.SetAACDescriptor(aac.HEAACv2, int(v.SampleRate))
|
|
}
|
|
case codec.CodecID_PCMA:
|
|
stsd := newTrak.Mdia.Minf.Stbl.Stsd
|
|
pcma := mp4.CreateAudioSampleEntryBox("pcma",
|
|
uint16(v.Channels),
|
|
uint16(v.SampleSize), uint16(v.SampleRate), nil)
|
|
stsd.AddChild(pcma)
|
|
case codec.CodecID_PCMU:
|
|
stsd := newTrak.Mdia.Minf.Stbl.Stsd
|
|
pcmu := mp4.CreateAudioSampleEntryBox("pcmu",
|
|
uint16(v.Channels),
|
|
uint16(v.SampleSize), uint16(v.SampleRate), nil)
|
|
stsd.AddChild(pcmu)
|
|
}
|
|
r.AddTrack(v)
|
|
case ISubscriber:
|
|
defaultFtyp.Encode(r)
|
|
r.Moov.Encode(r)
|
|
go r.start()
|
|
case *AudioFrame:
|
|
if r.audio.trackId != 0 {
|
|
r.audio.push(r, v.AbsTime, v.DeltaTime, util.ConcatBuffers(v.Raw), mp4.SyncSampleFlags)
|
|
}
|
|
case *VideoFrame:
|
|
if r.Fragment != 0 && r.newFile {
|
|
r.newFile = false
|
|
r.Close()
|
|
if file, err := r.CreateFileFn(filepath.Join(r.Stream.Path, strconv.FormatInt(time.Now().Unix(), 10)+r.Ext), false); err == nil {
|
|
r.SetIO(file)
|
|
r.audio.absSet = false
|
|
r.video.absSet = false
|
|
r.InitSegment = mp4.CreateEmptyInit()
|
|
r.Moov.Mvhd.NextTrackID = 1
|
|
if r.Video.Track != nil {
|
|
r.OnEvent(r.Video.Track)
|
|
}
|
|
if r.Audio.Track != nil {
|
|
r.OnEvent(r.Audio.Track)
|
|
}
|
|
defaultFtyp.Encode(r)
|
|
r.Moov.Encode(r)
|
|
r.seqNumber = 0
|
|
}
|
|
}
|
|
if r.video.trackId != 0 {
|
|
flag := mp4.NonSyncSampleFlags
|
|
if v.IFrame {
|
|
flag = mp4.SyncSampleFlags
|
|
}
|
|
if data := util.ConcatBuffers(v.AVCC); len(data) > 5 {
|
|
r.video.push(r, v.AbsTime-r.SkipTS, v.DeltaTime, data[5:], flag)
|
|
}
|
|
}
|
|
}
|
|
}
|