install go2rtc on bob

This commit is contained in:
2026-04-04 19:36:14 +02:00
parent f0b56e63d1
commit ccf88187b8
537 changed files with 69213 additions and 0 deletions
@@ -0,0 +1,40 @@
## PCM
**RTSP**
- PayloadType=10 - L16/44100/2 - Linear PCM 16-bit big endian
- PayloadType=11 - L16/44100/1 - Linear PCM 16-bit big endian
https://en.wikipedia.org/wiki/RTP_payload_formats
**Apple QuickTime**
- `raw` - 16-bit data is stored in little endian format
- `twos` - 16-bit data is stored in big endian format
- `sowt` - 16-bit data is stored in little endian format
- `in24` - denotes 24-bit, big endian
- `in32` - denotes 32-bit, big endian
- `fl32` - denotes 32-bit floating point PCM
- `fl64` - denotes 64-bit floating point PCM
- `alaw` - denotes A-law logarithmic PCM
- `ulaw` - denotes mu-law logarithmic PCM
https://wiki.multimedia.cx/index.php/PCM
**FFmpeg RTSP**
```
pcm_s16be, 44100 Hz, stereo => 10
pcm_s16be, 48000 Hz, stereo => 96 L16/48000/2
pcm_s16be, 44100 Hz, mono => 11
pcm_s16le, 48000 Hz, stereo => 96 (b=AS:1536)
pcm_s16le, 44100 Hz, stereo => 96 (b=AS:1411)
pcm_s16le, 16000 Hz, stereo => 96 (b=AS:512)
pcm_s16le, 8000 Hz, stereo => 96 (b=AS:256)
pcm_s16le, 48000 Hz, mono => 96 (b=AS:768)
pcm_s16le, 44100 Hz, mono => 96 (b=AS:705)
pcm_s16le, 16000 Hz, mono => 96 (b=AS:256)
pcm_s16le, 8000 Hz, mono => 96 (b=AS:128)
```
+284
View File
@@ -0,0 +1,284 @@
package core
import (
"encoding/base64"
"encoding/json"
"fmt"
"strings"
"unicode"
"github.com/pion/sdp/v3"
)
type Codec struct {
Name string // H264, PCMU, PCMA, opus...
ClockRate uint32 // 90000, 8000, 16000...
Channels uint8 // 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:
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:]
}
}
}
switch c.Name {
case "PCM":
// https://www.reddit.com/r/Hikvision/comments/17elxex/comment/k642g2r/
// check pkg/rtsp/rtsp_test.go TestHikvisionPCM
c.Name = CodecPCML
case "":
// 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
}
func ParseCodecString(s string) *Codec {
var codec Codec
ss := strings.Split(s, "/")
switch strings.ToLower(ss[0]) {
case "pcm_s16be", "s16be", "pcm":
codec.Name = CodecPCM
case "pcm_s16le", "s16le", "pcml":
codec.Name = CodecPCML
case "pcm_alaw", "alaw", "pcma", "g711a":
codec.Name = CodecPCMA
case "pcm_mulaw", "mulaw", "pcmu", "g711u":
codec.Name = CodecPCMU
case "aac", "mpeg4-generic":
codec.Name = CodecAAC
case "opus":
codec.Name = CodecOpus
case "flac":
codec.Name = CodecFLAC
default:
return nil
}
if len(ss) >= 2 {
codec.ClockRate = uint32(Atoi(ss[1]))
}
if len(ss) >= 3 {
codec.Channels = uint8(Atoi(ss[2]))
}
return &codec
}
@@ -0,0 +1,144 @@
package core
import (
"io"
"net/http"
"reflect"
"sync/atomic"
)
func NewID() uint32 {
return id.Add(1)
}
// Deprecated: use NewID instead
func ID(v any) uint32 {
p := uintptr(reflect.ValueOf(v).UnsafePointer())
return 0x8000_0000 | uint32(p)
}
var id atomic.Uint32
type Info interface {
SetProtocol(string)
SetRemoteAddr(string)
SetSource(string)
SetURL(string)
WithRequest(*http.Request)
GetSource() string
}
// Connection just like webrtc.PeerConnection
// - ID and RemoteAddr used for building Connection(s) graph
// - FormatName, Protocol, RemoteAddr, Source, URL, SDP, UserAgent used for info about Connection
// - FormatName and Protocol has FFmpeg compatible names
// - Transport used for auto closing on Stop
type Connection struct {
ID uint32 `json:"id,omitempty"`
FormatName string `json:"format_name,omitempty"` // rtsp, webrtc, mp4, mjpeg, mpjpeg...
Protocol string `json:"protocol,omitempty"` // tcp, udp, http, ws, pipe...
RemoteAddr string `json:"remote_addr,omitempty"` // host:port other info
Source string `json:"source,omitempty"`
URL string `json:"url,omitempty"`
SDP string `json:"sdp,omitempty"`
UserAgent string `json:"user_agent,omitempty"`
Medias []*Media `json:"medias,omitempty"`
Receivers []*Receiver `json:"receivers,omitempty"`
Senders []*Sender `json:"senders,omitempty"`
Recv int `json:"bytes_recv,omitempty"`
Send int `json:"bytes_send,omitempty"`
Transport any `json:"-"`
}
func (c *Connection) GetMedias() []*Media {
return c.Medias
}
func (c *Connection) GetTrack(media *Media, codec *Codec) (*Receiver, error) {
for _, receiver := range c.Receivers {
if receiver.Codec == codec {
return receiver, nil
}
}
receiver := NewReceiver(media, codec)
c.Receivers = append(c.Receivers, receiver)
return receiver, nil
}
func (c *Connection) Stop() error {
for _, receiver := range c.Receivers {
receiver.Close()
}
for _, sender := range c.Senders {
sender.Close()
}
if closer, ok := c.Transport.(io.Closer); ok {
return closer.Close()
}
return nil
}
// Deprecated:
func (c *Connection) Codecs() []*Codec {
codecs := make([]*Codec, len(c.Senders))
for i, sender := range c.Senders {
codecs[i] = sender.Codec
}
return codecs
}
func (c *Connection) SetProtocol(s string) {
c.Protocol = s
}
func (c *Connection) SetRemoteAddr(s string) {
if c.RemoteAddr == "" {
c.RemoteAddr = s
} else {
c.RemoteAddr += " forwarded " + s
}
}
func (c *Connection) SetSource(s string) {
c.Source = s
}
func (c *Connection) SetURL(s string) {
c.URL = s
}
func (c *Connection) WithRequest(r *http.Request) {
if r.Header.Get("Upgrade") == "websocket" {
c.Protocol = "ws"
} else {
c.Protocol = "http"
}
c.RemoteAddr = r.RemoteAddr
if remote := r.Header.Get("X-Forwarded-For"); remote != "" {
c.RemoteAddr += " forwarded " + remote
}
c.UserAgent = r.UserAgent()
}
func (c *Connection) GetSource() string {
return c.Source
}
// Create like os.Create, init Consumer with existing Transport
func Create(w io.Writer) (*Connection, error) {
return &Connection{Transport: w}, nil
}
// Open like os.Open, init Producer from existing Transport
func Open(r io.Reader) (*Connection, error) {
return &Connection{Transport: r}, nil
}
// Dial like net.Dial, init Producer via Dialing
func Dial(rawURL string) (*Connection, error) {
return &Connection{}, nil
}
+97
View File
@@ -0,0 +1,97 @@
package core
import "encoding/json"
const (
DirectionRecvonly = "recvonly"
DirectionSendonly = "sendonly"
DirectionSendRecv = "sendrecv"
)
const (
KindVideo = "video"
KindAudio = "audio"
)
const (
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"
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
type Producer interface {
// GetMedias - return Media(s) with local Media.Direction:
// - recvonly for Producer Video/Audio
// - sendonly for Producer backchannel
GetMedias() []*Media
// GetTrack - return Receiver, that can only produce rtp.Packet(s)
GetTrack(media *Media, codec *Codec) (*Receiver, error)
// Deprecated: rename to Run()
Start() error
// Deprecated: rename to Close()
Stop() error
}
type Consumer interface {
// GetMedias - return Media(s) with local Media.Direction:
// - sendonly for Consumer Video/Audio
// - recvonly for Consumer backchannel
GetMedias() []*Media
AddTrack(media *Media, codec *Codec, track *Receiver) error
// Deprecated: rename to Close()
Stop() error
}
type Mode byte
const (
ModeActiveProducer Mode = iota + 1 // typical source (client)
ModePassiveConsumer
ModePassiveProducer
ModeActiveConsumer
)
func (m Mode) String() string {
switch m {
case ModeActiveProducer:
return "active producer"
case ModePassiveConsumer:
return "passive consumer"
case ModePassiveProducer:
return "passive producer"
case ModeActiveConsumer:
return "active consumer"
}
return "unknown"
}
func (m Mode) MarshalJSON() ([]byte, error) {
return json.Marshal(m.String())
}
@@ -0,0 +1,134 @@
package core
import (
"fmt"
"testing"
"github.com/stretchr/testify/require"
)
type producer struct {
Medias []*Media
Receivers []*Receiver
id byte
}
func (p *producer) GetMedias() []*Media {
return p.Medias
}
func (p *producer) GetTrack(_ *Media, codec *Codec) (*Receiver, error) {
for _, receiver := range p.Receivers {
if receiver.Codec == codec {
return receiver, nil
}
}
receiver := NewReceiver(nil, codec)
p.Receivers = append(p.Receivers, receiver)
return receiver, nil
}
func (p *producer) Start() error {
pkt := &Packet{Payload: []byte{p.id}}
p.Receivers[0].Input(pkt)
return nil
}
func (p *producer) Stop() error {
for _, receiver := range p.Receivers {
receiver.Close()
}
return nil
}
type consumer struct {
Medias []*Media
Senders []*Sender
cache chan byte
}
func (c *consumer) GetMedias() []*Media {
return c.Medias
}
func (c *consumer) AddTrack(_ *Media, _ *Codec, track *Receiver) error {
c.cache = make(chan byte, 1)
sender := NewSender(nil, track.Codec)
sender.Output = func(packet *Packet) {
c.cache <- packet.Payload[0]
}
sender.HandleRTP(track)
c.Senders = append(c.Senders, sender)
return nil
}
func (c *consumer) Stop() error {
for _, sender := range c.Senders {
sender.Close()
}
return nil
}
func (c *consumer) read() byte {
return <-c.cache
}
func TestName(t *testing.T) {
GetProducer := func(b byte) Producer {
return &producer{
Medias: []*Media{
{
Kind: KindVideo,
Direction: DirectionRecvonly,
Codecs: []*Codec{
{Name: CodecH264},
},
},
},
id: b,
}
}
// stage1
prod1 := GetProducer(1)
cons2 := &consumer{}
media1 := prod1.GetMedias()[0]
track1, _ := prod1.GetTrack(media1, media1.Codecs[0])
_ = cons2.AddTrack(nil, nil, track1)
_ = prod1.Start()
require.Equal(t, byte(1), cons2.read())
// stage2
prod2 := GetProducer(2)
media2 := prod2.GetMedias()[0]
require.NotEqual(t, fmt.Sprintf("%p", media1), fmt.Sprintf("%p", media2))
track2, _ := prod2.GetTrack(media2, media2.Codecs[0])
track1.Replace(track2)
_ = prod1.Stop()
_ = prod2.Start()
require.Equal(t, byte(2), cons2.read())
// stage3
_ = prod2.Stop()
}
func TestStripUserinfo(t *testing.T) {
s := `streams:
test:
- ffmpeg:rtsp://username:password@10.1.2.3:554/stream1
- ffmpeg:rtsp://10.1.2.3:554/stream1@#video=copy
`
s = StripUserinfo(s)
require.Equal(t, `streams:
test:
- ffmpeg:rtsp://***@10.1.2.3:554/stream1
- ffmpeg:rtsp://10.1.2.3:554/stream1@#video=copy
`, s)
}
@@ -0,0 +1,94 @@
package core
import (
"crypto/rand"
"runtime"
"strconv"
"strings"
"time"
)
const (
BufferSize = 64 * 1024 // 64K
ConnDialTimeout = 5 * time.Second
ConnDeadline = 5 * time.Second
ProbeTimeout = 5 * time.Second
)
// Now90000 - timestamp for Video (clock rate = 90000 samples per second)
func Now90000() uint32 {
return uint32(time.Duration(time.Now().UnixNano()) * 90000 / time.Second)
}
const symbols = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_"
// RandString base10 - numbers, base16 - hex, base36 - digits+letters
// base64 - URL safe symbols, base0 - crypto random
func RandString(size, base byte) string {
b := make([]byte, size)
if _, err := rand.Read(b); err != nil {
panic(err)
}
if base == 0 {
return string(b)
}
for i := byte(0); i < size; i++ {
b[i] = symbols[b[i]%base]
}
return string(b)
}
func Before(s, sep string) string {
if i := strings.Index(s, sep); i > 0 {
return s[:i]
}
return s
}
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
}
// ParseByte - fast parsing string to byte function
func ParseByte(s string) (b byte) {
for i, ch := range []byte(s) {
ch -= '0'
if ch > 9 {
return 0
}
if i > 0 {
b *= 10
}
b += ch
}
return
}
func Assert(ok bool) {
if !ok {
_, file, line, _ := runtime.Caller(1)
panic(file + ":" + strconv.Itoa(line))
}
}
func Caller() string {
_, file, line, _ := runtime.Caller(1)
return file + ":" + strconv.Itoa(line)
}
@@ -0,0 +1,18 @@
package core
type EventFunc func(msg any)
// Listener base struct for all classes with support feedback
type Listener struct {
events []EventFunc
}
func (l *Listener) Listen(f EventFunc) {
l.events = append(l.events, f)
}
func (l *Listener) Fire(msg any) {
for _, f := range l.events {
f(msg)
}
}
+211
View File
@@ -0,0 +1,211 @@
package core
import (
"encoding/json"
"fmt"
"strings"
"github.com/pion/sdp/v3"
)
// Media take best from:
// - deepch/vdk/format/rtsp/sdp.Media
// - pion/sdp.MediaDescription
type Media struct {
Kind string `json:"kind,omitempty"` // video or audio
Direction string `json:"direction,omitempty"` // sendonly, recvonly
Codecs []*Codec `json:"codecs,omitempty"`
ID string `json:"id,omitempty"` // MID for WebRTC, Control for RTSP
}
func (m *Media) String() string {
s := fmt.Sprintf("%s, %s", m.Kind, m.Direction)
for _, codec := range m.Codecs {
name := codec.String()
if strings.Contains(s, name) {
continue
}
s += ", " + name
}
return s
}
func (m *Media) MarshalJSON() ([]byte, error) {
return json.Marshal(m.String())
}
func (m *Media) Clone() *Media {
clone := *m
clone.Codecs = make([]*Codec, len(m.Codecs))
for i, codec := range m.Codecs {
clone.Codecs[i] = codec.Clone()
}
return &clone
}
func (m *Media) MatchMedia(remote *Media) (codec, remoteCodec *Codec) {
// check same kind and opposite dirrection
if m.Kind != remote.Kind ||
m.Direction == DirectionSendonly && remote.Direction != DirectionRecvonly ||
m.Direction == DirectionRecvonly && remote.Direction != DirectionSendonly {
return nil, nil
}
for _, codec = range m.Codecs {
for _, remoteCodec = range remote.Codecs {
if codec.Match(remoteCodec) {
return
}
}
}
return nil, nil
}
func (m *Media) MatchCodec(remote *Codec) *Codec {
for _, codec := range m.Codecs {
if codec.Match(remote) {
return codec
}
}
return nil
}
func (m *Media) MatchAll() bool {
for _, codec := range m.Codecs {
if codec.Name == CodecAll {
return true
}
}
return false
}
func (m *Media) Equal(media *Media) bool {
if media.ID != "" {
return m.ID == media.ID
}
return m.String() == media.String()
}
func GetKind(name string) string {
switch name {
case CodecH264, CodecH265, CodecVP8, CodecVP9, CodecAV1, CodecJPEG, CodecRAW:
return KindVideo
case CodecPCMU, CodecPCMA, CodecAAC, CodecOpus, CodecG722, CodecMP3, CodecPCM, CodecPCML, CodecELD, CodecFLAC:
return KindAudio
}
return ""
}
func MarshalSDP(name string, medias []*Media) ([]byte, error) {
sd := &sdp.SessionDescription{
Origin: sdp.Origin{
Username: "-", SessionID: 1, SessionVersion: 1,
NetworkType: "IN", AddressType: "IP4", UnicastAddress: "0.0.0.0",
},
SessionName: sdp.SessionName(name),
ConnectionInformation: &sdp.ConnectionInformation{
NetworkType: "IN", AddressType: "IP4", Address: &sdp.Address{
Address: "0.0.0.0",
},
},
TimeDescriptions: []sdp.TimeDescription{
{Timing: sdp.Timing{}},
},
}
for _, media := range medias {
if media.Codecs == nil {
continue
}
codec := media.Codecs[0]
switch codec.Name {
case CodecELD:
name = CodecAAC
case CodecPCML:
name = CodecPCM // beacuse we using pcm.LittleToBig for RTSP server
default:
name = codec.Name
}
md := &sdp.MediaDescription{
MediaName: sdp.MediaName{
Media: media.Kind,
Protos: []string{"RTP", "AVP"},
},
}
md.WithCodec(codec.PayloadType, name, codec.ClockRate, uint16(codec.Channels), codec.FmtpLine)
if media.Direction != "" {
md.WithPropertyAttribute(media.Direction)
}
if media.ID != "" {
md.WithValueAttribute("control", media.ID)
}
sd.MediaDescriptions = append(sd.MediaDescriptions, md)
}
return sd.Marshal()
}
func UnmarshalMedia(md *sdp.MediaDescription) *Media {
m := &Media{
Kind: md.MediaName.Media,
}
for _, attr := range md.Attributes {
switch attr.Key {
case DirectionSendonly, DirectionRecvonly, DirectionSendRecv:
m.Direction = attr.Key
case "control", "mid":
m.ID = attr.Value
}
}
for _, format := range md.MediaName.Formats {
m.Codecs = append(m.Codecs, UnmarshalCodec(md, format))
}
return m
}
func ParseQuery(query map[string][]string) (medias []*Media) {
// set media candidates from query list
for key, values := range query {
switch key {
case KindVideo, KindAudio:
for _, value := range values {
media := &Media{Kind: key, Direction: DirectionSendonly}
for _, name := range strings.Split(value, ",") {
name = strings.ToUpper(name)
// check aliases
switch name {
case "", "COPY":
name = CodecAny
case "MJPEG":
name = CodecJPEG
case "AAC":
name = CodecAAC
case "MP3":
name = CodecMP3
}
media.Codecs = append(media.Codecs, &Codec{Name: name})
}
medias = append(medias, media)
}
}
}
return
}
@@ -0,0 +1,64 @@
package core
import (
"fmt"
"net/url"
"testing"
"github.com/pion/sdp/v3"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestSDP(t *testing.T) {
medias := []*Media{{
Kind: KindAudio, Direction: DirectionSendonly,
Codecs: []*Codec{
{Name: CodecPCMU, ClockRate: 8000},
},
}}
data, err := MarshalSDP("go2rtc/1.0.0", medias)
assert.Empty(t, err)
sd := &sdp.SessionDescription{}
err = sd.Unmarshal(data)
assert.Empty(t, err)
}
func TestParseQuery(t *testing.T) {
u, _ := url.Parse("rtsp://localhost:8554/camera1")
medias := ParseQuery(u.Query())
assert.Nil(t, medias)
for _, rawULR := range []string{
"rtsp://localhost:8554/camera1?video",
"rtsp://localhost:8554/camera1?video=copy",
"rtsp://localhost:8554/camera1?video=any",
} {
u, _ = url.Parse(rawULR)
medias = ParseQuery(u.Query())
assert.Equal(t, []*Media{
{Kind: KindVideo, Direction: DirectionSendonly, Codecs: []*Codec{{Name: CodecAny}}},
}, medias)
}
}
func TestClone(t *testing.T) {
media1 := &Media{
Kind: KindVideo,
Direction: DirectionRecvonly,
Codecs: []*Codec{
{Name: CodecPCMU, ClockRate: 8000},
},
}
media2 := media1.Clone()
p1 := fmt.Sprintf("%p", media1)
p2 := fmt.Sprintf("%p", media2)
require.NotEqualValues(t, p1, p2)
p3 := fmt.Sprintf("%p", media1.Codecs[0])
p4 := fmt.Sprintf("%p", media2.Codecs[0])
require.NotEqualValues(t, p3, p4)
}
+88
View File
@@ -0,0 +1,88 @@
package core
import (
"sync"
"github.com/pion/rtp"
)
//type Packet struct {
// Payload []byte
// Timestamp uint32 // PTS if DTS == 0 else DTS
// Composition uint32 // CTS = PTS-DTS (for support B-frames)
// Sequence uint16
//}
type Packet = rtp.Packet
// HandlerFunc - process input packets (just like http.HandlerFunc)
type HandlerFunc func(packet *Packet)
// Filter - a decorator for any HandlerFunc
type Filter func(handler HandlerFunc) HandlerFunc
// Node - Receiver or Sender or Filter (transform)
type Node struct {
Codec *Codec
Input HandlerFunc
Output HandlerFunc
id uint32
childs []*Node
parent *Node
mu sync.Mutex
}
func (n *Node) WithParent(parent *Node) *Node {
parent.AppendChild(n)
return n
}
func (n *Node) AppendChild(child *Node) {
n.mu.Lock()
n.childs = append(n.childs, child)
n.mu.Unlock()
child.parent = n
}
func (n *Node) RemoveChild(child *Node) {
n.mu.Lock()
for i, ch := range n.childs {
if ch == child {
n.childs = append(n.childs[:i], n.childs[i+1:]...)
break
}
}
n.mu.Unlock()
}
func (n *Node) Close() {
if parent := n.parent; parent != nil {
parent.RemoveChild(n)
if len(parent.childs) == 0 {
parent.Close()
}
} else {
for _, childs := range n.childs {
childs.Close()
}
}
}
func MoveNode(dst, src *Node) {
src.mu.Lock()
childs := src.childs
src.childs = nil
src.mu.Unlock()
dst.mu.Lock()
dst.childs = childs
dst.mu.Unlock()
for _, child := range childs {
child.parent = dst
}
}
@@ -0,0 +1,114 @@
package core
import (
"errors"
"io"
)
// ProbeSize
// in my tests MPEG-TS 40Mbit/s 4K-video require more than 1MB for probe
const ProbeSize = 5 * 1024 * 1024 // 5MB
const (
BufferDisable = 0
BufferDrainAndClear = -1
)
// ReadBuffer support buffering and Seek over buffer
// positive BufferSize will enable buffering mode
// Seek to negative offset will clear buffer
// Seek with a positive BufferSize will continue buffering after the last read from the buffer
// Seek with a negative BufferSize will clear buffer after the last read from the buffer
// Read more than BufferSize will raise error
type ReadBuffer struct {
io.Reader
BufferSize int
buf []byte
pos int
}
func NewReadBuffer(rd io.Reader) *ReadBuffer {
if rs, ok := rd.(*ReadBuffer); ok {
return rs
}
return &ReadBuffer{Reader: rd}
}
func (r *ReadBuffer) Read(p []byte) (n int, err error) {
// with zero buffer - read as usual
if r.BufferSize == BufferDisable {
return r.Reader.Read(p)
}
// if buffer not empty - read from it
if r.pos < len(r.buf) {
n = copy(p, r.buf[r.pos:])
r.pos += n
return
}
// with negative buffer - empty it and read as usual
if r.BufferSize < 0 {
r.BufferSize = BufferDisable
r.buf = nil
r.pos = 0
return r.Reader.Read(p)
}
n, err = r.Reader.Read(p)
if len(r.buf)+n > r.BufferSize {
return 0, errors.New("probe reader overflow")
}
r.buf = append(r.buf, p[:n]...)
r.pos += n
return
}
func (r *ReadBuffer) Close() error {
if closer, ok := r.Reader.(io.Closer); ok {
return closer.Close()
}
return nil
}
func (r *ReadBuffer) Seek(offset int64, whence int) (int64, error) {
var pos int
switch whence {
case io.SeekStart:
pos = int(offset)
case io.SeekCurrent:
pos = r.pos + int(offset)
case io.SeekEnd:
pos = len(r.buf) + int(offset)
}
// negative offset - empty buffer
if pos < 0 {
r.buf = nil
r.pos = 0
} else if pos >= len(r.buf) {
r.pos = len(r.buf)
} else {
r.pos = pos
}
return int64(r.pos), nil
}
func (r *ReadBuffer) Peek(n int) ([]byte, error) {
r.BufferSize = n
b := make([]byte, n)
if _, err := io.ReadAtLeast(r, b, n); err != nil {
return nil, err
}
r.Reset()
return b, nil
}
func (r *ReadBuffer) Reset() {
r.BufferSize = BufferDrainAndClear
r.pos = 0
}
@@ -0,0 +1,64 @@
package core
import (
"bytes"
"io"
"testing"
"github.com/stretchr/testify/require"
)
func TestReadSeeker(t *testing.T) {
b := []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
buf := bytes.NewReader(b)
rd := NewReadBuffer(buf)
rd.BufferSize = ProbeSize
// 1. Read to buffer
b = make([]byte, 3)
n, err := rd.Read(b)
require.Nil(t, err)
require.Equal(t, []byte{0, 1, 2}, b[:n])
// 2. Seek to start
_, err = rd.Seek(0, io.SeekStart)
require.Nil(t, err)
// 3. Read from buffer
b = make([]byte, 2)
n, err = rd.Read(b)
require.Nil(t, err)
require.Equal(t, []byte{0, 1}, b[:n])
// 4. Read from buffer
n, err = rd.Read(b)
require.Nil(t, err)
require.Equal(t, []byte{2}, b[:n])
// 5. Read to buffer
n, err = rd.Read(b)
require.Nil(t, err)
require.Equal(t, []byte{3, 4}, b[:n])
// 6. Seek to start
_, err = rd.Seek(0, io.SeekStart)
require.Nil(t, err)
// 7. Disable buffer
rd.BufferSize = -1
// 8. Read from buffer
b = make([]byte, 10)
n, err = rd.Read(b)
require.Nil(t, err)
require.Equal(t, []byte{0, 1, 2, 3, 4}, b[:n])
// 9. Direct read
n, err = rd.Read(b)
require.Nil(t, err)
require.Equal(t, []byte{5, 6, 7, 8, 9}, b[:n])
// 10. Check buffer empty
require.Nil(t, rd.buf)
}
@@ -0,0 +1,43 @@
package core
// This code copied from go1.21 for backward support in go1.20.
// We need to support go1.20 for Windows 7
// Index returns the index of the first occurrence of v in s,
// or -1 if not present.
func Index[S ~[]E, E comparable](s S, v E) int {
for i := range s {
if v == s[i] {
return i
}
}
return -1
}
// Contains reports whether v is present in s.
func Contains[S ~[]E, E comparable](s S, v E) bool {
return Index(s, v) >= 0
}
type Ordered interface {
~int | ~int8 | ~int16 | ~int32 | ~int64 |
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr |
~float32 | ~float64 |
~string
}
// Max returns the maximal value in x. It panics if x is empty.
// For floating-point E, Max propagates NaNs (any NaN value in x
// forces the output to be NaN).
func Max[S ~[]E, E Ordered](x S) E {
if len(x) < 1 {
panic("slices.Max: empty list")
}
m := x[0]
for i := 1; i < len(x); i++ {
if x[i] > m {
m = x[i]
}
}
return m
}
+217
View File
@@ -0,0 +1,217 @@
package core
import (
"encoding/json"
"errors"
"github.com/pion/rtp"
)
var ErrCantGetTrack = errors.New("can't get track")
type Receiver struct {
Node
// Deprecated: should be removed
Media *Media `json:"-"`
// Deprecated: should be removed
ID byte `json:"-"` // Channel for RTSP, PayloadType for MPEG-TS
Bytes int `json:"bytes,omitempty"`
Packets int `json:"packets,omitempty"`
}
func NewReceiver(media *Media, codec *Codec) *Receiver {
r := &Receiver{
Node: Node{id: NewID(), Codec: codec},
Media: media,
}
r.Input = func(packet *Packet) {
r.Bytes += len(packet.Payload)
r.Packets++
for _, child := range r.childs {
child.Input(packet)
}
}
return r
}
// Deprecated: should be removed
func (r *Receiver) WriteRTP(packet *rtp.Packet) {
r.Input(packet)
}
// Deprecated: should be removed
func (r *Receiver) Senders() []*Sender {
if len(r.childs) > 0 {
return []*Sender{{}}
} else {
return nil
}
}
// Deprecated: should be removed
func (r *Receiver) Replace(target *Receiver) {
MoveNode(&target.Node, &r.Node)
}
func (r *Receiver) Close() {
r.Node.Close()
}
type Sender struct {
Node
// Deprecated:
Media *Media `json:"-"`
// Deprecated:
Handler HandlerFunc `json:"-"`
Bytes int `json:"bytes,omitempty"`
Packets int `json:"packets,omitempty"`
Drops int `json:"drops,omitempty"`
buf chan *Packet
done chan struct{}
}
func NewSender(media *Media, codec *Codec) *Sender {
var bufSize uint16
if GetKind(codec.Name) == KindVideo {
if codec.IsRTP() {
// in my tests 40Mbit/s 4K-video can generate up to 1500 items
// for the h264.RTPDepay => RTPPay queue
bufSize = 4096
} else {
bufSize = 64
}
} else {
bufSize = 128
}
buf := make(chan *Packet, bufSize)
s := &Sender{
Node: Node{id: NewID(), Codec: codec},
Media: media,
buf: buf,
}
s.Input = func(packet *Packet) {
s.mu.Lock()
// unblock write to nil chan - OK, write to closed chan - panic
select {
case s.buf <- packet:
s.Bytes += len(packet.Payload)
s.Packets++
default:
s.Drops++
}
s.mu.Unlock()
}
s.Output = func(packet *Packet) {
s.Handler(packet)
}
return s
}
// Deprecated: should be removed
func (s *Sender) HandleRTP(parent *Receiver) {
s.WithParent(parent)
s.Start()
}
// Deprecated: should be removed
func (s *Sender) Bind(parent *Receiver) {
s.WithParent(parent)
}
func (s *Sender) WithParent(parent *Receiver) *Sender {
s.Node.WithParent(&parent.Node)
return s
}
func (s *Sender) Start() {
s.mu.Lock()
defer s.mu.Unlock()
if s.buf == nil || s.done != nil {
return
}
s.done = make(chan struct{})
// pass buf directly so that it's impossible for buf to be nil
go func(buf chan *Packet) {
for packet := range buf {
s.Output(packet)
}
close(s.done)
}(s.buf)
}
func (s *Sender) Wait() {
if done := s.done; done != nil {
<-done
}
}
func (s *Sender) State() string {
if s.buf == nil {
return "closed"
}
if s.done == nil {
return "new"
}
return "connected"
}
func (s *Sender) Close() {
// close buffer if exists
s.mu.Lock()
if s.buf != nil {
close(s.buf) // exit from for range loop
s.buf = nil // prevent writing to closed chan
}
s.mu.Unlock()
s.Node.Close()
}
func (r *Receiver) MarshalJSON() ([]byte, error) {
v := struct {
ID uint32 `json:"id"`
Codec *Codec `json:"codec"`
Childs []uint32 `json:"childs,omitempty"`
Bytes int `json:"bytes,omitempty"`
Packets int `json:"packets,omitempty"`
}{
ID: r.Node.id,
Codec: r.Node.Codec,
Bytes: r.Bytes,
Packets: r.Packets,
}
for _, child := range r.childs {
v.Childs = append(v.Childs, child.id)
}
return json.Marshal(v)
}
func (s *Sender) MarshalJSON() ([]byte, error) {
v := struct {
ID uint32 `json:"id"`
Codec *Codec `json:"codec"`
Parent uint32 `json:"parent,omitempty"`
Bytes int `json:"bytes,omitempty"`
Packets int `json:"packets,omitempty"`
Drops int `json:"drops,omitempty"`
}{
ID: s.Node.id,
Codec: s.Node.Codec,
Bytes: s.Bytes,
Packets: s.Packets,
Drops: s.Drops,
}
if s.parent != nil {
v.Parent = s.parent.id
}
return json.Marshal(v)
}
@@ -0,0 +1,53 @@
package core
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestSenser(t *testing.T) {
recv := make(chan *Packet) // blocking receiver
sender := NewSender(nil, &Codec{})
sender.Output = func(packet *Packet) {
recv <- packet
}
require.Equal(t, "new", sender.State())
sender.Start()
require.Equal(t, "connected", sender.State())
sender.Input(&Packet{})
sender.Input(&Packet{})
require.Equal(t, 2, sender.Packets)
require.Equal(t, 0, sender.Drops)
// important to read one before close
// because goroutine in Start() can run with nil chan
// it's OK in real life, but bad for test
_, ok := <-recv
require.True(t, ok)
sender.Close()
require.Equal(t, "closed", sender.State())
sender.Input(&Packet{})
require.Equal(t, 2, sender.Packets)
require.Equal(t, 1, sender.Drops)
// read 2nd
_, ok = <-recv
require.True(t, ok)
// read 3rd
select {
case <-recv:
ok = true
default:
ok = false
}
require.False(t, ok)
}
@@ -0,0 +1,74 @@
package core
import (
"sync"
)
// Waiter support:
// - autotart on first Wait
// - block new waiters after last Done
// - safe Done after finish
type Waiter struct {
sync.WaitGroup
mu sync.Mutex
state int // state < 0 means finish
err error
}
func (w *Waiter) Add(delta int) {
w.mu.Lock()
if w.state >= 0 {
w.state += delta
w.WaitGroup.Add(delta)
}
w.mu.Unlock()
}
func (w *Waiter) Wait() error {
w.mu.Lock()
// first wait auto start waiter
if w.state == 0 {
w.state++
w.WaitGroup.Add(1)
}
w.mu.Unlock()
w.WaitGroup.Wait()
return w.err
}
func (w *Waiter) Done(err error) {
w.mu.Lock()
// safe run Done only when have tasks
if w.state > 0 {
w.state--
w.WaitGroup.Done()
}
// block waiter for any operations after last done
if w.state == 0 {
w.state = -1
w.err = err
}
w.mu.Unlock()
}
func (w *Waiter) WaitChan() <-chan error {
var ch chan error
w.mu.Lock()
if w.state >= 0 {
ch = make(chan error)
go func() {
ch <- w.Wait()
}()
}
w.mu.Unlock()
return ch
}
@@ -0,0 +1,52 @@
package core
import (
"time"
)
type Worker struct {
timer *time.Timer
done chan struct{}
}
// NewWorker run f after d
func NewWorker(d time.Duration, f func() time.Duration) *Worker {
timer := time.NewTimer(d)
done := make(chan struct{})
go func() {
for {
select {
case <-timer.C:
if d = f(); d > 0 {
timer.Reset(d)
continue
}
case <-done:
timer.Stop()
}
break
}
}()
return &Worker{timer: timer, done: done}
}
// Do - instant timer run
func (w *Worker) Do() {
if w == nil {
return
}
w.timer.Reset(0)
}
func (w *Worker) Stop() {
if w == nil {
return
}
select {
case w.done <- struct{}{}:
default:
}
}
@@ -0,0 +1,114 @@
package core
import (
"bytes"
"io"
"net/http"
"sync"
)
// WriteBuffer by defaul Write(s) to bytes.Buffer.
// But after WriteTo to new io.Writer - calls Reset.
// Reset will flush current buffer data to new writer and starts to Write to new io.Writer
// WriteTo will be locked until Write fails or Close will be called.
type WriteBuffer struct {
io.Writer
err error
mu sync.Mutex
wg sync.WaitGroup
state byte
}
func NewWriteBuffer(wr io.Writer) *WriteBuffer {
if wr == nil {
wr = bytes.NewBuffer(nil)
}
return &WriteBuffer{Writer: wr}
}
func (w *WriteBuffer) Write(p []byte) (n int, err error) {
w.mu.Lock()
if w.err != nil {
err = w.err
} else if n, err = w.Writer.Write(p); err != nil {
w.err = err
w.done()
} else if f, ok := w.Writer.(http.Flusher); ok {
f.Flush()
}
w.mu.Unlock()
return
}
func (w *WriteBuffer) WriteTo(wr io.Writer) (n int64, err error) {
w.Reset(wr)
w.wg.Wait()
return 0, w.err // TODO: fix counter
}
func (w *WriteBuffer) Close() error {
if closer, ok := w.Writer.(io.Closer); ok {
return closer.Close()
}
w.mu.Lock()
w.done()
w.mu.Unlock()
return nil
}
func (w *WriteBuffer) Reset(wr io.Writer) {
w.mu.Lock()
w.add()
if buf, ok := w.Writer.(*bytes.Buffer); ok && buf.Len() != 0 {
if _, err := io.Copy(wr, buf); err != nil {
w.err = err
w.done()
}
}
w.Writer = wr
w.mu.Unlock()
}
const (
none = iota
start
end
)
func (w *WriteBuffer) add() {
if w.state == none {
w.state = start
w.wg.Add(1)
}
}
func (w *WriteBuffer) done() {
if w.state == start {
w.state = end
w.wg.Done()
}
}
// OnceBuffer will catch only first message
type OnceBuffer struct {
buf []byte
}
func (o *OnceBuffer) Write(p []byte) (n int, err error) {
if o.buf == nil {
o.buf = p
}
return 0, io.EOF
}
func (o *OnceBuffer) WriteTo(w io.Writer) (n int64, err error) {
return io.Copy(w, bytes.NewReader(o.buf))
}
func (o *OnceBuffer) Buffer() []byte {
return o.buf
}
func (o *OnceBuffer) Len() int {
return len(o.buf)
}