Compare commits

...

26 Commits

Author SHA1 Message Date
Alexey Khit
3acea1ed5a Update version to 1.0.1 2023-01-24 22:29:15 +03:00
Alexey Khit
3fb8d9af66 Disable release autobuild 2023-01-24 22:29:04 +03:00
Alexey Khit
9bbaf41d54 Second fix for Chinese buggy cameras 2023-01-24 21:38:58 +03:00
Alexey Khit
c43530fbd3 Fix mp4f consumer 2023-01-24 21:05:51 +03:00
Alexey Khit
15777a3d94 Fix Chinese buggy cameras 2023-01-24 21:05:35 +03:00
Alexey Khit
6e61ac6d2f Fix HTTP-FLV for Reolink cameras 2023-01-24 17:48:31 +03:00
Alexey Khit
6d7d5f53d8 Update websocket disconnect log message 2023-01-24 17:48:08 +03:00
Alexey Khit
d2bca8d461 Update processing HTTP-FLV without video or audio 2023-01-24 17:47:26 +03:00
Alexey Khit
94b089d1e3 Fix bug in URL for D-Link cameras 2023-01-23 21:14:52 +03:00
Alexey Khit
b3d16c9fcc Update TOC in readme 2023-01-23 15:37:06 +03:00
Alexey Khit
f0def68482 Update readme 2023-01-20 17:45:35 +03:00
Alexey Khit
9ddbb326b4 Update version to 1.0.0 2023-01-20 17:07:43 +03:00
Alexey Khit
a2e58d928e Fix timezone in logs 2023-01-20 13:45:01 +03:00
Alexey Khit
3c48fb8bea Simplify Dockerfile 2023-01-20 11:23:28 +03:00
Alexey Khit
4b0cbb5a73 Add support basic auth for API 2023-01-20 10:54:26 +03:00
Alexey Khit
e28b49ea86 Ignore errors for RTCP packets 2023-01-20 10:26:57 +03:00
Alexey Khit
5c17d8fcb6 Add support AAC audio for HTTP-FLV 2023-01-19 21:44:15 +03:00
Alexey Khit
e040fb591f Disable CGO for git releases 2023-01-18 15:07:42 +03:00
Alexey Khit
140014f2a6 Fix info for WS/MP4 2023-01-18 15:04:06 +03:00
Alexey Khit
23f72d111e Add Teardown handler for RTSP server (untested) 2023-01-18 12:21:54 +03:00
Alexey Khit
f9d5ab9d0a Fix RTSP server SDP for some clients 2023-01-18 11:45:39 +03:00
Alexey Khit
8628c48db8 Add no-cache for all GET API requests 2023-01-18 10:01:00 +03:00
Alexey Khit
6e49d51c33 Update GET config API when config file not set 2023-01-18 10:00:20 +03:00
Alexey Khit
6a61b5234e Fix HTTP-FLV support for Reolink cameras 2023-01-18 09:36:32 +03:00
Alexey Khit
7a0091777d Fix relative config path #171 2023-01-16 11:00:04 +03:00
Alexey Khit
d23d2a7eff Fix release binaries for mac 2023-01-16 00:40:02 +03:00
25 changed files with 698 additions and 117 deletions

View File

@@ -2,9 +2,9 @@ name: release
on:
workflow_dispatch:
push:
tags:
- 'v*'
# push:
# tags:
# - 'v*'
jobs:
build-and-release:
@@ -19,6 +19,8 @@ jobs:
run: |
#!/bin/bash
esport CGO_ENABLED=0
mkdir artifacts
export GOOS=windows
export GOARCH=amd64
@@ -63,12 +65,12 @@ jobs:
export GOOS=darwin
export GOARCH=amd64
export FILENAME=go2rtc_mac_amd64.zip
export FILENAME=artifacts/go2rtc_mac_amd64.zip
go build -ldflags "-s -w" -trimpath && 7z a -mx9 -sdel "$FILENAME" go2rtc
export GOOS=darwin
export GOARCH=arm64
export FILENAME=go2rtc_mac_arm64.zip
export FILENAME=artifacts/go2rtc_mac_arm64.zip
go build -ldflags "-s -w" -trimpath && 7z a -mx9 -sdel "$FILENAME" go2rtc
parallel --jobs $(nproc) "upx {}" ::: artifacts/go2rtc_linux_*

View File

@@ -33,13 +33,12 @@ FROM scratch AS rootfs
COPY --from=build /build/go2rtc /usr/local/bin/
COPY --from=ngrok /bin/ngrok /usr/local/bin/
COPY ./build/docker/run.sh /
# 3. Final image
FROM base
# Install ffmpeg, bash (for run.sh), tini (for signal handling),
# Install ffmpeg, tini (for signal handling),
# and other common tools for the echo source.
RUN apk add --no-cache tini ffmpeg bash curl jq
@@ -55,8 +54,8 @@ RUN if [ "${TARGETARCH}" = "amd64" ]; then apk add --no-cache libva-intel-driver
COPY --from=rootfs / /
RUN chmod a+x /run.sh && mkdir -p /config
ENTRYPOINT ["/sbin/tini", "--"]
VOLUME /config
WORKDIR /config
CMD ["/run.sh"]
CMD ["go2rtc", "-config", "/config/go2rtc.yaml"]

