hls: support generating streams with multiple audio tracks (#2728) (#3793)

This commit is contained in:
Alessandro Ros 2024-10-03 19:38:54 +02:00 committed by GitHub
parent 8960cbae5f
commit 4d0ce87f09
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 181 additions and 116 deletions

2
go.mod
View File

@ -9,7 +9,7 @@ require (
github.com/abema/go-mp4 v1.2.0
github.com/alecthomas/kong v1.2.1
github.com/asticode/go-astits v1.13.0
github.com/bluenviron/gohlslib v1.4.0
github.com/bluenviron/gohlslib/v2 v2.0.0-20241003172246-076f27fbe0f8
github.com/bluenviron/gortsplib/v4 v4.10.6
github.com/bluenviron/mediacommon v1.12.4
github.com/datarhei/gosrt v0.7.0

4
go.sum
View File

@ -20,8 +20,8 @@ github.com/asticode/go-astits v1.13.0 h1:XOgkaadfZODnyZRR5Y0/DWkA9vrkLLPLeeOvDwf
github.com/asticode/go-astits v1.13.0/go.mod h1:QSHmknZ51pf6KJdHKZHJTLlMegIrhega3LPWz3ND/iI=
github.com/benburkert/openpgp v0.0.0-20160410205803-c2471f86866c h1:8XZeJrs4+ZYhJeJ2aZxADI2tGADS15AzIF8MQ8XAhT4=
github.com/benburkert/openpgp v0.0.0-20160410205803-c2471f86866c/go.mod h1:x1vxHcL/9AVzuk5HOloOEPrtJY0MaalYr78afXZ+pWI=
github.com/bluenviron/gohlslib v1.4.0 h1:3a9W1x8eqlxJUKt1sJCunPGtti5ALIY2ik4GU0RVe7E=
github.com/bluenviron/gohlslib v1.4.0/go.mod h1:q5ZElzNw5GRbV1VEI45qkcPbKBco6BP58QEY5HyFsmo=
github.com/bluenviron/gohlslib/v2 v2.0.0-20241003172246-076f27fbe0f8 h1:OQeYfxJg5otVKa33HWJ63E+IxCJ5Ty0qwCBPD2JcIso=
github.com/bluenviron/gohlslib/v2 v2.0.0-20241003172246-076f27fbe0f8/go.mod h1:DVvQIj+MjYydWuYDCgP+s0/GplDgUSpDNXCA/BVLhu4=
github.com/bluenviron/gortsplib/v4 v4.10.6 h1:KMvVcU21xxQQu1Jqn6D/z/FoIMn+QEKE1dBDWt4aWvg=
github.com/bluenviron/gortsplib/v4 v4.10.6/go.mod h1:/7C8qoGEsIQupuVw8YnXANpqBMNBpZ+51xFreLGiN2g=
github.com/bluenviron/mediacommon v1.12.4 h1:7VrA/W/iDB7VELquXqRjgjzUSJT3llZYgXjFN9WkByo=

View File

@ -13,7 +13,7 @@ import (
"strings"
"time"
"github.com/bluenviron/gohlslib"
"github.com/bluenviron/gohlslib/v2"
"github.com/bluenviron/gortsplib/v4"
"github.com/bluenviron/gortsplib/v4/pkg/auth"

View File

@ -4,7 +4,7 @@ import (
"encoding/json"
"fmt"
"github.com/bluenviron/gohlslib"
"github.com/bluenviron/gohlslib/v2"
)
// HLSVariant is the hlsVariant parameter.

View File

@ -5,8 +5,8 @@ import (
"errors"
"fmt"
"github.com/bluenviron/gohlslib"
"github.com/bluenviron/gohlslib/pkg/codecs"
"github.com/bluenviron/gohlslib/v2"
"github.com/bluenviron/gohlslib/v2/pkg/codecs"
"github.com/bluenviron/gortsplib/v4/pkg/format"
"github.com/bluenviron/mediamtx/internal/asyncwriter"
"github.com/bluenviron/mediamtx/internal/logger"
@ -22,11 +22,18 @@ func setupVideoTrack(
stream *stream.Stream,
writer *asyncwriter.Writer,
muxer *gohlslib.Muxer,
) format.Format {
setuppedFormats map[format.Format]struct{},
) {
var videoFormatAV1 *format.AV1
videoMedia := stream.Desc().FindFormat(&videoFormatAV1)
if videoFormatAV1 != nil {
track := &gohlslib.Track{
Codec: &codecs.AV1{},
}
muxer.Tracks = append(muxer.Tracks, track)
setuppedFormats[videoFormatAV1] = struct{}{}
stream.AddReader(writer, videoMedia, videoFormatAV1, func(u unit.Unit) error {
tunit := u.(*unit.AV1)
@ -34,7 +41,7 @@ func setupVideoTrack(
return nil
}
err := muxer.WriteAV1(tunit.NTP, tunit.PTS, tunit.TU)
err := muxer.WriteAV1(track, tunit.NTP, tunit.PTS, tunit.TU)
if err != nil {
return fmt.Errorf("muxer error: %w", err)
}
@ -42,16 +49,19 @@ func setupVideoTrack(
return nil
})
muxer.VideoTrack = &gohlslib.Track{
Codec: &codecs.AV1{},
}
return videoFormatAV1
return
}
var videoFormatVP9 *format.VP9
videoMedia = stream.Desc().FindFormat(&videoFormatVP9)
if videoFormatVP9 != nil {
track := &gohlslib.Track{
Codec: &codecs.VP9{},
}
muxer.Tracks = append(muxer.Tracks, track)
setuppedFormats[videoFormatVP9] = struct{}{}
stream.AddReader(writer, videoMedia, videoFormatVP9, func(u unit.Unit) error {
tunit := u.(*unit.VP9)
@ -59,7 +69,7 @@ func setupVideoTrack(
return nil
}
err := muxer.WriteVP9(tunit.NTP, tunit.PTS, tunit.Frame)
err := muxer.WriteVP9(track, tunit.NTP, tunit.PTS, tunit.Frame)
if err != nil {
return fmt.Errorf("muxer error: %w", err)
}
@ -67,16 +77,24 @@ func setupVideoTrack(
return nil
})
muxer.VideoTrack = &gohlslib.Track{
Codec: &codecs.VP9{},
}
return videoFormatVP9
return
}
var videoFormatH265 *format.H265
videoMedia = stream.Desc().FindFormat(&videoFormatH265)
if videoFormatH265 != nil {
vps, sps, pps := videoFormatH265.SafeParams()
track := &gohlslib.Track{
Codec: &codecs.H265{
VPS: vps,
SPS: sps,
PPS: pps,
},
}
muxer.Tracks = append(muxer.Tracks, track)
setuppedFormats[videoFormatH265] = struct{}{}
stream.AddReader(writer, videoMedia, videoFormatH265, func(u unit.Unit) error {
tunit := u.(*unit.H265)
@ -84,7 +102,7 @@ func setupVideoTrack(
return nil
}
err := muxer.WriteH265(tunit.NTP, tunit.PTS, tunit.AU)
err := muxer.WriteH265(track, tunit.NTP, tunit.PTS, tunit.AU)
if err != nil {
return fmt.Errorf("muxer error: %w", err)
}
@ -92,22 +110,23 @@ func setupVideoTrack(
return nil
})
vps, sps, pps := videoFormatH265.SafeParams()
muxer.VideoTrack = &gohlslib.Track{
Codec: &codecs.H265{
VPS: vps,
SPS: sps,
PPS: pps,
},
}
return videoFormatH265
return
}
var videoFormatH264 *format.H264
videoMedia = stream.Desc().FindFormat(&videoFormatH264)
if videoFormatH264 != nil {
sps, pps := videoFormatH264.SafeParams()
track := &gohlslib.Track{
Codec: &codecs.H264{
SPS: sps,
PPS: pps,
},
}
muxer.Tracks = append(muxer.Tracks, track)
setuppedFormats[videoFormatH264] = struct{}{}
stream.AddReader(writer, videoMedia, videoFormatH264, func(u unit.Unit) error {
tunit := u.(*unit.H264)
@ -115,7 +134,7 @@ func setupVideoTrack(
return nil
}
err := muxer.WriteH264(tunit.NTP, tunit.PTS, tunit.AU)
err := muxer.WriteH264(track, tunit.NTP, tunit.PTS, tunit.AU)
if err != nil {
return fmt.Errorf("muxer error: %w", err)
}
@ -123,85 +142,76 @@ func setupVideoTrack(
return nil
})
sps, pps := videoFormatH264.SafeParams()
muxer.VideoTrack = &gohlslib.Track{
Codec: &codecs.H264{
SPS: sps,
PPS: pps,
},
}
return videoFormatH264
return
}
return nil
}
func setupAudioTrack(
func setupAudioTracks(
stream *stream.Stream,
writer *asyncwriter.Writer,
muxer *gohlslib.Muxer,
) format.Format {
var audioFormatOpus *format.Opus
audioMedia := stream.Desc().FindFormat(&audioFormatOpus)
setuppedFormats map[format.Format]struct{},
) {
for _, media := range stream.Desc().Medias {
for _, forma := range media.Formats {
switch forma := forma.(type) {
case *format.Opus:
track := &gohlslib.Track{
Codec: &codecs.Opus{
ChannelCount: forma.ChannelCount,
},
}
muxer.Tracks = append(muxer.Tracks, track)
setuppedFormats[forma] = struct{}{}
if audioFormatOpus != nil {
stream.AddReader(writer, audioMedia, audioFormatOpus, func(u unit.Unit) error {
tunit := u.(*unit.Opus)
stream.AddReader(writer, media, forma, func(u unit.Unit) error {
tunit := u.(*unit.Opus)
err := muxer.WriteOpus(
tunit.NTP,
tunit.PTS,
tunit.Packets)
if err != nil {
return fmt.Errorf("muxer error: %w", err)
}
err := muxer.WriteOpus(
track,
tunit.NTP,
tunit.PTS,
tunit.Packets)
if err != nil {
return fmt.Errorf("muxer error: %w", err)
}
return nil
})
muxer.AudioTrack = &gohlslib.Track{
Codec: &codecs.Opus{
ChannelCount: audioFormatOpus.ChannelCount,
},
}
return audioFormatOpus
}
var audioFormatMPEG4Audio *format.MPEG4Audio
audioMedia = stream.Desc().FindFormat(&audioFormatMPEG4Audio)
if audioFormatMPEG4Audio != nil {
co := audioFormatMPEG4Audio.GetConfig()
if co != nil {
stream.AddReader(writer, audioMedia, audioFormatMPEG4Audio, func(u unit.Unit) error {
tunit := u.(*unit.MPEG4Audio)
if tunit.AUs == nil {
return nil
})
case *format.MPEG4Audio:
co := forma.GetConfig()
if co != nil {
track := &gohlslib.Track{
Codec: &codecs.MPEG4Audio{
Config: *co,
},
}
muxer.Tracks = append(muxer.Tracks, track)
setuppedFormats[forma] = struct{}{}
stream.AddReader(writer, media, forma, func(u unit.Unit) error {
tunit := u.(*unit.MPEG4Audio)
if tunit.AUs == nil {
return nil
}
err := muxer.WriteMPEG4Audio(
track,
tunit.NTP,
tunit.PTS,
tunit.AUs)
if err != nil {
return fmt.Errorf("muxer error: %w", err)
}
return nil
})
}
err := muxer.WriteMPEG4Audio(
tunit.NTP,
tunit.PTS,
tunit.AUs)
if err != nil {
return fmt.Errorf("muxer error: %w", err)
}
return nil
})
muxer.AudioTrack = &gohlslib.Track{
Codec: &codecs.MPEG4Audio{
Config: *co,
},
}
return audioFormatMPEG4Audio
}
}
return nil
}
// FromStream maps a MediaMTX stream to a HLS muxer.
@ -211,26 +221,30 @@ func FromStream(
muxer *gohlslib.Muxer,
l logger.Writer,
) error {
videoFormat := setupVideoTrack(
setuppedFormats := make(map[format.Format]struct{})
setupVideoTrack(
stream,
writer,
muxer,
setuppedFormats,
)
audioFormat := setupAudioTrack(
setupAudioTracks(
stream,
writer,
muxer,
setuppedFormats,
)
if videoFormat == nil && audioFormat == nil {
if len(muxer.Tracks) == 0 {
return ErrNoSupportedCodecs
}
n := 1
for _, media := range stream.Desc().Medias {
for _, forma := range media.Formats {
if forma != videoFormat && forma != audioFormat {
if _, ok := setuppedFormats[forma]; !ok {
l.Log(logger.Warn, "skipping track %d (%s)", n, forma.Codec())
}
n++

View File

@ -4,7 +4,7 @@ import (
"fmt"
"testing"
"github.com/bluenviron/gohlslib"
"github.com/bluenviron/gohlslib/v2"
"github.com/bluenviron/gortsplib/v4/pkg/description"
"github.com/bluenviron/gortsplib/v4/pkg/format"
"github.com/bluenviron/mediamtx/internal/asyncwriter"
@ -32,7 +32,9 @@ func TestFromStreamNoSupportedCodecs(t *testing.T) {
t.Error("should not happen")
})
err = FromStream(stream, writer, nil, l)
m := &gohlslib.Muxer{}
err = FromStream(stream, writer, m, l)
require.Equal(t, ErrNoSupportedCodecs, err)
}

View File

@ -3,8 +3,8 @@ package hls
import (
"time"
"github.com/bluenviron/gohlslib"
"github.com/bluenviron/gohlslib/pkg/codecs"
"github.com/bluenviron/gohlslib/v2"
"github.com/bluenviron/gohlslib/v2/pkg/codecs"
"github.com/bluenviron/gortsplib/v4/pkg/description"
"github.com/bluenviron/gortsplib/v4/pkg/format"
"github.com/bluenviron/mediamtx/internal/stream"

View File

@ -3,7 +3,7 @@ package hls
import (
"testing"
"github.com/bluenviron/gohlslib"
"github.com/bluenviron/gohlslib/v2"
"github.com/stretchr/testify/require"
)

View File

@ -36,12 +36,41 @@ html, body {
box-sizing: border-box;
text-shadow: 0 0 5px black;
}
#lang-icon {
display: none;
position: absolute;
top: 20px;
right: 20px;
width: 30px;
height: 30px;
background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1MCA1MCIgZmlsbD0iI2ZmZiIgeG1sbnM6dj0iaHR0cHM6Ly92ZWN0YS5pby9uYW5vIj48cGF0aCBkPSJNMzguNSAzMy45bC0xLjktMS42YzIuNS0yLjkgMy44LTYuMyAzLjgtOS45IDAtMy4xLTEtNi4xLTIuOS04LjhsMi4xLTEuNWMyLjIgMy4xIDMuNCA2LjYgMy40IDEwLjItLjEgNC4zLTEuNiA4LjMtNC41IDExLjZ6TTUuNiAyMy4yaC0zYy0uNSAwLTEgLjUtMSAxLjF2MTAuNWMwIC42LjQgMS4xIDEgMS4xaDNjLjIgMCAuMyAwIC40LjFsMTMuOCA3LjhjLjYuNCAxLjQtLjIgMS40LTFWMTYuM2MwLS44LS44LTEuMy0xLjQtMUw2LjEgMjMuMWMtLjIuMS0uMy4xLS41LjF6bTIxLTE2LjlMMTIuOCAxNGMtLjEuMS0uMy4xLS40LjFoLTNjLS41IDAtMSAuNS0xIDEuMVYyMGwxMi4yLTYuOGExLjM2IDEuMzYgMCAwIDEgMS41IDBjLjUuMy44LjguOCAxLjV2MTcuOWwzLjcgMi4xYy42LjQgMS40LS4yIDEuNC0xVjcuMmMuMS0uOC0uNy0xLjMtMS40LS45em0xNi41IDMwLjJsLTEuOS0xLjZjMy4xLTMuNyA0LjctOCA0LjctMTIuNSAwLTQtMS4zLTcuOC0zLjctMTEuMmwyLjEtMS41YzIuNyAzLjggNC4yIDguMiA0LjIgMTIuNy0uMiA1LjEtMiA5LjktNS40IDE0LjF6TTM1IDMxLjFsLTItMS42YzEuNy0yLjEgMi42LTQuNiAyLjYtNy4yIDAtMi40LS44LTQuNy0yLjItNi43bDItMS41YzEuOCAyLjUgMi43IDUuMyAyLjcgOC4yIDAgMy4yLTEuMSA2LjItMy4xIDguOHoiLz48L3N2Zz4=");
background-size: 80%;
background-position: center;
background-repeat: no-repeat;
cursor: pointer;
}
#lang-list {
display: none;
position: absolute;
top: 100%;
right: 0;
background: rgb(190, 190, 190);
color: black;
}
#lang-icon:hover #lang-list {
display: block;
}
#lang-list div {
border-bottom: 1px solid black;
padding: 5px 15px;
}
</style>
</head>
<body>
<video id="video"></video>
<div id="message"></div>
<div id="lang-icon"><div id="lang-list"></div></div>
<script src="hls.min.js"></script>
@ -51,6 +80,8 @@ const retryPause = 2000;
const video = document.getElementById('video');
const message = document.getElementById('message');
const langIcon = document.getElementById('lang-icon');
const langList = document.getElementById('lang-list');
let defaultControls = false;
@ -83,8 +114,11 @@ const loadStream = () => {
if (data.fatal) {
hls.destroy();
langIcon.style.display = 'none';
langList.innerHTML = '';
if (data.details === 'manifestIncompatibleCodecsError') {
setMessage('stream makes use of codecs which are incompatible with this browser or operative system');
setMessage('stream makes use of codecs which are not compatible with this browser or operative system');
} else if (data.response && data.response.code === 404) {
setMessage('stream not found, retrying in some seconds');
} else {
@ -99,7 +133,19 @@ const loadStream = () => {
hls.loadSource('index.m3u8' + window.location.search);
});
hls.on(Hls.Events.MANIFEST_PARSED, () => {
hls.on(Hls.Events.MANIFEST_LOADED, () => {
if (hls.audioTracks.length > 1) {
for (const track of hls.audioTracks) {
const div = document.createElement('DIV');
div.innerText = track.name;
div.addEventListener('click', () => {
hls.audioTrack = track.id;
});
langList.appendChild(div);
}
langIcon.style.display = 'block';
}
setMessage('');
video.play();
});

View File

@ -5,7 +5,7 @@ import (
"path/filepath"
"time"
"github.com/bluenviron/gohlslib"
"github.com/bluenviron/gohlslib/v2"
"github.com/bluenviron/mediamtx/internal/asyncwriter"
"github.com/bluenviron/mediamtx/internal/conf"
"github.com/bluenviron/mediamtx/internal/defs"
@ -42,12 +42,15 @@ func (mi *muxerInstance) initialize() error {
}
mi.hmuxer = &gohlslib.Muxer{
Variant: gohlslib.MuxerVariant(mi.variant),
SegmentCount: mi.segmentCount,
SegmentDuration: time.Duration(mi.segmentDuration),
PartDuration: time.Duration(mi.partDuration),
SegmentMaxSize: uint64(mi.segmentMaxSize),
Directory: muxerDirectory,
Variant: gohlslib.MuxerVariant(mi.variant),
SegmentCount: mi.segmentCount,
SegmentMinDuration: time.Duration(mi.segmentDuration),
PartMinDuration: time.Duration(mi.partDuration),
SegmentMaxSize: uint64(mi.segmentMaxSize),
Directory: muxerDirectory,
OnEncodeError: func(err error) {
mi.Log(logger.Warn, err.Error())
},
}
err := hls.FromStream(mi.stream, mi.writer, mi.hmuxer, mi)

View File

@ -9,8 +9,8 @@ import (
"testing"
"time"
"github.com/bluenviron/gohlslib"
"github.com/bluenviron/gohlslib/pkg/codecs"
"github.com/bluenviron/gohlslib/v2"
"github.com/bluenviron/gohlslib/v2/pkg/codecs"
"github.com/bluenviron/gortsplib/v4/pkg/description"
"github.com/bluenviron/mediamtx/internal/conf"
"github.com/bluenviron/mediamtx/internal/defs"

View File

@ -5,7 +5,7 @@ import (
"net/http"
"time"
"github.com/bluenviron/gohlslib"
"github.com/bluenviron/gohlslib/v2"
"github.com/bluenviron/gortsplib/v4/pkg/description"
"github.com/bluenviron/mediamtx/internal/conf"