Browse Source
* hls source: support fMP4s video streams * hls source: start reading live streams from (end of playlist - starting point) * hls client: wait processing of current fMP4 segment before downloading another one * hls client: support fmp4 trun boxes with default sample duration, flags and size * hls client: merge fmp4 init file reader and writer * hls client: merge fmp4 part reader and writer * hls client: improve precision of go <-> mp4 time conversion * hls client: fix esds generation in go-mp4 * hls client: support audio in separate playlist * hls client: support an arbitrary number of tracks in fmp4 init files * hls client: support EXT-X-BYTERANGE * hls client: support fmp4 segments with multiple parts at once * hls client: support an arbitrary number of mpeg-ts tracks * hls client: synchronize tracks around a primary track * update go-mp4 * hls: synchronize track reproduction around a leading one * hls client: reset stream if playback is too late * hls client: add limit on DTS-RTC difference * hls client: support again streams that don't provide codecs in master playlistpull/1198/head
38 changed files with 3476 additions and 2249 deletions
@ -1,278 +0,0 @@
@@ -1,278 +0,0 @@
|
||||
package hls |
||||
|
||||
import ( |
||||
"context" |
||||
"crypto/sha256" |
||||
"crypto/tls" |
||||
"encoding/hex" |
||||
"fmt" |
||||
"io" |
||||
"net/http" |
||||
"net/url" |
||||
"strings" |
||||
"time" |
||||
|
||||
"github.com/aler9/gortsplib" |
||||
"github.com/grafov/m3u8" |
||||
|
||||
"github.com/aler9/rtsp-simple-server/internal/logger" |
||||
) |
||||
|
||||
type clientDownloader struct { |
||||
primaryPlaylistURL *url.URL |
||||
segmentQueue *clientSegmentQueue |
||||
logger ClientLogger |
||||
onTracks func(*gortsplib.TrackH264, *gortsplib.TrackMPEG4Audio) error |
||||
onVideoData func(time.Duration, [][]byte) |
||||
onAudioData func(time.Duration, []byte) |
||||
rp *clientRoutinePool |
||||
|
||||
streamPlaylistURL *url.URL |
||||
downloadedSegmentURIs []string |
||||
httpClient *http.Client |
||||
lastDownloadTime time.Time |
||||
firstPlaylistReceived bool |
||||
} |
||||
|
||||
func newClientDownloader( |
||||
primaryPlaylistURL *url.URL, |
||||
fingerprint string, |
||||
segmentQueue *clientSegmentQueue, |
||||
logger ClientLogger, |
||||
onTracks func(*gortsplib.TrackH264, *gortsplib.TrackMPEG4Audio) error, |
||||
onVideoData func(time.Duration, [][]byte), |
||||
onAudioData func(time.Duration, []byte), |
||||
rp *clientRoutinePool, |
||||
) *clientDownloader { |
||||
var tlsConfig *tls.Config |
||||
if fingerprint != "" { |
||||
tlsConfig = &tls.Config{ |
||||
InsecureSkipVerify: true, |
||||
VerifyConnection: func(cs tls.ConnectionState) error { |
||||
h := sha256.New() |
||||
h.Write(cs.PeerCertificates[0].Raw) |
||||
hstr := hex.EncodeToString(h.Sum(nil)) |
||||
fingerprintLower := strings.ToLower(fingerprint) |
||||
|
||||
if hstr != fingerprintLower { |
||||
return fmt.Errorf("server fingerprint do not match: expected %s, got %s", |
||||
fingerprintLower, hstr) |
||||
} |
||||
|
||||
return nil |
||||
}, |
||||
} |
||||
} |
||||
|
||||
return &clientDownloader{ |
||||
primaryPlaylistURL: primaryPlaylistURL, |
||||
segmentQueue: segmentQueue, |
||||
logger: logger, |
||||
onTracks: onTracks, |
||||
onVideoData: onVideoData, |
||||
onAudioData: onAudioData, |
||||
rp: rp, |
||||
httpClient: &http.Client{ |
||||
Transport: &http.Transport{ |
||||
TLSClientConfig: tlsConfig, |
||||
}, |
||||
}, |
||||
} |
||||
} |
||||
|
||||
func (d *clientDownloader) run(ctx context.Context) error { |
||||
for { |
||||
ok := d.segmentQueue.waitUntilSizeIsBelow(ctx, clientMinSegmentsBeforeDownloading) |
||||
if !ok { |
||||
return fmt.Errorf("terminated") |
||||
} |
||||
|
||||
_, err := d.fillSegmentQueue(ctx) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
} |
||||
} |
||||
|
||||
func (d *clientDownloader) fillSegmentQueue(ctx context.Context) (bool, error) { |
||||
minTime := d.lastDownloadTime.Add(clientMinDownloadPause) |
||||
now := time.Now() |
||||
if now.Before(minTime) { |
||||
select { |
||||
case <-time.After(minTime.Sub(now)): |
||||
case <-ctx.Done(): |
||||
return false, fmt.Errorf("terminated") |
||||
} |
||||
} |
||||
|
||||
d.lastDownloadTime = now |
||||
|
||||
pl, err := func() (*m3u8.MediaPlaylist, error) { |
||||
if d.streamPlaylistURL == nil { |
||||
return d.downloadPrimaryPlaylist(ctx) |
||||
} |
||||
return d.downloadStreamPlaylist(ctx) |
||||
}() |
||||
if err != nil { |
||||
return false, err |
||||
} |
||||
|
||||
if !d.firstPlaylistReceived { |
||||
d.firstPlaylistReceived = true |
||||
|
||||
if pl.Map != nil && pl.Map.URI != "" { |
||||
return false, fmt.Errorf("fMP4 streams are not supported yet") |
||||
} |
||||
|
||||
proc := newClientProcessorMPEGTS( |
||||
d.segmentQueue, |
||||
d.logger, |
||||
d.rp, |
||||
d.onTracks, |
||||
d.onVideoData, |
||||
d.onAudioData, |
||||
) |
||||
d.rp.add(proc.run) |
||||
} |
||||
|
||||
added := false |
||||
|
||||
for _, seg := range pl.Segments { |
||||
if seg == nil { |
||||
break |
||||
} |
||||
|
||||
if !d.segmentWasDownloaded(seg.URI) { |
||||
d.downloadedSegmentURIs = append(d.downloadedSegmentURIs, seg.URI) |
||||
byts, err := d.downloadSegment(ctx, seg.URI) |
||||
if err != nil { |
||||
return false, err |
||||
} |
||||
|
||||
d.segmentQueue.push(byts) |
||||
added = true |
||||
} |
||||
} |
||||
|
||||
return added, nil |
||||
} |
||||
|
||||
func (d *clientDownloader) segmentWasDownloaded(ur string) bool { |
||||
for _, q := range d.downloadedSegmentURIs { |
||||
if q == ur { |
||||
return true |
||||
} |
||||
} |
||||
return false |
||||
} |
||||
|
||||
func (d *clientDownloader) downloadPrimaryPlaylist(ctx context.Context) (*m3u8.MediaPlaylist, error) { |
||||
d.logger.Log(logger.Debug, "downloading primary playlist %s", d.primaryPlaylistURL) |
||||
|
||||
pl, err := d.downloadPlaylist(ctx, d.primaryPlaylistURL) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
switch plt := pl.(type) { |
||||
case *m3u8.MediaPlaylist: |
||||
d.streamPlaylistURL = d.primaryPlaylistURL |
||||
return plt, nil |
||||
|
||||
case *m3u8.MasterPlaylist: |
||||
// choose the variant with the highest bandwidth
|
||||
var chosenVariant *m3u8.Variant |
||||
for _, v := range plt.Variants { |
||||
if chosenVariant == nil || |
||||
v.VariantParams.Bandwidth > chosenVariant.VariantParams.Bandwidth { |
||||
chosenVariant = v |
||||
} |
||||
} |
||||
|
||||
if chosenVariant == nil { |
||||
return nil, fmt.Errorf("no variants found") |
||||
} |
||||
|
||||
u, err := clientURLAbsolute(d.primaryPlaylistURL, chosenVariant.URI) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
d.streamPlaylistURL = u |
||||
|
||||
return d.downloadStreamPlaylist(ctx) |
||||
|
||||
default: |
||||
return nil, fmt.Errorf("invalid playlist") |
||||
} |
||||
} |
||||
|
||||
func (d *clientDownloader) downloadStreamPlaylist(ctx context.Context) (*m3u8.MediaPlaylist, error) { |
||||
d.logger.Log(logger.Debug, "downloading stream playlist %s", d.streamPlaylistURL.String()) |
||||
|
||||
pl, err := d.downloadPlaylist(ctx, d.streamPlaylistURL) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
plt, ok := pl.(*m3u8.MediaPlaylist) |
||||
if !ok { |
||||
return nil, fmt.Errorf("invalid playlist") |
||||
} |
||||
|
||||
return plt, nil |
||||
} |
||||
|
||||
func (d *clientDownloader) downloadPlaylist(ctx context.Context, ur *url.URL) (m3u8.Playlist, error) { |
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, ur.String(), nil) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
res, err := d.httpClient.Do(req) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
defer res.Body.Close() |
||||
|
||||
if res.StatusCode != http.StatusOK { |
||||
return nil, fmt.Errorf("bad status code: %d", res.StatusCode) |
||||
} |
||||
|
||||
pl, _, err := m3u8.DecodeFrom(res.Body, true) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
return pl, nil |
||||
} |
||||
|
||||
func (d *clientDownloader) downloadSegment(ctx context.Context, segmentURI string) ([]byte, error) { |
||||
u, err := clientURLAbsolute(d.streamPlaylistURL, segmentURI) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
d.logger.Log(logger.Debug, "downloading segment %s", u) |
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
res, err := d.httpClient.Do(req) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
defer res.Body.Close() |
||||
|
||||
if res.StatusCode != http.StatusOK { |
||||
return nil, fmt.Errorf("bad status code: %d", res.StatusCode) |
||||
} |
||||
|
||||
byts, err := io.ReadAll(res.Body) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
return byts, nil |
||||
} |
@ -0,0 +1,325 @@
@@ -0,0 +1,325 @@
|
||||
package hls |
||||
|
||||
import ( |
||||
"context" |
||||
"crypto/sha256" |
||||
"crypto/tls" |
||||
"encoding/hex" |
||||
"fmt" |
||||
"io" |
||||
"net/http" |
||||
"net/url" |
||||
"strings" |
||||
"time" |
||||
|
||||
"github.com/aler9/gortsplib" |
||||
gm3u8 "github.com/grafov/m3u8" |
||||
|
||||
"github.com/aler9/rtsp-simple-server/internal/hls/m3u8" |
||||
"github.com/aler9/rtsp-simple-server/internal/logger" |
||||
) |
||||
|
||||
func clientDownloadPlaylist(ctx context.Context, httpClient *http.Client, ur *url.URL) (m3u8.Playlist, error) { |
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, ur.String(), nil) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
res, err := httpClient.Do(req) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
defer res.Body.Close() |
||||
|
||||
if res.StatusCode != http.StatusOK { |
||||
return nil, fmt.Errorf("bad status code: %d", res.StatusCode) |
||||
} |
||||
|
||||
byts, err := io.ReadAll(res.Body) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
return m3u8.Unmarshal(byts) |
||||
} |
||||
|
||||
func allCodecsAreSupported(codecs string) bool { |
||||
for _, codec := range strings.Split(codecs, ",") { |
||||
if !strings.HasPrefix(codec, "avc1") && |
||||
!strings.HasPrefix(codec, "mp4a") { |
||||
return false |
||||
} |
||||
} |
||||
return true |
||||
} |
||||
|
||||
func pickLeadingPlaylist(variants []*gm3u8.Variant) *gm3u8.Variant { |
||||
var candidates []*gm3u8.Variant //nolint:prealloc
|
||||
for _, v := range variants { |
||||
if v.Codecs != "" && !allCodecsAreSupported(v.Codecs) { |
||||
continue |
||||
} |
||||
candidates = append(candidates, v) |
||||
} |
||||
if candidates == nil { |
||||
return nil |
||||
} |
||||
|
||||
// pick the variant with the greatest bandwidth
|
||||
var leadingPlaylist *gm3u8.Variant |
||||
for _, v := range candidates { |
||||
if leadingPlaylist == nil || |
||||
v.VariantParams.Bandwidth > leadingPlaylist.VariantParams.Bandwidth { |
||||
leadingPlaylist = v |
||||
} |
||||
} |
||||
return leadingPlaylist |
||||
} |
||||
|
||||
func pickAudioPlaylist(alternatives []*gm3u8.Alternative, groupID string) *gm3u8.Alternative { |
||||
candidates := func() []*gm3u8.Alternative { |
||||
var ret []*gm3u8.Alternative |
||||
for _, alt := range alternatives { |
||||
if alt.GroupId == groupID { |
||||
ret = append(ret, alt) |
||||
} |
||||
} |
||||
return ret |
||||
}() |
||||
if candidates == nil { |
||||
return nil |
||||
} |
||||
|
||||
// pick the default audio playlist
|
||||
for _, alt := range candidates { |
||||
if alt.Default { |
||||
return alt |
||||
} |
||||
} |
||||
|
||||
// alternatively, pick the first one
|
||||
return candidates[0] |
||||
} |
||||
|
||||
type clientTimeSync interface{} |
||||
|
||||
type clientDownloaderPrimary struct { |
||||
primaryPlaylistURL *url.URL |
||||
logger ClientLogger |
||||
onTracks func(*gortsplib.TrackH264, *gortsplib.TrackMPEG4Audio) error |
||||
onVideoData func(time.Duration, [][]byte) |
||||
onAudioData func(time.Duration, []byte) |
||||
rp *clientRoutinePool |
||||
|
||||
httpClient *http.Client |
||||
leadingTimeSync clientTimeSync |
||||
|
||||
// in
|
||||
streamTracks chan []gortsplib.Track |
||||
|
||||
// out
|
||||
startStreaming chan struct{} |
||||
leadingTimeSyncReady chan struct{} |
||||
} |
||||
|
||||
func newClientDownloaderPrimary( |
||||
primaryPlaylistURL *url.URL, |
||||
fingerprint string, |
||||
logger ClientLogger, |
||||
rp *clientRoutinePool, |
||||
onTracks func(*gortsplib.TrackH264, *gortsplib.TrackMPEG4Audio) error, |
||||
onVideoData func(time.Duration, [][]byte), |
||||
onAudioData func(time.Duration, []byte), |
||||
) *clientDownloaderPrimary { |
||||
var tlsConfig *tls.Config |
||||
if fingerprint != "" { |
||||
tlsConfig = &tls.Config{ |
||||
InsecureSkipVerify: true, |
||||
VerifyConnection: func(cs tls.ConnectionState) error { |
||||
h := sha256.New() |
||||
h.Write(cs.PeerCertificates[0].Raw) |
||||
hstr := hex.EncodeToString(h.Sum(nil)) |
||||
fingerprintLower := strings.ToLower(fingerprint) |
||||
|
||||
if hstr != fingerprintLower { |
||||
return fmt.Errorf("server fingerprint do not match: expected %s, got %s", |
||||
fingerprintLower, hstr) |
||||
} |
||||
|
||||
return nil |
||||
}, |
||||
} |
||||
} |
||||
|
||||
return &clientDownloaderPrimary{ |
||||
primaryPlaylistURL: primaryPlaylistURL, |
||||
logger: logger, |
||||
onTracks: onTracks, |
||||
onVideoData: onVideoData, |
||||
onAudioData: onAudioData, |
||||
rp: rp, |
||||
httpClient: &http.Client{ |
||||
Transport: &http.Transport{ |
||||
TLSClientConfig: tlsConfig, |
||||
}, |
||||
}, |
||||
streamTracks: make(chan []gortsplib.Track), |
||||
startStreaming: make(chan struct{}), |
||||
leadingTimeSyncReady: make(chan struct{}), |
||||
} |
||||
} |
||||
|
||||
func (d *clientDownloaderPrimary) run(ctx context.Context) error { |
||||
d.logger.Log(logger.Debug, "downloading primary playlist %s", d.primaryPlaylistURL) |
||||
|
||||
pl, err := clientDownloadPlaylist(ctx, d.httpClient, d.primaryPlaylistURL) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
streamCount := 0 |
||||
|
||||
switch plt := pl.(type) { |
||||
case *m3u8.MediaPlaylist: |
||||
d.logger.Log(logger.Debug, "primary playlist is a stream playlist") |
||||
ds := newClientDownloaderStream( |
||||
true, |
||||
d.httpClient, |
||||
d.primaryPlaylistURL, |
||||
plt, |
||||
d.logger, |
||||
d.rp, |
||||
d.onStreamTracks, |
||||
d.onSetLeadingTimeSync, |
||||
d.onGetLeadingTimeSync, |
||||
d.onVideoData, |
||||
d.onAudioData) |
||||
d.rp.add(ds) |
||||
streamCount++ |
||||
|
||||
case *m3u8.MasterPlaylist: |
||||
leadingPlaylist := pickLeadingPlaylist(plt.Variants) |
||||
if leadingPlaylist == nil { |
||||
return fmt.Errorf("no variants with supported codecs found") |
||||
} |
||||
|
||||
u, err := clientAbsoluteURL(d.primaryPlaylistURL, leadingPlaylist.URI) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
ds := newClientDownloaderStream( |
||||
true, |
||||
d.httpClient, |
||||
u, |
||||
nil, |
||||
d.logger, |
||||
d.rp, |
||||
d.onStreamTracks, |
||||
d.onSetLeadingTimeSync, |
||||
d.onGetLeadingTimeSync, |
||||
d.onVideoData, |
||||
d.onAudioData) |
||||
d.rp.add(ds) |
||||
streamCount++ |
||||
|
||||
if leadingPlaylist.Audio != "" { |
||||
audioPlaylist := pickAudioPlaylist(plt.Alternatives, leadingPlaylist.Audio) |
||||
if audioPlaylist == nil { |
||||
return fmt.Errorf("audio playlist with id \"%s\" not found", leadingPlaylist.Audio) |
||||
} |
||||
|
||||
u, err := clientAbsoluteURL(d.primaryPlaylistURL, audioPlaylist.URI) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
ds := newClientDownloaderStream( |
||||
false, |
||||
d.httpClient, |
||||
u, |
||||
nil, |
||||
d.logger, |
||||
d.rp, |
||||
d.onStreamTracks, |
||||
d.onSetLeadingTimeSync, |
||||
d.onGetLeadingTimeSync, |
||||
d.onVideoData, |
||||
d.onAudioData) |
||||
d.rp.add(ds) |
||||
streamCount++ |
||||
} |
||||
|
||||
default: |
||||
return fmt.Errorf("invalid playlist") |
||||
} |
||||
|
||||
var tracks []gortsplib.Track |
||||
|
||||
for i := 0; i < streamCount; i++ { |
||||
select { |
||||
case streamTracks := <-d.streamTracks: |
||||
tracks = append(tracks, streamTracks...) |
||||
case <-ctx.Done(): |
||||
return fmt.Errorf("terminated") |
||||
} |
||||
} |
||||
|
||||
var videoTrack *gortsplib.TrackH264 |
||||
var audioTrack *gortsplib.TrackMPEG4Audio |
||||
|
||||
for _, track := range tracks { |
||||
switch ttrack := track.(type) { |
||||
case *gortsplib.TrackH264: |
||||
if videoTrack != nil { |
||||
return fmt.Errorf("multiple video tracks are not supported") |
||||
} |
||||
videoTrack = ttrack |
||||
|
||||
case *gortsplib.TrackMPEG4Audio: |
||||
if audioTrack != nil { |
||||
return fmt.Errorf("multiple audio tracks are not supported") |
||||
} |
||||
audioTrack = ttrack |
||||
} |
||||
} |
||||
|
||||
err = d.onTracks(videoTrack, audioTrack) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
close(d.startStreaming) |
||||
|
||||
return nil |
||||
} |
||||
|
||||
func (d *clientDownloaderPrimary) onStreamTracks(ctx context.Context, tracks []gortsplib.Track) bool { |
||||
select { |
||||
case d.streamTracks <- tracks: |
||||
case <-ctx.Done(): |
||||
return false |
||||
} |
||||
|
||||
select { |
||||
case <-d.startStreaming: |
||||
case <-ctx.Done(): |
||||
return false |
||||
} |
||||
|
||||
return true |
||||
} |
||||
|
||||
func (d *clientDownloaderPrimary) onSetLeadingTimeSync(ts clientTimeSync) { |
||||
d.leadingTimeSync = ts |
||||
close(d.leadingTimeSyncReady) |
||||
} |
||||
|
||||
func (d *clientDownloaderPrimary) onGetLeadingTimeSync(ctx context.Context) (clientTimeSync, bool) { |
||||
select { |
||||
case <-d.leadingTimeSyncReady: |
||||
case <-ctx.Done(): |
||||
return nil, false |
||||
} |
||||
return d.leadingTimeSync, true |
||||
} |
@ -0,0 +1,258 @@
@@ -0,0 +1,258 @@
|
||||
package hls |
||||
|
||||
import ( |
||||
"context" |
||||
"fmt" |
||||
"io" |
||||
"net/http" |
||||
"net/url" |
||||
"strconv" |
||||
"time" |
||||
|
||||
"github.com/aler9/gortsplib" |
||||
gm3u8 "github.com/grafov/m3u8" |
||||
|
||||
"github.com/aler9/rtsp-simple-server/internal/hls/m3u8" |
||||
"github.com/aler9/rtsp-simple-server/internal/logger" |
||||
) |
||||
|
||||
func segmentsLen(segments []*gm3u8.MediaSegment) int { |
||||
for i, seg := range segments { |
||||
if seg == nil { |
||||
return i |
||||
} |
||||
} |
||||
return 0 |
||||
} |
||||
|
||||
func findSegmentWithInvPosition(segments []*gm3u8.MediaSegment, pos int) *gm3u8.MediaSegment { |
||||
index := len(segments) - pos |
||||
if index < 0 { |
||||
return nil |
||||
} |
||||
|
||||
return segments[index] |
||||
} |
||||
|
||||
func findSegmentWithID(seqNo uint64, segments []*gm3u8.MediaSegment, id uint64) (*gm3u8.MediaSegment, int) { |
||||
index := int(int64(id) - int64(seqNo)) |
||||
if (index) >= len(segments) { |
||||
return nil, 0 |
||||
} |
||||
|
||||
return segments[index], len(segments) - index |
||||
} |
||||
|
||||
type clientDownloaderStream struct { |
||||
isLeading bool |
||||
httpClient *http.Client |
||||
playlistURL *url.URL |
||||
initialPlaylist *m3u8.MediaPlaylist |
||||
logger ClientLogger |
||||
rp *clientRoutinePool |
||||
onStreamTracks func(context.Context, []gortsplib.Track) bool |
||||
onSetLeadingTimeSync func(clientTimeSync) |
||||
onGetLeadingTimeSync func(context.Context) (clientTimeSync, bool) |
||||
onVideoData func(time.Duration, [][]byte) |
||||
onAudioData func(time.Duration, []byte) |
||||
|
||||
curSegmentID *uint64 |
||||
} |
||||
|
||||
func newClientDownloaderStream( |
||||
isLeading bool, |
||||
httpClient *http.Client, |
||||
playlistURL *url.URL, |
||||
initialPlaylist *m3u8.MediaPlaylist, |
||||
logger ClientLogger, |
||||
rp *clientRoutinePool, |
||||
onStreamTracks func(context.Context, []gortsplib.Track) bool, |
||||
onSetLeadingTimeSync func(clientTimeSync), |
||||
onGetLeadingTimeSync func(context.Context) (clientTimeSync, bool), |
||||
onVideoData func(time.Duration, [][]byte), |
||||
onAudioData func(time.Duration, []byte), |
||||
) *clientDownloaderStream { |
||||
return &clientDownloaderStream{ |
||||
isLeading: isLeading, |
||||
httpClient: httpClient, |
||||
playlistURL: playlistURL, |
||||
initialPlaylist: initialPlaylist, |
||||
logger: logger, |
||||
rp: rp, |
||||
onStreamTracks: onStreamTracks, |
||||
onSetLeadingTimeSync: onSetLeadingTimeSync, |
||||
onGetLeadingTimeSync: onGetLeadingTimeSync, |
||||
onVideoData: onVideoData, |
||||
onAudioData: onAudioData, |
||||
} |
||||
} |
||||
|
||||
func (d *clientDownloaderStream) run(ctx context.Context) error { |
||||
initialPlaylist := d.initialPlaylist |
||||
d.initialPlaylist = nil |
||||
if initialPlaylist == nil { |
||||
var err error |
||||
initialPlaylist, err = d.downloadPlaylist(ctx) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
} |
||||
|
||||
segmentQueue := newClientSegmentQueue() |
||||
|
||||
if initialPlaylist.Map != nil && initialPlaylist.Map.URI != "" { |
||||
byts, err := d.downloadSegment(ctx, initialPlaylist.Map.URI, initialPlaylist.Map.Offset, initialPlaylist.Map.Limit) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
proc, err := newClientProcessorFMP4( |
||||
ctx, |
||||
d.isLeading, |
||||
byts, |
||||
segmentQueue, |
||||
d.logger, |
||||
d.rp, |
||||
d.onStreamTracks, |
||||
d.onSetLeadingTimeSync, |
||||
d.onGetLeadingTimeSync, |
||||
d.onVideoData, |
||||
d.onAudioData, |
||||
) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
d.rp.add(proc) |
||||
} else { |
||||
proc := newClientProcessorMPEGTS( |
||||
d.isLeading, |
||||
segmentQueue, |
||||
d.logger, |
||||
d.rp, |
||||
d.onStreamTracks, |
||||
d.onSetLeadingTimeSync, |
||||
d.onGetLeadingTimeSync, |
||||
d.onVideoData, |
||||
d.onAudioData, |
||||
) |
||||
d.rp.add(proc) |
||||
} |
||||
|
||||
for { |
||||
ok := segmentQueue.waitUntilSizeIsBelow(ctx, 1) |
||||
if !ok { |
||||
return fmt.Errorf("terminated") |
||||
} |
||||
|
||||
err := d.fillSegmentQueue(ctx, segmentQueue) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
} |
||||
} |
||||
|
||||
func (d *clientDownloaderStream) downloadPlaylist(ctx context.Context) (*m3u8.MediaPlaylist, error) { |
||||
d.logger.Log(logger.Debug, "downloading stream playlist %s", d.playlistURL.String()) |
||||
|
||||
pl, err := clientDownloadPlaylist(ctx, d.httpClient, d.playlistURL) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
plt, ok := pl.(*m3u8.MediaPlaylist) |
||||
if !ok { |
||||
return nil, fmt.Errorf("invalid playlist") |
||||
} |
||||
|
||||
return plt, nil |
||||
} |
||||
|
||||
func (d *clientDownloaderStream) downloadSegment(ctx context.Context, |
||||
uri string, offset int64, limit int64, |
||||
) ([]byte, error) { |
||||
u, err := clientAbsoluteURL(d.playlistURL, uri) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
d.logger.Log(logger.Debug, "downloading segment %s", u) |
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
if limit != 0 { |
||||
req.Header.Add("Range", "bytes="+strconv.FormatInt(offset, 10)+"-"+strconv.FormatInt(offset+limit-1, 10)) |
||||
} |
||||
|
||||
res, err := d.httpClient.Do(req) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
defer res.Body.Close() |
||||
|
||||
if res.StatusCode != http.StatusOK && res.StatusCode != http.StatusPartialContent { |
||||
return nil, fmt.Errorf("bad status code: %d", res.StatusCode) |
||||
} |
||||
|
||||
byts, err := io.ReadAll(res.Body) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
return byts, nil |
||||
} |
||||
|
||||
func (d *clientDownloaderStream) fillSegmentQueue(ctx context.Context, segmentQueue *clientSegmentQueue) error { |
||||
pl, err := d.downloadPlaylist(ctx) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
pl.Segments = pl.Segments[:segmentsLen(pl.Segments)] |
||||
var seg *gm3u8.MediaSegment |
||||
|
||||
if d.curSegmentID == nil { |
||||
if !pl.Closed { // live stream: start from clientLiveStartingInvPosition
|
||||
seg = findSegmentWithInvPosition(pl.Segments, clientLiveStartingInvPosition) |
||||
if seg == nil { |
||||
return fmt.Errorf("there aren't enough segments to fill the buffer") |
||||
} |
||||
} else { // VOD stream: start from beginning
|
||||
if len(pl.Segments) == 0 { |
||||
return fmt.Errorf("no segments found") |
||||
} |
||||
seg = pl.Segments[0] |
||||
} |
||||
} else { |
||||
var invPos int |
||||
seg, invPos = findSegmentWithID(pl.SeqNo, pl.Segments, *d.curSegmentID+1) |
||||
if seg == nil { |
||||
return fmt.Errorf("following segment not found or not ready yet") |
||||
} |
||||
|
||||
d.logger.Log(logger.Debug, "segment inverse position: %d", invPos) |
||||
|
||||
if !pl.Closed && invPos > clientLiveMaxInvPosition { |
||||
return fmt.Errorf("playback is too late") |
||||
} |
||||
} |
||||
|
||||
v := seg.SeqId |
||||
d.curSegmentID = &v |
||||
|
||||
byts, err := d.downloadSegment(ctx, seg.URI, seg.Offset, seg.Limit) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
segmentQueue.push(byts) |
||||
|
||||
if pl.Closed && pl.Segments[len(pl.Segments)-1] == seg { |
||||
<-ctx.Done() |
||||
return fmt.Errorf("stream has ended") |
||||
} |
||||
|
||||
return nil |
||||
} |
@ -0,0 +1,217 @@
@@ -0,0 +1,217 @@
|
||||
package hls |
||||
|
||||
import ( |
||||
"context" |
||||
"fmt" |
||||
"time" |
||||
|
||||
"github.com/aler9/gortsplib" |
||||
"github.com/aler9/gortsplib/pkg/h264" |
||||
|
||||
"github.com/aler9/rtsp-simple-server/internal/hls/fmp4" |
||||
) |
||||
|
||||
func fmp4PickLeadingTrack(init *fmp4.Init) int { |
||||
// pick first video track
|
||||
for _, track := range init.Tracks { |
||||
if _, ok := track.Track.(*gortsplib.TrackH264); ok { |
||||
return track.ID |
||||
} |
||||
} |
||||
|
||||
// otherwise, pick first track
|
||||
return init.Tracks[0].ID |
||||
} |
||||
|
||||
type clientProcessorFMP4 struct { |
||||
isLeading bool |
||||
segmentQueue *clientSegmentQueue |
||||
logger ClientLogger |
||||
rp *clientRoutinePool |
||||
onSetLeadingTimeSync func(clientTimeSync) |
||||
onGetLeadingTimeSync func(context.Context) (clientTimeSync, bool) |
||||
onVideoData func(time.Duration, [][]byte) |
||||
onAudioData func(time.Duration, []byte) |
||||
|
||||
init fmp4.Init |
||||
leadingTrackID int |
||||
trackProcs map[int]*clientProcessorFMP4Track |
||||
|
||||
// in
|
||||
subpartProcessed chan struct{} |
||||
} |
||||
|
||||
func newClientProcessorFMP4( |
||||
ctx context.Context, |
||||
isLeading bool, |
||||
initFile []byte, |
||||
segmentQueue *clientSegmentQueue, |
||||
logger ClientLogger, |
||||
rp *clientRoutinePool, |
||||
onStreamTracks func(context.Context, []gortsplib.Track) bool, |
||||
onSetLeadingTimeSync func(clientTimeSync), |
||||
onGetLeadingTimeSync func(context.Context) (clientTimeSync, bool), |
||||
onVideoData func(time.Duration, [][]byte), |
||||
onAudioData func(time.Duration, []byte), |
||||
) (*clientProcessorFMP4, error) { |
||||
p := &clientProcessorFMP4{ |
||||
isLeading: isLeading, |
||||
segmentQueue: segmentQueue, |
||||
logger: logger, |
||||
rp: rp, |
||||
onSetLeadingTimeSync: onSetLeadingTimeSync, |
||||
onGetLeadingTimeSync: onGetLeadingTimeSync, |
||||
onVideoData: onVideoData, |
||||
onAudioData: onAudioData, |
||||
subpartProcessed: make(chan struct{}, clientFMP4MaxPartTracksPerSegment), |
||||
} |
||||
|
||||
err := p.init.Unmarshal(initFile) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
p.leadingTrackID = fmp4PickLeadingTrack(&p.init) |
||||
|
||||
tracks := make([]gortsplib.Track, len(p.init.Tracks)) |
||||
for i, track := range p.init.Tracks { |
||||
tracks[i] = track.Track |
||||
} |
||||
|
||||
ok := onStreamTracks(ctx, tracks) |
||||
if !ok { |
||||
return nil, fmt.Errorf("terminated") |
||||
} |
||||
|
||||
return p, nil |
||||
} |
||||
|
||||
func (p *clientProcessorFMP4) run(ctx context.Context) error { |
||||
for { |
||||
seg, ok := p.segmentQueue.pull(ctx) |
||||
if !ok { |
||||
return fmt.Errorf("terminated") |
||||
} |
||||
|
||||
err := p.processSegment(ctx, seg) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
} |
||||
} |
||||
|
||||
func (p *clientProcessorFMP4) processSegment(ctx context.Context, byts []byte) error { |
||||
var parts fmp4.Parts |
||||
err := parts.Unmarshal(byts) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
processingCount := 0 |
||||
|
||||
for _, part := range parts { |
||||
for _, track := range part.Tracks { |
||||
if p.trackProcs == nil { |
||||
var ts *clientTimeSyncFMP4 |
||||
|
||||
if p.isLeading { |
||||
if track.ID != p.leadingTrackID { |
||||
continue |
||||
} |
||||
|
||||
timeScale := func() uint32 { |
||||
for _, track := range p.init.Tracks { |
||||
if track.ID == p.leadingTrackID { |
||||
return track.TimeScale |
||||
} |
||||
} |
||||
return 0 |
||||
}() |
||||
ts = newClientTimeSyncFMP4(timeScale, track.BaseTime) |
||||
p.onSetLeadingTimeSync(ts) |
||||
} else { |
||||
rawTS, ok := p.onGetLeadingTimeSync(ctx) |
||||
if !ok { |
||||
return fmt.Errorf("terminated") |
||||
} |
||||
|
||||
ts, ok = rawTS.(*clientTimeSyncFMP4) |
||||
if !ok { |
||||
return fmt.Errorf("stream playlists are mixed MPEGTS/FMP4") |
||||
} |
||||
} |
||||
|
||||
p.initializeTrackProcs(ts) |
||||
} |
||||
|
||||
proc, ok := p.trackProcs[track.ID] |
||||
if !ok { |
||||
return fmt.Errorf("track ID %d not present in init file", track.ID) |
||||
} |
||||
|
||||
if processingCount >= (clientFMP4MaxPartTracksPerSegment - 1) { |
||||
return fmt.Errorf("too many part tracks at once") |
||||
} |
||||
|
||||
select { |
||||
case proc.queue <- track: |
||||
case <-ctx.Done(): |
||||
return fmt.Errorf("terminated") |
||||
} |
||||
processingCount++ |
||||
} |
||||
} |
||||
|
||||
for i := 0; i < processingCount; i++ { |
||||
select { |
||||
case <-p.subpartProcessed: |
||||
case <-ctx.Done(): |
||||
return fmt.Errorf("terminated") |
||||
} |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
func (p *clientProcessorFMP4) onPartTrackProcessed(ctx context.Context) { |
||||
select { |
||||
case p.subpartProcessed <- struct{}{}: |
||||
case <-ctx.Done(): |
||||
} |
||||
} |
||||
|
||||
func (p *clientProcessorFMP4) initializeTrackProcs(ts *clientTimeSyncFMP4) { |
||||
p.trackProcs = make(map[int]*clientProcessorFMP4Track) |
||||
|
||||
for _, track := range p.init.Tracks { |
||||
var cb func(time.Duration, []byte) error |
||||
|
||||
switch track.Track.(type) { |
||||
case *gortsplib.TrackH264: |
||||
cb = func(pts time.Duration, payload []byte) error { |
||||
nalus, err := h264.AVCCUnmarshal(payload) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
p.onVideoData(pts, nalus) |
||||
return nil |
||||
} |
||||
|
||||
case *gortsplib.TrackMPEG4Audio: |
||||
cb = func(pts time.Duration, payload []byte) error { |
||||
p.onAudioData(pts, payload) |
||||
return nil |
||||
} |
||||
} |
||||
|
||||
proc := newClientProcessorFMP4Track( |
||||
track.TimeScale, |
||||
ts, |
||||
p.onPartTrackProcessed, |
||||
cb, |
||||
) |
||||
p.rp.add(proc) |
||||
p.trackProcs[track.ID] = proc |
||||
} |
||||
} |
@ -0,0 +1,70 @@
@@ -0,0 +1,70 @@
|
||||
package hls |
||||
|
||||
import ( |
||||
"context" |
||||
"time" |
||||
|
||||
"github.com/aler9/rtsp-simple-server/internal/hls/fmp4" |
||||
) |
||||
|
||||
type clientProcessorFMP4Track struct { |
||||
timeScale uint32 |
||||
ts *clientTimeSyncFMP4 |
||||
onPartTrackProcessed func(context.Context) |
||||
onEntry func(time.Duration, []byte) error |
||||
|
||||
// in
|
||||
queue chan *fmp4.PartTrack |
||||
} |
||||
|
||||
func newClientProcessorFMP4Track( |
||||
timeScale uint32, |
||||
ts *clientTimeSyncFMP4, |
||||
onPartTrackProcessed func(context.Context), |
||||
onEntry func(time.Duration, []byte) error, |
||||
) *clientProcessorFMP4Track { |
||||
return &clientProcessorFMP4Track{ |
||||
timeScale: timeScale, |
||||
ts: ts, |
||||
onPartTrackProcessed: onPartTrackProcessed, |
||||
onEntry: onEntry, |
||||
queue: make(chan *fmp4.PartTrack, clientFMP4MaxPartTracksPerSegment), |
||||
} |
||||
} |
||||
|
||||
func (t *clientProcessorFMP4Track) run(ctx context.Context) error { |
||||
for { |
||||
select { |
||||
case entry := <-t.queue: |
||||
err := t.processPartTrack(ctx, entry) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
t.onPartTrackProcessed(ctx) |
||||
|
||||
case <-ctx.Done(): |
||||
return nil |
||||
} |
||||
} |
||||
} |
||||
|
||||
func (t *clientProcessorFMP4Track) processPartTrack(ctx context.Context, pt *fmp4.PartTrack) error { |
||||
rawDTS := pt.BaseTime |
||||
|
||||
for _, sample := range pt.Samples { |
||||
pts, err := t.ts.convertAndSync(ctx, t.timeScale, rawDTS, sample.PTSOffset) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
err = t.onEntry(pts, sample.Payload) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
rawDTS += uint64(sample.Duration) |
||||
} |
||||
|
||||
return nil |
||||
} |
@ -0,0 +1,59 @@
@@ -0,0 +1,59 @@
|
||||
package hls |
||||
|
||||
import ( |
||||
"context" |
||||
"fmt" |
||||
"time" |
||||
) |
||||
|
||||
func durationGoToMp4(v time.Duration, timeScale uint32) uint64 { |
||||
timeScale64 := uint64(timeScale) |
||||
secs := v / time.Second |
||||
dec := v % time.Second |
||||
return uint64(secs)*timeScale64 + uint64(dec)*timeScale64/uint64(time.Second) |
||||
} |
||||
|
||||
func durationMp4ToGo(v uint64, timeScale uint32) time.Duration { |
||||
timeScale64 := uint64(timeScale) |
||||
secs := v / timeScale64 |
||||
dec := v % timeScale64 |
||||
return time.Duration(secs)*time.Second + time.Duration(dec)*time.Second/time.Duration(timeScale64) |
||||
} |
||||
|
||||
type clientTimeSyncFMP4 struct { |
||||
startRTC time.Time |
||||
startDTS time.Duration |
||||
} |
||||
|
||||
func newClientTimeSyncFMP4(timeScale uint32, baseTime uint64) *clientTimeSyncFMP4 { |
||||
return &clientTimeSyncFMP4{ |
||||
startRTC: time.Now(), |
||||
startDTS: durationMp4ToGo(baseTime, timeScale), |
||||
} |
||||
} |
||||
|
||||
func (ts *clientTimeSyncFMP4) convertAndSync(ctx context.Context, timeScale uint32, |
||||
rawDTS uint64, ptsOffset int32, |
||||
) (time.Duration, error) { |
||||
pts := durationMp4ToGo(rawDTS+uint64(ptsOffset), timeScale) |
||||
dts := durationMp4ToGo(rawDTS, timeScale) |
||||
|
||||
pts -= ts.startDTS |
||||
dts -= ts.startDTS |
||||
|
||||
elapsed := time.Since(ts.startRTC) |
||||
if dts > elapsed { |
||||
diff := dts - elapsed |
||||
if diff > clientMaxDTSRTCDiff { |
||||
return 0, fmt.Errorf("difference between DTS and RTC is too big") |
||||
} |
||||
|
||||
select { |
||||
case <-time.After(diff): |
||||
case <-ctx.Done(): |
||||
return 0, fmt.Errorf("terminated") |
||||
} |
||||
} |
||||
|
||||
return pts, nil |
||||
} |
@ -0,0 +1,47 @@
@@ -0,0 +1,47 @@
|
||||
package hls |
||||
|
||||
import ( |
||||
"context" |
||||
"fmt" |
||||
"time" |
||||
|
||||
"github.com/aler9/rtsp-simple-server/internal/hls/mpegts" |
||||
) |
||||
|
||||
type clientTimeSyncMPEGTS struct { |
||||
startRTC time.Time |
||||
startDTS int64 |
||||
td *mpegts.TimeDecoder |
||||
} |
||||
|
||||
func newClientTimeSyncMPEGTS(startDTS int64) *clientTimeSyncMPEGTS { |
||||
return &clientTimeSyncMPEGTS{ |
||||
startRTC: time.Now(), |
||||
startDTS: startDTS, |
||||
td: mpegts.NewTimeDecoder(), |
||||
} |
||||
} |
||||
|
||||
func (ts *clientTimeSyncMPEGTS) convertAndSync(ctx context.Context, rawDTS int64, rawPTS int64) (time.Duration, error) { |
||||
rawDTS = (rawDTS - ts.startDTS) & 0x1FFFFFFFF |
||||
rawPTS = (rawPTS - ts.startDTS) & 0x1FFFFFFFF |
||||
|
||||
dts := ts.td.Decode(rawDTS) |
||||
pts := ts.td.Decode(rawPTS) |
||||
|
||||
elapsed := time.Since(ts.startRTC) |
||||
if dts > elapsed { |
||||
diff := dts - elapsed |
||||
if diff > clientMaxDTSRTCDiff { |
||||
return 0, fmt.Errorf("difference between DTS and RTC is too big") |
||||
} |
||||
|
||||
select { |
||||
case <-time.After(diff): |
||||
case <-ctx.Done(): |
||||
return 0, fmt.Errorf("terminated") |
||||
} |
||||
} |
||||
|
||||
return pts, nil |
||||
} |
@ -1,17 +0,0 @@
@@ -1,17 +0,0 @@
|
||||
package fmp4 |
||||
|
||||
import ( |
||||
"time" |
||||
) |
||||
|
||||
// AudioSample is an audio sample.
|
||||
type AudioSample struct { |
||||
AU []byte |
||||
PTS time.Duration |
||||
Next *AudioSample |
||||
} |
||||
|
||||
// Duration returns the sample duration.
|
||||
func (s AudioSample) Duration() time.Duration { |
||||
return s.Next.PTS - s.PTS |
||||
} |
@ -0,0 +1,261 @@
@@ -0,0 +1,261 @@
|
||||
package fmp4 |
||||
|
||||
import ( |
||||
"bytes" |
||||
"fmt" |
||||
|
||||
gomp4 "github.com/abema/go-mp4" |
||||
"github.com/aler9/gortsplib" |
||||
"github.com/aler9/gortsplib/pkg/mpeg4audio" |
||||
) |
||||
|
||||
// Init is a FMP4 initialization file.
|
||||
type Init struct { |
||||
Tracks []*InitTrack |
||||
} |
||||
|
||||
// Unmarshal decodes a FMP4 initialization file.
|
||||
func (i *Init) Unmarshal(byts []byte) error { |
||||
type readState int |
||||
|
||||
const ( |
||||
waitingTrak readState = iota |
||||
waitingTkhd |
||||
waitingMdhd |
||||
waitingCodec |
||||
waitingAvcc |
||||
waitingEsds |
||||
) |
||||
|
||||
state := waitingTrak |
||||
var curTrack *InitTrack |
||||
|
||||
_, err := gomp4.ReadBoxStructure(bytes.NewReader(byts), func(h *gomp4.ReadHandle) (interface{}, error) { |
||||
switch h.BoxInfo.Type.String() { |
||||
case "trak": |
||||
if state != waitingTrak { |
||||
return nil, fmt.Errorf("parse error") |
||||
} |
||||
|
||||
curTrack = &InitTrack{} |
||||
i.Tracks = append(i.Tracks, curTrack) |
||||
state = waitingTkhd |
||||
|
||||
case "tkhd": |
||||
if state != waitingTkhd { |
||||
return nil, fmt.Errorf("parse error") |
||||
} |
||||
|
||||
box, _, err := h.ReadPayload() |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
tkhd := box.(*gomp4.Tkhd) |
||||
|
||||
curTrack.ID = int(tkhd.TrackID) |
||||
state = waitingMdhd |
||||
|
||||
case "mdhd": |
||||
if state != waitingMdhd { |
||||
return nil, fmt.Errorf("parse error") |
||||
} |
||||
|
||||
box, _, err := h.ReadPayload() |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
mdhd := box.(*gomp4.Mdhd) |
||||
|
||||
curTrack.TimeScale = mdhd.Timescale |
||||
state = waitingCodec |
||||
|
||||
case "avc1": |
||||
if state != waitingCodec { |
||||
return nil, fmt.Errorf("parse error") |
||||
} |
||||
|
||||
state = waitingAvcc |
||||
|
||||
case "avcC": |
||||
if state != waitingAvcc { |
||||
return nil, fmt.Errorf("parse error") |
||||
} |
||||
|
||||
box, _, err := h.ReadPayload() |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
conf := box.(*gomp4.AVCDecoderConfiguration) |
||||
|
||||
if len(conf.SequenceParameterSets) > 1 { |
||||
return nil, fmt.Errorf("multiple SPS are not supported") |
||||
} |
||||
|
||||
var sps []byte |
||||
if len(conf.SequenceParameterSets) == 1 { |
||||
sps = conf.SequenceParameterSets[0].NALUnit |
||||
} |
||||
|
||||
if len(conf.PictureParameterSets) > 1 { |
||||
return nil, fmt.Errorf("multiple PPS are not supported") |
||||
} |
||||
|
||||
var pps []byte |
||||
if len(conf.PictureParameterSets) == 1 { |
||||
pps = conf.PictureParameterSets[0].NALUnit |
||||
} |
||||
|
||||
curTrack.Track = &gortsplib.TrackH264{ |
||||
PayloadType: 96, |
||||
SPS: sps, |
||||
PPS: pps, |
||||
} |
||||
state = waitingTrak |
||||
|
||||
case "mp4a": |
||||
if state != waitingCodec { |
||||
return nil, fmt.Errorf("parse error") |
||||
} |
||||
|
||||
state = waitingEsds |
||||
|
||||
case "esds": |
||||
if state != waitingEsds { |
||||
return nil, fmt.Errorf("parse error") |
||||
} |
||||
|
||||
box, _, err := h.ReadPayload() |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
esds := box.(*gomp4.Esds) |
||||
|
||||
encodedConf := func() []byte { |
||||
for _, desc := range esds.Descriptors { |
||||
if desc.Tag == gomp4.DecSpecificInfoTag { |
||||
return desc.Data |
||||
} |
||||
} |
||||
return nil |
||||
}() |
||||
if encodedConf == nil { |
||||
return nil, fmt.Errorf("unable to find MPEG4-audio configuration") |
||||
} |
||||
|
||||
var c mpeg4audio.Config |
||||
err = c.Unmarshal(encodedConf) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("invalid MPEG4-audio configuration: %s", err) |
||||
} |
||||
|
||||
curTrack.Track = &gortsplib.TrackMPEG4Audio{ |
||||
PayloadType: 96, |
||||
Config: &c, |
||||
SizeLength: 13, |
||||
IndexLength: 3, |
||||
IndexDeltaLength: 3, |
||||
} |
||||
state = waitingTrak |
||||
|
||||
case "ac-3": |
||||
return nil, fmt.Errorf("AC-3 codec is not supported (yet)") |
||||
} |
||||
|
||||
return h.Expand() |
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
if state != waitingTrak { |
||||
return fmt.Errorf("parse error") |
||||
} |
||||
|
||||
if i.Tracks == nil { |
||||
return fmt.Errorf("no tracks found") |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
// Marshal encodes a FMP4 initialization file.
|
||||
func (i *Init) Marshal() ([]byte, error) { |
||||
/* |
||||
- ftyp |
||||
- moov |
||||
- mvhd |
||||
- trak |
||||
- trak |
||||
- ... |
||||
- mvex |
||||
- trex |
||||
- trex |
||||
- ... |
||||
*/ |
||||
|
||||
w := newMP4Writer() |
||||
|
||||
_, err := w.WriteBox(&gomp4.Ftyp{ // <ftyp/>
|
||||
MajorBrand: [4]byte{'m', 'p', '4', '2'}, |
||||
MinorVersion: 1, |
||||
CompatibleBrands: []gomp4.CompatibleBrandElem{ |
||||
{CompatibleBrand: [4]byte{'m', 'p', '4', '1'}}, |
||||
{CompatibleBrand: [4]byte{'m', 'p', '4', '2'}}, |
||||
{CompatibleBrand: [4]byte{'i', 's', 'o', 'm'}}, |
||||
{CompatibleBrand: [4]byte{'h', 'l', 's', 'f'}}, |
||||
}, |
||||
}) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
_, err = w.writeBoxStart(&gomp4.Moov{}) // <moov>
|
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
_, err = w.WriteBox(&gomp4.Mvhd{ // <mvhd/>
|
||||
Timescale: 1000, |
||||
Rate: 65536, |
||||
Volume: 256, |
||||
Matrix: [9]int32{0x00010000, 0, 0, 0, 0x00010000, 0, 0, 0, 0x40000000}, |
||||
NextTrackID: 4294967295, |
||||
}) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
for _, track := range i.Tracks { |
||||
err := track.marshal(w) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
} |
||||
|
||||
_, err = w.writeBoxStart(&gomp4.Mvex{}) // <mvex>
|
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
for _, track := range i.Tracks { |
||||
_, err = w.WriteBox(&gomp4.Trex{ // <trex/>
|
||||
TrackID: uint32(track.ID), |
||||
DefaultSampleDescriptionIndex: 1, |
||||
}) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
} |
||||
|
||||
err = w.writeBoxEnd() // </mvex>
|
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
err = w.writeBoxEnd() // </moov>
|
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
return w.bytes(), nil |
||||
} |
@ -1,104 +0,0 @@
@@ -1,104 +0,0 @@
|
||||
package fmp4 |
||||
|
||||
import ( |
||||
"bytes" |
||||
"fmt" |
||||
|
||||
gomp4 "github.com/abema/go-mp4" |
||||
"github.com/aler9/gortsplib" |
||||
) |
||||
|
||||
type initReadState int |
||||
|
||||
const ( |
||||
waitingTrak initReadState = iota |
||||
waitingCodec |
||||
waitingAVCC |
||||
) |
||||
|
||||
// InitRead reads a FMP4 initialization file.
|
||||
func InitRead(byts []byte) (*gortsplib.TrackH264, *gortsplib.TrackMPEG4Audio, error) { |
||||
state := waitingTrak |
||||
var videoTrack *gortsplib.TrackH264 |
||||
var audioTrack *gortsplib.TrackMPEG4Audio |
||||
|
||||
_, err := gomp4.ReadBoxStructure(bytes.NewReader(byts), func(h *gomp4.ReadHandle) (interface{}, error) { |
||||
switch h.BoxInfo.Type.String() { |
||||
case "trak": |
||||
if state != waitingTrak { |
||||
return nil, fmt.Errorf("parse error") |
||||
} |
||||
state = waitingCodec |
||||
|
||||
case "avc1": |
||||
if state != waitingCodec { |
||||
return nil, fmt.Errorf("parse error") |
||||
} |
||||
|
||||
if videoTrack != nil { |
||||
return nil, fmt.Errorf("multiple video tracks are not supported") |
||||
} |
||||
|
||||
state = waitingAVCC |
||||
|
||||
case "avcC": |
||||
if state != waitingAVCC { |
||||
return nil, fmt.Errorf("parse error") |
||||
} |
||||
|
||||
box, _, err := h.ReadPayload() |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
conf := box.(*gomp4.AVCDecoderConfiguration) |
||||
|
||||
if len(conf.SequenceParameterSets) > 1 { |
||||
return nil, fmt.Errorf("multiple SPS are not supported") |
||||
} |
||||
|
||||
var sps []byte |
||||
if len(conf.SequenceParameterSets) == 1 { |
||||
sps = conf.SequenceParameterSets[0].NALUnit |
||||
} |
||||
|
||||
if len(conf.PictureParameterSets) > 1 { |
||||
return nil, fmt.Errorf("multiple PPS are not supported") |
||||
} |
||||
|
||||
var pps []byte |
||||
if len(conf.PictureParameterSets) == 1 { |
||||
pps = conf.PictureParameterSets[0].NALUnit |
||||
} |
||||
|
||||
videoTrack = &gortsplib.TrackH264{ |
||||
PayloadType: 96, |
||||
SPS: sps, |
||||
PPS: pps, |
||||
} |
||||
|
||||
state = waitingTrak |
||||
|
||||
case "mp4a": |
||||
if state != waitingCodec { |
||||
return nil, fmt.Errorf("parse error") |
||||
} |
||||
|
||||
if audioTrack != nil { |
||||
return nil, fmt.Errorf("multiple audio tracks are not supported") |
||||
} |
||||
|
||||
return nil, fmt.Errorf("TODO: MP4a") |
||||
} |
||||
|
||||
return h.Expand() |
||||
}) |
||||
if err != nil { |
||||
return nil, nil, err |
||||
} |
||||
|
||||
if state != waitingTrak { |
||||
return nil, nil, fmt.Errorf("parse error") |
||||
} |
||||
|
||||
return videoTrack, audioTrack, nil |
||||
} |
@ -0,0 +1,689 @@
@@ -0,0 +1,689 @@
|
||||
//nolint:dupl
|
||||
package fmp4 |
||||
|
||||
import ( |
||||
"testing" |
||||
|
||||
"github.com/aler9/gortsplib" |
||||
"github.com/aler9/gortsplib/pkg/mpeg4audio" |
||||
"github.com/stretchr/testify/require" |
||||
) |
||||
|
||||
var testSPS = []byte{ |
||||
0x67, 0x42, 0xc0, 0x28, 0xd9, 0x00, 0x78, 0x02, |
||||
0x27, 0xe5, 0x84, 0x00, 0x00, 0x03, 0x00, 0x04, |
||||
0x00, 0x00, 0x03, 0x00, 0xf0, 0x3c, 0x60, 0xc9, |
||||
0x20, |
||||
} |
||||
|
||||
var testVideoTrack = &gortsplib.TrackH264{ |
||||
PayloadType: 96, |
||||
SPS: testSPS, |
||||
PPS: []byte{0x08}, |
||||
} |
||||
|
||||
var testAudioTrack = &gortsplib.TrackMPEG4Audio{ |
||||
PayloadType: 97, |
||||
Config: &mpeg4audio.Config{ |
||||
Type: 2, |
||||
SampleRate: 44100, |
||||
ChannelCount: 2, |
||||
}, |
||||
SizeLength: 13, |
||||
IndexLength: 3, |
||||
IndexDeltaLength: 3, |
||||
} |
||||
|
||||
func TestInitMarshal(t *testing.T) { |
||||
t.Run("video + audio", func(t *testing.T) { |
||||
init := Init{ |
||||
Tracks: []*InitTrack{ |
||||
{ |
||||
ID: 1, |
||||
TimeScale: 90000, |
||||
Track: testVideoTrack, |
||||
}, |
||||
{ |
||||
ID: 2, |
||||
TimeScale: uint32(testAudioTrack.ClockRate()), |
||||
Track: testAudioTrack, |
||||
}, |
||||
}, |
||||
} |
||||
|
||||
byts, err := init.Marshal() |
||||
require.NoError(t, err) |
||||
|
||||
require.Equal(t, []byte{ |
||||
0x00, 0x00, 0x00, 0x20, |
||||
'f', 't', 'y', 'p', |
||||
0x6d, 0x70, 0x34, 0x32, 0x00, 0x00, 0x00, 0x01, |
||||
0x6d, 0x70, 0x34, 0x31, 0x6d, 0x70, 0x34, 0x32, |
||||
0x69, 0x73, 0x6f, 0x6d, 0x68, 0x6c, 0x73, 0x66, |
||||
0x00, 0x00, 0x04, 0x64, |
||||
'm', 'o', 'o', 'v', |
||||
0x00, 0x00, 0x00, 0x6c, |
||||
'm', 'v', 'h', 'd', |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xe8, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, |
||||
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x01, 0xec, |
||||
't', 'r', 'a', 'k', |
||||
0x00, 0x00, 0x00, 0x5c, |
||||
't', 'k', 'h', 'd', |
||||
0x00, 0x00, 0x00, 0x03, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, |
||||
0x07, 0x80, 0x00, 0x00, 0x04, 0x38, 0x00, 0x00, |
||||
0x00, 0x00, 0x01, 0x88, 0x6d, 0x64, 0x69, 0x61, |
||||
0x00, 0x00, 0x00, 0x20, |
||||
'm', 'd', 'h', 'd', |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x5f, 0x90, |
||||
0x00, 0x00, 0x00, 0x00, 0x55, 0xc4, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x2d, 0x68, 0x64, 0x6c, 0x72, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x76, 0x69, 0x64, 0x65, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x56, 0x69, 0x64, 0x65, 0x6f, 0x48, 0x61, 0x6e, |
||||
0x64, 0x6c, 0x65, 0x72, 0x00, 0x00, 0x00, 0x01, |
||||
0x33, |
||||
'm', 'i', 'n', 'f', |
||||
0x00, 0x00, 0x00, 0x14, |
||||
'v', 'm', 'h', 'd', |
||||
0x00, 0x00, 0x00, |
||||
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x24, 0x64, 0x69, 0x6e, |
||||
0x66, 0x00, 0x00, 0x00, 0x1c, 0x64, 0x72, 0x65, |
||||
0x66, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x01, 0x00, 0x00, 0x00, 0x0c, 0x75, 0x72, 0x6c, |
||||
0x20, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, |
||||
0xf3, 0x73, 0x74, 0x62, 0x6c, 0x00, 0x00, 0x00, |
||||
0xa7, 0x73, 0x74, 0x73, 0x64, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, |
||||
0x97, 0x61, 0x76, 0x63, 0x31, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x80, 0x04, |
||||
0x38, 0x00, 0x48, 0x00, 0x00, 0x00, 0x48, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x18, 0xff, 0xff, 0x00, 0x00, 0x00, 0x2d, 0x61, |
||||
0x76, 0x63, 0x43, 0x01, 0x42, 0xc0, 0x28, 0x03, |
||||
0x01, 0x00, 0x19, 0x67, 0x42, 0xc0, 0x28, 0xd9, |
||||
0x00, 0x78, 0x02, 0x27, 0xe5, 0x84, 0x00, 0x00, |
||||
0x03, 0x00, 0x04, 0x00, 0x00, 0x03, 0x00, 0xf0, |
||||
0x3c, 0x60, 0xc9, 0x20, 0x01, 0x00, 0x01, 0x08, |
||||
0x00, 0x00, 0x00, 0x14, 0x62, 0x74, 0x72, 0x74, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x42, 0x40, |
||||
0x00, 0x0f, 0x42, 0x40, 0x00, 0x00, 0x00, 0x10, |
||||
0x73, 0x74, 0x74, 0x73, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, |
||||
0x73, 0x74, 0x73, 0x63, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x14, |
||||
0x73, 0x74, 0x73, 0x7a, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x10, 0x73, 0x74, 0x63, 0x6f, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x01, 0xbc, |
||||
't', 'r', 'a', 'k', |
||||
0x00, 0x00, 0x00, 0x5c, |
||||
't', 'k', 'h', 'd', |
||||
0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, |
||||
0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x58, |
||||
0x6d, 0x64, 0x69, 0x61, 0x00, 0x00, 0x00, 0x20, |
||||
'm', 'd', 'h', 'd', |
||||
0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0xac, 0x44, 0x00, 0x00, 0x00, 0x00, |
||||
0x55, 0xc4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2d, |
||||
0x68, 0x64, 0x6c, 0x72, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x73, 0x6f, 0x75, 0x6e, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x53, 0x6f, 0x75, 0x6e, |
||||
0x64, 0x48, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x72, |
||||
0x00, 0x00, 0x00, 0x01, 0x03, 0x6d, 0x69, 0x6e, |
||||
0x66, 0x00, 0x00, 0x00, 0x10, |
||||
's', 'm', 'h', 'd', |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x24, 0x64, 0x69, 0x6e, |
||||
0x66, 0x00, 0x00, 0x00, 0x1c, 0x64, 0x72, 0x65, |
||||
0x66, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x01, 0x00, 0x00, 0x00, 0x0c, 0x75, 0x72, 0x6c, |
||||
0x20, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, |
||||
0xc7, 0x73, 0x74, 0x62, 0x6c, 0x00, 0x00, 0x00, |
||||
0x7b, 0x73, 0x74, 0x73, 0x64, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, |
||||
0x6b, 0x6d, 0x70, 0x34, 0x61, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, |
||||
0x10, 0x00, 0x00, 0x00, 0x00, 0xac, 0x44, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x33, 0x65, 0x73, 0x64, |
||||
0x73, 0x00, 0x00, 0x00, 0x00, 0x03, 0x80, 0x80, |
||||
0x80, 0x22, 0x00, 0x02, 0x00, 0x04, 0x80, 0x80, |
||||
0x80, 0x14, 0x40, 0x15, 0x00, 0x00, 0x00, 0x00, |
||||
0x01, 0xf7, 0x39, 0x00, 0x01, 0xf7, 0x39, 0x05, |
||||
0x80, 0x80, 0x80, 0x02, 0x12, 0x10, 0x06, 0x80, |
||||
0x80, 0x80, 0x01, 0x02, 0x00, 0x00, 0x00, 0x14, |
||||
0x62, 0x74, 0x72, 0x74, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x01, 0xf7, 0x39, 0x00, 0x01, 0xf7, 0x39, |
||||
0x00, 0x00, 0x00, 0x10, 0x73, 0x74, 0x74, 0x73, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x10, 0x73, 0x74, 0x73, 0x63, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x14, 0x73, 0x74, 0x73, 0x7a, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, |
||||
0x73, 0x74, 0x63, 0x6f, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x48, |
||||
0x6d, 0x76, 0x65, 0x78, 0x00, 0x00, 0x00, 0x20, |
||||
0x74, 0x72, 0x65, 0x78, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, |
||||
0x74, 0x72, 0x65, 0x78, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, |
||||
}, byts) |
||||
}) |
||||
|
||||
t.Run("video only", func(t *testing.T) { |
||||
init := Init{ |
||||
Tracks: []*InitTrack{ |
||||
{ |
||||
ID: 1, |
||||
TimeScale: 90000, |
||||
Track: testVideoTrack, |
||||
}, |
||||
}, |
||||
} |
||||
|
||||
byts, err := init.Marshal() |
||||
require.NoError(t, err) |
||||
|
||||
require.Equal(t, []byte{ |
||||
0x00, 0x00, 0x00, 0x20, |
||||
'f', 't', 'y', 'p', |
||||
0x6d, 0x70, 0x34, 0x32, 0x00, 0x00, 0x00, 0x01, |
||||
0x6d, 0x70, 0x34, 0x31, 0x6d, 0x70, 0x34, 0x32, |
||||
0x69, 0x73, 0x6f, 0x6d, 0x68, 0x6c, 0x73, 0x66, |
||||
0x00, 0x00, 0x02, 0x88, |
||||
'm', 'o', 'o', 'v', |
||||
0x00, 0x00, 0x00, 0x6c, |
||||
'm', 'v', 'h', 'd', |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xe8, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, |
||||
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x01, 0xec, |
||||
't', 'r', 'a', 'k', |
||||
0x00, 0x00, 0x00, 0x5c, |
||||
't', 'k', 'h', 'd', |
||||
0x00, 0x00, 0x00, 0x03, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, |
||||
0x07, 0x80, 0x00, 0x00, 0x04, 0x38, 0x00, 0x00, |
||||
0x00, 0x00, 0x01, 0x88, 0x6d, 0x64, 0x69, 0x61, |
||||
0x00, 0x00, 0x00, 0x20, |
||||
'm', 'd', 'h', 'd', |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x5f, 0x90, |
||||
0x00, 0x00, 0x00, 0x00, 0x55, 0xc4, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x2d, 0x68, 0x64, 0x6c, 0x72, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x76, 0x69, 0x64, 0x65, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x56, 0x69, 0x64, 0x65, 0x6f, 0x48, 0x61, 0x6e, |
||||
0x64, 0x6c, 0x65, 0x72, 0x00, 0x00, 0x00, 0x01, |
||||
0x33, |
||||
'm', 'i', 'n', 'f', |
||||
0x00, 0x00, 0x00, |
||||
0x14, |
||||
'v', 'm', 'h', 'd', |
||||
0x00, 0x00, 0x00, |
||||
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x24, |
||||
'd', 'i', 'n', 'f', |
||||
0x00, 0x00, 0x00, 0x1c, 0x64, 0x72, 0x65, |
||||
0x66, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x01, 0x00, 0x00, 0x00, 0x0c, 0x75, 0x72, 0x6c, |
||||
0x20, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, |
||||
0xf3, 0x73, 0x74, 0x62, 0x6c, 0x00, 0x00, 0x00, |
||||
0xa7, 0x73, 0x74, 0x73, 0x64, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, |
||||
0x97, 0x61, 0x76, 0x63, 0x31, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x80, 0x04, |
||||
0x38, 0x00, 0x48, 0x00, 0x00, 0x00, 0x48, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x18, 0xff, 0xff, 0x00, 0x00, 0x00, 0x2d, 0x61, |
||||
0x76, 0x63, 0x43, 0x01, 0x42, 0xc0, 0x28, 0x03, |
||||
0x01, 0x00, 0x19, 0x67, 0x42, 0xc0, 0x28, 0xd9, |
||||
0x00, 0x78, 0x02, 0x27, 0xe5, 0x84, 0x00, 0x00, |
||||
0x03, 0x00, 0x04, 0x00, 0x00, 0x03, 0x00, 0xf0, |
||||
0x3c, 0x60, 0xc9, 0x20, 0x01, 0x00, 0x01, 0x08, |
||||
0x00, 0x00, 0x00, 0x14, 0x62, 0x74, 0x72, 0x74, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x42, 0x40, |
||||
0x00, 0x0f, 0x42, 0x40, 0x00, 0x00, 0x00, 0x10, |
||||
0x73, 0x74, 0x74, 0x73, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, |
||||
0x73, 0x74, 0x73, 0x63, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x14, |
||||
0x73, 0x74, 0x73, 0x7a, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x10, 0x73, 0x74, 0x63, 0x6f, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x28, 0x6d, 0x76, 0x65, 0x78, |
||||
0x00, 0x00, 0x00, 0x20, 0x74, 0x72, 0x65, 0x78, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, |
||||
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
}, byts) |
||||
}) |
||||
|
||||
t.Run("audio only", func(t *testing.T) { |
||||
init := &Init{ |
||||
Tracks: []*InitTrack{ |
||||
{ |
||||
ID: 1, |
||||
TimeScale: uint32(testAudioTrack.ClockRate()), |
||||
Track: testAudioTrack, |
||||
}, |
||||
}, |
||||
} |
||||
|
||||
byts, err := init.Marshal() |
||||
require.NoError(t, err) |
||||
|
||||
require.Equal(t, []byte{ |
||||
0x00, 0x00, 0x00, 0x20, |
||||
'f', 't', 'y', 'p', |
||||
0x6d, 0x70, 0x34, 0x32, 0x00, 0x00, 0x00, 0x01, |
||||
0x6d, 0x70, 0x34, 0x31, 0x6d, 0x70, 0x34, 0x32, |
||||
0x69, 0x73, 0x6f, 0x6d, 0x68, 0x6c, 0x73, 0x66, |
||||
0x00, 0x00, 0x02, 0x58, |
||||
'm', 'o', 'o', 'v', |
||||
0x00, 0x00, 0x00, 0x6c, |
||||
'm', 'v', 'h', 'd', |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xe8, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, |
||||
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x01, 0xbc, |
||||
't', 'r', 'a', 'k', |
||||
0x00, 0x00, 0x00, 0x5c, |
||||
't', 'k', 'h', 'd', |
||||
0x00, 0x00, 0x00, 0x03, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, |
||||
0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x01, 0x58, |
||||
'm', 'd', 'i', 'a', |
||||
0x00, 0x00, 0x00, 0x20, |
||||
'm', 'd', 'h', 'd', |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xac, 0x44, |
||||
0x00, 0x00, 0x00, 0x00, 0x55, 0xc4, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x2d, |
||||
'h', 'd', 'l', 'r', |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x73, 0x6f, 0x75, 0x6e, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x53, 0x6f, 0x75, 0x6e, 0x64, 0x48, 0x61, 0x6e, |
||||
0x64, 0x6c, 0x65, 0x72, 0x00, 0x00, 0x00, 0x01, |
||||
0x03, |
||||
'm', 'i', 'n', 'f', |
||||
0x00, 0x00, 0x00, 0x10, |
||||
's', 'm', 'h', 'd', |
||||
0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x24, |
||||
'd', 'i', 'n', 'f', |
||||
0x00, 0x00, 0x00, |
||||
0x1c, 0x64, 0x72, 0x65, 0x66, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, |
||||
0x0c, 0x75, 0x72, 0x6c, 0x20, 0x00, 0x00, 0x00, |
||||
0x01, 0x00, 0x00, 0x00, 0xc7, 0x73, 0x74, 0x62, |
||||
0x6c, 0x00, 0x00, 0x00, 0x7b, 0x73, 0x74, 0x73, |
||||
0x64, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x01, 0x00, 0x00, 0x00, 0x6b, |
||||
'm', 'p', '4', 'a', |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x02, 0x00, 0x10, 0x00, 0x00, 0x00, |
||||
0x00, 0xac, 0x44, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x33, |
||||
'e', 's', 'd', 's', |
||||
0x00, 0x00, 0x00, |
||||
0x00, 0x03, 0x80, 0x80, 0x80, 0x22, 0x00, 0x01, |
||||
0x00, 0x04, 0x80, 0x80, 0x80, 0x14, 0x40, 0x15, |
||||
0x00, 0x00, 0x00, 0x00, 0x01, 0xf7, 0x39, 0x00, |
||||
0x01, 0xf7, 0x39, 0x05, 0x80, 0x80, 0x80, 0x02, |
||||
0x12, 0x10, 0x06, 0x80, 0x80, 0x80, 0x01, 0x02, |
||||
0x00, 0x00, 0x00, 0x14, |
||||
'b', 't', 'r', 't', |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xf7, 0x39, |
||||
0x00, 0x01, 0xf7, 0x39, 0x00, 0x00, 0x00, 0x10, |
||||
0x73, 0x74, 0x74, 0x73, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, |
||||
0x73, 0x74, 0x73, 0x63, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x14, |
||||
0x73, 0x74, 0x73, 0x7a, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x10, 0x73, 0x74, 0x63, 0x6f, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x28, |
||||
'm', 'v', 'e', 'x', |
||||
0x00, 0x00, 0x00, 0x20, |
||||
't', 'r', 'e', 'x', |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, |
||||
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
}, byts) |
||||
}) |
||||
} |
||||
|
||||
func TestInitUnmarshal(t *testing.T) { |
||||
t.Run("video", func(t *testing.T) { |
||||
byts := []byte{ |
||||
0x00, 0x00, 0x00, 0x1c, |
||||
'f', 't', 'y', 'p', |
||||
0x64, 0x61, 0x73, 0x68, 0x00, 0x00, 0x00, 0x01, |
||||
0x69, 0x73, 0x6f, 0x6d, 0x61, 0x76, 0x63, 0x31, |
||||
0x64, 0x61, 0x73, 0x68, 0x00, 0x00, 0x02, 0x92, |
||||
'm', 'o', 'o', 'v', |
||||
0x00, 0x00, 0x00, 0x6c, |
||||
'm', 'v', 'h', 'd', |
||||
0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x98, 0x96, 0x80, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, |
||||
0x00, 0x00, 0x01, 0xf6, |
||||
't', 'r', 'a', 'k', |
||||
0x00, 0x00, 0x00, 0x5c, |
||||
't', 'k', 'h', 'd', |
||||
0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x40, 0x00, 0x00, 0x00, 0x03, 0xc0, 0x00, 0x00, |
||||
0x02, 0x1c, 0x00, 0x00, 0x00, 0x00, 0x01, 0x92, |
||||
0x6d, 0x64, 0x69, 0x61, 0x00, 0x00, 0x00, 0x20, |
||||
'm', 'd', 'h', 'd', |
||||
0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x98, 0x96, 0x80, 0x00, 0x00, 0x00, 0x00, |
||||
0x55, 0xc4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x38, |
||||
0x68, 0x64, 0x6c, 0x72, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x76, 0x69, 0x64, 0x65, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x42, 0x72, 0x6f, 0x61, |
||||
0x64, 0x70, 0x65, 0x61, 0x6b, 0x20, 0x56, 0x69, |
||||
0x64, 0x65, 0x6f, 0x20, 0x48, 0x61, 0x6e, 0x64, |
||||
0x6c, 0x65, 0x72, 0x00, 0x00, 0x00, 0x01, 0x32, |
||||
'm', 'i', 'n', 'f', |
||||
0x00, 0x00, 0x00, 0x14, |
||||
'v', 'm', 'h', 'd', |
||||
0x00, 0x00, 0x00, 0x01, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x24, |
||||
'd', 'i', 'n', 'f', |
||||
0x00, 0x00, 0x00, 0x1c, 0x64, 0x72, 0x65, 0x66, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, |
||||
0x00, 0x00, 0x00, 0x0c, 0x75, 0x72, 0x6c, 0x20, |
||||
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0xf2, |
||||
0x73, 0x74, 0x62, 0x6c, 0x00, 0x00, 0x00, 0xa6, |
||||
0x73, 0x74, 0x73, 0x64, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x96, |
||||
0x61, 0x76, 0x63, 0x31, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x03, 0xc0, 0x02, 0x1c, |
||||
0x00, 0x48, 0x00, 0x00, 0x00, 0x48, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x04, 0x68, |
||||
0x32, 0x36, 0x34, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, |
||||
0xff, 0xff, 0x00, 0x00, 0x00, 0x30, 0x61, 0x76, |
||||
0x63, 0x43, 0x01, 0x42, 0xc0, 0x1f, 0xff, 0xe1, |
||||
0x00, 0x19, 0x67, 0x42, 0xc0, 0x1f, 0xd9, 0x00, |
||||
0xf0, 0x11, 0x7e, 0xf0, 0x11, 0x00, 0x00, 0x03, |
||||
0x00, 0x01, 0x00, 0x00, 0x03, 0x00, 0x30, 0x8f, |
||||
0x18, 0x32, 0x48, 0x01, 0x00, 0x04, 0x68, 0xcb, |
||||
0x8c, 0xb2, 0x00, 0x00, 0x00, 0x10, 0x70, 0x61, |
||||
0x73, 0x70, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, |
||||
0x00, 0x01, 0x00, 0x00, 0x00, 0x10, 0x73, 0x74, |
||||
0x74, 0x73, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x73, 0x74, |
||||
0x73, 0x63, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x14, 0x73, 0x74, |
||||
0x73, 0x7a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x10, 0x73, 0x74, 0x63, 0x6f, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x28, 0x6d, 0x76, 0x65, 0x78, 0x00, 0x00, |
||||
0x00, 0x20, 0x74, 0x72, 0x65, 0x78, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, |
||||
0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
} |
||||
|
||||
var init Init |
||||
err := init.Unmarshal(byts) |
||||
require.NoError(t, err) |
||||
|
||||
require.Equal(t, Init{ |
||||
Tracks: []*InitTrack{ |
||||
{ |
||||
ID: 256, |
||||
TimeScale: 10000000, |
||||
Track: &gortsplib.TrackH264{ |
||||
PayloadType: 96, |
||||
SPS: []byte{ |
||||
0x67, 0x42, 0xc0, 0x1f, 0xd9, 0x00, 0xf0, 0x11, |
||||
0x7e, 0xf0, 0x11, 0x00, 0x00, 0x03, 0x00, 0x01, |
||||
0x00, 0x00, 0x03, 0x00, 0x30, 0x8f, 0x18, 0x32, |
||||
0x48, |
||||
}, |
||||
PPS: []byte{ |
||||
0x68, 0xcb, 0x8c, 0xb2, |
||||
}, |
||||
}, |
||||
}, |
||||
}, |
||||
}, init) |
||||
}) |
||||
|
||||
t.Run("audio", func(t *testing.T) { |
||||
byts := []byte{ |
||||
0x00, 0x00, 0x00, 0x18, |
||||
'f', 't', 'y', 'p', |
||||
0x69, 0x73, 0x6f, 0x35, 0x00, 0x00, 0x00, 0x01, |
||||
0x69, 0x73, 0x6f, 0x35, 0x64, 0x61, 0x73, 0x68, |
||||
0x00, 0x00, 0x02, 0x43, |
||||
'm', 'o', 'o', 'v', |
||||
0x00, 0x00, 0x00, 0x6c, |
||||
'm', 'v', 'h', 'd', |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x98, 0x96, 0x80, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, |
||||
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x01, 0xa7, |
||||
't', 'r', 'a', 'k', |
||||
0x00, 0x00, 0x00, 0x5c, |
||||
't', 'k', 'h', 'd', |
||||
0x00, 0x00, 0x00, 0x07, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x01, 0x43, 0x6d, 0x64, 0x69, 0x61, |
||||
0x00, 0x00, 0x00, 0x20, |
||||
'm', 'd', 'h', 'd', |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x98, 0x96, 0x80, |
||||
0x00, 0x00, 0x00, 0x00, 0x55, 0xc4, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x38, 0x68, 0x64, 0x6c, 0x72, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x73, 0x6f, 0x75, 0x6e, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x42, 0x72, 0x6f, 0x61, 0x64, 0x70, 0x65, 0x61, |
||||
0x6b, 0x20, 0x53, 0x6f, 0x75, 0x6e, 0x64, 0x20, |
||||
0x48, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x72, 0x00, |
||||
0x00, 0x00, 0x00, 0xe3, |
||||
'm', 'i', 'n', 'f', |
||||
0x00, 0x00, 0x00, 0x10, |
||||
's', 'm', 'h', 'd', |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x24, |
||||
'd', 'i', 'n', 'f', |
||||
0x00, 0x00, 0x00, 0x1c, 0x64, 0x72, 0x65, 0x66, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, |
||||
0x00, 0x00, 0x00, 0x0c, 0x75, 0x72, 0x6c, 0x20, |
||||
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0xa7, |
||||
0x73, 0x74, 0x62, 0x6c, 0x00, 0x00, 0x00, 0x5b, |
||||
0x73, 0x74, 0x73, 0x64, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x4b, |
||||
0x6d, 0x70, 0x34, 0x61, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x10, |
||||
0x00, 0x00, 0x00, 0x00, 0xbb, 0x80, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x27, 0x65, 0x73, 0x64, 0x73, |
||||
0x00, 0x00, 0x00, 0x00, 0x03, 0x19, 0x00, 0x00, |
||||
0x00, 0x04, 0x11, 0x40, 0x15, 0x00, 0x30, 0x00, |
||||
0x00, 0x11, 0x94, 0x00, 0x00, 0x11, 0x94, 0x00, |
||||
0x05, 0x02, 0x11, 0x90, 0x06, 0x01, 0x02, 0x00, |
||||
0x00, 0x00, 0x10, 0x73, 0x74, 0x74, 0x73, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x10, 0x73, 0x74, 0x73, 0x63, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x14, 0x73, 0x74, 0x73, 0x7a, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x73, |
||||
0x74, 0x63, 0x6f, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x28, 0x6d, |
||||
0x76, 0x65, 0x78, 0x00, 0x00, 0x00, 0x20, 0x74, |
||||
0x72, 0x65, 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, |
||||
} |
||||
|
||||
var init Init |
||||
err := init.Unmarshal(byts) |
||||
require.NoError(t, err) |
||||
|
||||
require.Equal(t, Init{ |
||||
Tracks: []*InitTrack{ |
||||
{ |
||||
ID: 257, |
||||
TimeScale: 10000000, |
||||
Track: &gortsplib.TrackMPEG4Audio{ |
||||
PayloadType: 96, |
||||
Config: &mpeg4audio.Config{ |
||||
Type: mpeg4audio.ObjectTypeAACLC, |
||||
SampleRate: 48000, |
||||
ChannelCount: 2, |
||||
}, |
||||
SizeLength: 13, |
||||
IndexLength: 3, |
||||
IndexDeltaLength: 3, |
||||
}, |
||||
}, |
||||
}, |
||||
}, init) |
||||
}) |
||||
} |
@ -0,0 +1,382 @@
@@ -0,0 +1,382 @@
|
||||
package fmp4 |
||||
|
||||
import ( |
||||
gomp4 "github.com/abema/go-mp4" |
||||
"github.com/aler9/gortsplib" |
||||
|
||||
"github.com/aler9/gortsplib/pkg/h264" |
||||
) |
||||
|
||||
// InitTrack is a track of Init.
|
||||
type InitTrack struct { |
||||
ID int |
||||
TimeScale uint32 |
||||
Track gortsplib.Track |
||||
} |
||||
|
||||
func (track *InitTrack) marshal(w *mp4Writer) error { |
||||
/* |
||||
trak |
||||
- tkhd |
||||
- mdia |
||||
- mdhd |
||||
- hdlr |
||||
- minf |
||||
- vmhd (video only) |
||||
- smhd (audio only) |
||||
- dinf |
||||
- dref |
||||
- url |
||||
- stbl |
||||
- stsd |
||||
- avc1 (h264 only) |
||||
- avcC |
||||
- pasp |
||||
- btrt |
||||
- mp4a (mpeg4audio only) |
||||
- esds |
||||
- btrt |
||||
- stts |
||||
- stsc |
||||
- stsz |
||||
- stco |
||||
*/ |
||||
|
||||
_, err := w.writeBoxStart(&gomp4.Trak{}) // <trak>
|
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
var sps []byte |
||||
var pps []byte |
||||
var spsp h264.SPS |
||||
var width int |
||||
var height int |
||||
|
||||
switch ttrack := track.Track.(type) { |
||||
case *gortsplib.TrackH264: |
||||
sps = ttrack.SafeSPS() |
||||
pps = ttrack.SafePPS() |
||||
|
||||
err = spsp.Unmarshal(sps) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
width = spsp.Width() |
||||
height = spsp.Height() |
||||
|
||||
_, err = w.WriteBox(&gomp4.Tkhd{ // <tkhd/>
|
||||
FullBox: gomp4.FullBox{ |
||||
Flags: [3]byte{0, 0, 3}, |
||||
}, |
||||
TrackID: uint32(track.ID), |
||||
Width: uint32(width * 65536), |
||||
Height: uint32(height * 65536), |
||||
Matrix: [9]int32{0x00010000, 0, 0, 0, 0x00010000, 0, 0, 0, 0x40000000}, |
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
case *gortsplib.TrackMPEG4Audio: |
||||
_, err = w.WriteBox(&gomp4.Tkhd{ // <tkhd/>
|
||||
FullBox: gomp4.FullBox{ |
||||
Flags: [3]byte{0, 0, 3}, |
||||
}, |
||||
TrackID: uint32(track.ID), |
||||
AlternateGroup: 1, |
||||
Volume: 256, |
||||
Matrix: [9]int32{0x00010000, 0, 0, 0, 0x00010000, 0, 0, 0, 0x40000000}, |
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
} |
||||
|
||||
_, err = w.writeBoxStart(&gomp4.Mdia{}) // <mdia>
|
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
_, err = w.WriteBox(&gomp4.Mdhd{ // <mdhd/>
|
||||
Timescale: track.TimeScale, |
||||
Language: [3]byte{'u', 'n', 'd'}, |
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
switch track.Track.(type) { |
||||
case *gortsplib.TrackH264: |
||||
_, err = w.WriteBox(&gomp4.Hdlr{ // <hdlr/>
|
||||
HandlerType: [4]byte{'v', 'i', 'd', 'e'}, |
||||
Name: "VideoHandler", |
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
case *gortsplib.TrackMPEG4Audio: |
||||
_, err = w.WriteBox(&gomp4.Hdlr{ // <hdlr/>
|
||||
HandlerType: [4]byte{'s', 'o', 'u', 'n'}, |
||||
Name: "SoundHandler", |
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
} |
||||
|
||||
_, err = w.writeBoxStart(&gomp4.Minf{}) // <minf>
|
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
switch track.Track.(type) { |
||||
case *gortsplib.TrackH264: |
||||
_, err = w.WriteBox(&gomp4.Vmhd{ // <vmhd/>
|
||||
FullBox: gomp4.FullBox{ |
||||
Flags: [3]byte{0, 0, 1}, |
||||
}, |
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
case *gortsplib.TrackMPEG4Audio: |
||||
_, err = w.WriteBox(&gomp4.Smhd{ // <smhd/>
|
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
} |
||||
|
||||
_, err = w.writeBoxStart(&gomp4.Dinf{}) // <dinf>
|
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
_, err = w.writeBoxStart(&gomp4.Dref{ // <dref>
|
||||
EntryCount: 1, |
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
_, err = w.WriteBox(&gomp4.Url{ // <url/>
|
||||
FullBox: gomp4.FullBox{ |
||||
Flags: [3]byte{0, 0, 1}, |
||||
}, |
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
err = w.writeBoxEnd() // </dref>
|
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
err = w.writeBoxEnd() // </dinf>
|
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
_, err = w.writeBoxStart(&gomp4.Stbl{}) // <stbl>
|
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
_, err = w.writeBoxStart(&gomp4.Stsd{ // <stsd>
|
||||
EntryCount: 1, |
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
switch ttrack := track.Track.(type) { |
||||
case *gortsplib.TrackH264: |
||||
_, err = w.writeBoxStart(&gomp4.VisualSampleEntry{ // <avc1>
|
||||
SampleEntry: gomp4.SampleEntry{ |
||||
AnyTypeBox: gomp4.AnyTypeBox{ |
||||
Type: gomp4.BoxTypeAvc1(), |
||||
}, |
||||
DataReferenceIndex: 1, |
||||
}, |
||||
Width: uint16(width), |
||||
Height: uint16(height), |
||||
Horizresolution: 4718592, |
||||
Vertresolution: 4718592, |
||||
FrameCount: 1, |
||||
Depth: 24, |
||||
PreDefined3: -1, |
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
_, err = w.WriteBox(&gomp4.AVCDecoderConfiguration{ // <avcc/>
|
||||
AnyTypeBox: gomp4.AnyTypeBox{ |
||||
Type: gomp4.BoxTypeAvcC(), |
||||
}, |
||||
ConfigurationVersion: 1, |
||||
Profile: spsp.ProfileIdc, |
||||
ProfileCompatibility: sps[2], |
||||
Level: spsp.LevelIdc, |
||||
LengthSizeMinusOne: 3, |
||||
NumOfSequenceParameterSets: 1, |
||||
SequenceParameterSets: []gomp4.AVCParameterSet{ |
||||
{ |
||||
Length: uint16(len(sps)), |
||||
NALUnit: sps, |
||||
}, |
||||
}, |
||||
NumOfPictureParameterSets: 1, |
||||
PictureParameterSets: []gomp4.AVCParameterSet{ |
||||
{ |
||||
Length: uint16(len(pps)), |
||||
NALUnit: pps, |
||||
}, |
||||
}, |
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
_, err = w.WriteBox(&gomp4.Btrt{ // <btrt/>
|
||||
MaxBitrate: 1000000, |
||||
AvgBitrate: 1000000, |
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
err = w.writeBoxEnd() // </avc1>
|
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
case *gortsplib.TrackMPEG4Audio: |
||||
_, err = w.writeBoxStart(&gomp4.AudioSampleEntry{ // <mp4a>
|
||||
SampleEntry: gomp4.SampleEntry{ |
||||
AnyTypeBox: gomp4.AnyTypeBox{ |
||||
Type: gomp4.BoxTypeMp4a(), |
||||
}, |
||||
DataReferenceIndex: 1, |
||||
}, |
||||
ChannelCount: uint16(ttrack.Config.ChannelCount), |
||||
SampleSize: 16, |
||||
SampleRate: uint32(ttrack.ClockRate() * 65536), |
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
enc, _ := ttrack.Config.Marshal() |
||||
|
||||
_, err = w.WriteBox(&gomp4.Esds{ // <esds/>
|
||||
FullBox: gomp4.FullBox{ |
||||
Version: 0, |
||||
Flags: [3]byte{0x00, 0x00, 0x00}, |
||||
}, |
||||
Descriptors: []gomp4.Descriptor{ |
||||
{ |
||||
Tag: gomp4.ESDescrTag, |
||||
Size: 32 + uint32(len(enc)), |
||||
ESDescriptor: &gomp4.ESDescriptor{ |
||||
ESID: uint16(track.ID), |
||||
}, |
||||
}, |
||||
{ |
||||
Tag: gomp4.DecoderConfigDescrTag, |
||||
Size: 18 + uint32(len(enc)), |
||||
DecoderConfigDescriptor: &gomp4.DecoderConfigDescriptor{ |
||||
ObjectTypeIndication: 0x40, |
||||
StreamType: 0x05, |
||||
UpStream: false, |
||||
Reserved: true, |
||||
MaxBitrate: 128825, |
||||
AvgBitrate: 128825, |
||||
}, |
||||
}, |
||||
{ |
||||
Tag: gomp4.DecSpecificInfoTag, |
||||
Size: uint32(len(enc)), |
||||
Data: enc, |
||||
}, |
||||
{ |
||||
Tag: gomp4.SLConfigDescrTag, |
||||
Size: 1, |
||||
Data: []byte{0x02}, |
||||
}, |
||||
}, |
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
_, err = w.WriteBox(&gomp4.Btrt{ // <btrt/>
|
||||
MaxBitrate: 128825, |
||||
AvgBitrate: 128825, |
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
err = w.writeBoxEnd() // </mp4a>
|
||||
if err != nil { |
||||
return err |
||||
} |
||||
} |
||||
|
||||
err = w.writeBoxEnd() // </stsd>
|
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
_, err = w.WriteBox(&gomp4.Stts{ // <stts>
|
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
_, err = w.WriteBox(&gomp4.Stsc{ // <stsc>
|
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
_, err = w.WriteBox(&gomp4.Stsz{ // <stsz>
|
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
_, err = w.WriteBox(&gomp4.Stco{ // <stco>
|
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
err = w.writeBoxEnd() // </stbl>
|
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
err = w.writeBoxEnd() // </minf>
|
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
err = w.writeBoxEnd() // </mdia>
|
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
err = w.writeBoxEnd() // </trak>
|
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
return nil |
||||
} |
@ -1,618 +0,0 @@
@@ -1,618 +0,0 @@
|
||||
package fmp4 |
||||
|
||||
import ( |
||||
gomp4 "github.com/abema/go-mp4" |
||||
"github.com/aler9/gortsplib" |
||||
"github.com/aler9/gortsplib/pkg/h264" |
||||
) |
||||
|
||||
type myEsds struct { |
||||
gomp4.FullBox `mp4:"0,extend"` |
||||
Data []byte `mp4:"1,size=8"` |
||||
} |
||||
|
||||
func (*myEsds) GetType() gomp4.BoxType { |
||||
return gomp4.StrToBoxType("esds") |
||||
} |
||||
|
||||
func init() { //nolint:gochecknoinits
|
||||
gomp4.AddBoxDef(&myEsds{}, 0) |
||||
} |
||||
|
||||
func initWriteVideoTrack(w *mp4Writer, trackID int, videoTrack *gortsplib.TrackH264) error { |
||||
/* |
||||
trak |
||||
- tkhd |
||||
- mdia |
||||
- mdhd |
||||
- hdlr |
||||
- minf |
||||
- vmhd |
||||
- dinf |
||||
- dref |
||||
- url |
||||
- stbl |
||||
- stsd |
||||
- avc1 |
||||
- avcC |
||||
- pasp |
||||
- btrt |
||||
- stts |
||||
- stsc |
||||
- stsz |
||||
- stco |
||||
*/ |
||||
|
||||
_, err := w.writeBoxStart(&gomp4.Trak{}) // <trak>
|
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
sps := videoTrack.SafeSPS() |
||||
pps := videoTrack.SafePPS() |
||||
|
||||
var spsp h264.SPS |
||||
err = spsp.Unmarshal(sps) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
width := spsp.Width() |
||||
height := spsp.Height() |
||||
|
||||
_, err = w.WriteBox(&gomp4.Tkhd{ // <tkhd/>
|
||||
FullBox: gomp4.FullBox{ |
||||
Flags: [3]byte{0, 0, 3}, |
||||
}, |
||||
TrackID: uint32(trackID), |
||||
Width: uint32(width * 65536), |
||||
Height: uint32(height * 65536), |
||||
Matrix: [9]int32{0x00010000, 0, 0, 0, 0x00010000, 0, 0, 0, 0x40000000}, |
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
_, err = w.writeBoxStart(&gomp4.Mdia{}) // <mdia>
|
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
_, err = w.WriteBox(&gomp4.Mdhd{ // <mdhd/>
|
||||
Timescale: videoTimescale, // the number of time units that pass per second
|
||||
Language: [3]byte{'u', 'n', 'd'}, |
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
_, err = w.WriteBox(&gomp4.Hdlr{ // <hdlr/>
|
||||
HandlerType: [4]byte{'v', 'i', 'd', 'e'}, |
||||
Name: "VideoHandler", |
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
_, err = w.writeBoxStart(&gomp4.Minf{}) // <minf>
|
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
_, err = w.WriteBox(&gomp4.Vmhd{ // <vmhd/>
|
||||
FullBox: gomp4.FullBox{ |
||||
Flags: [3]byte{0, 0, 1}, |
||||
}, |
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
_, err = w.writeBoxStart(&gomp4.Dinf{}) // <dinf>
|
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
_, err = w.writeBoxStart(&gomp4.Dref{ // <dref>
|
||||
EntryCount: 1, |
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
_, err = w.WriteBox(&gomp4.Url{ // <url/>
|
||||
FullBox: gomp4.FullBox{ |
||||
Flags: [3]byte{0, 0, 1}, |
||||
}, |
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
err = w.writeBoxEnd() // </dref>
|
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
err = w.writeBoxEnd() // </dinf>
|
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
_, err = w.writeBoxStart(&gomp4.Stbl{}) // <stbl>
|
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
_, err = w.writeBoxStart(&gomp4.Stsd{ // <stsd>
|
||||
EntryCount: 1, |
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
_, err = w.writeBoxStart(&gomp4.VisualSampleEntry{ // <avc1>
|
||||
SampleEntry: gomp4.SampleEntry{ |
||||
AnyTypeBox: gomp4.AnyTypeBox{ |
||||
Type: gomp4.BoxTypeAvc1(), |
||||
}, |
||||
DataReferenceIndex: 1, |
||||
}, |
||||
Width: uint16(width), |
||||
Height: uint16(height), |
||||
Horizresolution: 4718592, |
||||
Vertresolution: 4718592, |
||||
FrameCount: 1, |
||||
Depth: 24, |
||||
PreDefined3: -1, |
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
_, err = w.WriteBox(&gomp4.AVCDecoderConfiguration{ // <avcc/>
|
||||
AnyTypeBox: gomp4.AnyTypeBox{ |
||||
Type: gomp4.BoxTypeAvcC(), |
||||
}, |
||||
ConfigurationVersion: 1, |
||||
Profile: spsp.ProfileIdc, |
||||
ProfileCompatibility: sps[2], |
||||
Level: spsp.LevelIdc, |
||||
LengthSizeMinusOne: 3, |
||||
NumOfSequenceParameterSets: 1, |
||||
SequenceParameterSets: []gomp4.AVCParameterSet{ |
||||
{ |
||||
Length: uint16(len(sps)), |
||||
NALUnit: sps, |
||||
}, |
||||
}, |
||||
NumOfPictureParameterSets: 1, |
||||
PictureParameterSets: []gomp4.AVCParameterSet{ |
||||
{ |
||||
Length: uint16(len(pps)), |
||||
NALUnit: pps, |
||||
}, |
||||
}, |
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
_, err = w.WriteBox(&gomp4.Btrt{ // <btrt/>
|
||||
MaxBitrate: 1000000, |
||||
AvgBitrate: 1000000, |
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
err = w.writeBoxEnd() // </avc1>
|
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
err = w.writeBoxEnd() // </stsd>
|
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
_, err = w.WriteBox(&gomp4.Stts{ // <stts>
|
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
_, err = w.WriteBox(&gomp4.Stsc{ // <stsc>
|
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
_, err = w.WriteBox(&gomp4.Stsz{ // <stsz>
|
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
_, err = w.WriteBox(&gomp4.Stco{ // <stco>
|
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
err = w.writeBoxEnd() // </stbl>
|
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
err = w.writeBoxEnd() // </minf>
|
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
err = w.writeBoxEnd() // </mdia>
|
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
err = w.writeBoxEnd() // </trak>
|
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
func initWriteAudioTrack(w *mp4Writer, trackID int, audioTrack *gortsplib.TrackMPEG4Audio) error { |
||||
/* |
||||
trak |
||||
- tkhd |
||||
- mdia |
||||
- mdhd |
||||
- hdlr |
||||
- minf |
||||
- smhd |
||||
- dinf |
||||
- dref |
||||
- url |
||||
- stbl |
||||
- stsd |
||||
- mp4a |
||||
- esds |
||||
- btrt |
||||
- stts |
||||
- stsc |
||||
- stsz |
||||
- stco |
||||
*/ |
||||
|
||||
_, err := w.writeBoxStart(&gomp4.Trak{}) // <trak>
|
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
_, err = w.WriteBox(&gomp4.Tkhd{ // <tkhd/>
|
||||
FullBox: gomp4.FullBox{ |
||||
Flags: [3]byte{0, 0, 3}, |
||||
}, |
||||
TrackID: uint32(trackID), |
||||
AlternateGroup: 1, |
||||
Volume: 256, |
||||
Matrix: [9]int32{0x00010000, 0, 0, 0, 0x00010000, 0, 0, 0, 0x40000000}, |
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
_, err = w.writeBoxStart(&gomp4.Mdia{}) // <mdia>
|
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
_, err = w.WriteBox(&gomp4.Mdhd{ // <mdhd/>
|
||||
Timescale: uint32(audioTrack.ClockRate()), |
||||
Language: [3]byte{'u', 'n', 'd'}, |
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
_, err = w.WriteBox(&gomp4.Hdlr{ // <hdlr/>
|
||||
HandlerType: [4]byte{'s', 'o', 'u', 'n'}, |
||||
Name: "SoundHandler", |
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
_, err = w.writeBoxStart(&gomp4.Minf{}) // <minf>
|
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
_, err = w.WriteBox(&gomp4.Smhd{ // <smhd/>
|
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
_, err = w.writeBoxStart(&gomp4.Dinf{}) // <dinf>
|
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
_, err = w.writeBoxStart(&gomp4.Dref{ // <dref>
|
||||
EntryCount: 1, |
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
_, err = w.WriteBox(&gomp4.Url{ // <url/>
|
||||
FullBox: gomp4.FullBox{ |
||||
Flags: [3]byte{0, 0, 1}, |
||||
}, |
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
err = w.writeBoxEnd() // </dref>
|
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
err = w.writeBoxEnd() // </dinf>
|
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
_, err = w.writeBoxStart(&gomp4.Stbl{}) // <stbl>
|
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
_, err = w.writeBoxStart(&gomp4.Stsd{ // <stsd>
|
||||
EntryCount: 1, |
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
_, err = w.writeBoxStart(&gomp4.AudioSampleEntry{ // <mp4a>
|
||||
SampleEntry: gomp4.SampleEntry{ |
||||
AnyTypeBox: gomp4.AnyTypeBox{ |
||||
Type: gomp4.BoxTypeMp4a(), |
||||
}, |
||||
DataReferenceIndex: 1, |
||||
}, |
||||
ChannelCount: uint16(audioTrack.Config.ChannelCount), |
||||
SampleSize: 16, |
||||
SampleRate: uint32(audioTrack.ClockRate() * 65536), |
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
enc, _ := audioTrack.Config.Marshal() |
||||
|
||||
decSpecificInfoTagSize := uint8(len(enc)) |
||||
decSpecificInfoTag := append( |
||||
[]byte{ |
||||
gomp4.DecSpecificInfoTag, |
||||
0x80, 0x80, 0x80, decSpecificInfoTagSize, // size
|
||||
}, |
||||
enc..., |
||||
) |
||||
|
||||
esDescrTag := []byte{ |
||||
gomp4.ESDescrTag, |
||||
0x80, 0x80, 0x80, 32 + decSpecificInfoTagSize, // size
|
||||
0x00, |
||||
byte(trackID), // ES_ID
|
||||
0x00, |
||||
} |
||||
|
||||
decoderConfigDescrTag := []byte{ |
||||
gomp4.DecoderConfigDescrTag, |
||||
0x80, 0x80, 0x80, 18 + decSpecificInfoTagSize, // size
|
||||
0x40, // object type indicator (MPEG-4 Audio)
|
||||
0x15, 0x00, |
||||
0x00, 0x00, 0x00, 0x01, |
||||
0xf7, 0x39, 0x00, 0x01, |
||||
0xf7, 0x39, |
||||
} |
||||
|
||||
slConfigDescrTag := []byte{ |
||||
gomp4.SLConfigDescrTag, |
||||
0x80, 0x80, 0x80, 0x01, // size (1)
|
||||
0x02, |
||||
} |
||||
|
||||
data := make([]byte, len(esDescrTag)+len(decoderConfigDescrTag)+len(decSpecificInfoTag)+len(slConfigDescrTag)) |
||||
pos := 0 |
||||
|
||||
pos += copy(data[pos:], esDescrTag) |
||||
pos += copy(data[pos:], decoderConfigDescrTag) |
||||
pos += copy(data[pos:], decSpecificInfoTag) |
||||
copy(data[pos:], slConfigDescrTag) |
||||
|
||||
_, err = w.WriteBox(&myEsds{ // <esds/>
|
||||
Data: data, |
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
_, err = w.WriteBox(&gomp4.Btrt{ // <btrt/>
|
||||
MaxBitrate: 128825, |
||||
AvgBitrate: 128825, |
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
err = w.writeBoxEnd() // </mp4a>
|
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
err = w.writeBoxEnd() // </stsd>
|
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
_, err = w.WriteBox(&gomp4.Stts{ // <stts>
|
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
_, err = w.WriteBox(&gomp4.Stsc{ // <stsc>
|
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
_, err = w.WriteBox(&gomp4.Stsz{ // <stsz>
|
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
_, err = w.WriteBox(&gomp4.Stco{ // <stco>
|
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
err = w.writeBoxEnd() // </stbl>
|
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
err = w.writeBoxEnd() // </minf>
|
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
err = w.writeBoxEnd() // </mdia>
|
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
err = w.writeBoxEnd() // </trak>
|
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
// InitWrite generates a FMP4 initialization file.
|
||||
func InitWrite( |
||||
videoTrack *gortsplib.TrackH264, |
||||
audioTrack *gortsplib.TrackMPEG4Audio, |
||||
) ([]byte, error) { |
||||
/* |
||||
- ftyp |
||||
- moov |
||||
- mvhd |
||||
- trak (video) |
||||
- trak (audio) |
||||
- mvex |
||||
- trex (video) |
||||
- trex (audio) |
||||
*/ |
||||
|
||||
w := newMP4Writer() |
||||
|
||||
_, err := w.WriteBox(&gomp4.Ftyp{ // <ftyp/>
|
||||
MajorBrand: [4]byte{'m', 'p', '4', '2'}, |
||||
MinorVersion: 1, |
||||
CompatibleBrands: []gomp4.CompatibleBrandElem{ |
||||
{CompatibleBrand: [4]byte{'m', 'p', '4', '1'}}, |
||||
{CompatibleBrand: [4]byte{'m', 'p', '4', '2'}}, |
||||
{CompatibleBrand: [4]byte{'i', 's', 'o', 'm'}}, |
||||
{CompatibleBrand: [4]byte{'h', 'l', 's', 'f'}}, |
||||
}, |
||||
}) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
_, err = w.writeBoxStart(&gomp4.Moov{}) // <moov>
|
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
_, err = w.WriteBox(&gomp4.Mvhd{ // <mvhd/>
|
||||
Timescale: 1000, |
||||
Rate: 65536, |
||||
Volume: 256, |
||||
Matrix: [9]int32{0x00010000, 0, 0, 0, 0x00010000, 0, 0, 0, 0x40000000}, |
||||
NextTrackID: 2, |
||||
}) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
trackID := 1 |
||||
|
||||
if videoTrack != nil { |
||||
err := initWriteVideoTrack(w, trackID, videoTrack) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
trackID++ |
||||
} |
||||
|
||||
if audioTrack != nil { |
||||
err := initWriteAudioTrack(w, trackID, audioTrack) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
} |
||||
|
||||
_, err = w.writeBoxStart(&gomp4.Mvex{}) // <mvex>
|
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
trackID = 1 |
||||
|
||||
if videoTrack != nil { |
||||
_, err = w.WriteBox(&gomp4.Trex{ // <trex/>
|
||||
TrackID: uint32(trackID), |
||||
DefaultSampleDescriptionIndex: 1, |
||||
}) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
trackID++ |
||||
} |
||||
|
||||
if audioTrack != nil { |
||||
_, err = w.WriteBox(&gomp4.Trex{ // <trex/>
|
||||
TrackID: uint32(trackID), |
||||
DefaultSampleDescriptionIndex: 1, |
||||
}) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
} |
||||
|
||||
err = w.writeBoxEnd() // </mvex>
|
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
err = w.writeBoxEnd() // </moov>
|
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
return w.bytes(), nil |
||||
} |
@ -1,318 +0,0 @@
@@ -1,318 +0,0 @@
|
||||
//nolint:dupl
|
||||
package fmp4 |
||||
|
||||
import ( |
||||
"bytes" |
||||
"testing" |
||||
|
||||
gomp4 "github.com/abema/go-mp4" |
||||
"github.com/aler9/gortsplib" |
||||
"github.com/aler9/gortsplib/pkg/mpeg4audio" |
||||
"github.com/stretchr/testify/require" |
||||
) |
||||
|
||||
func testMP4(t *testing.T, byts []byte, boxes []gomp4.BoxPath) { |
||||
i := 0 |
||||
_, err := gomp4.ReadBoxStructure(bytes.NewReader(byts), func(h *gomp4.ReadHandle) (interface{}, error) { |
||||
require.Equal(t, boxes[i], h.Path) |
||||
i++ |
||||
return h.Expand() |
||||
}) |
||||
require.NoError(t, err) |
||||
} |
||||
|
||||
var testSPS = []byte{ |
||||
0x67, 0x42, 0xc0, 0x28, 0xd9, 0x00, 0x78, 0x02, |
||||
0x27, 0xe5, 0x84, 0x00, 0x00, 0x03, 0x00, 0x04, |
||||
0x00, 0x00, 0x03, 0x00, 0xf0, 0x3c, 0x60, 0xc9, |
||||
0x20, |
||||
} |
||||
|
||||
var testVideoTrack = &gortsplib.TrackH264{ |
||||
PayloadType: 96, |
||||
SPS: testSPS, |
||||
PPS: []byte{0x08}, |
||||
} |
||||
|
||||
var testAudioTrack = &gortsplib.TrackMPEG4Audio{ |
||||
PayloadType: 97, |
||||
Config: &mpeg4audio.Config{ |
||||
Type: 2, |
||||
SampleRate: 44100, |
||||
ChannelCount: 2, |
||||
}, |
||||
SizeLength: 13, |
||||
IndexLength: 3, |
||||
IndexDeltaLength: 3, |
||||
} |
||||
|
||||
func TestInitWrite(t *testing.T) { |
||||
t.Run("video + audio", func(t *testing.T) { |
||||
byts, err := InitWrite(testVideoTrack, testAudioTrack) |
||||
require.NoError(t, err) |
||||
|
||||
boxes := []gomp4.BoxPath{ |
||||
{gomp4.BoxTypeFtyp()}, |
||||
{gomp4.BoxTypeMoov()}, |
||||
{gomp4.BoxTypeMoov(), gomp4.BoxTypeMvhd()}, |
||||
{gomp4.BoxTypeMoov(), gomp4.BoxTypeTrak()}, |
||||
{gomp4.BoxTypeMoov(), gomp4.BoxTypeTrak(), gomp4.BoxTypeTkhd()}, |
||||
{gomp4.BoxTypeMoov(), gomp4.BoxTypeTrak(), gomp4.BoxTypeMdia()}, |
||||
{gomp4.BoxTypeMoov(), gomp4.BoxTypeTrak(), gomp4.BoxTypeMdia(), gomp4.BoxTypeMdhd()}, |
||||
{gomp4.BoxTypeMoov(), gomp4.BoxTypeTrak(), gomp4.BoxTypeMdia(), gomp4.BoxTypeHdlr()}, |
||||
{gomp4.BoxTypeMoov(), gomp4.BoxTypeTrak(), gomp4.BoxTypeMdia(), gomp4.BoxTypeMinf()}, |
||||
{gomp4.BoxTypeMoov(), gomp4.BoxTypeTrak(), gomp4.BoxTypeMdia(), gomp4.BoxTypeMinf(), gomp4.BoxTypeVmhd()}, |
||||
{gomp4.BoxTypeMoov(), gomp4.BoxTypeTrak(), gomp4.BoxTypeMdia(), gomp4.BoxTypeMinf(), gomp4.BoxTypeDinf()}, |
||||
{ |
||||
gomp4.BoxTypeMoov(), gomp4.BoxTypeTrak(), gomp4.BoxTypeMdia(), gomp4.BoxTypeMinf(), |
||||
gomp4.BoxTypeDinf(), gomp4.BoxTypeDref(), |
||||
}, |
||||
{ |
||||
gomp4.BoxTypeMoov(), gomp4.BoxTypeTrak(), gomp4.BoxTypeMdia(), gomp4.BoxTypeMinf(), |
||||
gomp4.BoxTypeDinf(), gomp4.BoxTypeDref(), gomp4.BoxTypeUrl(), |
||||
}, |
||||
{ |
||||
gomp4.BoxTypeMoov(), gomp4.BoxTypeTrak(), gomp4.BoxTypeMdia(), gomp4.BoxTypeMinf(), |
||||
gomp4.BoxTypeStbl(), |
||||
}, |
||||
{ |
||||
gomp4.BoxTypeMoov(), gomp4.BoxTypeTrak(), gomp4.BoxTypeMdia(), gomp4.BoxTypeMinf(), |
||||
gomp4.BoxTypeStbl(), gomp4.BoxTypeStsd(), |
||||
}, |
||||
{ |
||||
gomp4.BoxTypeMoov(), gomp4.BoxTypeTrak(), gomp4.BoxTypeMdia(), gomp4.BoxTypeMinf(), |
||||
gomp4.BoxTypeStbl(), gomp4.BoxTypeStsd(), gomp4.BoxTypeAvc1(), |
||||
}, |
||||
{ |
||||
gomp4.BoxTypeMoov(), gomp4.BoxTypeTrak(), gomp4.BoxTypeMdia(), gomp4.BoxTypeMinf(), |
||||
gomp4.BoxTypeStbl(), gomp4.BoxTypeStsd(), gomp4.BoxTypeAvc1(), gomp4.BoxTypeAvcC(), |
||||
}, |
||||
{ |
||||
gomp4.BoxTypeMoov(), gomp4.BoxTypeTrak(), gomp4.BoxTypeMdia(), gomp4.BoxTypeMinf(), |
||||
gomp4.BoxTypeStbl(), gomp4.BoxTypeStsd(), gomp4.BoxTypeAvc1(), gomp4.BoxTypeBtrt(), |
||||
}, |
||||
{ |
||||
gomp4.BoxTypeMoov(), gomp4.BoxTypeTrak(), gomp4.BoxTypeMdia(), gomp4.BoxTypeMinf(), |
||||
gomp4.BoxTypeStbl(), gomp4.BoxTypeStts(), |
||||
}, |
||||
{ |
||||
gomp4.BoxTypeMoov(), gomp4.BoxTypeTrak(), gomp4.BoxTypeMdia(), gomp4.BoxTypeMinf(), |
||||
gomp4.BoxTypeStbl(), gomp4.BoxTypeStsc(), |
||||
}, |
||||
{ |
||||
gomp4.BoxTypeMoov(), gomp4.BoxTypeTrak(), gomp4.BoxTypeMdia(), gomp4.BoxTypeMinf(), |
||||
gomp4.BoxTypeStbl(), gomp4.BoxTypeStsz(), |
||||
}, |
||||
{ |
||||
gomp4.BoxTypeMoov(), gomp4.BoxTypeTrak(), gomp4.BoxTypeMdia(), gomp4.BoxTypeMinf(), |
||||
gomp4.BoxTypeStbl(), gomp4.BoxTypeStco(), |
||||
}, |
||||
{gomp4.BoxTypeMoov(), gomp4.BoxTypeTrak()}, |
||||
{gomp4.BoxTypeMoov(), gomp4.BoxTypeTrak(), gomp4.BoxTypeTkhd()}, |
||||
{gomp4.BoxTypeMoov(), gomp4.BoxTypeTrak(), gomp4.BoxTypeMdia()}, |
||||
{gomp4.BoxTypeMoov(), gomp4.BoxTypeTrak(), gomp4.BoxTypeMdia(), gomp4.BoxTypeMdhd()}, |
||||
{gomp4.BoxTypeMoov(), gomp4.BoxTypeTrak(), gomp4.BoxTypeMdia(), gomp4.BoxTypeHdlr()}, |
||||
{gomp4.BoxTypeMoov(), gomp4.BoxTypeTrak(), gomp4.BoxTypeMdia(), gomp4.BoxTypeMinf()}, |
||||
{ |
||||
gomp4.BoxTypeMoov(), gomp4.BoxTypeTrak(), gomp4.BoxTypeMdia(), gomp4.BoxTypeMinf(), |
||||
gomp4.BoxTypeSmhd(), |
||||
}, |
||||
{ |
||||
gomp4.BoxTypeMoov(), gomp4.BoxTypeTrak(), gomp4.BoxTypeMdia(), gomp4.BoxTypeMinf(), |
||||
gomp4.BoxTypeDinf(), |
||||
}, |
||||
{ |
||||
gomp4.BoxTypeMoov(), gomp4.BoxTypeTrak(), gomp4.BoxTypeMdia(), gomp4.BoxTypeMinf(), |
||||
gomp4.BoxTypeDinf(), gomp4.BoxTypeDref(), |
||||
}, |
||||
{ |
||||
gomp4.BoxTypeMoov(), gomp4.BoxTypeTrak(), gomp4.BoxTypeMdia(), gomp4.BoxTypeMinf(), |
||||
gomp4.BoxTypeDinf(), gomp4.BoxTypeDref(), gomp4.BoxTypeUrl(), |
||||
}, |
||||
{ |
||||
gomp4.BoxTypeMoov(), gomp4.BoxTypeTrak(), gomp4.BoxTypeMdia(), gomp4.BoxTypeMinf(), |
||||
gomp4.BoxTypeStbl(), |
||||
}, |
||||
{ |
||||
gomp4.BoxTypeMoov(), gomp4.BoxTypeTrak(), gomp4.BoxTypeMdia(), gomp4.BoxTypeMinf(), |
||||
gomp4.BoxTypeStbl(), gomp4.BoxTypeStsd(), |
||||
}, |
||||
{ |
||||
gomp4.BoxTypeMoov(), gomp4.BoxTypeTrak(), gomp4.BoxTypeMdia(), gomp4.BoxTypeMinf(), |
||||
gomp4.BoxTypeStbl(), gomp4.BoxTypeStsd(), gomp4.BoxTypeMp4a(), |
||||
}, |
||||
{ |
||||
gomp4.BoxTypeMoov(), gomp4.BoxTypeTrak(), gomp4.BoxTypeMdia(), gomp4.BoxTypeMinf(), |
||||
gomp4.BoxTypeStbl(), gomp4.BoxTypeStsd(), gomp4.BoxTypeMp4a(), gomp4.BoxTypeEsds(), |
||||
}, |
||||
{ |
||||
gomp4.BoxTypeMoov(), gomp4.BoxTypeTrak(), gomp4.BoxTypeMdia(), gomp4.BoxTypeMinf(), |
||||
gomp4.BoxTypeStbl(), gomp4.BoxTypeStsd(), gomp4.BoxTypeMp4a(), gomp4.BoxTypeBtrt(), |
||||
}, |
||||
{ |
||||
gomp4.BoxTypeMoov(), gomp4.BoxTypeTrak(), gomp4.BoxTypeMdia(), gomp4.BoxTypeMinf(), |
||||
gomp4.BoxTypeStbl(), gomp4.BoxTypeStts(), |
||||
}, |
||||
{ |
||||
gomp4.BoxTypeMoov(), gomp4.BoxTypeTrak(), gomp4.BoxTypeMdia(), gomp4.BoxTypeMinf(), |
||||
gomp4.BoxTypeStbl(), gomp4.BoxTypeStsc(), |
||||
}, |
||||
{ |
||||
gomp4.BoxTypeMoov(), gomp4.BoxTypeTrak(), gomp4.BoxTypeMdia(), gomp4.BoxTypeMinf(), |
||||
gomp4.BoxTypeStbl(), gomp4.BoxTypeStsz(), |
||||
}, |
||||
{ |
||||
gomp4.BoxTypeMoov(), gomp4.BoxTypeTrak(), gomp4.BoxTypeMdia(), gomp4.BoxTypeMinf(), |
||||
gomp4.BoxTypeStbl(), gomp4.BoxTypeStco(), |
||||
}, |
||||
{gomp4.BoxTypeMoov(), gomp4.BoxTypeMvex()}, |
||||
{gomp4.BoxTypeMoov(), gomp4.BoxTypeMvex(), gomp4.BoxTypeTrex()}, |
||||
{gomp4.BoxTypeMoov(), gomp4.BoxTypeMvex(), gomp4.BoxTypeTrex()}, |
||||
} |
||||
testMP4(t, byts, boxes) |
||||
}) |
||||
|
||||
t.Run("video only", func(t *testing.T) { |
||||
byts, err := InitWrite(testVideoTrack, nil) |
||||
require.NoError(t, err) |
||||
|
||||
boxes := []gomp4.BoxPath{ |
||||
{gomp4.BoxTypeFtyp()}, |
||||
{gomp4.BoxTypeMoov()}, |
||||
{gomp4.BoxTypeMoov(), gomp4.BoxTypeMvhd()}, |
||||
{gomp4.BoxTypeMoov(), gomp4.BoxTypeTrak()}, |
||||
{gomp4.BoxTypeMoov(), gomp4.BoxTypeTrak(), gomp4.BoxTypeTkhd()}, |
||||
{gomp4.BoxTypeMoov(), gomp4.BoxTypeTrak(), gomp4.BoxTypeMdia()}, |
||||
{gomp4.BoxTypeMoov(), gomp4.BoxTypeTrak(), gomp4.BoxTypeMdia(), gomp4.BoxTypeMdhd()}, |
||||
{gomp4.BoxTypeMoov(), gomp4.BoxTypeTrak(), gomp4.BoxTypeMdia(), gomp4.BoxTypeHdlr()}, |
||||
{gomp4.BoxTypeMoov(), gomp4.BoxTypeTrak(), gomp4.BoxTypeMdia(), gomp4.BoxTypeMinf()}, |
||||
{ |
||||
gomp4.BoxTypeMoov(), gomp4.BoxTypeTrak(), gomp4.BoxTypeMdia(), gomp4.BoxTypeMinf(), |
||||
gomp4.BoxTypeVmhd(), |
||||
}, |
||||
{ |
||||
gomp4.BoxTypeMoov(), gomp4.BoxTypeTrak(), gomp4.BoxTypeMdia(), gomp4.BoxTypeMinf(), |
||||
gomp4.BoxTypeDinf(), |
||||
}, |
||||
{ |
||||
gomp4.BoxTypeMoov(), gomp4.BoxTypeTrak(), gomp4.BoxTypeMdia(), gomp4.BoxTypeMinf(), |
||||
gomp4.BoxTypeDinf(), gomp4.BoxTypeDref(), |
||||
}, |
||||
{ |
||||
gomp4.BoxTypeMoov(), gomp4.BoxTypeTrak(), gomp4.BoxTypeMdia(), gomp4.BoxTypeMinf(), |
||||
gomp4.BoxTypeDinf(), gomp4.BoxTypeDref(), gomp4.BoxTypeUrl(), |
||||
}, |
||||
{ |
||||
gomp4.BoxTypeMoov(), gomp4.BoxTypeTrak(), gomp4.BoxTypeMdia(), gomp4.BoxTypeMinf(), |
||||
gomp4.BoxTypeStbl(), |
||||
}, |
||||
{ |
||||
gomp4.BoxTypeMoov(), gomp4.BoxTypeTrak(), gomp4.BoxTypeMdia(), gomp4.BoxTypeMinf(), |
||||
gomp4.BoxTypeStbl(), gomp4.BoxTypeStsd(), |
||||
}, |
||||
{ |
||||
gomp4.BoxTypeMoov(), gomp4.BoxTypeTrak(), gomp4.BoxTypeMdia(), gomp4.BoxTypeMinf(), |
||||
gomp4.BoxTypeStbl(), gomp4.BoxTypeStsd(), gomp4.BoxTypeAvc1(), |
||||
}, |
||||
{ |
||||
gomp4.BoxTypeMoov(), gomp4.BoxTypeTrak(), gomp4.BoxTypeMdia(), gomp4.BoxTypeMinf(), |
||||
gomp4.BoxTypeStbl(), gomp4.BoxTypeStsd(), gomp4.BoxTypeAvc1(), gomp4.BoxTypeAvcC(), |
||||
}, |
||||
{ |
||||
gomp4.BoxTypeMoov(), gomp4.BoxTypeTrak(), gomp4.BoxTypeMdia(), gomp4.BoxTypeMinf(), |
||||
gomp4.BoxTypeStbl(), gomp4.BoxTypeStsd(), gomp4.BoxTypeAvc1(), gomp4.BoxTypeBtrt(), |
||||
}, |
||||
{ |
||||
gomp4.BoxTypeMoov(), gomp4.BoxTypeTrak(), gomp4.BoxTypeMdia(), gomp4.BoxTypeMinf(), |
||||
gomp4.BoxTypeStbl(), gomp4.BoxTypeStts(), |
||||
}, |
||||
{ |
||||
gomp4.BoxTypeMoov(), gomp4.BoxTypeTrak(), gomp4.BoxTypeMdia(), gomp4.BoxTypeMinf(), |
||||
gomp4.BoxTypeStbl(), gomp4.BoxTypeStsc(), |
||||
}, |
||||
{ |
||||
gomp4.BoxTypeMoov(), gomp4.BoxTypeTrak(), gomp4.BoxTypeMdia(), gomp4.BoxTypeMinf(), |
||||
gomp4.BoxTypeStbl(), gomp4.BoxTypeStsz(), |
||||
}, |
||||
{ |
||||
gomp4.BoxTypeMoov(), gomp4.BoxTypeTrak(), gomp4.BoxTypeMdia(), gomp4.BoxTypeMinf(), |
||||
gomp4.BoxTypeStbl(), gomp4.BoxTypeStco(), |
||||
}, |
||||
{gomp4.BoxTypeMoov(), gomp4.BoxTypeMvex()}, |
||||
{gomp4.BoxTypeMoov(), gomp4.BoxTypeMvex(), gomp4.BoxTypeTrex()}, |
||||
} |
||||
testMP4(t, byts, boxes) |
||||
}) |
||||
|
||||
t.Run("audio only", func(t *testing.T) { |
||||
byts, err := InitWrite(nil, testAudioTrack) |
||||
require.NoError(t, err) |
||||
|
||||
boxes := []gomp4.BoxPath{ |
||||
{gomp4.BoxTypeFtyp()}, |
||||
{gomp4.BoxTypeMoov()}, |
||||
{gomp4.BoxTypeMoov(), gomp4.BoxTypeMvhd()}, |
||||
{gomp4.BoxTypeMoov(), gomp4.BoxTypeTrak()}, |
||||
{gomp4.BoxTypeMoov(), gomp4.BoxTypeTrak(), gomp4.BoxTypeTkhd()}, |
||||
{gomp4.BoxTypeMoov(), gomp4.BoxTypeTrak(), gomp4.BoxTypeMdia()}, |
||||
{gomp4.BoxTypeMoov(), gomp4.BoxTypeTrak(), gomp4.BoxTypeMdia(), gomp4.BoxTypeMdhd()}, |
||||
{gomp4.BoxTypeMoov(), gomp4.BoxTypeTrak(), gomp4.BoxTypeMdia(), gomp4.BoxTypeHdlr()}, |
||||
{gomp4.BoxTypeMoov(), gomp4.BoxTypeTrak(), gomp4.BoxTypeMdia(), gomp4.BoxTypeMinf()}, |
||||
{ |
||||
gomp4.BoxTypeMoov(), gomp4.BoxTypeTrak(), gomp4.BoxTypeMdia(), gomp4.BoxTypeMinf(), |
||||
gomp4.BoxTypeSmhd(), |
||||
}, |
||||
{ |
||||
gomp4.BoxTypeMoov(), gomp4.BoxTypeTrak(), gomp4.BoxTypeMdia(), gomp4.BoxTypeMinf(), |
||||
gomp4.BoxTypeDinf(), |
||||
}, |
||||
{ |
||||
gomp4.BoxTypeMoov(), gomp4.BoxTypeTrak(), gomp4.BoxTypeMdia(), gomp4.BoxTypeMinf(), |
||||
gomp4.BoxTypeDinf(), gomp4.BoxTypeDref(), |
||||
}, |
||||
{ |
||||
gomp4.BoxTypeMoov(), gomp4.BoxTypeTrak(), gomp4.BoxTypeMdia(), gomp4.BoxTypeMinf(), |
||||
gomp4.BoxTypeDinf(), gomp4.BoxTypeDref(), gomp4.BoxTypeUrl(), |
||||
}, |
||||
{ |
||||
gomp4.BoxTypeMoov(), gomp4.BoxTypeTrak(), gomp4.BoxTypeMdia(), gomp4.BoxTypeMinf(), |
||||
gomp4.BoxTypeStbl(), |
||||
}, |
||||
{ |
||||
gomp4.BoxTypeMoov(), gomp4.BoxTypeTrak(), gomp4.BoxTypeMdia(), gomp4.BoxTypeMinf(), |
||||
gomp4.BoxTypeStbl(), gomp4.BoxTypeStsd(), |
||||
}, |
||||
{ |
||||
gomp4.BoxTypeMoov(), gomp4.BoxTypeTrak(), gomp4.BoxTypeMdia(), gomp4.BoxTypeMinf(), |
||||
gomp4.BoxTypeStbl(), gomp4.BoxTypeStsd(), gomp4.BoxTypeMp4a(), |
||||
}, |
||||
{ |
||||
gomp4.BoxTypeMoov(), gomp4.BoxTypeTrak(), gomp4.BoxTypeMdia(), gomp4.BoxTypeMinf(), |
||||
gomp4.BoxTypeStbl(), gomp4.BoxTypeStsd(), gomp4.BoxTypeMp4a(), gomp4.BoxTypeEsds(), |
||||
}, |
||||
{ |
||||
gomp4.BoxTypeMoov(), gomp4.BoxTypeTrak(), gomp4.BoxTypeMdia(), gomp4.BoxTypeMinf(), |
||||
gomp4.BoxTypeStbl(), gomp4.BoxTypeStsd(), gomp4.BoxTypeMp4a(), gomp4.BoxTypeBtrt(), |
||||
}, |
||||
{ |
||||
gomp4.BoxTypeMoov(), gomp4.BoxTypeTrak(), gomp4.BoxTypeMdia(), gomp4.BoxTypeMinf(), |
||||
gomp4.BoxTypeStbl(), gomp4.BoxTypeStts(), |
||||
}, |
||||
{ |
||||
gomp4.BoxTypeMoov(), gomp4.BoxTypeTrak(), gomp4.BoxTypeMdia(), gomp4.BoxTypeMinf(), |
||||
gomp4.BoxTypeStbl(), gomp4.BoxTypeStsc(), |
||||
}, |
||||
{ |
||||
gomp4.BoxTypeMoov(), gomp4.BoxTypeTrak(), gomp4.BoxTypeMdia(), gomp4.BoxTypeMinf(), |
||||
gomp4.BoxTypeStbl(), gomp4.BoxTypeStsz(), |
||||
}, |
||||
{ |
||||
gomp4.BoxTypeMoov(), gomp4.BoxTypeTrak(), gomp4.BoxTypeMdia(), gomp4.BoxTypeMinf(), |
||||
gomp4.BoxTypeStbl(), gomp4.BoxTypeStco(), |
||||
}, |
||||
{gomp4.BoxTypeMoov(), gomp4.BoxTypeMvex()}, |
||||
{gomp4.BoxTypeMoov(), gomp4.BoxTypeMvex(), gomp4.BoxTypeTrex()}, |
||||
} |
||||
testMP4(t, byts, boxes) |
||||
}) |
||||
} |
@ -0,0 +1,259 @@
@@ -0,0 +1,259 @@
|
||||
package fmp4 |
||||
|
||||
import ( |
||||
"bytes" |
||||
"fmt" |
||||
|
||||
gomp4 "github.com/abema/go-mp4" |
||||
) |
||||
|
||||
const ( |
||||
trunFlagDataOffsetPreset = 0x01 |
||||
trunFlagSampleDurationPresent = 0x100 |
||||
trunFlagSampleSizePresent = 0x200 |
||||
trunFlagSampleFlagsPresent = 0x400 |
||||
trunFlagSampleCompositionTimeOffsetPresentOrV1 = 0x800 |
||||
) |
||||
|
||||
// Part is a FMP4 part file.
|
||||
type Part struct { |
||||
Tracks []*PartTrack |
||||
} |
||||
|
||||
// Parts is a sequence of FMP4 parts.
|
||||
type Parts []*Part |
||||
|
||||
// Unmarshal decodes one or more FMP4 parts.
|
||||
func (ps *Parts) Unmarshal(byts []byte) error { |
||||
type readState int |
||||
|
||||
const ( |
||||
waitingMoof readState = iota |
||||
waitingTraf |
||||
waitingTfdtTfhdTrun |
||||
) |
||||
|
||||
state := waitingMoof |
||||
var curPart *Part |
||||
var moofOffset uint64 |
||||
var curTrack *PartTrack |
||||
var tfdt *gomp4.Tfdt |
||||
var tfhd *gomp4.Tfhd |
||||
|
||||
_, err := gomp4.ReadBoxStructure(bytes.NewReader(byts), func(h *gomp4.ReadHandle) (interface{}, error) { |
||||
switch h.BoxInfo.Type.String() { |
||||
case "moof": |
||||
if state != waitingMoof { |
||||
return nil, fmt.Errorf("unexpected moof") |
||||
} |
||||
|
||||
curPart = &Part{} |
||||
*ps = append(*ps, curPart) |
||||
moofOffset = h.BoxInfo.Offset |
||||
state = waitingTraf |
||||
|
||||
case "traf": |
||||
if state != waitingTraf && state != waitingTfdtTfhdTrun { |
||||
return nil, fmt.Errorf("unexpected traf") |
||||
} |
||||
|
||||
if curTrack != nil { |
||||
if tfdt == nil || tfhd == nil || curTrack.Samples == nil { |
||||
return nil, fmt.Errorf("parse error") |
||||
} |
||||
} |
||||
|
||||
curTrack = &PartTrack{} |
||||
curPart.Tracks = append(curPart.Tracks, curTrack) |
||||
tfdt = nil |
||||
tfhd = nil |
||||
state = waitingTfdtTfhdTrun |
||||
|
||||
case "tfhd": |
||||
if state != waitingTfdtTfhdTrun || tfhd != nil { |
||||
return nil, fmt.Errorf("unexpected tfhd") |
||||
} |
||||
|
||||
box, _, err := h.ReadPayload() |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
tfhd = box.(*gomp4.Tfhd) |
||||
curTrack.ID = int(tfhd.TrackID) |
||||
|
||||
case "tfdt": |
||||
if state != waitingTfdtTfhdTrun || tfdt != nil { |
||||
return nil, fmt.Errorf("unexpected tfdt") |
||||
} |
||||
|
||||
box, _, err := h.ReadPayload() |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
tfdt = box.(*gomp4.Tfdt) |
||||
|
||||
if tfdt.FullBox.Version != 1 { |
||||
return nil, fmt.Errorf("unsupported tfdt version") |
||||
} |
||||
|
||||
curTrack.BaseTime = tfdt.BaseMediaDecodeTimeV1 |
||||
|
||||
case "trun": |
||||
if state != waitingTfdtTfhdTrun || tfhd == nil { |
||||
return nil, fmt.Errorf("unexpected trun") |
||||
} |
||||
|
||||
box, _, err := h.ReadPayload() |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
trun := box.(*gomp4.Trun) |
||||
|
||||
flags := uint16(trun.Flags[1])<<8 | uint16(trun.Flags[2]) |
||||
if (flags & trunFlagDataOffsetPreset) == 0 { |
||||
return nil, fmt.Errorf("unsupported flags") |
||||
} |
||||
|
||||
existing := len(curTrack.Samples) |
||||
tmp := make([]*PartSample, existing+len(trun.Entries)) |
||||
copy(tmp, curTrack.Samples) |
||||
curTrack.Samples = tmp |
||||
|
||||
ptr := byts[uint64(trun.DataOffset)+moofOffset:] |
||||
|
||||
for i, e := range trun.Entries { |
||||
s := &PartSample{} |
||||
|
||||
if (flags & trunFlagSampleDurationPresent) != 0 { |
||||
s.Duration = e.SampleDuration |
||||
} else { |
||||
s.Duration = tfhd.DefaultSampleDuration |
||||
} |
||||
|
||||
s.PTSOffset = e.SampleCompositionTimeOffsetV1 |
||||
|
||||
if (flags & trunFlagSampleFlagsPresent) != 0 { |
||||
s.Flags = e.SampleFlags |
||||
} else { |
||||
s.Flags = tfhd.DefaultSampleFlags |
||||
} |
||||
|
||||
var size uint32 |
||||
if (flags & trunFlagSampleSizePresent) != 0 { |
||||
size = e.SampleSize |
||||
} else { |
||||
size = tfhd.DefaultSampleSize |
||||
} |
||||
|
||||
s.Payload = ptr[:size] |
||||
ptr = ptr[size:] |
||||
|
||||
curTrack.Samples[existing+i] = s |
||||
} |
||||
|
||||
case "mdat": |
||||
if state != waitingTraf && state != waitingTfdtTfhdTrun { |
||||
return nil, fmt.Errorf("unexpected mdat") |
||||
} |
||||
|
||||
if curTrack != nil { |
||||
if tfdt == nil || tfhd == nil || curTrack.Samples == nil { |
||||
return nil, fmt.Errorf("parse error") |
||||
} |
||||
} |
||||
|
||||
state = waitingMoof |
||||
return nil, nil |
||||
} |
||||
|
||||
return h.Expand() |
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
if state != waitingMoof { |
||||
return fmt.Errorf("decode error") |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
// Marshal encodes a FMP4 part file.
|
||||
func (p *Part) Marshal() ([]byte, error) { |
||||
/* |
||||
moof |
||||
- mfhd |
||||
- traf (video) |
||||
- traf (audio) |
||||
mdat |
||||
*/ |
||||
|
||||
w := newMP4Writer() |
||||
|
||||
moofOffset, err := w.writeBoxStart(&gomp4.Moof{}) // <moof>
|
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
_, err = w.WriteBox(&gomp4.Mfhd{ // <mfhd/>
|
||||
SequenceNumber: 0, |
||||
}) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
trackLen := len(p.Tracks) |
||||
truns := make([]*gomp4.Trun, trackLen) |
||||
trunOffsets := make([]int, trackLen) |
||||
dataOffsets := make([]int, trackLen) |
||||
dataSize := 0 |
||||
|
||||
for i, track := range p.Tracks { |
||||
trun, trunOffset, err := track.marshal(w) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
dataOffsets[i] = dataSize |
||||
|
||||
for _, sample := range track.Samples { |
||||
dataSize += len(sample.Payload) |
||||
} |
||||
|
||||
truns[i] = trun |
||||
trunOffsets[i] = trunOffset |
||||
} |
||||
|
||||
err = w.writeBoxEnd() // </moof>
|
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
mdat := &gomp4.Mdat{} // <mdat/>
|
||||
mdat.Data = make([]byte, dataSize) |
||||
pos := 0 |
||||
|
||||
for _, track := range p.Tracks { |
||||
for _, sample := range track.Samples { |
||||
pos += copy(mdat.Data[pos:], sample.Payload) |
||||
} |
||||
} |
||||
|
||||
mdatOffset, err := w.WriteBox(mdat) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
for i := range p.Tracks { |
||||
truns[i].DataOffset = int32(dataOffsets[i] + mdatOffset - moofOffset + 8) |
||||
err = w.rewriteBox(trunOffsets[i], truns[i]) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
} |
||||
|
||||
return w.bytes(), nil |
||||
} |
@ -1,96 +0,0 @@
@@ -1,96 +0,0 @@
|
||||
package fmp4 |
||||
|
||||
import ( |
||||
"bytes" |
||||
"fmt" |
||||
|
||||
gomp4 "github.com/abema/go-mp4" |
||||
) |
||||
|
||||
type partReadState int |
||||
|
||||
const ( |
||||
waitingTraf partReadState = iota |
||||
waitingTfhd |
||||
waitingTfdt |
||||
waitingTrun |
||||
) |
||||
|
||||
// PartRead reads a FMP4 part file.
|
||||
func PartRead( |
||||
byts []byte, |
||||
cb func(), |
||||
) error { |
||||
state := waitingTraf |
||||
var trackID uint32 |
||||
var baseTime uint64 |
||||
var entries []gomp4.TrunEntry |
||||
|
||||
_, err := gomp4.ReadBoxStructure(bytes.NewReader(byts), func(h *gomp4.ReadHandle) (interface{}, error) { |
||||
switch h.BoxInfo.Type.String() { |
||||
case "traf": |
||||
if state != waitingTraf { |
||||
return nil, fmt.Errorf("decode error") |
||||
} |
||||
state = waitingTfhd |
||||
|
||||
case "tfhd": |
||||
if state != waitingTfhd { |
||||
return nil, fmt.Errorf("decode error") |
||||
} |
||||
|
||||
box, _, err := h.ReadPayload() |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
trackID = box.(*gomp4.Tfhd).TrackID |
||||
|
||||
state = waitingTfdt |
||||
|
||||
case "tfdt": |
||||
if state != waitingTfdt { |
||||
return nil, fmt.Errorf("decode error") |
||||
} |
||||
|
||||
box, _, err := h.ReadPayload() |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
t := box.(*gomp4.Tfdt) |
||||
|
||||
if t.FullBox.Version != 1 { |
||||
return nil, fmt.Errorf("unsupported tfdt version") |
||||
} |
||||
|
||||
baseTime = t.BaseMediaDecodeTimeV1 |
||||
state = waitingTrun |
||||
|
||||
case "trun": |
||||
if state != waitingTrun { |
||||
return nil, fmt.Errorf("decode error") |
||||
} |
||||
|
||||
box, _, err := h.ReadPayload() |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
t := box.(*gomp4.Trun) |
||||
|
||||
entries = t.Entries |
||||
state = waitingTraf |
||||
} |
||||
|
||||
return h.Expand() |
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
if state != waitingTraf { |
||||
return fmt.Errorf("parse error") |
||||
} |
||||
|
||||
fmt.Println("TODO", trackID, baseTime, entries) |
||||
|
||||
return nil |
||||
} |
@ -0,0 +1,249 @@
@@ -0,0 +1,249 @@
|
||||
package fmp4 |
||||
|
||||
import ( |
||||
"bytes" |
||||
"testing" |
||||
|
||||
gomp4 "github.com/abema/go-mp4" |
||||
"github.com/stretchr/testify/require" |
||||
) |
||||
|
||||
func testMP4(t *testing.T, byts []byte, boxes []gomp4.BoxPath) { |
||||
i := 0 |
||||
_, err := gomp4.ReadBoxStructure(bytes.NewReader(byts), func(h *gomp4.ReadHandle) (interface{}, error) { |
||||
require.Equal(t, boxes[i], h.Path) |
||||
i++ |
||||
return h.Expand() |
||||
}) |
||||
require.NoError(t, err) |
||||
} |
||||
|
||||
func TestPartMarshal(t *testing.T) { |
||||
testVideoSamples := []*PartSample{ |
||||
{ |
||||
Duration: 2 * 90000, |
||||
Payload: []byte{ |
||||
0x00, 0x00, 0x00, 0x04, |
||||
0x01, 0x02, 0x03, 0x04, // SPS
|
||||
0x00, 0x00, 0x00, 0x01, |
||||
0x08, // PPS
|
||||
0x00, 0x00, 0x00, 0x01, |
||||
0x05, // IDR
|
||||
}, |
||||
}, |
||||
{ |
||||
Duration: 2 * 90000, |
||||
Payload: []byte{ |
||||
0x00, 0x00, 0x00, 0x01, |
||||
0x01, // non-IDR
|
||||
}, |
||||
Flags: 1 << 16, |
||||
}, |
||||
{ |
||||
Duration: 1 * 90000, |
||||
Payload: []byte{ |
||||
0x00, 0x00, 0x00, 0x01, |
||||
0x01, // non-IDR
|
||||
}, |
||||
Flags: 1 << 16, |
||||
}, |
||||
} |
||||
|
||||
testAudioSamples := []*PartSample{ |
||||
{ |
||||
Duration: 500 * 48000 / 1000, |
||||
Payload: []byte{ |
||||
0x01, 0x02, 0x03, 0x04, |
||||
}, |
||||
}, |
||||
{ |
||||
Duration: 1 * 48000, |
||||
Payload: []byte{ |
||||
0x01, 0x02, 0x03, 0x04, |
||||
}, |
||||
}, |
||||
} |
||||
|
||||
t.Run("video + audio", func(t *testing.T) { |
||||
part := Part{ |
||||
Tracks: []*PartTrack{ |
||||
{ |
||||
ID: 1, |
||||
Samples: testVideoSamples, |
||||
IsVideo: true, |
||||
}, |
||||
{ |
||||
ID: 2, |
||||
BaseTime: 3 * 48000, |
||||
Samples: testAudioSamples, |
||||
}, |
||||
}, |
||||
} |
||||
|
||||
byts, err := part.Marshal() |
||||
require.NoError(t, err) |
||||
|
||||
boxes := []gomp4.BoxPath{ |
||||
{gomp4.BoxTypeMoof()}, |
||||
{gomp4.BoxTypeMoof(), gomp4.BoxTypeMfhd()}, |
||||
{gomp4.BoxTypeMoof(), gomp4.BoxTypeTraf()}, |
||||
{gomp4.BoxTypeMoof(), gomp4.BoxTypeTraf(), gomp4.BoxTypeTfhd()}, |
||||
{gomp4.BoxTypeMoof(), gomp4.BoxTypeTraf(), gomp4.BoxTypeTfdt()}, |
||||
{gomp4.BoxTypeMoof(), gomp4.BoxTypeTraf(), gomp4.BoxTypeTrun()}, |
||||
{gomp4.BoxTypeMoof(), gomp4.BoxTypeTraf()}, |
||||
{gomp4.BoxTypeMoof(), gomp4.BoxTypeTraf(), gomp4.BoxTypeTfhd()}, |
||||
{gomp4.BoxTypeMoof(), gomp4.BoxTypeTraf(), gomp4.BoxTypeTfdt()}, |
||||
{gomp4.BoxTypeMoof(), gomp4.BoxTypeTraf(), gomp4.BoxTypeTrun()}, |
||||
{gomp4.BoxTypeMdat()}, |
||||
} |
||||
testMP4(t, byts, boxes) |
||||
}) |
||||
|
||||
t.Run("video only", func(t *testing.T) { |
||||
part := Part{ |
||||
Tracks: []*PartTrack{ |
||||
{ |
||||
ID: 1, |
||||
Samples: testVideoSamples, |
||||
IsVideo: true, |
||||
}, |
||||
}, |
||||
} |
||||
|
||||
byts, err := part.Marshal() |
||||
require.NoError(t, err) |
||||
|
||||
boxes := []gomp4.BoxPath{ |
||||
{gomp4.BoxTypeMoof()}, |
||||
{gomp4.BoxTypeMoof(), gomp4.BoxTypeMfhd()}, |
||||
{gomp4.BoxTypeMoof(), gomp4.BoxTypeTraf()}, |
||||
{gomp4.BoxTypeMoof(), gomp4.BoxTypeTraf(), gomp4.BoxTypeTfhd()}, |
||||
{gomp4.BoxTypeMoof(), gomp4.BoxTypeTraf(), gomp4.BoxTypeTfdt()}, |
||||
{gomp4.BoxTypeMoof(), gomp4.BoxTypeTraf(), gomp4.BoxTypeTrun()}, |
||||
{gomp4.BoxTypeMdat()}, |
||||
} |
||||
testMP4(t, byts, boxes) |
||||
}) |
||||
|
||||
t.Run("audio only", func(t *testing.T) { |
||||
part := Part{ |
||||
Tracks: []*PartTrack{ |
||||
{ |
||||
ID: 1, |
||||
Samples: testAudioSamples, |
||||
}, |
||||
}, |
||||
} |
||||
|
||||
byts, err := part.Marshal() |
||||
require.NoError(t, err) |
||||
|
||||
boxes := []gomp4.BoxPath{ |
||||
{gomp4.BoxTypeMoof()}, |
||||
{gomp4.BoxTypeMoof(), gomp4.BoxTypeMfhd()}, |
||||
{gomp4.BoxTypeMoof(), gomp4.BoxTypeTraf()}, |
||||
{gomp4.BoxTypeMoof(), gomp4.BoxTypeTraf(), gomp4.BoxTypeTfhd()}, |
||||
{gomp4.BoxTypeMoof(), gomp4.BoxTypeTraf(), gomp4.BoxTypeTfdt()}, |
||||
{gomp4.BoxTypeMoof(), gomp4.BoxTypeTraf(), gomp4.BoxTypeTrun()}, |
||||
{gomp4.BoxTypeMdat()}, |
||||
} |
||||
testMP4(t, byts, boxes) |
||||
}) |
||||
} |
||||
|
||||
func TestPartUnmarshal(t *testing.T) { |
||||
byts := []byte{ |
||||
0x00, 0x00, 0x00, 0xd8, 0x6d, 0x6f, 0x6f, 0x66, |
||||
0x00, 0x00, 0x00, 0x10, 0x6d, 0x66, 0x68, 0x64, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x70, 0x74, 0x72, 0x61, 0x66, |
||||
0x00, 0x00, 0x00, 0x10, 0x74, 0x66, 0x68, 0x64, |
||||
0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, |
||||
0x00, 0x00, 0x00, 0x14, 0x74, 0x66, 0x64, 0x74, |
||||
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, |
||||
0x74, 0x72, 0x75, 0x6e, 0x01, 0x00, 0x0f, 0x01, |
||||
0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0xe0, |
||||
0x00, 0x02, 0xbf, 0x20, 0x00, 0x00, 0x00, 0x12, |
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x02, 0xbf, 0x20, 0x00, 0x00, 0x00, 0x05, |
||||
0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x01, 0x5f, 0x90, 0x00, 0x00, 0x00, 0x05, |
||||
0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x00, 0x00, 0x50, 0x74, 0x72, 0x61, 0x66, |
||||
0x00, 0x00, 0x00, 0x10, 0x74, 0x66, 0x68, 0x64, |
||||
0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, |
||||
0x00, 0x00, 0x00, 0x14, 0x74, 0x66, 0x64, 0x74, |
||||
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
0x00, 0x02, 0x32, 0x80, 0x00, 0x00, 0x00, 0x24, |
||||
0x74, 0x72, 0x75, 0x6e, 0x01, 0x00, 0x03, 0x01, |
||||
0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0xfc, |
||||
0x00, 0x00, 0x5d, 0xc0, 0x00, 0x00, 0x00, 0x04, |
||||
0x00, 0x00, 0xbb, 0x80, 0x00, 0x00, 0x00, 0x04, |
||||
0x00, 0x00, 0x00, 0x2c, 0x6d, 0x64, 0x61, 0x74, |
||||
0x00, 0x00, 0x00, 0x04, 0x01, 0x02, 0x03, 0x04, |
||||
0x00, 0x00, 0x00, 0x01, 0x08, 0x00, 0x00, 0x00, |
||||
0x01, 0x05, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, |
||||
0x00, 0x00, 0x01, 0x01, 0x01, 0x02, 0x03, 0x04, |
||||
0x01, 0x02, 0x03, 0x04, |
||||
} |
||||
|
||||
var parts Parts |
||||
err := parts.Unmarshal(byts) |
||||
require.NoError(t, err) |
||||
|
||||
require.Equal(t, Parts{{ |
||||
Tracks: []*PartTrack{ |
||||
{ |
||||
ID: 1, |
||||
Samples: []*PartSample{ |
||||
{ |
||||
Duration: 2 * 90000, |
||||
Payload: []byte{ |
||||
0x00, 0x00, 0x00, 0x04, |
||||
0x01, 0x02, 0x03, 0x04, // SPS
|
||||
0x00, 0x00, 0x00, 0x01, |
||||
0x08, // PPS
|
||||
0x00, 0x00, 0x00, 0x01, |
||||
0x05, // IDR
|
||||
}, |
||||
}, |
||||
{ |
||||
Duration: 2 * 90000, |
||||
Payload: []byte{ |
||||
0x00, 0x00, 0x00, 0x01, |
||||
0x01, // non-IDR
|
||||
}, |
||||
Flags: 1 << 16, |
||||
}, |
||||
{ |
||||
Duration: 1 * 90000, |
||||
Payload: []byte{ |
||||
0x00, 0x00, 0x00, 0x01, |
||||
0x01, // non-IDR
|
||||
}, |
||||
Flags: 1 << 16, |
||||
}, |
||||
}, |
||||
}, |
||||
{ |
||||
ID: 2, |
||||
BaseTime: 3 * 48000, |
||||
Samples: []*PartSample{ |
||||
{ |
||||
Duration: 500 * 48000 / 1000, |
||||
Payload: []byte{ |
||||
0x01, 0x02, 0x03, 0x04, |
||||
}, |
||||
}, |
||||
{ |
||||
Duration: 1 * 48000, |
||||
Payload: []byte{ |
||||
0x01, 0x02, 0x03, 0x04, |
||||
}, |
||||
}, |
||||
}, |
||||
}, |
||||
}, |
||||
}}, parts) |
||||
} |
@ -0,0 +1,106 @@
@@ -0,0 +1,106 @@
|
||||
package fmp4 |
||||
|
||||
import ( |
||||
gomp4 "github.com/abema/go-mp4" |
||||
) |
||||
|
||||
// PartSample is a sample of a PartTrack.
|
||||
type PartSample struct { |
||||
Duration uint32 |
||||
PTSOffset int32 |
||||
Flags uint32 |
||||
Payload []byte |
||||
} |
||||
|
||||
// PartTrack is a track of Part.
|
||||
type PartTrack struct { |
||||
ID int |
||||
BaseTime uint64 |
||||
Samples []*PartSample |
||||
IsVideo bool // marshal only
|
||||
} |
||||
|
||||
func (pt *PartTrack) marshal(w *mp4Writer) (*gomp4.Trun, int, error) { |
||||
/* |
||||
traf |
||||
- tfhd |
||||
- tfdt |
||||
- trun |
||||
*/ |
||||
|
||||
_, err := w.writeBoxStart(&gomp4.Traf{}) // <traf>
|
||||
if err != nil { |
||||
return nil, 0, err |
||||
} |
||||
|
||||
flags := 0 |
||||
|
||||
_, err = w.WriteBox(&gomp4.Tfhd{ // <tfhd/>
|
||||
FullBox: gomp4.FullBox{ |
||||
Flags: [3]byte{2, byte(flags >> 8), byte(flags)}, |
||||
}, |
||||
TrackID: uint32(pt.ID), |
||||
}) |
||||
if err != nil { |
||||
return nil, 0, err |
||||
} |
||||
|
||||
_, err = w.WriteBox(&gomp4.Tfdt{ // <tfdt/>
|
||||
FullBox: gomp4.FullBox{ |
||||
Version: 1, |
||||
}, |
||||
// sum of decode durations of all earlier samples
|
||||
BaseMediaDecodeTimeV1: pt.BaseTime, |
||||
}) |
||||
if err != nil { |
||||
return nil, 0, err |
||||
} |
||||
|
||||
if pt.IsVideo { |
||||
flags = trunFlagDataOffsetPreset | |
||||
trunFlagSampleDurationPresent | |
||||
trunFlagSampleSizePresent | |
||||
trunFlagSampleFlagsPresent | |
||||
trunFlagSampleCompositionTimeOffsetPresentOrV1 |
||||
} else { |
||||
flags = trunFlagDataOffsetPreset | |
||||
trunFlagSampleDurationPresent | |
||||
trunFlagSampleSizePresent |
||||
} |
||||
|
||||
trun := &gomp4.Trun{ // <trun/>
|
||||
FullBox: gomp4.FullBox{ |
||||
Version: 1, |
||||
Flags: [3]byte{0, byte(flags >> 8), byte(flags)}, |
||||
}, |
||||
SampleCount: uint32(len(pt.Samples)), |
||||
} |
||||
|
||||
for _, sample := range pt.Samples { |
||||
if pt.IsVideo { |
||||
trun.Entries = append(trun.Entries, gomp4.TrunEntry{ |
||||
SampleDuration: sample.Duration, |
||||
SampleSize: uint32(len(sample.Payload)), |
||||
SampleFlags: sample.Flags, |
||||
SampleCompositionTimeOffsetV1: sample.PTSOffset, |
||||
}) |
||||
} else { |
||||
trun.Entries = append(trun.Entries, gomp4.TrunEntry{ |
||||
SampleDuration: sample.Duration, |
||||
SampleSize: uint32(len(sample.Payload)), |
||||
}) |
||||
} |
||||
} |
||||
|
||||
trunOffset, err := w.WriteBox(trun) |
||||
if err != nil { |
||||
return nil, 0, err |
||||
} |
||||
|
||||
err = w.writeBoxEnd() // </traf>
|
||||
if err != nil { |
||||
return nil, 0, err |
||||
} |
||||
|
||||
return trun, trunOffset, nil |
||||
} |
@ -1,300 +0,0 @@
@@ -1,300 +0,0 @@
|
||||
package fmp4 |
||||
|
||||
import ( |
||||
"math" |
||||
"time" |
||||
|
||||
gomp4 "github.com/abema/go-mp4" |
||||
"github.com/aler9/gortsplib" |
||||
"github.com/aler9/gortsplib/pkg/h264" |
||||
) |
||||
|
||||
func durationGoToMp4(v time.Duration, timescale time.Duration) int64 { |
||||
return int64(math.Round(float64(v*timescale) / float64(time.Second))) |
||||
} |
||||
|
||||
func partWriteVideoInfo( |
||||
w *mp4Writer, |
||||
trackID int, |
||||
videoSamples []*VideoSample, |
||||
) (*gomp4.Trun, int, error) { |
||||
/* |
||||
traf |
||||
- tfhd |
||||
- tfdt |
||||
- trun |
||||
*/ |
||||
|
||||
_, err := w.writeBoxStart(&gomp4.Traf{}) // <traf>
|
||||
if err != nil { |
||||
return nil, 0, err |
||||
} |
||||
|
||||
flags := 0 |
||||
|
||||
_, err = w.WriteBox(&gomp4.Tfhd{ // <tfhd/>
|
||||
FullBox: gomp4.FullBox{ |
||||
Flags: [3]byte{2, byte(flags >> 8), byte(flags)}, |
||||
}, |
||||
TrackID: uint32(trackID), |
||||
}) |
||||
if err != nil { |
||||
return nil, 0, err |
||||
} |
||||
|
||||
_, err = w.WriteBox(&gomp4.Tfdt{ // <tfdt/>
|
||||
FullBox: gomp4.FullBox{ |
||||
Version: 1, |
||||
}, |
||||
// sum of decode durations of all earlier samples
|
||||
BaseMediaDecodeTimeV1: uint64(durationGoToMp4(videoSamples[0].DTS, videoTimescale)), |
||||
}) |
||||
if err != nil { |
||||
return nil, 0, err |
||||
} |
||||
|
||||
flags = 0 |
||||
flags |= 0x01 // data offset present
|
||||
flags |= 0x100 // sample duration present
|
||||
flags |= 0x200 // sample size present
|
||||
flags |= 0x400 // sample flags present
|
||||
flags |= 0x800 // sample composition time offset present or v1
|
||||
|
||||
trun := &gomp4.Trun{ // <trun/>
|
||||
FullBox: gomp4.FullBox{ |
||||
Version: 1, |
||||
Flags: [3]byte{0, byte(flags >> 8), byte(flags)}, |
||||
}, |
||||
SampleCount: uint32(len(videoSamples)), |
||||
} |
||||
|
||||
for _, e := range videoSamples { |
||||
off := e.PTS - e.DTS |
||||
|
||||
flags := uint32(0) |
||||
if !e.IDRPresent { |
||||
flags |= 1 << 16 // sample_is_non_sync_sample
|
||||
} |
||||
|
||||
trun.Entries = append(trun.Entries, gomp4.TrunEntry{ |
||||
SampleDuration: uint32(durationGoToMp4(e.Duration(), videoTimescale)), |
||||
SampleSize: uint32(len(e.avcc)), |
||||
SampleFlags: flags, |
||||
SampleCompositionTimeOffsetV1: int32(durationGoToMp4(off, videoTimescale)), |
||||
}) |
||||
} |
||||
|
||||
trunOffset, err := w.WriteBox(trun) |
||||
if err != nil { |
||||
return nil, 0, err |
||||
} |
||||
|
||||
err = w.writeBoxEnd() // </traf>
|
||||
if err != nil { |
||||
return nil, 0, err |
||||
} |
||||
|
||||
return trun, trunOffset, nil |
||||
} |
||||
|
||||
func partWriteAudioInfo( |
||||
w *mp4Writer, |
||||
trackID int, |
||||
audioTrack *gortsplib.TrackMPEG4Audio, |
||||
audioSamples []*AudioSample, |
||||
) (*gomp4.Trun, int, error) { |
||||
/* |
||||
traf |
||||
- tfhd |
||||
- tfdt |
||||
- trun |
||||
*/ |
||||
|
||||
if len(audioSamples) == 0 { |
||||
return nil, 0, nil |
||||
} |
||||
|
||||
_, err := w.writeBoxStart(&gomp4.Traf{}) // <traf>
|
||||
if err != nil { |
||||
return nil, 0, err |
||||
} |
||||
|
||||
flags := 0 |
||||
|
||||
_, err = w.WriteBox(&gomp4.Tfhd{ // <tfhd/>
|
||||
FullBox: gomp4.FullBox{ |
||||
Flags: [3]byte{2, byte(flags >> 8), byte(flags)}, |
||||
}, |
||||
TrackID: uint32(trackID), |
||||
}) |
||||
if err != nil { |
||||
return nil, 0, err |
||||
} |
||||
|
||||
_, err = w.WriteBox(&gomp4.Tfdt{ // <tfdt/>
|
||||
FullBox: gomp4.FullBox{ |
||||
Version: 1, |
||||
}, |
||||
// sum of decode durations of all earlier samples
|
||||
BaseMediaDecodeTimeV1: uint64(durationGoToMp4(audioSamples[0].PTS, time.Duration(audioTrack.ClockRate()))), |
||||
}) |
||||
if err != nil { |
||||
return nil, 0, err |
||||
} |
||||
|
||||
flags = 0 |
||||
flags |= 0x01 // data offset present
|
||||
flags |= 0x100 // sample duration present
|
||||
flags |= 0x200 // sample size present
|
||||
|
||||
trun := &gomp4.Trun{ // <trun/>
|
||||
FullBox: gomp4.FullBox{ |
||||
Version: 0, |
||||
Flags: [3]byte{0, byte(flags >> 8), byte(flags)}, |
||||
}, |
||||
SampleCount: uint32(len(audioSamples)), |
||||
} |
||||
|
||||
for _, e := range audioSamples { |
||||
trun.Entries = append(trun.Entries, gomp4.TrunEntry{ |
||||
SampleDuration: uint32(durationGoToMp4(e.Duration(), time.Duration(audioTrack.ClockRate()))), |
||||
SampleSize: uint32(len(e.AU)), |
||||
}) |
||||
} |
||||
|
||||
trunOffset, err := w.WriteBox(trun) |
||||
if err != nil { |
||||
return nil, 0, err |
||||
} |
||||
|
||||
err = w.writeBoxEnd() // </traf>
|
||||
if err != nil { |
||||
return nil, 0, err |
||||
} |
||||
|
||||
return trun, trunOffset, nil |
||||
} |
||||
|
||||
// PartWrite generates a FMP4 part file.
|
||||
func PartWrite( |
||||
videoTrack *gortsplib.TrackH264, |
||||
audioTrack *gortsplib.TrackMPEG4Audio, |
||||
videoSamples []*VideoSample, |
||||
audioSamples []*AudioSample, |
||||
) ([]byte, error) { |
||||
/* |
||||
moof |
||||
- mfhd |
||||
- traf (video) |
||||
- traf (audio) |
||||
mdat |
||||
*/ |
||||
|
||||
w := newMP4Writer() |
||||
|
||||
moofOffset, err := w.writeBoxStart(&gomp4.Moof{}) // <moof>
|
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
_, err = w.WriteBox(&gomp4.Mfhd{ // <mfhd/>
|
||||
SequenceNumber: 0, |
||||
}) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
trackID := 1 |
||||
|
||||
var videoTrun *gomp4.Trun |
||||
var videoTrunOffset int |
||||
if videoTrack != nil { |
||||
for _, e := range videoSamples { |
||||
var err error |
||||
e.avcc, err = h264.AVCCMarshal(e.NALUs) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
} |
||||
|
||||
var err error |
||||
videoTrun, videoTrunOffset, err = partWriteVideoInfo( |
||||
w, trackID, videoSamples) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
trackID++ |
||||
} |
||||
|
||||
var audioTrun *gomp4.Trun |
||||
var audioTrunOffset int |
||||
if audioTrack != nil { |
||||
var err error |
||||
audioTrun, audioTrunOffset, err = partWriteAudioInfo(w, trackID, audioTrack, audioSamples) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
} |
||||
|
||||
err = w.writeBoxEnd() // </moof>
|
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
mdat := &gomp4.Mdat{} // <mdat/>
|
||||
|
||||
dataSize := 0 |
||||
videoDataSize := 0 |
||||
|
||||
if videoTrack != nil { |
||||
for _, e := range videoSamples { |
||||
dataSize += len(e.avcc) |
||||
} |
||||
videoDataSize = dataSize |
||||
} |
||||
|
||||
if audioTrack != nil { |
||||
for _, e := range audioSamples { |
||||
dataSize += len(e.AU) |
||||
} |
||||
} |
||||
|
||||
mdat.Data = make([]byte, dataSize) |
||||
pos := 0 |
||||
|
||||
if videoTrack != nil { |
||||
for _, e := range videoSamples { |
||||
pos += copy(mdat.Data[pos:], e.avcc) |
||||
} |
||||
} |
||||
|
||||
if audioTrack != nil { |
||||
for _, e := range audioSamples { |
||||
pos += copy(mdat.Data[pos:], e.AU) |
||||
} |
||||
} |
||||
|
||||
mdatOffset, err := w.WriteBox(mdat) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
if videoTrack != nil { |
||||
videoTrun.DataOffset = int32(mdatOffset - moofOffset + 8) |
||||
err = w.rewriteBox(videoTrunOffset, videoTrun) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
} |
||||
|
||||
if audioTrack != nil && audioTrun != nil { |
||||
audioTrun.DataOffset = int32(videoDataSize + mdatOffset - moofOffset + 8) |
||||
err = w.rewriteBox(audioTrunOffset, audioTrun) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
} |
||||
|
||||
return w.bytes(), nil |
||||
} |
@ -1,143 +0,0 @@
@@ -1,143 +0,0 @@
|
||||
package fmp4 |
||||
|
||||
import ( |
||||
"testing" |
||||
"time" |
||||
|
||||
gomp4 "github.com/abema/go-mp4" |
||||
"github.com/aler9/gortsplib/pkg/h264" |
||||
"github.com/stretchr/testify/require" |
||||
) |
||||
|
||||
func TestPartWrite(t *testing.T) { |
||||
testVideoSamples := []*VideoSample{ |
||||
{ |
||||
NALUs: [][]byte{ |
||||
{0x06}, |
||||
{0x07}, |
||||
}, |
||||
PTS: 0, |
||||
DTS: 0, |
||||
}, |
||||
{ |
||||
NALUs: [][]byte{ |
||||
testSPS, // SPS
|
||||
{8}, // PPS
|
||||
{5}, // IDR
|
||||
}, |
||||
PTS: 2 * time.Second, |
||||
DTS: 2 * time.Second, |
||||
}, |
||||
|
||||
{ |
||||
NALUs: [][]byte{ |
||||
{1}, // non-IDR
|
||||
}, |
||||
PTS: 4 * time.Second, |
||||
DTS: 4 * time.Second, |
||||
}, |
||||
|
||||
{ |
||||
NALUs: [][]byte{ |
||||
{1}, // non-IDR
|
||||
}, |
||||
PTS: 6 * time.Second, |
||||
DTS: 6 * time.Second, |
||||
}, |
||||
{ |
||||
NALUs: [][]byte{ |
||||
{5}, // IDR
|
||||
}, |
||||
PTS: 7 * time.Second, |
||||
DTS: 7 * time.Second, |
||||
}, |
||||
} |
||||
|
||||
testAudioSamples := []*AudioSample{ |
||||
{ |
||||
AU: []byte{ |
||||
0x01, 0x02, 0x03, 0x04, |
||||
}, |
||||
PTS: 3 * time.Second, |
||||
}, |
||||
{ |
||||
AU: []byte{ |
||||
0x01, 0x02, 0x03, 0x04, |
||||
}, |
||||
PTS: 3500 * time.Millisecond, |
||||
}, |
||||
{ |
||||
AU: []byte{ |
||||
0x01, 0x02, 0x03, 0x04, |
||||
}, |
||||
PTS: 4500 * time.Millisecond, |
||||
}, |
||||
} |
||||
|
||||
for i, sample := range testVideoSamples { |
||||
sample.IDRPresent = h264.IDRPresent(sample.NALUs) |
||||
if i != len(testVideoSamples)-1 { |
||||
sample.Next = testVideoSamples[i+1] |
||||
} |
||||
} |
||||
testVideoSamples = testVideoSamples[:len(testVideoSamples)-1] |
||||
|
||||
for i, sample := range testAudioSamples { |
||||
if i != len(testAudioSamples)-1 { |
||||
sample.Next = testAudioSamples[i+1] |
||||
} |
||||
} |
||||
testAudioSamples = testAudioSamples[:len(testAudioSamples)-1] |
||||
|
||||
t.Run("video + audio", func(t *testing.T) { |
||||
byts, err := PartWrite(testVideoTrack, testAudioTrack, testVideoSamples, testAudioSamples) |
||||
require.NoError(t, err) |
||||
|
||||
boxes := []gomp4.BoxPath{ |
||||
{gomp4.BoxTypeMoof()}, |
||||
{gomp4.BoxTypeMoof(), gomp4.BoxTypeMfhd()}, |
||||
{gomp4.BoxTypeMoof(), gomp4.BoxTypeTraf()}, |
||||
{gomp4.BoxTypeMoof(), gomp4.BoxTypeTraf(), gomp4.BoxTypeTfhd()}, |
||||
{gomp4.BoxTypeMoof(), gomp4.BoxTypeTraf(), gomp4.BoxTypeTfdt()}, |
||||
{gomp4.BoxTypeMoof(), gomp4.BoxTypeTraf(), gomp4.BoxTypeTrun()}, |
||||
{gomp4.BoxTypeMoof(), gomp4.BoxTypeTraf()}, |
||||
{gomp4.BoxTypeMoof(), gomp4.BoxTypeTraf(), gomp4.BoxTypeTfhd()}, |
||||
{gomp4.BoxTypeMoof(), gomp4.BoxTypeTraf(), gomp4.BoxTypeTfdt()}, |
||||
{gomp4.BoxTypeMoof(), gomp4.BoxTypeTraf(), gomp4.BoxTypeTrun()}, |
||||
{gomp4.BoxTypeMdat()}, |
||||
} |
||||
testMP4(t, byts, boxes) |
||||
}) |
||||
|
||||
t.Run("video only", func(t *testing.T) { |
||||
byts, err := PartWrite(testVideoTrack, nil, testVideoSamples, nil) |
||||
require.NoError(t, err) |
||||
|
||||
boxes := []gomp4.BoxPath{ |
||||
{gomp4.BoxTypeMoof()}, |
||||
{gomp4.BoxTypeMoof(), gomp4.BoxTypeMfhd()}, |
||||
{gomp4.BoxTypeMoof(), gomp4.BoxTypeTraf()}, |
||||
{gomp4.BoxTypeMoof(), gomp4.BoxTypeTraf(), gomp4.BoxTypeTfhd()}, |
||||
{gomp4.BoxTypeMoof(), gomp4.BoxTypeTraf(), gomp4.BoxTypeTfdt()}, |
||||
{gomp4.BoxTypeMoof(), gomp4.BoxTypeTraf(), gomp4.BoxTypeTrun()}, |
||||
{gomp4.BoxTypeMdat()}, |
||||
} |
||||
testMP4(t, byts, boxes) |
||||
}) |
||||
|
||||
t.Run("audio only", func(t *testing.T) { |
||||
byts, err := PartWrite(nil, testAudioTrack, nil, testAudioSamples) |
||||
require.NoError(t, err) |
||||
|
||||
boxes := []gomp4.BoxPath{ |
||||
{gomp4.BoxTypeMoof()}, |
||||
{gomp4.BoxTypeMoof(), gomp4.BoxTypeMfhd()}, |
||||
{gomp4.BoxTypeMoof(), gomp4.BoxTypeTraf()}, |
||||
{gomp4.BoxTypeMoof(), gomp4.BoxTypeTraf(), gomp4.BoxTypeTfhd()}, |
||||
{gomp4.BoxTypeMoof(), gomp4.BoxTypeTraf(), gomp4.BoxTypeTfdt()}, |
||||
{gomp4.BoxTypeMoof(), gomp4.BoxTypeTraf(), gomp4.BoxTypeTrun()}, |
||||
{gomp4.BoxTypeMdat()}, |
||||
} |
||||
testMP4(t, byts, boxes) |
||||
}) |
||||
} |
@ -1,25 +0,0 @@
@@ -1,25 +0,0 @@
|
||||
package fmp4 |
||||
|
||||
import ( |
||||
"time" |
||||
) |
||||
|
||||
const ( |
||||
videoTimescale = 90000 |
||||
) |
||||
|
||||
// VideoSample is a video sample.
|
||||
type VideoSample struct { |
||||
NALUs [][]byte |
||||
PTS time.Duration |
||||
DTS time.Duration |
||||
IDRPresent bool |
||||
Next *VideoSample |
||||
|
||||
avcc []byte |
||||
} |
||||
|
||||
// Duration returns the sample duration.
|
||||
func (s VideoSample) Duration() time.Duration { |
||||
return s.Next.DTS - s.DTS |
||||
} |
@ -0,0 +1,106 @@
@@ -0,0 +1,106 @@
|
||||
// Package m3u8 contains a M3U8 parser.
|
||||
package m3u8 |
||||
|
||||
import ( |
||||
"bytes" |
||||
"errors" |
||||
"regexp" |
||||
"strings" |
||||
|
||||
gm3u8 "github.com/grafov/m3u8" |
||||
) |
||||
|
||||
var reKeyValue = regexp.MustCompile(`([a-zA-Z0-9_-]+)=("[^"]+"|[^",]+)`) |
||||
|
||||
func decodeParamsLine(line string) map[string]string { |
||||
out := make(map[string]string) |
||||
for _, kv := range reKeyValue.FindAllStringSubmatch(line, -1) { |
||||
k, v := kv[1], kv[2] |
||||
out[k] = strings.Trim(v, ` "`) |
||||
} |
||||
return out |
||||
} |
||||
|
||||
// MasterPlaylist is a master playlist.
|
||||
type MasterPlaylist struct { |
||||
gm3u8.MasterPlaylist |
||||
Alternatives []*gm3u8.Alternative |
||||
} |
||||
|
||||
func (MasterPlaylist) isPlaylist() {} |
||||
|
||||
func newMasterPlaylist(byts []byte, mpl *gm3u8.MasterPlaylist) (*MasterPlaylist, error) { |
||||
var alternatives []*gm3u8.Alternative |
||||
|
||||
// https://github.com/grafov/m3u8/blob/036100c52a87e26c62be56df85450e9c703201a6/reader.go#L301
|
||||
for _, line := range strings.Split(string(byts), "\n") { |
||||
if strings.HasPrefix(line, "#EXT-X-MEDIA:") { |
||||
var alt gm3u8.Alternative |
||||
for k, v := range decodeParamsLine(line[13:]) { |
||||
switch k { |
||||
case "TYPE": |
||||
alt.Type = v |
||||
case "GROUP-ID": |
||||
alt.GroupId = v |
||||
case "LANGUAGE": |
||||
alt.Language = v |
||||
case "NAME": |
||||
alt.Name = v |
||||
case "DEFAULT": |
||||
switch { |
||||
case strings.ToUpper(v) == "YES": |
||||
alt.Default = true |
||||
case strings.ToUpper(v) == "NO": |
||||
alt.Default = false |
||||
default: |
||||
return nil, errors.New("value must be YES or NO") |
||||
} |
||||
case "AUTOSELECT": |
||||
alt.Autoselect = v |
||||
case "FORCED": |
||||
alt.Forced = v |
||||
case "CHARACTERISTICS": |
||||
alt.Characteristics = v |
||||
case "SUBTITLES": |
||||
alt.Subtitles = v |
||||
case "URI": |
||||
alt.URI = v |
||||
} |
||||
} |
||||
alternatives = append(alternatives, &alt) |
||||
} |
||||
} |
||||
|
||||
return &MasterPlaylist{ |
||||
MasterPlaylist: *mpl, |
||||
Alternatives: alternatives, |
||||
}, nil |
||||
} |
||||
|
||||
// MediaPlaylist is a media playlist.
|
||||
type MediaPlaylist gm3u8.MediaPlaylist |
||||
|
||||
func (MediaPlaylist) isPlaylist() {} |
||||
|
||||
// Playlist is a M3U8 playlist.
|
||||
type Playlist interface { |
||||
isPlaylist() |
||||
} |
||||
|
||||
// Unmarshal decodes a M3U8 Playlist.
|
||||
func Unmarshal(byts []byte) (Playlist, error) { |
||||
pl, _, err := gm3u8.Decode(*(bytes.NewBuffer(byts)), true) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
switch tpl := pl.(type) { |
||||
case *gm3u8.MasterPlaylist: |
||||
return newMasterPlaylist(byts, tpl) |
||||
|
||||
case *gm3u8.MediaPlaylist: |
||||
return (*MediaPlaylist)(tpl), nil |
||||
} |
||||
|
||||
panic("unexpected playlist type") |
||||
} |
@ -1,30 +1,34 @@
@@ -1,30 +1,34 @@
|
||||
// Package mpegtstimedec contains a MPEG-TS timestamp decoder.
|
||||
package mpegtstimedec |
||||
package mpegts |
||||
|
||||
import ( |
||||
"sync" |
||||
"time" |
||||
) |
||||
|
||||
const ( |
||||
maximum = 0x1FFFFFFFF // 33 bits
|
||||
negativeThreshold = 0xFFFFFFF |
||||
negativeThreshold = 0x1FFFFFFFF / 2 |
||||
clockRate = 90000 |
||||
) |
||||
|
||||
// Decoder is a MPEG-TS timestamp decoder.
|
||||
type Decoder struct { |
||||
// TimeDecoder is a MPEG-TS timestamp decoder.
|
||||
type TimeDecoder struct { |
||||
initialized bool |
||||
tsOverall time.Duration |
||||
tsPrev int64 |
||||
mutex sync.Mutex |
||||
} |
||||
|
||||
// New allocates a Decoder.
|
||||
func New() *Decoder { |
||||
return &Decoder{} |
||||
// NewTimeDecoder allocates a TimeDecoder.
|
||||
func NewTimeDecoder() *TimeDecoder { |
||||
return &TimeDecoder{} |
||||
} |
||||
|
||||
// Decode decodes a MPEG-TS timestamp.
|
||||
func (d *Decoder) Decode(ts int64) time.Duration { |
||||
func (d *TimeDecoder) Decode(ts int64) time.Duration { |
||||
d.mutex.Lock() |
||||
defer d.mutex.Unlock() |
||||
|
||||
if !d.initialized { |
||||
d.initialized = true |
||||
d.tsPrev = ts |
@ -0,0 +1,101 @@
@@ -0,0 +1,101 @@
|
||||
package mpegts |
||||
|
||||
import ( |
||||
"bytes" |
||||
"context" |
||||
"fmt" |
||||
|
||||
"github.com/aler9/gortsplib" |
||||
"github.com/aler9/gortsplib/pkg/mpeg4audio" |
||||
"github.com/asticode/go-astits" |
||||
) |
||||
|
||||
func findMPEG4AudioConfig(dem *astits.Demuxer, pid uint16) (*mpeg4audio.Config, error) { |
||||
for { |
||||
data, err := dem.NextData() |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
if data.PES == nil || data.PID != pid { |
||||
continue |
||||
} |
||||
|
||||
var adtsPkts mpeg4audio.ADTSPackets |
||||
err = adtsPkts.Unmarshal(data.PES.Data) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("unable to decode ADTS: %s", err) |
||||
} |
||||
|
||||
pkt := adtsPkts[0] |
||||
return &mpeg4audio.Config{ |
||||
Type: pkt.Type, |
||||
SampleRate: pkt.SampleRate, |
||||
ChannelCount: pkt.ChannelCount, |
||||
}, nil |
||||
} |
||||
} |
||||
|
||||
// Track is a MPEG-TS track.
|
||||
type Track struct { |
||||
ES *astits.PMTElementaryStream |
||||
Track gortsplib.Track |
||||
} |
||||
|
||||
// FindTracks finds the tracks in a MPEG-TS stream.
|
||||
func FindTracks(byts []byte) ([]*Track, error) { |
||||
var tracks []*Track |
||||
dem := astits.NewDemuxer(context.Background(), bytes.NewReader(byts)) |
||||
|
||||
for { |
||||
data, err := dem.NextData() |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
if data.PMT != nil { |
||||
for _, es := range data.PMT.ElementaryStreams { |
||||
switch es.StreamType { |
||||
case astits.StreamTypeH264Video, |
||||
astits.StreamTypeAACAudio: |
||||
default: |
||||
return nil, fmt.Errorf("track type %d not supported (yet)", es.StreamType) |
||||
} |
||||
|
||||
tracks = append(tracks, &Track{ |
||||
ES: es, |
||||
}) |
||||
} |
||||
break |
||||
} |
||||
} |
||||
|
||||
if tracks == nil { |
||||
return nil, fmt.Errorf("no tracks found") |
||||
} |
||||
|
||||
for _, t := range tracks { |
||||
switch t.ES.StreamType { |
||||
case astits.StreamTypeH264Video: |
||||
t.Track = &gortsplib.TrackH264{ |
||||
PayloadType: 96, |
||||
} |
||||
|
||||
case astits.StreamTypeAACAudio: |
||||
conf, err := findMPEG4AudioConfig(dem, t.ES.ElementaryPID) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
t.Track = &gortsplib.TrackMPEG4Audio{ |
||||
PayloadType: 96, |
||||
Config: conf, |
||||
SizeLength: 13, |
||||
IndexLength: 3, |
||||
IndexDeltaLength: 3, |
||||
} |
||||
} |
||||
} |
||||
|
||||
return tracks, nil |
||||
} |
Loading…
Reference in new issue