diff --git a/README.md b/README.md index 93c5b98..d71d766 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,7 @@ record: - `/record/api/list/recording` 罗列所有正在录制中的流的信息 - `/record/api/list?type=flv` 罗列所有录制的flv文件 -- `/record/api/start?type=flv&streamPath=live/rtc` 开始录制某个流,返回一个字符串用于停止录制用的id +- `/record/api/start?type=flv&streamPath=live/rtc&fileName=xxx` 开始录制某个流,返回一个字符串用于停止录制用的id(fileName是可选的,且只用于非切片情况) - `/record/api/stop?id=xxx` 停止录制某个流 其中将type值改为mp4则录制成fmp4格式。 diff --git a/config.go b/config.go index 782f2bc..2ee2d04 100644 --- a/config.go +++ b/config.go @@ -10,7 +10,7 @@ import ( "strings" "time" - "m7s.live/engine/v4" + "m7s.live/engine/v4/util" ) type FileWr interface { @@ -35,7 +35,7 @@ type Record struct { fs http.Handler CreateFileFn func(filename string, append bool) (FileWr, error) `json:"-" yaml:"-"` GetDurationFn func(file io.ReadSeeker) uint32 `json:"-" yaml:"-"` - recording map[string]engine.ISubscriber + recording map[string]IRecorder } func (r *Record) ServeHTTP(w http.ResponseWriter, req *http.Request) { @@ -43,14 +43,11 @@ func (r *Record) ServeHTTP(w http.ResponseWriter, req *http.Request) { } func (r *Record) NeedRecord(streamPath string) bool { - if _, ok := r.recording[streamPath]; ok { - return false - } return r.AutoRecord && (r.filterReg == nil || r.filterReg.MatchString(streamPath)) } func (r *Record) Init() { - r.recording = make(map[string]engine.ISubscriber) + r.recording = make(map[string]IRecorder) os.MkdirAll(r.Path, 0766) if r.Filter != "" { r.filterReg = regexp.MustCompile(r.Filter) @@ -58,16 +55,10 @@ func (r *Record) Init() { r.fs = http.FileServer(http.Dir(r.Path)) r.CreateFileFn = func(filename string, append bool) (file FileWr, err error) { filePath := filepath.Join(r.Path, filename) - flag := os.O_CREATE - if append { - flag = flag | os.O_RDWR | os.O_APPEND - } else { - flag = flag | os.O_RDWR | os.O_TRUNC - } if err = os.MkdirAll(filepath.Dir(filePath), 0766); err != nil { return file, err } - file, err = os.OpenFile(filePath, flag, 0766) + file, err = os.OpenFile(filePath, os.O_CREATE | os.O_RDWR | util.Conditoinal(append, os.O_APPEND, os.O_TRUNC), 0766) return } } diff --git a/flv.go b/flv.go index 9707117..679c740 100644 --- a/flv.go +++ b/flv.go @@ -4,8 +4,6 @@ import ( "io" "net" "os" - "path/filepath" - "strconv" "time" "go.uber.org/zap" @@ -22,22 +20,18 @@ type FLVRecorder struct { duration int64 } +func NewFLVRecorder() (r *FLVRecorder) { + r = &FLVRecorder{} + r.Record = RecordPluginConfig.Flv + return r +} + func (r *FLVRecorder) Start(streamPath string) (err error) { - r.Record = &RecordPluginConfig.Flv r.ID = streamPath + "/flv" - if _, ok := RecordPluginConfig.recordings.Load(r.ID); ok { - return ErrRecordExist - } - return plugin.Subscribe(streamPath, r) + return r.start(r, streamPath, SUBTYPE_FLV) } -func (r *FLVRecorder) start() { - RecordPluginConfig.recordings.Store(r.ID, r) - r.PlayFLV() - RecordPluginConfig.recordings.Delete(r.ID) -} - -func (r *FLVRecorder) writeMetaData(file *os.File, duration int64) { +func (r *FLVRecorder) writeMetaData(file FileWr, duration int64) { defer file.Close() at, vt := r.Audio, r.Video hasAudio, hasVideo := at != nil, vt != nil @@ -127,46 +121,34 @@ func (r *FLVRecorder) writeMetaData(file *os.File, duration int64) { } func (r *FLVRecorder) OnEvent(event any) { + r.Recorder.OnEvent(event) switch v := event.(type) { - case ISubscriber: - filename := strconv.FormatInt(time.Now().Unix(), 10) + r.Ext - if r.Fragment == 0 { - filename = r.Stream.Path + r.Ext + case FileWr: + // 写入文件头 + if !r.append { + v.Write(codec.FLVHeader) } else { - filename = filepath.Join(r.Stream.Path, filename) - } - if file, err := r.CreateFileFn(filename, r.append); err == nil { - r.SetIO(file) - // 写入文件头 - if !r.append { - r.Write(codec.FLVHeader) + if _, err := v.Seek(-4, io.SeekEnd); err != nil { + r.Error("seek file failed", zap.Error(err)) + v.Write(codec.FLVHeader) } else { - if _, err = file.Seek(-4, io.SeekEnd); err != nil { - r.Error("seek file failed", zap.Error(err)) - r.Write(codec.FLVHeader) - } else { - tmp := make(util.Buffer, 4) - tmp2 := tmp - file.Read(tmp) - tagSize := tmp.ReadUint32() - tmp = tmp2 - file.Seek(int64(tagSize), io.SeekEnd) - file.Read(tmp2) - ts := tmp2.ReadUint24() | (uint32(tmp[3]) << 24) - r.Info("append flv", zap.String("filename", filename), zap.Uint32("last tagSize", tagSize), zap.Uint32("last ts", ts)) - if r.VideoReader != nil { - r.VideoReader.StartTs = time.Duration(ts) * time.Millisecond - } - if r.AudioReader != nil { - r.AudioReader.StartTs = time.Duration(ts) * time.Millisecond - } - file.Seek(0, io.SeekEnd) + tmp := make(util.Buffer, 4) + tmp2 := tmp + v.Read(tmp) + tagSize := tmp.ReadUint32() + tmp = tmp2 + v.Seek(int64(tagSize), io.SeekEnd) + v.Read(tmp2) + ts := tmp2.ReadUint24() | (uint32(tmp[3]) << 24) + r.Info("append flv", zap.Uint32("last tagSize", tagSize), zap.Uint32("last ts", ts)) + if r.VideoReader != nil { + r.VideoReader.StartTs = time.Duration(ts) * time.Millisecond } + if r.AudioReader != nil { + r.AudioReader.StartTs = time.Duration(ts) * time.Millisecond + } + v.Seek(0, io.SeekEnd) } - go r.start() - } else { - r.Error("create file failed", zap.Error(err)) - r.Stop() } case FLVFrame: check := false @@ -186,15 +168,15 @@ func (r *FLVRecorder) OnEvent(event any) { if r.duration = int64(absTime); r.Fragment > 0 && check && time.Duration(r.duration)*time.Millisecond >= r.Fragment { r.Close() r.Offset = 0 - 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.Write(codec.FLVHeader) + if file, err := r.createFile(); err == nil { + r.File = file + file.Write(codec.FLVHeader) var dcflv net.Buffers if r.VideoReader != nil { r.VideoReader.ResetAbsTime() dcflv = codec.VideoAVCC2FLV(0, r.VideoReader.Track.SequenceHead) flv := append(dcflv, codec.VideoAVCC2FLV(0, r.VideoReader.Value.AVCC.ToBuffers()...)...) - flv.WriteTo(r) + flv.WriteTo(file) } if r.AudioReader != nil { r.AudioReader.ResetAbsTime() @@ -202,32 +184,27 @@ func (r *FLVRecorder) OnEvent(event any) { dcflv = codec.AudioAVCC2FLV(0, r.AudioReader.Track.SequenceHead) } flv := append(dcflv, codec.AudioAVCC2FLV(0, r.AudioReader.Value.AVCC.ToBuffers()...)...) - flv.WriteTo(r) + flv.WriteTo(file) } return } } - if n, err := v.WriteTo(r); err != nil { + if n, err := v.WriteTo(r.File); err != nil { r.Error("write file failed", zap.Error(err)) - r.Stop() + r.Stop(zap.Error(err)) } else { r.Offset += n } - default: - r.Subscriber.OnEvent(event) } } -func (r *FLVRecorder) SetIO(file any) { - r.Subscriber.SetIO(file) - r.Closer = r -} - func (r *FLVRecorder) Close() error { - if file, ok := r.Writer.(*os.File); ok && !r.append { - go r.writeMetaData(file, r.duration) - } else if closer, ok := r.Writer.(io.Closer); ok { - return closer.Close() + if r.File != nil { + if !r.append { + go r.writeMetaData(r.File, r.duration) + } else { + return r.File.Close() + } } return nil } diff --git a/fmp4.go b/fmp4.go new file mode 100644 index 0000000..724c2c3 --- /dev/null +++ b/fmp4.go @@ -0,0 +1,149 @@ +package record + +import ( + "github.com/edgeware/mp4ff/aac" + "github.com/edgeware/mp4ff/mp4" + . "m7s.live/engine/v4" + "m7s.live/engine/v4/codec" +) + +type mediaContext struct { + trackId uint32 + fragment *mp4.Fragment + ts uint32 // 每个小片段起始时间戳 +} + +func (m *mediaContext) push(recoder *FMP4Recorder, dt uint32, dur uint32, data []byte, flags uint32) { + if m.fragment != nil && dt-m.ts > 1000 { + 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 FMP4Recorder struct { + Recorder + *mp4.InitSegment `json:"-" yaml:"-"` + video mediaContext + audio mediaContext + seqNumber uint32 + ftyp *mp4.FtypBox +} + +func NewFMP4Recorder() *FMP4Recorder { + r := &FMP4Recorder{} + r.Record = RecordPluginConfig.Fmp4 + return r +} + +func (r *FMP4Recorder) Start(streamPath string) (err error) { + r.ID = streamPath + "/fmp4" + return r.start(r, streamPath, SUBTYPE_RAW) +} + +func (r *FMP4Recorder) Close() error { + if r.File != nil { + if r.video.fragment != nil { + r.video.fragment.Encode(r.File) + r.video.fragment = nil + } + if r.audio.fragment != nil { + r.audio.fragment.Encode(r.File) + r.audio.fragment = nil + } + r.File.Close() + } + return nil +} + +func (r *FMP4Recorder) OnEvent(event any) { + r.Recorder.OnEvent(event) + switch v := event.(type) { + case FileWr: + r.InitSegment = mp4.CreateEmptyInit() + r.Moov.Mvhd.NextTrackID = 1 + if r.VideoReader != nil { + 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 r.Video.CodecID { + case codec.CodecID_H264: + r.ftyp = mp4.NewFtyp("isom", 0x200, []string{ + "isom", "iso2", "avc1", "mp41", + }) + newTrak.SetAVCDescriptor("avc1", r.Video.ParamaterSets[0:1], r.Video.ParamaterSets[1:2], true) + case codec.CodecID_H265: + r.ftyp = mp4.NewFtyp("isom", 0x200, []string{ + "isom", "iso2", "hvc1", "mp41", + }) + newTrak.SetHEVCDescriptor("hvc1", r.Video.ParamaterSets[0:1], r.Video.ParamaterSets[1:2], r.Video.ParamaterSets[2:3], true) + } + } + if r.AudioReader != nil { + 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 r.Audio.CodecID { + case codec.CodecID_AAC: + switch r.Audio.AudioObjectType { + case 1: + newTrak.SetAACDescriptor(aac.HEAACv1, int(r.Audio.SampleRate)) + case 2: + newTrak.SetAACDescriptor(aac.AAClc, int(r.Audio.SampleRate)) + case 3: + newTrak.SetAACDescriptor(aac.HEAACv2, int(r.Audio.SampleRate)) + } + case codec.CodecID_PCMA: + stsd := newTrak.Mdia.Minf.Stbl.Stsd + pcma := mp4.CreateAudioSampleEntryBox("pcma", + uint16(r.Audio.Channels), + uint16(r.Audio.SampleSize), uint16(r.Audio.SampleRate), nil) + stsd.AddChild(pcma) + case codec.CodecID_PCMU: + stsd := newTrak.Mdia.Minf.Stbl.Stsd + pcmu := mp4.CreateAudioSampleEntryBox("pcmu", + uint16(r.Audio.Channels), + uint16(r.Audio.SampleSize), uint16(r.Audio.SampleRate), nil) + stsd.AddChild(pcmu) + } + } + r.ftyp.Encode(r) + r.Moov.Encode(r) + r.seqNumber = 0 + case AudioFrame: + if r.audio.trackId != 0 { + r.audio.push(r, v.AbsTime, v.DeltaTime, v.AUList.ToBytes(), mp4.SyncSampleFlags) + } + case VideoFrame: + if r.video.trackId != 0 { + flag := mp4.NonSyncSampleFlags + if v.IFrame { + flag = mp4.SyncSampleFlags + } + if data := v.AVCC.ToBytes(); len(data) > 5 { + r.video.push(r, v.AbsTime, v.DeltaTime, data[5:], flag) + } + } + } +} diff --git a/go.mod b/go.mod index 3aa1643..981c19f 100644 --- a/go.mod +++ b/go.mod @@ -4,13 +4,19 @@ go 1.19 require ( github.com/edgeware/mp4ff v0.28.0 - go.uber.org/zap v1.23.0 - m7s.live/engine/v4 v4.12.6 - m7s.live/plugin/hls/v4 v4.0.0-20220619163635-447976e65ab9 + github.com/yapingcat/gomedia v0.0.0-20230727105416-c491e66c9d2a + go.uber.org/zap v1.24.0 + m7s.live/engine/v4 v4.13.8 + m7s.live/plugin/hls/v4 v4.3.2 ) require ( - github.com/aler9/gortsplib/v2 v2.2.2 // indirect + github.com/abema/go-mp4 v0.10.1 // indirect + github.com/aler9/writerseeker v0.0.0-20220601075008-6f0e685b9c82 // indirect + github.com/asticode/go-astikit v0.30.0 // indirect + github.com/asticode/go-astits v1.11.0 // indirect + github.com/bluenviron/gohlslib v0.2.5 // indirect + github.com/bluenviron/mediacommon v0.7.0 // indirect github.com/cnotch/ipchub v1.1.0 // indirect github.com/denisbrodbeck/machineid v1.0.1 // indirect github.com/go-ole/go-ole v1.2.6 // indirect @@ -23,8 +29,8 @@ require ( github.com/mcuadros/go-defaults v1.2.0 // indirect github.com/onsi/ginkgo/v2 v2.2.0 // indirect github.com/pion/randutil v0.1.0 // indirect - github.com/pion/rtp v1.7.13 // indirect - github.com/pion/webrtc/v3 v3.1.49 // indirect + github.com/pion/rtp v1.8.0 // indirect + github.com/pion/webrtc/v3 v3.1.56 // indirect github.com/power-devops/perfstat v0.0.0-20220216144756-c35f1ee13d7c // indirect github.com/q191201771/naza v0.30.8 // indirect github.com/quangngotan95/go-m3u8 v0.1.0 // indirect @@ -32,19 +38,18 @@ require ( github.com/quic-go/qtls-go1-19 v0.2.0 // indirect github.com/quic-go/qtls-go1-20 v0.1.0 // indirect github.com/quic-go/quic-go v0.32.0 // indirect - github.com/shirou/gopsutil/v3 v3.22.10 // indirect + github.com/shirou/gopsutil/v3 v3.22.11 // indirect github.com/tklauser/go-sysconf v0.3.11 // indirect github.com/tklauser/numcpus v0.6.0 // indirect - github.com/yapingcat/gomedia v0.0.0-20230426092936-387031404274 // indirect github.com/yusufpapurcu/wmi v1.2.2 // indirect go.uber.org/atomic v1.10.0 // indirect go.uber.org/multierr v1.8.0 // indirect - golang.org/x/crypto v0.4.0 // indirect + golang.org/x/crypto v0.11.0 // indirect golang.org/x/exp v0.0.0-20221205204356-47842c84f3db // indirect golang.org/x/mod v0.7.0 // indirect - golang.org/x/net v0.8.0 // indirect + golang.org/x/net v0.12.0 // indirect golang.org/x/sync v0.1.0 // indirect - golang.org/x/sys v0.6.0 // indirect + golang.org/x/sys v0.10.0 // indirect golang.org/x/tools v0.3.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 734265c..fe523ef 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,19 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/aler9/gortsplib/v2 v2.2.2 h1:tTw8pdKSOEjlZjjE1S4ftXPHJkYOqjNNv3hjQ0Nto9M= -github.com/aler9/gortsplib/v2 v2.2.2/go.mod h1:k6uBVHGwsIc/0L5SLLqWwi6bSJUb4VR0HfvncyHlKQI= +github.com/abema/go-mp4 v0.10.1 h1:wOhZgNxjduc8r4FJdwPa5x/gdBSSX+8MTnfNj/xkJaE= +github.com/abema/go-mp4 v0.10.1/go.mod h1:vPl9t5ZK7K0x68jh12/+ECWBCXoWuIDtNgPtU2f04ws= +github.com/aler9/writerseeker v0.0.0-20220601075008-6f0e685b9c82 h1:9WgSzBLo3a9ToSVV7sRTBYZ1GGOZUpq4+5H3SN0UZq4= +github.com/aler9/writerseeker v0.0.0-20220601075008-6f0e685b9c82/go.mod h1:qsMrZCbeBf/mCLOeF16KDkPu4gktn/pOWyaq1aYQE7U= +github.com/asticode/go-astikit v0.30.0 h1:DkBkRQRIxYcknlaU7W7ksNfn4gMFsB0tqMJflxkRsZA= +github.com/asticode/go-astikit v0.30.0/go.mod h1:h4ly7idim1tNhaVkdVBeXQZEE3L0xblP7fCWbgwipF0= +github.com/asticode/go-astits v1.11.0 h1:GTHUXht0ZXAJXsVbsLIcyfHr1Bchi4QQwMARw2ZWAng= +github.com/asticode/go-astits v1.11.0/go.mod h1:QSHmknZ51pf6KJdHKZHJTLlMegIrhega3LPWz3ND/iI= github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= +github.com/bluenviron/gohlslib v0.2.5 h1:M+uWOLjOa7hirUITLndf9bYarA8F7HR1m7+5dG/3WVI= +github.com/bluenviron/gohlslib v0.2.5/go.mod h1:IvhV5h+92FljHy/xHlcOZm09rDeqg1RR88MjCTBx67Y= +github.com/bluenviron/mediacommon v0.7.0 h1:dJWLLL9oDbAqfK8KuNfnDUQwNbeMAtGeRjZc9Vo95js= +github.com/bluenviron/mediacommon v0.7.0/go.mod h1:wuLJdxcITiSPgY1MvQqrX+qPlKmNfeV9wNvXth5M98I= +github.com/bytedance/sonic v1.8.0 h1:ea0Xadu+sHlu7x5O3gKhRpQ1IKiMrSiHttPF0ybECuA= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= @@ -13,6 +25,7 @@ github.com/cnotch/queue v0.0.0-20200326024423-6e88bdbf2ad4/go.mod h1:zOssjAlNusO github.com/cnotch/queue v0.0.0-20201224060551-4191569ce8f6/go.mod h1:zOssjAlNusOxvtaqT+EMA+Iyi8rrtKr4/XfzN1Fgoeg= github.com/cnotch/scheduler v0.0.0-20200522024700-1d2da93eefc5/go.mod h1:F4GE3SZkJZ8an1Y0ZCqvSM3jeozNuKzoC67erG1PhIo= github.com/cnotch/xlog v0.0.0-20201208005456-cfda439cd3a0/go.mod h1:RW9oHsR79ffl3sR3yMGgxYupMn2btzdtJUwoxFPUE5E= +github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -23,12 +36,18 @@ github.com/edgeware/mp4ff v0.28.0/go.mod h1:GNUeA6tEFksH2CrjJF2FSGdJolba8yPGmo16 github.com/emitter-io/address v1.0.0/go.mod h1:GfZb5+S/o8694B1GMGK2imUYQyn2skszMvGNA5D84Ug= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-gonic/gin v1.9.0 h1:OjyFBKICoexlu99ctXNR2gg+c5pKrKMuyjgARg9qeY8= github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/validator/v10 v10.11.2 h1:q3SHpufmypg+erIExEKUmsgmhDTyhcJ38oeKGACXohU= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= +github.com/goccy/go-json v0.10.0 h1:mXKd9Qw4NuzShiRlOXKews24ufknHO7gx30lsDyokKA= github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -50,25 +69,33 @@ github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE= github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/kelindar/process v0.0.0-20170730150328-69a29e249ec3/go.mod h1:+lTCLnZFXOkqwD8sLPl6u4erAc0cP8wFegQHfipz7KE= github.com/kelindar/rate v1.0.0/go.mod h1:AjT4G+hTItNwt30lucEGZIz8y7Uk5zPho6vurIZ+1Es= github.com/kelindar/tcp v1.0.0/go.mod h1:JB5hj1cshLU60XrLij2BBxW3JQ4hOye8vqbyvuKb52k= +github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8= github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/lufia/plan9stats v0.0.0-20220913051719-115f729f3c8c h1:VtwQ41oftZwlMnOEbMWQtSEUgU64U4s+GHk7hZK+jtY= github.com/lufia/plan9stats v0.0.0-20220913051719-115f729f3c8c/go.mod h1:JKx41uQRwqlTZabZc+kILPrO/3jlKnQ2Z8b7YiVw5cE= +github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= github.com/mcuadros/go-defaults v1.2.0 h1:FODb8WSf0uGaY8elWJAkoLL0Ri6AlZ1bFlenk56oZtc= github.com/mcuadros/go-defaults v1.2.0/go.mod h1:WEZtHEVIGYVDqkKSWBdWKUVdRyKlMfulPaGDWIVeCWY= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= @@ -85,34 +112,37 @@ github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7J github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= github.com/onsi/gomega v1.20.1 h1:PA/3qinGoukvymdIDV8pii6tiZgC8kbmJO6Z5+b002Q= -github.com/pion/datachannel v1.5.2/go.mod h1:FTGQWaHrdCwIJ1rw6xBIfZVkslikjShim5yr05XFuCQ= -github.com/pion/dtls/v2 v2.1.5/go.mod h1:BqCE7xPZbPSubGasRoDFJeTsyJtdD1FanJYL0JGheqY= -github.com/pion/ice/v2 v2.2.12/go.mod h1:z2KXVFyRkmjetRlaVRgjO9U3ShKwzhlUylvxKfHfd5A= -github.com/pion/interceptor v0.1.11/go.mod h1:tbtKjZY14awXd7Bq0mmWvgtHB5MDaRN7HV3OZ/uy7s8= +github.com/orcaman/writerseeker v0.0.0-20200621085525-1d3f536ff85e h1:s2RNOM/IGdY0Y6qfTeUKhDawdHDpK9RGBdx80qN4Ttw= +github.com/orcaman/writerseeker v0.0.0-20200621085525-1d3f536ff85e/go.mod h1:nBdnFKj15wFbf94Rwfq4m30eAcyY9V/IyKAGQFtqkW0= +github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU= +github.com/pion/datachannel v1.5.5/go.mod h1:iMz+lECmfdCMqFRhXhcA/219B0SQlbpoR2V118yimL0= +github.com/pion/dtls/v2 v2.2.6/go.mod h1:t8fWJCIquY5rlQZwA2yWxUS1+OCrAdXrhVKXB5oD/wY= +github.com/pion/ice/v2 v2.3.1/go.mod h1:aq2kc6MtYNcn4XmMhobAv6hTNJiHzvD0yXRz80+bnP8= +github.com/pion/interceptor v0.1.12/go.mod h1:bDtgAD9dRkBZpWHGKaoKb42FhDHTG2rX8Ii9LRALLVA= github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= -github.com/pion/mdns v0.0.5/go.mod h1:UgssrvdD3mxpi8tMxAXbsppL3vJ4Jipw1mTCW+al01g= +github.com/pion/mdns v0.0.7/go.mod h1:4iP2UbeFhLI/vWju/bw6ZfwjJzk0z8DNValjGxR/dD8= github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA= github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= -github.com/pion/rtcp v1.2.9/go.mod h1:qVPhiCzAm4D/rxb6XzKeyZiQK69yJpbUDJSF7TgrqNo= github.com/pion/rtcp v1.2.10/go.mod h1:ztfEwXZNLGyF1oQDttz/ZKIBaeeg/oWbRYqzBM9TL1I= github.com/pion/rtp v1.6.2/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko= -github.com/pion/rtp v1.7.13 h1:qcHwlmtiI50t1XivvoawdCGTP4Uiypzfrsap+bijcoA= github.com/pion/rtp v1.7.13/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko= -github.com/pion/sctp v1.8.0/go.mod h1:xFe9cLMZ5Vj6eOzpyiKjT9SwGM4KpK/8Jbw5//jc+0s= -github.com/pion/sctp v1.8.3/go.mod h1:OHbDjdk7kg+L+7TJim9q/qGVefdEJohuA2SZyihccgI= +github.com/pion/rtp v1.8.0 h1:SYD7040IR+NqrGBOc2GDU5iDjAR+0m5rnX/EWCUMNhw= +github.com/pion/rtp v1.8.0/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU= +github.com/pion/sctp v1.8.5/go.mod h1:SUFFfDpViyKejTAdwD1d/HQsCu+V/40cCs2nZIvC3s0= +github.com/pion/sctp v1.8.6/go.mod h1:SUFFfDpViyKejTAdwD1d/HQsCu+V/40cCs2nZIvC3s0= github.com/pion/sdp/v3 v3.0.6/go.mod h1:iiFWFpQO8Fy3S5ldclBkpXqmWy02ns78NOKoLLL0YQw= -github.com/pion/srtp/v2 v2.0.10/go.mod h1:XEeSWaK9PfuMs7zxXyiN252AHPbH12NX5q/CFDWtUuA= -github.com/pion/stun v0.3.5/go.mod h1:gDMim+47EeEtfWogA37n6qXZS88L5V6LqFcf+DZA2UA= -github.com/pion/transport v0.12.2/go.mod h1:N3+vZQD9HlDP5GWkZ85LohxNsDcNgofQmyL6ojX5d8Q= -github.com/pion/transport v0.12.3/go.mod h1:OViWW9SP2peE/HbwBvARicmAVnesphkNkCVZIWJ6q9A= -github.com/pion/transport v0.13.0/go.mod h1:yxm9uXpK9bpBBWkITk13cLo1y5/ur5VQpG22ny6EP7g= -github.com/pion/transport v0.13.1/go.mod h1:EBxbqzyv+ZrmDb82XswEE0BjfQFtuw1Nu6sjnjWCsGg= -github.com/pion/turn/v2 v2.0.8/go.mod h1:+y7xl719J8bAEVpSXBXvTxStjJv3hbz9YFflvkpcGPw= -github.com/pion/udp v0.1.1/go.mod h1:6AFo+CMdKQm7UiA0eUPA8/eVCTx8jBIITLZHc9DWX5M= -github.com/pion/webrtc/v3 v3.1.49 h1:rbsNGxK9jMYts+xE6zYAJMUQHnGwmk/JYze8yttW+to= -github.com/pion/webrtc/v3 v3.1.49/go.mod h1:kHf/o47QW4No1rgpsFux/h7lUhtUnwFnSFDZOXeLapw= +github.com/pion/srtp/v2 v2.0.12/go.mod h1:C3Ep44hlOo2qEYaq4ddsmK5dL63eLehXFbHaZ9F5V9Y= +github.com/pion/stun v0.4.0/go.mod h1:QPsh1/SbXASntw3zkkrIk3ZJVKz4saBY2G7S10P3wCw= +github.com/pion/transport v0.14.1/go.mod h1:4tGmbk00NeYA3rUa9+n+dzCCoKkcy3YlYb99Jn2fNnI= +github.com/pion/transport/v2 v2.0.0/go.mod h1:HS2MEBJTwD+1ZI2eSXSvHJx/HnzQqRy2/LXxt6eVMHc= +github.com/pion/transport/v2 v2.0.2/go.mod h1:vrz6bUbFr/cjdwbnxq8OdDDzHf7JJfGsIRkxfpZoTA0= +github.com/pion/turn/v2 v2.1.0/go.mod h1:yrT5XbXSGX1VFSF31A3c1kCNB5bBZgk/uu5LET162qs= +github.com/pion/udp/v2 v2.0.1/go.mod h1:B7uvTMP00lzWdyMr/1PVZXtV3wpPIxBRd4Wl6AksXn8= +github.com/pion/webrtc/v3 v3.1.56 h1:ScaiqKQN3liQwT+kJwOBaYP6TwSfixzdUnZmzHAo0a0= +github.com/pion/webrtc/v3 v3.1.56/go.mod h1:7VhbA6ihqJlz6R/INHjyh1b8HpiV9Ct4UQvE1OB/xoM= github.com/pixelbender/go-sdp v1.1.0/go.mod h1:6IBlz9+BrUHoFTea7gcp4S54khtOhjCW/nVDLhmZBAs= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/profile v1.4.0/go.mod h1:NWz/XGvpEW1FyYQ7fCx4dqYBLlfTcE+A9FLAkNKqjFE= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= @@ -131,28 +161,30 @@ github.com/quic-go/qtls-go1-20 v0.1.0/go.mod h1:JKtK6mjbAVcUTN/9jZpvLbGxvdWIKS8u github.com/quic-go/quic-go v0.32.0 h1:lY02md31s1JgPiiyfqJijpu/UX/Iun304FI3yUqX7tA= github.com/quic-go/quic-go v0.32.0/go.mod h1:/fCsKANhQIeD5l76c2JFU+07gVE3KaA0FP+0zMWwfwo= github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw= -github.com/shirou/gopsutil/v3 v3.22.10 h1:4KMHdfBRYXGF9skjDWiL4RA2N+E8dRdodU/bOZpPoVg= -github.com/shirou/gopsutil/v3 v3.22.10/go.mod h1:QNza6r4YQoydyCfo6rH0blGfKahgibh4dQmV5xdFkQk= +github.com/shirou/gopsutil/v3 v3.22.11 h1:kxsPKS+Eeo+VnEQ2XCaGJepeP6KY53QoRTETx3+1ndM= +github.com/shirou/gopsutil/v3 v3.22.11/go.mod h1:xl0EeL4vXJ+hQMAGN8B9VFpxukEMA0XdevQOe5MZ1oY= github.com/sqs/goreturns v0.0.0-20181028201513-538ac6014518/go.mod h1:CKI4AZ4XmGV240rTHfO0hfE83S6/a3/Q1siZJ/vXf7A= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= -github.com/tklauser/go-sysconf v0.3.10/go.mod h1:C8XykCvCb+Gn0oNCWPIlcb0RuglQTYaQ2hGm7jmxEFk= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/sunfish-shogi/bufseekio v0.0.0-20210207115823-a4185644b365/go.mod h1:dEzdXgvImkQ3WLI+0KQpmEx8T/C/ma9KeS3AfmU899I= github.com/tklauser/go-sysconf v0.3.11 h1:89WgdJhk5SNwJfu+GKyYveZ4IaJ7xAkecBo+KdJV0CM= github.com/tklauser/go-sysconf v0.3.11/go.mod h1:GqXfhXY3kiPa0nAXPDIQIWzJbMCB7AmcWpGR8lSZfqI= -github.com/tklauser/numcpus v0.4.0/go.mod h1:1+UI3pD8NW14VMwdgJNJ1ESk2UnwhAnz5hMwiKKqXCQ= github.com/tklauser/numcpus v0.6.0 h1:kebhY2Qt+3U6RNK7UqpYNA+tJ23IBEGKkB7JQBfDYms= github.com/tklauser/numcpus v0.6.0/go.mod h1:FEZLMke0lhOUG6w2JadTzp0a+Nl8PF/GFkQ5UVIcaL4= -github.com/yapingcat/gomedia v0.0.0-20230426092936-387031404274 h1:cj4I+bvWX9I+Hg6tnZ7DAiOVxzhyLhdvYVKp+WpM/2c= -github.com/yapingcat/gomedia v0.0.0-20230426092936-387031404274/go.mod h1:WSZ59bidJOO40JSJmLqlkBJrjZCtjbKKkygEMfzY/kc= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/ugorji/go/codec v1.2.9 h1:rmenucSohSTiyL09Y+l2OCk+FrMxGMzho2+tjr5ticU= +github.com/yapingcat/gomedia v0.0.0-20230727105416-c491e66c9d2a h1:x60q0A7QmoUTzixNz7zVTdEA9JC0oYqm8S51PdbTWgs= +github.com/yapingcat/gomedia v0.0.0-20230727105416-c491e66c9d2a/go.mod h1:WSZ59bidJOO40JSJmLqlkBJrjZCtjbKKkygEMfzY/kc= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= @@ -164,17 +196,18 @@ go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0 go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= go.uber.org/multierr v1.8.0 h1:dg6GjLku4EH+249NNmoIciG9N/jURbDG+pFlTkhzIC8= go.uber.org/multierr v1.8.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= -go.uber.org/zap v1.23.0 h1:OjGQ5KQDEUawVHxNwQgPpiypGHOxo2mNZsOqTak4fFY= -go.uber.org/zap v1.23.0/go.mod h1:D+nX8jyLsMHMYrln8A0rJjFt/T/9/bGgIhAqxv5URuY= +go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= +go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.0.0-20221010152910-d6f0a8c073c2/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.4.0 h1:UVQgzMY87xqpKNgb+kDsll2Igd33HszWHFLmpaRMq/8= -golang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80= +golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU= +golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= +golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA= +golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= golang.org/x/exp v0.0.0-20221205204356-47842c84f3db h1:D/cFflL63o2KSLJIwjlcIt8PR064j/xsmdEJL/YvY/o= golang.org/x/exp v0.0.0-20221205204356-47842c84f3db/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -187,19 +220,16 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201201195509-5d6afe98e0b7/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= -golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211201190559-0a0e4e1bb54c/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220531201128-c960675eff93/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= -golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= -golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50= +golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -210,6 +240,7 @@ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -225,26 +256,28 @@ golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220608164250-635b8c9b7f68/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= +golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= +golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= @@ -264,24 +297,28 @@ google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzi google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= +google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= +gopkg.in/src-d/go-billy.v4 v4.3.2 h1:0SQA1pRztfTFx2miS8sA97XvooFeNOmvUenF4o0EcVg= +gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -m7s.live/engine/v4 v4.12.6 h1:JWUwxOVHzL9wh8JbWUtZaDsRBXzyAzPI+4UEMGftsU0= -m7s.live/engine/v4 v4.12.6/go.mod h1:LoALBfV5rmsz5TJQr6cmLxM33mfUE5BKBq/sMtXOVlc= -m7s.live/plugin/hls/v4 v4.0.0-20220619163635-447976e65ab9 h1:EcB8awppfwza+s4ECjUr3xLTtl9BgJcZ12EgfE/L2YA= -m7s.live/plugin/hls/v4 v4.0.0-20220619163635-447976e65ab9/go.mod h1:Qn4dDz5xlyBJwO+eZ3w8CUQ8Hl6KN1nmv0a3IsOjJvw= +m7s.live/engine/v4 v4.13.8 h1:pDl8YWxip5aTidw2Q4NuU+8A6irBraLRfoeBi42S6iQ= +m7s.live/engine/v4 v4.13.8/go.mod h1:k/6iFSuJxmhJL8VO45NAga8BbgZHLLfRXOwCcCzk2s8= +m7s.live/plugin/hls/v4 v4.3.1 h1:FXcQShWnNTE0Guxg6YMzb5pbvMpn6MKiwcZ05N6nzhs= +m7s.live/plugin/hls/v4 v4.3.1/go.mod h1:HSV/yO9NMvmNkompn+faM/0en0t3q0axp4zEA2mdyWw= diff --git a/hls.go b/hls.go index 43496e4..d3862ba 100644 --- a/hls.go +++ b/hls.go @@ -22,27 +22,31 @@ type HLSRecorder struct { MemoryTs } +func NewHLSRecorder() (r *HLSRecorder) { + r = &HLSRecorder{} + r.Record = RecordPluginConfig.Hls + return r +} + func (h *HLSRecorder) Start(streamPath string) error { - h.Record = &RecordPluginConfig.Hls h.ID = streamPath + "/hls" - if _, ok := RecordPluginConfig.recordings.Load(h.ID); ok { - return ErrRecordExist - } - h.BytesPool = make(util.BytesPool, 17) - return plugin.Subscribe(streamPath, h) + return h.start(h, streamPath, SUBTYPE_RAW) } func (h *HLSRecorder) OnEvent(event any) { var err error defer func() { if err != nil { - h.Error("HLSRecorder Stop", zap.Error(err)) - h.Stop() + h.Stop(zap.Error(err)) } }() - h.Recorder.OnEvent(event) switch v := event.(type) { case *HLSRecorder: + h.BytesPool = make(util.BytesPool, 17) + if h.Writer, err = h.createFile(); err != nil { + return + } + h.SetIO(h.Writer) h.playlist = hls.Playlist{ Writer: h.Writer, Version: 3, @@ -52,12 +56,11 @@ func (h *HLSRecorder) OnEvent(event any) { if err = h.playlist.Init(); err != nil { return } - if err = h.createHlsTsSegmentFile(); err != nil { - h.Stop() + if h.File, err = h.CreateFile(); err != nil { return } - go h.start() case AudioFrame: + h.Recorder.OnEvent(event) pes := &mpegts.MpegtsPESFrame{ Pid: mpegts.PID_AUDIO, IsKeyFrame: false, @@ -65,18 +68,12 @@ func (h *HLSRecorder) OnEvent(event any) { ProgramClockReferenceBase: uint64(v.DTS), } h.WriteAudioFrame(v, pes) - h.BLL.WriteTo(h) + h.BLL.WriteTo(h.File) h.Recycle() h.Clear() h.audio_cc = pes.ContinuityCounter case VideoFrame: - if h.Fragment != 0 && h.newFile { - h.newFile = false - h.Close() - if err = h.createHlsTsSegmentFile(); err != nil { - return - } - } + h.Recorder.OnEvent(event) pes := &mpegts.MpegtsPESFrame{ Pid: mpegts.PID_VIDEO, IsKeyFrame: v.IFrame, @@ -86,21 +83,25 @@ func (h *HLSRecorder) OnEvent(event any) { if err = h.WriteVideoFrame(v, pes); err != nil { return } - h.BLL.WriteTo(h) + h.BLL.WriteTo(h.File) h.Recycle() h.Clear() h.video_cc = pes.ContinuityCounter + default: + h.Recorder.OnEvent(v) } } // 创建一个新的ts文件 -func (h *HLSRecorder) createHlsTsSegmentFile() (err error) { +func (h *HLSRecorder) CreateFile() (fw FileWr, err error) { tsFilename := strconv.FormatInt(time.Now().Unix(), 10) + ".ts" - fw, err := h.CreateFileFn(filepath.Join(h.Stream.Path, tsFilename), false) + filePath := filepath.Join(h.Stream.Path, tsFilename) + fw, err = h.CreateFileFn(filePath, false) if err != nil { - return err + h.Error("create file", zap.String("path", filePath), zap.Error(err)) + return } - h.SetIO(fw) + h.Info("create file", zap.String("path", filePath)) inf := hls.PlaylistInf{ Duration: h.Fragment.Seconds(), Title: tsFilename, @@ -109,7 +110,7 @@ func (h *HLSRecorder) createHlsTsSegmentFile() (err error) { return } if err = mpegts.WriteDefaultPATPacket(fw); err != nil { - return err + return } var vcodec codec.VideoCodecID = 0 var acodec codec.AudioCodecID = 0 @@ -120,5 +121,5 @@ func (h *HLSRecorder) createHlsTsSegmentFile() (err error) { acodec = h.Audio.CodecID } mpegts.WritePMTPacket(fw, vcodec, acodec) - return err + return } diff --git a/main.go b/main.go index 33eb63e..a8fb55c 100644 --- a/main.go +++ b/main.go @@ -17,6 +17,7 @@ type RecordConfig struct { config.Subscribe Flv Record Mp4 Record + Fmp4 Record Hls Record Raw Record RawAudio Record @@ -33,6 +34,10 @@ var RecordPluginConfig = &RecordConfig{ Ext: ".flv", GetDurationFn: getFLVDuration, }, + Fmp4: Record{ + Path: "record/fmp4", + Ext: ".mp4", + }, Mp4: Record{ Path: "record/mp4", Ext: ".mp4", @@ -58,43 +63,29 @@ func (conf *RecordConfig) OnEvent(event any) { case FirstConfig, config.Config: conf.Flv.Init() conf.Mp4.Init() + conf.Fmp4.Init() conf.Hls.Init() conf.Raw.Init() conf.RawAudio.Init() - case SEclose: - streamPath := v.Target.Path - delete(conf.Flv.recording, streamPath) - delete(conf.Mp4.recording, streamPath) - delete(conf.Hls.recording, streamPath) - delete(conf.Raw.recording, streamPath) - delete(conf.RawAudio.recording, streamPath) case SEpublish: streamPath := v.Target.Path if conf.Flv.NeedRecord(streamPath) { - var flv FLVRecorder - conf.Flv.recording[streamPath] = &flv - go flv.Start(streamPath) + go NewFLVRecorder().Start(streamPath) } if conf.Mp4.NeedRecord(streamPath) { - recoder := NewMP4Recorder() - conf.Mp4.recording[streamPath] = recoder - go recoder.Start(streamPath) + go NewMP4Recorder().Start(streamPath) + } + if conf.Fmp4.NeedRecord(streamPath) { + go NewFMP4Recorder().Start(streamPath) } if conf.Hls.NeedRecord(streamPath) { - var hls HLSRecorder - conf.Hls.recording[streamPath] = &hls - go hls.Start(streamPath) + go NewHLSRecorder().Start(streamPath) } if conf.Raw.NeedRecord(streamPath) { - var raw RawRecorder - conf.Raw.recording[streamPath] = &raw - go raw.Start(streamPath) + go NewRawRecorder().Start(streamPath) } if conf.RawAudio.NeedRecord(streamPath) { - var raw RawRecorder - raw.IsAudio = true - conf.RawAudio.recording[streamPath] = &raw - go raw.Start(streamPath) + go NewRawAudioRecorder().Start(streamPath) } } } @@ -104,6 +95,8 @@ func (conf *RecordConfig) getRecorderConfigByType(t string) (recorder *Record) { recorder = &conf.Flv case "mp4": recorder = &conf.Mp4 + case "fmp4": + recorder = &conf.Fmp4 case "hls": recorder = &conf.Hls case "raw": diff --git a/mp4.go b/mp4.go index 17aef30..8baac94 100644 --- a/mp4.go +++ b/mp4.go @@ -1,178 +1,82 @@ package record import ( - "path/filepath" - "strconv" - "time" - - "github.com/edgeware/mp4ff/aac" - "github.com/edgeware/mp4ff/mp4" + "github.com/yapingcat/gomedia/go-mp4" + "go.uber.org/zap" . "m7s.live/engine/v4" "m7s.live/engine/v4/codec" - "m7s.live/engine/v4/track" + "m7s.live/engine/v4/util" ) -type mediaContext struct { - trackId uint32 - fragment *mp4.Fragment - ts uint32 // 每个小片段起始时间戳 -} - -func (m *mediaContext) push(recoder *MP4Recorder, dt uint32, dur uint32, data []byte, flags uint32) { - if m.fragment != nil && dt-m.ts > 1000 { - 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:"-" yaml:"-"` - video mediaContext - audio mediaContext - seqNumber uint32 - ftyp *mp4.FtypBox + *mp4.Movmuxer `json:"-" yaml:"-"` + videoId uint32 + audioId uint32 } func NewMP4Recorder() *MP4Recorder { - r := &MP4Recorder{ - InitSegment: mp4.CreateEmptyInit(), - } - r.Moov.Mvhd.NextTrackID = 1 + r := &MP4Recorder{} + r.Record = RecordPluginConfig.Mp4 return r } func (r *MP4Recorder) Start(streamPath string) (err error) { - r.Record = &RecordPluginConfig.Mp4 r.ID = streamPath + "/mp4" - if _, ok := RecordPluginConfig.recordings.Load(r.ID); ok { - return ErrRecordExist - } - return plugin.Subscribe(streamPath, r) + return r.start(r, streamPath, SUBTYPE_RAW) } -func (r *MP4Recorder) Close() error { - if r.Writer != nil { - if r.video.fragment != nil { - r.video.fragment.Encode(r.Writer) - r.video.fragment = nil +func (r *MP4Recorder) Close() (err error) { + if r.File != nil { + err = r.Movmuxer.WriteTrailer() + if err != nil { + r.Error("mp4 write trailer", zap.Error(err)) + } else { + // _, err = r.file.Write(r.cache.buf) + r.Info("mp4 write trailer", zap.Error(err)) } - if r.audio.fragment != nil { - r.audio.fragment.Encode(r.Writer) - r.audio.fragment = nil - } - r.Closer.Close() + err = r.File.Close() } - return nil + return } - -func (r *MP4Recorder) OnEvent(event any) { - r.Recorder.OnEvent(event) - if 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.InitSegment = mp4.CreateEmptyInit() - r.Moov.Mvhd.NextTrackID = 1 - if r.VideoReader != nil { - r.OnEvent(r.Video) - } - if r.AudioReader != nil { - r.OnEvent(r.Audio) - } - r.ftyp.Encode(r) - r.Moov.Encode(r) - r.seqNumber = 0 - } - } - 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: - r.ftyp = mp4.NewFtyp("isom", 0x200, []string{ - "isom", "iso2", "avc1", "mp41", - }) - newTrak.SetAVCDescriptor("avc1", v.ParamaterSets[0:1], v.ParamaterSets[1:2], true) - case codec.CodecID_H265: - r.ftyp = mp4.NewFtyp("isom", 0x200, []string{ - "isom", "iso2", "hvc1", "mp41", - }) - newTrak.SetHEVCDescriptor("hvc1", v.ParamaterSets[0:1], v.ParamaterSets[1:2], v.ParamaterSets[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 { +func (r *MP4Recorder) setTracks() { + if r.Audio != nil { + switch r.Audio.CodecID { case codec.CodecID_AAC: - switch v.AudioObjectType { - case 1: - newTrak.SetAACDescriptor(aac.HEAACv1, int(v.SampleRate)) - case 2: - newTrak.SetAACDescriptor(aac.AAClc, int(v.SampleRate)) - case 3: - newTrak.SetAACDescriptor(aac.HEAACv2, int(v.SampleRate)) - } + r.audioId = r.AddAudioTrack(mp4.MP4_CODEC_AAC) 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) + r.audioId = r.AddAudioTrack(mp4.MP4_CODEC_G711A) 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.audioId = r.AddAudioTrack(mp4.MP4_CODEC_G711U) } - r.AddTrack(v) - case ISubscriber: - if r.ftyp != nil && r.Writer != nil { - r.ftyp.Encode(r) - r.Moov.Encode(r) - go r.start() + } + if r.Video != nil { + switch r.Video.CodecID { + case codec.CodecID_H264: + r.videoId = r.AddVideoTrack(mp4.MP4_CODEC_H264) + case codec.CodecID_H265: + r.videoId = r.AddVideoTrack(mp4.MP4_CODEC_H265) + } + } +} +func (r *MP4Recorder) OnEvent(event any) { + var err error + r.Recorder.OnEvent(event) + switch v := event.(type) { + case FileWr: + r.Movmuxer, err = mp4.CreateMp4Muxer(v) + if err != nil { + r.Error("mp4 create muxer", zap.Error(err)) + } else { + r.setTracks() } case AudioFrame: - if r.audio.trackId != 0 { - r.audio.push(r, v.AbsTime, v.DeltaTime, v.AUList.ToBytes(), mp4.SyncSampleFlags) + if r.audioId != 0 { + r.Write(r.audioId, util.ConcatBuffers(v.GetADTS()), uint64(v.AbsTime+(v.PTS-v.DTS)/90), uint64(v.AbsTime)) } case VideoFrame: - if r.video.trackId != 0 { - flag := mp4.NonSyncSampleFlags - if v.IFrame { - flag = mp4.SyncSampleFlags - } - if data := v.AVCC.ToBytes(); len(data) > 5 { - r.video.push(r, v.AbsTime, v.DeltaTime, data[5:], flag) - } + if r.videoId != 0 { + r.Write(r.videoId, util.ConcatBuffers(v.GetAnnexB()), uint64(v.AbsTime+(v.PTS-v.DTS)/90), uint64(v.AbsTime)) } } } diff --git a/raw.go b/raw.go index 2c1f5cd..e97d1d2 100644 --- a/raw.go +++ b/raw.go @@ -1,11 +1,6 @@ package record import ( - "path/filepath" - "strconv" - "time" - - "go.uber.org/zap" . "m7s.live/engine/v4" "m7s.live/engine/v4/codec" "m7s.live/engine/v4/track" @@ -16,38 +11,32 @@ type RawRecorder struct { IsAudio bool } +func NewRawRecorder() (r *RawRecorder) { + r = &RawRecorder{} + r.Record = RecordPluginConfig.Raw + return r +} + +func NewRawAudioRecorder() (r *RawRecorder) { + r = &RawRecorder{IsAudio: true} + r.Record = RecordPluginConfig.RawAudio + return r +} + func (r *RawRecorder) Start(streamPath string) error { - if r.IsAudio { - r.Record = &RecordPluginConfig.RawAudio - } else { - r.Record = &RecordPluginConfig.Raw - } r.ID = streamPath + "/raw" if r.IsAudio { r.ID += "_audio" } - if _, ok := RecordPluginConfig.recordings.Load(r.ID); ok { - return ErrRecordExist - } - return plugin.Subscribe(streamPath, r) + return r.start(r, streamPath, SUBTYPE_RAW) } func (r *RawRecorder) OnEvent(event any) { switch v := event.(type) { + case FileWr: + r.SetIO(v) case *RawRecorder: - filename := strconv.FormatInt(time.Now().Unix(), 10) + r.Ext - if r.Fragment == 0 { - filename = r.Stream.Path + r.Ext - } else { - filename = filepath.Join(r.Stream.Path, filename) - } - if file, err := r.CreateFileFn(filename, r.append); err == nil { - r.SetIO(file) - } else { - r.Error("create file failed", zap.Error(err)) - r.Stop() - } - go r.start() + r.Recorder.OnEvent(event) case *track.Video: if r.IsAudio { break @@ -76,26 +65,10 @@ func (r *RawRecorder) OnEvent(event any) { } r.AddTrack(v) case AudioFrame: - if r.Fragment > 0 { - if r.cut(v.AbsTime); 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.Recorder.OnEvent(event) v.WriteRawTo(r) case VideoFrame: - if r.Fragment > 0 && v.IFrame { - if r.cut(v.AbsTime); 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.Recorder.OnEvent(event) v.WriteAnnexBTo(r) default: r.IO.OnEvent(v) diff --git a/restful.go b/restful.go index f664ecd..7ccdfb9 100644 --- a/restful.go +++ b/restful.go @@ -2,6 +2,7 @@ package record import ( "encoding/json" + "fmt" "net/http" "time" @@ -16,7 +17,7 @@ func (conf *RecordConfig) API_list(w http.ResponseWriter, r *http.Request) { var err error recorder := conf.getRecorderConfigByType(t) if recorder == nil { - for _, t = range []string{"flv", "mp4", "hls", "raw", "raw_audio"} { + for _, t = range []string{"flv", "mp4", "fmp4", "hls", "raw", "raw_audio"} { recorder = conf.getRecorderConfigByType(t) var fs []*VideoFileInfo if fs, err = recorder.Tree(recorder.Path, 0); err == nil { @@ -41,6 +42,8 @@ func (conf *RecordConfig) API_list(w http.ResponseWriter, r *http.Request) { func (conf *RecordConfig) API_start(w http.ResponseWriter, r *http.Request) { query := r.URL.Query() streamPath := query.Get("streamPath") + fileName := query.Get("fileName") + fragment := query.Get("fragment") if streamPath == "" { http.Error(w, "no streamPath", http.StatusBadRequest) return @@ -48,43 +51,40 @@ func (conf *RecordConfig) API_start(w http.ResponseWriter, r *http.Request) { t := query.Get("type") var id string var err error + var irecorder IRecorder switch t { case "": t = "flv" fallthrough case "flv": - var flvRecoder FLVRecorder - flvRecoder.append = query.Get("append") != "" - err = flvRecoder.Start(streamPath) - id = flvRecoder.ID + irecorder = NewFLVRecorder() case "mp4": - recorder := NewMP4Recorder() - err = recorder.Start(streamPath) - id = recorder.ID + irecorder = NewMP4Recorder() + case "fmp4": + irecorder = NewFMP4Recorder() case "hls": - var recorder HLSRecorder - err = recorder.Start(streamPath) - id = recorder.ID + irecorder = NewHLSRecorder() case "raw": - var recorder RawRecorder - recorder.append = query.Get("append") != "" - err = recorder.Start(streamPath) - id = recorder.ID + irecorder = NewRawRecorder() case "raw_audio": - var recorder RawRecorder - recorder.IsAudio = true - recorder.append = query.Get("append") != "" - err = recorder.Start(streamPath) - id = recorder.ID + irecorder = NewRawAudioRecorder() default: http.Error(w, "type not supported", http.StatusBadRequest) return } + recorder := irecorder.GetRecorder() + if fragment != "" { + recorder.Fragment, err = time.ParseDuration(fragment) + } + recorder.FileName = fileName + recorder.append = query.Get("append") != "" + err = irecorder.Start(streamPath) + id = recorder.ID if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } - w.Write([]byte(id)) + fmt.Fprintf(w, id) } func (conf *RecordConfig) API_list_recording(w http.ResponseWriter, r *http.Request) { diff --git a/subscriber.go b/subscriber.go index 7f4fed2..2ea292d 100644 --- a/subscriber.go +++ b/subscriber.go @@ -1,6 +1,7 @@ package record import ( + "io" "path/filepath" "strconv" "time" @@ -9,41 +10,100 @@ import ( . "m7s.live/engine/v4" ) -type Recorder struct { - Subscriber - SkipTS uint32 - *Record `json:"-" yaml:"-"` - newFile bool // 创建了新的文件 - append bool // 是否追加模式 +type IRecorder interface { + ISubscriber + GetRecorder() *Recorder + Start(streamPath string) error + io.Closer + CreateFile() (FileWr, error) } -func (r *Recorder) start() { - RecordPluginConfig.recordings.Store(r.ID, r) - r.PlayRaw() - RecordPluginConfig.recordings.Delete(r.ID) +type Recorder struct { + Subscriber + SkipTS uint32 + Record `json:"-" yaml:"-"` + File FileWr `json:"-" yaml:"-"` + FileName string // 自定义文件名,分段录像无效 + append bool // 是否追加模式 +} + +func (r *Recorder) GetRecorder() *Recorder { + return r +} + +func (r *Recorder) CreateFile() (FileWr, error) { + return r.createFile() +} + +func (r *Recorder) Close() error { + if r.File != nil { + return r.File.Close() + } + return nil +} + +func (r *Recorder) createFile() (f FileWr, err error) { + filePath := r.getFileName(r.Stream.Path) + r.Ext + f, err = r.CreateFileFn(filePath, r.append) + if err == nil { + r.Info("create file", zap.String("path", filePath)) + } else { + r.Error("create file", zap.String("path", filePath), zap.Error(err)) + } + return +} + +func (r *Recorder) getFileName(streamPath string) (filename string) { + filename = streamPath + if r.Fragment == 0 { + if r.FileName != "" { + filename = filepath.Join(filename, r.FileName) + } + } else { + filename = filepath.Join(filename, strconv.FormatInt(time.Now().Unix(), 10)) + } + return +} + +func (r *Recorder) start(re IRecorder, streamPath string, subType byte) (err error) { + err = plugin.Subscribe(streamPath, re) + if err == nil { + if _, loaded := RecordPluginConfig.recordings.LoadOrStore(r.ID, re); loaded { + return ErrRecordExist + } + r.recording[streamPath] = re + r.Closer = re + go func() { + r.PlayBlock(subType) + RecordPluginConfig.recordings.Delete(r.ID) + delete(r.recording, streamPath) + re.Close() + }() + } + return } func (r *Recorder) cut(absTime uint32) { if ts := absTime - r.SkipTS; time.Duration(ts)*time.Millisecond >= r.Fragment { r.SkipTS = absTime - r.newFile = true + r.Close() + if file, err := r.Spesific.(IRecorder).CreateFile(); err == nil { + r.File = file + r.Spesific.OnEvent(file) + } else { + r.Stop(zap.Error(err)) + } } } func (r *Recorder) OnEvent(event any) { switch v := event.(type) { - case ISubscriber: - filename := strconv.FormatInt(time.Now().Unix(), 10) + r.Ext - if r.Fragment == 0 { - filename = r.Stream.Path + r.Ext + case IRecorder: + if file, err := r.Spesific.(IRecorder).CreateFile(); err == nil { + r.File = file + r.Spesific.OnEvent(file) } else { - filename = filepath.Join(r.Stream.Path, filename) - } - if file, err := r.CreateFileFn(filename, r.append); err == nil { - r.SetIO(file) - } else { - r.Error("create file failed", zap.Error(err)) - r.Stop() + r.Stop(zap.Error(err)) } case AudioFrame: // 纯音频流的情况下需要切割文件