mirror of
https://github.com/AlexxIT/go2rtc.git
synced 2025-10-05 08:16:55 +08:00
BIG core logic rewrite
This commit is contained in:
@@ -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
|
||||||
}
|
}
|
||||||
|
@@ -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
|
||||||
|
@@ -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()
|
||||||
|
@@ -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
|
||||||
|
@@ -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}
|
||||||
}
|
}
|
||||||
|
@@ -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}
|
||||||
}
|
}
|
||||||
|
@@ -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}
|
||||||
}
|
}
|
||||||
|
@@ -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
|
||||||
|
@@ -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")
|
||||||
|
@@ -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)
|
||||||
}
|
}
|
||||||
|
@@ -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()
|
||||||
|
@@ -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
|
||||||
|
@@ -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 {
|
||||||
|
@@ -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
|
||||||
|
@@ -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 {
|
||||||
|
@@ -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
|
||||||
|
@@ -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)
|
||||||
|
@@ -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
|
||||||
|
@@ -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")
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@@ -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)
|
|
||||||
}
|
|
@@ -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)
|
||||||
|
@@ -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
|
||||||
|
@@ -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 {
|
||||||
|
case core.DirectionRecvonly:
|
||||||
|
for _, receiver := range p.receivers {
|
||||||
|
codec := media.MatchCodec(receiver.Codec)
|
||||||
|
if codec == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
// convert all old producer tracks to new tracks
|
track, err := p.conn.GetTrack(media, codec)
|
||||||
for i, oldTrack := range p.tracks {
|
if err != nil {
|
||||||
// match new element medias with old track codec
|
continue
|
||||||
for _, media := range medias {
|
}
|
||||||
codec := media.MatchCodec(oldTrack.Codec)
|
|
||||||
if codec == nil {
|
receiver.Replace(track)
|
||||||
continue
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
// move sink from old track to new track
|
case core.DirectionSendonly:
|
||||||
newTrack := p.element.GetTrack(media, codec)
|
for _, sender := range p.senders {
|
||||||
newTrack.GetSink(oldTrack)
|
codec := media.MatchCodec(sender.Codec)
|
||||||
p.tracks[i] = newTrack
|
if codec == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
break
|
_ = p.conn.(core.Consumer).AddTrack(media, codec, sender)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
go p.worker(p.element, workerID)
|
go p.worker(p.conn, workerID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Producer) stop() {
|
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
|
||||||
}
|
}
|
||||||
|
@@ -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() {
|
continue
|
||||||
log.Trace().Stringer("media", prodMedia).
|
}
|
||||||
Msgf("[streams] producer=%d candidate=%d", ip, ipc)
|
|
||||||
|
|
||||||
|
// Step 2. Get producer medias (not tracks yet)
|
||||||
|
for _, prodMedia := range prod.GetMedias() {
|
||||||
collectCodecs(prodMedia, &codecs)
|
collectCodecs(prodMedia, &codecs)
|
||||||
|
|
||||||
// Step 3. Match consumer/producer codecs list
|
// Step 3. Match consumer/producer codecs list
|
||||||
prodCodec := prodMedia.MatchMedia(consMedia)
|
prodCodec, consCodec := prodMedia.MatchMedia(consMedia)
|
||||||
if prodCodec != nil {
|
if prodCodec == nil {
|
||||||
log.Trace().Stringer("codec", prodCodec).
|
continue
|
||||||
Msgf("[streams] match producer:%d:%d => consumer:%d:%d", ip, ipc, ic, icc)
|
}
|
||||||
|
|
||||||
// Step 4. Get producer track
|
var track *core.Receiver
|
||||||
prodTrack := prod.GetTrack(prodMedia, prodCodec)
|
|
||||||
if prodTrack == nil {
|
switch prodMedia.Direction {
|
||||||
log.Warn().Str("url", prod.url).Msg("[streams] can't get track")
|
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
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 5. Add track to consumer and get new track
|
case core.DirectionSendonly:
|
||||||
consTrack := consumer.element.AddTrack(consMedia, prodTrack)
|
// Step 4. Get recvonly track from consumer (backchannel)
|
||||||
|
if track, err = cons.(core.Producer).GetTrack(consMedia, consCodec); err != nil {
|
||||||
consumer.tracks = append(consumer.tracks, consTrack)
|
log.Info().Err(err).Msg("[streams] can't get track")
|
||||||
producers = append(producers, prod)
|
continue
|
||||||
if !consMedia.MatchAll() {
|
|
||||||
break producers
|
|
||||||
}
|
}
|
||||||
|
// Step 5. Add track to producer
|
||||||
|
if err = prod.AddTrack(prodMedia, prodCodec, track); err != nil {
|
||||||
|
log.Info().Err(err).Msg("[streams] can't add track")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
producers = append(producers, prod)
|
||||||
|
|
||||||
|
if !consMedia.MatchAll() {
|
||||||
|
break producers
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// stop producers if they don't have readers
|
||||||
if atomic.AddInt32(&s.requests, -1) == 0 {
|
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")
|
||||||
@@ -200,8 +197,8 @@ 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) {
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
@@ -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 {
|
||||||
|
@@ -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 {
|
||||||
|
@@ -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
|
||||||
|
@@ -2,62 +2,59 @@ 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
|
|
||||||
|
|
||||||
//log.Printf("[RTP/AAC] units: %d, size: %4d, ts: %10d, %t", headersSize/2, len(packet.Payload), packet.Timestamp, packet.Marker)
|
//log.Printf("[RTP/AAC] units: %d, size: %4d, ts: %10d, %t", headersSize/2, len(packet.Payload), packet.Timestamp, packet.Marker)
|
||||||
|
|
||||||
data := packet.Payload[2+headersSize:]
|
data := packet.Payload[2+headersSize:]
|
||||||
if IsADTS(data) {
|
if IsADTS(data) {
|
||||||
data = data[7:]
|
data = data[7:]
|
||||||
}
|
|
||||||
|
|
||||||
clone := *packet
|
|
||||||
clone.Version = RTPPacketVersionAAC
|
|
||||||
clone.Payload = data
|
|
||||||
return push(&clone)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clone := *packet
|
||||||
|
clone.Version = RTPPacketVersionAAC
|
||||||
|
clone.Payload = data
|
||||||
|
handler(&clone)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func RTPPay(mtu uint16) streamer.WrapperFunc {
|
func RTPPay(handler core.HandlerFunc) core.HandlerFunc {
|
||||||
sequencer := rtp.NewRandomSequencer()
|
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 {
|
handler(packet)
|
||||||
return push(packet)
|
return
|
||||||
}
|
|
||||||
|
|
||||||
// support ONLY one unit in payload
|
|
||||||
size := uint16(len(packet.Payload))
|
|
||||||
// 2 bytes header size + 2 bytes first payload size
|
|
||||||
payload := make([]byte, 2+2+size)
|
|
||||||
payload[1] = 16 // header size in bits
|
|
||||||
binary.BigEndian.PutUint16(payload[2:], size<<3)
|
|
||||||
copy(payload[4:], packet.Payload)
|
|
||||||
|
|
||||||
clone := rtp.Packet{
|
|
||||||
Header: rtp.Header{
|
|
||||||
Version: 2,
|
|
||||||
Marker: true,
|
|
||||||
SequenceNumber: sequencer.NextSequenceNumber(),
|
|
||||||
Timestamp: packet.Timestamp,
|
|
||||||
},
|
|
||||||
Payload: payload,
|
|
||||||
}
|
|
||||||
return push(&clone)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// support ONLY one unit in payload
|
||||||
|
size := uint16(len(packet.Payload))
|
||||||
|
// 2 bytes header size + 2 bytes first payload size
|
||||||
|
payload := make([]byte, 2+2+size)
|
||||||
|
payload[1] = 16 // header size in bits
|
||||||
|
binary.BigEndian.PutUint16(payload[2:], size<<3)
|
||||||
|
copy(payload[4:], packet.Payload)
|
||||||
|
|
||||||
|
clone := rtp.Packet{
|
||||||
|
Header: rtp.Header{
|
||||||
|
Version: 2,
|
||||||
|
Marker: true,
|
||||||
|
SequenceNumber: sequencer.NextSequenceNumber(),
|
||||||
|
Timestamp: packet.Timestamp,
|
||||||
|
},
|
||||||
|
Payload: payload,
|
||||||
|
}
|
||||||
|
handler(&clone)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
142
pkg/core/codec.go
Normal file
142
pkg/core/codec.go
Normal 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
99
pkg/core/core.go
Normal 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"
|
||||||
|
)
|
@@ -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
18
pkg/core/listener.go
Normal 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
191
pkg/core/media.go
Normal 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
|
||||||
|
}
|
@@ -1,4 +1,4 @@
|
|||||||
package streamer
|
package core
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
31
pkg/core/probe.go
Normal file
31
pkg/core/probe.go
Normal 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
188
pkg/core/track.go
Normal 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())
|
||||||
|
}
|
@@ -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 {
|
||||||
|
@@ -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 nil, core.ErrCantGetTrack
|
||||||
return c.audioTrack
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
||||||
|
}
|
||||||
|
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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
|
||||||
}
|
}
|
||||||
|
187
pkg/h264/rtp.go
187
pkg/h264/rtp.go
@@ -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,119 +11,112 @@ 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
|
|
||||||
// Reolink Duo 2: sends SPS with Marker and PPS without
|
|
||||||
if packet.Marker && len(payload) < PSMaxSize {
|
|
||||||
switch NALUType(payload) {
|
|
||||||
case NALUTypeSPS, NALUTypePPS:
|
|
||||||
buf = append(buf, payload...)
|
|
||||||
return nil
|
|
||||||
case NALUTypeSEI:
|
|
||||||
// RtspServer https://github.com/AlexxIT/go2rtc/issues/244
|
|
||||||
// sends, marked SPS, marked PPS, marked SEI, marked IFrame
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(buf) == 0 {
|
|
||||||
for {
|
|
||||||
// Amcrest IP4M-1051: 9, 7, 8, 6, 28...
|
|
||||||
// Amcrest IP4M-1051: 9, 6, 1
|
|
||||||
switch NALUType(payload) {
|
|
||||||
case NALUTypeIFrame:
|
|
||||||
// fix IFrame without SPS,PPS
|
|
||||||
buf = append(buf, ps...)
|
|
||||||
case NALUTypeSEI, NALUTypeAUD:
|
|
||||||
// fix ffmpeg with transcoding first frame
|
|
||||||
i := int(4 + binary.BigEndian.Uint32(payload))
|
|
||||||
|
|
||||||
// check if only one NAL (fix ffmpeg transcoding for Reolink RLC-510A)
|
|
||||||
if i == len(payload) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
payload = payload[i:]
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// collect all NALs for Access Unit
|
|
||||||
if !packet.Marker {
|
|
||||||
buf = append(buf, payload...)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(buf) > 0 {
|
|
||||||
payload = append(buf, payload...)
|
|
||||||
buf = buf[:0]
|
|
||||||
}
|
|
||||||
|
|
||||||
// should not be that huge SPS
|
|
||||||
if NALUType(payload) == NALUTypeSPS && binary.BigEndian.Uint32(payload) >= PSMaxSize {
|
|
||||||
// some Chinese buggy cameras has single packet with SPS+PPS+IFrame separated by 00 00 00 01
|
|
||||||
// https://github.com/AlexxIT/WebRTC/issues/391
|
|
||||||
// https://github.com/AlexxIT/WebRTC/issues/392
|
|
||||||
AnnexB2AVC(payload)
|
|
||||||
}
|
|
||||||
|
|
||||||
//log.Printf("[AVC] %v, len: %d, ts: %10d, seq: %d", Types(payload), len(payload), packet.Timestamp, packet.SequenceNumber)
|
|
||||||
|
|
||||||
clone := *packet
|
|
||||||
clone.Version = RTPPacketVersionAVC
|
|
||||||
clone.Payload = payload
|
|
||||||
return push(&clone)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fix TP-Link Tapo TC70: sends SPS and PPS with packet.Marker = true
|
||||||
|
// Reolink Duo 2: sends SPS with Marker and PPS without
|
||||||
|
if packet.Marker && len(payload) < PSMaxSize {
|
||||||
|
switch NALUType(payload) {
|
||||||
|
case NALUTypeSPS, NALUTypePPS:
|
||||||
|
buf = append(buf, payload...)
|
||||||
|
return
|
||||||
|
case NALUTypeSEI:
|
||||||
|
// RtspServer https://github.com/AlexxIT/go2rtc/issues/244
|
||||||
|
// sends, marked SPS, marked PPS, marked SEI, marked IFrame
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(buf) == 0 {
|
||||||
|
for {
|
||||||
|
// Amcrest IP4M-1051: 9, 7, 8, 6, 28...
|
||||||
|
// Amcrest IP4M-1051: 9, 6, 1
|
||||||
|
switch NALUType(payload) {
|
||||||
|
case NALUTypeIFrame:
|
||||||
|
// fix IFrame without SPS,PPS
|
||||||
|
buf = append(buf, ps...)
|
||||||
|
case NALUTypeSEI, NALUTypeAUD:
|
||||||
|
// fix ffmpeg with transcoding first frame
|
||||||
|
i := int(4 + binary.BigEndian.Uint32(payload))
|
||||||
|
|
||||||
|
// check if only one NAL (fix ffmpeg transcoding for Reolink RLC-510A)
|
||||||
|
if i == len(payload) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
payload = payload[i:]
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// collect all NALs for Access Unit
|
||||||
|
if !packet.Marker {
|
||||||
|
buf = append(buf, payload...)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(buf) > 0 {
|
||||||
|
payload = append(buf, payload...)
|
||||||
|
buf = buf[:0]
|
||||||
|
}
|
||||||
|
|
||||||
|
// should not be that huge SPS
|
||||||
|
if NALUType(payload) == NALUTypeSPS && binary.BigEndian.Uint32(payload) >= PSMaxSize {
|
||||||
|
// some Chinese buggy cameras has single packet with SPS+PPS+IFrame separated by 00 00 00 01
|
||||||
|
// https://github.com/AlexxIT/WebRTC/issues/391
|
||||||
|
// https://github.com/AlexxIT/WebRTC/issues/392
|
||||||
|
AnnexB2AVC(payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
//log.Printf("[AVC] %v, len: %d, ts: %10d, seq: %d", Types(payload), len(payload), packet.Timestamp, packet.SequenceNumber)
|
||||||
|
|
||||||
|
clone := *packet
|
||||||
|
clone.Version = RTPPacketVersionAVC
|
||||||
|
clone.Payload = payload
|
||||||
|
handler(&clone)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func RTPPay(mtu uint16) streamer.WrapperFunc {
|
func RTPPay(mtu uint16, handler core.HandlerFunc) core.HandlerFunc {
|
||||||
payloader := &Payloader{IsAVC: true}
|
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 {
|
handler(packet)
|
||||||
return push(packet)
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
payloads := payloader.Payload(mtu, packet.Payload)
|
payloads := payloader.Payload(mtu, packet.Payload)
|
||||||
last := len(payloads) - 1
|
last := len(payloads) - 1
|
||||||
for i, payload := range payloads {
|
for i, payload := range payloads {
|
||||||
clone := rtp.Packet{
|
clone := rtp.Packet{
|
||||||
Header: rtp.Header{
|
Header: rtp.Header{
|
||||||
Version: 2,
|
Version: 2,
|
||||||
Marker: i == last,
|
Marker: i == last,
|
||||||
SequenceNumber: sequencer.NextSequenceNumber(),
|
SequenceNumber: sequencer.NextSequenceNumber(),
|
||||||
Timestamp: packet.Timestamp,
|
Timestamp: packet.Timestamp,
|
||||||
},
|
},
|
||||||
Payload: payload,
|
Payload: payload,
|
||||||
}
|
|
||||||
if err := push(&clone); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
handler(&clone)
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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
|
||||||
|
264
pkg/h265/rtp.go
264
pkg/h265/rtp.go
@@ -2,189 +2,177 @@ 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)
|
|
||||||
|
|
||||||
// Fix for RtspServer https://github.com/AlexxIT/go2rtc/issues/244
|
// Fix for RtspServer https://github.com/AlexxIT/go2rtc/issues/244
|
||||||
if packet.Marker && len(data) < h264.PSMaxSize {
|
if packet.Marker && len(data) < h264.PSMaxSize {
|
||||||
switch nuType {
|
switch nuType {
|
||||||
case NALUTypeVPS, NALUTypeSPS, NALUTypePPS:
|
case NALUTypeVPS, NALUTypeSPS, NALUTypePPS:
|
||||||
packet.Marker = false
|
packet.Marker = false
|
||||||
case NALUTypePrefixSEI, NALUTypeSuffixSEI:
|
case NALUTypePrefixSEI, NALUTypeSuffixSEI:
|
||||||
return nil
|
return
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if nuType == NALUTypeFU {
|
if nuType == NALUTypeFU {
|
||||||
switch data[2] >> 6 {
|
switch data[2] >> 6 {
|
||||||
case 2: // begin
|
case 2: // begin
|
||||||
nuType = data[2] & 0x3F
|
nuType = data[2] & 0x3F
|
||||||
|
|
||||||
// push PS data before keyframe
|
// push PS data before keyframe
|
||||||
//if len(buf) == 0 && nuType >= 19 && nuType <= 21 {
|
//if len(buf) == 0 && nuType >= 19 && nuType <= 21 {
|
||||||
// buf = append(buf, ps...)
|
// buf = append(buf, ps...)
|
||||||
//}
|
//}
|
||||||
|
|
||||||
nuStart = len(buf)
|
|
||||||
buf = append(buf, 0, 0, 0, 0) // NAL unit size
|
|
||||||
buf = append(buf, (data[0]&0x81)|(nuType<<1), data[1])
|
|
||||||
buf = append(buf, data[3:]...)
|
|
||||||
return nil
|
|
||||||
case 0: // continue
|
|
||||||
buf = append(buf, data[3:]...)
|
|
||||||
return nil
|
|
||||||
case 1: // end
|
|
||||||
buf = append(buf, data[3:]...)
|
|
||||||
binary.BigEndian.PutUint32(buf[nuStart:], uint32(len(buf)-nuStart-4))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
nuStart = len(buf)
|
nuStart = len(buf)
|
||||||
buf = append(buf, 0, 0, 0, 0) // NAL unit size
|
buf = append(buf, 0, 0, 0, 0) // NAL unit size
|
||||||
buf = append(buf, data...)
|
buf = append(buf, (data[0]&0x81)|(nuType<<1), data[1])
|
||||||
binary.BigEndian.PutUint32(buf[nuStart:], uint32(len(data)))
|
buf = append(buf, data[3:]...)
|
||||||
|
return
|
||||||
|
case 0: // continue
|
||||||
|
buf = append(buf, data[3:]...)
|
||||||
|
return
|
||||||
|
case 1: // end
|
||||||
|
buf = append(buf, data[3:]...)
|
||||||
|
binary.BigEndian.PutUint32(buf[nuStart:], uint32(len(buf)-nuStart-4))
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
// collect all NAL Units for Access Unit
|
nuStart = len(buf)
|
||||||
if !packet.Marker {
|
buf = append(buf, 0, 0, 0, 0) // NAL unit size
|
||||||
return nil
|
buf = append(buf, data...)
|
||||||
}
|
binary.BigEndian.PutUint32(buf[nuStart:], uint32(len(data)))
|
||||||
|
|
||||||
//log.Printf("[HEVC] %v, len: %d", Types(buf), len(buf))
|
|
||||||
|
|
||||||
clone := *packet
|
|
||||||
clone.Version = h264.RTPPacketVersionAVC
|
|
||||||
clone.Payload = buf
|
|
||||||
|
|
||||||
buf = buf[:0]
|
|
||||||
|
|
||||||
return push(&clone)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// collect all NAL Units for Access Unit
|
||||||
|
if !packet.Marker {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//log.Printf("[HEVC] %v, len: %d", Types(buf), len(buf))
|
||||||
|
|
||||||
|
clone := *packet
|
||||||
|
clone.Version = h264.RTPPacketVersionAVC
|
||||||
|
clone.Payload = buf
|
||||||
|
|
||||||
|
buf = buf[:0]
|
||||||
|
|
||||||
|
handler(&clone)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func RTPPay(mtu uint16) streamer.WrapperFunc {
|
func RTPPay(mtu uint16, handler core.HandlerFunc) core.HandlerFunc {
|
||||||
payloader := &Payloader{}
|
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 {
|
handler(packet)
|
||||||
return push(packet)
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
payloads := payloader.Payload(mtu, packet.Payload)
|
payloads := payloader.Payload(mtu, packet.Payload)
|
||||||
last := len(payloads) - 1
|
last := len(payloads) - 1
|
||||||
for i, payload := range payloads {
|
for i, payload := range payloads {
|
||||||
clone := rtp.Packet{
|
clone := rtp.Packet{
|
||||||
Header: rtp.Header{
|
Header: rtp.Header{
|
||||||
Version: 2,
|
Version: 2,
|
||||||
Marker: i == last,
|
Marker: i == last,
|
||||||
SequenceNumber: sequencer.NextSequenceNumber(),
|
SequenceNumber: sequencer.NextSequenceNumber(),
|
||||||
Timestamp: packet.Timestamp,
|
Timestamp: packet.Timestamp,
|
||||||
},
|
},
|
||||||
Payload: payload,
|
Payload: payload,
|
||||||
}
|
|
||||||
if err := push(&clone); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
handler(&clone)
|
||||||
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 {
|
handler(packet)
|
||||||
return push(packet)
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// protect original packets from modification
|
||||||
|
au := make([]byte, len(packet.Payload))
|
||||||
|
copy(au, packet.Payload)
|
||||||
|
|
||||||
|
var start byte
|
||||||
|
|
||||||
|
for i := 0; i < len(au); {
|
||||||
|
size := int(binary.BigEndian.Uint32(au[i:])) + 4
|
||||||
|
|
||||||
|
// convert AVC to Annex-B
|
||||||
|
au[i] = 0
|
||||||
|
au[i+1] = 0
|
||||||
|
au[i+2] = 0
|
||||||
|
au[i+3] = 1
|
||||||
|
|
||||||
|
switch NALUType(au[i:]) {
|
||||||
|
case NALUTypeIFrame, NALUTypeIFrame2, NALUTypeIFrame3:
|
||||||
|
start = 3
|
||||||
|
default:
|
||||||
|
if start == 0 {
|
||||||
|
start = 2
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// protect original packets from modification
|
i += size
|
||||||
au := make([]byte, len(packet.Payload))
|
}
|
||||||
copy(au, packet.Payload)
|
|
||||||
|
|
||||||
var start byte
|
// rtp.Packet payload
|
||||||
|
b := make([]byte, 1, size)
|
||||||
|
size-- // minus header byte
|
||||||
|
|
||||||
for i := 0; i < len(au); {
|
for au != nil {
|
||||||
size := int(binary.BigEndian.Uint32(au[i:])) + 4
|
b[0] = start
|
||||||
|
|
||||||
// convert AVC to Annex-B
|
if start > 1 {
|
||||||
au[i] = 0
|
start -= 2
|
||||||
au[i+1] = 0
|
|
||||||
au[i+2] = 0
|
|
||||||
au[i+3] = 1
|
|
||||||
|
|
||||||
switch NALUType(au[i:]) {
|
|
||||||
case NALUTypeIFrame, NALUTypeIFrame2, NALUTypeIFrame3:
|
|
||||||
start = 3
|
|
||||||
default:
|
|
||||||
if start == 0 {
|
|
||||||
start = 2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
i += size
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// rtp.Packet payload
|
if len(au) > size {
|
||||||
b := make([]byte, 1, size)
|
b = append(b, au[:size]...)
|
||||||
size-- // minus header byte
|
au = au[size:]
|
||||||
|
} else {
|
||||||
for au != nil {
|
b = append(b, au...)
|
||||||
b[0] = start
|
au = nil
|
||||||
|
|
||||||
if start > 1 {
|
|
||||||
start -= 2
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(au) > size {
|
|
||||||
b = append(b, au[:size]...)
|
|
||||||
au = au[size:]
|
|
||||||
} else {
|
|
||||||
b = append(b, au...)
|
|
||||||
au = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
clone := rtp.Packet{
|
|
||||||
Header: rtp.Header{
|
|
||||||
Version: 2,
|
|
||||||
Marker: au == nil,
|
|
||||||
SequenceNumber: sequencer.NextSequenceNumber(),
|
|
||||||
Timestamp: packet.Timestamp,
|
|
||||||
},
|
|
||||||
Payload: b,
|
|
||||||
}
|
|
||||||
if err := push(&clone); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
b = b[:1] // clear buffer
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
clone := rtp.Packet{
|
||||||
|
Header: rtp.Header{
|
||||||
|
Version: 2,
|
||||||
|
Marker: au == nil,
|
||||||
|
SequenceNumber: sequencer.NextSequenceNumber(),
|
||||||
|
Timestamp: packet.Timestamp,
|
||||||
|
},
|
||||||
|
Payload: b,
|
||||||
|
}
|
||||||
|
handler(&clone)
|
||||||
|
|
||||||
|
b = b[:1] // clear buffer
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -11,14 +11,14 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Character struct {
|
type Character struct {
|
||||||
AID int `json:"aid,omitempty"`
|
AID int `json:"aid,omitempty"`
|
||||||
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"`
|
||||||
|
|
||||||
listeners map[io.Writer]bool
|
listeners map[io.Writer]bool
|
||||||
|
@@ -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
|
||||||
|
@@ -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)
|
||||||
}
|
}
|
||||||
|
@@ -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,16 +11,15 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Client struct {
|
type Client struct {
|
||||||
streamer.Element
|
core.Listener
|
||||||
|
|
||||||
url string
|
|
||||||
|
|
||||||
medias []*streamer.Media
|
|
||||||
tracks []*streamer.Track
|
|
||||||
|
|
||||||
|
url string
|
||||||
channel string
|
channel string
|
||||||
conn net.Conn
|
conn net.Conn
|
||||||
send int
|
|
||||||
|
medias []*core.Media
|
||||||
|
sender *core.Sender
|
||||||
|
send int
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewClient(rawURL string) (*Client, error) {
|
func NewClient(rawURL string) (*Client, error) {
|
||||||
@@ -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},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@@ -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)
|
||||||
}
|
}
|
||||||
|
@@ -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)
|
|
||||||
}
|
|
@@ -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)
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
panic(fmt.Sprintf("wrong media/codec: %+v %+v", media, codec))
|
return nil, core.ErrCantGetTrack
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
packet = &rtp.Packet{Header: rtp.Header{Timestamp: now()}, Payload: buf}
|
if c.receiver != nil {
|
||||||
_ = c.track.WriteRTP(packet)
|
packet = &rtp.Packet{Header: rtp.Header{Timestamp: now()}, Payload: buf}
|
||||||
|
c.receiver.WriteRTP(packet)
|
||||||
|
}
|
||||||
|
|
||||||
atomic.AddUint32(&c.recv, uint32(len(buf)))
|
c.recv += len(buf)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -109,10 +111,12 @@ func (c *Client) startMJPEG(boundary string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
packet := &rtp.Packet{Header: rtp.Header{Timestamp: now()}, Payload: buf}
|
if c.receiver != nil {
|
||||||
_ = c.track.WriteRTP(packet)
|
packet := &rtp.Packet{Header: rtp.Header{Timestamp: now()}, Payload: buf}
|
||||||
|
c.receiver.WriteRTP(packet)
|
||||||
|
}
|
||||||
|
|
||||||
atomic.AddUint32(&c.recv, uint32(len(buf)))
|
c.recv += len(buf)
|
||||||
|
|
||||||
if _, err = r.Discard(2); err != nil {
|
if _, err = r.Discard(2); err != nil {
|
||||||
return err
|
return err
|
||||||
|
@@ -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.Fire(packet.Payload)
|
c.sender = core.NewSender(media, track.Codec)
|
||||||
atomic.AddUint32(&c.send, uint32(len(packet.Payload)))
|
c.sender.Handler = func(packet *rtp.Packet) {
|
||||||
return nil
|
c.Fire(packet.Payload)
|
||||||
|
c.send += len(packet.Payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
if track.Codec.IsRTP() {
|
||||||
|
c.sender.Handler = RTPDepay(c.sender.Handler)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if track.Codec.IsRTP() {
|
c.sender.HandleRTP(track)
|
||||||
wrapper := RTPDepay(track)
|
return nil
|
||||||
push = wrapper(push)
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return track.Bind(push)
|
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)
|
||||||
}
|
}
|
||||||
|
@@ -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)
|
||||||
}
|
}
|
||||||
|
290
pkg/mjpeg/rtp.go
290
pkg/mjpeg/rtp.go
@@ -3,86 +3,84 @@ 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
|
||||||
b := packet.Payload
|
b := packet.Payload
|
||||||
|
|
||||||
// 3.1. JPEG header
|
// 3.1. JPEG header
|
||||||
t := b[4]
|
t := b[4]
|
||||||
|
|
||||||
// 3.1.7. Restart Marker header
|
// 3.1.7. Restart Marker header
|
||||||
if 64 <= t && t <= 127 {
|
if 64 <= t && t <= 127 {
|
||||||
b = b[12:] // skip it
|
b = b[12:] // skip it
|
||||||
} else {
|
} else {
|
||||||
b = b[8:]
|
b = b[8:]
|
||||||
}
|
|
||||||
|
|
||||||
if len(buf) == 0 {
|
|
||||||
var lqt, cqt []byte
|
|
||||||
|
|
||||||
// 3.1.8. Quantization Table header
|
|
||||||
q := packet.Payload[5]
|
|
||||||
if q >= 128 {
|
|
||||||
lqt = b[4:68]
|
|
||||||
cqt = b[68:132]
|
|
||||||
b = b[132:]
|
|
||||||
} else {
|
|
||||||
lqt, cqt = MakeTables(q)
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://www.rfc-editor.org/rfc/rfc2435#section-3.1.5
|
|
||||||
// The maximum width is 2040 pixels.
|
|
||||||
w := uint16(packet.Payload[6]) << 3
|
|
||||||
h := uint16(packet.Payload[7]) << 3
|
|
||||||
|
|
||||||
// fix sizes more than 2040
|
|
||||||
switch {
|
|
||||||
// 512x1920 512x1440
|
|
||||||
case w == cutSize(2560) && (h == 1920 || h == 1440):
|
|
||||||
w = 2560
|
|
||||||
// 1792x112
|
|
||||||
case w == cutSize(3840) && h == cutSize(2160):
|
|
||||||
w = 3840
|
|
||||||
h = 2160
|
|
||||||
// 256x1296
|
|
||||||
case w == cutSize(2304) && h == 1296:
|
|
||||||
w = 2304
|
|
||||||
}
|
|
||||||
|
|
||||||
//fmt.Printf("t: %d, q: %d, w: %d, h: %d\n", t, q, w, h)
|
|
||||||
buf = MakeHeaders(buf, t, w, h, lqt, cqt)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3.1.9. JPEG Payload
|
|
||||||
buf = append(buf, b...)
|
|
||||||
|
|
||||||
if !packet.Marker {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if end := buf[len(buf)-2:]; end[0] != 0xFF && end[1] != 0xD9 {
|
|
||||||
buf = append(buf, 0xFF, 0xD9)
|
|
||||||
}
|
|
||||||
|
|
||||||
clone := *packet
|
|
||||||
clone.Payload = buf
|
|
||||||
|
|
||||||
buf = buf[:0] // clear buffer
|
|
||||||
|
|
||||||
return push(&clone)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(buf) == 0 {
|
||||||
|
var lqt, cqt []byte
|
||||||
|
|
||||||
|
// 3.1.8. Quantization Table header
|
||||||
|
q := packet.Payload[5]
|
||||||
|
if q >= 128 {
|
||||||
|
lqt = b[4:68]
|
||||||
|
cqt = b[68:132]
|
||||||
|
b = b[132:]
|
||||||
|
} else {
|
||||||
|
lqt, cqt = MakeTables(q)
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://www.rfc-editor.org/rfc/rfc2435#section-3.1.5
|
||||||
|
// The maximum width is 2040 pixels.
|
||||||
|
w := uint16(packet.Payload[6]) << 3
|
||||||
|
h := uint16(packet.Payload[7]) << 3
|
||||||
|
|
||||||
|
// fix sizes more than 2040
|
||||||
|
switch {
|
||||||
|
// 512x1920 512x1440
|
||||||
|
case w == cutSize(2560) && (h == 1920 || h == 1440):
|
||||||
|
w = 2560
|
||||||
|
// 1792x112
|
||||||
|
case w == cutSize(3840) && h == cutSize(2160):
|
||||||
|
w = 3840
|
||||||
|
h = 2160
|
||||||
|
// 256x1296
|
||||||
|
case w == cutSize(2304) && h == 1296:
|
||||||
|
w = 2304
|
||||||
|
}
|
||||||
|
|
||||||
|
//fmt.Printf("t: %d, q: %d, w: %d, h: %d\n", t, q, w, h)
|
||||||
|
buf = MakeHeaders(buf, t, w, h, lqt, cqt)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3.1.9. JPEG Payload
|
||||||
|
buf = append(buf, b...)
|
||||||
|
|
||||||
|
if !packet.Marker {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if end := buf[len(buf)-2:]; end[0] != 0xFF && end[1] != 0xD9 {
|
||||||
|
buf = append(buf, 0xFF, 0xD9)
|
||||||
|
}
|
||||||
|
|
||||||
|
clone := *packet
|
||||||
|
clone.Payload = buf
|
||||||
|
|
||||||
|
buf = buf[:0] // clear buffer
|
||||||
|
|
||||||
|
handlerFunc(&clone)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,102 +88,96 @@ func cutSize(size uint16) uint16 {
|
|||||||
return ((size >> 3) & 0xFF) << 3
|
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
|
||||||
return err
|
}
|
||||||
|
|
||||||
|
h1 := make([]byte, 8)
|
||||||
|
h1[4] = 1 // Type
|
||||||
|
h1[5] = 255 // Q
|
||||||
|
|
||||||
|
// MBZ=0, Precision=0, Length=128
|
||||||
|
h2 := make([]byte, 4, 132)
|
||||||
|
h2[3] = 128
|
||||||
|
|
||||||
|
var jpgData []byte
|
||||||
|
for jpgData == nil {
|
||||||
|
// 2 bytes h1
|
||||||
|
if p[0] != 0xFF {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 := make([]byte, 8)
|
size := binary.BigEndian.Uint16(p[2:]) + 2
|
||||||
h1[4] = 1 // Type
|
|
||||||
h1[5] = 255 // Q
|
|
||||||
|
|
||||||
// MBZ=0, Precision=0, Length=128
|
// 2 bytes payload size (include 2 bytes)
|
||||||
h2 := make([]byte, 4, 132)
|
switch p[1] {
|
||||||
h2[3] = 128
|
case 0xD8: // 0. Start Of Image (size=0)
|
||||||
|
p = p[2:]
|
||||||
var jpgData []byte
|
continue
|
||||||
for jpgData == nil {
|
case 0xDB: // 1. Define Quantization Table (size=130)
|
||||||
// 2 bytes h1
|
for i := uint16(4 + 1); i < size; i += 1 + 64 {
|
||||||
if p[0] != 0xFF {
|
h2 = append(h2, p[i:i+64]...)
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
case 0xC0: // 2. Start Of Frame (size=15)
|
||||||
size := binary.BigEndian.Uint16(p[2:]) + 2
|
if p[4] != 8 {
|
||||||
|
return
|
||||||
// 2 bytes payload size (include 2 bytes)
|
|
||||||
switch p[1] {
|
|
||||||
case 0xD8: // 0. Start Of Image (size=0)
|
|
||||||
p = p[2:]
|
|
||||||
continue
|
|
||||||
case 0xDB: // 1. Define Quantization Table (size=130)
|
|
||||||
for i := uint16(4 + 1); i < size; i += 1 + 64 {
|
|
||||||
h2 = append(h2, p[i:i+64]...)
|
|
||||||
}
|
|
||||||
case 0xC0: // 2. Start Of Frame (size=15)
|
|
||||||
if p[4] != 8 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
h := binary.BigEndian.Uint16(p[5:])
|
|
||||||
w := binary.BigEndian.Uint16(p[7:])
|
|
||||||
h1[6] = uint8(w >> 3)
|
|
||||||
h1[7] = uint8(h >> 3)
|
|
||||||
case 0xC4: // 3. Define Huffman Table (size=416)
|
|
||||||
case 0xDA: // 4. Start Of Scan (size=10)
|
|
||||||
jpgData = p[size:]
|
|
||||||
}
|
}
|
||||||
|
h := binary.BigEndian.Uint16(p[5:])
|
||||||
p = p[size:]
|
w := binary.BigEndian.Uint16(p[7:])
|
||||||
|
h1[6] = uint8(w >> 3)
|
||||||
|
h1[7] = uint8(h >> 3)
|
||||||
|
case 0xC4: // 3. Define Huffman Table (size=416)
|
||||||
|
case 0xDA: // 4. Start Of Scan (size=10)
|
||||||
|
jpgData = p[size:]
|
||||||
}
|
}
|
||||||
|
|
||||||
offset := 0
|
p = p[size:]
|
||||||
p = make([]byte, 0)
|
}
|
||||||
|
|
||||||
for jpgData != nil {
|
offset := 0
|
||||||
p = p[:0]
|
p = make([]byte, 0)
|
||||||
|
|
||||||
if offset > 0 {
|
for jpgData != nil {
|
||||||
h1[1] = byte(offset >> 16)
|
p = p[:0]
|
||||||
h1[2] = byte(offset >> 8)
|
|
||||||
h1[3] = byte(offset)
|
|
||||||
p = append(p, h1...)
|
|
||||||
} else {
|
|
||||||
p = append(p, h1...)
|
|
||||||
p = append(p, h2...)
|
|
||||||
}
|
|
||||||
|
|
||||||
dataLen := packetSize - len(p)
|
if offset > 0 {
|
||||||
if dataLen < len(jpgData) {
|
h1[1] = byte(offset >> 16)
|
||||||
p = append(p, jpgData[:dataLen]...)
|
h1[2] = byte(offset >> 8)
|
||||||
jpgData = jpgData[dataLen:]
|
h1[3] = byte(offset)
|
||||||
offset += dataLen
|
p = append(p, h1...)
|
||||||
} else {
|
} else {
|
||||||
p = append(p, jpgData...)
|
p = append(p, h1...)
|
||||||
jpgData = nil
|
p = append(p, h2...)
|
||||||
}
|
|
||||||
|
|
||||||
clone := rtp.Packet{
|
|
||||||
Header: rtp.Header{
|
|
||||||
Version: 2,
|
|
||||||
Marker: jpgData == nil,
|
|
||||||
SequenceNumber: sequencer.NextSequenceNumber(),
|
|
||||||
Timestamp: packet.Timestamp,
|
|
||||||
},
|
|
||||||
Payload: p,
|
|
||||||
}
|
|
||||||
if err := push(&clone); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
dataLen := packetSize - len(p)
|
||||||
|
if dataLen < len(jpgData) {
|
||||||
|
p = append(p, jpgData[:dataLen]...)
|
||||||
|
jpgData = jpgData[dataLen:]
|
||||||
|
offset += dataLen
|
||||||
|
} else {
|
||||||
|
p = append(p, jpgData...)
|
||||||
|
jpgData = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
clone := rtp.Packet{
|
||||||
|
Header: rtp.Header{
|
||||||
|
Version: 2,
|
||||||
|
Marker: jpgData == nil,
|
||||||
|
SequenceNumber: sequencer.NextSequenceNumber(),
|
||||||
|
Timestamp: packet.Timestamp,
|
||||||
|
},
|
||||||
|
Payload: p,
|
||||||
|
}
|
||||||
|
handlerFunc(&clone)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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
|
||||||
|
|
||||||
muxer *Muxer
|
senders []*core.Sender
|
||||||
codecs []*streamer.Codec
|
|
||||||
wait byte
|
|
||||||
|
|
||||||
send uint32
|
muxer *Muxer
|
||||||
|
wait byte
|
||||||
|
|
||||||
|
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,
|
||||||
return streamer.ParseQuery(query)
|
Codecs: []*core.Codec{
|
||||||
}
|
{Name: core.CodecH264},
|
||||||
|
{Name: core.CodecH265},
|
||||||
const (
|
},
|
||||||
waitNone byte = iota
|
|
||||||
waitKeyframe
|
|
||||||
waitInit
|
|
||||||
)
|
|
||||||
|
|
||||||
func (c *Consumer) GetMedias() []*streamer.Media {
|
|
||||||
if c.Medias != nil {
|
|
||||||
return c.Medias
|
|
||||||
}
|
|
||||||
|
|
||||||
// default medias
|
|
||||||
return []*streamer.Media{
|
|
||||||
{
|
|
||||||
Kind: streamer.KindVideo,
|
|
||||||
Direction: streamer.DirectionRecvonly,
|
|
||||||
Codecs: []*streamer.Codec{
|
|
||||||
{Name: streamer.CodecH264},
|
|
||||||
{Name: streamer.CodecH265},
|
|
||||||
},
|
},
|
||||||
},
|
{
|
||||||
{
|
Kind: core.KindAudio,
|
||||||
Kind: streamer.KindAudio,
|
Direction: core.DirectionSendonly,
|
||||||
Direction: streamer.DirectionRecvonly,
|
Codecs: []*core.Codec{
|
||||||
Codecs: []*streamer.Codec{
|
{Name: core.CodecAAC},
|
||||||
{Name: streamer.CodecAAC},
|
},
|
||||||
},
|
},
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
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
19
pkg/mp4/helpers.go
Normal 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
|
||||||
|
)
|
@@ -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,
|
||||||
)
|
)
|
||||||
|
@@ -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)
|
||||||
}
|
}
|
||||||
|
@@ -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 {
|
@@ -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,
|
@@ -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,
|
@@ -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
|
||||||
|
@@ -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,46 +21,50 @@ 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 {
|
||||||
// count track on probe state even if not support it
|
receiver.WriteRTP(packet)
|
||||||
probe.Append(packet.PayloadType)
|
continue reading
|
||||||
|
|
||||||
media := GetMedia(packet)
|
|
||||||
if media == nil {
|
|
||||||
continue // unsupported codec
|
|
||||||
}
|
}
|
||||||
|
|
||||||
track = streamer.NewTrack(media, nil)
|
|
||||||
|
|
||||||
c.medias = append(c.medias, media)
|
|
||||||
c.tracks[packet.PayloadType] = track
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = track.WriteRTP(packet)
|
// count track on probe state even if not support it
|
||||||
|
probe.Append(packet.PayloadType)
|
||||||
|
|
||||||
|
media := GetMedia(packet)
|
||||||
|
if media == nil {
|
||||||
|
continue // unsupported codec
|
||||||
|
}
|
||||||
|
|
||||||
|
c.medias = append(c.medias, media)
|
||||||
|
|
||||||
|
receiver := core.NewReceiver(media, media.Codecs[0])
|
||||||
|
receiver.ID = packet.PayloadType
|
||||||
|
c.receivers = append(c.receivers, receiver)
|
||||||
|
|
||||||
|
receiver.WriteRTP(packet)
|
||||||
|
|
||||||
//log.Printf("[AVC] %v, len: %d, pts: %d ts: %10d", h264.Types(packet.Payload), len(packet.Payload), pkt.PTS, packet.Timestamp)
|
//log.Printf("[AVC] %v, len: %d, pts: %d ts: %10d", h264.Types(packet.Payload), len(packet.Payload), pkt.PTS, packet.Timestamp)
|
||||||
}
|
}
|
||||||
|
@@ -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},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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:])
|
||||||
}
|
//}
|
||||||
|
@@ -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)
|
||||||
|
}
|
||||||
|
118
pkg/mpegts/ts.go
118
pkg/mpegts/ts.go
@@ -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()
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if codec.IsRTP() {
|
if track.Codec.IsRTP() {
|
||||||
wrapper := aac.RTPDepay(track)
|
handler.Handler = aac.RTPDepay(handler.Handler)
|
||||||
push = wrapper(push)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return track.Bind(push)
|
default:
|
||||||
|
panic("unsupported codec")
|
||||||
}
|
}
|
||||||
|
|
||||||
panic("unsupported codec")
|
handler.HandleRTP(track)
|
||||||
|
c.senders = append(c.senders, handler)
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Consumer) MimeCodecs() string {
|
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)
|
||||||
|
}
|
||||||
|
@@ -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
|
||||||
|
|
||||||
|
@@ -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
41
pkg/rtmp/producer.go
Normal 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)
|
||||||
|
}
|
@@ -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)
|
|
||||||
}
|
|
@@ -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
274
pkg/rtsp/conn.go
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -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 {
|
||||||
switch c.mode {
|
core.Assert(c.Medias != nil)
|
||||||
// send our track to RTSP consumer (ex. FFmpeg)
|
return c.Medias
|
||||||
case ModeServerConsumer:
|
}
|
||||||
i := len(c.tracks)
|
|
||||||
channelID := byte(i << 1)
|
|
||||||
|
|
||||||
codec := track.Codec.Clone()
|
func (c *Conn) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) (err error) {
|
||||||
codec.PayloadType = uint8(96 + i)
|
core.Assert(media.Direction == core.DirectionSendonly)
|
||||||
|
|
||||||
if media.MatchAll() {
|
for _, sender := range c.senders {
|
||||||
// fill consumer medias list
|
if sender.Codec == codec {
|
||||||
c.Medias = append(c.Medias, &streamer.Media{
|
sender.HandleRTP(track)
|
||||||
Kind: media.Kind, Direction: media.Direction,
|
return
|
||||||
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)
|
|
||||||
track.Codec = codec
|
|
||||||
c.tracks = append(c.tracks, track)
|
|
||||||
|
|
||||||
return track
|
|
||||||
|
|
||||||
// camera with backchannel support
|
|
||||||
case ModeClientProducer:
|
|
||||||
consCodec := media.MatchCodec(track.Codec)
|
|
||||||
consTrack := c.GetTrack(media, consCodec)
|
|
||||||
if consTrack == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return track.Bind(func(packet *rtp.Packet) error {
|
|
||||||
return consTrack.WriteRTP(packet)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
println("WARNING: rtsp: AddTrack to wrong mode")
|
var channel byte
|
||||||
|
|
||||||
|
switch c.mode {
|
||||||
|
case core.ModeActiveProducer: // backchannel
|
||||||
|
if channel, err = c.SetupMedia(media, true); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
case core.ModePassiveConsumer:
|
||||||
|
channel = byte(len(c.senders)) * 2
|
||||||
|
|
||||||
|
// for consumer is better to use original track codec
|
||||||
|
codec = track.Codec.Clone()
|
||||||
|
// generate new payload type, starting from 96
|
||||||
|
codec.PayloadType = byte(96 + len(c.senders))
|
||||||
|
|
||||||
|
default:
|
||||||
|
panic(core.Caller())
|
||||||
|
}
|
||||||
|
|
||||||
|
// save original codec to sender (can have Codec.Name = ANY)
|
||||||
|
sender := core.NewSender(media, codec)
|
||||||
|
sender.Handler = c.packetWriter(codec, channel)
|
||||||
|
sender.HandleRTP(track)
|
||||||
|
|
||||||
|
c.senders = append(c.senders, sender)
|
||||||
return nil
|
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
|
||||||
}
|
}
|
||||||
|
@@ -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
|
||||||
}
|
}
|
||||||
|
@@ -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)
|
||||||
}
|
}
|
||||||
|
@@ -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)
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
}
|
}
|
||||||
|
@@ -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{}{}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -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
|
|
||||||
}
|
|
@@ -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
|
|
||||||
}
|
|
@@ -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
|
|
||||||
}
|
|
@@ -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
|
|
||||||
}
|
|
@@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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
|
}
|
||||||
|
|
||||||
|
w := mpegts.NewWriter()
|
||||||
|
w.AddPES(68, mpegts.StreamTypePCMATapo)
|
||||||
|
w.WritePAT()
|
||||||
|
w.WritePMT()
|
||||||
|
|
||||||
|
c.sender = core.NewSender(media, track.Codec)
|
||||||
|
c.sender.Handler = func(packet *rtp.Packet) {
|
||||||
|
// don't know why 68 and 192
|
||||||
|
w.WritePES(68, 192, packet.Payload)
|
||||||
|
_ = c.WriteBackchannel(w.Bytes())
|
||||||
|
w.Reset()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return track.Bind(func(packet *rtp.Packet) error {
|
c.sender.HandleRTP(track)
|
||||||
return consTrack.WriteRTP(packet)
|
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
|
||||||
}
|
}
|
||||||
|
@@ -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 {
|
if err := c.SetupStream(); err != nil {
|
||||||
c.tracks = map[byte]*streamer.Track{}
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if media.Direction == streamer.DirectionSendonly {
|
track := core.NewReceiver(media, codec)
|
||||||
var payloadType byte
|
switch media.Kind {
|
||||||
if media.Kind == streamer.KindVideo {
|
case core.KindVideo:
|
||||||
payloadType = mpegts.StreamTypeH264
|
track.ID = mpegts.StreamTypeH264
|
||||||
} else {
|
case core.KindAudio:
|
||||||
payloadType = mpegts.StreamTypePCMATapo
|
track.ID = mpegts.StreamTypePCMATapo
|
||||||
}
|
|
||||||
|
|
||||||
if err := c.SetupStream(); err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
track = streamer.NewTrack(media, codec)
|
|
||||||
c.tracks[payloadType] = track
|
|
||||||
} else {
|
|
||||||
if err := c.SetupBackchannel(); err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if w := c.backchannelWriter(); w != nil {
|
|
||||||
track = streamer.NewTrack(media, codec)
|
|
||||||
track.Bind(w)
|
|
||||||
c.tracks[0] = track
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
c.receivers = append(c.receivers, track)
|
||||||
return
|
return track, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
||||||
|
}
|
||||||
|
@@ -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()
|
|
||||||
}
|
|
@@ -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, "/")
|
||||||
|
@@ -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
|
||||||
}
|
}
|
||||||
|
@@ -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)
|
||||||
|
@@ -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,19 +9,20 @@ 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
|
||||||
remote string
|
remote 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 {
|
||||||
|
continue
|
||||||
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:
|
// search Media for this MID
|
||||||
// Situation:
|
for _, media := range c.medias {
|
||||||
// - go2rtc (active producer) connects to remote server (ex. webtorrent) for receiving AV
|
if media.ID != tr.Mid() || media.Direction != core.DirectionRecvonly {
|
||||||
// - remote server sends remote tracks, this tracks marked as remote "sendonly"
|
|
||||||
for _, track := range c.tracks {
|
|
||||||
if track.Direction == streamer.DirectionSendonly && track.Codec.PayloadType == payloadType {
|
|
||||||
return track
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
case streamer.ModePassiveProducer:
|
|
||||||
// Situation:
|
|
||||||
// - OBS Studio (passive producer) connects to go2rtc for send AV
|
|
||||||
// - OBS sends remote tracks, this tracks marked as remote "sendonly"
|
|
||||||
for i, media := range c.medias {
|
|
||||||
// check only tracks with same kind
|
|
||||||
if media.Kind != remote.Kind().String() {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// check only incoming tracks (remote media "sendonly")
|
|
||||||
if media.Direction != streamer.DirectionSendonly {
|
|
||||||
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:
|
|
||||||
panic("not implemented")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
// fix moment when core.ModePassiveProducer or core.ModeActiveProducer
|
||||||
|
// sends new codec with new payload type to same media
|
||||||
|
// check GetTrack
|
||||||
|
panic(core.Caller())
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
@@ -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)
|
||||||
}
|
}
|
||||||
|
@@ -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
Reference in New Issue
Block a user