View File

@@ -27,6 +27,40 @@ Ultimate camera streaming application with support RTSP, WebRTC, HomeKit, FFmpeg
- [MediaSoup](https://mediasoup.org/) framework routing idea
- HomeKit Accessory Protocol from [@brutella](https://github.com/brutella/hap)
---
* [Fast start](#fast-start)
* [go2rtc: Binary](#go2rtc-binary)
* [go2rtc: Home Assistant Add-on](#go2rtc-home-assistant-add-on)
* [go2rtc: Docker](#go2rtc-docker)
* [Configuration](#configuration)
* [Module: Streams](#module-streams)
* [Source: RTSP](#source-rtsp)
* [Source: RTMP](#source-rtmp)
* [Source: HTTP](#source-http)
* [Source: FFmpeg](#source-ffmpeg)
* [Source: FFmpeg Device](#source-ffmpeg-device)
* [Source: Exec](#source-exec)
* [Source: Echo](#source-echo)
* [Source: HomeKit](#source-homekit)
* [Source: Ivideon](#source-ivideon)
* [Source: Hass](#source-hass)
* [Module: API](#module-api)
* [Module: RTSP](#module-rtsp)
* [Module: WebRTC](#module-webrtc)
* [Module: Ngrok](#module-ngrok)
* [Module: Hass](#module-hass)
* [From go2rtc to Hass](#from-go2rtc-to-hass)
* [From Hass to go2rtc](#from-hass-to-go2rtc)
* [Module: MP4](#module-mp4)
* [Module: MJPEG](#module-mjpeg)
* [Module: Log](#module-log)
* [Security](#security)
* [Codecs madness](#codecs-madness)
* [Codecs negotiation](#codecs-negotiation)
* [TIPS](#tips)
* [FAQ](#faq)
## Fast start
1. Download [binary](#go2rtc-binary) or use [Docker](#go2rtc-docker) or [Home Assistant Add-on](#go2rtc-home-assistant-add-on)
@@ -36,7 +70,6 @@ Ultimate camera streaming application with support RTSP, WebRTC, HomeKit, FFmpeg
- add your [streams](#module-streams) to [config](#configuration) file
- setup [external access](#module-webrtc) to webrtc
- setup [external access](#module-ngrok) to web interface
**Developers:**
@@ -74,14 +107,14 @@ Container [alexxit/go2rtc](https://hub.docker.com/r/alexxit/go2rtc) with support
## Configuration
Create file `go2rtc.yaml`. go2rtc will search this file in current work dirrectory by default.
- by default, you need to config only your `streams` links
- by default go2rtc will search `go2rtc.yaml` in the current work dirrectory
- `api` server will start on default **1984 port** (TCP)
- `rtsp` server will start on default **8554 port** (TCP)
- `webrtc` will use port **8555** (TCP/UDP) for connections
- `ffmpeg` will use default transcoding options
Configuration options and a complete list of settings can be found in [the wiki](https://github.com/AlexxIT/go2rtc/wiki/Configuration).
Available modules:
- [streams](#module-streams)
@@ -95,8 +128,6 @@ Available modules:
- [hass](#module-hass) - Home Assistant integration
- [log](#module-log) - logs config
Full default config [example](https://github.com/AlexxIT/go2rtc/wiki/Configuration).
### Module: Streams
**go2rtc** support different stream source types. You can config one or multiple links of any type as stream source.
@@ -359,6 +390,8 @@ go2rtc has simple HTML page (`stream.html`) with support params in URL:
```yaml
api:
listen: ":1984" # default ":1984", HTTP API port ("" - disabled)
username: "admin" # default "", Basic auth for WebUI
password: "pass" # default "", Basic auth for WebUI
base_path: "/rtc" # default "", API prefix for serve on suburl (/api => /rtc/api)
static_dir: "www" # default "", folder for static files (custom web interface)
origin: "*" # default "", allow CORS requests (only * supported)
@@ -366,7 +399,7 @@ api:
**PS:**
- go2rtc doesn't provide HTTPS or password protection. Use [Nginx](https://nginx.org/) or [Ngrok](#module-ngrok) or [Home Assistant Add-on](#go2rtc-home-assistant-add-on) for this tasks
- go2rtc doesn't provide HTTPS. Use [Nginx](https://nginx.org/) or [Ngrok](#module-ngrok) or [Home Assistant Add-on](#go2rtc-home-assistant-add-on) for this tasks
- you can access microphone (for 2-way audio) only with HTTPS ([read more](https://stackoverflow.com/questions/52759992/how-to-access-camera-and-microphone-in-chrome-without-https))
- MJPEG over WebSocket plays better than native MJPEG because Chrome [bug](https://bugs.chromium.org/p/chromium/issues/detail?id=527446)
- MP4 over WebSocket was created only for Apple iOS because it doesn't support MSE and native MP4
@@ -384,9 +417,9 @@ Password protection always disabled for localhost calls (ex. FFmpeg or Hass on s
```yaml
rtsp:
listen: ":8554" # RTSP Server TCP port, default - 8554
username: admin # optional, default - disabled
password: pass # optional, default - disabled
listen: ":8554" # RTSP Server TCP port, default - 8554
username: "admin" # optional, default - disabled
password: "pass" # optional, default - disabled
```
### Module: WebRTC
@@ -399,7 +432,7 @@ WebRTC usually works without problems in the local network. But external access
```yaml
webrtc:
listen: ":8555" # address of your local server and port (TCP/UDP)
listen: ":8555" # address of your local server and port (TCP/UDP)
```
**Static public IP**

View File

@@ -8,6 +8,7 @@ import (
"net/http"
"os"
"strconv"
"strings"
"sync"
)
@@ -15,6 +16,8 @@ func Init() {
var cfg struct {
Mod struct {
Listen string `yaml:"listen"`
Username string `yaml:"username"`
Password string `yaml:"password"`
BasePath string `yaml:"base_path"`
StaticDir string `yaml:"static_dir"`
Origin string `yaml:"origin"`
@@ -52,14 +55,18 @@ func Init() {
log.Info().Str("addr", cfg.Mod.Listen).Msg("[api] listen")
s := http.Server{}
s.Handler = http.DefaultServeMux
if log.Trace().Enabled() {
s.Handler = middlewareLog(s.Handler)
}
s.Handler = http.DefaultServeMux // 4th
if cfg.Mod.Origin == "*" {
s.Handler = middlewareCORS(s.Handler)
s.Handler = middlewareCORS(s.Handler) // 3rd
}
if cfg.Mod.Username != "" {
s.Handler = middlewareAuth(cfg.Mod.Username, cfg.Mod.Password, s.Handler) // 2nd
}
if log.Trace().Enabled() {
s.Handler = middlewareLog(s.Handler) // 1st
}
go func() {
@@ -87,7 +94,22 @@ var log zerolog.Logger
func middlewareLog(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Trace().Msgf("[api] %s %s", r.Method, r.URL)
log.Trace().Msgf("[api] %s %s %s", r.Method, r.URL, r.RemoteAddr)
next.ServeHTTP(w, r)
})
}
func middlewareAuth(username, password string, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !strings.HasPrefix(r.RemoteAddr, "127.") && !strings.HasPrefix(r.RemoteAddr, "[::1]") {
user, pass, ok := r.BasicAuth()
if !ok || user != username || pass != password {
w.Header().Set("Www-Authenticate", `Basic realm="go2rtc"`)
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
}
next.ServeHTTP(w, r)
})
}

View File

@@ -9,11 +9,16 @@ import (
)
func configHandler(w http.ResponseWriter, r *http.Request) {
if app.ConfigPath == "" {
http.Error(w, "", http.StatusGone)
return
}
switch r.Method {
case "GET":
data, err := os.ReadFile(app.ConfigPath)
if err != nil {
http.NotFound(w, r)
http.Error(w, "", http.StatusNotFound)
return
}
if _, err = w.Write(data); err != nil {

View File

@@ -81,7 +81,9 @@ func apiWS(w http.ResponseWriter, r *http.Request) {
for {
msg := new(Message)
if err = ws.ReadJSON(msg); err != nil {
log.Trace().Err(err).Caller().Send()
if !websocket.IsCloseError(err, websocket.CloseNoStatusReceived) {
log.Trace().Err(err).Caller().Send()
}
_ = ws.Close()
break
}

View File

@@ -8,12 +8,13 @@ import (
"gopkg.in/yaml.v3"
"io"
"os"
"path"
"path/filepath"
"runtime"
"strings"
"time"
)
var Version = "0.1-rc.9"
var Version = "1.0.1"
var UserAgent = "go2rtc/" + Version
var ConfigPath string
@@ -52,8 +53,10 @@ func Init() {
}
if ConfigPath != "" {
if cwd, err := os.Getwd(); err == nil {
ConfigPath = path.Join(cwd, ConfigPath)
if !filepath.IsAbs(ConfigPath) {
if cwd, err := os.Getwd(); err == nil {
ConfigPath = filepath.Join(cwd, ConfigPath)
}
}
Info["config_path"] = ConfigPath
}
@@ -81,7 +84,7 @@ func NewLogger(format string, level string) zerolog.Logger {
}
}
zerolog.TimeFieldFormat = zerolog.TimeFormatUnixMs
zerolog.TimeFieldFormat = time.RFC3339Nano
lvl, err := zerolog.ParseLevel(level)
if err != nil || lvl == zerolog.NoLevel {

View File

@@ -71,7 +71,11 @@ func handlerWSMP4(tr *api.Transport, msg *api.Message) error {
return errors.New(api.StreamNotFound)
}
cons := &mp4.Segment{OnlyKeyframe: true}
cons := &mp4.Segment{
RemoteAddr: tr.Request.RemoteAddr,
UserAgent: tr.Request.UserAgent(),
OnlyKeyframe: true,
}
if codecs, ok := msg.Value.(string); ok {
log.Trace().Str("codecs", codecs).Msgf("[mp4] new WS/MP4 consumer")

View File

@@ -162,6 +162,8 @@ func tcpHandler(conn *rtsp.Conn) {
log.Debug().Str("stream", name).Msg("[rtsp] new consumer")
conn.SessionName = app.UserAgent
initMedias(conn)
if err := stream.AddConsumer(conn); err != nil {

View File

@@ -1,6 +1,7 @@
package h264
import (
"bytes"
"encoding/binary"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/pion/rtp"
@@ -27,7 +28,8 @@ func RTPDepay(track *streamer.Track) streamer.WrapperFunc {
}
// Fix TP-Link Tapo TC70: sends SPS and PPS with packet.Marker = true
if packet.Marker {
// Reolink Duo 2: sends SPS with Marker and PPS without
if packet.Marker && len(payload) < 128 {
switch NALUType(payload) {
case NALUTypeSPS, NALUTypePPS:
buf = append(buf, payload...)
@@ -68,6 +70,27 @@ func RTPDepay(track *streamer.Track) streamer.WrapperFunc {
if len(buf) > 0 {
payload = append(buf, payload...)
buf = buf[:0]
} else {
// 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
for i := 0; i < len(payload); {
if i+4 >= len(payload) {
break
}
size := bytes.Index(payload[i+4:], []byte{0, 0, 0, 1})
if size < 0 {
if i == 0 {
break
}
size = len(payload) - (i + 4)
}
binary.BigEndian.PutUint32(payload[i:], uint32(size))
i += size + 4
}
}
//log.Printf("[AVC] %v, len: %d, ts: %10d, seq: %d", Types(payload), len(payload), packet.Timestamp, packet.SequenceNumber)

165
pkg/httpflv/amf0.go Normal file
View File

@@ -0,0 +1,165 @@
package httpflv
import (
"encoding/binary"
"errors"
"math"
)
const (
TypeNumber byte = iota
TypeBoolean
TypeString
TypeObject
TypeEcmaArray = 8
TypeObjectEnd = 9
)
var Err = errors.New("amf0 read error")
// AMF0 spec: http://download.macromedia.com/pub/labs/amf/amf0_spec_121207.pdf
type AMF0 struct {
buf []byte
pos int
}
func NewReader(b []byte) *AMF0 {
return &AMF0{buf: b}
}
func (a *AMF0) ReadMetaData() map[string]interface{} {
if b, _ := a.ReadByte(); b != TypeString {
return nil
}
if s, _ := a.ReadString(); s != "onMetaData" {
return nil
}
b, _ := a.ReadByte()
switch b {
case TypeObject:
v, _ := a.ReadObject()
return v
case TypeEcmaArray:
v, _ := a.ReadEcmaArray()
return v
}
return nil
}
func (a *AMF0) ReadMap() (map[interface{}]interface{}, error) {
dict := make(map[interface{}]interface{})
for a.pos < len(a.buf) {
k, err := a.ReadItem()
if err != nil {
return nil, err
}
v, err := a.ReadItem()
if err != nil {
return nil, err
}
dict[k] = v
}
return dict, nil
}
func (a *AMF0) ReadItem() (interface{}, error) {
dataType, err := a.ReadByte()
if err != nil {
return nil, err
}
switch dataType {
case TypeNumber:
return a.ReadNumber()
case TypeBoolean:
v, err := a.ReadByte()
return v != 0, err
case TypeString:
return a.ReadString()
case TypeObject:
return a.ReadObject()
case TypeObjectEnd:
return nil, nil
}
return nil, Err
}
func (a *AMF0) ReadByte() (byte, error) {
if a.pos >= len(a.buf) {
return 0, Err
}
v := a.buf[a.pos]
a.pos++
return v, nil
}
func (a *AMF0) ReadNumber() (float64, error) {
if a.pos+8 >= len(a.buf) {
return 0, Err
}
v := binary.BigEndian.Uint64(a.buf[a.pos : a.pos+8])
a.pos += 8
return math.Float64frombits(v), nil
}
func (a *AMF0) ReadString() (string, error) {
if a.pos+2 >= len(a.buf) {
return "", Err
}
size := int(binary.BigEndian.Uint16(a.buf[a.pos:]))
a.pos += 2
if a.pos+size >= len(a.buf) {
return "", Err
}
s := string(a.buf[a.pos : a.pos+size])
a.pos += size
return s, nil
}
func (a *AMF0) ReadObject() (map[string]interface{}, error) {
obj := make(map[string]interface{})
for {
k, err := a.ReadString()
if err != nil {
return nil, err
}
v, err := a.ReadItem()
if err != nil {
return nil, err
}
if k == "" {
break
}
obj[k] = v
}
return obj, nil
}
func (a *AMF0) ReadEcmaArray() (map[string]interface{}, error) {
if a.pos+4 >= len(a.buf) {
return nil, Err
}
a.pos += 4 // skip size
return a.ReadObject()
}

97
pkg/httpflv/flvio.go Normal file
View File

@@ -0,0 +1,97 @@
package httpflv
import (
"fmt"
"github.com/deepch/vdk/format/flv/flvio"
"github.com/deepch/vdk/utils/bits/pio"
"io"
)
// TODO: rewrite all of this someday
func ReadTag(r io.Reader, b []byte) (tag flvio.Tag, ts int32, err error) {
if _, err = io.ReadFull(r, b[:flvio.TagHeaderLength]); err != nil {
return
}
var datalen int
if tag, ts, datalen, err = flvio.ParseTagHeader(b); err != nil {
return
}
data := make([]byte, datalen)
if _, err = io.ReadFull(r, data); err != nil {
return
}
n, err := ParseHeader(&tag, data)
if err != nil {
return
}
tag.Data = data[n:]
if _, err = io.ReadFull(r, b[:4]); err != nil {
return
}
return
}
func ParseHeader(self *flvio.Tag, b []byte) (n int, err error) {
switch self.Type {
case flvio.TAG_AUDIO:
return audioParseHeader(self, b)
case flvio.TAG_VIDEO:
return videoParseHeader(self, b)
}
return
}
func audioParseHeader(tag *flvio.Tag, b []byte) (n int, err error) {
if len(b) < n+1 {
err = fmt.Errorf("audiodata: parse invalid")
return
}
flags := b[n]
n++
tag.SoundFormat = flags >> 4
tag.SoundRate = (flags >> 2) & 0x3
tag.SoundSize = (flags >> 1) & 0x1
tag.SoundType = flags & 0x1
switch tag.SoundFormat {
case flvio.SOUND_AAC:
if len(b) < n+1 {
err = fmt.Errorf("audiodata: parse invalid")
return
}
tag.AACPacketType = b[n]
n++
}
return
}
func videoParseHeader(tag *flvio.Tag, b []byte) (n int, err error) {
if len(b) < n+1 {
err = fmt.Errorf("videodata: parse invalid")
return
}
flags := b[n]
tag.FrameType = flags >> 4
tag.CodecID = flags & 0xf
n++
if len(b) < n+4 {
err = fmt.Errorf("videodata: parse invalid")
return
}
tag.AVCPacketType = b[n]
n++
tag.CompositionTime = pio.I24BE(b[n:])
n += 3
return
}

View File

@@ -2,8 +2,9 @@ package httpflv
import (
"bufio"
"errors"
"bytes"
"github.com/deepch/vdk/av"
"github.com/deepch/vdk/codec/aacparser"
"github.com/deepch/vdk/codec/h264parser"
"github.com/deepch/vdk/format/flv/flvio"
"github.com/deepch/vdk/utils/bits/pio"
@@ -41,8 +42,12 @@ func Accept(res *http.Response) (*Conn, error) {
return nil, err
}
if flags&flvio.FILE_HAS_VIDEO == 0 {
return nil, errors.New("not supported")
if flags&flvio.FILE_HAS_VIDEO != 0 {
c.videoIdx = -1
}
if flags&flvio.FILE_HAS_AUDIO != 0 {
c.audioIdx = -1
}
if _, err = c.reader.Discard(n); err != nil {
@@ -56,49 +61,154 @@ type Conn struct {
conn io.ReadCloser
reader *bufio.Reader
buf []byte
videoIdx int8
audioIdx int8
}
func (c *Conn) Streams() ([]av.CodecData, error) {
for {
var video, audio av.CodecData
// Normal software sends:
// 1. Video/audio flag in header
// 2. MetaData as first tag (with video/audio codec info)
// 3. Video/audio headers in 2nd and 3rd tag
// Reolink camera sends:
// 1. Empty video/audio flag
// 2. MedaData without stereo key for AAC
// 3. Audio header after Video keyframe tag
waitVideo := c.videoIdx != 0
waitAudio := c.audioIdx != 0
for i := 0; i < 20; i++ {
tag, _, err := flvio.ReadTag(c.reader, c.buf)
if err != nil {
return nil, err
}
if tag.Type != flvio.TAG_VIDEO || tag.AVCPacketType != flvio.AAC_SEQHDR {
continue
//log.Printf("[FLV] type=%d avc=%d aac=%d video=%t audio=%t", tag.Type, tag.AVCPacketType, tag.AACPacketType, video != nil, audio != nil)
switch tag.Type {
case flvio.TAG_SCRIPTDATA:
if meta := NewReader(tag.Data).ReadMetaData(); meta != nil {
waitVideo = meta["videocodecid"] != nil
// don't wait audio tag because parse all info from MetaData
waitAudio = false
audio = parseAudioConfig(meta)
} else {
waitVideo = bytes.Contains(tag.Data, []byte("videocodecid"))
waitAudio = bytes.Contains(tag.Data, []byte("audiocodecid"))
}
case flvio.TAG_VIDEO:
if tag.AVCPacketType == flvio.AVC_SEQHDR {
video, _ = h264parser.NewCodecDataFromAVCDecoderConfRecord(tag.Data)
}
waitVideo = false
case flvio.TAG_AUDIO:
if tag.SoundFormat == flvio.SOUND_AAC && tag.AACPacketType == flvio.AAC_SEQHDR {
audio, _ = aacparser.NewCodecDataFromMPEG4AudioConfigBytes(tag.Data)
}
waitAudio = false
}
stream, err := h264parser.NewCodecDataFromAVCDecoderConfRecord(tag.Data)
if err != nil {
return nil, err
if !waitVideo && !waitAudio {
break
}
return []av.CodecData{stream}, nil
}
if video != nil && audio != nil {
c.videoIdx = 0
c.audioIdx = 1
return []av.CodecData{video, audio}, nil
} else if video != nil {
c.videoIdx = 0
c.audioIdx = -1
return []av.CodecData{video}, nil
} else if audio != nil {
c.videoIdx = -1
c.audioIdx = 0
return []av.CodecData{audio}, nil
}
return nil, nil
}
func (c *Conn) ReadPacket() (av.Packet, error) {
for {
tag, ts, err := flvio.ReadTag(c.reader, c.buf)
tag, ts, err := ReadTag(c.reader, c.buf)
if err != nil {
return av.Packet{}, err
}
if tag.Type != flvio.TAG_VIDEO || tag.AVCPacketType != flvio.AVC_NALU {
continue
}
switch tag.Type {
case flvio.TAG_VIDEO:
if c.videoIdx < 0 || tag.AVCPacketType != flvio.AVC_NALU {
continue
}
return av.Packet{
Idx: 0,
Data: tag.Data,
CompositionTime: flvio.TsToTime(tag.CompositionTime),
IsKeyFrame: tag.FrameType == flvio.FRAME_KEY,
Time: flvio.TsToTime(ts),
}, nil
//log.Printf("[FLV] %v, len: %d, ts: %10d", h264.Types(tag.Data), len(tag.Data), flvio.TsToTime(ts))
return av.Packet{
Idx: c.videoIdx,
Data: tag.Data,
CompositionTime: flvio.TsToTime(tag.CompositionTime),
IsKeyFrame: tag.FrameType == flvio.FRAME_KEY,
Time: flvio.TsToTime(ts),
}, nil
case flvio.TAG_AUDIO:
if c.audioIdx < 0 || tag.SoundFormat != flvio.SOUND_AAC || tag.AACPacketType != flvio.AAC_RAW {
continue
}
return av.Packet{Idx: c.audioIdx, Data: tag.Data, Time: flvio.TsToTime(ts)}, nil
}
}
}
func (c *Conn) Close() (err error) {
return c.conn.Close()
}
func parseAudioConfig(meta map[string]interface{}) av.CodecData {
if meta["audiocodecid"] != float64(10) {
return nil
}
config := aacparser.MPEG4AudioConfig{
ObjectType: aacparser.AOT_AAC_LC,
}
switch v := meta["audiosamplerate"].(type) {
case float64:
config.SampleRate = int(v)
default:
return nil
}
switch meta["stereo"] {
case true:
config.ChannelConfig = 2
config.ChannelLayout = av.CH_STEREO
default:
// Reolink doesn't have this setting
config.ChannelConfig = 1
config.ChannelLayout = av.CH_MONO
}
buf := &bytes.Buffer{}
if err := aacparser.WriteMPEG4AudioConfig(buf, config); err != nil {
return nil
}
return aacparser.CodecData{
Config: config,
ConfigBytes: buf.Bytes(),
}
}

View File

@@ -1,18 +1,25 @@
package mp4
import (
"encoding/json"
"github.com/AlexxIT/go2rtc/pkg/h264"
"github.com/AlexxIT/go2rtc/pkg/h265"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/pion/rtp"
"sync/atomic"
)
type Segment struct {
streamer.Element
Medias []*streamer.Media
Medias []*streamer.Media
UserAgent string
RemoteAddr string
MimeType string
OnlyKeyframe bool
send uint32
}
func (c *Segment) GetMedias() []*streamer.Media {
@@ -56,6 +63,7 @@ func (c *Segment) AddTrack(media *streamer.Media, track *streamer.Track) *stream
}
buf := muxer.Marshal(0, packet)
atomic.AddUint32(&c.send, uint32(len(buf)))
c.Fire(append(init, buf...))
return nil
@@ -73,6 +81,7 @@ func (c *Segment) AddTrack(media *streamer.Media, track *streamer.Track) *stream
buf = append(buf, b...)
}
atomic.AddUint32(&c.send, uint32(len(buf)))
c.Fire(buf)
buf = buf[:0]
@@ -106,6 +115,7 @@ func (c *Segment) AddTrack(media *streamer.Media, track *streamer.Track) *stream
}
buf := muxer.Marshal(0, packet)
atomic.AddUint32(&c.send, uint32(len(buf)))
c.Fire(append(init, buf...))
return nil
@@ -121,3 +131,13 @@ func (c *Segment) AddTrack(media *streamer.Media, track *streamer.Track) *stream
panic("unsupported codec")
}
func (c *Segment) MarshalJSON() ([]byte, error) {
info := &streamer.Info{
Type: "WS/MP4 client",
RemoteAddr: c.RemoteAddr,
UserAgent: c.UserAgent,
Send: atomic.LoadUint32(&c.send),
}
return json.Marshal(info)
}

View File

@@ -14,6 +14,7 @@ import (
type Consumer struct {
streamer.Element
Medias []*streamer.Media
UserAgent string
RemoteAddr string
@@ -26,6 +27,10 @@ type Consumer struct {
}
func (c *Consumer) GetMedias() []*streamer.Media {
if c.Medias != nil {
return c.Medias
}
return []*streamer.Media{
{
Kind: streamer.KindVideo,

View File

@@ -2,7 +2,6 @@ package rtsp
import (
"bufio"
"bytes"
"crypto/tls"
"encoding/binary"
"errors"
@@ -78,6 +77,7 @@ type Conn struct {
// public
Backchannel bool
SessionName string
Medias []*streamer.Media
Session string
@@ -280,7 +280,7 @@ func (c *Conn) Options() error {
}
if val := res.Header.Get("Content-Base"); val != "" {
c.URL, err = url.Parse(val)
c.URL, err = urlParse(val)
if err != nil {
return err
}
@@ -310,7 +310,7 @@ func (c *Conn) Describe() error {
}
if val := res.Header.Get("Content-Base"); val != "" {
c.URL, err = url.Parse(val)
c.URL, err = urlParse(val)
if err != nil {
return err
}
@@ -379,7 +379,7 @@ func (c *Conn) SetupMedia(
}
rawURL += media.Control
}
trackURL, err := url.Parse(rawURL)
trackURL, err := urlParse(rawURL)
if err != nil {
return nil, err
}
@@ -618,7 +618,7 @@ func (c *Conn) Accept() error {
medias = append(medias, media)
}
res.Body, err = streamer.MarshalSDP(medias)
res.Body, err = streamer.MarshalSDP(c.SessionName, medias)
if err != nil {
return err
}
@@ -654,6 +654,12 @@ func (c *Conn) Accept() error {
}
return err
case MethodTeardown:
res := &tcp.Response{Request: req}
_ = c.Response(res)
c.state = StateNone
return c.conn.Close()
default:
return fmt.Errorf("unsupported method: %s", req.Method)
}
@@ -792,12 +798,12 @@ func (c *Conn) Handle() (err error) {
msg := &RTCP{Channel: channelID}
if err = msg.Header.Unmarshal(buf); err != nil {
return
continue
}
msg.Packets, err = rtcp.Unmarshal(buf)
if err != nil {
return
continue
}
c.Fire(msg)
@@ -875,42 +881,3 @@ func (c *Conn) bindTrack(
return track.Bind(push)
}
type RTCP struct {
Channel byte
Header rtcp.Header
Packets []rtcp.Packet
}
const sdpHeader = `v=0
o=- 0 0 IN IP4 0.0.0.0
s=-
t=0 0`
func UnmarshalSDP(rawSDP []byte) ([]*streamer.Media, error) {
medias, err := streamer.UnmarshalSDP(rawSDP)
if err != nil {
// fix SDP header for some cameras
i := bytes.Index(rawSDP, []byte("\nm="))
if i > 0 {
rawSDP = append([]byte(sdpHeader), rawSDP[i:]...)
medias, err = streamer.UnmarshalSDP(rawSDP)
}
if err != nil {
return nil, err
}
}
// fix bug in ONVIF spec
// https://www.onvif.org/specs/stream/ONVIF-Streaming-Spec-v241.pdf
for _, media := range medias {
switch media.Direction {
case streamer.DirectionRecvonly, "":
media.Direction = streamer.DirectionSendonly
case streamer.DirectionSendonly:
media.Direction = streamer.DirectionRecvonly
}
}
return medias, nil
}

63
pkg/rtsp/helpers.go Normal file
View File

@@ -0,0 +1,63 @@
package rtsp
import (
"bytes"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/pion/rtcp"
"net/url"
"strings"
)
type RTCP struct {
Channel byte
Header rtcp.Header
Packets []rtcp.Packet
}
const sdpHeader = `v=0
o=- 0 0 IN IP4 0.0.0.0
s=-
t=0 0`
func UnmarshalSDP(rawSDP []byte) ([]*streamer.Media, error) {
medias, err := streamer.UnmarshalSDP(rawSDP)
if err != nil {
// fix SDP header for some cameras
i := bytes.Index(rawSDP, []byte("\nm="))
if i > 0 {
rawSDP = append([]byte(sdpHeader), rawSDP[i:]...)
medias, err = streamer.UnmarshalSDP(rawSDP)
}
if err != nil {
return nil, err
}
}
// fix bug in ONVIF spec
// https://www.onvif.org/specs/stream/ONVIF-Streaming-Spec-v241.pdf
for _, media := range medias {
switch media.Direction {
case streamer.DirectionRecvonly, "":
media.Direction = streamer.DirectionSendonly
case streamer.DirectionSendonly:
media.Direction = streamer.DirectionRecvonly
}
}
return medias, nil
}
// urlParse fix bug in URL from D-Link camera:
// Content-Base: rtsp://::ffff:192.168.1.123/onvif/profile.1/
func urlParse(rawURL string) (*url.URL, error) {
u, err := url.Parse(rawURL)
if err != nil && strings.HasSuffix(err.Error(), "after host") {
if i1 := strings.Index(rawURL, "://"); i1 > 0 {
if i2 := strings.IndexByte(rawURL[i1+3:], '/'); i2 > 0 {
return urlParse(rawURL[:i1+3+i2] + ":" + rawURL[i1+3+i2:])
}
}
}
return u, err
}

12
pkg/rtsp/rtsp_test.go Normal file
View File

@@ -0,0 +1,12 @@
package rtsp
import (
"github.com/stretchr/testify/assert"
"testing"
)
func TestURLParse(t *testing.T) {
base := "rtsp://::ffff:192.168.1.123/onvif/profile.1/"
_, err := urlParse(base)
assert.Empty(t, err)
}

View File

@@ -183,8 +183,22 @@ func UnmarshalSDP(rawSDP []byte) ([]*Media, error) {
return medias, nil
}
func MarshalSDP(medias []*Media) ([]byte, error) {
sd := &sdp.SessionDescription{}
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)

View File

@@ -0,0 +1,23 @@
package streamer
import (
"github.com/pion/sdp/v3"
"github.com/stretchr/testify/assert"
"testing"
)
func TestSDP(t *testing.T) {
medias := []*Media{{
Kind: KindAudio, Direction: DirectionSendonly,
Codecs: []*Codec{
{Name: CodecPCMU, ClockRate: 8000},
},
}}
data, err := MarshalSDP("go2rtc/1.0.0", medias)
assert.Empty(t, err)
sd := &sdp.SessionDescription{}
err = sd.Unmarshal(data)
assert.Empty(t, err)
}

View File

@@ -58,7 +58,7 @@
0, location.pathname.lastIndexOf("/")
);
fetch(`${baseUrl}/api/devices`)
fetch(`${baseUrl}/api/devices`, {cache: 'no-cache'})
.then(r => r.json())
.then(data => {
document.querySelector("body > table > tbody").innerHTML =

View File

@@ -41,7 +41,7 @@
method: 'POST', body: editor.getValue()
}).then(r => {
if (r.ok) {
alert("OK");
alert('OK');
fetch('api/exit', {method: 'POST'});
} else {
r.text().then(alert);
@@ -50,8 +50,18 @@
});
window.addEventListener('load', () => {
fetch('api/config').then(r => r.text()).then(data => {
editor.setValue(data);
fetch('api/config', {cache: 'no-cache'}).then(r => {
if (r.status === 410) {
alert('Config file is not set');
} else if (r.status === 404) {
editor.setValue(''); // config file not exist
} else if (r.ok) {
r.text().then(data => {
editor.setValue(data);
});
} else {
alert(`Unknown error: ${r.statusText} (${r.status})`);
}
});
})
</script>

View File

@@ -65,7 +65,7 @@
0, location.pathname.lastIndexOf("/")
);
fetch(`${baseUrl}/api/homekit`)
fetch(`${baseUrl}/api/homekit`, {cache: 'no-cache'})
.then(r => r.json())
.then(data => {
document.querySelector("body > table > tbody").innerHTML =

View File

@@ -133,7 +133,7 @@
function reload() {
const url = new URL("api/streams", location.href);
fetch(url).then(r => r.json()).then(data => {
fetch(url, {cache: 'no-cache'}).then(r => r.json()).then(data => {
tbody.innerHTML = "";
for (const [name, value] of Object.entries(data)) {
@@ -153,7 +153,7 @@
}
const url = new URL("api", location.href);
fetch(url).then(r => r.json()).then(data => {
fetch(url, {cache: 'no-cache'}).then(r => r.json()).then(data => {
const info = document.querySelector(".info");
info.innerText = `Version: ${data.version}, Config: ${data.config_path}`;

View File

@@ -395,11 +395,11 @@ export class VideoRTC extends HTMLElement {
bufLen = 0;
sb.appendBuffer(data);
} else if (sb.buffered && sb.buffered.length) {
const end = sb.buffered.end(sb.buffered.length - 1) - 5;
const end = sb.buffered.end(sb.buffered.length - 1) - 15;
const start = sb.buffered.start(0);
if (end > start) {
sb.remove(start, end);
ms.setLiveSeekableRange(end, end + 5);
ms.setLiveSeekableRange(end, end + 15);
}
// console.debug("VideoRTC.buffered", start, end);
}