Skip to content

Commit

Permalink
muxer: support multiple audio tracks (bluenviron/mediamtx#2728) (#181)
Browse files Browse the repository at this point in the history
* muxer: support multiple audio tracks (bluenviron/mediamtx#2728)

* fix support for audio-only multitracks

* fix tests and rendition flags

* update README
  • Loading branch information
aler9 authored Oct 2, 2024
1 parent 56d25b4 commit f13296d
Show file tree
Hide file tree
Showing 6 changed files with 405 additions and 215 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
[![Lint](https://github.com/bluenviron/gohlslib/workflows/lint/badge.svg)](https://github.com/bluenviron/gohlslib/actions?query=workflow:lint)
[![Go Report Card](https://goreportcard.com/badge/github.com/bluenviron/gohlslib)](https://goreportcard.com/report/github.com/bluenviron/gohlslib)
[![CodeCov](https://codecov.io/gh/bluenviron/gohlslib/branch/main/graph/badge.svg)](https://app.codecov.io/gh/bluenviron/gohlslib/branch/main)
[![PkgGoDev](https://pkg.go.dev/badge/github.com/bluenviron/gohlslib)](https://pkg.go.dev/github.com/bluenviron/gohlslib#pkg-index)
[![PkgGoDev](https://pkg.go.dev/badge/github.com/bluenviron/gohlslib/v2)](https://pkg.go.dev/github.com/bluenviron/gohlslib/v2#pkg-index)

HLS client and muxer library for the Go programming language, written for [MediaMTX](https://github.com/bluenviron/mediamtx).

Expand All @@ -15,12 +15,14 @@ Features:
* Client

* Read streams in MPEG-TS, fMP4 or Low-latency format
* Read a single video track and/or a single audio track
* Read tracks encoded with AV1, VP9, H265, H264, Opus, MPEG-4 Audio (AAC)
* Get absolute timestamp of incoming data

* Muxer

* Generate streams in MPEG-TS, fMP4 or Low-latency format
* Write a single video track and/or multiple audio tracks
* Write tracks encoded with AV1, VP9, H265, H264, Opus, MPEG-4 audio (AAC)
* Save generated segments on disk

Expand Down
12 changes: 7 additions & 5 deletions examples/muxer/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,13 @@ func handleIndex(wrapped http.HandlerFunc) http.HandlerFunc {
}

func main() {
videoTrack := &gohlslib.Track{
Codec: &codecs.H264{},
}

// create the HLS muxer
mux := &gohlslib.Muxer{
VideoTrack: &gohlslib.Track{
Codec: &codecs.H264{},
},
Tracks: []*gohlslib.Track{videoTrack},
}
err := mux.Start()
if err != nil {
Expand Down Expand Up @@ -80,7 +82,7 @@ func main() {
for _, track := range r.Tracks() {
if _, ok := track.Codec.(*mpegts.CodecH264); ok {
// setup a callback that is called once a H264 access unit is received
r.OnDataH26x(track, func(rawPTS int64, _ int64, au [][]byte) error {
r.OnDataH264(track, func(rawPTS int64, _ int64, au [][]byte) error {
// decode the time
if timeDec == nil {
timeDec = mpegts.NewTimeDecoder(rawPTS)
Expand All @@ -89,7 +91,7 @@ func main() {

// pass the access unit to the HLS muxer
log.Printf("visit http://localhost:8080 - encoding access unit with PTS = %v", pts)
mux.WriteH264(time.Now(), pts, au)
mux.WriteH264(videoTrack, time.Now(), pts, au)

return nil
})
Expand Down
158 changes: 99 additions & 59 deletions muxer.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,14 @@ func (w *switchableWriter) Write(p []byte) (int, error) {
return w.w.Write(p)
}

func isVideo(codec codecs.Codec) bool {
switch codec.(type) {
case *codecs.AV1, *codecs.VP9, *codecs.H265, *codecs.H264:
return true
}
return false
}

// a prefix is needed to prevent usage of cached segments
// from previous muxing sessions.
func generatePrefix() (string, error) {
Expand Down Expand Up @@ -81,12 +89,10 @@ type fmp4AugmentedSample struct {
// Muxer is a HLS muxer.
type Muxer struct {
//
// parameters (all optional except VideoTrack or AudioTrack).
// parameters (all optional except Tracks).
//
// video track.
VideoTrack *Track
// audio track.
AudioTrack *Track
// tracks.
Tracks []*Track
// Variant to use.
// It defaults to MuxerVariantLowLatency
Variant MuxerVariant
Expand Down Expand Up @@ -152,21 +158,44 @@ func (m *Muxer) Start() error {
m.SegmentMaxSize = 50 * 1024 * 1024
}

if m.VideoTrack == nil && m.AudioTrack == nil {
return fmt.Errorf("one between VideoTrack and AudioTrack is required")
if len(m.Tracks) == 0 {
return fmt.Errorf("at least one track must be provided")
}

hasVideo := false
hasAudio := false

if m.Variant == MuxerVariantMPEGTS {
if m.VideoTrack != nil {
if _, ok := m.VideoTrack.Codec.(*codecs.H264); !ok {
return fmt.Errorf(
"the MPEG-TS variant of HLS supports H264 video only")
for _, track := range m.Tracks {
if isVideo(track.Codec) {
if hasVideo {
return fmt.Errorf("the MPEG-TS variant of HLS supports a single video track only")
}
if _, ok := track.Codec.(*codecs.H264); !ok {
return fmt.Errorf(
"the MPEG-TS variant of HLS supports H264 video only")
}
hasVideo = true
} else {
if hasAudio {
return fmt.Errorf("the MPEG-TS variant of HLS supports a single audio track only")
}
if _, ok := track.Codec.(*codecs.MPEG4Audio); !ok {
return fmt.Errorf(
"the MPEG-TS variant of HLS supports MPEG-4 Audio only")
}
hasAudio = true
}
}
if m.AudioTrack != nil {
if _, ok := m.AudioTrack.Codec.(*codecs.MPEG4Audio); !ok {
return fmt.Errorf(
"the MPEG-TS variant of HLS supports MPEG-4 Audio only")
} else {
for _, track := range m.Tracks {
if isVideo(track.Codec) {
if hasVideo {
return fmt.Errorf("only one video track is currently supported")
}
hasVideo = true
} else {
hasAudio = true
}
}
}
Expand Down Expand Up @@ -196,26 +225,15 @@ func (m *Muxer) Start() error {
}
m.server.initialize()

if m.VideoTrack != nil {
track := &muxerTrack{
Track: m.VideoTrack,
variant: m.Variant,
isLeading: true,
}
track.initialize()
m.mtracks = append(m.mtracks, track)
m.mtracksByTrack[m.VideoTrack] = track
}

if m.AudioTrack != nil {
track := &muxerTrack{
Track: m.AudioTrack,
for i, track := range m.Tracks {
mtrack := &muxerTrack{
Track: track,
variant: m.Variant,
isLeading: m.VideoTrack == nil,
isLeading: isVideo(track.Codec) || (!hasVideo && i == 0),
}
track.initialize()
m.mtracks = append(m.mtracks, track)
m.mtracksByTrack[m.AudioTrack] = track
mtrack.initialize()
m.mtracks = append(m.mtracks, mtrack)
m.mtracksByTrack[track] = mtrack
}

if m.Variant == MuxerVariantMPEGTS {
Expand All @@ -230,33 +248,45 @@ func (m *Muxer) Start() error {
switch {
case m.Variant == MuxerVariantMPEGTS:
stream := &muxerStream{
muxer: m,
tracks: m.mtracks,
id: "main",
muxer: m,
tracks: m.mtracks,
id: "main",
isLeading: true,
}
stream.initialize()
m.streams = append(m.streams, stream)

default:
if m.VideoTrack != nil {
videoStream := &muxerStream{
muxer: m,
tracks: []*muxerTrack{m.mtracksByTrack[m.VideoTrack]},
id: "video",
defaultRenditionChosen := false

for i, track := range m.mtracks {
var id string
if isVideo(track.Codec) {
id = "video" + strconv.FormatInt(int64(i+1), 10)
} else {
id = "audio" + strconv.FormatInt(int64(i+1), 10)
}
videoStream.initialize()
m.streams = append(m.streams, videoStream)
}

if m.AudioTrack != nil {
audioStream := &muxerStream{
muxer: m,
tracks: []*muxerTrack{m.mtracksByTrack[m.AudioTrack]},
id: "audio",
isRendition: m.VideoTrack != nil,
isRendition := !track.isLeading || (!isVideo(track.Codec) && len(m.Tracks) > 1)

var isDefaultRendition bool
if isRendition && !defaultRenditionChosen {
isDefaultRendition = true
defaultRenditionChosen = true
} else {
isDefaultRendition = false
}
audioStream.initialize()
m.streams = append(m.streams, audioStream)

stream := &muxerStream{
muxer: m,
tracks: []*muxerTrack{track},
id: id,
isLeading: track.isLeading,
isRendition: isRendition,
isDefaultRendition: isDefaultRendition,
}
stream.initialize()
m.streams = append(m.streams, stream)
}
}

Expand Down Expand Up @@ -290,52 +320,62 @@ func (m *Muxer) Close() {

// WriteAV1 writes an AV1 temporal unit.
func (m *Muxer) WriteAV1(
track *Track,
ntp time.Time,
pts time.Duration,
tu [][]byte,
) error {
return m.segmenter.writeAV1(ntp, pts, tu)
return m.segmenter.writeAV1(m.mtracksByTrack[track], ntp, pts, tu)
}

// WriteVP9 writes a VP9 frame.
func (m *Muxer) WriteVP9(
track *Track,
ntp time.Time,
pts time.Duration,
frame []byte,
) error {
return m.segmenter.writeVP9(ntp, pts, frame)
return m.segmenter.writeVP9(m.mtracksByTrack[track], ntp, pts, frame)
}

// WriteH265 writes an H265 access unit.
func (m *Muxer) WriteH265(
track *Track,
ntp time.Time,
pts time.Duration,
au [][]byte,
) error {
return m.segmenter.writeH265(ntp, pts, au)
return m.segmenter.writeH265(m.mtracksByTrack[track], ntp, pts, au)
}

// WriteH264 writes an H264 access unit.
func (m *Muxer) WriteH264(
track *Track,
ntp time.Time,
pts time.Duration,
au [][]byte,
) error {
return m.segmenter.writeH264(ntp, pts, au)
return m.segmenter.writeH264(m.mtracksByTrack[track], ntp, pts, au)
}

// WriteOpus writes Opus packets.
func (m *Muxer) WriteOpus(
track *Track,
ntp time.Time,
pts time.Duration,
packets [][]byte,
) error {
return m.segmenter.writeOpus(ntp, pts, packets)
return m.segmenter.writeOpus(m.mtracksByTrack[track], ntp, pts, packets)
}

// WriteMPEG4Audio writes MPEG-4 Audio access units.
func (m *Muxer) WriteMPEG4Audio(ntp time.Time, pts time.Duration, aus [][]byte) error {
return m.segmenter.writeMPEG4Audio(ntp, pts, aus)
func (m *Muxer) WriteMPEG4Audio(
track *Track,
ntp time.Time,
pts time.Duration,
aus [][]byte,
) error {
return m.segmenter.writeMPEG4Audio(m.mtracksByTrack[track], ntp, pts, aus)
}

// Handle handles a HTTP request.
Expand Down
Loading

0 comments on commit f13296d

Please sign in to comment.