Browse Source

hls source: refactor client

pull/1198/head
aler9 3 years ago
parent
commit
31d9429c18
  1. 2
      go.mod
  2. 4
      go.sum
  3. 8
      internal/core/hls_source.go
  4. 509
      internal/hls/client.go
  5. 114
      internal/hls/client_audio_processor.go
  6. 278
      internal/hls/client_downloader.go
  7. 294
      internal/hls/client_processor_mpegts.go
  8. 83
      internal/hls/client_processor_mpegts_track.go
  9. 44
      internal/hls/client_routine_pool.go
  10. 12
      internal/hls/client_segment_queue.go
  11. 104
      internal/hls/client_video_processor.go
  12. 2
      internal/hls/fmp4/fmp4.go
  13. 104
      internal/hls/fmp4/init_read.go
  14. 90
      internal/hls/fmp4/init_write.go
  15. 8
      internal/hls/fmp4/init_write_test.go
  16. 23
      internal/hls/fmp4/mp4_writer.go
  17. 96
      internal/hls/fmp4/part_read.go
  18. 30
      internal/hls/fmp4/part_write.go
  19. 8
      internal/hls/fmp4/part_write_test.go
  20. 2
      internal/hls/muxer_variant_fmp4.go
  21. 2
      internal/hls/muxer_variant_fmp4_part.go

2
go.mod

