Files
monibuca/plugin/rtsp/pkg/codec.go
2024-10-03 15:19:50 +08:00

305 lines
6.0 KiB
Go

package rtsp
import (
"encoding/base64"
"encoding/json"
"fmt"
"strconv"
"strings"
"unicode"
"github.com/pion/sdp/v3"
)
const (
DirectionRecvonly = "recvonly"
DirectionSendonly = "sendonly"
DirectionSendRecv = "sendrecv"
KindVideo = "video"
KindAudio = "audio"
CodecH264 = "H264" // payloadType: 96
CodecH265 = "H265"
CodecVP8 = "VP8"
CodecVP9 = "VP9"
CodecAV1 = "AV1"
CodecJPEG = "JPEG" // payloadType: 26
CodecRAW = "RAW"
CodecPCMU = "PCMU" // payloadType: 0
CodecPCMA = "PCMA" // payloadType: 8
CodecAAC = "MPEG4-GENERIC"
CodecLATM = "MP4A-LATM"
CodecOpus = "OPUS" // payloadType: 111
CodecG722 = "G722"
CodecMP3 = "MPA" // payload: 14, aka MPEG-1 Layer III
CodecPCM = "L16" // Linear PCM (big endian)
CodecPCML = "PCML" // Linear PCM (little endian)
CodecELD = "ELD" // AAC-ELD
CodecFLAC = "FLAC"
CodecAll = "ALL"
CodecAny = "ANY"
)
const PayloadTypeRAW byte = 255
func Between(s, sub1, sub2 string) string {
i := strings.Index(s, sub1)
if i < 0 {
return ""
}
s = s[i+len(sub1):]
if i = strings.Index(s, sub2); i >= 0 {
return s[:i]
}
return s
}
func Atoi(s string) (i int) {
if s != "" {
i, _ = strconv.Atoi(s)
}
return
}
type Codec struct {
Name string // H264, PCMU, PCMA, opus...
ClockRate uint32 // 90000, 8000, 16000...
Channels uint16 // 0, 1, 2
FmtpLine string
PayloadType uint8
}
// MarshalJSON - return FFprobe compatible output
func (c *Codec) MarshalJSON() ([]byte, error) {
info := map[string]any{}
if name := FFmpegCodecName(c.Name); name != "" {
info["codec_name"] = name
info["codec_type"] = c.Kind()
}
if c.Name == CodecH264 {
profile, level := DecodeH264(c.FmtpLine)
if profile != "" {
info["profile"] = profile
info["level"] = level
}
}
if c.ClockRate != 0 && c.ClockRate != 90000 {
info["sample_rate"] = c.ClockRate
}
if c.Channels > 0 {
info["channels"] = c.Channels
}
return json.Marshal(info)
}
func FFmpegCodecName(name string) string {
switch name {
case CodecH264:
return "h264"
case CodecH265:
return "hevc"
case CodecJPEG:
return "mjpeg"
case CodecRAW:
return "rawvideo"
case CodecPCMA:
return "pcm_alaw"
case CodecPCMU:
return "pcm_mulaw"
case CodecPCM:
return "pcm_s16be"
case CodecPCML:
return "pcm_s16le"
case CodecAAC, CodecLATM:
return "aac"
case CodecOpus:
return "opus"
case CodecVP8:
return "vp8"
case CodecVP9:
return "vp9"
case CodecAV1:
return "av1"
case CodecELD:
return "aac/eld"
case CodecFLAC:
return "flac"
case CodecMP3:
return "mp3"
}
return name
}
func (c *Codec) String() (s string) {
s = c.Name
if c.ClockRate != 0 && c.ClockRate != 90000 {
s += fmt.Sprintf("/%d", c.ClockRate)
}
if c.Channels > 0 {
s += fmt.Sprintf("/%d", c.Channels)
}
return
}
func (c *Codec) IsRTP() bool {
return c.PayloadType != PayloadTypeRAW
}
func (c *Codec) IsVideo() bool {
return c.Kind() == KindVideo
}
func (c *Codec) IsAudio() bool {
return c.Kind() == KindAudio
}
func (c *Codec) Kind() string {
return GetKind(c.Name)
}
func (c *Codec) PrintName() string {
switch c.Name {
case CodecAAC:
return "AAC"
case CodecPCM:
return "S16B"
case CodecPCML:
return "S16L"
}
return c.Name
}
func (c *Codec) Clone() *Codec {
clone := *c
return &clone
}
func (c *Codec) Match(remote *Codec) bool {
switch remote.Name {
case CodecAll, CodecAny:
return true
}
return c.Name == remote.Name &&
(c.ClockRate == remote.ClockRate || remote.ClockRate == 0) &&
(c.Channels == remote.Channels || remote.Channels == 0)
}
func UnmarshalCodec(md *sdp.MediaDescription, payloadType string) *Codec {
c := &Codec{PayloadType: byte(Atoi(payloadType))}
for _, attr := range md.Attributes {
switch {
case c.Name == "" && attr.Key == "rtpmap" && strings.HasPrefix(attr.Value, payloadType):
i := strings.IndexByte(attr.Value, ' ')
ss := strings.Split(attr.Value[i+1:], "/")
c.Name = strings.ToUpper(ss[0])
// fix tailing space: `a=rtpmap:96 H264/90000 `
c.ClockRate = uint32(Atoi(strings.TrimRightFunc(ss[1], unicode.IsSpace)))
if len(ss) == 3 && ss[2] == "2" {
c.Channels = 2
}
case c.FmtpLine == "" && attr.Key == "fmtp" && strings.HasPrefix(attr.Value, payloadType):
if i := strings.IndexByte(attr.Value, ' '); i > 0 {
c.FmtpLine = attr.Value[i+1:]
}
}
}
if c.Name == "" {
// https://en.wikipedia.org/wiki/RTP_payload_formats
switch payloadType {
case "0":
c.Name = CodecPCMU
c.ClockRate = 8000
case "8":
c.Name = CodecPCMA
c.ClockRate = 8000
case "10":
c.Name = CodecPCM
c.ClockRate = 44100
c.Channels = 2
case "11":
c.Name = CodecPCM
c.ClockRate = 44100
case "14":
c.Name = CodecMP3
c.ClockRate = 90000 // it's not real sample rate
case "26":
c.Name = CodecJPEG
c.ClockRate = 90000
case "96", "97", "98":
if len(md.Bandwidth) == 0 {
c.Name = payloadType
break
}
// FFmpeg + RTSP + pcm_s16le = doesn't pass info about codec name and params
// so try to guess the codec based on bitrate
// https://github.com/AlexxIT/go2rtc/issues/523
switch md.Bandwidth[0].Bandwidth {
case 128:
c.ClockRate = 8000
case 256:
c.ClockRate = 16000
case 384:
c.ClockRate = 24000
case 512:
c.ClockRate = 32000
case 705:
c.ClockRate = 44100
case 768:
c.ClockRate = 48000
case 1411:
// default Windows DShow
c.ClockRate = 44100
c.Channels = 2
case 1536:
// default Linux ALSA
c.ClockRate = 48000
c.Channels = 2
default:
c.Name = payloadType
break
}
c.Name = CodecPCML
default:
c.Name = payloadType
}
}
return c
}
func DecodeH264(fmtp string) (profile string, level byte) {
if ps := Between(fmtp, "sprop-parameter-sets=", ","); ps != "" {
if sps, _ := base64.StdEncoding.DecodeString(ps); len(sps) >= 4 {
switch sps[1] {
case 0x42:
profile = "Baseline"
case 0x4D:
profile = "Main"
case 0x58:
profile = "Extended"
case 0x64:
profile = "High"
default:
profile = fmt.Sprintf("0x%02X", sps[1])
}
level = sps[3]
}
}
return
}