yapingcat / gomedia

golang library for rtmp, mpeg-ts,mpeg-ps,flv,mp4,ogg,rtsp
MIT License
362 stars 63 forks source link

mp4: read segment without init #115

Open 3052 opened 6 months ago

3052 commented 6 months ago

other tools can read segment without init:

mp4ff-info index_video_5_0_1.mp4

https://github.com/Eyevinn/mp4ff

mp4tool dump index_video_5_0_1.mp4

https://github.com/abema/go-mp4

but this module cannot:

go run example_demux_fmp4.go index_video_5_0_1.mp4
go run example_demux_mp4.go -mp4file index_video_5_0_1.mp4
go run example_demux_mp4_memeory_io.go -mp4file index_video_5_0_1.mp4
yapingcat commented 6 months ago
3052 commented 6 months ago

I need to parse segment only:

https://github.com/yapingcat/gomedia/files/13692755/index_video_5_0_1.zip

> mp4ff-info index_video_5_0_1.mp4
[moof] size=2574
  [mfhd] size=16 version=0 flags=000000
   - sequenceNumber: 1
  [pssh] size=634 version=0 flags=000000
   - systemID: 9a04f079-9840-4286-ab92-e65be0885f95 (PlayReady)
  [pssh] size=67 version=0 flags=000000
   - systemID: edef8ba9-79d6-4ace-a3c8-27dcd51d21ed (Widevine)
  [traf] size=1849
    [tfhd] size=20 version=0 flags=020020
     - trackID: 1
     - defaultBaseIsMoof: true
     - defaultSampleFlags: 00610000 (isLeading=0 dependsOn=0 isDependedOn=1 hasRedundancy=2 padding=0 isNonSync=true degradationPriority=0)
    [tfdt] size=20 version=1 flags=000000
     - baseMediaDecodeTime: 3158
    [trun] size=600 version=1 flags=000b05
     - sampleCount: 48
    [senc] size=1072 version=0 flags=000002
     - sampleCount: 48
     - perSampleIVSize: 8
    [saio] size=32 version=1 flags=000001
     - auxInfoType: cenc
     - auxInfoTypeParameter: 0
     - sampleCount: 1
     - offset[1]=1389
    [saiz] size=25 version=0 flags=000001
     - auxInfoType: cenc
     - auxInfoTypeParameter: 0
     - defaultSampleInfoSize: 22
     - sampleCount: 48
    [sbgp] size=28 version=0 flags=000000
     - groupingType: seig
     - entryCount: 1
    [sgpd] size=44 version=1 flags=000000
       groupingType: seig
     - defaultLength: 20
     - entryCount: 1
     - GroupingType "seig" size=20
     -  * cryptByteBlock: 0
     -  * skipByteBlock: 0
     -  * isProtected: 1
     -  * perSampleIVSize: 8
     -  * KID: bdfa4d6c-db39-702e-5b68-1f90617f9a7e
[mdat] size=1279712
[styp] size=24
 - majorBrand: msdh
 - minorVersion: 0
 - compatibleBrand: msdh
 - compatibleBrand: msix
[sidx] size=52 version=1 flags=000000
 - referenceID: 1
 - timeScale: 24000
 - earliestPresentationTime: 3158
 - firstOffset: 0

and:

> mp4tool dump index_video_5_0_1.mp4
[moof] Size=2574
  [mfhd] Size=16 Version=0 Flags=0x000000 SequenceNumber=1
  [pssh] Size=634 ... (use "-full pssh" to show all)
  [pssh] Size=67 ... (use "-full pssh" to show all)
  [traf] Size=1849
    [tfhd] Size=20 Version=0 Flags=0x020020 TrackID=1 DefaultSampleFlags=0x610000
    [tfdt] Size=20 Version=1 Flags=0x000000 BaseMediaDecodeTimeV1=3158
    [trun] Size=600 ... (use "-full trun" to show all)
    [senc] (unsupported box type) Size=1072 Data=[...] (use "-full senc" to show all)
    [saio] Size=32 Version=1 Flags=0x000001 AuxInfoType="cenc" AuxInfoTypeParameter=0x0 EntryCount=1 OffsetV1=[1389]
    [saiz] Size=25 Version=0 Flags=0x000001 AuxInfoType="cenc" AuxInfoTypeParameter=0x0 DefaultSampleInfoSize=22 SampleCount=48
    [sbgp] Size=28 Version=0 Flags=0x000000 GroupingType=1936025959 EntryCount=1 Entries=[{SampleCount=48 GroupDescriptionIndex=65537}]
    [sgpd] Size=44 ... (use "-full sgpd" to show all)
