rtmp: support reading and writing FLAC (#5778) (#5789)

This commit is contained in:
Alessandro Ros
2026-05-21 14:50:06 +02:00
committed by GitHub
parent 111302f9e3
commit 0edc3dbe86
41 changed files with 272 additions and 91 deletions
+44 -2
View File
@@ -5,6 +5,8 @@ import (
"errors"
"net"
"slices"
"strconv"
"strings"
"time"
"github.com/bluenviron/gortmplib"
@@ -13,6 +15,7 @@ import (
"github.com/bluenviron/gortsplib/v5/pkg/description"
"github.com/bluenviron/gortsplib/v5/pkg/format"
"github.com/bluenviron/mediacommon/v2/pkg/codecs/ac3"
"github.com/bluenviron/mediacommon/v2/pkg/codecs/flac"
"github.com/bluenviron/mediacommon/v2/pkg/codecs/h264"
"github.com/bluenviron/mediacommon/v2/pkg/codecs/h265"
"github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg1audio"
@@ -26,7 +29,7 @@ import (
var errNoSupportedCodecsFrom = errors.New(
"the stream doesn't contain any supported codec, which are currently " +
"AV1, VP9, H265, H264, Opus, MPEG-4 Audio, MPEG-1/2 Audio, AC-3, G711, LPCM")
"AV1, VP9, H265, H264, Opus, FLAC, MPEG-4 Audio (AAC), MPEG-1/2 Audio (MP3), AC-3, G711, LPCM")
func multiplyAndDivide2(v, m, d time.Duration) time.Duration {
secs := v / d
@@ -212,7 +215,11 @@ func FromStream(
if slices.Contains(conn.FourCcList, any(fourCCToString(message.FourCCOpus))) {
track := &gortmplib.Track{
Codec: &codecs.Opus{
ChannelCount: forma.ChannelCount,
IDHeader: &opus.IDHeader{
Version: 0x1,
ChannelCount: uint8(forma.ChannelCount),
PreSkip: 3840,
},
},
}
tracks = append(tracks, track)
@@ -453,6 +460,41 @@ func FromStream(
)
})
}
case *format.Generic:
if strings.HasPrefix(strings.ToLower(forma.RTPMap()), "flac/") &&
slices.Contains(conn.FourCcList, any(fourCCToString(message.FourCCFLAC))) {
sampleRate, _ := strconv.Atoi(forma.FMTP()["samplerate"])
channelCount, _ := strconv.Atoi(forma.FMTP()["channels"])
bitDepth, _ := strconv.Atoi(forma.FMTP()["bitdepth"])
track := &gortmplib.Track{
Codec: &codecs.FLAC{
StreamInfo: &flac.StreamInfo{
SampleRate: uint32(sampleRate),
ChannelCount: uint8(channelCount),
BitDepth: uint8(bitDepth),
},
},
}
tracks = append(tracks, track)
r.OnData(
media,
forma,
func(u *unit.Unit) error {
if u.NilPayload() {
return nil
}
nconn.SetWriteDeadline(time.Now().Add(writeTimeout))
return (*w).WriteFLAC(
track,
timestampToDuration(u.PTS, forma.ClockRate()),
u.Payload.(unit.PayloadFLAC),
)
})
}
}
}
}
+48 -2
View File
@@ -12,7 +12,9 @@ import (
"github.com/bluenviron/gortmplib/pkg/codecs"
"github.com/bluenviron/gortsplib/v5/pkg/description"
"github.com/bluenviron/gortsplib/v5/pkg/format"
"github.com/bluenviron/mediacommon/v2/pkg/codecs/flac"
"github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4audio"
"github.com/bluenviron/mediacommon/v2/pkg/codecs/opus"
"github.com/bluenviron/mediamtx/internal/logger"
"github.com/bluenviron/mediamtx/internal/stream"
"github.com/bluenviron/mediamtx/internal/test"
@@ -224,7 +226,12 @@ func TestFromStream(t *testing.T) {
},
expectedTracks: []*gortmplib.Track{
{Codec: &codecs.Opus{
ChannelCount: 2,
IDHeader: &opus.IDHeader{
Version: 0x1,
ChannelCount: 2,
PreSkip: 3840,
ChannelMappingTable: []uint8{},
},
}},
},
writeUnits: func(medias []*description.Media, subStream *stream.SubStream) {
@@ -445,6 +452,40 @@ func TestFromStream(t *testing.T) {
}
},
},
{
name: "flac",
medias: []*description.Media{
{
Formats: []format.Format{&format.Generic{
PayloadTyp: 96,
RTPMa: "FLAC/90000",
ClockRat: 90000,
FMT: map[string]string{
"samplerate": "44100",
"channels": "2",
"bitdepth": "16",
},
}},
},
},
expectedTracks: []*gortmplib.Track{
{Codec: &codecs.FLAC{
StreamInfo: &flac.StreamInfo{
SampleRate: 44100,
ChannelCount: 2,
BitDepth: 16,
},
}},
},
writeUnits: func(medias []*description.Media, subStream *stream.SubStream) {
for i := range 2 {
subStream.WriteUnit(medias[0], medias[0].Formats[0], &unit.Unit{
PTS: 90000 * 5 * int64(i),
Payload: unit.PayloadFLAC{3, 4},
})
}
},
},
{
name: "h265 + h264 + vp9 + av1 + opus + aac",
medias: []*description.Media{
@@ -489,7 +530,12 @@ func TestFromStream(t *testing.T) {
{Codec: &codecs.VP9{}},
{Codec: &codecs.AV1{}},
{Codec: &codecs.Opus{
ChannelCount: 2,
IDHeader: &opus.IDHeader{
Version: 0x1,
ChannelCount: 2,
PreSkip: 3840,
ChannelMappingTable: []uint8{},
},
}},
{Codec: &codecs.MPEG4Audio{
Config: test.FormatMPEG4Audio.Config,
+39 -5
View File
@@ -2,6 +2,7 @@ package rtmp
import (
"errors"
"strconv"
"time"
"github.com/bluenviron/gortmplib"
@@ -15,7 +16,7 @@ import (
var errNoSupportedCodecsTo = errors.New(
"the stream doesn't contain any supported codec, which are currently " +
"AV1, VP9, H265, H264, MPEG-4 Audio, MPEG-1/2 Audio, G711, LPCM")
"AV1, VP9, H265, H264, Opus, FLAC, MPEG-4 Audio (AAC), MPEG-1/2 Audio (MP3), AC-3, G711, LPCM")
func multiplyAndDivide(v, m, d int64) int64 {
secs := v / d
@@ -115,9 +116,13 @@ func ToStream(
})
case *codecs.Opus:
channelCount := 2
if codec.IDHeader != nil {
channelCount = int(codec.IDHeader.ChannelCount)
}
forma := &format.Opus{
PayloadTyp: 96,
ChannelCount: codec.ChannelCount,
ChannelCount: channelCount,
}
medi := &description.Media{
Type: description.MediaTypeAudio,
@@ -132,6 +137,38 @@ func ToStream(
})
})
case *codecs.FLAC:
sampleRate := 0
channelCount := 0
bitDepth := 0
if codec.StreamInfo != nil {
sampleRate = int(codec.StreamInfo.SampleRate)
channelCount = int(codec.StreamInfo.ChannelCount)
bitDepth = int(codec.StreamInfo.BitDepth)
}
forma := &format.Generic{
PayloadTyp: 96,
RTPMa: "flac/90000",
ClockRat: 90000,
FMT: map[string]string{
"samplerate": strconv.Itoa(sampleRate),
"channels": strconv.Itoa(channelCount),
"bitdepth": strconv.Itoa(bitDepth),
},
}
medi := &description.Media{
Type: description.MediaTypeApplication,
Formats: []format.Format{forma},
}
medias = append(medias, medi)
r.OnDataFLAC(track, func(pts time.Duration, frame []byte) {
(*subStream).WriteUnit(medi, forma, &unit.Unit{
PTS: durationToTimestamp(pts, forma.ClockRate()),
Payload: unit.PayloadFLAC(frame),
})
})
case *codecs.MPEG4Audio:
forma := &format.MPEG4Audio{
PayloadTyp: 96,
@@ -235,9 +272,6 @@ func ToStream(
Payload: unit.PayloadLPCM(samples),
})
})
default:
panic("should not happen")
}
}