mirror of
https://github.com/AlexxIT/go2rtc.git
synced 2025-09-27 04:36:12 +08:00
Compare commits
12 Commits
v0.1-beta.
...
v0.1-beta.
Author | SHA1 | Date | |
---|---|---|---|
![]() |
945b486fe0 | ||
![]() |
d72d7b089c | ||
![]() |
d339fbe712 | ||
![]() |
3aeb278c47 | ||
![]() |
c92c1fc3e9 | ||
![]() |
def57119f4 | ||
![]() |
b20275d2b5 | ||
![]() |
a11ca1da6e | ||
![]() |
0fb7132947 | ||
![]() |
0f9e3c97c5 | ||
![]() |
e049a17216 | ||
![]() |
217c8c2bf6 |
29
README.md
29
README.md
@@ -5,11 +5,10 @@ Ultimate camera streaming application with support RTSP, WebRTC, HomeKit, FFmpeg
|
||||

|
||||
|
||||
- zero-dependency and zero-config [small app](#go2rtc-binary) for all OS (Windows, macOS, Linux, ARM)
|
||||
- zero-delay for all supported protocols (lowest possible streaming latency)
|
||||
- zero-delay for many supported protocols (lowest possible streaming latency)
|
||||
- streaming from [RTSP](#source-rtsp), [RTMP](#source-rtmp), [MJPEG](#source-ffmpeg), [HLS](#source-ffmpeg), [USB Cameras](#source-ffmpeg-device), [files](#source-ffmpeg) and [other sources](#module-streams)
|
||||
- streaming to [RTSP](#module-rtsp), [WebRTC](#module-webrtc) or [MSE](#module-api)
|
||||
- streaming to [RTSP](#module-rtsp), [WebRTC](#module-webrtc) or [MSE](#module-mp4)
|
||||
- first project in the World with support streaming from [HomeKit Cameras](#source-homekit)
|
||||
- low CPU load for supported codecs
|
||||
- on the fly transcoding for unsupported codecs via [FFmpeg](#source-ffmpeg)
|
||||
- multi-source 2-way [codecs negotiation](#codecs-negotiation)
|
||||
- mixing tracks from different sources to single stream
|
||||
@@ -104,7 +103,7 @@ Don't forget to fix the rights `chmod +x go2rtc_xxx_xxx` on Linux and Mac.
|
||||
|
||||
### go2rtc: Docker
|
||||
|
||||
Container [alexxit/go2rtc](https://hub.docker.com/r/alexxit/go2rtc) with support `amd64`, `386`, `arm64`, `arm`. This container same as [Home Assistant Add-on](#go2rtc-home-assistant-add-on), but can be used separately from the Home Assistant. Container has preinstalled [FFmpeg](#source-ffmpeg) and [Ngrok](#module-ngrok) applications.
|
||||
Container [alexxit/go2rtc](https://hub.docker.com/r/alexxit/go2rtc) with support `amd64`, `386`, `arm64`, `arm`. This container same as [Home Assistant Add-on](#go2rtc-home-assistant-add-on), but can be used separately from the Home Assistant. Container has preinstalled [FFmpeg](#source-ffmpeg), [Ngrok](#module-ngrok) and [Python](#source-echo).
|
||||
|
||||
```yaml
|
||||
services:
|
||||
@@ -148,6 +147,7 @@ Available source types:
|
||||
- [ffmpeg](#source-ffmpeg) - FFmpeg integration (`MJPEG`, `HLS`, `files` and source types)
|
||||
- [ffmpeg:device](#source-ffmpeg-device) - local USB Camera or Webcam
|
||||
- [exec](#source-exec) - advanced FFmpeg and GStreamer integration
|
||||
- [echo](#source-echo) - get stream link via bash or python
|
||||
- [homekit](#source-homekit) - streaming from HomeKit Camera
|
||||
- [hass](#source-hass) - Home Assistant integration
|
||||
|
||||
@@ -254,6 +254,19 @@ streams:
|
||||
stream1: exec:ffmpeg -hide_banner -re -stream_loop -1 -i /media/BigBuckBunny.mp4 -c copy -rtsp_transport tcp -f rtsp {output}
|
||||
```
|
||||
|
||||
#### Source: Echo
|
||||
|
||||
Some sources may have a dynamic link. And you will need to get it using a bash or python script. Your script should echo a link to the source. RTSP, FFmpeg or any of the [supported sources](#module-streams).
|
||||
|
||||
**Docker** and **Hass Add-on** users has preinstalled `python3`, `curl`, `jq`.
|
||||
|
||||
Check examples in [wiki](https://github.com/AlexxIT/go2rtc/wiki/Source-Echo-examples).
|
||||
|
||||
```yaml
|
||||
streams:
|
||||
apple_hls: echo:python3 hls.py https://developer.apple.com/streaming/examples/basic-stream-osx-ios5.html
|
||||
```
|
||||
|
||||
#### Source: HomeKit
|
||||
|
||||
**Important:**
|
||||
@@ -463,6 +476,14 @@ In other cases you need to use IP-address of server with **go2rtc** application.
|
||||
|
||||
PS. Default Home Assistant lovelace cards don't support 2-way audio. You can use 2-way audio from [Add-on Web UI](https://my.home-assistant.io/redirect/supervisor_addon/?addon=a889bffc_go2rtc&repository_url=https%3A%2F%2Fgithub.com%2FAlexxIT%2Fhassio-addons). But you need use HTTPS to access the microphone. This is a browser restriction and cannot be avoided.
|
||||
|
||||
### Module: MP4
|
||||
|
||||
Provides several features:
|
||||
|
||||
1. MSE stream (fMP4 over WebSocket)
|
||||
2. Camera snapshots in MP4 format (single frame), can be sent to [Telegram](https://www.telegram.org/)
|
||||
3. Progressive MP4 stream - bad format for streaming because of high latency, doesn't work in Safari
|
||||
|
||||
### Module: Log
|
||||
|
||||
You can set different log levels for different modules.
|
||||
|
@@ -1,23 +1,40 @@
|
||||
ARG BUILD_FROM
|
||||
FROM $BUILD_FROM
|
||||
|
||||
RUN apk add --no-cache git go ffmpeg
|
||||
FROM $BUILD_FROM as build
|
||||
|
||||
ARG BUILD_ARCH
|
||||
# 1. Build go2rtc
|
||||
RUN apk add --no-cache git go
|
||||
|
||||
RUN git clone https://github.com/AlexxIT/go2rtc \
|
||||
&& cd go2rtc \
|
||||
&& CGO_ENABLED=0 go build -ldflags "-s -w" -trimpath -o /usr/local/bin
|
||||
&& CGO_ENABLED=0 go build -ldflags "-s -w" -trimpath
|
||||
|
||||
# 2. Download ngrok
|
||||
ARG BUILD_ARCH
|
||||
|
||||
# https://github.com/home-assistant/docker-base/blob/master/alpine/Dockerfile
|
||||
RUN if [ "${BUILD_ARCH}" = "aarch64" ]; then BUILD_ARCH="arm64"; \
|
||||
elif [ "${BUILD_ARCH}" = "armv7" ]; then BUILD_ARCH="arm"; fi \
|
||||
&& cd go2rtc \
|
||||
&& curl $(curl -s "https://raw.githubusercontent.com/ngrok/docker-ngrok/main/releases.json" | jq -r ".${BUILD_ARCH}.url") -o ngrok.zip \
|
||||
&& unzip ngrok -d /usr/local/bin
|
||||
&& unzip ngrok
|
||||
|
||||
RUN rm -r /go2rtc
|
||||
|
||||
|
||||
# https://devopscube.com/reduce-docker-image-size/
|
||||
FROM $BUILD_FROM
|
||||
|
||||
# 3. Copy go2rtc and ngrok to release
|
||||
COPY --from=build /go2rtc/go2rtc /usr/local/bin
|
||||
COPY --from=build /go2rtc/ngrok /usr/local/bin
|
||||
|
||||
# 4. Install ffmpeg
|
||||
# apk base OK: 22 MiB in 40 packages
|
||||
# ffmpeg OK: 113 MiB in 110 packages
|
||||
# python3 OK: 161 MiB in 114 packages
|
||||
RUN apk add --no-cache ffmpeg python3
|
||||
|
||||
# 5. Copy run to release
|
||||
COPY run.sh /
|
||||
RUN chmod a+x /run.sh
|
||||
|
||||
|
@@ -2,23 +2,13 @@
|
||||
|
||||
set +e
|
||||
|
||||
# add the feature for update to any version
|
||||
if [ -f "/config/go2rtc.version" ]; then
|
||||
branch=`cat /config/go2rtc.version`
|
||||
echo "Update to version $branch"
|
||||
git clone --depth 1 --branch "$branch" https://github.com/AlexxIT/go2rtc \
|
||||
&& cd go2rtc \
|
||||
&& CGO_ENABLED=0 go build -ldflags "-s -w" -trimpath -o /usr/local/bin \
|
||||
&& rm -r /go2rtc && rm /config/go2rtc.version
|
||||
fi
|
||||
|
||||
# set cwd for go2rtc (for config file, Hass itegration, etc)
|
||||
# set cwd for go2rtc (for config file, Hass integration, etc)
|
||||
cd /config
|
||||
|
||||
# add the feature to override go2rtc binary from Hass config folder
|
||||
export PATH="/config:$PATH"
|
||||
|
||||
while true; do
|
||||
go2rtc
|
||||
sleep 5
|
||||
go2rtc
|
||||
sleep 5
|
||||
done
|
29
cmd/echo/echo.go
Normal file
29
cmd/echo/echo.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package echo
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"github.com/AlexxIT/go2rtc/cmd/app"
|
||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/shell"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
func Init() {
|
||||
log := app.GetLogger("echo")
|
||||
|
||||
streams.HandleFunc("echo", func(url string) (streamer.Producer, error) {
|
||||
args := shell.QuoteSplit(url[5:])
|
||||
|
||||
b, err := exec.Command(args[0], args[1:]...).Output()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
b = bytes.TrimSpace(b)
|
||||
|
||||
log.Debug().Str("url", url).Msgf("[echo] %s", b)
|
||||
|
||||
return streams.GetProducer(string(b))
|
||||
})
|
||||
}
|
@@ -8,6 +8,7 @@ import (
|
||||
"github.com/AlexxIT/go2rtc/cmd/rtsp"
|
||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
||||
pkg "github.com/AlexxIT/go2rtc/pkg/rtsp"
|
||||
"github.com/AlexxIT/go2rtc/pkg/shell"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"github.com/rs/zerolog"
|
||||
"os"
|
||||
@@ -49,7 +50,7 @@ func Handle(url string) (streamer.Producer, error) {
|
||||
)
|
||||
|
||||
// remove `exec:`
|
||||
args := QuoteSplit(url[5:])
|
||||
args := shell.QuoteSplit(url[5:])
|
||||
cmd := exec.Command(args[0], args[1:]...)
|
||||
|
||||
if log.Trace().Enabled() {
|
||||
@@ -86,39 +87,3 @@ func Handle(url string) (streamer.Producer, error) {
|
||||
|
||||
var log zerolog.Logger
|
||||
var waiters map[string]chan streamer.Producer
|
||||
|
||||
func QuoteSplit(s string) []string {
|
||||
var a []string
|
||||
|
||||
for len(s) > 0 {
|
||||
is := strings.IndexByte(s, ' ')
|
||||
if is >= 0 {
|
||||
// skip prefix and double spaces
|
||||
if is == 0 {
|
||||
// goto next symbol
|
||||
s = s[1:]
|
||||
continue
|
||||
}
|
||||
|
||||
// check if quote in word
|
||||
if i := strings.IndexByte(s[:is], '"'); i >= 0 {
|
||||
// search quote end
|
||||
if is = strings.Index(s, `" `); is > 0 {
|
||||
is += 1
|
||||
} else {
|
||||
is = -1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if is >= 0 {
|
||||
a = append(a, strings.ReplaceAll(s[:is], `"`, ""))
|
||||
s = s[is+1:]
|
||||
} else {
|
||||
//add last word
|
||||
a = append(a, s)
|
||||
break
|
||||
}
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
19
cmd/ivideon/ivideon.go
Normal file
19
cmd/ivideon/ivideon.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package ivideon
|
||||
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/ivideon"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func Init() {
|
||||
streams.HandleFunc("ivideon", func(url string) (streamer.Producer, error) {
|
||||
id := strings.Replace(url[8:], "/", ":", 1)
|
||||
prod := ivideon.NewClient(id)
|
||||
if err := prod.Dial(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return prod, nil
|
||||
})
|
||||
}
|
@@ -48,13 +48,17 @@ func handlerKeyframe(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
defer stream.RemoveConsumer(cons)
|
||||
|
||||
w.Header().Set("Content-Type", cons.MimeType())
|
||||
|
||||
data := cons.Init()
|
||||
data, err := cons.Init()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("[api.keyframe] init")
|
||||
return
|
||||
}
|
||||
data = append(data, <-exit...)
|
||||
|
||||
stream.RemoveConsumer(cons)
|
||||
|
||||
// Apple Safari won't show frame without length
|
||||
w.Header().Set("Content-Length", strconv.Itoa(len(data)))
|
||||
|
||||
@@ -97,8 +101,13 @@ func handlerMP4(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
w.Header().Set("Content-Type", cons.MimeType())
|
||||
|
||||
data := cons.Init()
|
||||
if _, err := w.Write(data); err != nil {
|
||||
data, err := cons.Init()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("[api.mp4] init")
|
||||
return
|
||||
}
|
||||
|
||||
if _, err = w.Write(data); err != nil {
|
||||
log.Error().Err(err).Msg("[api.mp4] write")
|
||||
return
|
||||
}
|
||||
|
@@ -41,5 +41,12 @@ func handlerWS(ctx *api.Context, msg *streamer.Message) {
|
||||
Type: MsgTypeMSE, Value: cons.MimeType(),
|
||||
})
|
||||
|
||||
ctx.Write(cons.Init())
|
||||
data, err := cons.Init()
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("[api.mse] init")
|
||||
ctx.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Write(data)
|
||||
}
|
||||
|
@@ -8,6 +8,7 @@ import (
|
||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
||||
"github.com/rs/zerolog"
|
||||
"net"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func Init() {
|
||||
@@ -47,6 +48,15 @@ var OnProducer func(conn streamer.Producer) bool // TODO: maybe rewrite...
|
||||
var log zerolog.Logger
|
||||
|
||||
func rtspHandler(url string) (streamer.Producer, error) {
|
||||
backchannel := true
|
||||
|
||||
if i := strings.IndexByte(url, '#'); i > 0 {
|
||||
if url[i+1:] == "backchannel=0" {
|
||||
backchannel = false
|
||||
}
|
||||
url = url[:i]
|
||||
}
|
||||
|
||||
conn, err := rtsp.NewClient(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -67,8 +77,12 @@ func rtspHandler(url string) (streamer.Producer, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
conn.Backchannel = true
|
||||
conn.Backchannel = backchannel
|
||||
if err = conn.Describe(); err != nil {
|
||||
if !backchannel {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// second try without backchannel, we need to reconnect
|
||||
if err = conn.Dial(); err != nil {
|
||||
return nil, err
|
||||
|
@@ -79,7 +79,11 @@ func (p *Producer) start() {
|
||||
log.Debug().Str("url", p.url).Msg("[streams] start producer")
|
||||
|
||||
p.state = stateStart
|
||||
go p.element.Start()
|
||||
go func() {
|
||||
if err := p.element.Start(); err != nil {
|
||||
log.Warn().Err(err).Str("url", p.url).Msg("[streams] start")
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (p *Producer) stop() {
|
||||
|
6
main.go
6
main.go
@@ -4,10 +4,12 @@ import (
|
||||
"github.com/AlexxIT/go2rtc/cmd/api"
|
||||
"github.com/AlexxIT/go2rtc/cmd/app"
|
||||
"github.com/AlexxIT/go2rtc/cmd/debug"
|
||||
"github.com/AlexxIT/go2rtc/cmd/echo"
|
||||
"github.com/AlexxIT/go2rtc/cmd/exec"
|
||||
"github.com/AlexxIT/go2rtc/cmd/ffmpeg"
|
||||
"github.com/AlexxIT/go2rtc/cmd/hass"
|
||||
"github.com/AlexxIT/go2rtc/cmd/homekit"
|
||||
"github.com/AlexxIT/go2rtc/cmd/ivideon"
|
||||
"github.com/AlexxIT/go2rtc/cmd/mp4"
|
||||
"github.com/AlexxIT/go2rtc/cmd/ngrok"
|
||||
"github.com/AlexxIT/go2rtc/cmd/rtmp"
|
||||
@@ -24,6 +26,8 @@ func main() {
|
||||
app.Init() // init config and logs
|
||||
streams.Init() // load streams list
|
||||
|
||||
echo.Init()
|
||||
|
||||
rtsp.Init() // add support RTSP client and RTSP server
|
||||
rtmp.Init() // add support RTMP client
|
||||
exec.Init() // add support exec scheme (depends on RTSP server)
|
||||
@@ -38,6 +42,8 @@ func main() {
|
||||
srtp.Init()
|
||||
homekit.Init()
|
||||
|
||||
ivideon.Init()
|
||||
|
||||
ngrok.Init()
|
||||
debug.Init()
|
||||
|
||||
|
@@ -58,3 +58,21 @@ func RepairAVC(track *streamer.Track) streamer.WrapperFunc {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func SplitAVC(data []byte) [][]byte {
|
||||
var nals [][]byte
|
||||
for {
|
||||
// get AVC length
|
||||
size := int(binary.BigEndian.Uint32(data))
|
||||
|
||||
// check if multiple items in one packet
|
||||
if size+4 < len(data) {
|
||||
nals = append(nals, data[:size+4])
|
||||
data = data[size+4:]
|
||||
} else {
|
||||
nals = append(nals, data)
|
||||
break
|
||||
}
|
||||
}
|
||||
return nals
|
||||
}
|
||||
|
286
pkg/ivideon/client.go
Normal file
286
pkg/ivideon/client.go
Normal file
@@ -0,0 +1,286 @@
|
||||
package ivideon
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/AlexxIT/go2rtc/pkg/h264"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"github.com/deepch/vdk/codec/h264parser"
|
||||
"github.com/deepch/vdk/format/fmp4/fmp4io"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/pion/rtp"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
streamer.Element
|
||||
|
||||
ID string
|
||||
|
||||
conn *websocket.Conn
|
||||
medias []*streamer.Media
|
||||
tracks map[byte]*streamer.Track
|
||||
|
||||
closed bool
|
||||
|
||||
msg *message
|
||||
t0 time.Time
|
||||
|
||||
buffer chan []byte
|
||||
}
|
||||
|
||||
func NewClient(id string) *Client {
|
||||
return &Client{ID: id}
|
||||
}
|
||||
|
||||
func (c *Client) Dial() (err error) {
|
||||
resp, err := http.Get(
|
||||
"https://openapi-alpha.ivideon.com/cameras/" + c.ID +
|
||||
"/live_stream?op=GET&access_token=public&q=2&" +
|
||||
"video_codecs=h264&format=ws-fmp4",
|
||||
)
|
||||
|
||||
data, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var v liveResponse
|
||||
if err = json.Unmarshal(data, &v); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !v.Success {
|
||||
return fmt.Errorf("wrong response: %s", data)
|
||||
}
|
||||
|
||||
c.conn, _, err = websocket.DefaultDialer.Dial(v.Result.URL, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = c.getTracks(); err != nil {
|
||||
_ = c.conn.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) Handle() error {
|
||||
c.buffer = make(chan []byte, 5)
|
||||
// add delay to the stream for smooth playing (not a best solution)
|
||||
c.t0 = time.Now().Add(time.Second)
|
||||
|
||||
// processing stream in separate thread for lower delay between packets
|
||||
go c.worker()
|
||||
|
||||
_, data, err := c.conn.ReadMessage()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
track := c.tracks[c.msg.Track]
|
||||
if track != nil {
|
||||
c.buffer <- data
|
||||
}
|
||||
|
||||
// we have one unprocessed msg after getTracks
|
||||
for {
|
||||
_, data, err = c.conn.ReadMessage()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var msg message
|
||||
if err = json.Unmarshal(data, &msg); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch msg.Type {
|
||||
case "stream-init":
|
||||
continue
|
||||
|
||||
case "fragment":
|
||||
_, data, err = c.conn.ReadMessage()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
track = c.tracks[msg.Track]
|
||||
if track != nil {
|
||||
c.buffer <- data
|
||||
}
|
||||
|
||||
default:
|
||||
return fmt.Errorf("wrong message type: %s", data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) Close() error {
|
||||
if c.conn == nil {
|
||||
return nil
|
||||
}
|
||||
close(c.buffer)
|
||||
c.closed = true
|
||||
return c.conn.Close()
|
||||
}
|
||||
|
||||
func (c *Client) getTracks() error {
|
||||
c.tracks = map[byte]*streamer.Track{}
|
||||
|
||||
for {
|
||||
_, data, err := c.conn.ReadMessage()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var msg message
|
||||
if err = json.Unmarshal(data, &msg); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch msg.Type {
|
||||
case "stream-init":
|
||||
s := msg.CodecString
|
||||
i := strings.IndexByte(s, '.')
|
||||
if i > 0 {
|
||||
s = s[:i]
|
||||
}
|
||||
|
||||
switch s {
|
||||
case "avc1": // avc1.4d0029
|
||||
// skip multiple identical init
|
||||
if c.tracks[msg.TrackID] != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
codec := streamer.NewCodec(streamer.CodecH264)
|
||||
codec.FmtpLine = "profile-level-id=" + msg.CodecString[i+1:]
|
||||
codec.PayloadType = h264.PayloadTypeAVC
|
||||
|
||||
i = bytes.Index(msg.Data, []byte("avcC")) - 4
|
||||
if i < 0 {
|
||||
return fmt.Errorf("wrong AVC: %s", msg.Data)
|
||||
}
|
||||
|
||||
avccLen := binary.BigEndian.Uint32(msg.Data[i:])
|
||||
data = msg.Data[i+8 : i+int(avccLen)]
|
||||
|
||||
record := h264parser.AVCDecoderConfRecord{}
|
||||
if _, err = record.Unmarshal(data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
codec.FmtpLine += ";sprop-parameter-sets=" +
|
||||
base64.StdEncoding.EncodeToString(record.SPS[0]) + "," +
|
||||
base64.StdEncoding.EncodeToString(record.PPS[0])
|
||||
|
||||
media := &streamer.Media{
|
||||
Kind: streamer.KindVideo,
|
||||
Direction: streamer.DirectionSendonly,
|
||||
Codecs: []*streamer.Codec{codec},
|
||||
}
|
||||
c.medias = append(c.medias, media)
|
||||
|
||||
track := &streamer.Track{
|
||||
Direction: streamer.DirectionSendonly,
|
||||
Codec: codec,
|
||||
}
|
||||
c.tracks[msg.TrackID] = track
|
||||
|
||||
case "mp4a": // mp4a.40.2
|
||||
}
|
||||
|
||||
case "fragment":
|
||||
c.msg = &msg
|
||||
return nil
|
||||
|
||||
default:
|
||||
return fmt.Errorf("wrong message type: %s", data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) worker() {
|
||||
var track *streamer.Track
|
||||
for _, track = range c.tracks {
|
||||
break
|
||||
}
|
||||
|
||||
for data := range c.buffer {
|
||||
moof := &fmp4io.MovieFrag{}
|
||||
if _, err := moof.Unmarshal(data, 0); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
moofLen := binary.BigEndian.Uint32(data)
|
||||
_ = moofLen
|
||||
|
||||
mdat := moof.Unknowns[0]
|
||||
if mdat.Tag() != fmp4io.MDAT {
|
||||
continue
|
||||
}
|
||||
i, _ := mdat.Pos() // offset, size
|
||||
data = data[i+8:]
|
||||
|
||||
traf := moof.Tracks[0]
|
||||
ts := uint32(traf.DecodeTime.Time)
|
||||
|
||||
//println("!!!", (time.Duration(ts) * time.Millisecond).String(), time.Since(c.t0).String())
|
||||
|
||||
for _, entry := range traf.Run.Entries {
|
||||
// synchronize framerate for WebRTC and MSE
|
||||
d := time.Duration(ts)*time.Millisecond - time.Since(c.t0)
|
||||
if d < 0 {
|
||||
d = time.Duration(entry.Duration) * time.Millisecond / 2
|
||||
}
|
||||
time.Sleep(d)
|
||||
|
||||
// can be SPS, PPS and IFrame in one packet
|
||||
for _, payload := range h264.SplitAVC(data[:entry.Size]) {
|
||||
packet := &rtp.Packet{
|
||||
// ivideon clockrate=1000, RTP clockrate=90000
|
||||
Header: rtp.Header{Timestamp: ts * 90},
|
||||
Payload: payload,
|
||||
}
|
||||
_ = track.WriteRTP(packet)
|
||||
}
|
||||
|
||||
data = data[entry.Size:]
|
||||
ts += entry.Duration
|
||||
}
|
||||
|
||||
if len(data) != 0 {
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type liveResponse struct {
|
||||
Result struct {
|
||||
URL string `json:"url"`
|
||||
} `json:"result"`
|
||||
Success bool `json:"success"`
|
||||
}
|
||||
|
||||
type message struct {
|
||||
Type string `json:"type"`
|
||||
|
||||
CodecString string `json:"codec_string"`
|
||||
Data []byte `json:"data"`
|
||||
TrackID byte `json:"track_id"`
|
||||
|
||||
Track byte `json:"track"`
|
||||
StartTime float32 `json:"start_time"`
|
||||
Duration float32 `json:"duration"`
|
||||
IsKey bool `json:"is_key"`
|
||||
DataOffset uint32 `json:"data_offset"`
|
||||
}
|
31
pkg/ivideon/streamer.go
Normal file
31
pkg/ivideon/streamer.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package ivideon
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
)
|
||||
|
||||
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 {
|
||||
err := c.Handle()
|
||||
if c.closed {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Client) Stop() error {
|
||||
return c.Close()
|
||||
}
|
@@ -86,7 +86,7 @@ func (c *Consumer) MimeType() string {
|
||||
return c.muxer.MimeType(c.codecs)
|
||||
}
|
||||
|
||||
func (c *Consumer) Init() []byte {
|
||||
func (c *Consumer) Init() ([]byte, error) {
|
||||
if c.muxer == nil {
|
||||
c.muxer = &Muxer{}
|
||||
}
|
||||
|
@@ -2,6 +2,7 @@ package mp4
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"github.com/AlexxIT/go2rtc/pkg/h264"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"github.com/deepch/vdk/codec/h264parser"
|
||||
@@ -32,18 +33,21 @@ func (m *Muxer) MimeType(codecs []*streamer.Codec) string {
|
||||
return s + `"`
|
||||
}
|
||||
|
||||
func (m *Muxer) GetInit(codecs []*streamer.Codec) []byte {
|
||||
func (m *Muxer) GetInit(codecs []*streamer.Codec) ([]byte, error) {
|
||||
moov := MOOV()
|
||||
|
||||
for _, codec := range codecs {
|
||||
switch codec.Name {
|
||||
case streamer.CodecH264:
|
||||
sps, pps := h264.GetParameterSet(codec.FmtpLine)
|
||||
if sps == nil {
|
||||
return nil, fmt.Errorf("empty SPS: %#v", codec)
|
||||
}
|
||||
|
||||
// TODO: remove
|
||||
codecData, err := h264parser.NewCodecDataFromSPSAndPPS(sps, pps)
|
||||
if err != nil {
|
||||
return nil
|
||||
return nil, err
|
||||
}
|
||||
|
||||
width := codecData.Width()
|
||||
@@ -83,7 +87,7 @@ func (m *Muxer) GetInit(codecs []*streamer.Codec) []byte {
|
||||
data := make([]byte, moov.Len())
|
||||
moov.Marshal(data)
|
||||
|
||||
return append(FTYP(), data...)
|
||||
return append(FTYP(), data...), nil
|
||||
}
|
||||
|
||||
func (m *Muxer) Rewind() {
|
||||
@@ -121,13 +125,15 @@ func (m *Muxer) Marshal(packet *rtp.Packet) []byte {
|
||||
}
|
||||
|
||||
entry := mp4io.TrackFragRunEntry{
|
||||
Duration: 90000,
|
||||
//Duration: 90000,
|
||||
Size: uint32(len(packet.Payload)),
|
||||
}
|
||||
|
||||
newTime := packet.Timestamp
|
||||
if m.pts > 0 {
|
||||
m.dts += uint64(newTime - m.pts)
|
||||
//m.dts += uint64(newTime - m.pts)
|
||||
entry.Duration = newTime - m.pts
|
||||
m.dts += uint64(entry.Duration)
|
||||
}
|
||||
m.pts = newTime
|
||||
|
||||
|
@@ -2,7 +2,6 @@ package rtmp
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"github.com/AlexxIT/go2rtc/pkg/h264"
|
||||
@@ -134,7 +133,7 @@ func (c *Client) Handle() (err error) {
|
||||
|
||||
var payloads [][]byte
|
||||
if track.Codec.Name == streamer.CodecH264 {
|
||||
payloads = splitAVC(pkt.Data)
|
||||
payloads = h264.SplitAVC(pkt.Data)
|
||||
} else {
|
||||
payloads = [][]byte{pkt.Data}
|
||||
}
|
||||
@@ -156,21 +155,3 @@ func (c *Client) Close() error {
|
||||
c.closed = true
|
||||
return c.conn.Close()
|
||||
}
|
||||
|
||||
func splitAVC(data []byte) [][]byte {
|
||||
var nals [][]byte
|
||||
for {
|
||||
// get AVC length
|
||||
size := int(binary.BigEndian.Uint32(data))
|
||||
|
||||
// check if multiple items in one packet
|
||||
if size+4 < len(data) {
|
||||
nals = append(nals, data[:size+4])
|
||||
data = data[size+4:]
|
||||
} else {
|
||||
nals = append(nals, data)
|
||||
break
|
||||
}
|
||||
}
|
||||
return nals
|
||||
}
|
||||
|
@@ -61,10 +61,10 @@ type Conn struct {
|
||||
|
||||
auth *tcp.Auth
|
||||
conn net.Conn
|
||||
mode Mode
|
||||
reader *bufio.Reader
|
||||
sequence int
|
||||
|
||||
mode Mode
|
||||
uri string
|
||||
|
||||
tracks []*streamer.Track
|
||||
channels map[byte]*streamer.Track
|
||||
@@ -76,24 +76,10 @@ type Conn struct {
|
||||
}
|
||||
|
||||
func NewClient(uri string) (*Conn, error) {
|
||||
var err error
|
||||
|
||||
c := new(Conn)
|
||||
c.URL, err = url.Parse(uri)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if strings.IndexByte(c.URL.Host, ':') < 0 {
|
||||
c.URL.Host += ":554"
|
||||
}
|
||||
|
||||
// remove UserInfo from URL
|
||||
c.auth = tcp.NewAuth(c.URL.User)
|
||||
c.mode = ModeClientProducer
|
||||
c.URL.User = nil
|
||||
|
||||
return c, nil
|
||||
c.uri = uri
|
||||
return c, c.parseURI()
|
||||
}
|
||||
|
||||
func NewServer(conn net.Conn) *Conn {
|
||||
@@ -104,12 +90,29 @@ func NewServer(conn net.Conn) *Conn {
|
||||
return c
|
||||
}
|
||||
|
||||
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 {
|
||||
c.URL.Host += ":554"
|
||||
}
|
||||
|
||||
// remove UserInfo from URL
|
||||
c.auth = tcp.NewAuth(c.URL.User)
|
||||
c.URL.User = nil
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Conn) Dial() (err error) {
|
||||
//if c.state != StateClientInit {
|
||||
// panic("wrong state")
|
||||
//}
|
||||
if c.conn != nil && c.auth != nil {
|
||||
c.auth.Reset()
|
||||
if c.conn != nil {
|
||||
_ = c.parseURI()
|
||||
}
|
||||
|
||||
c.conn, err = net.DialTimeout(
|
||||
@@ -359,7 +362,21 @@ func (c *Conn) SetupMedia(
|
||||
var res *tcp.Response
|
||||
res, err = c.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
// Dahua VTO2111D fail on this step because of backchannel
|
||||
if c.Backchannel {
|
||||
if err = c.Dial(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.Backchannel = false
|
||||
if err = c.Describe(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res, err = c.Do(req)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if c.Session == "" {
|
||||
|
41
pkg/shell/shell.go
Normal file
41
pkg/shell/shell.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package shell
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
func QuoteSplit(s string) []string {
|
||||
var a []string
|
||||
|
||||
for len(s) > 0 {
|
||||
is := strings.IndexByte(s, ' ')
|
||||
if is >= 0 {
|
||||
// skip prefix and double spaces
|
||||
if is == 0 {
|
||||
// goto next symbol
|
||||
s = s[1:]
|
||||
continue
|
||||
}
|
||||
|
||||
// check if quote in word
|
||||
if i := strings.IndexByte(s[:is], '"'); i >= 0 {
|
||||
// search quote end
|
||||
if is = strings.Index(s, `" `); is > 0 {
|
||||
is += 1
|
||||
} else {
|
||||
is = -1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if is >= 0 {
|
||||
a = append(a, strings.ReplaceAll(s[:is], `"`, ""))
|
||||
s = s[is+1:]
|
||||
} else {
|
||||
//add last word
|
||||
a = append(a, s)
|
||||
break
|
||||
}
|
||||
}
|
||||
return a
|
||||
}
|
@@ -80,12 +80,6 @@ func (a *Auth) Write(req *Request) {
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Auth) Reset() {
|
||||
if a.Method == AuthDigest {
|
||||
a.Method = AuthUnknown
|
||||
}
|
||||
}
|
||||
|
||||
func Between(s, sub1, sub2 string) string {
|
||||
i := strings.Index(s, sub1)
|
||||
if i < 0 {
|
||||
|
Reference in New Issue
Block a user