mirror of
https://github.com/bluenviron/mediamtx.git
synced 2026-01-26 21:39:16 -08:00
feat: add resolution and bitrate information to API responses
This commit is contained in:
parent
6acf9f1d2b
commit
79ee4c3db5
9 changed files with 234 additions and 0 deletions
|
|
@ -592,6 +592,14 @@ components:
|
|||
type: array
|
||||
items:
|
||||
type: string
|
||||
resolutions:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
bitrates:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
bytesReceived:
|
||||
type: integer
|
||||
format: int64
|
||||
|
|
|
|||
|
|
@ -42,6 +42,8 @@ func TestPathsList(t *testing.T) {
|
|||
Ready: true,
|
||||
ReadyTime: &now,
|
||||
Tracks: []string{"H264", "Opus"},
|
||||
Resolutions: []string{"1920x1080", ""},
|
||||
Bitrates: []string{"4500kbps", ""},
|
||||
BytesReceived: 1000,
|
||||
BytesSent: 2000,
|
||||
Readers: []defs.APIPathSourceOrReader{
|
||||
|
|
|
|||
|
|
@ -309,3 +309,12 @@ func (t *h264) ProcessRTPPacket( //nolint:dupl
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ExtractH264Resolution extracts width and height from H264 SPS
|
||||
func ExtractH264Resolution(sps []byte) (int, int) {
|
||||
var s mch264.SPS
|
||||
if err := s.Unmarshal(sps); err != nil {
|
||||
return 0, 0
|
||||
}
|
||||
return s.Width(), s.Height()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -341,3 +341,12 @@ func (t *h265) ProcessRTPPacket( //nolint:dupl
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ExtractH265Resolution extracts width and height from H265 SPS
|
||||
func ExtractH265Resolution(sps []byte) (int, int) {
|
||||
var s mch265.SPS
|
||||
if err := s.Unmarshal(sps); err != nil {
|
||||
return 0, 0
|
||||
}
|
||||
return s.Width(), s.Height()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -108,3 +108,28 @@ func (t *mpeg1Video) ProcessRTPPacket( //nolint:dupl
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
// ExtractMPEG1Resolution extracts width and height from MPEG-1/2 Video config
|
||||
func ExtractMPEG1Resolution(config []byte) (int, int) {
|
||||
// MPEG-1/2 Video sequence header parsing for resolution
|
||||
// Look for sequence header start code 0x00 0x00 0x01 0xB3
|
||||
if len(config) < 12 {
|
||||
return 0, 0
|
||||
}
|
||||
for i := 0; i < len(config)-4; i++ {
|
||||
if config[i] == 0x00 && config[i+1] == 0x00 && config[i+2] == 0x01 && config[i+3] == 0xB3 {
|
||||
// Sequence header starts after start code
|
||||
data := config[i+4:]
|
||||
if len(data) < 8 {
|
||||
continue
|
||||
}
|
||||
// horizontal_size_value: 12 bits
|
||||
width := (uint32(data[0]) << 4) | (uint32(data[1]) >> 4)
|
||||
// vertical_size_value: 12 bits
|
||||
height := ((uint32(data[1]) & 0x0F) << 8) | uint32(data[2])
|
||||
return int(width), int(height)
|
||||
}
|
||||
}
|
||||
return 0, 0
|
||||
}
|
||||
|
|
|
|||
|
|
@ -157,3 +157,119 @@ func (t *mpeg4Video) ProcessRTPPacket( //nolint:dupl
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
// ExtractMPEG4Resolution extracts width and height from MPEG-4 Video config
|
||||
func ExtractMPEG4Resolution(config []byte) (int, int) {
|
||||
// MPEG-4 Video Object Layer (VOL) parsing for resolution
|
||||
// Look for VOL start code 0x00 0x00 0x01 0x20
|
||||
if len(config) < 20 {
|
||||
return 0, 0
|
||||
}
|
||||
for i := 0; i < len(config)-4; i++ {
|
||||
if config[i] == 0x00 && config[i+1] == 0x00 && config[i+2] == 0x01 && config[i+3] == 0x20 {
|
||||
// VOL header starts after start code
|
||||
data := config[i+4:]
|
||||
if len(data) < 10 {
|
||||
continue
|
||||
}
|
||||
// Skip vol_id (4 bits), random_accessible_vol (1), video_object_type_indication (8)
|
||||
// is_object_layer_identifier (1)
|
||||
bitPos := 0
|
||||
// vol_id: 4 bits
|
||||
bitPos += 4
|
||||
// random_accessible_vol: 1 bit
|
||||
bitPos += 1
|
||||
// video_object_type_indication: 8 bits
|
||||
bitPos += 8
|
||||
// is_object_layer_identifier: 1 bit
|
||||
isObjectLayer := getBit(data, bitPos)
|
||||
bitPos += 1
|
||||
if isObjectLayer {
|
||||
// video_object_layer_verid: 4 bits
|
||||
bitPos += 4
|
||||
// video_object_layer_priority: 3 bits
|
||||
bitPos += 3
|
||||
}
|
||||
// aspect_ratio_info: 4 bits
|
||||
aspectRatio := getBits(data, bitPos, 4)
|
||||
bitPos += 4
|
||||
if aspectRatio == 15 { // extended_PAR
|
||||
// par_width: 8 bits
|
||||
bitPos += 8
|
||||
// par_height: 8 bits
|
||||
bitPos += 8
|
||||
}
|
||||
// vol_control_parameters: 1 bit
|
||||
volControl := getBit(data, bitPos)
|
||||
bitPos += 1
|
||||
if volControl {
|
||||
// chroma_format: 2 bits
|
||||
bitPos += 2
|
||||
// low_delay: 1 bit
|
||||
bitPos += 1
|
||||
// vbv_parameters: 1 bit
|
||||
vbv := getBit(data, bitPos)
|
||||
bitPos += 1
|
||||
if vbv {
|
||||
// bit_rate: 15 bits
|
||||
bitPos += 15
|
||||
// buffer_size: 15 bits
|
||||
bitPos += 15
|
||||
// vbv_occupancy: 15 bits
|
||||
bitPos += 15
|
||||
}
|
||||
}
|
||||
// video_object_layer_shape: 2 bits
|
||||
shape := getBits(data, bitPos, 2)
|
||||
bitPos += 2
|
||||
if shape == 3 { // gray_scale
|
||||
// video_object_layer_shape_extension: 4 bits
|
||||
bitPos += 4
|
||||
}
|
||||
// marker_bit: 1 bit
|
||||
bitPos += 1
|
||||
// vop_time_increment_resolution: 16 bits
|
||||
bitPos += 16
|
||||
// marker_bit: 1 bit
|
||||
bitPos += 1
|
||||
// fixed_vop_rate: 1 bit
|
||||
fixedVop := getBit(data, bitPos)
|
||||
bitPos += 1
|
||||
if fixedVop {
|
||||
// fixed_vop_time_increment: variable bits
|
||||
// skip for now
|
||||
}
|
||||
// marker_bit: 1 bit
|
||||
bitPos += 1
|
||||
// video_object_layer_width: 13 bits
|
||||
width := getBits(data, bitPos, 13)
|
||||
bitPos += 13
|
||||
// marker_bit: 1 bit
|
||||
bitPos += 1
|
||||
// video_object_layer_height: 13 bits
|
||||
height := getBits(data, bitPos, 13)
|
||||
return int(width), int(height)
|
||||
}
|
||||
}
|
||||
return 0, 0
|
||||
}
|
||||
|
||||
func getBit(data []byte, bitPos int) bool {
|
||||
byteIndex := bitPos / 8
|
||||
bitIndex := 7 - (bitPos % 8)
|
||||
if byteIndex >= len(data) {
|
||||
return false
|
||||
}
|
||||
return (data[byteIndex] & (1 << bitIndex)) != 0
|
||||
}
|
||||
|
||||
func getBits(data []byte, bitPos int, numBits int) uint32 {
|
||||
var val uint32
|
||||
for i := 0; i < numBits; i++ {
|
||||
if getBit(data, bitPos+i) {
|
||||
val |= 1 << (numBits - 1 - i)
|
||||
}
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
|
@ -571,6 +571,18 @@ func (pa *path) doAPIPathsGet(req pathAPIPathsGetReq) {
|
|||
}
|
||||
return defs.MediasToCodecs(pa.stream.Desc.Medias)
|
||||
}(),
|
||||
Resolutions: func() []string {
|
||||
if !pa.isReady() {
|
||||
return []string{}
|
||||
}
|
||||
return defs.MediasToResolutions(pa.stream.Desc.Medias)
|
||||
}(),
|
||||
Bitrates: func() []string {
|
||||
if !pa.isReady() {
|
||||
return []string{}
|
||||
}
|
||||
return pa.bitrates()
|
||||
}(),
|
||||
BytesReceived: func() uint64 {
|
||||
if !pa.isReady() {
|
||||
return 0
|
||||
|
|
@ -951,3 +963,17 @@ func (pa *path) APIPathsGet(req pathAPIPathsGetReq) (*defs.APIPath, error) {
|
|||
return nil, fmt.Errorf("terminated")
|
||||
}
|
||||
}
|
||||
|
||||
func (pa *path) bitrates() []string {
|
||||
elapsed := time.Since(pa.readyTime).Seconds()
|
||||
if elapsed > 0 {
|
||||
totalBps := float64(pa.stream.BytesReceived()) * 8 / elapsed
|
||||
bitrateStr := fmt.Sprintf("%.0f kbps", totalBps/1000)
|
||||
bitrates := make([]string, len(pa.stream.Desc.Medias))
|
||||
for i := range bitrates {
|
||||
bitrates[i] = bitrateStr
|
||||
}
|
||||
return bitrates
|
||||
}
|
||||
return []string{}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -88,6 +88,8 @@ type APIPath struct {
|
|||
Ready bool `json:"ready"`
|
||||
ReadyTime *time.Time `json:"readyTime"`
|
||||
Tracks []string `json:"tracks"`
|
||||
Resolutions []string `json:"resolutions"`
|
||||
Bitrates []string `json:"bitrates"`
|
||||
BytesReceived uint64 `json:"bytesReceived"`
|
||||
BytesSent uint64 `json:"bytesSent"`
|
||||
Readers []APIPathSourceOrReader `json:"readers"`
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import (
|
|||
"github.com/bluenviron/gortsplib/v5/pkg/format"
|
||||
|
||||
"github.com/bluenviron/mediamtx/internal/logger"
|
||||
"github.com/bluenviron/mediamtx/internal/codecprocessor"
|
||||
)
|
||||
|
||||
// Source is an entity that can provide a stream.
|
||||
|
|
@ -52,6 +53,42 @@ func MediasToCodecs(medias []*description.Media) []string {
|
|||
return FormatsToCodecs(formats)
|
||||
}
|
||||
|
||||
// MediasToResolutions returns the resolutions of given medias.
|
||||
func MediasToResolutions(medias []*description.Media) []string {
|
||||
var ret []string
|
||||
for _, media := range medias {
|
||||
for _, forma := range media.Formats {
|
||||
ret = append(ret, getResolution(forma))
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func getResolution(forma format.Format) string {
|
||||
if h264f, ok := forma.(*format.H264); ok {
|
||||
width, height := codecprocessor.ExtractH264Resolution(h264f.SPS)
|
||||
if width > 0 && width <= 10000 && height > 0 && height <= 10000 {
|
||||
return fmt.Sprintf("%dx%d", width, height)
|
||||
}
|
||||
} else if h265f, ok := forma.(*format.H265); ok {
|
||||
width, height := codecprocessor.ExtractH265Resolution(h265f.SPS)
|
||||
if width > 0 && width <= 10000 && height > 0 && height <= 10000 {
|
||||
return fmt.Sprintf("%dx%d", width, height)
|
||||
}
|
||||
} else if mpeg4f, ok := forma.(*format.MPEG4Video); ok {
|
||||
width, height := codecprocessor.ExtractMPEG4Resolution(mpeg4f.Config)
|
||||
if width > 0 && width <= 10000 && height > 0 && height <= 10000 {
|
||||
return fmt.Sprintf("%dx%d", width, height)
|
||||
}
|
||||
} else if _, ok := forma.(*format.MPEG1Video); ok {
|
||||
width, height := codecprocessor.ExtractMPEG1Resolution(codecprocessor.MPEG1VideoDefaultConfig)
|
||||
if width > 0 && width <= 10000 && height > 0 && height <= 10000 {
|
||||
return fmt.Sprintf("%dx%d", width, height)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// MediasInfo returns a description of medias.
|
||||
func MediasInfo(medias []*description.Media) string {
|
||||
var formats []format.Format
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue