47 changed files with 70 additions and 8846 deletions
@ -1,127 +0,0 @@ |
|||||||
package hls |
|
||||||
|
|
||||||
import ( |
|
||||||
"context" |
|
||||||
"fmt" |
|
||||||
"net/url" |
|
||||||
"time" |
|
||||||
|
|
||||||
"github.com/aler9/gortsplib/v2/pkg/format" |
|
||||||
|
|
||||||
"github.com/aler9/rtsp-simple-server/internal/logger" |
|
||||||
) |
|
||||||
|
|
||||||
const ( |
|
||||||
clientMPEGTSEntryQueueSize = 100 |
|
||||||
clientFMP4MaxPartTracksPerSegment = 200 |
|
||||||
clientLiveStartingInvPosition = 3 |
|
||||||
clientLiveMaxInvPosition = 5 |
|
||||||
clientMaxDTSRTCDiff = 10 * time.Second |
|
||||||
) |
|
||||||
|
|
||||||
func clientAbsoluteURL(base *url.URL, relative string) (*url.URL, error) { |
|
||||||
u, err := url.Parse(relative) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
return base.ResolveReference(u), nil |
|
||||||
} |
|
||||||
|
|
||||||
// ClientLogger allows to receive log lines.
|
|
||||||
type ClientLogger interface { |
|
||||||
Log(level logger.Level, format string, args ...interface{}) |
|
||||||
} |
|
||||||
|
|
||||||
// Client is a HLS client.
|
|
||||||
type Client struct { |
|
||||||
fingerprint string |
|
||||||
logger ClientLogger |
|
||||||
|
|
||||||
ctx context.Context |
|
||||||
ctxCancel func() |
|
||||||
onTracks func([]format.Format) error |
|
||||||
onData map[format.Format]func(time.Duration, interface{}) |
|
||||||
playlistURL *url.URL |
|
||||||
|
|
||||||
// out
|
|
||||||
outErr chan error |
|
||||||
} |
|
||||||
|
|
||||||
// NewClient allocates a Client.
|
|
||||||
func NewClient( |
|
||||||
playlistURLStr string, |
|
||||||
fingerprint string, |
|
||||||
logger ClientLogger, |
|
||||||
) (*Client, error) { |
|
||||||
playlistURL, err := url.Parse(playlistURLStr) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
|
|
||||||
ctx, ctxCancel := context.WithCancel(context.Background()) |
|
||||||
|
|
||||||
c := &Client{ |
|
||||||
fingerprint: fingerprint, |
|
||||||
logger: logger, |
|
||||||
ctx: ctx, |
|
||||||
ctxCancel: ctxCancel, |
|
||||||
playlistURL: playlistURL, |
|
||||||
onData: make(map[format.Format]func(time.Duration, interface{})), |
|
||||||
outErr: make(chan error, 1), |
|
||||||
} |
|
||||||
|
|
||||||
return c, nil |
|
||||||
} |
|
||||||
|
|
||||||
// Start starts the client.
|
|
||||||
func (c *Client) Start() { |
|
||||||
go c.run() |
|
||||||
} |
|
||||||
|
|
||||||
// Close closes all the Client resources.
|
|
||||||
func (c *Client) Close() { |
|
||||||
c.ctxCancel() |
|
||||||
} |
|
||||||
|
|
||||||
// Wait waits for any error of the Client.
|
|
||||||
func (c *Client) Wait() chan error { |
|
||||||
return c.outErr |
|
||||||
} |
|
||||||
|
|
||||||
// OnTracks sets a callback that is called when tracks are read.
|
|
||||||
func (c *Client) OnTracks(cb func([]format.Format) error) { |
|
||||||
c.onTracks = cb |
|
||||||
} |
|
||||||
|
|
||||||
// OnData sets a callback that is called when data arrives.
|
|
||||||
func (c *Client) OnData(forma format.Format, cb func(time.Duration, interface{})) { |
|
||||||
c.onData[forma] = cb |
|
||||||
} |
|
||||||
|
|
||||||
func (c *Client) run() { |
|
||||||
c.outErr <- c.runInner() |
|
||||||
} |
|
||||||
|
|
||||||
func (c *Client) runInner() error { |
|
||||||
rp := newClientRoutinePool() |
|
||||||
|
|
||||||
dl := newClientDownloaderPrimary( |
|
||||||
c.playlistURL, |
|
||||||
c.fingerprint, |
|
||||||
c.logger, |
|
||||||
rp, |
|
||||||
c.onTracks, |
|
||||||
c.onData, |
|
||||||
) |
|
||||||
rp.add(dl) |
|
||||||
|
|
||||||
select { |
|
||||||
case err := <-rp.errorChan(): |
|
||||||
rp.close() |
|
||||||
return err |
|
||||||
|
|
||||||
case <-c.ctx.Done(): |
|
||||||
rp.close() |
|
||||||
return fmt.Errorf("terminated") |
|
||||||
} |
|
||||||
} |
|
||||||
@ -1,297 +0,0 @@ |
|||||||
package hls |
|
||||||
|
|
||||||
import ( |
|
||||||
"context" |
|
||||||
"crypto/sha256" |
|
||||||
"crypto/tls" |
|
||||||
"encoding/hex" |
|
||||||
"fmt" |
|
||||||
"io" |
|
||||||
"net/http" |
|
||||||
"net/url" |
|
||||||
"strings" |
|
||||||
"time" |
|
||||||
|
|
||||||
"github.com/aler9/gortsplib/v2/pkg/format" |
|
||||||
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 pickLeadingPlaylist(variants []*gm3u8.Variant) *gm3u8.Variant { |
|
||||||
var candidates []*gm3u8.Variant //nolint:prealloc
|
|
||||||
for _, v := range variants { |
|
||||||
if v.Codecs != "" && !codecParametersAreSupported(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([]format.Format) error |
|
||||||
onData map[format.Format]func(time.Duration, interface{}) |
|
||||||
rp *clientRoutinePool |
|
||||||
|
|
||||||
httpClient *http.Client |
|
||||||
leadingTimeSync clientTimeSync |
|
||||||
|
|
||||||
// in
|
|
||||||
streamTracks chan []format.Format |
|
||||||
|
|
||||||
// out
|
|
||||||
startStreaming chan struct{} |
|
||||||
leadingTimeSyncReady chan struct{} |
|
||||||
} |
|
||||||
|
|
||||||
func newClientDownloaderPrimary( |
|
||||||
primaryPlaylistURL *url.URL, |
|
||||||
fingerprint string, |
|
||||||
logger ClientLogger, |
|
||||||
rp *clientRoutinePool, |
|
||||||
onTracks func([]format.Format) error, |
|
||||||
onData map[format.Format]func(time.Duration, interface{}), |
|
||||||
) *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, |
|
||||||
onData: onData, |
|
||||||
rp: rp, |
|
||||||
httpClient: &http.Client{ |
|
||||||
Transport: &http.Transport{ |
|
||||||
TLSClientConfig: tlsConfig, |
|
||||||
}, |
|
||||||
}, |
|
||||||
streamTracks: make(chan []format.Format), |
|
||||||
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.onData, |
|
||||||
) |
|
||||||
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.onData, |
|
||||||
) |
|
||||||
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.onData, |
|
||||||
) |
|
||||||
d.rp.add(ds) |
|
||||||
streamCount++ |
|
||||||
} |
|
||||||
|
|
||||||
default: |
|
||||||
return fmt.Errorf("invalid playlist") |
|
||||||
} |
|
||||||
|
|
||||||
var tracks []format.Format |
|
||||||
|
|
||||||
for i := 0; i < streamCount; i++ { |
|
||||||
select { |
|
||||||
case streamTracks := <-d.streamTracks: |
|
||||||
tracks = append(tracks, streamTracks...) |
|
||||||
case <-ctx.Done(): |
|
||||||
return fmt.Errorf("terminated") |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
if len(tracks) == 0 { |
|
||||||
return fmt.Errorf("no supported tracks found") |
|
||||||
} |
|
||||||
|
|
||||||
err = d.onTracks(tracks) |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
close(d.startStreaming) |
|
||||||
|
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
func (d *clientDownloaderPrimary) onStreamTracks(ctx context.Context, tracks []format.Format) 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 |
|
||||||
} |
|
||||||
@ -1,260 +0,0 @@ |
|||||||
package hls |
|
||||||
|
|
||||||
import ( |
|
||||||
"context" |
|
||||||
"fmt" |
|
||||||
"io" |
|
||||||
"net/http" |
|
||||||
"net/url" |
|
||||||
"strconv" |
|
||||||
"time" |
|
||||||
|
|
||||||
"github.com/aler9/gortsplib/v2/pkg/format" |
|
||||||
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 < 0 || 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, []format.Format) bool |
|
||||||
onSetLeadingTimeSync func(clientTimeSync) |
|
||||||
onGetLeadingTimeSync func(context.Context) (clientTimeSync, bool) |
|
||||||
onData map[format.Format]func(time.Duration, interface{}) |
|
||||||
|
|
||||||
curSegmentID *uint64 |
|
||||||
} |
|
||||||
|
|
||||||
func newClientDownloaderStream( |
|
||||||
isLeading bool, |
|
||||||
httpClient *http.Client, |
|
||||||
playlistURL *url.URL, |
|
||||||
initialPlaylist *m3u8.MediaPlaylist, |
|
||||||
logger ClientLogger, |
|
||||||
rp *clientRoutinePool, |
|
||||||
onStreamTracks func(context.Context, []format.Format) bool, |
|
||||||
onSetLeadingTimeSync func(clientTimeSync), |
|
||||||
onGetLeadingTimeSync func(context.Context) (clientTimeSync, bool), |
|
||||||
onData map[format.Format]func(time.Duration, interface{}), |
|
||||||
) *clientDownloaderStream { |
|
||||||
return &clientDownloaderStream{ |
|
||||||
isLeading: isLeading, |
|
||||||
httpClient: httpClient, |
|
||||||
playlistURL: playlistURL, |
|
||||||
initialPlaylist: initialPlaylist, |
|
||||||
logger: logger, |
|
||||||
rp: rp, |
|
||||||
onStreamTracks: onStreamTracks, |
|
||||||
onSetLeadingTimeSync: onSetLeadingTimeSync, |
|
||||||
onGetLeadingTimeSync: onGetLeadingTimeSync, |
|
||||||
onData: onData, |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
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.onData, |
|
||||||
) |
|
||||||
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.onData, |
|
||||||
) |
|
||||||
d.rp.add(proc) |
|
||||||
} |
|
||||||
|
|
||||||
err := d.fillSegmentQueue(ctx, initialPlaylist, segmentQueue) |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
for { |
|
||||||
ok := segmentQueue.waitUntilSizeIsBelow(ctx, 1) |
|
||||||
if !ok { |
|
||||||
return fmt.Errorf("terminated") |
|
||||||
} |
|
||||||
|
|
||||||
pl, err := d.downloadPlaylist(ctx) |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
err = d.fillSegmentQueue(ctx, pl, 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, |
|
||||||
pl *m3u8.MediaPlaylist, segmentQueue *clientSegmentQueue, |
|
||||||
) error { |
|
||||||
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 |
|
||||||
} |
|
||||||
@ -1,221 +0,0 @@ |
|||||||
package hls |
|
||||||
|
|
||||||
import ( |
|
||||||
"context" |
|
||||||
"fmt" |
|
||||||
"time" |
|
||||||
|
|
||||||
"github.com/aler9/gortsplib/v2/pkg/codecs/h264" |
|
||||||
"github.com/aler9/gortsplib/v2/pkg/format" |
|
||||||
|
|
||||||
"github.com/aler9/rtsp-simple-server/internal/hls/fmp4" |
|
||||||
) |
|
||||||
|
|
||||||
func fmp4PickLeadingTrack(init *fmp4.Init) int { |
|
||||||
// pick first video track
|
|
||||||
for _, track := range init.Tracks { |
|
||||||
switch track.Format.(type) { |
|
||||||
case *format.H264, *format.H265: |
|
||||||
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) |
|
||||||
onData map[format.Format]func(time.Duration, interface{}) |
|
||||||
|
|
||||||
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, |
|
||||||
onStreamFormats func(context.Context, []format.Format) bool, |
|
||||||
onSetLeadingTimeSync func(clientTimeSync), |
|
||||||
onGetLeadingTimeSync func(context.Context) (clientTimeSync, bool), |
|
||||||
onData map[format.Format]func(time.Duration, interface{}), |
|
||||||
) (*clientProcessorFMP4, error) { |
|
||||||
p := &clientProcessorFMP4{ |
|
||||||
isLeading: isLeading, |
|
||||||
segmentQueue: segmentQueue, |
|
||||||
logger: logger, |
|
||||||
rp: rp, |
|
||||||
onSetLeadingTimeSync: onSetLeadingTimeSync, |
|
||||||
onGetLeadingTimeSync: onGetLeadingTimeSync, |
|
||||||
onData: onData, |
|
||||||
subpartProcessed: make(chan struct{}, clientFMP4MaxPartTracksPerSegment), |
|
||||||
} |
|
||||||
|
|
||||||
err := p.init.Unmarshal(initFile) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
|
|
||||||
p.leadingTrackID = fmp4PickLeadingTrack(&p.init) |
|
||||||
|
|
||||||
tracks := make([]format.Format, len(p.init.Tracks)) |
|
||||||
for i, track := range p.init.Tracks { |
|
||||||
tracks[i] = track.Format |
|
||||||
} |
|
||||||
|
|
||||||
ok := onStreamFormats(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 { |
|
||||||
continue |
|
||||||
} |
|
||||||
|
|
||||||
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 |
|
||||||
|
|
||||||
cb2, ok := p.onData[track.Format] |
|
||||||
if !ok { |
|
||||||
cb2 = func(time.Duration, interface{}) { |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
switch track.Format.(type) { |
|
||||||
case *format.H264, *format.H265: |
|
||||||
cb = func(pts time.Duration, payload []byte) error { |
|
||||||
nalus, err := h264.AVCCUnmarshal(payload) |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
cb2(pts, nalus) |
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
case *format.MPEG4Audio, *format.Opus: |
|
||||||
cb = func(pts time.Duration, payload []byte) error { |
|
||||||
cb2(pts, payload) |
|
||||||
return nil |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
proc := newClientProcessorFMP4Track( |
|
||||||
track.TimeScale, |
|
||||||
ts, |
|
||||||
p.onPartTrackProcessed, |
|
||||||
cb, |
|
||||||
) |
|
||||||
p.rp.add(proc) |
|
||||||
p.trackProcs[track.ID] = proc |
|
||||||
} |
|
||||||
} |
|
||||||
@ -1,72 +0,0 @@ |
|||||||
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 |
|
||||||
} |
|
||||||
|
|
||||||
if pts >= 0 { // silently discard packets prior to the first packet of the leading track
|
|
||||||
err = t.onEntry(pts, sample.Payload) |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
rawDTS += uint64(sample.Duration) |
|
||||||
} |
|
||||||
|
|
||||||
return nil |
|
||||||
} |
|
||||||
@ -1,221 +0,0 @@ |
|||||||
package hls |
|
||||||
|
|
||||||
import ( |
|
||||||
"bytes" |
|
||||||
"context" |
|
||||||
"fmt" |
|
||||||
"strings" |
|
||||||
"time" |
|
||||||
|
|
||||||
"github.com/aler9/gortsplib/v2/pkg/codecs/h264" |
|
||||||
"github.com/aler9/gortsplib/v2/pkg/codecs/mpeg4audio" |
|
||||||
"github.com/aler9/gortsplib/v2/pkg/format" |
|
||||||
"github.com/asticode/go-astits" |
|
||||||
|
|
||||||
"github.com/aler9/rtsp-simple-server/internal/hls/mpegts" |
|
||||||
"github.com/aler9/rtsp-simple-server/internal/logger" |
|
||||||
) |
|
||||||
|
|
||||||
func mpegtsPickLeadingTrack(mpegtsTracks []*mpegts.Track) uint16 { |
|
||||||
// pick first video track
|
|
||||||
for _, mt := range mpegtsTracks { |
|
||||||
if _, ok := mt.Format.(*format.H264); ok { |
|
||||||
return mt.ES.ElementaryPID |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// otherwise, pick first track
|
|
||||||
return mpegtsTracks[0].ES.ElementaryPID |
|
||||||
} |
|
||||||
|
|
||||||
type clientProcessorMPEGTS struct { |
|
||||||
isLeading bool |
|
||||||
segmentQueue *clientSegmentQueue |
|
||||||
logger ClientLogger |
|
||||||
rp *clientRoutinePool |
|
||||||
onStreamFormats func(context.Context, []format.Format) bool |
|
||||||
onSetLeadingTimeSync func(clientTimeSync) |
|
||||||
onGetLeadingTimeSync func(context.Context) (clientTimeSync, bool) |
|
||||||
onData map[format.Format]func(time.Duration, interface{}) |
|
||||||
|
|
||||||
mpegtsTracks []*mpegts.Track |
|
||||||
leadingTrackPID uint16 |
|
||||||
trackProcs map[uint16]*clientProcessorMPEGTSTrack |
|
||||||
} |
|
||||||
|
|
||||||
func newClientProcessorMPEGTS( |
|
||||||
isLeading bool, |
|
||||||
segmentQueue *clientSegmentQueue, |
|
||||||
logger ClientLogger, |
|
||||||
rp *clientRoutinePool, |
|
||||||
onStreamFormats func(context.Context, []format.Format) bool, |
|
||||||
onSetLeadingTimeSync func(clientTimeSync), |
|
||||||
onGetLeadingTimeSync func(context.Context) (clientTimeSync, bool), |
|
||||||
onData map[format.Format]func(time.Duration, interface{}), |
|
||||||
) *clientProcessorMPEGTS { |
|
||||||
return &clientProcessorMPEGTS{ |
|
||||||
isLeading: isLeading, |
|
||||||
segmentQueue: segmentQueue, |
|
||||||
logger: logger, |
|
||||||
rp: rp, |
|
||||||
onStreamFormats: onStreamFormats, |
|
||||||
onSetLeadingTimeSync: onSetLeadingTimeSync, |
|
||||||
onGetLeadingTimeSync: onGetLeadingTimeSync, |
|
||||||
onData: onData, |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
func (p *clientProcessorMPEGTS) 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 *clientProcessorMPEGTS) processSegment(ctx context.Context, byts []byte) error { |
|
||||||
if p.mpegtsTracks == nil { |
|
||||||
var err error |
|
||||||
p.mpegtsTracks, err = mpegts.FindTracks(byts) |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
p.leadingTrackPID = mpegtsPickLeadingTrack(p.mpegtsTracks) |
|
||||||
|
|
||||||
tracks := make([]format.Format, len(p.mpegtsTracks)) |
|
||||||
for i, mt := range p.mpegtsTracks { |
|
||||||
tracks[i] = mt.Format |
|
||||||
} |
|
||||||
|
|
||||||
ok := p.onStreamFormats(ctx, tracks) |
|
||||||
if !ok { |
|
||||||
return fmt.Errorf("terminated") |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
dem := astits.NewDemuxer(context.Background(), bytes.NewReader(byts)) |
|
||||||
|
|
||||||
for { |
|
||||||
data, err := dem.NextData() |
|
||||||
if err != nil { |
|
||||||
if err == astits.ErrNoMorePackets { |
|
||||||
return nil |
|
||||||
} |
|
||||||
if strings.HasPrefix(err.Error(), "astits: parsing PES data failed") { |
|
||||||
continue |
|
||||||
} |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
if data.PES == nil { |
|
||||||
continue |
|
||||||
} |
|
||||||
|
|
||||||
if data.PES.Header.OptionalHeader == nil || |
|
||||||
data.PES.Header.OptionalHeader.PTSDTSIndicator == astits.PTSDTSIndicatorNoPTSOrDTS || |
|
||||||
data.PES.Header.OptionalHeader.PTSDTSIndicator == astits.PTSDTSIndicatorIsForbidden { |
|
||||||
return fmt.Errorf("PTS is missing") |
|
||||||
} |
|
||||||
|
|
||||||
if p.trackProcs == nil { |
|
||||||
var ts *clientTimeSyncMPEGTS |
|
||||||
|
|
||||||
if p.isLeading { |
|
||||||
if data.PID != p.leadingTrackPID { |
|
||||||
continue |
|
||||||
} |
|
||||||
|
|
||||||
var dts int64 |
|
||||||
if data.PES.Header.OptionalHeader.PTSDTSIndicator == astits.PTSDTSIndicatorBothPresent { |
|
||||||
dts = data.PES.Header.OptionalHeader.DTS.Base |
|
||||||
} else { |
|
||||||
dts = data.PES.Header.OptionalHeader.PTS.Base |
|
||||||
} |
|
||||||
|
|
||||||
ts = newClientTimeSyncMPEGTS(dts) |
|
||||||
p.onSetLeadingTimeSync(ts) |
|
||||||
} else { |
|
||||||
rawTS, ok := p.onGetLeadingTimeSync(ctx) |
|
||||||
if !ok { |
|
||||||
return fmt.Errorf("terminated") |
|
||||||
} |
|
||||||
|
|
||||||
ts, ok = rawTS.(*clientTimeSyncMPEGTS) |
|
||||||
if !ok { |
|
||||||
return fmt.Errorf("stream playlists are mixed MPEGTS/FMP4") |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
p.initializeTrackProcs(ts) |
|
||||||
} |
|
||||||
|
|
||||||
proc, ok := p.trackProcs[data.PID] |
|
||||||
if !ok { |
|
||||||
continue |
|
||||||
} |
|
||||||
|
|
||||||
select { |
|
||||||
case proc.queue <- data.PES: |
|
||||||
case <-ctx.Done(): |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
func (p *clientProcessorMPEGTS) initializeTrackProcs(ts *clientTimeSyncMPEGTS) { |
|
||||||
p.trackProcs = make(map[uint16]*clientProcessorMPEGTSTrack) |
|
||||||
|
|
||||||
for _, track := range p.mpegtsTracks { |
|
||||||
var cb func(time.Duration, []byte) error |
|
||||||
|
|
||||||
cb2, ok := p.onData[track.Format] |
|
||||||
if !ok { |
|
||||||
cb2 = func(time.Duration, interface{}) { |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
switch track.Format.(type) { |
|
||||||
case *format.H264: |
|
||||||
cb = func(pts time.Duration, payload []byte) error { |
|
||||||
nalus, err := h264.AnnexBUnmarshal(payload) |
|
||||||
if err != nil { |
|
||||||
p.logger.Log(logger.Warn, "unable to decode Annex-B: %s", err) |
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
cb2(pts, nalus) |
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
case *format.MPEG4Audio: |
|
||||||
cb = func(pts time.Duration, payload []byte) error { |
|
||||||
var adtsPkts mpeg4audio.ADTSPackets |
|
||||||
err := adtsPkts.Unmarshal(payload) |
|
||||||
if err != nil { |
|
||||||
return fmt.Errorf("unable to decode ADTS: %s", err) |
|
||||||
} |
|
||||||
|
|
||||||
for i, pkt := range adtsPkts { |
|
||||||
cb2( |
|
||||||
pts+time.Duration(i)*mpeg4audio.SamplesPerAccessUnit*time.Second/time.Duration(pkt.SampleRate), |
|
||||||
pkt.AU) |
|
||||||
} |
|
||||||
|
|
||||||
return nil |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
proc := newClientProcessorMPEGTSTrack( |
|
||||||
ts, |
|
||||||
cb, |
|
||||||
) |
|
||||||
p.rp.add(proc) |
|
||||||
p.trackProcs[track.ES.ElementaryPID] = proc |
|
||||||
} |
|
||||||
} |
|
||||||
@ -1,63 +0,0 @@ |
|||||||
package hls |
|
||||||
|
|
||||||
import ( |
|
||||||
"context" |
|
||||||
"time" |
|
||||||
|
|
||||||
"github.com/asticode/go-astits" |
|
||||||
) |
|
||||||
|
|
||||||
type clientProcessorMPEGTSTrack struct { |
|
||||||
ts *clientTimeSyncMPEGTS |
|
||||||
onEntry func(time.Duration, []byte) error |
|
||||||
|
|
||||||
queue chan *astits.PESData |
|
||||||
} |
|
||||||
|
|
||||||
func newClientProcessorMPEGTSTrack( |
|
||||||
ts *clientTimeSyncMPEGTS, |
|
||||||
onEntry func(time.Duration, []byte) error, |
|
||||||
) *clientProcessorMPEGTSTrack { |
|
||||||
return &clientProcessorMPEGTSTrack{ |
|
||||||
ts: ts, |
|
||||||
onEntry: onEntry, |
|
||||||
queue: make(chan *astits.PESData, clientMPEGTSEntryQueueSize), |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
func (t *clientProcessorMPEGTSTrack) run(ctx context.Context) error { |
|
||||||
for { |
|
||||||
select { |
|
||||||
case pes := <-t.queue: |
|
||||||
err := t.processEntry(ctx, pes) |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
case <-ctx.Done(): |
|
||||||
return nil |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
func (t *clientProcessorMPEGTSTrack) processEntry(ctx context.Context, pes *astits.PESData) error { |
|
||||||
rawPTS := pes.Header.OptionalHeader.PTS.Base |
|
||||||
var rawDTS int64 |
|
||||||
if pes.Header.OptionalHeader.PTSDTSIndicator == astits.PTSDTSIndicatorBothPresent { |
|
||||||
rawDTS = pes.Header.OptionalHeader.DTS.Base |
|
||||||
} else { |
|
||||||
rawDTS = rawPTS |
|
||||||
} |
|
||||||
|
|
||||||
pts, err := t.ts.convertAndSync(ctx, rawDTS, rawPTS) |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
// silently discard packets prior to the first packet of the leading track
|
|
||||||
if pts < 0 { |
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
return t.onEntry(pts, pes.Data) |
|
||||||
} |
|
||||||
@ -1,52 +0,0 @@ |
|||||||
package hls |
|
||||||
|
|
||||||
import ( |
|
||||||
"context" |
|
||||||
"sync" |
|
||||||
) |
|
||||||
|
|
||||||
type clientRoutinePoolRunnable interface { |
|
||||||
run(context.Context) error |
|
||||||
} |
|
||||||
|
|
||||||
type clientRoutinePool struct { |
|
||||||
ctx context.Context |
|
||||||
ctxCancel func() |
|
||||||
wg sync.WaitGroup |
|
||||||
|
|
||||||
err chan error |
|
||||||
} |
|
||||||
|
|
||||||
func newClientRoutinePool() *clientRoutinePool { |
|
||||||
ctx, ctxCancel := context.WithCancel(context.Background()) |
|
||||||
|
|
||||||
return &clientRoutinePool{ |
|
||||||
ctx: ctx, |
|
||||||
ctxCancel: ctxCancel, |
|
||||||
err: make(chan error), |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
func (rp *clientRoutinePool) close() { |
|
||||||
rp.ctxCancel() |
|
||||||
rp.wg.Wait() |
|
||||||
} |
|
||||||
|
|
||||||
func (rp *clientRoutinePool) errorChan() chan error { |
|
||||||
return rp.err |
|
||||||
} |
|
||||||
|
|
||||||
func (rp *clientRoutinePool) add(r clientRoutinePoolRunnable) { |
|
||||||
rp.wg.Add(1) |
|
||||||
go func() { |
|
||||||
defer rp.wg.Done() |
|
||||||
|
|
||||||
err := r.run(rp.ctx) |
|
||||||
if err != nil { |
|
||||||
select { |
|
||||||
case rp.err <- err: |
|
||||||
case <-rp.ctx.Done(): |
|
||||||
} |
|
||||||
} |
|
||||||
}() |
|
||||||
} |
|
||||||
@ -1,79 +0,0 @@ |
|||||||
package hls |
|
||||||
|
|
||||||
import ( |
|
||||||
"context" |
|
||||||
"sync" |
|
||||||
) |
|
||||||
|
|
||||||
type clientSegmentQueue struct { |
|
||||||
mutex sync.Mutex |
|
||||||
queue [][]byte |
|
||||||
didPush chan struct{} |
|
||||||
didPull chan struct{} |
|
||||||
} |
|
||||||
|
|
||||||
func newClientSegmentQueue() *clientSegmentQueue { |
|
||||||
return &clientSegmentQueue{ |
|
||||||
didPush: make(chan struct{}), |
|
||||||
didPull: make(chan struct{}), |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
func (q *clientSegmentQueue) push(seg []byte) { |
|
||||||
q.mutex.Lock() |
|
||||||
|
|
||||||
queueWasEmpty := (len(q.queue) == 0) |
|
||||||
q.queue = append(q.queue, seg) |
|
||||||
|
|
||||||
if queueWasEmpty { |
|
||||||
close(q.didPush) |
|
||||||
q.didPush = make(chan struct{}) |
|
||||||
} |
|
||||||
|
|
||||||
q.mutex.Unlock() |
|
||||||
} |
|
||||||
|
|
||||||
func (q *clientSegmentQueue) waitUntilSizeIsBelow(ctx context.Context, n int) bool { |
|
||||||
q.mutex.Lock() |
|
||||||
|
|
||||||
for len(q.queue) > n { |
|
||||||
q.mutex.Unlock() |
|
||||||
|
|
||||||
select { |
|
||||||
case <-q.didPull: |
|
||||||
case <-ctx.Done(): |
|
||||||
return false |
|
||||||
} |
|
||||||
|
|
||||||
q.mutex.Lock() |
|
||||||
} |
|
||||||
|
|
||||||
q.mutex.Unlock() |
|
||||||
return true |
|
||||||
} |
|
||||||
|
|
||||||
func (q *clientSegmentQueue) pull(ctx context.Context) ([]byte, bool) { |
|
||||||
q.mutex.Lock() |
|
||||||
|
|
||||||
for len(q.queue) == 0 { |
|
||||||
didPush := q.didPush |
|
||||||
q.mutex.Unlock() |
|
||||||
|
|
||||||
select { |
|
||||||
case <-didPush: |
|
||||||
case <-ctx.Done(): |
|
||||||
return nil, false |
|
||||||
} |
|
||||||
|
|
||||||
q.mutex.Lock() |
|
||||||
} |
|
||||||
|
|
||||||
var seg []byte |
|
||||||
seg, q.queue = q.queue[0], q.queue[1:] |
|
||||||
|
|
||||||
close(q.didPull) |
|
||||||
q.didPull = make(chan struct{}) |
|
||||||
|
|
||||||
q.mutex.Unlock() |
|
||||||
return seg, true |
|
||||||
} |
|
||||||
@ -1,445 +0,0 @@ |
|||||||
package hls |
|
||||||
|
|
||||||
import ( |
|
||||||
"bytes" |
|
||||||
"context" |
|
||||||
"io" |
|
||||||
"log" |
|
||||||
"net" |
|
||||||
"net/http" |
|
||||||
"os" |
|
||||||
"testing" |
|
||||||
"time" |
|
||||||
|
|
||||||
"github.com/aler9/gortsplib/v2/pkg/codecs/h264" |
|
||||||
"github.com/aler9/gortsplib/v2/pkg/format" |
|
||||||
"github.com/asticode/go-astits" |
|
||||||
"github.com/gin-gonic/gin" |
|
||||||
"github.com/stretchr/testify/require" |
|
||||||
|
|
||||||
"github.com/aler9/rtsp-simple-server/internal/hls/fmp4" |
|
||||||
"github.com/aler9/rtsp-simple-server/internal/logger" |
|
||||||
) |
|
||||||
|
|
||||||
type testLogger struct{} |
|
||||||
|
|
||||||
func (testLogger) Log(level logger.Level, format string, args ...interface{}) { |
|
||||||
log.Printf(format, args...) |
|
||||||
} |
|
||||||
|
|
||||||
var serverCert = []byte(`-----BEGIN CERTIFICATE----- |
|
||||||
MIIDazCCAlOgAwIBAgIUXw1hEC3LFpTsllv7D3ARJyEq7sIwDQYJKoZIhvcNAQEL |
|
||||||
BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM |
|
||||||
GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yMDEyMTMxNzQ0NThaFw0zMDEy |
|
||||||
MTExNzQ0NThaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw |
|
||||||
HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEB |
|
||||||
AQUAA4IBDwAwggEKAoIBAQDG8DyyS51810GsGwgWr5rjJK7OE1kTTLSNEEKax8Bj |
|
||||||
zOyiaz8rA2JGl2VUEpi2UjDr9Cm7nd+YIEVs91IIBOb7LGqObBh1kGF3u5aZxLkv |
|
||||||
NJE+HrLVvUhaDobK2NU+Wibqc/EI3DfUkt1rSINvv9flwTFu1qHeuLWhoySzDKEp |
|
||||||
OzYxpFhwjVSokZIjT4Red3OtFz7gl2E6OAWe2qoh5CwLYVdMWtKR0Xuw3BkDPk9I |
|
||||||
qkQKx3fqv97LPEzhyZYjDT5WvGrgZ1WDAN3booxXF3oA1H3GHQc4m/vcLatOtb8e |
|
||||||
nI59gMQLEbnp08cl873bAuNuM95EZieXTHNbwUnq5iybAgMBAAGjUzBRMB0GA1Ud |
|
||||||
DgQWBBQBKhJh8eWu0a4au9X/2fKhkFX2vjAfBgNVHSMEGDAWgBQBKhJh8eWu0a4a |
|
||||||
u9X/2fKhkFX2vjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBj |
|
||||||
3aCW0YPKukYgVK9cwN0IbVy/D0C1UPT4nupJcy/E0iC7MXPZ9D/SZxYQoAkdptdO |
|
||||||
xfI+RXkpQZLdODNx9uvV+cHyZHZyjtE5ENu/i5Rer2cWI/mSLZm5lUQyx+0KZ2Yu |
|
||||||
tEI1bsebDK30msa8QSTn0WidW9XhFnl3gRi4wRdimcQapOWYVs7ih+nAlSvng7NI |
|
||||||
XpAyRs8PIEbpDDBMWnldrX4TP6EWYUi49gCp8OUDRREKX3l6Ls1vZ02F34yHIt/7 |
|
||||||
7IV/XSKG096bhW+icKBWV0IpcEsgTzPK1J1hMxgjhzIMxGboAeUU+kidthOob6Sd |
|
||||||
XQxaORfgM//NzX9LhUPk
|
|
||||||
-----END CERTIFICATE----- |
|
||||||
`) |
|
||||||
|
|
||||||
var serverKey = []byte(`-----BEGIN RSA PRIVATE KEY----- |
|
||||||
MIIEogIBAAKCAQEAxvA8skudfNdBrBsIFq+a4ySuzhNZE0y0jRBCmsfAY8zsoms/ |
|
||||||
KwNiRpdlVBKYtlIw6/Qpu53fmCBFbPdSCATm+yxqjmwYdZBhd7uWmcS5LzSRPh6y |
|
||||||
1b1IWg6GytjVPlom6nPxCNw31JLda0iDb7/X5cExbtah3ri1oaMkswyhKTs2MaRY |
|
||||||
cI1UqJGSI0+EXndzrRc+4JdhOjgFntqqIeQsC2FXTFrSkdF7sNwZAz5PSKpECsd3 |
|
||||||
6r/eyzxM4cmWIw0+Vrxq4GdVgwDd26KMVxd6ANR9xh0HOJv73C2rTrW/HpyOfYDE |
|
||||||
CxG56dPHJfO92wLjbjPeRGYnl0xzW8FJ6uYsmwIDAQABAoIBACi0BKcyQ3HElSJC |
|
||||||
kaAao+Uvnzh4yvPg8Nwf5JDIp/uDdTMyIEWLtrLczRWrjGVZYbsVROinP5VfnPTT |
|
||||||
kYwkfKINj2u+gC6lsNuPnRuvHXikF8eO/mYvCTur1zZvsQnF5kp4GGwIqr+qoPUP |
|
||||||
bB0UMndG1PdpoMryHe+JcrvTrLHDmCeH10TqOwMsQMLHYLkowvxwJWsmTY7/Qr5S |
|
||||||
Wm3PPpOcW2i0uyPVuyuv4yD1368fqnqJ8QFsQp1K6QtYsNnJ71Hut1/IoxK/e6hj |
|
||||||
5Z+byKtHVtmcLnABuoOT7BhleJNFBksX9sh83jid4tMBgci+zXNeGmgqo2EmaWAb |
|
||||||
agQslkECgYEA8B1rzjOHVQx/vwSzDa4XOrpoHQRfyElrGNz9JVBvnoC7AorezBXQ |
|
||||||
M9WTHQIFTGMjzD8pb+YJGi3gj93VN51r0SmJRxBaBRh1ZZI9kFiFzngYev8POgD3 |
|
||||||
ygmlS3kTHCNxCK/CJkB+/jMBgtPj5ygDpCWVcTSuWlQFphePkW7jaaECgYEA1Blz |
|
||||||
ulqgAyJHZaqgcbcCsI2q6m527hVr9pjzNjIVmkwu38yS9RTCgdlbEVVDnS0hoifl |
|
||||||
+jVMEGXjF3xjyMvL50BKbQUH+KAa+V4n1WGlnZOxX9TMny8MBjEuSX2+362vQ3BX |
|
||||||
4vOlX00gvoc+sY+lrzvfx/OdPCHQGVYzoKCxhLsCgYA07HcviuIAV/HsO2/vyvhp |
|
||||||
xF5gTu+BqNUHNOZDDDid+ge+Jre2yfQLCL8VPLXIQW3Jff53IH/PGl+NtjphuLvj |
|
||||||
7UDJvgvpZZuymIojP6+2c3gJ3CASC9aR3JBnUzdoE1O9s2eaoMqc4scpe+SWtZYf |
|
||||||
3vzSZ+cqF6zrD/Rf/M35IQKBgHTU4E6ShPm09CcoaeC5sp2WK8OevZw/6IyZi78a |
|
||||||
r5Oiy18zzO97U/k6xVMy6F+38ILl/2Rn31JZDVJujniY6eSkIVsUHmPxrWoXV1HO |
|
||||||
y++U32uuSFiXDcSLarfIsE992MEJLSAynbF1Rsgsr3gXbGiuToJRyxbIeVy7gwzD |
|
||||||
94TpAoGAY4/PejWQj9psZfAhyk5dRGra++gYRQ/gK1IIc1g+Dd2/BxbT/RHr05GK |
|
||||||
6vwrfjsoRyMWteC1SsNs/CurjfQ/jqCfHNP5XPvxgd5Ec8sRJIiV7V5RTuWJsPu1 |
|
||||||
+3K6cnKEyg+0ekYmLertRFIY6SwWmY1fyKgTvxudMcsBY7dC4xs= |
|
||||||
-----END RSA PRIVATE KEY----- |
|
||||||
`) |
|
||||||
|
|
||||||
func writeTempFile(byts []byte) (string, error) { |
|
||||||
tmpf, err := os.CreateTemp(os.TempDir(), "rtsp-") |
|
||||||
if err != nil { |
|
||||||
return "", err |
|
||||||
} |
|
||||||
defer tmpf.Close() |
|
||||||
|
|
||||||
_, err = tmpf.Write(byts) |
|
||||||
if err != nil { |
|
||||||
return "", err |
|
||||||
} |
|
||||||
|
|
||||||
return tmpf.Name(), nil |
|
||||||
} |
|
||||||
|
|
||||||
func mpegtsSegment(w io.Writer) { |
|
||||||
mux := astits.NewMuxer(context.Background(), w) |
|
||||||
mux.AddElementaryStream(astits.PMTElementaryStream{ |
|
||||||
ElementaryPID: 256, |
|
||||||
StreamType: astits.StreamTypeH264Video, |
|
||||||
}) |
|
||||||
mux.SetPCRPID(256) |
|
||||||
mux.WriteTables() |
|
||||||
|
|
||||||
enc, _ := h264.AnnexBMarshal([][]byte{ |
|
||||||
{7, 1, 2, 3}, // SPS
|
|
||||||
{8}, // PPS
|
|
||||||
{5}, // IDR
|
|
||||||
}) |
|
||||||
|
|
||||||
mux.WriteData(&astits.MuxerData{ |
|
||||||
PID: 256, |
|
||||||
PES: &astits.PESData{ |
|
||||||
Header: &astits.PESHeader{ |
|
||||||
OptionalHeader: &astits.PESOptionalHeader{ |
|
||||||
MarkerBits: 2, |
|
||||||
PTSDTSIndicator: astits.PTSDTSIndicatorBothPresent, |
|
||||||
PTS: &astits.ClockReference{Base: 90000}, // +1 sec
|
|
||||||
DTS: &astits.ClockReference{Base: 0x1FFFFFFFF - 90000 + 1}, // -1 sec
|
|
||||||
}, |
|
||||||
StreamID: 224, // = video
|
|
||||||
}, |
|
||||||
Data: enc, |
|
||||||
}, |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
func mp4Init(t *testing.T, w io.Writer) { |
|
||||||
i := &fmp4.Init{ |
|
||||||
Tracks: []*fmp4.InitTrack{ |
|
||||||
{ |
|
||||||
ID: 1, |
|
||||||
TimeScale: 90000, |
|
||||||
Format: &format.H264{ |
|
||||||
SPS: []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, |
|
||||||
}, |
|
||||||
PPS: []byte{0x01, 0x02, 0x03, 0x04}, |
|
||||||
}, |
|
||||||
}, |
|
||||||
}, |
|
||||||
} |
|
||||||
|
|
||||||
byts, err := i.Marshal() |
|
||||||
require.NoError(t, err) |
|
||||||
|
|
||||||
_, err = w.Write(byts) |
|
||||||
require.NoError(t, err) |
|
||||||
} |
|
||||||
|
|
||||||
func mp4Segment(t *testing.T, w io.Writer) { |
|
||||||
payload, _ := h264.AVCCMarshal([][]byte{ |
|
||||||
{7, 1, 2, 3}, // SPS
|
|
||||||
{8}, // PPS
|
|
||||||
{5}, // IDR
|
|
||||||
}) |
|
||||||
|
|
||||||
p := &fmp4.Part{ |
|
||||||
Tracks: []*fmp4.PartTrack{ |
|
||||||
{ |
|
||||||
ID: 1, |
|
||||||
IsVideo: true, |
|
||||||
Samples: []*fmp4.PartSample{{ |
|
||||||
Duration: 90000 / 30, |
|
||||||
PTSOffset: 90000 * 2, |
|
||||||
Payload: payload, |
|
||||||
}}, |
|
||||||
}, |
|
||||||
}, |
|
||||||
} |
|
||||||
|
|
||||||
byts, err := p.Marshal() |
|
||||||
require.NoError(t, err) |
|
||||||
|
|
||||||
_, err = w.Write(byts) |
|
||||||
require.NoError(t, err) |
|
||||||
} |
|
||||||
|
|
||||||
type testHLSServer struct { |
|
||||||
s *http.Server |
|
||||||
} |
|
||||||
|
|
||||||
func newTestHLSServer(router http.Handler, isTLS bool) (*testHLSServer, error) { |
|
||||||
ln, err := net.Listen("tcp", "localhost:5780") |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
|
|
||||||
s := &testHLSServer{ |
|
||||||
s: &http.Server{Handler: router}, |
|
||||||
} |
|
||||||
|
|
||||||
if isTLS { |
|
||||||
go func() { |
|
||||||
serverCertFpath, err := writeTempFile(serverCert) |
|
||||||
if err != nil { |
|
||||||
panic(err) |
|
||||||
} |
|
||||||
defer os.Remove(serverCertFpath) |
|
||||||
|
|
||||||
serverKeyFpath, err := writeTempFile(serverKey) |
|
||||||
if err != nil { |
|
||||||
panic(err) |
|
||||||
} |
|
||||||
defer os.Remove(serverKeyFpath) |
|
||||||
|
|
||||||
s.s.ServeTLS(ln, serverCertFpath, serverKeyFpath) |
|
||||||
}() |
|
||||||
} else { |
|
||||||
go s.s.Serve(ln) |
|
||||||
} |
|
||||||
|
|
||||||
return s, nil |
|
||||||
} |
|
||||||
|
|
||||||
func (s *testHLSServer) close() { |
|
||||||
s.s.Shutdown(context.Background()) |
|
||||||
} |
|
||||||
|
|
||||||
func TestClientMPEGTS(t *testing.T) { |
|
||||||
for _, ca := range []string{ |
|
||||||
"plain", |
|
||||||
"tls", |
|
||||||
"segment with query", |
|
||||||
} { |
|
||||||
t.Run(ca, func(t *testing.T) { |
|
||||||
gin.SetMode(gin.ReleaseMode) |
|
||||||
router := gin.New() |
|
||||||
|
|
||||||
segment := "segment.ts" |
|
||||||
if ca == "segment with query" { |
|
||||||
segment = "segment.ts?key=val" |
|
||||||
} |
|
||||||
sent := false |
|
||||||
|
|
||||||
router.GET("/stream.m3u8", func(ctx *gin.Context) { |
|
||||||
if sent { |
|
||||||
return |
|
||||||
} |
|
||||||
sent = true |
|
||||||
|
|
||||||
ctx.Writer.Header().Set("Content-Type", `application/x-mpegURL`) |
|
||||||
io.Copy(ctx.Writer, bytes.NewReader([]byte(`#EXTM3U |
|
||||||
#EXT-X-VERSION:3 |
|
||||||
#EXT-X-ALLOW-CACHE:NO |
|
||||||
#EXT-X-TARGETDURATION:2 |
|
||||||
#EXT-X-MEDIA-SEQUENCE:0 |
|
||||||
#EXTINF:2, |
|
||||||
`+segment+` |
|
||||||
#EXT-X-ENDLIST |
|
||||||
`))) |
|
||||||
}) |
|
||||||
|
|
||||||
router.GET("/segment.ts", func(ctx *gin.Context) { |
|
||||||
if ca == "segment with query" { |
|
||||||
require.Equal(t, "val", ctx.Query("key")) |
|
||||||
} |
|
||||||
ctx.Writer.Header().Set("Content-Type", `video/MP2T`) |
|
||||||
mpegtsSegment(ctx.Writer) |
|
||||||
}) |
|
||||||
|
|
||||||
s, err := newTestHLSServer(router, ca == "tls") |
|
||||||
require.NoError(t, err) |
|
||||||
defer s.close() |
|
||||||
|
|
||||||
packetRecv := make(chan struct{}) |
|
||||||
|
|
||||||
prefix := "http" |
|
||||||
if ca == "tls" { |
|
||||||
prefix = "https" |
|
||||||
} |
|
||||||
|
|
||||||
c, err := NewClient( |
|
||||||
prefix+"://localhost:5780/stream.m3u8", |
|
||||||
"33949E05FFFB5FF3E8AA16F8213A6251B4D9363804BA53233C4DA9A46D6F2739", |
|
||||||
testLogger{}, |
|
||||||
) |
|
||||||
require.NoError(t, err) |
|
||||||
|
|
||||||
onH264 := func(pts time.Duration, unit interface{}) { |
|
||||||
require.Equal(t, 2*time.Second, pts) |
|
||||||
require.Equal(t, [][]byte{ |
|
||||||
{7, 1, 2, 3}, |
|
||||||
{8}, |
|
||||||
{5}, |
|
||||||
}, unit) |
|
||||||
close(packetRecv) |
|
||||||
} |
|
||||||
|
|
||||||
c.OnTracks(func(tracks []format.Format) error { |
|
||||||
require.Equal(t, 1, len(tracks)) |
|
||||||
require.Equal(t, &format.H264{ |
|
||||||
PayloadTyp: 96, |
|
||||||
PacketizationMode: 1, |
|
||||||
}, tracks[0]) |
|
||||||
c.OnData(tracks[0], onH264) |
|
||||||
return nil |
|
||||||
}) |
|
||||||
|
|
||||||
c.Start() |
|
||||||
|
|
||||||
<-packetRecv |
|
||||||
|
|
||||||
c.Close() |
|
||||||
<-c.Wait() |
|
||||||
}) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
func TestClientFMP4(t *testing.T) { |
|
||||||
gin.SetMode(gin.ReleaseMode) |
|
||||||
router := gin.New() |
|
||||||
|
|
||||||
router.GET("/stream.m3u8", func(ctx *gin.Context) { |
|
||||||
ctx.Writer.Header().Set("Content-Type", `application/x-mpegURL`) |
|
||||||
io.Copy(ctx.Writer, bytes.NewReader([]byte(`#EXTM3U |
|
||||||
#EXT-X-VERSION:7 |
|
||||||
#EXT-X-MEDIA-SEQUENCE:20 |
|
||||||
#EXT-X-INDEPENDENT-SEGMENTS |
|
||||||
#EXT-X-MAP:URI="init.mp4" |
|
||||||
#EXTINF:2, |
|
||||||
segment.mp4 |
|
||||||
#EXT-X-ENDLIST |
|
||||||
`))) |
|
||||||
}) |
|
||||||
|
|
||||||
router.GET("/init.mp4", func(ctx *gin.Context) { |
|
||||||
ctx.Writer.Header().Set("Content-Type", `video/mp4`) |
|
||||||
mp4Init(t, ctx.Writer) |
|
||||||
}) |
|
||||||
|
|
||||||
router.GET("/segment.mp4", func(ctx *gin.Context) { |
|
||||||
ctx.Writer.Header().Set("Content-Type", `video/mp4`) |
|
||||||
mp4Segment(t, ctx.Writer) |
|
||||||
}) |
|
||||||
|
|
||||||
s, err := newTestHLSServer(router, false) |
|
||||||
require.NoError(t, err) |
|
||||||
defer s.close() |
|
||||||
|
|
||||||
packetRecv := make(chan struct{}) |
|
||||||
|
|
||||||
onH264 := func(pts time.Duration, unit interface{}) { |
|
||||||
require.Equal(t, 2*time.Second, pts) |
|
||||||
require.Equal(t, [][]byte{ |
|
||||||
{7, 1, 2, 3}, |
|
||||||
{8}, |
|
||||||
{5}, |
|
||||||
}, unit) |
|
||||||
close(packetRecv) |
|
||||||
} |
|
||||||
|
|
||||||
c, err := NewClient( |
|
||||||
"http://localhost:5780/stream.m3u8", |
|
||||||
"", |
|
||||||
testLogger{}, |
|
||||||
) |
|
||||||
require.NoError(t, err) |
|
||||||
|
|
||||||
c.OnTracks(func(tracks []format.Format) error { |
|
||||||
require.Equal(t, 1, len(tracks)) |
|
||||||
_, ok := tracks[0].(*format.H264) |
|
||||||
require.Equal(t, true, ok) |
|
||||||
c.OnData(tracks[0], onH264) |
|
||||||
return nil |
|
||||||
}) |
|
||||||
|
|
||||||
c.Start() |
|
||||||
|
|
||||||
<-packetRecv |
|
||||||
|
|
||||||
c.Close() |
|
||||||
<-c.Wait() |
|
||||||
} |
|
||||||
|
|
||||||
func TestClientInvalidSequenceID(t *testing.T) { |
|
||||||
router := gin.New() |
|
||||||
firstPlaylist := true |
|
||||||
|
|
||||||
router.GET("/stream.m3u8", func(ctx *gin.Context) { |
|
||||||
ctx.Writer.Header().Set("Content-Type", `application/x-mpegURL`) |
|
||||||
|
|
||||||
if firstPlaylist { |
|
||||||
firstPlaylist = false |
|
||||||
io.Copy(ctx.Writer, bytes.NewReader([]byte( |
|
||||||
`#EXTM3U |
|
||||||
#EXT-X-VERSION:3 |
|
||||||
#EXT-X-ALLOW-CACHE:NO |
|
||||||
#EXT-X-TARGETDURATION:2 |
|
||||||
#EXT-X-MEDIA-SEQUENCE:2 |
|
||||||
#EXTINF:2, |
|
||||||
segment1.ts |
|
||||||
#EXTINF:2, |
|
||||||
segment1.ts |
|
||||||
#EXTINF:2, |
|
||||||
segment1.ts |
|
||||||
`))) |
|
||||||
} else { |
|
||||||
io.Copy(ctx.Writer, bytes.NewReader([]byte( |
|
||||||
`#EXTM3U |
|
||||||
#EXT-X-VERSION:3 |
|
||||||
#EXT-X-ALLOW-CACHE:NO |
|
||||||
#EXT-X-TARGETDURATION:2 |
|
||||||
#EXT-X-MEDIA-SEQUENCE:4 |
|
||||||
#EXTINF:2, |
|
||||||
segment1.ts |
|
||||||
#EXTINF:2, |
|
||||||
segment1.ts |
|
||||||
#EXTINF:2, |
|
||||||
segment1.ts |
|
||||||
`))) |
|
||||||
} |
|
||||||
}) |
|
||||||
|
|
||||||
router.GET("/segment1.ts", func(ctx *gin.Context) { |
|
||||||
ctx.Writer.Header().Set("Content-Type", `video/MP2T`) |
|
||||||
mpegtsSegment(ctx.Writer) |
|
||||||
}) |
|
||||||
|
|
||||||
s, err := newTestHLSServer(router, false) |
|
||||||
require.NoError(t, err) |
|
||||||
defer s.close() |
|
||||||
|
|
||||||
c, err := NewClient( |
|
||||||
"http://localhost:5780/stream.m3u8", |
|
||||||
"", |
|
||||||
testLogger{}, |
|
||||||
) |
|
||||||
require.NoError(t, err) |
|
||||||
|
|
||||||
c.OnTracks(func(tracks []format.Format) error { |
|
||||||
return nil |
|
||||||
}) |
|
||||||
|
|
||||||
c.Start() |
|
||||||
|
|
||||||
err = <-c.Wait() |
|
||||||
require.EqualError(t, err, "following segment not found or not ready yet") |
|
||||||
|
|
||||||
c.Close() |
|
||||||
} |
|
||||||
@ -1,59 +0,0 @@ |
|||||||
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 |
|
||||||
} |
|
||||||
@ -1,46 +0,0 @@ |
|||||||
package hls |
|
||||||
|
|
||||||
import ( |
|
||||||
"context" |
|
||||||
"fmt" |
|
||||||
"sync" |
|
||||||
"time" |
|
||||||
|
|
||||||
"github.com/aler9/rtsp-simple-server/internal/hls/mpegtstimedec" |
|
||||||
) |
|
||||||
|
|
||||||
type clientTimeSyncMPEGTS struct { |
|
||||||
startRTC time.Time |
|
||||||
td *mpegtstimedec.Decoder |
|
||||||
mutex sync.Mutex |
|
||||||
} |
|
||||||
|
|
||||||
func newClientTimeSyncMPEGTS(startDTS int64) *clientTimeSyncMPEGTS { |
|
||||||
return &clientTimeSyncMPEGTS{ |
|
||||||
startRTC: time.Now(), |
|
||||||
td: mpegtstimedec.New(startDTS), |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
func (ts *clientTimeSyncMPEGTS) convertAndSync(ctx context.Context, rawDTS int64, rawPTS int64) (time.Duration, error) { |
|
||||||
ts.mutex.Lock() |
|
||||||
dts := ts.td.Decode(rawDTS) |
|
||||||
pts := ts.td.Decode(rawPTS) |
|
||||||
ts.mutex.Unlock() |
|
||||||
|
|
||||||
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,143 +0,0 @@ |
|||||||
package hls |
|
||||||
|
|
||||||
import ( |
|
||||||
"encoding/hex" |
|
||||||
"fmt" |
|
||||||
"strconv" |
|
||||||
"strings" |
|
||||||
|
|
||||||
"github.com/aler9/gortsplib/v2/pkg/codecs/h265" |
|
||||||
"github.com/aler9/gortsplib/v2/pkg/format" |
|
||||||
) |
|
||||||
|
|
||||||
func encodeProfileSpace(v uint8) string { |
|
||||||
switch v { |
|
||||||
case 1: |
|
||||||
return "A" |
|
||||||
case 2: |
|
||||||
return "B" |
|
||||||
case 3: |
|
||||||
return "C" |
|
||||||
} |
|
||||||
return "" |
|
||||||
} |
|
||||||
|
|
||||||
func encodeCompatibilityFlag(v [32]bool) string { |
|
||||||
var o uint32 |
|
||||||
for i, b := range v { |
|
||||||
if b { |
|
||||||
o |= 1 << i |
|
||||||
} |
|
||||||
} |
|
||||||
return fmt.Sprintf("%x", o) |
|
||||||
} |
|
||||||
|
|
||||||
func encodeGeneralTierFlag(v uint8) string { |
|
||||||
if v > 0 { |
|
||||||
return "H" |
|
||||||
} |
|
||||||
return "L" |
|
||||||
} |
|
||||||
|
|
||||||
func encodeGeneralConstraintIndicatorFlags(v *h265.SPS_ProfileTierLevel) string { |
|
||||||
var ret []string |
|
||||||
|
|
||||||
var o1 uint8 |
|
||||||
if v.GeneralProgressiveSourceFlag { |
|
||||||
o1 |= 1 << 7 |
|
||||||
} |
|
||||||
if v.GeneralInterlacedSourceFlag { |
|
||||||
o1 |= 1 << 6 |
|
||||||
} |
|
||||||
if v.GeneralNonPackedConstraintFlag { |
|
||||||
o1 |= 1 << 5 |
|
||||||
} |
|
||||||
if v.GeneralFrameOnlyConstraintFlag { |
|
||||||
o1 |= 1 << 4 |
|
||||||
} |
|
||||||
if v.GeneralMax12bitConstraintFlag { |
|
||||||
o1 |= 1 << 3 |
|
||||||
} |
|
||||||
if v.GeneralMax10bitConstraintFlag { |
|
||||||
o1 |= 1 << 2 |
|
||||||
} |
|
||||||
if v.GeneralMax8bitConstraintFlag { |
|
||||||
o1 |= 1 << 1 |
|
||||||
} |
|
||||||
if v.GeneralMax422ChromeConstraintFlag { |
|
||||||
o1 |= 1 << 0 |
|
||||||
} |
|
||||||
|
|
||||||
ret = append(ret, fmt.Sprintf("%x", o1)) |
|
||||||
|
|
||||||
var o2 uint8 |
|
||||||
if v.GeneralMax420ChromaConstraintFlag { |
|
||||||
o2 |= 1 << 7 |
|
||||||
} |
|
||||||
if v.GeneralMaxMonochromeConstraintFlag { |
|
||||||
o2 |= 1 << 6 |
|
||||||
} |
|
||||||
if v.GeneralIntraConstraintFlag { |
|
||||||
o2 |= 1 << 5 |
|
||||||
} |
|
||||||
if v.GeneralOnePictureOnlyConstraintFlag { |
|
||||||
o2 |= 1 << 4 |
|
||||||
} |
|
||||||
if v.GeneralLowerBitRateConstraintFlag { |
|
||||||
o2 |= 1 << 3 |
|
||||||
} |
|
||||||
if v.GeneralMax14BitConstraintFlag { |
|
||||||
o2 |= 1 << 2 |
|
||||||
} |
|
||||||
|
|
||||||
if o2 != 0 { |
|
||||||
ret = append(ret, fmt.Sprintf("%x", o2)) |
|
||||||
} |
|
||||||
|
|
||||||
return strings.Join(ret, ".") |
|
||||||
} |
|
||||||
|
|
||||||
func codecParametersGenerate(track format.Format) string { |
|
||||||
switch ttrack := track.(type) { |
|
||||||
case *format.H264: |
|
||||||
sps := ttrack.SafeSPS() |
|
||||||
if len(sps) >= 4 { |
|
||||||
return "avc1." + hex.EncodeToString(sps[1:4]) |
|
||||||
} |
|
||||||
|
|
||||||
case *format.H265: |
|
||||||
var sps h265.SPS |
|
||||||
err := sps.Unmarshal(ttrack.SafeSPS()) |
|
||||||
if err == nil { |
|
||||||
return "hvc1." + |
|
||||||
encodeProfileSpace(sps.ProfileTierLevel.GeneralProfileSpace) + |
|
||||||
strconv.FormatInt(int64(sps.ProfileTierLevel.GeneralProfileIdc), 10) + "." + |
|
||||||
encodeCompatibilityFlag(sps.ProfileTierLevel.GeneralProfileCompatibilityFlag) + "." + |
|
||||||
encodeGeneralTierFlag(sps.ProfileTierLevel.GeneralTierFlag) + |
|
||||||
strconv.FormatInt(int64(sps.ProfileTierLevel.GeneralLevelIdc), 10) + "." + |
|
||||||
encodeGeneralConstraintIndicatorFlags(&sps.ProfileTierLevel) |
|
||||||
} |
|
||||||
|
|
||||||
case *format.MPEG4Audio: |
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/Media/Formats/codecs_parameter
|
|
||||||
return "mp4a.40." + strconv.FormatInt(int64(ttrack.Config.Type), 10) |
|
||||||
|
|
||||||
case *format.Opus: |
|
||||||
return "opus" |
|
||||||
} |
|
||||||
|
|
||||||
return "" |
|
||||||
} |
|
||||||
|
|
||||||
func codecParametersAreSupported(codecs string) bool { |
|
||||||
for _, codec := range strings.Split(codecs, ",") { |
|
||||||
if !strings.HasPrefix(codec, "avc1.") && |
|
||||||
!strings.HasPrefix(codec, "hvc1.") && |
|
||||||
!strings.HasPrefix(codec, "hev1.") && |
|
||||||
!strings.HasPrefix(codec, "mp4a.") && |
|
||||||
codec != "opus" { |
|
||||||
return false |
|
||||||
} |
|
||||||
} |
|
||||||
return true |
|
||||||
} |
|
||||||
@ -1,53 +0,0 @@ |
|||||||
//nolint:gochecknoinits,revive,gocritic
|
|
||||||
package fmp4 |
|
||||||
|
|
||||||
import ( |
|
||||||
gomp4 "github.com/abema/go-mp4" |
|
||||||
) |
|
||||||
|
|
||||||
func BoxTypeOpus() gomp4.BoxType { return gomp4.StrToBoxType("Opus") } |
|
||||||
|
|
||||||
func init() { |
|
||||||
gomp4.AddAnyTypeBoxDef(&gomp4.AudioSampleEntry{}, BoxTypeOpus()) |
|
||||||
} |
|
||||||
|
|
||||||
func BoxTypeDOps() gomp4.BoxType { return gomp4.StrToBoxType("dOps") } |
|
||||||
|
|
||||||
func init() { |
|
||||||
gomp4.AddBoxDef(&DOps{}) |
|
||||||
} |
|
||||||
|
|
||||||
type DOpsChannelMappingTable struct{} |
|
||||||
|
|
||||||
type DOps struct { |
|
||||||
gomp4.Box |
|
||||||
Version uint8 `mp4:"0,size=8"` |
|
||||||
OutputChannelCount uint8 `mp4:"1,size=8"` |
|
||||||
PreSkip uint16 `mp4:"2,size=16"` |
|
||||||
InputSampleRate uint32 `mp4:"3,size=32"` |
|
||||||
OutputGain int16 `mp4:"4,size=16"` |
|
||||||
ChannelMappingFamily uint8 `mp4:"5,size=8"` |
|
||||||
StreamCount uint8 `mp4:"6,opt=dynamic,size=8"` |
|
||||||
CoupledCount uint8 `mp4:"7,opt=dynamic,size=8"` |
|
||||||
ChannelMapping []uint8 `mp4:"8,opt=dynamic,size=8,len=dynamic"` |
|
||||||
} |
|
||||||
|
|
||||||
func (DOps) GetType() gomp4.BoxType { |
|
||||||
return BoxTypeDOps() |
|
||||||
} |
|
||||||
|
|
||||||
func (dops DOps) IsOptFieldEnabled(name string, ctx gomp4.Context) bool { |
|
||||||
switch name { |
|
||||||
case "StreamCount", "CoupledCount", "ChannelMapping": |
|
||||||
return dops.ChannelMappingFamily != 0 |
|
||||||
} |
|
||||||
return false |
|
||||||
} |
|
||||||
|
|
||||||
func (ops DOps) GetFieldLength(name string, ctx gomp4.Context) uint { |
|
||||||
switch name { |
|
||||||
case "ChannelMapping": |
|
||||||
return uint(ops.OutputChannelCount) |
|
||||||
} |
|
||||||
return 0 |
|
||||||
} |
|
||||||
@ -1,2 +0,0 @@ |
|||||||
// Package fmp4 contains a fMP4 reader and writer.
|
|
||||||
package fmp4 |
|
||||||
@ -1,363 +0,0 @@ |
|||||||
package fmp4 |
|
||||||
|
|
||||||
import ( |
|
||||||
"bytes" |
|
||||||
"fmt" |
|
||||||
|
|
||||||
gomp4 "github.com/abema/go-mp4" |
|
||||||
"github.com/aler9/gortsplib/v2/pkg/codecs/h265" |
|
||||||
"github.com/aler9/gortsplib/v2/pkg/codecs/mpeg4audio" |
|
||||||
"github.com/aler9/gortsplib/v2/pkg/format" |
|
||||||
) |
|
||||||
|
|
||||||
// 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 |
|
||||||
waitingHvcC |
|
||||||
waitingEsds |
|
||||||
waitingDOps |
|
||||||
) |
|
||||||
|
|
||||||
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("unexpected box 'trak'") |
|
||||||
} |
|
||||||
|
|
||||||
curTrack = &InitTrack{} |
|
||||||
i.Tracks = append(i.Tracks, curTrack) |
|
||||||
state = waitingTkhd |
|
||||||
|
|
||||||
case "tkhd": |
|
||||||
if state != waitingTkhd { |
|
||||||
return nil, fmt.Errorf("unexpected box 'tkhd'") |
|
||||||
} |
|
||||||
|
|
||||||
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("unexpected box 'mdhd'") |
|
||||||
} |
|
||||||
|
|
||||||
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("unexpected box 'avc1'") |
|
||||||
} |
|
||||||
state = waitingAvcC |
|
||||||
|
|
||||||
case "avcC": |
|
||||||
if state != waitingAvcC { |
|
||||||
return nil, fmt.Errorf("unexpected box 'avcC'") |
|
||||||
} |
|
||||||
|
|
||||||
box, _, err := h.ReadPayload() |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
avcc := box.(*gomp4.AVCDecoderConfiguration) |
|
||||||
|
|
||||||
if len(avcc.SequenceParameterSets) > 1 { |
|
||||||
return nil, fmt.Errorf("multiple SPS are not supported") |
|
||||||
} |
|
||||||
|
|
||||||
var sps []byte |
|
||||||
if len(avcc.SequenceParameterSets) == 1 { |
|
||||||
sps = avcc.SequenceParameterSets[0].NALUnit |
|
||||||
} |
|
||||||
|
|
||||||
if len(avcc.PictureParameterSets) > 1 { |
|
||||||
return nil, fmt.Errorf("multiple PPS are not supported") |
|
||||||
} |
|
||||||
|
|
||||||
var pps []byte |
|
||||||
if len(avcc.PictureParameterSets) == 1 { |
|
||||||
pps = avcc.PictureParameterSets[0].NALUnit |
|
||||||
} |
|
||||||
|
|
||||||
curTrack.Format = &format.H264{ |
|
||||||
PayloadTyp: 96, |
|
||||||
SPS: sps, |
|
||||||
PPS: pps, |
|
||||||
PacketizationMode: 1, |
|
||||||
} |
|
||||||
state = waitingTrak |
|
||||||
|
|
||||||
case "hev1", "hvc1": |
|
||||||
if state != waitingCodec { |
|
||||||
return nil, fmt.Errorf("unexpected box 'hev1'") |
|
||||||
} |
|
||||||
state = waitingHvcC |
|
||||||
|
|
||||||
case "hvcC": |
|
||||||
if state != waitingHvcC { |
|
||||||
return nil, fmt.Errorf("unexpected box 'hvcC'") |
|
||||||
} |
|
||||||
|
|
||||||
box, _, err := h.ReadPayload() |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
hvcc := box.(*gomp4.HvcC) |
|
||||||
|
|
||||||
var vps []byte |
|
||||||
var sps []byte |
|
||||||
var pps []byte |
|
||||||
|
|
||||||
for _, arr := range hvcc.NaluArrays { |
|
||||||
switch h265.NALUType(arr.NaluType) { |
|
||||||
case h265.NALUType_VPS_NUT, h265.NALUType_SPS_NUT, h265.NALUType_PPS_NUT: |
|
||||||
if arr.NumNalus != 1 { |
|
||||||
return nil, fmt.Errorf("multiple VPS/SPS/PPS are not supported") |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
switch h265.NALUType(arr.NaluType) { |
|
||||||
case h265.NALUType_VPS_NUT: |
|
||||||
vps = arr.Nalus[0].NALUnit |
|
||||||
|
|
||||||
case h265.NALUType_SPS_NUT: |
|
||||||
sps = arr.Nalus[0].NALUnit |
|
||||||
|
|
||||||
case h265.NALUType_PPS_NUT: |
|
||||||
pps = arr.Nalus[0].NALUnit |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
if vps == nil { |
|
||||||
return nil, fmt.Errorf("VPS not provided") |
|
||||||
} |
|
||||||
|
|
||||||
if sps == nil { |
|
||||||
return nil, fmt.Errorf("SPS not provided") |
|
||||||
} |
|
||||||
|
|
||||||
if pps == nil { |
|
||||||
return nil, fmt.Errorf("PPS not provided") |
|
||||||
} |
|
||||||
|
|
||||||
curTrack.Format = &format.H265{ |
|
||||||
PayloadTyp: 96, |
|
||||||
VPS: vps, |
|
||||||
SPS: sps, |
|
||||||
PPS: pps, |
|
||||||
} |
|
||||||
state = waitingTrak |
|
||||||
|
|
||||||
case "mp4a": |
|
||||||
if state != waitingCodec { |
|
||||||
return nil, fmt.Errorf("unexpected box 'mp4a'") |
|
||||||
} |
|
||||||
state = waitingEsds |
|
||||||
|
|
||||||
case "esds": |
|
||||||
if state != waitingEsds { |
|
||||||
return nil, fmt.Errorf("unexpected box 'esds'") |
|
||||||
} |
|
||||||
|
|
||||||
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.Format = &format.MPEG4Audio{ |
|
||||||
PayloadTyp: 96, |
|
||||||
Config: &c, |
|
||||||
SizeLength: 13, |
|
||||||
IndexLength: 3, |
|
||||||
IndexDeltaLength: 3, |
|
||||||
} |
|
||||||
state = waitingTrak |
|
||||||
|
|
||||||
case "Opus": |
|
||||||
if state != waitingCodec { |
|
||||||
return nil, fmt.Errorf("unexpected box 'Opus'") |
|
||||||
} |
|
||||||
state = waitingDOps |
|
||||||
|
|
||||||
case "dOps": |
|
||||||
if state != waitingDOps { |
|
||||||
return nil, fmt.Errorf("unexpected box 'dOps'") |
|
||||||
} |
|
||||||
|
|
||||||
box, _, err := h.ReadPayload() |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
dops := box.(*DOps) |
|
||||||
|
|
||||||
curTrack.Format = &format.Opus{ |
|
||||||
PayloadTyp: 96, |
|
||||||
SampleRate: int(dops.InputSampleRate), |
|
||||||
ChannelCount: int(dops.OutputChannelCount), |
|
||||||
} |
|
||||||
state = waitingTrak |
|
||||||
|
|
||||||
case "ac-3": // ac-3, not supported yet
|
|
||||||
i.Tracks = i.Tracks[:len(i.Tracks)-1] |
|
||||||
state = waitingTrak |
|
||||||
return nil, nil |
|
||||||
|
|
||||||
case "ec-3": // ec-3, not supported yet
|
|
||||||
i.Tracks = i.Tracks[:len(i.Tracks)-1] |
|
||||||
state = waitingTrak |
|
||||||
return nil, nil |
|
||||||
|
|
||||||
case "c608", "c708": // closed captions, not supported yet
|
|
||||||
i.Tracks = i.Tracks[:len(i.Tracks)-1] |
|
||||||
state = waitingTrak |
|
||||||
return nil, nil |
|
||||||
|
|
||||||
case "chrm", "nmhd": |
|
||||||
return nil, nil |
|
||||||
} |
|
||||||
|
|
||||||
return h.Expand() |
|
||||||
}) |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
if state != waitingTrak { |
|
||||||
return fmt.Errorf("parse error") |
|
||||||
} |
|
||||||
|
|
||||||
if len(i.Tracks) == 0 { |
|
||||||
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 |
|
||||||
} |
|
||||||
File diff suppressed because it is too large
Load Diff
@ -1,526 +0,0 @@ |
|||||||
package fmp4 |
|
||||||
|
|
||||||
import ( |
|
||||||
gomp4 "github.com/abema/go-mp4" |
|
||||||
"github.com/aler9/gortsplib/v2/pkg/codecs/h264" |
|
||||||
"github.com/aler9/gortsplib/v2/pkg/codecs/h265" |
|
||||||
"github.com/aler9/gortsplib/v2/pkg/format" |
|
||||||
) |
|
||||||
|
|
||||||
// InitTrack is a track of Init.
|
|
||||||
type InitTrack struct { |
|
||||||
ID int |
|
||||||
TimeScale uint32 |
|
||||||
Format format.Format |
|
||||||
} |
|
||||||
|
|
||||||
func (track *InitTrack) marshal(w *mp4Writer) error { |
|
||||||
/* |
|
||||||
trak |
|
||||||
- tkhd |
|
||||||
- mdia |
|
||||||
- mdhd |
|
||||||
- hdlr |
|
||||||
- minf |
|
||||||
- vmhd (video) |
|
||||||
- smhd (audio) |
|
||||||
- dinf |
|
||||||
- dref |
|
||||||
- url |
|
||||||
- stbl |
|
||||||
- stsd |
|
||||||
- avc1 (h264) |
|
||||||
- avcC |
|
||||||
- btrt |
|
||||||
- hev1 (h265) |
|
||||||
- hvcC |
|
||||||
- mp4a (mpeg4audio) |
|
||||||
- esds |
|
||||||
- btrt |
|
||||||
- Opus (opus) |
|
||||||
- dOps |
|
||||||
- btrt |
|
||||||
- stts |
|
||||||
- stsc |
|
||||||
- stsz |
|
||||||
- stco |
|
||||||
*/ |
|
||||||
|
|
||||||
_, err := w.writeBoxStart(&gomp4.Trak{}) // <trak>
|
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
var h264SPS []byte |
|
||||||
var h264PPS []byte |
|
||||||
var h264SPSP h264.SPS |
|
||||||
|
|
||||||
var h265VPS []byte |
|
||||||
var h265SPS []byte |
|
||||||
var h265PPS []byte |
|
||||||
var h265SPSP h265.SPS |
|
||||||
|
|
||||||
var width int |
|
||||||
var height int |
|
||||||
|
|
||||||
switch ttrack := track.Format.(type) { |
|
||||||
case *format.H264: |
|
||||||
h264SPS = ttrack.SafeSPS() |
|
||||||
h264PPS = ttrack.SafePPS() |
|
||||||
|
|
||||||
err = h264SPSP.Unmarshal(h264SPS) |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
width = h264SPSP.Width() |
|
||||||
height = h264SPSP.Height() |
|
||||||
|
|
||||||
case *format.H265: |
|
||||||
h265VPS = ttrack.SafeVPS() |
|
||||||
h265SPS = ttrack.SafeSPS() |
|
||||||
h265PPS = ttrack.SafePPS() |
|
||||||
|
|
||||||
err = h265SPSP.Unmarshal(h265SPS) |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
width = h265SPSP.Width() |
|
||||||
height = h265SPSP.Height() |
|
||||||
} |
|
||||||
|
|
||||||
switch track.Format.(type) { |
|
||||||
case *format.H264, *format.H265: |
|
||||||
_, 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{0x10000, 0, 0, 0, 0x10000, 0, 0, 0, 0x40000000}, |
|
||||||
}) |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
case *format.MPEG4Audio, *format.Opus: |
|
||||||
_, 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{0x10000, 0, 0, 0, 0x10000, 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.Format.(type) { |
|
||||||
case *format.H264, *format.H265: |
|
||||||
_, err = w.WriteBox(&gomp4.Hdlr{ // <hdlr/>
|
|
||||||
HandlerType: [4]byte{'v', 'i', 'd', 'e'}, |
|
||||||
Name: "VideoHandler", |
|
||||||
}) |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
case *format.MPEG4Audio, *format.Opus: |
|
||||||
_, 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.Format.(type) { |
|
||||||
case *format.H264, *format.H265: |
|
||||||
_, err = w.WriteBox(&gomp4.Vmhd{ // <vmhd/>
|
|
||||||
FullBox: gomp4.FullBox{ |
|
||||||
Flags: [3]byte{0, 0, 1}, |
|
||||||
}, |
|
||||||
}) |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
case *format.MPEG4Audio, *format.Opus: |
|
||||||
_, 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.Format.(type) { |
|
||||||
case *format.H264: |
|
||||||
_, 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: h264SPSP.ProfileIdc, |
|
||||||
ProfileCompatibility: h264SPS[2], |
|
||||||
Level: h264SPSP.LevelIdc, |
|
||||||
LengthSizeMinusOne: 3, |
|
||||||
NumOfSequenceParameterSets: 1, |
|
||||||
SequenceParameterSets: []gomp4.AVCParameterSet{ |
|
||||||
{ |
|
||||||
Length: uint16(len(h264SPS)), |
|
||||||
NALUnit: h264SPS, |
|
||||||
}, |
|
||||||
}, |
|
||||||
NumOfPictureParameterSets: 1, |
|
||||||
PictureParameterSets: []gomp4.AVCParameterSet{ |
|
||||||
{ |
|
||||||
Length: uint16(len(h264PPS)), |
|
||||||
NALUnit: h264PPS, |
|
||||||
}, |
|
||||||
}, |
|
||||||
}) |
|
||||||
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 *format.H265: |
|
||||||
_, err = w.writeBoxStart(&gomp4.VisualSampleEntry{ // <hev1>
|
|
||||||
SampleEntry: gomp4.SampleEntry{ |
|
||||||
AnyTypeBox: gomp4.AnyTypeBox{ |
|
||||||
Type: gomp4.BoxTypeHev1(), |
|
||||||
}, |
|
||||||
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.HvcC{ // <hvcC/>
|
|
||||||
ConfigurationVersion: 1, |
|
||||||
GeneralProfileIdc: h265SPSP.ProfileTierLevel.GeneralProfileIdc, |
|
||||||
GeneralProfileCompatibility: h265SPSP.ProfileTierLevel.GeneralProfileCompatibilityFlag, |
|
||||||
GeneralConstraintIndicator: [6]uint8{ |
|
||||||
h265SPS[7], h265SPS[8], h265SPS[9], |
|
||||||
h265SPS[10], h265SPS[11], h265SPS[12], |
|
||||||
}, |
|
||||||
GeneralLevelIdc: h265SPSP.ProfileTierLevel.GeneralLevelIdc, |
|
||||||
// MinSpatialSegmentationIdc
|
|
||||||
// ParallelismType
|
|
||||||
ChromaFormatIdc: uint8(h265SPSP.ChromaFormatIdc), |
|
||||||
BitDepthLumaMinus8: uint8(h265SPSP.BitDepthLumaMinus8), |
|
||||||
BitDepthChromaMinus8: uint8(h265SPSP.BitDepthChromaMinus8), |
|
||||||
// AvgFrameRate
|
|
||||||
// ConstantFrameRate
|
|
||||||
NumTemporalLayers: 1, |
|
||||||
// TemporalIdNested
|
|
||||||
LengthSizeMinusOne: 3, |
|
||||||
NumOfNaluArrays: 3, |
|
||||||
NaluArrays: []gomp4.HEVCNaluArray{ |
|
||||||
{ |
|
||||||
NaluType: byte(h265.NALUType_VPS_NUT), |
|
||||||
NumNalus: 1, |
|
||||||
Nalus: []gomp4.HEVCNalu{{ |
|
||||||
Length: uint16(len(h265VPS)), |
|
||||||
NALUnit: h265VPS, |
|
||||||
}}, |
|
||||||
}, |
|
||||||
{ |
|
||||||
NaluType: byte(h265.NALUType_SPS_NUT), |
|
||||||
NumNalus: 1, |
|
||||||
Nalus: []gomp4.HEVCNalu{{ |
|
||||||
Length: uint16(len(h265SPS)), |
|
||||||
NALUnit: h265SPS, |
|
||||||
}}, |
|
||||||
}, |
|
||||||
{ |
|
||||||
NaluType: byte(h265.NALUType_PPS_NUT), |
|
||||||
NumNalus: 1, |
|
||||||
Nalus: []gomp4.HEVCNalu{{ |
|
||||||
Length: uint16(len(h265PPS)), |
|
||||||
NALUnit: h265PPS, |
|
||||||
}}, |
|
||||||
}, |
|
||||||
}, |
|
||||||
}) |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
_, err = w.WriteBox(&gomp4.Btrt{ // <btrt/>
|
|
||||||
MaxBitrate: 1000000, |
|
||||||
AvgBitrate: 1000000, |
|
||||||
}) |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
err = w.writeBoxEnd() // </hev1>
|
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
case *format.MPEG4Audio: |
|
||||||
_, 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/>
|
|
||||||
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 |
|
||||||
} |
|
||||||
|
|
||||||
case *format.Opus: |
|
||||||
_, err = w.writeBoxStart(&gomp4.AudioSampleEntry{ // <Opus>
|
|
||||||
SampleEntry: gomp4.SampleEntry{ |
|
||||||
AnyTypeBox: gomp4.AnyTypeBox{ |
|
||||||
Type: BoxTypeOpus(), |
|
||||||
}, |
|
||||||
DataReferenceIndex: 1, |
|
||||||
}, |
|
||||||
ChannelCount: uint16(ttrack.ChannelCount), |
|
||||||
SampleSize: 16, |
|
||||||
SampleRate: uint32(ttrack.ClockRate() * 65536), |
|
||||||
}) |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
_, err = w.WriteBox(&DOps{ // <dOps/>
|
|
||||||
OutputChannelCount: uint8(ttrack.ChannelCount), |
|
||||||
PreSkip: 312, |
|
||||||
InputSampleRate: uint32(ttrack.ClockRate()), |
|
||||||
}) |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
_, err = w.WriteBox(&gomp4.Btrt{ // <btrt/>
|
|
||||||
MaxBitrate: 128825, |
|
||||||
AvgBitrate: 128825, |
|
||||||
}) |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
err = w.writeBoxEnd() // </Opus>
|
|
||||||
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,93 +0,0 @@ |
|||||||
package fmp4 |
|
||||||
|
|
||||||
import ( |
|
||||||
"io" |
|
||||||
|
|
||||||
gomp4 "github.com/abema/go-mp4" |
|
||||||
"github.com/orcaman/writerseeker" |
|
||||||
) |
|
||||||
|
|
||||||
type mp4Writer struct { |
|
||||||
buf *writerseeker.WriterSeeker |
|
||||||
w *gomp4.Writer |
|
||||||
} |
|
||||||
|
|
||||||
func newMP4Writer() *mp4Writer { |
|
||||||
w := &mp4Writer{ |
|
||||||
buf: &writerseeker.WriterSeeker{}, |
|
||||||
} |
|
||||||
|
|
||||||
w.w = gomp4.NewWriter(w.buf) |
|
||||||
|
|
||||||
return w |
|
||||||
} |
|
||||||
|
|
||||||
func (w *mp4Writer) writeBoxStart(box gomp4.IImmutableBox) (int, error) { |
|
||||||
bi := &gomp4.BoxInfo{ |
|
||||||
Type: box.GetType(), |
|
||||||
} |
|
||||||
var err error |
|
||||||
bi, err = w.w.StartBox(bi) |
|
||||||
if err != nil { |
|
||||||
return 0, err |
|
||||||
} |
|
||||||
|
|
||||||
_, err = gomp4.Marshal(w.w, box, gomp4.Context{}) |
|
||||||
if err != nil { |
|
||||||
return 0, err |
|
||||||
} |
|
||||||
|
|
||||||
return int(bi.Offset), nil |
|
||||||
} |
|
||||||
|
|
||||||
func (w *mp4Writer) writeBoxEnd() error { |
|
||||||
_, err := w.w.EndBox() |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
func (w *mp4Writer) WriteBox(box gomp4.IImmutableBox) (int, error) { |
|
||||||
off, err := w.writeBoxStart(box) |
|
||||||
if err != nil { |
|
||||||
return 0, err |
|
||||||
} |
|
||||||
|
|
||||||
err = w.writeBoxEnd() |
|
||||||
if err != nil { |
|
||||||
return 0, err |
|
||||||
} |
|
||||||
|
|
||||||
return off, nil |
|
||||||
} |
|
||||||
|
|
||||||
func (w *mp4Writer) rewriteBox(off int, box gomp4.IImmutableBox) error { |
|
||||||
prevOff, err := w.w.Seek(0, io.SeekCurrent) |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
_, err = w.w.Seek(int64(off), io.SeekStart) |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
_, err = w.writeBoxStart(box) |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
err = w.writeBoxEnd() |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
_, err = w.w.Seek(prevOff, io.SeekStart) |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
func (w *mp4Writer) bytes() []byte { |
|
||||||
return w.buf.Bytes() |
|
||||||
} |
|
||||||
@ -1,263 +0,0 @@ |
|||||||
package fmp4 |
|
||||||
|
|
||||||
import ( |
|
||||||
"bytes" |
|
||||||
"fmt" |
|
||||||
|
|
||||||
gomp4 "github.com/abema/go-mp4" |
|
||||||
) |
|
||||||
|
|
||||||
const ( |
|
||||||
trunFlagDataOffsetPreset = 0x01 |
|
||||||
trunFlagSampleDurationPresent = 0x100 |
|
||||||
trunFlagSampleSizePresent = 0x200 |
|
||||||
trunFlagSampleFlagsPresent = 0x400 |
|
||||||
trunFlagSampleCompositionTimeOffsetPresentOrV1 = 0x800 |
|
||||||
|
|
||||||
sampleFlagIsNonSyncSample = 1 << 16 |
|
||||||
) |
|
||||||
|
|
||||||
// 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) |
|
||||||
|
|
||||||
trunFlags := uint16(trun.Flags[1])<<8 | uint16(trun.Flags[2]) |
|
||||||
if (trunFlags & 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 (trunFlags & trunFlagSampleDurationPresent) != 0 { |
|
||||||
s.Duration = e.SampleDuration |
|
||||||
} else { |
|
||||||
s.Duration = tfhd.DefaultSampleDuration |
|
||||||
} |
|
||||||
|
|
||||||
s.PTSOffset = e.SampleCompositionTimeOffsetV1 |
|
||||||
|
|
||||||
var sampleFlags uint32 |
|
||||||
if (trunFlags & trunFlagSampleFlagsPresent) != 0 { |
|
||||||
sampleFlags = e.SampleFlags |
|
||||||
} else { |
|
||||||
sampleFlags = tfhd.DefaultSampleFlags |
|
||||||
} |
|
||||||
s.IsNonSyncSample = ((sampleFlags & sampleFlagIsNonSyncSample) != 0) |
|
||||||
|
|
||||||
var size uint32 |
|
||||||
if (trunFlags & 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,249 +0,0 @@ |
|||||||
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
|
|
||||||
}, |
|
||||||
IsNonSyncSample: true, |
|
||||||
}, |
|
||||||
{ |
|
||||||
Duration: 1 * 90000, |
|
||||||
Payload: []byte{ |
|
||||||
0x00, 0x00, 0x00, 0x01, |
|
||||||
0x01, // non-IDR
|
|
||||||
}, |
|
||||||
IsNonSyncSample: true, |
|
||||||
}, |
|
||||||
} |
|
||||||
|
|
||||||
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
|
|
||||||
}, |
|
||||||
IsNonSyncSample: true, |
|
||||||
}, |
|
||||||
{ |
|
||||||
Duration: 1 * 90000, |
|
||||||
Payload: []byte{ |
|
||||||
0x00, 0x00, 0x00, 0x01, |
|
||||||
0x01, // non-IDR
|
|
||||||
}, |
|
||||||
IsNonSyncSample: true, |
|
||||||
}, |
|
||||||
}, |
|
||||||
}, |
|
||||||
{ |
|
||||||
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) |
|
||||||
} |
|
||||||
@ -1,111 +0,0 @@ |
|||||||
package fmp4 |
|
||||||
|
|
||||||
import ( |
|
||||||
gomp4 "github.com/abema/go-mp4" |
|
||||||
) |
|
||||||
|
|
||||||
// PartSample is a sample of a PartTrack.
|
|
||||||
type PartSample struct { |
|
||||||
Duration uint32 |
|
||||||
PTSOffset int32 |
|
||||||
IsNonSyncSample bool |
|
||||||
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 { |
|
||||||
var flags uint32 |
|
||||||
if sample.IsNonSyncSample { |
|
||||||
flags |= sampleFlagIsNonSyncSample |
|
||||||
} |
|
||||||
|
|
||||||
trun.Entries = append(trun.Entries, gomp4.TrunEntry{ |
|
||||||
SampleDuration: sample.Duration, |
|
||||||
SampleSize: uint32(len(sample.Payload)), |
|
||||||
SampleFlags: 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,106 +0,0 @@ |
|||||||
// 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,106 +0,0 @@ |
|||||||
package mpegts |
|
||||||
|
|
||||||
import ( |
|
||||||
"bytes" |
|
||||||
"context" |
|
||||||
"fmt" |
|
||||||
|
|
||||||
"github.com/aler9/gortsplib/v2/pkg/codecs/mpeg4audio" |
|
||||||
"github.com/aler9/gortsplib/v2/pkg/format" |
|
||||||
"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 |
|
||||||
Format format.Format |
|
||||||
} |
|
||||||
|
|
||||||
// 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: |
|
||||||
|
|
||||||
case astits.StreamTypeMetadata: |
|
||||||
continue |
|
||||||
|
|
||||||
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.Format = &format.H264{ |
|
||||||
PayloadTyp: 96, |
|
||||||
PacketizationMode: 1, |
|
||||||
} |
|
||||||
|
|
||||||
case astits.StreamTypeAACAudio: |
|
||||||
conf, err := findMPEG4AudioConfig(dem, t.ES.ElementaryPID) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
|
|
||||||
t.Format = &format.MPEG4Audio{ |
|
||||||
PayloadTyp: 96, |
|
||||||
Config: conf, |
|
||||||
SizeLength: 13, |
|
||||||
IndexLength: 3, |
|
||||||
IndexDeltaLength: 3, |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
return tracks, nil |
|
||||||
} |
|
||||||
@ -1,201 +0,0 @@ |
|||||||
// Package mpegts contains a MPEG-TS reader and writer.
|
|
||||||
package mpegts |
|
||||||
|
|
||||||
import ( |
|
||||||
"bytes" |
|
||||||
"context" |
|
||||||
"time" |
|
||||||
|
|
||||||
"github.com/aler9/gortsplib/v2/pkg/codecs/h264" |
|
||||||
"github.com/aler9/gortsplib/v2/pkg/codecs/mpeg4audio" |
|
||||||
"github.com/aler9/gortsplib/v2/pkg/format" |
|
||||||
"github.com/asticode/go-astits" |
|
||||||
) |
|
||||||
|
|
||||||
const ( |
|
||||||
pcrOffset = 400 * time.Millisecond // 2 samples @ 5fps
|
|
||||||
) |
|
||||||
|
|
||||||
type writerFunc func(p []byte) (int, error) |
|
||||||
|
|
||||||
func (f writerFunc) Write(p []byte) (int, error) { |
|
||||||
return f(p) |
|
||||||
} |
|
||||||
|
|
||||||
// Writer is a MPEG-TS writer.
|
|
||||||
type Writer struct { |
|
||||||
videoFormat *format.H264 |
|
||||||
audioFormat *format.MPEG4Audio |
|
||||||
|
|
||||||
buf *bytes.Buffer |
|
||||||
inner *astits.Muxer |
|
||||||
pcrCounter int |
|
||||||
} |
|
||||||
|
|
||||||
// NewWriter allocates a Writer.
|
|
||||||
func NewWriter( |
|
||||||
videoFormat *format.H264, |
|
||||||
audioFormat *format.MPEG4Audio, |
|
||||||
) *Writer { |
|
||||||
w := &Writer{ |
|
||||||
videoFormat: videoFormat, |
|
||||||
audioFormat: audioFormat, |
|
||||||
buf: bytes.NewBuffer(nil), |
|
||||||
} |
|
||||||
|
|
||||||
w.inner = astits.NewMuxer( |
|
||||||
context.Background(), |
|
||||||
writerFunc(func(p []byte) (int, error) { |
|
||||||
return w.buf.Write(p) |
|
||||||
})) |
|
||||||
|
|
||||||
if videoFormat != nil { |
|
||||||
w.inner.AddElementaryStream(astits.PMTElementaryStream{ |
|
||||||
ElementaryPID: 256, |
|
||||||
StreamType: astits.StreamTypeH264Video, |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
if audioFormat != nil { |
|
||||||
w.inner.AddElementaryStream(astits.PMTElementaryStream{ |
|
||||||
ElementaryPID: 257, |
|
||||||
StreamType: astits.StreamTypeAACAudio, |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
if videoFormat != nil { |
|
||||||
w.inner.SetPCRPID(256) |
|
||||||
} else { |
|
||||||
w.inner.SetPCRPID(257) |
|
||||||
} |
|
||||||
|
|
||||||
// WriteTable() is not necessary
|
|
||||||
// since it's called automatically when WriteData() is called with
|
|
||||||
// * PID == PCRPID
|
|
||||||
// * AdaptationField != nil
|
|
||||||
// * RandomAccessIndicator = true
|
|
||||||
|
|
||||||
return w |
|
||||||
} |
|
||||||
|
|
||||||
// GenerateSegment generates a MPEG-TS segment.
|
|
||||||
func (w *Writer) GenerateSegment() []byte { |
|
||||||
w.pcrCounter = 0 |
|
||||||
ret := w.buf.Bytes() |
|
||||||
w.buf = bytes.NewBuffer(nil) |
|
||||||
return ret |
|
||||||
} |
|
||||||
|
|
||||||
// WriteH264 writes a H264 access unit.
|
|
||||||
func (w *Writer) WriteH264( |
|
||||||
pcr time.Duration, |
|
||||||
dts time.Duration, |
|
||||||
pts time.Duration, |
|
||||||
idrPresent bool, |
|
||||||
nalus [][]byte, |
|
||||||
) error { |
|
||||||
// prepend an AUD. This is required by video.js and iOS
|
|
||||||
nalus = append([][]byte{{byte(h264.NALUTypeAccessUnitDelimiter), 240}}, nalus...) |
|
||||||
|
|
||||||
enc, err := h264.AnnexBMarshal(nalus) |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
var af *astits.PacketAdaptationField |
|
||||||
|
|
||||||
if idrPresent { |
|
||||||
af = &astits.PacketAdaptationField{} |
|
||||||
af.RandomAccessIndicator = true |
|
||||||
} |
|
||||||
|
|
||||||
// send PCR once in a while
|
|
||||||
if w.pcrCounter == 0 { |
|
||||||
if af == nil { |
|
||||||
af = &astits.PacketAdaptationField{} |
|
||||||
} |
|
||||||
af.HasPCR = true |
|
||||||
af.PCR = &astits.ClockReference{Base: int64(pcr.Seconds() * 90000)} |
|
||||||
w.pcrCounter = 3 |
|
||||||
} |
|
||||||
w.pcrCounter-- |
|
||||||
|
|
||||||
oh := &astits.PESOptionalHeader{ |
|
||||||
MarkerBits: 2, |
|
||||||
} |
|
||||||
|
|
||||||
if dts == pts { |
|
||||||
oh.PTSDTSIndicator = astits.PTSDTSIndicatorOnlyPTS |
|
||||||
oh.PTS = &astits.ClockReference{Base: int64((pts + pcrOffset).Seconds() * 90000)} |
|
||||||
} else { |
|
||||||
oh.PTSDTSIndicator = astits.PTSDTSIndicatorBothPresent |
|
||||||
oh.DTS = &astits.ClockReference{Base: int64((dts + pcrOffset).Seconds() * 90000)} |
|
||||||
oh.PTS = &astits.ClockReference{Base: int64((pts + pcrOffset).Seconds() * 90000)} |
|
||||||
} |
|
||||||
|
|
||||||
_, err = w.inner.WriteData(&astits.MuxerData{ |
|
||||||
PID: 256, |
|
||||||
AdaptationField: af, |
|
||||||
PES: &astits.PESData{ |
|
||||||
Header: &astits.PESHeader{ |
|
||||||
OptionalHeader: oh, |
|
||||||
StreamID: 224, // video
|
|
||||||
}, |
|
||||||
Data: enc, |
|
||||||
}, |
|
||||||
}) |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
// WriteAAC writes an AAC AU.
|
|
||||||
func (w *Writer) WriteAAC( |
|
||||||
pcr time.Duration, |
|
||||||
pts time.Duration, |
|
||||||
au []byte, |
|
||||||
) error { |
|
||||||
pkts := mpeg4audio.ADTSPackets{ |
|
||||||
{ |
|
||||||
Type: w.audioFormat.Config.Type, |
|
||||||
SampleRate: w.audioFormat.Config.SampleRate, |
|
||||||
ChannelCount: w.audioFormat.Config.ChannelCount, |
|
||||||
AU: au, |
|
||||||
}, |
|
||||||
} |
|
||||||
|
|
||||||
enc, err := pkts.Marshal() |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
af := &astits.PacketAdaptationField{ |
|
||||||
RandomAccessIndicator: true, |
|
||||||
} |
|
||||||
|
|
||||||
if w.videoFormat == nil { |
|
||||||
// send PCR once in a while
|
|
||||||
if w.pcrCounter == 0 { |
|
||||||
af.HasPCR = true |
|
||||||
af.PCR = &astits.ClockReference{Base: int64(pcr.Seconds() * 90000)} |
|
||||||
w.pcrCounter = 3 |
|
||||||
} |
|
||||||
w.pcrCounter-- |
|
||||||
} |
|
||||||
|
|
||||||
_, err = w.inner.WriteData(&astits.MuxerData{ |
|
||||||
PID: 257, |
|
||||||
AdaptationField: af, |
|
||||||
PES: &astits.PESData{ |
|
||||||
Header: &astits.PESHeader{ |
|
||||||
OptionalHeader: &astits.PESOptionalHeader{ |
|
||||||
MarkerBits: 2, |
|
||||||
PTSDTSIndicator: astits.PTSDTSIndicatorOnlyPTS, |
|
||||||
PTS: &astits.ClockReference{Base: int64((pts + pcrOffset).Seconds() * 90000)}, |
|
||||||
}, |
|
||||||
PacketLength: uint16(len(enc) + 8), |
|
||||||
StreamID: 192, // audio
|
|
||||||
}, |
|
||||||
Data: enc, |
|
||||||
}, |
|
||||||
}) |
|
||||||
return err |
|
||||||
} |
|
||||||
@ -1,371 +0,0 @@ |
|||||||
package mpegts |
|
||||||
|
|
||||||
import ( |
|
||||||
"bytes" |
|
||||||
"context" |
|
||||||
"testing" |
|
||||||
"time" |
|
||||||
|
|
||||||
"github.com/aler9/gortsplib/v2/pkg/codecs/h264" |
|
||||||
"github.com/aler9/gortsplib/v2/pkg/codecs/mpeg4audio" |
|
||||||
"github.com/aler9/gortsplib/v2/pkg/format" |
|
||||||
"github.com/asticode/go-astits" |
|
||||||
"github.com/stretchr/testify/require" |
|
||||||
) |
|
||||||
|
|
||||||
func TestWriter(t *testing.T) { |
|
||||||
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, |
|
||||||
} |
|
||||||
|
|
||||||
testVideoTrack := &format.H264{ |
|
||||||
PayloadTyp: 96, |
|
||||||
SPS: testSPS, |
|
||||||
PPS: []byte{0x08}, |
|
||||||
PacketizationMode: 1, |
|
||||||
} |
|
||||||
|
|
||||||
testAudioTrack := &format.MPEG4Audio{ |
|
||||||
PayloadTyp: 97, |
|
||||||
Config: &mpeg4audio.Config{ |
|
||||||
Type: 2, |
|
||||||
SampleRate: 44100, |
|
||||||
ChannelCount: 2, |
|
||||||
}, |
|
||||||
SizeLength: 13, |
|
||||||
IndexLength: 3, |
|
||||||
IndexDeltaLength: 3, |
|
||||||
} |
|
||||||
|
|
||||||
type videoSample struct { |
|
||||||
NALUs [][]byte |
|
||||||
PTS time.Duration |
|
||||||
DTS time.Duration |
|
||||||
} |
|
||||||
|
|
||||||
type audioSample struct { |
|
||||||
AU []byte |
|
||||||
PTS time.Duration |
|
||||||
} |
|
||||||
|
|
||||||
type sample interface{} |
|
||||||
|
|
||||||
testSamples := []sample{ |
|
||||||
videoSample{ |
|
||||||
NALUs: [][]byte{ |
|
||||||
testSPS, // SPS
|
|
||||||
{8}, // PPS
|
|
||||||
{5}, // IDR
|
|
||||||
}, |
|
||||||
PTS: 2 * time.Second, |
|
||||||
DTS: 2 * time.Second, |
|
||||||
}, |
|
||||||
audioSample{ |
|
||||||
AU: []byte{ |
|
||||||
0x01, 0x02, 0x03, 0x04, |
|
||||||
}, |
|
||||||
PTS: 3 * time.Second, |
|
||||||
}, |
|
||||||
audioSample{ |
|
||||||
AU: []byte{ |
|
||||||
0x01, 0x02, 0x03, 0x04, |
|
||||||
}, |
|
||||||
PTS: 3500 * time.Millisecond, |
|
||||||
}, |
|
||||||
videoSample{ |
|
||||||
NALUs: [][]byte{ |
|
||||||
{1}, // non-IDR
|
|
||||||
}, |
|
||||||
PTS: 4 * time.Second, |
|
||||||
DTS: 4 * time.Second, |
|
||||||
}, |
|
||||||
audioSample{ |
|
||||||
AU: []byte{ |
|
||||||
0x01, 0x02, 0x03, 0x04, |
|
||||||
}, |
|
||||||
PTS: 4500 * time.Millisecond, |
|
||||||
}, |
|
||||||
videoSample{ |
|
||||||
NALUs: [][]byte{ |
|
||||||
{1}, // non-IDR
|
|
||||||
}, |
|
||||||
PTS: 6 * time.Second, |
|
||||||
DTS: 6 * time.Second, |
|
||||||
}, |
|
||||||
} |
|
||||||
|
|
||||||
t.Run("video + audio", func(t *testing.T) { |
|
||||||
w := NewWriter(testVideoTrack, testAudioTrack) |
|
||||||
|
|
||||||
for _, sample := range testSamples { |
|
||||||
switch tsample := sample.(type) { |
|
||||||
case videoSample: |
|
||||||
err := w.WriteH264( |
|
||||||
tsample.DTS-2*time.Second, |
|
||||||
tsample.DTS, |
|
||||||
tsample.PTS, |
|
||||||
h264.IDRPresent(tsample.NALUs), |
|
||||||
tsample.NALUs) |
|
||||||
require.NoError(t, err) |
|
||||||
|
|
||||||
case audioSample: |
|
||||||
err := w.WriteAAC( |
|
||||||
tsample.PTS-2*time.Second, |
|
||||||
tsample.PTS, |
|
||||||
tsample.AU) |
|
||||||
require.NoError(t, err) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
byts := w.GenerateSegment() |
|
||||||
|
|
||||||
dem := astits.NewDemuxer(context.Background(), bytes.NewReader(byts), |
|
||||||
astits.DemuxerOptPacketSize(188)) |
|
||||||
|
|
||||||
// PMT
|
|
||||||
pkt, err := dem.NextPacket() |
|
||||||
require.NoError(t, err) |
|
||||||
require.Equal(t, &astits.Packet{ |
|
||||||
Header: &astits.PacketHeader{ |
|
||||||
HasPayload: true, |
|
||||||
PayloadUnitStartIndicator: true, |
|
||||||
PID: 0, |
|
||||||
}, |
|
||||||
Payload: append([]byte{ |
|
||||||
0x00, 0x00, 0xb0, 0x0d, 0x00, 0x00, 0xc1, 0x00, |
|
||||||
0x00, 0x00, 0x01, 0xf0, 0x00, 0x71, 0x10, 0xd8, |
|
||||||
0x78, |
|
||||||
}, bytes.Repeat([]byte{0xff}, 167)...), |
|
||||||
}, pkt) |
|
||||||
|
|
||||||
// PAT
|
|
||||||
pkt, err = dem.NextPacket() |
|
||||||
require.NoError(t, err) |
|
||||||
require.Equal(t, &astits.Packet{ |
|
||||||
Header: &astits.PacketHeader{ |
|
||||||
HasPayload: true, |
|
||||||
PayloadUnitStartIndicator: true, |
|
||||||
PID: 4096, |
|
||||||
}, |
|
||||||
Payload: append([]byte{ |
|
||||||
0x00, 0x02, 0xb0, 0x17, 0x00, 0x01, 0xc1, 0x00, |
|
||||||
0x00, 0xe1, 0x00, 0xf0, 0x00, 0x1b, 0xe1, 0x00, |
|
||||||
0xf0, 0x00, 0x0f, 0xe1, 0x01, 0xf0, 0x00, 0x2f, |
|
||||||
0x44, 0xb9, 0x9b, |
|
||||||
}, bytes.Repeat([]byte{0xff}, 157)...), |
|
||||||
}, pkt) |
|
||||||
|
|
||||||
// PES (H264)
|
|
||||||
pkt, err = dem.NextPacket() |
|
||||||
require.NoError(t, err) |
|
||||||
require.Equal(t, &astits.Packet{ |
|
||||||
AdaptationField: &astits.PacketAdaptationField{ |
|
||||||
Length: 124, |
|
||||||
StuffingLength: 117, |
|
||||||
HasPCR: true, |
|
||||||
PCR: &astits.ClockReference{}, |
|
||||||
RandomAccessIndicator: true, |
|
||||||
}, |
|
||||||
Header: &astits.PacketHeader{ |
|
||||||
HasAdaptationField: true, |
|
||||||
HasPayload: true, |
|
||||||
PayloadUnitStartIndicator: true, |
|
||||||
PID: 256, |
|
||||||
}, |
|
||||||
Payload: []byte{ |
|
||||||
0x00, 0x00, 0x01, 0xe0, 0x00, 0x00, 0x80, 0x80, |
|
||||||
0x05, 0x21, 0x00, 0x0d, 0x97, 0x81, 0x00, 0x00, |
|
||||||
0x00, 0x01, 0x09, 0xf0, 0x00, 0x00, 0x00, 0x01, |
|
||||||
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, 0x00, 0x00, 0x00, 0x01, 0x08, 0x00, 0x00, |
|
||||||
0x00, 0x01, 0x05, |
|
||||||
}, |
|
||||||
}, pkt) |
|
||||||
|
|
||||||
// PES (AAC)
|
|
||||||
pkt, err = dem.NextPacket() |
|
||||||
require.NoError(t, err) |
|
||||||
require.Equal(t, &astits.Packet{ |
|
||||||
AdaptationField: &astits.PacketAdaptationField{ |
|
||||||
Length: 158, |
|
||||||
StuffingLength: 157, |
|
||||||
RandomAccessIndicator: true, |
|
||||||
}, |
|
||||||
Header: &astits.PacketHeader{ |
|
||||||
HasAdaptationField: true, |
|
||||||
HasPayload: true, |
|
||||||
PayloadUnitStartIndicator: true, |
|
||||||
PID: 257, |
|
||||||
}, |
|
||||||
Payload: []byte{ |
|
||||||
0x00, 0x00, 0x01, 0xc0, 0x00, 0x13, 0x80, 0x80, |
|
||||||
0x05, 0x21, 0x00, 0x13, 0x56, 0xa1, 0xff, 0xf1, |
|
||||||
0x50, 0x80, 0x01, 0x7f, 0xfc, 0x01, 0x02, 0x03, |
|
||||||
0x04, |
|
||||||
}, |
|
||||||
}, pkt) |
|
||||||
}) |
|
||||||
|
|
||||||
t.Run("video only", func(t *testing.T) { |
|
||||||
w := NewWriter(testVideoTrack, nil) |
|
||||||
|
|
||||||
for _, sample := range testSamples { |
|
||||||
if tsample, ok := sample.(videoSample); ok { |
|
||||||
err := w.WriteH264( |
|
||||||
tsample.DTS-2*time.Second, |
|
||||||
tsample.DTS, |
|
||||||
tsample.PTS, |
|
||||||
h264.IDRPresent(tsample.NALUs), |
|
||||||
tsample.NALUs) |
|
||||||
require.NoError(t, err) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
byts := w.GenerateSegment() |
|
||||||
|
|
||||||
dem := astits.NewDemuxer(context.Background(), bytes.NewReader(byts), |
|
||||||
astits.DemuxerOptPacketSize(188)) |
|
||||||
|
|
||||||
// PMT
|
|
||||||
pkt, err := dem.NextPacket() |
|
||||||
require.NoError(t, err) |
|
||||||
require.Equal(t, &astits.Packet{ |
|
||||||
Header: &astits.PacketHeader{ |
|
||||||
HasPayload: true, |
|
||||||
PayloadUnitStartIndicator: true, |
|
||||||
PID: 0, |
|
||||||
}, |
|
||||||
Payload: append([]byte{ |
|
||||||
0x00, 0x00, 0xb0, 0x0d, 0x00, 0x00, 0xc1, 0x00, |
|
||||||
0x00, 0x00, 0x01, 0xf0, 0x00, 0x71, 0x10, 0xd8, |
|
||||||
0x78, |
|
||||||
}, bytes.Repeat([]byte{0xff}, 167)...), |
|
||||||
}, pkt) |
|
||||||
|
|
||||||
// PAT
|
|
||||||
pkt, err = dem.NextPacket() |
|
||||||
require.NoError(t, err) |
|
||||||
require.Equal(t, &astits.Packet{ |
|
||||||
Header: &astits.PacketHeader{ |
|
||||||
HasPayload: true, |
|
||||||
PayloadUnitStartIndicator: true, |
|
||||||
PID: 4096, |
|
||||||
}, |
|
||||||
Payload: append([]byte{ |
|
||||||
0x00, 0x02, 0xb0, 0x12, 0x00, 0x01, 0xc1, 0x00, |
|
||||||
0x00, 0xe1, 0x00, 0xf0, 0x00, 0x1b, 0xe1, 0x00, |
|
||||||
0xf0, 0x00, 0x15, 0xbd, 0x4d, 0x56, |
|
||||||
}, bytes.Repeat([]byte{0xff}, 162)...), |
|
||||||
}, pkt) |
|
||||||
|
|
||||||
// PES (H264)
|
|
||||||
pkt, err = dem.NextPacket() |
|
||||||
require.NoError(t, err) |
|
||||||
require.Equal(t, &astits.Packet{ |
|
||||||
AdaptationField: &astits.PacketAdaptationField{ |
|
||||||
Length: 124, |
|
||||||
StuffingLength: 117, |
|
||||||
HasPCR: true, |
|
||||||
PCR: &astits.ClockReference{}, |
|
||||||
RandomAccessIndicator: true, |
|
||||||
}, |
|
||||||
Header: &astits.PacketHeader{ |
|
||||||
HasAdaptationField: true, |
|
||||||
HasPayload: true, |
|
||||||
PayloadUnitStartIndicator: true, |
|
||||||
PID: 256, |
|
||||||
}, |
|
||||||
Payload: []byte{ |
|
||||||
0x00, 0x00, 0x01, 0xe0, 0x00, 0x00, 0x80, 0x80, |
|
||||||
0x05, 0x21, 0x00, 0x0d, 0x97, 0x81, 0x00, 0x00, |
|
||||||
0x00, 0x01, 0x09, 0xf0, 0x00, 0x00, 0x00, 0x01, |
|
||||||
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, 0x00, 0x00, 0x00, 0x01, 0x08, 0x00, 0x00, |
|
||||||
0x00, 0x01, 0x05, |
|
||||||
}, |
|
||||||
}, pkt) |
|
||||||
}) |
|
||||||
|
|
||||||
t.Run("audio only", func(t *testing.T) { |
|
||||||
w := NewWriter(nil, testAudioTrack) |
|
||||||
|
|
||||||
for _, sample := range testSamples { |
|
||||||
if tsample, ok := sample.(audioSample); ok { |
|
||||||
err := w.WriteAAC( |
|
||||||
tsample.PTS-2*time.Second, |
|
||||||
tsample.PTS, |
|
||||||
tsample.AU) |
|
||||||
require.NoError(t, err) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
byts := w.GenerateSegment() |
|
||||||
|
|
||||||
dem := astits.NewDemuxer(context.Background(), bytes.NewReader(byts), |
|
||||||
astits.DemuxerOptPacketSize(188)) |
|
||||||
|
|
||||||
// PMT
|
|
||||||
pkt, err := dem.NextPacket() |
|
||||||
require.NoError(t, err) |
|
||||||
require.Equal(t, &astits.Packet{ |
|
||||||
Header: &astits.PacketHeader{ |
|
||||||
HasPayload: true, |
|
||||||
PayloadUnitStartIndicator: true, |
|
||||||
PID: 0, |
|
||||||
}, |
|
||||||
Payload: append([]byte{ |
|
||||||
0x00, 0x00, 0xb0, 0x0d, 0x00, 0x00, 0xc1, 0x00, |
|
||||||
0x00, 0x00, 0x01, 0xf0, 0x00, 0x71, 0x10, 0xd8, |
|
||||||
0x78, |
|
||||||
}, bytes.Repeat([]byte{0xff}, 167)...), |
|
||||||
}, pkt) |
|
||||||
|
|
||||||
// PAT
|
|
||||||
pkt, err = dem.NextPacket() |
|
||||||
require.NoError(t, err) |
|
||||||
require.Equal(t, &astits.Packet{ |
|
||||||
Header: &astits.PacketHeader{ |
|
||||||
HasPayload: true, |
|
||||||
PayloadUnitStartIndicator: true, |
|
||||||
PID: 4096, |
|
||||||
}, |
|
||||||
Payload: append([]byte{ |
|
||||||
0x00, 0x02, 0xb0, 0x12, 0x00, 0x01, 0xc1, 0x00, |
|
||||||
0x00, 0xe1, 0x01, 0xf0, 0x00, 0x0f, 0xe1, 0x01, |
|
||||||
0xf0, 0x00, 0xec, 0xe2, 0xb0, 0x94, |
|
||||||
}, bytes.Repeat([]byte{0xff}, 162)...), |
|
||||||
}, pkt) |
|
||||||
|
|
||||||
// PES (AAC)
|
|
||||||
pkt, err = dem.NextPacket() |
|
||||||
require.NoError(t, err) |
|
||||||
require.Equal(t, &astits.Packet{ |
|
||||||
AdaptationField: &astits.PacketAdaptationField{ |
|
||||||
Length: 158, |
|
||||||
StuffingLength: 151, |
|
||||||
RandomAccessIndicator: true, |
|
||||||
HasPCR: true, |
|
||||||
PCR: &astits.ClockReference{Base: 90000}, |
|
||||||
}, |
|
||||||
Header: &astits.PacketHeader{ |
|
||||||
HasAdaptationField: true, |
|
||||||
HasPayload: true, |
|
||||||
PayloadUnitStartIndicator: true, |
|
||||||
PID: 257, |
|
||||||
}, |
|
||||||
Payload: []byte{ |
|
||||||
0x00, 0x00, 0x01, 0xc0, 0x00, 0x13, 0x80, 0x80, |
|
||||||
0x05, 0x21, 0x00, 0x13, 0x56, 0xa1, 0xff, 0xf1, |
|
||||||
0x50, 0x80, 0x01, 0x7f, 0xfc, 0x01, 0x02, 0x03, |
|
||||||
0x04, |
|
||||||
}, |
|
||||||
}, pkt) |
|
||||||
}) |
|
||||||
} |
|
||||||
@ -1,46 +0,0 @@ |
|||||||
// Package mpegtstimedec contains a MPEG-TS timestamp decoder.
|
|
||||||
package mpegtstimedec |
|
||||||
|
|
||||||
import ( |
|
||||||
"time" |
|
||||||
) |
|
||||||
|
|
||||||
const ( |
|
||||||
maximum = 0x1FFFFFFFF // 33 bits
|
|
||||||
negativeThreshold = 0x1FFFFFFFF / 2 |
|
||||||
clockRate = 90000 |
|
||||||
) |
|
||||||
|
|
||||||
// Decoder is a MPEG-TS timestamp decoder.
|
|
||||||
type Decoder struct { |
|
||||||
overall time.Duration |
|
||||||
prev int64 |
|
||||||
} |
|
||||||
|
|
||||||
// New allocates a Decoder.
|
|
||||||
func New(start int64) *Decoder { |
|
||||||
return &Decoder{ |
|
||||||
prev: start, |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// Decode decodes a MPEG-TS timestamp.
|
|
||||||
func (d *Decoder) Decode(ts int64) time.Duration { |
|
||||||
diff := (ts - d.prev) & maximum |
|
||||||
|
|
||||||
// negative difference
|
|
||||||
if diff > negativeThreshold { |
|
||||||
diff = (d.prev - ts) & maximum |
|
||||||
d.prev = ts |
|
||||||
d.overall -= time.Duration(diff) |
|
||||||
} else { |
|
||||||
d.prev = ts |
|
||||||
d.overall += time.Duration(diff) |
|
||||||
} |
|
||||||
|
|
||||||
// avoid an int64 overflow and preserve resolution by splitting division into two parts:
|
|
||||||
// first add the integer part, then the decimal part.
|
|
||||||
secs := d.overall / clockRate |
|
||||||
dec := d.overall % clockRate |
|
||||||
return secs*time.Second + dec*time.Second/clockRate |
|
||||||
} |
|
||||||
@ -1,72 +0,0 @@ |
|||||||
package mpegtstimedec |
|
||||||
|
|
||||||
import ( |
|
||||||
"testing" |
|
||||||
"time" |
|
||||||
|
|
||||||
"github.com/stretchr/testify/require" |
|
||||||
) |
|
||||||
|
|
||||||
func TestNegativeDiff(t *testing.T) { |
|
||||||
d := New(64523434) |
|
||||||
|
|
||||||
ts := d.Decode(64523434 - 90000) |
|
||||||
require.Equal(t, -1*time.Second, ts) |
|
||||||
|
|
||||||
ts = d.Decode(64523434) |
|
||||||
require.Equal(t, time.Duration(0), ts) |
|
||||||
|
|
||||||
ts = d.Decode(64523434 + 90000*2) |
|
||||||
require.Equal(t, 2*time.Second, ts) |
|
||||||
|
|
||||||
ts = d.Decode(64523434 + 90000) |
|
||||||
require.Equal(t, 1*time.Second, ts) |
|
||||||
} |
|
||||||
|
|
||||||
func TestOverflow(t *testing.T) { |
|
||||||
d := New(0x1FFFFFFFF - 20) |
|
||||||
|
|
||||||
i := int64(0x1FFFFFFFF - 20) |
|
||||||
secs := time.Duration(0) |
|
||||||
const stride = 150 |
|
||||||
lim := int64(uint64(0x1FFFFFFFF - (stride * 90000))) |
|
||||||
|
|
||||||
for n := 0; n < 100; n++ { |
|
||||||
// overflow
|
|
||||||
i += 90000 * stride |
|
||||||
secs += stride |
|
||||||
ts := d.Decode(i) |
|
||||||
require.Equal(t, secs*time.Second, ts) |
|
||||||
|
|
||||||
// reach 2^32 slowly
|
|
||||||
secs += stride |
|
||||||
i += 90000 * stride |
|
||||||
for ; i < lim; i += 90000 * stride { |
|
||||||
ts = d.Decode(i) |
|
||||||
require.Equal(t, secs*time.Second, ts) |
|
||||||
secs += stride |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
func TestOverflowAndBack(t *testing.T) { |
|
||||||
d := New(0x1FFFFFFFF - 90000 + 1) |
|
||||||
|
|
||||||
ts := d.Decode(0x1FFFFFFFF - 90000 + 1) |
|
||||||
require.Equal(t, time.Duration(0), ts) |
|
||||||
|
|
||||||
ts = d.Decode(90000) |
|
||||||
require.Equal(t, 2*time.Second, ts) |
|
||||||
|
|
||||||
ts = d.Decode(0x1FFFFFFFF - 90000 + 1) |
|
||||||
require.Equal(t, time.Duration(0), ts) |
|
||||||
|
|
||||||
ts = d.Decode(0x1FFFFFFFF - 90000*2 + 1) |
|
||||||
require.Equal(t, -1*time.Second, ts) |
|
||||||
|
|
||||||
ts = d.Decode(0x1FFFFFFFF - 90000 + 1) |
|
||||||
require.Equal(t, time.Duration(0), ts) |
|
||||||
|
|
||||||
ts = d.Decode(90000) |
|
||||||
require.Equal(t, 2*time.Second, ts) |
|
||||||
} |
|
||||||
@ -1,100 +0,0 @@ |
|||||||
// Package hls contains a HLS muxer and client.
|
|
||||||
package hls |
|
||||||
|
|
||||||
import ( |
|
||||||
"io" |
|
||||||
"time" |
|
||||||
|
|
||||||
"github.com/aler9/gortsplib/v2/pkg/format" |
|
||||||
) |
|
||||||
|
|
||||||
// MuxerFileResponse is a response of the Muxer's File() func.
|
|
||||||
type MuxerFileResponse struct { |
|
||||||
Status int |
|
||||||
Header map[string]string |
|
||||||
Body io.Reader |
|
||||||
} |
|
||||||
|
|
||||||
// Muxer is a HLS muxer.
|
|
||||||
type Muxer struct { |
|
||||||
primaryPlaylist *muxerPrimaryPlaylist |
|
||||||
variant muxerVariant |
|
||||||
} |
|
||||||
|
|
||||||
// NewMuxer allocates a Muxer.
|
|
||||||
func NewMuxer( |
|
||||||
variant MuxerVariant, |
|
||||||
segmentCount int, |
|
||||||
segmentDuration time.Duration, |
|
||||||
partDuration time.Duration, |
|
||||||
segmentMaxSize uint64, |
|
||||||
videoTrack format.Format, |
|
||||||
audioTrack format.Format, |
|
||||||
) (*Muxer, error) { |
|
||||||
m := &Muxer{} |
|
||||||
|
|
||||||
switch variant { |
|
||||||
case MuxerVariantMPEGTS: |
|
||||||
var err error |
|
||||||
m.variant, err = newMuxerVariantMPEGTS( |
|
||||||
segmentCount, |
|
||||||
segmentDuration, |
|
||||||
segmentMaxSize, |
|
||||||
videoTrack, |
|
||||||
audioTrack, |
|
||||||
) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
|
|
||||||
case MuxerVariantFMP4: |
|
||||||
m.variant = newMuxerVariantFMP4( |
|
||||||
false, |
|
||||||
segmentCount, |
|
||||||
segmentDuration, |
|
||||||
partDuration, |
|
||||||
segmentMaxSize, |
|
||||||
videoTrack, |
|
||||||
audioTrack, |
|
||||||
) |
|
||||||
|
|
||||||
default: // MuxerVariantLowLatency
|
|
||||||
m.variant = newMuxerVariantFMP4( |
|
||||||
true, |
|
||||||
segmentCount, |
|
||||||
segmentDuration, |
|
||||||
partDuration, |
|
||||||
segmentMaxSize, |
|
||||||
videoTrack, |
|
||||||
audioTrack, |
|
||||||
) |
|
||||||
} |
|
||||||
|
|
||||||
m.primaryPlaylist = newMuxerPrimaryPlaylist(variant != MuxerVariantMPEGTS, videoTrack, audioTrack) |
|
||||||
|
|
||||||
return m, nil |
|
||||||
} |
|
||||||
|
|
||||||
// Close closes a Muxer.
|
|
||||||
func (m *Muxer) Close() { |
|
||||||
m.variant.close() |
|
||||||
} |
|
||||||
|
|
||||||
// WriteH26x writes an H264 or an H265 access unit.
|
|
||||||
func (m *Muxer) WriteH26x(ntp time.Time, pts time.Duration, au [][]byte) error { |
|
||||||
return m.variant.writeH26x(ntp, pts, au) |
|
||||||
} |
|
||||||
|
|
||||||
// WriteAudio writes an audio access unit.
|
|
||||||
func (m *Muxer) WriteAudio(ntp time.Time, pts time.Duration, au []byte) error { |
|
||||||
return m.variant.writeAudio(ntp, pts, au) |
|
||||||
} |
|
||||||
|
|
||||||
// File returns a file reader.
|
|
||||||
func (m *Muxer) File(name string, msn string, part string, skip string) *MuxerFileResponse { |
|
||||||
if name == "index.m3u8" { |
|
||||||
return m.primaryPlaylist.file() |
|
||||||
} |
|
||||||
|
|
||||||
return m.variant.file(name, msn, part, skip) |
|
||||||
} |
|
||||||
@ -1,62 +0,0 @@ |
|||||||
package hls |
|
||||||
|
|
||||||
import ( |
|
||||||
"bytes" |
|
||||||
"io" |
|
||||||
"net/http" |
|
||||||
"strconv" |
|
||||||
"strings" |
|
||||||
|
|
||||||
"github.com/aler9/gortsplib/v2/pkg/format" |
|
||||||
) |
|
||||||
|
|
||||||
type muxerPrimaryPlaylist struct { |
|
||||||
fmp4 bool |
|
||||||
videoTrack format.Format |
|
||||||
audioTrack format.Format |
|
||||||
} |
|
||||||
|
|
||||||
func newMuxerPrimaryPlaylist( |
|
||||||
fmp4 bool, |
|
||||||
videoTrack format.Format, |
|
||||||
audioTrack format.Format, |
|
||||||
) *muxerPrimaryPlaylist { |
|
||||||
return &muxerPrimaryPlaylist{ |
|
||||||
fmp4: fmp4, |
|
||||||
videoTrack: videoTrack, |
|
||||||
audioTrack: audioTrack, |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
func (p *muxerPrimaryPlaylist) file() *MuxerFileResponse { |
|
||||||
return &MuxerFileResponse{ |
|
||||||
Status: http.StatusOK, |
|
||||||
Header: map[string]string{ |
|
||||||
"Content-Type": `application/x-mpegURL`, |
|
||||||
}, |
|
||||||
Body: func() io.Reader { |
|
||||||
var codecs []string |
|
||||||
|
|
||||||
if p.videoTrack != nil { |
|
||||||
codecs = append(codecs, codecParametersGenerate(p.videoTrack)) |
|
||||||
} |
|
||||||
if p.audioTrack != nil { |
|
||||||
codecs = append(codecs, codecParametersGenerate(p.audioTrack)) |
|
||||||
} |
|
||||||
|
|
||||||
var version int |
|
||||||
if !p.fmp4 { |
|
||||||
version = 3 |
|
||||||
} else { |
|
||||||
version = 9 |
|
||||||
} |
|
||||||
|
|
||||||
return bytes.NewReader([]byte("#EXTM3U\n" + |
|
||||||
"#EXT-X-VERSION:" + strconv.FormatInt(int64(version), 10) + "\n" + |
|
||||||
"#EXT-X-INDEPENDENT-SEGMENTS\n" + |
|
||||||
"\n" + |
|
||||||
"#EXT-X-STREAM-INF:BANDWIDTH=200000,CODECS=\"" + strings.Join(codecs, ",") + "\"\n" + |
|
||||||
"stream.m3u8\n")) |
|
||||||
}(), |
|
||||||
} |
|
||||||
} |
|
||||||
@ -1,595 +0,0 @@ |
|||||||
package hls |
|
||||||
|
|
||||||
import ( |
|
||||||
"io" |
|
||||||
"net/http" |
|
||||||
"regexp" |
|
||||||
"testing" |
|
||||||
"time" |
|
||||||
|
|
||||||
"github.com/aler9/gortsplib/v2/pkg/codecs/mpeg4audio" |
|
||||||
"github.com/aler9/gortsplib/v2/pkg/format" |
|
||||||
"github.com/stretchr/testify/require" |
|
||||||
) |
|
||||||
|
|
||||||
var testTime = time.Date(2010, 0o1, 0o1, 0o1, 0o1, 0o1, 0, time.UTC) |
|
||||||
|
|
||||||
// baseline profile without POC
|
|
||||||
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, |
|
||||||
} |
|
||||||
|
|
||||||
func TestMuxerVideoAudio(t *testing.T) { |
|
||||||
videoTrack := &format.H264{ |
|
||||||
PayloadTyp: 96, |
|
||||||
SPS: testSPS, |
|
||||||
PPS: []byte{0x08}, |
|
||||||
PacketizationMode: 1, |
|
||||||
} |
|
||||||
|
|
||||||
audioTrack := &format.MPEG4Audio{ |
|
||||||
PayloadTyp: 97, |
|
||||||
Config: &mpeg4audio.Config{ |
|
||||||
Type: 2, |
|
||||||
SampleRate: 44100, |
|
||||||
ChannelCount: 2, |
|
||||||
}, |
|
||||||
SizeLength: 13, |
|
||||||
IndexLength: 3, |
|
||||||
IndexDeltaLength: 3, |
|
||||||
} |
|
||||||
|
|
||||||
for _, ca := range []string{ |
|
||||||
"mpegts", |
|
||||||
"fmp4", |
|
||||||
"lowLatency", |
|
||||||
} { |
|
||||||
t.Run(ca, func(t *testing.T) { |
|
||||||
var v MuxerVariant |
|
||||||
switch ca { |
|
||||||
case "mpegts": |
|
||||||
v = MuxerVariantMPEGTS |
|
||||||
|
|
||||||
case "fmp4": |
|
||||||
v = MuxerVariantFMP4 |
|
||||||
|
|
||||||
case "lowLatency": |
|
||||||
v = MuxerVariantLowLatency |
|
||||||
} |
|
||||||
|
|
||||||
m, err := NewMuxer(v, 3, 1*time.Second, 0, 50*1024*1024, videoTrack, audioTrack) |
|
||||||
require.NoError(t, err) |
|
||||||
defer m.Close() |
|
||||||
|
|
||||||
// access unit without IDR
|
|
||||||
d := 1 * time.Second |
|
||||||
err = m.WriteH26x(testTime.Add(d-1*time.Second), d, [][]byte{ |
|
||||||
{0x06}, |
|
||||||
{0x07}, |
|
||||||
}) |
|
||||||
require.NoError(t, err) |
|
||||||
|
|
||||||
// access unit with IDR
|
|
||||||
d = 2 * time.Second |
|
||||||
err = m.WriteH26x(testTime.Add(d-1*time.Second), d, [][]byte{ |
|
||||||
testSPS, // SPS
|
|
||||||
{8}, // PPS
|
|
||||||
{5}, // IDR
|
|
||||||
}) |
|
||||||
require.NoError(t, err) |
|
||||||
|
|
||||||
d = 3 * time.Second |
|
||||||
err = m.WriteAudio(testTime.Add(d-1*time.Second), d, []byte{ |
|
||||||
0x01, 0x02, 0x03, 0x04, |
|
||||||
}) |
|
||||||
require.NoError(t, err) |
|
||||||
|
|
||||||
d = 3500 * time.Millisecond |
|
||||||
err = m.WriteAudio(testTime.Add(d-1*time.Second), d, []byte{ |
|
||||||
0x01, 0x02, 0x03, 0x04, |
|
||||||
}) |
|
||||||
require.NoError(t, err) |
|
||||||
|
|
||||||
// access unit without IDR
|
|
||||||
d = 4 * time.Second |
|
||||||
err = m.WriteH26x(testTime.Add(d-1*time.Second), d, [][]byte{ |
|
||||||
{1}, // non-IDR
|
|
||||||
}) |
|
||||||
require.NoError(t, err) |
|
||||||
|
|
||||||
d = 4500 * time.Millisecond |
|
||||||
err = m.WriteAudio(testTime.Add(d-1*time.Second), d, []byte{ |
|
||||||
0x01, 0x02, 0x03, 0x04, |
|
||||||
}) |
|
||||||
require.NoError(t, err) |
|
||||||
|
|
||||||
// access unit with IDR
|
|
||||||
d = 6 * time.Second |
|
||||||
err = m.WriteH26x(testTime.Add(d-1*time.Second), d, [][]byte{ |
|
||||||
{5}, // IDR
|
|
||||||
}) |
|
||||||
require.NoError(t, err) |
|
||||||
|
|
||||||
// access unit with IDR
|
|
||||||
d = 7 * time.Second |
|
||||||
err = m.WriteH26x(testTime.Add(d-1*time.Second), d, [][]byte{ |
|
||||||
{5}, // IDR
|
|
||||||
}) |
|
||||||
require.NoError(t, err) |
|
||||||
|
|
||||||
byts, err := io.ReadAll(m.File("index.m3u8", "", "", "").Body) |
|
||||||
require.NoError(t, err) |
|
||||||
|
|
||||||
switch ca { |
|
||||||
case "mpegts": |
|
||||||
require.Equal(t, "#EXTM3U\n"+ |
|
||||||
"#EXT-X-VERSION:3\n"+ |
|
||||||
"#EXT-X-INDEPENDENT-SEGMENTS\n"+ |
|
||||||
"\n"+ |
|
||||||
"#EXT-X-STREAM-INF:BANDWIDTH=200000,CODECS=\"avc1.42c028,mp4a.40.2\"\n"+ |
|
||||||
"stream.m3u8\n", string(byts)) |
|
||||||
|
|
||||||
case "fmp4", "lowLatency": |
|
||||||
require.Equal(t, "#EXTM3U\n"+ |
|
||||||
"#EXT-X-VERSION:9\n"+ |
|
||||||
"#EXT-X-INDEPENDENT-SEGMENTS\n"+ |
|
||||||
"\n"+ |
|
||||||
"#EXT-X-STREAM-INF:BANDWIDTH=200000,CODECS=\"avc1.42c028,mp4a.40.2\"\n"+ |
|
||||||
"stream.m3u8\n", string(byts)) |
|
||||||
} |
|
||||||
|
|
||||||
byts, err = io.ReadAll(m.File("stream.m3u8", "", "", "").Body) |
|
||||||
require.NoError(t, err) |
|
||||||
|
|
||||||
switch ca { |
|
||||||
case "mpegts": |
|
||||||
re := regexp.MustCompile(`^#EXTM3U\n` + |
|
||||||
`#EXT-X-VERSION:3\n` + |
|
||||||
`#EXT-X-ALLOW-CACHE:NO\n` + |
|
||||||
`#EXT-X-TARGETDURATION:4\n` + |
|
||||||
`#EXT-X-MEDIA-SEQUENCE:0\n` + |
|
||||||
`#EXT-X-PROGRAM-DATE-TIME:(.*?)\n` + |
|
||||||
`#EXTINF:4,\n` + |
|
||||||
`(seg0\.ts)\n` + |
|
||||||
`#EXT-X-PROGRAM-DATE-TIME:(.*?)\n` + |
|
||||||
`#EXTINF:1,\n` + |
|
||||||
`(seg1\.ts)\n$`) |
|
||||||
ma := re.FindStringSubmatch(string(byts)) |
|
||||||
require.NotEqual(t, 0, len(ma)) |
|
||||||
|
|
||||||
seg := m.File(ma[2], "", "", "") |
|
||||||
require.Equal(t, http.StatusOK, seg.Status) |
|
||||||
_, err := io.ReadAll(seg.Body) |
|
||||||
require.NoError(t, err) |
|
||||||
|
|
||||||
case "fmp4": |
|
||||||
re := regexp.MustCompile(`^#EXTM3U\n` + |
|
||||||
`#EXT-X-VERSION:9\n` + |
|
||||||
`#EXT-X-TARGETDURATION:4\n` + |
|
||||||
`#EXT-X-MEDIA-SEQUENCE:0\n` + |
|
||||||
`#EXT-X-MAP:URI="init.mp4"\n` + |
|
||||||
`#EXT-X-PROGRAM-DATE-TIME:(.*?)\n` + |
|
||||||
`#EXTINF:4.00000,\n` + |
|
||||||
`(seg0\.mp4)\n` + |
|
||||||
`#EXT-X-PROGRAM-DATE-TIME:(.*?)\n` + |
|
||||||
`#EXTINF:1.00000,\n` + |
|
||||||
`(seg1\.mp4)\n$`) |
|
||||||
ma := re.FindStringSubmatch(string(byts)) |
|
||||||
require.NotEqual(t, 0, len(ma)) |
|
||||||
|
|
||||||
init := m.File("init.mp4", "", "", "") |
|
||||||
require.Equal(t, http.StatusOK, init.Status) |
|
||||||
_, err := io.ReadAll(init.Body) |
|
||||||
require.NoError(t, err) |
|
||||||
|
|
||||||
seg := m.File(ma[2], "", "", "") |
|
||||||
require.Equal(t, http.StatusOK, seg.Status) |
|
||||||
_, err = io.ReadAll(seg.Body) |
|
||||||
require.NoError(t, err) |
|
||||||
|
|
||||||
case "lowLatency": |
|
||||||
require.Equal(t, |
|
||||||
"#EXTM3U\n"+ |
|
||||||
"#EXT-X-VERSION:9\n"+ |
|
||||||
"#EXT-X-TARGETDURATION:4\n"+ |
|
||||||
"#EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,PART-HOLD-BACK=5.00000,CAN-SKIP-UNTIL=24\n"+ |
|
||||||
"#EXT-X-PART-INF:PART-TARGET=2\n"+ |
|
||||||
"#EXT-X-MEDIA-SEQUENCE:2\n"+ |
|
||||||
"#EXT-X-MAP:URI=\"init.mp4\"\n"+ |
|
||||||
"#EXT-X-GAP\n"+ |
|
||||||
"#EXTINF:4.00000,\n"+ |
|
||||||
"gap.mp4\n"+ |
|
||||||
"#EXT-X-GAP\n"+ |
|
||||||
"#EXTINF:4.00000,\n"+ |
|
||||||
"gap.mp4\n"+ |
|
||||||
"#EXT-X-GAP\n"+ |
|
||||||
"#EXTINF:4.00000,\n"+ |
|
||||||
"gap.mp4\n"+ |
|
||||||
"#EXT-X-GAP\n"+ |
|
||||||
"#EXTINF:4.00000,\n"+ |
|
||||||
"gap.mp4\n"+ |
|
||||||
"#EXT-X-GAP\n"+ |
|
||||||
"#EXTINF:4.00000,\n"+ |
|
||||||
"gap.mp4\n"+ |
|
||||||
"#EXT-X-PROGRAM-DATE-TIME:2010-01-01T01:01:02Z\n"+ |
|
||||||
"#EXT-X-PART:DURATION=2.00000,URI=\"part0.mp4\",INDEPENDENT=YES\n"+ |
|
||||||
"#EXT-X-PART:DURATION=2.00000,URI=\"part1.mp4\"\n"+ |
|
||||||
"#EXTINF:4.00000,\n"+ |
|
||||||
"seg7.mp4\n"+ |
|
||||||
"#EXT-X-PROGRAM-DATE-TIME:2010-01-01T01:01:06Z\n"+ |
|
||||||
"#EXT-X-PART:DURATION=1.00000,URI=\"part3.mp4\",INDEPENDENT=YES\n"+ |
|
||||||
"#EXTINF:1.00000,\n"+ |
|
||||||
"seg8.mp4\n"+ |
|
||||||
"#EXT-X-PRELOAD-HINT:TYPE=PART,URI=\"part4.mp4\"\n", string(byts)) |
|
||||||
|
|
||||||
part := m.File("part3.mp4", "", "", "") |
|
||||||
require.Equal(t, http.StatusOK, part.Status) |
|
||||||
_, err = io.ReadAll(part.Body) |
|
||||||
require.NoError(t, err) |
|
||||||
|
|
||||||
recv := make(chan struct{}) |
|
||||||
|
|
||||||
go func() { |
|
||||||
part = m.File("part4.mp4", "", "", "") |
|
||||||
_, err := io.ReadAll(part.Body) |
|
||||||
require.NoError(t, err) |
|
||||||
close(recv) |
|
||||||
}() |
|
||||||
|
|
||||||
d = 9 * time.Second |
|
||||||
err = m.WriteH26x(testTime.Add(d-1*time.Second), d, [][]byte{ |
|
||||||
{1}, // non-IDR
|
|
||||||
}) |
|
||||||
require.NoError(t, err) |
|
||||||
|
|
||||||
<-recv |
|
||||||
} |
|
||||||
}) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
func TestMuxerVideoOnly(t *testing.T) { |
|
||||||
videoTrack := &format.H264{ |
|
||||||
PayloadTyp: 96, |
|
||||||
SPS: testSPS, |
|
||||||
PPS: []byte{0x08}, |
|
||||||
PacketizationMode: 1, |
|
||||||
} |
|
||||||
|
|
||||||
for _, ca := range []string{ |
|
||||||
"mpegts", |
|
||||||
"fmp4", |
|
||||||
} { |
|
||||||
t.Run(ca, func(t *testing.T) { |
|
||||||
var v MuxerVariant |
|
||||||
if ca == "mpegts" { |
|
||||||
v = MuxerVariantMPEGTS |
|
||||||
} else { |
|
||||||
v = MuxerVariantFMP4 |
|
||||||
} |
|
||||||
|
|
||||||
m, err := NewMuxer(v, 3, 1*time.Second, 0, 50*1024*1024, videoTrack, nil) |
|
||||||
require.NoError(t, err) |
|
||||||
defer m.Close() |
|
||||||
|
|
||||||
// access unit with IDR
|
|
||||||
d := 2 * time.Second |
|
||||||
err = m.WriteH26x(testTime.Add(d-2*time.Second), d, [][]byte{ |
|
||||||
testSPS, // SPS
|
|
||||||
{8}, // PPS
|
|
||||||
{5}, // IDR
|
|
||||||
}) |
|
||||||
require.NoError(t, err) |
|
||||||
|
|
||||||
// access unit with IDR
|
|
||||||
d = 6 * time.Second |
|
||||||
err = m.WriteH26x(testTime.Add(d-2*time.Second), d, [][]byte{ |
|
||||||
{5}, // IDR
|
|
||||||
}) |
|
||||||
require.NoError(t, err) |
|
||||||
|
|
||||||
// access unit with IDR
|
|
||||||
d = 7 * time.Second |
|
||||||
err = m.WriteH26x(testTime.Add(d-2*time.Second), d, [][]byte{ |
|
||||||
{5}, // IDR
|
|
||||||
}) |
|
||||||
require.NoError(t, err) |
|
||||||
|
|
||||||
byts, err := io.ReadAll(m.File("index.m3u8", "", "", "").Body) |
|
||||||
require.NoError(t, err) |
|
||||||
|
|
||||||
if ca == "mpegts" { |
|
||||||
require.Equal(t, "#EXTM3U\n"+ |
|
||||||
"#EXT-X-VERSION:3\n"+ |
|
||||||
"#EXT-X-INDEPENDENT-SEGMENTS\n"+ |
|
||||||
"\n"+ |
|
||||||
"#EXT-X-STREAM-INF:BANDWIDTH=200000,CODECS=\"avc1.42c028\"\n"+ |
|
||||||
"stream.m3u8\n", string(byts)) |
|
||||||
} else { |
|
||||||
require.Equal(t, "#EXTM3U\n"+ |
|
||||||
"#EXT-X-VERSION:9\n"+ |
|
||||||
"#EXT-X-INDEPENDENT-SEGMENTS\n"+ |
|
||||||
"\n"+ |
|
||||||
"#EXT-X-STREAM-INF:BANDWIDTH=200000,CODECS=\"avc1.42c028\"\n"+ |
|
||||||
"stream.m3u8\n", string(byts)) |
|
||||||
} |
|
||||||
|
|
||||||
byts, err = io.ReadAll(m.File("stream.m3u8", "", "", "").Body) |
|
||||||
require.NoError(t, err) |
|
||||||
|
|
||||||
var ma []string |
|
||||||
if ca == "mpegts" { |
|
||||||
re := regexp.MustCompile(`^#EXTM3U\n` + |
|
||||||
`#EXT-X-VERSION:3\n` + |
|
||||||
`#EXT-X-ALLOW-CACHE:NO\n` + |
|
||||||
`#EXT-X-TARGETDURATION:4\n` + |
|
||||||
`#EXT-X-MEDIA-SEQUENCE:0\n` + |
|
||||||
`#EXT-X-PROGRAM-DATE-TIME:(.*?)\n` + |
|
||||||
`#EXTINF:4,\n` + |
|
||||||
`(seg0\.ts)\n` + |
|
||||||
`#EXT-X-PROGRAM-DATE-TIME:(.*?)\n` + |
|
||||||
`#EXTINF:1,\n` + |
|
||||||
`(seg1\.ts)\n$`) |
|
||||||
ma = re.FindStringSubmatch(string(byts)) |
|
||||||
} else { |
|
||||||
re := regexp.MustCompile(`^#EXTM3U\n` + |
|
||||||
`#EXT-X-VERSION:9\n` + |
|
||||||
`#EXT-X-TARGETDURATION:4\n` + |
|
||||||
`#EXT-X-MEDIA-SEQUENCE:0\n` + |
|
||||||
`#EXT-X-MAP:URI="init.mp4"\n` + |
|
||||||
`#EXT-X-PROGRAM-DATE-TIME:(.*?)\n` + |
|
||||||
`#EXTINF:4.00000,\n` + |
|
||||||
`(seg0\.mp4)\n` + |
|
||||||
`#EXT-X-PROGRAM-DATE-TIME:(.*?)\n` + |
|
||||||
`#EXTINF:1.00000,\n` + |
|
||||||
`(seg1\.mp4)\n$`) |
|
||||||
ma = re.FindStringSubmatch(string(byts)) |
|
||||||
} |
|
||||||
require.NotEqual(t, 0, len(ma)) |
|
||||||
|
|
||||||
if ca == "mpegts" { |
|
||||||
_, err := io.ReadAll(m.File(ma[2], "", "", "").Body) |
|
||||||
require.NoError(t, err) |
|
||||||
} else { |
|
||||||
_, err := io.ReadAll(m.File("init.mp4", "", "", "").Body) |
|
||||||
require.NoError(t, err) |
|
||||||
|
|
||||||
_, err = io.ReadAll(m.File(ma[2], "", "", "").Body) |
|
||||||
require.NoError(t, err) |
|
||||||
} |
|
||||||
}) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
func TestMuxerAudioOnly(t *testing.T) { |
|
||||||
audioTrack := &format.MPEG4Audio{ |
|
||||||
PayloadTyp: 97, |
|
||||||
Config: &mpeg4audio.Config{ |
|
||||||
Type: 2, |
|
||||||
SampleRate: 44100, |
|
||||||
ChannelCount: 2, |
|
||||||
}, |
|
||||||
SizeLength: 13, |
|
||||||
IndexLength: 3, |
|
||||||
IndexDeltaLength: 3, |
|
||||||
} |
|
||||||
|
|
||||||
for _, ca := range []string{ |
|
||||||
"mpegts", |
|
||||||
"fmp4", |
|
||||||
} { |
|
||||||
t.Run(ca, func(t *testing.T) { |
|
||||||
var v MuxerVariant |
|
||||||
if ca == "mpegts" { |
|
||||||
v = MuxerVariantMPEGTS |
|
||||||
} else { |
|
||||||
v = MuxerVariantFMP4 |
|
||||||
} |
|
||||||
|
|
||||||
m, err := NewMuxer(v, 3, 1*time.Second, 0, 50*1024*1024, nil, audioTrack) |
|
||||||
require.NoError(t, err) |
|
||||||
defer m.Close() |
|
||||||
|
|
||||||
for i := 0; i < 100; i++ { |
|
||||||
d := 1 * time.Second |
|
||||||
err = m.WriteAudio(testTime.Add(d-1*time.Second), d, []byte{ |
|
||||||
0x01, 0x02, 0x03, 0x04, |
|
||||||
}) |
|
||||||
require.NoError(t, err) |
|
||||||
} |
|
||||||
|
|
||||||
d := 2 * time.Second |
|
||||||
err = m.WriteAudio(testTime.Add(d-1*time.Second), d, []byte{ |
|
||||||
0x01, 0x02, 0x03, 0x04, |
|
||||||
}) |
|
||||||
require.NoError(t, err) |
|
||||||
|
|
||||||
d = 3 * time.Second |
|
||||||
err = m.WriteAudio(testTime.Add(d-1*time.Second), d, []byte{ |
|
||||||
0x01, 0x02, 0x03, 0x04, |
|
||||||
}) |
|
||||||
require.NoError(t, err) |
|
||||||
|
|
||||||
byts, err := io.ReadAll(m.File("index.m3u8", "", "", "").Body) |
|
||||||
require.NoError(t, err) |
|
||||||
|
|
||||||
if ca == "mpegts" { |
|
||||||
require.Equal(t, "#EXTM3U\n"+ |
|
||||||
"#EXT-X-VERSION:3\n"+ |
|
||||||
"#EXT-X-INDEPENDENT-SEGMENTS\n"+ |
|
||||||
"\n"+ |
|
||||||
"#EXT-X-STREAM-INF:BANDWIDTH=200000,CODECS=\"mp4a.40.2\"\n"+ |
|
||||||
"stream.m3u8\n", string(byts)) |
|
||||||
} else { |
|
||||||
require.Equal(t, "#EXTM3U\n"+ |
|
||||||
"#EXT-X-VERSION:9\n"+ |
|
||||||
"#EXT-X-INDEPENDENT-SEGMENTS\n"+ |
|
||||||
"\n"+ |
|
||||||
"#EXT-X-STREAM-INF:BANDWIDTH=200000,CODECS=\"mp4a.40.2\"\n"+ |
|
||||||
"stream.m3u8\n", string(byts)) |
|
||||||
} |
|
||||||
|
|
||||||
byts, err = io.ReadAll(m.File("stream.m3u8", "", "", "").Body) |
|
||||||
require.NoError(t, err) |
|
||||||
|
|
||||||
var ma []string |
|
||||||
if ca == "mpegts" { |
|
||||||
re := regexp.MustCompile(`^#EXTM3U\n` + |
|
||||||
`#EXT-X-VERSION:3\n` + |
|
||||||
`#EXT-X-ALLOW-CACHE:NO\n` + |
|
||||||
`#EXT-X-TARGETDURATION:1\n` + |
|
||||||
`#EXT-X-MEDIA-SEQUENCE:0\n` + |
|
||||||
`#EXT-X-PROGRAM-DATE-TIME:(.*?)\n` + |
|
||||||
`#EXTINF:1,\n` + |
|
||||||
`(seg0\.ts)\n$`) |
|
||||||
ma = re.FindStringSubmatch(string(byts)) |
|
||||||
} else { |
|
||||||
re := regexp.MustCompile(`^#EXTM3U\n` + |
|
||||||
`#EXT-X-VERSION:9\n` + |
|
||||||
`#EXT-X-TARGETDURATION:2\n` + |
|
||||||
`#EXT-X-MEDIA-SEQUENCE:0\n` + |
|
||||||
`#EXT-X-MAP:URI="init.mp4"\n` + |
|
||||||
`#EXT-X-PROGRAM-DATE-TIME:(.*?)\n` + |
|
||||||
`#EXTINF:2.32200,\n` + |
|
||||||
`(seg0\.mp4)\n` + |
|
||||||
`#EXT-X-PROGRAM-DATE-TIME:(.*?)\n` + |
|
||||||
`#EXTINF:0.02322,\n` + |
|
||||||
`(seg1\.mp4)\n$`) |
|
||||||
ma = re.FindStringSubmatch(string(byts)) |
|
||||||
} |
|
||||||
require.NotEqual(t, 0, len(ma)) |
|
||||||
|
|
||||||
if ca == "mpegts" { |
|
||||||
_, err := io.ReadAll(m.File(ma[2], "", "", "").Body) |
|
||||||
require.NoError(t, err) |
|
||||||
} else { |
|
||||||
_, err := io.ReadAll(m.File("init.mp4", "", "", "").Body) |
|
||||||
require.NoError(t, err) |
|
||||||
|
|
||||||
_, err = io.ReadAll(m.File(ma[2], "", "", "").Body) |
|
||||||
require.NoError(t, err) |
|
||||||
} |
|
||||||
}) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
func TestMuxerCloseBeforeFirstSegmentReader(t *testing.T) { |
|
||||||
videoTrack := &format.H264{ |
|
||||||
PayloadTyp: 96, |
|
||||||
SPS: testSPS, |
|
||||||
PPS: []byte{0x08}, |
|
||||||
PacketizationMode: 1, |
|
||||||
} |
|
||||||
|
|
||||||
m, err := NewMuxer(MuxerVariantMPEGTS, 3, 1*time.Second, 0, 50*1024*1024, videoTrack, nil) |
|
||||||
require.NoError(t, err) |
|
||||||
|
|
||||||
// access unit with IDR
|
|
||||||
err = m.WriteH26x(testTime, 2*time.Second, [][]byte{ |
|
||||||
testSPS, // SPS
|
|
||||||
{8}, // PPS
|
|
||||||
{5}, // IDR
|
|
||||||
}) |
|
||||||
require.NoError(t, err) |
|
||||||
|
|
||||||
m.Close() |
|
||||||
|
|
||||||
b := m.File("stream.m3u8", "", "", "").Body |
|
||||||
require.Equal(t, nil, b) |
|
||||||
} |
|
||||||
|
|
||||||
func TestMuxerMaxSegmentSize(t *testing.T) { |
|
||||||
videoTrack := &format.H264{ |
|
||||||
PayloadTyp: 96, |
|
||||||
SPS: testSPS, |
|
||||||
PPS: []byte{0x08}, |
|
||||||
PacketizationMode: 1, |
|
||||||
} |
|
||||||
|
|
||||||
m, err := NewMuxer(MuxerVariantMPEGTS, 3, 1*time.Second, 0, 0, videoTrack, nil) |
|
||||||
require.NoError(t, err) |
|
||||||
defer m.Close() |
|
||||||
|
|
||||||
err = m.WriteH26x(testTime, 2*time.Second, [][]byte{ |
|
||||||
testSPS, |
|
||||||
{5}, // IDR
|
|
||||||
}) |
|
||||||
require.EqualError(t, err, "reached maximum segment size") |
|
||||||
} |
|
||||||
|
|
||||||
func TestMuxerDoubleRead(t *testing.T) { |
|
||||||
videoTrack := &format.H264{ |
|
||||||
PayloadTyp: 96, |
|
||||||
SPS: testSPS, |
|
||||||
PPS: []byte{0x08}, |
|
||||||
PacketizationMode: 1, |
|
||||||
} |
|
||||||
|
|
||||||
m, err := NewMuxer(MuxerVariantMPEGTS, 3, 1*time.Second, 0, 50*1024*1024, videoTrack, nil) |
|
||||||
require.NoError(t, err) |
|
||||||
defer m.Close() |
|
||||||
|
|
||||||
err = m.WriteH26x(testTime, 0, [][]byte{ |
|
||||||
testSPS, |
|
||||||
{5}, // IDR
|
|
||||||
{1}, |
|
||||||
}) |
|
||||||
require.NoError(t, err) |
|
||||||
|
|
||||||
err = m.WriteH26x(testTime, 2*time.Second, [][]byte{ |
|
||||||
{5}, // IDR
|
|
||||||
{2}, |
|
||||||
}) |
|
||||||
require.NoError(t, err) |
|
||||||
|
|
||||||
byts, err := io.ReadAll(m.File("stream.m3u8", "", "", "").Body) |
|
||||||
require.NoError(t, err) |
|
||||||
|
|
||||||
re := regexp.MustCompile(`^#EXTM3U\n` + |
|
||||||
`#EXT-X-VERSION:3\n` + |
|
||||||
`#EXT-X-ALLOW-CACHE:NO\n` + |
|
||||||
`#EXT-X-TARGETDURATION:2\n` + |
|
||||||
`#EXT-X-MEDIA-SEQUENCE:0\n` + |
|
||||||
`#EXT-X-PROGRAM-DATE-TIME:(.*?)\n` + |
|
||||||
`#EXTINF:2,\n` + |
|
||||||
`(seg0\.ts)\n$`) |
|
||||||
ma := re.FindStringSubmatch(string(byts)) |
|
||||||
require.NotEqual(t, 0, len(ma)) |
|
||||||
|
|
||||||
byts1, err := io.ReadAll(m.File(ma[2], "", "", "").Body) |
|
||||||
require.NoError(t, err) |
|
||||||
|
|
||||||
byts2, err := io.ReadAll(m.File(ma[2], "", "", "").Body) |
|
||||||
require.NoError(t, err) |
|
||||||
require.Equal(t, byts1, byts2) |
|
||||||
} |
|
||||||
|
|
||||||
func TestMuxerFMP4ZeroDuration(t *testing.T) { |
|
||||||
videoTrack := &format.H264{ |
|
||||||
PayloadTyp: 96, |
|
||||||
SPS: testSPS, |
|
||||||
PPS: []byte{0x08}, |
|
||||||
PacketizationMode: 1, |
|
||||||
} |
|
||||||
|
|
||||||
m, err := NewMuxer(MuxerVariantLowLatency, 3, 1*time.Second, 0, 50*1024*1024, videoTrack, nil) |
|
||||||
require.NoError(t, err) |
|
||||||
defer m.Close() |
|
||||||
|
|
||||||
err = m.WriteH26x(time.Now(), 0, [][]byte{ |
|
||||||
testSPS, // SPS
|
|
||||||
{8}, // PPS
|
|
||||||
{5}, // IDR
|
|
||||||
}) |
|
||||||
require.NoError(t, err) |
|
||||||
|
|
||||||
err = m.WriteH26x(time.Now(), 1*time.Nanosecond, [][]byte{ |
|
||||||
testSPS, // SPS
|
|
||||||
{8}, // PPS
|
|
||||||
{5}, // IDR
|
|
||||||
}) |
|
||||||
require.NoError(t, err) |
|
||||||
} |
|
||||||
@ -1,22 +0,0 @@ |
|||||||
package hls |
|
||||||
|
|
||||||
import ( |
|
||||||
"time" |
|
||||||
) |
|
||||||
|
|
||||||
// MuxerVariant is a muxer variant.
|
|
||||||
type MuxerVariant int |
|
||||||
|
|
||||||
// supported variants.
|
|
||||||
const ( |
|
||||||
MuxerVariantMPEGTS MuxerVariant = iota |
|
||||||
MuxerVariantFMP4 |
|
||||||
MuxerVariantLowLatency |
|
||||||
) |
|
||||||
|
|
||||||
type muxerVariant interface { |
|
||||||
close() |
|
||||||
writeH26x(time.Time, time.Duration, [][]byte) error |
|
||||||
writeAudio(time.Time, time.Duration, []byte) error |
|
||||||
file(name string, msn string, part string, skip string) *MuxerFileResponse |
|
||||||
} |
|
||||||
@ -1,163 +0,0 @@ |
|||||||
package hls |
|
||||||
|
|
||||||
import ( |
|
||||||
"bytes" |
|
||||||
"net/http" |
|
||||||
"sync" |
|
||||||
"time" |
|
||||||
|
|
||||||
"github.com/aler9/gortsplib/v2/pkg/format" |
|
||||||
|
|
||||||
"github.com/aler9/rtsp-simple-server/internal/hls/fmp4" |
|
||||||
) |
|
||||||
|
|
||||||
func extractVideoParams(track format.Format) [][]byte { |
|
||||||
switch ttrack := track.(type) { |
|
||||||
case *format.H264: |
|
||||||
params := make([][]byte, 2) |
|
||||||
params[0] = ttrack.SafeSPS() |
|
||||||
params[1] = ttrack.SafePPS() |
|
||||||
return params |
|
||||||
|
|
||||||
case *format.H265: |
|
||||||
params := make([][]byte, 3) |
|
||||||
params[0] = ttrack.SafeVPS() |
|
||||||
params[1] = ttrack.SafeSPS() |
|
||||||
params[2] = ttrack.SafePPS() |
|
||||||
return params |
|
||||||
|
|
||||||
default: |
|
||||||
return nil |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
func videoParamsEqual(p1 [][]byte, p2 [][]byte) bool { |
|
||||||
if len(p1) != len(p2) { |
|
||||||
return true |
|
||||||
} |
|
||||||
|
|
||||||
for i, p := range p1 { |
|
||||||
if !bytes.Equal(p2[i], p) { |
|
||||||
return false |
|
||||||
} |
|
||||||
} |
|
||||||
return true |
|
||||||
} |
|
||||||
|
|
||||||
type muxerVariantFMP4 struct { |
|
||||||
playlist *muxerVariantFMP4Playlist |
|
||||||
segmenter *muxerVariantFMP4Segmenter |
|
||||||
videoTrack format.Format |
|
||||||
audioTrack format.Format |
|
||||||
|
|
||||||
mutex sync.Mutex |
|
||||||
lastVideoParams [][]byte |
|
||||||
initContent []byte |
|
||||||
} |
|
||||||
|
|
||||||
func newMuxerVariantFMP4( |
|
||||||
lowLatency bool, |
|
||||||
segmentCount int, |
|
||||||
segmentDuration time.Duration, |
|
||||||
partDuration time.Duration, |
|
||||||
segmentMaxSize uint64, |
|
||||||
videoTrack format.Format, |
|
||||||
audioTrack format.Format, |
|
||||||
) *muxerVariantFMP4 { |
|
||||||
v := &muxerVariantFMP4{ |
|
||||||
videoTrack: videoTrack, |
|
||||||
audioTrack: audioTrack, |
|
||||||
} |
|
||||||
|
|
||||||
v.playlist = newMuxerVariantFMP4Playlist( |
|
||||||
lowLatency, |
|
||||||
segmentCount, |
|
||||||
videoTrack, |
|
||||||
audioTrack, |
|
||||||
) |
|
||||||
|
|
||||||
v.segmenter = newMuxerVariantFMP4Segmenter( |
|
||||||
lowLatency, |
|
||||||
segmentCount, |
|
||||||
segmentDuration, |
|
||||||
partDuration, |
|
||||||
segmentMaxSize, |
|
||||||
videoTrack, |
|
||||||
audioTrack, |
|
||||||
v.playlist.onSegmentFinalized, |
|
||||||
v.playlist.onPartFinalized, |
|
||||||
) |
|
||||||
|
|
||||||
return v |
|
||||||
} |
|
||||||
|
|
||||||
func (v *muxerVariantFMP4) close() { |
|
||||||
v.playlist.close() |
|
||||||
} |
|
||||||
|
|
||||||
func (v *muxerVariantFMP4) writeH26x(ntp time.Time, pts time.Duration, au [][]byte) error { |
|
||||||
return v.segmenter.writeH26x(ntp, pts, au) |
|
||||||
} |
|
||||||
|
|
||||||
func (v *muxerVariantFMP4) writeAudio(ntp time.Time, pts time.Duration, au []byte) error { |
|
||||||
return v.segmenter.writeAudio(ntp, pts, au) |
|
||||||
} |
|
||||||
|
|
||||||
func (v *muxerVariantFMP4) mustRegenerateInit() bool { |
|
||||||
if v.videoTrack == nil { |
|
||||||
return false |
|
||||||
} |
|
||||||
|
|
||||||
videoParams := extractVideoParams(v.videoTrack) |
|
||||||
if !videoParamsEqual(videoParams, v.lastVideoParams) { |
|
||||||
v.lastVideoParams = videoParams |
|
||||||
return true |
|
||||||
} |
|
||||||
|
|
||||||
return false |
|
||||||
} |
|
||||||
|
|
||||||
func (v *muxerVariantFMP4) file(name string, msn string, part string, skip string) *MuxerFileResponse { |
|
||||||
if name == "init.mp4" { |
|
||||||
v.mutex.Lock() |
|
||||||
defer v.mutex.Unlock() |
|
||||||
|
|
||||||
if v.initContent == nil || v.mustRegenerateInit() { |
|
||||||
init := fmp4.Init{} |
|
||||||
trackID := 1 |
|
||||||
|
|
||||||
if v.videoTrack != nil { |
|
||||||
init.Tracks = append(init.Tracks, &fmp4.InitTrack{ |
|
||||||
ID: trackID, |
|
||||||
TimeScale: 90000, |
|
||||||
Format: v.videoTrack, |
|
||||||
}) |
|
||||||
trackID++ |
|
||||||
} |
|
||||||
|
|
||||||
if v.audioTrack != nil { |
|
||||||
init.Tracks = append(init.Tracks, &fmp4.InitTrack{ |
|
||||||
ID: trackID, |
|
||||||
TimeScale: uint32(v.audioTrack.ClockRate()), |
|
||||||
Format: v.audioTrack, |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
var err error |
|
||||||
v.initContent, err = init.Marshal() |
|
||||||
if err != nil { |
|
||||||
return &MuxerFileResponse{Status: http.StatusNotFound} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
return &MuxerFileResponse{ |
|
||||||
Status: http.StatusOK, |
|
||||||
Header: map[string]string{ |
|
||||||
"Content-Type": "video/mp4", |
|
||||||
}, |
|
||||||
Body: bytes.NewReader(v.initContent), |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
return v.playlist.file(name, msn, part, skip) |
|
||||||
} |
|
||||||
@ -1,140 +0,0 @@ |
|||||||
package hls |
|
||||||
|
|
||||||
import ( |
|
||||||
"bytes" |
|
||||||
"io" |
|
||||||
"strconv" |
|
||||||
"time" |
|
||||||
|
|
||||||
"github.com/aler9/gortsplib/v2/pkg/codecs/mpeg4audio" |
|
||||||
"github.com/aler9/gortsplib/v2/pkg/format" |
|
||||||
|
|
||||||
"github.com/aler9/rtsp-simple-server/internal/hls/fmp4" |
|
||||||
) |
|
||||||
|
|
||||||
func fmp4PartName(id uint64) string { |
|
||||||
return "part" + strconv.FormatUint(id, 10) |
|
||||||
} |
|
||||||
|
|
||||||
type muxerVariantFMP4Part struct { |
|
||||||
videoTrack format.Format |
|
||||||
audioTrack format.Format |
|
||||||
id uint64 |
|
||||||
|
|
||||||
isIndependent bool |
|
||||||
videoSamples []*fmp4.PartSample |
|
||||||
audioSamples []*fmp4.PartSample |
|
||||||
content []byte |
|
||||||
renderedDuration time.Duration |
|
||||||
videoStartDTSFilled bool |
|
||||||
videoStartDTS time.Duration |
|
||||||
audioStartDTSFilled bool |
|
||||||
audioStartDTS time.Duration |
|
||||||
} |
|
||||||
|
|
||||||
func newMuxerVariantFMP4Part( |
|
||||||
videoTrack format.Format, |
|
||||||
audioTrack format.Format, |
|
||||||
id uint64, |
|
||||||
) *muxerVariantFMP4Part { |
|
||||||
p := &muxerVariantFMP4Part{ |
|
||||||
videoTrack: videoTrack, |
|
||||||
audioTrack: audioTrack, |
|
||||||
id: id, |
|
||||||
} |
|
||||||
|
|
||||||
if videoTrack == nil { |
|
||||||
p.isIndependent = true |
|
||||||
} |
|
||||||
|
|
||||||
return p |
|
||||||
} |
|
||||||
|
|
||||||
func (p *muxerVariantFMP4Part) name() string { |
|
||||||
return fmp4PartName(p.id) |
|
||||||
} |
|
||||||
|
|
||||||
func (p *muxerVariantFMP4Part) reader() io.Reader { |
|
||||||
return bytes.NewReader(p.content) |
|
||||||
} |
|
||||||
|
|
||||||
func (p *muxerVariantFMP4Part) duration() time.Duration { |
|
||||||
if p.videoTrack != nil { |
|
||||||
ret := uint64(0) |
|
||||||
for _, e := range p.videoSamples { |
|
||||||
ret += uint64(e.Duration) |
|
||||||
} |
|
||||||
return durationMp4ToGo(ret, 90000) |
|
||||||
} |
|
||||||
|
|
||||||
// use the sum of the default duration of all samples,
|
|
||||||
// not the real duration,
|
|
||||||
// otherwise on iPhone iOS the stream freezes.
|
|
||||||
return time.Duration(len(p.audioSamples)) * time.Second * |
|
||||||
time.Duration(mpeg4audio.SamplesPerAccessUnit) / time.Duration(p.audioTrack.ClockRate()) |
|
||||||
} |
|
||||||
|
|
||||||
func (p *muxerVariantFMP4Part) finalize() error { |
|
||||||
if p.videoSamples != nil || p.audioSamples != nil { |
|
||||||
part := fmp4.Part{} |
|
||||||
|
|
||||||
if p.videoSamples != nil { |
|
||||||
part.Tracks = append(part.Tracks, &fmp4.PartTrack{ |
|
||||||
ID: 1, |
|
||||||
BaseTime: durationGoToMp4(p.videoStartDTS, 90000), |
|
||||||
Samples: p.videoSamples, |
|
||||||
IsVideo: true, |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
if p.audioSamples != nil { |
|
||||||
var id int |
|
||||||
if p.videoTrack != nil { |
|
||||||
id = 2 |
|
||||||
} else { |
|
||||||
id = 1 |
|
||||||
} |
|
||||||
|
|
||||||
part.Tracks = append(part.Tracks, &fmp4.PartTrack{ |
|
||||||
ID: id, |
|
||||||
BaseTime: durationGoToMp4(p.audioStartDTS, uint32(p.audioTrack.ClockRate())), |
|
||||||
Samples: p.audioSamples, |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
var err error |
|
||||||
p.content, err = part.Marshal() |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
p.renderedDuration = p.duration() |
|
||||||
} |
|
||||||
|
|
||||||
p.videoSamples = nil |
|
||||||
p.audioSamples = nil |
|
||||||
|
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
func (p *muxerVariantFMP4Part) writeH264(sample *augmentedVideoSample) { |
|
||||||
if !p.videoStartDTSFilled { |
|
||||||
p.videoStartDTSFilled = true |
|
||||||
p.videoStartDTS = sample.dts |
|
||||||
} |
|
||||||
|
|
||||||
if !sample.IsNonSyncSample { |
|
||||||
p.isIndependent = true |
|
||||||
} |
|
||||||
|
|
||||||
p.videoSamples = append(p.videoSamples, &sample.PartSample) |
|
||||||
} |
|
||||||
|
|
||||||
func (p *muxerVariantFMP4Part) writeAudio(sample *augmentedAudioSample) { |
|
||||||
if !p.audioStartDTSFilled { |
|
||||||
p.audioStartDTSFilled = true |
|
||||||
p.audioStartDTS = sample.dts |
|
||||||
} |
|
||||||
|
|
||||||
p.audioSamples = append(p.audioSamples, &sample.PartSample) |
|
||||||
} |
|
||||||
@ -1,489 +0,0 @@ |
|||||||
package hls |
|
||||||
|
|
||||||
import ( |
|
||||||
"bytes" |
|
||||||
"io" |
|
||||||
"math" |
|
||||||
"net/http" |
|
||||||
"strconv" |
|
||||||
"strings" |
|
||||||
"sync" |
|
||||||
"time" |
|
||||||
|
|
||||||
"github.com/aler9/gortsplib/v2/pkg/format" |
|
||||||
) |
|
||||||
|
|
||||||
type muxerVariantFMP4SegmentOrGap interface { |
|
||||||
getRenderedDuration() time.Duration |
|
||||||
} |
|
||||||
|
|
||||||
type muxerVariantFMP4Gap struct { |
|
||||||
renderedDuration time.Duration |
|
||||||
} |
|
||||||
|
|
||||||
func (g muxerVariantFMP4Gap) getRenderedDuration() time.Duration { |
|
||||||
return g.renderedDuration |
|
||||||
} |
|
||||||
|
|
||||||
func targetDuration(segments []muxerVariantFMP4SegmentOrGap) uint { |
|
||||||
ret := uint(0) |
|
||||||
|
|
||||||
// EXTINF, when rounded to the nearest integer, must be <= EXT-X-TARGETDURATION
|
|
||||||
for _, sog := range segments { |
|
||||||
v := uint(math.Round(sog.getRenderedDuration().Seconds())) |
|
||||||
if v > ret { |
|
||||||
ret = v |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
return ret |
|
||||||
} |
|
||||||
|
|
||||||
func partTargetDuration( |
|
||||||
segments []muxerVariantFMP4SegmentOrGap, |
|
||||||
nextSegmentParts []*muxerVariantFMP4Part, |
|
||||||
) time.Duration { |
|
||||||
var ret time.Duration |
|
||||||
|
|
||||||
for _, sog := range segments { |
|
||||||
seg, ok := sog.(*muxerVariantFMP4Segment) |
|
||||||
if !ok { |
|
||||||
continue |
|
||||||
} |
|
||||||
|
|
||||||
for _, part := range seg.parts { |
|
||||||
if part.renderedDuration > ret { |
|
||||||
ret = part.renderedDuration |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
for _, part := range nextSegmentParts { |
|
||||||
if part.renderedDuration > ret { |
|
||||||
ret = part.renderedDuration |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
return ret |
|
||||||
} |
|
||||||
|
|
||||||
type muxerVariantFMP4Playlist struct { |
|
||||||
lowLatency bool |
|
||||||
segmentCount int |
|
||||||
videoTrack format.Format |
|
||||||
audioTrack format.Format |
|
||||||
|
|
||||||
mutex sync.Mutex |
|
||||||
cond *sync.Cond |
|
||||||
closed bool |
|
||||||
segments []muxerVariantFMP4SegmentOrGap |
|
||||||
segmentsByName map[string]*muxerVariantFMP4Segment |
|
||||||
segmentDeleteCount int |
|
||||||
parts []*muxerVariantFMP4Part |
|
||||||
partsByName map[string]*muxerVariantFMP4Part |
|
||||||
nextSegmentID uint64 |
|
||||||
nextSegmentParts []*muxerVariantFMP4Part |
|
||||||
nextPartID uint64 |
|
||||||
} |
|
||||||
|
|
||||||
func newMuxerVariantFMP4Playlist( |
|
||||||
lowLatency bool, |
|
||||||
segmentCount int, |
|
||||||
videoTrack format.Format, |
|
||||||
audioTrack format.Format, |
|
||||||
) *muxerVariantFMP4Playlist { |
|
||||||
p := &muxerVariantFMP4Playlist{ |
|
||||||
lowLatency: lowLatency, |
|
||||||
segmentCount: segmentCount, |
|
||||||
videoTrack: videoTrack, |
|
||||||
audioTrack: audioTrack, |
|
||||||
segmentsByName: make(map[string]*muxerVariantFMP4Segment), |
|
||||||
partsByName: make(map[string]*muxerVariantFMP4Part), |
|
||||||
} |
|
||||||
p.cond = sync.NewCond(&p.mutex) |
|
||||||
|
|
||||||
return p |
|
||||||
} |
|
||||||
|
|
||||||
func (p *muxerVariantFMP4Playlist) close() { |
|
||||||
func() { |
|
||||||
p.mutex.Lock() |
|
||||||
defer p.mutex.Unlock() |
|
||||||
p.closed = true |
|
||||||
}() |
|
||||||
|
|
||||||
p.cond.Broadcast() |
|
||||||
} |
|
||||||
|
|
||||||
func (p *muxerVariantFMP4Playlist) hasContent() bool { |
|
||||||
if p.lowLatency { |
|
||||||
return len(p.segments) >= 1 |
|
||||||
} |
|
||||||
return len(p.segments) >= 2 |
|
||||||
} |
|
||||||
|
|
||||||
func (p *muxerVariantFMP4Playlist) hasPart(segmentID uint64, partID uint64) bool { |
|
||||||
if !p.hasContent() { |
|
||||||
return false |
|
||||||
} |
|
||||||
|
|
||||||
for _, sop := range p.segments { |
|
||||||
seg, ok := sop.(*muxerVariantFMP4Segment) |
|
||||||
if !ok { |
|
||||||
continue |
|
||||||
} |
|
||||||
|
|
||||||
if segmentID != seg.id { |
|
||||||
continue |
|
||||||
} |
|
||||||
|
|
||||||
// If the Client requests a Part Index greater than that of the final
|
|
||||||
// Partial Segment of the Parent Segment, the Server MUST treat the
|
|
||||||
// request as one for Part Index 0 of the following Parent Segment.
|
|
||||||
if partID >= uint64(len(seg.parts)) { |
|
||||||
segmentID++ |
|
||||||
partID = 0 |
|
||||||
continue |
|
||||||
} |
|
||||||
|
|
||||||
return true |
|
||||||
} |
|
||||||
|
|
||||||
if segmentID != p.nextSegmentID { |
|
||||||
return false |
|
||||||
} |
|
||||||
|
|
||||||
if partID >= uint64(len(p.nextSegmentParts)) { |
|
||||||
return false |
|
||||||
} |
|
||||||
|
|
||||||
return true |
|
||||||
} |
|
||||||
|
|
||||||
func (p *muxerVariantFMP4Playlist) file(name string, msn string, part string, skip string) *MuxerFileResponse { |
|
||||||
switch { |
|
||||||
case name == "stream.m3u8": |
|
||||||
return p.playlistReader(msn, part, skip) |
|
||||||
|
|
||||||
case strings.HasSuffix(name, ".mp4"): |
|
||||||
return p.segmentReader(name) |
|
||||||
|
|
||||||
default: |
|
||||||
return &MuxerFileResponse{Status: http.StatusNotFound} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
func (p *muxerVariantFMP4Playlist) playlistReader(msn string, part string, skip string) *MuxerFileResponse { |
|
||||||
isDeltaUpdate := false |
|
||||||
|
|
||||||
if p.lowLatency { |
|
||||||
isDeltaUpdate = skip == "YES" || skip == "v2" |
|
||||||
|
|
||||||
var msnint uint64 |
|
||||||
if msn != "" { |
|
||||||
var err error |
|
||||||
msnint, err = strconv.ParseUint(msn, 10, 64) |
|
||||||
if err != nil { |
|
||||||
return &MuxerFileResponse{Status: http.StatusBadRequest} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
var partint uint64 |
|
||||||
if part != "" { |
|
||||||
var err error |
|
||||||
partint, err = strconv.ParseUint(part, 10, 64) |
|
||||||
if err != nil { |
|
||||||
return &MuxerFileResponse{Status: http.StatusBadRequest} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
if msn != "" { |
|
||||||
p.mutex.Lock() |
|
||||||
defer p.mutex.Unlock() |
|
||||||
|
|
||||||
// If the _HLS_msn is greater than the Media Sequence Number of the last
|
|
||||||
// Media Segment in the current Playlist plus two, or if the _HLS_part
|
|
||||||
// exceeds the last Partial Segment in the current Playlist by the
|
|
||||||
// Advance Part Limit, then the server SHOULD immediately return Bad
|
|
||||||
// Request, such as HTTP 400.
|
|
||||||
if msnint > (p.nextSegmentID + 1) { |
|
||||||
return &MuxerFileResponse{Status: http.StatusBadRequest} |
|
||||||
} |
|
||||||
|
|
||||||
for !p.closed && !p.hasPart(msnint, partint) { |
|
||||||
p.cond.Wait() |
|
||||||
} |
|
||||||
|
|
||||||
if p.closed { |
|
||||||
return &MuxerFileResponse{Status: http.StatusNotFound} |
|
||||||
} |
|
||||||
|
|
||||||
return &MuxerFileResponse{ |
|
||||||
Status: http.StatusOK, |
|
||||||
Header: map[string]string{ |
|
||||||
"Content-Type": `application/x-mpegURL`, |
|
||||||
}, |
|
||||||
Body: p.fullPlaylist(isDeltaUpdate), |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// part without msn is not supported.
|
|
||||||
if part != "" { |
|
||||||
return &MuxerFileResponse{Status: http.StatusBadRequest} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
p.mutex.Lock() |
|
||||||
defer p.mutex.Unlock() |
|
||||||
|
|
||||||
for !p.closed && !p.hasContent() { |
|
||||||
p.cond.Wait() |
|
||||||
} |
|
||||||
|
|
||||||
if p.closed { |
|
||||||
return &MuxerFileResponse{Status: http.StatusNotFound} |
|
||||||
} |
|
||||||
|
|
||||||
return &MuxerFileResponse{ |
|
||||||
Status: http.StatusOK, |
|
||||||
Header: map[string]string{ |
|
||||||
"Content-Type": `application/x-mpegURL`, |
|
||||||
}, |
|
||||||
Body: p.fullPlaylist(isDeltaUpdate), |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
func (p *muxerVariantFMP4Playlist) fullPlaylist(isDeltaUpdate bool) io.Reader { |
|
||||||
cnt := "#EXTM3U\n" |
|
||||||
cnt += "#EXT-X-VERSION:9\n" |
|
||||||
|
|
||||||
targetDuration := targetDuration(p.segments) |
|
||||||
cnt += "#EXT-X-TARGETDURATION:" + strconv.FormatUint(uint64(targetDuration), 10) + "\n" |
|
||||||
|
|
||||||
skipBoundary := float64(targetDuration * 6) |
|
||||||
|
|
||||||
if p.lowLatency { |
|
||||||
partTargetDuration := partTargetDuration(p.segments, p.nextSegmentParts) |
|
||||||
|
|
||||||
// The value is an enumerated-string whose value is YES if the server
|
|
||||||
// supports Blocking Playlist Reload
|
|
||||||
cnt += "#EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES" |
|
||||||
|
|
||||||
// The value is a decimal-floating-point number of seconds that
|
|
||||||
// indicates the server-recommended minimum distance from the end of
|
|
||||||
// the Playlist at which clients should begin to play or to which
|
|
||||||
// they should seek when playing in Low-Latency Mode. Its value MUST
|
|
||||||
// be at least twice the Part Target Duration. Its value SHOULD be
|
|
||||||
// at least three times the Part Target Duration.
|
|
||||||
cnt += ",PART-HOLD-BACK=" + strconv.FormatFloat((partTargetDuration).Seconds()*2.5, 'f', 5, 64) |
|
||||||
|
|
||||||
// Indicates that the Server can produce Playlist Delta Updates in
|
|
||||||
// response to the _HLS_skip Delivery Directive. Its value is the
|
|
||||||
// Skip Boundary, a decimal-floating-point number of seconds. The
|
|
||||||
// Skip Boundary MUST be at least six times the Target Duration.
|
|
||||||
cnt += ",CAN-SKIP-UNTIL=" + strconv.FormatFloat(skipBoundary, 'f', -1, 64) |
|
||||||
|
|
||||||
cnt += "\n" |
|
||||||
|
|
||||||
cnt += "#EXT-X-PART-INF:PART-TARGET=" + strconv.FormatFloat(partTargetDuration.Seconds(), 'f', -1, 64) + "\n" |
|
||||||
} |
|
||||||
|
|
||||||
cnt += "#EXT-X-MEDIA-SEQUENCE:" + strconv.FormatInt(int64(p.segmentDeleteCount), 10) + "\n" |
|
||||||
|
|
||||||
skipped := 0 |
|
||||||
|
|
||||||
if !isDeltaUpdate { |
|
||||||
cnt += "#EXT-X-MAP:URI=\"init.mp4\"\n" |
|
||||||
} else { |
|
||||||
var curDuration time.Duration |
|
||||||
shown := 0 |
|
||||||
for _, segment := range p.segments { |
|
||||||
curDuration += segment.getRenderedDuration() |
|
||||||
if curDuration.Seconds() >= skipBoundary { |
|
||||||
break |
|
||||||
} |
|
||||||
shown++ |
|
||||||
} |
|
||||||
skipped = len(p.segments) - shown |
|
||||||
cnt += "#EXT-X-SKIP:SKIPPED-SEGMENTS=" + strconv.FormatInt(int64(skipped), 10) + "\n" |
|
||||||
} |
|
||||||
|
|
||||||
for i, sog := range p.segments { |
|
||||||
if i < skipped { |
|
||||||
continue |
|
||||||
} |
|
||||||
|
|
||||||
switch seg := sog.(type) { |
|
||||||
case *muxerVariantFMP4Segment: |
|
||||||
if (len(p.segments) - i) <= 2 { |
|
||||||
cnt += "#EXT-X-PROGRAM-DATE-TIME:" + seg.startTime.Format("2006-01-02T15:04:05.999Z07:00") + "\n" |
|
||||||
} |
|
||||||
|
|
||||||
if p.lowLatency && (len(p.segments)-i) <= 2 { |
|
||||||
for _, part := range seg.parts { |
|
||||||
cnt += "#EXT-X-PART:DURATION=" + strconv.FormatFloat(part.renderedDuration.Seconds(), 'f', 5, 64) + |
|
||||||
",URI=\"" + part.name() + ".mp4\"" |
|
||||||
if part.isIndependent { |
|
||||||
cnt += ",INDEPENDENT=YES" |
|
||||||
} |
|
||||||
cnt += "\n" |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
cnt += "#EXTINF:" + strconv.FormatFloat(seg.renderedDuration.Seconds(), 'f', 5, 64) + ",\n" + |
|
||||||
seg.name + ".mp4\n" |
|
||||||
|
|
||||||
case *muxerVariantFMP4Gap: |
|
||||||
cnt += "#EXT-X-GAP\n" + |
|
||||||
"#EXTINF:" + strconv.FormatFloat(seg.renderedDuration.Seconds(), 'f', 5, 64) + ",\n" + |
|
||||||
"gap.mp4\n" |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
if p.lowLatency { |
|
||||||
for _, part := range p.nextSegmentParts { |
|
||||||
cnt += "#EXT-X-PART:DURATION=" + strconv.FormatFloat(part.renderedDuration.Seconds(), 'f', 5, 64) + |
|
||||||
",URI=\"" + part.name() + ".mp4\"" |
|
||||||
if part.isIndependent { |
|
||||||
cnt += ",INDEPENDENT=YES" |
|
||||||
} |
|
||||||
cnt += "\n" |
|
||||||
} |
|
||||||
|
|
||||||
// preload hint must always be present
|
|
||||||
// otherwise hls.js goes into a loop
|
|
||||||
cnt += "#EXT-X-PRELOAD-HINT:TYPE=PART,URI=\"" + fmp4PartName(p.nextPartID) + ".mp4\"\n" |
|
||||||
} |
|
||||||
|
|
||||||
return bytes.NewReader([]byte(cnt)) |
|
||||||
} |
|
||||||
|
|
||||||
func (p *muxerVariantFMP4Playlist) segmentReader(fname string) *MuxerFileResponse { |
|
||||||
switch { |
|
||||||
case strings.HasPrefix(fname, "seg"): |
|
||||||
base := strings.TrimSuffix(fname, ".mp4") |
|
||||||
|
|
||||||
p.mutex.Lock() |
|
||||||
segment, ok := p.segmentsByName[base] |
|
||||||
p.mutex.Unlock() |
|
||||||
|
|
||||||
if !ok { |
|
||||||
return &MuxerFileResponse{Status: http.StatusNotFound} |
|
||||||
} |
|
||||||
|
|
||||||
return &MuxerFileResponse{ |
|
||||||
Status: http.StatusOK, |
|
||||||
Header: map[string]string{ |
|
||||||
"Content-Type": "video/mp4", |
|
||||||
}, |
|
||||||
Body: segment.reader(), |
|
||||||
} |
|
||||||
|
|
||||||
case strings.HasPrefix(fname, "part"): |
|
||||||
base := strings.TrimSuffix(fname, ".mp4") |
|
||||||
|
|
||||||
p.mutex.Lock() |
|
||||||
part, ok := p.partsByName[base] |
|
||||||
nextPartID := p.nextPartID |
|
||||||
p.mutex.Unlock() |
|
||||||
|
|
||||||
if ok { |
|
||||||
return &MuxerFileResponse{ |
|
||||||
Status: http.StatusOK, |
|
||||||
Header: map[string]string{ |
|
||||||
"Content-Type": "video/mp4", |
|
||||||
}, |
|
||||||
Body: part.reader(), |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// EXT-X-PRELOAD-HINT support
|
|
||||||
nextPartName := fmp4PartName(p.nextPartID) |
|
||||||
if base == nextPartName { |
|
||||||
p.mutex.Lock() |
|
||||||
defer p.mutex.Unlock() |
|
||||||
|
|
||||||
for { |
|
||||||
if p.closed { |
|
||||||
break |
|
||||||
} |
|
||||||
|
|
||||||
if p.nextPartID > nextPartID { |
|
||||||
break |
|
||||||
} |
|
||||||
|
|
||||||
p.cond.Wait() |
|
||||||
} |
|
||||||
|
|
||||||
if p.closed { |
|
||||||
return &MuxerFileResponse{Status: http.StatusNotFound} |
|
||||||
} |
|
||||||
|
|
||||||
return &MuxerFileResponse{ |
|
||||||
Status: http.StatusOK, |
|
||||||
Header: map[string]string{ |
|
||||||
"Content-Type": "video/mp4", |
|
||||||
}, |
|
||||||
Body: p.partsByName[nextPartName].reader(), |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
return &MuxerFileResponse{Status: http.StatusNotFound} |
|
||||||
|
|
||||||
default: |
|
||||||
return &MuxerFileResponse{Status: http.StatusNotFound} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
func (p *muxerVariantFMP4Playlist) onSegmentFinalized(segment *muxerVariantFMP4Segment) { |
|
||||||
func() { |
|
||||||
p.mutex.Lock() |
|
||||||
defer p.mutex.Unlock() |
|
||||||
|
|
||||||
// add initial gaps, required by iOS LL-HLS
|
|
||||||
if p.lowLatency && len(p.segments) == 0 { |
|
||||||
for i := 0; i < 7; i++ { |
|
||||||
p.segments = append(p.segments, &muxerVariantFMP4Gap{ |
|
||||||
renderedDuration: segment.renderedDuration, |
|
||||||
}) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
p.segmentsByName[segment.name] = segment |
|
||||||
p.segments = append(p.segments, segment) |
|
||||||
p.nextSegmentID = segment.id + 1 |
|
||||||
p.nextSegmentParts = p.nextSegmentParts[:0] |
|
||||||
|
|
||||||
if len(p.segments) > p.segmentCount { |
|
||||||
toDelete := p.segments[0] |
|
||||||
|
|
||||||
if toDeleteSeg, ok := toDelete.(*muxerVariantFMP4Segment); ok { |
|
||||||
for _, part := range toDeleteSeg.parts { |
|
||||||
delete(p.partsByName, part.name()) |
|
||||||
} |
|
||||||
p.parts = p.parts[len(toDeleteSeg.parts):] |
|
||||||
|
|
||||||
delete(p.segmentsByName, toDeleteSeg.name) |
|
||||||
} |
|
||||||
|
|
||||||
p.segments = p.segments[1:] |
|
||||||
p.segmentDeleteCount++ |
|
||||||
} |
|
||||||
}() |
|
||||||
|
|
||||||
p.cond.Broadcast() |
|
||||||
} |
|
||||||
|
|
||||||
func (p *muxerVariantFMP4Playlist) onPartFinalized(part *muxerVariantFMP4Part) { |
|
||||||
func() { |
|
||||||
p.mutex.Lock() |
|
||||||
defer p.mutex.Unlock() |
|
||||||
|
|
||||||
p.partsByName[part.name()] = part |
|
||||||
p.parts = append(p.parts, part) |
|
||||||
p.nextSegmentParts = append(p.nextSegmentParts, part) |
|
||||||
p.nextPartID = part.id + 1 |
|
||||||
}() |
|
||||||
|
|
||||||
p.cond.Broadcast() |
|
||||||
} |
|
||||||
@ -1,186 +0,0 @@ |
|||||||
package hls |
|
||||||
|
|
||||||
import ( |
|
||||||
"fmt" |
|
||||||
"io" |
|
||||||
"strconv" |
|
||||||
"time" |
|
||||||
|
|
||||||
"github.com/aler9/gortsplib/v2/pkg/format" |
|
||||||
) |
|
||||||
|
|
||||||
type partsReader struct { |
|
||||||
parts []*muxerVariantFMP4Part |
|
||||||
curPart int |
|
||||||
curPos int |
|
||||||
} |
|
||||||
|
|
||||||
func (mbr *partsReader) Read(p []byte) (int, error) { |
|
||||||
n := 0 |
|
||||||
lenp := len(p) |
|
||||||
|
|
||||||
for { |
|
||||||
if mbr.curPart >= len(mbr.parts) { |
|
||||||
return n, io.EOF |
|
||||||
} |
|
||||||
|
|
||||||
copied := copy(p[n:], mbr.parts[mbr.curPart].content[mbr.curPos:]) |
|
||||||
mbr.curPos += copied |
|
||||||
n += copied |
|
||||||
|
|
||||||
if mbr.curPos == len(mbr.parts[mbr.curPart].content) { |
|
||||||
mbr.curPart++ |
|
||||||
mbr.curPos = 0 |
|
||||||
} |
|
||||||
|
|
||||||
if n == lenp { |
|
||||||
return n, nil |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
type muxerVariantFMP4Segment struct { |
|
||||||
lowLatency bool |
|
||||||
id uint64 |
|
||||||
startTime time.Time |
|
||||||
startDTS time.Duration |
|
||||||
segmentMaxSize uint64 |
|
||||||
videoTrack format.Format |
|
||||||
audioTrack format.Format |
|
||||||
genPartID func() uint64 |
|
||||||
onPartFinalized func(*muxerVariantFMP4Part) |
|
||||||
|
|
||||||
name string |
|
||||||
size uint64 |
|
||||||
parts []*muxerVariantFMP4Part |
|
||||||
currentPart *muxerVariantFMP4Part |
|
||||||
renderedDuration time.Duration |
|
||||||
} |
|
||||||
|
|
||||||
func newMuxerVariantFMP4Segment( |
|
||||||
lowLatency bool, |
|
||||||
id uint64, |
|
||||||
startTime time.Time, |
|
||||||
startDTS time.Duration, |
|
||||||
segmentMaxSize uint64, |
|
||||||
videoTrack format.Format, |
|
||||||
audioTrack format.Format, |
|
||||||
genPartID func() uint64, |
|
||||||
onPartFinalized func(*muxerVariantFMP4Part), |
|
||||||
) *muxerVariantFMP4Segment { |
|
||||||
s := &muxerVariantFMP4Segment{ |
|
||||||
lowLatency: lowLatency, |
|
||||||
id: id, |
|
||||||
startTime: startTime, |
|
||||||
startDTS: startDTS, |
|
||||||
segmentMaxSize: segmentMaxSize, |
|
||||||
videoTrack: videoTrack, |
|
||||||
audioTrack: audioTrack, |
|
||||||
genPartID: genPartID, |
|
||||||
onPartFinalized: onPartFinalized, |
|
||||||
name: "seg" + strconv.FormatUint(id, 10), |
|
||||||
} |
|
||||||
|
|
||||||
s.currentPart = newMuxerVariantFMP4Part( |
|
||||||
s.videoTrack, |
|
||||||
s.audioTrack, |
|
||||||
s.genPartID(), |
|
||||||
) |
|
||||||
|
|
||||||
return s |
|
||||||
} |
|
||||||
|
|
||||||
func (s *muxerVariantFMP4Segment) reader() io.Reader { |
|
||||||
return &partsReader{parts: s.parts} |
|
||||||
} |
|
||||||
|
|
||||||
func (s *muxerVariantFMP4Segment) getRenderedDuration() time.Duration { |
|
||||||
return s.renderedDuration |
|
||||||
} |
|
||||||
|
|
||||||
func (s *muxerVariantFMP4Segment) finalize( |
|
||||||
nextVideoSampleDTS time.Duration, |
|
||||||
) error { |
|
||||||
err := s.currentPart.finalize() |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
if s.currentPart.content != nil { |
|
||||||
s.onPartFinalized(s.currentPart) |
|
||||||
s.parts = append(s.parts, s.currentPart) |
|
||||||
} |
|
||||||
|
|
||||||
s.currentPart = nil |
|
||||||
|
|
||||||
if s.videoTrack != nil { |
|
||||||
s.renderedDuration = nextVideoSampleDTS - s.startDTS |
|
||||||
} else { |
|
||||||
s.renderedDuration = 0 |
|
||||||
for _, pa := range s.parts { |
|
||||||
s.renderedDuration += pa.renderedDuration |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
func (s *muxerVariantFMP4Segment) writeH264(sample *augmentedVideoSample, adjustedPartDuration time.Duration) error { |
|
||||||
size := uint64(len(sample.Payload)) |
|
||||||
if (s.size + size) > s.segmentMaxSize { |
|
||||||
return fmt.Errorf("reached maximum segment size") |
|
||||||
} |
|
||||||
s.size += size |
|
||||||
|
|
||||||
s.currentPart.writeH264(sample) |
|
||||||
|
|
||||||
// switch part
|
|
||||||
if s.lowLatency && |
|
||||||
s.currentPart.duration() >= adjustedPartDuration { |
|
||||||
err := s.currentPart.finalize() |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
s.parts = append(s.parts, s.currentPart) |
|
||||||
s.onPartFinalized(s.currentPart) |
|
||||||
|
|
||||||
s.currentPart = newMuxerVariantFMP4Part( |
|
||||||
s.videoTrack, |
|
||||||
s.audioTrack, |
|
||||||
s.genPartID(), |
|
||||||
) |
|
||||||
} |
|
||||||
|
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
func (s *muxerVariantFMP4Segment) writeAudio(sample *augmentedAudioSample, adjustedPartDuration time.Duration) error { |
|
||||||
size := uint64(len(sample.Payload)) |
|
||||||
if (s.size + size) > s.segmentMaxSize { |
|
||||||
return fmt.Errorf("reached maximum segment size") |
|
||||||
} |
|
||||||
s.size += size |
|
||||||
|
|
||||||
s.currentPart.writeAudio(sample) |
|
||||||
|
|
||||||
// switch part
|
|
||||||
if s.lowLatency && s.videoTrack == nil && |
|
||||||
s.currentPart.duration() >= adjustedPartDuration { |
|
||||||
err := s.currentPart.finalize() |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
s.parts = append(s.parts, s.currentPart) |
|
||||||
s.onPartFinalized(s.currentPart) |
|
||||||
|
|
||||||
s.currentPart = newMuxerVariantFMP4Part( |
|
||||||
s.videoTrack, |
|
||||||
s.audioTrack, |
|
||||||
s.genPartID(), |
|
||||||
) |
|
||||||
} |
|
||||||
|
|
||||||
return nil |
|
||||||
} |
|
||||||
@ -1,406 +0,0 @@ |
|||||||
package hls |
|
||||||
|
|
||||||
import ( |
|
||||||
"fmt" |
|
||||||
"time" |
|
||||||
|
|
||||||
"github.com/aler9/gortsplib/v2/pkg/codecs/h264" |
|
||||||
"github.com/aler9/gortsplib/v2/pkg/codecs/h265" |
|
||||||
"github.com/aler9/gortsplib/v2/pkg/format" |
|
||||||
|
|
||||||
"github.com/aler9/rtsp-simple-server/internal/hls/fmp4" |
|
||||||
) |
|
||||||
|
|
||||||
func partDurationIsCompatible(partDuration time.Duration, sampleDuration time.Duration) bool { |
|
||||||
if sampleDuration > partDuration { |
|
||||||
return false |
|
||||||
} |
|
||||||
|
|
||||||
f := (partDuration / sampleDuration) |
|
||||||
if (partDuration % sampleDuration) != 0 { |
|
||||||
f++ |
|
||||||
} |
|
||||||
f *= sampleDuration |
|
||||||
|
|
||||||
return partDuration > ((f * 85) / 100) |
|
||||||
} |
|
||||||
|
|
||||||
func partDurationIsCompatibleWithAll(partDuration time.Duration, sampleDurations map[time.Duration]struct{}) bool { |
|
||||||
for sd := range sampleDurations { |
|
||||||
if !partDurationIsCompatible(partDuration, sd) { |
|
||||||
return false |
|
||||||
} |
|
||||||
} |
|
||||||
return true |
|
||||||
} |
|
||||||
|
|
||||||
func findCompatiblePartDuration( |
|
||||||
minPartDuration time.Duration, |
|
||||||
sampleDurations map[time.Duration]struct{}, |
|
||||||
) time.Duration { |
|
||||||
i := minPartDuration |
|
||||||
for ; i < 5*time.Second; i += 5 * time.Millisecond { |
|
||||||
if partDurationIsCompatibleWithAll(i, sampleDurations) { |
|
||||||
break |
|
||||||
} |
|
||||||
} |
|
||||||
return i |
|
||||||
} |
|
||||||
|
|
||||||
type dtsExtractor interface { |
|
||||||
Extract([][]byte, time.Duration) (time.Duration, error) |
|
||||||
} |
|
||||||
|
|
||||||
func allocateDTSExtractor(track format.Format) dtsExtractor { |
|
||||||
switch track.(type) { |
|
||||||
case *format.H264: |
|
||||||
return h264.NewDTSExtractor() |
|
||||||
|
|
||||||
case *format.H265: |
|
||||||
return h265.NewDTSExtractor() |
|
||||||
} |
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
type augmentedVideoSample struct { |
|
||||||
fmp4.PartSample |
|
||||||
dts time.Duration |
|
||||||
ntp time.Time |
|
||||||
} |
|
||||||
|
|
||||||
type augmentedAudioSample struct { |
|
||||||
fmp4.PartSample |
|
||||||
dts time.Duration |
|
||||||
ntp time.Time |
|
||||||
} |
|
||||||
|
|
||||||
type muxerVariantFMP4Segmenter struct { |
|
||||||
lowLatency bool |
|
||||||
segmentDuration time.Duration |
|
||||||
partDuration time.Duration |
|
||||||
segmentMaxSize uint64 |
|
||||||
videoTrack format.Format |
|
||||||
audioTrack format.Format |
|
||||||
onSegmentFinalized func(*muxerVariantFMP4Segment) |
|
||||||
onPartFinalized func(*muxerVariantFMP4Part) |
|
||||||
|
|
||||||
startDTS time.Duration |
|
||||||
videoFirstRandomAccessReceived bool |
|
||||||
videoDTSExtractor dtsExtractor |
|
||||||
lastVideoParams [][]byte |
|
||||||
currentSegment *muxerVariantFMP4Segment |
|
||||||
nextSegmentID uint64 |
|
||||||
nextPartID uint64 |
|
||||||
nextVideoSample *augmentedVideoSample |
|
||||||
nextAudioSample *augmentedAudioSample |
|
||||||
firstSegmentFinalized bool |
|
||||||
sampleDurations map[time.Duration]struct{} |
|
||||||
adjustedPartDuration time.Duration |
|
||||||
} |
|
||||||
|
|
||||||
func newMuxerVariantFMP4Segmenter( |
|
||||||
lowLatency bool, |
|
||||||
segmentCount int, |
|
||||||
segmentDuration time.Duration, |
|
||||||
partDuration time.Duration, |
|
||||||
segmentMaxSize uint64, |
|
||||||
videoTrack format.Format, |
|
||||||
audioTrack format.Format, |
|
||||||
onSegmentFinalized func(*muxerVariantFMP4Segment), |
|
||||||
onPartFinalized func(*muxerVariantFMP4Part), |
|
||||||
) *muxerVariantFMP4Segmenter { |
|
||||||
m := &muxerVariantFMP4Segmenter{ |
|
||||||
lowLatency: lowLatency, |
|
||||||
segmentDuration: segmentDuration, |
|
||||||
partDuration: partDuration, |
|
||||||
segmentMaxSize: segmentMaxSize, |
|
||||||
videoTrack: videoTrack, |
|
||||||
audioTrack: audioTrack, |
|
||||||
onSegmentFinalized: onSegmentFinalized, |
|
||||||
onPartFinalized: onPartFinalized, |
|
||||||
sampleDurations: make(map[time.Duration]struct{}), |
|
||||||
} |
|
||||||
|
|
||||||
// add initial gaps, required by iOS LL-HLS
|
|
||||||
if m.lowLatency { |
|
||||||
m.nextSegmentID = 7 |
|
||||||
} |
|
||||||
|
|
||||||
return m |
|
||||||
} |
|
||||||
|
|
||||||
func (m *muxerVariantFMP4Segmenter) genSegmentID() uint64 { |
|
||||||
id := m.nextSegmentID |
|
||||||
m.nextSegmentID++ |
|
||||||
return id |
|
||||||
} |
|
||||||
|
|
||||||
func (m *muxerVariantFMP4Segmenter) genPartID() uint64 { |
|
||||||
id := m.nextPartID |
|
||||||
m.nextPartID++ |
|
||||||
return id |
|
||||||
} |
|
||||||
|
|
||||||
// iPhone iOS fails if part durations are less than 85% of maximum part duration.
|
|
||||||
// find a part duration that is compatible with all received sample durations
|
|
||||||
func (m *muxerVariantFMP4Segmenter) adjustPartDuration(du time.Duration) { |
|
||||||
if !m.lowLatency || m.firstSegmentFinalized { |
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
// avoid a crash by skipping invalid durations
|
|
||||||
if du == 0 { |
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
if _, ok := m.sampleDurations[du]; !ok { |
|
||||||
m.sampleDurations[du] = struct{}{} |
|
||||||
m.adjustedPartDuration = findCompatiblePartDuration( |
|
||||||
m.partDuration, |
|
||||||
m.sampleDurations, |
|
||||||
) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
func (m *muxerVariantFMP4Segmenter) writeH26x(ntp time.Time, pts time.Duration, au [][]byte) error { |
|
||||||
randomAccessPresent := false |
|
||||||
|
|
||||||
switch m.videoTrack.(type) { |
|
||||||
case *format.H264: |
|
||||||
nonIDRPresent := false |
|
||||||
|
|
||||||
for _, nalu := range au { |
|
||||||
typ := h264.NALUType(nalu[0] & 0x1F) |
|
||||||
|
|
||||||
switch typ { |
|
||||||
case h264.NALUTypeIDR: |
|
||||||
randomAccessPresent = true |
|
||||||
|
|
||||||
case h264.NALUTypeNonIDR: |
|
||||||
nonIDRPresent = true |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
if !randomAccessPresent && !nonIDRPresent { |
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
case *format.H265: |
|
||||||
for _, nalu := range au { |
|
||||||
typ := h265.NALUType((nalu[0] >> 1) & 0b111111) |
|
||||||
|
|
||||||
switch typ { |
|
||||||
case h265.NALUType_IDR_W_RADL, h265.NALUType_IDR_N_LP, h265.NALUType_CRA_NUT: |
|
||||||
randomAccessPresent = true |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
return m.writeH26xEntry(ntp, pts, au, randomAccessPresent) |
|
||||||
} |
|
||||||
|
|
||||||
func (m *muxerVariantFMP4Segmenter) writeH26xEntry( |
|
||||||
ntp time.Time, |
|
||||||
pts time.Duration, |
|
||||||
au [][]byte, |
|
||||||
randomAccessPresent bool, |
|
||||||
) error { |
|
||||||
var dts time.Duration |
|
||||||
|
|
||||||
if !m.videoFirstRandomAccessReceived { |
|
||||||
// skip sample silently until we find one with an IDR
|
|
||||||
if !randomAccessPresent { |
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
m.videoFirstRandomAccessReceived = true |
|
||||||
m.videoDTSExtractor = allocateDTSExtractor(m.videoTrack) |
|
||||||
m.lastVideoParams = extractVideoParams(m.videoTrack) |
|
||||||
|
|
||||||
var err error |
|
||||||
dts, err = m.videoDTSExtractor.Extract(au, pts) |
|
||||||
if err != nil { |
|
||||||
return fmt.Errorf("unable to extract DTS: %v", err) |
|
||||||
} |
|
||||||
|
|
||||||
m.startDTS = dts |
|
||||||
dts = 0 |
|
||||||
pts -= m.startDTS |
|
||||||
} else { |
|
||||||
var err error |
|
||||||
dts, err = m.videoDTSExtractor.Extract(au, pts) |
|
||||||
if err != nil { |
|
||||||
return fmt.Errorf("unable to extract DTS: %v", err) |
|
||||||
} |
|
||||||
|
|
||||||
dts -= m.startDTS |
|
||||||
pts -= m.startDTS |
|
||||||
} |
|
||||||
|
|
||||||
avcc, err := h264.AVCCMarshal(au) |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
sample := &augmentedVideoSample{ |
|
||||||
PartSample: fmp4.PartSample{ |
|
||||||
PTSOffset: int32(durationGoToMp4(pts-dts, 90000)), |
|
||||||
IsNonSyncSample: !randomAccessPresent, |
|
||||||
Payload: avcc, |
|
||||||
}, |
|
||||||
dts: dts, |
|
||||||
ntp: ntp, |
|
||||||
} |
|
||||||
|
|
||||||
// put samples into a queue in order to
|
|
||||||
// - compute sample duration
|
|
||||||
// - check if next sample is IDR
|
|
||||||
sample, m.nextVideoSample = m.nextVideoSample, sample |
|
||||||
if sample == nil { |
|
||||||
return nil |
|
||||||
} |
|
||||||
sample.Duration = uint32(durationGoToMp4(m.nextVideoSample.dts-sample.dts, 90000)) |
|
||||||
|
|
||||||
if m.currentSegment == nil { |
|
||||||
// create first segment
|
|
||||||
m.currentSegment = newMuxerVariantFMP4Segment( |
|
||||||
m.lowLatency, |
|
||||||
m.genSegmentID(), |
|
||||||
sample.ntp, |
|
||||||
sample.dts, |
|
||||||
m.segmentMaxSize, |
|
||||||
m.videoTrack, |
|
||||||
m.audioTrack, |
|
||||||
m.genPartID, |
|
||||||
m.onPartFinalized, |
|
||||||
) |
|
||||||
} |
|
||||||
|
|
||||||
m.adjustPartDuration(durationMp4ToGo(uint64(sample.Duration), 90000)) |
|
||||||
|
|
||||||
err = m.currentSegment.writeH264(sample, m.adjustedPartDuration) |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
// switch segment
|
|
||||||
if randomAccessPresent { |
|
||||||
videoParams := extractVideoParams(m.videoTrack) |
|
||||||
paramsChanged := !videoParamsEqual(m.lastVideoParams, videoParams) |
|
||||||
|
|
||||||
if (m.nextVideoSample.dts-m.currentSegment.startDTS) >= m.segmentDuration || |
|
||||||
paramsChanged { |
|
||||||
err := m.currentSegment.finalize(m.nextVideoSample.dts) |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
m.onSegmentFinalized(m.currentSegment) |
|
||||||
|
|
||||||
m.firstSegmentFinalized = true |
|
||||||
|
|
||||||
m.currentSegment = newMuxerVariantFMP4Segment( |
|
||||||
m.lowLatency, |
|
||||||
m.genSegmentID(), |
|
||||||
m.nextVideoSample.ntp, |
|
||||||
m.nextVideoSample.dts, |
|
||||||
m.segmentMaxSize, |
|
||||||
m.videoTrack, |
|
||||||
m.audioTrack, |
|
||||||
m.genPartID, |
|
||||||
m.onPartFinalized, |
|
||||||
) |
|
||||||
|
|
||||||
if paramsChanged { |
|
||||||
m.lastVideoParams = videoParams |
|
||||||
m.firstSegmentFinalized = false |
|
||||||
|
|
||||||
// reset adjusted part duration
|
|
||||||
m.sampleDurations = make(map[time.Duration]struct{}) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
func (m *muxerVariantFMP4Segmenter) writeAudio(ntp time.Time, dts time.Duration, au []byte) error { |
|
||||||
if m.videoTrack != nil { |
|
||||||
// wait for the video track
|
|
||||||
if !m.videoFirstRandomAccessReceived { |
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
dts -= m.startDTS |
|
||||||
if dts < 0 { |
|
||||||
return nil |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
sample := &augmentedAudioSample{ |
|
||||||
PartSample: fmp4.PartSample{ |
|
||||||
Payload: au, |
|
||||||
}, |
|
||||||
dts: dts, |
|
||||||
ntp: ntp, |
|
||||||
} |
|
||||||
|
|
||||||
// put samples into a queue in order to compute the sample duration
|
|
||||||
sample, m.nextAudioSample = m.nextAudioSample, sample |
|
||||||
if sample == nil { |
|
||||||
return nil |
|
||||||
} |
|
||||||
sample.Duration = uint32(durationGoToMp4(m.nextAudioSample.dts-sample.dts, uint32(m.audioTrack.ClockRate()))) |
|
||||||
|
|
||||||
if m.videoTrack == nil { |
|
||||||
if m.currentSegment == nil { |
|
||||||
// create first segment
|
|
||||||
m.currentSegment = newMuxerVariantFMP4Segment( |
|
||||||
m.lowLatency, |
|
||||||
m.genSegmentID(), |
|
||||||
sample.ntp, |
|
||||||
sample.dts, |
|
||||||
m.segmentMaxSize, |
|
||||||
m.videoTrack, |
|
||||||
m.audioTrack, |
|
||||||
m.genPartID, |
|
||||||
m.onPartFinalized, |
|
||||||
) |
|
||||||
} |
|
||||||
} else { |
|
||||||
// wait for the video track
|
|
||||||
if m.currentSegment == nil { |
|
||||||
return nil |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
err := m.currentSegment.writeAudio(sample, m.partDuration) |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
// switch segment
|
|
||||||
if m.videoTrack == nil && |
|
||||||
(m.nextAudioSample.dts-m.currentSegment.startDTS) >= m.segmentDuration { |
|
||||||
err := m.currentSegment.finalize(0) |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
m.onSegmentFinalized(m.currentSegment) |
|
||||||
|
|
||||||
m.firstSegmentFinalized = true |
|
||||||
|
|
||||||
m.currentSegment = newMuxerVariantFMP4Segment( |
|
||||||
m.lowLatency, |
|
||||||
m.genSegmentID(), |
|
||||||
m.nextAudioSample.ntp, |
|
||||||
m.nextAudioSample.dts, |
|
||||||
m.segmentMaxSize, |
|
||||||
m.videoTrack, |
|
||||||
m.audioTrack, |
|
||||||
m.genPartID, |
|
||||||
m.onPartFinalized, |
|
||||||
) |
|
||||||
} |
|
||||||
|
|
||||||
return nil |
|
||||||
} |
|
||||||
@ -1,73 +0,0 @@ |
|||||||
package hls |
|
||||||
|
|
||||||
import ( |
|
||||||
"fmt" |
|
||||||
"time" |
|
||||||
|
|
||||||
"github.com/aler9/gortsplib/v2/pkg/format" |
|
||||||
) |
|
||||||
|
|
||||||
type muxerVariantMPEGTS struct { |
|
||||||
playlist *muxerVariantMPEGTSPlaylist |
|
||||||
segmenter *muxerVariantMPEGTSSegmenter |
|
||||||
} |
|
||||||
|
|
||||||
func newMuxerVariantMPEGTS( |
|
||||||
segmentCount int, |
|
||||||
segmentDuration time.Duration, |
|
||||||
segmentMaxSize uint64, |
|
||||||
videoTrack format.Format, |
|
||||||
audioTrack format.Format, |
|
||||||
) (*muxerVariantMPEGTS, error) { |
|
||||||
var videoTrackH264 *format.H264 |
|
||||||
if videoTrack != nil { |
|
||||||
var ok bool |
|
||||||
videoTrackH264, ok = videoTrack.(*format.H264) |
|
||||||
if !ok { |
|
||||||
return nil, fmt.Errorf( |
|
||||||
"the MPEG-TS variant of HLS only supports H264 video. Use the fMP4 or Low-Latency variants instead") |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
var audioTrackMPEG4Audio *format.MPEG4Audio |
|
||||||
if audioTrack != nil { |
|
||||||
var ok bool |
|
||||||
audioTrackMPEG4Audio, ok = audioTrack.(*format.MPEG4Audio) |
|
||||||
if !ok { |
|
||||||
return nil, fmt.Errorf( |
|
||||||
"the MPEG-TS variant of HLS only supports MPEG4-audio. Use the fMP4 or Low-Latency variants instead") |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
v := &muxerVariantMPEGTS{} |
|
||||||
|
|
||||||
v.playlist = newMuxerVariantMPEGTSPlaylist(segmentCount) |
|
||||||
|
|
||||||
v.segmenter = newMuxerVariantMPEGTSSegmenter( |
|
||||||
segmentDuration, |
|
||||||
segmentMaxSize, |
|
||||||
videoTrackH264, |
|
||||||
audioTrackMPEG4Audio, |
|
||||||
func(seg *muxerVariantMPEGTSSegment) { |
|
||||||
v.playlist.pushSegment(seg) |
|
||||||
}, |
|
||||||
) |
|
||||||
|
|
||||||
return v, nil |
|
||||||
} |
|
||||||
|
|
||||||
func (v *muxerVariantMPEGTS) close() { |
|
||||||
v.playlist.close() |
|
||||||
} |
|
||||||
|
|
||||||
func (v *muxerVariantMPEGTS) writeH26x(ntp time.Time, pts time.Duration, nalus [][]byte) error { |
|
||||||
return v.segmenter.writeH264(ntp, pts, nalus) |
|
||||||
} |
|
||||||
|
|
||||||
func (v *muxerVariantMPEGTS) writeAudio(ntp time.Time, pts time.Duration, au []byte) error { |
|
||||||
return v.segmenter.writeAAC(ntp, pts, au) |
|
||||||
} |
|
||||||
|
|
||||||
func (v *muxerVariantMPEGTS) file(name string, msn string, part string, skip string) *MuxerFileResponse { |
|
||||||
return v.playlist.file(name) |
|
||||||
} |
|
||||||
@ -1,145 +0,0 @@ |
|||||||
package hls |
|
||||||
|
|
||||||
import ( |
|
||||||
"bytes" |
|
||||||
"io" |
|
||||||
"math" |
|
||||||
"net/http" |
|
||||||
"strconv" |
|
||||||
"strings" |
|
||||||
"sync" |
|
||||||
) |
|
||||||
|
|
||||||
type muxerVariantMPEGTSPlaylist struct { |
|
||||||
segmentCount int |
|
||||||
|
|
||||||
mutex sync.Mutex |
|
||||||
cond *sync.Cond |
|
||||||
closed bool |
|
||||||
segments []*muxerVariantMPEGTSSegment |
|
||||||
segmentByName map[string]*muxerVariantMPEGTSSegment |
|
||||||
segmentDeleteCount int |
|
||||||
} |
|
||||||
|
|
||||||
func newMuxerVariantMPEGTSPlaylist(segmentCount int) *muxerVariantMPEGTSPlaylist { |
|
||||||
p := &muxerVariantMPEGTSPlaylist{ |
|
||||||
segmentCount: segmentCount, |
|
||||||
segmentByName: make(map[string]*muxerVariantMPEGTSSegment), |
|
||||||
} |
|
||||||
p.cond = sync.NewCond(&p.mutex) |
|
||||||
|
|
||||||
return p |
|
||||||
} |
|
||||||
|
|
||||||
func (p *muxerVariantMPEGTSPlaylist) close() { |
|
||||||
func() { |
|
||||||
p.mutex.Lock() |
|
||||||
defer p.mutex.Unlock() |
|
||||||
p.closed = true |
|
||||||
}() |
|
||||||
|
|
||||||
p.cond.Broadcast() |
|
||||||
} |
|
||||||
|
|
||||||
func (p *muxerVariantMPEGTSPlaylist) file(name string) *MuxerFileResponse { |
|
||||||
switch { |
|
||||||
case name == "stream.m3u8": |
|
||||||
return p.playlistReader() |
|
||||||
|
|
||||||
case strings.HasSuffix(name, ".ts"): |
|
||||||
return p.segmentReader(name) |
|
||||||
|
|
||||||
default: |
|
||||||
return &MuxerFileResponse{Status: http.StatusNotFound} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
func (p *muxerVariantMPEGTSPlaylist) playlist() io.Reader { |
|
||||||
cnt := "#EXTM3U\n" |
|
||||||
cnt += "#EXT-X-VERSION:3\n" |
|
||||||
cnt += "#EXT-X-ALLOW-CACHE:NO\n" |
|
||||||
|
|
||||||
targetDuration := func() uint { |
|
||||||
ret := uint(0) |
|
||||||
|
|
||||||
// EXTINF, when rounded to the nearest integer, must be <= EXT-X-TARGETDURATION
|
|
||||||
for _, s := range p.segments { |
|
||||||
v2 := uint(math.Round(s.duration().Seconds())) |
|
||||||
if v2 > ret { |
|
||||||
ret = v2 |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
return ret |
|
||||||
}() |
|
||||||
cnt += "#EXT-X-TARGETDURATION:" + strconv.FormatUint(uint64(targetDuration), 10) + "\n" |
|
||||||
|
|
||||||
cnt += "#EXT-X-MEDIA-SEQUENCE:" + strconv.FormatInt(int64(p.segmentDeleteCount), 10) + "\n" |
|
||||||
|
|
||||||
for _, s := range p.segments { |
|
||||||
cnt += "#EXT-X-PROGRAM-DATE-TIME:" + s.startTime.Format("2006-01-02T15:04:05.999Z07:00") + "\n" + |
|
||||||
"#EXTINF:" + strconv.FormatFloat(s.duration().Seconds(), 'f', -1, 64) + ",\n" + |
|
||||||
s.name + ".ts\n" |
|
||||||
} |
|
||||||
|
|
||||||
return bytes.NewReader([]byte(cnt)) |
|
||||||
} |
|
||||||
|
|
||||||
func (p *muxerVariantMPEGTSPlaylist) playlistReader() *MuxerFileResponse { |
|
||||||
p.mutex.Lock() |
|
||||||
defer p.mutex.Unlock() |
|
||||||
|
|
||||||
if !p.closed && len(p.segments) == 0 { |
|
||||||
p.cond.Wait() |
|
||||||
} |
|
||||||
|
|
||||||
if p.closed { |
|
||||||
return &MuxerFileResponse{Status: http.StatusNotFound} |
|
||||||
} |
|
||||||
|
|
||||||
return &MuxerFileResponse{ |
|
||||||
Status: http.StatusOK, |
|
||||||
Header: map[string]string{ |
|
||||||
"Content-Type": `application/x-mpegURL`, |
|
||||||
}, |
|
||||||
Body: p.playlist(), |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
func (p *muxerVariantMPEGTSPlaylist) segmentReader(fname string) *MuxerFileResponse { |
|
||||||
base := strings.TrimSuffix(fname, ".ts") |
|
||||||
|
|
||||||
p.mutex.Lock() |
|
||||||
f, ok := p.segmentByName[base] |
|
||||||
p.mutex.Unlock() |
|
||||||
|
|
||||||
if !ok { |
|
||||||
return &MuxerFileResponse{Status: http.StatusNotFound} |
|
||||||
} |
|
||||||
|
|
||||||
return &MuxerFileResponse{ |
|
||||||
Status: http.StatusOK, |
|
||||||
Header: map[string]string{ |
|
||||||
"Content-Type": "video/MP2T", |
|
||||||
}, |
|
||||||
Body: f.reader(), |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
func (p *muxerVariantMPEGTSPlaylist) pushSegment(t *muxerVariantMPEGTSSegment) { |
|
||||||
func() { |
|
||||||
p.mutex.Lock() |
|
||||||
defer p.mutex.Unlock() |
|
||||||
|
|
||||||
p.segmentByName[t.name] = t |
|
||||||
p.segments = append(p.segments, t) |
|
||||||
|
|
||||||
if len(p.segments) > p.segmentCount { |
|
||||||
delete(p.segmentByName, p.segments[0].name) |
|
||||||
p.segments = p.segments[1:] |
|
||||||
p.segmentDeleteCount++ |
|
||||||
} |
|
||||||
}() |
|
||||||
|
|
||||||
p.cond.Broadcast() |
|
||||||
} |
|
||||||
@ -1,118 +0,0 @@ |
|||||||
package hls |
|
||||||
|
|
||||||
import ( |
|
||||||
"bytes" |
|
||||||
"fmt" |
|
||||||
"io" |
|
||||||
"strconv" |
|
||||||
"time" |
|
||||||
|
|
||||||
"github.com/aler9/gortsplib/v2/pkg/format" |
|
||||||
|
|
||||||
"github.com/aler9/rtsp-simple-server/internal/hls/mpegts" |
|
||||||
) |
|
||||||
|
|
||||||
type muxerVariantMPEGTSSegment struct { |
|
||||||
segmentMaxSize uint64 |
|
||||||
videoTrack *format.H264 |
|
||||||
audioTrack *format.MPEG4Audio |
|
||||||
writer *mpegts.Writer |
|
||||||
|
|
||||||
size uint64 |
|
||||||
startTime time.Time |
|
||||||
name string |
|
||||||
startDTS *time.Duration |
|
||||||
endDTS time.Duration |
|
||||||
audioAUCount int |
|
||||||
content []byte |
|
||||||
} |
|
||||||
|
|
||||||
func newMuxerVariantMPEGTSSegment( |
|
||||||
id uint64, |
|
||||||
startTime time.Time, |
|
||||||
segmentMaxSize uint64, |
|
||||||
videoTrack *format.H264, |
|
||||||
audioTrack *format.MPEG4Audio, |
|
||||||
writer *mpegts.Writer, |
|
||||||
) *muxerVariantMPEGTSSegment { |
|
||||||
t := &muxerVariantMPEGTSSegment{ |
|
||||||
segmentMaxSize: segmentMaxSize, |
|
||||||
videoTrack: videoTrack, |
|
||||||
audioTrack: audioTrack, |
|
||||||
writer: writer, |
|
||||||
startTime: startTime, |
|
||||||
name: "seg" + strconv.FormatUint(id, 10), |
|
||||||
} |
|
||||||
|
|
||||||
return t |
|
||||||
} |
|
||||||
|
|
||||||
func (t *muxerVariantMPEGTSSegment) duration() time.Duration { |
|
||||||
return t.endDTS - *t.startDTS |
|
||||||
} |
|
||||||
|
|
||||||
func (t *muxerVariantMPEGTSSegment) reader() io.Reader { |
|
||||||
return bytes.NewReader(t.content) |
|
||||||
} |
|
||||||
|
|
||||||
func (t *muxerVariantMPEGTSSegment) finalize(endDTS time.Duration) { |
|
||||||
t.endDTS = endDTS |
|
||||||
t.content = t.writer.GenerateSegment() |
|
||||||
} |
|
||||||
|
|
||||||
func (t *muxerVariantMPEGTSSegment) writeH264( |
|
||||||
pcr time.Duration, |
|
||||||
dts time.Duration, |
|
||||||
pts time.Duration, |
|
||||||
idrPresent bool, |
|
||||||
nalus [][]byte, |
|
||||||
) error { |
|
||||||
size := uint64(0) |
|
||||||
for _, nalu := range nalus { |
|
||||||
size += uint64(len(nalu)) |
|
||||||
} |
|
||||||
if (t.size + size) > t.segmentMaxSize { |
|
||||||
return fmt.Errorf("reached maximum segment size") |
|
||||||
} |
|
||||||
t.size += size |
|
||||||
|
|
||||||
err := t.writer.WriteH264(pcr, dts, pts, idrPresent, nalus) |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
if t.startDTS == nil { |
|
||||||
t.startDTS = &dts |
|
||||||
} |
|
||||||
t.endDTS = dts |
|
||||||
|
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
func (t *muxerVariantMPEGTSSegment) writeAAC( |
|
||||||
pcr time.Duration, |
|
||||||
pts time.Duration, |
|
||||||
au []byte, |
|
||||||
) error { |
|
||||||
size := uint64(len(au)) |
|
||||||
if (t.size + size) > t.segmentMaxSize { |
|
||||||
return fmt.Errorf("reached maximum segment size") |
|
||||||
} |
|
||||||
t.size += size |
|
||||||
|
|
||||||
err := t.writer.WriteAAC(pcr, pts, au) |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
if t.videoTrack == nil { |
|
||||||
t.audioAUCount++ |
|
||||||
|
|
||||||
if t.startDTS == nil { |
|
||||||
t.startDTS = &pts |
|
||||||
} |
|
||||||
t.endDTS = pts |
|
||||||
} |
|
||||||
|
|
||||||
return nil |
|
||||||
} |
|
||||||
@ -1,193 +0,0 @@ |
|||||||
package hls |
|
||||||
|
|
||||||
import ( |
|
||||||
"fmt" |
|
||||||
"time" |
|
||||||
|
|
||||||
"github.com/aler9/gortsplib/v2/pkg/codecs/h264" |
|
||||||
"github.com/aler9/gortsplib/v2/pkg/format" |
|
||||||
|
|
||||||
"github.com/aler9/rtsp-simple-server/internal/hls/mpegts" |
|
||||||
) |
|
||||||
|
|
||||||
const ( |
|
||||||
mpegtsSegmentMinAUCount = 100 |
|
||||||
) |
|
||||||
|
|
||||||
type muxerVariantMPEGTSSegmenter struct { |
|
||||||
segmentDuration time.Duration |
|
||||||
segmentMaxSize uint64 |
|
||||||
videoTrack *format.H264 |
|
||||||
audioTrack *format.MPEG4Audio |
|
||||||
onSegmentReady func(*muxerVariantMPEGTSSegment) |
|
||||||
|
|
||||||
writer *mpegts.Writer |
|
||||||
nextSegmentID uint64 |
|
||||||
currentSegment *muxerVariantMPEGTSSegment |
|
||||||
videoDTSExtractor *h264.DTSExtractor |
|
||||||
startPCR time.Time |
|
||||||
startDTS time.Duration |
|
||||||
} |
|
||||||
|
|
||||||
func newMuxerVariantMPEGTSSegmenter( |
|
||||||
segmentDuration time.Duration, |
|
||||||
segmentMaxSize uint64, |
|
||||||
videoTrack *format.H264, |
|
||||||
audioTrack *format.MPEG4Audio, |
|
||||||
onSegmentReady func(*muxerVariantMPEGTSSegment), |
|
||||||
) *muxerVariantMPEGTSSegmenter { |
|
||||||
m := &muxerVariantMPEGTSSegmenter{ |
|
||||||
segmentDuration: segmentDuration, |
|
||||||
segmentMaxSize: segmentMaxSize, |
|
||||||
videoTrack: videoTrack, |
|
||||||
audioTrack: audioTrack, |
|
||||||
onSegmentReady: onSegmentReady, |
|
||||||
} |
|
||||||
|
|
||||||
m.writer = mpegts.NewWriter( |
|
||||||
videoTrack, |
|
||||||
audioTrack) |
|
||||||
|
|
||||||
return m |
|
||||||
} |
|
||||||
|
|
||||||
func (m *muxerVariantMPEGTSSegmenter) genSegmentID() uint64 { |
|
||||||
id := m.nextSegmentID |
|
||||||
m.nextSegmentID++ |
|
||||||
return id |
|
||||||
} |
|
||||||
|
|
||||||
func (m *muxerVariantMPEGTSSegmenter) writeH264(ntp time.Time, pts time.Duration, nalus [][]byte) error { |
|
||||||
idrPresent := false |
|
||||||
nonIDRPresent := false |
|
||||||
|
|
||||||
for _, nalu := range nalus { |
|
||||||
typ := h264.NALUType(nalu[0] & 0x1F) |
|
||||||
switch typ { |
|
||||||
case h264.NALUTypeIDR: |
|
||||||
idrPresent = true |
|
||||||
|
|
||||||
case h264.NALUTypeNonIDR: |
|
||||||
nonIDRPresent = true |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
var dts time.Duration |
|
||||||
|
|
||||||
if m.currentSegment == nil { |
|
||||||
// skip groups silently until we find one with a IDR
|
|
||||||
if !idrPresent { |
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
m.videoDTSExtractor = h264.NewDTSExtractor() |
|
||||||
|
|
||||||
var err error |
|
||||||
dts, err = m.videoDTSExtractor.Extract(nalus, pts) |
|
||||||
if err != nil { |
|
||||||
return fmt.Errorf("unable to extract DTS: %v", err) |
|
||||||
} |
|
||||||
|
|
||||||
m.startPCR = ntp |
|
||||||
m.startDTS = dts |
|
||||||
dts = 0 |
|
||||||
pts -= m.startDTS |
|
||||||
|
|
||||||
// create first segment
|
|
||||||
m.currentSegment = newMuxerVariantMPEGTSSegment( |
|
||||||
m.genSegmentID(), |
|
||||||
ntp, |
|
||||||
m.segmentMaxSize, |
|
||||||
m.videoTrack, |
|
||||||
m.audioTrack, |
|
||||||
m.writer) |
|
||||||
} else { |
|
||||||
if !idrPresent && !nonIDRPresent { |
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
var err error |
|
||||||
dts, err = m.videoDTSExtractor.Extract(nalus, pts) |
|
||||||
if err != nil { |
|
||||||
return fmt.Errorf("unable to extract DTS: %v", err) |
|
||||||
} |
|
||||||
|
|
||||||
dts -= m.startDTS |
|
||||||
pts -= m.startDTS |
|
||||||
|
|
||||||
// switch segment
|
|
||||||
if idrPresent && |
|
||||||
(dts-*m.currentSegment.startDTS) >= m.segmentDuration { |
|
||||||
m.currentSegment.finalize(dts) |
|
||||||
m.onSegmentReady(m.currentSegment) |
|
||||||
m.currentSegment = newMuxerVariantMPEGTSSegment( |
|
||||||
m.genSegmentID(), |
|
||||||
ntp, |
|
||||||
m.segmentMaxSize, |
|
||||||
m.videoTrack, |
|
||||||
m.audioTrack, |
|
||||||
m.writer) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
err := m.currentSegment.writeH264( |
|
||||||
ntp.Sub(m.startPCR), |
|
||||||
dts, |
|
||||||
pts, |
|
||||||
idrPresent, |
|
||||||
nalus) |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
func (m *muxerVariantMPEGTSSegmenter) writeAAC(ntp time.Time, pts time.Duration, au []byte) error { |
|
||||||
if m.videoTrack == nil { |
|
||||||
if m.currentSegment == nil { |
|
||||||
m.startPCR = ntp |
|
||||||
m.startDTS = pts |
|
||||||
pts = 0 |
|
||||||
|
|
||||||
// create first segment
|
|
||||||
m.currentSegment = newMuxerVariantMPEGTSSegment( |
|
||||||
m.genSegmentID(), |
|
||||||
ntp, |
|
||||||
m.segmentMaxSize, |
|
||||||
m.videoTrack, |
|
||||||
m.audioTrack, |
|
||||||
m.writer) |
|
||||||
} else { |
|
||||||
pts -= m.startDTS |
|
||||||
|
|
||||||
// switch segment
|
|
||||||
if m.currentSegment.audioAUCount >= mpegtsSegmentMinAUCount && |
|
||||||
(pts-*m.currentSegment.startDTS) >= m.segmentDuration { |
|
||||||
m.currentSegment.finalize(pts) |
|
||||||
m.onSegmentReady(m.currentSegment) |
|
||||||
m.currentSegment = newMuxerVariantMPEGTSSegment( |
|
||||||
m.genSegmentID(), |
|
||||||
ntp, |
|
||||||
m.segmentMaxSize, |
|
||||||
m.videoTrack, |
|
||||||
m.audioTrack, |
|
||||||
m.writer) |
|
||||||
} |
|
||||||
} |
|
||||||
} else { |
|
||||||
// wait for the video track
|
|
||||||
if m.currentSegment == nil { |
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
pts -= m.startDTS |
|
||||||
} |
|
||||||
|
|
||||||
err := m.currentSegment.writeAAC(ntp.Sub(m.startPCR), pts, au) |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
return nil |
|
||||||
} |
|
||||||
Loading…
Reference in new issue