From 180e766a24e6f9529f6ea1893265a0e9e3ed501f Mon Sep 17 00:00:00 2001 From: langhuihui <178529795@qq.com> Date: Wed, 5 Feb 2025 18:52:52 +0800 Subject: [PATCH] feat: vod hlsv7 (fmp4) --- example/default/config.yaml | 21 +- plugin/hls/hls.js/fmp4.html | 330 +++++++++++++++++++++++++ plugin/hls/index.go | 105 ++++++++ plugin/mp4/index.go | 156 +++++++----- plugin/mp4/pkg/bits/reader.go | 59 +++++ plugin/mp4/pkg/box/box.go | 20 +- plugin/mp4/pkg/box/ftyp.go | 4 - plugin/mp4/pkg/box/mp4table.go | 2 +- plugin/mp4/pkg/box/mvhd.go | 58 ++--- plugin/mp4/pkg/box/tfdt.go | 12 +- plugin/mp4/pkg/box/tfhd.go | 32 +-- plugin/mp4/pkg/box/tfra.go | 33 ++- plugin/mp4/pkg/box/trun.go | 111 ++++++--- plugin/mp4/pkg/demuxer.go | 2 +- plugin/mp4/pkg/memory_file.go | 81 +++++++ plugin/mp4/pkg/muxer.go | 428 +++++++++++++++++---------------- plugin/mp4/pkg/muxer_test.go | 370 ++++++++++++++++++++++++++++ plugin/mp4/pkg/record.go | 75 +++--- plugin/mp4/pkg/track.go | 329 +++++++++++++++++-------- plugin/webrtc/api.go | 34 --- website/index.html | 175 -------------- website/main.js | 93 ------- website/style.css | 419 -------------------------------- 23 files changed, 1722 insertions(+), 1227 deletions(-) create mode 100644 plugin/hls/hls.js/fmp4.html create mode 100644 plugin/mp4/pkg/bits/reader.go create mode 100644 plugin/mp4/pkg/memory_file.go create mode 100644 plugin/mp4/pkg/muxer_test.go delete mode 100644 website/index.html delete mode 100644 website/main.js delete mode 100644 website/style.css diff --git a/example/default/config.yaml b/example/default/config.yaml index 7ddbda11..ab238064 100755 --- a/example/default/config.yaml +++ b/example/default/config.yaml @@ -2,7 +2,10 @@ global: location: "^/hdl/(.*)": "/flv/$1" loglevel: debug - enablelogin: false + admin: + enablelogin: false + subscribe: + subaudio: false # db: # dbtype: mysql # dsn: root:Monibuca#!4@tcp(sh-cynosdbmysql-grp-kxt43lv6.sql.tencentcdb.com:28520)/lkm7s_v5?parseTime=true @@ -22,9 +25,14 @@ gb28181: .* : $0 mp4: # enable: false - # publish: - # delayclosetimeout: 3s - + publish: + delayclosetimeout: 3s + # onpub: + # record: + # ^live/.+: + # fragment: 10s + # filepath: record/$0 + # type: fmp4 onsub: pull: ^vod_mp4_\d+/(.+)$: $1 @@ -41,12 +49,17 @@ flv: # ^live/.+: # fragment: 1m # filepath: record/$0 + publish: + delayclosetimeout: 3s onsub: pull: ^vod_flv_\d+/(.+)$: $1 # pull: # live/test: https://livecb.alicdn.com/mediaplatform/afb241b3-408c-42dd-b665-04d22b64f9df.flv?auth_key=1734575216-0-0-c62721303ce751c8e5b2c95a2ec242a0&F=pc&source=34675810_null_live_detail&ali_flv_retain=2 hls: + # onsub: + # pull: + # ^vod_hls_\d+/(.+)$: $1 # pull: # live/test: https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/gear3/prog_index.m3u8 # onpub: diff --git a/plugin/hls/hls.js/fmp4.html b/plugin/hls/hls.js/fmp4.html new file mode 100644 index 00000000..0971106e --- /dev/null +++ b/plugin/hls/hls.js/fmp4.html @@ -0,0 +1,330 @@ + + + + + + + M3U8 to MP4 Player + + + + +
+ + +
+ +
+ + + + + \ No newline at end of file diff --git a/plugin/hls/index.go b/plugin/hls/index.go index 6b0e0dae..3d0c8db0 100644 --- a/plugin/hls/index.go +++ b/plugin/hls/index.go @@ -39,6 +39,12 @@ func (p *HLSPlugin) OnInit() (err error) { return } +func (p *HLSPlugin) RegisterHandler() map[string]http.HandlerFunc { + return map[string]http.HandlerFunc{ + "/vod/{streamPath...}": p.vod, + } +} + func (p *HLSPlugin) OnPullProxyAdd(pullProxy *m7s.PullProxy) any { d := &m7s.HTTPPullProxy{} d.PullProxy = pullProxy @@ -46,6 +52,105 @@ func (p *HLSPlugin) OnPullProxyAdd(pullProxy *m7s.PullProxy) any { return d } +func (config *HLSPlugin) vod(w http.ResponseWriter, r *http.Request) { + recordType := "ts" + if r.PathValue("streamPath") == "mp4.m3u8" { + recordType = "mp4" + } else if r.PathValue("streamPath") == "fmp4.m3u8" { + recordType = "fmp4" + } + query := r.URL.Query() + fileName := query.Get("streamPath") + waitTimeout, err := time.ParseDuration(query.Get("timeout")) + if err == nil { + config.Debug("request", "fileName", fileName, "timeout", waitTimeout) + } else { + waitTimeout = time.Second * 10 + } + // waitStart := time.Now() + if strings.HasSuffix(r.URL.Path, ".m3u8") { + w.Header().Add("Content-Type", "application/vnd.apple.mpegurl") + streamPath := strings.TrimSuffix(fileName, ".m3u8") + // If memory lookup failed or returned empty, try database + startTime, endTime, _ := util.TimeRangeQueryParse(query) + if !startTime.IsZero() { + if config.DB != nil { + var records []m7s.RecordStream + if endTime.IsZero() { + query := `stream_path = ? AND type = ? AND start_time IS NOT NULL AND end_time > ?` + config.DB.Where(query, streamPath, recordType, startTime).Find(&records) + } else { + query := `stream_path = ? AND type = ? AND start_time IS NOT NULL AND end_time IS NOT NULL AND ? <= end_time AND ? >= start_time` + config.DB.Where(query, streamPath, recordType, startTime, endTime).Find(&records) + } + if len(records) > 0 { + playlist := hls.Playlist{ + Version: 7, + Sequence: 0, + Targetduration: 90, + } + var plBuffer util.Buffer + playlist.Writer = &plBuffer + playlist.Init() + + for _, record := range records { + duration := record.EndTime.Sub(record.StartTime).Seconds() + playlist.WriteInf(hls.PlaylistInf{ + Duration: duration, + Title: record.FilePath, + FilePath: record.FilePath, + }) + } + plBuffer.WriteString("#EXT-X-ENDLIST\n") + w.Write(plBuffer) + return + } + } + } + + // if v, ok := hls.MemoryM3u8.Load(streamPath); ok && v.(string) != "" { + // w.Write([]byte(v.(string))) + // return + // } + // for { + // if v, ok := hls.MemoryM3u8.Load(streamPath); ok && v.(string) != "" { + // w.Write([]byte(v.(string))) + // return + // } + // if waitTimeout > 0 && time.Since(waitStart) < waitTimeout { + // config.Server.OnSubscribe(streamPath, r.URL.Query()) + // time.Sleep(time.Second) + // continue + // } else { + // break + // } + // } + } else if strings.HasSuffix(r.URL.Path, ".mp4") { + w.Header().Add("Content-Type", "video/mp4") //video/mp4 + data, err := os.ReadFile(r.PathValue("streamPath")) + if err == nil { + w.Write(data) + return + } + // streamPath := path.Dir(fileName) + // tsData, ok := hls.MemoryTs.Load(streamPath) + // if !ok { + // tsData, ok = hls.MemoryTs.Load(path.Dir(streamPath)) + // } + // if ok { + // if tsData, ok := tsData.(hls.TsCacher).GetTs(fileName); ok { + // switch v := tsData.(type) { + // case *hls.TsInMemory: + // v.WriteTo(w) + // case util.Buffer: + // w.Write(v) + // } + // return + // } + // } + } +} + func (config *HLSPlugin) ServeHTTP(w http.ResponseWriter, r *http.Request) { fileName := strings.TrimPrefix(r.URL.Path, "/") query := r.URL.Query() diff --git a/plugin/mp4/index.go b/plugin/mp4/index.go index 8783b44b..6ce84bb8 100644 --- a/plugin/mp4/index.go +++ b/plugin/mp4/index.go @@ -12,9 +12,9 @@ import ( "m7s.live/v5" v5 "m7s.live/v5/pkg" "m7s.live/v5/pkg/codec" - "m7s.live/v5/pkg/util" "m7s.live/v5/plugin/mp4/pb" pkg "m7s.live/v5/plugin/mp4/pkg" + "m7s.live/v5/plugin/mp4/pkg/box" rtmp "m7s.live/v5/plugin/rtmp/pkg" ) @@ -23,7 +23,10 @@ type MediaContext struct { conn net.Conn wto time.Duration seqNumber uint32 - audio, video TrackContext + muxer *pkg.Muxer + audio, video *pkg.Track + buffer []byte + offset int64 } func (m *MediaContext) Write(p []byte) (n int, err error) { @@ -33,6 +36,33 @@ func (m *MediaContext) Write(p []byte) (n int, err error) { return m.Writer.Write(p) } +func (m *MediaContext) Read(p []byte) (n int, err error) { + if m.offset >= int64(len(m.buffer)) { + return 0, io.EOF + } + n = copy(p, m.buffer[m.offset:]) + m.offset += int64(n) + return +} + +func (m *MediaContext) Seek(offset int64, whence int) (int64, error) { + switch whence { + case io.SeekStart: + m.offset = offset + case io.SeekCurrent: + m.offset += offset + case io.SeekEnd: + m.offset = int64(len(m.buffer)) + offset + } + if m.offset < 0 { + m.offset = 0 + } + if m.offset > int64(len(m.buffer)) { + m.offset = int64(len(m.buffer)) + } + return m.offset, nil +} + type TrackContext struct { TrackId uint32 fragment *mp4.Fragment @@ -146,108 +176,112 @@ func (p *MP4Plugin) ServeHTTP(w http.ResponseWriter, r *http.Request) { } } - initSegment := mp4.CreateEmptyInit() - initSegment.Moov.Mvhd.NextTrackID = 1 + if ctx.conn != nil { + ctx.Writer = ctx.conn + } else { + ctx.Writer = w + w.(http.Flusher).Flush() + } ctx.wto = p.GetCommonConf().WriteTimeout - var ftyp *mp4.FtypBox + ctx.muxer = pkg.NewMuxer(pkg.FLAG_FRAGMENT) + ctx.muxer.WriteInitSegment(ctx.Writer) var offsetAudio, offsetVideo = 1, 5 - var durAudio, durVideo uint32 = 40, 40 + if sub.Publisher.HasVideoTrack() { v := sub.Publisher.VideoTrack.AVTrack if err = v.WaitReady(); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } - moov := initSegment.Moov - trackID := moov.Mvhd.NextTrackID - moov.Mvhd.NextTrackID++ - newTrak := mp4.CreateEmptyTrak(trackID, 1000, "video", "chi") - moov.AddChild(newTrak) - moov.Mvex.AddChild(mp4.CreateTrex(trackID)) - ctx.video.TrackId = trackID - ftyp = mp4.NewFtyp("isom", 0x200, []string{ - "isom", "iso2", v.ICodecCtx.FourCC().String(), "mp41", - }) + var codecID box.MP4_CODEC_TYPE + switch v.ICodecCtx.FourCC() { + case codec.FourCC_H264: + codecID = box.MP4_CODEC_H264 + case codec.FourCC_H265: + codecID = box.MP4_CODEC_H265 + } + ctx.video = ctx.muxer.AddTrack(codecID) + ctx.video.Timescale = 1000 + switch v.ICodecCtx.FourCC() { case codec.FourCC_H264: h264Ctx := v.ICodecCtx.GetBase().(*codec.H264Ctx) - durVideo = uint32(h264Ctx.PacketDuration(nil) / time.Millisecond) - newTrak.SetAVCDescriptor("avc1", h264Ctx.RecordInfo.SPS, h264Ctx.RecordInfo.PPS, true) + ctx.video.ExtraData = h264Ctx.Record + ctx.video.Width = uint32(h264Ctx.Width()) + ctx.video.Height = uint32(h264Ctx.Height()) case codec.FourCC_H265: h265Ctx := v.ICodecCtx.GetBase().(*codec.H265Ctx) - durVideo = uint32(h265Ctx.PacketDuration(nil) / time.Millisecond) - newTrak.SetHEVCDescriptor("hvc1", h265Ctx.RecordInfo.VPS, h265Ctx.RecordInfo.SPS, h265Ctx.RecordInfo.PPS, nil, true) - case codec.FourCC_AV1: - //av1Ctx := v.ICodecCtx.GetBase().(*codec.AV1Ctx) - //durVideo = uint32(av1Ctx.PacketDuration(nil) / time.Millisecond) + ctx.video.ExtraData = h265Ctx.Record + ctx.video.Width = uint32(h265Ctx.Width()) + ctx.video.Height = uint32(h265Ctx.Height()) } } + if sub.Publisher.HasAudioTrack() { a := sub.Publisher.AudioTrack.AVTrack if err = a.WaitReady(); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } - moov := initSegment.Moov - trackID := moov.Mvhd.NextTrackID - moov.Mvhd.NextTrackID++ - newTrak := mp4.CreateEmptyTrak(trackID, 1000, "audio", "chi") - moov.AddChild(newTrak) - moov.Mvex.AddChild(mp4.CreateTrex(trackID)) - ctx.audio.TrackId = trackID + var codecID box.MP4_CODEC_TYPE + switch a.ICodecCtx.FourCC() { + case codec.FourCC_MP4A: + codecID = box.MP4_CODEC_AAC + } + ctx.audio = ctx.muxer.AddTrack(codecID) + ctx.audio.Timescale = 1000 audioCtx := a.ICodecCtx.(v5.IAudioCodecCtx) + ctx.audio.SampleRate = uint32(audioCtx.GetSampleRate()) + ctx.audio.ChannelCount = uint8(audioCtx.GetChannels()) + ctx.audio.SampleSize = uint16(audioCtx.GetSampleSize()) + switch a.ICodecCtx.FourCC() { case codec.FourCC_MP4A: offsetAudio = 2 - aacCtx := a.ICodecCtx.GetBase().(*codec.AACCtx) - newTrak.SetAACDescriptor(byte(aacCtx.Config.ObjectType), aacCtx.Config.SampleRate) - case codec.FourCC_ALAW: - stsd := newTrak.Mdia.Minf.Stbl.Stsd - pcma := mp4.CreateAudioSampleEntryBox("pcma", - uint16(audioCtx.GetChannels()), - uint16(audioCtx.GetSampleSize()), uint16(audioCtx.GetSampleRate()), nil) - stsd.AddChild(pcma) - case codec.FourCC_ULAW: - stsd := newTrak.Mdia.Minf.Stbl.Stsd - pcmu := mp4.CreateAudioSampleEntryBox("pcmu", - uint16(audioCtx.GetChannels()), - uint16(audioCtx.GetSampleSize()), uint16(audioCtx.GetSampleRate()), nil) - stsd.AddChild(pcmu) + ctx.audio.ExtraData = a.ICodecCtx.GetBase().(*codec.AACCtx).ConfigBytes + default: + offsetAudio = 1 } } - if ctx.conn != nil { - ctx.Writer = ctx.conn - } else { - ctx.Writer = w - w.(http.Flusher).Flush() + + err = ctx.muxer.WriteInitSegment(&ctx) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return } - ftyp.Encode(&ctx) - initSegment.Moov.Encode(&ctx) - var lastATime, lastVTime uint32 + m7s.PlayBlock(sub, func(audio *rtmp.RTMPAudio) error { bs := audio.Memory.ToBytes() if offsetAudio == 2 && bs[1] == 0 { return nil } - if lastATime > 0 { - durAudio = audio.Timestamp - lastATime + sample := box.Sample{ + Offset: 0, + Data: bs[offsetAudio:], + Size: len(bs) - offsetAudio, + DTS: uint64(audio.Timestamp), + PTS: uint64(audio.Timestamp), + KeyFrame: true, } - ctx.audio.Push(&ctx, audio.Timestamp, durAudio, bs[offsetAudio:], mp4.SyncSampleFlags) - lastATime = audio.Timestamp + ctx.audio.AddSampleEntry(sample) return nil }, func(video *rtmp.RTMPVideo) error { - if lastVTime > 0 { - durVideo = video.Timestamp - lastVTime - } bs := video.Memory.ToBytes() if ctx, ok := sub.VideoReader.Track.ICodecCtx.(*rtmp.H265Ctx); ok && ctx.Enhanced && bs[0]&0b1111 == rtmp.PacketTypeCodedFrames { offsetVideo = 8 } else { offsetVideo = 5 } - ctx.video.Push(&ctx, video.Timestamp, durVideo, bs[offsetVideo:], util.Conditional(sub.VideoReader.Value.IDR, mp4.SyncSampleFlags, mp4.NonSyncSampleFlags)) - lastVTime = video.Timestamp + sample := box.Sample{ + Offset: 0, + Data: bs[offsetVideo:], + Size: len(bs) - offsetVideo, + DTS: uint64(video.Timestamp), + PTS: uint64(video.Timestamp), + KeyFrame: sub.VideoReader.Value.IDR, + } + ctx.video.AddSampleEntry(sample) return nil }) } diff --git a/plugin/mp4/pkg/bits/reader.go b/plugin/mp4/pkg/bits/reader.go new file mode 100644 index 00000000..1273219a --- /dev/null +++ b/plugin/mp4/pkg/bits/reader.go @@ -0,0 +1,59 @@ +package bits + +// Reader is a bit stream reader +type Reader struct { + Data []byte + Offset int +} + +// Skip skips n bits +func (r *Reader) Skip(n int) { + r.Offset += n +} + +// ReadBit reads a single bit +func (r *Reader) ReadBit() (uint, error) { + if r.Offset/8 >= len(r.Data) { + return 0, nil + } + b := r.Data[r.Offset/8] + v := (b >> (7 - (r.Offset % 8))) & 0x01 + r.Offset++ + return uint(v), nil +} + +// ReadExpGolomb reads an Exp-Golomb code +func (r *Reader) ReadExpGolomb() (uint, error) { + leadingZeroBits := 0 + for { + b, err := r.ReadBit() + if err != nil { + return 0, err + } + if b == 1 { + break + } + leadingZeroBits++ + } + + result := uint(1<> 1) + (val & 0x01)) * uint(sign) + return int(val), nil +} diff --git a/plugin/mp4/pkg/box/box.go b/plugin/mp4/pkg/box/box.go index 4f484230..6e2ce0e2 100644 --- a/plugin/mp4/pkg/box/box.go +++ b/plugin/mp4/pkg/box/box.go @@ -6,8 +6,8 @@ import ( ) const ( - BasicBoxLen = 8 - FullBoxLen = 12 + BasicBoxLen = 8 // size(4) + type(4) + FullBoxLen = 12 // BasicBoxLen + version(1) + flags(3) ) func f(s string) [4]byte { @@ -208,3 +208,19 @@ func (box *FullBox) Encode() (int, []byte) { copy(buf[offset+1:], box.Flags[:]) return offset + 4, buf } + +type TimeToSampleEntry struct { + SampleCount uint32 + SampleDelta uint32 +} + +type CompositionTimeToSampleEntry struct { + SampleCount uint32 + SampleOffset int32 +} + +type SampleToChunkEntry struct { + FirstChunk uint32 + SamplesPerChunk uint32 + SampleDescriptionIndex uint32 +} diff --git a/plugin/mp4/pkg/box/ftyp.go b/plugin/mp4/pkg/box/ftyp.go index b92cfa05..d661610a 100644 --- a/plugin/mp4/pkg/box/ftyp.go +++ b/plugin/mp4/pkg/box/ftyp.go @@ -5,10 +5,6 @@ import ( "io" ) -func mov_tag(tag [4]byte) uint32 { - return binary.LittleEndian.Uint32(tag[:]) -} - type FileTypeBox struct { Major_brand [4]byte Minor_version uint32 diff --git a/plugin/mp4/pkg/box/mp4table.go b/plugin/mp4/pkg/box/mp4table.go index 9c0933b8..2699b53e 100644 --- a/plugin/mp4/pkg/box/mp4table.go +++ b/plugin/mp4/pkg/box/mp4table.go @@ -39,7 +39,7 @@ type TrunEntry struct { SampleDuration uint32 SampleSize uint32 SampleFlags uint32 - SampleCompositionTimeOffset uint32 + SampleCompositionTimeOffset int32 } type SENC struct { diff --git a/plugin/mp4/pkg/box/mvhd.go b/plugin/mp4/pkg/box/mvhd.go index af4b75ae..2882ca81 100644 --- a/plugin/mp4/pkg/box/mvhd.go +++ b/plugin/mp4/pkg/box/mvhd.go @@ -41,10 +41,13 @@ type MovieHeaderBox struct { } func NewMovieHeaderBox() *MovieHeaderBox { - _, offset := time.Now().Zone() + // MP4/QuickTime epoch starts from Jan 1, 1904 + // Add offset between Unix epoch (1970) and QuickTime epoch (1904) + const mp4Epoch = 2082844800 // seconds between 1904 and 1970 + now := uint64(time.Now().Unix() + mp4Epoch) return &MovieHeaderBox{ - Creation_time: uint64(time.Now().Unix() + int64(offset) + 0x7C25B080), - Modification_time: uint64(time.Now().Unix() + int64(offset) + 0x7C25B080), + Creation_time: now, + Modification_time: now, Timescale: 1000, Rate: 0x00010000, Volume: 0x0100, @@ -106,44 +109,35 @@ func (mvhd *MovieHeaderBox) Decode(r io.Reader, basebox *BasicBox) (offset int, } func (mvhd *MovieHeaderBox) Encode() (int, []byte) { + // Always use version 0 for better compatibility var fullbox = NewFullBox(TypeMVHD, 0) - if fullbox.Version == 1 { - fullbox.Box.Size = FullBoxLen + 108 - } else { - fullbox.Box.Size = FullBoxLen + 96 - } + fullbox.Box.Size = FullBoxLen + 96 // version 0 size offset, buf := fullbox.Encode() - if fullbox.Version == 1 { - binary.BigEndian.PutUint64(buf[offset:], mvhd.Creation_time) - offset += 8 - binary.BigEndian.PutUint64(buf[offset:], mvhd.Modification_time) - offset += 8 - binary.BigEndian.PutUint32(buf[offset:], mvhd.Timescale) - offset += 4 - binary.BigEndian.PutUint64(buf[offset:], mvhd.Duration) - offset += 8 - } else { - binary.BigEndian.PutUint32(buf[offset:], uint32(mvhd.Creation_time)) - offset += 4 - binary.BigEndian.PutUint32(buf[offset:], uint32(mvhd.Modification_time)) - offset += 4 - binary.BigEndian.PutUint32(buf[offset:], uint32(mvhd.Timescale)) - offset += 4 - binary.BigEndian.PutUint32(buf[offset:], uint32(mvhd.Duration)) - offset += 4 - } + + // Version 0: all 32-bit values + binary.BigEndian.PutUint32(buf[offset:], uint32(mvhd.Creation_time)) + offset += 4 + binary.BigEndian.PutUint32(buf[offset:], uint32(mvhd.Modification_time)) + offset += 4 + binary.BigEndian.PutUint32(buf[offset:], mvhd.Timescale) + offset += 4 + binary.BigEndian.PutUint32(buf[offset:], uint32(mvhd.Duration)) + offset += 4 + binary.BigEndian.PutUint32(buf[offset:], mvhd.Rate) offset += 4 binary.BigEndian.PutUint16(buf[offset:], mvhd.Volume) offset += 2 - offset += 10 - for i, _ := range mvhd.Matrix { - binary.BigEndian.PutUint32(buf[offset:], mvhd.Matrix[i]) + offset += 10 // reserved + + for _, matrix := range mvhd.Matrix { + binary.BigEndian.PutUint32(buf[offset:], matrix) offset += 4 } - offset += 24 + + offset += 24 // pre-defined binary.BigEndian.PutUint32(buf[offset:], mvhd.Next_track_ID) - return offset + 2, buf + return offset + 4, buf } func MakeMvhdBox(trackid uint32, duration uint32) []byte { diff --git a/plugin/mp4/pkg/box/tfdt.go b/plugin/mp4/pkg/box/tfdt.go index 5a61759a..bf611d02 100644 --- a/plugin/mp4/pkg/box/tfdt.go +++ b/plugin/mp4/pkg/box/tfdt.go @@ -15,16 +15,25 @@ import ( type TrackFragmentBaseMediaDecodeTimeBox struct { BaseMediaDecodeTime uint64 + version uint8 } func NewTrackFragmentBaseMediaDecodeTimeBox(fragStart uint64) *TrackFragmentBaseMediaDecodeTimeBox { + version := uint8(0) + if fragStart > 0xFFFFFFFF { + version = 1 + } return &TrackFragmentBaseMediaDecodeTimeBox{ + version: version, BaseMediaDecodeTime: fragStart, } } func (tfdt *TrackFragmentBaseMediaDecodeTimeBox) Size() uint64 { - return FullBoxLen + 8 + if tfdt.version == 1 { + return FullBoxLen + 8 // 8 bytes for base_media_decode_time + } + return FullBoxLen + 4 // 4 bytes for base_media_decode_time } func (tfdt *TrackFragmentBaseMediaDecodeTimeBox) Decode(r io.Reader, size uint32) (offset int, err error) { @@ -49,6 +58,7 @@ func (tfdt *TrackFragmentBaseMediaDecodeTimeBox) Decode(r io.Reader, size uint32 func (tfdt *TrackFragmentBaseMediaDecodeTimeBox) Encode() (int, []byte) { fullbox := NewFullBox(TypeTFDT, 1) + tfdt.version = fullbox.Version fullbox.Box.Size = tfdt.Size() offset, boxdata := fullbox.Encode() binary.BigEndian.PutUint64(boxdata[offset:], tfdt.BaseMediaDecodeTime) diff --git a/plugin/mp4/pkg/box/tfhd.go b/plugin/mp4/pkg/box/tfhd.go index a182302b..77a94201 100644 --- a/plugin/mp4/pkg/box/tfhd.go +++ b/plugin/mp4/pkg/box/tfhd.go @@ -16,24 +16,25 @@ import ( // } const ( - TF_FLAG_BASE_DATA_OFFSET uint32 = 0x000001 + TF_FLAG_BASE_DATA_OFFSET_PRESENT uint32 = 0x000001 TF_FLAG_SAMPLE_DESCRIPTION_INDEX_PRESENT uint32 = 0x000002 TF_FLAG_DEFAULT_SAMPLE_DURATION_PRESENT uint32 = 0x000008 TF_FLAG_DEFAULT_SAMPLE_SIZE_PRESENT uint32 = 0x000010 - TF_FLAG_DEAAULT_SAMPLE_FLAGS_PRESENT uint32 = 0x000020 + TF_FLAG_DEFAULT_SAMPLE_FLAGS_PRESENT uint32 = 0x000020 TF_FLAG_DURATION_IS_EMPTY uint32 = 0x010000 - TF_FLAG_DEAAULT_BASE_IS_MOOF uint32 = 0x020000 + TF_FLAG_DEFAULT_BASE_IS_MOOF uint32 = 0x020000 - //ffmpeg isom.h + // Sample flags MOV_FRAG_SAMPLE_FLAG_DEGRADATION_PRIORITY_MASK uint32 = 0x0000ffff - MOV_FRAG_SAMPLE_FLAG_IS_NON_SYNC uint32 = 0x00010000 + MOV_FRAG_SAMPLE_FLAG_IS_NON_SYNC uint32 = 0x10000000 + MOV_FRAG_SAMPLE_FLAG_IS_SYNC uint32 = 0x00000000 MOV_FRAG_SAMPLE_FLAG_PADDING_MASK uint32 = 0x000e0000 MOV_FRAG_SAMPLE_FLAG_REDUNDANCY_MASK uint32 = 0x00300000 MOV_FRAG_SAMPLE_FLAG_DEPENDED_MASK uint32 = 0x00c00000 MOV_FRAG_SAMPLE_FLAG_DEPENDS_MASK uint32 = 0x03000000 - - MOV_FRAG_SAMPLE_FLAG_DEPENDS_NO uint32 = 0x02000000 - MOV_FRAG_SAMPLE_FLAG_DEPENDS_YES uint32 = 0x01000000 + MOV_FRAG_SAMPLE_FLAG_DEPENDS_RESERVED uint32 = 0x03000000 + MOV_FRAG_SAMPLE_FLAG_DEPENDS_NO uint32 = 0x02000000 + MOV_FRAG_SAMPLE_FLAG_DEPENDS_YES uint32 = 0x01000000 ) type TrackFragmentHeaderBox struct { @@ -55,7 +56,7 @@ func NewTrackFragmentHeaderBox(trackid uint32) *TrackFragmentHeaderBox { func (tfhd *TrackFragmentHeaderBox) Size(thfdFlags uint32) uint64 { n := uint64(FullBoxLen) n += 4 - if thfdFlags&TF_FLAG_BASE_DATA_OFFSET > 0 { + if thfdFlags&TF_FLAG_BASE_DATA_OFFSET_PRESENT > 0 && thfdFlags&TF_FLAG_DEFAULT_BASE_IS_MOOF == 0 { n += 8 } if thfdFlags&TF_FLAG_SAMPLE_DESCRIPTION_INDEX_PRESENT > 0 { @@ -67,7 +68,7 @@ func (tfhd *TrackFragmentHeaderBox) Size(thfdFlags uint32) uint64 { if thfdFlags&TF_FLAG_DEFAULT_SAMPLE_SIZE_PRESENT > 0 { n += 4 } - if thfdFlags&TF_FLAG_DEAAULT_SAMPLE_FLAGS_PRESENT > 0 { + if thfdFlags&TF_FLAG_DEFAULT_SAMPLE_FLAGS_PRESENT > 0 { n += 4 } return n @@ -86,10 +87,11 @@ func (tfhd *TrackFragmentHeaderBox) Decode(r io.Reader, size uint32, moofOffset tfhd.Track_ID = binary.BigEndian.Uint32(buf[n:]) n += 4 tfhdFlags := uint32(fullbox.Flags[0])<<16 | uint32(fullbox.Flags[1])<<8 | uint32(fullbox.Flags[2]) - if tfhdFlags&uint32(TF_FLAG_BASE_DATA_OFFSET) > 0 { + if tfhdFlags&uint32(TF_FLAG_BASE_DATA_OFFSET_PRESENT) > 0 { tfhd.BaseDataOffset = binary.BigEndian.Uint64(buf[n:]) n += 8 - } else if tfhdFlags&uint32(TF_FLAG_DEAAULT_BASE_IS_MOOF) > 0 { + } + if tfhdFlags&uint32(TF_FLAG_DEFAULT_BASE_IS_MOOF) > 0 { tfhd.BaseDataOffset = moofOffset } else { //TODO,In some cases, it is wrong @@ -108,7 +110,7 @@ func (tfhd *TrackFragmentHeaderBox) Decode(r io.Reader, size uint32, moofOffset tfhd.DefaultSampleSize = binary.BigEndian.Uint32(buf[n:]) n += 4 } - if tfhdFlags&uint32(TF_FLAG_DEAAULT_SAMPLE_FLAGS_PRESENT) > 0 { + if tfhdFlags&uint32(TF_FLAG_DEFAULT_SAMPLE_FLAGS_PRESENT) > 0 { tfhd.DefaultSampleFlags = binary.BigEndian.Uint32(buf[n:]) n += 4 } @@ -126,7 +128,7 @@ func (tfhd *TrackFragmentHeaderBox) Encode(tFfFlags uint32) (int, []byte) { binary.BigEndian.PutUint32(buf[offset:], tfhd.Track_ID) offset += 4 thfdFlags := uint32(fullbox.Flags[0])<<16 | uint32(fullbox.Flags[1])<<8 | uint32(fullbox.Flags[2]) - if thfdFlags&uint32(TF_FLAG_BASE_DATA_OFFSET) > 0 { + if thfdFlags&uint32(TF_FLAG_BASE_DATA_OFFSET_PRESENT) > 0 && thfdFlags&uint32(TF_FLAG_DEFAULT_BASE_IS_MOOF) == 0 { binary.BigEndian.PutUint64(buf[offset:], tfhd.BaseDataOffset) offset += 8 } @@ -142,7 +144,7 @@ func (tfhd *TrackFragmentHeaderBox) Encode(tFfFlags uint32) (int, []byte) { binary.BigEndian.PutUint32(buf[offset:], tfhd.DefaultSampleSize) offset += 4 } - if thfdFlags&uint32(TF_FLAG_DEAAULT_SAMPLE_FLAGS_PRESENT) > 0 { + if thfdFlags&uint32(TF_FLAG_DEFAULT_SAMPLE_FLAGS_PRESENT) > 0 { binary.BigEndian.PutUint32(buf[offset:], tfhd.DefaultSampleFlags) offset += 4 } diff --git a/plugin/mp4/pkg/box/tfra.go b/plugin/mp4/pkg/box/tfra.go index 6a863f52..f80ae71d 100644 --- a/plugin/mp4/pkg/box/tfra.go +++ b/plugin/mp4/pkg/box/tfra.go @@ -44,7 +44,15 @@ func NewTrackFragmentRandomAccessBox(trackid uint32) *TrackFragmentRandomAccessB } func (tfra *TrackFragmentRandomAccessBox) Size() uint64 { - return tfra.Box.Size() + 12 + uint64(len(tfra.FragEntrys))*19 + entrySize := 0 + if tfra.Box.Version == 1 { + entrySize = 16 // 8 bytes for time + 8 bytes for moof_offset + } else { + entrySize = 8 // 4 bytes for time + 4 bytes for moof_offset + } + // Add size for traf_number, trun_number, and sample_number + entrySize += int(tfra.LengthSizeOfTrafNum + tfra.LengthSizeOfTrunNum + tfra.LengthSizeOfSampleNum + 3) + return tfra.Box.Size() + 12 + uint64(len(tfra.FragEntrys)*entrySize) // 12 = 4(track_id) + 4(reserved) + 4(number_of_entry) } func (tfra *TrackFragmentRandomAccessBox) Decode(r io.Reader) (offset int, err error) { @@ -90,7 +98,11 @@ func (tfra *TrackFragmentRandomAccessBox) Encode() (int, []byte) { offset, boxdata := tfra.Box.Encode() binary.BigEndian.PutUint32(boxdata[offset:], tfra.TrackID) offset += 4 - binary.BigEndian.PutUint32(boxdata[offset:], 0) + // Pack length size fields into the reserved uint32 + lengthSizeFlags := uint32(tfra.LengthSizeOfTrafNum&0x03)<<4 | + uint32(tfra.LengthSizeOfTrunNum&0x03)<<2 | + uint32(tfra.LengthSizeOfSampleNum&0x03) + binary.BigEndian.PutUint32(boxdata[offset:], lengthSizeFlags) offset += 4 binary.BigEndian.PutUint32(boxdata[offset:], uint32(len(tfra.FragEntrys))) offset += 4 @@ -106,10 +118,19 @@ func (tfra *TrackFragmentRandomAccessBox) Encode() (int, []byte) { binary.BigEndian.PutUint32(boxdata[offset:], uint32(frag.MoofOffset)) offset += 4 } - boxdata[offset] = 1 - boxdata[offset+1] = 1 - boxdata[offset+2] = 1 - offset += 3 + // Write traf_number, trun_number, and sample_number based on length sizes + for i := uint8(0); i < tfra.LengthSizeOfTrafNum+1; i++ { + boxdata[offset] = 1 + offset++ + } + for i := uint8(0); i < tfra.LengthSizeOfTrunNum+1; i++ { + boxdata[offset] = 1 + offset++ + } + for i := uint8(0); i < tfra.LengthSizeOfSampleNum+1; i++ { + boxdata[offset] = 1 + offset++ + } } return offset, boxdata } diff --git a/plugin/mp4/pkg/box/trun.go b/plugin/mp4/pkg/box/trun.go index 45c993c5..19ca6a0e 100644 --- a/plugin/mp4/pkg/box/trun.go +++ b/plugin/mp4/pkg/box/trun.go @@ -5,7 +5,7 @@ import ( "io" ) -// aligned(8) class TrackRunBox extends FullBox(‘trun’, version, tr_flags) { +// aligned(8) class TrackRunBox extends FullBox('trun', version, tr_flags) { // unsigned int(32) sample_count; // // the following are optional fields // signed int(32) data_offset; @@ -47,109 +47,148 @@ func NewTrackRunBox() *TrackRunBox { } func (trun *TrackRunBox) Size(trunFlags uint32) uint64 { - n := uint64(FullBoxLen) - n += 4 - if trunFlags&uint32(TR_FLAG_DATA_OFFSET) > 0 { - n += 4 - } - if trunFlags&uint32(TR_FLAG_DATA_FIRST_SAMPLE_FLAGS) > 0 { - n += 4 - } - if trunFlags&uint32(TR_FLAG_DATA_SAMPLE_DURATION) > 0 { - n += 4 * uint64(trun.SampleCount) - } - if trunFlags&uint32(TR_FLAG_DATA_SAMPLE_SIZE) > 0 { - n += 4 * uint64(trun.SampleCount) + size := uint64(8) // box header + size += 4 // version and flags + size += 4 // sample count + + // data offset is always present if flag is set + if trunFlags&TR_FLAG_DATA_OFFSET != 0 { + size += 4 } - if trunFlags&uint32(TR_FLAG_DATA_SAMPLE_FLAGS) > 0 { - n += 4 * uint64(trun.SampleCount) + + // first sample flags is present if flag is set + if trunFlags&TR_FLAG_DATA_FIRST_SAMPLE_FLAGS != 0 { + size += 4 } - if trunFlags&uint32(TR_FLAG_DATA_SAMPLE_COMPOSITION_TIME) > 0 { - n += 4 * uint64(trun.SampleCount) + + // calculate size for each sample entry + for i := 0; i < int(trun.SampleCount); i++ { + // sample duration is present if flag is set + if trunFlags&TR_FLAG_DATA_SAMPLE_DURATION != 0 { + size += 4 + } + // sample size is present if flag is set + if trunFlags&TR_FLAG_DATA_SAMPLE_SIZE != 0 { + size += 4 + } + // sample flags is present if flag is set + if trunFlags&TR_FLAG_DATA_SAMPLE_FLAGS != 0 { + size += 4 + } + // sample composition time offset is present if flag is set + if trunFlags&TR_FLAG_DATA_SAMPLE_COMPOSITION_TIME != 0 { + size += 4 + } } - return n + return size } -func (trun *TrackRunBox) Decode(r io.Reader, size uint32, dataOffset uint32) (offset int, err error) { +func (trun *TrackRunBox) Decode(r io.Reader, size uint32, dataOffset int32) (offset int, err error) { var fullbox FullBox if offset, err = fullbox.Decode(r); err != nil { return } buf := make([]byte, size-12) if _, err = io.ReadFull(r, buf); err != nil { - return 0, err + return } + n := 0 trun.SampleCount = binary.BigEndian.Uint32(buf[n:]) n += 4 + trunFlags := uint32(fullbox.Flags[0])<<16 | uint32(fullbox.Flags[1])<<8 | uint32(fullbox.Flags[2]) - if trunFlags&uint32(TR_FLAG_DATA_OFFSET) > 0 { + if trunFlags&TR_FLAG_DATA_OFFSET != 0 { trun.Dataoffset = int32(binary.BigEndian.Uint32(buf[n:])) n += 4 } else { - trun.Dataoffset = int32(dataOffset) + trun.Dataoffset = dataOffset } - if trunFlags&uint32(TR_FLAG_DATA_FIRST_SAMPLE_FLAGS) > 0 { + + if trunFlags&TR_FLAG_DATA_FIRST_SAMPLE_FLAGS != 0 { trun.FirstSampleFlags = binary.BigEndian.Uint32(buf[n:]) n += 4 } + trun.EntryList = make([]TrunEntry, trun.SampleCount) for i := 0; i < int(trun.SampleCount); i++ { - if trunFlags&uint32(TR_FLAG_DATA_SAMPLE_DURATION) > 0 { + if trunFlags&TR_FLAG_DATA_SAMPLE_DURATION != 0 { trun.EntryList[i].SampleDuration = binary.BigEndian.Uint32(buf[n:]) n += 4 } - if trunFlags&uint32(TR_FLAG_DATA_SAMPLE_SIZE) > 0 { + if trunFlags&TR_FLAG_DATA_SAMPLE_SIZE != 0 { trun.EntryList[i].SampleSize = binary.BigEndian.Uint32(buf[n:]) n += 4 } - if trunFlags&uint32(TR_FLAG_DATA_SAMPLE_FLAGS) > 0 { + if trunFlags&TR_FLAG_DATA_SAMPLE_FLAGS != 0 { trun.EntryList[i].SampleFlags = binary.BigEndian.Uint32(buf[n:]) n += 4 } - if trunFlags&uint32(TR_FLAG_DATA_SAMPLE_COMPOSITION_TIME) > 0 { - trun.EntryList[i].SampleCompositionTimeOffset = binary.BigEndian.Uint32(buf[n:]) + if trunFlags&TR_FLAG_DATA_SAMPLE_COMPOSITION_TIME != 0 { + if fullbox.Version == 0 { + trun.EntryList[i].SampleCompositionTimeOffset = int32(binary.BigEndian.Uint32(buf[n:])) + } else { + trun.EntryList[i].SampleCompositionTimeOffset = int32(binary.BigEndian.Uint32(buf[n:])) + } n += 4 } } + offset += n return } func (trun *TrackRunBox) Encode(trunFlags uint32) (int, []byte) { + // Always use version 1 for signed composition time offsets fullbox := NewFullBox(TypeTRUN, 1) fullbox.Box.Size = trun.Size(trunFlags) + fullbox.Flags[0] = byte(trunFlags >> 16) + fullbox.Flags[1] = byte(trunFlags >> 8) + fullbox.Flags[2] = byte(trunFlags) offset, buf := fullbox.Encode() + + // Write sample count binary.BigEndian.PutUint32(buf[offset:], trun.SampleCount) offset += 4 - if trunFlags&uint32(TR_FLAG_DATA_OFFSET) > 0 { + // Write data offset if present + if trunFlags&TR_FLAG_DATA_OFFSET != 0 { + // Write data offset as int32 binary.BigEndian.PutUint32(buf[offset:], uint32(trun.Dataoffset)) offset += 4 } - if trunFlags&uint32(TR_FLAG_DATA_FIRST_SAMPLE_FLAGS) > 0 { + + // Write first sample flags if present + if trunFlags&TR_FLAG_DATA_FIRST_SAMPLE_FLAGS != 0 { binary.BigEndian.PutUint32(buf[offset:], trun.FirstSampleFlags) offset += 4 } + // Write sample entries in the correct order for i := 0; i < int(trun.SampleCount); i++ { - if trunFlags&uint32(TR_FLAG_DATA_SAMPLE_DURATION) != 0 { + // Write sample duration if present + if trunFlags&TR_FLAG_DATA_SAMPLE_DURATION != 0 { binary.BigEndian.PutUint32(buf[offset:], trun.EntryList[i].SampleDuration) offset += 4 } - if trunFlags&uint32(TR_FLAG_DATA_SAMPLE_SIZE) != 0 { + // Write sample size if present + if trunFlags&TR_FLAG_DATA_SAMPLE_SIZE != 0 { binary.BigEndian.PutUint32(buf[offset:], trun.EntryList[i].SampleSize) offset += 4 } - if trunFlags&uint32(TR_FLAG_DATA_SAMPLE_FLAGS) != 0 { + // Write sample flags if present + if trunFlags&TR_FLAG_DATA_SAMPLE_FLAGS != 0 { binary.BigEndian.PutUint32(buf[offset:], trun.EntryList[i].SampleFlags) offset += 4 } - if trunFlags&uint32(TR_FLAG_DATA_SAMPLE_COMPOSITION_TIME) != 0 { - binary.BigEndian.PutUint32(buf[offset:], trun.EntryList[i].SampleCompositionTimeOffset) + // Write sample composition time offset if present + if trunFlags&TR_FLAG_DATA_SAMPLE_COMPOSITION_TIME != 0 { + // Version 1 uses signed int32 for composition time offset + binary.BigEndian.PutUint32(buf[offset:], uint32(trun.EntryList[i].SampleCompositionTimeOffset)) offset += 4 } } + return offset, buf } diff --git a/plugin/mp4/pkg/demuxer.go b/plugin/mp4/pkg/demuxer.go index 5ace5a3a..6badd37a 100644 --- a/plugin/mp4/pkg/demuxer.go +++ b/plugin/mp4/pkg/demuxer.go @@ -355,7 +355,7 @@ func (d *Demuxer) Demux() (err error) { d.currentTrack.StartDts = tfdt.BaseMediaDecodeTime case TypeTRUN: var trun TrackRunBox - trun.Decode(d.reader, uint32(basebox.Size), uint32(d.dataOffset)) + trun.Decode(d.reader, uint32(basebox.Size), int32(d.dataOffset)) d.decodeTRUN(&trun) case TypeSENC: var senc SencBox diff --git a/plugin/mp4/pkg/memory_file.go b/plugin/mp4/pkg/memory_file.go new file mode 100644 index 00000000..28294fb1 --- /dev/null +++ b/plugin/mp4/pkg/memory_file.go @@ -0,0 +1,81 @@ +package mp4 + +import ( + "bytes" + "io" + "os" +) + +type MemoryFile struct { + *bytes.Buffer + pos int64 +} + +func NewMemoryFile(buf *bytes.Buffer) *MemoryFile { + if buf == nil { + buf = bytes.NewBuffer(nil) + } + return &MemoryFile{Buffer: buf} +} + +func (m *MemoryFile) Seek(offset int64, whence int) (int64, error) { + var newPos int64 + switch whence { + case io.SeekStart: + newPos = offset + case io.SeekCurrent: + newPos = m.pos + offset + case io.SeekEnd: + newPos = int64(m.Buffer.Len()) + offset + default: + return 0, os.ErrInvalid + } + + if newPos < 0 { + return 0, os.ErrInvalid + } + + if newPos > int64(m.Buffer.Len()) { + // Extend buffer if seeking beyond end + m.Buffer.Write(make([]byte, newPos-int64(m.Buffer.Len()))) + } + + m.pos = newPos + return m.pos, nil +} + +func (m *MemoryFile) Read(p []byte) (n int, err error) { + if m.pos >= int64(m.Buffer.Len()) { + return 0, io.EOF + } + n = copy(p, m.Buffer.Bytes()[m.pos:]) + m.pos += int64(n) + if n < len(p) { + err = io.EOF + } + return +} + +func (m *MemoryFile) Write(p []byte) (n int, err error) { + // If writing beyond current size, extend the buffer + if m.pos > int64(m.Buffer.Len()) { + m.Buffer.Write(make([]byte, m.pos-int64(m.Buffer.Len()))) + } + + // If writing at the end, use Buffer.Write + if m.pos == int64(m.Buffer.Len()) { + n, err = m.Buffer.Write(p) + m.pos += int64(n) + return + } + + // Otherwise, copy data at the current position + n = copy(m.Buffer.Bytes()[m.pos:], p) + if n < len(p) { + // If we need more space, extend the buffer + m.Buffer.Write(p[n:]) + n = len(p) + } + m.pos += int64(n) + return +} diff --git a/plugin/mp4/pkg/muxer.go b/plugin/mp4/pkg/muxer.go index 808976b7..ad3903da 100644 --- a/plugin/mp4/pkg/muxer.go +++ b/plugin/mp4/pkg/muxer.go @@ -2,10 +2,10 @@ package mp4 import ( "encoding/binary" - "errors" "io" "os" + "m7s.live/v5/pkg" . "m7s.live/v5/plugin/mp4/pkg/box" ) @@ -30,14 +30,6 @@ type ( mdatOffset uint64 mdatSize uint64 } - FileMuxer struct { - *Muxer - *os.File - } - FMP4Muxer struct { - *Muxer - writer io.WriteSeeker - } ) func (m Muxer) isFragment() bool { @@ -52,58 +44,51 @@ func (m Muxer) has(flag Flag) bool { return (m.Flag & flag) != 0 } -func NewFileMuxer(f *os.File) (muxer *FileMuxer, err error) { - muxer = &FileMuxer{ - File: f, - Muxer: NewMuxer(0), - } - err = muxer.WriteInitSegment(f) - if err != nil { - return nil, err - } - err = muxer.WriteEmptyMdat(f) - if err != nil { - return nil, err - } - return -} - -func NewFMP4Muxer(w io.WriteSeeker) *FMP4Muxer { - muxer := &FMP4Muxer{ - writer: w, - Muxer: NewMuxer(FLAG_FRAGMENT), - } - return muxer -} - func NewMuxer(flag Flag) *Muxer { return &Muxer{ nextTrackId: 1, nextFragmentId: 1, Tracks: make(map[uint32]*Track), Flag: flag, + fragDuration: 2000, } } func (m *Muxer) WriteInitSegment(w io.Writer) (err error) { var n int - n, err = w.Write(MakeFtypBox(TypeISOM, 0x200, TypeISOM, TypeISO2, TypeAVC1, TypeMP41)) + var ftypBox []byte + if m.isFragment() { + // 对于 FMP4,使用 iso5 作为主品牌,兼容 iso5, iso6, mp41 + ftypBox = MakeFtypBox(TypeISO5, 0x200, TypeISO5, TypeISO6, TypeMP41) + } else { + // 对于普通 MP4,使用 isom 作为主品牌 + ftypBox = MakeFtypBox(TypeISOM, 0x200, TypeISOM, TypeISO2, TypeAVC1, TypeMP41) + } + n, err = w.Write(ftypBox) if err != nil { return } m.CurrentOffset = int64(n) - n, err = w.Write((new(FreeBox)).Encode()) - if err != nil { - return + if !m.isFragment() { + n, err = w.Write((new(FreeBox)).Encode()) + if err != nil { + return + } + m.CurrentOffset += int64(n) + err = m.WriteEmptyMdat(w) + if err != nil { + return + } } - m.CurrentOffset += int64(n) return } func (m *Muxer) WriteEmptyMdat(w io.Writer) (err error) { - mdat := BasicBox{Type: TypeMDAT, Size: 8} + // Write mdat box header with initial size + mdat := MediaDataBox(0) mdatlen, mdatBox := mdat.Encode() m.mdatOffset = uint64(m.CurrentOffset + 8) + m.mdatSize = 0 var n int n, err = w.Write(mdatBox[0:mdatlen]) if err != nil { @@ -113,14 +98,6 @@ func (m *Muxer) WriteEmptyMdat(w io.Writer) (err error) { return } -// func (d *Muxer) WriteInitSegment(w io.Writer) error { -// _, err := w.Write(MakeFtypBox(TypeISO5, 0x200, TypeISO5, TypeISO6, TypeMP41)) -// if err != nil { -// return err -// } -// return d.writeMoov(w) -// } - func (m *Muxer) AddTrack(cid MP4_CODEC_TYPE) *Track { track := &Track{ Cid: cid, @@ -135,85 +112,73 @@ func (m *Muxer) AddTrack(cid MP4_CODEC_TYPE) *Track { return track } -func (m *FMP4Muxer) WriteSample(t *Track, sample Sample) (err error) { - if sample.Offset, err = t.writer.Seek(0, io.SeekCurrent); err != nil { - return - } - if sample.Size, err = t.writer.Write(sample.Data); err != nil { - return - } - sample.Data = nil - t.AddSampleEntry(sample) - - // isKeyFrag := muxer.movFlag.has(MP4_FLAG_KEYFRAME) - // if isKeyFrag { - // if data.KeyFrame && track.duration > 0 { - // err = muxer.flushFragment() - // if err != nil { - // return - // } - // if muxer.onNewFragment != nil { - // muxer.onNewFragment(track.duration, track.startPts, track.startDts) - // } - // } - // } - return -} - -func (m *FileMuxer) WriteSample(t *Track, sample Sample) (err error) { - return m.Muxer.WriteSample(m.File, t, sample) -} - func (m *Muxer) WriteSample(w io.Writer, t *Track, sample Sample) (err error) { - if len(sample.Data) == 0 { - return errors.New("sample data is empty") - } - sample.Offset = m.CurrentOffset - sample.Size, err = w.Write(sample.Data) - if err != nil { - return + if m.isFragment() { + // For fragmented MP4, write to track's buffer + if sample.Offset, err = t.writer.Seek(0, io.SeekCurrent); err != nil { + return + } + if sample.Size, err = t.writer.Write(sample.Data); err != nil { + return + } + defer func() { + // For fragmented MP4, check if we should create a new fragment + if sample.KeyFrame && t.Duration >= m.fragDuration { + err = m.flushFragment(w) + } + }() + } else { + // For regular MP4, write directly to output + sample.Offset = m.CurrentOffset + sample.Size, err = w.Write(sample.Data) + if err != nil { + return + } + m.CurrentOffset += int64(sample.Size) } - m.CurrentOffset += int64(sample.Size) sample.Data = nil t.AddSampleEntry(sample) return } -func (m *FileMuxer) reWriteMdatSize() (err error) { +func (m *Muxer) reWriteMdatSize(w io.WriteSeeker) (err error) { m.mdatSize = uint64(m.CurrentOffset) - (m.mdatOffset) if m.mdatSize+BasicBoxLen > 0xFFFFFFFF { _, mdatBox := MediaDataBox(m.mdatSize).Encode() - if _, err = m.Seek(int64(m.mdatOffset-16), io.SeekStart); err != nil { + if _, err = w.Seek(int64(m.mdatOffset-16), io.SeekStart); err != nil { return } - if _, err = m.Write(mdatBox); err != nil { + if _, err = w.Write(mdatBox); err != nil { return } - if _, err = m.Seek(m.CurrentOffset, io.SeekStart); err != nil { + if _, err = w.Seek(m.CurrentOffset, io.SeekStart); err != nil { return } } else { - if _, err = m.Seek(int64(m.mdatOffset-8), io.SeekStart); err != nil { + if _, err = w.Seek(int64(m.mdatOffset-8), io.SeekStart); err != nil { return } tmpdata := make([]byte, 4) binary.BigEndian.PutUint32(tmpdata, uint32(m.mdatSize)+BasicBoxLen) - if _, err = m.Write(tmpdata); err != nil { + if _, err = w.Write(tmpdata); err != nil { return } - if _, err = m.Seek(m.CurrentOffset, io.SeekStart); err != nil { + if _, err = w.Seek(m.CurrentOffset, io.SeekStart); err != nil { return } } return } -func (m *FileMuxer) ReWriteWithMoov(f *os.File) (err error) { - _, err = m.Seek(0, io.SeekStart) +func (m *Muxer) ReWriteWithMoov(f io.WriteSeeker, r io.Reader) (err error) { + if m.isFragment() { + return pkg.ErrSkip + } + _, err = f.Seek(0, io.SeekStart) if err != nil { return } - _, err = io.CopyN(f, m, int64(m.mdatOffset)-16) + _, err = io.CopyN(f, r, int64(m.mdatOffset)-16) if err != nil { return } @@ -226,19 +191,32 @@ func (m *FileMuxer) ReWriteWithMoov(f *os.File) (err error) { if err != nil { return } - _, err = io.CopyN(f, m, int64(m.mdatSize)+16) + _, err = io.CopyN(f, r, int64(m.mdatSize)+16) return } func (m *Muxer) makeMvex() []byte { + mvex := BasicBox{Type: TypeMVEX} trexs := make([]byte, 0, 64) for i := uint32(1); i < m.nextTrackId; i++ { - trex := NewTrackExtendsBox(m.Tracks[i].TrackId) - trex.DefaultSampleDescriptionIndex = 1 - _, boxData := trex.Encode() - trexs = append(trexs, boxData...) + if track := m.Tracks[i]; track != nil { + trex := NewTrackExtendsBox(track.TrackId) + trex.DefaultSampleDescriptionIndex = 1 + trex.DefaultSampleDuration = 0 + trex.DefaultSampleSize = 0 + if track.Cid.IsVideo() { + trex.DefaultSampleFlags = 0x00010000 // NonSyncSampleFlags in mp4ff + } else { + trex.DefaultSampleFlags = 0x02000000 // SyncSampleFlags in mp4ff + } + _, boxData := trex.Encode() + trexs = append(trexs, boxData...) + } } - return trexs + mvex.Size = 8 + uint64(len(trexs)) + offset, mvexBox := mvex.Encode() + copy(mvexBox[offset:], trexs) + return mvexBox } func (m *Muxer) makeTrak(track *Track) []byte { @@ -310,166 +288,196 @@ func (m *Muxer) WriteMoov(w io.Writer) (err error) { copy(moovBox[offset:], trak) offset += len(trak) } - copy(moovBox[offset:], mvex) + if mvex != nil { + copy(moovBox[offset:], mvex) + } + + // Write moov box _, err = w.Write(moovBox) m.moov = &moov + m.CurrentOffset += int64(moov.Size) return } -func (m *FMP4Muxer) WriteTrailer() (err error) { - err = m.flushFragment() - if err != nil { - return err - } - //for _, track := range m.Tracks { - // if track.Cid.IsAudio() { - // continue - // } - //} - return m.writeMfra() -} +func (m *Muxer) WriteTrailer(file *os.File) (err error) { + if m.isFragment() { + // Flush any remaining samples + if err = m.flushFragment(file); err != nil { + return err + } -func (m *FileMuxer) WriteTrailer() (err error) { - if err = m.reWriteMdatSize(); err != nil { - return err - } - return m.WriteMoov(m.File) -} + // Write mfra box + mfraSize := 0 + tfras := make([][]byte, len(m.Tracks)) + for i := uint32(1); i < m.nextTrackId; i++ { + if track := m.Tracks[i]; track != nil && len(track.fragments) > 0 { + tfras[i-1] = track.makeTfraBox() + mfraSize += len(tfras[i-1]) + } + } -func (m *FMP4Muxer) writeMfra() (err error) { - mfraSize := 0 - tfras := make([][]byte, len(m.Tracks)) - for i := uint32(1); i < m.nextTrackId; i++ { - tfras[i-1] = m.Tracks[i].makeTfraBox() - mfraSize += len(tfras[i-1]) - } + // Only write mfra if we have fragments + if mfraSize > 0 { + mfro := MakeMfroBox(uint32(mfraSize) + 16) + mfraSize += len(mfro) + mfra := BasicBox{Type: TypeMFRA} + mfra.Size = 8 + uint64(mfraSize) + offset, mfraBox := mfra.Encode() + for _, tfra := range tfras { + if tfra == nil { + continue + } + copy(mfraBox[offset:], tfra) + offset += len(tfra) + } + copy(mfraBox[offset:], mfro) + if _, err = file.Write(mfraBox); err != nil { + return err + } + } - mfro := MakeMfroBox(uint32(mfraSize) + 16) - mfraSize += len(mfro) - mfra := BasicBox{Type: TypeMFRA} - mfra.Size = 8 + uint64(mfraSize) - offset, mfraBox := mfra.Encode() - for _, tfra := range tfras { - copy(mfraBox[offset:], tfra) - offset += len(tfra) + // Clean up any remaining buffers + for i := uint32(1); i < m.nextTrackId; i++ { + if track := m.Tracks[i]; track != nil && track.writer != nil { + if ws, ok := track.writer.(*Fmp4WriterSeeker); ok { + ws.Buffer = nil + } + } + } + } else { + if err = m.reWriteMdatSize(file); err != nil { + return err + } + return m.WriteMoov(file) } - copy(mfraBox[offset:], mfro) - _, err = m.writer.Write(mfraBox) - return + return nil } -func (m *FMP4Muxer) flushFragment() (err error) { - - if m.isFragment() { - if m.nextFragmentId == 1 { //first fragment ,write moov - _, err := m.writer.Write(MakeFtypBox(TypeISO5, 0x200, TypeISO5, TypeISO6, TypeMP41)) - if err != nil { - return err - } - m.WriteMoov(m.writer) +func (m *Muxer) flushFragment(w io.Writer) (err error) { + // Check if there are any samples to write + hasSamples := false + for i := uint32(1); i < m.nextTrackId; i++ { + if len(m.Tracks[i].Samplelist) > 0 { + hasSamples = true + break } } + if !hasSamples { + return nil + } - var moofOffset int64 - if moofOffset, err = m.writer.Seek(0, io.SeekCurrent); err != nil { - return err + // Write moov box if not written yet + if m.moov == nil { + if err = m.WriteMoov(w); err != nil { + return err + } } - var mdatlen uint64 = 0 + // Calculate mdat size first + var mdatSize uint64 = 8 // mdat box header for i := uint32(1); i < m.nextTrackId; i++ { if len(m.Tracks[i].Samplelist) == 0 { continue } - for j := 0; j < len(m.Tracks[i].Samplelist); j++ { - m.Tracks[i].Samplelist[j].Offset += int64(mdatlen) - } ws := m.Tracks[i].writer.(*Fmp4WriterSeeker) - mdatlen += uint64(len(ws.Buffer)) + mdatSize += uint64(len(ws.Buffer)) } - mdatlen += 8 - moofSize := 0 + // Write moof box mfhd := MakeMfhdBox(m.nextFragmentId) - - moofSize += len(mfhd) trafs := make([][]byte, len(m.Tracks)) + moofSize := len(mfhd) + trunOffsets := make([]int, len(m.Tracks)) // track index -> trun data_offset position in moof box + var boxOffset int = 8 + len(mfhd) // 8 for moof header for i := uint32(1); i < m.nextTrackId; i++ { - traf := m.Tracks[i].makeTraf(moofOffset, 0) - moofSize += len(traf) - trafs[i-1] = traf - } - - moofSize += 8 //moof box - mfhd = MakeMfhdBox(m.nextFragmentId) - trafs = make([][]byte, len(m.Tracks)) - for i := uint32(1); i < m.nextTrackId; i++ { - traf := m.Tracks[i].makeTraf(moofOffset, int64(moofSize)+8) //moofSize + 8(mdat box) + if len(m.Tracks[i].Samplelist) == 0 { + continue + } + track := m.Tracks[i] + // 传递 moof 偏移和 mdat 大小 + traf := track.makeTraf(&trunOffsets[int(i-1)]) // +8 for moof box header + // Record trun data_offset position: current offset + 16 (after trun header) trafs[i-1] = traf + trunOffsets[int(i-1)] += boxOffset + boxOffset += len(traf) + moofSize += len(traf) } - m.nextFragmentId++ + // Write moof box moof := BasicBox{Type: TypeMOOF} - moof.Size = uint64(moofSize) + moof.Size = uint64(moofSize + 8) // Add 8 for moof box header offset, moofBox := moof.Encode() copy(moofBox[offset:], mfhd) offset += len(mfhd) - for i := range trafs { - copy(moofBox[offset:], trafs[i]) - offset += len(trafs[i]) - } - - mdat := BasicBox{Type: TypeMDAT} - mdat.Size = 8 - _, mdatBox := mdat.Encode() - - if m.isDash() { - _, err := m.writer.Write(MakeStypBox(TypeMSDH, 0, TypeMSDH, TypeMSIX)) - if err != nil { - return err - } - - for i := uint32(1); i < m.nextTrackId; i++ { - sidx := m.Tracks[i].makeSidxBox(52*(m.nextTrackId-1-i), uint32(mdatlen)+uint32(len(moofBox))+52*(m.nextTrackId-i-1)) - _, err := m.writer.Write(sidx) - if err != nil { - return err - } + for i, traf := range trafs { + if traf == nil { + continue } + copy(moofBox[offset:], traf) + // Update trun data_offset + binary.BigEndian.PutUint32(moofBox[trunOffsets[i]:trunOffsets[i]+4], uint32(moof.Size)+8) // +8 for mdat header + offset += len(traf) } - _, err = m.writer.Write(moofBox) - if err != nil { + if _, err = w.Write(moofBox); err != nil { return err } - binary.BigEndian.PutUint32(mdatBox, uint32(mdatlen)) - _, err = m.writer.Write(mdatBox) - if err != nil { + + // Write mdat box + mdat := BasicBox{Type: TypeMDAT} + mdat.Size = mdatSize + offset, mdatBox := mdat.Encode() + if _, err = w.Write(mdatBox[:offset]); err != nil { return err } + // Write sample data + var sampleOffset int64 = 0 for i := uint32(1); i < m.nextTrackId; i++ { - if len(m.Tracks[i].Samplelist) > 0 { - firstPts := m.Tracks[i].Samplelist[0].PTS - firstDts := m.Tracks[i].Samplelist[0].DTS - lastPts := m.Tracks[i].Samplelist[len(m.Tracks[i].Samplelist)-1].PTS - lastDts := m.Tracks[i].Samplelist[len(m.Tracks[i].Samplelist)-1].DTS + if len(m.Tracks[i].Samplelist) == 0 { + continue + } + track := m.Tracks[i] + ws := track.writer.(*Fmp4WriterSeeker) + + // Update sample offsets relative to mdat start + for j := range track.Samplelist { + track.Samplelist[j].Offset = sampleOffset + sampleOffset += int64(track.Samplelist[j].Size) + } + + if _, err = w.Write(ws.Buffer); err != nil { + return err + } + + // Record fragment info + if len(track.Samplelist) > 0 { + firstPts := track.Samplelist[0].PTS + firstDts := track.Samplelist[0].DTS + lastPts := track.Samplelist[len(track.Samplelist)-1].PTS + lastDts := track.Samplelist[len(track.Samplelist)-1].DTS frag := Fragment{ - Offset: uint64(moofOffset), - Duration: m.Tracks[i].Duration, + Offset: uint64(m.CurrentOffset), + Duration: track.Duration, FirstDts: firstDts, FirstPts: firstPts, LastPts: lastPts, LastDts: lastDts, } - m.Tracks[i].fragments = append(m.Tracks[i].fragments, frag) - } - ws := m.Tracks[i].writer.(*Fmp4WriterSeeker) - _, err = m.writer.Write(ws.Buffer) - if err != nil { - return err + track.fragments = append(track.fragments, frag) } + + // Clear track buffers ws.Buffer = ws.Buffer[:0] ws.Offset = 0 - m.Tracks[i].Samplelist = m.Tracks[i].Samplelist[:0] + track.Samplelist = track.Samplelist[:0] + track.Duration = 0 } + m.CurrentOffset += int64(moof.Size) + int64(mdatSize) + m.nextFragmentId++ return nil } + +// SetFragmentDuration sets the target duration for each fragment in milliseconds +func (m *Muxer) SetFragmentDuration(duration uint32) { + m.fragDuration = duration +} diff --git a/plugin/mp4/pkg/muxer_test.go b/plugin/mp4/pkg/muxer_test.go new file mode 100644 index 00000000..14ef3df6 --- /dev/null +++ b/plugin/mp4/pkg/muxer_test.go @@ -0,0 +1,370 @@ +package mp4 + +import ( + "encoding/binary" + "fmt" + "io" + "os" + "os/exec" + "testing" + + "m7s.live/v5/plugin/mp4/pkg/box" +) + +type ( + FLVHeader struct { + Signature [3]byte + Version uint8 + Flags uint8 + DataOffset uint32 + } + + FLVTag struct { + TagType uint8 + DataSize uint32 + Timestamp uint32 + StreamID uint32 + Data []byte + } +) + +// validateAndFixAVCC 验证并修复 AVCC 格式的 NALU +func validateAndFixAVCC(data []byte) ([]byte, error) { + if len(data) < 4 { + return nil, fmt.Errorf("data too short for AVCC") + } + + var pos int + var output []byte + + for pos < len(data) { + if pos+4 > len(data) { + return nil, fmt.Errorf("incomplete NALU length at position %d", pos) + } + + // 读取 NALU 长度(4字节,大端序) + naluLen := binary.BigEndian.Uint32(data[pos : pos+4]) + + // 验证 NALU 长度 + if naluLen == 0 || pos+4+int(naluLen) > len(data) { + return nil, fmt.Errorf("invalid NALU length %d at position %d", naluLen, pos) + } + + // 验证 NALU 类型 + naluType := data[pos+4] & 0x1F + if naluType == 0 || naluType > 12 { + return nil, fmt.Errorf("invalid NALU type %d at position %d", naluType, pos) + } + + // 复制长度前缀和 NALU 数据 + output = append(output, data[pos:pos+4+int(naluLen)]...) + pos += 4 + int(naluLen) + } + + return output, nil +} + +func readFLVHeader(r io.Reader) (*FLVHeader, error) { + header := &FLVHeader{} + if err := binary.Read(r, binary.BigEndian, &header.Signature); err != nil { + return nil, fmt.Errorf("error reading signature: %v", err) + } + if err := binary.Read(r, binary.BigEndian, &header.Version); err != nil { + return nil, fmt.Errorf("error reading version: %v", err) + } + if err := binary.Read(r, binary.BigEndian, &header.Flags); err != nil { + return nil, fmt.Errorf("error reading flags: %v", err) + } + if err := binary.Read(r, binary.BigEndian, &header.DataOffset); err != nil { + return nil, fmt.Errorf("error reading data offset: %v", err) + } + + // Validate FLV signature + if string(header.Signature[:]) != "FLV" { + return nil, fmt.Errorf("invalid FLV signature: %s", string(header.Signature[:])) + } + + fmt.Printf("FLV Header: Version=%d, Flags=%d, DataOffset=%d\n", header.Version, header.Flags, header.DataOffset) + return header, nil +} + +func readFLVTag(r io.Reader) (*FLVTag, error) { + tag := &FLVTag{} + + // Read previous tag size (4 bytes) + var prevTagSize uint32 + if err := binary.Read(r, binary.BigEndian, &prevTagSize); err != nil { + return nil, err + } + fmt.Printf("Previous tag size: %d\n", prevTagSize) + + // Read tag type (1 byte) + if err := binary.Read(r, binary.BigEndian, &tag.TagType); err != nil { + return nil, err + } + fmt.Printf("Tag type: %d\n", tag.TagType) + + // Read data size (3 bytes) + var dataSize [3]byte + if _, err := io.ReadFull(r, dataSize[:]); err != nil { + return nil, err + } + tag.DataSize = uint32(dataSize[0])<<16 | uint32(dataSize[1])<<8 | uint32(dataSize[2]) + fmt.Printf("Data size: %d\n", tag.DataSize) + + // Read timestamp (3 bytes + 1 byte extended) + var timestamp [3]byte + if _, err := io.ReadFull(r, timestamp[:]); err != nil { + return nil, err + } + var timestampExtended uint8 + if err := binary.Read(r, binary.BigEndian, ×tampExtended); err != nil { + return nil, err + } + tag.Timestamp = uint32(timestamp[0])<<16 | uint32(timestamp[1])<<8 | uint32(timestamp[2]) | uint32(timestampExtended)<<24 + fmt.Printf("Timestamp: %d\n", tag.Timestamp) + + // Read stream ID (3 bytes) + var streamID [3]byte + if _, err := io.ReadFull(r, streamID[:]); err != nil { + return nil, err + } + tag.StreamID = uint32(streamID[0])<<16 | uint32(streamID[1])<<8 | uint32(streamID[2]) + fmt.Printf("Stream ID: %d\n", tag.StreamID) + + // Read tag data + tag.Data = make([]byte, tag.DataSize) + if _, err := io.ReadFull(r, tag.Data); err != nil { + return nil, err + } + fmt.Printf("Read tag data of size %d\n", len(tag.Data)) + + return tag, nil +} + +func findBoxOffsets(filename string) error { + file, err := os.Open(filename) + if err != nil { + return err + } + defer file.Close() + + type boxInfo struct { + name string + offset int64 + size uint32 + } + + var boxes []boxInfo + + // Read the entire file + data, err := io.ReadAll(file) + if err != nil { + return err + } + + // Search for boxes + var i int + for i < len(data)-8 { + // Read box size (4 bytes) and type (4 bytes) + size := binary.BigEndian.Uint32(data[i : i+4]) + boxType := string(data[i+4 : i+8]) + + // Validate box size + if size < 8 || int64(size) > int64(len(data))-int64(i) { + i++ + continue + } + + if boxType == "tfdt" || boxType == "mdat" || boxType == "moof" || boxType == "traf" { + boxes = append(boxes, boxInfo{ + name: boxType, + offset: int64(i), + size: size, + }) + // Print the entire box content for small boxes + if size <= 256 { + fmt.Printf("\nFull %s box at offset %d (0x%x):\n", boxType, i, i) + // Print box header + fmt.Printf("Header: % x\n", data[i:i+8]) + // Print content in chunks of 32 bytes + for j := i + 8; j < i+int(size); j += 32 { + end := j + 32 + if end > i+int(size) { + end = i + int(size) + } + fmt.Printf("Content [%d-%d]: % x\n", j-i, end-i, data[j:end]) + } + } + } + // Move to the next box + i += int(size) + } + + // Print box information in order of appearance + fmt.Println("\nBox layout:") + for _, box := range boxes { + fmt.Printf("%s box at offset %d (0x%x), size: %d bytes\n", + box.name, box.offset, box.offset, box.size) + + // Print the first few bytes of the box content + start := box.offset + 8 // skip size and type + end := start + 32 + if end > box.offset+int64(box.size) { + end = box.offset + int64(box.size) + } + fmt.Printf("%s content: % x\n", box.name, data[start:end]) + + // For tfdt box, also print the previous and next 8 bytes + if box.name == "tfdt" { + prevStart := box.offset - 8 + if prevStart < 0 { + prevStart = 0 + } + nextEnd := box.offset + int64(box.size) + 8 + if nextEnd > int64(len(data)) { + nextEnd = int64(len(data)) + } + fmt.Printf("Context around tfdt:\n") + fmt.Printf("Previous 8 bytes: % x\n", data[prevStart:box.offset]) + fmt.Printf("Next 8 bytes: % x\n", data[box.offset+int64(box.size):nextEnd]) + } + } + return nil +} + +func TestFLVToFMP4(t *testing.T) { + // Open FLV file + flvFile, err := os.Open("/Users/dexter/Movies/frame_counter_4k_60fps.flv") + if err != nil { + t.Fatalf("Failed to open FLV file: %v", err) + } + defer flvFile.Close() + + // Create output FMP4 file + outFile, err := os.Create("test.mp4") + if err != nil { + t.Fatalf("Failed to create output file: %v", err) + } + defer outFile.Close() + + // Create FMP4 muxer + muxer := NewMuxer(FLAG_FRAGMENT) + muxer.WriteInitSegment(outFile) + // Read FLV header + header, err := readFLVHeader(flvFile) + if err != nil { + t.Fatalf("Failed to read FLV header: %v", err) + } + + hasVideo := header.Flags&0x01 != 0 + + // Skip to the first tag + if _, err := flvFile.Seek(int64(header.DataOffset), io.SeekStart); err != nil { + t.Fatalf("Failed to seek to first tag: %v", err) + } + + // Create tracks + var videoTrack *Track + if hasVideo { + videoTrack = muxer.AddTrack(box.MP4_CODEC_H264) + videoTrack.Width = 3840 // 4K resolution + videoTrack.Height = 2160 + videoTrack.Timescale = 1000 + } + + // Variables to store codec configuration + var videoConfig []byte + var frameCount int + + // Process FLV tags +TagLoop: + for { + tag, err := readFLVTag(flvFile) + if err != nil { + if err == io.EOF { + break + } + t.Fatalf("Failed to read FLV tag: %v", err) + } + + switch tag.TagType { + case 9: // Video + if !hasVideo || videoTrack == nil { + continue + } + + codecID := tag.Data[0] & 0x0f + frameType := tag.Data[0] >> 4 + if codecID == 7 { // AVC/H.264 + if tag.Data[1] == 0 { // AVC sequence header + fmt.Println("Found AVC sequence header") + videoConfig = tag.Data[5:] // Store AVC config (skip composition time) + videoTrack.ExtraData = videoConfig + } else if len(videoConfig) > 0 { // Video data + if len(tag.Data) <= 5 { + fmt.Printf("Skipping empty video sample at timestamp %d\n", tag.Timestamp) + continue + } + + // Read composition time offset (24 bits, signed) + compositionTime := int32(tag.Data[2])<<16 | int32(tag.Data[3])<<8 | int32(tag.Data[4]) + // Convert 24-bit signed integer to 32-bit signed integer + if compositionTime&0x800000 != 0 { + compositionTime |= ^0xffffff + } + + // 验证和修复 AVCC 格式 + validData, err := validateAndFixAVCC(tag.Data[5:]) + if err != nil { + fmt.Printf("Warning: Invalid AVCC data at timestamp %d: %v\n", tag.Timestamp, err) + continue + } + + sample := box.Sample{ + Data: validData, + DTS: uint64(tag.Timestamp), + PTS: uint64(int64(tag.Timestamp) + int64(compositionTime)), + KeyFrame: frameType == 1, + } + if err := muxer.WriteSample(outFile, videoTrack, sample); err != nil { + t.Fatalf("Failed to write video sample: %v", err) + } + frameCount++ + + if frameCount >= 5 { + fmt.Println("Wrote 5 frames, stopping") + break TagLoop + } + } + } + } + } + + // Create sample table boxes before writing trailer + if videoTrack != nil { + videoTrack.makeStblBox() + } + + // Write trailer + if err := muxer.WriteTrailer(outFile); err != nil { + t.Fatalf("Failed to write trailer: %v", err) + } + + fmt.Println("Conversion completed successfully") + + // Find and analyze box positions + if err := findBoxOffsets("test.mp4"); err != nil { + t.Fatalf("Failed to analyze boxes: %v", err) + } + + // Validate the generated MP4 file using MP4Box + cmd := exec.Command("MP4Box", "-info", "test.mp4") + output, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("MP4Box validation failed: %v\nOutput: %s", err, output) + } + fmt.Printf("MP4Box validation output:\n%s\n", output) + + t.Log("Test completed successfully") +} diff --git a/plugin/mp4/pkg/record.go b/plugin/mp4/pkg/record.go index abb00aee..0bfec439 100644 --- a/plugin/mp4/pkg/record.go +++ b/plugin/mp4/pkg/record.go @@ -11,6 +11,7 @@ import ( m7s "m7s.live/v5" "m7s.live/v5/pkg" "m7s.live/v5/pkg/codec" + "m7s.live/v5/pkg/config" "m7s.live/v5/pkg/task" "m7s.live/v5/plugin/mp4/pkg/box" rtmp "m7s.live/v5/plugin/rtmp/pkg" @@ -24,52 +25,58 @@ var writeTrailerQueueTask WriteTrailerQueueTask type writeTrailerTask struct { task.Task - muxer *FileMuxer + muxer *Muxer + file *os.File } func (task *writeTrailerTask) Start() (err error) { - err = task.muxer.WriteTrailer() + err = task.muxer.WriteTrailer(task.file) if err != nil { task.Error("write trailer", "err", err) - if errClose := task.muxer.File.Close(); errClose != nil { - return errClose + if task.file != nil { + if errClose := task.file.Close(); errClose != nil { + return errClose + } } } return } -func (task *writeTrailerTask) Run() (err error) { - task.Info("write trailer") +func (t *writeTrailerTask) Run() (err error) { + t.Info("write trailer") var temp *os.File temp, err = os.CreateTemp("", "*.mp4") if err != nil { - task.Error("create temp file", "err", err) + t.Error("create temp file", "err", err) return } defer os.Remove(temp.Name()) - err = task.muxer.ReWriteWithMoov(temp) + err = t.muxer.ReWriteWithMoov(temp, t.file) if err != nil { - task.Error("rewrite with moov", "err", err) + if err == pkg.ErrSkip { + return task.ErrTaskComplete + } + t.Error("rewrite with moov", "err", err) return } - if _, err = task.muxer.File.Seek(0, io.SeekStart); err != nil { - task.Error("seek file", "err", err) + 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 { - task.Error("seek temp file", "err", err) + t.Error("seek temp file", "err", err) return } - if _, err = io.Copy(task.muxer.File, temp); err != nil { - task.Error("copy file", "err", err) + if _, err = io.Copy(t.file, temp); err != nil { + t.Error("copy file", "err", err) return } - if err = task.muxer.File.Close(); err != nil { - task.Error("close file", "err", err) + if err = t.file.Close(); err != nil { + t.Error("close file", "err", err) return } if err = temp.Close(); err != nil { - task.Error("close temp file", "err", err) + t.Error("close temp file", "err", err) } return } @@ -112,13 +119,14 @@ func init() { m7s.Servers.AddTask(&writeTrailerQueueTask) } -func NewRecorder() m7s.IRecorder { +func NewRecorder(conf config.Record) m7s.IRecorder { return &Recorder{} } type Recorder struct { m7s.DefaultRecorder - muxer *FileMuxer + muxer *Muxer + file *os.File stream m7s.RecordStream } @@ -133,20 +141,20 @@ func (r *Recorder) writeTailer(end time.Time) { } writeTrailerQueueTask.AddTask(&writeTrailerTask{ muxer: r.muxer, + file: r.file, }, r.Logger) } var CustomFileName = func(job *m7s.RecordJob) string { - if job.Fragment == 0 { - return fmt.Sprintf("%s.mp4", job.FilePath) + if job.RecConf.Fragment == 0 { + return fmt.Sprintf("%s.mp4", job.RecConf.FilePath) } - return filepath.Join(job.FilePath, fmt.Sprintf("%d.mp4", time.Now().Unix())) + return filepath.Join(job.RecConf.FilePath, fmt.Sprintf("%d.mp4", time.Now().Unix())) } func (r *Recorder) createStream(start time.Time) (err error) { recordJob := &r.RecordJob sub := recordJob.Subscriber - var file *os.File r.stream = m7s.RecordStream{ StartTime: start, StreamPath: sub.StreamPath, @@ -164,10 +172,17 @@ func (r *Recorder) createStream(start time.Time) (err error) { if err = os.MkdirAll(dir, 0755); err != nil { return } - if file, err = os.Create(r.stream.FilePath); err != nil { + r.file, err = os.Create(r.stream.FilePath) + if err != nil { return } - r.muxer, err = NewFileMuxer(file) + if recordJob.RecConf.Type == "fmp4" { + r.stream.Type = "fmp4" + r.muxer = NewMuxer(FLAG_FRAGMENT) + } else { + r.muxer = NewMuxer(0) + } + r.muxer.WriteInitSegment(r.file) if sub.Publisher.HasAudioTrack() { r.stream.AudioCodec = sub.Publisher.AudioTrack.ICodecCtx.FourCC().String() } @@ -210,7 +225,7 @@ func (r *Recorder) Run() (err error) { } checkFragment := func(absTime uint32) (err error) { - if duration := int64(absTime); time.Duration(duration)*time.Millisecond >= recordJob.Fragment { + if duration := int64(absTime); time.Duration(duration)*time.Millisecond >= recordJob.RecConf.Fragment { now := time.Now() r.writeTailer(now) err = r.createStream(now) @@ -238,7 +253,7 @@ func (r *Recorder) Run() (err error) { return err } } - if recordJob.Fragment != 0 { + if recordJob.RecConf.Fragment != 0 { err := checkFragment(sub.AudioReader.AbsTime) if err != nil { return err @@ -267,7 +282,7 @@ func (r *Recorder) Run() (err error) { } } dts := sub.AudioReader.AbsTime - return r.muxer.WriteSample(audioTrack, box.Sample{ + return r.muxer.WriteSample(r.file, audioTrack, box.Sample{ Data: audio.ToBytes(), PTS: uint64(dts), DTS: uint64(dts), @@ -280,7 +295,7 @@ func (r *Recorder) Run() (err error) { return err } } - if recordJob.Fragment != 0 { + if recordJob.RecConf.Fragment != 0 { err := checkFragment(sub.VideoReader.AbsTime) if err != nil { return err @@ -348,7 +363,7 @@ func (r *Recorder) Run() (err error) { return nil } } - return r.muxer.WriteSample(videoTrack, box.Sample{ + return r.muxer.WriteSample(r.file, videoTrack, box.Sample{ KeyFrame: sub.VideoReader.Value.IDR, Data: bytes[offset:], PTS: uint64(sub.VideoReader.AbsTime) + uint64(video.CTS), diff --git a/plugin/mp4/pkg/track.go b/plugin/mp4/pkg/track.go index 2f156bfc..afeda004 100644 --- a/plugin/mp4/pkg/track.go +++ b/plugin/mp4/pkg/track.go @@ -1,6 +1,7 @@ package mp4 import ( + "fmt" "io" . "m7s.live/v5/plugin/mp4/pkg/box" @@ -11,27 +12,29 @@ type ( Cid MP4_CODEC_TYPE TrackId uint32 SampleTable - Duration uint32 - Height uint32 - Width uint32 - SampleRate uint32 - SampleSize uint16 - SampleCount uint32 - ChannelCount uint8 - Timescale uint32 - StartDts uint64 - EndDts uint64 - StartPts uint64 - EndPts uint64 - Samplelist []Sample - ELST *EditListBox - ExtraData []byte - writer io.WriteSeeker - fragments []Fragment - defaultSize uint32 - defaultDuration uint32 - defaultSampleFlags uint32 - baseDataOffset uint64 + Duration uint32 + Height uint32 + Width uint32 + SampleRate uint32 + SampleSize uint16 + SampleCount uint32 + ChannelCount uint8 + Timescale uint32 + StartDts uint64 + EndDts uint64 + StartPts uint64 + EndPts uint64 + Samplelist []Sample + ELST *EditListBox + ExtraData []byte + writer io.WriteSeeker + fragments []Fragment + defaultSize uint32 + defaultDuration uint32 + defaultSampleFlags uint32 + baseDataOffset uint64 + stbl []byte + FragmentSequenceNumber uint32 //for subsample defaultIsProtected uint8 @@ -255,79 +258,182 @@ func (track *Track) makeStsd(handler_type HandlerType) []byte { } // fmp4 -func (track *Track) makeTraf(moofOffset int64, moofSize int64) []byte { - tfhd := track.makeTfhdBox(uint64(moofOffset)) - tfdt := track.makeTfdtBox() - trun := track.makeTrunBoxes(moofSize) +func (track *Track) makeTraf(dataOffsetOffset *int) []byte { + // Create tfhd box + tfFlags := uint32(0) + tfFlags |= TF_FLAG_DEFAULT_BASE_IS_MOOF + tfFlags |= TF_FLAG_SAMPLE_DESCRIPTION_INDEX_PRESENT + tfFlags |= TF_FLAG_DEFAULT_SAMPLE_DURATION_PRESENT + tfFlags |= TF_FLAG_DEFAULT_SAMPLE_SIZE_PRESENT + tfFlags |= TF_FLAG_DEFAULT_SAMPLE_FLAGS_PRESENT - traf := BasicBox{Type: TypeTRAF, Size: 8 + uint64(len(tfhd)+len(tfdt)+len(trun))} + tfhd := NewTrackFragmentHeaderBox(track.TrackId) + tfhd.SampleDescriptionIndex = 1 + + // Calculate default sample duration + if len(track.Samplelist) > 1 { + var totalDuration uint64 = 0 + var count int = 0 + for i := 1; i < len(track.Samplelist); i++ { + duration := track.Samplelist[i].DTS - track.Samplelist[i-1].DTS + if duration > 0 { + totalDuration += duration + count++ + } + } + if count > 0 { + tfhd.DefaultSampleDuration = uint32(totalDuration / uint64(count)) + } + } + + // Set default sample size + if len(track.Samplelist) > 0 { + tfhd.DefaultSampleSize = uint32(track.Samplelist[0].Size) + } + + // Set default sample flags + if track.Cid.IsVideo() { + tfhd.DefaultSampleFlags = MOV_FRAG_SAMPLE_FLAG_DEPENDS_YES | MOV_FRAG_SAMPLE_FLAG_IS_NON_SYNC + } else { + tfhd.DefaultSampleFlags = MOV_FRAG_SAMPLE_FLAG_DEPENDS_NO + } + + // Create tfdt box + tfdt := NewTrackFragmentBaseMediaDecodeTimeBox(uint64(track.Samplelist[0].DTS)) + + // Calculate traf size first (including header) + _, tfhdbox := tfhd.Encode(tfFlags) + _, tfdtbox := tfdt.Encode() + trafSize := uint64(8) + uint64(len(tfhdbox)) + uint64(len(tfdtbox)) + // Create trun box with total moof size + // moof = header(8) + mfhd(16) + traf + // traf = header(8) + tfhd + tfdt + trun + // So the data offset should be relative to moof start + trun := track.makeTrunBox(0, len(track.Samplelist)) + + // Update traf size with trun size + trafSize += uint64(len(trun)) + + // Create traf box + traf := BasicBox{Type: TypeTRAF, Size: trafSize} offset, boxData := traf.Encode() - copy(boxData[offset:], tfhd) - offset += len(tfhd) - copy(boxData[offset:], tfdt) - offset += len(tfdt) + copy(boxData[offset:], tfhdbox) + offset += len(tfhdbox) + copy(boxData[offset:], tfdtbox) + offset += len(tfdtbox) copy(boxData[offset:], trun) + *dataOffsetOffset = offset + 12 + 4 // 12 for trun header, 4 for trun sample count offset += len(trun) + + if offset != int(trafSize) { + panic("traf box size mismatch") + } + return boxData } func (track *Track) makeTfhdBox(offset uint64) []byte { - tfFlags := TF_FLAG_SAMPLE_DESCRIPTION_INDEX_PRESENT - tfFlags |= TF_FLAG_DEAAULT_BASE_IS_MOOF - tfhd := NewTrackFragmentHeaderBox(track.TrackId) - tfhd.BaseDataOffset = offset - if len(track.Samplelist) > 1 { - tfhd.DefaultSampleDuration = uint32(track.Samplelist[1].DTS - track.Samplelist[0].DTS) - } else if len(track.Samplelist) == 1 && len(track.fragments) > 0 { - tfhd.DefaultSampleDuration = uint32(track.Samplelist[0].DTS - track.fragments[len(track.fragments)-1].LastDts) - } else { - tfhd.DefaultSampleDuration = 0 - tfFlags |= TF_FLAG_DURATION_IS_EMPTY + // Set flags in the correct order + tfFlags := uint32(0) + + // Set base is moof flag (0x020000) + tfFlags |= TF_FLAG_DEFAULT_BASE_IS_MOOF + + // Then set sample description index flag (0x000002) + tfFlags |= TF_FLAG_SAMPLE_DESCRIPTION_INDEX_PRESENT + + // Then set default sample duration flag (0x000008) + if len(track.Samplelist) > 0 { + // Calculate average duration + var totalDuration uint64 = 0 + var count int = 0 + for i := 1; i < len(track.Samplelist); i++ { + duration := track.Samplelist[i].DTS - track.Samplelist[i-1].DTS + if duration > 0 { + totalDuration += duration + count++ + } + } + + if count > 0 { + tfFlags |= TF_FLAG_DEFAULT_SAMPLE_DURATION_PRESENT + } else if len(track.fragments) > 0 { + lastFrag := track.fragments[len(track.fragments)-1] + if lastFrag.Duration > 0 { + tfFlags |= TF_FLAG_DEFAULT_SAMPLE_DURATION_PRESENT + } + } } + + // Then set default sample size flag (0x000010) if len(track.Samplelist) > 0 { - tfFlags |= TF_FLAG_DEAAULT_SAMPLE_FLAGS_PRESENT - tfFlags |= TF_FLAG_DEFAULT_SAMPLE_DURATION_PRESENT tfFlags |= TF_FLAG_DEFAULT_SAMPLE_SIZE_PRESENT + } + + // Then set default sample flags flag (0x000020) + tfFlags |= TF_FLAG_DEFAULT_SAMPLE_FLAGS_PRESENT + + // Create tfhd box + tfhd := NewTrackFragmentHeaderBox(track.TrackId) + + // Set default values based on flags + if tfFlags&TF_FLAG_DEFAULT_SAMPLE_DURATION_PRESENT != 0 { + if len(track.Samplelist) > 1 { + var totalDuration uint64 = 0 + var count int = 0 + for i := 1; i < len(track.Samplelist); i++ { + duration := track.Samplelist[i].DTS - track.Samplelist[i-1].DTS + if duration > 0 { + totalDuration += duration + count++ + } + } + if count > 0 { + tfhd.DefaultSampleDuration = uint32(totalDuration / uint64(count)) + } + } else if len(track.fragments) > 0 { + lastFrag := track.fragments[len(track.fragments)-1] + if lastFrag.Duration > 0 { + tfhd.DefaultSampleDuration = lastFrag.Duration + } + } + } + + if tfFlags&TF_FLAG_DEFAULT_SAMPLE_SIZE_PRESENT != 0 { tfhd.DefaultSampleSize = uint32(track.Samplelist[0].Size) - } else { - tfhd.DefaultSampleSize = 0 } - //ffmpeg movenc.c mov_write_tfhd_tag - if track.Cid.IsVideo() { - tfhd.DefaultSampleFlags = MOV_FRAG_SAMPLE_FLAG_DEPENDS_YES | MOV_FRAG_SAMPLE_FLAG_IS_NON_SYNC - } else { - tfhd.DefaultSampleFlags = MOV_FRAG_SAMPLE_FLAG_DEPENDS_NO + + if tfFlags&TF_FLAG_DEFAULT_SAMPLE_FLAGS_PRESENT != 0 { + if track.Cid.IsVideo() { + tfhd.DefaultSampleFlags = MOV_FRAG_SAMPLE_FLAG_DEPENDS_YES | MOV_FRAG_SAMPLE_FLAG_IS_NON_SYNC + } else { + tfhd.DefaultSampleFlags = MOV_FRAG_SAMPLE_FLAG_DEPENDS_NO + } } + + // Store default values for later use track.defaultDuration = tfhd.DefaultSampleDuration track.defaultSize = tfhd.DefaultSampleSize track.defaultSampleFlags = tfhd.DefaultSampleFlags + + // Print tfhd box details + fmt.Printf("tfhd box: flags=0x%08x, track_id=%d\n", tfFlags, track.TrackId) + fmt.Printf("tfhd box: default_sample_duration=%d, default_sample_size=%d, default_sample_flags=0x%08x\n", + tfhd.DefaultSampleDuration, tfhd.DefaultSampleSize, tfhd.DefaultSampleFlags) + _, boxData := tfhd.Encode(tfFlags) + fmt.Printf("tfhd box first 32 bytes: % 02X\n", boxData[:32]) return boxData } func (track *Track) makeTfdtBox() []byte { tfdt := NewTrackFragmentBaseMediaDecodeTimeBox(uint64(track.Samplelist[0].DTS)) - _, boxData := tfdt.Encode() - return boxData -} - -func (track *Track) makeTrunBoxes(moofSize int64) []byte { - boxes := make([]byte, 0, 128) - start := 0 - end := 0 - for i := 1; i < len(track.Samplelist); i++ { - if track.Samplelist[i].Offset == track.Samplelist[i-1].Offset+int64(track.Samplelist[i-1].Size) { - continue - } - end = i - boxes = append(boxes, track.makeTrunBox(start, end, moofSize)...) - start = end + offset, boxData := tfdt.Encode() + expectedSize := tfdt.Size() + if uint64(offset) != expectedSize { + panic("tfdt box size is wrong") } - - if start < len(track.Samplelist) { - boxes = append(boxes, track.makeTrunBox(start, len(track.Samplelist), moofSize)...) - } - return boxes + return boxData[:offset] } func (track *Track) makeStssBox() (boxdata []byte) { @@ -356,51 +462,68 @@ func (track *Track) makeTfraBox() []byte { return tfraData } -func (track *Track) makeTrunBox(start, end int, moofSize int64) []byte { +func (track *Track) makeTrunBox(start, end int) []byte { + // Create a new TrackRunBox + trun := NewTrackRunBox() + trun.SampleCount = uint32(end - start) + + // Set flags in the correct order flag := TR_FLAG_DATA_OFFSET - if track.Cid.IsVideo() && track.Samplelist[start].KeyFrame { - flag |= TR_FLAG_DATA_FIRST_SAMPLE_FLAGS + + // Then set sample duration flag (0x000100) + flag |= TR_FLAG_DATA_SAMPLE_DURATION + + // Then set sample size flag (0x000200) + flag |= TR_FLAG_DATA_SAMPLE_SIZE + + // Then set sample flags if needed (0x000400) + if track.Cid.IsVideo() { + flag |= TR_FLAG_DATA_SAMPLE_FLAGS } - for j := start; j < end; j++ { - if track.Samplelist[j].Size != int(track.defaultSize) { - flag |= TR_FLAG_DATA_SAMPLE_SIZE - } - if j+1 < end { - if track.Samplelist[j+1].DTS-track.Samplelist[j].DTS != uint64(track.defaultDuration) { - flag |= TR_FLAG_DATA_SAMPLE_DURATION - } - } else { - // if track.lastSample.DTS-track.Samplelist[j].DTS != uint64(track.defaultDuration) { - // flag |= TR_FLAG_DATA_SAMPLE_DURATION - // } - } - if track.Samplelist[j].PTS != track.Samplelist[j].DTS { + // Finally set composition time offset flag if needed (0x000800) + for i := start; i < end; i++ { + if track.Samplelist[i].PTS != track.Samplelist[i].DTS { flag |= TR_FLAG_DATA_SAMPLE_COMPOSITION_TIME + break } } - trun := NewTrackRunBox() - trun.SampleCount = uint32(end - start) - - trun.Dataoffset = int32(moofSize + track.Samplelist[start].Offset) - trun.FirstSampleFlags = MOV_FRAG_SAMPLE_FLAG_DEPENDS_NO - for i := start; i < end; i++ { - sampleDuration := uint32(0) - if i == len(track.Samplelist)-1 { - sampleDuration = track.defaultDuration + // Fill entry list + trun.EntryList = make([]TrunEntry, trun.SampleCount) + for i := 0; i < int(trun.SampleCount); i++ { + sample := &track.Samplelist[start+i] + // Duration + if i < int(trun.SampleCount)-1 { + trun.EntryList[i].SampleDuration = uint32(track.Samplelist[start+i+1].DTS - sample.DTS) } else { - sampleDuration = uint32(track.Samplelist[i+1].DTS - track.Samplelist[i].DTS) + trun.EntryList[i].SampleDuration = trun.EntryList[i-1].SampleDuration } - - entry := TrunEntry{ - SampleDuration: sampleDuration, - SampleSize: uint32(track.Samplelist[i].Size), - SampleCompositionTimeOffset: uint32(track.Samplelist[i].PTS - track.Samplelist[i].DTS), + // Size + trun.EntryList[i].SampleSize = uint32(sample.Size) + // Flags + if flag&TR_FLAG_DATA_SAMPLE_FLAGS != 0 { + if sample.KeyFrame { + trun.EntryList[i].SampleFlags = MOV_FRAG_SAMPLE_FLAG_DEPENDS_NO | MOV_FRAG_SAMPLE_FLAG_IS_SYNC + } else { + trun.EntryList[i].SampleFlags = MOV_FRAG_SAMPLE_FLAG_DEPENDS_YES | MOV_FRAG_SAMPLE_FLAG_IS_NON_SYNC + } + } + // Composition time offset + if flag&TR_FLAG_DATA_SAMPLE_COMPOSITION_TIME != 0 { + trun.EntryList[i].SampleCompositionTimeOffset = int32(sample.PTS - sample.DTS) } - trun.EntryList = append(trun.EntryList, entry) } + + // Calculate data offset + // Data offset is relative to the start of the moof box + // When TF_FLAG_DEFAULT_BASE_IS_MOOF is set, we need to add the size of the moof box + // to point to the start of the data in the mdat box + // trun.Dataoffset = int32(moofSize + 8) // +8 for mdat header + + // Print trun box details _, boxData := trun.Encode(flag) + return boxData } diff --git a/plugin/webrtc/api.go b/plugin/webrtc/api.go index ed42f1d5..168baa00 100644 --- a/plugin/webrtc/api.go +++ b/plugin/webrtc/api.go @@ -251,37 +251,3 @@ func (conf *WebRTCPlugin) Batch(w http.ResponseWriter, r *http.Request) { http.Error(w, err.Error(), http.StatusBadRequest) } } - -// 在Connection结构体中添加状态标记 -type Connection struct { - // ...其他字段 - initialOffer bool -} - -func (conn *Connection) GetAnswer() (SessionDescription, error) { - if conn.initialOffer { - // 完整SDP处理 - answer, err := conn.PeerConnection.CreateAnswer(nil) - conn.initialOffer = false - return answer, err - } else { - // 增量更新时生成部分SDP - return SessionDescription{ - SDP: conn.generatePartialSDP(), - Type: SDPTypeAnswer, - }, nil - } -} - -// 生成部分SDP的逻辑 -func (conn *Connection) generatePartialSDP() string { - var sdp strings.Builder - // 这里简化实现,实际需要根据变化生成对应媒体部分 - sdp.WriteString("v=0\r\no=- 0 0 IN IP4 127.0.0.1\r\ns=-\r\n") - for _, transceiver := range conn.PeerConnection.GetTransceivers() { - if transceiver.Direction() == RTPTransceiverDirectionRecvonly { - sdp.WriteString(fmt.Sprintf("m=video 9 UDP/TLS/RTP/SAVPF 96\r\na=recvonly\r\n")) - } - } - return sdp.String() -} diff --git a/website/index.html b/website/index.html deleted file mode 100644 index 96a6aa41..00000000 --- a/website/index.html +++ /dev/null @@ -1,175 +0,0 @@ - - - - - - - Monibuca - 高性能流媒体服务器框架 - - - - -
- -
- -
-
-
-

下一代流媒体服务器框架

-

高性能、可扩展、插件化的纯 Go 流媒体服务器开发框架

- -
-
-
🚀
-

高性能

-

采用纯 Go 开发,充分利用 Go 的并发特性

-
-
-
🔌
-

插件化

-

核心功能都以插件形式提供,可按需加载

-
-
-
🛠
-

可扩展

-

支持自定义插件开发,灵活扩展功能

-
-
-
📽
-

多协议

-

支持 RTMP、HTTP-FLV、HLS、WebRTC 等

-
-
-
-
- -
-
-

核心特性

-
-
-

🎯 低延迟

-

针对实时性场景优化,提供极低的传输延迟

-
-
-

📊 实时监控

-

支持 Prometheus 监控集成,实时掌握系统状态

-
-
-

🔄 集群支持

-

支持分布式部署,轻松扩展系统规模

-
-
-
-
- -
-
-

快速开始

-
-
- 安装 - -
-
mkdir my-m7s-server && cd my-m7s-server
-go mod init my-m7s-server
-
-
-
- 创建主程序 - -
-
package main
-
-import (
-    "context"
-    "m7s.live/v5"
-    _ "m7s.live/v5/plugin/rtmp"
-    _ "m7s.live/v5/plugin/flv"
-    _ "m7s.live/v5/plugin/hls"
-)
-
-func main() {
-    m7s.Run(context.Background(), "config.yaml")
-}
-
-
-
- -
-
-

官方插件

-
-
-

RTMP

-

支持 RTMP 协议推拉流

-
-
-

HTTP-FLV

-

支持 HTTP-FLV 协议直播

-
-
-

HLS

-

支持 HLS 协议直播点播

-
-
-

WebRTC

-

支持 WebRTC 协议互动直播

-
-
-

GB28181

-

支持国标协议设备接入

-
-
-

SRT

-

支持 SRT 协议传输

-
-
-
-
-
- - - - - - - \ No newline at end of file diff --git a/website/main.js b/website/main.js deleted file mode 100644 index 9a72e36f..00000000 --- a/website/main.js +++ /dev/null @@ -1,93 +0,0 @@ -// Copy button functionality -document.querySelectorAll('.copy-button').forEach(button => { - button.addEventListener('click', () => { - const codeId = button.getAttribute('data-target'); - const codeElement = document.getElementById(codeId); - const text = codeElement.textContent; - - navigator.clipboard.writeText(text).then(() => { - const originalText = button.textContent; - button.textContent = '已复制!'; - button.style.background = 'rgba(0,255,0,0.2)'; - - setTimeout(() => { - button.textContent = originalText; - button.style.background = 'transparent'; - }, 2000); - }); - }); -}); - -// Smooth scrolling for anchor links -document.querySelectorAll('a[href^="#"]').forEach(anchor => { - anchor.addEventListener('click', function (e) { - e.preventDefault(); - const target = document.querySelector(this.getAttribute('href')); - if (target) { - target.scrollIntoView({ - behavior: 'smooth', - block: 'start' - }); - } - }); -}); - -// Header scroll effect -const header = document.querySelector('header'); -let lastScrollY = window.scrollY; - -window.addEventListener('scroll', () => { - if (window.scrollY > lastScrollY) { - header.style.transform = 'translateY(-100%)'; - } else { - header.style.transform = 'translateY(0)'; - } - lastScrollY = window.scrollY; -}); - -// Add transition to header -header.style.transition = 'transform 0.3s ease-in-out'; - -// Mobile menu functionality (if needed in the future) -// const mobileMenuButton = document.querySelector('.mobile-menu-button'); -// const navLinks = document.querySelector('.nav-links'); - -// if (mobileMenuButton) { -// mobileMenuButton.addEventListener('click', () => { -// navLinks.classList.toggle('active'); -// }); -// } - -// Intersection Observer for fade-in animations -const observerOptions = { - root: null, - rootMargin: '0px', - threshold: 0.1 -}; - -const observer = new IntersectionObserver((entries) => { - entries.forEach(entry => { - if (entry.isIntersecting) { - entry.target.classList.add('fade-in'); - observer.unobserve(entry.target); - } - }); -}, observerOptions); - -// Observe all sections -document.querySelectorAll('section').forEach(section => { - section.style.opacity = '0'; - section.style.transform = 'translateY(20px)'; - section.style.transition = 'opacity 0.6s ease-out, transform 0.6s ease-out'; - observer.observe(section); -}); - -// Add fade-in class for animation -const style = document.createElement('style'); -style.textContent = ` - .fade-in { - opacity: 1 !important; - transform: translateY(0) !important; - } -`; -document.head.appendChild(style); \ No newline at end of file diff --git a/website/style.css b/website/style.css deleted file mode 100644 index aadb6258..00000000 --- a/website/style.css +++ /dev/null @@ -1,419 +0,0 @@ -:root { - --primary-color: #646cff; - --primary-color-dark: #535bf2; - --text-color: #213547; - --text-color-light: #666; - --background-color: #242424; - --text-color-dark: rgba(255, 255, 255, 0.87); - --text-color-dark-2: rgba(255, 255, 255, 0.6); - --border-color: #eee; - --code-background: #1a1a1a; -} - -* { - margin: 0; - padding: 0; - box-sizing: border-box; -} - -body { - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; - color: var(--text-color-dark); - line-height: 1.6; - background: var(--background-color); - min-height: 100vh; -} - -.container { - max-width: 1200px; - margin: 0 auto; - padding: 0 2rem; -} - -/* Header & Navigation */ -header { - background: rgba(36, 36, 36, 0.8); - -webkit-backdrop-filter: blur(12px); - backdrop-filter: blur(12px); - border-bottom: 1px solid rgba(255, 255, 255, 0.1); - position: fixed; - width: 100%; - top: 0; - z-index: 1000; -} - -nav { - height: 64px; - display: flex; - align-items: center; -} - -nav .container { - display: flex; - justify-content: space-between; - align-items: center; - width: 100%; -} - -.logo { - display: flex; - align-items: center; - gap: 0.5rem; -} - -.logo img { - height: 32px; -} - -.logo span { - font-size: 1.5rem; - font-weight: 600; - color: var(--primary-color); -} - -.nav-links { - display: flex; - gap: 2rem; -} - -.nav-links a { - text-decoration: none; - color: var(--text-color-dark-2); - font-weight: 500; - transition: color 0.2s; -} - -.nav-links a:hover { - color: var(--text-color-dark); -} - -.github-link { - display: flex; - align-items: center; - gap: 0.5rem; -} - -/* Hero Section */ -.hero { - padding: 120px 0 80px; - text-align: center; - position: relative; - overflow: hidden; -} - -.hero::before { - content: ''; - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: - radial-gradient(circle at 50% 0%, rgba(100, 108, 255, 0.15), rgba(36, 36, 36, 0) 50%), - radial-gradient(circle at 0% 100%, rgba(255, 182, 255, 0.1), rgba(36, 36, 36, 0) 50%), - radial-gradient(circle at 100% 100%, rgba(100, 108, 255, 0.1), rgba(36, 36, 36, 0) 50%); - z-index: -1; -} - -.hero h1 { - font-size: 3.5rem; - font-weight: 800; - margin-bottom: 1rem; - background: linear-gradient(120deg, #bd34fe 30%, #47caff); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; -} - -.hero .subtitle { - font-size: 1.5rem; - color: var(--text-color-dark-2); - margin-bottom: 2rem; -} - -.cta-buttons { - display: flex; - gap: 1rem; - justify-content: center; - margin-bottom: 4rem; -} - -.primary-button, .secondary-button { - padding: 0.75rem 2rem; - border-radius: 20px; - font-weight: 600; - text-decoration: none; - transition: all 0.2s; - border: 1px solid transparent; -} - -.primary-button { - background: linear-gradient(to right, #bd34fe 30%, #47caff); - color: white; - box-shadow: 0 2px 12px rgba(189, 52, 254, 0.3); -} - -.primary-button:hover { - background: linear-gradient(to right, #a925e5 30%, #38b8eb); - transform: translateY(-2px); - box-shadow: 0 4px 16px rgba(189, 52, 254, 0.4); -} - -.secondary-button { - background: rgba(255, 255, 255, 0.1); - color: var(--text-color-dark); - border: 1px solid rgba(255, 255, 255, 0.2); -} - -.secondary-button:hover { - background: rgba(255, 255, 255, 0.15); - transform: translateY(-2px); - border: 1px solid rgba(255, 255, 255, 0.3); -} - -/* Features Grid */ -.features-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); - gap: 2rem; - margin-top: 4rem; -} - -.feature-card { - background: var(--code-background); - padding: 2rem; - border-radius: 12px; - border: 1px solid rgba(255, 255, 255, 0.1); - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); - transition: all 0.2s; -} - -.feature-card:hover { - transform: translateY(-5px); - border-color: rgba(255, 255, 255, 0.2); - box-shadow: 0 6px 16px rgba(0, 0, 0, 0.4); -} - -.feature-card .icon { - font-size: 2.5rem; - margin-bottom: 1rem; -} - -.feature-card h3 { - margin-bottom: 0.5rem; - color: #bd34fe; -} - -.feature-card p { - color: var(--text-color-dark-2); -} - -/* Features Section */ -.features { - padding: 80px 0; - background: var(--background-color); - position: relative; -} - -.features::before { - content: ''; - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: - radial-gradient(circle at 0% 0%, rgba(189, 52, 254, 0.15), rgba(36, 36, 36, 0) 50%), - radial-gradient(circle at 100% 0%, rgba(71, 202, 255, 0.15), rgba(36, 36, 36, 0) 50%); - z-index: -1; -} - -.features h2 { - color: var(--text-color-dark); -} - -.features-list { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); - gap: 2rem; -} - -.feature { - background: var(--code-background); - border: 1px solid rgba(255, 255, 255, 0.1); -} - -.feature h3 { - color: #47caff; -} - -.feature p { - color: var(--text-color-dark-2); -} - -/* Quickstart Section */ -.quickstart { - padding: 80px 0; -} - -.quickstart h2 { - text-align: center; - margin-bottom: 3rem; - font-size: 2.5rem; -} - -.code-block { - background: var(--code-background); - border-radius: 8px; - margin-bottom: 2rem; - overflow: hidden; -} - -.code-header { - display: flex; - justify-content: space-between; - align-items: center; - padding: 0.75rem 1rem; - background: rgba(255,255,255,0.1); -} - -.code-header span { - color: #fff; -} - -.copy-button { - background: transparent; - border: 1px solid rgba(255,255,255,0.2); - color: #fff; - padding: 0.25rem 0.75rem; - border-radius: 4px; - cursor: pointer; - transition: all 0.2s; -} - -.copy-button:hover { - background: rgba(255,255,255,0.1); -} - -pre { - margin: 0; - padding: 1.5rem; -} - -code { - color: #fff; - font-family: 'Fira Code', monospace; - font-size: 0.9rem; -} - -/* Plugins Section */ -.plugins { - padding: 80px 0; - background: var(--background-color); - position: relative; -} - -.plugins::before { - content: ''; - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: - radial-gradient(circle at 100% 50%, rgba(71, 202, 255, 0.15), rgba(36, 36, 36, 0) 50%), - radial-gradient(circle at 0% 50%, rgba(189, 52, 254, 0.15), rgba(36, 36, 36, 0) 50%); - z-index: -1; -} - -.plugins h2 { - color: var(--text-color-dark); -} - -.plugins-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); - gap: 2rem; -} - -.plugin-card { - background: var(--code-background); - border: 1px solid rgba(255, 255, 255, 0.1); - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); -} - -.plugin-card:hover { - border-color: rgba(255, 255, 255, 0.2); - box-shadow: 0 6px 16px rgba(0, 0, 0, 0.4); -} - -.plugin-card h3 { - background: linear-gradient(120deg, #bd34fe 30%, #47caff); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; -} - -.plugin-card p { - color: var(--text-color-dark-2); -} - -/* Footer */ -footer { - background: var(--code-background); - color: var(--text-color-dark); - border-top: 1px solid rgba(255, 255, 255, 0.1); -} - -footer::before { - background: - radial-gradient(circle at 0% 0%, rgba(189, 52, 254, 0.15), rgba(26, 26, 26, 0) 50%), - radial-gradient(circle at 100% 100%, rgba(71, 202, 255, 0.15), rgba(26, 26, 26, 0) 50%); -} - -.footer-content { - position: relative; - z-index: 1; - display: grid; - grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); - gap: 3rem; -} - -.footer-section h4 { - color: #bd34fe; -} - -.footer-section a { - color: var(--text-color-dark-2); -} - -.footer-section a:hover { - color: var(--text-color-dark); -} - -/* Responsive Design */ -@media (max-width: 768px) { - .nav-links { - display: none; - } - - .hero h1 { - font-size: 2.5rem; - } - - .hero .subtitle { - font-size: 1.2rem; - } - - .features-grid, - .plugins-grid { - grid-template-columns: 1fr; - } - - .cta-buttons { - flex-direction: column; - } - - .footer-content { - grid-template-columns: 1fr; - text-align: center; - } -} \ No newline at end of file