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 ( import (
"github.com/AlexxIT/go2rtc/cmd/api" "github.com/AlexxIT/go2rtc/cmd/api"
"github.com/AlexxIT/go2rtc/cmd/streams" "github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/pkg/streamer" "github.com/AlexxIT/go2rtc/pkg/core"
) )
func Init() { func Init() {
@@ -12,6 +12,6 @@ func Init() {
streams.HandleFunc("null", nullHandler) streams.HandleFunc("null", nullHandler)
} }
func nullHandler(string) (streamer.Producer, error) { func nullHandler(string) (core.Producer, error) {
return nil, nil return nil, nil
} }

View File

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

View File

@@ -4,15 +4,15 @@ import (
"bytes" "bytes"
"github.com/AlexxIT/go2rtc/cmd/app" "github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/cmd/streams" "github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/shell" "github.com/AlexxIT/go2rtc/pkg/shell"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"os/exec" "os/exec"
) )
func Init() { func Init() {
log := app.GetLogger("echo") 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:]) args := shell.QuoteSplit(url[5:])
b, err := exec.Command(args[0], args[1:]...).Output() 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/app"
"github.com/AlexxIT/go2rtc/cmd/rtsp" "github.com/AlexxIT/go2rtc/cmd/rtsp"
"github.com/AlexxIT/go2rtc/cmd/streams" "github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
pkg "github.com/AlexxIT/go2rtc/pkg/rtsp" pkg "github.com/AlexxIT/go2rtc/pkg/rtsp"
"github.com/AlexxIT/go2rtc/pkg/shell" "github.com/AlexxIT/go2rtc/pkg/shell"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"os" "os"
"os/exec" "os/exec"
@@ -48,7 +48,7 @@ func Init() {
log = app.GetLogger("exec") log = app.GetLogger("exec")
} }
func Handle(url string) (streamer.Producer, error) { func Handle(url string) (core.Producer, error) {
sum := md5.Sum([]byte(url)) sum := md5.Sum([]byte(url))
path := "/" + hex.EncodeToString(sum[:]) path := "/" + hex.EncodeToString(sum[:])
@@ -67,7 +67,7 @@ func Handle(url string) (streamer.Producer, error) {
cmd.Stderr = os.Stderr cmd.Stderr = os.Stderr
} }
ch := make(chan streamer.Producer) ch := make(chan core.Producer)
waitersMu.Lock() waitersMu.Lock()
waiters[path] = ch waiters[path] = ch
@@ -116,5 +116,5 @@ func Handle(url string) (streamer.Producer, error) {
// internal // internal
var log zerolog.Logger var log zerolog.Logger
var waiters = map[string]chan streamer.Producer{} var waiters = map[string]chan core.Producer{}
var waitersMu sync.Mutex var waitersMu sync.Mutex

View File

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

View File

@@ -2,7 +2,7 @@ package device
import ( import (
"bytes" "bytes"
"github.com/AlexxIT/go2rtc/pkg/streamer" "github.com/AlexxIT/go2rtc/pkg/core"
"io/ioutil" "io/ioutil"
"os/exec" "os/exec"
"strings" "strings"
@@ -12,8 +12,8 @@ import (
const deviceInputPrefix = "-f v4l2" const deviceInputPrefix = "-f v4l2"
func deviceInputSuffix(videoIdx, audioIdx int) string { func deviceInputSuffix(videoIdx, audioIdx int) string {
video := findMedia(streamer.KindVideo, videoIdx) video := findMedia(core.KindVideo, videoIdx)
return video.MID return video.ID
} }
func loadMedias() { func loadMedias() {
@@ -23,8 +23,8 @@ func loadMedias() {
} }
for _, file := range files { for _, file := range files {
log.Trace().Msg("[ffmpeg] " + file.Name()) log.Trace().Msg("[ffmpeg] " + file.Name())
if strings.HasPrefix(file.Name(), streamer.KindVideo) { if strings.HasPrefix(file.Name(), core.KindVideo) {
media := loadMedia(streamer.KindVideo, "/dev/"+file.Name()) media := loadMedia(core.KindVideo, "/dev/"+file.Name())
if media != nil { if media != nil {
medias = append(medias, media) 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( cmd := exec.Command(
Bin, "-hide_banner", "-f", "v4l2", "-list_formats", "all", "-i", name, Bin, "-hide_banner", "-f", "v4l2", "-list_formats", "all", "-i", name,
) )
@@ -44,5 +44,5 @@ func loadMedia(kind, name string) *streamer.Media {
return nil 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 ( import (
"bytes" "bytes"
"github.com/AlexxIT/go2rtc/pkg/streamer" "github.com/AlexxIT/go2rtc/pkg/core"
"os/exec" "os/exec"
"strings" "strings"
) )
@@ -11,15 +11,15 @@ import (
const deviceInputPrefix = "-f dshow" const deviceInputPrefix = "-f dshow"
func deviceInputSuffix(videoIdx, audioIdx int) string { func deviceInputSuffix(videoIdx, audioIdx int) string {
video := findMedia(streamer.KindVideo, videoIdx) video := findMedia(core.KindVideo, videoIdx)
audio := findMedia(streamer.KindAudio, audioIdx) audio := findMedia(core.KindAudio, audioIdx)
switch { switch {
case video != nil && audio != nil: case video != nil && audio != nil:
return `video="` + video.MID + `":audio=` + audio.MID + `"` return `video="` + video.ID + `":audio=` + audio.ID + `"`
case video != nil: case video != nil:
return `video="` + video.MID + `"` return `video="` + video.ID + `"`
case audio != nil: case audio != nil:
return `audio="` + audio.MID + `"` return `audio="` + audio.ID + `"`
} }
return "" return ""
} }
@@ -37,9 +37,9 @@ func loadMedias() {
for _, line := range lines { for _, line := range lines {
var kind string var kind string
if strings.HasSuffix(line, "(video)") { if strings.HasSuffix(line, "(video)") {
kind = streamer.KindVideo kind = core.KindVideo
} else if strings.HasSuffix(line, "(audio)") { } else if strings.HasSuffix(line, "(audio)") {
kind = streamer.KindAudio kind = core.KindAudio
} else { } else {
continue continue
} }
@@ -52,6 +52,6 @@ func loadMedias() {
} }
} }
func loadMedia(kind, name string) *streamer.Media { func loadMedia(kind, name string) *core.Media {
return &streamer.Media{Kind: kind, MID: name} return &core.Media{Kind: kind, ID: name}
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,9 +3,9 @@ package rtsp
import ( import (
"github.com/AlexxIT/go2rtc/cmd/app" "github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/cmd/streams" "github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/mp4" "github.com/AlexxIT/go2rtc/pkg/mp4"
"github.com/AlexxIT/go2rtc/pkg/rtsp" "github.com/AlexxIT/go2rtc/pkg/rtsp"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/AlexxIT/go2rtc/pkg/tcp" "github.com/AlexxIT/go2rtc/pkg/tcp"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"net" "net"
@@ -86,9 +86,9 @@ var Port string
var log zerolog.Logger var log zerolog.Logger
var handlers []Handler 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 backchannel := true
if i := strings.IndexByte(url, '#'); i > 0 { if i := strings.IndexByte(url, '#'); i > 0 {
@@ -98,11 +98,7 @@ func rtspHandler(url string) (streamer.Producer, error) {
url = url[:i] url = url[:i]
} }
conn, err := rtsp.NewClient(url) conn := rtsp.NewClient(url)
if err != nil {
return nil, err
}
conn.UserAgent = app.UserAgent conn.UserAgent = app.UserAgent
if log.Trace().Enabled() { 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 return nil, err
} }
conn.Backchannel = backchannel conn.Backchannel = backchannel
if err = conn.Describe(); err != nil { if err := conn.Describe(); err != nil {
if !backchannel { if !backchannel {
return nil, err return nil, err
} }
@@ -211,9 +207,6 @@ func tcpHandler(conn *rtsp.Conn) {
closer = func() { closer = func() {
stream.RemoveProducer(conn) 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 ( import (
"fmt" "fmt"
"github.com/AlexxIT/go2rtc/pkg/streamer" "github.com/AlexxIT/go2rtc/pkg/core"
"strings" "strings"
"sync" "sync"
) )
type Handler func(url string) (streamer.Producer, error) type Handler func(url string) (core.Producer, error)
var handlers = map[string]Handler{} var handlers = map[string]Handler{}
var handlersMu sync.Mutex var handlersMu sync.Mutex
@@ -32,7 +32,7 @@ func HasProducer(url string) bool {
return getHandler(url) != nil return getHandler(url) != nil
} }
func GetProducer(url string) (streamer.Producer, error) { func GetProducer(url string) (core.Producer, error) {
handler := getHandler(url) handler := getHandler(url)
if handler == nil { if handler == nil {
return nil, fmt.Errorf("unsupported scheme: %s", url) return nil, fmt.Errorf("unsupported scheme: %s", url)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,15 +2,14 @@ package aac
import ( import (
"encoding/binary" "encoding/binary"
"github.com/AlexxIT/go2rtc/pkg/streamer" "github.com/AlexxIT/go2rtc/pkg/core"
"github.com/pion/rtp" "github.com/pion/rtp"
) )
const RTPPacketVersionAAC = 0 const RTPPacketVersionAAC = 0
func RTPDepay(track *streamer.Track) streamer.WrapperFunc { func RTPDepay(handler core.HandlerFunc) core.HandlerFunc {
return func(push streamer.WriterFunc) streamer.WriterFunc { return func(packet *rtp.Packet) {
return func(packet *rtp.Packet) error {
// support ONLY 2 bytes header size! // support ONLY 2 bytes header size!
// streamtype=5;profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=1408 // streamtype=5;profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=1408
headersSize := binary.BigEndian.Uint16(packet.Payload) >> 3 headersSize := binary.BigEndian.Uint16(packet.Payload) >> 3
@@ -25,18 +24,17 @@ func RTPDepay(track *streamer.Track) streamer.WrapperFunc {
clone := *packet clone := *packet
clone.Version = RTPPacketVersionAAC clone.Version = RTPPacketVersionAAC
clone.Payload = data clone.Payload = data
return push(&clone) handler(&clone)
}
} }
} }
func RTPPay(mtu uint16) streamer.WrapperFunc { func RTPPay(handler core.HandlerFunc) core.HandlerFunc {
sequencer := rtp.NewRandomSequencer() sequencer := rtp.NewRandomSequencer()
return func(push streamer.WriterFunc) streamer.WriterFunc { return func(packet *rtp.Packet) {
return func(packet *rtp.Packet) error {
if packet.Version != RTPPacketVersionAAC { if packet.Version != RTPPacketVersionAAC {
return push(packet) handler(packet)
return
} }
// support ONLY one unit in payload // support ONLY one unit in payload
@@ -56,8 +54,7 @@ func RTPPay(mtu uint16) streamer.WrapperFunc {
}, },
Payload: payload, Payload: payload,
} }
return push(&clone) 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 ( import (
cryptorand "crypto/rand" cryptorand "crypto/rand"
"github.com/rs/zerolog/log"
"runtime"
"strconv"
"strings"
) )
const digits = "0123456789abcdefghijklmnopqrstuvwxyz" const digits = "0123456789abcdefghijklmnopqrstuvwxyz"
@@ -17,3 +21,35 @@ func RandString(size byte) string {
} }
return string(b) 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 ( import (
"fmt" "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" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/h264" "github.com/AlexxIT/go2rtc/pkg/h264"
"github.com/AlexxIT/go2rtc/pkg/h265" "github.com/AlexxIT/go2rtc/pkg/h265"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/pion/rtp" "github.com/pion/rtp"
"io" "io"
"net" "net"
@@ -19,7 +19,7 @@ import (
) )
type Client struct { type Client struct {
streamer.Element core.Listener
uri string uri string
conn net.Conn conn net.Conn
@@ -28,14 +28,17 @@ type Client struct {
seq uint32 seq uint32
stream string stream string
medias []*streamer.Media medias []*core.Media
videoTrack *streamer.Track receivers []*core.Receiver
audioTrack *streamer.Track videoTrack *core.Receiver
audioTrack *core.Receiver
videoTS uint32 videoTS uint32
videoDT uint32 videoDT uint32
audioTS uint32 audioTS uint32
audioSeq uint16 audioSeq uint16
recv uint32
} }
type Response map[string]any 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) //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 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) //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 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) //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 return
} }
c.recv += 20
if b[0] != 255 { if b[0] != 255 {
return nil, errors.New("read error") return nil, errors.New("read error")
} }
@@ -307,6 +312,8 @@ func (c *Client) Response() (b []byte, err error) {
return return
} }
c.recv += size
return return
} }
@@ -328,21 +335,21 @@ func (c *Client) ResponseJSON() (res Response, err error) {
} }
func (c *Client) AddVideoTrack(mediaCode byte, payload []byte) { func (c *Client) AddVideoTrack(mediaCode byte, payload []byte) {
var codec *streamer.Codec var codec *core.Codec
switch mediaCode { switch mediaCode {
case 2: case 2:
codec = &streamer.Codec{ codec = &core.Codec{
Name: streamer.CodecH264, Name: core.CodecH264,
ClockRate: 90000, ClockRate: 90000,
PayloadType: streamer.PayloadTypeRAW, PayloadType: core.PayloadTypeRAW,
FmtpLine: h264.GetFmtpLine(payload), FmtpLine: h264.GetFmtpLine(payload),
} }
case 0x03, 0x13: case 0x03, 0x13:
codec = &streamer.Codec{ codec = &core.Codec{
Name: streamer.CodecH265, Name: core.CodecH265,
ClockRate: 90000, ClockRate: 90000,
PayloadType: streamer.PayloadTypeRAW, PayloadType: core.PayloadTypeRAW,
FmtpLine: "profile-id=1", FmtpLine: "profile-id=1",
} }
@@ -369,14 +376,15 @@ func (c *Client) AddVideoTrack(mediaCode byte, payload []byte) {
return return
} }
media := &streamer.Media{ media := &core.Media{
Kind: streamer.KindVideo, Kind: core.KindVideo,
Direction: streamer.DirectionSendonly, Direction: core.DirectionRecvonly,
Codecs: []*streamer.Codec{codec}, Codecs: []*core.Codec{codec},
} }
c.medias = append(c.medias, media) 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} 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) { 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 // 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 // PCM8 = 7, G729, IMA_ADPCM, G711U, G721, PCM8_VWIS, MS_ADPCM, G711A, PCM16
var codec *streamer.Codec var codec *core.Codec
switch mediaCode { switch mediaCode {
case 10: // G711U case 10: // G711U
codec = &streamer.Codec{ codec = &core.Codec{
Name: streamer.CodecPCMU, Name: core.CodecPCMU,
} }
case 14: // G711A case 14: // G711A
codec = &streamer.Codec{ codec = &core.Codec{
Name: streamer.CodecPCMA, Name: core.CodecPCMA,
} }
default: default:
println("[DVRIP] unsupported audio codec:", mediaCode) println("[DVRIP] unsupported audio codec:", mediaCode)
@@ -403,14 +411,15 @@ func (c *Client) AddAudioTrack(mediaCode byte, sampleRate byte) {
codec.ClockRate = sampleRates[sampleRate-1] codec.ClockRate = sampleRates[sampleRate-1]
} }
media := &streamer.Media{ media := &core.Media{
Kind: streamer.KindAudio, Kind: core.KindAudio,
Direction: streamer.DirectionSendonly, Direction: core.DirectionRecvonly,
Codecs: []*streamer.Codec{codec}, Codecs: []*core.Codec{codec},
} }
c.medias = append(c.medias, media) 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 { func SofiaHash(password string) string {

View File

@@ -1,19 +1,21 @@
package dvrip 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 return c.medias
} }
func (c *Client) GetTrack(media *streamer.Media, codec *streamer.Codec) *streamer.Track { func (c *Client) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) {
if c.videoTrack != nil && c.videoTrack.Codec == codec { for _, track := range c.receivers {
return c.videoTrack 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 { func (c *Client) Start() error {
@@ -21,5 +23,19 @@ func (c *Client) Start() error {
} }
func (c *Client) Stop() error { func (c *Client) Stop() error {
for _, receiver := range c.receivers {
receiver.Close()
}
return c.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 ( import (
"bytes" "bytes"
"encoding/binary" "encoding/binary"
"github.com/AlexxIT/go2rtc/pkg/streamer" "github.com/AlexxIT/go2rtc/pkg/core"
"github.com/pion/rtp" "github.com/pion/rtp"
) )
@@ -164,17 +164,15 @@ func EncodeAVC(nals ...[]byte) (avc []byte) {
return return
} }
func RepairAVC(track *streamer.Track) streamer.WrapperFunc { func RepairAVC(codec *core.Codec, handler core.HandlerFunc) core.HandlerFunc {
sps, pps := GetParameterSet(track.Codec.FmtpLine) sps, pps := GetParameterSet(codec.FmtpLine)
ps := EncodeAVC(sps, pps) ps := EncodeAVC(sps, pps)
return func(push streamer.WriterFunc) streamer.WriterFunc { return func(packet *rtp.Packet) {
return func(packet *rtp.Packet) (err error) {
if NALUType(packet.Payload) == NALUTypeIFrame { if NALUType(packet.Payload) == NALUTypeIFrame {
packet.Payload = Join(ps, packet.Payload) packet.Payload = Join(ps, packet.Payload)
} }
return push(packet) handler(packet)
}
} }
} }

View File

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

View File

@@ -2,7 +2,7 @@ package h264
import ( import (
"encoding/binary" "encoding/binary"
"github.com/AlexxIT/go2rtc/pkg/streamer" "github.com/AlexxIT/go2rtc/pkg/core"
"github.com/pion/rtp" "github.com/pion/rtp"
"github.com/pion/rtp/codecs" "github.com/pion/rtp/codecs"
) )
@@ -11,21 +11,20 @@ const RTPPacketVersionAVC = 0
const PSMaxSize = 128 // the biggest SPS I've seen is 48 (EZVIZ CS-CV210) 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} depack := &codecs.H264Packet{IsAVC: true}
sps, pps := GetParameterSet(track.Codec.FmtpLine) sps, pps := GetParameterSet(codec.FmtpLine)
ps := EncodeAVC(sps, pps) ps := EncodeAVC(sps, pps)
buf := make([]byte, 0, 512*1024) // 512K buf := make([]byte, 0, 512*1024) // 512K
return func(push streamer.WriterFunc) streamer.WriterFunc { return func(packet *rtp.Packet) {
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) //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) payload, err := depack.Unmarshal(packet.Payload)
if len(payload) == 0 || err != nil { if len(payload) == 0 || err != nil {
return nil return
} }
// Fix TP-Link Tapo TC70: sends SPS and PPS with packet.Marker = true // Fix TP-Link Tapo TC70: sends SPS and PPS with packet.Marker = true
@@ -34,11 +33,11 @@ func RTPDepay(track *streamer.Track) streamer.WrapperFunc {
switch NALUType(payload) { switch NALUType(payload) {
case NALUTypeSPS, NALUTypePPS: case NALUTypeSPS, NALUTypePPS:
buf = append(buf, payload...) buf = append(buf, payload...)
return nil return
case NALUTypeSEI: case NALUTypeSEI:
// RtspServer https://github.com/AlexxIT/go2rtc/issues/244 // RtspServer https://github.com/AlexxIT/go2rtc/issues/244
// sends, marked SPS, marked PPS, marked SEI, marked IFrame // sends, marked SPS, marked PPS, marked SEI, marked IFrame
return nil return
} }
} }
@@ -56,7 +55,7 @@ func RTPDepay(track *streamer.Track) streamer.WrapperFunc {
// check if only one NAL (fix ffmpeg transcoding for Reolink RLC-510A) // check if only one NAL (fix ffmpeg transcoding for Reolink RLC-510A)
if i == len(payload) { if i == len(payload) {
return nil return
} }
payload = payload[i:] payload = payload[i:]
@@ -69,7 +68,7 @@ func RTPDepay(track *streamer.Track) streamer.WrapperFunc {
// collect all NALs for Access Unit // collect all NALs for Access Unit
if !packet.Marker { if !packet.Marker {
buf = append(buf, payload...) buf = append(buf, payload...)
return nil return
} }
if len(buf) > 0 { if len(buf) > 0 {
@@ -90,20 +89,19 @@ func RTPDepay(track *streamer.Track) streamer.WrapperFunc {
clone := *packet clone := *packet
clone.Version = RTPPacketVersionAVC clone.Version = RTPPacketVersionAVC
clone.Payload = payload clone.Payload = payload
return push(&clone) handler(&clone)
}
} }
} }
func RTPPay(mtu uint16) streamer.WrapperFunc { func RTPPay(mtu uint16, handler core.HandlerFunc) core.HandlerFunc {
payloader := &Payloader{IsAVC: true} payloader := &Payloader{IsAVC: true}
sequencer := rtp.NewRandomSequencer() sequencer := rtp.NewRandomSequencer()
mtu -= 12 // rtp.Header size mtu -= 12 // rtp.Header size
return func(push streamer.WriterFunc) streamer.WriterFunc { return func(packet *rtp.Packet) {
return func(packet *rtp.Packet) error {
if packet.Version != RTPPacketVersionAVC { if packet.Version != RTPPacketVersionAVC {
return push(packet) handler(packet)
return
} }
payloads := payloader.Payload(mtu, packet.Payload) payloads := payloader.Payload(mtu, packet.Payload)
@@ -118,12 +116,7 @@ func RTPPay(mtu uint16) streamer.WrapperFunc {
}, },
Payload: payload, Payload: payload,
} }
if err := push(&clone); err != nil { handler(&clone)
return err
}
}
return nil
} }
} }
} }

View File

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

View File

@@ -2,20 +2,19 @@ package h265
import ( import (
"encoding/binary" "encoding/binary"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/h264" "github.com/AlexxIT/go2rtc/pkg/h264"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/pion/rtp" "github.com/pion/rtp"
) )
func RTPDepay(track *streamer.Track) streamer.WrapperFunc { func RTPDepay(codec *core.Codec, handler core.HandlerFunc) core.HandlerFunc {
//vps, sps, pps := GetParameterSet(track.Codec.FmtpLine) //vps, sps, pps := GetParameterSet(codec.FmtpLine)
//ps := h264.EncodeAVC(vps, sps, pps) //ps := h264.EncodeAVC(vps, sps, pps)
buf := make([]byte, 0, 512*1024) // 512K buf := make([]byte, 0, 512*1024) // 512K
var nuStart int var nuStart int
return func(push streamer.WriterFunc) streamer.WriterFunc { return func(packet *rtp.Packet) {
return func(packet *rtp.Packet) error {
data := packet.Payload data := packet.Payload
nuType := (data[0] >> 1) & 0x3F 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) //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)
@@ -26,7 +25,7 @@ func RTPDepay(track *streamer.Track) streamer.WrapperFunc {
case NALUTypeVPS, NALUTypeSPS, NALUTypePPS: case NALUTypeVPS, NALUTypeSPS, NALUTypePPS:
packet.Marker = false packet.Marker = false
case NALUTypePrefixSEI, NALUTypeSuffixSEI: case NALUTypePrefixSEI, NALUTypeSuffixSEI:
return nil return
} }
} }
@@ -44,10 +43,10 @@ func RTPDepay(track *streamer.Track) streamer.WrapperFunc {
buf = append(buf, 0, 0, 0, 0) // NAL unit size buf = append(buf, 0, 0, 0, 0) // NAL unit size
buf = append(buf, (data[0]&0x81)|(nuType<<1), data[1]) buf = append(buf, (data[0]&0x81)|(nuType<<1), data[1])
buf = append(buf, data[3:]...) buf = append(buf, data[3:]...)
return nil return
case 0: // continue case 0: // continue
buf = append(buf, data[3:]...) buf = append(buf, data[3:]...)
return nil return
case 1: // end case 1: // end
buf = append(buf, data[3:]...) buf = append(buf, data[3:]...)
binary.BigEndian.PutUint32(buf[nuStart:], uint32(len(buf)-nuStart-4)) binary.BigEndian.PutUint32(buf[nuStart:], uint32(len(buf)-nuStart-4))
@@ -61,7 +60,7 @@ func RTPDepay(track *streamer.Track) streamer.WrapperFunc {
// collect all NAL Units for Access Unit // collect all NAL Units for Access Unit
if !packet.Marker { if !packet.Marker {
return nil return
} }
//log.Printf("[HEVC] %v, len: %d", Types(buf), len(buf)) //log.Printf("[HEVC] %v, len: %d", Types(buf), len(buf))
@@ -72,20 +71,19 @@ func RTPDepay(track *streamer.Track) streamer.WrapperFunc {
buf = buf[:0] buf = buf[:0]
return push(&clone) handler(&clone)
}
} }
} }
func RTPPay(mtu uint16) streamer.WrapperFunc { func RTPPay(mtu uint16, handler core.HandlerFunc) core.HandlerFunc {
payloader := &Payloader{} payloader := &Payloader{}
sequencer := rtp.NewRandomSequencer() sequencer := rtp.NewRandomSequencer()
mtu -= 12 // rtp.Header size mtu -= 12 // rtp.Header size
return func(push streamer.WriterFunc) streamer.WriterFunc { return func(packet *rtp.Packet) {
return func(packet *rtp.Packet) error {
if packet.Version != h264.RTPPacketVersionAVC { if packet.Version != h264.RTPPacketVersionAVC {
return push(packet) handler(packet)
return
} }
payloads := payloader.Payload(mtu, packet.Payload) payloads := payloader.Payload(mtu, packet.Payload)
@@ -100,26 +98,21 @@ func RTPPay(mtu uint16) streamer.WrapperFunc {
}, },
Payload: payload, Payload: payload,
} }
if err := push(&clone); err != nil { handler(&clone)
return err
}
}
return nil
} }
} }
} }
// SafariPay - generate Safari friendly payload for H265 // SafariPay - generate Safari friendly payload for H265
// https://github.com/AlexxIT/Blog/issues/5 // 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() sequencer := rtp.NewRandomSequencer()
size := int(mtu - 12) // rtp.Header size size := int(mtu - 12) // rtp.Header size
return func(push streamer.WriterFunc) streamer.WriterFunc { return func(packet *rtp.Packet) {
return func(packet *rtp.Packet) error {
if packet.Version != h264.RTPPacketVersionAVC { if packet.Version != h264.RTPPacketVersionAVC {
return push(packet) handler(packet)
return
} }
// protect original packets from modification // protect original packets from modification
@@ -177,14 +170,9 @@ func SafariPay(mtu uint16) streamer.WrapperFunc {
}, },
Payload: b, Payload: b,
} }
if err := push(&clone); err != nil { handler(&clone)
return err
}
b = b[:1] // clear buffer b = b[:1] // clear buffer
} }
return nil
}
} }
} }

View File

@@ -15,8 +15,8 @@ type Character struct {
IID int `json:"iid"` IID int `json:"iid"`
Type string `json:"type,omitempty"` Type string `json:"type,omitempty"`
Format string `json:"format,omitempty"` Format string `json:"format,omitempty"`
Value interface{} `json:"value,omitempty"` Value any `json:"value,omitempty"`
Event interface{} `json:"ev,omitempty"` Event any `json:"ev,omitempty"`
Perms []string `json:"perms,omitempty"` Perms []string `json:"perms,omitempty"`
Description string `json:"description,omitempty"` Description string `json:"description,omitempty"`
//MaxDataLen int `json:"maxDataLen"` //MaxDataLen int `json:"maxDataLen"`

View File

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

View File

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

View File

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

View File

@@ -1,18 +1,63 @@
package isapi package isapi
import ( import (
"github.com/AlexxIT/go2rtc/pkg/streamer" "encoding/json"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/pion/rtp" "github.com/pion/rtp"
) )
func (c *Client) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.Track { func (c *Client) GetMedias() []*core.Media {
consCodec := media.MatchCodec(track.Codec) return c.medias
consTrack := c.GetTrack(media, consCodec) }
if consTrack == nil {
return nil 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 { c.sender.HandleRTP(track)
return consTrack.WriteRTP(packet) 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 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) { func (m *Movie) WriteVideo(codec string, width, height uint16, conf []byte) {
// https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap3/qtff3.html // https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap3/qtff3.html
switch codec { switch codec {
case streamer.CodecH264: case core.CodecH264:
m.StartAtom("avc1") m.StartAtom("avc1")
case streamer.CodecH265: case core.CodecH265:
m.StartAtom("hev1") m.StartAtom("hev1")
default: default:
panic("unsupported iso video: " + codec) 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) m.WriteUint16(0xFFFF) // color table id (-1)
switch codec { switch codec {
case streamer.CodecH264: case core.CodecH264:
m.StartAtom("avcC") m.StartAtom("avcC")
case streamer.CodecH265: case core.CodecH265:
m.StartAtom("hvcC") m.StartAtom("hvcC")
} }
m.Write(conf) 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) { func (m *Movie) WriteAudio(codec string, channels uint16, sampleRate uint32, conf []byte) {
switch codec { switch codec {
case streamer.CodecAAC, streamer.CodecMP3: case core.CodecAAC, core.CodecMP3:
m.StartAtom("mp4a") m.StartAtom("mp4a")
case streamer.CodecOpus: case core.CodecOpus:
m.StartAtom("Opus") m.StartAtom("Opus")
case streamer.CodecPCMU: case core.CodecPCMU:
m.StartAtom("ulaw") m.StartAtom("ulaw")
case streamer.CodecPCMA: case core.CodecPCMA:
m.StartAtom("alaw") m.StartAtom("alaw")
default: default:
panic("unsupported iso audio: " + codec) 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 m.WriteFloat32(float64(sampleRate)) // sample_rate
switch codec { switch codec {
case streamer.CodecAAC: case core.CodecAAC:
m.WriteEsdsAAC(conf) m.WriteEsdsAAC(conf)
case streamer.CodecMP3: case core.CodecMP3:
m.WriteEsdsMP3() m.WriteEsdsMP3()
case streamer.CodecOpus: case core.CodecOpus:
// don't know what means this magic // don't know what means this magic
m.StartAtom("dOps") m.StartAtom("dOps")
m.WriteBytes(0, 0x02, 0x01, 0x38, 0, 0, 0xBB, 0x80, 0, 0, 0) m.WriteBytes(0, 0x02, 0x01, 0x38, 0, 0, 0xBB, 0x80, 0, 0, 0)
m.EndAtom() m.EndAtom()
case streamer.CodecPCMU, streamer.CodecPCMA: case core.CodecPCMU, core.CodecPCMA:
// don't know what means this magic // don't know what means this magic
m.StartAtom("chan") m.StartAtom("chan")
m.WriteBytes(0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0) 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/binary"
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/AlexxIT/go2rtc/pkg/streamer" "github.com/AlexxIT/go2rtc/pkg/core"
"github.com/deepch/vdk/codec/h264parser" "github.com/deepch/vdk/codec/h264parser"
"github.com/deepch/vdk/format/fmp4/fmp4io" "github.com/deepch/vdk/format/fmp4/fmp4io"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
@@ -15,7 +15,6 @@ import (
"net/http" "net/http"
"strings" "strings"
"sync" "sync"
"sync/atomic"
"time" "time"
) )
@@ -28,13 +27,14 @@ const (
) )
type Client struct { type Client struct {
streamer.Element core.Listener
ID string ID string
conn *websocket.Conn conn *websocket.Conn
medias []*streamer.Media
tracks map[byte]*streamer.Track medias []*core.Media
receiver *core.Receiver
msg *message msg *message
t0 time.Time t0 time.Time
@@ -43,7 +43,7 @@ type Client struct {
state State state State
mu sync.Mutex mu sync.Mutex
recv uint32 recv int
} }
func NewClient(id string) *Client { func NewClient(id string) *Client {
@@ -107,12 +107,11 @@ func (c *Client) Handle() error {
return err return err
} }
track := c.tracks[c.msg.Track] if c.receiver != nil && c.receiver.ID == c.msg.Track {
if track != nil {
c.mu.Lock() c.mu.Lock()
if c.state == StateHandle { if c.state == StateHandle {
c.buffer <- data c.buffer <- data
atomic.AddUint32(&c.recv, uint32(len(data))) c.recv += len(data)
} }
c.mu.Unlock() c.mu.Unlock()
} }
@@ -139,12 +138,11 @@ func (c *Client) Handle() error {
return err return err
} }
track = c.tracks[msg.Track] if c.receiver != nil && c.receiver.ID == msg.Track {
if track != nil {
c.mu.Lock() c.mu.Lock()
if c.state == StateHandle { if c.state == StateHandle {
c.buffer <- data c.buffer <- data
atomic.AddUint32(&c.recv, uint32(len(data))) c.recv += len(data)
} }
c.mu.Unlock() c.mu.Unlock()
} }
@@ -173,8 +171,6 @@ func (c *Client) Close() error {
} }
func (c *Client) getTracks() error { func (c *Client) getTracks() error {
c.tracks = map[byte]*streamer.Track{}
for { for {
_, data, err := c.conn.ReadMessage() _, data, err := c.conn.ReadMessage()
if err != nil { if err != nil {
@@ -197,15 +193,15 @@ func (c *Client) getTracks() error {
switch s { switch s {
case "avc1": // avc1.4d0029 case "avc1": // avc1.4d0029
// skip multiple identical init // skip multiple identical init
if c.tracks[msg.TrackID] != nil { if c.receiver != nil {
continue continue
} }
codec := &streamer.Codec{ codec := &core.Codec{
Name: streamer.CodecH264, Name: core.CodecH264,
ClockRate: 90000, ClockRate: 90000,
FmtpLine: "profile-level-id=" + msg.CodecString[i+1:], FmtpLine: "profile-level-id=" + msg.CodecString[i+1:],
PayloadType: streamer.PayloadTypeRAW, PayloadType: core.PayloadTypeRAW,
} }
i = bytes.Index(msg.Data, []byte("avcC")) - 4 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.SPS[0]) + "," +
base64.StdEncoding.EncodeToString(record.PPS[0]) base64.StdEncoding.EncodeToString(record.PPS[0])
media := &streamer.Media{ media := &core.Media{
Kind: streamer.KindVideo, Kind: core.KindVideo,
Direction: streamer.DirectionSendonly, Direction: core.DirectionRecvonly,
Codecs: []*streamer.Codec{codec}, Codecs: []*core.Codec{codec},
} }
c.medias = append(c.medias, media) c.medias = append(c.medias, media)
track := streamer.NewTrack(media, codec) c.receiver = core.NewReceiver(media, codec)
c.tracks[msg.TrackID] = track c.receiver.ID = msg.TrackID
case "mp4a": // mp4a.40.2 case "mp4a": // mp4a.40.2
} }
@@ -249,11 +245,6 @@ func (c *Client) getTracks() error {
} }
func (c *Client) worker(buffer chan []byte) { func (c *Client) worker(buffer chan []byte) {
var track *streamer.Track
for _, track = range c.tracks {
break
}
for data := range buffer { for data := range buffer {
moof := &fmp4io.MovieFrag{} moof := &fmp4io.MovieFrag{}
if _, err := moof.Unmarshal(data, 0); err != nil { 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}, Header: rtp.Header{Timestamp: ts * 90},
Payload: data[:entry.Size], Payload: data[:entry.Size],
} }
_ = track.WriteRTP(packet) c.receiver.WriteRTP(packet)
data = data[entry.Size:] data = data[entry.Size:]
ts += entry.Duration ts += entry.Duration

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,17 +3,16 @@ package mjpeg
import ( import (
"bytes" "bytes"
"encoding/binary" "encoding/binary"
"github.com/AlexxIT/go2rtc/pkg/streamer" "github.com/AlexxIT/go2rtc/pkg/core"
"github.com/pion/rtp" "github.com/pion/rtp"
"image" "image"
"image/jpeg" "image/jpeg"
) )
func RTPDepay(track *streamer.Track) streamer.WrapperFunc { func RTPDepay(handlerFunc core.HandlerFunc) core.HandlerFunc {
buf := make([]byte, 0, 512*1024) // 512K buf := make([]byte, 0, 512*1024) // 512K
return func(push streamer.WriterFunc) streamer.WriterFunc { return func(packet *rtp.Packet) {
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) //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 // https://www.rfc-editor.org/rfc/rfc2435#section-3.1
@@ -69,7 +68,7 @@ func RTPDepay(track *streamer.Track) streamer.WrapperFunc {
buf = append(buf, b...) buf = append(buf, b...)
if !packet.Marker { if !packet.Marker {
return nil return
} }
if end := buf[len(buf)-2:]; end[0] != 0xFF && end[1] != 0xD9 { if end := buf[len(buf)-2:]; end[0] != 0xFF && end[1] != 0xD9 {
@@ -81,8 +80,7 @@ func RTPDepay(track *streamer.Track) streamer.WrapperFunc {
buf = buf[:0] // clear buffer buf = buf[:0] // clear buffer
return push(&clone) handlerFunc(&clone)
}
} }
} }
@@ -90,17 +88,16 @@ func cutSize(size uint16) uint16 {
return ((size >> 3) & 0xFF) << 3 return ((size >> 3) & 0xFF) << 3
} }
func RTPPay() streamer.WrapperFunc { func RTPPay(handlerFunc core.HandlerFunc) core.HandlerFunc {
const packetSize = 1436 const packetSize = 1436
sequencer := rtp.NewRandomSequencer() sequencer := rtp.NewRandomSequencer()
return func(push streamer.WriterFunc) streamer.WriterFunc { return func(packet *rtp.Packet) {
return func(packet *rtp.Packet) error {
// reincode image to more common form // reincode image to more common form
p, err := Transcode(packet.Payload) p, err := Transcode(packet.Payload)
if err != nil { if err != nil {
return err return
} }
h1 := make([]byte, 8) h1 := make([]byte, 8)
@@ -115,7 +112,7 @@ func RTPPay() streamer.WrapperFunc {
for jpgData == nil { for jpgData == nil {
// 2 bytes h1 // 2 bytes h1
if p[0] != 0xFF { if p[0] != 0xFF {
return nil return
} }
size := binary.BigEndian.Uint16(p[2:]) + 2 size := binary.BigEndian.Uint16(p[2:]) + 2
@@ -131,7 +128,7 @@ func RTPPay() streamer.WrapperFunc {
} }
case 0xC0: // 2. Start Of Frame (size=15) case 0xC0: // 2. Start Of Frame (size=15)
if p[4] != 8 { if p[4] != 8 {
return nil return
} }
h := binary.BigEndian.Uint16(p[5:]) h := binary.BigEndian.Uint16(p[5:])
w := binary.BigEndian.Uint16(p[7:]) w := binary.BigEndian.Uint16(p[7:])
@@ -180,12 +177,7 @@ func RTPPay() streamer.WrapperFunc {
}, },
Payload: p, Payload: p,
} }
if err := push(&clone); err != nil { handlerFunc(&clone)
return err
}
}
return nil
} }
} }
} }

View File

@@ -3,176 +3,165 @@ package mp4
import ( import (
"encoding/json" "encoding/json"
"github.com/AlexxIT/go2rtc/pkg/aac" "github.com/AlexxIT/go2rtc/pkg/aac"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/h264" "github.com/AlexxIT/go2rtc/pkg/h264"
"github.com/AlexxIT/go2rtc/pkg/h265" "github.com/AlexxIT/go2rtc/pkg/h265"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/pion/rtp" "github.com/pion/rtp"
"sync/atomic"
) )
type Consumer struct { type Consumer struct {
streamer.Element core.Listener
Medias []*streamer.Media Medias []*core.Media
UserAgent string UserAgent string
RemoteAddr string RemoteAddr string
senders []*core.Sender
muxer *Muxer muxer *Muxer
codecs []*streamer.Codec
wait byte wait byte
send uint32 send int
} }
// ParseQuery - like usual parse, but with mp4 param handler func (c *Consumer) GetMedias() []*core.Media {
func ParseQuery(query map[string][]string) []*streamer.Media { if c.Medias == nil {
if query["mp4"] != nil { // default local medias
cons := Consumer{} c.Medias = []*core.Media{
return cons.GetMedias() {
Kind: core.KindVideo,
Direction: core.DirectionSendonly,
Codecs: []*core.Codec{
{Name: core.CodecH264},
{Name: core.CodecH265},
},
},
{
Kind: core.KindAudio,
Direction: core.DirectionSendonly,
Codecs: []*core.Codec{
{Name: core.CodecAAC},
},
},
}
} }
return streamer.ParseQuery(query)
}
const (
waitNone byte = iota
waitKeyframe
waitInit
)
func (c *Consumer) GetMedias() []*streamer.Media {
if c.Medias != nil {
return c.Medias return c.Medias
}
// default medias
return []*streamer.Media{
{
Kind: streamer.KindVideo,
Direction: streamer.DirectionRecvonly,
Codecs: []*streamer.Codec{
{Name: streamer.CodecH264},
{Name: streamer.CodecH265},
},
},
{
Kind: streamer.KindAudio,
Direction: streamer.DirectionRecvonly,
Codecs: []*streamer.Codec{
{Name: streamer.CodecAAC},
},
},
}
} }
func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.Track { func (c *Consumer) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiver) error {
trackID := byte(len(c.codecs)) trackID := byte(len(c.senders))
c.codecs = append(c.codecs, track.Codec)
codec := track.Codec handler := core.NewSender(media, track.Codec)
switch codec.Name {
case streamer.CodecH264: switch track.Codec.Name {
case core.CodecH264:
c.wait = waitInit c.wait = waitInit
push := func(packet *rtp.Packet) error { handler.Handler = func(packet *rtp.Packet) {
if packet.Version != h264.RTPPacketVersionAVC { if packet.Version != h264.RTPPacketVersionAVC {
return nil return
} }
if c.wait != waitNone { if c.wait != waitNone {
if c.wait == waitInit || !h264.IsKeyframe(packet.Payload) { if c.wait == waitInit || !h264.IsKeyframe(packet.Payload) {
return nil return
} }
c.wait = waitNone c.wait = waitNone
} }
buf := c.muxer.Marshal(trackID, packet) buf := c.muxer.Marshal(trackID, packet)
atomic.AddUint32(&c.send, uint32(len(buf)))
c.Fire(buf) c.Fire(buf)
return nil c.send += len(buf)
} }
var wrapper streamer.WrapperFunc if track.Codec.IsRTP() {
if codec.IsRTP() { handler.Handler = h264.RTPDepay(track.Codec, handler.Handler)
wrapper = h264.RTPDepay(track)
} else { } else {
wrapper = h264.RepairAVC(track) handler.Handler = h264.RepairAVC(track.Codec, handler.Handler)
} }
push = wrapper(push)
return track.Bind(push) case core.CodecH265:
case streamer.CodecH265:
c.wait = waitInit c.wait = waitInit
push := func(packet *rtp.Packet) error { handler.Handler = func(packet *rtp.Packet) {
if packet.Version != h264.RTPPacketVersionAVC { if packet.Version != h264.RTPPacketVersionAVC {
return nil return
} }
if c.wait != waitNone { if c.wait != waitNone {
if c.wait == waitInit || !h265.IsKeyframe(packet.Payload) { if c.wait == waitInit || !h265.IsKeyframe(packet.Payload) {
return nil return
} }
c.wait = waitNone c.wait = waitNone
} }
buf := c.muxer.Marshal(trackID, packet) buf := c.muxer.Marshal(trackID, packet)
atomic.AddUint32(&c.send, uint32(len(buf)))
c.Fire(buf) c.Fire(buf)
return nil c.send += len(buf)
} }
if codec.IsRTP() { if track.Codec.IsRTP() {
wrapper := h265.RTPDepay(track) handler.Handler = h265.RTPDepay(track.Codec, handler.Handler)
push = wrapper(push)
} }
return track.Bind(push) case core.CodecAAC:
handler.Handler = func(packet *rtp.Packet) {
case streamer.CodecAAC:
push := func(packet *rtp.Packet) error {
if c.wait != waitNone { if c.wait != waitNone {
return nil return
} }
buf := c.muxer.Marshal(trackID, packet) buf := c.muxer.Marshal(trackID, packet)
atomic.AddUint32(&c.send, uint32(len(buf)))
c.Fire(buf) c.Fire(buf)
return nil c.send += len(buf)
} }
if codec.IsRTP() { if track.Codec.IsRTP() {
wrapper := aac.RTPDepay(track) handler.Handler = aac.RTPDepay(handler.Handler)
push = wrapper(push)
} }
return track.Bind(push) case core.CodecOpus, core.CodecMP3, core.CodecPCMU, core.CodecPCMA:
handler.Handler = func(packet *rtp.Packet) {
case streamer.CodecOpus, streamer.CodecMP3, streamer.CodecPCMU, streamer.CodecPCMA:
push := func(packet *rtp.Packet) error {
if c.wait != waitNone { if c.wait != waitNone {
return nil return
} }
buf := c.muxer.Marshal(trackID, packet) buf := c.muxer.Marshal(trackID, packet)
atomic.AddUint32(&c.send, uint32(len(buf)))
c.Fire(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 { func (c *Consumer) MimeCodecs() string {
return c.muxer.MimeCodecs(c.codecs) return c.muxer.MimeCodecs(c.Codecs())
} }
func (c *Consumer) MimeType() string { func (c *Consumer) MimeType() string {
@@ -181,7 +170,7 @@ func (c *Consumer) MimeType() string {
func (c *Consumer) Init() ([]byte, error) { func (c *Consumer) Init() ([]byte, error) {
c.muxer = &Muxer{} c.muxer = &Muxer{}
return c.muxer.GetInit(c.codecs) return c.muxer.GetInit(c.Codecs())
} }
func (c *Consumer) Start() { func (c *Consumer) Start() {
@@ -190,14 +179,14 @@ func (c *Consumer) Start() {
} }
} }
//
func (c *Consumer) MarshalJSON() ([]byte, error) { func (c *Consumer) MarshalJSON() ([]byte, error) {
info := &streamer.Info{ info := &core.Info{
Type: "MP4 client", Type: "MP4 passive consumer",
RemoteAddr: c.RemoteAddr, RemoteAddr: c.RemoteAddr,
UserAgent: c.UserAgent, UserAgent: c.UserAgent,
Send: atomic.LoadUint32(&c.send), Medias: c.Medias,
Senders: c.senders,
Send: c.send,
} }
return json.Marshal(info) 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 ( import (
"encoding/hex" "encoding/hex"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/h264" "github.com/AlexxIT/go2rtc/pkg/h264"
"github.com/AlexxIT/go2rtc/pkg/h265" "github.com/AlexxIT/go2rtc/pkg/h265"
"github.com/AlexxIT/go2rtc/pkg/iso" "github.com/AlexxIT/go2rtc/pkg/iso"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/deepch/vdk/codec/h264parser" "github.com/deepch/vdk/codec/h264parser"
"github.com/deepch/vdk/codec/h265parser" "github.com/deepch/vdk/codec/h265parser"
"github.com/pion/rtp" "github.com/pion/rtp"
@@ -24,7 +24,7 @@ const (
MimeOpus = "opus" MimeOpus = "opus"
) )
func (m *Muxer) MimeCodecs(codecs []*streamer.Codec) string { func (m *Muxer) MimeCodecs(codecs []*core.Codec) string {
var s string var s string
for i, codec := range codecs { for i, codec := range codecs {
@@ -33,15 +33,15 @@ func (m *Muxer) MimeCodecs(codecs []*streamer.Codec) string {
} }
switch codec.Name { switch codec.Name {
case streamer.CodecH264: case core.CodecH264:
s += "avc1." + h264.GetProfileLevelID(codec.FmtpLine) s += "avc1." + h264.GetProfileLevelID(codec.FmtpLine)
case streamer.CodecH265: case core.CodecH265:
// H.265 profile=main level=5.1 // H.265 profile=main level=5.1
// hvc1 - supported in Safari, hev1 - doesn't, both supported in Chrome // hvc1 - supported in Safari, hev1 - doesn't, both supported in Chrome
s += MimeH265 s += MimeH265
case streamer.CodecAAC: case core.CodecAAC:
s += MimeAAC s += MimeAAC
case streamer.CodecOpus: case core.CodecOpus:
s += MimeOpus s += MimeOpus
} }
} }
@@ -49,7 +49,7 @@ func (m *Muxer) MimeCodecs(codecs []*streamer.Codec) string {
return s 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 := iso.NewMovie(1024)
mv.WriteFileType() mv.WriteFileType()
@@ -58,7 +58,7 @@ func (m *Muxer) GetInit(codecs []*streamer.Codec) ([]byte, error) {
for i, codec := range codecs { for i, codec := range codecs {
switch codec.Name { switch codec.Name {
case streamer.CodecH264: case core.CodecH264:
sps, pps := h264.GetParameterSet(codec.FmtpLine) sps, pps := h264.GetParameterSet(codec.FmtpLine)
if sps == nil { if sps == nil {
// some dummy SPS and PPS not a problem // some dummy SPS and PPS not a problem
@@ -77,7 +77,7 @@ func (m *Muxer) GetInit(codecs []*streamer.Codec) ([]byte, error) {
codecData.AVCDecoderConfRecordBytes(), codecData.AVCDecoderConfRecordBytes(),
) )
case streamer.CodecH265: case core.CodecH265:
vps, sps, pps := h265.GetParameterSet(codec.FmtpLine) vps, sps, pps := h265.GetParameterSet(codec.FmtpLine)
if sps == nil { if sps == nil {
// some dummy SPS and PPS not a problem // some dummy SPS and PPS not a problem
@@ -97,8 +97,8 @@ func (m *Muxer) GetInit(codecs []*streamer.Codec) ([]byte, error) {
codecData.AVCDecoderConfRecordBytes(), codecData.AVCDecoderConfRecordBytes(),
) )
case streamer.CodecAAC: case core.CodecAAC:
s := streamer.Between(codec.FmtpLine, "config=", ";") s := core.Between(codec.FmtpLine, "config=", ";")
b, err := hex.DecodeString(s) b, err := hex.DecodeString(s)
if err != nil { if err != nil {
return nil, err 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, 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( mv.WriteAudioTrack(
uint32(i+1), codec.Name, codec.ClockRate, codec.Channels, nil, uint32(i+1), codec.Name, codec.ClockRate, codec.Channels, nil,
) )

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,17 +1,19 @@
package mpegts package mpegts
import ( import (
"github.com/AlexxIT/go2rtc/pkg/streamer" "github.com/AlexxIT/go2rtc/pkg/core"
"net/http" "net/http"
) )
type Client struct { type Client struct {
streamer.Element core.Listener
medias []*streamer.Media medias []*core.Media
tracks map[byte]*streamer.Track receivers []*core.Receiver
res *http.Response res *http.Response
recv int
} }
func NewClient(res *http.Response) *Client { func NewClient(res *http.Response) *Client {
@@ -19,31 +21,35 @@ func NewClient(res *http.Response) *Client {
} }
func (c *Client) Handle() error { func (c *Client) Handle() error {
if c.tracks == nil {
c.tracks = map[byte]*streamer.Track{}
}
reader := NewReader() reader := NewReader()
b := make([]byte, 1024*1024*256) // 256K b := make([]byte, 1024*1024*256) // 256K
probe := streamer.NewProbe(c.medias == nil) probe := core.NewProbe(c.medias == nil)
for probe == nil || probe.Active() { for probe == nil || probe.Active() {
n, err := c.res.Body.Read(b) n, err := c.res.Body.Read(b)
if err != nil { if err != nil {
return err return err
} }
c.recv += n
reader.AppendBuffer(b[:n]) reader.AppendBuffer(b[:n])
reading:
for { for {
packet := reader.GetPacket() packet := reader.GetPacket()
if packet == nil { if packet == nil {
break break
} }
track := c.tracks[packet.PayloadType] for _, receiver := range c.receivers {
if track == nil { if receiver.ID == packet.PayloadType {
receiver.WriteRTP(packet)
continue reading
}
}
// count track on probe state even if not support it // count track on probe state even if not support it
probe.Append(packet.PayloadType) probe.Append(packet.PayloadType)
@@ -52,13 +58,13 @@ func (c *Client) Handle() error {
continue // unsupported codec continue // unsupported codec
} }
track = streamer.NewTrack(media, nil)
c.medias = append(c.medias, media) c.medias = append(c.medias, media)
c.tracks[packet.PayloadType] = track
}
_ = track.WriteRTP(packet) 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) //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 package mpegts
import ( import (
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/h264" "github.com/AlexxIT/go2rtc/pkg/h264"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/pion/rtp" "github.com/pion/rtp"
"time" "time"
) )
@@ -13,7 +13,7 @@ const (
) )
const ( const (
StreamTypePrivate = 0x06 // PCMU or PCMA from FFmpeg StreamTypePrivate = 0x06 // PCMU or PCMA or FLAC from FFmpeg
StreamTypeAAC = 0x0F StreamTypeAAC = 0x0F
StreamTypeH264 = 0x1B StreamTypeH264 = 0x1B
StreamTypePCMATapo = 0x90 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) 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 { func GetMedia(pkt *rtp.Packet) *core.Media {
var codec *streamer.Codec var codec *core.Codec
var kind string var kind string
switch pkt.PayloadType { switch pkt.PayloadType {
case StreamTypeH264: case StreamTypeH264:
codec = &streamer.Codec{ codec = &core.Codec{
Name: streamer.CodecH264, Name: core.CodecH264,
ClockRate: 90000, ClockRate: 90000,
PayloadType: streamer.PayloadTypeRAW, PayloadType: core.PayloadTypeRAW,
FmtpLine: h264.GetFmtpLine(pkt.Payload), FmtpLine: h264.GetFmtpLine(pkt.Payload),
} }
kind = streamer.KindVideo kind = core.KindVideo
case StreamTypePCMATapo: case StreamTypePCMATapo:
codec = &streamer.Codec{ codec = &core.Codec{
Name: streamer.CodecPCMA, Name: core.CodecPCMA,
ClockRate: 8000, ClockRate: 8000,
} }
kind = streamer.KindAudio kind = core.KindAudio
default: default:
return nil return nil
} }
return &streamer.Media{ return &core.Media{
Kind: kind, Kind: kind,
Direction: streamer.DirectionSendonly, Direction: core.DirectionRecvonly,
Codecs: []*streamer.Codec{codec}, Codecs: []*core.Codec{codec},
} }
} }

View File

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

View File

@@ -1,20 +1,21 @@
package mpegts package mpegts
import ( 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 return c.medias
} }
func (c *Client) GetTrack(media *streamer.Media, codec *streamer.Codec) *streamer.Track { func (c *Client) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) {
for _, track := range c.tracks { for _, track := range c.receivers {
if track.Codec == codec { if track.Codec == codec {
return track return track, nil
} }
} }
return nil return nil, core.ErrCantGetTrack
} }
func (c *Client) Start() error { func (c *Client) Start() error {
@@ -22,5 +23,19 @@ func (c *Client) Start() error {
} }
func (c *Client) Stop() error { func (c *Client) Stop() error {
for _, receiver := range c.receivers {
receiver.Close()
}
return c.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 ( import (
"bytes" "bytes"
"encoding/hex" "encoding/hex"
"encoding/json"
"github.com/AlexxIT/go2rtc/pkg/aac" "github.com/AlexxIT/go2rtc/pkg/aac"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/h264" "github.com/AlexxIT/go2rtc/pkg/h264"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/deepch/vdk/av" "github.com/deepch/vdk/av"
"github.com/deepch/vdk/codec/aacparser" "github.com/deepch/vdk/codec/aacparser"
"github.com/deepch/vdk/codec/h264parser" "github.com/deepch/vdk/codec/h264parser"
"github.com/deepch/vdk/format/ts" "github.com/deepch/vdk/format/ts"
"github.com/pion/rtp" "github.com/pion/rtp"
"sync/atomic"
"time" "time"
) )
type Consumer struct { type Consumer struct {
streamer.Element core.Listener
UserAgent string UserAgent string
RemoteAddr string RemoteAddr string
senders []*core.Sender
buf *bytes.Buffer buf *bytes.Buffer
muxer *ts.Muxer muxer *ts.Muxer
mimeType string mimeType string
@@ -28,35 +30,36 @@ type Consumer struct {
start bool start bool
init []byte init []byte
send uint32 send int
} }
func (c *Consumer) GetMedias() []*streamer.Media { func (c *Consumer) GetMedias() []*core.Media {
return []*streamer.Media{ return []*core.Media{
{ {
Kind: streamer.KindVideo, Kind: core.KindVideo,
Direction: streamer.DirectionRecvonly, Direction: core.DirectionSendonly,
Codecs: []*streamer.Codec{ Codecs: []*core.Codec{
{Name: streamer.CodecH264}, {Name: core.CodecH264},
}, },
}, },
//{ //{
// Kind: streamer.KindAudio, // Kind: core.KindAudio,
// Direction: streamer.DirectionRecvonly, // Direction: core.DirectionSendonly,
// Codecs: []*streamer.Codec{ // Codecs: []*core.Codec{
// {Name: streamer.CodecAAC}, // {Name: core.CodecAAC},
// }, // },
//}, //},
} }
} }
func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.Track { func (c *Consumer) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiver) error {
codec := track.Codec
trackID := int8(len(c.streams)) trackID := int8(len(c.streams))
switch codec.Name { handler := core.NewSender(media, track.Codec)
case streamer.CodecH264:
sps, pps := h264.GetParameterSet(codec.FmtpLine) switch track.Codec.Name {
case core.CodecH264:
sps, pps := h264.GetParameterSet(track.Codec.FmtpLine)
stream, err := h264parser.NewCodecDataFromSPSAndPPS(sps, pps) stream, err := h264parser.NewCodecDataFromSPSAndPPS(sps, pps)
if err != nil { if err != nil {
return nil return nil
@@ -66,21 +69,21 @@ func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *strea
c.mimeType += "," c.mimeType += ","
} }
c.mimeType += "avc1." + h264.GetProfileLevelID(codec.FmtpLine) c.mimeType += "avc1." + h264.GetProfileLevelID(track.Codec.FmtpLine)
c.streams = append(c.streams, stream) c.streams = append(c.streams, stream)
pkt := av.Packet{Idx: trackID, CompositionTime: time.Millisecond} 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 { if packet.Version != h264.RTPPacketVersionAVC {
return nil return
} }
if !c.start { if !c.start {
return nil return
} }
pkt.Data = packet.Payload pkt.Data = packet.Payload
@@ -91,28 +94,26 @@ func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *strea
pkt.Time = newTime pkt.Time = newTime
if err = c.muxer.WritePacket(pkt); err != nil { if err = c.muxer.WritePacket(pkt); err != nil {
return err return
} }
// clone bytes from buffer, so next packet won't overwrite it // clone bytes from buffer, so next packet won't overwrite it
buf := append([]byte{}, c.buf.Bytes()...) buf := append([]byte{}, c.buf.Bytes()...)
atomic.AddUint32(&c.send, uint32(len(buf)))
c.Fire(buf) c.Fire(buf)
c.send += len(buf)
c.buf.Reset() c.buf.Reset()
return nil
} }
if codec.IsRTP() { if track.Codec.IsRTP() {
wrapper := h264.RTPDepay(track) handler.Handler = h264.RTPDepay(track.Codec, handler.Handler)
push = wrapper(push) } else {
handler.Handler = h264.RepairAVC(track.Codec, handler.Handler)
} }
return track.Bind(push) case core.CodecAAC:
s := core.Between(track.Codec.FmtpLine, "config=", ";")
case streamer.CodecAAC:
s := streamer.Between(codec.FmtpLine, "config=", ";")
b, err := hex.DecodeString(s) b, err := hex.DecodeString(s)
if err != nil { 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} 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 { if !c.start {
return nil return
} }
pkt.Data = packet.Payload pkt.Data = packet.Payload
@@ -147,29 +148,31 @@ func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *strea
} }
pkt.Time = newTime pkt.Time = newTime
if err := c.muxer.WritePacket(pkt); err != nil { if err = c.muxer.WritePacket(pkt); err != nil {
return err return
} }
// clone bytes from buffer, so next packet won't overwrite it // clone bytes from buffer, so next packet won't overwrite it
buf := append([]byte{}, c.buf.Bytes()...) buf := append([]byte{}, c.buf.Bytes()...)
atomic.AddUint32(&c.send, uint32(len(buf)))
c.Fire(buf) c.Fire(buf)
c.send += len(buf)
c.buf.Reset() c.buf.Reset()
}
if track.Codec.IsRTP() {
handler.Handler = aac.RTPDepay(handler.Handler)
}
default:
panic("unsupported codec")
}
handler.HandleRTP(track)
c.senders = append(c.senders, handler)
return nil return nil
}
if codec.IsRTP() {
wrapper := aac.RTPDepay(track)
push = wrapper(push)
}
return track.Bind(push)
}
panic("unsupported codec")
} }
func (c *Consumer) MimeCodecs() string { func (c *Consumer) MimeCodecs() string {
@@ -192,3 +195,22 @@ func (c *Consumer) Init() ([]byte, error) {
func (c *Consumer) Start() { func (c *Consumer) Start() {
c.start = true 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 ( import (
"bufio" "bufio"
"encoding/json" "encoding/json"
"github.com/AlexxIT/go2rtc/pkg/streamer" "github.com/AlexxIT/go2rtc/pkg/core"
"io" "io"
"os/exec" "os/exec"
"strings" "strings"
) )
type Ngrok struct { type Ngrok struct {
streamer.Element core.Listener
Tunnels map[string]string Tunnels map[string]string

View File

@@ -4,15 +4,14 @@ import (
"encoding/base64" "encoding/base64"
"encoding/hex" "encoding/hex"
"fmt" "fmt"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/httpflv" "github.com/AlexxIT/go2rtc/pkg/httpflv"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/deepch/vdk/av" "github.com/deepch/vdk/av"
"github.com/deepch/vdk/codec/aacparser" "github.com/deepch/vdk/codec/aacparser"
"github.com/deepch/vdk/codec/h264parser" "github.com/deepch/vdk/codec/h264parser"
"github.com/deepch/vdk/format/rtmp" "github.com/deepch/vdk/format/rtmp"
"github.com/pion/rtp" "github.com/pion/rtp"
"net/http" "net/http"
"sync/atomic"
"time" "time"
) )
@@ -24,17 +23,17 @@ type Conn interface {
} }
type Client struct { type Client struct {
streamer.Element core.Listener
URI string URI string
medias []*streamer.Media medias []*core.Media
tracks []*streamer.Track receivers []*core.Receiver
conn Conn conn Conn
closed bool closed bool
recv uint32 recv int
} }
func NewClient(uri string) *Client { func NewClient(uri string) *Client {
@@ -74,61 +73,55 @@ func (c *Client) Describe() (err error) {
base64.StdEncoding.EncodeToString(info.PPS[0]), base64.StdEncoding.EncodeToString(info.PPS[0]),
) )
codec := &streamer.Codec{ codec := &core.Codec{
Name: streamer.CodecH264, Name: core.CodecH264,
ClockRate: 90000, ClockRate: 90000,
FmtpLine: fmtp, FmtpLine: fmtp,
PayloadType: streamer.PayloadTypeRAW, PayloadType: core.PayloadTypeRAW,
} }
media := &streamer.Media{ media := &core.Media{
Kind: streamer.KindVideo, Kind: core.KindVideo,
Direction: streamer.DirectionSendonly, Direction: core.DirectionRecvonly,
Codecs: []*streamer.Codec{codec}, Codecs: []*core.Codec{codec},
} }
c.medias = append(c.medias, media) c.medias = append(c.medias, media)
track := streamer.NewTrack(media, codec) track := core.NewReceiver(media, codec)
c.tracks = append(c.tracks, track) c.receivers = append(c.receivers, track)
case av.AAC: case av.AAC:
// TODO: fix support // TODO: fix support
cd := stream.(aacparser.CodecData) cd := stream.(aacparser.CodecData)
codec := &streamer.Codec{ codec := &core.Codec{
Name: streamer.CodecAAC, Name: core.CodecAAC,
ClockRate: uint32(cd.Config.SampleRate), ClockRate: uint32(cd.Config.SampleRate),
Channels: uint16(cd.Config.ChannelConfig), 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 // 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), 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{ media := &core.Media{
Kind: streamer.KindAudio, Kind: core.KindAudio,
Direction: streamer.DirectionSendonly, Direction: core.DirectionRecvonly,
Codecs: []*streamer.Codec{codec}, Codecs: []*core.Codec{codec},
} }
c.medias = append(c.medias, media) c.medias = append(c.medias, media)
track := streamer.NewTrack(media, codec) track := core.NewReceiver(media, codec)
c.tracks = append(c.tracks, track) c.receivers = append(c.receivers, track)
default: default:
fmt.Printf("[rtmp] unsupported codec %+v\n", stream) fmt.Printf("[rtmp] unsupported codec %+v\n", stream)
} }
} }
c.Fire(streamer.StateReady)
return return
} }
func (c *Client) Handle() (err error) { func (c *Client) Handle() (err error) {
defer c.Fire(streamer.StateNull)
c.Fire(streamer.StatePlaying)
for { for {
var pkt av.Packet var pkt av.Packet
pkt, err = c.conn.ReadPacket() pkt, err = c.conn.ReadPacket()
@@ -139,9 +132,9 @@ func (c *Client) Handle() (err error) {
return 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 // convert seconds to RTP timestamp
timestamp := uint32(pkt.Time * time.Duration(track.Codec.ClockRate) / time.Second) 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}, Header: rtp.Header{Timestamp: timestamp},
Payload: pkt.Data, 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 ( import (
"bufio" "bufio"
"crypto/tls" "crypto/tls"
"encoding/binary"
"errors" "errors"
"fmt" "fmt"
"github.com/AlexxIT/go2rtc/pkg/streamer" "github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/tcp" "github.com/AlexxIT/go2rtc/pkg/tcp"
"github.com/pion/rtcp"
"github.com/pion/rtp"
"io"
"net" "net"
"net/http" "net/http"
"net/url" "net/url"
"strconv" "strconv"
"strings" "strings"
"sync"
"time" "time"
) )
const ( func NewClient(uri string) *Conn {
ProtoRTSP = "RTSP/1.0" return &Conn{uri: uri}
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))
} }
const ( func (c *Conn) Dial() (err error) {
StateNone State = iota if c.URL, err = url.Parse(c.uri); err != nil {
StateConn return
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
} }
if strings.IndexByte(c.URL.Host, ':') < 0 { 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.auth = tcp.NewAuth(c.URL.User)
c.URL.User = nil 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) c.conn, err = net.DialTimeout("tcp", c.URL.Host, time.Second*5)
if err != nil { if err != nil {
return return
@@ -314,7 +216,7 @@ func (c *Conn) Describe() error {
return err return err
} }
c.mode = ModeClientProducer c.mode = core.ModeActiveProducer
return nil 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 { if err != nil {
return err return err
} }
@@ -342,7 +244,7 @@ func (c *Conn) Announce() (err error) {
func (c *Conn) Setup() error { func (c *Conn) Setup() error {
for _, media := range c.Medias { for _, media := range c.Medias {
_, err := c.SetupMedia(media, media.Codecs[0], true) _, err := c.SetupMedia(media, true)
if err != nil { if err != nil {
return err return err
} }
@@ -351,7 +253,7 @@ func (c *Conn) Setup() error {
return nil 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 // TODO: rewrite recoonection and first flag
if first { if first {
c.stateMu.Lock() 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 { 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) var transport string
if ch < 0 {
return nil, fmt.Errorf("wrong media: %v", media) // 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, "://") { if !strings.Contains(rawURL, "://") {
rawURL = c.URL.String() rawURL = c.URL.String()
if !strings.HasSuffix(rawURL, "/") { if !strings.HasSuffix(rawURL, "/") {
rawURL += "/" rawURL += "/"
} }
rawURL += media.Control rawURL += media.ID
} }
trackURL, err := urlParse(rawURL) trackURL, err := urlParse(rawURL)
if err != nil { if err != nil {
return nil, err return 0, err
} }
req := &tcp.Request{ req := &tcp.Request{
Method: MethodSetup, Method: MethodSetup,
URL: trackURL, URL: trackURL,
Header: map[string][]string{ Header: map[string][]string{
"Transport": {fmt.Sprintf( "Transport": {transport},
// i - RTP (data channel)
// i+1 - RTCP (control channel)
"RTP/AVP/TCP;unicast;interleaved=%d-%d", ch*2, ch*2+1,
)},
}, },
} }
@@ -400,20 +311,20 @@ func (c *Conn) SetupMedia(media *streamer.Media, codec *streamer.Codec, first bo
if c.Backchannel { if c.Backchannel {
c.Backchannel = false c.Backchannel = false
if err := c.Dial(); err != nil { if err := c.Dial(); err != nil {
return nil, err return 0, err
} }
if err := c.Describe(); err != nil { if err := c.Describe(); err != nil {
return nil, err return 0, err
} }
for _, newMedia := range c.Medias { for _, newMedia := range c.Medias {
if newMedia.Control == media.Control { if newMedia.ID == media.ID {
return c.SetupMedia(newMedia, newMedia.Codecs[0], false) return c.SetupMedia(newMedia, false)
} }
} }
} }
return nil, err return 0, err
} }
if c.Session == "" { 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 // 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;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;unicast;destination=192.168.1.111;source=192.168.1.222;interleaved=0
// Transport: RTP/AVP/TCP;ssrc=22345682;interleaved=0-1 // Transport: RTP/AVP/TCP;ssrc=22345682;interleaved=0-1
s := res.Header.Get("Transport") transport = res.Header.Get("Transport")
// TODO: rewrite if !strings.HasPrefix(transport, "RTP/AVP/TCP;") {
if !strings.HasPrefix(s, "RTP/AVP/TCP;") {
// Escam Q6 has a bug: // Escam Q6 has a bug:
// Transport: RTP/AVP;unicast;destination=192.168.1.111;source=192.168.1.222;interleaved=0-1 // Transport: RTP/AVP;unicast;destination=192.168.1.111;source=192.168.1.222;interleaved=0-1
if !strings.Contains(s, ";interleaved=") { if !strings.Contains(transport, ";interleaved=") {
return nil, fmt.Errorf("wrong transport: %s", s) 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.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) { func (c *Conn) Play() (err error) {
@@ -516,224 +396,3 @@ func (c *Conn) Close() error {
c.state = StateNone c.state = StateNone
return c.conn.Close() 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 package rtsp
import ( import (
"encoding/binary"
"github.com/AlexxIT/go2rtc/pkg/aac" "github.com/AlexxIT/go2rtc/pkg/aac"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/h264" "github.com/AlexxIT/go2rtc/pkg/h264"
"github.com/AlexxIT/go2rtc/pkg/h265" "github.com/AlexxIT/go2rtc/pkg/h265"
"github.com/AlexxIT/go2rtc/pkg/mjpeg" "github.com/AlexxIT/go2rtc/pkg/mjpeg"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/pion/rtp" "github.com/pion/rtp"
) )
func (c *Conn) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.Track { func (c *Conn) GetMedias() []*core.Media {
core.Assert(c.Medias != nil)
return c.Medias
}
func (c *Conn) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) (err error) {
core.Assert(media.Direction == core.DirectionSendonly)
for _, sender := range c.senders {
if sender.Codec == codec {
sender.HandleRTP(track)
return
}
}
var channel byte
switch c.mode { switch c.mode {
// send our track to RTSP consumer (ex. FFmpeg) case core.ModeActiveProducer: // backchannel
case ModeServerConsumer: if channel, err = c.SetupMedia(media, true); err != nil {
i := len(c.tracks) return
channelID := byte(i << 1)
codec := track.Codec.Clone()
codec.PayloadType = uint8(96 + i)
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
}
}
} }
track = c.bindTrack(track, channelID, codec.PayloadType) case core.ModePassiveConsumer:
track.Codec = codec channel = byte(len(c.senders)) * 2
c.tracks = append(c.tracks, track)
return track // 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))
// camera with backchannel support default:
case ModeClientProducer: panic(core.Caller())
consCodec := media.MatchCodec(track.Codec)
consTrack := c.GetTrack(media, consCodec)
if consTrack == nil {
return nil
} }
return track.Bind(func(packet *rtp.Packet) error { // save original codec to sender (can have Codec.Name = ANY)
return consTrack.WriteRTP(packet) sender := core.NewSender(media, codec)
}) sender.Handler = c.packetWriter(codec, channel)
} sender.HandleRTP(track)
println("WARNING: rtsp: AddTrack to wrong mode") c.senders = append(c.senders, sender)
return nil return nil
} }
func (c *Conn) bindTrack( func (c *Conn) packetWriter(codec *core.Codec, channel uint8) core.HandlerFunc {
track *streamer.Track, channel uint8, payloadType uint8, handlerFunc := func(packet *rtp.Packet) {
) *streamer.Track {
push := func(packet *rtp.Packet) error {
if c.state == StateNone { 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 := make([]byte, 4+size)
data[0] = '$' data[0] = '$'
data[1] = channel 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 { if _, err := clone.MarshalTo(data[4:]); err != nil {
return nil return
} }
if _, err := c.conn.Write(data); err != nil { n, err := c.conn.Write(data)
return err if err != nil {
return
} }
c.send += size c.send += n
return nil
} }
if !track.Codec.IsRTP() { if !codec.IsRTP() {
switch track.Codec.Name { switch codec.Name {
case streamer.CodecH264: case core.CodecH264:
wrapper := h264.RTPPay(1500) handlerFunc = h264.RTPPay(1500, handlerFunc)
push = wrapper(push) case core.CodecH265:
case streamer.CodecH265: handlerFunc = h265.RTPPay(1500, handlerFunc)
wrapper := h265.RTPPay(1500) case core.CodecAAC:
push = wrapper(push) handlerFunc = aac.RTPPay(handlerFunc)
case streamer.CodecAAC: case core.CodecJPEG:
wrapper := aac.RTPPay(1500) handlerFunc = mjpeg.RTPPay(handlerFunc)
push = wrapper(push)
case streamer.CodecJPEG:
wrapper := mjpeg.RTPPay()
push = wrapper(push)
} }
} }
return track.Bind(push) return handlerFunc
} }

View File

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

View File

@@ -3,87 +3,74 @@ package rtsp
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/AlexxIT/go2rtc/pkg/streamer" "github.com/AlexxIT/go2rtc/pkg/core"
) )
func (c *Conn) GetMedias() []*streamer.Media { func (c *Conn) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) {
if c.Medias != nil { core.Assert(media.Direction == core.DirectionRecvonly)
return c.Medias
}
return []*streamer.Media{ for _, track := range c.receivers {
{
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 {
if track.Codec == codec { if track.Codec == codec {
return track return track, nil
} }
} }
// can't setup new tracks from play state - forcing a reconnection feature
switch c.state { switch c.state {
case StatePlay, StateHandle: case StateConn, StateSetup:
go c.Close() default:
return streamer.NewTrack(media, codec) 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 { 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 { func (c *Conn) Start() error {
switch c.mode { switch c.mode {
case ModeClientProducer: case core.ModeActiveProducer:
if err := c.Play(); err != nil { if err := c.Play(); err != nil {
return err return err
} }
case ModeServerProducer: case core.ModePassiveProducer:
default: default:
return fmt.Errorf("start wrong mode: %d", c.mode) 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 { func (c *Conn) Stop() error {
for _, receiver := range c.receivers {
receiver.Close()
}
for _, sender := range c.senders {
sender.Close()
}
return c.Close() return c.Close()
} }
func (c *Conn) MarshalJSON() ([]byte, error) { func (c *Conn) MarshalJSON() ([]byte, error) {
info := &streamer.Info{ info := &core.Info{
Type: "RTSP " + c.mode.String(),
UserAgent: c.UserAgent, UserAgent: c.UserAgent,
Medias: c.Medias, Medias: c.Medias,
Tracks: c.tracks, Receivers: c.receivers,
Recv: uint32(c.receive), Senders: c.senders,
Send: uint32(c.send), Recv: c.recv,
} Send: c.send,
switch c.mode {
case ModeUnknown:
info.Type = "RTSP unknown"
case ModeClientProducer, ModeServerProducer:
info.Type = "RTSP source"
case ModeServerConsumer:
info.Type = "RTSP client"
} }
if c.URL != nil { if c.URL != nil {
@@ -93,14 +80,5 @@ func (c *Conn) MarshalJSON() ([]byte, error) {
info.RemoteAddr = c.conn.RemoteAddr().String() 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) return json.Marshal(info)
} }

View File

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

View File

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

View File

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

View File

@@ -1,18 +1,62 @@
package tapo package tapo
import ( import (
"github.com/AlexxIT/go2rtc/pkg/streamer" "bytes"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/mpegts"
"github.com/pion/rtp" "github.com/pion/rtp"
"strconv"
) )
func (c *Client) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.Track { func (c *Client) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiver) error {
consCodec := media.MatchCodec(track.Codec) if c.sender == nil {
consTrack := c.GetTrack(media, consCodec) if err := c.SetupBackchannel(); err != nil {
if consTrack == nil {
return nil return nil
} }
return track.Bind(func(packet *rtp.Packet) error { w := mpegts.NewWriter()
return consTrack.WriteRTP(packet) 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()
}
}
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 package tapo
import ( import (
"encoding/json"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/mpegts" "github.com/AlexxIT/go2rtc/pkg/mpegts"
"github.com/AlexxIT/go2rtc/pkg/streamer"
) )
func (c *Client) GetMedias() []*streamer.Media { func (c *Client) GetMedias() []*core.Media {
// producer should have persistent medias
if c.medias == nil { if c.medias == nil {
// don't know if all Tapo has this capabilities... // don't know if all Tapo has this capabilities...
c.medias = []*streamer.Media{ c.medias = []*core.Media{
{ {
Kind: streamer.KindVideo, Kind: core.KindVideo,
Direction: streamer.DirectionSendonly, Direction: core.DirectionRecvonly,
Codecs: []*streamer.Codec{ Codecs: []*core.Codec{
{Name: streamer.CodecH264, ClockRate: 90000, PayloadType: streamer.PayloadTypeRAW}, {Name: core.CodecH264, ClockRate: 90000, PayloadType: core.PayloadTypeRAW},
}, },
}, },
{ {
Kind: streamer.KindAudio, Kind: core.KindAudio,
Direction: streamer.DirectionSendonly, Direction: core.DirectionRecvonly,
Codecs: []*streamer.Codec{ Codecs: []*core.Codec{
{Name: streamer.CodecPCMA, ClockRate: 8000, PayloadType: 8}, {Name: core.CodecPCMA, ClockRate: 8000, PayloadType: 8},
}, },
}, },
{ {
Kind: streamer.KindAudio, Kind: core.KindAudio,
Direction: streamer.DirectionRecvonly, Direction: core.DirectionSendonly,
Codecs: []*streamer.Codec{ Codecs: []*core.Codec{
{Name: streamer.CodecPCMA, ClockRate: 8000, PayloadType: 8}, {Name: core.CodecPCMA, ClockRate: 8000, PayloadType: 8},
}, },
}, },
} }
@@ -37,44 +37,26 @@ func (c *Client) GetMedias() []*streamer.Media {
return c.medias return c.medias
} }
func (c *Client) GetTrack(media *streamer.Media, codec *streamer.Codec) (track *streamer.Track) { func (c *Client) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) {
for _, track := range c.tracks { for _, track := range c.receivers {
if track.Codec == codec { if track.Codec == codec {
return track return track, nil
} }
} }
if c.tracks == nil {
c.tracks = map[byte]*streamer.Track{}
}
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 { if err := c.SetupStream(); err != nil {
return nil return nil, err
} }
track = streamer.NewTrack(media, codec) track := core.NewReceiver(media, codec)
c.tracks[payloadType] = track switch media.Kind {
} else { case core.KindVideo:
if err := c.SetupBackchannel(); err != nil { track.ID = mpegts.StreamTypeH264
return nil case core.KindAudio:
track.ID = mpegts.StreamTypePCMATapo
} }
c.receivers = append(c.receivers, track)
if w := c.backchannelWriter(); w != nil { return track, nil
track = streamer.NewTrack(media, codec)
track.Bind(w)
c.tracks[0] = track
}
}
return
} }
func (c *Client) Start() error { func (c *Client) Start() error {
@@ -82,5 +64,25 @@ func (c *Client) Start() error {
} }
func (c *Client) Stop() error { func (c *Client) Stop() error {
for _, receiver := range c.receivers {
receiver.Close()
}
if c.sender != nil {
c.sender.Close()
}
return c.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" "strings"
) )
// ReceiveMTU = Ethernet MTU (1500) - IP Header (20) - UDP Header (8)
const ReceiveMTU = 1472
func NewAPI(address string) (*webrtc.API, error) { func NewAPI(address string) (*webrtc.API, error) {
// for debug logs add to env: `PION_LOG_DEBUG=all` // for debug logs add to env: `PION_LOG_DEBUG=all`
m := &webrtc.MediaEngine{} m := &webrtc.MediaEngine{}
@@ -41,8 +44,7 @@ func NewAPI(address string) (*webrtc.API, error) {
// fix https://github.com/pion/webrtc/pull/2407 // fix https://github.com/pion/webrtc/pull/2407
s.SetDTLSInsecureSkipHelloVerify(true) s.SetDTLSInsecureSkipHelloVerify(true)
// Ethernet MTU (1500) - IP Header (20) - UDP Header (8) s.SetReceiveMTU(ReceiveMTU)
s.SetReceiveMTU(1472)
if address != "" { if address != "" {
address, network, _ := strings.Cut(address, "/") address, network, _ := strings.Cut(address, "/")

View File

@@ -1,27 +1,27 @@
package webrtc package webrtc
import ( import (
"github.com/AlexxIT/go2rtc/pkg/streamer" "github.com/AlexxIT/go2rtc/pkg/core"
"github.com/pion/sdp/v3" "github.com/pion/sdp/v3"
"github.com/pion/webrtc/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 // 1. Create transeivers with proper kind and direction
for _, media := range medias { for _, media := range medias {
var err error var err error
switch media.Direction { switch media.Direction {
case streamer.DirectionRecvonly: case core.DirectionRecvonly:
_, err = c.pc.AddTransceiverFromKind( _, err = c.pc.AddTransceiverFromKind(
webrtc.NewRTPCodecType(media.Kind), webrtc.NewRTPCodecType(media.Kind),
webrtc.RTPTransceiverInit{Direction: webrtc.RTPTransceiverDirectionRecvonly}, webrtc.RTPTransceiverInit{Direction: webrtc.RTPTransceiverDirectionRecvonly},
) )
case streamer.DirectionSendonly: case core.DirectionSendonly:
_, err = c.pc.AddTransceiverFromTrack( _, err = c.pc.AddTransceiverFromTrack(
NewTrack(media.Kind), NewTrack(media.Kind),
webrtc.RTPTransceiverInit{Direction: webrtc.RTPTransceiverDirectionSendonly}, webrtc.RTPTransceiverInit{Direction: webrtc.RTPTransceiverDirectionSendonly},
) )
case streamer.DirectionSendRecv: case core.DirectionSendRecv:
// default transceiver is sendrecv // default transceiver is sendrecv
_, err = c.pc.AddTransceiverFromTrack(NewTrack(media.Kind)) _, 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 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 { if _, err := c.CreateOffer(medias); err != nil {
return "", err return "", err
} }
@@ -68,21 +68,7 @@ func (c *Conn) SetAnswer(answer string) (err error) {
return return
} }
medias := streamer.UnmarshalMedias(sd.MediaDescriptions) c.medias = 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)
}
}
return nil return nil
} }

View File

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

View File

@@ -2,9 +2,6 @@ package webrtc
import ( import (
"github.com/AlexxIT/go2rtc/pkg/core" "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/rtcp"
"github.com/pion/rtp" "github.com/pion/rtp"
"github.com/pion/webrtc/v3" "github.com/pion/webrtc/v3"
@@ -12,18 +9,19 @@ import (
) )
type Conn struct { type Conn struct {
streamer.Element core.Listener
UserAgent string UserAgent string
Desc string Desc string
Mode streamer.Mode Mode core.Mode
pc *webrtc.PeerConnection pc *webrtc.PeerConnection
medias []*streamer.Media medias []*core.Media
tracks []*streamer.Track receivers []*core.Receiver
senders []*core.Sender
receive int recv int
send int send int
offer string offer string
@@ -56,13 +54,26 @@ func NewConn(pc *webrtc.PeerConnection) *Conn {
) )
}) })
pc.OnTrack(func(remote *webrtc.TrackRemote, _ *webrtc.RTPReceiver) { pc.OnTrack(func(remote *webrtc.TrackRemote, receiver *webrtc.RTPReceiver) {
track := c.getRecvTrack(remote) media, codec := c.getMediaCodec(remote)
if track == nil { if media == nil {
return // it's OK when we not need, for example, audio from producer 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() { go func() {
pkts := []rtcp.Packet{&rtcp.PictureLossIndication{MediaSSRC: uint32(remote.SSRC())}} pkts := []rtcp.Packet{&rtcp.PictureLossIndication{MediaSSRC: uint32(remote.SSRC())}}
for range time.NewTicker(time.Second * 2).C { for range time.NewTicker(time.Second * 2).C {
@@ -74,15 +85,20 @@ func NewConn(pc *webrtc.PeerConnection) *Conn {
} }
for { for {
packet, _, err := remote.ReadRTP() b := make([]byte, ReceiveMTU)
n, _, err := remote.Read(b)
if err != nil { if err != nil {
return 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 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 func (c *Conn) getMediaCodec(remote *webrtc.TrackRemote) (*core.Media, *core.Codec) {
for _, tr := range c.pc.GetTransceivers() {
// important to get remote PayloadType // search Transeiver for this TrackRemote
payloadType := media.MatchCodec(codec).PayloadType if tr.Receiver() == nil || tr.Receiver().Track() != remote {
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
}
}
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 continue
} }
// check only incoming tracks (remote media "sendonly") // search Media for this MID
if media.Direction != streamer.DirectionSendonly { for _, media := range c.medias {
if media.ID != tr.Mid() || media.Direction != core.DirectionRecvonly {
continue continue
} }
// search codec for this PayloadType
for _, codec := range media.Codecs { for _, codec := range media.Codecs {
if codec.PayloadType != payloadType { if codec.PayloadType != uint8(remote.PayloadType()) {
continue continue
} }
return media, codec
// 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)
} }
} }
default: // fix moment when core.ModePassiveProducer or core.ModeActiveProducer
panic("not implemented") // sends new codec with new payload type to same media
} // check GetTrack
panic(core.Caller())
return nil return nil, nil
} }

View File

@@ -2,72 +2,77 @@ package webrtc
import ( import (
"encoding/json" "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/rtp"
"github.com/pion/webrtc/v3"
) )
func (c *Conn) GetMedias() []*streamer.Media { func (c *Conn) GetMedias() []*core.Media {
return c.medias return c.medias
} }
func (c *Conn) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.Track { func (c *Conn) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) error {
switch c.Mode { core.Assert(media.Direction == core.DirectionSendonly)
case streamer.ModePassiveConsumer:
switch track.Direction {
case streamer.DirectionSendonly:
// send our track to WebRTC consumer
return c.addSendTrack(media, track)
case streamer.DirectionRecvonly: for _, sender := range c.senders {
// receive track from WebRTC consumer (microphone, backchannel, two way audio) if sender.Codec == codec {
return c.addConsumerRecvTrack(media, track) sender.HandleRTP(track)
}
case streamer.ModePassiveProducer:
// "Stream to camera" function
consCodec := media.MatchCodec(track.Codec)
consTrack := c.GetTrack(media, consCodec)
if consTrack == nil {
return nil return nil
} }
return track.Bind(func(packet *rtp.Packet) error {
return consTrack.WriteRTP(packet)
})
} }
panic("not implemented") switch c.Mode {
} case core.ModePassiveConsumer: // video/audio for browser
case core.ModeActiveProducer: // go2rtc as WebRTC client (backchannel)
func (c *Conn) addConsumerRecvTrack(media *streamer.Media, track *streamer.Track) *streamer.Track { case core.ModePassiveProducer: // WebRTC/WHIP
params := webrtc.RTPCodecParameters{ default:
RTPCodecCapability: webrtc.RTPCodecCapability{ panic(core.Caller())
MimeType: MimeType(track.Codec),
ClockRate: track.Codec.ClockRate,
Channels: track.Codec.Channels,
},
PayloadType: 0, // don't know if this necessary
} }
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 sender := core.NewSender(media, track.Codec)
_ = tr.SetCodecPreferences([]webrtc.RTPCodecParameters{params}) 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) switch codec.Name {
return track 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) { func (c *Conn) MarshalJSON() ([]byte, error) {
info := &streamer.Info{ info := &core.Info{
Type: c.Desc + " " + c.Mode.String(), Type: c.Desc + " " + c.Mode.String(),
RemoteAddr: c.remote, RemoteAddr: c.remote,
UserAgent: c.UserAgent, UserAgent: c.UserAgent,
Medias: c.medias, Medias: c.medias,
Tracks: c.tracks, Receivers: c.receivers,
Recv: uint32(c.receive), Senders: c.senders,
Send: uint32(c.send), Recv: c.recv,
Send: c.send,
} }
return json.Marshal(info) return json.Marshal(info)
} }

View File

@@ -3,8 +3,9 @@ package webrtc
import ( import (
"errors" "errors"
"fmt" "fmt"
"github.com/AlexxIT/go2rtc/pkg/streamer" "github.com/AlexxIT/go2rtc/pkg/core"
"github.com/pion/ice/v2" "github.com/pion/ice/v2"
"github.com/pion/sdp/v3"
"github.com/pion/stun" "github.com/pion/stun"
"github.com/pion/webrtc/v3" "github.com/pion/webrtc/v3"
"hash/crc32" "hash/crc32"
@@ -14,6 +15,43 @@ import (
"time" "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) { func NewCandidate(network, address string) (string, error) {
i := strings.LastIndexByte(address, ':') i := strings.LastIndexByte(address, ':')
if i < 0 { if i < 0 {
@@ -135,25 +173,25 @@ func IsIP(host string) bool {
return true return true
} }
func MimeType(codec *streamer.Codec) string { func MimeType(codec *core.Codec) string {
switch codec.Name { switch codec.Name {
case streamer.CodecH264: case core.CodecH264:
return webrtc.MimeTypeH264 return webrtc.MimeTypeH264
case streamer.CodecH265: case core.CodecH265:
return webrtc.MimeTypeH265 return webrtc.MimeTypeH265
case streamer.CodecVP8: case core.CodecVP8:
return webrtc.MimeTypeVP8 return webrtc.MimeTypeVP8
case streamer.CodecVP9: case core.CodecVP9:
return webrtc.MimeTypeVP9 return webrtc.MimeTypeVP9
case streamer.CodecAV1: case core.CodecAV1:
return webrtc.MimeTypeAV1 return webrtc.MimeTypeAV1
case streamer.CodecPCMU: case core.CodecPCMU:
return webrtc.MimeTypePCMU return webrtc.MimeTypePCMU
case streamer.CodecPCMA: case core.CodecPCMA:
return webrtc.MimeTypePCMA return webrtc.MimeTypePCMA
case streamer.CodecOpus: case core.CodecOpus:
return webrtc.MimeTypeOpus return webrtc.MimeTypeOpus
case streamer.CodecG722: case core.CodecG722:
return webrtc.MimeTypeG722 return webrtc.MimeTypeG722
} }
panic("not implemented") panic("not implemented")

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