Skip to content

Commit 43040f0

Browse files
committed
feat: MPEG-H support including test assets
1 parent babc4a8 commit 43040f0

28 files changed

Lines changed: 208 additions & 3 deletions

CHANGELOG.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
### Added
1111

1212
- HEVC encryption support
13-
- AC-4 audio support including encryption
14-
- AC-3 and EC-3 encryption support
13+
- AC-4 audio support (including full encryption)
14+
- MPEG-H audio support (including full encryption)
15+
- AC-3 and EC-3 full encryption support
1516
- Support for CMAF-compliant audio edit list
1617
- Documented the CMAF edit list support for both audio and video
1718

1819
### Changed
1920

20-
- Minimal Go version is now 1.23
21+
- Minimal Go version increased to 1.23
2122
- Updated dash-mpd to v0.13.0 (Full DASH Ed. 6 support)
23+
- Updated mp4ff to v0.50.0 (more codecs support)
2224

2325
### Fixed
2426

cmd/livesim2/app/livesegment_test.go

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818

1919
"github.com/Dash-Industry-Forum/livesim2/pkg/drm"
2020
"github.com/Dash-Industry-Forum/livesim2/pkg/logging"
21+
m "github.com/Eyevinn/dash-mpd/mpd"
2122
"github.com/Eyevinn/mp4ff/bits"
2223
"github.com/Eyevinn/mp4ff/mp4"
2324
"github.com/stretchr/testify/assert"
@@ -728,6 +729,174 @@ func TestSegmentStatusCodeResponse(t *testing.T) {
728729
}
729730
}
730731

