BIG core logic rewrite
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"github.com/pion/sdp/v3"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
type Codec struct {
|
||||
Name string // H264, PCMU, PCMA, opus...
|
||||
ClockRate uint32 // 90000, 8000, 16000...
|
||||
Channels uint16 // 0, 1, 2
|
||||
FmtpLine string
|
||||
PayloadType uint8
|
||||
}
|
||||
|
||||
func (c *Codec) String() string {
|
||||
s := fmt.Sprintf("%d %s", c.PayloadType, c.Name)
|
||||
if c.ClockRate != 0 && c.ClockRate != 90000 {
|
||||
s = fmt.Sprintf("%s/%d", s, c.ClockRate)
|
||||
}
|
||||
if c.Channels > 0 {
|
||||
s = fmt.Sprintf("%s/%d", s, c.Channels)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (c *Codec) Text() string {
|
||||
switch c.Name {
|
||||
case CodecH264:
|
||||
if profile := DecodeH264(c.FmtpLine); profile != "" {
|
||||
return "H.264 " + profile
|
||||
}
|
||||
return c.Name
|
||||
}
|
||||
|
||||
s := c.Name
|
||||
if c.ClockRate != 0 && c.ClockRate != 90000 {
|
||||
s += "/" + strconv.Itoa(int(c.ClockRate))
|
||||
}
|
||||
if c.Channels > 0 {
|
||||
s += "/" + strconv.Itoa(int(c.Channels))
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (c *Codec) IsRTP() bool {
|
||||
return c.PayloadType != PayloadTypeRAW
|
||||
}
|
||||
|
||||
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 "14":
|
||||
c.Name = CodecMP3
|
||||
c.ClockRate = 44100
|
||||
case "26":
|
||||
c.Name = CodecJPEG
|
||||
c.ClockRate = 90000
|
||||
default:
|
||||
c.Name = payloadType
|
||||
}
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
func atoi(s string) (i int) {
|
||||
i, _ = strconv.Atoi(s)
|
||||
return
|
||||
}
|
||||
|
||||
func DecodeH264(fmtp string) string {
|
||||
if ps := Between(fmtp, "sprop-parameter-sets=", ","); ps != "" {
|
||||
if sps, _ := base64.StdEncoding.DecodeString(ps); len(sps) >= 4 {
|
||||
var profile string
|
||||
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])
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s %d.%d", profile, sps[3]/10, sps[3]%10)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package core
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
CodecELD = "ELD" // AAC-ELD
|
||||
|
||||
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)
|
||||
|
||||
Start() error
|
||||
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
|
||||
|
||||
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"
|
||||
}
|
||||
|
||||
type Info struct {
|
||||
Type string `json:"type,omitempty"`
|
||||
URL string `json:"url,omitempty"`
|
||||
RemoteAddr string `json:"remote_addr,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:"recv,omitempty"`
|
||||
Send int `json:"send,omitempty"`
|
||||
}
|
||||
|
||||
const (
|
||||
UnsupportedCodec = "unsupported codec"
|
||||
WrongMediaDirection = "wrong media direction"
|
||||
)
|
||||
@@ -2,6 +2,10 @@ package core
|
||||
|
||||
import (
|
||||
cryptorand "crypto/rand"
|
||||
"github.com/rs/zerolog/log"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const digits = "0123456789abcdefghijklmnopqrstuvwxyz"
|
||||
@@ -17,3 +21,35 @@ func RandString(size byte) string {
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func Between(s, sub1, sub2 string) string {
|
||||
i := strings.Index(s, sub1)
|
||||
if i < 0 {
|
||||
return ""
|
||||
}
|
||||
s = s[i+len(sub1):]
|
||||
|
||||
if len(sub2) == 1 {
|
||||
i = strings.IndexByte(s, sub2[0])
|
||||
} else {
|
||||
i = strings.Index(s, sub2)
|
||||
}
|
||||
if i >= 0 {
|
||||
return s[:i]
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func Assert(ok bool) {
|
||||
if !ok {
|
||||
_, file, line, _ := runtime.Caller(1)
|
||||
panic(file + ":" + strconv.Itoa(line))
|
||||
}
|
||||
}
|
||||
|
||||
func Caller() string {
|
||||
log.Error().Caller(0).Send()
|
||||
_, 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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/pion/sdp/v3"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// 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.Text()
|
||||
|
||||
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 GetKind(name string) string {
|
||||
switch name {
|
||||
case CodecH264, CodecH265, CodecVP8, CodecVP9, CodecAV1, CodecJPEG:
|
||||
return KindVideo
|
||||
case CodecPCMU, CodecPCMA, CodecAAC, CodecOpus, CodecG722, CodecMP3, CodecELD:
|
||||
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]
|
||||
|
||||
name := codec.Name
|
||||
if name == CodecELD {
|
||||
name = CodecAAC
|
||||
}
|
||||
|
||||
md := &sdp.MediaDescription{
|
||||
MediaName: sdp.MediaName{
|
||||
Media: media.Kind,
|
||||
Protos: []string{"RTP", "AVP"},
|
||||
},
|
||||
}
|
||||
md.WithCodec(codec.PayloadType, name, codec.ClockRate, codec.Channels, codec.FmtpLine)
|
||||
|
||||
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,63 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/pion/sdp/v3"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"net/url"
|
||||
"testing"
|
||||
)
|
||||
|
||||
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: DirectionRecvonly, 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)
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package core
|
||||
|
||||
import "time"
|
||||
|
||||
type Probe struct {
|
||||
deadline time.Time
|
||||
items map[any]struct{}
|
||||
}
|
||||
|
||||
func NewProbe(enable bool) *Probe {
|
||||
if enable {
|
||||
return &Probe{
|
||||
deadline: time.Now().Add(time.Second * 3),
|
||||
items: map[any]struct{}{},
|
||||
}
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Active return true if probe enabled and not finish
|
||||
func (p *Probe) Active() bool {
|
||||
return len(p.items) < 2 && time.Now().Before(p.deadline)
|
||||
}
|
||||
|
||||
// Append safe to run if Probe is nil
|
||||
func (p *Probe) Append(v any) {
|
||||
if p != nil {
|
||||
p.items[v] = struct{}{}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/pion/rtp"
|
||||
"strconv"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var ErrCantGetTrack = errors.New("can't get track")
|
||||
|
||||
type Receiver struct {
|
||||
Codec *Codec
|
||||
Media *Media
|
||||
|
||||
ID byte // Channel for RTSP, PayloadType for MPEG-TS
|
||||
|
||||
senders map[*Sender]chan *rtp.Packet
|
||||
mu sync.Mutex
|
||||
bytes int
|
||||
}
|
||||
|
||||
func NewReceiver(media *Media, codec *Codec) *Receiver {
|
||||
Assert(codec != nil)
|
||||
return &Receiver{Codec: codec, Media: media}
|
||||
}
|
||||
|
||||
// WriteRTP - fast and non blocking write to all readers buffers
|
||||
func (t *Receiver) WriteRTP(packet *rtp.Packet) {
|
||||
t.mu.Lock()
|
||||
t.bytes += len(packet.Payload)
|
||||
for sender, buffer := range t.senders {
|
||||
if len(buffer) < cap(buffer) {
|
||||
buffer <- packet
|
||||
} else {
|
||||
sender.overflow++
|
||||
}
|
||||
}
|
||||
t.mu.Unlock()
|
||||
}
|
||||
|
||||
func (t *Receiver) Senders() (senders []*Sender) {
|
||||
t.mu.Lock()
|
||||
for sender := range t.senders {
|
||||
senders = append(senders, sender)
|
||||
}
|
||||
t.mu.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
func (t *Receiver) Close() {
|
||||
t.mu.Lock()
|
||||
// close all sender channel buffers and erase senders list
|
||||
for _, buffer := range t.senders {
|
||||
close(buffer)
|
||||
}
|
||||
t.senders = nil
|
||||
t.mu.Unlock()
|
||||
}
|
||||
|
||||
func (t *Receiver) Replace(target *Receiver) {
|
||||
// move this receiver senders to new receiver
|
||||
t.mu.Lock()
|
||||
senders := t.senders
|
||||
t.mu.Unlock()
|
||||
|
||||
target.mu.Lock()
|
||||
target.senders = senders
|
||||
target.mu.Unlock()
|
||||
}
|
||||
|
||||
func (t *Receiver) String() string {
|
||||
s := t.Codec.String() + ", bytes=" + strconv.Itoa(t.bytes)
|
||||
if t.mu.TryLock() {
|
||||
s += fmt.Sprintf(", senders=%d", len(t.senders))
|
||||
t.mu.Unlock()
|
||||
} else {
|
||||
s += fmt.Sprintf(", senders=?")
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (t *Receiver) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(t.String())
|
||||
}
|
||||
|
||||
type Sender struct {
|
||||
Codec *Codec
|
||||
Media *Media
|
||||
|
||||
Handler HandlerFunc
|
||||
|
||||
receivers []*Receiver
|
||||
mu sync.Mutex
|
||||
bytes int
|
||||
|
||||
overflow int
|
||||
}
|
||||
|
||||
func NewSender(media *Media, codec *Codec) *Sender {
|
||||
return &Sender{Codec: codec, Media: media}
|
||||
}
|
||||
|
||||
// HandlerFunc like http.HandlerFunc
|
||||
type HandlerFunc func(packet *rtp.Packet)
|
||||
|
||||
func (s *Sender) HandleRTP(track *Receiver) {
|
||||
bufferSize := 100
|
||||
|
||||
if GetKind(track.Codec.Name) == KindVideo {
|
||||
if track.Codec.IsRTP() {
|
||||
// H.264 2560x1440 4096kbs can have 700+ packets between 25 frames
|
||||
// H.265 5120x1440 can have 700+ packets between two keyframes
|
||||
bufferSize = 1000
|
||||
} else {
|
||||
bufferSize = 50
|
||||
}
|
||||
}
|
||||
|
||||
buffer := make(chan *rtp.Packet, bufferSize)
|
||||
|
||||
track.mu.Lock()
|
||||
if track.senders == nil {
|
||||
track.senders = map[*Sender]chan *rtp.Packet{}
|
||||
}
|
||||
track.senders[s] = buffer
|
||||
track.mu.Unlock()
|
||||
|
||||
s.mu.Lock()
|
||||
s.receivers = append(s.receivers, track)
|
||||
s.mu.Unlock()
|
||||
|
||||
go func() {
|
||||
// read packets from buffer channel until it will be closed
|
||||
for packet := range buffer {
|
||||
s.bytes += len(packet.Payload)
|
||||
s.Handler(packet)
|
||||
}
|
||||
|
||||
// remove current receiver from list
|
||||
// it can only happen when receiver close buffer channel
|
||||
s.mu.Lock()
|
||||
for i, receiver := range s.receivers {
|
||||
if receiver == track {
|
||||
s.receivers = append(s.receivers[:i], s.receivers[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
s.mu.Unlock()
|
||||
}()
|
||||
}
|
||||
|
||||
func (s *Sender) Close() {
|
||||
s.mu.Lock()
|
||||
// remove this sender from all receivers list
|
||||
for _, receiver := range s.receivers {
|
||||
receiver.mu.Lock()
|
||||
if buffer := receiver.senders[s]; buffer != nil {
|
||||
// remove channel from list
|
||||
delete(receiver.senders, s)
|
||||
// close channel
|
||||
close(buffer)
|
||||
}
|
||||
receiver.mu.Unlock()
|
||||
}
|
||||
s.receivers = nil
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
func (s *Sender) String() string {
|
||||
info := s.Codec.String() + ", bytes=" + strconv.Itoa(s.bytes)
|
||||
if s.mu.TryLock() {
|
||||
info += ", receivers=" + strconv.Itoa(len(s.receivers))
|
||||
s.mu.Unlock()
|
||||
} else {
|
||||
info += ", receivers=?"
|
||||
}
|
||||
if s.overflow > 0 {
|
||||
info += ", overflow=" + strconv.Itoa(s.overflow)
|
||||
}
|
||||
return info
|
||||
}
|
||||
|
||||
func (s *Sender) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(s.String())
|
||||
}
|
||||
Reference in New Issue
Block a user