mirror of
https://github.com/bluenviron/mediamtx.git
synced 2025-12-20 02:00:05 -08:00
Some checks are pending
code_lint / go (push) Waiting to run
code_lint / go_mod (push) Waiting to run
code_lint / docs (push) Waiting to run
code_lint / api_docs (push) Waiting to run
code_test / test_64 (push) Waiting to run
code_test / test_32 (push) Waiting to run
code_test / test_e2e (push) Waiting to run
Thanks to the new mtxi MP4 box, it's possible to check whether two segments are consecutive without involving dates or timestamps. When the new mtxi box is present in both segments, do not check if the end of the first segment corresponds to the start of the second segment.
427 lines
9 KiB
Go
427 lines
9 KiB
Go
package playback
|
|
|
|
import (
|
|
"io"
|
|
"os"
|
|
"testing"
|
|
"time"
|
|
|
|
amp4 "github.com/abema/go-mp4"
|
|
"github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4audio"
|
|
"github.com/bluenviron/mediacommon/v2/pkg/formats/fmp4"
|
|
"github.com/bluenviron/mediacommon/v2/pkg/formats/mp4"
|
|
"github.com/bluenviron/mediamtx/internal/recordstore"
|
|
"github.com/bluenviron/mediamtx/internal/test"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func writeBenchInit(f io.WriteSeeker) {
|
|
init := fmp4.Init{
|
|
Tracks: []*fmp4.InitTrack{
|
|
{
|
|
ID: 1,
|
|
TimeScale: 90000,
|
|
Codec: &mp4.CodecH264{
|
|
SPS: test.FormatH264.SPS,
|
|
PPS: test.FormatH264.PPS,
|
|
},
|
|
},
|
|
{
|
|
ID: 2,
|
|
TimeScale: 90000,
|
|
Codec: &mp4.CodecMPEG4Audio{
|
|
Config: mpeg4audio.AudioSpecificConfig{
|
|
Type: mpeg4audio.ObjectTypeAACLC,
|
|
SampleRate: 48000,
|
|
ChannelCount: 2,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
err := init.Marshal(f)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
_, err = f.Write([]byte{
|
|
0x00, 0x00, 0x00, 0x10, 'm', 'o', 'o', 'f',
|
|
})
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
|
|
func BenchmarkFMP4ReadHeader(b *testing.B) {
|
|
f, err := os.CreateTemp(os.TempDir(), "mediamtx-playback-fmp4-")
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
defer os.Remove(f.Name())
|
|
|
|
writeBenchInit(f)
|
|
f.Close()
|
|
|
|
for b.Loop() {
|
|
func() {
|
|
f, err = os.Open(f.Name())
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
defer f.Close()
|
|
|
|
_, _, err = segmentFMP4ReadHeader(f)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
}()
|
|
}
|
|
}
|
|
|
|
func TestSegmentFMP4CanBeConcatenated(t *testing.T) {
|
|
baseTime := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)
|
|
streamID1 := [16]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}
|
|
streamID2 := [16]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 17}
|
|
|
|
baseTracks := []*fmp4.InitTrack{
|
|
{
|
|
ID: 1,
|
|
TimeScale: 90000,
|
|
Codec: &mp4.CodecH264{
|
|
SPS: test.FormatH264.SPS,
|
|
PPS: test.FormatH264.PPS,
|
|
},
|
|
},
|
|
{
|
|
ID: 2,
|
|
TimeScale: 48000,
|
|
Codec: &mp4.CodecMPEG4Audio{
|
|
Config: mpeg4audio.AudioSpecificConfig{
|
|
Type: mpeg4audio.ObjectTypeAACLC,
|
|
SampleRate: 48000,
|
|
ChannelCount: 2,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
differentTracks := []*fmp4.InitTrack{
|
|
{
|
|
ID: 1,
|
|
TimeScale: 90000,
|
|
Codec: &mp4.CodecH264{
|
|
SPS: test.FormatH264.SPS,
|
|
PPS: test.FormatH264.PPS,
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range []struct {
|
|
name string
|
|
prevInit *fmp4.Init
|
|
prevEnd time.Time
|
|
curInit *fmp4.Init
|
|
curStart time.Time
|
|
want bool
|
|
}{
|
|
{
|
|
name: "with mtxi - consecutive segments, same stream",
|
|
prevInit: &fmp4.Init{
|
|
Tracks: baseTracks,
|
|
UserData: []amp4.IBox{
|
|
&recordstore.Mtxi{
|
|
StreamID: streamID1,
|
|
SegmentNumber: 1,
|
|
},
|
|
},
|
|
},
|
|
prevEnd: baseTime,
|
|
curInit: &fmp4.Init{
|
|
Tracks: baseTracks,
|
|
UserData: []amp4.IBox{
|
|
&recordstore.Mtxi{
|
|
StreamID: streamID1,
|
|
SegmentNumber: 2,
|
|
},
|
|
},
|
|
},
|
|
curStart: baseTime.Add(5 * time.Second),
|
|
want: true,
|
|
},
|
|
{
|
|
name: "with mtxi - non-consecutive segments, same stream",
|
|
prevInit: &fmp4.Init{
|
|
Tracks: baseTracks,
|
|
UserData: []amp4.IBox{
|
|
&recordstore.Mtxi{
|
|
StreamID: streamID1,
|
|
SegmentNumber: 1,
|
|
},
|
|
},
|
|
},
|
|
prevEnd: baseTime,
|
|
curInit: &fmp4.Init{
|
|
Tracks: baseTracks,
|
|
UserData: []amp4.IBox{
|
|
&recordstore.Mtxi{
|
|
StreamID: streamID1,
|
|
SegmentNumber: 3,
|
|
},
|
|
},
|
|
},
|
|
curStart: baseTime.Add(5 * time.Second),
|
|
want: false,
|
|
},
|
|
{
|
|
name: "with mtxi - consecutive segments, different streams",
|
|
prevInit: &fmp4.Init{
|
|
Tracks: baseTracks,
|
|
UserData: []amp4.IBox{
|
|
&recordstore.Mtxi{
|
|
StreamID: streamID1,
|
|
SegmentNumber: 1,
|
|
},
|
|
},
|
|
},
|
|
prevEnd: baseTime,
|
|
curInit: &fmp4.Init{
|
|
Tracks: baseTracks,
|
|
UserData: []amp4.IBox{
|
|
&recordstore.Mtxi{
|
|
StreamID: streamID2,
|
|
SegmentNumber: 2,
|
|
},
|
|
},
|
|
},
|
|
curStart: baseTime.Add(5 * time.Second),
|
|
want: false,
|
|
},
|
|
{
|
|
name: "prev has mtxi, current does not",
|
|
prevInit: &fmp4.Init{
|
|
Tracks: baseTracks,
|
|
UserData: []amp4.IBox{
|
|
&recordstore.Mtxi{
|
|
StreamID: streamID1,
|
|
SegmentNumber: 1,
|
|
},
|
|
},
|
|
},
|
|
prevEnd: baseTime,
|
|
curInit: &fmp4.Init{
|
|
Tracks: baseTracks,
|
|
UserData: []amp4.IBox{},
|
|
},
|
|
curStart: baseTime.Add(5 * time.Second),
|
|
want: false,
|
|
},
|
|
{
|
|
name: "prev does not have mtxi, current has mtxi",
|
|
prevInit: &fmp4.Init{
|
|
Tracks: baseTracks,
|
|
UserData: []amp4.IBox{},
|
|
},
|
|
prevEnd: baseTime,
|
|
curInit: &fmp4.Init{
|
|
Tracks: baseTracks,
|
|
UserData: []amp4.IBox{
|
|
&recordstore.Mtxi{
|
|
StreamID: streamID1,
|
|
SegmentNumber: 1,
|
|
},
|
|
},
|
|
},
|
|
curStart: baseTime.Add(5 * time.Second),
|
|
want: false,
|
|
},
|
|
{
|
|
name: "legacy mode - same tracks, within time tolerance",
|
|
prevInit: &fmp4.Init{
|
|
Tracks: baseTracks,
|
|
UserData: []amp4.IBox{},
|
|
},
|
|
prevEnd: baseTime.Add(10 * time.Second),
|
|
curInit: &fmp4.Init{
|
|
Tracks: baseTracks,
|
|
UserData: []amp4.IBox{},
|
|
},
|
|
curStart: baseTime.Add(10 * time.Second),
|
|
want: true,
|
|
},
|
|
{
|
|
name: "legacy mode - same tracks, exactly at tolerance boundary (before)",
|
|
prevInit: &fmp4.Init{
|
|
Tracks: baseTracks,
|
|
UserData: []amp4.IBox{},
|
|
},
|
|
prevEnd: baseTime.Add(10 * time.Second),
|
|
curInit: &fmp4.Init{
|
|
Tracks: baseTracks,
|
|
UserData: []amp4.IBox{},
|
|
},
|
|
curStart: baseTime.Add(9 * time.Second),
|
|
want: true,
|
|
},
|
|
{
|
|
name: "legacy mode - same tracks, exactly at tolerance boundary (after)",
|
|
prevInit: &fmp4.Init{
|
|
Tracks: baseTracks,
|
|
UserData: []amp4.IBox{},
|
|
},
|
|
prevEnd: baseTime.Add(10 * time.Second),
|
|
curInit: &fmp4.Init{
|
|
Tracks: baseTracks,
|
|
UserData: []amp4.IBox{},
|
|
},
|
|
curStart: baseTime.Add(11 * time.Second),
|
|
want: true,
|
|
},
|
|
{
|
|
name: "legacy mode - same tracks, outside time tolerance (too early)",
|
|
prevInit: &fmp4.Init{
|
|
Tracks: baseTracks,
|
|
UserData: []amp4.IBox{},
|
|
},
|
|
prevEnd: baseTime.Add(10 * time.Second),
|
|
curInit: &fmp4.Init{
|
|
Tracks: baseTracks,
|
|
UserData: []amp4.IBox{},
|
|
},
|
|
curStart: baseTime.Add(8*time.Second + 999*time.Millisecond),
|
|
want: false,
|
|
},
|
|
{
|
|
name: "legacy mode - same tracks, outside time tolerance (too late)",
|
|
prevInit: &fmp4.Init{
|
|
Tracks: baseTracks,
|
|
UserData: []amp4.IBox{},
|
|
},
|
|
prevEnd: baseTime.Add(10 * time.Second),
|
|
curInit: &fmp4.Init{
|
|
Tracks: baseTracks,
|
|
UserData: []amp4.IBox{},
|
|
},
|
|
curStart: baseTime.Add(11*time.Second + 1*time.Millisecond),
|
|
want: false,
|
|
},
|
|
{
|
|
name: "legacy mode - different number of tracks",
|
|
prevInit: &fmp4.Init{
|
|
Tracks: baseTracks,
|
|
UserData: []amp4.IBox{},
|
|
},
|
|
prevEnd: baseTime,
|
|
curInit: &fmp4.Init{
|
|
Tracks: differentTracks,
|
|
UserData: []amp4.IBox{},
|
|
},
|
|
curStart: baseTime.Add(5 * time.Second),
|
|
want: false,
|
|
},
|
|
{
|
|
name: "legacy mode - different track IDs",
|
|
prevInit: &fmp4.Init{
|
|
Tracks: []*fmp4.InitTrack{
|
|
{
|
|
ID: 1,
|
|
TimeScale: 90000,
|
|
Codec: &mp4.CodecH264{
|
|
SPS: test.FormatH264.SPS,
|
|
PPS: test.FormatH264.PPS,
|
|
},
|
|
},
|
|
},
|
|
UserData: []amp4.IBox{},
|
|
},
|
|
prevEnd: baseTime,
|
|
curInit: &fmp4.Init{
|
|
Tracks: []*fmp4.InitTrack{
|
|
{
|
|
ID: 2,
|
|
TimeScale: 90000,
|
|
Codec: &mp4.CodecH264{
|
|
SPS: test.FormatH264.SPS,
|
|
PPS: test.FormatH264.PPS,
|
|
},
|
|
},
|
|
},
|
|
UserData: []amp4.IBox{},
|
|
},
|
|
curStart: baseTime.Add(5 * time.Second),
|
|
want: false,
|
|
},
|
|
{
|
|
name: "legacy mode - different time scales",
|
|
prevInit: &fmp4.Init{
|
|
Tracks: []*fmp4.InitTrack{
|
|
{
|
|
ID: 1,
|
|
TimeScale: 90000,
|
|
Codec: &mp4.CodecH264{
|
|
SPS: test.FormatH264.SPS,
|
|
PPS: test.FormatH264.PPS,
|
|
},
|
|
},
|
|
},
|
|
UserData: []amp4.IBox{},
|
|
},
|
|
prevEnd: baseTime,
|
|
curInit: &fmp4.Init{
|
|
Tracks: []*fmp4.InitTrack{
|
|
{
|
|
ID: 1,
|
|
TimeScale: 48000,
|
|
Codec: &mp4.CodecH264{
|
|
SPS: test.FormatH264.SPS,
|
|
PPS: test.FormatH264.PPS,
|
|
},
|
|
},
|
|
},
|
|
UserData: []amp4.IBox{},
|
|
},
|
|
curStart: baseTime.Add(5 * time.Second),
|
|
want: false,
|
|
},
|
|
{
|
|
name: "legacy mode - different codec types",
|
|
prevInit: &fmp4.Init{
|
|
Tracks: []*fmp4.InitTrack{
|
|
{
|
|
ID: 1,
|
|
TimeScale: 90000,
|
|
Codec: &mp4.CodecH264{
|
|
SPS: test.FormatH264.SPS,
|
|
PPS: test.FormatH264.PPS,
|
|
},
|
|
},
|
|
},
|
|
UserData: []amp4.IBox{},
|
|
},
|
|
prevEnd: baseTime,
|
|
curInit: &fmp4.Init{
|
|
Tracks: []*fmp4.InitTrack{
|
|
{
|
|
ID: 1,
|
|
TimeScale: 90000,
|
|
Codec: &mp4.CodecMPEG4Audio{
|
|
Config: mpeg4audio.AudioSpecificConfig{
|
|
Type: mpeg4audio.ObjectTypeAACLC,
|
|
SampleRate: 48000,
|
|
ChannelCount: 2,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
UserData: []amp4.IBox{},
|
|
},
|
|
curStart: baseTime.Add(5 * time.Second),
|
|
want: false,
|
|
},
|
|
} {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := segmentFMP4CanBeConcatenated(tt.prevInit, tt.prevEnd, tt.curInit, tt.curStart)
|
|
require.Equal(t, tt.want, got)
|
|
})
|
|
}
|
|
}
|