solve codec labels internally (#5603)

stop using gortsplib format.Format.Codec() for getting codec names.
This commit is contained in:
Alessandro Ros
2026-03-21 18:22:49 +01:00
committed by GitHub
parent 0f42f087ab
commit 3381b196fb
23 changed files with 539 additions and 585 deletions
+3 -3
View File
@@ -65,13 +65,13 @@ components:
- H265
- H264
- MPEG-4 Video
- MPEG-1 Video
- MJPEG
- MPEG-1/2 Video
- M-JPEG
- Opus
- Vorbis
- MPEG-4 Audio
- MPEG-4 Audio LATM
- MPEG-1 Audio
- MPEG-1/2 Audio
- AC3
- Speex
- G726
+3 -2
View File
@@ -7,6 +7,7 @@ import (
"github.com/bluenviron/mediamtx/internal/conf"
"github.com/bluenviron/mediamtx/internal/defs"
"github.com/bluenviron/mediamtx/internal/formatlabel"
"github.com/bluenviron/mediamtx/internal/test"
"github.com/stretchr/testify/require"
)
@@ -41,7 +42,7 @@ func TestPathsList(t *testing.T) {
Source: &defs.APIPathSource{Type: defs.APIPathSourceTypeRTSPSession, ID: "pub1"},
Ready: true,
ReadyTime: &now,
Tracks: []defs.APIPathTrackCodec{defs.APIPathTrackCodecH264, defs.APIPathTrackCodecOpus},
Tracks: []defs.APIPathTrackCodec{formatlabel.H264, formatlabel.Opus},
InboundBytes: 1000,
OutboundBytes: 2000,
InboundFramesInError: 3,
@@ -100,7 +101,7 @@ func TestPathsGet(t *testing.T) {
Source: &defs.APIPathSource{Type: defs.APIPathSourceTypeRTSPSession, ID: "session123"},
Ready: true,
ReadyTime: &now,
Tracks: []defs.APIPathTrackCodec{defs.APIPathTrackCodecH264, defs.APIPathTrackCodecOpus},
Tracks: []defs.APIPathTrackCodec{formatlabel.H264, formatlabel.Opus},
InboundBytes: 123456,
OutboundBytes: 789012,
InboundFramesInError: 12,
+4 -3
View File
@@ -27,6 +27,7 @@ import (
"github.com/stretchr/testify/require"
"github.com/bluenviron/mediamtx/internal/defs"
"github.com/bluenviron/mediamtx/internal/formatlabel"
"github.com/bluenviron/mediamtx/internal/protocols/webrtc"
"github.com/bluenviron/mediamtx/internal/protocols/whip"
"github.com/bluenviron/mediamtx/internal/test"
@@ -140,7 +141,7 @@ func TestAPIPathsList(t *testing.T) {
Type: "rtspSession",
},
Ready: true,
Tracks: []defs.APIPathTrackCodec{defs.APIPathTrackCodecH264, defs.APIPathTrackCodecMPEG4Audio},
Tracks: []defs.APIPathTrackCodec{formatlabel.H264, formatlabel.MPEG4Audio},
InboundBytes: 17,
InboundFramesInError: 0,
BytesReceived: 17,
@@ -190,7 +191,7 @@ func TestAPIPathsList(t *testing.T) {
Type: "rtspsSession",
},
Ready: true,
Tracks: []defs.APIPathTrackCodec{defs.APIPathTrackCodecH264, defs.APIPathTrackCodecMPEG4Audio},
Tracks: []defs.APIPathTrackCodec{formatlabel.H264, formatlabel.MPEG4Audio},
}},
}, out)
})
@@ -338,7 +339,7 @@ func TestAPIPathsGet(t *testing.T) {
Type: "rtspSession",
},
Ready: true,
Tracks: []defs.APIPathTrackCodec{defs.APIPathTrackCodecH264},
Tracks: []defs.APIPathTrackCodec{formatlabel.H264},
}, out)
} else {
res, err := hc.Get("http://localhost:9997/v3/paths/get/" + pathName)
+2 -1
View File
@@ -14,6 +14,7 @@ import (
"github.com/bluenviron/mediamtx/internal/conf"
"github.com/bluenviron/mediamtx/internal/defs"
"github.com/bluenviron/mediamtx/internal/externalcmd"
"github.com/bluenviron/mediamtx/internal/formatlabel"
"github.com/bluenviron/mediamtx/internal/hooks"
"github.com/bluenviron/mediamtx/internal/logger"
"github.com/bluenviron/mediamtx/internal/recorder"
@@ -659,7 +660,7 @@ func (pa *path) doAPIPathsGet(req pathAPIPathsGetReq) {
if !pa.isAvailable() {
return []defs.APIPathTrackCodec{}
}
return defs.MediasToCodecs(pa.stream.Desc.Medias)
return formatlabel.MediasToLabels(pa.stream.Desc.Medias)
}(),
Tracks2: func() []defs.APIPathTrack {
if !pa.isAvailable() {
+49
View File
@@ -1,7 +1,56 @@
package defs
import (
"github.com/bluenviron/gortsplib/v5/pkg/description"
"github.com/bluenviron/gortsplib/v5/pkg/format"
"github.com/bluenviron/mediamtx/internal/formatlabel"
)
// APIPathTrack is a track.
type APIPathTrack struct {
Codec APIPathTrackCodec `json:"codec"`
CodecProps APIPathTrackCodecProps `json:"codecProps"`
}
func formatToTrack(forma format.Format) APIPathTrack {
return APIPathTrack{
Codec: formatlabel.FormatToLabel(forma),
CodecProps: formatToTrackCodecProps(forma),
}
}
func gatherFormats(medias []*description.Media) []format.Format {
n := 0
for _, media := range medias {
n += len(media.Formats)
}
if n == 0 {
return nil
}
formats := make([]format.Format, n)
n = 0
for _, media := range medias {
n += copy(formats[n:], media.Formats)
}
return formats
}
// FormatsToTracks returns tracks of given formats.
func FormatsToTracks(formats []format.Format) []APIPathTrack {
ret := make([]APIPathTrack, len(formats))
for i, forma := range formats {
ret[i] = formatToTrack(forma)
}
return ret
}
// MediasToTracks returns tracks of given medias.
func MediasToTracks(medias []*description.Media) []APIPathTrack {
return FormatsToTracks(gatherFormats(medias))
}
+3 -314
View File
@@ -1,317 +1,6 @@
package defs
import (
"fmt"
"strconv"
import "github.com/bluenviron/mediamtx/internal/formatlabel"
"github.com/bluenviron/gortsplib/v5/pkg/description"
"github.com/bluenviron/gortsplib/v5/pkg/format"
codecsh264 "github.com/bluenviron/mediacommon/v2/pkg/codecs/h264"
codecsh265 "github.com/bluenviron/mediacommon/v2/pkg/codecs/h265"
)
// APIPathTrackCodec is a path track codec.
type APIPathTrackCodec string
// path track codecs.
const (
// video
APIPathTrackCodecAV1 APIPathTrackCodec = "AV1"
APIPathTrackCodecVP9 APIPathTrackCodec = "VP9"
APIPathTrackCodecVP8 APIPathTrackCodec = "VP8"
APIPathTrackCodecH265 APIPathTrackCodec = "H265"
APIPathTrackCodecH264 APIPathTrackCodec = "H264"
APIPathTrackCodecMPEG4Video APIPathTrackCodec = "MPEG-4 Video"
APIPathTrackCodecMPEG1Video APIPathTrackCodec = "MPEG-1 Video"
APIPathTrackCodecMJPEG APIPathTrackCodec = "MJPEG"
// audio
APIPathTrackCodecOpus APIPathTrackCodec = "Opus"
APIPathTrackCodecVorbis APIPathTrackCodec = "Vorbis"
APIPathTrackCodecMPEG4Audio APIPathTrackCodec = "MPEG-4 Audio"
APIPathTrackCodecMPEG4AudioLATM APIPathTrackCodec = "MPEG-4 Audio LATM"
APIPathTrackCodecMPEG1Audio APIPathTrackCodec = "MPEG-1 Audio"
APIPathTrackCodecAC3 APIPathTrackCodec = "AC3"
APIPathTrackCodecSpeex APIPathTrackCodec = "Speex"
APIPathTrackCodecG726 APIPathTrackCodec = "G726"
APIPathTrackCodecG722 APIPathTrackCodec = "G722"
APIPathTrackCodecG711 APIPathTrackCodec = "G711"
APIPathTrackCodecLPCM APIPathTrackCodec = "LPCM"
// other
APIPathTrackCodecMPEGTS APIPathTrackCodec = "MPEG-TS"
APIPathTrackCodecKLV APIPathTrackCodec = "KLV"
APIPathTrackCodecGeneric APIPathTrackCodec = "Generic"
)
func formatToCodec(forma format.Format) APIPathTrackCodec {
switch forma.(type) {
// video
case *format.AV1:
return APIPathTrackCodecAV1
case *format.VP9:
return APIPathTrackCodecVP9
case *format.VP8:
return APIPathTrackCodecVP8
case *format.H265:
return APIPathTrackCodecH265
case *format.H264:
return APIPathTrackCodecH264
case *format.MPEG4Video:
return APIPathTrackCodecMPEG4Video
case *format.MPEG1Video:
return APIPathTrackCodecMPEG1Video
case *format.MJPEG:
return APIPathTrackCodecMJPEG
// audio
case *format.Opus:
return APIPathTrackCodecOpus
case *format.Vorbis:
return APIPathTrackCodecVorbis
case *format.MPEG4Audio:
return APIPathTrackCodecMPEG4Audio
case *format.MPEG4AudioLATM:
return APIPathTrackCodecMPEG4AudioLATM
case *format.MPEG1Audio:
return APIPathTrackCodecMPEG1Audio
case *format.AC3:
return APIPathTrackCodecAC3
case *format.Speex:
return APIPathTrackCodecSpeex
case *format.G726:
return APIPathTrackCodecG726
case *format.G722:
return APIPathTrackCodecG722
case *format.G711:
return APIPathTrackCodecG711
case *format.LPCM:
return APIPathTrackCodecLPCM
// other
case *format.MPEGTS:
return APIPathTrackCodecMPEGTS
case *format.KLV:
return APIPathTrackCodecKLV
default:
return APIPathTrackCodecGeneric
}
}
func h264ProfileString(profileIdc uint8) string {
switch profileIdc {
case 66:
return "Baseline"
case 77:
return "Main"
case 88:
return "Extended"
case 100:
return "High"
case 110:
return "High 10"
case 122:
return "High 4:2:2"
case 244:
return "High 4:4:4 Predictive"
default:
return strconv.Itoa(int(profileIdc))
}
}
func h264LevelString(levelIdc uint8) string {
major := levelIdc / 10
minor := levelIdc % 10
if minor == 0 {
return fmt.Sprintf("%d", major)
}
return fmt.Sprintf("%d.%d", major, minor)
}
func h265ProfileString(profileIdc uint8) string {
switch profileIdc {
case 1:
return "Main"
case 2:
return "Main 10"
case 3:
return "Main Still Picture"
case 4:
return "Range Extensions"
case 5:
return "High Throughput"
case 6:
return "Multiview Main"
case 7:
return "Scalable Main"
case 8:
return "3D Main"
case 9:
return "Screen Content Coding Extensions"
case 10:
return "Scalable Range Extensions"
default:
return strconv.Itoa(int(profileIdc))
}
}
func h265LevelString(levelIdc uint8) string {
// H.265 level_idc = 30 * level, so 3.0 = 90, 4.0 = 120, 4.1 = 123
level := float64(levelIdc) / 30.0
// Check if it's a clean level like 3.0, 4.0
if levelIdc%30 == 0 {
return fmt.Sprintf("%.0f", level)
}
return fmt.Sprintf("%.1f", level)
}
// FormatsToCodecs returns codecs of given formats.
func FormatsToCodecs(formats []format.Format) []APIPathTrackCodec {
ret := make([]APIPathTrackCodec, len(formats))
for i, forma := range formats {
ret[i] = formatToCodec(forma)
}
return ret
}
func formatToTrack(forma format.Format) APIPathTrack {
return APIPathTrack{
Codec: formatToCodec(forma),
CodecProps: formatToTrackCodecProps(forma),
}
}
func formatToTrackCodecProps(forma format.Format) APIPathTrackCodecProps {
switch forma := forma.(type) {
case *format.AV1:
props := &APIPathTrackCodecPropsAV1{}
if forma.Profile != nil {
props.Profile = *forma.Profile
}
if forma.LevelIdx != nil {
props.Level = *forma.LevelIdx
}
if forma.Tier != nil {
props.Tier = *forma.Tier
}
return props
case *format.VP9:
props := &APIPathTrackCodecPropsVP9{}
if forma.ProfileID != nil {
props.Profile = *forma.ProfileID
}
return props
case *format.H265:
props := &APIPathTrackCodecPropsH265{}
_, sps, _ := forma.SafeParams()
if sps != nil {
var s codecsh265.SPS
if err := s.Unmarshal(sps); err == nil {
props.Width = s.Width()
props.Height = s.Height()
props.Profile = h265ProfileString(s.ProfileTierLevel.GeneralProfileIdc)
props.Level = h265LevelString(s.ProfileTierLevel.GeneralLevelIdc)
}
}
return props
case *format.H264:
props := &APIPathTrackCodecPropsH264{}
sps, _ := forma.SafeParams()
if sps != nil {
var s codecsh264.SPS
if err := s.Unmarshal(sps); err == nil {
props.Width = s.Width()
props.Height = s.Height()
props.Profile = h264ProfileString(s.ProfileIdc)
props.Level = h264LevelString(s.LevelIdc)
}
}
return props
case *format.Opus:
return &APIPathTrackCodecPropsOpus{
ChannelCount: forma.ChannelCount,
}
case *format.MPEG4Audio:
props := &APIPathTrackCodecPropsMPEG4Audio{}
if forma.Config != nil {
props.SampleRate = forma.Config.SampleRate
props.ChannelCount = int(forma.Config.ChannelConfig)
}
return props
case *format.AC3:
return &APIPathTrackCodecPropsAC3{
SampleRate: forma.SampleRate,
ChannelCount: forma.ChannelCount,
}
case *format.G711:
return &APIPathTrackCodecPropsG711{
MULaw: forma.MULaw,
SampleRate: forma.SampleRate,
ChannelCount: forma.ChannelCount,
}
case *format.LPCM:
return &APIPathTrackCodecPropsLPCM{
BitDepth: forma.BitDepth,
SampleRate: forma.SampleRate,
ChannelCount: forma.ChannelCount,
}
default:
return nil
}
}
func gatherFormats(medias []*description.Media) []format.Format {
n := 0
for _, media := range medias {
n += len(media.Formats)
}
if n == 0 {
return nil
}
formats := make([]format.Format, n)
n = 0
for _, media := range medias {
n += copy(formats[n:], media.Formats)
}
return formats
}
// MediasToCodecs returns codecs of given medias.
func MediasToCodecs(medias []*description.Media) []APIPathTrackCodec {
return FormatsToCodecs(gatherFormats(medias))
}
// FormatsToTracks returns tracks of given formats.
func FormatsToTracks(formats []format.Format) []APIPathTrack {
ret := make([]APIPathTrack, len(formats))
for i, forma := range formats {
ret[i] = formatToTrack(forma)
}
return ret
}
// MediasToTracks returns tracks of given medias.
func MediasToTracks(medias []*description.Media) []APIPathTrack {
return FormatsToTracks(gatherFormats(medias))
}
// APIPathTrackCodec is a track codec.
type APIPathTrackCodec = formatlabel.Label
+174
View File
@@ -1,5 +1,14 @@
package defs
import (
"fmt"
"strconv"
"github.com/bluenviron/gortsplib/v5/pkg/format"
codecsh264 "github.com/bluenviron/mediacommon/v2/pkg/codecs/h264"
codecsh265 "github.com/bluenviron/mediacommon/v2/pkg/codecs/h265"
)
// APIPathTrackCodecProps contains codec-specific properties.
type APIPathTrackCodecProps interface {
apiPathTrackCodecProps()
@@ -83,3 +92,168 @@ type APIPathTrackCodecPropsLPCM struct {
}
func (*APIPathTrackCodecPropsLPCM) apiPathTrackCodecProps() {}
func h264ProfileString(profileIdc uint8) string {
switch profileIdc {
case 66:
return "Baseline"
case 77:
return "Main"
case 88:
return "Extended"
case 100:
return "High"
case 110:
return "High 10"
case 122:
return "High 4:2:2"
case 244:
return "High 4:4:4 Predictive"
default:
return strconv.Itoa(int(profileIdc))
}
}
func h264LevelString(levelIdc uint8) string {
major := levelIdc / 10
minor := levelIdc % 10
if minor == 0 {
return fmt.Sprintf("%d", major)
}
return fmt.Sprintf("%d.%d", major, minor)
}
func h265ProfileString(profileIdc uint8) string {
switch profileIdc {
case 1:
return "Main"
case 2:
return "Main 10"
case 3:
return "Main Still Picture"
case 4:
return "Range Extensions"
case 5:
return "High Throughput"
case 6:
return "Multiview Main"
case 7:
return "Scalable Main"
case 8:
return "3D Main"
case 9:
return "Screen Content Coding Extensions"
case 10:
return "Scalable Range Extensions"
default:
return strconv.Itoa(int(profileIdc))
}
}
func h265LevelString(levelIdc uint8) string {
// H.265 level_idc = 30 * level, so 3.0 = 90, 4.0 = 120, 4.1 = 123
level := float64(levelIdc) / 30.0
// Check if it's a clean level like 3.0, 4.0
if levelIdc%30 == 0 {
return fmt.Sprintf("%.0f", level)
}
return fmt.Sprintf("%.1f", level)
}
func formatToTrackCodecProps(forma format.Format) APIPathTrackCodecProps {
switch forma := forma.(type) {
case *format.AV1:
props := &APIPathTrackCodecPropsAV1{}
if forma.Profile != nil {
props.Profile = *forma.Profile
}
if forma.LevelIdx != nil {
props.Level = *forma.LevelIdx
}
if forma.Tier != nil {
props.Tier = *forma.Tier
}
return props
case *format.VP9:
props := &APIPathTrackCodecPropsVP9{}
if forma.ProfileID != nil {
props.Profile = *forma.ProfileID
}
return props
case *format.H265:
props := &APIPathTrackCodecPropsH265{}
_, sps, _ := forma.SafeParams()
if sps != nil {
var s codecsh265.SPS
if err := s.Unmarshal(sps); err == nil {
props.Width = s.Width()
props.Height = s.Height()
props.Profile = h265ProfileString(s.ProfileTierLevel.GeneralProfileIdc)
props.Level = h265LevelString(s.ProfileTierLevel.GeneralLevelIdc)
}
}
return props
case *format.H264:
props := &APIPathTrackCodecPropsH264{}
sps, _ := forma.SafeParams()
if sps != nil {
var s codecsh264.SPS
if err := s.Unmarshal(sps); err == nil {
props.Width = s.Width()
props.Height = s.Height()
props.Profile = h264ProfileString(s.ProfileIdc)
props.Level = h264LevelString(s.LevelIdc)
}
}
return props
case *format.Opus:
return &APIPathTrackCodecPropsOpus{
ChannelCount: forma.ChannelCount,
}
case *format.MPEG4Audio:
props := &APIPathTrackCodecPropsMPEG4Audio{}
if forma.Config != nil {
props.SampleRate = forma.Config.SampleRate
props.ChannelCount = int(forma.Config.ChannelConfig)
}
return props
case *format.AC3:
return &APIPathTrackCodecPropsAC3{
SampleRate: forma.SampleRate,
ChannelCount: forma.ChannelCount,
}
case *format.G711:
return &APIPathTrackCodecPropsG711{
MULaw: forma.MULaw,
SampleRate: forma.SampleRate,
ChannelCount: forma.ChannelCount,
}
case *format.LPCM:
return &APIPathTrackCodecPropsLPCM{
BitDepth: forma.BitDepth,
SampleRate: forma.SampleRate,
ChannelCount: forma.ChannelCount,
}
default:
return nil
}
}
-206
View File
@@ -1,206 +0,0 @@
package defs
import (
"testing"
"github.com/bluenviron/gortsplib/v5/pkg/description"
"github.com/bluenviron/gortsplib/v5/pkg/format"
"github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4audio"
"github.com/stretchr/testify/require"
)
func ptr[T any](v T) *T {
return &v
}
func testFormatH264() *format.H264 {
return &format.H264{
PayloadTyp: 96,
SPS: []byte{
0x67, 0x42, 0xc0, 0x28, 0xd9, 0x00, 0x78, 0x02,
0x27, 0xe5, 0x84, 0x00, 0x00, 0x03, 0x00, 0x04,
0x00, 0x00, 0x03, 0x00, 0xf0, 0x3c, 0x60, 0xc9, 0x20,
},
PPS: []byte{0x08, 0x06, 0x07, 0x08},
PacketizationMode: 1,
}
}
func testFormatMPEG4Audio() *format.MPEG4Audio {
return &format.MPEG4Audio{
PayloadTyp: 96,
Config: &mpeg4audio.AudioSpecificConfig{
Type: 2,
SampleRate: 44100,
ChannelCount: 2,
ChannelConfig: 2,
},
SizeLength: 13,
IndexLength: 3,
IndexDeltaLength: 3,
}
}
func testFormatH265() *format.H265 {
return &format.H265{
PayloadTyp: 96,
VPS: []byte{
0x40, 0x01, 0x0c, 0x01, 0xff, 0xff, 0x02, 0x20,
0x00, 0x00, 0x03, 0x00, 0xb0, 0x00, 0x00, 0x03,
0x00, 0x00, 0x03, 0x00, 0x7b, 0x18, 0xb0, 0x24,
},
SPS: []byte{
0x42, 0x01, 0x01, 0x02, 0x20, 0x00, 0x00, 0x03,
0x00, 0xb0, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03,
0x00, 0x7b, 0xa0, 0x07, 0x82, 0x00, 0x88, 0x7d,
0xb6, 0x71, 0x8b, 0x92, 0x44, 0x80, 0x53, 0x88,
0x88, 0x92, 0xcf, 0x24, 0xa6, 0x92, 0x72, 0xc9,
0x12, 0x49, 0x22, 0xdc, 0x91, 0xaa, 0x48, 0xfc,
0xa2, 0x23, 0xff, 0x00, 0x01, 0x00, 0x01, 0x6a,
0x02, 0x02, 0x02, 0x01,
},
PPS: []byte{
0x44, 0x01, 0xc0, 0x25, 0x2f, 0x05, 0x32, 0x40,
},
}
}
func TestFormatsToCodecs(t *testing.T) {
codecs := FormatsToCodecs([]format.Format{
&format.AV1{},
&format.VP9{},
&format.VP8{},
&format.H265{},
&format.H264{},
&format.MPEG4Video{},
&format.MPEG1Video{},
&format.MJPEG{},
&format.Opus{},
&format.Vorbis{},
&format.MPEG4Audio{},
&format.MPEG4AudioLATM{},
&format.MPEG1Audio{},
&format.AC3{},
&format.Speex{},
&format.G726{},
&format.G722{},
&format.G711{},
&format.LPCM{},
&format.MPEGTS{},
&format.KLV{},
&format.Generic{},
})
require.Equal(t, []APIPathTrackCodec{
APIPathTrackCodecAV1,
APIPathTrackCodecVP9,
APIPathTrackCodecVP8,
APIPathTrackCodecH265,
APIPathTrackCodecH264,
APIPathTrackCodecMPEG4Video,
APIPathTrackCodecMPEG1Video,
APIPathTrackCodecMJPEG,
APIPathTrackCodecOpus,
APIPathTrackCodecVorbis,
APIPathTrackCodecMPEG4Audio,
APIPathTrackCodecMPEG4AudioLATM,
APIPathTrackCodecMPEG1Audio,
APIPathTrackCodecAC3,
APIPathTrackCodecSpeex,
APIPathTrackCodecG726,
APIPathTrackCodecG722,
APIPathTrackCodecG711,
APIPathTrackCodecLPCM,
APIPathTrackCodecMPEGTS,
APIPathTrackCodecKLV,
APIPathTrackCodecGeneric,
}, codecs)
}
func TestMediasToTracks(t *testing.T) {
tracks := MediasToTracks([]*description.Media{
{Formats: []format.Format{testFormatH264()}},
{Formats: []format.Format{testFormatH265()}},
{
Formats: []format.Format{
&format.AV1{Profile: ptr(1), LevelIdx: ptr(9), Tier: ptr(0)},
&format.VP9{ProfileID: ptr(2)},
&format.MJPEG{},
&format.Opus{ChannelCount: 2},
&format.G711{MULaw: true, SampleRate: 8000, ChannelCount: 1},
&format.LPCM{BitDepth: 16, SampleRate: 48000, ChannelCount: 2},
},
},
{
Formats: []format.Format{testFormatMPEG4Audio()},
},
})
require.Equal(t, []APIPathTrack{
{
Codec: APIPathTrackCodecH264,
CodecProps: &APIPathTrackCodecPropsH264{
Width: 1920,
Height: 1080,
Profile: "Baseline",
Level: "4",
},
},
{
Codec: APIPathTrackCodecH265,
CodecProps: &APIPathTrackCodecPropsH265{
Width: 960,
Height: 540,
Profile: "Main 10",
Level: "4.1",
},
},
{
Codec: APIPathTrackCodecAV1,
CodecProps: &APIPathTrackCodecPropsAV1{
Profile: 1,
Level: 9,
Tier: 0,
},
},
{
Codec: APIPathTrackCodecVP9,
CodecProps: &APIPathTrackCodecPropsVP9{
Profile: 2,
},
},
{
Codec: APIPathTrackCodecMJPEG,
CodecProps: nil,
},
{
Codec: APIPathTrackCodecOpus,
CodecProps: &APIPathTrackCodecPropsOpus{
ChannelCount: 2,
},
},
{
Codec: APIPathTrackCodecG711,
CodecProps: &APIPathTrackCodecPropsG711{
MULaw: true,
SampleRate: 8000,
ChannelCount: 1,
},
},
{
Codec: APIPathTrackCodecLPCM,
CodecProps: &APIPathTrackCodecPropsLPCM{
BitDepth: 16,
SampleRate: 48000,
ChannelCount: 2,
},
},
{
Codec: APIPathTrackCodecMPEG4Audio,
CodecProps: &APIPathTrackCodecPropsMPEG4Audio{
SampleRate: 44100,
ChannelCount: 2,
},
},
}, tracks)
}
+5 -4
View File
@@ -7,6 +7,7 @@ import (
"github.com/bluenviron/gortsplib/v5/pkg/description"
"github.com/bluenviron/gortsplib/v5/pkg/format"
"github.com/bluenviron/mediamtx/internal/formatlabel"
"github.com/bluenviron/mediamtx/internal/logger"
)
@@ -22,10 +23,10 @@ type Source interface {
// FormatsInfo returns a description of formats.
func FormatsInfo(formats []format.Format) string {
codecs := FormatsToCodecs(formats)
codecNames := make([]string, len(codecs))
for i, codec := range codecs {
codecNames[i] = string(codec)
labels := formatlabel.FormatsToLabels(formats)
codecNames := make([]string, len(labels))
for i, label := range labels {
codecNames[i] = string(label)
}
return fmt.Sprintf("%d %s (%s)",
+126
View File
@@ -0,0 +1,126 @@
// Package formatlabel contains format label definitions.
package formatlabel
import (
"github.com/bluenviron/gortsplib/v5/pkg/description"
"github.com/bluenviron/gortsplib/v5/pkg/format"
)
// Label is a format label.
type Label string
// labels.
const (
// video
AV1 Label = "AV1"
VP9 Label = "VP9"
VP8 Label = "VP8"
H265 Label = "H265"
H264 Label = "H264"
MPEG4Video Label = "MPEG-4 Video"
MPEG1Video Label = "MPEG-1/2 Video"
MJPEG Label = "M-JPEG"
// audio
Opus Label = "Opus"
Vorbis Label = "Vorbis"
MPEG4Audio Label = "MPEG-4 Audio"
MPEG4AudioLATM Label = "MPEG-4 Audio LATM"
MPEG1Audio Label = "MPEG-1/2 Audio"
AC3 Label = "AC3"
Speex Label = "Speex"
G726 Label = "G726"
G722 Label = "G722"
G711 Label = "G711"
LPCM Label = "LPCM"
// other
MPEGTS Label = "MPEG-TS"
KLV Label = "KLV"
Generic Label = "Generic"
)
// FormatToLabel returns the label associated with a format.
func FormatToLabel(forma format.Format) Label {
switch forma.(type) {
// video
case *format.AV1:
return AV1
case *format.VP9:
return VP9
case *format.VP8:
return VP8
case *format.H265:
return H265
case *format.H264:
return H264
case *format.MPEG4Video:
return MPEG4Video
case *format.MPEG1Video:
return MPEG1Video
case *format.MJPEG:
return MJPEG
// audio
case *format.Opus:
return Opus
case *format.Vorbis:
return Vorbis
case *format.MPEG4Audio:
return MPEG4Audio
case *format.MPEG4AudioLATM:
return MPEG4AudioLATM
case *format.MPEG1Audio:
return MPEG1Audio
case *format.AC3:
return AC3
case *format.Speex:
return Speex
case *format.G726:
return G726
case *format.G722:
return G722
case *format.G711:
return G711
case *format.LPCM:
return LPCM
// other
case *format.MPEGTS:
return MPEGTS
case *format.KLV:
return KLV
default:
return Generic
}
}
func gatherFormats(medias []*description.Media) []format.Format {
n := 0
for _, media := range medias {
n += len(media.Formats)
}
if n == 0 {
return nil
}
formats := make([]format.Format, n)
n = 0
for _, media := range medias {
n += copy(formats[n:], media.Formats)
}
return formats
}
// FormatsToLabels returns labels of given formats.
func FormatsToLabels(formats []format.Format) []Label {
ret := make([]Label, len(formats))
for i, forma := range formats {
ret[i] = FormatToLabel(forma)
}
return ret
}
// MediasToLabels returns labels of given medias.
func MediasToLabels(medias []*description.Media) []Label {
return FormatsToLabels(gatherFormats(medias))
}
+88
View File
@@ -0,0 +1,88 @@
package formatlabel
import (
"testing"
"github.com/bluenviron/gortsplib/v5/pkg/description"
"github.com/bluenviron/gortsplib/v5/pkg/format"
"github.com/stretchr/testify/require"
)
func TestFormatToLabel(t *testing.T) {
codecs := []Label{
FormatToLabel(&format.AV1{}),
FormatToLabel(&format.VP9{}),
FormatToLabel(&format.VP8{}),
FormatToLabel(&format.H265{}),
FormatToLabel(&format.H264{}),
FormatToLabel(&format.MPEG4Video{}),
FormatToLabel(&format.MPEG1Video{}),
FormatToLabel(&format.MJPEG{}),
FormatToLabel(&format.Opus{}),
FormatToLabel(&format.Vorbis{}),
FormatToLabel(&format.MPEG4Audio{}),
FormatToLabel(&format.MPEG4AudioLATM{}),
FormatToLabel(&format.MPEG1Audio{}),
FormatToLabel(&format.AC3{}),
FormatToLabel(&format.Speex{}),
FormatToLabel(&format.G726{}),
FormatToLabel(&format.G722{}),
FormatToLabel(&format.G711{}),
FormatToLabel(&format.LPCM{}),
FormatToLabel(&format.MPEGTS{}),
FormatToLabel(&format.KLV{}),
FormatToLabel(&format.Generic{}),
}
require.Equal(t, []Label{
AV1,
VP9,
VP8,
H265,
H264,
MPEG4Video,
MPEG1Video,
MJPEG,
Opus,
Vorbis,
MPEG4Audio,
MPEG4AudioLATM,
MPEG1Audio,
AC3,
Speex,
G726,
G722,
G711,
LPCM,
MPEGTS,
KLV,
Generic,
}, codecs)
}
func TestFormatsToLabels(t *testing.T) {
require.Equal(t, []Label{
H264,
Opus,
KLV,
Generic,
}, FormatsToLabels([]format.Format{
&format.H264{},
&format.Opus{},
&format.KLV{},
&format.Generic{},
}))
}
func TestMediasToLabels(t *testing.T) {
require.Equal(t, []Label{
H264,
Opus,
G711,
KLV,
}, MediasToLabels([]*description.Media{
{Formats: []format.Format{&format.H264{}}},
{Formats: []format.Format{&format.Opus{}, &format.G711{}}},
{Formats: []format.Format{&format.KLV{}}},
}))
}
+58 -3
View File
@@ -46,6 +46,7 @@ func wrapRef(rt reflect.Type, p openAPIProperty) openAPIProperty {
type openAPISchema struct {
Type string `yaml:"type"`
Enum []string `yaml:"enum"`
OneOf []openAPIProperty `yaml:"oneOf"`
Properties map[string]openAPIProperty `yaml:"properties"`
}
@@ -62,6 +63,10 @@ func schemaName(rt reflect.Type) string {
return "PathConf"
}
if rt == reflect.TypeOf(defs.APIPathTrackCodec("")) {
return "PathTrackCodec"
}
return name
}
@@ -190,13 +195,13 @@ func goEnumToApi(rt reflect.Type) (openAPISchema, bool) {
"H265",
"H264",
"MPEG-4 Video",
"MPEG-1 Video",
"MJPEG",
"MPEG-1/2 Video",
"M-JPEG",
"Opus",
"Vorbis",
"MPEG-4 Audio",
"MPEG-4 Audio LATM",
"MPEG-1 Audio",
"MPEG-1/2 Audio",
"AC3",
"Speex",
"G726",
@@ -379,6 +384,42 @@ func TestGo2API(t *testing.T) {
"PathTrack",
defs.APIPathTrack{},
},
{
"PathTrackCodecPropsAV1",
defs.APIPathTrackCodecPropsAV1{},
},
{
"PathTrackCodecPropsVP9",
defs.APIPathTrackCodecPropsVP9{},
},
{
"PathTrackCodecPropsH265",
defs.APIPathTrackCodecPropsH265{},
},
{
"PathTrackCodecPropsH264",
defs.APIPathTrackCodecPropsH264{},
},
{
"PathTrackCodecPropsOpus",
defs.APIPathTrackCodecPropsOpus{},
},
{
"PathTrackCodecPropsMPEG4Audio",
defs.APIPathTrackCodecPropsMPEG4Audio{},
},
{
"PathTrackCodecPropsAC3",
defs.APIPathTrackCodecPropsAC3{},
},
{
"PathTrackCodecPropsG711",
defs.APIPathTrackCodecPropsG711{},
},
{
"PathTrackCodecPropsLPCM",
defs.APIPathTrackCodecPropsLPCM{},
},
{
"Recording",
defs.APIRecording{},
@@ -464,6 +505,20 @@ func TestGo2API(t *testing.T) {
}
})
t.Run("oneOfs", func(t *testing.T) {
require.Equal(t, openAPISchema{OneOf: []openAPIProperty{
{Ref: "#/components/schemas/PathTrackCodecPropsAV1"},
{Ref: "#/components/schemas/PathTrackCodecPropsVP9"},
{Ref: "#/components/schemas/PathTrackCodecPropsH265"},
{Ref: "#/components/schemas/PathTrackCodecPropsH264"},
{Ref: "#/components/schemas/PathTrackCodecPropsOpus"},
{Ref: "#/components/schemas/PathTrackCodecPropsMPEG4Audio"},
{Ref: "#/components/schemas/PathTrackCodecPropsAC3"},
{Ref: "#/components/schemas/PathTrackCodecPropsG711"},
{Ref: "#/components/schemas/PathTrackCodecPropsLPCM"},
}}, doc.Components.Schemas["PathTrackCodecProps"])
})
t.Run("enums", func(t *testing.T) {
for _, rt := range []reflect.Type{
reflect.TypeOf(defs.APIOKStatus("")),
+2 -1
View File
@@ -11,6 +11,7 @@ import (
"github.com/bluenviron/mediamtx/internal/auth"
"github.com/bluenviron/mediamtx/internal/conf"
"github.com/bluenviron/mediamtx/internal/defs"
"github.com/bluenviron/mediamtx/internal/formatlabel"
"github.com/bluenviron/mediamtx/internal/logger"
"github.com/bluenviron/mediamtx/internal/test"
"github.com/google/uuid"
@@ -50,7 +51,7 @@ func (dummyPathManager) APIPathsList() (*defs.APIPathList, error) {
},
Ready: true,
ReadyTime: ptrOf(time.Date(2003, 11, 4, 23, 15, 7, 0, time.UTC)),
Tracks: []defs.APIPathTrackCodec{defs.APIPathTrackCodecH264, defs.APIPathTrackCodecH265},
Tracks: []defs.APIPathTrackCodec{formatlabel.H264, formatlabel.H265},
InboundBytes: 123,
OutboundBytes: 456,
InboundFramesInError: 7,
+2 -1
View File
@@ -11,6 +11,7 @@ import (
"github.com/bluenviron/gortsplib/v5/pkg/description"
"github.com/bluenviron/gortsplib/v5/pkg/format"
"github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4audio"
"github.com/bluenviron/mediamtx/internal/formatlabel"
"github.com/bluenviron/mediamtx/internal/logger"
"github.com/bluenviron/mediamtx/internal/stream"
"github.com/bluenviron/mediamtx/internal/unit"
@@ -314,7 +315,7 @@ func FromStream(
for _, media := range desc.Medias {
for _, forma := range media.Formats {
if !slices.Contains(setuppedFormats, forma) {
r.Parent.Log(logger.Warn, "skipping track %d (%s)", n, forma.Codec())
r.Parent.Log(logger.Warn, "skipping track %d (%s)", n, formatlabel.FormatToLabel(forma))
}
n++
}
+3 -2
View File
@@ -17,6 +17,7 @@ import (
"github.com/bluenviron/mediacommon/v2/pkg/formats/mpegts/substructs"
srt "github.com/datarhei/gosrt"
"github.com/bluenviron/mediamtx/internal/formatlabel"
"github.com/bluenviron/mediamtx/internal/logger"
"github.com/bluenviron/mediamtx/internal/stream"
"github.com/bluenviron/mediamtx/internal/unit"
@@ -185,7 +186,7 @@ func FromStream(
if !firstReceived {
firstReceived = true
} else if u.PTS < lastPTS {
return fmt.Errorf("MPEG-1 Video streams with B-frames are not supported (yet)")
return fmt.Errorf("MPEG-1/2 Video streams with B-frames are not supported (yet)")
}
lastPTS = u.PTS
@@ -402,7 +403,7 @@ func FromStream(
for _, medi := range desc.Medias {
for _, forma := range medi.Formats {
if !slices.Contains(setuppedFormats, forma) {
r.Parent.Log(logger.Warn, "skipping track %d (%s)", n, forma.Codec())
r.Parent.Log(logger.Warn, "skipping track %d (%s)", n, formatlabel.FormatToLabel(forma))
}
n++
}
+1 -1
View File
@@ -19,7 +19,7 @@ import (
var errNoSupportedCodecs = errors.New(
"the stream doesn't contain any supported codec, which are currently " +
"H265, H264, MPEG-4 Video, MPEG-1/2 Video, Opus, MPEG-4 Audio, MPEG-1 Audio, AC-3")
"H265, H264, MPEG-4 Video, MPEG-1/2 Video, Opus, MPEG-4 Audio, MPEG-1/2 Audio, AC-3")
// ToStream maps a MPEG-TS stream to a MediaMTX stream.
func ToStream(
+2 -1
View File
@@ -18,6 +18,7 @@ import (
"github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg1audio"
"github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4audio"
"github.com/bluenviron/mediacommon/v2/pkg/codecs/opus"
"github.com/bluenviron/mediamtx/internal/formatlabel"
"github.com/bluenviron/mediamtx/internal/logger"
"github.com/bluenviron/mediamtx/internal/stream"
"github.com/bluenviron/mediamtx/internal/unit"
@@ -475,7 +476,7 @@ func FromStream(
for _, media := range desc.Medias {
for _, forma := range media.Formats {
if !slices.Contains(setuppedFormats, forma) {
r.Parent.Log(logger.Warn, "skipping track %d (%s)", n, forma.Codec())
r.Parent.Log(logger.Warn, "skipping track %d (%s)", n, formatlabel.FormatToLabel(forma))
}
n++
}
+2 -1
View File
@@ -17,6 +17,7 @@ import (
"github.com/bluenviron/gortsplib/v5/pkg/format/rtpvp9"
"github.com/bluenviron/mediacommon/v2/pkg/codecs/g711"
"github.com/bluenviron/mediacommon/v2/pkg/codecs/opus"
"github.com/bluenviron/mediamtx/internal/formatlabel"
"github.com/bluenviron/mediamtx/internal/logger"
"github.com/bluenviron/mediamtx/internal/stream"
"github.com/bluenviron/mediamtx/internal/unit"
@@ -711,7 +712,7 @@ func FromStream(
for _, media := range desc.Medias {
for _, forma := range media.Formats {
if !slices.Contains(setuppedFormats, forma) {
r.Parent.Log(logger.Warn, "skipping track %d (%s)", n, forma.Codec())
r.Parent.Log(logger.Warn, "skipping track %d (%s)", n, formatlabel.FormatToLabel(forma))
}
n++
}
+3 -2
View File
@@ -22,6 +22,7 @@ import (
mcodecs "github.com/bluenviron/mediacommon/v2/pkg/formats/mp4/codecs"
"github.com/bluenviron/mediamtx/internal/defs"
"github.com/bluenviron/mediamtx/internal/formatlabel"
"github.com/bluenviron/mediamtx/internal/logger"
"github.com/bluenviron/mediamtx/internal/unit"
)
@@ -576,7 +577,7 @@ func (f *formatFMP4) initialize() bool {
}
firstReceived = true
} else if u.PTS < lastPTS {
return fmt.Errorf("MPEG-1 Video streams with B-frames are not supported (yet)")
return fmt.Errorf("MPEG-1/2 Video streams with B-frames are not supported (yet)")
}
lastPTS = u.PTS
@@ -921,7 +922,7 @@ func (f *formatFMP4) initialize() bool {
for _, medi := range f.ri.stream.Desc.Medias {
for _, forma := range medi.Formats {
if !slices.Contains(setuppedFormats, forma) {
f.ri.Log(logger.Warn, "skipping track %d (%s)", n, forma.Codec())
f.ri.Log(logger.Warn, "skipping track %d (%s)", n, formatlabel.FormatToLabel(forma))
}
n++
}
+3 -2
View File
@@ -18,6 +18,7 @@ import (
tscodecs "github.com/bluenviron/mediacommon/v2/pkg/formats/mpegts/codecs"
"github.com/bluenviron/mediamtx/internal/defs"
"github.com/bluenviron/mediamtx/internal/formatlabel"
"github.com/bluenviron/mediamtx/internal/logger"
"github.com/bluenviron/mediamtx/internal/unit"
)
@@ -221,7 +222,7 @@ func (f *formatMPEGTS) initialize() bool {
if !firstReceived {
firstReceived = true
} else if u.PTS < lastPTS {
return fmt.Errorf("MPEG-1 Video streams with B-frames are not supported (yet)")
return fmt.Errorf("MPEG-1/2 Video streams with B-frames are not supported (yet)")
}
lastPTS = u.PTS
@@ -424,7 +425,7 @@ func (f *formatMPEGTS) initialize() bool {
for _, medi := range f.ri.stream.Desc.Medias {
for _, forma := range medi.Formats {
if !slices.Contains(setuppedFormats, forma) {
f.ri.Log(logger.Warn, "skipping track %d (%s)", n, forma.Codec())
f.ri.Log(logger.Warn, "skipping track %d (%s)", n, formatlabel.FormatToLabel(forma))
}
n++
}
+4 -36
View File
@@ -7,43 +7,11 @@ import (
"github.com/bluenviron/gortsplib/v5/pkg/description"
"github.com/bluenviron/gortsplib/v5/pkg/format"
"github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4audio"
"github.com/bluenviron/mediamtx/internal/formatlabel"
"github.com/bluenviron/mediamtx/internal/logger"
"github.com/bluenviron/mediamtx/internal/unit"
)
// FormatsToCodecs returns the name of codecs of given formats.
func FormatsToCodecs(formats []format.Format) []string {
ret := make([]string, len(formats))
for i, forma := range formats {
ret[i] = forma.Codec()
}
return ret
}
func gatherFormats(medias []*description.Media) []format.Format {
n := 0
for _, media := range medias {
n += len(media.Formats)
}
if n == 0 {
return nil
}
formats := make([]format.Format, n)
n = 0
for _, media := range medias {
n += copy(formats[n:], media.Formats)
}
return formats
}
func mediasToCodecs(medias []*description.Media) []string {
return FormatsToCodecs(gatherFormats(medias))
}
func formatMPEG4AudioConfig(asc *mpeg4audio.AudioSpecificConfig) string {
return fmt.Sprintf("type=%d, sampleRate=%d, channelCount=%d",
asc.Type, asc.SampleRate, asc.ChannelConfig)
@@ -62,19 +30,19 @@ func formatLPCMConfig(f *format.LPCM) string {
func mediasAreCompatible(medias1 []*description.Media, medias2 []*description.Media) error {
if len(medias1) != len(medias2) {
return fmt.Errorf("wants to publish %v, but stream expects %v",
mediasToCodecs(medias2), mediasToCodecs(medias1))
formatlabel.MediasToLabels(medias2), formatlabel.MediasToLabels(medias1))
}
for i := range medias1 {
if len(medias1[i].Formats) != len(medias2[i].Formats) {
return fmt.Errorf("wants to publish %v, but stream expects %v",
mediasToCodecs(medias2), mediasToCodecs(medias1))
formatlabel.MediasToLabels(medias2), formatlabel.MediasToLabels(medias1))
}
for j := range medias1[i].Formats {
if reflect.TypeOf(medias1[i].Formats[j]) != reflect.TypeOf(medias2[i].Formats[j]) {
return fmt.Errorf("wants to publish %v, but stream expects %v",
mediasToCodecs(medias2), mediasToCodecs(medias1))
formatlabel.MediasToLabels(medias2), formatlabel.MediasToLabels(medias1))
}
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
package unit
// PayloadMPEG1Audio is the payload of a MPEG-1 Audio track.
// PayloadMPEG1Audio is the payload of a MPEG-1/2 Audio track.
type PayloadMPEG1Audio [][]byte
func (PayloadMPEG1Audio) isPayload() {}
+1 -1
View File
@@ -1,6 +1,6 @@
package unit
// PayloadMPEG1Video is the payload of a MPEG-1 Video track.
// PayloadMPEG1Video is the payload of a MPEG-1/2 Video track.
type PayloadMPEG1Video []byte
func (PayloadMPEG1Video) isPayload() {}