Skip to content

Commit

Permalink
muxer: keep TARGETDURATION constant to minimize iOS errors (#187)
Browse files Browse the repository at this point in the history
  • Loading branch information
aler9 authored Oct 2, 2024
1 parent f13296d commit a94e6df
Show file tree
Hide file tree
Showing 4 changed files with 92 additions and 56 deletions.
21 changes: 14 additions & 7 deletions muxer.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,12 @@ import (
"encoding/hex"
"fmt"
"io"
"log"
"net/http"
"strconv"
"sync"
"time"

"github.com/bluenviron/mediacommon/pkg/formats/fmp4"

"github.com/bluenviron/gohlslib/v2/pkg/codecs"
"github.com/bluenviron/gohlslib/v2/pkg/storage"
)
Expand Down Expand Up @@ -80,11 +79,8 @@ func fmp4TimeScale(c codecs.Codec) uint32 {
return 90000
}

type fmp4AugmentedSample struct {
fmp4.PartSample
dts time.Duration
ntp time.Time
}
// MuxerOnEncodeErrorFunc is the prototype of Muxer.OnEncodeError.
type MuxerOnEncodeErrorFunc func(err error)

// Muxer is a HLS muxer.
type Muxer struct {
Expand Down Expand Up @@ -121,6 +117,12 @@ type Muxer struct {
// than saving them on RAM, but allows to preserve RAM.
Directory string

//
// callbacks (all optional)
//
// called when a non-fatal encode error occurs.
OnEncodeError MuxerOnEncodeErrorFunc

//
// private
//
Expand Down Expand Up @@ -157,6 +159,11 @@ func (m *Muxer) Start() error {
if m.SegmentMaxSize == 0 {
m.SegmentMaxSize = 50 * 1024 * 1024
}
if m.OnEncodeError == nil {
m.OnEncodeError = func(e error) {
log.Printf("%v", e)
}
}

if len(m.Tracks) == 0 {
return fmt.Errorf("at least one track must be provided")
Expand Down
6 changes: 6 additions & 0 deletions muxer_segmenter.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,12 @@ func findCompatiblePartDuration(
return i
}

type fmp4AugmentedSample struct {
fmp4.PartSample
dts time.Duration
ntp time.Time
}

type muxerSegmenter struct {
muxer *Muxer // TODO: remove

Expand Down
45 changes: 0 additions & 45 deletions muxer_server.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package gohlslib

import (
"math"
"net/http"
"net/url"
"path/filepath"
Expand All @@ -21,50 +20,6 @@ func boolPtr(v bool) *bool {
return &v
}

func targetDuration(segments []muxerSegment) int {
ret := int(0)

// EXTINF, when rounded to the nearest integer, must be <= EXT-X-TARGETDURATION
for _, sog := range segments {
v := int(math.Round(sog.getDuration().Seconds()))
if v > ret {
ret = v
}
}

return ret
}

func partTargetDuration(
streams []*muxerStream,
) time.Duration {
var ret time.Duration

for _, stream := range streams {
for _, seg := range stream.segments {
seg, ok := seg.(*muxerSegmentFMP4)
if !ok {
continue
}

for _, part := range seg.parts {
if part.getDuration() > ret {
ret = part.getDuration()
}
}
}

for _, part := range stream.nextSegment.(*muxerSegmentFMP4).parts {
if part.getDuration() > ret {
ret = part.getDuration()
}
}
}

// round to milliseconds to avoid changes, that are illegal on iOS
return time.Millisecond * time.Duration(math.Ceil(float64(ret)/float64(time.Millisecond)))
}

func parseMSNPart(msn string, part string) (uint64, uint64, error) {
var msnint uint64
if msn != "" {
Expand Down
76 changes: 72 additions & 4 deletions muxer_stream.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package gohlslib

import (
"fmt"
"io"
"math"
"net/http"
"net/url"
"strconv"
Expand Down Expand Up @@ -42,6 +44,49 @@ func containsCodec(cs []string, c string) bool {
return false
}

func targetDuration(segments []muxerSegment) int {
ret := int(0)

// EXTINF, when rounded to the nearest integer, must be <= EXT-X-TARGETDURATION
for _, sog := range segments {
v := int(math.Round(sog.getDuration().Seconds()))
if v > ret {
ret = v
}
}

return ret
}

func partTargetDuration(
segments []muxerSegment,
nextSegmentParts []*muxerPart,
) time.Duration {
var ret time.Duration

for _, seg := range segments {
seg, ok := seg.(*muxerSegmentFMP4)
if !ok {
continue
}

for _, part := range seg.parts {
if part.getDuration() > ret {
ret = part.getDuration()
}
}
}

for _, part := range nextSegmentParts {
if part.getDuration() > ret {
ret = part.getDuration()
}
}

// round to milliseconds to minimize changes
return time.Millisecond * time.Duration(math.Ceil(float64(ret)/float64(time.Millisecond)))
}

type generateMediaPlaylistFunc func(
isDeltaUpdate bool,
rawQuery string,
Expand All @@ -63,6 +108,8 @@ type muxerStream struct {
nextSegment muxerSegment
nextPart *muxerPart // low-latency only
initFilePresent bool // fmp4 only
targetDuration int
partTargetDuration time.Duration
}

func (s *muxerStream) initialize() {
Expand Down Expand Up @@ -343,7 +390,7 @@ func (s *muxerStream) generateMediaPlaylistMPEGTS(
pl := &playlist.Media{
Version: 3,
AllowCache: boolPtr(false),
TargetDuration: targetDuration(s.segments),
TargetDuration: s.targetDuration,
MediaSequence: s.muxer.segmentDeleteCount,
}

Expand Down Expand Up @@ -380,8 +427,7 @@ func (s *muxerStream) generateMediaPlaylistFMP4(
}

if s.muxer.Variant == MuxerVariantLowLatency {
partTarget := partTargetDuration(s.muxer.streams)
partHoldBack := (partTarget * 25) / 10
partHoldBack := (s.partTargetDuration * 25) / 10

pl.ServerControl = &playlist.MediaServerControl{
CanBlockReload: true,
Expand All @@ -390,7 +436,7 @@ func (s *muxerStream) generateMediaPlaylistFMP4(
}

pl.PartInf = &playlist.MediaPartInf{
PartTarget: partTarget,
PartTarget: s.partTargetDuration,
}
}

Expand Down Expand Up @@ -619,6 +665,19 @@ func (s *muxerStream) rotateParts(nextDTS time.Duration, createNew bool) error {
}
}

// while segment target duration can be increased indefinitely,
// part target duration cannot, since
// "The duration of a Partial Segment MUST be at least 85% of the Part Target Duration"
// so it's better to reset it every time.
partTargetDuration := partTargetDuration(s.segments, part.segment.parts)
if s.partTargetDuration == 0 {
s.partTargetDuration = partTargetDuration
} else if partTargetDuration != s.partTargetDuration {
s.muxer.OnEncodeError(fmt.Errorf("part duration changed from %v to %v - this will cause an error in iOS clients",
s.partTargetDuration, partTargetDuration))
s.partTargetDuration = partTargetDuration
}

if createNew {
nextPart := &muxerPart{
stream: s,
Expand Down Expand Up @@ -710,6 +769,15 @@ func (s *muxerStream) rotateSegments(
}
}

targetDuration := targetDuration(s.segments)
if s.targetDuration == 0 {
s.targetDuration = targetDuration
} else if targetDuration > s.targetDuration {
s.muxer.OnEncodeError(fmt.Errorf("segment duration changed from %ds to %ds - this will cause an error in iOS clients",
s.targetDuration, targetDuration))
s.targetDuration = targetDuration
}

// create next segment
var nextSegment muxerSegment
if s.muxer.Variant == MuxerVariantMPEGTS {
Expand Down

0 comments on commit a94e6df

Please sign in to comment.