Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f60b55b6fa | |||
| c42413866d | |||
| b137eb66d0 | |||
| 6a40039645 | |||
| 2e4b28d871 | |||
| 58146b7e7e | |||
| 23db40220b | |||
| 557aac185d | |||
| 9ed4d4cedb | |||
| b05cbdf3d3 | |||
| 497594f53f | |||
| 73cdb39335 | |||
| a388002b12 | |||
| 6d1c0a2459 | |||
| da3137b6f0 | |||
| d21ce3d27d | |||
| 8cee4179f2 | |||
| 1153ee3652 | |||
| 3240301f27 | |||
| 2a20251dbd | |||
| 5a2d7de56b | |||
| b7391f58a5 |
@@ -15,13 +15,19 @@ jobs:
|
|||||||
- name: Generate changelog
|
- name: Generate changelog
|
||||||
run: |
|
run: |
|
||||||
echo -e "$(git log $(git describe --tags --abbrev=0)..HEAD --oneline | awk '{print "- "$0}')" > CHANGELOG.md
|
echo -e "$(git log $(git describe --tags --abbrev=0)..HEAD --oneline | awk '{print "- "$0}')" > CHANGELOG.md
|
||||||
|
- name: install lipo
|
||||||
|
run: |
|
||||||
|
curl -L -o /tmp/lipo https://github.com/konoui/lipo/releases/latest/download/lipo_Linux_amd64
|
||||||
|
chmod +x /tmp/lipo
|
||||||
|
mv /tmp/lipo /usr/local/bin
|
||||||
- name: Build Go binaries
|
- name: Build Go binaries
|
||||||
run: |
|
run: |
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
esport CGO_ENABLED=0
|
export CGO_ENABLED=0
|
||||||
|
|
||||||
mkdir artifacts
|
mkdir -p artifacts
|
||||||
|
|
||||||
export GOOS=windows
|
export GOOS=windows
|
||||||
export GOARCH=amd64
|
export GOARCH=amd64
|
||||||
export FILENAME=artifacts/go2rtc_win64.zip
|
export FILENAME=artifacts/go2rtc_win64.zip
|
||||||
@@ -65,13 +71,14 @@ jobs:
|
|||||||
|
|
||||||
export GOOS=darwin
|
export GOOS=darwin
|
||||||
export GOARCH=amd64
|
export GOARCH=amd64
|
||||||
export FILENAME=artifacts/go2rtc_mac_amd64.zip
|
go build -ldflags "-s -w" -trimpath -o go2rtc.amd64
|
||||||
go build -ldflags "-s -w" -trimpath && 7z a -mx9 -sdel "$FILENAME" go2rtc
|
|
||||||
|
|
||||||
export GOOS=darwin
|
export GOOS=darwin
|
||||||
export GOARCH=arm64
|
export GOARCH=arm64
|
||||||
export FILENAME=artifacts/go2rtc_mac_arm64.zip
|
go build -ldflags "-s -w" -trimpath -o go2rtc.arm64
|
||||||
go build -ldflags "-s -w" -trimpath && 7z a -mx9 -sdel "$FILENAME" go2rtc
|
|
||||||
|
export FILENAME=artifacts/go2rtc_mac_universal.zip
|
||||||
|
lipo -output go2rtc -create go2rtc.arm64 go2rtc.amd64 && 7z a -mx9 -sdel "$FILENAME" go2rtc
|
||||||
|
|
||||||
parallel --jobs $(nproc) "upx {}" ::: artifacts/go2rtc_linux_*
|
parallel --jobs $(nproc) "upx {}" ::: artifacts/go2rtc_linux_*
|
||||||
- name: Setup tmate session
|
- name: Setup tmate session
|
||||||
|
|||||||
@@ -59,6 +59,8 @@ Ultimate camera streaming application with support RTSP, WebRTC, HomeKit, FFmpeg
|
|||||||
* [Codecs filters](#codecs-filters)
|
* [Codecs filters](#codecs-filters)
|
||||||
* [Codecs madness](#codecs-madness)
|
* [Codecs madness](#codecs-madness)
|
||||||
* [Codecs negotiation](#codecs-negotiation)
|
* [Codecs negotiation](#codecs-negotiation)
|
||||||
|
* [Projects using go2rtc](#projects-using-go2rtc)
|
||||||
|
* [Camera experience](#cameras-experience)
|
||||||
* [TIPS](#tips)
|
* [TIPS](#tips)
|
||||||
* [FAQ](#faq)
|
* [FAQ](#faq)
|
||||||
|
|
||||||
@@ -761,6 +763,21 @@ streams:
|
|||||||
|
|
||||||
**PS.** You can select `PCMU` or `PCMA` codec in camera setting and don't use transcoding at all. Or you can select `AAC` codec for main stream and `PCMU` codec for second stream and add both RTSP to YAML config, this also will work fine.
|
**PS.** You can select `PCMU` or `PCMA` codec in camera setting and don't use transcoding at all. Or you can select `AAC` codec for main stream and `PCMU` codec for second stream and add both RTSP to YAML config, this also will work fine.
|
||||||
|
|
||||||
|
## Projects using go2rtc
|
||||||
|
|
||||||
|
- [Frigate 12+](https://frigate.video/) - open source NVR built around real-time AI object detection
|
||||||
|
- [ring-mqtt](https://github.com/tsightler/ring-mqtt) - Ring devices to MQTT Bridge
|
||||||
|
- [EufyP2PStream](https://github.com/oischinger/eufyp2pstream) - A small project that provides a Video/Audio Stream from Eufy cameras that don't directly support RTSP
|
||||||
|
|
||||||
|
## Cameras experience
|
||||||
|
|
||||||
|
- [Dahua](https://www.dahuasecurity.com/) - reference implementation streaming protocols, a lot of settings, high stream quality, multiple streaming clients
|
||||||
|
- [Hikvision](https://www.hikvision.com/) - a lot of proprietary streaming technologies
|
||||||
|
- [Reolink](https://reolink.com/) - some models has awful unusable RTSP realisation and not best HTTP-FLV alternative (I recommend that you contact Reolink support for new firmware), few settings
|
||||||
|
- [Sonoff](https://sonoff.tech/) - very low stream quality, no settings, not best protocol implementation
|
||||||
|
- [TP-Link](https://www.tp-link.com/) - few streaming clients, packet loss?
|
||||||
|
- Chinese cheap noname cameras, Wyze Cams, Xiaomi cameras with hacks (usual has `/live/ch00_1` in RTSP URL) - awful but usable RTSP protocol realisation, low stream quality, few settings, packet loss?
|
||||||
|
|
||||||
## TIPS
|
## TIPS
|
||||||
|
|
||||||
**Using apps for low RTSP delay**
|
**Using apps for low RTSP delay**
|
||||||
|
|||||||
+1
-1
@@ -14,7 +14,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
var Version = "1.1.1"
|
var Version = "1.1.2"
|
||||||
var UserAgent = "go2rtc/" + Version
|
var UserAgent = "go2rtc/" + Version
|
||||||
|
|
||||||
var ConfigPath string
|
var ConfigPath string
|
||||||
|
|||||||
@@ -112,6 +112,8 @@ func rtspHandler(url string) (streamer.Producer, error) {
|
|||||||
log.Trace().Msgf("[rtsp] client request:\n%s", msg)
|
log.Trace().Msgf("[rtsp] client request:\n%s", msg)
|
||||||
case *tcp.Response:
|
case *tcp.Response:
|
||||||
log.Trace().Msgf("[rtsp] client response:\n%s", msg)
|
log.Trace().Msgf("[rtsp] client response:\n%s", msg)
|
||||||
|
case string:
|
||||||
|
log.Trace().Msgf("[rtsp] client msg: %s", msg)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
+29
-10
@@ -3,6 +3,7 @@ package h264
|
|||||||
import (
|
import (
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -48,21 +49,39 @@ func Join(ps, iframe []byte) []byte {
|
|||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetProfileLevelID - get profile from fmtp line
|
||||||
|
// Some devices won't play video with high level, so limit max profile and max level.
|
||||||
|
// And return some profile even if fmtp line is empty.
|
||||||
func GetProfileLevelID(fmtp string) string {
|
func GetProfileLevelID(fmtp string) string {
|
||||||
if fmtp == "" {
|
// avc1.640029 - H.264 high 4.1 (Chromecast 1st and 2nd Gen)
|
||||||
return ""
|
profile := byte(0x64)
|
||||||
}
|
capab := byte(0)
|
||||||
|
level := byte(0x29)
|
||||||
|
|
||||||
// some cameras has wrong profile-level-id
|
if fmtp != "" {
|
||||||
// https://github.com/AlexxIT/go2rtc/issues/155
|
var conf []byte
|
||||||
if s := streamer.Between(fmtp, "sprop-parameter-sets=", ","); s != "" {
|
// some cameras has wrong profile-level-id
|
||||||
sps, _ := base64.StdEncoding.DecodeString(s)
|
// https://github.com/AlexxIT/go2rtc/issues/155
|
||||||
if len(sps) >= 4 {
|
if s := streamer.Between(fmtp, "sprop-parameter-sets=", ","); s != "" {
|
||||||
return fmt.Sprintf("%06X", sps[1:4])
|
if sps, _ := base64.StdEncoding.DecodeString(s); len(sps) >= 4 {
|
||||||
|
conf = sps[1:4]
|
||||||
|
}
|
||||||
|
} else if s = streamer.Between(fmtp, "profile-level-id=", ";"); s != "" {
|
||||||
|
conf, _ = hex.DecodeString(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
if conf != nil {
|
||||||
|
if conf[0] < profile {
|
||||||
|
profile = conf[0]
|
||||||
|
capab = conf[1]
|
||||||
|
}
|
||||||
|
if conf[2] < level {
|
||||||
|
level = conf[2]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return streamer.Between(fmtp, "profile-level-id=", ";")
|
return fmt.Sprintf("%02X%02X%02X", profile, capab, level)
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetParameterSet(fmtp string) (sps, pps []byte) {
|
func GetParameterSet(fmtp string) (sps, pps []byte) {
|
||||||
|
|||||||
+11
-2
@@ -10,6 +10,8 @@ import (
|
|||||||
|
|
||||||
const RTPPacketVersionAVC = 0
|
const RTPPacketVersionAVC = 0
|
||||||
|
|
||||||
|
const PSMaxSize = 128 // the biggest SPS I've seen is 48 (EZVIZ CS-CV210)
|
||||||
|
|
||||||
func RTPDepay(track *streamer.Track) streamer.WrapperFunc {
|
func RTPDepay(track *streamer.Track) streamer.WrapperFunc {
|
||||||
depack := &codecs.H264Packet{IsAVC: true}
|
depack := &codecs.H264Packet{IsAVC: true}
|
||||||
|
|
||||||
@@ -29,11 +31,15 @@ func RTPDepay(track *streamer.Track) streamer.WrapperFunc {
|
|||||||
|
|
||||||
// Fix TP-Link Tapo TC70: sends SPS and PPS with packet.Marker = true
|
// Fix TP-Link Tapo TC70: sends SPS and PPS with packet.Marker = true
|
||||||
// Reolink Duo 2: sends SPS with Marker and PPS without
|
// Reolink Duo 2: sends SPS with Marker and PPS without
|
||||||
if packet.Marker && len(payload) < 128 {
|
if packet.Marker && len(payload) < PSMaxSize {
|
||||||
switch NALUType(payload) {
|
switch NALUType(payload) {
|
||||||
case NALUTypeSPS, NALUTypePPS:
|
case NALUTypeSPS, NALUTypePPS:
|
||||||
buf = append(buf, payload...)
|
buf = append(buf, payload...)
|
||||||
return nil
|
return nil
|
||||||
|
case NALUTypeSEI:
|
||||||
|
// RtspServer https://github.com/AlexxIT/go2rtc/issues/244
|
||||||
|
// sends, marked SPS, marked PPS, marked SEI, marked IFrame
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,7 +76,10 @@ func RTPDepay(track *streamer.Track) streamer.WrapperFunc {
|
|||||||
if len(buf) > 0 {
|
if len(buf) > 0 {
|
||||||
payload = append(buf, payload...)
|
payload = append(buf, payload...)
|
||||||
buf = buf[:0]
|
buf = buf[:0]
|
||||||
} else {
|
}
|
||||||
|
|
||||||
|
// 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
|
// 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/391
|
||||||
// https://github.com/AlexxIT/WebRTC/issues/392
|
// https://github.com/AlexxIT/WebRTC/issues/392
|
||||||
|
|||||||
+10
-8
@@ -7,14 +7,16 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
NALUTypePFrame = 1
|
NALUTypePFrame = 1
|
||||||
NALUTypeIFrame = 19
|
NALUTypeIFrame = 19
|
||||||
NALUTypeIFrame2 = 20
|
NALUTypeIFrame2 = 20
|
||||||
NALUTypeIFrame3 = 21
|
NALUTypeIFrame3 = 21
|
||||||
NALUTypeVPS = 32
|
NALUTypeVPS = 32
|
||||||
NALUTypeSPS = 33
|
NALUTypeSPS = 33
|
||||||
NALUTypePPS = 34
|
NALUTypePPS = 34
|
||||||
NALUTypeFU = 49
|
NALUTypePrefixSEI = 39
|
||||||
|
NALUTypeSuffixSEI = 40
|
||||||
|
NALUTypeFU = 49
|
||||||
)
|
)
|
||||||
|
|
||||||
func NALUType(b []byte) byte {
|
func NALUType(b []byte) byte {
|
||||||
|
|||||||
@@ -20,6 +20,16 @@ func RTPDepay(track *streamer.Track) streamer.WrapperFunc {
|
|||||||
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
|
||||||
|
if packet.Marker && len(data) < h264.PSMaxSize {
|
||||||
|
switch nuType {
|
||||||
|
case NALUTypeVPS, NALUTypeSPS, NALUTypePPS:
|
||||||
|
packet.Marker = false
|
||||||
|
case NALUTypePrefixSEI, NALUTypeSuffixSEI:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if nuType == NALUTypeFU {
|
if nuType == NALUTypeFU {
|
||||||
switch data[2] >> 6 {
|
switch data[2] >> 6 {
|
||||||
case 2: // begin
|
case 2: // begin
|
||||||
|
|||||||
+5
-1
@@ -122,7 +122,11 @@ func (c *Client) startJPEG() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) startMJPEG(boundary string) error {
|
func (c *Client) startMJPEG(boundary string) error {
|
||||||
boundary = "--" + boundary
|
// some cameras add prefix to boundary header:
|
||||||
|
// https://github.com/TheTimeWalker/wallpanel-android
|
||||||
|
if !strings.HasPrefix(boundary, "--") {
|
||||||
|
boundary = "--" + boundary
|
||||||
|
}
|
||||||
|
|
||||||
r := bufio.NewReader(c.res.Body)
|
r := bufio.NewReader(c.res.Body)
|
||||||
tp := textproto.NewReader(r)
|
tp := textproto.NewReader(r)
|
||||||
|
|||||||
+15
-2
@@ -43,9 +43,18 @@ func RTPDepay(track *streamer.Track) streamer.WrapperFunc {
|
|||||||
w := uint16(packet.Payload[6]) << 3
|
w := uint16(packet.Payload[6]) << 3
|
||||||
h := uint16(packet.Payload[7]) << 3
|
h := uint16(packet.Payload[7]) << 3
|
||||||
|
|
||||||
// fix 2560x1920 and 2560x1440
|
// fix sizes more than 2040
|
||||||
if w == 512 && (h == 1920 || h == 1440) {
|
switch {
|
||||||
|
// 512x1920 512x1440
|
||||||
|
case w == cutSize(2560) && (h == 1920 || h == 1440):
|
||||||
w = 2560
|
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)
|
//fmt.Printf("t: %d, q: %d, w: %d, h: %d\n", t, q, w, h)
|
||||||
@@ -81,6 +90,10 @@ func RTPPay() streamer.WrapperFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func cutSize(size uint16) uint16 {
|
||||||
|
return ((size >> 3) & 0xFF) << 3
|
||||||
|
}
|
||||||
|
|
||||||
//func RTPPay() streamer.WrapperFunc {
|
//func RTPPay() streamer.WrapperFunc {
|
||||||
// const packetSize = 1436
|
// const packetSize = 1436
|
||||||
//
|
//
|
||||||
|
|||||||
+57
-14
@@ -304,6 +304,12 @@ func (c *Conn) Describe() error {
|
|||||||
req.Header.Set("Require", "www.onvif.org/ver20/backchannel")
|
req.Header.Set("Require", "www.onvif.org/ver20/backchannel")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if c.UserAgent != "" {
|
||||||
|
// this camera will answer with 401 on DESCRIBE without User-Agent
|
||||||
|
// https://github.com/AlexxIT/go2rtc/issues/235
|
||||||
|
req.Header.Set("User-Agent", c.UserAgent)
|
||||||
|
}
|
||||||
|
|
||||||
res, err := c.Do(req)
|
res, err := c.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -743,6 +749,9 @@ func (c *Conn) Handle() (err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var channelID byte
|
||||||
|
var size uint16
|
||||||
|
|
||||||
if buf4[0] != '$' {
|
if buf4[0] != '$' {
|
||||||
switch string(buf4) {
|
switch string(buf4) {
|
||||||
case "RTSP":
|
case "RTSP":
|
||||||
@@ -751,26 +760,62 @@ func (c *Conn) Handle() (err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.Fire(res)
|
c.Fire(res)
|
||||||
|
continue
|
||||||
|
|
||||||
case "OPTI", "TEAR", "DESC", "SETU", "PLAY", "PAUS", "RECO", "ANNO", "GET_", "SET_":
|
case "OPTI", "TEAR", "DESC", "SETU", "PLAY", "PAUS", "RECO", "ANNO", "GET_", "SET_":
|
||||||
var req *tcp.Request
|
var req *tcp.Request
|
||||||
if req, err = tcp.ReadRequest(c.reader); err != nil {
|
if req, err = tcp.ReadRequest(c.reader); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.Fire(req)
|
c.Fire(req)
|
||||||
|
continue
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("RTSP wrong input")
|
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")
|
||||||
}
|
}
|
||||||
continue
|
} else {
|
||||||
}
|
// hope that the odd channels are always RTCP
|
||||||
|
channelID = buf4[1]
|
||||||
|
|
||||||
// hope that the odd channels are always RTCP
|
// get data size
|
||||||
channelID := buf4[1]
|
size = binary.BigEndian.Uint16(buf4[2:])
|
||||||
|
|
||||||
// get data size
|
// skip 4 bytes from c.reader.Peek
|
||||||
size := int(binary.BigEndian.Uint16(buf4[2:]))
|
if _, err = c.reader.Discard(4); err != nil {
|
||||||
|
return
|
||||||
if _, err = c.reader.Discard(4); err != nil {
|
}
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// init memory for data
|
// init memory for data
|
||||||
@@ -779,7 +824,7 @@ func (c *Conn) Handle() (err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.receive += size
|
c.receive += int(size)
|
||||||
|
|
||||||
if channelID&1 == 0 {
|
if channelID&1 == 0 {
|
||||||
packet := &rtp.Packet{}
|
packet := &rtp.Packet{}
|
||||||
@@ -790,10 +835,8 @@ func (c *Conn) Handle() (err error) {
|
|||||||
track := c.channels[channelID]
|
track := c.channels[channelID]
|
||||||
if track != nil {
|
if track != nil {
|
||||||
_ = track.WriteRTP(packet)
|
_ = track.WriteRTP(packet)
|
||||||
//return fmt.Errorf("wrong channelID: %d", channelID)
|
|
||||||
} else {
|
} else {
|
||||||
continue // TODO: maybe fix this
|
//c.Fire("wrong channelID: " + strconv.Itoa(int(channelID)))
|
||||||
//panic("wrong channelID")
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
msg := &RTCP{Channel: channelID}
|
msg := &RTCP{Channel: channelID}
|
||||||
|
|||||||
+42
-7
@@ -4,7 +4,10 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||||
"github.com/pion/rtcp"
|
"github.com/pion/rtcp"
|
||||||
|
"github.com/pion/sdp/v3"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -20,22 +23,43 @@ s=-
|
|||||||
t=0 0`
|
t=0 0`
|
||||||
|
|
||||||
func UnmarshalSDP(rawSDP []byte) ([]*streamer.Media, error) {
|
func UnmarshalSDP(rawSDP []byte) ([]*streamer.Media, error) {
|
||||||
medias, err := streamer.UnmarshalSDP(rawSDP)
|
// fix bug from Reolink Doorbell
|
||||||
if err != nil {
|
if i := bytes.Index(rawSDP, []byte("a=sendonlym=")); i > 0 {
|
||||||
|
rawSDP = append(rawSDP[:i+11], rawSDP[i+10:]...)
|
||||||
|
rawSDP[i+10] = '\n'
|
||||||
|
}
|
||||||
|
|
||||||
|
sd := &sdp.SessionDescription{}
|
||||||
|
if err := sd.Unmarshal(rawSDP); err != nil {
|
||||||
|
// fix multiple `s=` https://github.com/AlexxIT/WebRTC/issues/417
|
||||||
|
re, _ := regexp.Compile("\ns=[^\n]+")
|
||||||
|
rawSDP = re.ReplaceAll(rawSDP, nil)
|
||||||
|
|
||||||
// fix SDP header for some cameras
|
// fix SDP header for some cameras
|
||||||
i := bytes.Index(rawSDP, []byte("\nm="))
|
if i := bytes.Index(rawSDP, []byte("\nm=")); i > 0 {
|
||||||
if i > 0 {
|
|
||||||
rawSDP = append([]byte(sdpHeader), rawSDP[i:]...)
|
rawSDP = append([]byte(sdpHeader), rawSDP[i:]...)
|
||||||
medias, err = streamer.UnmarshalSDP(rawSDP)
|
sd = &sdp.SessionDescription{}
|
||||||
|
err = sd.Unmarshal(rawSDP)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// fix bug in ONVIF spec
|
medias := streamer.UnmarshalMedias(sd.MediaDescriptions)
|
||||||
// https://www.onvif.org/specs/stream/ONVIF-Streaming-Spec-v241.pdf
|
|
||||||
for _, media := range medias {
|
for _, media := range medias {
|
||||||
|
// Check buggy SDP with fmtp for H264 on another track
|
||||||
|
// https://github.com/AlexxIT/WebRTC/issues/419
|
||||||
|
for _, codec := range media.Codecs {
|
||||||
|
if codec.Name == streamer.CodecH264 && codec.FmtpLine == "" {
|
||||||
|
codec.FmtpLine = findFmtpLine(codec.PayloadType, sd.MediaDescriptions)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// fix bug in ONVIF spec
|
||||||
|
// https://www.onvif.org/specs/stream/ONVIF-Streaming-Spec-v241.pdf
|
||||||
switch media.Direction {
|
switch media.Direction {
|
||||||
case streamer.DirectionRecvonly, "":
|
case streamer.DirectionRecvonly, "":
|
||||||
media.Direction = streamer.DirectionSendonly
|
media.Direction = streamer.DirectionSendonly
|
||||||
@@ -47,6 +71,17 @@ func UnmarshalSDP(rawSDP []byte) ([]*streamer.Media, error) {
|
|||||||
return medias, nil
|
return medias, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func findFmtpLine(payloadType uint8, descriptions []*sdp.MediaDescription) string {
|
||||||
|
s := strconv.Itoa(int(payloadType))
|
||||||
|
for _, md := range descriptions {
|
||||||
|
codec := streamer.UnmarshalCodec(md, s)
|
||||||
|
if codec.FmtpLine != "" {
|
||||||
|
return codec.FmtpLine
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
// urlParse fix bugs:
|
// urlParse fix bugs:
|
||||||
// 1. Content-Base: rtsp://::ffff:192.168.1.123/onvif/profile.1/
|
// 1. Content-Base: rtsp://::ffff:192.168.1.123/onvif/profile.1/
|
||||||
// 2. Content-Base: rtsp://rtsp://turret2-cam.lan:554/stream1/
|
// 2. Content-Base: rtsp://rtsp://turret2-cam.lan:554/stream1/
|
||||||
|
|||||||
@@ -18,3 +18,91 @@ func TestURLParse(t *testing.T) {
|
|||||||
assert.Empty(t, err)
|
assert.Empty(t, err)
|
||||||
assert.Equal(t, "turret2-cam.lan:554", u.Host)
|
assert.Equal(t, "turret2-cam.lan:554", u.Host)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBugSDP1(t *testing.T) {
|
||||||
|
// https://github.com/AlexxIT/WebRTC/issues/417
|
||||||
|
s := `v=0
|
||||||
|
o=- 91674849066 1 IN IP4 192.168.1.123
|
||||||
|
s=RtspServer
|
||||||
|
i=live
|
||||||
|
t=0 0
|
||||||
|
a=control:*
|
||||||
|
a=range:npt=0-
|
||||||
|
m=video 0 RTP/AVP 96
|
||||||
|
c=IN IP4 0.0.0.0
|
||||||
|
s=RtspServer
|
||||||
|
i=live
|
||||||
|
a=control:track0
|
||||||
|
a=range:npt=0-
|
||||||
|
a=rtpmap:96 H264/90000
|
||||||
|
a=fmtp:96 packetization-mode=1;profile-level-id=42001E;sprop-parameter-sets=Z0IAHvQCgC3I,aM48gA==
|
||||||
|
a=control:track0
|
||||||
|
m=audio 0 RTP/AVP 97
|
||||||
|
c=IN IP4 0.0.0.0
|
||||||
|
s=RtspServer
|
||||||
|
i=live
|
||||||
|
a=control:track1
|
||||||
|
a=range:npt=0-
|
||||||
|
a=rtpmap:97 MPEG4-GENERIC/8000/1
|
||||||
|
a=fmtp:97 profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=1588
|
||||||
|
a=control:track1
|
||||||
|
`
|
||||||
|
medias, err := UnmarshalSDP([]byte(s))
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.NotNil(t, medias)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBugSDP2(t *testing.T) {
|
||||||
|
// https://github.com/AlexxIT/WebRTC/issues/419
|
||||||
|
s := `v=0
|
||||||
|
o=- 1675628282 1675628283 IN IP4 192.168.1.123
|
||||||
|
s=streamed by the RTSP server
|
||||||
|
t=0 0
|
||||||
|
m=video 0 RTP/AVP 96
|
||||||
|
a=rtpmap:96 H264/90000
|
||||||
|
a=control:track0
|
||||||
|
m=audio 0 RTP/AVP 8
|
||||||
|
a=rtpmap:0 pcma/8000/1
|
||||||
|
a=control:track1
|
||||||
|
a=framerate:25
|
||||||
|
a=range:npt=now-
|
||||||
|
a=fmtp:96 packetization-mode=1;profile-level-id=64001F;sprop-parameter-sets=Z0IAH5WoFAFuQA==,aM48gA==
|
||||||
|
`
|
||||||
|
medias, err := UnmarshalSDP([]byte(s))
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.NotNil(t, medias)
|
||||||
|
assert.NotEqual(t, "", medias[0].Codecs[0].FmtpLine)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBugSDP3(t *testing.T) {
|
||||||
|
s := `v=0
|
||||||
|
o=- 1675775048103026 1 IN IP4 192.168.1.123
|
||||||
|
s=Session streamed by "preview"
|
||||||
|
t=0 0
|
||||||
|
a=tool:LIVE555 Streaming Media v2020.08.12
|
||||||
|
a=type:broadcast
|
||||||
|
a=control:*
|
||||||
|
a=range:npt=0-
|
||||||
|
a=x-qt-text-nam:Session streamed by "preview"
|
||||||
|
m=video 0 RTP/AVP 96
|
||||||
|
c=IN IP4 0.0.0.0
|
||||||
|
b=AS:8192
|
||||||
|
a=rtpmap:96 H264/90000
|
||||||
|
a=fmtp:96 packetization-mode=1;profile-level-id=640033;sprop-parameter-sets=Z2QAM6wVFKAoAPGQ,aO48sA==
|
||||||
|
a=recvonly
|
||||||
|
a=control:track1
|
||||||
|
m=audio 0 RTP/AVP 8
|
||||||
|
a=control:track2
|
||||||
|
a=rtpmap:8 PCMA/8000
|
||||||
|
a=sendonlym=audio 0 RTP/AVP 98
|
||||||
|
c=IN IP4 0.0.0.0
|
||||||
|
b=AS:8192
|
||||||
|
a=rtpmap:98 MPEG4-GENERIC/16000
|
||||||
|
a=fmtp:98 streamtype=5;profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=1408;
|
||||||
|
a=recvonly
|
||||||
|
a=control:track3
|
||||||
|
`
|
||||||
|
medias, err := UnmarshalSDP([]byte(s))
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Len(t, medias, 3)
|
||||||
|
}
|
||||||
|
|||||||
@@ -166,14 +166,8 @@ func (c *Codec) Match(codec *Codec) bool {
|
|||||||
(c.Channels == codec.Channels || codec.Channels == 0)
|
(c.Channels == codec.Channels || codec.Channels == 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func UnmarshalSDP(rawSDP []byte) ([]*Media, error) {
|
func UnmarshalMedias(descriptions []*sdp.MediaDescription) (medias []*Media) {
|
||||||
sd := &sdp.SessionDescription{}
|
for _, md := range descriptions {
|
||||||
if err := sd.Unmarshal(rawSDP); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var medias []*Media
|
|
||||||
for _, md := range sd.MediaDescriptions {
|
|
||||||
media := UnmarshalMedia(md)
|
media := UnmarshalMedia(md)
|
||||||
|
|
||||||
if media.Direction == DirectionSendRecv {
|
if media.Direction == DirectionSendRecv {
|
||||||
@@ -187,7 +181,7 @@ func UnmarshalSDP(rawSDP []byte) ([]*Media, error) {
|
|||||||
medias = append(medias, media)
|
medias = append(medias, media)
|
||||||
}
|
}
|
||||||
|
|
||||||
return medias, nil
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func MarshalSDP(name string, medias []*Media) ([]byte, error) {
|
func MarshalSDP(name string, medias []*Media) ([]byte, error) {
|
||||||
|
|||||||
+3
-1
@@ -70,7 +70,9 @@ func (a *Auth) Write(req *Request) {
|
|||||||
case AuthBasic:
|
case AuthBasic:
|
||||||
req.Header.Set("Authorization", a.header)
|
req.Header.Set("Authorization", a.header)
|
||||||
case AuthDigest:
|
case AuthDigest:
|
||||||
uri := req.URL.RequestURI()
|
// important to use String except RequestURL for RtspServer:
|
||||||
|
// https://github.com/AlexxIT/go2rtc/issues/244
|
||||||
|
uri := req.URL.String()
|
||||||
h2 := HexMD5(req.Method, uri)
|
h2 := HexMD5(req.Method, uri)
|
||||||
response := HexMD5(a.h1nonce, h2)
|
response := HexMD5(a.h1nonce, h2)
|
||||||
header := a.header + fmt.Sprintf(
|
header := a.header + fmt.Sprintf(
|
||||||
|
|||||||
+1
-7
@@ -66,13 +66,7 @@ func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *strea
|
|||||||
c.mimeType += ","
|
c.mimeType += ","
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: fixme
|
c.mimeType += "avc1." + h264.GetProfileLevelID(codec.FmtpLine)
|
||||||
// some devices won't play high level
|
|
||||||
if stream.RecordInfo.AVCLevelIndication <= 0x29 {
|
|
||||||
c.mimeType += "avc1." + h264.GetProfileLevelID(codec.FmtpLine)
|
|
||||||
} else {
|
|
||||||
c.mimeType += "avc1.640029"
|
|
||||||
}
|
|
||||||
|
|
||||||
c.streams = append(c.streams, stream)
|
c.streams = append(c.streams, stream)
|
||||||
|
|
||||||
|
|||||||
+6
-2
@@ -2,6 +2,7 @@ package webrtc
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||||
|
"github.com/pion/sdp/v3"
|
||||||
"github.com/pion/webrtc/v3"
|
"github.com/pion/webrtc/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -90,12 +91,15 @@ func (c *Conn) SetOffer(offer string) (err error) {
|
|||||||
if err = c.Conn.SetRemoteDescription(sdOffer); err != nil {
|
if err = c.Conn.SetRemoteDescription(sdOffer); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
rawSDP := []byte(c.Conn.RemoteDescription().SDP)
|
rawSDP := []byte(c.Conn.RemoteDescription().SDP)
|
||||||
medias, err := streamer.UnmarshalSDP(rawSDP)
|
sd := &sdp.SessionDescription{}
|
||||||
if err != nil {
|
if err = sd.Unmarshal(rawSDP); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
medias := streamer.UnmarshalMedias(sd.MediaDescriptions)
|
||||||
|
|
||||||
// sort medias, so video will always be before audio
|
// sort medias, so video will always be before audio
|
||||||
// and ignore application media from Hass default lovelace card
|
// and ignore application media from Hass default lovelace card
|
||||||
for _, media := range medias {
|
for _, media := range medias {
|
||||||
|
|||||||
@@ -48,6 +48,18 @@ pc.ontrack = ev => {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Chromecast 1
|
||||||
|
|
||||||
|
2023-02-02. Error:
|
||||||
|
|
||||||
|
```
|
||||||
|
InvalidStateError: Failed to execute 'addTransceiver' on 'RTCPeerConnection': This operation is only supported in 'unified-plan'. 'unified-plan' will become the default behavior in the future, but it is currently experimental. To try it out, construct the RTCPeerConnection with sdpSemantics:'unified-plan' present in the RTCConfiguration argument.
|
||||||
|
```
|
||||||
|
|
||||||
|
User-Agent: `Mozilla/5.0 (X11; Linux armv7l) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.47 Safari/537.36 CrKey/1.36.159268`
|
||||||
|
|
||||||
|
https://webrtc.org/getting-started/unified-plan-transition-guide?hl=en
|
||||||
|
|
||||||
## Useful links
|
## Useful links
|
||||||
|
|
||||||
- https://www.webrtc-experiment.com/DetectRTC/
|
- https://www.webrtc-experiment.com/DetectRTC/
|
||||||
@@ -58,3 +70,4 @@ pc.ontrack = ev => {
|
|||||||
- https://chromium.googlesource.com/external/w3c/web-platform-tests/+/refs/heads/master/media-source/mediasource-is-type-supported.html
|
- https://chromium.googlesource.com/external/w3c/web-platform-tests/+/refs/heads/master/media-source/mediasource-is-type-supported.html
|
||||||
- https://googlechrome.github.io/samples/media/sourcebuffer-changetype.html
|
- https://googlechrome.github.io/samples/media/sourcebuffer-changetype.html
|
||||||
- https://chromestatus.com/feature/5100845653819392
|
- https://chromestatus.com/feature/5100845653819392
|
||||||
|
- https://developer.apple.com/documentation/webkit/delivering_video_content_for_safari
|
||||||
|
|||||||
+7
-2
@@ -11,6 +11,7 @@
|
|||||||
* - MediaSource for Safari iOS all
|
* - MediaSource for Safari iOS all
|
||||||
* - Customized built-in elements (extends HTMLVideoElement) because all Safari
|
* - Customized built-in elements (extends HTMLVideoElement) because all Safari
|
||||||
* - Public class fields because old Safari (before 14.0)
|
* - Public class fields because old Safari (before 14.0)
|
||||||
|
* - Autoplay for Safari
|
||||||
*/
|
*/
|
||||||
export class VideoRTC extends HTMLElement {
|
export class VideoRTC extends HTMLElement {
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -60,7 +61,10 @@ export class VideoRTC extends HTMLElement {
|
|||||||
* [config] WebRTC configuration
|
* [config] WebRTC configuration
|
||||||
* @type {RTCConfiguration}
|
* @type {RTCConfiguration}
|
||||||
*/
|
*/
|
||||||
this.pcConfig = {iceServers: [{urls: "stun:stun.l.google.com:19302"}]};
|
this.pcConfig = {
|
||||||
|
iceServers: [{urls: 'stun:stun.l.google.com:19302'}],
|
||||||
|
sdpSemantics: 'unified-plan', // important for Chromecast 1
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* [info] WebSocket connection state. Values: CONNECTING, OPEN, CLOSED
|
* [info] WebSocket connection state. Values: CONNECTING, OPEN, CLOSED
|
||||||
@@ -189,8 +193,8 @@ export class VideoRTC extends HTMLElement {
|
|||||||
const seek = this.video.seekable;
|
const seek = this.video.seekable;
|
||||||
if (seek.length > 0) {
|
if (seek.length > 0) {
|
||||||
this.video.currentTime = seek.end(seek.length - 1);
|
this.video.currentTime = seek.end(seek.length - 1);
|
||||||
this.play();
|
|
||||||
}
|
}
|
||||||
|
this.play();
|
||||||
} else {
|
} else {
|
||||||
this.oninit();
|
this.oninit();
|
||||||
}
|
}
|
||||||
@@ -558,6 +562,7 @@ export class VideoRTC extends HTMLElement {
|
|||||||
/** @type {HTMLVideoElement} */
|
/** @type {HTMLVideoElement} */
|
||||||
const video2 = document.createElement("video");
|
const video2 = document.createElement("video");
|
||||||
video2.autoplay = true;
|
video2.autoplay = true;
|
||||||
|
video2.playsInline = true;
|
||||||
video2.muted = true;
|
video2.muted = true;
|
||||||
|
|
||||||
video2.addEventListener("loadeddata", ev => {
|
video2.addEventListener("loadeddata", ev => {
|
||||||
|
|||||||
Reference in New Issue
Block a user