From b3c10a387d0b18b869a8c72965a77fd368521e6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torbjo=CC=88rn=20Einarsson?= Date: Fri, 7 Feb 2025 14:46:36 +0100 Subject: [PATCH 1/2] fix: HEVC Slice Header CollocatedFromL0Flag should be true by default --- CHANGELOG.md | 1 + hevc/slice.go | 4 +++- hevc/slice_test.go | 5 ++++- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6fae3efa..c46a6b67 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - support short ESDS without SLConfig descriptor (issue #393) +- HEVC Slice Header CollocatedFromL0Flag should be true by default ## [0.47.0] - 2024-11-12 diff --git a/hevc/slice.go b/hevc/slice.go index ed7ce88e..2d8c45d5 100644 --- a/hevc/slice.go +++ b/hevc/slice.go @@ -109,7 +109,9 @@ type WeightingFactors struct { } func ParseSliceHeader(nalu []byte, spsMap map[uint32]*SPS, ppsMap map[uint32]*PPS) (*SliceHeader, error) { - sh := &SliceHeader{} + sh := &SliceHeader{ + CollocatedFromL0Flag: true, // Default according to Section 7.4.7.1 + } buf := bytes.NewBuffer(nalu) r := bits.NewEBSPReader(buf) diff --git a/hevc/slice_test.go b/hevc/slice_test.go index 3a0ed0d0..13477497 100644 --- a/hevc/slice_test.go +++ b/hevc/slice_test.go @@ -20,6 +20,7 @@ func TestParseSliceHeader(t *testing.T) { LoopFilterAcrossSlicesEnabledFlag: true, NumEntryPointOffsets: 1, OffsetLenMinus1: 3, + CollocatedFromL0Flag: true, EntryPointOffsetMinus1: []uint32{12}, Size: 6}, NALU_TRAIL_N: { @@ -46,6 +47,7 @@ func TestParseSliceHeader(t *testing.T) { LoopFilterAcrossSlicesEnabledFlag: true, NumEntryPointOffsets: 1, OffsetLenMinus1: 1, + CollocatedFromL0Flag: false, EntryPointOffsetMinus1: []uint32{1}, Size: 10, }, @@ -74,6 +76,7 @@ func TestParseSliceHeader(t *testing.T) { }, }, }, + CollocatedFromL0Flag: true, FiveMinusMaxNumMergeCand: 2, QpDelta: 7, NumEntryPointOffsets: 1, @@ -113,6 +116,6 @@ func TestParseSliceHeader(t *testing.T) { } } if diff := deep.Equal(wantedHdr, gotHdr); diff != nil { - t.Errorf("Got Slice Header: %+v\n Diff is %v", gotHdr, diff) + t.Errorf("Got Slice Headers: %+v\n Diff is %v", gotHdr, diff) } } From abcbe5dffd1d28c48fcd3ee30c1bd154e3af8ca0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torbjo=CC=88rn=20Einarsson?= Date: Wed, 5 Feb 2025 23:56:08 +0100 Subject: [PATCH 2/2] feat: encrypt HEVC (hvc1) video --- CHANGELOG.md | 1 + avc/avc_test.go | 43 +++++++++ cmd/mp4ff-decrypt/doc.go | 6 +- cmd/mp4ff-decrypt/main.go | 1 + cmd/mp4ff-encrypt/doc.go | 16 ++-- cmd/mp4ff-encrypt/main.go | 2 + hevc/hevc.go | 7 +- hevc/hevc_test.go | 48 ++++++++++ mp4/crypto.go | 177 ++++++++++++++++++++++++++++-------- mp4/crypto_test.go | 17 ++-- mp4/sinf.go | 4 +- mp4/testdata/hvc1_init.mp4 | Bin 0 -> 3142 bytes mp4/testdata/hvc1_seg_1.m4s | Bin 0 -> 27780 bytes 13 files changed, 265 insertions(+), 57 deletions(-) create mode 100644 mp4/testdata/hvc1_init.mp4 create mode 100644 mp4/testdata/hvc1_seg_1.m4s diff --git a/CHANGELOG.md b/CHANGELOG.md index c46a6b67..aed311fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Support for SMPTE-2086 Mastering Display Metadata Box (SmDm) - Support for Content Light Level Box (CoLL) - Better test coverage for VisualSampleEntryBox +- IsVideoNaluType functions in both avc and hevc packages ### Fixed diff --git a/avc/avc_test.go b/avc/avc_test.go index 277b630e..948e6621 100644 --- a/avc/avc_test.go +++ b/avc/avc_test.go @@ -118,3 +118,46 @@ func TestGetParameterSets(t *testing.T) { } } } + +func TestIsVideoNaluType(t *testing.T) { + testCases := []struct { + name string + naluType NaluType + want bool + }{ + { + name: "video type - NALU_NON_IDR (1)", + naluType: NALU_NON_IDR, + want: true, + }, + { + name: "video type - NALU_IDR (5)", + naluType: NALU_IDR, + want: true, + }, + { + name: "non-video type - NALU_SEI (6)", + naluType: NALU_SEI, + want: false, + }, + { + name: "non-video type - NALU_SPS (7)", + naluType: NALU_SPS, + want: false, + }, + { + name: "non-video type - NALU_AUD (9)", + naluType: NALU_AUD, + want: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got := IsVideoNaluType(tc.naluType) + if got != tc.want { + t.Errorf("IsVideoNaluType(%d) = %v; want %v", tc.naluType, got, tc.want) + } + }) + } +} diff --git a/cmd/mp4ff-decrypt/doc.go b/cmd/mp4ff-decrypt/doc.go index 3eb5035a..74f3e0f8 100644 --- a/cmd/mp4ff-decrypt/doc.go +++ b/cmd/mp4ff-decrypt/doc.go @@ -8,10 +8,10 @@ mp4ff-decrypt [options] infile outfile options: -init string - Path to init file with encryption info (scheme, kid, pssh) + Path to init file with encryption info (scheme, kid, pssh) -key string - Required: key (32 hex or 24 base64 chars) + Required: key (32 hex or 24 base64 chars) -version - Get mp4ff version + Get mp4ff version */ package main diff --git a/cmd/mp4ff-decrypt/main.go b/cmd/mp4ff-decrypt/main.go index 3c0ac901..61b1bb62 100644 --- a/cmd/mp4ff-decrypt/main.go +++ b/cmd/mp4ff-decrypt/main.go @@ -74,6 +74,7 @@ func run(args []string) error { var outFilePath = fs.Arg(1) if opts.keyStr == "" { + fs.Usage() return fmt.Errorf("no key specified") } diff --git a/cmd/mp4ff-encrypt/doc.go b/cmd/mp4ff-encrypt/doc.go index 0741729a..19b24c4f 100644 --- a/cmd/mp4ff-encrypt/doc.go +++ b/cmd/mp4ff-encrypt/doc.go @@ -2,6 +2,8 @@ mp4ff-encrypt encrypts a fragmented mp4 file using Common Encryption with cenc or cbcs scheme. A combined fragmented file with init segment and media segment(s) will be encrypted. For a pure media segment, an init segment with encryption information is needed. +For video, only AVC with avc1 and HEVC with hvc1 sample entries are currently supported. +For audio, all supported audio codecs should work. Usage of mp4ff-encrypt: @@ -10,18 +12,18 @@ mp4ff-encrypt [options] infile outfile options: -init string - Path to init file with encryption info (scheme, kid, pssh) + Path to init file with encryption info (scheme, kid, pssh) -iv string - Required: iv (16 or 32 hex chars) + Required: iv (16 or 32 hex chars) -key string - Required: key (32 hex or 24 base64 chars) + Required: key (32 hex or 24 base64 chars) -kid string - key id (32 hex or 24 base64 chars). Required if initFilePath empty + key id (32 hex or 24 base64 chars). Required if initFilePath empty -pssh string - file with one or more pssh box(es) in binary format. Will be added at end of moov box + file with one or more pssh box(es) in binary format. Will be added at end of moov box -scheme string - cenc or cbcs. Required if initFilePath empty (default "cenc") + cenc or cbcs. Required if initFilePath empty (default "cenc") -version - Get mp4ff version + Get mp4ff version */ package main diff --git a/cmd/mp4ff-encrypt/main.go b/cmd/mp4ff-encrypt/main.go index 68e55ea1..4fe8a731 100644 --- a/cmd/mp4ff-encrypt/main.go +++ b/cmd/mp4ff-encrypt/main.go @@ -19,6 +19,8 @@ const ( var usg = `%s encrypts a fragmented mp4 file using Common Encryption with cenc or cbcs scheme. A combined fragmented file with init segment and media segment(s) will be encrypted. For a pure media segment, an init segment with encryption information is needed. +For video, only AVC with avc1 and HEVC with hvc1 sample entries are currently supported. +For audio, all supported audio codecs should work. Usage of %s: ` diff --git a/hevc/hevc.go b/hevc/hevc.go index 7d4ec0ea..9406b73f 100644 --- a/hevc/hevc.go +++ b/hevc/hevc.go @@ -122,13 +122,18 @@ func FindNaluTypesUpToFirstVideoNalu(sample []byte) []NaluType { naluType := GetNaluType(sample[pos]) naluList = append(naluList, naluType) pos += naluLength - if naluType <= highestVideoNaluType { + if IsVideoNaluType(naluType) { break // Video has started } } return naluList } +// IsVideoNaluType returns true if NaluType is a video type (<= 31) +func IsVideoNaluType(naluType NaluType) bool { + return naluType <= highestVideoNaluType +} + // ContainsNaluType - is specific NaluType present in sample func ContainsNaluType(sample []byte, specificNaluType NaluType) bool { var pos uint32 = 0 diff --git a/hevc/hevc_test.go b/hevc/hevc_test.go index 09941f05..b010937f 100644 --- a/hevc/hevc_test.go +++ b/hevc/hevc_test.go @@ -165,3 +165,51 @@ func TestNaluTypeStrings(t *testing.T) { t.Errorf("got %d named instead of 22", named) } } + +func TestIsVideoNaluType(t *testing.T) { + testCases := []struct { + name string + naluType NaluType + want bool + }{ + { + name: "video type - NALU_TRAIL_N (0)", + naluType: NALU_TRAIL_N, + want: true, + }, + { + name: "video type - NALU_TRAIL_R (1)", + naluType: NALU_TRAIL_R, + want: true, + }, + { + name: "video type - NALU_IDR_W_RADL (19)", + naluType: NALU_IDR_W_RADL, + want: true, + }, + { + name: "video type - highest (31)", + naluType: 31, + want: true, + }, + { + name: "non-video type - VPS (32)", + naluType: NALU_VPS, + want: false, + }, + { + name: "non-video type - SPS (33)", + naluType: NALU_SPS, + want: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got := IsVideoNaluType(tc.naluType) + if got != tc.want { + t.Errorf("IsVideoNaluType(%d) = %v; want %v", tc.naluType, got, tc.want) + } + }) + } +} diff --git a/mp4/crypto.go b/mp4/crypto.go index 2e74d4e9..ab110230 100644 --- a/mp4/crypto.go +++ b/mp4/crypto.go @@ -5,9 +5,9 @@ import ( "crypto/cipher" "encoding/binary" "fmt" - "log" "github.com/Eyevinn/mp4ff/avc" + "github.com/Eyevinn/mp4ff/hevc" ) type cryptoDir int @@ -19,7 +19,7 @@ const ( dirDec ) -// GetAVCProtectRanges for common encryption from a sample with 4-byt NALU lengths. +// GetAVCProtectRanges for common encryption from a sample with 4-byte NALU lengths. // THe spsMap and ppsMap are only needed for CBCS mode. // For scheme cenc, protection ranges must be a multiple of 16 bytes leaving header and some more in the clear // For scheme cbcs, protection range must start after the slice header. @@ -42,8 +42,7 @@ func GetAVCProtectRanges(spsMap map[uint32]*avc.SPS, ppsMap map[uint32]*avc.PPS, naluType := avc.GetNaluType(sample[pos]) var bytesToProtect uint32 = 0 clearEnd = pos + naluLength - if naluType < avc.NALU_SEI { - //VCL NALU + if avc.IsVideoNaluType(naluType) { nalu := sample[pos : pos+naluLength] switch scheme { case "cenc": @@ -80,6 +79,62 @@ func GetAVCProtectRanges(spsMap map[uint32]*avc.SPS, ppsMap map[uint32]*avc.PPS, return ssps, nil } +func GetHEVCProtectRanges(spsMap map[uint32]*hevc.SPS, ppsMap map[uint32]*hevc.PPS, + sample []byte, scheme string) ([]SubSamplePattern, error) { + var ssps []SubSamplePattern + length := len(sample) + if length < 4 { + return nil, fmt.Errorf("less than 4 bytes, No NALUs") + } + var pos uint32 = 0 + clearStart := uint32(0) + clearEnd := uint32(0) + for pos < uint32(length-4) { + naluLength := binary.BigEndian.Uint32(sample[pos : pos+4]) + pos += 4 + if int(pos+naluLength) > len(sample) { + return nil, fmt.Errorf("NALU length fields are bad") + } + naluType := hevc.GetNaluType(sample[pos]) + var bytesToProtect uint32 = 0 + clearEnd = pos + naluLength + if hevc.IsVideoNaluType(naluType) { + nalu := sample[pos : pos+naluLength] + switch scheme { + case "cenc": + if naluLength+naluHdrLen >= minClearSize+16 { + // Calculate a multiple of 16 bytes to protect + bytesToProtect = (naluLength + naluHdrLen - minClearSize) & 0xfffffff0 + if bytesToProtect > 0 { + clearEnd -= bytesToProtect + } + } + case "cbcs": + sh, err := hevc.ParseSliceHeader(nalu, spsMap, ppsMap) + if err != nil { + return nil, err + } + clearHeadSize := uint32(sh.Size) + clearEnd = pos + clearHeadSize + bytesToProtect = naluLength - clearHeadSize + default: + return nil, fmt.Errorf("unknown protect scheme %s", scheme) + } + } + if bytesToProtect > 0 { + ssps = appendProtectRange(ssps, clearEnd-clearStart, bytesToProtect) + clearStart = clearEnd + bytesToProtect + clearEnd = clearStart + } + + pos += naluLength + } + if clearEnd > clearStart { + ssps = appendProtectRange(ssps, clearEnd-clearStart, 0) + } + return ssps, nil +} + // appendProtectRange appends a SubSamplePattern to a slice of SubSamplePattern, splitting into multiple if needed. func appendProtectRange(ssps []SubSamplePattern, nrClear, nrProtected uint32) []SubSamplePattern { for { @@ -258,12 +313,13 @@ func InitProtect(init *InitSegment, key, iv []byte, scheme string, kid UUID, pss iv = make([]byte, 16) copy(iv, iv8) } - var err error ipd.Trex = moov.Mvex.Trex sinf := SinfBox{} + var mediaType string switch se := stsd.Children[0].(type) { case *VisualSampleEntryBox: + mediaType = "video" veType := se.Type() se.SetType("encv") frma := FrmaBox{DataFormat: veType} @@ -275,36 +331,21 @@ func InitProtect(init *InitSegment, key, iv []byte, scheme string, kid UUID, pss if err != nil { return nil, fmt.Errorf("get avc protect func: %w", err) } - switch scheme { - case "cenc": - ipd.Tenc = &TencBox{Version: 0, DefaultIsProtected: 1, DefaultPerSampleIVSize: 16, DefaultKID: kid} - case "cbcs": - ipd.Tenc = &TencBox{Version: 1, DefaultCryptByteBlock: 1, DefaultSkipByteBlock: 9, - DefaultIsProtected: 1, DefaultPerSampleIVSize: 0, DefaultKID: kid, - DefaultConstantIV: iv} - default: - return nil, fmt.Errorf("unknown protection mode %s", scheme) + case "hvc1", "hev1": + ipd.ProtFunc, err = getHEVCProtFunc(se.HvcC) + if err != nil { + return nil, fmt.Errorf("get hevc protect func: %w", err) } - default: return nil, fmt.Errorf("visual sample entry type %s not yet supported", veType) } case *AudioSampleEntryBox: + mediaType = "audio" aeType := se.Type() se.SetType("enca") frma := FrmaBox{DataFormat: aeType} sinf.AddChild(&frma) se.AddChild(&sinf) - switch scheme { - case "cenc": - ipd.Tenc = &TencBox{Version: 0, DefaultIsProtected: 1, DefaultPerSampleIVSize: 16, DefaultKID: kid} - case "cbcs": - ipd.Tenc = &TencBox{Version: 1, DefaultCryptByteBlock: 0, DefaultSkipByteBlock: 0, - DefaultIsProtected: 1, DefaultPerSampleIVSize: 0, DefaultKID: kid, - DefaultConstantIV: iv} - default: - return nil, fmt.Errorf("unknown protection scheme %s", scheme) - } ipd.ProtFunc = getAudioProtectRanges default: return nil, fmt.Errorf("sample entry type %s should not be encrypted", se.Type()) @@ -312,8 +353,19 @@ func InitProtect(init *InitSegment, key, iv []byte, scheme string, kid UUID, pss schi := SchiBox{} switch scheme { case "cenc": + ipd.Tenc = &TencBox{Version: 0, DefaultIsProtected: 1, DefaultPerSampleIVSize: 16, DefaultKID: kid} sinf.AddChild(&SchmBox{SchemeType: "cenc", SchemeVersion: 65536}) case "cbcs": + switch mediaType { + case "video": + ipd.Tenc = &TencBox{Version: 1, DefaultCryptByteBlock: 1, DefaultSkipByteBlock: 9, + DefaultIsProtected: 1, DefaultPerSampleIVSize: 0, DefaultKID: kid, + DefaultConstantIV: iv} + case "audio": + ipd.Tenc = &TencBox{Version: 1, DefaultCryptByteBlock: 0, DefaultSkipByteBlock: 0, + DefaultIsProtected: 1, DefaultPerSampleIVSize: 0, DefaultKID: kid, + DefaultConstantIV: iv} + } sinf.AddChild(&SchmBox{SchemeType: "cbcs", SchemeVersion: 65536}) default: return nil, fmt.Errorf("unknown protection scheme %s", scheme) @@ -326,38 +378,81 @@ func InitProtect(init *InitSegment, key, iv []byte, scheme string, kid UUID, pss return &ipd, nil } -func getSPSMap(spss [][]byte) map[uint32]*avc.SPS { +func getAVCPSMaps(spss [][]byte, ppss [][]byte) (map[uint32]*avc.SPS, map[uint32]*avc.PPS, error) { spsMap := make(map[uint32]*avc.SPS, 1) for _, spsNalu := range spss { sps, err := avc.ParseSPSNALUnit(spsNalu, false) if err != nil { - log.Fatal(err) + return nil, nil, err } spsMap[sps.ParameterID] = sps } - return spsMap -} - -func getPPSMap(ppss [][]byte, spsMap map[uint32]*avc.SPS) map[uint32]*avc.PPS { ppsMap := make(map[uint32]*avc.PPS, 1) for _, ppsNalu := range ppss { pps, err := avc.ParsePPSNALUnit(ppsNalu, spsMap) if err != nil { - log.Fatal(err) + return nil, nil, err } ppsMap[pps.PicParameterSetID] = pps } - return ppsMap + return spsMap, ppsMap, nil } func getAVCProtFunc(avcC *AvcCBox) (ProtectionRangeFunc, error) { - spsMap := getSPSMap(avcC.SPSnalus) - ppsMap := getPPSMap(avcC.PPSnalus, spsMap) + spsMap, ppsMap, err := getAVCPSMaps(avcC.SPSnalus, avcC.PPSnalus) + if err != nil { + return nil, fmt.Errorf("get avc ps maps: %w", err) + } return func(sample []byte, scheme string) ([]SubSamplePattern, error) { return GetAVCProtectRanges(spsMap, ppsMap, sample, scheme) }, nil } +func getHEVCPSMaps(arrays []hevc.NaluArray) (map[uint32]*hevc.SPS, map[uint32]*hevc.PPS, error) { + // First check SPS + spsMap := make(map[uint32]*hevc.SPS, 1) + for _, naluArray := range arrays { + naluType := naluArray.NaluType() + switch naluType { + case hevc.NALU_SPS: + for _, nalu := range naluArray.Nalus { + sps, err := hevc.ParseSPSNALUnit(nalu) + if err != nil { + return nil, nil, err + } + spsMap[uint32(sps.SpsID)] = sps + } + } + } + // Then check PPS + ppsMap := make(map[uint32]*hevc.PPS, 1) + for _, naluArray := range arrays { + naluType := naluArray.NaluType() + switch naluType { + case hevc.NALU_PPS: + for _, nalu := range naluArray.Nalus { + pps, err := hevc.ParsePPSNALUnit(nalu, spsMap) + if err != nil { + return nil, nil, err + } + ppsMap[pps.PicParameterSetID] = pps + } + } + } + return spsMap, ppsMap, nil +} + +func getHEVCProtFunc(hvcC *HvcCBox) (ProtectionRangeFunc, error) { + return func(sample []byte, scheme string) ([]SubSamplePattern, error) { + spsMap, ppsMap, err := getHEVCPSMaps(hvcC.NaluArrays) + if err != nil { + return nil, fmt.Errorf("get hevc ps maps: %w", err) + } + return GetHEVCProtectRanges(spsMap, ppsMap, sample, scheme) + }, nil + +} + func EncryptFragment(f *Fragment, key, iv []byte, ipd *InitProtectData) error { if ipd == nil { return fmt.Errorf("no protection data") @@ -402,7 +497,7 @@ func EncryptFragment(f *Fragment, key, iv []byte, ipd *InitProtectData) error { sample := fs.Data subsamplePatterns, err := ipd.ProtFunc(sample, ipd.Scheme) if err != nil { - return fmt.Errorf("get avc protect ranges: %w", err) + return fmt.Errorf("get protect ranges: %w", err) } switch ipd.Scheme { case "cenc": @@ -663,12 +758,18 @@ func ExtractInitProtectData(inSeg *InitSegment) (*InitProtectData, error) { case *VisualSampleEntryBox: sinf = box.Sinf frma := sinf.Frma - if frma.DataFormat == "avc1" { + switch frma.DataFormat { + case "avc1": ipd.ProtFunc, err = getAVCProtFunc(box.AvcC) if err != nil { return nil, fmt.Errorf("get AVC protect func: %w", err) } - } else { + case "hvc1": + ipd.ProtFunc, err = getHEVCProtFunc(box.HvcC) + if err != nil { + return nil, fmt.Errorf("get HEVC protect func: %w", err) + } + default: return nil, fmt.Errorf("unsupported video codec descriptor %s", frma.DataFormat) } case *AudioSampleEntryBox: diff --git a/mp4/crypto_test.go b/mp4/crypto_test.go index 20381aae..3dc076ea 100644 --- a/mp4/crypto_test.go +++ b/mp4/crypto_test.go @@ -96,8 +96,10 @@ func TestFindAVCSubsampleRanges(t *testing.T) { } func TestEncryptDecrypt(t *testing.T) { - videoInit := "testdata/init.mp4" - videoSeg := "testdata/1.m4s" + videoAVCInit := "testdata/init.mp4" + videoAVCSeg := "testdata/1.m4s" + videoHEVCInit := "testdata/hvc1_init.mp4" + videoHEVCSeg := "testdata/hvc1_seg_1.m4s" audioInit := "testdata/aac_init.mp4" audioSeg := "testdata/aac_1.m4s" keyHex := "00112233445566778899aabbccddeeff" @@ -125,10 +127,13 @@ func TestEncryptDecrypt(t *testing.T) { iv string hasPssh bool }{ - {desc: "video, cenc, iv8", init: videoInit, seg: videoSeg, scheme: "cenc", iv: ivHex8}, - {desc: "video, cbcs, iv8", init: videoInit, seg: videoSeg, scheme: "cbcs", iv: ivHex8}, - {desc: "video, cbcs, iv16", init: videoInit, seg: videoSeg, scheme: "cbcs", iv: ivHex16}, - {desc: "audio, cbcs, iv16", init: audioInit, seg: audioSeg, scheme: "cbcs", iv: ivHex16, hasPssh: true}, + {desc: "video AVC cenc iv8", init: videoAVCInit, seg: videoAVCSeg, scheme: "cenc", iv: ivHex8}, + {desc: "video AVC cbcs iv8", init: videoAVCInit, seg: videoAVCSeg, scheme: "cbcs", iv: ivHex8}, + {desc: "video AVC cbcs iv16", init: videoAVCInit, seg: videoAVCSeg, scheme: "cbcs", iv: ivHex16}, + {desc: "video HEVC cenc iv8", init: videoHEVCInit, seg: videoHEVCSeg, scheme: "cenc", iv: ivHex8}, + {desc: "video HEVC cbcs iv8", init: videoHEVCInit, seg: videoHEVCSeg, scheme: "cbcs", iv: ivHex8}, + {desc: "video HEVC cbcs iv16", init: videoHEVCInit, seg: videoHEVCSeg, scheme: "cbcs", iv: ivHex16}, + {desc: "audio AAC cbcs iv16", init: audioInit, seg: audioSeg, scheme: "cbcs", iv: ivHex16, hasPssh: true}, } for _, c := range testCases { t.Run(c.desc, func(t *testing.T) { diff --git a/mp4/sinf.go b/mp4/sinf.go index c9362fe5..b76a40b6 100644 --- a/mp4/sinf.go +++ b/mp4/sinf.go @@ -68,12 +68,12 @@ func (b *SinfBox) GetChildren() []Box { return b.Children } -// Encode - write minf container to w +// Encode - write sinf container to w func (b *SinfBox) Encode(w io.Writer) error { return EncodeContainer(b, w) } -// Encode - write minf container to sw +// Encode - write sinf container to sw func (b *SinfBox) EncodeSW(sw bits.SliceWriter) error { return EncodeContainerSW(b, sw) } diff --git a/mp4/testdata/hvc1_init.mp4 b/mp4/testdata/hvc1_init.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..c19877d060815bab205fb0da78cc7fa5130a2b3f GIT binary patch literal 3142 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

|bax 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 literal 0 HcmV?d00001