diff --git a/api/openapi.yaml b/api/openapi.yaml index b00daafa..5342f763 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -973,8 +973,13 @@ components: nullable: true tracks: type: array + deprecated: true items: $ref: '#/components/schemas/PathTrackCodec' + tracks2: + type: array + items: + $ref: '#/components/schemas/PathTrack' inboundBytes: type: integer format: uint64 @@ -1027,6 +1032,134 @@ components: id: type: string + PathTrack: + type: object + properties: + codec: + $ref: '#/components/schemas/PathTrackCodec' + codecProps: + type: object + nullable: true + allOf: + - $ref: '#/components/schemas/PathTrackCodecProps' + + PathTrackCodecProps: + oneOf: + - $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' + + PathTrackCodecPropsAV1: + type: object + properties: + width: + type: integer + format: int64 + height: + type: integer + format: int64 + profile: + type: integer + format: int64 + level: + type: integer + format: int64 + tier: + type: integer + format: int64 + PathTrackCodecPropsVP9: + type: object + properties: + profile: + type: integer + format: int64 + + PathTrackCodecPropsH265: + type: object + properties: + width: + type: integer + format: int64 + height: + type: integer + format: int64 + profile: + type: string + level: + type: string + + PathTrackCodecPropsH264: + type: object + properties: + width: + type: integer + format: int64 + height: + type: integer + format: int64 + profile: + type: string + level: + type: string + + PathTrackCodecPropsOpus: + type: object + properties: + channelCount: + type: integer + format: int64 + + PathTrackCodecPropsMPEG4Audio: + type: object + properties: + sampleRate: + type: integer + format: int64 + channelCount: + type: integer + format: int64 + + PathTrackCodecPropsAC3: + type: object + properties: + sampleRate: + type: integer + format: int64 + channelCount: + type: integer + format: int64 + + PathTrackCodecPropsG711: + type: object + properties: + muLaw: + type: boolean + sampleRate: + type: integer + format: int64 + channelCount: + type: integer + format: int64 + + PathTrackCodecPropsLPCM: + type: object + properties: + bitDepth: + type: integer + format: int64 + sampleRate: + type: integer + format: int64 + channelCount: + type: integer + format: int64 + HLSMuxer: type: object properties: diff --git a/go.mod b/go.mod index 3c072375..dacef650 100644 --- a/go.mod +++ b/go.mod @@ -10,10 +10,10 @@ require ( github.com/abema/go-mp4 v1.5.0 github.com/alecthomas/kong v1.14.0 github.com/asticode/go-astits v1.15.0 - github.com/bluenviron/gohlslib/v2 v2.2.8 + github.com/bluenviron/gohlslib/v2 v2.2.9 github.com/bluenviron/gortmplib v0.3.0 - github.com/bluenviron/gortsplib/v5 v5.4.1-0.20260316202619-e63876e33be9 - github.com/bluenviron/mediacommon/v2 v2.8.2 + github.com/bluenviron/gortsplib/v5 v5.5.0 + github.com/bluenviron/mediacommon/v2 v2.8.3 github.com/datarhei/gosrt v0.10.0 github.com/fsnotify/fsnotify v1.9.0 github.com/gin-contrib/pprof v1.5.3 diff --git a/go.sum b/go.sum index 3c89985d..ff99685d 100644 --- a/go.sum +++ b/go.sum @@ -33,14 +33,14 @@ github.com/asticode/go-astits v1.15.0 h1:yRyCiUc8Jj4F7clt2GDxHghMpWuFL5rkaLuGUd2 github.com/asticode/go-astits v1.15.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/v2 v2.2.8 h1:+KueZeydRDciIq2t3fwT4pshqN+Vreu4c2IMPLVx2Kk= -github.com/bluenviron/gohlslib/v2 v2.2.8/go.mod h1:c4b9vKhyksO6fHhWdCpuuczxUNFeZtBahlFiwrvW2oA= +github.com/bluenviron/gohlslib/v2 v2.2.9 h1:ahRbuBzw+FvdVDwli731RgJsErunyBDWhghpS5fT2qQ= +github.com/bluenviron/gohlslib/v2 v2.2.9/go.mod h1:kA0hTg96hmTZjeZ/Vxwpu/Njy0emAoidEoDGQ/KlMH0= github.com/bluenviron/gortmplib v0.3.0 h1:jBNl7bYtXSq+USx70gQ4kzRsffM1h0XRjGwlHQHZVq8= github.com/bluenviron/gortmplib v0.3.0/go.mod h1:3tUsWceMOrs8Ylt5UyQEHNHCVl52V1gi9tz+IaLTafI= -github.com/bluenviron/gortsplib/v5 v5.4.1-0.20260316202619-e63876e33be9 h1:BSz3Mt+1AqqFtB0zaScPCf3lrHr9L8aQ4XtBFWgrcxU= -github.com/bluenviron/gortsplib/v5 v5.4.1-0.20260316202619-e63876e33be9/go.mod h1:4K3mXlLQ3vN4AyQKtxQE8T5I0Fr1KTNanBCzxDBJKug= -github.com/bluenviron/mediacommon/v2 v2.8.2 h1:ZHuxthfO8o4/fhlG6JvoK5lT27tMWw4k5efpJlpu0yE= -github.com/bluenviron/mediacommon/v2 v2.8.2/go.mod h1:4AsE74EnTxkHeUs1VMER31fivU0jufZUAepaKFRV1lM= +github.com/bluenviron/gortsplib/v5 v5.5.0 h1:2wlUKhlSw1ptKEVnkEuZMpQuN8Xt311Fdd/SB124JPA= +github.com/bluenviron/gortsplib/v5 v5.5.0/go.mod h1:otcPqR836QZej/EYx7njn8vl4TLj8Ya3QAf+GBwh2cQ= +github.com/bluenviron/mediacommon/v2 v2.8.3 h1:T6xb7ZK3eBixi/HynzhtGRCEIrazwcmGIeu0WDTVISY= +github.com/bluenviron/mediacommon/v2 v2.8.3/go.mod h1:CsYjGgzIz8RbloQf4BHR4uReogZsB4PEKWfePVIzJv8= github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE= diff --git a/internal/core/path.go b/internal/core/path.go index ecaadc09..2e0e1296 100644 --- a/internal/core/path.go +++ b/internal/core/path.go @@ -661,6 +661,12 @@ func (pa *path) doAPIPathsGet(req pathAPIPathsGetReq) { } return defs.MediasToCodecs(pa.stream.Desc.Medias) }(), + Tracks2: func() []defs.APIPathTrack { + if !pa.isAvailable() { + return []defs.APIPathTrack{} + } + return defs.MediasToTracks(pa.stream.Desc.Medias) + }(), InboundBytes: func() uint64 { if !pa.isAvailable() { return 0 diff --git a/internal/defs/api_path.go b/internal/defs/api_path.go index 36733231..830766c1 100644 --- a/internal/defs/api_path.go +++ b/internal/defs/api_path.go @@ -72,7 +72,8 @@ type APIPath struct { Online bool `json:"online"` OnlineTime *time.Time `json:"onlineTime"` Source *APIPathSource `json:"source"` - Tracks []APIPathTrackCodec `json:"tracks"` + Tracks []APIPathTrackCodec `json:"tracks" deprecated:"true"` + Tracks2 []APIPathTrack `json:"tracks2"` Readers []APIPathReader `json:"readers"` InboundBytes uint64 `json:"inboundBytes"` OutboundBytes uint64 `json:"outboundBytes"` diff --git a/internal/defs/api_path_track.go b/internal/defs/api_path_track.go new file mode 100644 index 00000000..3caca9d2 --- /dev/null +++ b/internal/defs/api_path_track.go @@ -0,0 +1,7 @@ +package defs + +// APIPathTrack is a track. +type APIPathTrack struct { + Codec APIPathTrackCodec `json:"codec"` + CodecProps APIPathTrackCodecProps `json:"codecProps"` +} diff --git a/internal/defs/api_path_track_codec.go b/internal/defs/api_path_track_codec.go index df670840..6934a602 100644 --- a/internal/defs/api_path_track_codec.go +++ b/internal/defs/api_path_track_codec.go @@ -1,8 +1,13 @@ package defs import ( + "fmt" + "strconv" + "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. @@ -37,65 +42,239 @@ const ( 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 { - switch forma.(type) { - // video - case *format.AV1: - ret[i] = APIPathTrackCodecAV1 - case *format.VP9: - ret[i] = APIPathTrackCodecVP9 - case *format.VP8: - ret[i] = APIPathTrackCodecVP8 - case *format.H265: - ret[i] = APIPathTrackCodecH265 - case *format.H264: - ret[i] = APIPathTrackCodecH264 - case *format.MPEG4Video: - ret[i] = APIPathTrackCodecMPEG4Video - case *format.MPEG1Video: - ret[i] = APIPathTrackCodecMPEG1Video - case *format.MJPEG: - ret[i] = APIPathTrackCodecMJPEG - // audio - case *format.Opus: - ret[i] = APIPathTrackCodecOpus - case *format.Vorbis: - ret[i] = APIPathTrackCodecVorbis - case *format.MPEG4Audio: - ret[i] = APIPathTrackCodecMPEG4Audio - case *format.MPEG4AudioLATM: - ret[i] = APIPathTrackCodecMPEG4AudioLATM - case *format.MPEG1Audio: - ret[i] = APIPathTrackCodecMPEG1Audio - case *format.AC3: - ret[i] = APIPathTrackCodecAC3 - case *format.Speex: - ret[i] = APIPathTrackCodecSpeex - case *format.G726: - ret[i] = APIPathTrackCodecG726 - case *format.G722: - ret[i] = APIPathTrackCodecG722 - case *format.G711: - ret[i] = APIPathTrackCodecG711 - case *format.LPCM: - ret[i] = APIPathTrackCodecLPCM - // other - case *format.MPEGTS: - ret[i] = APIPathTrackCodecMPEGTS - case *format.KLV: - ret[i] = APIPathTrackCodecKLV - default: - ret[i] = APIPathTrackCodecGeneric - } + 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 { @@ -120,3 +299,19 @@ func gatherFormats(medias []*description.Media) []format.Format { 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)) +} diff --git a/internal/defs/api_path_track_codec_props.go b/internal/defs/api_path_track_codec_props.go new file mode 100644 index 00000000..5f183e00 --- /dev/null +++ b/internal/defs/api_path_track_codec_props.go @@ -0,0 +1,85 @@ +package defs + +// APIPathTrackCodecProps contains codec-specific properties. +type APIPathTrackCodecProps interface { + apiPathTrackCodecProps() +} + +// APIPathTrackCodecPropsAV1 contains codec-specific properties of AV1. +type APIPathTrackCodecPropsAV1 struct { + Width int `json:"width"` + Height int `json:"height"` + Profile int `json:"profile"` + Level int `json:"level"` + Tier int `json:"tier"` +} + +func (*APIPathTrackCodecPropsAV1) apiPathTrackCodecProps() {} + +// APIPathTrackCodecPropsVP9 contains codec-specific properties of VP9. +type APIPathTrackCodecPropsVP9 struct { + Profile int `json:"profile"` +} + +func (*APIPathTrackCodecPropsVP9) apiPathTrackCodecProps() {} + +// APIPathTrackCodecPropsH265 contains codec-specific properties of H265. +type APIPathTrackCodecPropsH265 struct { + Width int `json:"width"` + Height int `json:"height"` + Profile string `json:"profile"` + Level string `json:"level"` +} + +func (*APIPathTrackCodecPropsH265) apiPathTrackCodecProps() {} + +// APIPathTrackCodecPropsH264 contains codec-specific properties of H264. +type APIPathTrackCodecPropsH264 struct { + Width int `json:"width"` + Height int `json:"height"` + Profile string `json:"profile"` + Level string `json:"level"` +} + +func (*APIPathTrackCodecPropsH264) apiPathTrackCodecProps() {} + +// APIPathTrackCodecPropsOpus contains codec-specific properties of Opus. +type APIPathTrackCodecPropsOpus struct { + ChannelCount int `json:"channelCount"` +} + +func (*APIPathTrackCodecPropsOpus) apiPathTrackCodecProps() {} + +// APIPathTrackCodecPropsMPEG4Audio contains codec-specific properties of MPEG4Audio. +type APIPathTrackCodecPropsMPEG4Audio struct { + SampleRate int `json:"sampleRate"` + ChannelCount int `json:"channelCount"` +} + +func (*APIPathTrackCodecPropsMPEG4Audio) apiPathTrackCodecProps() {} + +// APIPathTrackCodecPropsAC3 contains codec-specific properties of AC3. +type APIPathTrackCodecPropsAC3 struct { + SampleRate int `json:"sampleRate"` + ChannelCount int `json:"channelCount"` +} + +func (*APIPathTrackCodecPropsAC3) apiPathTrackCodecProps() {} + +// APIPathTrackCodecPropsG711 contains codec-specific properties of G711. +type APIPathTrackCodecPropsG711 struct { + MULaw bool `json:"muLaw"` + SampleRate int `json:"sampleRate"` + ChannelCount int `json:"channelCount"` +} + +func (*APIPathTrackCodecPropsG711) apiPathTrackCodecProps() {} + +// APIPathTrackCodecPropsLPCM contains codec-specific properties of LPCM. +type APIPathTrackCodecPropsLPCM struct { + BitDepth int `json:"bitDepth"` + SampleRate int `json:"sampleRate"` + ChannelCount int `json:"channelCount"` +} + +func (*APIPathTrackCodecPropsLPCM) apiPathTrackCodecProps() {} diff --git a/internal/defs/api_path_track_codec_test.go b/internal/defs/api_path_track_codec_test.go index 529bc849..3bf1295e 100644 --- a/internal/defs/api_path_track_codec_test.go +++ b/internal/defs/api_path_track_codec_test.go @@ -3,10 +3,68 @@ 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{}, @@ -58,3 +116,91 @@ func TestFormatsToCodecs(t *testing.T) { 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) +} diff --git a/internal/linters/go2api/go2api_test.go b/internal/linters/go2api/go2api_test.go index 76348abc..bad41e07 100644 --- a/internal/linters/go2api/go2api_test.go +++ b/internal/linters/go2api/go2api_test.go @@ -77,6 +77,14 @@ func goStructToApi(t *testing.T, rt reflect.Type) openAPIProperty { return openAPIProperty{Ref: "#/components/schemas/" + schemaName(rt)} } + if rt == reflect.TypeOf((*defs.APIPathTrackCodecProps)(nil)).Elem() { + return openAPIProperty{ + Type: "object", + AllOf: []openAPIProperty{{Ref: "#/components/schemas/" + schemaName(rt)}}, + Nullable: true, + } + } + switch { case rt == reflect.TypeOf(""): return openAPIProperty{Type: "string"} @@ -367,6 +375,10 @@ func TestGo2API(t *testing.T) { "PathSource", defs.APIPathSource{}, }, + { + "PathTrack", + defs.APIPathTrack{}, + }, { "Recording", defs.APIRecording{},