732+
// TestMpeghAssets tests MPEG-H assets with 1.6s segment duration
733+
func TestMpeghAssets(t *testing.T) {
734+
vodFS := os.DirFS("testdata/assets")
735+
am := newAssetMgr(vodFS, "", false)
736+
logger := slog.Default()
737+
err := am.discoverAssets(logger)
738+
require.NoError(t, err)
739+
740+
testCases := []struct {
741+
asset string
742+
mpdName string
743+
audioRepId string
744+
videoRepId string
745+
expectedAudioTimescale int
746+
expectedVideoTimescale int
747+
expectedSegmentDurMs int
748+
expectedAudioCodec string
749+
expectedVideoCodec string
750+
}{
751+
{
752+
asset: "mpegh_BL_1_6",
753+
mpdName: "BL_1_6.mpd",
754+
audioRepId: "mhm1_64kbps_per_signal",
755+
videoRepId: "hvc1_10000kbps",
756+
expectedAudioTimescale: 48000,
757+
expectedVideoTimescale: 50,
758+
expectedSegmentDurMs: 1600, // 1.6 seconds
759+
expectedAudioCodec: "mhm1.0x10",
760+
expectedVideoCodec: "hvc1.1.6.L123.90",
761+
},
762+
{
763+
asset: "mpegh_LC_1_6",
764+
mpdName: "LC_1_6.mpd",
765+
audioRepId: "mhm1_64kbps_per_signal",
766+
videoRepId: "hvc1_10000kbps",
767+
expectedAudioTimescale: 48000,
768+
expectedVideoTimescale: 50,
769+
expectedSegmentDurMs: 1600, // 1.6 seconds
770+
expectedAudioCodec: "mhm1.0x0B",
771+
expectedVideoCodec: "hvc1.1.6.L123.90",
772+
},
773+
}
774+
775+
var drmCfg *drm.DrmConfig = nil
776+
for _, tc := range testCases {
777+
t.Run(tc.asset, func(t *testing.T) {
778+
asset, ok := am.findAsset(tc.asset)
779+
require.True(t, ok, "Asset %s not found", tc.asset)
780+
781+
cfg := NewResponseConfig()
782+
nowMS := 10_000 // 10 seconds into stream
783+
784+
// Test MPD generation
785+
liveMPD, err := LiveMPD(asset, tc.mpdName, cfg, nil, nowMS)
786+
require.NoError(t, err, "Failed to generate live MPD")
787+
require.NotNil(t, liveMPD, "Live MPD is nil")
788+
require.Len(t, liveMPD.Periods, 1, "Expected exactly one period")
789+
require.Len(t, liveMPD.Periods[0].AdaptationSets, 2, "Expected exactly two adaptation sets")
790+
791+
// Test audio and video adaptation sets
792+
var audioAS, videoAS *m.AdaptationSetType
793+
for _, as := range liveMPD.Periods[0].AdaptationSets {
794+
switch as.ContentType {
795+
case "audio":
796+
audioAS = as
797+
case "video":
798+
videoAS = as
799+
}
800+
}
801+
require.NotNil(t, audioAS, "Audio adaptation set not found")
802+
require.NotNil(t, videoAS, "Video adaptation set not found")
803+
804+
// Verify audio adaptation set
805+
require.Equal(t, tc.expectedAudioCodec, audioAS.Codecs, "Audio codec mismatch")
806+
require.Equal(t, "audio/mp4", audioAS.MimeType, "Audio mime type mismatch")
807+
require.Equal(t, tc.expectedAudioTimescale, int(*audioAS.SegmentTemplate.Timescale), "Audio timescale mismatch")
808+
expectedAudioDuration := tc.expectedSegmentDurMs * tc.expectedAudioTimescale / 1000
809+
require.Equal(t, expectedAudioDuration, int(*audioAS.SegmentTemplate.Duration), "Audio duration mismatch")
810+
811+
// Verify video adaptation set
812+
require.Equal(t, tc.expectedVideoCodec, videoAS.Codecs, "Video codec mismatch")
813+
require.Equal(t, "video/mp4", videoAS.MimeType, "Video mime type mismatch")
814+
require.Equal(t, tc.expectedVideoTimescale, int(*videoAS.SegmentTemplate.Timescale), "Video timescale mismatch")
815+
expectedVideoDuration := tc.expectedSegmentDurMs * tc.expectedVideoTimescale / 1000
816+
require.Equal(t, expectedVideoDuration, int(*videoAS.SegmentTemplate.Duration), "Video duration mismatch")
817+
818+
// Test audio init segment
819+
audioInitPath := fmt.Sprintf("%s_init.mp4", tc.audioRepId)
820+
rr := httptest.NewRecorder()
821+
wroteInit, err := writeInitSegment(logger, rr, cfg, drmCfg, asset, audioInitPath)
822+
require.True(t, wroteInit, "Failed to write audio init segment")
823+
require.NoError(t, err, "Error writing audio init segment")
824+
require.Equal(t, http.StatusOK, rr.Code, "Audio init segment returned wrong status code")
825+
826+
// Verify audio init segment content
827+
initSeg := rr.Body.Bytes()
828+
require.Greater(t, len(initSeg), 0, "Audio init segment is empty")
829+
sr := bits.NewFixedSliceReader(initSeg)
830+
mp4File, err := mp4.DecodeFileSR(sr)
831+
require.NoError(t, err, "Failed to parse audio init segment")
832+
require.NotNil(t, mp4File.Init, "Init segment has no init part")
833+
audioTimescale := int(mp4File.Moov.Trak.Mdia.Mdhd.Timescale)
834+
require.Equal(t, tc.expectedAudioTimescale, audioTimescale, "Audio init segment timescale mismatch")
835+
836+
// Test audio media segment (segment 0)
837+
audioMediaPath := fmt.Sprintf("%s_0.m4s", tc.audioRepId)
838+
so, err := genLiveSegment(logger, vodFS, asset, cfg, audioMediaPath, nowMS, false)
839+
require.NoError(t, err, "Failed to generate audio segment")
840+
require.NotNil(t, so, "Audio segment output is nil")
841+
require.Equal(t, "audio/mp4", so.meta.rep.SegmentType(), "Audio segment wrong type")
842+
843+
// Verify audio segment timing (1.6s duration = 76800 at 48kHz)
844+
seg := so.seg
845+
require.Len(t, seg.Fragments, 1, "Expected exactly one fragment in audio segment")
846+
baseDecodeTime := seg.Fragments[0].Moof.Traf.Tfdt.BaseMediaDecodeTime()
847+
require.Equal(t, 0, int(baseDecodeTime), "First audio segment should start at time 0")
848+
849+
// Test video init segment
850+
videoInitPath := fmt.Sprintf("%s_init.mp4", tc.videoRepId)
851+
rr = httptest.NewRecorder()
852+
wroteInit, err = writeInitSegment(logger, rr, cfg, drmCfg, asset, videoInitPath)
853+
require.True(t, wroteInit, "Failed to write video init segment")
854+
require.NoError(t, err, "Error writing video init segment")
855+
require.Equal(t, http.StatusOK, rr.Code, "Video init segment returned wrong status code")
856+
857+
// Verify video init segment content
858+
initSeg = rr.Body.Bytes()
859+
require.Greater(t, len(initSeg), 0, "Video init segment is empty")
860+
sr = bits.NewFixedSliceReader(initSeg)
861+
mp4File, err = mp4.DecodeFileSR(sr)
862+
require.NoError(t, err, "Failed to parse video init segment")
863+
require.NotNil(t, mp4File.Init, "Init segment has no init part")
864+
videoTimescale := int(mp4File.Moov.Trak.Mdia.Mdhd.Timescale)
865+
require.Equal(t, tc.expectedVideoTimescale, videoTimescale, "Video init segment timescale mismatch")
866+
867+
// Test video media segment (segment 0)
868+
videoMediaPath := fmt.Sprintf("%s_0.m4s", tc.videoRepId)
869+
so, err = genLiveSegment(logger, vodFS, asset, cfg, videoMediaPath, nowMS, false)
870+
require.NoError(t, err, "Failed to generate video segment")
871+
require.NotNil(t, so, "Video segment output is nil")
872+
require.Equal(t, "video/mp4", so.meta.rep.SegmentType(), "Video segment wrong type")
873+
874+
// Verify video segment timing (1.6s duration = 80 at 50Hz)
875+
seg = so.seg
876+
require.Len(t, seg.Fragments, 1, "Expected exactly one fragment in video segment")
877+
baseDecodeTime = seg.Fragments[0].Moof.Traf.Tfdt.BaseMediaDecodeTime()
878+
require.Equal(t, 0, int(baseDecodeTime), "First video segment should start at time 0")
879+
880+
// Test segment 1 to verify timing progression
881+
audioMediaPath = fmt.Sprintf("%s_1.m4s", tc.audioRepId)
882+
so, err = genLiveSegment(logger, vodFS, asset, cfg, audioMediaPath, nowMS, false)
883+
require.NoError(t, err, "Failed to generate audio segment 1")
884+
baseDecodeTime = so.seg.Fragments[0].Moof.Traf.Tfdt.BaseMediaDecodeTime()
885+
expectedAudioTime := 76800 // 1.6s * 48000
886+
require.Equal(t, expectedAudioTime, int(baseDecodeTime), "Second audio segment timing incorrect")
887+
888+
videoMediaPath = fmt.Sprintf("%s_1.m4s", tc.videoRepId)
889+
so, err = genLiveSegment(logger, vodFS, asset, cfg, videoMediaPath, nowMS, false)
890+
require.NoError(t, err, "Failed to generate video segment 1")
891+
baseDecodeTime = so.seg.Fragments[0].Moof.Traf.Tfdt.BaseMediaDecodeTime()
892+
expectedVideoTime := 80 // 1.6s * 50
893+
require.Equal(t, expectedVideoTime, int(baseDecodeTime), "Second video segment timing incorrect")
894+
895+
t.Logf("Successfully tested MPEG-H asset %s with 1.6s segments", tc.asset)
896+
})
897+
}
898+
}
899+
731900
func TestMehdBoxRemovedFromInitSegment(t *testing.T) {
732901
var drmCfg *drm.DrmConfig = nil
733902
vodFS := os.DirFS("testdata/assets")
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<MPD xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3+
xmlns="urn:mpeg:dash:schema:mpd:2011" xsi:schemaLocation="urn:mpeg:DASH:schema:MPD:2011 DASH-MPD.xsd"
4+
profiles="urn:mpeg:dash:profile:isoff-live:2011"
5+
minBufferTime="PT3.2S" type="static" mediaPresentationDuration="PT8S">
6+
<Period start="PT0S" id="0">
7+
<AdaptationSet id="0" codecs="mhm1.0x10" contentType="audio" mimeType="audio/mp4" segmentAlignment="true" bitstreamSwitching="true" audioSamplingRate="48000">
8+
<SegmentTemplate initialization="$RepresentationID$_init.mp4" media="$RepresentationID$_$Number$.m4s" timescale="48000" duration="76800" startNumber="0"/>
9+
<AudioChannelConfiguration schemeIdUri="urn:mpeg:mpegB:cicp:ChannelConfiguration" value="2"/>
10+
<Representation id="mhm1_64kbps_per_signal" bandwidth="130319"/>
11+
</AdaptationSet>
12+
<AdaptationSet codecs="hvc1.1.6.L123.90" contentType="video" width="1920" height="1080" mimeType="video/mp4" segmentAlignment="true" bitstreamSwitching="true">
13+
<SegmentTemplate initialization="$RepresentationID$_init.mp4" media="$RepresentationID$_$Number$.m4s" timescale="50" duration="80" startNumber="0"/>
14+
<Representation id="hvc1_10000kbps" bandwidth="1159185" sar="1:1"/>
15+
</AdaptationSet>
16+
</Period>
17+
</MPD>
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

0 commit comments

Comments
 (0)