diff --git a/README.md b/README.md index 78f23d62..b9b9ea43 100644 --- a/README.md +++ b/README.md @@ -200,6 +200,7 @@ The examples are: This tool has been extended to support generation of segments with multiple tracks as well as reading and writing `mdat` in lazy mode 4. `multitrack` parses a fragmented file with multiple tracks +5. `decrypt-cenc` decrypts a segmented mp4 file encrypted in `cenc` mode ## Stability The APIs should be fairly stable, but minor non-backwards-compatible changes may happen until version 1. diff --git a/examples/decrypt-cenc/decrypt_cenc_test.go b/examples/decrypt-cenc/decrypt_cenc_test.go index b9cdc791..75130a4a 100644 --- a/examples/decrypt-cenc/decrypt_cenc_test.go +++ b/examples/decrypt-cenc/decrypt_cenc_test.go @@ -1,13 +1,34 @@ package main -import "testing" +import ( + "bytes" + "io/ioutil" + "os" + "testing" + + "github.com/go-test/deep" +) func TestDecodeCenc(t *testing.T) { inFile := "testdata/prog_8s_enc_dashinit.mp4" - outFile := "testdata/dec.mp4" + expectedOutFile := "testdata/prog_8s_dec_dashinit.mp4" hexString := "63cb5f7184dd4b689a5c5ff11ee6a328" - err := start(inFile, outFile, hexString) + ifh, err := os.Open(inFile) + if err != nil { + t.Error(err) + } + buf := bytes.Buffer{} + err = start(ifh, &buf, hexString) if err != nil { t.Error(err) } + expectedOut, err := ioutil.ReadFile(expectedOutFile) + if err != nil { + t.Error(err) + } + gotOut := buf.Bytes() + diff := deep.Equal(expectedOut, gotOut) + if diff != nil { + t.Errorf("Mismatch: %s", diff) + } } diff --git a/examples/decrypt-cenc/main.go b/examples/decrypt-cenc/main.go index 3f3913e0..20c32745 100644 --- a/examples/decrypt-cenc/main.go +++ b/examples/decrypt-cenc/main.go @@ -1,3 +1,8 @@ +// decrypt-cenc - decrypt a segmented mp4 file encrypted in cenc mode +// +// The output is in the same format as the input but with samples decrypted +// and encryption information boxes such as pssh and schm removed. +// An example file is given in testdata/prog_8s_enc_dashinit.mp4 package main import ( @@ -13,25 +18,26 @@ import ( ) func main() { - inFilePath := flag.String("i", "", "Required: Path to input file") outFilePath := flag.String("o", "", "Required: Output file") hexKey := flag.String("k", "", "Required: key (hex)") - err := start(*inFilePath, *outFilePath, *hexKey) + ifh, err := os.Open(*inFilePath) + if err != nil { + log.Fatal(err) + } + defer ifh.Close() + ofh, err := os.Create(*outFilePath) + if err != nil { + log.Fatal(err) + } + err = start(ifh, ofh, *hexKey) if err != nil { log.Fatalln(err) } - } -func start(inPath, outPath, hexKey string) error { - - ifh, err := os.Open(inPath) - if err != nil { - return err - } - defer ifh.Close() +func start(r io.Reader, w io.Writer, hexKey string) error { if len(hexKey) != 32 { return fmt.Errorf("Hex key must have length 32 chars") @@ -41,7 +47,7 @@ func start(inPath, outPath, hexKey string) error { return err } - err = decryptMP4withCenc(ifh, key, outPath) + err = decryptMP4withCenc(r, key, w) if err != nil { return err } @@ -64,7 +70,7 @@ func findTrackInfo(tracks []trackInfo, trackID uint32) trackInfo { } // decryptMP4withCenc - decrypt segmented mp4 file with CENC encryption -func decryptMP4withCenc(r io.Reader, key []byte, outPath string) error { +func decryptMP4withCenc(r io.Reader, key []byte, w io.Writer) error { inMp4, err := mp4.DecodeFile(r) if err != nil { return err @@ -73,11 +79,6 @@ func decryptMP4withCenc(r io.Reader, key []byte, outPath string) error { return fmt.Errorf("file not fragmented. Not supported") } - ofh, err := os.Create(outPath) - if err != nil { - return err - } - tracks := make([]trackInfo, 0, len(inMp4.Init.Moov.Traks)) moov := inMp4.Init.Moov @@ -135,16 +136,15 @@ func decryptMP4withCenc(r io.Reader, key []byte, outPath string) error { } // Write the modified init segment - err = inMp4.Init.Encode(ofh) + err = inMp4.Init.Encode(w) if err != nil { return err } - err = decryptAndWriteSegments(inMp4.Segments, tracks, key, ofh) + err = decryptAndWriteSegments(inMp4.Segments, tracks, key, w) if err != nil { return err } - ofh.Close() return nil } @@ -176,13 +176,24 @@ func decryptFragment(frag *mp4.Fragment, tracks []trackInfo, key []byte) error { moof := frag.Moof var nrBytesRemoved uint64 = 0 for _, traf := range moof.Trafs { - senc := traf.Senc + hasSenc, isParsed := traf.ContainsSencBox() + if !hasSenc { + return fmt.Errorf("no senc box in traf") + } ti := findTrackInfo(tracks, traf.Tfhd.TrackID) + if !isParsed { + defaultIVSize := ti.sinf.Schi.Tenc.DefaultPerSampleIVSize + err := traf.ParseReadSenc(defaultIVSize, moof.StartPos) + if err != nil { + return fmt.Errorf("parseReadSenc: %w", err) + } + } samples, err := frag.GetFullSamples(ti.trex) if err != nil { return err } - err = decryptSamplesInPlace(samples, key, senc) + + err = decryptSamplesInPlace(samples, key, traf.Senc) if err != nil { return err } diff --git a/examples/decrypt-cenc/testdata/prog_8s_dec_dashinit.mp4 b/examples/decrypt-cenc/testdata/prog_8s_dec_dashinit.mp4 new file mode 100644 index 00000000..cc97656c Binary files /dev/null and b/examples/decrypt-cenc/testdata/prog_8s_dec_dashinit.mp4 differ diff --git a/mp4/file.go b/mp4/file.go index bb1ebc11..2724a4e5 100644 --- a/mp4/file.go +++ b/mp4/file.go @@ -162,12 +162,31 @@ LoopBoxes: if err != nil { return nil, err } - if boxType == "mdat" { + switch boxType { + case "mdat": if f.isFragmented { if lastBoxType != "moof" { return nil, fmt.Errorf("Does not support %v between moof and mdat", lastBoxType) } } + case "moof": + moof := box.(*MoofBox) + for _, traf := range moof.Trafs { + if ok, parsed := traf.ContainsSencBox(); ok && !parsed { + defaultIVSize := byte(0) // Should get this from tenc in sinf + if f.Moov != nil { + trackID := traf.Tfhd.TrackID + sinf := f.Moov.GetSinf(trackID) + if sinf != nil && sinf.Schi != nil && sinf.Schi.Tenc != nil { + defaultIVSize = sinf.Schi.Tenc.DefaultPerSampleIVSize + } + } + err = traf.ParseReadSenc(defaultIVSize, moof.StartPos) + if err != nil { + return nil, err + } + } + } } f.AddChild(box, boxStartPos) lastBoxType = boxType diff --git a/mp4/moov.go b/mp4/moov.go index 0b2a2c4d..82c5193c 100644 --- a/mp4/moov.go +++ b/mp4/moov.go @@ -15,6 +15,7 @@ type MoovBox struct { Pssh *PsshBox Psshs []*PsshBox Children []Box + StartPos uint64 } // NewMoovBox - Generate a new empty moov box @@ -24,7 +25,6 @@ func NewMoovBox() *MoovBox { // AddChild - Add a child box func (m *MoovBox) AddChild(box Box) { - switch box.Type() { case "mvhd": m.Mvhd = box.(*MvhdBox) @@ -62,6 +62,7 @@ func DecodeMoov(hdr *boxHeader, startPos uint64, r io.Reader) (Box, error) { return nil, err } m := NewMoovBox() + m.StartPos = startPos for _, b := range l { m.AddChild(b) } @@ -111,3 +112,19 @@ func (m *MoovBox) RemovePsshs() []*PsshBox { return psshs } + +func (m *MoovBox) GetSinf(trackID uint32) *SinfBox { + for _, trak := range m.Traks { + if trak.Tkhd.TrackID == trackID { + stsd := trak.Mdia.Minf.Stbl.Stsd + sd := stsd.Children[0] // Get first (and only) + if visual, ok := sd.(*VisualSampleEntryBox); ok { + return visual.Sinf + } + if audio, ok := sd.(*AudioSampleEntryBox); ok { + return audio.Sinf + } + } + } + return nil +} diff --git a/mp4/saio.go b/mp4/saio.go index c36b7c3e..b7512c4f 100644 --- a/mp4/saio.go +++ b/mp4/saio.go @@ -5,7 +5,7 @@ import ( "io/ioutil" ) -// SaioBox - Sample Auxiliary Information Offsets Box (saiz) +// SaioBox - Sample Auxiliary Information Offsets Box (saiz) (in stbl or traf box) type SaioBox struct { Version byte Flags uint32 @@ -101,8 +101,9 @@ func (b *SaioBox) Info(w io.Writer, specificBoxLevels, indent, indentStep string } bd.write(" - sampleCount: %d", len(b.Offset)) level := getInfoLevel(b, specificBoxLevels) + bd.write(" - offset[%d]=%d", 1, b.Offset[0]) if level > 0 { - for i := 0; i < len(b.Offset); i++ { + for i := 1; i < len(b.Offset); i++ { bd.write(" - offset[%d]=%d", i+1, b.Offset[i]) } } diff --git a/mp4/saiz.go b/mp4/saiz.go index 752c53d5..cf87a788 100644 --- a/mp4/saiz.go +++ b/mp4/saiz.go @@ -5,7 +5,7 @@ import ( "io/ioutil" ) -// SaizBox - Sample Auxiliary Information Sizes Box (saiz) +// SaizBox - Sample Auxiliary Information Sizes Box (saiz) (in stbl or traf box) type SaizBox struct { Version byte Flags uint32 diff --git a/mp4/schm.go b/mp4/schm.go index cd2fdf9c..0855cc8d 100644 --- a/mp4/schm.go +++ b/mp4/schm.go @@ -76,7 +76,7 @@ func (b *SchmBox) Encode(w io.Writer) error { func (b *SchmBox) Info(w io.Writer, specificBoxLevels, indent, indentStep string) (err error) { bd := newInfoDumper(w, indent, b, int(b.Version), b.Flags) bd.write(" - schemeType: %s", b.SchemeType) - bd.write(" - schemeVersion: %d", b.SchemeVersion) + bd.write(" - schemeVersion: %d (%d.%d)", b.SchemeVersion, b.SchemeVersion>>16, b.SchemeVersion&0xffff) if b.Flags&0x01 != 0 { bd.write(" - schemeURI: %q", b.SchemeURI) } diff --git a/mp4/senc.go b/mp4/senc.go index 3258ec98..49fa5466 100644 --- a/mp4/senc.go +++ b/mp4/senc.go @@ -1,6 +1,7 @@ package mp4 import ( + "encoding/binary" "encoding/hex" "fmt" "io" @@ -19,15 +20,21 @@ type SubSamplePattern struct { // InitializationVector (8 or 16 bytes) type InitializationVector []byte -// SencBox - Sample Encryption Box (senc) -// See ISO/IEC 23001-7 Section 7.2 +// SencBox - Sample Encryption Box (senc) (in trak or traf box) +// Should only be decoded after saio and saiz provide relevant offset and sizes +// Here we make a two-step decode, with first step reading, and other parsing. +// See ISO/IEC 23001-7 Section 7.2 and CMAF specification // Full Box + SampleCount type SencBox struct { - Version byte - Flags uint32 - SampleCount uint32 - IVs []InitializationVector // 8 or 16 bytes if present - SubSamples [][]SubSamplePattern + Version byte + readButNotParsed bool + perSampleIVSize byte + Flags uint32 + SampleCount uint32 + StartPos uint64 + rawData []byte + IVs []InitializationVector // 8 or 16 bytes if present + SubSamples [][]SubSamplePattern } // CreateSencBox - create an empty SencBox @@ -43,11 +50,14 @@ type SencSample struct { // AddSample - add a senc sample with possible IV and subsamples func (s *SencBox) AddSample(sample SencSample) error { - if s.SampleCount > 0 { - if len(sample.IV) != s.GetPerSampleIVSize() { - return fmt.Errorf("Mix of PerSampleIV lengths") + if s.SampleCount == 0 { + s.perSampleIVSize = byte(len(sample.IV)) + } else { + if len(sample.IV) != int(s.perSampleIVSize) { + return fmt.Errorf("mix of IV lengths") } } + if len(sample.IV) != 0 { s.IVs = append(s.IVs, sample.IV) } @@ -65,60 +75,93 @@ func DecodeSenc(hdr *boxHeader, startPos uint64, r io.Reader) (Box, error) { if err != nil { return nil, err } - s := NewSliceReader(data) - versionAndFlags := s.ReadUint32() - senc := &SencBox{ - Version: byte(versionAndFlags >> 24), - Flags: versionAndFlags & flagsMask, - SampleCount: s.ReadUint32(), - } + versionAndFlags := binary.BigEndian.Uint32(data[0:4]) + sampleCount := binary.BigEndian.Uint32(data[4:8]) - if senc.SampleCount == 0 { - return senc, nil + senc := SencBox{ + Version: byte(versionAndFlags >> 24), + rawData: data[8:], // After the first 8 bytes of box content + Flags: versionAndFlags & flagsMask, + StartPos: startPos, + SampleCount: sampleCount, + readButNotParsed: true, } - // We now deduce the PerSampleIVSize from the rest of the content - // by trial and error but it should be available in the saiz box + if senc.SampleCount == 0 || len(senc.rawData) == 0 { + senc.readButNotParsed = false + return &senc, nil + } + return &senc, nil +} - nrBytesLeft := uint32(s.NrRemainingBytes()) +// ParseReadBox - second phase when perSampleIVSize should be known from tenc or sgpd boxes +// if perSampleIVSize is 0, we try to find the appropriate error given data length +func (s *SencBox) ParseReadBox(perSampleIVSize byte, saiz *SaizBox) error { + if !s.readButNotParsed { + if perSampleIVSize != 0 { + s.perSampleIVSize = byte(perSampleIVSize) + } + return fmt.Errorf("senc box already parsed") + } + sr := NewSliceReader(s.rawData) + nrBytesLeft := uint32(sr.NrRemainingBytes()) - if senc.Flags&UseSubSampleEncryption == 0 { + if s.Flags&UseSubSampleEncryption == 0 { // No subsamples - perSampleIVSize := uint16(nrBytesLeft / senc.SampleCount) - senc.IVs = make([]InitializationVector, 0, senc.SampleCount) + if perSampleIVSize == 0 { // Infer the size + perSampleIVSize = byte(nrBytesLeft / s.SampleCount) + } + + s.IVs = make([]InitializationVector, 0, s.SampleCount) switch perSampleIVSize { case 0: // Nothing to do case 8: - for i := 0; i < int(senc.SampleCount); i++ { - senc.IVs = append(senc.IVs, s.ReadBytes(8)) + for i := 0; i < int(s.SampleCount); i++ { + s.IVs = append(s.IVs, sr.ReadBytes(8)) } case 16: - for i := 0; i < int(senc.SampleCount); i++ { - senc.IVs = append(senc.IVs, s.ReadBytes(16)) + for i := 0; i < int(s.SampleCount); i++ { + s.IVs = append(s.IVs, sr.ReadBytes(16)) } default: - return nil, fmt.Errorf("Strange derived PerSampleIvSize: %d", perSampleIVSize) - } - } else { // Now we have 6 bytes of subsamplecount per subsample - startPos := s.GetPos() - for perSampleIVSize := 0; perSampleIVSize <= 16; perSampleIVSize += 8 { - s.SetPos(startPos) - ok := senc.parseAndFillSamples(s, perSampleIVSize) - if ok { - break // We have found a working perSampleIVSize - } + return fmt.Errorf("Strange derived PerSampleIVSize: %d", perSampleIVSize) + } + s.readButNotParsed = false + return nil + } + // 6 bytes of subsamplecount per subsample and known perSampleIVSize + // The total length for each sample should correspond to + // sizes in saiz (defaultSampleInfoSize or SampleInfo value) + // We don't check that though, but it could be implemented here. + if perSampleIVSize != 0 { + if ok := s.parseAndFillSamples(sr, perSampleIVSize); !ok { + return fmt.Errorf("error decoding senc with perSampleIVSize = %d", perSampleIVSize) } - if err != nil { - return nil, fmt.Errorf("Could not decode senc") + s.readButNotParsed = false + return nil + } + + // Finally, 6 bytes of subsamplecount per subsample and unknown perSampleIVSize + startPos := sr.GetPos() + ok := false + for perSampleIVSize := byte(0); perSampleIVSize <= 16; perSampleIVSize += 8 { + sr.SetPos(startPos) + ok = s.parseAndFillSamples(sr, perSampleIVSize) + if ok { + break // We have found a working perSampleIVSize } } - return senc, nil + if !ok { + return fmt.Errorf("Could not decode senc") + } + s.readButNotParsed = false + return nil } // parseAndFillSamples - parse and fill senc samples given perSampleIVSize -func (s *SencBox) parseAndFillSamples(sr *SliceReader, perSampleIVSize int) (ok bool) { +func (s *SencBox) parseAndFillSamples(sr *SliceReader, perSampleIVSize byte) (ok bool) { ok = true s.SubSamples = make([][]SubSamplePattern, s.SampleCount) for i := 0; i < int(s.SampleCount); i++ { @@ -127,7 +170,7 @@ func (s *SencBox) parseAndFillSamples(sr *SliceReader, perSampleIVSize int) (ok ok = false break } - s.IVs = append(s.IVs, sr.ReadBytes(perSampleIVSize)) + s.IVs = append(s.IVs, sr.ReadBytes(int(perSampleIVSize))) } if sr.NrRemainingBytes() < 2 { ok = false @@ -150,6 +193,7 @@ func (s *SencBox) parseAndFillSamples(sr *SliceReader, perSampleIVSize int) (ok s.SubSamples = nil ok = false } + s.perSampleIVSize = byte(perSampleIVSize) return ok } @@ -160,6 +204,9 @@ func (s *SencBox) Type() string { // Size - box-specific type func (s *SencBox) Size() uint64 { + if s.readButNotParsed { + return boxHeaderSize + 8 + uint64(len(s.rawData)) // read 8 bytes after header + } totalSize := boxHeaderSize + 8 perSampleIVSize := s.GetPerSampleIVSize() for i := 0; i < int(s.SampleCount); i++ { @@ -209,6 +256,11 @@ func (s *SencBox) Encode(w io.Writer) error { // Info - write box-specific information func (s *SencBox) Info(w io.Writer, specificBoxLevels, indent, indentStep string) error { bd := newInfoDumper(w, indent, s, int(s.Version), s.Flags) + bd.write(" - sampleCount: %d", s.SampleCount) + if s.readButNotParsed { + bd.write(" - NOT YET PARSED, call ParseReadBox to parse it") + return nil + } for _, subSamples := range s.SubSamples { if len(subSamples) > 0 { s.Flags |= UseSubSampleEncryption @@ -216,7 +268,6 @@ func (s *SencBox) Info(w io.Writer, specificBoxLevels, indent, indentStep string } perSampleIVSize := s.GetPerSampleIVSize() bd.write(" - perSampleIVSize: %d", perSampleIVSize) - bd.write(" - sampleCount: %d", s.SampleCount) level := getInfoLevel(s, specificBoxLevels) if level > 0 { for i := 0; i < int(s.SampleCount); i++ { @@ -238,12 +289,5 @@ func (s *SencBox) Info(w io.Writer, specificBoxLevels, indent, indentStep string // GetPerSampleIVSize - return perSampleIVSize func (s *SencBox) GetPerSampleIVSize() int { - perSampleIVSize := 0 - for _, iv := range s.IVs { - if len(iv) != 0 { - perSampleIVSize = len(iv) - break - } - } - return perSampleIVSize + return int(s.perSampleIVSize) } diff --git a/mp4/senc_test.go b/mp4/senc_test.go index 05574ab9..1ed21d08 100644 --- a/mp4/senc_test.go +++ b/mp4/senc_test.go @@ -1,52 +1,78 @@ package mp4 -import "testing" +import ( + "bytes" + "testing" + + "github.com/go-test/deep" +) func TestSencDirectValues(t *testing.T) { iv8 := InitializationVector("12345678") iv16 := InitializationVector("0123456789abcdef") sencBoxes := []*SencBox{ { - Version: 0, - Flags: 0, - }, - { - Version: 0, - Flags: 0, - SampleCount: 1, - IVs: []InitializationVector{iv8}, + Version: 0, + Flags: 0, + SampleCount: 431, // No perSampleIVs + perSampleIVSize: 0, }, { - Version: 0, - Flags: 0, - SampleCount: 1, - SubSamples: [][]SubSamplePattern{{{10, 1000}}}, + Version: 0, + Flags: 0, + SampleCount: 1, + perSampleIVSize: 8, + IVs: []InitializationVector{iv8}, + SubSamples: [][]SubSamplePattern{{{10, 1000}}}, }, { - Version: 0, - Flags: 0, - SampleCount: 1, - IVs: []InitializationVector{iv8}, - SubSamples: [][]SubSamplePattern{{{10, 1000}}}, + Version: 0, + Flags: 0, + SampleCount: 1, + perSampleIVSize: 16, + IVs: []InitializationVector{iv16}, + SubSamples: [][]SubSamplePattern{{{10, 1000}, {20, 2000}}}, }, { - Version: 0, - Flags: 0, - SampleCount: 1, - IVs: []InitializationVector{iv16}, - SubSamples: [][]SubSamplePattern{{{10, 1000}, {20, 2000}}}, - }, - { - Version: 0, - Flags: 0, - SampleCount: 2, - IVs: []InitializationVector{iv16, iv16}, - SubSamples: [][]SubSamplePattern{{{10, 1000}}, {{20, 2000}}}, + Version: 0, + Flags: 0, + SampleCount: 2, + perSampleIVSize: 16, + IVs: []InitializationVector{iv16, iv16}, + SubSamples: [][]SubSamplePattern{{{10, 1000}}, {{20, 2000}}}, }, } for _, senc := range sencBoxes { - boxDiffAfterEncodeAndDecode(t, senc) + sencDiffAfterEncodeAndDecode(t, senc, 0) + sencDiffAfterEncodeAndDecode(t, senc, senc.perSampleIVSize) + } +} + +func sencDiffAfterEncodeAndDecode(t *testing.T, senc *SencBox, perSampleIVSize byte) { + t.Helper() + buf := bytes.Buffer{} + err := senc.Encode(&buf) + if err != nil { + t.Error(err) + } + + boxDec, err := DecodeBox(0, &buf) + if err != nil { + t.Error(err) + } + decSenc := boxDec.(*SencBox) + var saizBox *SaizBox + + if decSenc.readButNotParsed { + err = decSenc.ParseReadBox(perSampleIVSize, saizBox) + if err != nil { + t.Error(err) + } + } + + if diff := deep.Equal(decSenc, senc); diff != nil { + t.Error(diff) } } @@ -58,34 +84,39 @@ func TestAddSamples(t *testing.T) { senc := CreateSencBox() err := senc.AddSample(SencSample{iv0, []SubSamplePattern{{10, 1000}}}) assertNoError(t, err) - boxDiffAfterEncodeAndDecode(t, senc) + sencDiffAfterEncodeAndDecode(t, senc, 0) senc = CreateSencBox() err = senc.AddSample(SencSample{iv8, nil}) assertNoError(t, err) - boxDiffAfterEncodeAndDecode(t, senc) + sencDiffAfterEncodeAndDecode(t, senc, 0) + sencDiffAfterEncodeAndDecode(t, senc, 8) senc = CreateSencBox() err = senc.AddSample(SencSample{iv8, []SubSamplePattern{{10, 1000}}}) assertNoError(t, err) - boxDiffAfterEncodeAndDecode(t, senc) + sencDiffAfterEncodeAndDecode(t, senc, 0) + sencDiffAfterEncodeAndDecode(t, senc, 8) senc = CreateSencBox() err = senc.AddSample(SencSample{iv8, []SubSamplePattern{{10, 1000}}}) assertNoError(t, err) - boxDiffAfterEncodeAndDecode(t, senc) + sencDiffAfterEncodeAndDecode(t, senc, 0) + sencDiffAfterEncodeAndDecode(t, senc, 8) senc = CreateSencBox() err = senc.AddSample(SencSample{iv16, []SubSamplePattern{{10, 1000}, {20, 2000}}}) assertNoError(t, err) - boxDiffAfterEncodeAndDecode(t, senc) + sencDiffAfterEncodeAndDecode(t, senc, 0) + sencDiffAfterEncodeAndDecode(t, senc, 16) senc = CreateSencBox() err = senc.AddSample(SencSample{iv16, []SubSamplePattern{{10, 1000}}}) assertNoError(t, err) err = senc.AddSample(SencSample{iv16, []SubSamplePattern{{20, 2000}}}) assertNoError(t, err) - boxDiffAfterEncodeAndDecode(t, senc) + sencDiffAfterEncodeAndDecode(t, senc, 0) + sencDiffAfterEncodeAndDecode(t, senc, 16) senc = CreateSencBox() err = senc.AddSample(SencSample{iv16, []SubSamplePattern{{10, 1000}}}) diff --git a/mp4/testdata/golden_init_cenc_cmfv_dump.txt b/mp4/testdata/golden_init_cenc_cmfv_dump.txt index 28dc9989..6bd65f87 100644 --- a/mp4/testdata/golden_init_cenc_cmfv_dump.txt +++ b/mp4/testdata/golden_init_cenc_cmfv_dump.txt @@ -48,7 +48,7 @@ - dataFormat: avc3 [schm] size=20 version=0 flags=000000 - schemeType: cenc - - schemeVersion: 65536 + - schemeVersion: 65536 (1.0) [schi] size=40 [tenc] size=32 version=0 flags=000000 - defaultIsProtected: 1 diff --git a/mp4/testdata/golden_moof_enc_m4s_dump.txt b/mp4/testdata/golden_moof_enc_m4s_dump.txt index 047e0a76..5241d140 100644 --- a/mp4/testdata/golden_moof_enc_m4s_dump.txt +++ b/mp4/testdata/golden_moof_enc_m4s_dump.txt @@ -32,7 +32,7 @@ [sbgp] size=28 version=0 flags=000000 - groupingType: seig - entryCount: 1 - - entry[1] sampleCount=96 groupDescriptionIndex=65537 + - entry[1] sampleCount=96 groupDescriptionIndex=65537 (index 1 inside fragment) [saiz] size=121 version=0 flags=000001 - auxInfoType: cenc - auxInfoTypeParameter: 0 @@ -239,8 +239,8 @@ - sample[95]: size=369 flags=00010000 (isLeading=0 dependsOn=0 isDependedOn=0 hasRedundancy=0 padding=0 isNonSync=true degradationPriority=0) compositionTimeOffset=1 - sample[96]: size=378 flags=00010000 (isLeading=0 dependsOn=0 isDependedOn=0 hasRedundancy=0 padding=0 isNonSync=true degradationPriority=0) compositionTimeOffset=1 [senc] size=1558 version=0 flags=000002 - - perSampleIVSize: 8 - sampleCount: 96 + - perSampleIVSize: 8 - sample[1]: iv=89d2843aeaa409b6 - subSample[1]: nrBytesClear=5 nrBytesProtected=758 - subSample[2]: nrBytesClear=5 nrBytesProtected=15511 diff --git a/mp4/traf.go b/mp4/traf.go index 0579876c..b5bbc9fd 100644 --- a/mp4/traf.go +++ b/mp4/traf.go @@ -2,6 +2,7 @@ package mp4 import ( "errors" + "fmt" "io" ) @@ -14,6 +15,8 @@ type TrafBox struct { Tfdt *TfdtBox Saiz *SaizBox Saio *SaioBox + Sbgp *SbgpBox + Sgpd *SgpdBox Senc *SencBox Trun *TrunBox // The first TrunBox Truns []*TrunBox @@ -36,6 +39,58 @@ func DecodeTraf(hdr *boxHeader, startPos uint64, r io.Reader) (Box, error) { return t, nil } +// ContainsSencBox - is there a senc box in traf and is it parsed +// If not parsed, call ParseReadSenc to parse it +func (t *TrafBox) ContainsSencBox() (ok, parsed bool) { + if t.Senc != nil { + return true, !t.Senc.readButNotParsed + } + return false, false +} + +func (t *TrafBox) ParseReadSenc(defaultIVSize byte, moofStartPos uint64) error { + if t.Senc == nil { + return fmt.Errorf("no senc box") + } + if t.Saio != nil { + // saio should be present, but we try without it, if it doesn't exist + posFromSaio := t.Saio.Offset[0] + int64(moofStartPos) + if uint64(posFromSaio) != t.Senc.StartPos+16 { + return fmt.Errorf("offset from saio (%d) and moof differs from senc data start %d", posFromSaio, t.Senc.StartPos+16) + } + } + perSampleIVSize := defaultIVSize + if t.Sbgp != nil && t.Sgpd != nil { + sbgp, sgpd := t.Sbgp, t.Sgpd + if sbgp.GroupingType != "seig" { + return fmt.Errorf("sbgp grouping type %s not supported", sbgp.GroupingType) + } + nrSbgpEntries := len(sbgp.SampleCounts) + if nrSbgpEntries != 1 { + return fmt.Errorf("sbgp entries = %d, only 1 supported for now", nrSbgpEntries) + } + sgpdEntryNr := sbgp.GroupDescriptionIndices[0] + if sgpdEntryNr != sbgpInsideOffset+1 { + return fmt.Errorf("sgpd entry number must be first inside = 65536 + 1") + } + if sgpd.GroupingType != "seig" { + return fmt.Errorf("sgpd grouping type %s not supported", sgpd.GroupingType) + } + + sgpdEntry := sgpd.SampleGroupEntries[sgpdEntryNr-sbgpInsideOffset-1] + if sgpdEntry.Type() != "seig" { + return fmt.Errorf("expected sgpd entry type seig but found %q", sgpdEntry.Type()) + } + seigEntry := sgpdEntry.(*SeigSampleGroupEntry) + perSampleIVSize = seigEntry.PerSampleIVSize + } + err := t.Senc.ParseReadBox(perSampleIVSize, t.Saiz) + if err != nil { + return err + } + return nil +} + // AddChild - add child box func (t *TrafBox) AddChild(b Box) error { switch b.Type() { @@ -47,6 +102,10 @@ func (t *TrafBox) AddChild(b Box) error { t.Saiz = b.(*SaizBox) case "saio": t.Saio = b.(*SaioBox) + case "sbgp": + t.Sbgp = b.(*SbgpBox) + case "sgpd": + t.Sgpd = b.(*SgpdBox) case "senc": t.Senc = b.(*SencBox) case "trun":