[mdat] Size=1279712 Data=[...] (use "-full mdat" to show all)
[styp] Size=24 MajorBrand="msdh" MinorVersion=0 CompatibleBrands=[{CompatibleBrand="msdh"}, {CompatibleBrand="msix"}]
[sidx] Size=52 ... (use "-full sidx" to show all)
yapingcat commented 6 months ago

gomedia is used to extract video/audio frame from mp4,so gomedia need moov box to parse dash segment.

3052 commented 6 months ago

right, but with the current module, you have to parse the init data for EACH segment, which is really wasteful

3052 commented 2 months ago

OK I think this works, but it seems like bad code to have to call mp4.CreateMp4Demuxer for every segment:

package sidx

import (
   "bytes"
   "crypto/aes"
   "crypto/cipher"
   "errors"
   "fmt"
   "github.com/yapingcat/gomedia/go-mp4"
   "io"
   "net/http"
)

// github.com/Eyevinn/mp4ff/blob/v0.40.2/mp4/crypto.go#L101
func Decrypt_CENC(sample []byte, key []byte, subSample *mp4.SubSample) error {
   block, err := aes.NewCipher(key)
   if err != nil {
      return err
   }
   stream := cipher.NewCTR(block, subSample.IV[:])
   if len(subSample.Patterns) != 0 {
      var pos uint32 = 0
      for j := 0; j < len(subSample.Patterns); j++ {
         ss := subSample.Patterns[j]
         nrClear := uint32(ss.BytesClear)
         if nrClear > 0 {
            pos += nrClear
         }
         nrEnc := ss.BytesProtected
         if nrEnc > 0 {
            stream.XORKeyStream(sample[pos:pos+nrEnc], sample[pos:pos+nrEnc])
            pos += nrEnc
         }
      }
   } else {
      stream.XORKeyStream(sample, sample)
   }
   return nil
}

func get(url string, start, end uint32) ([]byte, error) {
   req, err := http.NewRequest("GET", url, nil)
   if err != nil {
      return nil, err
   }
   req.Header.Set("Range", fmt.Sprintf("bytes=%v-%v", start, end))
   fmt.Println(start, end)
   res, err := http.DefaultClient.Do(req)
   if err != nil {
      return nil, err
   }
   defer res.Body.Close()
   if res.StatusCode != http.StatusPartialContent {
      return nil, errors.New(res.Status)
   }
   return io.ReadAll(res.Body)
}

func byte_ranges(r io.Reader, start uint32) ([][]uint32, error) {
   sidx := mp4.SegmentIndexBox{
      Box: &mp4.FullBox{
         Box: &mp4.BasicBox{},
      },
   }
   if _, err := sidx.Box.Box.Decode(r); err != nil {
      return nil, err
   }
   if _, err := sidx.Decode(r); err != nil {
      return nil, err
   }
   var rs [][]uint32
   for _, e := range sidx.Entrys {
      r := []uint32{start, start + e.ReferencedSize - 1}
      rs = append(rs, r)
      start += e.ReferencedSize
   }
   return rs, nil
}

func mux(
   dst io.WriteSeeker,
   url string,
   start_sidx, start_segment uint32,
   key []byte,
) error {
   muxer, err := mp4.CreateMp4Muxer(dst)
   if err != nil {
      return err
   }
   vid := muxer.AddVideoTrack(mp4.MP4_CODEC_H264)
   init, err := get(url, 0, start_sidx-1)
   if err != nil {
      return err
   }
   sidx, err := get(url, start_sidx, start_segment-1)
   if err != nil {
      return err
   }
   ranges, err := byte_ranges(bytes.NewReader(sidx), start_segment)
   if err != nil {
      return err
   }
   for _, r := range ranges {
      segment, err := get(url, r[0], r[1])
      if err != nil {
         return err
      }
      segment = append(init, segment...)
      demuxer := mp4.CreateMp4Demuxer(bytes.NewReader(segment))
      if _, err := demuxer.ReadHead(); err != nil {
         return err
      }
      demuxer.OnRawSample = func(_ mp4.MP4_CODEC_TYPE, sample []byte, subSample *mp4.SubSample) error {
         return Decrypt_CENC(sample, key, subSample)
      }
      for {
         pkg, err := demuxer.ReadPacket()
         if err == io.EOF {
            break
         } else if err != nil {
            return err
         }
         if err := muxer.Write(vid, pkg.Data, pkg.Pts, pkg.Dts); err != nil {
            return err
         }
      }
   }
   return muxer.WriteTrailer()
}