@ -5,7 +5,7 @@ go 1.18 @@ -5,7 +5,7 @@ go 1.18
require (
code.cloudfoundry.org/bytefmt v0.0.0-20211005130812-5bb3c17173e5
github.com/abema/go-mp4 v0.7.2
github.com/aler9/gortsplib v0.0.0-20221008181346-b3c70f56f7a1
github.com/aler9/gortsplib v0.0.0-20221009091420-74f941be7166
github.com/asticode/go-astits v1.10.1-0.20220319093903-4abe66a9b757
github.com/fsnotify/fsnotify v1.4.9
github.com/gin-gonic/gin v1.8.1

4
go.sum

@ -6,8 +6,8 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafo @@ -6,8 +6,8 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafo
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d h1:UQZhZ2O0vMHr2cI+DC1Mbh0TJxzA3RcLoMsFw+aXw7E=
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
github.com/aler9/gortsplib v0.0.0-20221008181346-b3c70f56f7a1 h1:wMNW/KqUS6Mx5oF6vqtJoSeVC4jpOeE0443tr9pKMU8=
github.com/aler9/gortsplib v0.0.0-20221008181346-b3c70f56f7a1/go.mod h1:BOWNZ/QBkY/eVcRqUzJbPFEsRJshwxaxBT01K260Jeo=
github.com/aler9/gortsplib v0.0.0-20221009091420-74f941be7166 h1:E2fnA+6SAGY2/RobvgmpyLqj0p88M3tCrT5zyKBKiWo=
github.com/aler9/gortsplib v0.0.0-20221009091420-74f941be7166/go.mod h1:BOWNZ/QBkY/eVcRqUzJbPFEsRJshwxaxBT01K260Jeo=
github.com/aler9/writerseeker v0.0.0-20220601075008-6f0e685b9c82 h1:9WgSzBLo3a9ToSVV7sRTBYZ1GGOZUpq4+5H3SN0UZq4=
github.com/aler9/writerseeker v0.0.0-20220601075008-6f0e685b9c82/go.mod h1:qsMrZCbeBf/mCLOeF16KDkPu4gktn/pOWyaq1aYQE7U=
github.com/asticode/go-astikit v0.20.0 h1:+7N+J4E4lWx2QOkRdOf6DafWJMv6O4RRfgClwQokrH8=

8
internal/core/hls_source.go

@ -79,10 +79,6 @@ func (s *hlsSource) run(ctx context.Context) error { @@ -79,10 +79,6 @@ func (s *hlsSource) run(ctx context.Context) error {
}
onVideoData := func(pts time.Duration, nalus [][]byte) {
if stream == nil {
return
}
stream.writeData(&data{
trackID: videoTrackID,
ptsEqualsDTS: h264.IDRPresent(nalus),
@ -92,10 +88,6 @@ func (s *hlsSource) run(ctx context.Context) error { @@ -92,10 +88,6 @@ func (s *hlsSource) run(ctx context.Context) error {
}
onAudioData := func(pts time.Duration, au []byte) {
if stream == nil {
return
}
stream.writeData(&data{
trackID: audioTrackID,
ptsEqualsDTS: true,

509
internal/hls/client.go

@ -1,24 +1,13 @@ @@ -1,24 +1,13 @@
package hls
import (
"bytes"
"context"
"crypto/sha256"
"crypto/tls"
"encoding/hex"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"sync"
"time"
"github.com/aler9/gortsplib"
"github.com/asticode/go-astits"
"github.com/grafov/m3u8"
"github.com/aler9/rtsp-simple-server/internal/hls/mpegtstimedec"
"github.com/aler9/rtsp-simple-server/internal/logger"
)
@ -36,10 +25,6 @@ func clientURLAbsolute(base *url.URL, relative string) (*url.URL, error) { @@ -36,10 +25,6 @@ func clientURLAbsolute(base *url.URL, relative string) (*url.URL, error) {
return base.ResolveReference(u), nil
}
type clientAllocateProcsReq struct {
res chan struct{}
}
// ClientLogger allows to receive log lines.
type ClientLogger interface {
Log(level logger.Level, format string, args ...interface{})
@ -47,35 +32,15 @@ type ClientLogger interface { @@ -47,35 +32,15 @@ type ClientLogger interface {
// Client is a HLS client.
type Client struct {
fingerprint string
onTracks func(*gortsplib.TrackH264, *gortsplib.TrackMPEG4Audio) error
onVideoData func(time.Duration, [][]byte)
onAudioData func(time.Duration, []byte)
logger ClientLogger
ctx context.Context
ctxCancel func()
primaryPlaylistURL *url.URL
streamPlaylistURL *url.URL
httpClient *http.Client
lastDownloadTime time.Time
downloadedSegmentURIs []string
segmentQueue *clientSegmentQueue
pmtDownloaded bool
clockInitialized bool
timeDec *mpegtstimedec.Decoder
startDTS time.Duration
videoPID *uint16
audioPID *uint16
videoProc *clientVideoProcessor
audioProc *clientAudioProcessor
tracksMutex sync.RWMutex
videoTrack *gortsplib.TrackH264
audioTrack *gortsplib.TrackMPEG4Audio
// in
allocateProcs chan clientAllocateProcsReq
ctx context.Context
ctxCancel func()
primaryPlaylistURL *url.URL
// out
outErr chan error
@ -97,28 +62,8 @@ func NewClient( @@ -97,28 +62,8 @@ func NewClient(
ctx, ctxCancel := context.WithCancel(context.Background())
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
},
}
}
c := &Client{
fingerprint: fingerprint,
onTracks: onTracks,
onVideoData: onVideoData,
onAudioData: onAudioData,
@ -126,15 +71,7 @@ func NewClient( @@ -126,15 +71,7 @@ func NewClient(
ctx: ctx,
ctxCancel: ctxCancel,
primaryPlaylistURL: primaryPlaylistURL,
httpClient: &http.Client{
Transport: &http.Transport{
TLSClientConfig: tlsConfig,
},
},
segmentQueue: newClientSegmentQueue(),
timeDec: mpegtstimedec.New(),
allocateProcs: make(chan clientAllocateProcsReq),
outErr: make(chan error, 1),
outErr: make(chan error, 1),
}
go c.run()
@ -157,416 +94,28 @@ func (c *Client) run() { @@ -157,416 +94,28 @@ func (c *Client) run() {
}
func (c *Client) runInner() error {
innerCtx, innerCtxCancel := context.WithCancel(context.Background())
errChan := make(chan error)
go func() { errChan <- c.runDownloader(innerCtx) }()
go func() { errChan <- c.runProcessor(innerCtx) }()
for {
select {
case req := <-c.allocateProcs:
if c.videoPID != nil {
c.videoProc = newClientVideoProcessor(
innerCtx,
c.onVideoProcessorTrack,
c.onVideoProcessorData,
c.logger)
go func() { errChan <- c.videoProc.run() }()
}
if c.audioPID != nil {
c.audioProc = newClientAudioProcessor(
innerCtx,
c.onAudioProcessorTrack,
c.onAudioProcessorData)
go func() { errChan <- c.audioProc.run() }()
}
close(req.res)
case err := <-errChan:
innerCtxCancel()
<-errChan
if c.videoProc != nil {
<-errChan
}
if c.audioProc != nil {
<-errChan
}
return err
case <-c.ctx.Done():
innerCtxCancel()
<-errChan
<-errChan
if c.videoProc != nil {
<-errChan
}
if c.audioProc != nil {
<-errChan
}
return fmt.Errorf("terminated")
}
}
}
func (c *Client) runDownloader(innerCtx context.Context) error {
for {
c.segmentQueue.waitUntilSizeIsBelow(innerCtx, clientMinSegmentsBeforeDownloading)
_, err := c.fillSegmentQueue(innerCtx)
if err != nil {
return err
}
}
}
func (c *Client) fillSegmentQueue(innerCtx context.Context) (bool, error) {
minTime := c.lastDownloadTime.Add(clientMinDownloadPause)
now := time.Now()
if now.Before(minTime) {
select {
case <-time.After(minTime.Sub(now)):
case <-innerCtx.Done():
return false, fmt.Errorf("terminated")
}
}
c.lastDownloadTime = now
pl, err := func() (*m3u8.MediaPlaylist, error) {
if c.streamPlaylistURL == nil {
return c.downloadPrimaryPlaylist(innerCtx)
}
return c.downloadStreamPlaylist(innerCtx)
}()
if err != nil {
return false, err
}
added := false
for _, seg := range pl.Segments {
if seg == nil {
break
}
if !c.segmentWasDownloaded(seg.URI) {
c.downloadedSegmentURIs = append(c.downloadedSegmentURIs, seg.URI)
byts, err := c.downloadSegment(innerCtx, seg.URI)
if err != nil {
return false, err
}
c.segmentQueue.push(byts)
added = true
}
}
return added, nil
}
func (c *Client) segmentWasDownloaded(ur string) bool {
for _, q := range c.downloadedSegmentURIs {
if q == ur {
return true
}
}
return false
}
func (c *Client) downloadPrimaryPlaylist(innerCtx context.Context) (*m3u8.MediaPlaylist, error) {
c.logger.Log(logger.Debug, "downloading primary playlist %s", c.primaryPlaylistURL)
pl, err := c.downloadPlaylist(innerCtx, c.primaryPlaylistURL)
if err != nil {
return nil, err
}
switch plt := pl.(type) {
case *m3u8.MediaPlaylist:
c.streamPlaylistURL = c.primaryPlaylistURL
return plt, nil
case *m3u8.MasterPlaylist:
// choose the variant with the highest bandwidth
var chosenVariant *m3u8.Variant
for _, v := range plt.Variants {
if chosenVariant == nil ||
v.VariantParams.Bandwidth > chosenVariant.VariantParams.Bandwidth {
chosenVariant = v
}
}
if chosenVariant == nil {
return nil, fmt.Errorf("no variants found")
}
u, err := clientURLAbsolute(c.primaryPlaylistURL, chosenVariant.URI)
if err != nil {
return nil, err
}
c.streamPlaylistURL = u
return c.downloadStreamPlaylist(innerCtx)
default:
return nil, fmt.Errorf("invalid playlist")
}
}
func (c *Client) downloadStreamPlaylist(innerCtx context.Context) (*m3u8.MediaPlaylist, error) {
c.logger.Log(logger.Debug, "downloading stream playlist %s", c.streamPlaylistURL.String())
pl, err := c.downloadPlaylist(innerCtx, c.streamPlaylistURL)
if err != nil {
return nil, err
}
plt, ok := pl.(*m3u8.MediaPlaylist)
if !ok {
return nil, fmt.Errorf("invalid playlist")
}
return plt, nil
}
func (c *Client) downloadPlaylist(innerCtx context.Context, ur *url.URL) (m3u8.Playlist, error) {
req, err := http.NewRequestWithContext(innerCtx, http.MethodGet, ur.String(), nil)
if err != nil {
return nil, err
}
res, err := c.httpClient.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return nil, fmt.Errorf("bad status code: %d", res.StatusCode)
}
pl, _, err := m3u8.DecodeFrom(res.Body, true)
if err != nil {
return nil, err
}
return pl, nil
}
func (c *Client) downloadSegment(innerCtx context.Context, segmentURI string) ([]byte, error) {
u, err := clientURLAbsolute(c.streamPlaylistURL, segmentURI)
if err != nil {
return nil, err
}
c.logger.Log(logger.Debug, "downloading segment %s", u)
req, err := http.NewRequestWithContext(innerCtx, http.MethodGet, u.String(), nil)
if err != nil {
return nil, err
}
res, err := c.httpClient.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return nil, fmt.Errorf("bad status code: %d", res.StatusCode)
}
byts, err := io.ReadAll(res.Body)
if err != nil {
return nil, err
}
return byts, nil
}
func (c *Client) runProcessor(innerCtx context.Context) error {
for {
seg, err := c.segmentQueue.waitAndPull(innerCtx)
if err != nil {
return err
}
err = c.processSegment(innerCtx, seg)
if err != nil {
return err
}
}
}
func (c *Client) processSegment(innerCtx context.Context, byts []byte) error {
dem := astits.NewDemuxer(context.Background(), bytes.NewReader(byts))
// parse PMT
if !c.pmtDownloaded {
for {
data, err := dem.NextData()
if err != nil {
if err == astits.ErrNoMorePackets {
return nil
}
return err
}
if data.PMT != nil {
c.pmtDownloaded = true
for _, e := range data.PMT.ElementaryStreams {
switch e.StreamType {
case astits.StreamTypeH264Video:
if c.videoPID != nil {
return fmt.Errorf("multiple video/audio tracks are not supported")
}
v := e.ElementaryPID
c.videoPID = &v
case astits.StreamTypeAACAudio:
if c.audioPID != nil {
return fmt.Errorf("multiple video/audio tracks are not supported")
}
v := e.ElementaryPID
c.audioPID = &v
}
}
break
}
}
if c.videoPID == nil && c.audioPID == nil {
return fmt.Errorf("stream doesn't contain tracks with supported codecs (H264 or AAC)")
}
res := make(chan struct{})
select {
case c.allocateProcs <- clientAllocateProcsReq{res}:
<-res
case <-innerCtx.Done():
return nil
}
}
// process PES packets
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")
}
pts := c.timeDec.Decode(data.PES.Header.OptionalHeader.PTS.Base)
if c.videoPID != nil && data.PID == *c.videoPID {
var dts time.Duration
if data.PES.Header.OptionalHeader.PTSDTSIndicator == astits.PTSDTSIndicatorBothPresent {
diff := time.Duration((data.PES.Header.OptionalHeader.PTS.Base-
data.PES.Header.OptionalHeader.DTS.Base)&0x1FFFFFFFF) *
time.Second / 90000
dts = pts - diff
} else {
dts = pts
}
if !c.clockInitialized {
c.clockInitialized = true
now := time.Now()
if c.videoPID != nil {
c.videoProc.clockStartRTC = now
}
if c.audioPID != nil {
c.audioProc.clockStartRTC = now
}
c.startDTS = dts
}
pts -= c.startDTS
dts -= c.startDTS
c.videoProc.process(data.PES.Data, pts, dts)
} else if c.audioPID != nil && data.PID == *c.audioPID {
if !c.clockInitialized {
c.clockInitialized = true
now := time.Now()
if c.videoPID != nil {
c.videoProc.clockStartRTC = now
}
if c.audioPID != nil {
c.audioProc.clockStartRTC = now
}
c.startDTS = pts
}
pts -= c.startDTS
c.audioProc.process(data.PES.Data, pts)
}
}
}
func (c *Client) onVideoProcessorTrack(track *gortsplib.TrackH264) error {
c.tracksMutex.Lock()
defer c.tracksMutex.Unlock()
c.videoTrack = track
if c.audioPID == nil || c.audioTrack != nil {
return c.onTracks(c.videoTrack, c.audioTrack)
}
return nil
}
func (c *Client) onVideoProcessorData(pts time.Duration, nalus [][]byte) {
c.tracksMutex.RLock()
defer c.tracksMutex.RUnlock()
c.onVideoData(pts, nalus)
}
func (c *Client) onAudioProcessorTrack(track *gortsplib.TrackMPEG4Audio) error {
c.tracksMutex.Lock()
defer c.tracksMutex.Unlock()
c.audioTrack = track
if c.videoPID == nil || c.videoTrack != nil {
return c.onTracks(c.videoTrack, c.audioTrack)
rp := newClientRoutinePool()
segmentQueue := newClientSegmentQueue()
dl := newClientDownloader(
c.primaryPlaylistURL,
c.fingerprint,
segmentQueue,
c.logger,
c.onTracks,
c.onVideoData,
c.onAudioData,
rp,
)
rp.add(dl.run)
select {
case err := <-rp.errorChan():
rp.close()
return err
case <-c.ctx.Done():
rp.close()
return fmt.Errorf("terminated")
}
return nil
}
func (c *Client) onAudioProcessorData(pts time.Duration, au []byte) {
c.tracksMutex.RLock()
defer c.tracksMutex.RUnlock()
c.onAudioData(pts, au)
}

114
internal/hls/client_audio_processor.go

@ -1,114 +0,0 @@ @@ -1,114 +0,0 @@
package hls
import (
"context"
"fmt"
"time"
"github.com/aler9/gortsplib"
"github.com/aler9/gortsplib/pkg/mpeg4audio"
)
type clientAudioProcessorData struct {
data []byte
pts time.Duration
}
type clientAudioProcessor struct {
ctx context.Context
onTrack func(*gortsplib.TrackMPEG4Audio) error
onData func(time.Duration, []byte)
trackInitialized bool
queue chan clientAudioProcessorData
clockStartRTC time.Time
}
func newClientAudioProcessor(
ctx context.Context,
onTrack func(*gortsplib.TrackMPEG4Audio) error,
onData func(time.Duration, []byte),
) *clientAudioProcessor {
p := &clientAudioProcessor{
ctx: ctx,
onTrack: onTrack,
onData: onData,
queue: make(chan clientAudioProcessorData, clientQueueSize),
}
return p
}
func (p *clientAudioProcessor) run() error {
for {
select {
case item := <-p.queue:
err := p.doProcess(item.data, item.pts)
if err != nil {
return err
}
case <-p.ctx.Done():
return nil
}
}
}
func (p *clientAudioProcessor) doProcess(
data []byte,
pts time.Duration,
) error {
elapsed := time.Since(p.clockStartRTC)
if pts > elapsed {
select {
case <-p.ctx.Done():
return fmt.Errorf("terminated")
case <-time.After(pts - elapsed):
}
}
var adtsPkts mpeg4audio.ADTSPackets
err := adtsPkts.Unmarshal(data)
if err != nil {
return err
}
for i, pkt := range adtsPkts {
if !p.trackInitialized {
p.trackInitialized = true
track := &gortsplib.TrackMPEG4Audio{
PayloadType: 96,
Config: &mpeg4audio.Config{
Type: pkt.Type,
SampleRate: pkt.SampleRate,
ChannelCount: pkt.ChannelCount,
},
SizeLength: 13,
IndexLength: 3,
IndexDeltaLength: 3,
}
err = p.onTrack(track)
if err != nil {
return err
}
}
p.onData(
pts+time.Duration(i)*mpeg4audio.SamplesPerAccessUnit*time.Second/time.Duration(pkt.SampleRate),
pkt.AU)
}
return nil
}
func (p *clientAudioProcessor) process(
data []byte,
pts time.Duration,
) {
select {
case p.queue <- clientAudioProcessorData{data, pts}:
case <-p.ctx.Done():
}
}

278
internal/hls/client_downloader.go

@ -0,0 +1,278 @@ @@ -0,0 +1,278 @@
package hls
import (
"context"
"crypto/sha256"
"crypto/tls"
"encoding/hex"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"
"github.com/aler9/gortsplib"
"github.com/grafov/m3u8"
"github.com/aler9/rtsp-simple-server/internal/logger"
)
type clientDownloader struct {
primaryPlaylistURL *url.URL
segmentQueue *clientSegmentQueue
logger ClientLogger
onTracks func(*gortsplib.TrackH264, *gortsplib.TrackMPEG4Audio) error
onVideoData func(time.Duration, [][]byte)
onAudioData func(time.Duration, []byte)
rp *clientRoutinePool
streamPlaylistURL *url.URL
downloadedSegmentURIs []string
httpClient *http.Client
lastDownloadTime time.Time
firstPlaylistReceived bool
}
func newClientDownloader(
primaryPlaylistURL *url.URL,
fingerprint string,
segmentQueue *clientSegmentQueue,
logger ClientLogger,
onTracks func(*gortsplib.TrackH264, *gortsplib.TrackMPEG4Audio) error,
onVideoData func(time.Duration, [][]byte),
onAudioData func(time.Duration, []byte),
rp *clientRoutinePool,
) *clientDownloader {
var tlsConfig *tls.Config
if fingerprint != "" {
tlsConfig = &tls.Config{
InsecureSkipVerify: true,
VerifyConnection: func(cs tls.ConnectionState) error {
h := sha256.New()
h.Write(cs.PeerCertificates[0].Raw)
hstr := hex.EncodeToString(h.Sum(nil))
fingerprintLower := strings.ToLower(fingerprint)
if hstr != fingerprintLower {
return fmt.Errorf("server fingerprint do not match: expected %s, got %s",
fingerprintLower, hstr)
}
return nil
},
}
}
return &clientDownloader{
primaryPlaylistURL: primaryPlaylistURL,
segmentQueue: segmentQueue,
logger: logger,
onTracks: onTracks,
onVideoData: onVideoData,
onAudioData: onAudioData,
rp: rp,
httpClient: &http.Client{
Transport: &http.Transport{
TLSClientConfig: tlsConfig,
},
},
}
}
func (d *clientDownloader) run(ctx context.Context) error {
for {
ok := d.segmentQueue.waitUntilSizeIsBelow(ctx, clientMinSegmentsBeforeDownloading)
if !ok {
return fmt.Errorf("terminated")
}
_, err := d.fillSegmentQueue(ctx)
if err != nil {
return err
}
}
}
func (d *clientDownloader) fillSegmentQueue(ctx context.Context) (bool, error) {
minTime := d.lastDownloadTime.Add(clientMinDownloadPause)
now := time.Now()
if now.Before(minTime) {
select {
case <-time.After(minTime.Sub(now)):
case <-ctx.Done():
return false, fmt.Errorf("terminated")
}
}
d.lastDownloadTime = now
pl, err := func() (*m3u8.MediaPlaylist, error) {
if d.streamPlaylistURL == nil {
return d.downloadPrimaryPlaylist(ctx)
}
return d.downloadStreamPlaylist(ctx)
}()
if err != nil {
return false, err
}
if !d.firstPlaylistReceived {
d.firstPlaylistReceived = true
if pl.Map != nil && pl.Map.URI != "" {
return false, fmt.Errorf("fMP4 streams are not supported yet")
}
proc := newClientProcessorMPEGTS(
d.segmentQueue,
d.logger,
d.rp,
d.onTracks,
d.onVideoData,
d.onAudioData,
)
d.rp.add(proc.run)
}
added := false
for _, seg := range pl.Segments {
if seg == nil {
break
}
if !d.segmentWasDownloaded(seg.URI) {
d.downloadedSegmentURIs = append(d.downloadedSegmentURIs, seg.URI)
byts, err := d.downloadSegment(ctx, seg.URI)
if err != nil {
return false, err
}
d.segmentQueue.push(byts)
added = true
}
}
return added, nil
}
func (d *clientDownloader) segmentWasDownloaded(ur string) bool {
for _, q := range d.downloadedSegmentURIs {
if q == ur {
return true
}
}
return false
}
func (d *clientDownloader) downloadPrimaryPlaylist(ctx context.Context) (*m3u8.MediaPlaylist, error) {
d.logger.Log(logger.Debug, "downloading primary playlist %s", d.primaryPlaylistURL)
pl, err := d.downloadPlaylist(ctx, d.primaryPlaylistURL)
if err != nil {
return nil, err
}
switch plt := pl.(type) {
case *m3u8.MediaPlaylist:
d.streamPlaylistURL = d.primaryPlaylistURL
return plt, nil
case *m3u8.MasterPlaylist:
// choose the variant with the highest bandwidth
var chosenVariant *m3u8.Variant
for _, v := range plt.Variants {
if chosenVariant == nil ||
v.VariantParams.Bandwidth > chosenVariant.VariantParams.Bandwidth {
chosenVariant = v
}
}
if chosenVariant == nil {
return nil, fmt.Errorf("no variants found")
}
u, err := clientURLAbsolute(d.primaryPlaylistURL, chosenVariant.URI)
if err != nil {
return nil, err
}
d.streamPlaylistURL = u
return d.downloadStreamPlaylist(ctx)
default:
return nil, fmt.Errorf("invalid playlist")
}
}
func (d *clientDownloader) downloadStreamPlaylist(ctx context.Context) (*m3u8.MediaPlaylist, error) {
d.logger.Log(logger.Debug, "downloading stream playlist %s", d.streamPlaylistURL.String())
pl, err := d.downloadPlaylist(ctx, d.streamPlaylistURL)
if err != nil {
return nil, err
}
plt, ok := pl.(*m3u8.MediaPlaylist)
if !ok {
return nil, fmt.Errorf("invalid playlist")
}
return plt, nil
}
func (d *clientDownloader) downloadPlaylist(ctx context.Context, ur *url.URL) (m3u8.Playlist, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, ur.String(), nil)
if err != nil {
return nil, err
}
res, err := d.httpClient.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return nil, fmt.Errorf("bad status code: %d", res.StatusCode)
}
pl, _, err := m3u8.DecodeFrom(res.Body, true)
if err != nil {
return nil, err
}
return pl, nil
}
func (d *clientDownloader) downloadSegment(ctx context.Context, segmentURI string) ([]byte, error) {
u, err := clientURLAbsolute(d.streamPlaylistURL, segmentURI)
if err != nil {
return nil, err
}
d.logger.Log(logger.Debug, "downloading segment %s", u)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
if err != nil {
return nil, err
}
res, err := d.httpClient.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return nil, fmt.Errorf("bad status code: %d", res.StatusCode)
}
byts, err := io.ReadAll(res.Body)
if err != nil {
return nil, err
}
return byts, nil
}

294
internal/hls/client_processor_mpegts.go

@ -0,0 +1,294 @@ @@ -0,0 +1,294 @@
package hls
import (
"bytes"
"context"
"fmt"
"strings"
"time"
"github.com/aler9/gortsplib"
"github.com/aler9/gortsplib/pkg/h264"
"github.com/aler9/gortsplib/pkg/mpeg4audio"
"github.com/asticode/go-astits"
"github.com/aler9/rtsp-simple-server/internal/hls/mpegtstimedec"
"github.com/aler9/rtsp-simple-server/internal/logger"
)
type clientProcessorMPEGTS struct {
segmentQueue *clientSegmentQueue
logger ClientLogger
rp *clientRoutinePool
onTracks func(*gortsplib.TrackH264, *gortsplib.TrackMPEG4Audio) error
onVideoData func(time.Duration, [][]byte)
onAudioData func(time.Duration, []byte)
tracksParsed bool
clockInitialized bool
timeDec *mpegtstimedec.Decoder
startDTS time.Duration
videoPID *uint16
audioPID *uint16
videoTrack *gortsplib.TrackH264
audioTrack *gortsplib.TrackMPEG4Audio
videoProc *clientProcessorMPEGTSTrack
audioProc *clientProcessorMPEGTSTrack
}
func newClientProcessorMPEGTS(
segmentQueue *clientSegmentQueue,
logger ClientLogger,
rp *clientRoutinePool,
onTracks func(*gortsplib.TrackH264, *gortsplib.TrackMPEG4Audio) error,
onVideoData func(time.Duration, [][]byte),
onAudioData func(time.Duration, []byte),
) *clientProcessorMPEGTS {
return &clientProcessorMPEGTS{
segmentQueue: segmentQueue,
logger: logger,
rp: rp,
timeDec: mpegtstimedec.New(),
onTracks: onTracks,
onVideoData: onVideoData,
onAudioData: onAudioData,
}
}
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 {
p.logger.Log(logger.Debug, "processing segment")
dem := astits.NewDemuxer(context.Background(), bytes.NewReader(byts))
if !p.tracksParsed {
p.tracksParsed = true
err := p.parseTracks(dem)
if err != nil {
return err
}
// rewind demuxer in order to read again the audio packet that was used to create the track
if p.audioTrack != nil {
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")
}
pts := p.timeDec.Decode(data.PES.Header.OptionalHeader.PTS.Base)
if p.videoPID != nil && data.PID == *p.videoPID {
var dts time.Duration
if data.PES.Header.OptionalHeader.PTSDTSIndicator == astits.PTSDTSIndicatorBothPresent {
diff := time.Duration((data.PES.Header.OptionalHeader.PTS.Base-
data.PES.Header.OptionalHeader.DTS.Base)&0x1FFFFFFFF) *
time.Second / 90000
dts = pts - diff
} else {
dts = pts
}
if !p.clockInitialized {
p.clockInitialized = true
p.startDTS = dts
now := time.Now()
p.initializeTrackProcs(now)
}
pts -= p.startDTS
dts -= p.startDTS
p.videoProc.push(ctx, &clientProcessorMPEGTSTrackEntryVideo{
data: data.PES.Data,
pts: pts,
dts: dts,
})
} else if p.audioPID != nil && data.PID == *p.audioPID {
if !p.clockInitialized {
p.clockInitialized = true
p.startDTS = pts
now := time.Now()
p.initializeTrackProcs(now)
}
pts -= p.startDTS
p.audioProc.push(ctx, &clientProcessorMPEGTSTrackEntryAudio{
data: data.PES.Data,
pts: pts,
})
}
}
}
func (p *clientProcessorMPEGTS) parseTracks(dem *astits.Demuxer) error {
// find and parse PMT
for {
data, err := dem.NextData()
if err != nil {
return err
}
if data.PMT != nil {
for _, e := range data.PMT.ElementaryStreams {
switch e.StreamType {
case astits.StreamTypeH264Video:
if p.videoPID != nil {
return fmt.Errorf("multiple video/audio tracks are not supported")
}
v := e.ElementaryPID
p.videoPID = &v
case astits.StreamTypeAACAudio:
if p.audioPID != nil {
return fmt.Errorf("multiple video/audio tracks are not supported")
}
v := e.ElementaryPID
p.audioPID = &v
}
}
break
}
}
if p.videoPID == nil && p.audioPID == nil {
return fmt.Errorf("stream doesn't contain tracks with supported codecs (H264 or AAC)")
}
if p.videoPID != nil {
p.videoTrack = &gortsplib.TrackH264{
PayloadType: 96,
}
if p.audioPID == nil {
err := p.onTracks(p.videoTrack, nil)
if err != nil {
return err
}
}
}
// find and parse first audio packet
if p.audioPID != nil {
for {
data, err := dem.NextData()
if err != nil {
return err
}
if data.PES == nil || data.PID != *p.audioPID {
continue
}
var adtsPkts mpeg4audio.ADTSPackets
err = adtsPkts.Unmarshal(data.PES.Data)
if err != nil {
return fmt.Errorf("unable to decode ADTS: %s", err)
}
pkt := adtsPkts[0]
p.audioTrack = &gortsplib.TrackMPEG4Audio{
PayloadType: 96,
Config: &mpeg4audio.Config{
Type: pkt.Type,
SampleRate: pkt.SampleRate,
ChannelCount: pkt.ChannelCount,
},
SizeLength: 13,
IndexLength: 3,
IndexDeltaLength: 3,
}
err = p.onTracks(p.videoTrack, p.audioTrack)
if err != nil {
return err
}
break
}
}
return nil
}
func (p *clientProcessorMPEGTS) initializeTrackProcs(clockStartRTC time.Time) {
if p.videoTrack != nil {
p.videoProc = newClientProcessorMPEGTSTrack(
clockStartRTC,
func(e clientProcessorMPEGTSTrackEntry) error {
vd := e.(*clientProcessorMPEGTSTrackEntryVideo)
nalus, err := h264.AnnexBUnmarshal(vd.data)
if err != nil {
p.logger.Log(logger.Warn, "unable to decode Annex-B: %s", err)
return nil
}
p.onVideoData(vd.pts, nalus)
return nil
},
)
p.rp.add(p.videoProc.run)
}
if p.audioTrack != nil {
p.audioProc = newClientProcessorMPEGTSTrack(
clockStartRTC,
func(e clientProcessorMPEGTSTrackEntry) error {
ad := e.(*clientProcessorMPEGTSTrackEntryAudio)
var adtsPkts mpeg4audio.ADTSPackets
err := adtsPkts.Unmarshal(ad.data)
if err != nil {
return fmt.Errorf("unable to decode ADTS: %s", err)
}
for i, pkt := range adtsPkts {
p.onAudioData(
ad.pts+time.Duration(i)*mpeg4audio.SamplesPerAccessUnit*time.Second/time.Duration(pkt.SampleRate),
pkt.AU)
}
return nil
},
)
p.rp.add(p.audioProc.run)
}
}

83
internal/hls/client_processor_mpegts_track.go

@ -0,0 +1,83 @@ @@ -0,0 +1,83 @@
package hls
import (
"context"
"fmt"
"time"
)
type clientProcessorMPEGTSTrackEntry interface {
DTS() time.Duration
}
type clientProcessorMPEGTSTrackEntryVideo struct {
data []byte
pts time.Duration
dts time.Duration
}
func (e clientProcessorMPEGTSTrackEntryVideo) DTS() time.Duration {
return e.dts
}
type clientProcessorMPEGTSTrackEntryAudio struct {
data []byte
pts time.Duration
}
func (e clientProcessorMPEGTSTrackEntryAudio) DTS() time.Duration {
return e.pts
}
type clientProcessorMPEGTSTrack struct {
clockStartRTC time.Time
onEntry func(e clientProcessorMPEGTSTrackEntry) error
queue chan clientProcessorMPEGTSTrackEntry
}
func newClientProcessorMPEGTSTrack(
clockStartRTC time.Time,
onEntry func(e clientProcessorMPEGTSTrackEntry) error,
) *clientProcessorMPEGTSTrack {
return &clientProcessorMPEGTSTrack{
clockStartRTC: clockStartRTC,
onEntry: onEntry,
queue: make(chan clientProcessorMPEGTSTrackEntry, clientQueueSize),
}
}
func (t *clientProcessorMPEGTSTrack) run(ctx context.Context) error {
for {
select {
case entry := <-t.queue:
err := t.processEntry(ctx, entry)
if err != nil {
return err
}
case <-ctx.Done():
return nil
}
}
}
func (t *clientProcessorMPEGTSTrack) processEntry(ctx context.Context, entry clientProcessorMPEGTSTrackEntry) error {
elapsed := time.Since(t.clockStartRTC)
if entry.DTS() > elapsed {
select {
case <-ctx.Done():
return fmt.Errorf("terminated")
case <-time.After(entry.DTS() - elapsed):
}
}
return t.onEntry(entry)
}
func (t *clientProcessorMPEGTSTrack) push(ctx context.Context, entry clientProcessorMPEGTSTrackEntry) {
select {
case t.queue <- entry:
case <-ctx.Done():
}
}

44
internal/hls/client_routine_pool.go

@ -0,0 +1,44 @@ @@ -0,0 +1,44 @@
package hls
import (
"context"
"sync"
)
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(cb func(context.Context) error) {
rp.wg.Add(1)
go func() {
defer rp.wg.Done()
select {
case rp.err <- cb(rp.ctx):
case <-rp.ctx.Done():
}
}()
}

12
internal/hls/client_segment_queue.go

@ -2,7 +2,6 @@ package hls @@ -2,7 +2,6 @@ package hls
import (
"context"
"fmt"
"sync"
)
@ -34,7 +33,7 @@ func (q *clientSegmentQueue) push(seg []byte) { @@ -34,7 +33,7 @@ func (q *clientSegmentQueue) push(seg []byte) {
q.mutex.Unlock()
}
func (q *clientSegmentQueue) waitUntilSizeIsBelow(ctx context.Context, n int) {
func (q *clientSegmentQueue) waitUntilSizeIsBelow(ctx context.Context, n int) bool {
q.mutex.Lock()
for len(q.queue) > n {
@ -43,16 +42,17 @@ func (q *clientSegmentQueue) waitUntilSizeIsBelow(ctx context.Context, n int) { @@ -43,16 +42,17 @@ func (q *clientSegmentQueue) waitUntilSizeIsBelow(ctx context.Context, n int) {
select {
case <-q.didPull:
case <-ctx.Done():
return
return false
}
q.mutex.Lock()
}
q.mutex.Unlock()
return true
}
func (q *clientSegmentQueue) waitAndPull(ctx context.Context) ([]byte, error) {
func (q *clientSegmentQueue) pull(ctx context.Context) ([]byte, bool) {
q.mutex.Lock()
for len(q.queue) == 0 {
@ -62,7 +62,7 @@ func (q *clientSegmentQueue) waitAndPull(ctx context.Context) ([]byte, error) { @@ -62,7 +62,7 @@ func (q *clientSegmentQueue) waitAndPull(ctx context.Context) ([]byte, error) {
select {
case <-didPush:
case <-ctx.Done():
return nil, fmt.Errorf("terminated")
return nil, false
}
q.mutex.Lock()
@ -75,5 +75,5 @@ func (q *clientSegmentQueue) waitAndPull(ctx context.Context) ([]byte, error) { @@ -75,5 +75,5 @@ func (q *clientSegmentQueue) waitAndPull(ctx context.Context) ([]byte, error) {
q.didPull = make(chan struct{})
q.mutex.Unlock()
return seg, nil
return seg, true
}

104
internal/hls/client_video_processor.go

@ -1,104 +0,0 @@ @@ -1,104 +0,0 @@
package hls
import (
"context"
"fmt"
"time"
"github.com/aler9/gortsplib"
"github.com/aler9/gortsplib/pkg/h264"
"github.com/aler9/rtsp-simple-server/internal/logger"
)
type clientVideoProcessorData struct {
data []byte
pts time.Duration
dts time.Duration
}
type clientVideoProcessor struct {
ctx context.Context
onTrack func(*gortsplib.TrackH264) error
onData func(time.Duration, [][]byte)
logger ClientLogger
queue chan clientVideoProcessorData
clockStartRTC time.Time
}
func newClientVideoProcessor(
ctx context.Context,
onTrack func(*gortsplib.TrackH264) error,
onData func(time.Duration, [][]byte),
logger ClientLogger,
) *clientVideoProcessor {
p := &clientVideoProcessor{
ctx: ctx,
onTrack: onTrack,
onData: onData,
logger: logger,
queue: make(chan clientVideoProcessorData, clientQueueSize),
}
return p
}
func (p *clientVideoProcessor) run() error {
track := &gortsplib.TrackH264{
PayloadType: 96,
}
err := p.onTrack(track)
if err != nil {
return err
}
for {
select {
case item := <-p.queue:
err := p.doProcess(item.data, item.pts, item.dts)
if err != nil {
return err
}
case <-p.ctx.Done():
return nil
}
}
}
func (p *clientVideoProcessor) doProcess(
data []byte,
pts time.Duration,
dts time.Duration,
) error {
elapsed := time.Since(p.clockStartRTC)
if dts > elapsed {
select {
case <-p.ctx.Done():
return fmt.Errorf("terminated")
case <-time.After(dts - elapsed):
}
}
nalus, err := h264.AnnexBUnmarshal(data)
if err != nil {
p.logger.Log(logger.Warn, "unable to unmarshal Annex-B: %s", err)
return nil
}
p.onData(pts, nalus)
return nil
}
func (p *clientVideoProcessor) process(
data []byte,
pts time.Duration,
dts time.Duration,
) {
select {
case p.queue <- clientVideoProcessorData{data, pts, dts}:
case <-p.ctx.Done():
}
}

2
internal/hls/fmp4/fmp4.go

@ -0,0 +1,2 @@ @@ -0,0 +1,2 @@
// Package fmp4 contains a fMP4 reader and writer.
package fmp4

104
internal/hls/fmp4/init_read.go

@ -0,0 +1,104 @@ @@ -0,0 +1,104 @@
package fmp4
import (
"bytes"
"fmt"
gomp4 "github.com/abema/go-mp4"
"github.com/aler9/gortsplib"
)
type initReadState int
const (
waitingTrak initReadState = iota
waitingCodec
waitingAVCC
)
// InitRead reads a FMP4 initialization file.
func InitRead(byts []byte) (*gortsplib.TrackH264, *gortsplib.TrackMPEG4Audio, error) {
state := waitingTrak
var videoTrack *gortsplib.TrackH264
var audioTrack *gortsplib.TrackMPEG4Audio
_, err := gomp4.ReadBoxStructure(bytes.NewReader(byts), func(h *gomp4.ReadHandle) (interface{}, error) {
switch h.BoxInfo.Type.String() {
case "trak":
if state != waitingTrak {
return nil, fmt.Errorf("parse error")
}
state = waitingCodec
case "avc1":
if state != waitingCodec {
return nil, fmt.Errorf("parse error")
}
if videoTrack != nil {
return nil, fmt.Errorf("multiple video tracks are not supported")
}
state = waitingAVCC
case "avcC":
if state != waitingAVCC {
return nil, fmt.Errorf("parse error")
}
box, _, err := h.ReadPayload()
if err != nil {
return nil, err
}
conf := box.(*gomp4.AVCDecoderConfiguration)
if len(conf.SequenceParameterSets) > 1 {
return nil, fmt.Errorf("multiple SPS are not supported")
}
var sps []byte
if len(conf.SequenceParameterSets) == 1 {
sps = conf.SequenceParameterSets[0].NALUnit
}
if len(conf.PictureParameterSets) > 1 {
return nil, fmt.Errorf("multiple PPS are not supported")
}
var pps []byte
if len(conf.PictureParameterSets) == 1 {
pps = conf.PictureParameterSets[0].NALUnit
}
videoTrack = &gortsplib.TrackH264{
PayloadType: 96,
SPS: sps,
PPS: pps,
}
state = waitingTrak
case "mp4a":
if state != waitingCodec {
return nil, fmt.Errorf("parse error")
}
if audioTrack != nil {
return nil, fmt.Errorf("multiple audio tracks are not supported")
}
return nil, fmt.Errorf("TODO: MP4a")
}
return h.Expand()
})
if err != nil {
return nil, nil, err
}
if state != waitingTrak {
return nil, nil, fmt.Errorf("parse error")
}
return videoTrack, audioTrack, nil
}

90
internal/hls/fmp4/init.go → internal/hls/fmp4/init_write.go

@ -1,4 +1,3 @@ @@ -1,4 +1,3 @@
// Package fmp4 contains a fMP4 writer.
package fmp4
import (
@ -20,7 +19,7 @@ func init() { //nolint:gochecknoinits @@ -20,7 +19,7 @@ func init() { //nolint:gochecknoinits
gomp4.AddBoxDef(&myEsds{}, 0)
}
func generateInitVideoTrack(w *mp4Writer, trackID int, videoTrack *gortsplib.TrackH264) error {
func initWriteVideoTrack(w *mp4Writer, trackID int, videoTrack *gortsplib.TrackH264) error {
/*
trak
- tkhd
@ -44,7 +43,7 @@ func generateInitVideoTrack(w *mp4Writer, trackID int, videoTrack *gortsplib.Tra @@ -44,7 +43,7 @@ func generateInitVideoTrack(w *mp4Writer, trackID int, videoTrack *gortsplib.Tra
- stco
*/
_, err := w.WriteBoxStart(&gomp4.Trak{}) // <trak>
_, err := w.writeBoxStart(&gomp4.Trak{}) // <trak>
if err != nil {
return err
}
@ -74,7 +73,7 @@ func generateInitVideoTrack(w *mp4Writer, trackID int, videoTrack *gortsplib.Tra @@ -74,7 +73,7 @@ func generateInitVideoTrack(w *mp4Writer, trackID int, videoTrack *gortsplib.Tra
return err
}
_, err = w.WriteBoxStart(&gomp4.Mdia{}) // <mdia>
_, err = w.writeBoxStart(&gomp4.Mdia{}) // <mdia>
if err != nil {
return err
}
@ -95,7 +94,7 @@ func generateInitVideoTrack(w *mp4Writer, trackID int, videoTrack *gortsplib.Tra @@ -95,7 +94,7 @@ func generateInitVideoTrack(w *mp4Writer, trackID int, videoTrack *gortsplib.Tra
return err
}
_, err = w.WriteBoxStart(&gomp4.Minf{}) // <minf>
_, err = w.writeBoxStart(&gomp4.Minf{}) // <minf>
if err != nil {
return err
}
@ -109,12 +108,12 @@ func generateInitVideoTrack(w *mp4Writer, trackID int, videoTrack *gortsplib.Tra @@ -109,12 +108,12 @@ func generateInitVideoTrack(w *mp4Writer, trackID int, videoTrack *gortsplib.Tra
return err
}
_, err = w.WriteBoxStart(&gomp4.Dinf{}) // <dinf>
_, err = w.writeBoxStart(&gomp4.Dinf{}) // <dinf>
if err != nil {
return err
}
_, err = w.WriteBoxStart(&gomp4.Dref{ // <dref>
_, err = w.writeBoxStart(&gomp4.Dref{ // <dref>
EntryCount: 1,
})
if err != nil {
@ -130,29 +129,29 @@ func generateInitVideoTrack(w *mp4Writer, trackID int, videoTrack *gortsplib.Tra @@ -130,29 +129,29 @@ func generateInitVideoTrack(w *mp4Writer, trackID int, videoTrack *gortsplib.Tra
return err
}
err = w.WriteBoxEnd() // </dref>
err = w.writeBoxEnd() // </dref>
if err != nil {
return err
}
err = w.WriteBoxEnd() // </dinf>
err = w.writeBoxEnd() // </dinf>
if err != nil {
return err
}
_, err = w.WriteBoxStart(&gomp4.Stbl{}) // <stbl>
_, err = w.writeBoxStart(&gomp4.Stbl{}) // <stbl>
if err != nil {
return err
}
_, err = w.WriteBoxStart(&gomp4.Stsd{ // <stsd>
_, err = w.writeBoxStart(&gomp4.Stsd{ // <stsd>
EntryCount: 1,
})
if err != nil {
return err
}
_, err = w.WriteBoxStart(&gomp4.VisualSampleEntry{ // <avc1>
_, err = w.writeBoxStart(&gomp4.VisualSampleEntry{ // <avc1>
SampleEntry: gomp4.SampleEntry{
AnyTypeBox: gomp4.AnyTypeBox{
Type: gomp4.BoxTypeAvc1(),
@ -207,12 +206,12 @@ func generateInitVideoTrack(w *mp4Writer, trackID int, videoTrack *gortsplib.Tra @@ -207,12 +206,12 @@ func generateInitVideoTrack(w *mp4Writer, trackID int, videoTrack *gortsplib.Tra
return err
}
err = w.WriteBoxEnd() // </avc1>
err = w.writeBoxEnd() // </avc1>
if err != nil {
return err
}
err = w.WriteBoxEnd() // </stsd>
err = w.writeBoxEnd() // </stsd>
if err != nil {
return err
}
@ -241,22 +240,22 @@ func generateInitVideoTrack(w *mp4Writer, trackID int, videoTrack *gortsplib.Tra @@ -241,22 +240,22 @@ func generateInitVideoTrack(w *mp4Writer, trackID int, videoTrack *gortsplib.Tra
return err
}
err = w.WriteBoxEnd() // </stbl>
err = w.writeBoxEnd() // </stbl>
if err != nil {
return err
}
err = w.WriteBoxEnd() // </minf>
err = w.writeBoxEnd() // </minf>
if err != nil {
return err
}
err = w.WriteBoxEnd() // </mdia>
err = w.writeBoxEnd() // </mdia>
if err != nil {
return err
}
err = w.WriteBoxEnd() // </trak>
err = w.writeBoxEnd() // </trak>
if err != nil {
return err
}
@ -264,7 +263,7 @@ func generateInitVideoTrack(w *mp4Writer, trackID int, videoTrack *gortsplib.Tra @@ -264,7 +263,7 @@ func generateInitVideoTrack(w *mp4Writer, trackID int, videoTrack *gortsplib.Tra
return nil
}
func generateInitAudioTrack(w *mp4Writer, trackID int, audioTrack *gortsplib.TrackMPEG4Audio) error {
func initWriteAudioTrack(w *mp4Writer, trackID int, audioTrack *gortsplib.TrackMPEG4Audio) error {
/*
trak
- tkhd
@ -287,7 +286,7 @@ func generateInitAudioTrack(w *mp4Writer, trackID int, audioTrack *gortsplib.Tra @@ -287,7 +286,7 @@ func generateInitAudioTrack(w *mp4Writer, trackID int, audioTrack *gortsplib.Tra
- stco
*/
_, err := w.WriteBoxStart(&gomp4.Trak{}) // <trak>
_, err := w.writeBoxStart(&gomp4.Trak{}) // <trak>
if err != nil {
return err
}
@ -305,7 +304,7 @@ func generateInitAudioTrack(w *mp4Writer, trackID int, audioTrack *gortsplib.Tra @@ -305,7 +304,7 @@ func generateInitAudioTrack(w *mp4Writer, trackID int, audioTrack *gortsplib.Tra
return err
}
_, err = w.WriteBoxStart(&gomp4.Mdia{}) // <mdia>
_, err = w.writeBoxStart(&gomp4.Mdia{}) // <mdia>
if err != nil {
return err
}
@ -326,7 +325,7 @@ func generateInitAudioTrack(w *mp4Writer, trackID int, audioTrack *gortsplib.Tra @@ -326,7 +325,7 @@ func generateInitAudioTrack(w *mp4Writer, trackID int, audioTrack *gortsplib.Tra
return err
}
_, err = w.WriteBoxStart(&gomp4.Minf{}) // <minf>
_, err = w.writeBoxStart(&gomp4.Minf{}) // <minf>
if err != nil {
return err
}
@ -337,12 +336,12 @@ func generateInitAudioTrack(w *mp4Writer, trackID int, audioTrack *gortsplib.Tra @@ -337,12 +336,12 @@ func generateInitAudioTrack(w *mp4Writer, trackID int, audioTrack *gortsplib.Tra
return err
}
_, err = w.WriteBoxStart(&gomp4.Dinf{}) // <dinf>
_, err = w.writeBoxStart(&gomp4.Dinf{}) // <dinf>
if err != nil {
return err
}
_, err = w.WriteBoxStart(&gomp4.Dref{ // <dref>
_, err = w.writeBoxStart(&gomp4.Dref{ // <dref>
EntryCount: 1,
})
if err != nil {
@ -358,29 +357,29 @@ func generateInitAudioTrack(w *mp4Writer, trackID int, audioTrack *gortsplib.Tra @@ -358,29 +357,29 @@ func generateInitAudioTrack(w *mp4Writer, trackID int, audioTrack *gortsplib.Tra
return err
}
err = w.WriteBoxEnd() // </dref>
err = w.writeBoxEnd() // </dref>
if err != nil {
return err
}
err = w.WriteBoxEnd() // </dinf>
err = w.writeBoxEnd() // </dinf>
if err != nil {
return err
}
_, err = w.WriteBoxStart(&gomp4.Stbl{}) // <stbl>
_, err = w.writeBoxStart(&gomp4.Stbl{}) // <stbl>
if err != nil {
return err
}
_, err = w.WriteBoxStart(&gomp4.Stsd{ // <stsd>
_, err = w.writeBoxStart(&gomp4.Stsd{ // <stsd>
EntryCount: 1,
})
if err != nil {
return err
}
_, err = w.WriteBoxStart(&gomp4.AudioSampleEntry{ // <mp4a>
_, err = w.writeBoxStart(&gomp4.AudioSampleEntry{ // <mp4a>
SampleEntry: gomp4.SampleEntry{
AnyTypeBox: gomp4.AnyTypeBox{
Type: gomp4.BoxTypeMp4a(),
@ -453,12 +452,12 @@ func generateInitAudioTrack(w *mp4Writer, trackID int, audioTrack *gortsplib.Tra @@ -453,12 +452,12 @@ func generateInitAudioTrack(w *mp4Writer, trackID int, audioTrack *gortsplib.Tra
return err
}
err = w.WriteBoxEnd() // </mp4a>
err = w.writeBoxEnd() // </mp4a>
if err != nil {
return err
}
err = w.WriteBoxEnd() // </stsd>
err = w.writeBoxEnd() // </stsd>
if err != nil {
return err
}
@ -487,22 +486,22 @@ func generateInitAudioTrack(w *mp4Writer, trackID int, audioTrack *gortsplib.Tra @@ -487,22 +486,22 @@ func generateInitAudioTrack(w *mp4Writer, trackID int, audioTrack *gortsplib.Tra
return err
}
err = w.WriteBoxEnd() // </stbl>
err = w.writeBoxEnd() // </stbl>
if err != nil {
return err
}
err = w.WriteBoxEnd() // </minf>
err = w.writeBoxEnd() // </minf>
if err != nil {
return err
}
err = w.WriteBoxEnd() // </mdia>
err = w.writeBoxEnd() // </mdia>
if err != nil {
return err
}
err = w.WriteBoxEnd() // </trak>
err = w.writeBoxEnd() // </trak>
if err != nil {
return err
}
@ -510,8 +509,11 @@ func generateInitAudioTrack(w *mp4Writer, trackID int, audioTrack *gortsplib.Tra @@ -510,8 +509,11 @@ func generateInitAudioTrack(w *mp4Writer, trackID int, audioTrack *gortsplib.Tra
return nil
}
// GenerateInit generates a FMP4 initialization file.
func GenerateInit(videoTrack *gortsplib.TrackH264, audioTrack *gortsplib.TrackMPEG4Audio) ([]byte, error) {
// InitWrite generates a FMP4 initialization file.
func InitWrite(
videoTrack *gortsplib.TrackH264,
audioTrack *gortsplib.TrackMPEG4Audio,
) ([]byte, error) {
/*
- ftyp
- moov
@ -539,7 +541,7 @@ func GenerateInit(videoTrack *gortsplib.TrackH264, audioTrack *gortsplib.TrackMP @@ -539,7 +541,7 @@ func GenerateInit(videoTrack *gortsplib.TrackH264, audioTrack *gortsplib.TrackMP
return nil, err
}
_, err = w.WriteBoxStart(&gomp4.Moov{}) // <moov>
_, err = w.writeBoxStart(&gomp4.Moov{}) // <moov>
if err != nil {
return nil, err
}
@ -558,7 +560,7 @@ func GenerateInit(videoTrack *gortsplib.TrackH264, audioTrack *gortsplib.TrackMP @@ -558,7 +560,7 @@ func GenerateInit(videoTrack *gortsplib.TrackH264, audioTrack *gortsplib.TrackMP
trackID := 1
if videoTrack != nil {
err := generateInitVideoTrack(w, trackID, videoTrack)
err := initWriteVideoTrack(w, trackID, videoTrack)
if err != nil {
return nil, err
}
@ -567,13 +569,13 @@ func GenerateInit(videoTrack *gortsplib.TrackH264, audioTrack *gortsplib.TrackMP @@ -567,13 +569,13 @@ func GenerateInit(videoTrack *gortsplib.TrackH264, audioTrack *gortsplib.TrackMP
}
if audioTrack != nil {
err := generateInitAudioTrack(w, trackID, audioTrack)
err := initWriteAudioTrack(w, trackID, audioTrack)
if err != nil {
return nil, err
}
}
_, err = w.WriteBoxStart(&gomp4.Mvex{}) // <mvex>
_, err = w.writeBoxStart(&gomp4.Mvex{}) // <mvex>
if err != nil {
return nil, err
}
@ -602,15 +604,15 @@ func GenerateInit(videoTrack *gortsplib.TrackH264, audioTrack *gortsplib.TrackMP @@ -602,15 +604,15 @@ func GenerateInit(videoTrack *gortsplib.TrackH264, audioTrack *gortsplib.TrackMP
}
}
err = w.WriteBoxEnd() // </mvex>
err = w.writeBoxEnd() // </mvex>
if err != nil {
return nil, err
}
err = w.WriteBoxEnd() // </moov>
err = w.writeBoxEnd() // </moov>
if err != nil {
return nil, err
}
return w.Bytes(), nil
return w.bytes(), nil
}

8
internal/hls/fmp4/init_test.go → internal/hls/fmp4/init_write_test.go

@ -46,9 +46,9 @@ var testAudioTrack = &gortsplib.TrackMPEG4Audio{ @@ -46,9 +46,9 @@ var testAudioTrack = &gortsplib.TrackMPEG4Audio{
IndexDeltaLength: 3,
}
func TestGenerateInit(t *testing.T) {
func TestInitWrite(t *testing.T) {
t.Run("video + audio", func(t *testing.T) {
byts, err := GenerateInit(testVideoTrack, testAudioTrack)
byts, err := InitWrite(testVideoTrack, testAudioTrack)
require.NoError(t, err)
boxes := []gomp4.BoxPath{
@ -173,7 +173,7 @@ func TestGenerateInit(t *testing.T) { @@ -173,7 +173,7 @@ func TestGenerateInit(t *testing.T) {
})
t.Run("video only", func(t *testing.T) {
byts, err := GenerateInit(testVideoTrack, nil)
byts, err := InitWrite(testVideoTrack, nil)
require.NoError(t, err)
boxes := []gomp4.BoxPath{
@ -245,7 +245,7 @@ func TestGenerateInit(t *testing.T) { @@ -245,7 +245,7 @@ func TestGenerateInit(t *testing.T) {
})
t.Run("audio only", func(t *testing.T) {
byts, err := GenerateInit(nil, testAudioTrack)
byts, err := InitWrite(nil, testAudioTrack)
require.NoError(t, err)
boxes := []gomp4.BoxPath{

23
internal/hls/fmp4/mp4writer.go → internal/hls/fmp4/mp4_writer.go

@ -7,13 +7,11 @@ import ( @@ -7,13 +7,11 @@ import (
"github.com/orcaman/writerseeker"
)
// mp4Writer is a MP4 writer.
type mp4Writer struct {
buf *writerseeker.WriterSeeker
w *gomp4.Writer
}
// newMP4Writer allocates a mp4Writer.
func newMP4Writer() *mp4Writer {
w := &mp4Writer{
buf: &writerseeker.WriterSeeker{},
@ -24,8 +22,7 @@ func newMP4Writer() *mp4Writer { @@ -24,8 +22,7 @@ func newMP4Writer() *mp4Writer {
return w
}
// WriteBoxStart writes a box start.
func (w *mp4Writer) WriteBoxStart(box gomp4.IImmutableBox) (int, error) {
func (w *mp4Writer) writeBoxStart(box gomp4.IImmutableBox) (int, error) {
bi := &gomp4.BoxInfo{
Type: box.GetType(),
}
@ -43,20 +40,18 @@ func (w *mp4Writer) WriteBoxStart(box gomp4.IImmutableBox) (int, error) { @@ -43,20 +40,18 @@ func (w *mp4Writer) WriteBoxStart(box gomp4.IImmutableBox) (int, error) {
return int(bi.Offset), nil
}
// WriteBoxEnd writes a box end.
func (w *mp4Writer) WriteBoxEnd() error {
func (w *mp4Writer) writeBoxEnd() error {
_, err := w.w.EndBox()
return err
}
// WriteBox writes a self-closing box.
func (w *mp4Writer) WriteBox(box gomp4.IImmutableBox) (int, error) {
off, err := w.WriteBoxStart(box)
off, err := w.writeBoxStart(box)
if err != nil {
return 0, err
}
err = w.WriteBoxEnd()
err = w.writeBoxEnd()
if err != nil {
return 0, err
}
@ -64,8 +59,7 @@ func (w *mp4Writer) WriteBox(box gomp4.IImmutableBox) (int, error) { @@ -64,8 +59,7 @@ func (w *mp4Writer) WriteBox(box gomp4.IImmutableBox) (int, error) {
return off, nil
}
// RewriteBox rewrites a box.
func (w *mp4Writer) RewriteBox(off int, box gomp4.IImmutableBox) error {
func (w *mp4Writer) rewriteBox(off int, box gomp4.IImmutableBox) error {
prevOff, err := w.w.Seek(0, io.SeekCurrent)
if err != nil {
return err
@ -76,12 +70,12 @@ func (w *mp4Writer) RewriteBox(off int, box gomp4.IImmutableBox) error { @@ -76,12 +70,12 @@ func (w *mp4Writer) RewriteBox(off int, box gomp4.IImmutableBox) error {
return err
}
_, err = w.WriteBoxStart(box)
_, err = w.writeBoxStart(box)
if err != nil {
return err
}
err = w.WriteBoxEnd()
err = w.writeBoxEnd()
if err != nil {
return err
}
@ -94,7 +88,6 @@ func (w *mp4Writer) RewriteBox(off int, box gomp4.IImmutableBox) error { @@ -94,7 +88,6 @@ func (w *mp4Writer) RewriteBox(off int, box gomp4.IImmutableBox) error {
return nil
}
// Bytes returns the MP4 content.
func (w *mp4Writer) Bytes() []byte {
func (w *mp4Writer) bytes() []byte {
return w.buf.Bytes()
}

96
internal/hls/fmp4/part_read.go

@ -0,0 +1,96 @@ @@ -0,0 +1,96 @@
package fmp4
import (
"bytes"
"fmt"
gomp4 "github.com/abema/go-mp4"
)
type partReadState int
const (
waitingTraf partReadState = iota
waitingTfhd
waitingTfdt
waitingTrun
)
// PartRead reads a FMP4 part file.
func PartRead(
byts []byte,
cb func(),
) error {
state := waitingTraf
var trackID uint32
var baseTime uint64
var entries []gomp4.TrunEntry
_, err := gomp4.ReadBoxStructure(bytes.NewReader(byts), func(h *gomp4.ReadHandle) (interface{}, error) {
switch h.BoxInfo.Type.String() {
case "traf":
if state != waitingTraf {
return nil, fmt.Errorf("decode error")
}
state = waitingTfhd
case "tfhd":
if state != waitingTfhd {
return nil, fmt.Errorf("decode error")
}
box, _, err := h.ReadPayload()
if err != nil {
return nil, err
}
trackID = box.(*gomp4.Tfhd).TrackID
state = waitingTfdt
case "tfdt":
if state != waitingTfdt {
return nil, fmt.Errorf("decode error")
}
box, _, err := h.ReadPayload()
if err != nil {
return nil, err
}
t := box.(*gomp4.Tfdt)
if t.FullBox.Version != 1 {
return nil, fmt.Errorf("unsupported tfdt version")
}
baseTime = t.BaseMediaDecodeTimeV1
state = waitingTrun
case "trun":
if state != waitingTrun {
return nil, fmt.Errorf("decode error")
}
box, _, err := h.ReadPayload()
if err != nil {
return nil, err
}
t := box.(*gomp4.Trun)
entries = t.Entries
state = waitingTraf
}
return h.Expand()
})
if err != nil {
return err
}
if state != waitingTraf {
return fmt.Errorf("parse error")
}
fmt.Println("TODO", trackID, baseTime, entries)
return nil
}

30
internal/hls/fmp4/part.go → internal/hls/fmp4/part_write.go

@ -13,7 +13,7 @@ func durationGoToMp4(v time.Duration, timescale time.Duration) int64 { @@ -13,7 +13,7 @@ func durationGoToMp4(v time.Duration, timescale time.Duration) int64 {
return int64(math.Round(float64(v*timescale) / float64(time.Second)))
}
func generatePartVideoTraf(
func partWriteVideoInfo(
w *mp4Writer,
trackID int,
videoSamples []*VideoSample,
@ -25,7 +25,7 @@ func generatePartVideoTraf( @@ -25,7 +25,7 @@ func generatePartVideoTraf(
- trun
*/
_, err := w.WriteBoxStart(&gomp4.Traf{}) // <traf>
_, err := w.writeBoxStart(&gomp4.Traf{}) // <traf>
if err != nil {
return nil, 0, err
}
@ -89,7 +89,7 @@ func generatePartVideoTraf( @@ -89,7 +89,7 @@ func generatePartVideoTraf(
return nil, 0, err
}
err = w.WriteBoxEnd() // </traf>
err = w.writeBoxEnd() // </traf>
if err != nil {
return nil, 0, err
}
@ -97,7 +97,7 @@ func generatePartVideoTraf( @@ -97,7 +97,7 @@ func generatePartVideoTraf(
return trun, trunOffset, nil
}
func generatePartAudioTraf(
func partWriteAudioInfo(
w *mp4Writer,
trackID int,
audioTrack *gortsplib.TrackMPEG4Audio,
@ -114,7 +114,7 @@ func generatePartAudioTraf( @@ -114,7 +114,7 @@ func generatePartAudioTraf(
return nil, 0, nil
}
_, err := w.WriteBoxStart(&gomp4.Traf{}) // <traf>
_, err := w.writeBoxStart(&gomp4.Traf{}) // <traf>
if err != nil {
return nil, 0, err
}
@ -167,7 +167,7 @@ func generatePartAudioTraf( @@ -167,7 +167,7 @@ func generatePartAudioTraf(
return nil, 0, err
}
err = w.WriteBoxEnd() // </traf>
err = w.writeBoxEnd() // </traf>
if err != nil {
return nil, 0, err
}
@ -175,8 +175,8 @@ func generatePartAudioTraf( @@ -175,8 +175,8 @@ func generatePartAudioTraf(
return trun, trunOffset, nil
}
// GeneratePart generates a FMP4 part file.
func GeneratePart(
// PartWrite generates a FMP4 part file.
func PartWrite(
videoTrack *gortsplib.TrackH264,
audioTrack *gortsplib.TrackMPEG4Audio,
videoSamples []*VideoSample,
@ -192,7 +192,7 @@ func GeneratePart( @@ -192,7 +192,7 @@ func GeneratePart(
w := newMP4Writer()
moofOffset, err := w.WriteBoxStart(&gomp4.Moof{}) // <moof>
moofOffset, err := w.writeBoxStart(&gomp4.Moof{}) // <moof>
if err != nil {
return nil, err
}
@ -218,7 +218,7 @@ func GeneratePart( @@ -218,7 +218,7 @@ func GeneratePart(
}
var err error
videoTrun, videoTrunOffset, err = generatePartVideoTraf(
videoTrun, videoTrunOffset, err = partWriteVideoInfo(
w, trackID, videoSamples)
if err != nil {
return nil, err
@ -231,13 +231,13 @@ func GeneratePart( @@ -231,13 +231,13 @@ func GeneratePart(
var audioTrunOffset int
if audioTrack != nil {
var err error
audioTrun, audioTrunOffset, err = generatePartAudioTraf(w, trackID, audioTrack, audioSamples)
audioTrun, audioTrunOffset, err = partWriteAudioInfo(w, trackID, audioTrack, audioSamples)
if err != nil {
return nil, err
}
}
err = w.WriteBoxEnd() // </moof>
err = w.writeBoxEnd() // </moof>
if err != nil {
return nil, err
}
@ -282,7 +282,7 @@ func GeneratePart( @@ -282,7 +282,7 @@ func GeneratePart(
if videoTrack != nil {
videoTrun.DataOffset = int32(mdatOffset - moofOffset + 8)
err = w.RewriteBox(videoTrunOffset, videoTrun)
err = w.rewriteBox(videoTrunOffset, videoTrun)
if err != nil {
return nil, err
}
@ -290,11 +290,11 @@ func GeneratePart( @@ -290,11 +290,11 @@ func GeneratePart(
if audioTrack != nil && audioTrun != nil {
audioTrun.DataOffset = int32(videoDataSize + mdatOffset - moofOffset + 8)
err = w.RewriteBox(audioTrunOffset, audioTrun)
err = w.rewriteBox(audioTrunOffset, audioTrun)
if err != nil {
return nil, err
}
}
return w.Bytes(), nil
return w.bytes(), nil
}

8
internal/hls/fmp4/part_test.go → internal/hls/fmp4/part_write_test.go

@ -9,7 +9,7 @@ import ( @@ -9,7 +9,7 @@ import (
"github.com/stretchr/testify/require"
)
func TestGeneratePart(t *testing.T) {
func TestPartWrite(t *testing.T) {
testVideoSamples := []*VideoSample{
{
NALUs: [][]byte{
@ -90,7 +90,7 @@ func TestGeneratePart(t *testing.T) { @@ -90,7 +90,7 @@ func TestGeneratePart(t *testing.T) {
testAudioSamples = testAudioSamples[:len(testAudioSamples)-1]
t.Run("video + audio", func(t *testing.T) {
byts, err := GeneratePart(testVideoTrack, testAudioTrack, testVideoSamples, testAudioSamples)
byts, err := PartWrite(testVideoTrack, testAudioTrack, testVideoSamples, testAudioSamples)
require.NoError(t, err)
boxes := []gomp4.BoxPath{
@ -110,7 +110,7 @@ func TestGeneratePart(t *testing.T) { @@ -110,7 +110,7 @@ func TestGeneratePart(t *testing.T) {
})
t.Run("video only", func(t *testing.T) {
byts, err := GeneratePart(testVideoTrack, nil, testVideoSamples, nil)
byts, err := PartWrite(testVideoTrack, nil, testVideoSamples, nil)
require.NoError(t, err)
boxes := []gomp4.BoxPath{
@ -126,7 +126,7 @@ func TestGeneratePart(t *testing.T) { @@ -126,7 +126,7 @@ func TestGeneratePart(t *testing.T) {
})
t.Run("audio only", func(t *testing.T) {
byts, err := GeneratePart(nil, testAudioTrack, nil, testAudioSamples)
byts, err := PartWrite(nil, testAudioTrack, nil, testAudioSamples)
require.NoError(t, err)
boxes := []gomp4.BoxPath{

2
internal/hls/muxer_variant_fmp4.go

@ -85,7 +85,7 @@ func (v *muxerVariantFMP4) file(name string, msn string, part string, skip strin @@ -85,7 +85,7 @@ func (v *muxerVariantFMP4) file(name string, msn string, part string, skip strin
if v.initContent == nil ||
(v.videoTrack != nil && (!bytes.Equal(v.videoLastSPS, sps) || !bytes.Equal(v.videoLastPPS, pps))) {
initContent, err := fmp4.GenerateInit(v.videoTrack, v.audioTrack)
initContent, err := fmp4.InitWrite(v.videoTrack, v.audioTrack)
if err != nil {
return &MuxerFileResponse{Status: http.StatusInternalServerError}
}

2
internal/hls/muxer_variant_fmp4_part.go

@ -73,7 +73,7 @@ func (p *muxerVariantFMP4Part) duration() time.Duration { @@ -73,7 +73,7 @@ func (p *muxerVariantFMP4Part) duration() time.Duration {
func (p *muxerVariantFMP4Part) finalize() error {
if len(p.videoSamples) > 0 || len(p.audioSamples) > 0 {
var err error
p.content, err = fmp4.GeneratePart(
p.content, err = fmp4.PartWrite(
p.videoTrack,
p.audioTrack,
p.videoSamples,

Loading…
Cancel
Save