BIG core logic rewrite

This commit is contained in:
Alexey Khit
2023-03-17 06:48:02 +03:00
parent 2146ea470b
commit 12a7b96289
107 changed files with 3000 additions and 3024 deletions

View File

@@ -3,7 +3,7 @@ package debug
import (
"github.com/AlexxIT/go2rtc/cmd/api"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/AlexxIT/go2rtc/pkg/core"
)
func Init() {
@@ -12,6 +12,6 @@ func Init() {
streams.HandleFunc("null", nullHandler)
}
func nullHandler(string) (streamer.Producer, error) {
func nullHandler(string) (core.Producer, error) {
return nil, nil
}

View File

@@ -2,15 +2,15 @@ package dvrip
import (
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/dvrip"
"github.com/AlexxIT/go2rtc/pkg/streamer"
)
func Init() {
streams.HandleFunc("dvrip", handle)
}
func handle(url string) (streamer.Producer, error) {
func handle(url string) (core.Producer, error) {
conn := dvrip.NewClient(url)
if err := conn.Dial(); err != nil {
return nil, err

View File

@@ -4,15 +4,15 @@ import (
"bytes"
"github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/shell"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"os/exec"
)
func Init() {
log := app.GetLogger("echo")
streams.HandleFunc("echo", func(url string) (streamer.Producer, error) {
streams.HandleFunc("echo", func(url string) (core.Producer, error) {
args := shell.QuoteSplit(url[5:])
b, err := exec.Command(args[0], args[1:]...).Output()

View File

@@ -8,9 +8,9 @@ import (
"github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/cmd/rtsp"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
pkg "github.com/AlexxIT/go2rtc/pkg/rtsp"
"github.com/AlexxIT/go2rtc/pkg/shell"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/rs/zerolog"
"os"
"os/exec"
@@ -48,7 +48,7 @@ func Init() {
log = app.GetLogger("exec")
}
func Handle(url string) (streamer.Producer, error) {
func Handle(url string) (core.Producer, error) {
sum := md5.Sum([]byte(url))
path := "/" + hex.EncodeToString(sum[:])
@@ -67,7 +67,7 @@ func Handle(url string) (streamer.Producer, error) {
cmd.Stderr = os.Stderr
}
ch := make(chan streamer.Producer)
ch := make(chan core.Producer)
waitersMu.Lock()
waiters[path] = ch
@@ -116,5 +116,5 @@ func Handle(url string) (streamer.Producer, error) {
// internal
var log zerolog.Logger
var waiters = map[string]chan streamer.Producer{}
var waiters = map[string]chan core.Producer{}
var waitersMu sync.Mutex

View File

@@ -2,7 +2,7 @@ package device
import (
"bytes"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/AlexxIT/go2rtc/pkg/core"
"os/exec"
"strings"
)
@@ -11,15 +11,15 @@ import (
const deviceInputPrefix = "-f avfoundation"
func deviceInputSuffix(videoIdx, audioIdx int) string {
video := findMedia(streamer.KindVideo, videoIdx)
audio := findMedia(streamer.KindAudio, audioIdx)
video := findMedia(core.KindVideo, videoIdx)
audio := findMedia(core.KindAudio, audioIdx)
switch {
case video != nil && audio != nil:
return `"` + video.MID + `:` + audio.MID + `"`
return `"` + video.ID + `:` + audio.ID + `"`
case video != nil:
return `"` + video.MID + `"`
return `"` + video.ID + `"`
case audio != nil:
return `"` + audio.MID + `"`
return `"` + audio.ID + `"`
}
return ""
}
@@ -40,10 +40,10 @@ process:
for _, line := range lines {
switch {
case strings.HasSuffix(line, "video devices:"):
kind = streamer.KindVideo
kind = core.KindVideo
continue
case strings.HasSuffix(line, "audio devices:"):
kind = streamer.KindAudio
kind = core.KindAudio
continue
case strings.HasPrefix(line, "dummy"):
break process
@@ -56,6 +56,6 @@ process:
}
}
func loadMedia(kind, name string) *streamer.Media {
return &streamer.Media{Kind: kind, MID: name}
func loadMedia(kind, name string) *core.Media {
return &core.Media{Kind: kind, ID: name}
}

View File

@@ -2,7 +2,7 @@ package device
import (
"bytes"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/AlexxIT/go2rtc/pkg/core"
"io/ioutil"
"os/exec"
"strings"
@@ -12,8 +12,8 @@ import (
const deviceInputPrefix = "-f v4l2"
func deviceInputSuffix(videoIdx, audioIdx int) string {
video := findMedia(streamer.KindVideo, videoIdx)
return video.MID
video := findMedia(core.KindVideo, videoIdx)
return video.ID
}
func loadMedias() {
@@ -23,8 +23,8 @@ func loadMedias() {
}
for _, file := range files {
log.Trace().Msg("[ffmpeg] " + file.Name())
if strings.HasPrefix(file.Name(), streamer.KindVideo) {
media := loadMedia(streamer.KindVideo, "/dev/"+file.Name())
if strings.HasPrefix(file.Name(), core.KindVideo) {
media := loadMedia(core.KindVideo, "/dev/"+file.Name())
if media != nil {
medias = append(medias, media)
}
@@ -32,7 +32,7 @@ func loadMedias() {
}
}
func loadMedia(kind, name string) *streamer.Media {
func loadMedia(kind, name string) *core.Media {
cmd := exec.Command(
Bin, "-hide_banner", "-f", "v4l2", "-list_formats", "all", "-i", name,
)
@@ -44,5 +44,5 @@ func loadMedia(kind, name string) *streamer.Media {
return nil
}
return &streamer.Media{Kind: kind, MID: name}
return &core.Media{Kind: kind, ID: name}
}

View File

@@ -2,7 +2,7 @@ package device
import (
"bytes"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/AlexxIT/go2rtc/pkg/core"
"os/exec"
"strings"
)
@@ -11,15 +11,15 @@ import (
const deviceInputPrefix = "-f dshow"
func deviceInputSuffix(videoIdx, audioIdx int) string {
video := findMedia(streamer.KindVideo, videoIdx)
audio := findMedia(streamer.KindAudio, audioIdx)
video := findMedia(core.KindVideo, videoIdx)
audio := findMedia(core.KindAudio, audioIdx)
switch {
case video != nil && audio != nil:
return `video="` + video.MID + `":audio=` + audio.MID + `"`
return `video="` + video.ID + `":audio=` + audio.ID + `"`
case video != nil:
return `video="` + video.MID + `"`
return `video="` + video.ID + `"`
case audio != nil:
return `audio="` + audio.MID + `"`
return `audio="` + audio.ID + `"`
}
return ""
}
@@ -37,9 +37,9 @@ func loadMedias() {
for _, line := range lines {
var kind string
if strings.HasSuffix(line, "(video)") {
kind = streamer.KindVideo
kind = core.KindVideo
} else if strings.HasSuffix(line, "(audio)") {
kind = streamer.KindAudio
kind = core.KindAudio
} else {
continue
}
@@ -52,6 +52,6 @@ func loadMedias() {
}
}
func loadMedia(kind, name string) *streamer.Media {
return &streamer.Media{Kind: kind, MID: name}
func loadMedia(kind, name string) *core.Media {
return &core.Media{Kind: kind, ID: name}
}

View File

@@ -4,7 +4,7 @@ import (
"encoding/json"
"github.com/AlexxIT/go2rtc/cmd/api"
"github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/rs/zerolog"
"net/http"
"net/url"
@@ -52,9 +52,9 @@ func GetInput(src string) (string, error) {
var Bin string
var log zerolog.Logger
var medias []*streamer.Media
var medias []*core.Media
func findMedia(kind string, index int) *streamer.Media {
func findMedia(kind string, index int) *core.Media {
for _, media := range medias {
if media.Kind != kind {
continue

View File

@@ -8,7 +8,7 @@ import (
"github.com/AlexxIT/go2rtc/cmd/ffmpeg/device"
"github.com/AlexxIT/go2rtc/cmd/rtsp"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/AlexxIT/go2rtc/pkg/core"
"net/url"
"strconv"
"strings"
@@ -27,7 +27,7 @@ func Init() {
defaults["global"] += " -v error"
}
streams.HandleFunc("ffmpeg", func(url string) (streamer.Producer, error) {
streams.HandleFunc("ffmpeg", func(url string) (core.Producer, error) {
args := parseArgs(url[7:]) // remove `ffmpeg:`
if args == nil {
return nil, errors.New("can't generate ffmpeg command")

View File

@@ -5,7 +5,7 @@ import (
"fmt"
"github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/rs/zerolog"
"os"
"path"
@@ -38,7 +38,7 @@ func Init() {
urls := map[string]string{}
streams.HandleFunc("hass", func(url string) (streamer.Producer, error) {
streams.HandleFunc("hass", func(url string) (core.Producer, error) {
if hurl := urls[url[5:]]; hurl != "" {
return streams.GetProducer(hurl)
}

View File

@@ -4,9 +4,9 @@ import (
"fmt"
"github.com/AlexxIT/go2rtc/cmd/api"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/mp4"
"github.com/AlexxIT/go2rtc/pkg/mpegts"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/rs/zerolog/log"
"net/http"
"strconv"
@@ -27,7 +27,7 @@ func Init() {
}
type Consumer interface {
streamer.Consumer
core.Consumer
Init() ([]byte, error)
MimeCodecs() string
Start()

View File

@@ -5,8 +5,8 @@ import (
"github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/cmd/srtp"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/homekit"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/rs/zerolog"
)
@@ -20,12 +20,12 @@ func Init() {
var log zerolog.Logger
func streamHandler(url string) (streamer.Producer, error) {
func streamHandler(url string) (core.Producer, error) {
conn, err := homekit.NewClient(url, srtp.Server)
if err != nil {
return nil, err
}
if err = conn.Dial();err!=nil{
if err = conn.Dial(); err != nil {
return nil, err
}
return conn, nil

View File

@@ -4,10 +4,10 @@ import (
"errors"
"fmt"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/mjpeg"
"github.com/AlexxIT/go2rtc/pkg/mpegts"
"github.com/AlexxIT/go2rtc/pkg/rtmp"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/AlexxIT/go2rtc/pkg/tcp"
"net/http"
"strings"
@@ -18,7 +18,7 @@ func Init() {
streams.HandleFunc("https", handle)
}
func handle(url string) (streamer.Producer, error) {
func handle(url string) (core.Producer, error) {
// first we get the Content-Type to define supported producer
req, err := http.NewRequest("GET", url, nil)
if err != nil {

View File

@@ -2,15 +2,15 @@ package isapi
import (
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/isapi"
"github.com/AlexxIT/go2rtc/pkg/streamer"
)
func Init() {
streams.HandleFunc("isapi", handle)
}
func handle(url string) (streamer.Producer, error) {
func handle(url string) (core.Producer, error) {
conn, err := isapi.NewClient(url)
if err != nil {
return nil, err

View File

@@ -2,13 +2,13 @@ package ivideon
import (
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/ivideon"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"strings"
)
func Init() {
streams.HandleFunc("ivideon", func(url string) (streamer.Producer, error) {
streams.HandleFunc("ivideon", func(url string) (core.Producer, error) {
id := strings.Replace(url[8:], "/", ":", 1)
prod := ivideon.NewClient(id)
if err := prod.Dial(); err != nil {

View File

@@ -4,8 +4,8 @@ import (
"github.com/AlexxIT/go2rtc/cmd/api"
"github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/mp4"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/rs/zerolog"
"net/http"
"strconv"
@@ -105,10 +105,10 @@ func handlerMP4(w http.ResponseWriter, r *http.Request) {
cons := &mp4.Consumer{
RemoteAddr: r.RemoteAddr,
UserAgent: r.UserAgent(),
Medias: streamer.ParseQuery(r.URL.Query()),
Medias: core.ParseQuery(r.URL.Query()),
}
cons.Listen(func(msg interface{}) {
cons.Listen(func(msg any) {
if data, ok := msg.([]byte); ok {
if _, err := w.Write(data); err != nil && exit != nil {
exit <- err

View File

@@ -4,8 +4,8 @@ import (
"errors"
"github.com/AlexxIT/go2rtc/cmd/api"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/mp4"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"strings"
)
@@ -94,40 +94,40 @@ func handlerWSMP4(tr *api.Transport, msg *api.Message) error {
return nil
}
func parseMedias(codecs string, parseAudio bool) (medias []*streamer.Media) {
var videos []*streamer.Codec
var audios []*streamer.Codec
func parseMedias(codecs string, parseAudio bool) (medias []*core.Media) {
var videos []*core.Codec
var audios []*core.Codec
for _, name := range strings.Split(codecs, ",") {
switch name {
case mp4.MimeH264:
codec := &streamer.Codec{Name: streamer.CodecH264}
codec := &core.Codec{Name: core.CodecH264}
videos = append(videos, codec)
case mp4.MimeH265:
codec := &streamer.Codec{Name: streamer.CodecH265}
codec := &core.Codec{Name: core.CodecH265}
videos = append(videos, codec)
case mp4.MimeAAC:
codec := &streamer.Codec{Name: streamer.CodecAAC}
codec := &core.Codec{Name: core.CodecAAC}
audios = append(audios, codec)
case mp4.MimeOpus:
codec := &streamer.Codec{Name: streamer.CodecOpus}
codec := &core.Codec{Name: core.CodecOpus}
audios = append(audios, codec)
}
}
if videos != nil {
media := &streamer.Media{
Kind: streamer.KindVideo,
Direction: streamer.DirectionRecvonly,
media := &core.Media{
Kind: core.KindVideo,
Direction: core.DirectionSendonly,
Codecs: videos,
}
medias = append(medias, media)
}
if audios != nil && parseAudio {
media := &streamer.Media{
Kind: streamer.KindAudio,
Direction: streamer.DirectionRecvonly,
media := &core.Media{
Kind: core.KindAudio,
Direction: core.DirectionSendonly,
Codecs: audios,
}
medias = append(medias, media)

View File

@@ -3,8 +3,8 @@ package rtmp
import (
"github.com/AlexxIT/go2rtc/cmd/api"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/rtmp"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/rs/zerolog/log"
"io"
"net/http"
@@ -16,7 +16,7 @@ func Init() {
api.HandleFunc("api/stream.flv", apiHandle)
}
func streamsHandle(url string) (streamer.Producer, error) {
func streamsHandle(url string) (core.Producer, error) {
conn := rtmp.NewClient(url)
if err := conn.Dial(); err != nil {
return nil, err

View File

@@ -3,9 +3,9 @@ package rtsp
import (
"github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/mp4"
"github.com/AlexxIT/go2rtc/pkg/rtsp"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/AlexxIT/go2rtc/pkg/tcp"
"github.com/rs/zerolog"
"net"
@@ -86,9 +86,9 @@ var Port string
var log zerolog.Logger
var handlers []Handler
var defaultMedias []*streamer.Media
var defaultMedias []*core.Media
func rtspHandler(url string) (streamer.Producer, error) {
func rtspHandler(url string) (core.Producer, error) {
backchannel := true
if i := strings.IndexByte(url, '#'); i > 0 {
@@ -98,11 +98,7 @@ func rtspHandler(url string) (streamer.Producer, error) {
url = url[:i]
}
conn, err := rtsp.NewClient(url)
if err != nil {
return nil, err
}
conn := rtsp.NewClient(url)
conn.UserAgent = app.UserAgent
if log.Trace().Enabled() {
@@ -118,12 +114,12 @@ func rtspHandler(url string) (streamer.Producer, error) {
})
}
if err = conn.Dial(); err != nil {
if err := conn.Dial(); err != nil {
return nil, err
}
conn.Backchannel = backchannel
if err = conn.Describe(); err != nil {
if err := conn.Describe(); err != nil {
if !backchannel {
return nil, err
}
@@ -211,9 +207,6 @@ func tcpHandler(conn *rtsp.Conn) {
closer = func() {
stream.RemoveProducer(conn)
}
case streamer.StatePlaying:
log.Debug().Str("stream", name).Msg("[rtsp] start")
}
})

View File

@@ -1,15 +0,0 @@
package streams
import (
"encoding/json"
"github.com/AlexxIT/go2rtc/pkg/streamer"
)
type Consumer struct {
element streamer.Consumer
tracks []*streamer.Track
}
func (c *Consumer) MarshalJSON() ([]byte, error) {
return json.Marshal(c.element)
}

View File

@@ -2,12 +2,12 @@ package streams
import (
"fmt"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/AlexxIT/go2rtc/pkg/core"
"strings"
"sync"
)
type Handler func(url string) (streamer.Producer, error)
type Handler func(url string) (core.Producer, error)
var handlers = map[string]Handler{}
var handlersMu sync.Mutex
@@ -32,7 +32,7 @@ func HasProducer(url string) bool {
return getHandler(url) != nil
}
func GetProducer(url string) (streamer.Producer, error) {
func GetProducer(url string) (core.Producer, error) {
handler := getHandler(url)
if handler == nil {
return nil, fmt.Errorf("unsupported scheme: %s", url)

View File

@@ -2,14 +2,14 @@ package streams
import (
"errors"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/AlexxIT/go2rtc/pkg/core"
)
func (s *Stream) Play(source string) error {
s.mu.Lock()
for _, producer := range s.producers {
if producer.state == stateInternal && producer.element != nil {
_ = producer.element.Stop()
if producer.state == stateInternal && producer.conn != nil {
_ = producer.conn.Stop()
}
}
s.mu.Unlock()
@@ -18,14 +18,14 @@ func (s *Stream) Play(source string) error {
return nil
}
var src streamer.Producer
var src core.Producer
for _, producer := range s.producers {
if producer.element == nil {
if producer.conn == nil {
continue
}
cons, ok := producer.element.(streamer.Consumer)
cons, ok := producer.conn.(core.Consumer)
if !ok {
continue
}
@@ -59,7 +59,7 @@ func (s *Stream) Play(source string) error {
}
// check if client support consumer interface
cons, ok := dst.(streamer.Consumer)
cons, ok := dst.(core.Consumer)
if !ok {
_ = dst.Stop()
continue
@@ -98,50 +98,49 @@ func (s *Stream) Play(source string) error {
return errors.New("can't find consumer")
}
func (s *Stream) AddInternalProducer(prod streamer.Producer) {
producer := &Producer{element: prod, state: stateInternal}
func (s *Stream) AddInternalProducer(conn core.Producer) {
producer := &Producer{conn: conn, state: stateInternal}
s.mu.Lock()
s.producers = append(s.producers, producer)
s.mu.Unlock()
}
func (s *Stream) AddInternalConsumer(cons streamer.Consumer) {
consumer := &Consumer{element: cons}
func (s *Stream) AddInternalConsumer(conn core.Consumer) {
s.mu.Lock()
s.consumers = append(s.consumers, consumer)
s.consumers = append(s.consumers, conn)
s.mu.Unlock()
}
func (s *Stream) RemoveInternalConsumer(cons streamer.Consumer) {
func (s *Stream) RemoveInternalConsumer(conn core.Consumer) {
s.mu.Lock()
for i, consumer := range s.consumers {
if consumer.element == cons {
s.removeConsumer(i)
if consumer == conn {
s.consumers = append(s.consumers[:i], s.consumers[i+1:]...)
break
}
}
s.mu.Unlock()
}
func matchMedia(prod streamer.Producer, cons streamer.Consumer) bool {
func matchMedia(prod core.Producer, cons core.Consumer) bool {
for _, consMedia := range cons.GetMedias() {
for _, prodMedia := range prod.GetMedias() {
// codec negotiation
prodCodec := prodMedia.MatchMedia(consMedia)
if prodMedia.Direction != core.DirectionRecvonly {
continue
}
prodCodec, consCodec := prodMedia.MatchMedia(consMedia)
if prodCodec == nil {
continue
}
// setup producer track
prodTrack := prod.GetTrack(prodMedia, prodCodec)
if prodTrack == nil {
return false
track, err := prod.GetTrack(prodMedia, prodCodec)
if err != nil {
continue
}
// setup consumer track
consTrack := cons.AddTrack(consMedia, prodTrack)
if consTrack == nil {
return false
if err = cons.AddTrack(consMedia, consCodec, track); err != nil {
continue
}
return true

View File

@@ -2,7 +2,8 @@ package streams
import (
"encoding/json"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"errors"
"github.com/AlexxIT/go2rtc/pkg/core"
"strings"
"sync"
"time"
@@ -20,20 +21,95 @@ const (
)
type Producer struct {
streamer.Element
core.Listener
url string
template string
element streamer.Producer
conn core.Producer
receivers []*core.Receiver
senders []*core.Receiver
lastErr error
tracks []*streamer.Track
state state
mu sync.Mutex
workerID int
}
func (p *Producer) Dial() error {
p.mu.Lock()
defer p.mu.Unlock()
if p.state == stateNone {
conn, err := GetProducer(p.url)
if err != nil {
return err
}
p.conn = conn
p.state = stateMedias
}
return nil
}
func (p *Producer) GetMedias() []*core.Media {
p.mu.Lock()
defer p.mu.Unlock()
return p.conn.GetMedias()
}
func (p *Producer) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) {
p.mu.Lock()
defer p.mu.Unlock()
if p.state == stateNone {
return nil, errors.New("get track from none state")
}
for _, track := range p.receivers {
if track.Codec == codec {
return track, nil
}
}
track, err := p.conn.GetTrack(media, codec)
if err != nil {
return nil, err
}
p.receivers = append(p.receivers, track)
if p.state == stateMedias {
p.state = stateTracks
}
return track, nil
}
func (p *Producer) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) error {
p.mu.Lock()
defer p.mu.Unlock()
if p.state == stateNone {
return errors.New("add track from none state")
}
if err := p.conn.(core.Consumer).AddTrack(media, codec, track); err != nil {
return err
}
p.senders = append(p.senders, track)
if p.state == stateMedias {
p.state = stateTracks
}
return nil
}
func (p *Producer) SetSource(s string) {
if p.template == "" {
p.template = p.url
@@ -41,64 +117,12 @@ func (p *Producer) SetSource(s string) {
p.url = strings.Replace(p.template, "{input}", s, 1)
}
func (p *Producer) GetMedias() []*streamer.Media {
p.mu.Lock()
defer p.mu.Unlock()
if p.state == stateNone {
log.Debug().Msgf("[streams] probe producer url=%s", p.url)
p.element, p.lastErr = GetProducer(p.url)
if p.lastErr != nil || p.element == nil {
log.Error().Err(p.lastErr).Str("url", p.url).Caller().Send()
return nil
}
p.state = stateMedias
}
// if element in reconnect state
if p.element == nil {
return nil
}
return p.element.GetMedias()
}
func (p *Producer) GetTrack(media *streamer.Media, codec *streamer.Codec) *streamer.Track {
p.mu.Lock()
defer p.mu.Unlock()
if p.state == stateNone {
return nil
}
for _, track := range p.tracks {
if track.Codec == codec {
return track
}
}
track := p.element.GetTrack(media, codec)
if track == nil {
return nil
}
p.tracks = append(p.tracks, track)
if p.state == stateMedias {
p.state = stateTracks
}
return track
}
func (p *Producer) MarshalJSON() ([]byte, error) {
if p.element != nil {
return json.Marshal(p.element)
if p.conn != nil {
return json.Marshal(p.conn)
}
info := streamer.Info{URL: p.url}
info := core.Info{URL: p.url}
return json.Marshal(info)
}
@@ -117,11 +141,11 @@ func (p *Producer) start() {
p.state = stateStart
p.workerID++
go p.worker(p.element, p.workerID)
go p.worker(p.conn, p.workerID)
}
func (p *Producer) worker(element streamer.Producer, workerID int) {
if err := element.Start(); err != nil {
func (p *Producer) worker(conn core.Producer, workerID int) {
if err := conn.Start(); err != nil {
p.mu.Lock()
closed := p.workerID != workerID
p.mu.Unlock()
@@ -147,9 +171,8 @@ func (p *Producer) reconnect(workerID int) {
log.Debug().Msgf("[streams] reconnect to url=%s", p.url)
p.element, p.lastErr = GetProducer(p.url)
if p.lastErr != nil || p.element == nil {
log.Debug().Msgf("[streams] producer=%s", p.lastErr)
if err := p.Dial(); err != nil {
log.Debug().Msgf("[streams] producer=%s", err)
// TODO: dynamic timeout
time.AfterFunc(30*time.Second, func() {
p.reconnect(workerID)
@@ -157,27 +180,37 @@ func (p *Producer) reconnect(workerID int) {
return
}
medias := p.element.GetMedias()
for _, media := range p.conn.GetMedias() {
switch media.Direction {
case core.DirectionRecvonly:
for _, receiver := range p.receivers {
codec := media.MatchCodec(receiver.Codec)
if codec == nil {
continue
}
// convert all old producer tracks to new tracks
for i, oldTrack := range p.tracks {
// match new element medias with old track codec
for _, media := range medias {
codec := media.MatchCodec(oldTrack.Codec)
if codec == nil {
continue
track, err := p.conn.GetTrack(media, codec)
if err != nil {
continue
}
receiver.Replace(track)
break
}
// move sink from old track to new track
newTrack := p.element.GetTrack(media, codec)
newTrack.GetSink(oldTrack)
p.tracks[i] = newTrack
case core.DirectionSendonly:
for _, sender := range p.senders {
codec := media.MatchCodec(sender.Codec)
if codec == nil {
continue
}
break
_ = p.conn.(core.Consumer).AddTrack(media, codec, sender)
}
}
}
go p.worker(p.element, workerID)
go p.worker(p.conn, workerID)
}
func (p *Producer) stop() {
@@ -197,11 +230,12 @@ func (p *Producer) stop() {
log.Debug().Msgf("[streams] stop producer url=%s", p.url)
if p.element != nil {
_ = p.element.Stop()
p.element = nil
if p.conn != nil {
_ = p.conn.Stop()
p.conn = nil
}
p.state = stateNone
p.tracks = nil
p.receivers = nil
p.senders = nil
}

View File

@@ -4,7 +4,7 @@ import (
"encoding/json"
"errors"
"fmt"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/AlexxIT/go2rtc/pkg/core"
"strings"
"sync"
"sync/atomic"
@@ -12,12 +12,12 @@ import (
type Stream struct {
producers []*Producer
consumers []*Consumer
consumers []core.Consumer
mu sync.Mutex
requests int32
}
func NewStream(source interface{}) *Stream {
func NewStream(source any) *Stream {
switch source := source.(type) {
case string:
s := new(Stream)
@@ -38,7 +38,7 @@ func NewStream(source interface{}) *Stream {
case nil:
return new(Stream)
default:
panic("wrong source type")
panic(core.Caller())
}
}
@@ -48,57 +48,71 @@ func (s *Stream) SetSource(source string) {
}
}
func (s *Stream) AddConsumer(cons streamer.Consumer) (err error) {
func (s *Stream) AddConsumer(cons core.Consumer) (err error) {
// support for multiple simultaneous requests from different consumers
atomic.AddInt32(&s.requests, 1)
ic := len(s.consumers)
consumer := &Consumer{element: cons}
var producers []*Producer // matched producers for consumer
var codecs string
// Step 1. Get consumer medias
for icc, consMedia := range cons.GetMedias() {
log.Trace().Stringer("media", consMedia).
Msgf("[streams] consumer=%d candidate=%d", ic, icc)
for _, consMedia := range cons.GetMedias() {
producers:
for ip, prod := range s.producers {
// Step 2. Get producer medias (not tracks yet)
for ipc, prodMedia := range prod.GetMedias() {
log.Trace().Stringer("media", prodMedia).
Msgf("[streams] producer=%d candidate=%d", ip, ipc)
for _, prod := range s.producers {
if err = prod.Dial(); err != nil {
continue
}
// Step 2. Get producer medias (not tracks yet)
for _, prodMedia := range prod.GetMedias() {
collectCodecs(prodMedia, &codecs)
// Step 3. Match consumer/producer codecs list
prodCodec := prodMedia.MatchMedia(consMedia)
if prodCodec != nil {
log.Trace().Stringer("codec", prodCodec).
Msgf("[streams] match producer:%d:%d => consumer:%d:%d", ip, ipc, ic, icc)
prodCodec, consCodec := prodMedia.MatchMedia(consMedia)
if prodCodec == nil {
continue
}
// Step 4. Get producer track
prodTrack := prod.GetTrack(prodMedia, prodCodec)
if prodTrack == nil {
log.Warn().Str("url", prod.url).Msg("[streams] can't get track")
var track *core.Receiver
switch prodMedia.Direction {
case core.DirectionRecvonly:
// Step 4. Get recvonly track from producer
if track, err = prod.GetTrack(prodMedia, prodCodec); err != nil {
log.Info().Err(err).Msg("[streams] can't get track")
continue
}
// Step 5. Add track to consumer
if err = cons.AddTrack(consMedia, consCodec, track); err != nil {
log.Info().Err(err).Msg("[streams] can't add track")
continue
}
// Step 5. Add track to consumer and get new track
consTrack := consumer.element.AddTrack(consMedia, prodTrack)
consumer.tracks = append(consumer.tracks, consTrack)
producers = append(producers, prod)
if !consMedia.MatchAll() {
break producers
case core.DirectionSendonly:
// Step 4. Get recvonly track from consumer (backchannel)
if track, err = cons.(core.Producer).GetTrack(consMedia, consCodec); err != nil {
log.Info().Err(err).Msg("[streams] can't get track")
continue
}
// Step 5. Add track to producer
if err = prod.AddTrack(prodMedia, prodCodec, track); err != nil {
log.Info().Err(err).Msg("[streams] can't add track")
continue
}
}
producers = append(producers, prod)
if !consMedia.MatchAll() {
break producers
}
}
}
}
// stop producers if they don't have readers
if atomic.AddInt32(&s.requests, -1) == 0 {
s.stopProducers()
}
@@ -118,7 +132,7 @@ func (s *Stream) AddConsumer(cons streamer.Consumer) (err error) {
}
s.mu.Lock()
s.consumers = append(s.consumers, consumer)
s.consumers = append(s.consumers, cons)
s.mu.Unlock()
// there may be duplicates, but that's not a problem
@@ -129,16 +143,13 @@ func (s *Stream) AddConsumer(cons streamer.Consumer) (err error) {
return nil
}
func (s *Stream) RemoveConsumer(cons streamer.Consumer) {
func (s *Stream) RemoveConsumer(cons core.Consumer) {
_ = cons.Stop()
s.mu.Lock()
for i, consumer := range s.consumers {
if consumer.element == cons {
// remove consumer pads from all producers
for _, track := range consumer.tracks {
track.Unbind()
}
// remove consumer from slice
s.removeConsumer(i)
if consumer == cons {
s.consumers = append(s.consumers[:i], s.consumers[i+1:]...)
break
}
}
@@ -147,18 +158,18 @@ func (s *Stream) RemoveConsumer(cons streamer.Consumer) {
s.stopProducers()
}
func (s *Stream) AddProducer(prod streamer.Producer) {
producer := &Producer{element: prod, state: stateExternal}
func (s *Stream) AddProducer(prod core.Producer) {
producer := &Producer{conn: prod, state: stateExternal}
s.mu.Lock()
s.producers = append(s.producers, producer)
s.mu.Unlock()
}
func (s *Stream) RemoveProducer(prod streamer.Producer) {
func (s *Stream) RemoveProducer(prod core.Producer) {
s.mu.Lock()
for i, producer := range s.producers {
if producer.element == prod {
s.removeProducer(i)
if producer.conn == prod {
s.producers = append(s.producers[:i], s.producers[i+1:]...)
break
}
}
@@ -169,8 +180,8 @@ func (s *Stream) stopProducers() {
s.mu.Lock()
producers:
for _, producer := range s.producers {
for _, track := range producer.tracks {
if track.HasSink() {
for _, track := range producer.receivers {
if len(track.Senders()) > 0 {
continue producers
}
}
@@ -179,20 +190,6 @@ producers:
s.mu.Unlock()
}
//func (s *Stream) Active() bool {
// if len(s.consumers) > 0 {
// return true
// }
//
// for _, prod := range s.producers {
// if prod.element != nil {
// return true
// }
// }
//
// return false
//}
func (s *Stream) MarshalJSON() ([]byte, error) {
if !s.mu.TryLock() {
log.Warn().Msgf("[streams] json locked")
@@ -200,8 +197,8 @@ func (s *Stream) MarshalJSON() ([]byte, error) {
}
var info struct {
Producers []*Producer `json:"producers"`
Consumers []*Consumer `json:"consumers"`
Producers []*Producer `json:"producers"`
Consumers []core.Consumer `json:"consumers"`
}
info.Producers = s.producers
info.Consumers = s.consumers
@@ -211,40 +208,14 @@ func (s *Stream) MarshalJSON() ([]byte, error) {
return json.Marshal(info)
}
func (s *Stream) removeConsumer(i int) {
switch {
case len(s.consumers) == 1: // only one element
s.consumers = nil
case i == 0: // first element
s.consumers = s.consumers[1:]
case i == len(s.consumers)-1: // last element
s.consumers = s.consumers[:i]
default: // middle element
s.consumers = append(s.consumers[:i], s.consumers[i+1:]...)
}
}
func (s *Stream) removeProducer(i int) {
switch {
case len(s.producers) == 1: // only one element
s.producers = nil
case i == 0: // first element
s.producers = s.producers[1:]
case i == len(s.producers)-1: // last element
s.producers = s.producers[:i]
default: // middle element
s.producers = append(s.producers[:i], s.producers[i+1:]...)
}
}
func collectCodecs(media *streamer.Media, codecs *string) {
if media.Direction == streamer.DirectionRecvonly {
func collectCodecs(media *core.Media, codecs *string) {
if media.Direction == core.DirectionRecvonly {
return
}
for _, codec := range media.Codecs {
name := codec.Name
if name == streamer.CodecAAC {
if name == core.CodecAAC {
name = "AAC"
}
if strings.Contains(*codecs, name) {

View File

@@ -2,7 +2,7 @@ package tapo
import (
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/tapo"
)
@@ -10,7 +10,7 @@ func Init() {
streams.HandleFunc("tapo", handle)
}
func handle(url string) (streamer.Producer, error) {
func handle(url string) (core.Producer, error) {
conn := tapo.NewClient(url)
if err := conn.Dial(); err != nil {
return nil, err

View File

@@ -4,7 +4,6 @@ import (
"errors"
"github.com/AlexxIT/go2rtc/cmd/api"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/AlexxIT/go2rtc/pkg/webrtc"
"github.com/gorilla/websocket"
pion "github.com/pion/webrtc/v3"
@@ -14,7 +13,7 @@ import (
"time"
)
func streamsHandler(url string) (streamer.Producer, error) {
func streamsHandler(url string) (core.Producer, error) {
url = url[7:]
if i := strings.Index(url, "://"); i > 0 {
switch url[:i] {
@@ -29,7 +28,7 @@ func streamsHandler(url string) (streamer.Producer, error) {
// asyncClient can connect only to go2rtc server
// ex: ws://localhost:1984/api/ws?src=camera1
func asyncClient(url string) (streamer.Producer, error) {
func asyncClient(url string) (core.Producer, error) {
// 1. Connect to signalign server
ws, _, err := websocket.DefaultDialer.Dial(url, nil)
if err != nil {
@@ -52,7 +51,7 @@ func asyncClient(url string) (streamer.Producer, error) {
prod := webrtc.NewConn(pc)
prod.Desc = "WebRTC/WebSocket async"
prod.Mode = streamer.ModeActiveProducer
prod.Mode = core.ModeActiveProducer
prod.Listen(func(msg any) {
switch msg := msg.(type) {
case pion.PeerConnectionState:
@@ -67,10 +66,10 @@ func asyncClient(url string) (streamer.Producer, error) {
}
})
medias := []*streamer.Media{
{Kind: streamer.KindVideo, Direction: streamer.DirectionRecvonly},
{Kind: streamer.KindAudio, Direction: streamer.DirectionRecvonly},
{Kind: streamer.KindAudio, Direction: streamer.DirectionSendonly},
medias := []*core.Media{
{Kind: core.KindVideo, Direction: core.DirectionRecvonly},
{Kind: core.KindAudio, Direction: core.DirectionRecvonly},
{Kind: core.KindAudio, Direction: core.DirectionSendonly},
}
// 3. Create offer
@@ -129,7 +128,7 @@ func asyncClient(url string) (streamer.Producer, error) {
// syncClient - support WebRTC-HTTP Egress Protocol (WHEP)
// ex: http://localhost:1984/api/webrtc?src=camera1
func syncClient(url string) (streamer.Producer, error) {
func syncClient(url string) (core.Producer, error) {
// 2. Create PeerConnection
pc, err := PeerConnection(true)
if err != nil {
@@ -139,11 +138,11 @@ func syncClient(url string) (streamer.Producer, error) {
prod := webrtc.NewConn(pc)
prod.Desc = "WebRTC/WHEP sync"
prod.Mode = streamer.ModeActiveProducer
prod.Mode = core.ModeActiveProducer
medias := []*streamer.Media{
{Kind: streamer.KindVideo, Direction: streamer.DirectionRecvonly},
{Kind: streamer.KindAudio, Direction: streamer.DirectionRecvonly},
medias := []*core.Media{
{Kind: core.KindVideo, Direction: core.DirectionRecvonly},
{Kind: core.KindAudio, Direction: core.DirectionRecvonly},
}
// 3. Create offer

View File

@@ -6,7 +6,6 @@ import (
"github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/AlexxIT/go2rtc/pkg/webrtc"
pion "github.com/pion/webrtc/v3"
"github.com/rs/zerolog"
@@ -87,16 +86,16 @@ var PeerConnection func(active bool) (*pion.PeerConnection, error)
func asyncHandler(tr *api.Transport, msg *api.Message) error {
var stream *streams.Stream
var mode streamer.Mode
var mode core.Mode
query := tr.Request.URL.Query()
if name := query.Get("src"); name != "" {
stream = streams.GetOrNew(name)
mode = streamer.ModePassiveConsumer
mode = core.ModePassiveConsumer
log.Debug().Str("src", name).Msg("[webrtc] new consumer")
} else if name = query.Get("dst"); name != "" {
stream = streams.Get(name)
mode = streamer.ModePassiveProducer
mode = core.ModePassiveProducer
log.Debug().Str("src", name).Msg("[webrtc] new producer")
}
@@ -124,9 +123,9 @@ func asyncHandler(tr *api.Transport, msg *api.Message) error {
return
}
switch mode {
case streamer.ModePassiveConsumer:
case core.ModePassiveConsumer:
stream.RemoveConsumer(conn)
case streamer.ModePassiveProducer:
case core.ModePassiveProducer:
stream.RemoveProducer(conn)
}
@@ -158,14 +157,14 @@ func asyncHandler(tr *api.Transport, msg *api.Message) error {
}
switch mode {
case streamer.ModePassiveConsumer:
case core.ModePassiveConsumer:
// 2. AddConsumer, so we get new tracks
if err = stream.AddConsumer(conn); err != nil {
log.Debug().Err(err).Msg("[webrtc] add consumer")
_ = conn.Close()
return err
}
case streamer.ModePassiveProducer:
case core.ModePassiveProducer:
stream.AddProducer(conn)
}
@@ -202,9 +201,9 @@ func ExchangeSDP(stream *streams.Stream, offer, desc, userAgent string) (answer
// create new webrtc instance
conn := webrtc.NewConn(pc)
conn.Desc = desc
conn.Mode = streamer.ModePassiveConsumer
conn.Mode = core.ModePassiveConsumer
conn.UserAgent = userAgent
conn.Listen(func(msg interface{}) {
conn.Listen(func(msg any) {
switch msg := msg.(type) {
case pion.PeerConnectionState:
if msg == pion.PeerConnectionStateClosed {

View File

@@ -3,7 +3,7 @@ package webrtc
import (
"encoding/json"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/webrtc"
pion "github.com/pion/webrtc/v3"
"io"
@@ -161,7 +161,7 @@ func inputWebRTC(w http.ResponseWriter, r *http.Request) {
// create new webrtc instance
prod := webrtc.NewConn(pc)
prod.Desc = "WebRTC/WHIP sync"
prod.Mode = streamer.ModePassiveProducer
prod.Mode = core.ModePassiveProducer
prod.UserAgent = r.UserAgent()
if err = prod.SetOffer(string(offer)); err != nil {

View File

@@ -8,7 +8,6 @@ import (
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/cmd/webrtc"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/AlexxIT/go2rtc/pkg/webtorrent"
"github.com/rs/zerolog"
"net/http"
@@ -142,7 +141,7 @@ func apiHandle(w http.ResponseWriter, r *http.Request) {
}
}
func streamHandle(rawURL string) (streamer.Producer, error) {
func streamHandle(rawURL string) (core.Producer, error) {
u, err := url.Parse(rawURL)
if err != nil {
return nil, err

View File

@@ -2,62 +2,59 @@ package aac
import (
"encoding/binary"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/pion/rtp"
)
const RTPPacketVersionAAC = 0
func RTPDepay(track *streamer.Track) streamer.WrapperFunc {
return func(push streamer.WriterFunc) streamer.WriterFunc {
return func(packet *rtp.Packet) error {
// support ONLY 2 bytes header size!
// streamtype=5;profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=1408
headersSize := binary.BigEndian.Uint16(packet.Payload) >> 3
func RTPDepay(handler core.HandlerFunc) core.HandlerFunc {
return func(packet *rtp.Packet) {
// support ONLY 2 bytes header size!
// streamtype=5;profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=1408
headersSize := binary.BigEndian.Uint16(packet.Payload) >> 3
//log.Printf("[RTP/AAC] units: %d, size: %4d, ts: %10d, %t", headersSize/2, len(packet.Payload), packet.Timestamp, packet.Marker)
//log.Printf("[RTP/AAC] units: %d, size: %4d, ts: %10d, %t", headersSize/2, len(packet.Payload), packet.Timestamp, packet.Marker)
data := packet.Payload[2+headersSize:]
if IsADTS(data) {
data = data[7:]
}
clone := *packet
clone.Version = RTPPacketVersionAAC
clone.Payload = data
return push(&clone)
data := packet.Payload[2+headersSize:]
if IsADTS(data) {
data = data[7:]
}
clone := *packet
clone.Version = RTPPacketVersionAAC
clone.Payload = data
handler(&clone)
}
}
func RTPPay(mtu uint16) streamer.WrapperFunc {
func RTPPay(handler core.HandlerFunc) core.HandlerFunc {
sequencer := rtp.NewRandomSequencer()
return func(push streamer.WriterFunc) streamer.WriterFunc {
return func(packet *rtp.Packet) error {
if packet.Version != RTPPacketVersionAAC {
return push(packet)
}
// support ONLY one unit in payload
size := uint16(len(packet.Payload))
// 2 bytes header size + 2 bytes first payload size
payload := make([]byte, 2+2+size)
payload[1] = 16 // header size in bits
binary.BigEndian.PutUint16(payload[2:], size<<3)
copy(payload[4:], packet.Payload)
clone := rtp.Packet{
Header: rtp.Header{
Version: 2,
Marker: true,
SequenceNumber: sequencer.NextSequenceNumber(),
Timestamp: packet.Timestamp,
},
Payload: payload,
}
return push(&clone)
return func(packet *rtp.Packet) {
if packet.Version != RTPPacketVersionAAC {
handler(packet)
return
}
// support ONLY one unit in payload
size := uint16(len(packet.Payload))
// 2 bytes header size + 2 bytes first payload size
payload := make([]byte, 2+2+size)
payload[1] = 16 // header size in bits
binary.BigEndian.PutUint16(payload[2:], size<<3)
copy(payload[4:], packet.Payload)
clone := rtp.Packet{
Header: rtp.Header{
Version: 2,
Marker: true,
SequenceNumber: sequencer.NextSequenceNumber(),
Timestamp: packet.Timestamp,
},
Payload: payload,
}
handler(&clone)
}
}

142
pkg/core/codec.go Normal file
View File

@@ -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 ""
}

99
pkg/core/core.go Normal file
View File

@@ -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"
)

View File

@@ -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)
}

18
pkg/core/listener.go Normal file
View File

@@ -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)
}
}

191
pkg/core/media.go Normal file
View File

@@ -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
}

View File

@@ -1,4 +1,4 @@
package streamer
package core
import (
"fmt"

31
pkg/core/probe.go Normal file
View File

@@ -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{}{}
}
}

188
pkg/core/track.go Normal file
View File

@@ -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())
}

View File

@@ -8,9 +8,9 @@ import (
"encoding/json"
"errors"
"fmt"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/h264"
"github.com/AlexxIT/go2rtc/pkg/h265"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/pion/rtp"
"io"
"net"
@@ -19,7 +19,7 @@ import (
)
type Client struct {
streamer.Element
core.Listener
uri string
conn net.Conn
@@ -28,14 +28,17 @@ type Client struct {
seq uint32
stream string
medias []*streamer.Media
videoTrack *streamer.Track
audioTrack *streamer.Track
medias []*core.Media
receivers []*core.Receiver
videoTrack *core.Receiver
audioTrack *core.Receiver
videoTS uint32
videoDT uint32
audioTS uint32
audioSeq uint16
recv uint32
}
type Response map[string]any
@@ -196,7 +199,7 @@ func (c *Client) Handle() error {
//log.Printf("[AVC] %v, len: %d, ts: %10d", h265.Types(payload), len(payload), packet.Timestamp)
_ = c.videoTrack.WriteRTP(packet)
c.videoTrack.WriteRTP(packet)
}
case 0x1FD: // PFrame
@@ -210,7 +213,7 @@ func (c *Client) Handle() error {
//log.Printf("[DVR] %v, len: %d, ts: %10d", h265.Types(packet.Payload), len(packet.Payload), packet.Timestamp)
_ = c.videoTrack.WriteRTP(packet)
c.videoTrack.WriteRTP(packet)
}
case 0x1FA, 0x1F9: // audio
@@ -245,7 +248,7 @@ func (c *Client) Handle() error {
//log.Printf("[DVR] len: %d, ts: %10d", len(packet.Payload), packet.Timestamp)
_ = c.audioTrack.WriteRTP(packet)
c.audioTrack.WriteRTP(packet)
}
}
}
@@ -295,6 +298,8 @@ func (c *Client) Response() (b []byte, err error) {
return
}
c.recv += 20
if b[0] != 255 {
return nil, errors.New("read error")
}
@@ -307,6 +312,8 @@ func (c *Client) Response() (b []byte, err error) {
return
}
c.recv += size
return
}
@@ -328,21 +335,21 @@ func (c *Client) ResponseJSON() (res Response, err error) {
}
func (c *Client) AddVideoTrack(mediaCode byte, payload []byte) {
var codec *streamer.Codec
var codec *core.Codec
switch mediaCode {
case 2:
codec = &streamer.Codec{
Name: streamer.CodecH264,
codec = &core.Codec{
Name: core.CodecH264,
ClockRate: 90000,
PayloadType: streamer.PayloadTypeRAW,
PayloadType: core.PayloadTypeRAW,
FmtpLine: h264.GetFmtpLine(payload),
}
case 0x03, 0x13:
codec = &streamer.Codec{
Name: streamer.CodecH265,
codec = &core.Codec{
Name: core.CodecH265,
ClockRate: 90000,
PayloadType: streamer.PayloadTypeRAW,
PayloadType: core.PayloadTypeRAW,
FmtpLine: "profile-id=1",
}
@@ -369,14 +376,15 @@ func (c *Client) AddVideoTrack(mediaCode byte, payload []byte) {
return
}
media := &streamer.Media{
Kind: streamer.KindVideo,
Direction: streamer.DirectionSendonly,
Codecs: []*streamer.Codec{codec},
media := &core.Media{
Kind: core.KindVideo,
Direction: core.DirectionRecvonly,
Codecs: []*core.Codec{codec},
}
c.medias = append(c.medias, media)
c.videoTrack = streamer.NewTrack(media, codec)
c.videoTrack = core.NewReceiver(media, codec)
c.receivers = append(c.receivers, c.videoTrack)
}
var sampleRates = []uint32{4000, 8000, 11025, 16000, 20000, 22050, 32000, 44100, 48000}
@@ -384,15 +392,15 @@ var sampleRates = []uint32{4000, 8000, 11025, 16000, 20000, 22050, 32000, 44100,
func (c *Client) AddAudioTrack(mediaCode byte, sampleRate byte) {
// https://github.com/vigoss30611/buildroot-ltc/blob/master/system/qm/ipc/ProtocolService/src/ZhiNuo/inc/zn_dh_base_type.h
// PCM8 = 7, G729, IMA_ADPCM, G711U, G721, PCM8_VWIS, MS_ADPCM, G711A, PCM16
var codec *streamer.Codec
var codec *core.Codec
switch mediaCode {
case 10: // G711U
codec = &streamer.Codec{
Name: streamer.CodecPCMU,
codec = &core.Codec{
Name: core.CodecPCMU,
}
case 14: // G711A
codec = &streamer.Codec{
Name: streamer.CodecPCMA,
codec = &core.Codec{
Name: core.CodecPCMA,
}
default:
println("[DVRIP] unsupported audio codec:", mediaCode)
@@ -403,14 +411,15 @@ func (c *Client) AddAudioTrack(mediaCode byte, sampleRate byte) {
codec.ClockRate = sampleRates[sampleRate-1]
}
media := &streamer.Media{
Kind: streamer.KindAudio,
Direction: streamer.DirectionSendonly,
Codecs: []*streamer.Codec{codec},
media := &core.Media{
Kind: core.KindAudio,
Direction: core.DirectionRecvonly,
Codecs: []*core.Codec{codec},
}
c.medias = append(c.medias, media)
c.audioTrack = streamer.NewTrack(media, codec)
c.audioTrack = core.NewReceiver(media, codec)
c.receivers = append(c.receivers, c.audioTrack)
}
func SofiaHash(password string) string {

View File

@@ -1,19 +1,21 @@
package dvrip
import "github.com/AlexxIT/go2rtc/pkg/streamer"
import (
"encoding/json"
"github.com/AlexxIT/go2rtc/pkg/core"
)
func (c *Client) GetMedias() []*streamer.Media {
func (c *Client) GetMedias() []*core.Media {
return c.medias
}
func (c *Client) GetTrack(media *streamer.Media, codec *streamer.Codec) *streamer.Track {
if c.videoTrack != nil && c.videoTrack.Codec == codec {
return c.videoTrack
func (c *Client) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) {
for _, track := range c.receivers {
if track.Codec == codec {
return track, nil
}
}
if c.audioTrack != nil && c.audioTrack.Codec == codec {
return c.audioTrack
}
return nil
return nil, core.ErrCantGetTrack
}
func (c *Client) Start() error {
@@ -21,5 +23,19 @@ func (c *Client) Start() error {
}
func (c *Client) Stop() error {
for _, receiver := range c.receivers {
receiver.Close()
}
return c.Close()
}
func (c *Client) MarshalJSON() ([]byte, error) {
info := &core.Info{
Type: "DVRIP active producer",
RemoteAddr: c.conn.RemoteAddr().String(),
Medias: c.medias,
Receivers: c.receivers,
Recv: int(c.recv),
}
return json.Marshal(info)
}

View File

@@ -3,7 +3,7 @@ package h264
import (
"bytes"
"encoding/binary"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/pion/rtp"
)
@@ -164,17 +164,15 @@ func EncodeAVC(nals ...[]byte) (avc []byte) {
return
}
func RepairAVC(track *streamer.Track) streamer.WrapperFunc {
sps, pps := GetParameterSet(track.Codec.FmtpLine)
func RepairAVC(codec *core.Codec, handler core.HandlerFunc) core.HandlerFunc {
sps, pps := GetParameterSet(codec.FmtpLine)
ps := EncodeAVC(sps, pps)
return func(push streamer.WriterFunc) streamer.WriterFunc {
return func(packet *rtp.Packet) (err error) {
if NALUType(packet.Payload) == NALUTypeIFrame {
packet.Payload = Join(ps, packet.Payload)
}
return push(packet)
return func(packet *rtp.Packet) {
if NALUType(packet.Payload) == NALUTypeIFrame {
packet.Payload = Join(ps, packet.Payload)
}
handler(packet)
}
}

View File

@@ -5,7 +5,7 @@ import (
"encoding/binary"
"encoding/hex"
"fmt"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/AlexxIT/go2rtc/pkg/core"
"strings"
)
@@ -62,11 +62,11 @@ func GetProfileLevelID(fmtp string) string {
var conf []byte
// some cameras has wrong profile-level-id
// https://github.com/AlexxIT/go2rtc/issues/155
if s := streamer.Between(fmtp, "sprop-parameter-sets=", ","); s != "" {
if s := core.Between(fmtp, "sprop-parameter-sets=", ","); s != "" {
if sps, _ := base64.StdEncoding.DecodeString(s); len(sps) >= 4 {
conf = sps[1:4]
}
} else if s = streamer.Between(fmtp, "profile-level-id=", ";"); s != "" {
} else if s = core.Between(fmtp, "profile-level-id=", ";"); s != "" {
conf, _ = hex.DecodeString(s)
}
@@ -89,7 +89,7 @@ func GetParameterSet(fmtp string) (sps, pps []byte) {
return
}
s := streamer.Between(fmtp, "sprop-parameter-sets=", ";")
s := core.Between(fmtp, "sprop-parameter-sets=", ";")
if s == "" {
return
}

View File

@@ -2,7 +2,7 @@ package h264
import (
"encoding/binary"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/pion/rtp"
"github.com/pion/rtp/codecs"
)
@@ -11,119 +11,112 @@ const RTPPacketVersionAVC = 0
const PSMaxSize = 128 // the biggest SPS I've seen is 48 (EZVIZ CS-CV210)
func RTPDepay(track *streamer.Track) streamer.WrapperFunc {
func RTPDepay(codec *core.Codec, handler core.HandlerFunc) core.HandlerFunc {
depack := &codecs.H264Packet{IsAVC: true}
sps, pps := GetParameterSet(track.Codec.FmtpLine)
sps, pps := GetParameterSet(codec.FmtpLine)
ps := EncodeAVC(sps, pps)
buf := make([]byte, 0, 512*1024) // 512K
return func(push streamer.WriterFunc) streamer.WriterFunc {
return func(packet *rtp.Packet) error {
//log.Printf("[RTP] codec: %s, nalu: %2d, size: %6d, ts: %10d, pt: %2d, ssrc: %d, seq: %d, %v", track.Codec.Name, packet.Payload[0]&0x1F, len(packet.Payload), packet.Timestamp, packet.PayloadType, packet.SSRC, packet.SequenceNumber, packet.Marker)
return func(packet *rtp.Packet) {
//log.Printf("[RTP] codec: %s, nalu: %2d, size: %6d, ts: %10d, pt: %2d, ssrc: %d, seq: %d, %v", track.Codec.Name, packet.Payload[0]&0x1F, len(packet.Payload), packet.Timestamp, packet.PayloadType, packet.SSRC, packet.SequenceNumber, packet.Marker)
payload, err := depack.Unmarshal(packet.Payload)
if len(payload) == 0 || err != nil {
return nil
}
// Fix TP-Link Tapo TC70: sends SPS and PPS with packet.Marker = true
// Reolink Duo 2: sends SPS with Marker and PPS without
if packet.Marker && len(payload) < PSMaxSize {
switch NALUType(payload) {
case NALUTypeSPS, NALUTypePPS:
buf = append(buf, payload...)
return nil
case NALUTypeSEI:
// RtspServer https://github.com/AlexxIT/go2rtc/issues/244
// sends, marked SPS, marked PPS, marked SEI, marked IFrame
return nil
}
}
if len(buf) == 0 {
for {
// Amcrest IP4M-1051: 9, 7, 8, 6, 28...
// Amcrest IP4M-1051: 9, 6, 1
switch NALUType(payload) {
case NALUTypeIFrame:
// fix IFrame without SPS,PPS
buf = append(buf, ps...)
case NALUTypeSEI, NALUTypeAUD:
// fix ffmpeg with transcoding first frame
i := int(4 + binary.BigEndian.Uint32(payload))
// check if only one NAL (fix ffmpeg transcoding for Reolink RLC-510A)
if i == len(payload) {
return nil
}
payload = payload[i:]
continue
}
break
}
}
// collect all NALs for Access Unit
if !packet.Marker {
buf = append(buf, payload...)
return nil
}
if len(buf) > 0 {
payload = append(buf, payload...)
buf = buf[:0]
}
// should not be that huge SPS
if NALUType(payload) == NALUTypeSPS && binary.BigEndian.Uint32(payload) >= PSMaxSize {
// some Chinese buggy cameras has single packet with SPS+PPS+IFrame separated by 00 00 00 01
// https://github.com/AlexxIT/WebRTC/issues/391
// https://github.com/AlexxIT/WebRTC/issues/392
AnnexB2AVC(payload)
}
//log.Printf("[AVC] %v, len: %d, ts: %10d, seq: %d", Types(payload), len(payload), packet.Timestamp, packet.SequenceNumber)
clone := *packet
clone.Version = RTPPacketVersionAVC
clone.Payload = payload
return push(&clone)
payload, err := depack.Unmarshal(packet.Payload)
if len(payload) == 0 || err != nil {
return
}
// Fix TP-Link Tapo TC70: sends SPS and PPS with packet.Marker = true
// Reolink Duo 2: sends SPS with Marker and PPS without
if packet.Marker && len(payload) < PSMaxSize {
switch NALUType(payload) {
case NALUTypeSPS, NALUTypePPS:
buf = append(buf, payload...)
return
case NALUTypeSEI:
// RtspServer https://github.com/AlexxIT/go2rtc/issues/244
// sends, marked SPS, marked PPS, marked SEI, marked IFrame
return
}
}
if len(buf) == 0 {
for {
// Amcrest IP4M-1051: 9, 7, 8, 6, 28...
// Amcrest IP4M-1051: 9, 6, 1
switch NALUType(payload) {
case NALUTypeIFrame:
// fix IFrame without SPS,PPS
buf = append(buf, ps...)
case NALUTypeSEI, NALUTypeAUD:
// fix ffmpeg with transcoding first frame
i := int(4 + binary.BigEndian.Uint32(payload))
// check if only one NAL (fix ffmpeg transcoding for Reolink RLC-510A)
if i == len(payload) {
return
}
payload = payload[i:]
continue
}
break
}
}
// collect all NALs for Access Unit
if !packet.Marker {
buf = append(buf, payload...)
return
}
if len(buf) > 0 {
payload = append(buf, payload...)
buf = buf[:0]
}
// should not be that huge SPS
if NALUType(payload) == NALUTypeSPS && binary.BigEndian.Uint32(payload) >= PSMaxSize {
// some Chinese buggy cameras has single packet with SPS+PPS+IFrame separated by 00 00 00 01
// https://github.com/AlexxIT/WebRTC/issues/391
// https://github.com/AlexxIT/WebRTC/issues/392
AnnexB2AVC(payload)
}
//log.Printf("[AVC] %v, len: %d, ts: %10d, seq: %d", Types(payload), len(payload), packet.Timestamp, packet.SequenceNumber)
clone := *packet
clone.Version = RTPPacketVersionAVC
clone.Payload = payload
handler(&clone)
}
}
func RTPPay(mtu uint16) streamer.WrapperFunc {
func RTPPay(mtu uint16, handler core.HandlerFunc) core.HandlerFunc {
payloader := &Payloader{IsAVC: true}
sequencer := rtp.NewRandomSequencer()
mtu -= 12 // rtp.Header size
return func(push streamer.WriterFunc) streamer.WriterFunc {
return func(packet *rtp.Packet) error {
if packet.Version != RTPPacketVersionAVC {
return push(packet)
}
return func(packet *rtp.Packet) {
if packet.Version != RTPPacketVersionAVC {
handler(packet)
return
}
payloads := payloader.Payload(mtu, packet.Payload)
last := len(payloads) - 1
for i, payload := range payloads {
clone := rtp.Packet{
Header: rtp.Header{
Version: 2,
Marker: i == last,
SequenceNumber: sequencer.NextSequenceNumber(),
Timestamp: packet.Timestamp,
},
Payload: payload,
}
if err := push(&clone); err != nil {
return err
}
payloads := payloader.Payload(mtu, packet.Payload)
last := len(payloads) - 1
for i, payload := range payloads {
clone := rtp.Packet{
Header: rtp.Header{
Version: 2,
Marker: i == last,
SequenceNumber: sequencer.NextSequenceNumber(),
Timestamp: packet.Timestamp,
},
Payload: payload,
}
return nil
handler(&clone)
}
}
}

View File

@@ -3,7 +3,7 @@ package h265
import (
"encoding/base64"
"encoding/binary"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/AlexxIT/go2rtc/pkg/core"
)
const (
@@ -62,13 +62,13 @@ func GetParameterSet(fmtp string) (vps, sps, pps []byte) {
return
}
s := streamer.Between(fmtp, "sprop-vps=", ";")
s := core.Between(fmtp, "sprop-vps=", ";")
vps, _ = base64.StdEncoding.DecodeString(s)
s = streamer.Between(fmtp, "sprop-sps=", ";")
s = core.Between(fmtp, "sprop-sps=", ";")
sps, _ = base64.StdEncoding.DecodeString(s)
s = streamer.Between(fmtp, "sprop-pps=", ";")
s = core.Between(fmtp, "sprop-pps=", ";")
pps, _ = base64.StdEncoding.DecodeString(s)
return

View File

@@ -2,189 +2,177 @@ package h265
import (
"encoding/binary"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/h264"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/pion/rtp"
)
func RTPDepay(track *streamer.Track) streamer.WrapperFunc {
//vps, sps, pps := GetParameterSet(track.Codec.FmtpLine)
func RTPDepay(codec *core.Codec, handler core.HandlerFunc) core.HandlerFunc {
//vps, sps, pps := GetParameterSet(codec.FmtpLine)
//ps := h264.EncodeAVC(vps, sps, pps)
buf := make([]byte, 0, 512*1024) // 512K
var nuStart int
return func(push streamer.WriterFunc) streamer.WriterFunc {
return func(packet *rtp.Packet) error {
data := packet.Payload
nuType := (data[0] >> 1) & 0x3F
//log.Printf("[RTP] codec: %s, nalu: %2d, size: %6d, ts: %10d, pt: %2d, ssrc: %d, seq: %d, %v", track.Codec.Name, nuType, len(packet.Payload), packet.Timestamp, packet.PayloadType, packet.SSRC, packet.SequenceNumber, packet.Marker)
return func(packet *rtp.Packet) {
data := packet.Payload
nuType := (data[0] >> 1) & 0x3F
//log.Printf("[RTP] codec: %s, nalu: %2d, size: %6d, ts: %10d, pt: %2d, ssrc: %d, seq: %d, %v", track.Codec.Name, nuType, len(packet.Payload), packet.Timestamp, packet.PayloadType, packet.SSRC, packet.SequenceNumber, packet.Marker)
// Fix for RtspServer https://github.com/AlexxIT/go2rtc/issues/244
if packet.Marker && len(data) < h264.PSMaxSize {
switch nuType {
case NALUTypeVPS, NALUTypeSPS, NALUTypePPS:
packet.Marker = false
case NALUTypePrefixSEI, NALUTypeSuffixSEI:
return nil
}
// Fix for RtspServer https://github.com/AlexxIT/go2rtc/issues/244
if packet.Marker && len(data) < h264.PSMaxSize {
switch nuType {
case NALUTypeVPS, NALUTypeSPS, NALUTypePPS:
packet.Marker = false
case NALUTypePrefixSEI, NALUTypeSuffixSEI:
return
}
}
if nuType == NALUTypeFU {
switch data[2] >> 6 {
case 2: // begin
nuType = data[2] & 0x3F
if nuType == NALUTypeFU {
switch data[2] >> 6 {
case 2: // begin
nuType = data[2] & 0x3F
// push PS data before keyframe
//if len(buf) == 0 && nuType >= 19 && nuType <= 21 {
// buf = append(buf, ps...)
//}
// push PS data before keyframe
//if len(buf) == 0 && nuType >= 19 && nuType <= 21 {
// buf = append(buf, ps...)
//}
nuStart = len(buf)
buf = append(buf, 0, 0, 0, 0) // NAL unit size
buf = append(buf, (data[0]&0x81)|(nuType<<1), data[1])
buf = append(buf, data[3:]...)
return nil
case 0: // continue
buf = append(buf, data[3:]...)
return nil
case 1: // end
buf = append(buf, data[3:]...)
binary.BigEndian.PutUint32(buf[nuStart:], uint32(len(buf)-nuStart-4))
}
} else {
nuStart = len(buf)
buf = append(buf, 0, 0, 0, 0) // NAL unit size
buf = append(buf, data...)
binary.BigEndian.PutUint32(buf[nuStart:], uint32(len(data)))
buf = append(buf, (data[0]&0x81)|(nuType<<1), data[1])
buf = append(buf, data[3:]...)
return
case 0: // continue
buf = append(buf, data[3:]...)
return
case 1: // end
buf = append(buf, data[3:]...)
binary.BigEndian.PutUint32(buf[nuStart:], uint32(len(buf)-nuStart-4))
}
// collect all NAL Units for Access Unit
if !packet.Marker {
return nil
}
//log.Printf("[HEVC] %v, len: %d", Types(buf), len(buf))
clone := *packet
clone.Version = h264.RTPPacketVersionAVC
clone.Payload = buf
buf = buf[:0]
return push(&clone)
} else {
nuStart = len(buf)
buf = append(buf, 0, 0, 0, 0) // NAL unit size
buf = append(buf, data...)
binary.BigEndian.PutUint32(buf[nuStart:], uint32(len(data)))
}
// collect all NAL Units for Access Unit
if !packet.Marker {
return
}
//log.Printf("[HEVC] %v, len: %d", Types(buf), len(buf))
clone := *packet
clone.Version = h264.RTPPacketVersionAVC
clone.Payload = buf
buf = buf[:0]
handler(&clone)
}
}
func RTPPay(mtu uint16) streamer.WrapperFunc {
func RTPPay(mtu uint16, handler core.HandlerFunc) core.HandlerFunc {
payloader := &Payloader{}
sequencer := rtp.NewRandomSequencer()
mtu -= 12 // rtp.Header size
return func(push streamer.WriterFunc) streamer.WriterFunc {
return func(packet *rtp.Packet) error {
if packet.Version != h264.RTPPacketVersionAVC {
return push(packet)
}
return func(packet *rtp.Packet) {
if packet.Version != h264.RTPPacketVersionAVC {
handler(packet)
return
}
payloads := payloader.Payload(mtu, packet.Payload)
last := len(payloads) - 1
for i, payload := range payloads {
clone := rtp.Packet{
Header: rtp.Header{
Version: 2,
Marker: i == last,
SequenceNumber: sequencer.NextSequenceNumber(),
Timestamp: packet.Timestamp,
},
Payload: payload,
}
if err := push(&clone); err != nil {
return err
}
payloads := payloader.Payload(mtu, packet.Payload)
last := len(payloads) - 1
for i, payload := range payloads {
clone := rtp.Packet{
Header: rtp.Header{
Version: 2,
Marker: i == last,
SequenceNumber: sequencer.NextSequenceNumber(),
Timestamp: packet.Timestamp,
},
Payload: payload,
}
return nil
handler(&clone)
}
}
}
// SafariPay - generate Safari friendly payload for H265
// https://github.com/AlexxIT/Blog/issues/5
func SafariPay(mtu uint16) streamer.WrapperFunc {
func SafariPay(mtu uint16, handler core.HandlerFunc) core.HandlerFunc {
sequencer := rtp.NewRandomSequencer()
size := int(mtu - 12) // rtp.Header size
return func(push streamer.WriterFunc) streamer.WriterFunc {
return func(packet *rtp.Packet) error {
if packet.Version != h264.RTPPacketVersionAVC {
return push(packet)
return func(packet *rtp.Packet) {
if packet.Version != h264.RTPPacketVersionAVC {
handler(packet)
return
}
// protect original packets from modification
au := make([]byte, len(packet.Payload))
copy(au, packet.Payload)
var start byte
for i := 0; i < len(au); {
size := int(binary.BigEndian.Uint32(au[i:])) + 4
// convert AVC to Annex-B
au[i] = 0
au[i+1] = 0
au[i+2] = 0
au[i+3] = 1
switch NALUType(au[i:]) {
case NALUTypeIFrame, NALUTypeIFrame2, NALUTypeIFrame3:
start = 3
default:
if start == 0 {
start = 2
}
}
// protect original packets from modification
au := make([]byte, len(packet.Payload))
copy(au, packet.Payload)
i += size
}
var start byte
// rtp.Packet payload
b := make([]byte, 1, size)
size-- // minus header byte
for i := 0; i < len(au); {
size := int(binary.BigEndian.Uint32(au[i:])) + 4
for au != nil {
b[0] = start
// convert AVC to Annex-B
au[i] = 0
au[i+1] = 0
au[i+2] = 0
au[i+3] = 1
switch NALUType(au[i:]) {
case NALUTypeIFrame, NALUTypeIFrame2, NALUTypeIFrame3:
start = 3
default:
if start == 0 {
start = 2
}
}
i += size
if start > 1 {
start -= 2
}
// rtp.Packet payload
b := make([]byte, 1, size)
size-- // minus header byte
for au != nil {
b[0] = start
if start > 1 {
start -= 2
}
if len(au) > size {
b = append(b, au[:size]...)
au = au[size:]
} else {
b = append(b, au...)
au = nil
}
clone := rtp.Packet{
Header: rtp.Header{
Version: 2,
Marker: au == nil,
SequenceNumber: sequencer.NextSequenceNumber(),
Timestamp: packet.Timestamp,
},
Payload: b,
}
if err := push(&clone); err != nil {
return err
}
b = b[:1] // clear buffer
if len(au) > size {
b = append(b, au[:size]...)
au = au[size:]
} else {
b = append(b, au...)
au = nil
}
return nil
clone := rtp.Packet{
Header: rtp.Header{
Version: 2,
Marker: au == nil,
SequenceNumber: sequencer.NextSequenceNumber(),
Timestamp: packet.Timestamp,
},
Payload: b,
}
handler(&clone)
b = b[:1] // clear buffer
}
}
}

View File

@@ -11,14 +11,14 @@ import (
)
type Character struct {
AID int `json:"aid,omitempty"`
IID int `json:"iid"`
Type string `json:"type,omitempty"`
Format string `json:"format,omitempty"`
Value interface{} `json:"value,omitempty"`
Event interface{} `json:"ev,omitempty"`
Perms []string `json:"perms,omitempty"`
Description string `json:"description,omitempty"`
AID int `json:"aid,omitempty"`
IID int `json:"iid"`
Type string `json:"type,omitempty"`
Format string `json:"format,omitempty"`
Value any `json:"value,omitempty"`
Event any `json:"ev,omitempty"`
Perms []string `json:"perms,omitempty"`
Description string `json:"description,omitempty"`
//MaxDataLen int `json:"maxDataLen"`
listeners map[io.Writer]bool

View File

@@ -7,8 +7,8 @@ import (
"encoding/json"
"errors"
"fmt"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/hap/mdns"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/brutella/hap"
"github.com/brutella/hap/chacha20poly1305"
"github.com/brutella/hap/curve25519"
@@ -26,7 +26,7 @@ import (
// Conn for HomeKit. DevicePublic can be null.
type Conn struct {
streamer.Element
core.Listener
DeviceAddress string // including port
DeviceID string

View File

@@ -4,10 +4,10 @@ import (
"encoding/json"
"errors"
"fmt"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/hap"
"github.com/AlexxIT/go2rtc/pkg/hap/camera"
"github.com/AlexxIT/go2rtc/pkg/srtp"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/brutella/hap/characteristic"
"github.com/brutella/hap/rtp"
"net"
@@ -16,15 +16,15 @@ import (
)
type Client struct {
streamer.Element
core.Listener
conn *hap.Conn
exit chan error
server *srtp.Server
url string
medias []*streamer.Media
tracks []*streamer.Track
medias []*core.Media
receivers []*core.Receiver
sessions []*srtp.Session
}
@@ -62,7 +62,7 @@ func (c *Client) Dial() error {
return nil
}
func (c *Client) GetMedias() []*streamer.Media {
func (c *Client) GetMedias() []*core.Media {
if c.medias == nil {
c.medias = c.getMedias()
}
@@ -70,20 +70,20 @@ func (c *Client) GetMedias() []*streamer.Media {
return c.medias
}
func (c *Client) GetTrack(media *streamer.Media, codec *streamer.Codec) *streamer.Track {
for _, track := range c.tracks {
func (c *Client) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) {
for _, track := range c.receivers {
if track.Codec == codec {
return track
return track, nil
}
}
track := streamer.NewTrack(media, codec)
c.tracks = append(c.tracks, track)
return track
track := core.NewReceiver(media, codec)
c.receivers = append(c.receivers, track)
return track, nil
}
func (c *Client) Start() error {
if c.tracks == nil {
if c.receivers == nil {
return errors.New("producer without tracks")
}
@@ -161,11 +161,11 @@ func (c *Client) Start() error {
return err
}
for _, track := range c.tracks {
for _, track := range c.receivers {
switch track.Codec.Name {
case streamer.CodecH264:
case core.CodecH264:
vs.Track = track
case streamer.CodecELD:
case core.CodecELD:
as.Track = track
}
}
@@ -188,8 +188,8 @@ func (c *Client) Stop() error {
return err
}
func (c *Client) getMedias() []*streamer.Media {
var medias []*streamer.Media
func (c *Client) getMedias() []*core.Media {
var medias []*core.Media
accs, err := c.conn.GetAccessories()
if err != nil {
@@ -206,20 +206,20 @@ func (c *Client) getMedias() []*streamer.Media {
}
for _, hkCodec := range v1.Codecs {
codec := &streamer.Codec{ClockRate: 90000}
codec := &core.Codec{ClockRate: 90000}
switch hkCodec.Type {
case rtp.VideoCodecType_H264:
codec.Name = streamer.CodecH264
codec.Name = core.CodecH264
codec.FmtpLine = "profile-level-id=420029"
default:
fmt.Printf("unknown codec: %d", hkCodec.Type)
continue
}
media := &streamer.Media{
Kind: streamer.KindVideo, Direction: streamer.DirectionSendonly,
Codecs: []*streamer.Codec{codec},
media := &core.Media{
Kind: core.KindVideo, Direction: core.DirectionRecvonly,
Codecs: []*core.Codec{codec},
}
medias = append(medias, media)
}
@@ -231,7 +231,7 @@ func (c *Client) getMedias() []*streamer.Media {
}
for _, hkCodec := range v2.Codecs {
codec := &streamer.Codec{
codec := &core.Codec{
Channels: uint16(hkCodec.Parameters.Channels),
}
@@ -248,7 +248,7 @@ func (c *Client) getMedias() []*streamer.Media {
switch hkCodec.Type {
case rtp.AudioCodecType_AAC_ELD:
codec.Name = streamer.CodecELD
codec.Name = core.CodecELD
// only this value supported by FFmpeg
codec.FmtpLine = "profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=F8EC3000"
default:
@@ -256,9 +256,9 @@ func (c *Client) getMedias() []*streamer.Media {
continue
}
media := &streamer.Media{
Kind: streamer.KindAudio, Direction: streamer.DirectionSendonly,
Codecs: []*streamer.Codec{codec},
media := &core.Media{
Kind: core.KindAudio, Direction: core.DirectionRecvonly,
Codecs: []*core.Codec{codec},
}
medias = append(medias, media)
}
@@ -272,12 +272,12 @@ func (c *Client) MarshalJSON() ([]byte, error) {
recv += atomic.LoadUint32(&session.Recv)
}
info := &streamer.Info{
Type: "HomeKit source",
URL: c.conn.URL(),
Medias: c.medias,
Tracks: c.tracks,
Recv: recv,
info := &core.Info{
Type: "HomeKit active producer",
URL: c.conn.URL(),
Medias: c.medias,
Receivers: c.receivers,
Recv: int(recv),
}
return json.Marshal(info)
}

View File

@@ -2,7 +2,7 @@ package isapi
import (
"errors"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/tcp"
"io"
"net"
@@ -11,16 +11,15 @@ import (
)
type Client struct {
streamer.Element
url string
medias []*streamer.Media
tracks []*streamer.Track
core.Listener
url string
channel string
conn net.Conn
send int
medias []*core.Media
sender *core.Sender
send int
}
func NewClient(rawURL string) (*Client, error) {
@@ -60,22 +59,22 @@ func (c *Client) Dial() (err error) {
xml := string(b)
codec := streamer.Between(xml, `<audioCompressionType>`, `<`)
codec := core.Between(xml, `<audioCompressionType>`, `<`)
switch codec {
case "G.711ulaw":
codec = streamer.CodecPCMU
codec = core.CodecPCMU
case "G.711alaw":
codec = streamer.CodecPCMA
codec = core.CodecPCMA
default:
return nil
}
c.channel = streamer.Between(xml, `<id>`, `<`)
c.channel = core.Between(xml, `<id>`, `<`)
media := &streamer.Media{
Kind: streamer.KindAudio,
Direction: streamer.DirectionRecvonly,
Codecs: []*streamer.Codec{
media := &core.Media{
Kind: core.KindAudio,
Direction: core.DirectionSendonly,
Codecs: []*core.Codec{
{Name: codec, ClockRate: 8000},
},
}

View File

@@ -1,18 +1,63 @@
package isapi
import (
"github.com/AlexxIT/go2rtc/pkg/streamer"
"encoding/json"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/pion/rtp"
)
func (c *Client) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.Track {
consCodec := media.MatchCodec(track.Codec)
consTrack := c.GetTrack(media, consCodec)
if consTrack == nil {
return nil
func (c *Client) GetMedias() []*core.Media {
return c.medias
}
func (c *Client) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) {
return nil, core.ErrCantGetTrack
}
func (c *Client) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiver) error {
if c.sender == nil {
c.sender = core.NewSender(media, track.Codec)
c.sender.Handler = func(packet *rtp.Packet) {
if c.conn == nil {
return
}
c.send += len(packet.Payload)
_, _ = c.conn.Write(packet.Payload)
}
}
return track.Bind(func(packet *rtp.Packet) error {
return consTrack.WriteRTP(packet)
})
c.sender.HandleRTP(track)
return nil
}
func (c *Client) Start() (err error) {
if err = c.Open(); err != nil {
return
}
return
}
func (c *Client) Stop() (err error) {
if c.sender != nil {
c.sender.Close()
}
if c.conn != nil {
_ = c.Close()
return c.conn.Close()
}
return nil
}
func (c *Client) MarshalJSON() ([]byte, error) {
info := &core.Info{
Type: "ISAPI active consumer",
Medias: c.medias,
Send: c.send,
}
if c.sender != nil {
info.Senders = []*core.Sender{c.sender}
}
return json.Marshal(info)
}

View File

@@ -1,56 +0,0 @@
package isapi
import (
"encoding/json"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/pion/rtp"
)
func (c *Client) GetMedias() []*streamer.Media {
return c.medias
}
func (c *Client) GetTrack(media *streamer.Media, codec *streamer.Codec) *streamer.Track {
for _, track := range c.tracks {
if track.Codec == codec {
return track
}
}
track := streamer.NewTrack(media, codec)
track = track.Bind(func(packet *rtp.Packet) (err error) {
if c.conn != nil {
c.send += len(packet.Payload)
_, err = c.conn.Write(packet.Payload)
}
return
})
c.tracks = append(c.tracks, track)
return track
}
func (c *Client) Start() (err error) {
if err = c.Open(); err != nil {
return
}
return
}
func (c *Client) Stop() (err error) {
if c.conn == nil {
return
}
_ = c.Close()
return c.conn.Close()
}
func (c *Client) MarshalJSON() ([]byte, error) {
info := &streamer.Info{
Type: "ISAPI",
Medias: c.medias,
Tracks: c.tracks,
Send: uint32(c.send),
}
return json.Marshal(info)
}

View File

@@ -1,13 +1,15 @@
package iso
import "github.com/AlexxIT/go2rtc/pkg/streamer"
import (
"github.com/AlexxIT/go2rtc/pkg/core"
)
func (m *Movie) WriteVideo(codec string, width, height uint16, conf []byte) {
// https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap3/qtff3.html
switch codec {
case streamer.CodecH264:
case core.CodecH264:
m.StartAtom("avc1")
case streamer.CodecH265:
case core.CodecH265:
m.StartAtom("hev1")
default:
panic("unsupported iso video: " + codec)
@@ -30,9 +32,9 @@ func (m *Movie) WriteVideo(codec string, width, height uint16, conf []byte) {
m.WriteUint16(0xFFFF) // color table id (-1)
switch codec {
case streamer.CodecH264:
case core.CodecH264:
m.StartAtom("avcC")
case streamer.CodecH265:
case core.CodecH265:
m.StartAtom("hvcC")
}
m.Write(conf)
@@ -43,13 +45,13 @@ func (m *Movie) WriteVideo(codec string, width, height uint16, conf []byte) {
func (m *Movie) WriteAudio(codec string, channels uint16, sampleRate uint32, conf []byte) {
switch codec {
case streamer.CodecAAC, streamer.CodecMP3:
case core.CodecAAC, core.CodecMP3:
m.StartAtom("mp4a")
case streamer.CodecOpus:
case core.CodecOpus:
m.StartAtom("Opus")
case streamer.CodecPCMU:
case core.CodecPCMU:
m.StartAtom("ulaw")
case streamer.CodecPCMA:
case core.CodecPCMA:
m.StartAtom("alaw")
default:
panic("unsupported iso audio: " + codec)
@@ -66,16 +68,16 @@ func (m *Movie) WriteAudio(codec string, channels uint16, sampleRate uint32, con
m.WriteFloat32(float64(sampleRate)) // sample_rate
switch codec {
case streamer.CodecAAC:
case core.CodecAAC:
m.WriteEsdsAAC(conf)
case streamer.CodecMP3:
case core.CodecMP3:
m.WriteEsdsMP3()
case streamer.CodecOpus:
case core.CodecOpus:
// don't know what means this magic
m.StartAtom("dOps")
m.WriteBytes(0, 0x02, 0x01, 0x38, 0, 0, 0xBB, 0x80, 0, 0, 0)
m.EndAtom()
case streamer.CodecPCMU, streamer.CodecPCMA:
case core.CodecPCMU, core.CodecPCMA:
// don't know what means this magic
m.StartAtom("chan")
m.WriteBytes(0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0)

View File

@@ -6,7 +6,7 @@ import (
"encoding/binary"
"encoding/json"
"fmt"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/deepch/vdk/codec/h264parser"
"github.com/deepch/vdk/format/fmp4/fmp4io"
"github.com/gorilla/websocket"
@@ -15,7 +15,6 @@ import (
"net/http"
"strings"
"sync"
"sync/atomic"
"time"
)
@@ -28,13 +27,14 @@ const (
)
type Client struct {
streamer.Element
core.Listener
ID string
conn *websocket.Conn
medias []*streamer.Media
tracks map[byte]*streamer.Track
conn *websocket.Conn
medias []*core.Media
receiver *core.Receiver
msg *message
t0 time.Time
@@ -43,7 +43,7 @@ type Client struct {
state State
mu sync.Mutex
recv uint32
recv int
}
func NewClient(id string) *Client {
@@ -107,12 +107,11 @@ func (c *Client) Handle() error {
return err
}
track := c.tracks[c.msg.Track]
if track != nil {
if c.receiver != nil && c.receiver.ID == c.msg.Track {
c.mu.Lock()
if c.state == StateHandle {
c.buffer <- data
atomic.AddUint32(&c.recv, uint32(len(data)))
c.recv += len(data)
}
c.mu.Unlock()
}
@@ -139,12 +138,11 @@ func (c *Client) Handle() error {
return err
}
track = c.tracks[msg.Track]
if track != nil {
if c.receiver != nil && c.receiver.ID == msg.Track {
c.mu.Lock()
if c.state == StateHandle {
c.buffer <- data
atomic.AddUint32(&c.recv, uint32(len(data)))
c.recv += len(data)
}
c.mu.Unlock()
}
@@ -173,8 +171,6 @@ func (c *Client) Close() error {
}
func (c *Client) getTracks() error {
c.tracks = map[byte]*streamer.Track{}
for {
_, data, err := c.conn.ReadMessage()
if err != nil {
@@ -197,15 +193,15 @@ func (c *Client) getTracks() error {
switch s {
case "avc1": // avc1.4d0029
// skip multiple identical init
if c.tracks[msg.TrackID] != nil {
if c.receiver != nil {
continue
}
codec := &streamer.Codec{
Name: streamer.CodecH264,
codec := &core.Codec{
Name: core.CodecH264,
ClockRate: 90000,
FmtpLine: "profile-level-id=" + msg.CodecString[i+1:],
PayloadType: streamer.PayloadTypeRAW,
PayloadType: core.PayloadTypeRAW,
}
i = bytes.Index(msg.Data, []byte("avcC")) - 4
@@ -225,15 +221,15 @@ func (c *Client) getTracks() error {
base64.StdEncoding.EncodeToString(record.SPS[0]) + "," +
base64.StdEncoding.EncodeToString(record.PPS[0])
media := &streamer.Media{
Kind: streamer.KindVideo,
Direction: streamer.DirectionSendonly,
Codecs: []*streamer.Codec{codec},
media := &core.Media{
Kind: core.KindVideo,
Direction: core.DirectionRecvonly,
Codecs: []*core.Codec{codec},
}
c.medias = append(c.medias, media)
track := streamer.NewTrack(media, codec)
c.tracks[msg.TrackID] = track
c.receiver = core.NewReceiver(media, codec)
c.receiver.ID = msg.TrackID
case "mp4a": // mp4a.40.2
}
@@ -249,11 +245,6 @@ func (c *Client) getTracks() error {
}
func (c *Client) worker(buffer chan []byte) {
var track *streamer.Track
for _, track = range c.tracks {
break
}
for data := range buffer {
moof := &fmp4io.MovieFrag{}
if _, err := moof.Unmarshal(data, 0); err != nil {
@@ -289,7 +280,7 @@ func (c *Client) worker(buffer chan []byte) {
Header: rtp.Header{Timestamp: ts * 90},
Payload: data[:entry.Size],
}
_ = track.WriteRTP(packet)
c.receiver.WriteRTP(packet)
data = data[entry.Size:]
ts += entry.Duration

View File

@@ -2,22 +2,18 @@ package ivideon
import (
"encoding/json"
"fmt"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"sync/atomic"
"github.com/AlexxIT/go2rtc/pkg/core"
)
func (c *Client) GetMedias() []*streamer.Media {
func (c *Client) GetMedias() []*core.Media {
return c.medias
}
func (c *Client) GetTrack(media *streamer.Media, codec *streamer.Codec) *streamer.Track {
for _, track := range c.tracks {
if track.Codec == codec {
return track
}
func (c *Client) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) {
if c.receiver != nil {
return c.receiver, nil
}
panic(fmt.Sprintf("wrong media/codec: %+v %+v", media, codec))
return nil, core.ErrCantGetTrack
}
func (c *Client) Start() error {
@@ -29,21 +25,21 @@ func (c *Client) Start() error {
}
func (c *Client) Stop() error {
if c.receiver != nil {
c.receiver.Close()
}
return c.Close()
}
func (c *Client) MarshalJSON() ([]byte, error) {
var tracks []*streamer.Track
for _, track := range c.tracks {
tracks = append(tracks, track)
}
info := &streamer.Info{
Type: "Ivideon source",
info := &core.Info{
Type: "Ivideon active producer",
URL: c.ID,
Medias: c.medias,
Tracks: tracks,
Recv: atomic.LoadUint32(&c.recv),
Recv: c.recv,
}
if c.receiver != nil {
info.Receivers = []*core.Receiver{c.receiver}
}
return json.Marshal(info)
}

View File

@@ -3,7 +3,7 @@ package mjpeg
import (
"bufio"
"errors"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/tcp"
"github.com/pion/rtp"
"io"
@@ -11,12 +11,11 @@ import (
"net/textproto"
"strconv"
"strings"
"sync/atomic"
"time"
)
type Client struct {
streamer.Element
core.Listener
UserAgent string
RemoteAddr string
@@ -24,9 +23,10 @@ type Client struct {
closed bool
res *http.Response
medias []*streamer.Media
track *streamer.Track
recv uint32
medias []*core.Media
receiver *core.Receiver
recv int
}
func NewClient(res *http.Response) *Client {
@@ -40,9 +40,9 @@ func (c *Client) startJPEG() error {
}
packet := &rtp.Packet{Header: rtp.Header{Timestamp: now()}, Payload: buf}
_ = c.track.WriteRTP(packet)
c.receiver.WriteRTP(packet)
atomic.AddUint32(&c.recv, uint32(len(buf)))
c.recv += len(buf)
req := c.res.Request
@@ -61,10 +61,12 @@ func (c *Client) startJPEG() error {
return err
}
packet = &rtp.Packet{Header: rtp.Header{Timestamp: now()}, Payload: buf}
_ = c.track.WriteRTP(packet)
if c.receiver != nil {
packet = &rtp.Packet{Header: rtp.Header{Timestamp: now()}, Payload: buf}
c.receiver.WriteRTP(packet)
}
atomic.AddUint32(&c.recv, uint32(len(buf)))
c.recv += len(buf)
}
return nil
@@ -109,10 +111,12 @@ func (c *Client) startMJPEG(boundary string) error {
return err
}
packet := &rtp.Packet{Header: rtp.Header{Timestamp: now()}, Payload: buf}
_ = c.track.WriteRTP(packet)
if c.receiver != nil {
packet := &rtp.Packet{Header: rtp.Header{Timestamp: now()}, Payload: buf}
c.receiver.WriteRTP(packet)
}
atomic.AddUint32(&c.recv, uint32(len(buf)))
c.recv += len(buf)
if _, err = r.Discard(2); err != nil {
return err

View File

@@ -2,52 +2,71 @@ package mjpeg
import (
"encoding/json"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/pion/rtp"
"sync/atomic"
)
type Consumer struct {
streamer.Element
core.Listener
UserAgent string
RemoteAddr string
codecs []*streamer.Codec
start bool
medias []*core.Media
sender *core.Sender
send uint32
send int
}
func (c *Consumer) GetMedias() []*streamer.Media {
return []*streamer.Media{{
Kind: streamer.KindVideo,
Direction: streamer.DirectionRecvonly,
Codecs: []*streamer.Codec{{Name: streamer.CodecJPEG}},
}}
func (c *Consumer) GetMedias() []*core.Media {
if c.medias == nil {
c.medias = []*core.Media{
{
Kind: core.KindVideo,
Direction: core.DirectionSendonly,
Codecs: []*core.Codec{
{Name: core.CodecJPEG},
},
},
}
}
return c.medias
}
func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.Track {
push := func(packet *rtp.Packet) error {
c.Fire(packet.Payload)
atomic.AddUint32(&c.send, uint32(len(packet.Payload)))
return nil
func (c *Consumer) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiver) error {
if c.sender == nil {
c.sender = core.NewSender(media, track.Codec)
c.sender.Handler = func(packet *rtp.Packet) {
c.Fire(packet.Payload)
c.send += len(packet.Payload)
}
if track.Codec.IsRTP() {
c.sender.Handler = RTPDepay(c.sender.Handler)
}
}
if track.Codec.IsRTP() {
wrapper := RTPDepay(track)
push = wrapper(push)
}
c.sender.HandleRTP(track)
return nil
}
return track.Bind(push)
func (c *Consumer) Stop() error {
if c.sender != nil {
c.sender.Close()
}
return nil
}
func (c *Consumer) MarshalJSON() ([]byte, error) {
info := &streamer.Info{
Type: "MJPEG client",
info := &core.Info{
Type: "MJPEG passive consumer",
RemoteAddr: c.RemoteAddr,
UserAgent: c.UserAgent,
Send: atomic.LoadUint32(&c.send),
Medias: c.medias,
Send: c.send,
}
if c.sender != nil {
info.Senders = []*core.Sender{c.sender}
}
return json.Marshal(info)
}

View File

@@ -3,19 +3,18 @@ package mjpeg
import (
"encoding/json"
"errors"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/AlexxIT/go2rtc/pkg/core"
"strings"
"sync/atomic"
)
func (c *Client) GetMedias() []*streamer.Media {
func (c *Client) GetMedias() []*core.Media {
if c.medias == nil {
c.medias = []*streamer.Media{{
Kind: streamer.KindVideo,
Direction: streamer.DirectionSendonly,
Codecs: []*streamer.Codec{
c.medias = []*core.Media{{
Kind: core.KindVideo,
Direction: core.DirectionRecvonly,
Codecs: []*core.Codec{
{
Name: streamer.CodecJPEG, ClockRate: 90000, PayloadType: streamer.PayloadTypeRAW,
Name: core.CodecJPEG, ClockRate: 90000, PayloadType: core.PayloadTypeRAW,
},
},
}}
@@ -23,11 +22,11 @@ func (c *Client) GetMedias() []*streamer.Media {
return c.medias
}
func (c *Client) GetTrack(media *streamer.Media, codec *streamer.Codec) *streamer.Track {
if c.track == nil {
c.track = streamer.NewTrack(media, codec)
func (c *Client) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) {
if c.receiver == nil {
c.receiver = core.NewReceiver(media, codec)
}
return c.track
return c.receiver, nil
}
func (c *Client) Start() error {
@@ -46,6 +45,9 @@ func (c *Client) Start() error {
}
func (c *Client) Stop() error {
if c.receiver != nil {
c.receiver.Close()
}
// important for close reader/writer gorutines
_ = c.res.Body.Close()
c.closed = true
@@ -53,12 +55,16 @@ func (c *Client) Stop() error {
}
func (c *Client) MarshalJSON() ([]byte, error) {
info := &streamer.Info{
Type: "MJPEG source",
info := &core.Info{
Type: "MJPEG active producer",
URL: c.res.Request.URL.String(),
RemoteAddr: c.RemoteAddr,
UserAgent: c.UserAgent,
Recv: atomic.LoadUint32(&c.recv),
Medias: c.medias,
Recv: c.recv,
}
if c.receiver != nil {
info.Receivers = []*core.Receiver{c.receiver}
}
return json.Marshal(info)
}

View File

@@ -3,86 +3,84 @@ package mjpeg
import (
"bytes"
"encoding/binary"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/pion/rtp"
"image"
"image/jpeg"
)
func RTPDepay(track *streamer.Track) streamer.WrapperFunc {
func RTPDepay(handlerFunc core.HandlerFunc) core.HandlerFunc {
buf := make([]byte, 0, 512*1024) // 512K
return func(push streamer.WriterFunc) streamer.WriterFunc {
return func(packet *rtp.Packet) error {
//log.Printf("[RTP] codec: %s, size: %6d, ts: %10d, pt: %2d, ssrc: %d, seq: %d, mark: %v", track.Codec.Name, len(packet.Payload), packet.Timestamp, packet.PayloadType, packet.SSRC, packet.SequenceNumber, packet.Marker)
return func(packet *rtp.Packet) {
//log.Printf("[RTP] codec: %s, size: %6d, ts: %10d, pt: %2d, ssrc: %d, seq: %d, mark: %v", track.Codec.Name, len(packet.Payload), packet.Timestamp, packet.PayloadType, packet.SSRC, packet.SequenceNumber, packet.Marker)
// https://www.rfc-editor.org/rfc/rfc2435#section-3.1
b := packet.Payload
// https://www.rfc-editor.org/rfc/rfc2435#section-3.1
b := packet.Payload
// 3.1. JPEG header
t := b[4]
// 3.1. JPEG header
t := b[4]
// 3.1.7. Restart Marker header
if 64 <= t && t <= 127 {
b = b[12:] // skip it
} else {
b = b[8:]
}
if len(buf) == 0 {
var lqt, cqt []byte
// 3.1.8. Quantization Table header
q := packet.Payload[5]
if q >= 128 {
lqt = b[4:68]
cqt = b[68:132]
b = b[132:]
} else {
lqt, cqt = MakeTables(q)
}
// https://www.rfc-editor.org/rfc/rfc2435#section-3.1.5
// The maximum width is 2040 pixels.
w := uint16(packet.Payload[6]) << 3
h := uint16(packet.Payload[7]) << 3
// fix sizes more than 2040
switch {
// 512x1920 512x1440
case w == cutSize(2560) && (h == 1920 || h == 1440):
w = 2560
// 1792x112
case w == cutSize(3840) && h == cutSize(2160):
w = 3840
h = 2160
// 256x1296
case w == cutSize(2304) && h == 1296:
w = 2304
}
//fmt.Printf("t: %d, q: %d, w: %d, h: %d\n", t, q, w, h)
buf = MakeHeaders(buf, t, w, h, lqt, cqt)
}
// 3.1.9. JPEG Payload
buf = append(buf, b...)
if !packet.Marker {
return nil
}
if end := buf[len(buf)-2:]; end[0] != 0xFF && end[1] != 0xD9 {
buf = append(buf, 0xFF, 0xD9)
}
clone := *packet
clone.Payload = buf
buf = buf[:0] // clear buffer
return push(&clone)
// 3.1.7. Restart Marker header
if 64 <= t && t <= 127 {
b = b[12:] // skip it
} else {
b = b[8:]
}
if len(buf) == 0 {
var lqt, cqt []byte
// 3.1.8. Quantization Table header
q := packet.Payload[5]
if q >= 128 {
lqt = b[4:68]
cqt = b[68:132]
b = b[132:]
} else {
lqt, cqt = MakeTables(q)
}
// https://www.rfc-editor.org/rfc/rfc2435#section-3.1.5
// The maximum width is 2040 pixels.
w := uint16(packet.Payload[6]) << 3
h := uint16(packet.Payload[7]) << 3
// fix sizes more than 2040
switch {
// 512x1920 512x1440
case w == cutSize(2560) && (h == 1920 || h == 1440):
w = 2560
// 1792x112
case w == cutSize(3840) && h == cutSize(2160):
w = 3840
h = 2160
// 256x1296
case w == cutSize(2304) && h == 1296:
w = 2304
}
//fmt.Printf("t: %d, q: %d, w: %d, h: %d\n", t, q, w, h)
buf = MakeHeaders(buf, t, w, h, lqt, cqt)
}
// 3.1.9. JPEG Payload
buf = append(buf, b...)
if !packet.Marker {
return
}
if end := buf[len(buf)-2:]; end[0] != 0xFF && end[1] != 0xD9 {
buf = append(buf, 0xFF, 0xD9)
}
clone := *packet
clone.Payload = buf
buf = buf[:0] // clear buffer
handlerFunc(&clone)
}
}
@@ -90,102 +88,96 @@ func cutSize(size uint16) uint16 {
return ((size >> 3) & 0xFF) << 3
}
func RTPPay() streamer.WrapperFunc {
func RTPPay(handlerFunc core.HandlerFunc) core.HandlerFunc {
const packetSize = 1436
sequencer := rtp.NewRandomSequencer()
return func(push streamer.WriterFunc) streamer.WriterFunc {
return func(packet *rtp.Packet) error {
// reincode image to more common form
p, err := Transcode(packet.Payload)
if err != nil {
return err
return func(packet *rtp.Packet) {
// reincode image to more common form
p, err := Transcode(packet.Payload)
if err != nil {
return
}
h1 := make([]byte, 8)
h1[4] = 1 // Type
h1[5] = 255 // Q
// MBZ=0, Precision=0, Length=128
h2 := make([]byte, 4, 132)
h2[3] = 128
var jpgData []byte
for jpgData == nil {
// 2 bytes h1
if p[0] != 0xFF {
return
}
h1 := make([]byte, 8)
h1[4] = 1 // Type
h1[5] = 255 // Q
size := binary.BigEndian.Uint16(p[2:]) + 2
// MBZ=0, Precision=0, Length=128
h2 := make([]byte, 4, 132)
h2[3] = 128
var jpgData []byte
for jpgData == nil {
// 2 bytes h1
if p[0] != 0xFF {
return nil
// 2 bytes payload size (include 2 bytes)
switch p[1] {
case 0xD8: // 0. Start Of Image (size=0)
p = p[2:]
continue
case 0xDB: // 1. Define Quantization Table (size=130)
for i := uint16(4 + 1); i < size; i += 1 + 64 {
h2 = append(h2, p[i:i+64]...)
}
size := binary.BigEndian.Uint16(p[2:]) + 2
// 2 bytes payload size (include 2 bytes)
switch p[1] {
case 0xD8: // 0. Start Of Image (size=0)
p = p[2:]
continue
case 0xDB: // 1. Define Quantization Table (size=130)
for i := uint16(4 + 1); i < size; i += 1 + 64 {
h2 = append(h2, p[i:i+64]...)
}
case 0xC0: // 2. Start Of Frame (size=15)
if p[4] != 8 {
return nil
}
h := binary.BigEndian.Uint16(p[5:])
w := binary.BigEndian.Uint16(p[7:])
h1[6] = uint8(w >> 3)
h1[7] = uint8(h >> 3)
case 0xC4: // 3. Define Huffman Table (size=416)
case 0xDA: // 4. Start Of Scan (size=10)
jpgData = p[size:]
case 0xC0: // 2. Start Of Frame (size=15)
if p[4] != 8 {
return
}
p = p[size:]
h := binary.BigEndian.Uint16(p[5:])
w := binary.BigEndian.Uint16(p[7:])
h1[6] = uint8(w >> 3)
h1[7] = uint8(h >> 3)
case 0xC4: // 3. Define Huffman Table (size=416)
case 0xDA: // 4. Start Of Scan (size=10)
jpgData = p[size:]
}
offset := 0
p = make([]byte, 0)
p = p[size:]
}
for jpgData != nil {
p = p[:0]
offset := 0
p = make([]byte, 0)
if offset > 0 {
h1[1] = byte(offset >> 16)
h1[2] = byte(offset >> 8)
h1[3] = byte(offset)
p = append(p, h1...)
} else {
p = append(p, h1...)
p = append(p, h2...)
}
for jpgData != nil {
p = p[:0]
dataLen := packetSize - len(p)
if dataLen < len(jpgData) {
p = append(p, jpgData[:dataLen]...)
jpgData = jpgData[dataLen:]
offset += dataLen
} else {
p = append(p, jpgData...)
jpgData = nil
}
clone := rtp.Packet{
Header: rtp.Header{
Version: 2,
Marker: jpgData == nil,
SequenceNumber: sequencer.NextSequenceNumber(),
Timestamp: packet.Timestamp,
},
Payload: p,
}
if err := push(&clone); err != nil {
return err
}
if offset > 0 {
h1[1] = byte(offset >> 16)
h1[2] = byte(offset >> 8)
h1[3] = byte(offset)
p = append(p, h1...)
} else {
p = append(p, h1...)
p = append(p, h2...)
}
return nil
dataLen := packetSize - len(p)
if dataLen < len(jpgData) {
p = append(p, jpgData[:dataLen]...)
jpgData = jpgData[dataLen:]
offset += dataLen
} else {
p = append(p, jpgData...)
jpgData = nil
}
clone := rtp.Packet{
Header: rtp.Header{
Version: 2,
Marker: jpgData == nil,
SequenceNumber: sequencer.NextSequenceNumber(),
Timestamp: packet.Timestamp,
},
Payload: p,
}
handlerFunc(&clone)
}
}
}

View File

@@ -3,176 +3,165 @@ package mp4
import (
"encoding/json"
"github.com/AlexxIT/go2rtc/pkg/aac"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/h264"
"github.com/AlexxIT/go2rtc/pkg/h265"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/pion/rtp"
"sync/atomic"
)
type Consumer struct {
streamer.Element
core.Listener
Medias []*streamer.Media
Medias []*core.Media
UserAgent string
RemoteAddr string
muxer *Muxer
codecs []*streamer.Codec
wait byte
senders []*core.Sender
send uint32
muxer *Muxer
wait byte
send int
}
// ParseQuery - like usual parse, but with mp4 param handler
func ParseQuery(query map[string][]string) []*streamer.Media {
if query["mp4"] != nil {
cons := Consumer{}
return cons.GetMedias()
}
return streamer.ParseQuery(query)
}
const (
waitNone byte = iota
waitKeyframe
waitInit
)
func (c *Consumer) GetMedias() []*streamer.Media {
if c.Medias != nil {
return c.Medias
}
// default medias
return []*streamer.Media{
{
Kind: streamer.KindVideo,
Direction: streamer.DirectionRecvonly,
Codecs: []*streamer.Codec{
{Name: streamer.CodecH264},
{Name: streamer.CodecH265},
func (c *Consumer) GetMedias() []*core.Media {
if c.Medias == nil {
// default local medias
c.Medias = []*core.Media{
{
Kind: core.KindVideo,
Direction: core.DirectionSendonly,
Codecs: []*core.Codec{
{Name: core.CodecH264},
{Name: core.CodecH265},
},
},
},
{
Kind: streamer.KindAudio,
Direction: streamer.DirectionRecvonly,
Codecs: []*streamer.Codec{
{Name: streamer.CodecAAC},
{
Kind: core.KindAudio,
Direction: core.DirectionSendonly,
Codecs: []*core.Codec{
{Name: core.CodecAAC},
},
},
},
}
}
return c.Medias
}
func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.Track {
trackID := byte(len(c.codecs))
c.codecs = append(c.codecs, track.Codec)
func (c *Consumer) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiver) error {
trackID := byte(len(c.senders))
codec := track.Codec
switch codec.Name {
case streamer.CodecH264:
handler := core.NewSender(media, track.Codec)
switch track.Codec.Name {
case core.CodecH264:
c.wait = waitInit
push := func(packet *rtp.Packet) error {
handler.Handler = func(packet *rtp.Packet) {
if packet.Version != h264.RTPPacketVersionAVC {
return nil
return
}
if c.wait != waitNone {
if c.wait == waitInit || !h264.IsKeyframe(packet.Payload) {
return nil
return
}
c.wait = waitNone
}
buf := c.muxer.Marshal(trackID, packet)
atomic.AddUint32(&c.send, uint32(len(buf)))
c.Fire(buf)
return nil
c.send += len(buf)
}
var wrapper streamer.WrapperFunc
if codec.IsRTP() {
wrapper = h264.RTPDepay(track)
if track.Codec.IsRTP() {
handler.Handler = h264.RTPDepay(track.Codec, handler.Handler)
} else {
wrapper = h264.RepairAVC(track)
handler.Handler = h264.RepairAVC(track.Codec, handler.Handler)
}
push = wrapper(push)
return track.Bind(push)
case streamer.CodecH265:
case core.CodecH265:
c.wait = waitInit
push := func(packet *rtp.Packet) error {
handler.Handler = func(packet *rtp.Packet) {
if packet.Version != h264.RTPPacketVersionAVC {
return nil
return
}
if c.wait != waitNone {
if c.wait == waitInit || !h265.IsKeyframe(packet.Payload) {
return nil
return
}
c.wait = waitNone
}
buf := c.muxer.Marshal(trackID, packet)
atomic.AddUint32(&c.send, uint32(len(buf)))
c.Fire(buf)
return nil
c.send += len(buf)
}
if codec.IsRTP() {
wrapper := h265.RTPDepay(track)
push = wrapper(push)
if track.Codec.IsRTP() {
handler.Handler = h265.RTPDepay(track.Codec, handler.Handler)
}
return track.Bind(push)
case streamer.CodecAAC:
push := func(packet *rtp.Packet) error {
case core.CodecAAC:
handler.Handler = func(packet *rtp.Packet) {
if c.wait != waitNone {
return nil
return
}
buf := c.muxer.Marshal(trackID, packet)
atomic.AddUint32(&c.send, uint32(len(buf)))
c.Fire(buf)
return nil
c.send += len(buf)
}
if codec.IsRTP() {
wrapper := aac.RTPDepay(track)
push = wrapper(push)
if track.Codec.IsRTP() {
handler.Handler = aac.RTPDepay(handler.Handler)
}
return track.Bind(push)
case streamer.CodecOpus, streamer.CodecMP3, streamer.CodecPCMU, streamer.CodecPCMA:
push := func(packet *rtp.Packet) error {
case core.CodecOpus, core.CodecMP3, core.CodecPCMU, core.CodecPCMA:
handler.Handler = func(packet *rtp.Packet) {
if c.wait != waitNone {
return nil
return
}
buf := c.muxer.Marshal(trackID, packet)
atomic.AddUint32(&c.send, uint32(len(buf)))
c.Fire(buf)
return nil
c.send += len(buf)
}
return track.Bind(push)
default:
panic("unsupported codec")
}
panic("unsupported codec")
handler.HandleRTP(track)
c.senders = append(c.senders, handler)
return nil
}
func (c *Consumer) Stop() error {
for _, sender := range c.senders {
sender.Close()
}
return nil
}
func (c *Consumer) Codecs() []*core.Codec {
codecs := make([]*core.Codec, len(c.senders))
for i, sender := range c.senders {
codecs[i] = sender.Codec
}
return codecs
}
func (c *Consumer) MimeCodecs() string {
return c.muxer.MimeCodecs(c.codecs)
return c.muxer.MimeCodecs(c.Codecs())
}
func (c *Consumer) MimeType() string {
@@ -181,7 +170,7 @@ func (c *Consumer) MimeType() string {
func (c *Consumer) Init() ([]byte, error) {
c.muxer = &Muxer{}
return c.muxer.GetInit(c.codecs)
return c.muxer.GetInit(c.Codecs())
}
func (c *Consumer) Start() {
@@ -190,14 +179,14 @@ func (c *Consumer) Start() {
}
}
//
func (c *Consumer) MarshalJSON() ([]byte, error) {
info := &streamer.Info{
Type: "MP4 client",
info := &core.Info{
Type: "MP4 passive consumer",
RemoteAddr: c.RemoteAddr,
UserAgent: c.UserAgent,
Send: atomic.LoadUint32(&c.send),
Medias: c.Medias,
Senders: c.senders,
Send: c.send,
}
return json.Marshal(info)
}

19
pkg/mp4/helpers.go Normal file
View File

@@ -0,0 +1,19 @@
package mp4
import "github.com/AlexxIT/go2rtc/pkg/core"
// ParseQuery - like usual parse, but with mp4 param handler
func ParseQuery(query map[string][]string) []*core.Media {
if query["mp4"] != nil {
cons := Consumer{}
return cons.GetMedias()
}
return core.ParseQuery(query)
}
const (
waitNone byte = iota
waitKeyframe
waitInit
)

View File

@@ -2,10 +2,10 @@ package mp4
import (
"encoding/hex"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/h264"
"github.com/AlexxIT/go2rtc/pkg/h265"
"github.com/AlexxIT/go2rtc/pkg/iso"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/deepch/vdk/codec/h264parser"
"github.com/deepch/vdk/codec/h265parser"
"github.com/pion/rtp"
@@ -24,7 +24,7 @@ const (
MimeOpus = "opus"
)
func (m *Muxer) MimeCodecs(codecs []*streamer.Codec) string {
func (m *Muxer) MimeCodecs(codecs []*core.Codec) string {
var s string
for i, codec := range codecs {
@@ -33,15 +33,15 @@ func (m *Muxer) MimeCodecs(codecs []*streamer.Codec) string {
}
switch codec.Name {
case streamer.CodecH264:
case core.CodecH264:
s += "avc1." + h264.GetProfileLevelID(codec.FmtpLine)
case streamer.CodecH265:
case core.CodecH265:
// H.265 profile=main level=5.1
// hvc1 - supported in Safari, hev1 - doesn't, both supported in Chrome
s += MimeH265
case streamer.CodecAAC:
case core.CodecAAC:
s += MimeAAC
case streamer.CodecOpus:
case core.CodecOpus:
s += MimeOpus
}
}
@@ -49,7 +49,7 @@ func (m *Muxer) MimeCodecs(codecs []*streamer.Codec) string {
return s
}
func (m *Muxer) GetInit(codecs []*streamer.Codec) ([]byte, error) {
func (m *Muxer) GetInit(codecs []*core.Codec) ([]byte, error) {
mv := iso.NewMovie(1024)
mv.WriteFileType()
@@ -58,7 +58,7 @@ func (m *Muxer) GetInit(codecs []*streamer.Codec) ([]byte, error) {
for i, codec := range codecs {
switch codec.Name {
case streamer.CodecH264:
case core.CodecH264:
sps, pps := h264.GetParameterSet(codec.FmtpLine)
if sps == nil {
// some dummy SPS and PPS not a problem
@@ -77,7 +77,7 @@ func (m *Muxer) GetInit(codecs []*streamer.Codec) ([]byte, error) {
codecData.AVCDecoderConfRecordBytes(),
)
case streamer.CodecH265:
case core.CodecH265:
vps, sps, pps := h265.GetParameterSet(codec.FmtpLine)
if sps == nil {
// some dummy SPS and PPS not a problem
@@ -97,8 +97,8 @@ func (m *Muxer) GetInit(codecs []*streamer.Codec) ([]byte, error) {
codecData.AVCDecoderConfRecordBytes(),
)
case streamer.CodecAAC:
s := streamer.Between(codec.FmtpLine, "config=", ";")
case core.CodecAAC:
s := core.Between(codec.FmtpLine, "config=", ";")
b, err := hex.DecodeString(s)
if err != nil {
return nil, err
@@ -108,7 +108,7 @@ func (m *Muxer) GetInit(codecs []*streamer.Codec) ([]byte, error) {
uint32(i+1), codec.Name, codec.ClockRate, codec.Channels, b,
)
case streamer.CodecOpus, streamer.CodecMP3, streamer.CodecPCMU, streamer.CodecPCMA:
case core.CodecOpus, core.CodecMP3, core.CodecPCMU, core.CodecPCMA:
mv.WriteAudioTrack(
uint32(i+1), codec.Name, codec.ClockRate, codec.Channels, nil,
)

View File

@@ -2,48 +2,49 @@ package mp4
import (
"encoding/json"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/h264"
"github.com/AlexxIT/go2rtc/pkg/h265"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/pion/rtp"
"sync/atomic"
)
type Segment struct {
streamer.Element
core.Listener
Medias []*streamer.Media
Medias []*core.Media
UserAgent string
RemoteAddr string
senders []*core.Sender
MimeType string
OnlyKeyframe bool
send uint32
send int
}
func (c *Segment) GetMedias() []*streamer.Media {
func (c *Segment) GetMedias() []*core.Media {
if c.Medias != nil {
return c.Medias
}
// default medias
return []*streamer.Media{
// default local medias
return []*core.Media{
{
Kind: streamer.KindVideo,
Direction: streamer.DirectionRecvonly,
Codecs: []*streamer.Codec{
{Name: streamer.CodecH264},
{Name: streamer.CodecH265},
Kind: core.KindVideo,
Direction: core.DirectionSendonly,
Codecs: []*core.Codec{
{Name: core.CodecH264},
{Name: core.CodecH265},
},
},
}
}
func (c *Segment) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.Track {
func (c *Segment) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiver) error {
muxer := &Muxer{}
codecs := []*streamer.Codec{track.Codec}
codecs := []*core.Codec{track.Codec}
init, err := muxer.GetInit(codecs)
if err != nil {
@@ -52,26 +53,26 @@ func (c *Segment) AddTrack(media *streamer.Media, track *streamer.Track) *stream
c.MimeType = `video/mp4; codecs="` + muxer.MimeCodecs(codecs) + `"`
handler := core.NewSender(media, track.Codec)
switch track.Codec.Name {
case streamer.CodecH264:
var push streamer.WriterFunc
case core.CodecH264:
if c.OnlyKeyframe {
push = func(packet *rtp.Packet) error {
handler.Handler = func(packet *rtp.Packet) {
if !h264.IsKeyframe(packet.Payload) {
return nil
return
}
buf := muxer.Marshal(0, packet)
atomic.AddUint32(&c.send, uint32(len(buf)))
c.Fire(append(init, buf...))
return nil
c.send += len(buf)
}
} else {
var buf []byte
push = func(packet *rtp.Packet) error {
handler.Handler = func(packet *rtp.Packet) {
if h264.IsKeyframe(packet.Payload) {
// fist frame - send only IFrame
// other frames - send IFrame and all PFrames
@@ -81,9 +82,10 @@ func (c *Segment) AddTrack(media *streamer.Media, track *streamer.Track) *stream
buf = append(buf, b...)
}
atomic.AddUint32(&c.send, uint32(len(buf)))
c.Fire(buf)
c.send += len(buf)
buf = buf[:0]
buf = append(buf, init...)
muxer.Reset()
@@ -93,51 +95,56 @@ func (c *Segment) AddTrack(media *streamer.Media, track *streamer.Track) *stream
b := muxer.Marshal(0, packet)
buf = append(buf, b...)
}
return nil
}
}
var wrapper streamer.WrapperFunc
if track.Codec.IsRTP() {
wrapper = h264.RTPDepay(track)
handler.Handler = h264.RTPDepay(track.Codec, handler.Handler)
} else {
wrapper = h264.RepairAVC(track)
handler.Handler = h264.RepairAVC(track.Codec, handler.Handler)
}
push = wrapper(push)
return track.Bind(push)
case streamer.CodecH265:
push := func(packet *rtp.Packet) error {
case core.CodecH265:
handler.Handler = func(packet *rtp.Packet) {
if !h265.IsKeyframe(packet.Payload) {
return nil
return
}
buf := muxer.Marshal(0, packet)
atomic.AddUint32(&c.send, uint32(len(buf)))
c.Fire(append(init, buf...))
return nil
c.send += len(buf)
}
if track.Codec.IsRTP() {
wrapper := h265.RTPDepay(track)
push = wrapper(push)
handler.Handler = h265.RTPDepay(track.Codec, handler.Handler)
}
return track.Bind(push)
default:
panic(core.UnsupportedCodec)
}
panic("unsupported codec")
handler.HandleRTP(track)
c.senders = append(c.senders, handler)
return nil
}
func (c *Segment) Stop() error {
for _, sender := range c.senders {
sender.Close()
}
return nil
}
func (c *Segment) MarshalJSON() ([]byte, error) {
info := &streamer.Info{
Type: "WS/MP4 client",
info := &core.Info{
Type: "MP4/WebSocket passive consumer",
RemoteAddr: c.RemoteAddr,
UserAgent: c.UserAgent,
Send: atomic.LoadUint32(&c.send),
Medias: c.Medias,
Senders: c.senders,
Send: c.send,
}
return json.Marshal(info)
}

View File

@@ -3,8 +3,8 @@ package mp4
import (
"encoding/hex"
"github.com/AlexxIT/go2rtc/pkg/aac"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/h264"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/deepch/vdk/av"
"github.com/deepch/vdk/codec/aacparser"
"github.com/deepch/vdk/codec/h264parser"
@@ -14,9 +14,9 @@ import (
)
type Consumer struct {
streamer.Element
core.Listener
Medias []*streamer.Media
Medias []*core.Media
UserAgent string
RemoteAddr string
@@ -28,35 +28,35 @@ type Consumer struct {
send int
}
func (c *Consumer) GetMedias() []*streamer.Media {
func (c *Consumer) GetMedias() []*core.Media {
if c.Medias != nil {
return c.Medias
}
return []*streamer.Media{
return []*core.Media{
{
Kind: streamer.KindVideo,
Direction: streamer.DirectionRecvonly,
Codecs: []*streamer.Codec{
{Name: streamer.CodecH264, ClockRate: 90000},
Kind: core.KindVideo,
Direction: core.DirectionSendonly,
Codecs: []*core.Codec{
{Name: core.CodecH264, ClockRate: 90000},
},
},
{
Kind: streamer.KindAudio,
Direction: streamer.DirectionRecvonly,
Codecs: []*streamer.Codec{
{Name: streamer.CodecAAC, ClockRate: 16000},
Kind: core.KindAudio,
Direction: core.DirectionSendonly,
Codecs: []*core.Codec{
{Name: core.CodecAAC, ClockRate: 16000},
},
},
}
}
func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.Track {
func (c *Consumer) AddTrack(media *core.Media, track *core.Track) *core.Track {
codec := track.Codec
trackID := int8(len(c.streams))
switch codec.Name {
case streamer.CodecH264:
case core.CodecH264:
sps, pps := h264.GetParameterSet(codec.FmtpLine)
stream, err := h264parser.NewCodecDataFromSPSAndPPS(sps, pps)
if err != nil {
@@ -102,8 +102,8 @@ func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *strea
return track.Bind(push)
case streamer.CodecAAC:
s := streamer.Between(codec.FmtpLine, "config=", ";")
case core.CodecAAC:
s := core.Between(codec.FmtpLine, "config=", ";")
b, err := hex.DecodeString(s)
if err != nil {

View File

@@ -3,6 +3,7 @@ package mp4
import (
"encoding/json"
"github.com/AlexxIT/go2rtc/pkg/aac"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/h264"
"github.com/AlexxIT/go2rtc/pkg/h265"
"github.com/AlexxIT/go2rtc/pkg/streamer"
@@ -11,14 +12,14 @@ import (
)
type Consumer struct {
streamer.Element
core.Listener
Medias []*streamer.Media
Medias []*core.Media
UserAgent string
RemoteAddr string
muxer *Muxer
codecs []*streamer.Codec
codecs []*core.Codec
wait byte
send uint32
@@ -30,38 +31,38 @@ const (
waitInit
)
func (c *Consumer) GetMedias() []*streamer.Media {
func (c *Consumer) GetMedias() []*core.Media {
if c.Medias != nil {
return c.Medias
}
// default medias
return []*streamer.Media{
return []*core.Media{
{
Kind: streamer.KindVideo,
Direction: streamer.DirectionRecvonly,
Codecs: []*streamer.Codec{
{Name: streamer.CodecH264},
{Name: streamer.CodecH265},
Kind: core.KindVideo,
Direction: core.DirectionSendonly,
Codecs: []*core.Codec{
{Name: core.CodecH264},
{Name: core.CodecH265},
},
},
{
Kind: streamer.KindAudio,
Direction: streamer.DirectionRecvonly,
Codecs: []*streamer.Codec{
{Name: streamer.CodecAAC},
Kind: core.KindAudio,
Direction: core.DirectionSendonly,
Codecs: []*core.Codec{
{Name: core.CodecAAC},
},
},
}
}
func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.Track {
func (c *Consumer) AddTrack(media *core.Media, track *core.Track) *core.Track {
trackID := byte(len(c.codecs))
c.codecs = append(c.codecs, track.Codec)
codec := track.Codec
switch codec.Name {
case streamer.CodecH264:
case core.CodecH264:
c.wait = waitInit
push := func(packet *rtp.Packet) error {
@@ -93,7 +94,7 @@ func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *strea
return track.Bind(push)
case streamer.CodecH265:
case core.CodecH265:
c.wait = waitInit
push := func(packet *rtp.Packet) error {
@@ -164,7 +165,7 @@ func (c *Consumer) Start() {
//
func (c *Consumer) MarshalJSON() ([]byte, error) {
info := &streamer.Info{
info := &core.Info{
Type: "MP4 client",
RemoteAddr: c.RemoteAddr,
UserAgent: c.UserAgent,

View File

@@ -2,17 +2,17 @@ package mp4
import (
"encoding/json"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/h264"
"github.com/AlexxIT/go2rtc/pkg/h265"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/pion/rtp"
"sync/atomic"
)
type Segment struct {
streamer.Element
core.Listener
Medias []*streamer.Media
Medias []*core.Media
UserAgent string
RemoteAddr string
@@ -22,28 +22,28 @@ type Segment struct {
send uint32
}
func (c *Segment) GetMedias() []*streamer.Media {
func (c *Segment) GetMedias() []*core.Media {
if c.Medias != nil {
return c.Medias
}
// default medias
return []*streamer.Media{
return []*core.Media{
{
Kind: streamer.KindVideo,
Direction: streamer.DirectionRecvonly,
Codecs: []*streamer.Codec{
{Name: streamer.CodecH264},
{Name: streamer.CodecH265},
Kind: core.KindVideo,
Direction: core.DirectionSendonly,
Codecs: []*core.Codec{
{Name: core.CodecH264},
{Name: core.CodecH265},
},
},
}
}
func (c *Segment) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.Track {
func (c *Segment) AddTrack(media *core.Media, track *core.Track) *core.Track {
muxer := &Muxer{}
codecs := []*streamer.Codec{track.Codec}
codecs := []*core.Codec{track.Codec}
init, err := muxer.GetInit(codecs)
if err != nil {
@@ -53,8 +53,8 @@ func (c *Segment) AddTrack(media *streamer.Media, track *streamer.Track) *stream
c.MimeType = muxer.MimeType(codecs)
switch track.Codec.Name {
case streamer.CodecH264:
var push streamer.WriterFunc
case core.CodecH264:
var push core.WriterFunc
if c.OnlyKeyframe {
push = func(packet *rtp.Packet) error {
@@ -98,7 +98,7 @@ func (c *Segment) AddTrack(media *streamer.Media, track *streamer.Track) *stream
}
}
var wrapper streamer.WrapperFunc
var wrapper core.WrapperFunc
if track.Codec.IsRTP() {
wrapper = h264.RTPDepay(track)
} else {
@@ -108,7 +108,7 @@ func (c *Segment) AddTrack(media *streamer.Media, track *streamer.Track) *stream
return track.Bind(push)
case streamer.CodecH265:
case core.CodecH265:
push := func(packet *rtp.Packet) error {
if !h265.IsKeyframe(packet.Payload) {
return nil
@@ -133,7 +133,7 @@ func (c *Segment) AddTrack(media *streamer.Media, track *streamer.Track) *stream
}
func (c *Segment) MarshalJSON() ([]byte, error) {
info := &streamer.Info{
info := &core.Info{
Type: "WS/MP4 client",
RemoteAddr: c.RemoteAddr,
UserAgent: c.UserAgent,

View File

@@ -3,9 +3,9 @@ package mp4
import (
"encoding/binary"
"encoding/hex"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/h264"
"github.com/AlexxIT/go2rtc/pkg/h265"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/deepch/vdk/av"
"github.com/deepch/vdk/codec/h264parser"
"github.com/deepch/vdk/codec/h265parser"
@@ -21,7 +21,7 @@ type Muxer struct {
pts []uint32
}
func (m *Muxer) MimeType(codecs []*streamer.Codec) string {
func (m *Muxer) MimeType(codecs []*core.Codec) string {
s := `video/mp4; codecs="`
for i, codec := range codecs {
@@ -30,13 +30,13 @@ func (m *Muxer) MimeType(codecs []*streamer.Codec) string {
}
switch codec.Name {
case streamer.CodecH264:
case core.CodecH264:
s += "avc1." + h264.GetProfileLevelID(codec.FmtpLine)
case streamer.CodecH265:
case core.CodecH265:
// H.265 profile=main level=5.1
// hvc1 - supported in Safari, hev1 - doesn't, both supported in Chrome
s += "hvc1.1.6.L153.B0"
case streamer.CodecAAC:
case core.CodecAAC:
s += "mp4a.40.2"
}
}
@@ -44,12 +44,12 @@ func (m *Muxer) MimeType(codecs []*streamer.Codec) string {
return s + `"`
}
func (m *Muxer) GetInit(codecs []*streamer.Codec) ([]byte, error) {
func (m *Muxer) GetInit(codecs []*core.Codec) ([]byte, error) {
moov := MOOV()
for i, codec := range codecs {
switch codec.Name {
case streamer.CodecH264:
case core.CodecH264:
sps, pps := h264.GetParameterSet(codec.FmtpLine)
if sps == nil {
// some dummy SPS and PPS not a problem
@@ -92,7 +92,7 @@ func (m *Muxer) GetInit(codecs []*streamer.Codec) ([]byte, error) {
moov.Tracks = append(moov.Tracks, trak)
case streamer.CodecH265:
case core.CodecH265:
vps, sps, pps := h265.GetParameterSet(codec.FmtpLine)
if sps == nil {
// some dummy SPS and PPS not a problem
@@ -136,8 +136,8 @@ func (m *Muxer) GetInit(codecs []*streamer.Codec) ([]byte, error) {
moov.Tracks = append(moov.Tracks, trak)
case streamer.CodecAAC:
s := streamer.Between(codec.FmtpLine, "config=", ";")
case core.CodecAAC:
s := core.Between(codec.FmtpLine, "config=", ";")
b, err := hex.DecodeString(s)
if err != nil {
return nil, err

View File

@@ -1,17 +1,19 @@
package mpegts
import (
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/AlexxIT/go2rtc/pkg/core"
"net/http"
)
type Client struct {
streamer.Element
core.Listener
medias []*streamer.Media
tracks map[byte]*streamer.Track
medias []*core.Media
receivers []*core.Receiver
res *http.Response
recv int
}
func NewClient(res *http.Response) *Client {
@@ -19,46 +21,50 @@ func NewClient(res *http.Response) *Client {
}
func (c *Client) Handle() error {
if c.tracks == nil {
c.tracks = map[byte]*streamer.Track{}
}
reader := NewReader()
b := make([]byte, 1024*1024*256) // 256K
probe := streamer.NewProbe(c.medias == nil)
probe := core.NewProbe(c.medias == nil)
for probe == nil || probe.Active() {
n, err := c.res.Body.Read(b)
if err != nil {
return err
}
c.recv += n
reader.AppendBuffer(b[:n])
reading:
for {
packet := reader.GetPacket()
if packet == nil {
break
}
track := c.tracks[packet.PayloadType]
if track == nil {
// count track on probe state even if not support it
probe.Append(packet.PayloadType)
media := GetMedia(packet)
if media == nil {
continue // unsupported codec
for _, receiver := range c.receivers {
if receiver.ID == packet.PayloadType {
receiver.WriteRTP(packet)
continue reading
}
track = streamer.NewTrack(media, nil)
c.medias = append(c.medias, media)
c.tracks[packet.PayloadType] = track
}
_ = track.WriteRTP(packet)
// count track on probe state even if not support it
probe.Append(packet.PayloadType)
media := GetMedia(packet)
if media == nil {
continue // unsupported codec
}
c.medias = append(c.medias, media)
receiver := core.NewReceiver(media, media.Codecs[0])
receiver.ID = packet.PayloadType
c.receivers = append(c.receivers, receiver)
receiver.WriteRTP(packet)
//log.Printf("[AVC] %v, len: %d, pts: %d ts: %10d", h264.Types(packet.Payload), len(packet.Payload), pkt.PTS, packet.Timestamp)
}

View File

@@ -1,8 +1,8 @@
package mpegts
import (
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/h264"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/pion/rtp"
"time"
)
@@ -13,7 +13,7 @@ const (
)
const (
StreamTypePrivate = 0x06 // PCMU or PCMA from FFmpeg
StreamTypePrivate = 0x06 // PCMU or PCMA or FLAC from FFmpeg
StreamTypeAAC = 0x0F
StreamTypeH264 = 0x1B
StreamTypePCMATapo = 0x90
@@ -153,34 +153,34 @@ func ParseTime(b []byte) uint32 {
return (uint32(b[0]&0x0E) << 29) | (uint32(b[1]) << 22) | (uint32(b[2]&0xFE) << 14) | (uint32(b[3]) << 7) | (uint32(b[4]) >> 1)
}
func GetMedia(pkt *rtp.Packet) *streamer.Media {
var codec *streamer.Codec
func GetMedia(pkt *rtp.Packet) *core.Media {
var codec *core.Codec
var kind string
switch pkt.PayloadType {
case StreamTypeH264:
codec = &streamer.Codec{
Name: streamer.CodecH264,
codec = &core.Codec{
Name: core.CodecH264,
ClockRate: 90000,
PayloadType: streamer.PayloadTypeRAW,
PayloadType: core.PayloadTypeRAW,
FmtpLine: h264.GetFmtpLine(pkt.Payload),
}
kind = streamer.KindVideo
kind = core.KindVideo
case StreamTypePCMATapo:
codec = &streamer.Codec{
Name: streamer.CodecPCMA,
codec = &core.Codec{
Name: core.CodecPCMA,
ClockRate: 8000,
}
kind = streamer.KindAudio
kind = core.KindAudio
default:
return nil
}
return &streamer.Media{
return &core.Media{
Kind: kind,
Direction: streamer.DirectionSendonly,
Codecs: []*streamer.Codec{codec},
Direction: core.DirectionRecvonly,
Codecs: []*core.Codec{codec},
}
}

View File

@@ -21,28 +21,28 @@ func dec(s string) []byte {
return b
}
func TestStream(t *testing.T) {
// ffmpeg
annexb := dec("00000001 09f0 00000001 6764001fac2484014016ec0440000003004000000c23c60c92 00000001 68ee32c8b0 000001 6588808003 00000001 09")
avc, i := ParseAVC(annexb)
assert.Equal(t, dec("00000019 6764001fac2484014016ec0440000003004000000c23c60c92 00000005 68ee32c8b0 00000005 6588808003"), avc)
assert.Equal(t, dec("00000001 09"), annexb[i:])
// http mpeg ts
annexb = dec("00000001 0950 000001 6764001facd2014016e8400000fa400030e081 000001 68ea8f2c 000001 65b8400eff 00000001 09")
avc, i = ParseAVC(annexb)
assert.Equal(t, dec("00000013 6764001facd2014016e8400000fa400030e081 00000004 68ea8f2c 00000005 65b8400eff"), avc)
assert.Equal(t, dec("00000001 09"), annexb[i:])
// tapo TC60
annexb = dec("00000001 67640028ac1ad00a00b74dc0404050000003001000000301e8f1422a 00000001 68ee04c92240 00000001 45b80000d0 00000001 67")
avc, i = ParseAVC(annexb)
assert.Equal(t, dec("0000001C 67640028ac1ad00a00b74dc0404050000003001000000301e8f1422a 00000006 68ee04c92240 00000005 45b80000d0"), avc)
assert.Equal(t, dec("00000001 67"), annexb[i:])
// Tapo ?
annexb = dec("00000001 674d0032e90048014742000007d2000138d108 00000001 68ea8f20 00000001 65b8400cff 00000001 67")
avc, i = ParseAVC(annexb)
assert.Equal(t, dec("00000013 674d0032e90048014742000007d2000138d108 00000004 68ea8f20 00000005 65b8400cff"), avc)
assert.Equal(t, dec("00000001 67"), annexb[i:])
}
//func TestStream(t *testing.T) {
// // ffmpeg
// annexb := dec("00000001 09f0 00000001 6764001fac2484014016ec0440000003004000000c23c60c92 00000001 68ee32c8b0 000001 6588808003 00000001 09")
// avc, i := ParseAVC(annexb)
// assert.Equal(t, dec("00000019 6764001fac2484014016ec0440000003004000000c23c60c92 00000005 68ee32c8b0 00000005 6588808003"), avc)
// assert.Equal(t, dec("00000001 09"), annexb[i:])
//
// // http mpeg ts
// annexb = dec("00000001 0950 000001 6764001facd2014016e8400000fa400030e081 000001 68ea8f2c 000001 65b8400eff 00000001 09")
// avc, i = ParseAVC(annexb)
// assert.Equal(t, dec("00000013 6764001facd2014016e8400000fa400030e081 00000004 68ea8f2c 00000005 65b8400eff"), avc)
// assert.Equal(t, dec("00000001 09"), annexb[i:])
//
// // tapo TC60
// annexb = dec("00000001 67640028ac1ad00a00b74dc0404050000003001000000301e8f1422a 00000001 68ee04c92240 00000001 45b80000d0 00000001 67")
// avc, i = ParseAVC(annexb)
// assert.Equal(t, dec("0000001C 67640028ac1ad00a00b74dc0404050000003001000000301e8f1422a 00000006 68ee04c92240 00000005 45b80000d0"), avc)
// assert.Equal(t, dec("00000001 67"), annexb[i:])
//
// // Tapo ?
// annexb = dec("00000001 674d0032e90048014742000007d2000138d108 00000001 68ea8f20 00000001 65b8400cff 00000001 67")
// avc, i = ParseAVC(annexb)
// assert.Equal(t, dec("00000013 674d0032e90048014742000007d2000138d108 00000004 68ea8f20 00000005 65b8400cff"), avc)
// assert.Equal(t, dec("00000001 67"), annexb[i:])
//}

View File

@@ -1,20 +1,21 @@
package mpegts
import (
"github.com/AlexxIT/go2rtc/pkg/streamer"
"encoding/json"
"github.com/AlexxIT/go2rtc/pkg/core"
)
func (c *Client) GetMedias() []*streamer.Media {
func (c *Client) GetMedias() []*core.Media {
return c.medias
}
func (c *Client) GetTrack(media *streamer.Media, codec *streamer.Codec) *streamer.Track {
for _, track := range c.tracks {
func (c *Client) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) {
for _, track := range c.receivers {
if track.Codec == codec {
return track
return track, nil
}
}
return nil
return nil, core.ErrCantGetTrack
}
func (c *Client) Start() error {
@@ -22,5 +23,19 @@ func (c *Client) Start() error {
}
func (c *Client) Stop() error {
for _, receiver := range c.receivers {
receiver.Close()
}
return c.Close()
}
func (c *Client) MarshalJSON() ([]byte, error) {
info := &core.Info{
Type: "MPEG-TS active producer",
URL: c.res.Request.URL.String(),
Medias: c.medias,
Receivers: c.receivers,
Recv: c.recv,
}
return json.Marshal(info)
}

View File

@@ -3,24 +3,26 @@ package mpegts
import (
"bytes"
"encoding/hex"
"encoding/json"
"github.com/AlexxIT/go2rtc/pkg/aac"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/h264"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/deepch/vdk/av"
"github.com/deepch/vdk/codec/aacparser"
"github.com/deepch/vdk/codec/h264parser"
"github.com/deepch/vdk/format/ts"
"github.com/pion/rtp"
"sync/atomic"
"time"
)
type Consumer struct {
streamer.Element
core.Listener
UserAgent string
RemoteAddr string
senders []*core.Sender
buf *bytes.Buffer
muxer *ts.Muxer
mimeType string
@@ -28,35 +30,36 @@ type Consumer struct {
start bool
init []byte
send uint32
send int
}
func (c *Consumer) GetMedias() []*streamer.Media {
return []*streamer.Media{
func (c *Consumer) GetMedias() []*core.Media {
return []*core.Media{
{
Kind: streamer.KindVideo,
Direction: streamer.DirectionRecvonly,
Codecs: []*streamer.Codec{
{Name: streamer.CodecH264},
Kind: core.KindVideo,
Direction: core.DirectionSendonly,
Codecs: []*core.Codec{
{Name: core.CodecH264},
},
},
//{
// Kind: streamer.KindAudio,
// Direction: streamer.DirectionRecvonly,
// Codecs: []*streamer.Codec{
// {Name: streamer.CodecAAC},
// Kind: core.KindAudio,
// Direction: core.DirectionSendonly,
// Codecs: []*core.Codec{
// {Name: core.CodecAAC},
// },
//},
}
}
func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.Track {
codec := track.Codec
func (c *Consumer) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiver) error {
trackID := int8(len(c.streams))
switch codec.Name {
case streamer.CodecH264:
sps, pps := h264.GetParameterSet(codec.FmtpLine)
handler := core.NewSender(media, track.Codec)
switch track.Codec.Name {
case core.CodecH264:
sps, pps := h264.GetParameterSet(track.Codec.FmtpLine)
stream, err := h264parser.NewCodecDataFromSPSAndPPS(sps, pps)
if err != nil {
return nil
@@ -66,21 +69,21 @@ func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *strea
c.mimeType += ","
}
c.mimeType += "avc1." + h264.GetProfileLevelID(codec.FmtpLine)
c.mimeType += "avc1." + h264.GetProfileLevelID(track.Codec.FmtpLine)
c.streams = append(c.streams, stream)
pkt := av.Packet{Idx: trackID, CompositionTime: time.Millisecond}
ts2time := time.Second / time.Duration(codec.ClockRate)
ts2time := time.Second / time.Duration(track.Codec.ClockRate)
push := func(packet *rtp.Packet) error {
handler.Handler = func(packet *rtp.Packet) {
if packet.Version != h264.RTPPacketVersionAVC {
return nil
return
}
if !c.start {
return nil
return
}
pkt.Data = packet.Payload
@@ -91,28 +94,26 @@ func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *strea
pkt.Time = newTime
if err = c.muxer.WritePacket(pkt); err != nil {
return err
return
}
// clone bytes from buffer, so next packet won't overwrite it
buf := append([]byte{}, c.buf.Bytes()...)
atomic.AddUint32(&c.send, uint32(len(buf)))
c.Fire(buf)
c.send += len(buf)
c.buf.Reset()
return nil
}
if codec.IsRTP() {
wrapper := h264.RTPDepay(track)
push = wrapper(push)
if track.Codec.IsRTP() {
handler.Handler = h264.RTPDepay(track.Codec, handler.Handler)
} else {
handler.Handler = h264.RepairAVC(track.Codec, handler.Handler)
}
return track.Bind(push)
case streamer.CodecAAC:
s := streamer.Between(codec.FmtpLine, "config=", ";")
case core.CodecAAC:
s := core.Between(track.Codec.FmtpLine, "config=", ";")
b, err := hex.DecodeString(s)
if err != nil {
@@ -133,11 +134,11 @@ func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *strea
pkt := av.Packet{Idx: trackID, CompositionTime: time.Millisecond}
ts2time := time.Second / time.Duration(codec.ClockRate)
ts2time := time.Second / time.Duration(track.Codec.ClockRate)
push := func(packet *rtp.Packet) error {
handler.Handler = func(packet *rtp.Packet) {
if !c.start {
return nil
return
}
pkt.Data = packet.Payload
@@ -147,29 +148,31 @@ func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *strea
}
pkt.Time = newTime
if err := c.muxer.WritePacket(pkt); err != nil {
return err
if err = c.muxer.WritePacket(pkt); err != nil {
return
}
// clone bytes from buffer, so next packet won't overwrite it
buf := append([]byte{}, c.buf.Bytes()...)
atomic.AddUint32(&c.send, uint32(len(buf)))
c.Fire(buf)
c.send += len(buf)
c.buf.Reset()
return nil
}
if codec.IsRTP() {
wrapper := aac.RTPDepay(track)
push = wrapper(push)
if track.Codec.IsRTP() {
handler.Handler = aac.RTPDepay(handler.Handler)
}
return track.Bind(push)
default:
panic("unsupported codec")
}
panic("unsupported codec")
handler.HandleRTP(track)
c.senders = append(c.senders, handler)
return nil
}
func (c *Consumer) MimeCodecs() string {
@@ -192,3 +195,22 @@ func (c *Consumer) Init() ([]byte, error) {
func (c *Consumer) Start() {
c.start = true
}
func (c *Consumer) Stop() error {
for _, sender := range c.senders {
sender.Close()
}
return nil
}
func (c *Consumer) MarshalJSON() ([]byte, error) {
info := &core.Info{
Type: "TS passive consumer",
RemoteAddr: c.RemoteAddr,
UserAgent: c.UserAgent,
Medias: c.GetMedias(),
Senders: c.senders,
Send: c.send,
}
return json.Marshal(info)
}

View File

@@ -3,14 +3,14 @@ package ngrok
import (
"bufio"
"encoding/json"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/AlexxIT/go2rtc/pkg/core"
"io"
"os/exec"
"strings"
)
type Ngrok struct {
streamer.Element
core.Listener
Tunnels map[string]string

View File

@@ -4,15 +4,14 @@ import (
"encoding/base64"
"encoding/hex"
"fmt"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/httpflv"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/deepch/vdk/av"
"github.com/deepch/vdk/codec/aacparser"
"github.com/deepch/vdk/codec/h264parser"
"github.com/deepch/vdk/format/rtmp"
"github.com/pion/rtp"
"net/http"
"sync/atomic"
"time"
)
@@ -24,17 +23,17 @@ type Conn interface {
}
type Client struct {
streamer.Element
core.Listener
URI string
medias []*streamer.Media
tracks []*streamer.Track
medias []*core.Media
receivers []*core.Receiver
conn Conn
closed bool
recv uint32
recv int
}
func NewClient(uri string) *Client {
@@ -74,61 +73,55 @@ func (c *Client) Describe() (err error) {
base64.StdEncoding.EncodeToString(info.PPS[0]),
)
codec := &streamer.Codec{
Name: streamer.CodecH264,
codec := &core.Codec{
Name: core.CodecH264,
ClockRate: 90000,
FmtpLine: fmtp,
PayloadType: streamer.PayloadTypeRAW,
PayloadType: core.PayloadTypeRAW,
}
media := &streamer.Media{
Kind: streamer.KindVideo,
Direction: streamer.DirectionSendonly,
Codecs: []*streamer.Codec{codec},
media := &core.Media{
Kind: core.KindVideo,
Direction: core.DirectionRecvonly,
Codecs: []*core.Codec{codec},
}
c.medias = append(c.medias, media)
track := streamer.NewTrack(media, codec)
c.tracks = append(c.tracks, track)
track := core.NewReceiver(media, codec)
c.receivers = append(c.receivers, track)
case av.AAC:
// TODO: fix support
cd := stream.(aacparser.CodecData)
codec := &streamer.Codec{
Name: streamer.CodecAAC,
codec := &core.Codec{
Name: core.CodecAAC,
ClockRate: uint32(cd.Config.SampleRate),
Channels: uint16(cd.Config.ChannelConfig),
// a=fmtp:97 streamtype=5;profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=1588
FmtpLine: "streamtype=5;profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=" + hex.EncodeToString(cd.ConfigBytes),
PayloadType: streamer.PayloadTypeRAW,
PayloadType: core.PayloadTypeRAW,
}
media := &streamer.Media{
Kind: streamer.KindAudio,
Direction: streamer.DirectionSendonly,
Codecs: []*streamer.Codec{codec},
media := &core.Media{
Kind: core.KindAudio,
Direction: core.DirectionRecvonly,
Codecs: []*core.Codec{codec},
}
c.medias = append(c.medias, media)
track := streamer.NewTrack(media, codec)
c.tracks = append(c.tracks, track)
track := core.NewReceiver(media, codec)
c.receivers = append(c.receivers, track)
default:
fmt.Printf("[rtmp] unsupported codec %+v\n", stream)
}
}
c.Fire(streamer.StateReady)
return
}
func (c *Client) Handle() (err error) {
defer c.Fire(streamer.StateNull)
c.Fire(streamer.StatePlaying)
for {
var pkt av.Packet
pkt, err = c.conn.ReadPacket()
@@ -139,9 +132,9 @@ func (c *Client) Handle() (err error) {
return
}
atomic.AddUint32(&c.recv, uint32(len(pkt.Data)))
c.recv += len(pkt.Data)
track := c.tracks[int(pkt.Idx)]
track := c.receivers[int(pkt.Idx)]
// convert seconds to RTP timestamp
timestamp := uint32(pkt.Time * time.Duration(track.Codec.ClockRate) / time.Second)
@@ -150,7 +143,7 @@ func (c *Client) Handle() (err error) {
Header: rtp.Header{Timestamp: timestamp},
Payload: pkt.Data,
}
_ = track.WriteRTP(packet)
track.WriteRTP(packet)
}
}

41
pkg/rtmp/producer.go Normal file
View File

@@ -0,0 +1,41 @@
package rtmp
import (
"encoding/json"
"github.com/AlexxIT/go2rtc/pkg/core"
)
func (c *Client) GetMedias() []*core.Media {
return c.medias
}
func (c *Client) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) {
for _, track := range c.receivers {
if track.Codec == codec {
return track, nil
}
}
return nil, core.ErrCantGetTrack
}
func (c *Client) Start() error {
return c.Handle()
}
func (c *Client) Stop() error {
for _, receiver := range c.receivers {
receiver.Close()
}
return c.Close()
}
func (c *Client) MarshalJSON() ([]byte, error) {
info := &core.Info{
Type: "RTMP active producer",
URL: c.URI,
Medias: c.medias,
Receivers: c.receivers,
Recv: c.recv,
}
return json.Marshal(info)
}

View File

@@ -1,40 +0,0 @@
package rtmp
import (
"encoding/json"
"fmt"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"sync/atomic"
)
func (c *Client) GetMedias() []*streamer.Media {
return c.medias
}
func (c *Client) GetTrack(media *streamer.Media, codec *streamer.Codec) *streamer.Track {
for _, track := range c.tracks {
if track.Codec == codec {
return track
}
}
panic(fmt.Sprintf("wrong media/codec: %+v %+v", media, codec))
}
func (c *Client) Start() error {
return c.Handle()
}
func (c *Client) Stop() error {
return c.Close()
}
func (c *Client) MarshalJSON() ([]byte, error) {
info := &streamer.Info{
Type: "RTMP source",
URL: c.URI,
Medias: c.medias,
Tracks: c.tracks,
Recv: atomic.LoadUint32(&c.recv),
}
return json.Marshal(info)
}

View File

@@ -3,115 +3,25 @@ package rtsp
import (
"bufio"
"crypto/tls"
"encoding/binary"
"errors"
"fmt"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/tcp"
"github.com/pion/rtcp"
"github.com/pion/rtp"
"io"
"net"
"net/http"
"net/url"
"strconv"
"strings"
"sync"
"time"
)
const (
ProtoRTSP = "RTSP/1.0"
MethodOptions = "OPTIONS"
MethodSetup = "SETUP"
MethodTeardown = "TEARDOWN"
MethodDescribe = "DESCRIBE"
MethodPlay = "PLAY"
MethodPause = "PAUSE"
MethodAnnounce = "ANNOUNCE"
MethodRecord = "RECORD"
)
type Mode byte
const (
ModeUnknown Mode = iota
ModeClientProducer // conn act as RTSP client that receive data from RTSP server (ex. camera)
ModeServerUnknown
ModeServerProducer // conn act as RTSP server that reseive data from RTSP client (ex. ffmpeg output)
ModeServerConsumer // conn act as RTSP server that send data to RTSP client (ex. ffmpeg input)
)
type State byte
func (s State) String() string {
switch s {
case StateNone:
return "NONE"
case StateConn:
return "CONN"
case StateSetup:
return "SETUP"
case StatePlay:
return "PLAY"
case StateHandle:
return "HANDLE"
}
return strconv.Itoa(int(s))
func NewClient(uri string) *Conn {
return &Conn{uri: uri}
}
const (
StateNone State = iota
StateConn
StateSetup
StatePlay
StateHandle
)
type Conn struct {
streamer.Element
// public
Backchannel bool
SessionName string
Medias []*streamer.Media
Session string
UserAgent string
URL *url.URL
// internal
auth *tcp.Auth
conn net.Conn
mode Mode
state State
stateMu sync.Mutex
reader *bufio.Reader
sequence int
uri string
tracks []*streamer.Track
channels map[byte]*streamer.Track
// stats
receive int
send int
}
func NewClient(uri string) (*Conn, error) {
c := new(Conn)
c.mode = ModeClientProducer
c.uri = uri
return c, c.parseURI()
}
func (c *Conn) parseURI() (err error) {
c.URL, err = url.Parse(c.uri)
if err != nil {
return err
func (c *Conn) Dial() (err error) {
if c.URL, err = url.Parse(c.uri); err != nil {
return
}
if strings.IndexByte(c.URL.Host, ':') < 0 {
@@ -122,14 +32,6 @@ func (c *Conn) parseURI() (err error) {
c.auth = tcp.NewAuth(c.URL.User)
c.URL.User = nil
return nil
}
func (c *Conn) Dial() (err error) {
if c.conn != nil {
_ = c.parseURI()
}
c.conn, err = net.DialTimeout("tcp", c.URL.Host, time.Second*5)
if err != nil {
return
@@ -314,7 +216,7 @@ func (c *Conn) Describe() error {
return err
}
c.mode = ModeClientProducer
c.mode = core.ModeActiveProducer
return nil
}
@@ -328,7 +230,7 @@ func (c *Conn) Announce() (err error) {
},
}
req.Body, err = streamer.MarshalSDP(c.SessionName, c.Medias)
req.Body, err = core.MarshalSDP(c.SessionName, c.Medias)
if err != nil {
return err
}
@@ -342,7 +244,7 @@ func (c *Conn) Announce() (err error) {
func (c *Conn) Setup() error {
for _, media := range c.Medias {
_, err := c.SetupMedia(media, media.Codecs[0], true)
_, err := c.SetupMedia(media, true)
if err != nil {
return err
}
@@ -351,7 +253,7 @@ func (c *Conn) Setup() error {
return nil
}
func (c *Conn) SetupMedia(media *streamer.Media, codec *streamer.Codec, first bool) (*streamer.Track, error) {
func (c *Conn) SetupMedia(media *core.Media, first bool) (byte, error) {
// TODO: rewrite recoonection and first flag
if first {
c.stateMu.Lock()
@@ -359,36 +261,45 @@ func (c *Conn) SetupMedia(media *streamer.Media, codec *streamer.Codec, first bo
}
if c.state != StateConn && c.state != StateSetup {
return nil, fmt.Errorf("RTSP SETUP from wrong state: %s", c.state)
return 0, fmt.Errorf("RTSP SETUP from wrong state: %s", c.state)
}
ch := c.GetChannel(media)
if ch < 0 {
return nil, fmt.Errorf("wrong media: %v", media)
var transport string
// try to use media position as channel number
for i, m := range c.Medias {
if m.ID == media.ID {
transport = fmt.Sprintf(
// i - RTP (data channel)
// i+1 - RTCP (control channel)
"RTP/AVP/TCP;unicast;interleaved=%d-%d", i*2, i*2+1,
)
break
}
}
rawURL := media.Control
if transport == "" {
return 0, fmt.Errorf("wrong media: %v", media)
}
rawURL := media.ID // control
if !strings.Contains(rawURL, "://") {
rawURL = c.URL.String()
if !strings.HasSuffix(rawURL, "/") {
rawURL += "/"
}
rawURL += media.Control
rawURL += media.ID
}
trackURL, err := urlParse(rawURL)
if err != nil {
return nil, err
return 0, err
}
req := &tcp.Request{
Method: MethodSetup,
URL: trackURL,
Header: map[string][]string{
"Transport": {fmt.Sprintf(
// i - RTP (data channel)
// i+1 - RTCP (control channel)
"RTP/AVP/TCP;unicast;interleaved=%d-%d", ch*2, ch*2+1,
)},
"Transport": {transport},
},
}
@@ -400,20 +311,20 @@ func (c *Conn) SetupMedia(media *streamer.Media, codec *streamer.Codec, first bo
if c.Backchannel {
c.Backchannel = false
if err := c.Dial(); err != nil {
return nil, err
return 0, err
}
if err := c.Describe(); err != nil {
return nil, err
return 0, err
}
for _, newMedia := range c.Medias {
if newMedia.Control == media.Control {
return c.SetupMedia(newMedia, newMedia.Codecs[0], false)
if newMedia.ID == media.ID {
return c.SetupMedia(newMedia, false)
}
}
}
return nil, err
return 0, err
}
if c.Session == "" {
@@ -426,60 +337,29 @@ func (c *Conn) SetupMedia(media *streamer.Media, codec *streamer.Codec, first bo
}
}
// in case the track has already been setup before
if codec == nil {
c.state = StateSetup
return nil, nil
}
// we send our `interleaved`, but camera can answer with another
// Transport: RTP/AVP/TCP;unicast;interleaved=10-11;ssrc=10117CB7
// Transport: RTP/AVP/TCP;unicast;destination=192.168.1.111;source=192.168.1.222;interleaved=0
// Transport: RTP/AVP/TCP;ssrc=22345682;interleaved=0-1
s := res.Header.Get("Transport")
// TODO: rewrite
if !strings.HasPrefix(s, "RTP/AVP/TCP;") {
transport = res.Header.Get("Transport")
if !strings.HasPrefix(transport, "RTP/AVP/TCP;") {
// Escam Q6 has a bug:
// Transport: RTP/AVP;unicast;destination=192.168.1.111;source=192.168.1.222;interleaved=0-1
if !strings.Contains(s, ";interleaved=") {
return nil, fmt.Errorf("wrong transport: %s", s)
if !strings.Contains(transport, ";interleaved=") {
return 0, fmt.Errorf("wrong transport: %s", transport)
}
}
i := strings.Index(s, "interleaved=")
if i < 0 {
return nil, fmt.Errorf("wrong transport: %s", s)
}
s = s[i+len("interleaved="):]
i = strings.IndexAny(s, "-;")
if i > 0 {
s = s[:i]
}
ch, err = strconv.Atoi(s)
if err != nil {
return nil, err
}
track := streamer.NewTrack(media, codec)
switch track.Direction {
case streamer.DirectionSendonly:
if c.channels == nil {
c.channels = make(map[byte]*streamer.Track)
}
c.channels[byte(ch)] = track
case streamer.DirectionRecvonly:
track = c.bindTrack(track, byte(ch), codec.PayloadType)
}
c.state = StateSetup
c.tracks = append(c.tracks, track)
return track, nil
channel := core.Between(transport, "interleaved=", "-")
i, err := strconv.Atoi(channel)
if err != nil {
return 0, err
}
return byte(i), nil
}
func (c *Conn) Play() (err error) {
@@ -516,224 +396,3 @@ func (c *Conn) Close() error {
c.state = StateNone
return c.conn.Close()
}
func (c *Conn) Handle() (err error) {
c.stateMu.Lock()
switch c.state {
case StateNone: // Close after PLAY and before Handle is OK (because SETUP after PLAY)
case StatePlay:
c.state = StateHandle
default:
err = fmt.Errorf("RTSP HANDLE from wrong state: %s", c.state)
c.state = StateNone
_ = c.conn.Close()
}
ok := c.state == StateHandle
c.stateMu.Unlock()
if !ok {
return
}
defer func() {
c.stateMu.Lock()
defer c.stateMu.Unlock()
if c.state == StateNone {
err = nil
return
}
// may have gotten here because of the deadline
// so close the connection to stop keepalive
c.state = StateNone
_ = c.conn.Close()
}()
var timeout time.Duration
switch c.mode {
case ModeClientProducer:
// polling frames from remote RTSP Server (ex Camera)
go c.keepalive()
if c.HasSendTracks() {
// if we receiving video/audio from camera
timeout = time.Second * 5
} else {
// if we only send audio to camera
timeout = time.Second * 30
}
case ModeServerProducer:
// polling frames from remote RTSP Client (ex FFmpeg)
timeout = time.Second * 15
case ModeServerConsumer:
// pushing frames to remote RTSP Client (ex VLC)
timeout = time.Second * 60
default:
return fmt.Errorf("wrong RTSP conn mode: %d", c.mode)
}
for {
if c.state == StateNone {
return
}
if err = c.conn.SetReadDeadline(time.Now().Add(timeout)); err != nil {
return
}
// we can read:
// 1. RTP interleaved: `$` + 1B channel number + 2B size
// 2. RTSP response: RTSP/1.0 200 OK
// 3. RTSP request: OPTIONS ...
var buf4 []byte // `$` + 1B channel number + 2B size
buf4, err = c.reader.Peek(4)
if err != nil {
return
}
var channelID byte
var size uint16
if buf4[0] != '$' {
switch string(buf4) {
case "RTSP":
var res *tcp.Response
if res, err = tcp.ReadResponse(c.reader); err != nil {
return
}
c.Fire(res)
continue
case "OPTI", "TEAR", "DESC", "SETU", "PLAY", "PAUS", "RECO", "ANNO", "GET_", "SET_":
var req *tcp.Request
if req, err = tcp.ReadRequest(c.reader); err != nil {
return
}
c.Fire(req)
continue
default:
for i := 0; ; i++ {
// search next start symbol
if _, err = c.reader.ReadBytes('$'); err != nil {
return err
}
if channelID, err = c.reader.ReadByte(); err != nil {
return err
}
// check if channel ID exists
if c.channels[channelID] == nil {
continue
}
buf4 = make([]byte, 2)
if _, err = io.ReadFull(c.reader, buf4); err != nil {
return err
}
// check if size good for RTP
size = binary.BigEndian.Uint16(buf4)
if size <= 1500 {
break
}
// 10 tries to find good packet
if i >= 10 {
return fmt.Errorf("RTSP wrong input")
}
}
c.Fire("RTSP wrong input")
}
} else {
// hope that the odd channels are always RTCP
channelID = buf4[1]
// get data size
size = binary.BigEndian.Uint16(buf4[2:])
// skip 4 bytes from c.reader.Peek
if _, err = c.reader.Discard(4); err != nil {
return
}
}
// init memory for data
buf := make([]byte, size)
if _, err = io.ReadFull(c.reader, buf); err != nil {
return
}
c.receive += int(size)
if channelID&1 == 0 {
packet := &rtp.Packet{}
if err = packet.Unmarshal(buf); err != nil {
return
}
track := c.channels[channelID]
if track != nil {
_ = track.WriteRTP(packet)
} else {
//c.Fire("wrong channelID: " + strconv.Itoa(int(channelID)))
}
} else {
msg := &RTCP{Channel: channelID}
if err = msg.Header.Unmarshal(buf); err != nil {
continue
}
msg.Packets, err = rtcp.Unmarshal(buf)
if err != nil {
continue
}
c.Fire(msg)
}
}
}
func (c *Conn) keepalive() {
// TODO: rewrite to RTCP
req := &tcp.Request{Method: MethodOptions, URL: c.URL}
for {
time.Sleep(time.Second * 25)
if c.state == StateNone {
return
}
if err := c.Request(req); err != nil {
return
}
}
}
func (c *Conn) GetChannel(media *streamer.Media) int {
for i, m := range c.Medias {
if m == media {
return i
}
}
return -1
}
func (c *Conn) HasSendTracks() bool {
for _, track := range c.tracks {
if track.Direction == streamer.DirectionSendonly {
return true
}
}
return false
}

274
pkg/rtsp/conn.go Normal file
View File

@@ -0,0 +1,274 @@
package rtsp
import (
"bufio"
"encoding/binary"
"fmt"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/tcp"
"github.com/pion/rtcp"
"github.com/pion/rtp"
"io"
"net"
"net/url"
"strconv"
"sync"
"time"
)
type Conn struct {
core.Listener
// public
Backchannel bool
SessionName string
Medias []*core.Media
Session string
UserAgent string
URL *url.URL
// internal
auth *tcp.Auth
conn net.Conn
mode core.Mode
state State
stateMu sync.Mutex
reader *bufio.Reader
sequence int
uri string
receivers []*core.Receiver
senders []*core.Sender
// stats
recv int
send int
}
const (
ProtoRTSP = "RTSP/1.0"
MethodOptions = "OPTIONS"
MethodSetup = "SETUP"
MethodTeardown = "TEARDOWN"
MethodDescribe = "DESCRIBE"
MethodPlay = "PLAY"
MethodPause = "PAUSE"
MethodAnnounce = "ANNOUNCE"
MethodRecord = "RECORD"
)
type State byte
func (s State) String() string {
switch s {
case StateNone:
return "NONE"
case StateConn:
return "CONN"
case StateSetup:
return "SETUP"
case StatePlay:
return "PLAY"
case StateHandle:
return "HANDLE"
}
return strconv.Itoa(int(s))
}
const (
StateNone State = iota
StateConn
StateSetup
StatePlay
StateHandle
)
func (c *Conn) Handle() (err error) {
c.stateMu.Lock()
switch c.state {
case StateNone: // Close after PLAY and before Handle is OK (because SETUP after PLAY)
case StatePlay:
c.state = StateHandle
default:
err = fmt.Errorf("RTSP HANDLE from wrong state: %s", c.state)
c.state = StateNone
_ = c.conn.Close()
}
ok := c.state == StateHandle
c.stateMu.Unlock()
if !ok {
return
}
var timeout time.Duration
switch c.mode {
case core.ModeActiveProducer:
// polling frames from remote RTSP Server (ex Camera)
go c.keepalive()
if len(c.receivers) > 0 {
// if we receiving video/audio from camera
timeout = time.Second * 5
} else {
// if we only send audio to camera
timeout = time.Second * 30
}
case core.ModePassiveProducer:
// polling frames from remote RTSP Client (ex FFmpeg)
timeout = time.Second * 15
case core.ModePassiveConsumer:
// pushing frames to remote RTSP Client (ex VLC)
timeout = time.Second * 60
default:
return fmt.Errorf("wrong RTSP conn mode: %d", c.mode)
}
for c.state != StateNone {
if err = c.conn.SetReadDeadline(time.Now().Add(timeout)); err != nil {
return
}
// we can read:
// 1. RTP interleaved: `$` + 1B channel number + 2B size
// 2. RTSP response: RTSP/1.0 200 OK
// 3. RTSP request: OPTIONS ...
var buf4 []byte // `$` + 1B channel number + 2B size
buf4, err = c.reader.Peek(4)
if err != nil {
return
}
var channelID byte
var size uint16
if buf4[0] != '$' {
switch string(buf4) {
case "RTSP":
var res *tcp.Response
if res, err = tcp.ReadResponse(c.reader); err != nil {
return
}
c.Fire(res)
continue
case "OPTI", "TEAR", "DESC", "SETU", "PLAY", "PAUS", "RECO", "ANNO", "GET_", "SET_":
var req *tcp.Request
if req, err = tcp.ReadRequest(c.reader); err != nil {
return
}
c.Fire(req)
continue
default:
for i := 0; ; i++ {
// search next start symbol
if _, err = c.reader.ReadBytes('$'); err != nil {
return err
}
if channelID, err = c.reader.ReadByte(); err != nil {
return err
}
// TODO: better check maximum good channel ID
if channelID >= 20 {
continue
}
buf4 = make([]byte, 2)
if _, err = io.ReadFull(c.reader, buf4); err != nil {
return err
}
// check if size good for RTP
size = binary.BigEndian.Uint16(buf4)
if size <= 1500 {
break
}
// 10 tries to find good packet
if i >= 10 {
return fmt.Errorf("RTSP wrong input")
}
}
c.Fire("RTSP wrong input")
}
} else {
// hope that the odd channels are always RTCP
channelID = buf4[1]
// get data size
size = binary.BigEndian.Uint16(buf4[2:])
// skip 4 bytes from c.reader.Peek
if _, err = c.reader.Discard(4); err != nil {
return
}
}
// init memory for data
buf := make([]byte, size)
if _, err = io.ReadFull(c.reader, buf); err != nil {
return
}
c.recv += int(size)
if channelID&1 == 0 {
packet := &rtp.Packet{}
if err = packet.Unmarshal(buf); err != nil {
return
}
for _, receiver := range c.receivers {
if receiver.ID == channelID {
receiver.WriteRTP(packet)
break
}
}
} else {
msg := &RTCP{Channel: channelID}
if err = msg.Header.Unmarshal(buf); err != nil {
continue
}
msg.Packets, err = rtcp.Unmarshal(buf)
if err != nil {
continue
}
c.Fire(msg)
}
}
return
}
func (c *Conn) keepalive() {
// TODO: rewrite to RTCP
req := &tcp.Request{Method: MethodOptions, URL: c.URL}
for {
time.Sleep(time.Second * 25)
if c.state == StateNone {
return
}
if err := c.Request(req); err != nil {
return
}
}
}

View File

@@ -1,112 +1,101 @@
package rtsp
import (
"encoding/binary"
"github.com/AlexxIT/go2rtc/pkg/aac"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/h264"
"github.com/AlexxIT/go2rtc/pkg/h265"
"github.com/AlexxIT/go2rtc/pkg/mjpeg"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/pion/rtp"
)
func (c *Conn) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.Track {
switch c.mode {
// send our track to RTSP consumer (ex. FFmpeg)
case ModeServerConsumer:
i := len(c.tracks)
channelID := byte(i << 1)
func (c *Conn) GetMedias() []*core.Media {
core.Assert(c.Medias != nil)
return c.Medias
}
codec := track.Codec.Clone()
codec.PayloadType = uint8(96 + i)
func (c *Conn) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) (err error) {
core.Assert(media.Direction == core.DirectionSendonly)
if media.MatchAll() {
// fill consumer medias list
c.Medias = append(c.Medias, &streamer.Media{
Kind: media.Kind, Direction: media.Direction,
Codecs: []*streamer.Codec{codec},
})
} else {
// find consumer media and replace codec with right one
for i, m := range c.Medias {
if m == media {
media.Codecs = []*streamer.Codec{codec}
c.Medias[i] = media
break
}
}
for _, sender := range c.senders {
if sender.Codec == codec {
sender.HandleRTP(track)
return
}
track = c.bindTrack(track, channelID, codec.PayloadType)
track.Codec = codec
c.tracks = append(c.tracks, track)
return track
// camera with backchannel support
case ModeClientProducer:
consCodec := media.MatchCodec(track.Codec)
consTrack := c.GetTrack(media, consCodec)
if consTrack == nil {
return nil
}
return track.Bind(func(packet *rtp.Packet) error {
return consTrack.WriteRTP(packet)
})
}
println("WARNING: rtsp: AddTrack to wrong mode")
var channel byte
switch c.mode {
case core.ModeActiveProducer: // backchannel
if channel, err = c.SetupMedia(media, true); err != nil {
return
}
case core.ModePassiveConsumer:
channel = byte(len(c.senders)) * 2
// for consumer is better to use original track codec
codec = track.Codec.Clone()
// generate new payload type, starting from 96
codec.PayloadType = byte(96 + len(c.senders))
default:
panic(core.Caller())
}
// save original codec to sender (can have Codec.Name = ANY)
sender := core.NewSender(media, codec)
sender.Handler = c.packetWriter(codec, channel)
sender.HandleRTP(track)
c.senders = append(c.senders, sender)
return nil
}
func (c *Conn) bindTrack(
track *streamer.Track, channel uint8, payloadType uint8,
) *streamer.Track {
push := func(packet *rtp.Packet) error {
func (c *Conn) packetWriter(codec *core.Codec, channel uint8) core.HandlerFunc {
handlerFunc := func(packet *rtp.Packet) {
if c.state == StateNone {
return nil
return
}
packet.Header.PayloadType = payloadType
size := packet.MarshalSize()
clone := *packet
clone.Header.PayloadType = codec.PayloadType
//log.Printf("[RTP] codec: %s, size: %6d, ts: %10d, pt: %2d, ssrc: %d, seq: %d, mark: %v", track.Codec.Name, len(packet.Payload), packet.Timestamp, packet.PayloadType, packet.SSRC, packet.SequenceNumber, packet.Marker)
size := clone.MarshalSize()
//log.Printf("[RTP] codec: %s, size: %6d, ts: %10d, pt: %2d, ssrc: %d, seq: %d, mark: %v", codec.Name, len(packet.Payload), packet.Timestamp, packet.PayloadType, packet.SSRC, packet.SequenceNumber, packet.Marker)
data := make([]byte, 4+size)
data[0] = '$'
data[1] = channel
binary.BigEndian.PutUint16(data[2:], uint16(size))
data[2] = byte(size >> 8)
data[3] = byte(size)
if _, err := packet.MarshalTo(data[4:]); err != nil {
return nil
if _, err := clone.MarshalTo(data[4:]); err != nil {
return
}
if _, err := c.conn.Write(data); err != nil {
return err
n, err := c.conn.Write(data)
if err != nil {
return
}
c.send += size
return nil
c.send += n
}
if !track.Codec.IsRTP() {
switch track.Codec.Name {
case streamer.CodecH264:
wrapper := h264.RTPPay(1500)
push = wrapper(push)
case streamer.CodecH265:
wrapper := h265.RTPPay(1500)
push = wrapper(push)
case streamer.CodecAAC:
wrapper := aac.RTPPay(1500)
push = wrapper(push)
case streamer.CodecJPEG:
wrapper := mjpeg.RTPPay()
push = wrapper(push)
if !codec.IsRTP() {
switch codec.Name {
case core.CodecH264:
handlerFunc = h264.RTPPay(1500, handlerFunc)
case core.CodecH265:
handlerFunc = h265.RTPPay(1500, handlerFunc)
case core.CodecAAC:
handlerFunc = aac.RTPPay(handlerFunc)
case core.CodecJPEG:
handlerFunc = mjpeg.RTPPay(handlerFunc)
}
}
return track.Bind(push)
return handlerFunc
}

View File

@@ -2,7 +2,7 @@ package rtsp
import (
"bytes"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/pion/rtcp"
"github.com/pion/sdp/v3"
"net/url"
@@ -22,7 +22,7 @@ o=- 0 0 IN IP4 0.0.0.0
s=-
t=0 0`
func UnmarshalSDP(rawSDP []byte) ([]*streamer.Media, error) {
func UnmarshalSDP(rawSDP []byte) ([]*core.Media, error) {
// fix bug from Reolink Doorbell
if i := bytes.Index(rawSDP, []byte("a=sendonlym=")); i > 0 {
rawSDP = append(rawSDP[:i+11], rawSDP[i+10:]...)
@@ -47,25 +47,24 @@ func UnmarshalSDP(rawSDP []byte) ([]*streamer.Media, error) {
}
}
medias := streamer.UnmarshalMedias(sd.MediaDescriptions)
var medias []*core.Media
for _, md := range sd.MediaDescriptions {
media := core.UnmarshalMedia(md)
for _, media := range medias {
// Check buggy SDP with fmtp for H264 on another track
// https://github.com/AlexxIT/WebRTC/issues/419
for _, codec := range media.Codecs {
if codec.Name == streamer.CodecH264 && codec.FmtpLine == "" {
if codec.Name == core.CodecH264 && codec.FmtpLine == "" {
codec.FmtpLine = findFmtpLine(codec.PayloadType, sd.MediaDescriptions)
}
}
// fix bug in ONVIF spec
// https://www.onvif.org/specs/stream/ONVIF-Streaming-Spec-v241.pdf
switch media.Direction {
case streamer.DirectionRecvonly, "":
media.Direction = streamer.DirectionSendonly
case streamer.DirectionSendonly:
media.Direction = streamer.DirectionRecvonly
if media.Direction == "" {
media.Direction = core.DirectionRecvonly
}
medias = append(medias, media)
}
return medias, nil
@@ -74,7 +73,7 @@ func UnmarshalSDP(rawSDP []byte) ([]*streamer.Media, error) {
func findFmtpLine(payloadType uint8, descriptions []*sdp.MediaDescription) string {
s := strconv.Itoa(int(payloadType))
for _, md := range descriptions {
codec := streamer.UnmarshalCodec(md, s)
codec := core.UnmarshalCodec(md, s)
if codec.FmtpLine != "" {
return codec.FmtpLine
}

View File

@@ -3,87 +3,74 @@ package rtsp
import (
"encoding/json"
"fmt"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/AlexxIT/go2rtc/pkg/core"
)
func (c *Conn) GetMedias() []*streamer.Media {
if c.Medias != nil {
return c.Medias
}
func (c *Conn) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) {
core.Assert(media.Direction == core.DirectionRecvonly)
return []*streamer.Media{
{
Kind: streamer.KindVideo,
Direction: streamer.DirectionRecvonly,
Codecs: []*streamer.Codec{
{Name: streamer.CodecAll},
},
},
{
Kind: streamer.KindAudio,
Direction: streamer.DirectionRecvonly,
Codecs: []*streamer.Codec{
{Name: streamer.CodecAll},
},
},
}
}
func (c *Conn) GetTrack(media *streamer.Media, codec *streamer.Codec) *streamer.Track {
for _, track := range c.tracks {
for _, track := range c.receivers {
if track.Codec == codec {
return track
return track, nil
}
}
// can't setup new tracks from play state - forcing a reconnection feature
switch c.state {
case StatePlay, StateHandle:
go c.Close()
return streamer.NewTrack(media, codec)
case StateConn, StateSetup:
default:
return nil, fmt.Errorf("RTSP GetTrack from wrong state: %s", c.state)
}
track, err := c.SetupMedia(media, codec, true)
channel, err := c.SetupMedia(media, true)
if err != nil {
return nil
return nil, err
}
return track
track := core.NewReceiver(media, codec)
track.ID = byte(channel)
c.receivers = append(c.receivers, track)
return track, nil
}
func (c *Conn) Start() error {
switch c.mode {
case ModeClientProducer:
case core.ModeActiveProducer:
if err := c.Play(); err != nil {
return err
}
case ModeServerProducer:
case core.ModePassiveProducer:
default:
return fmt.Errorf("start wrong mode: %d", c.mode)
}
return c.Handle()
if err := c.Handle(); c.state != StateNone {
_ = c.conn.Close()
return err
}
return nil
}
func (c *Conn) Stop() error {
for _, receiver := range c.receivers {
receiver.Close()
}
for _, sender := range c.senders {
sender.Close()
}
return c.Close()
}
func (c *Conn) MarshalJSON() ([]byte, error) {
info := &streamer.Info{
info := &core.Info{
Type: "RTSP " + c.mode.String(),
UserAgent: c.UserAgent,
Medias: c.Medias,
Tracks: c.tracks,
Recv: uint32(c.receive),
Send: uint32(c.send),
}
switch c.mode {
case ModeUnknown:
info.Type = "RTSP unknown"
case ModeClientProducer, ModeServerProducer:
info.Type = "RTSP source"
case ModeServerConsumer:
info.Type = "RTSP client"
Receivers: c.receivers,
Senders: c.senders,
Recv: c.recv,
Send: c.send,
}
if c.URL != nil {
@@ -93,14 +80,5 @@ func (c *Conn) MarshalJSON() ([]byte, error) {
info.RemoteAddr = c.conn.RemoteAddr().String()
}
//for i, track := range c.tracks {
// k := "track:" + strconv.Itoa(i+1)
// if track.MimeType() == streamer.MimeTypeH264 {
// v[k] = h264.Describe(track.Caps())
// } else {
// v[k] = track.MimeType()
// }
//}
return json.Marshal(info)
}

View File

@@ -1,8 +1,8 @@
package rtsp
import (
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/h264"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/stretchr/testify/assert"
"strings"
"testing"
@@ -131,7 +131,7 @@ a=appversion:1.0
assert.Nil(t, err)
codec := medias[0].Codecs[0]
assert.Equal(t, streamer.CodecH264, codec.Name)
assert.Equal(t, core.CodecH264, codec.Name)
sps, _ := h264.GetParameterSet(codec.FmtpLine)
assert.Nil(t, sps)

View File

@@ -4,7 +4,7 @@ import (
"bufio"
"errors"
"fmt"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/tcp"
"net"
"net/url"
@@ -14,7 +14,6 @@ import (
func NewServer(conn net.Conn) *Conn {
c := new(Conn)
c.conn = conn
c.mode = ModeServerUnknown
c.reader = bufio.NewReader(conn)
return c
}
@@ -24,8 +23,6 @@ func (c *Conn) Auth(username, password string) {
c.auth = tcp.NewAuth(info)
}
const transport = "RTP/AVP/TCP;unicast;interleaved="
func (c *Conn) Accept() error {
for {
req, err := tcp.ReadRequest(c.reader)
@@ -76,14 +73,13 @@ func (c *Conn) Accept() error {
}
// TODO: fix someday...
c.channels = map[byte]*streamer.Track{}
for i, media := range c.Medias {
track := streamer.NewTrack(media, nil)
c.tracks = append(c.tracks, track)
c.channels[byte(i<<1)] = track
track := core.NewReceiver(media, media.Codecs[0])
track.ID = byte(i * 2)
c.receivers = append(c.receivers, track)
}
c.mode = ModeServerProducer
c.mode = core.ModePassiveProducer
c.Fire(MethodAnnounce)
res := &tcp.Response{Request: req}
@@ -92,10 +88,10 @@ func (c *Conn) Accept() error {
}
case MethodDescribe:
c.mode = ModeServerConsumer
c.mode = core.ModePassiveConsumer
c.Fire(MethodDescribe)
if c.tracks == nil {
if c.senders == nil {
res := &tcp.Response{
Status: "404 Not Found",
Request: req,
@@ -111,17 +107,17 @@ func (c *Conn) Accept() error {
}
// convert tracks to real output medias medias
var medias []*streamer.Media
for _, track := range c.tracks {
media := &streamer.Media{
Kind: streamer.GetKind(track.Codec.Name),
Direction: streamer.DirectionSendonly,
Codecs: []*streamer.Codec{track.Codec},
var medias []*core.Media
for _, track := range c.senders {
media := &core.Media{
Kind: core.GetKind(track.Codec.Name),
Direction: core.DirectionRecvonly,
Codecs: []*core.Codec{track.Codec},
}
medias = append(medias, media)
}
res.Body, err = streamer.MarshalSDP(c.SessionName, medias)
res.Body, err = core.MarshalSDP(c.SessionName, medias)
if err != nil {
return err
}
@@ -138,6 +134,7 @@ func (c *Conn) Accept() error {
Request: req,
}
const transport = "RTP/AVP/TCP;unicast;interleaved="
if strings.HasPrefix(tr, transport) {
c.Session = "1" // TODO: fixme
c.state = StateSetup

View File

@@ -1,7 +1,7 @@
package srtp
import (
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/pion/rtcp"
"github.com/pion/rtp"
"github.com/pion/srtp/v2"
@@ -16,7 +16,7 @@ type Session struct {
remoteCtx *srtp.Context // read context
Write func(b []byte) (int, error)
Track *streamer.Track
Track *core.Receiver
Recv uint32
lastSequence uint32
@@ -82,7 +82,7 @@ func (s *Session) HandleRTP(data []byte) (err error) {
s.lastTimestamp = packet.Timestamp
s.lastTime = now
_ = s.Track.WriteRTP(packet)
s.Track.WriteRTP(packet)
return
}

View File

@@ -1,104 +0,0 @@
package streamer
import (
"strings"
"time"
)
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"`
Tracks []*Track `json:"tracks,omitempty"`
Recv uint32 `json:"recv,omitempty"`
Send uint32 `json:"send,omitempty"`
}
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 Contains(medias []*Media, media *Media, codec *Codec) bool {
var ok1, ok2 bool
for _, m := range medias {
if m == media {
ok1 = true
break
}
}
for _, c := range media.Codecs {
if c == codec {
ok2 = true
break
}
}
return ok1 && ok2
}
type Probe struct {
deadline time.Time
items map[interface{}]struct{}
}
func NewProbe(enable bool) *Probe {
if enable {
return &Probe{
deadline: time.Now().Add(time.Second * 3),
items: map[interface{}]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 interface{}) {
if p != nil {
p.items[v] = struct{}{}
}
}

View File

@@ -1,352 +0,0 @@
package streamer
import (
"encoding/json"
"fmt"
"github.com/pion/sdp/v3"
"strconv"
"strings"
"unicode"
)
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
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 ""
}
// 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"`
Codecs []*Codec `json:"codecs,omitempty"`
MID string `json:"mid,omitempty"` // TODO: fixme?
Control string `json:"control,omitempty"` // TODO: fixme?
}
func (m *Media) String() string {
s := fmt.Sprintf("%s, %s", m.Kind, m.Direction)
for _, codec := range m.Codecs {
s += ", " + codec.String()
}
return s
}
func (m *Media) MarshalJSON() ([]byte, error) {
return json.Marshal(m.String())
}
func (m *Media) Clone() *Media {
clone := &Media{
Kind: m.Kind,
Direction: m.Direction,
Codecs: make([]*Codec, len(m.Codecs)),
MID: m.MID,
Control: m.Control,
}
for i, codec := range m.Codecs {
clone.Codecs[i] = codec.Clone()
}
return clone
}
func (m *Media) AV() bool {
return m.Kind == KindVideo || m.Kind == KindAudio
}
func (m *Media) MatchCodec(codec *Codec) *Codec {
for _, c := range m.Codecs {
if c.Match(codec) {
return c
}
}
return nil
}
func (m *Media) MatchMedia(media *Media) *Codec {
if m.Kind != media.Kind {
return nil
}
switch m.Direction {
case DirectionSendonly:
if media.Direction != DirectionRecvonly {
return nil
}
case DirectionRecvonly:
if media.Direction != DirectionSendonly {
return nil
}
default:
panic("wrong direction")
}
for _, localCodec := range m.Codecs {
for _, remoteCodec := range media.Codecs {
if localCodec.Match(remoteCodec) {
return localCodec
}
}
}
return nil
}
func (m *Media) MatchAll() bool {
return len(m.Codecs) > 0 && m.Codecs[0].Name == CodecAll
}
// Codec take best from:
// - deepch/vdk/av.CodecData
// - pion/webrtc.RTPCodecCapability
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 != 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) IsRTP() bool {
return c.PayloadType != PayloadTypeRAW
}
func (c *Codec) Clone() *Codec {
clone := *c
return &clone
}
func (c *Codec) Match(codec *Codec) bool {
switch codec.Name {
case CodecAll, CodecAny:
return true
}
return c.Name == codec.Name &&
(c.ClockRate == codec.ClockRate || codec.ClockRate == 0) &&
(c.Channels == codec.Channels || codec.Channels == 0)
}
func UnmarshalMedias(descriptions []*sdp.MediaDescription) (medias []*Media) {
for _, md := range descriptions {
media := UnmarshalMedia(md)
if media.Direction == DirectionSendRecv {
media.Direction = DirectionRecvonly
medias = append(medias, media)
media = media.Clone()
media.Direction = DirectionSendonly
}
medias = append(medias, media)
}
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{}},
},
}
payloadType := uint8(96)
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(payloadType, name, codec.ClockRate, codec.Channels, codec.FmtpLine)
sd.MediaDescriptions = append(sd.MediaDescriptions, md)
payloadType++
}
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":
m.Control = attr.Value
case "mid":
m.MID = attr.Value
}
}
for _, format := range md.MediaName.Formats {
m.Codecs = append(m.Codecs, UnmarshalCodec(md, format))
}
return m
}
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 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: DirectionRecvonly}
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
}
func atoi(s string) (i int) {
i, _ = strconv.Atoi(s)
return
}

View File

@@ -1,67 +0,0 @@
// Package streamer
//
// 1. Consumer.GetMedias - return list of Media, that Consumer can play/load/consume:
// - Media with DirectionRecvonly for audio/video
// - Media with DirectionSendonly for backchannel
//
// 2. Producer.GetMedias - return list of Media, that Producer can generate/create/produce
// - Media with DirectionSendonly for audio/video
// - Media with DirectionRecvonly for backchannel
//
// 3. Producer.GetTrack - get Media from Producer and Codec from that Media return Track from Producer:
// - Media with DirectionSendonly should Track.WriteRTP after Producer.Start
// - Media with DirectionRecvonly should Track.Bind and wait Track.WriteRTP from Consumer
//
// 4. Consumer.AddTrack - takes Media from Consumer and Track from Producer:
// - Media with DirectionRecvonly should Track.WriteRTP
// - Media with DirectionSendonly should Track.Bind
//
// 5. Producer.Start - run loop with reading rtp.Packet from source
package streamer
// States, Queries and Events
type EventType byte
const (
StateNull EventType = iota
StateReady
StatePaused
StatePlaying
)
// Element base struct for all classes with support feedback
type Element struct {
events []EventFunc
}
type EventFunc func(msg interface{})
func (e *Element) Listen(f EventFunc) {
e.events = append(e.events, f)
}
func (e *Element) Fire(msg interface{}) {
for _, f := range e.events {
f(msg)
}
}
func (e *Element) Push(msg interface{}) {
}
// Producer and Consumer interfaces
type Producer interface {
Listen(f EventFunc)
GetMedias() []*Media
GetTrack(media *Media, codec *Codec) *Track
Start() error
Stop() error
}
type Consumer interface {
Listen(f EventFunc)
GetMedias() []*Media
AddTrack(media *Media, track *Track) *Track
}

View File

@@ -1,89 +0,0 @@
package streamer
import (
"encoding/json"
"fmt"
"github.com/pion/rtp"
"sync"
)
type WriterFunc func(packet *rtp.Packet) error
type WrapperFunc func(push WriterFunc) WriterFunc
type Track struct {
Codec *Codec
Direction string
sink map[*Track]WriterFunc
sinkMu *sync.RWMutex
}
func NewTrack(media *Media, codec *Codec) *Track {
if codec == nil {
codec = media.Codecs[0]
}
return &Track{Codec: codec, Direction: media.Direction, sinkMu: new(sync.RWMutex)}
}
func (t *Track) String() string {
s := t.Codec.String()
if t.Codec.FmtpLine != "" {
s += " " + t.Codec.FmtpLine
}
if t.sinkMu.TryRLock() {
s += fmt.Sprintf(", sinks=%d", len(t.sink))
t.sinkMu.RUnlock()
} else {
s += fmt.Sprintf(", sinks=?")
}
return s
}
func (t *Track) MarshalJSON() ([]byte, error) {
return json.Marshal(t.String())
}
func (t *Track) WriteRTP(p *rtp.Packet) error {
t.sinkMu.RLock()
for _, f := range t.sink {
_ = f(p)
}
t.sinkMu.RUnlock()
return nil
}
// Bind - attach WriterFunc (Consumer) for receiving rtp.Packet(s)
// and return new Track copy. Later you can run Unbind for new Track
func (t *Track) Bind(w WriterFunc) *Track {
t.sinkMu.Lock()
if t.sink == nil {
t.sink = map[*Track]WriterFunc{}
}
clone := *t
t.sink[&clone] = w
t.sinkMu.Unlock()
return &clone
}
// Unbind - detach WriterFunc that related to this Track from
// consuming track data
func (t *Track) Unbind() {
t.sinkMu.Lock()
delete(t.sink, t)
t.sinkMu.Unlock()
}
func (t *Track) GetSink(from *Track) {
t.sinkMu.Lock()
t.sink = from.sink
t.sinkMu.Unlock()
}
func (t *Track) HasSink() bool {
t.sinkMu.RLock()
defer t.sinkMu.RUnlock()
return len(t.sink) > 0
}

View File

@@ -1,53 +0,0 @@
package tapo
import (
"bytes"
"github.com/AlexxIT/go2rtc/pkg/mpegts"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/pion/rtp"
"strconv"
)
func (c *Client) backchannelWriter() streamer.WriterFunc {
w := mpegts.NewWriter()
w.AddPES(68, mpegts.StreamTypePCMATapo)
w.WritePAT()
w.WritePMT()
return func(packet *rtp.Packet) (err error) {
// don't know why 68 and 192
w.WritePES(68, 192, packet.Payload)
err = c.WriteBackchannel(w.Bytes())
w.Reset()
return
}
}
func (c *Client) SetupBackchannel() (err error) {
// if conn1 is not used - we will use it for backchannel
// or we need to start another conn for session2
if c.session1 != "" {
if c.conn2, err = c.newConn(); err != nil {
return
}
} else {
c.conn2 = c.conn1
}
c.session2, err = c.Request(c.conn2, []byte(`{"params":{"talk":{"mode":"aec"},"method":"get"},"seq":3,"type":"request"}`))
return
}
func (c *Client) WriteBackchannel(body []byte) (err error) {
// TODO: fixme (size)
buf := bytes.NewBuffer(nil)
buf.WriteString("----client-stream-boundary--\r\n")
buf.WriteString("Content-Type: audio/mp2t\r\n")
buf.WriteString("X-If-Encrypt: 0\r\n")
buf.WriteString("X-Session-Id: " + c.session2 + "\r\n")
buf.WriteString("Content-Length: " + strconv.Itoa(len(body)) + "\r\n\r\n")
buf.Write(body)
_, err = buf.WriteTo(c.conn2)
return
}

View File

@@ -8,8 +8,8 @@ import (
"encoding/json"
"errors"
"fmt"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/mpegts"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/AlexxIT/go2rtc/pkg/tcp"
"mime/multipart"
"net"
@@ -19,12 +19,13 @@ import (
)
type Client struct {
streamer.Element
core.Listener
url string
medias []*streamer.Media
tracks map[byte]*streamer.Track
medias []*core.Media
receivers []*core.Receiver
sender *core.Sender
conn1 net.Conn
conn2 net.Conn
@@ -33,6 +34,9 @@ type Client struct {
session1 string
session2 string
recv int
send int
}
// block ciphers using cipher block chaining.
@@ -102,7 +106,7 @@ func (c *Client) newDectypter(res *http.Response, username, password string) {
// extract nonce from response
// cipher="AES_128_CBC" username="admin" padding="PKCS7_16" algorithm="MD5" nonce="***"
nonce := res.Header.Get("Key-Exchange")
nonce = streamer.Between(nonce, `nonce="`, `"`)
nonce = core.Between(nonce, `nonce="`, `"`)
key := md5.Sum([]byte(nonce + ":" + password))
iv := md5.Sum([]byte(username + ":" + nonce))
@@ -158,6 +162,8 @@ func (c *Client) Handle() error {
return err
}
c.recv += size
body := make([]byte, size)
b := body
@@ -178,8 +184,11 @@ func (c *Client) Handle() error {
break
}
if track := c.tracks[pkt.PayloadType]; track != nil {
_ = track.WriteRTP(pkt)
for _, receiver := range c.receivers {
if receiver.ID == pkt.PayloadType {
receiver.WriteRTP(pkt)
break
}
}
}
}

View File

@@ -1,18 +1,62 @@
package tapo
import (
"github.com/AlexxIT/go2rtc/pkg/streamer"
"bytes"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/mpegts"
"github.com/pion/rtp"
"strconv"
)
func (c *Client) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.Track {
consCodec := media.MatchCodec(track.Codec)
consTrack := c.GetTrack(media, consCodec)
if consTrack == nil {
return nil
func (c *Client) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiver) error {
if c.sender == nil {
if err := c.SetupBackchannel(); err != nil {
return nil
}
w := mpegts.NewWriter()
w.AddPES(68, mpegts.StreamTypePCMATapo)
w.WritePAT()
w.WritePMT()
c.sender = core.NewSender(media, track.Codec)
c.sender.Handler = func(packet *rtp.Packet) {
// don't know why 68 and 192
w.WritePES(68, 192, packet.Payload)
_ = c.WriteBackchannel(w.Bytes())
w.Reset()
}
}
return track.Bind(func(packet *rtp.Packet) error {
return consTrack.WriteRTP(packet)
})
c.sender.HandleRTP(track)
return nil
}
func (c *Client) SetupBackchannel() (err error) {
// if conn1 is not used - we will use it for backchannel
// or we need to start another conn for session2
if c.session1 != "" {
if c.conn2, err = c.newConn(); err != nil {
return
}
} else {
c.conn2 = c.conn1
}
c.session2, err = c.Request(c.conn2, []byte(`{"params":{"talk":{"mode":"aec"},"method":"get"},"seq":3,"type":"request"}`))
return
}
func (c *Client) WriteBackchannel(body []byte) (err error) {
// TODO: fixme (size)
buf := bytes.NewBuffer(nil)
buf.WriteString("----client-stream-boundary--\r\n")
buf.WriteString("Content-Type: audio/mp2t\r\n")
buf.WriteString("X-If-Encrypt: 0\r\n")
buf.WriteString("X-Session-Id: " + c.session2 + "\r\n")
buf.WriteString("Content-Length: " + strconv.Itoa(len(body)) + "\r\n\r\n")
buf.Write(body)
_, err = buf.WriteTo(c.conn2)
return
}

View File

@@ -1,34 +1,34 @@
package tapo
import (
"encoding/json"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/mpegts"
"github.com/AlexxIT/go2rtc/pkg/streamer"
)
func (c *Client) GetMedias() []*streamer.Media {
// producer should have persistent medias
func (c *Client) GetMedias() []*core.Media {
if c.medias == nil {
// don't know if all Tapo has this capabilities...
c.medias = []*streamer.Media{
c.medias = []*core.Media{
{
Kind: streamer.KindVideo,
Direction: streamer.DirectionSendonly,
Codecs: []*streamer.Codec{
{Name: streamer.CodecH264, ClockRate: 90000, PayloadType: streamer.PayloadTypeRAW},
Kind: core.KindVideo,
Direction: core.DirectionRecvonly,
Codecs: []*core.Codec{
{Name: core.CodecH264, ClockRate: 90000, PayloadType: core.PayloadTypeRAW},
},
},
{
Kind: streamer.KindAudio,
Direction: streamer.DirectionSendonly,
Codecs: []*streamer.Codec{
{Name: streamer.CodecPCMA, ClockRate: 8000, PayloadType: 8},
Kind: core.KindAudio,
Direction: core.DirectionRecvonly,
Codecs: []*core.Codec{
{Name: core.CodecPCMA, ClockRate: 8000, PayloadType: 8},
},
},
{
Kind: streamer.KindAudio,
Direction: streamer.DirectionRecvonly,
Codecs: []*streamer.Codec{
{Name: streamer.CodecPCMA, ClockRate: 8000, PayloadType: 8},
Kind: core.KindAudio,
Direction: core.DirectionSendonly,
Codecs: []*core.Codec{
{Name: core.CodecPCMA, ClockRate: 8000, PayloadType: 8},
},
},
}
@@ -37,44 +37,26 @@ func (c *Client) GetMedias() []*streamer.Media {
return c.medias
}
func (c *Client) GetTrack(media *streamer.Media, codec *streamer.Codec) (track *streamer.Track) {
for _, track := range c.tracks {
func (c *Client) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) {
for _, track := range c.receivers {
if track.Codec == codec {
return track
return track, nil
}
}
if c.tracks == nil {
c.tracks = map[byte]*streamer.Track{}
if err := c.SetupStream(); err != nil {
return nil, err
}
if media.Direction == streamer.DirectionSendonly {
var payloadType byte
if media.Kind == streamer.KindVideo {
payloadType = mpegts.StreamTypeH264
} else {
payloadType = mpegts.StreamTypePCMATapo
}
if err := c.SetupStream(); err != nil {
return nil
}
track = streamer.NewTrack(media, codec)
c.tracks[payloadType] = track
} else {
if err := c.SetupBackchannel(); err != nil {
return nil
}
if w := c.backchannelWriter(); w != nil {
track = streamer.NewTrack(media, codec)
track.Bind(w)
c.tracks[0] = track
}
track := core.NewReceiver(media, codec)
switch media.Kind {
case core.KindVideo:
track.ID = mpegts.StreamTypeH264
case core.KindAudio:
track.ID = mpegts.StreamTypePCMATapo
}
return
c.receivers = append(c.receivers, track)
return track, nil
}
func (c *Client) Start() error {
@@ -82,5 +64,25 @@ func (c *Client) Start() error {
}
func (c *Client) Stop() error {
for _, receiver := range c.receivers {
receiver.Close()
}
if c.sender != nil {
c.sender.Close()
}
return c.Close()
}
func (c *Client) MarshalJSON() ([]byte, error) {
info := &core.Info{
Type: "Tapo active producer",
Medias: c.medias,
Recv: c.recv,
Receivers: c.receivers,
Send: c.send,
}
if c.sender != nil {
info.Senders = []*core.Sender{c.sender}
}
return json.Marshal(info)
}

View File

@@ -1,36 +0,0 @@
package tcp
import (
"github.com/AlexxIT/go2rtc/pkg/streamer"
"net"
)
type Server struct {
streamer.Element
listener net.Listener
closed bool
}
func NewServer(address string) (srv *Server, err error) {
srv = &Server{}
srv.listener, err = net.Listen("tcp", address)
return
}
func (s *Server) Serve() {
for {
conn, err := s.listener.Accept()
if err != nil {
return
}
go func() {
s.Fire(conn)
_ = conn.Close()
}()
}
}
func (s *Server) Close() error {
return s.listener.Close()
}

View File

@@ -8,6 +8,9 @@ import (
"strings"
)
// ReceiveMTU = Ethernet MTU (1500) - IP Header (20) - UDP Header (8)
const ReceiveMTU = 1472
func NewAPI(address string) (*webrtc.API, error) {
// for debug logs add to env: `PION_LOG_DEBUG=all`
m := &webrtc.MediaEngine{}
@@ -41,8 +44,7 @@ func NewAPI(address string) (*webrtc.API, error) {
// fix https://github.com/pion/webrtc/pull/2407
s.SetDTLSInsecureSkipHelloVerify(true)
// Ethernet MTU (1500) - IP Header (20) - UDP Header (8)
s.SetReceiveMTU(1472)
s.SetReceiveMTU(ReceiveMTU)
if address != "" {
address, network, _ := strings.Cut(address, "/")

View File

@@ -1,27 +1,27 @@
package webrtc
import (
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/pion/sdp/v3"
"github.com/pion/webrtc/v3"
)
func (c *Conn) CreateOffer(medias []*streamer.Media) (string, error) {
func (c *Conn) CreateOffer(medias []*core.Media) (string, error) {
// 1. Create transeivers with proper kind and direction
for _, media := range medias {
var err error
switch media.Direction {
case streamer.DirectionRecvonly:
case core.DirectionRecvonly:
_, err = c.pc.AddTransceiverFromKind(
webrtc.NewRTPCodecType(media.Kind),
webrtc.RTPTransceiverInit{Direction: webrtc.RTPTransceiverDirectionRecvonly},
)
case streamer.DirectionSendonly:
case core.DirectionSendonly:
_, err = c.pc.AddTransceiverFromTrack(
NewTrack(media.Kind),
webrtc.RTPTransceiverInit{Direction: webrtc.RTPTransceiverDirectionSendonly},
)
case streamer.DirectionSendRecv:
case core.DirectionSendRecv:
// default transceiver is sendrecv
_, err = c.pc.AddTransceiverFromTrack(NewTrack(media.Kind))
}
@@ -45,7 +45,7 @@ func (c *Conn) CreateOffer(medias []*streamer.Media) (string, error) {
return c.pc.LocalDescription().SDP, nil
}
func (c *Conn) CreateCompleteOffer(medias []*streamer.Media) (string, error) {
func (c *Conn) CreateCompleteOffer(medias []*core.Media) (string, error) {
if _, err := c.CreateOffer(medias); err != nil {
return "", err
}
@@ -68,21 +68,7 @@ func (c *Conn) SetAnswer(answer string) (err error) {
return
}
medias := streamer.UnmarshalMedias(sd.MediaDescriptions)
// sort medias, so video will always be before audio
// and ignore application media from Hass default lovelace card
// ignore media without direction (inactive media)
for _, media := range medias {
if media.Kind == streamer.KindVideo && media.Direction != "" {
c.medias = append(c.medias, media)
}
}
for _, media := range medias {
if media.Kind == streamer.KindAudio && media.Direction != "" {
c.medias = append(c.medias, media)
}
}
c.medias = UnmarshalMedias(sd.MediaDescriptions)
return nil
}

View File

@@ -1,7 +1,7 @@
package webrtc
import (
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/pion/webrtc/v3"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -17,10 +17,10 @@ func TestClient(t *testing.T) {
prod := NewConn(pc)
medias := []*streamer.Media{
{Kind: streamer.KindVideo, Direction: streamer.DirectionRecvonly},
{Kind: streamer.KindAudio, Direction: streamer.DirectionRecvonly},
{Kind: streamer.KindAudio, Direction: streamer.DirectionSendonly},
medias := []*core.Media{
{Kind: core.KindVideo, Direction: core.DirectionRecvonly},
{Kind: core.KindAudio, Direction: core.DirectionRecvonly},
{Kind: core.KindAudio, Direction: core.DirectionSendonly},
}
offer, err := prod.CreateOffer(medias)

View File

@@ -2,9 +2,6 @@ package webrtc
import (
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/h264"
"github.com/AlexxIT/go2rtc/pkg/h265"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/pion/rtcp"
"github.com/pion/rtp"
"github.com/pion/webrtc/v3"
@@ -12,19 +9,20 @@ import (
)
type Conn struct {
streamer.Element
core.Listener
UserAgent string
Desc string
Mode streamer.Mode
Mode core.Mode
pc *webrtc.PeerConnection
medias []*streamer.Media
tracks []*streamer.Track
medias []*core.Media
receivers []*core.Receiver
senders []*core.Sender
receive int
send int
recv int
send int
offer string
remote string
@@ -56,13 +54,26 @@ func NewConn(pc *webrtc.PeerConnection) *Conn {
)
})
pc.OnTrack(func(remote *webrtc.TrackRemote, _ *webrtc.RTPReceiver) {
track := c.getRecvTrack(remote)
if track == nil {
return // it's OK when we not need, for example, audio from producer
pc.OnTrack(func(remote *webrtc.TrackRemote, receiver *webrtc.RTPReceiver) {
media, codec := c.getMediaCodec(remote)
if media == nil {
return
}
if c.Mode == streamer.ModePassiveProducer && remote.Kind() == webrtc.RTPCodecTypeVideo {
track, err := c.GetTrack(media, codec)
if err != nil {
return
}
switch c.Mode {
case core.ModePassiveProducer, core.ModeActiveProducer:
// replace the theoretical list of codecs with the actual list of codecs
if len(media.Codecs) > 1 {
media.Codecs = []*core.Codec{codec}
}
}
if c.Mode == core.ModePassiveProducer && remote.Kind() == webrtc.RTPCodecTypeVideo {
go func() {
pkts := []rtcp.Packet{&rtcp.PictureLossIndication{MediaSSRC: uint32(remote.SSRC())}}
for range time.NewTicker(time.Second * 2).C {
@@ -74,15 +85,20 @@ func NewConn(pc *webrtc.PeerConnection) *Conn {
}
for {
packet, _, err := remote.ReadRTP()
b := make([]byte, ReceiveMTU)
n, _, err := remote.Read(b)
if err != nil {
return
}
if len(packet.Payload) == 0 {
continue
c.recv += n
packet := &rtp.Packet{}
if err := packet.Unmarshal(b[:n]); err != nil {
return
}
c.receive += len(packet.Payload)
_ = track.WriteRTP(packet)
track.WriteRTP(packet)
}
})
@@ -127,106 +143,34 @@ func (c *Conn) getTranseiver(mid string) *webrtc.RTPTransceiver {
}
return nil
}
func (c *Conn) addSendTrack(media *streamer.Media, track *streamer.Track) *streamer.Track {
tr := c.getTranseiver(media.MID)
sender := tr.Sender()
localTrack := sender.Track().(*Track)
codec := track.Codec
// important to get remote PayloadType
payloadType := media.MatchCodec(codec).PayloadType
push := func(packet *rtp.Packet) error {
c.send += packet.MarshalSize()
return localTrack.WriteRTP(payloadType, packet)
}
switch codec.Name {
case streamer.CodecH264:
wrapper := h264.RTPPay(1200)
push = wrapper(push)
if codec.IsRTP() {
wrapper = h264.RTPDepay(track)
} else {
wrapper = h264.RepairAVC(track)
}
push = wrapper(push)
case streamer.CodecH265:
// SafariPay because it is the only browser in the world
// that supports WebRTC + H265
wrapper := h265.SafariPay(1200)
push = wrapper(push)
wrapper = h265.RTPDepay(track)
push = wrapper(push)
}
return track.Bind(push)
}
func (c *Conn) getRecvTrack(remote *webrtc.TrackRemote) *streamer.Track {
payloadType := uint8(remote.PayloadType())
switch c.Mode {
case streamer.ModePassiveConsumer:
// Situation:
// - Browser (passive consumer) connects to go2rtc for receiving AV from IP-camera
// - Video and audio tracks marked as local "sendonly"
// - Browser sends microphone remote track to go2rtc, this track marked as local "recvonly"
// - go2rtc should ReadRTP from this remote track and sends it to camera
for _, track := range c.tracks {
if track.Direction == streamer.DirectionRecvonly && track.Codec.PayloadType == payloadType {
return track
}
func (c *Conn) getMediaCodec(remote *webrtc.TrackRemote) (*core.Media, *core.Codec) {
for _, tr := range c.pc.GetTransceivers() {
// search Transeiver for this TrackRemote
if tr.Receiver() == nil || tr.Receiver().Track() != remote {
continue
}
case streamer.ModeActiveProducer:
// Situation:
// - go2rtc (active producer) connects to remote server (ex. webtorrent) for receiving AV
// - remote server sends remote tracks, this tracks marked as remote "sendonly"
for _, track := range c.tracks {
if track.Direction == streamer.DirectionSendonly && track.Codec.PayloadType == payloadType {
return track
}
}
case streamer.ModePassiveProducer:
// Situation:
// - OBS Studio (passive producer) connects to go2rtc for send AV
// - OBS sends remote tracks, this tracks marked as remote "sendonly"
for i, media := range c.medias {
// check only tracks with same kind
if media.Kind != remote.Kind().String() {
continue
}
// check only incoming tracks (remote media "sendonly")
if media.Direction != streamer.DirectionSendonly {
// search Media for this MID
for _, media := range c.medias {
if media.ID != tr.Mid() || media.Direction != core.DirectionRecvonly {
continue
}
// search codec for this PayloadType
for _, codec := range media.Codecs {
if codec.PayloadType != payloadType {
if codec.PayloadType != uint8(remote.PayloadType()) {
continue
}
// leave only one codec in supported media list
if len(media.Codecs) > 1 {
c.medias[i].Codecs = []*streamer.Codec{codec}
}
// forward request to passive producer GetTrack
// will create NewTrack for sendonly media
return c.GetTrack(media, codec)
return media, codec
}
}
default:
panic("not implemented")
}
return nil
// fix moment when core.ModePassiveProducer or core.ModeActiveProducer
// sends new codec with new payload type to same media
// check GetTrack
panic(core.Caller())
return nil, nil
}

View File

@@ -2,72 +2,77 @@ package webrtc
import (
"encoding/json"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/h264"
"github.com/AlexxIT/go2rtc/pkg/h265"
"github.com/pion/rtp"
"github.com/pion/webrtc/v3"
)
func (c *Conn) GetMedias() []*streamer.Media {
func (c *Conn) GetMedias() []*core.Media {
return c.medias
}
func (c *Conn) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.Track {
switch c.Mode {
case streamer.ModePassiveConsumer:
switch track.Direction {
case streamer.DirectionSendonly:
// send our track to WebRTC consumer
return c.addSendTrack(media, track)
func (c *Conn) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) error {
core.Assert(media.Direction == core.DirectionSendonly)
case streamer.DirectionRecvonly:
// receive track from WebRTC consumer (microphone, backchannel, two way audio)
return c.addConsumerRecvTrack(media, track)
}
case streamer.ModePassiveProducer:
// "Stream to camera" function
consCodec := media.MatchCodec(track.Codec)
consTrack := c.GetTrack(media, consCodec)
if consTrack == nil {
for _, sender := range c.senders {
if sender.Codec == codec {
sender.HandleRTP(track)
return nil
}
return track.Bind(func(packet *rtp.Packet) error {
return consTrack.WriteRTP(packet)
})
}
panic("not implemented")
}
func (c *Conn) addConsumerRecvTrack(media *streamer.Media, track *streamer.Track) *streamer.Track {
params := webrtc.RTPCodecParameters{
RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: MimeType(track.Codec),
ClockRate: track.Codec.ClockRate,
Channels: track.Codec.Channels,
},
PayloadType: 0, // don't know if this necessary
switch c.Mode {
case core.ModePassiveConsumer: // video/audio for browser
case core.ModeActiveProducer: // go2rtc as WebRTC client (backchannel)
case core.ModePassiveProducer: // WebRTC/WHIP
default:
panic(core.Caller())
}
tr := c.getTranseiver(media.MID)
localTrack := c.getTranseiver(media.ID).Sender().Track().(*Track)
// set codec for consumer recv track so remote peer should send media with this codec
_ = tr.SetCodecPreferences([]webrtc.RTPCodecParameters{params})
sender := core.NewSender(media, track.Codec)
sender.Handler = func(packet *rtp.Packet) {
c.send += packet.MarshalSize()
//important to send with remote PayloadType
_ = localTrack.WriteRTP(codec.PayloadType, packet)
}
c.tracks = append(c.tracks, track)
return track
switch codec.Name {
case core.CodecH264:
sender.Handler = h264.RTPPay(1200, sender.Handler)
if track.Codec.IsRTP() {
sender.Handler = h264.RTPDepay(track.Codec, sender.Handler)
} else {
sender.Handler = h264.RepairAVC(track.Codec, sender.Handler)
}
case core.CodecH265:
// SafariPay because it is the only browser in the world
// that supports WebRTC + H265
sender.Handler = h265.SafariPay(1200, sender.Handler)
if track.Codec.IsRTP() {
sender.Handler = h265.RTPDepay(track.Codec, sender.Handler)
}
}
sender.HandleRTP(track)
c.senders = append(c.senders, sender)
return nil
}
func (c *Conn) MarshalJSON() ([]byte, error) {
info := &streamer.Info{
info := &core.Info{
Type: c.Desc + " " + c.Mode.String(),
RemoteAddr: c.remote,
UserAgent: c.UserAgent,
Medias: c.medias,
Tracks: c.tracks,
Recv: uint32(c.receive),
Send: uint32(c.send),
Receivers: c.receivers,
Senders: c.senders,
Recv: c.recv,
Send: c.send,
}
return json.Marshal(info)
}

View File

@@ -3,8 +3,9 @@ package webrtc
import (
"errors"
"fmt"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/pion/ice/v2"
"github.com/pion/sdp/v3"
"github.com/pion/stun"
"github.com/pion/webrtc/v3"
"hash/crc32"
@@ -14,6 +15,43 @@ import (
"time"
)
func UnmarshalMedias(descriptions []*sdp.MediaDescription) (medias []*core.Media) {
// 1. Sort medias, so video will always be before audio
// 2. Ignore application media from Hass default lovelace card
// 3. Ignore media without direction (inactive media)
// 4. Inverse media direction (because it is remote peer medias list)
for _, kind := range []string{core.KindVideo, core.KindAudio} {
for _, md := range descriptions {
if md.MediaName.Media != kind {
continue
}
media := core.UnmarshalMedia(md)
switch media.Direction {
case core.DirectionSendRecv:
media.Direction = core.DirectionRecvonly
medias = append(medias, media)
media = media.Clone()
media.Direction = core.DirectionSendonly
case core.DirectionRecvonly:
media.Direction = core.DirectionSendonly
case core.DirectionSendonly:
media.Direction = core.DirectionRecvonly
case "":
continue
}
medias = append(medias, media)
}
}
return
}
func NewCandidate(network, address string) (string, error) {
i := strings.LastIndexByte(address, ':')
if i < 0 {
@@ -135,25 +173,25 @@ func IsIP(host string) bool {
return true
}
func MimeType(codec *streamer.Codec) string {
func MimeType(codec *core.Codec) string {
switch codec.Name {
case streamer.CodecH264:
case core.CodecH264:
return webrtc.MimeTypeH264
case streamer.CodecH265:
case core.CodecH265:
return webrtc.MimeTypeH265
case streamer.CodecVP8:
case core.CodecVP8:
return webrtc.MimeTypeVP8
case streamer.CodecVP9:
case core.CodecVP9:
return webrtc.MimeTypeVP9
case streamer.CodecAV1:
case core.CodecAV1:
return webrtc.MimeTypeAV1
case streamer.CodecPCMU:
case core.CodecPCMU:
return webrtc.MimeTypePCMU
case streamer.CodecPCMA:
case core.CodecPCMA:
return webrtc.MimeTypePCMA
case streamer.CodecOpus:
case core.CodecOpus:
return webrtc.MimeTypeOpus
case streamer.CodecG722:
case core.CodecG722:
return webrtc.MimeTypeG722
}
panic("not implemented")

Some files were not shown because too many files have changed in this diff Show More