mirror of
https://github.com/AlexxIT/go2rtc.git
synced 2025-10-04 07:56:33 +08:00
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 |
19
.github/workflows/release.yml
vendored
19
.github/workflows/release.yml
vendored
@@ -15,13 +15,19 @@ jobs:
|
||||
- name: Generate changelog
|
||||
run: |
|
||||
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
|
||||
run: |
|
||||
#!/bin/bash
|
||||
|
||||
esport CGO_ENABLED=0
|
||||
export CGO_ENABLED=0
|
||||
|
||||
mkdir artifacts
|
||||
mkdir -p artifacts
|
||||
|
||||
export GOOS=windows
|
||||
export GOARCH=amd64
|
||||
export FILENAME=artifacts/go2rtc_win64.zip
|
||||
@@ -65,13 +71,14 @@ jobs:
|
||||
|
||||
export GOOS=darwin
|
||||
export GOARCH=amd64
|
||||
export FILENAME=artifacts/go2rtc_mac_amd64.zip
|
||||
go build -ldflags "-s -w" -trimpath && 7z a -mx9 -sdel "$FILENAME" go2rtc
|
||||
go build -ldflags "-s -w" -trimpath -o go2rtc.amd64
|
||||
|
||||
export GOOS=darwin
|
||||
export GOARCH=arm64
|
||||
export FILENAME=artifacts/go2rtc_mac_arm64.zip
|
||||
go build -ldflags "-s -w" -trimpath && 7z a -mx9 -sdel "$FILENAME" go2rtc
|
||||
go build -ldflags "-s -w" -trimpath -o go2rtc.arm64
|
||||
|
||||
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_*
|
||||
- name: Setup tmate session
|
||||
|
17
README.md
17
README.md
@@ -59,6 +59,8 @@ Ultimate camera streaming application with support RTSP, WebRTC, HomeKit, FFmpeg
|
||||
* [Codecs filters](#codecs-filters)
|
||||
* [Codecs madness](#codecs-madness)
|
||||
* [Codecs negotiation](#codecs-negotiation)
|
||||
* [Projects using go2rtc](#projects-using-go2rtc)
|
||||
* [Camera experience](#cameras-experience)
|
||||
* [TIPS](#tips)
|
||||
* [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.
|
||||
|
||||
## 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
|
||||
|
||||
**Using apps for low RTSP delay**
|
||||
|
@@ -14,7 +14,7 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
var Version = "1.1.1"
|
||||
var Version = "1.1.2"
|
||||
var UserAgent = "go2rtc/" + Version
|
||||
|
||||
var ConfigPath string
|
||||
|
@@ -112,6 +112,8 @@ func rtspHandler(url string) (streamer.Producer, error) {
|
||||
log.Trace().Msgf("[rtsp] client request:\n%s", msg)
|
||||
case *tcp.Response:
|
||||
log.Trace().Msgf("[rtsp] client response:\n%s", msg)
|
||||
case string:
|
||||
log.Trace().Msgf("[rtsp] client msg: %s", msg)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@@ -3,6 +3,7 @@ package h264
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"strings"
|
||||
@@ -48,21 +49,39 @@ func Join(ps, iframe []byte) []byte {
|
||||
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 {
|
||||
if fmtp == "" {
|
||||
return ""
|
||||
}
|
||||
// avc1.640029 - H.264 high 4.1 (Chromecast 1st and 2nd Gen)
|
||||
profile := byte(0x64)
|
||||
capab := byte(0)
|
||||
level := byte(0x29)
|
||||
|
||||
// some cameras has wrong profile-level-id
|
||||
// https://github.com/AlexxIT/go2rtc/issues/155
|
||||
if s := streamer.Between(fmtp, "sprop-parameter-sets=", ","); s != "" {
|
||||
sps, _ := base64.StdEncoding.DecodeString(s)
|
||||
if len(sps) >= 4 {
|
||||
return fmt.Sprintf("%06X", sps[1:4])
|
||||
if fmtp != "" {
|
||||
var conf []byte
|
||||
// some cameras has wrong profile-level-id
|
||||
// https://github.com/AlexxIT/go2rtc/issues/155
|
||||
if s := streamer.Between(fmtp, "sprop-parameter-sets=", ","); s != "" {
|
||||
if 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) {
|
||||
|
@@ -10,6 +10,8 @@ import (
|
||||
|
||||
const RTPPacketVersionAVC = 0
|
||||
|
||||
const PSMaxSize = 128 // the biggest SPS I've seen is 48 (EZVIZ CS-CV210)
|
||||
|
||||
func RTPDepay(track *streamer.Track) streamer.WrapperFunc {
|
||||
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
|
||||
// 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) {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,7 +76,10 @@ func RTPDepay(track *streamer.Track) streamer.WrapperFunc {
|
||||
if len(buf) > 0 {
|
||||
payload = append(buf, payload...)
|
||||
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
|
||||
// https://github.com/AlexxIT/WebRTC/issues/391
|
||||
// https://github.com/AlexxIT/WebRTC/issues/392
|
||||
|
@@ -7,14 +7,16 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
NALUTypePFrame = 1
|
||||
NALUTypeIFrame = 19
|
||||
NALUTypeIFrame2 = 20
|
||||
NALUTypeIFrame3 = 21
|
||||
NALUTypeVPS = 32
|
||||
NALUTypeSPS = 33
|
||||
NALUTypePPS = 34
|
||||
NALUTypeFU = 49
|
||||
NALUTypePFrame = 1
|
||||
NALUTypeIFrame = 19
|
||||
NALUTypeIFrame2 = 20
|
||||
NALUTypeIFrame3 = 21
|
||||
NALUTypeVPS = 32
|
||||
NALUTypeSPS = 33
|
||||
NALUTypePPS = 34
|
||||
NALUTypePrefixSEI = 39
|
||||
NALUTypeSuffixSEI = 40
|
||||
NALUTypeFU = 49
|
||||
)
|
||||
|
||||
func NALUType(b []byte) byte {
|
||||
|
@@ -20,6 +20,16 @@ func RTPDepay(track *streamer.Track) streamer.WrapperFunc {
|
||||
nuType := (data[0] >> 1) & 0x3F
|
||||
//log.Printf("[RTP] codec: %s, nalu: %2d, size: %6d, ts: %10d, pt: %2d, ssrc: %d, seq: %d, %v", track.Codec.Name, nuType, len(packet.Payload), packet.Timestamp, packet.PayloadType, packet.SSRC, packet.SequenceNumber, packet.Marker)
|
||||
|
||||
// Fix for RtspServer https://github.com/AlexxIT/go2rtc/issues/244
|
||||
if packet.Marker && len(data) < h264.PSMaxSize {
|
||||
switch nuType {
|
||||
case NALUTypeVPS, NALUTypeSPS, NALUTypePPS:
|
||||
packet.Marker = false
|
||||
case NALUTypePrefixSEI, NALUTypeSuffixSEI:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if nuType == NALUTypeFU {
|
||||
switch data[2] >> 6 {
|
||||
case 2: // begin
|
||||
|
@@ -122,7 +122,11 @@ func (c *Client) startJPEG() 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)
|
||||
tp := textproto.NewReader(r)
|
||||
|
@@ -43,9 +43,18 @@ func RTPDepay(track *streamer.Track) streamer.WrapperFunc {
|
||||
w := uint16(packet.Payload[6]) << 3
|
||||
h := uint16(packet.Payload[7]) << 3
|
||||
|
||||
// fix 2560x1920 and 2560x1440
|
||||
if w == 512 && (h == 1920 || h == 1440) {
|
||||
// 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)
|
||||
@@ -81,6 +90,10 @@ func RTPPay() streamer.WrapperFunc {
|
||||
}
|
||||
}
|
||||
|
||||
func cutSize(size uint16) uint16 {
|
||||
return ((size >> 3) & 0xFF) << 3
|
||||
}
|
||||
|
||||
//func RTPPay() streamer.WrapperFunc {
|
||||
// const packetSize = 1436
|
||||
//
|
||||
|
@@ -304,6 +304,12 @@ func (c *Conn) Describe() error {
|
||||
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)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -743,6 +749,9 @@ func (c *Conn) Handle() (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
var channelID byte
|
||||
var size uint16
|
||||
|
||||
if buf4[0] != '$' {
|
||||
switch string(buf4) {
|
||||
case "RTSP":
|
||||
@@ -751,26 +760,62 @@ func (c *Conn) Handle() (err error) {
|
||||
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:
|
||||
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
|
||||
channelID := buf4[1]
|
||||
// get data size
|
||||
size = binary.BigEndian.Uint16(buf4[2:])
|
||||
|
||||
// get data size
|
||||
size := int(binary.BigEndian.Uint16(buf4[2:]))
|
||||
|
||||
if _, err = c.reader.Discard(4); err != nil {
|
||||
return
|
||||
// skip 4 bytes from c.reader.Peek
|
||||
if _, err = c.reader.Discard(4); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// init memory for data
|
||||
@@ -779,7 +824,7 @@ func (c *Conn) Handle() (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
c.receive += size
|
||||
c.receive += int(size)
|
||||
|
||||
if channelID&1 == 0 {
|
||||
packet := &rtp.Packet{}
|
||||
@@ -790,10 +835,8 @@ func (c *Conn) Handle() (err error) {
|
||||
track := c.channels[channelID]
|
||||
if track != nil {
|
||||
_ = track.WriteRTP(packet)
|
||||
//return fmt.Errorf("wrong channelID: %d", channelID)
|
||||
} else {
|
||||
continue // TODO: maybe fix this
|
||||
//panic("wrong channelID")
|
||||
//c.Fire("wrong channelID: " + strconv.Itoa(int(channelID)))
|
||||
}
|
||||
} else {
|
||||
msg := &RTCP{Channel: channelID}
|
||||
|
@@ -4,7 +4,10 @@ import (
|
||||
"bytes"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"github.com/pion/rtcp"
|
||||
"github.com/pion/sdp/v3"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -20,22 +23,43 @@ s=-
|
||||
t=0 0`
|
||||
|
||||
func UnmarshalSDP(rawSDP []byte) ([]*streamer.Media, error) {
|
||||
medias, err := streamer.UnmarshalSDP(rawSDP)
|
||||
if err != nil {
|
||||
// fix bug from Reolink Doorbell
|
||||
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
|
||||
i := bytes.Index(rawSDP, []byte("\nm="))
|
||||
if i > 0 {
|
||||
if i := bytes.Index(rawSDP, []byte("\nm=")); i > 0 {
|
||||
rawSDP = append([]byte(sdpHeader), rawSDP[i:]...)
|
||||
medias, err = streamer.UnmarshalSDP(rawSDP)
|
||||
sd = &sdp.SessionDescription{}
|
||||
err = sd.Unmarshal(rawSDP)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// fix bug in ONVIF spec
|
||||
// https://www.onvif.org/specs/stream/ONVIF-Streaming-Spec-v241.pdf
|
||||
medias := streamer.UnmarshalMedias(sd.MediaDescriptions)
|
||||
|
||||
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 {
|
||||
case streamer.DirectionRecvonly, "":
|
||||
media.Direction = streamer.DirectionSendonly
|
||||
@@ -47,6 +71,17 @@ func UnmarshalSDP(rawSDP []byte) ([]*streamer.Media, error) {
|
||||
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:
|
||||
// 1. Content-Base: rtsp://::ffff:192.168.1.123/onvif/profile.1/
|
||||
// 2. Content-Base: rtsp://rtsp://turret2-cam.lan:554/stream1/
|
||||
|
@@ -18,3 +18,91 @@ func TestURLParse(t *testing.T) {
|
||||
assert.Empty(t, err)
|
||||
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)
|
||||
}
|
||||
|
||||
func UnmarshalSDP(rawSDP []byte) ([]*Media, error) {
|
||||
sd := &sdp.SessionDescription{}
|
||||
if err := sd.Unmarshal(rawSDP); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var medias []*Media
|
||||
for _, md := range sd.MediaDescriptions {
|
||||
func UnmarshalMedias(descriptions []*sdp.MediaDescription) (medias []*Media) {
|
||||
for _, md := range descriptions {
|
||||
media := UnmarshalMedia(md)
|
||||
|
||||
if media.Direction == DirectionSendRecv {
|
||||
@@ -187,7 +181,7 @@ func UnmarshalSDP(rawSDP []byte) ([]*Media, error) {
|
||||
medias = append(medias, media)
|
||||
}
|
||||
|
||||
return medias, nil
|
||||
return
|
||||
}
|
||||
|
||||
func MarshalSDP(name string, medias []*Media) ([]byte, error) {
|
||||
|
@@ -70,7 +70,9 @@ func (a *Auth) Write(req *Request) {
|
||||
case AuthBasic:
|
||||
req.Header.Set("Authorization", a.header)
|
||||
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)
|
||||
response := HexMD5(a.h1nonce, h2)
|
||||
header := a.header + fmt.Sprintf(
|
||||
|
@@ -66,13 +66,7 @@ func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *strea
|
||||
c.mimeType += ","
|
||||
}
|
||||
|
||||
// TODO: fixme
|
||||
// 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.mimeType += "avc1." + h264.GetProfileLevelID(codec.FmtpLine)
|
||||
|
||||
c.streams = append(c.streams, stream)
|
||||
|
||||
|
@@ -2,6 +2,7 @@ package webrtc
|
||||
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"github.com/pion/sdp/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 {
|
||||
return
|
||||
}
|
||||
|
||||
rawSDP := []byte(c.Conn.RemoteDescription().SDP)
|
||||
medias, err := streamer.UnmarshalSDP(rawSDP)
|
||||
if err != nil {
|
||||
sd := &sdp.SessionDescription{}
|
||||
if err = sd.Unmarshal(rawSDP); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
medias := streamer.UnmarshalMedias(sd.MediaDescriptions)
|
||||
|
||||
// sort medias, so video will always be before audio
|
||||
// and ignore application media from Hass default lovelace card
|
||||
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
|
||||
|
||||
- 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://googlechrome.github.io/samples/media/sourcebuffer-changetype.html
|
||||
- https://chromestatus.com/feature/5100845653819392
|
||||
- https://developer.apple.com/documentation/webkit/delivering_video_content_for_safari
|
||||
|
@@ -11,6 +11,7 @@
|
||||
* - MediaSource for Safari iOS all
|
||||
* - Customized built-in elements (extends HTMLVideoElement) because all Safari
|
||||
* - Public class fields because old Safari (before 14.0)
|
||||
* - Autoplay for Safari
|
||||
*/
|
||||
export class VideoRTC extends HTMLElement {
|
||||
constructor() {
|
||||
@@ -60,7 +61,10 @@ export class VideoRTC extends HTMLElement {
|
||||
* [config] WebRTC configuration
|
||||
* @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
|
||||
@@ -189,8 +193,8 @@ export class VideoRTC extends HTMLElement {
|
||||
const seek = this.video.seekable;
|
||||
if (seek.length > 0) {
|
||||
this.video.currentTime = seek.end(seek.length - 1);
|
||||
this.play();
|
||||
}
|
||||
this.play();
|
||||
} else {
|
||||
this.oninit();
|
||||
}
|
||||
@@ -558,6 +562,7 @@ export class VideoRTC extends HTMLElement {
|
||||
/** @type {HTMLVideoElement} */
|
||||
const video2 = document.createElement("video");
|
||||
video2.autoplay = true;
|
||||
video2.playsInline = true;
|
||||
video2.muted = true;
|
||||
|
||||
video2.addEventListener("loadeddata", ev => {
|
||||
|
Reference in New Issue
Block a user