Compare commits

...

25 Commits

Author SHA1 Message Date
Alexey Khit
cecbe4166c Update version to 0.1-rc.9 2023-01-16 00:06:55 +03:00
Alexey Khit
dcb457235c Rewrite stream info API 2023-01-15 23:51:20 +03:00
Alexey Khit
bc4e032830 Update readme 2023-01-15 11:13:38 +03:00
Alexey Khit
8218cda149 Add version, config_path to web UI and fix RTSP link 2023-01-15 09:57:15 +03:00
Alexey Khit
d1e56feeb6 Update full path to config file 2023-01-15 09:55:32 +03:00
Alexey Khit
463d05dfd3 Update readme 2023-01-15 00:28:48 +03:00
Alexey Khit
a1a73f7b45 Rewrite WS+MP4 format to keyframes stream 2023-01-15 00:12:26 +03:00
Alexey Khit
39662e10af Fix errors in JS player 2023-01-15 00:11:31 +03:00
Alexey Khit
1c830d6e60 Code refactoring 2023-01-14 22:49:12 +03:00
Alex X
2039aa60b3 Merge pull request #170 from skrashevich/config-api-patch-method
PATH api/config method for merge configuration
2023-01-14 21:57:34 +03:00
Sergey Krashevich
b7016e798f Update config.go 2023-01-14 21:27:23 +03:00
Alexey Khit
0b291f5185 Support multiple configs and config in raw yaml form 2023-01-14 21:12:17 +03:00
Alexey Khit
395304654a Code refactoring 2023-01-14 19:15:13 +03:00
Alexey Khit
e472397705 Add general info API 2023-01-14 18:00:43 +03:00
Alexey Khit
7c1f48e0ad Support empty default environment value 2023-01-14 17:25:05 +03:00
Alexey Khit
f4346a104f Add support env variables in config file #143 2023-01-14 17:19:51 +03:00
Alexey Khit
030972b436 Auto build binaries on release #158 2023-01-14 14:14:23 +03:00
Alexey Khit
efddefa123 Add web config editor #153 2023-01-14 13:47:34 +03:00
Alexey Khit
3c1bdd0dab Fix WebRTC candidate type 2023-01-14 09:45:03 +03:00
Alexey Khit
7e7e15d7c8 Update readme 2023-01-14 09:22:22 +03:00
Alex X
a1a9f77535 Merge pull request #167 from felipecrs/master
Match docs with new webrtc udp fixed port
2023-01-14 09:10:46 +03:00
Alexey Khit
a06462729d Code refactoring 2023-01-14 09:04:54 +03:00
Alex X
331c5bbcad Merge pull request #166 from tsightler/udp-candidate-fix
Fix invalid tcpType for UDP candidate
2023-01-14 08:59:25 +03:00
Felipe Santos
58a76efc8a Match docs with new webrtc udp fixed port 2023-01-13 23:15:04 -03:00
tsightler
5e0f010885 Update helper.go 2023-01-13 18:18:39 -05:00
39 changed files with 713 additions and 289 deletions

View File

@@ -1,4 +1,4 @@
name: ci
name: docker
on:
workflow_dispatch:
@@ -19,7 +19,7 @@ jobs:
id: meta
uses: docker/metadata-action@v4
with:
images: alexxit/go2rtc
images: ${{ github.repository }}
tags: |
type=ref,event=branch
type=semver,pattern={{version}},enable=false
@@ -29,7 +29,7 @@ jobs:
id: meta-hw
uses: docker/metadata-action@v4
with:
images: alexxit/go2rtc
images: ${{ github.repository }}
flavor: |
suffix=-hardware
latest=false

90
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,90 @@
name: release
on:
workflow_dispatch:
push:
tags:
- 'v*'
jobs:
build-and-release:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Generate changelog
run: |
echo -e "$(git log $(git describe --tags --abbrev=0)..HEAD --oneline | awk '{print "- "$0}')" > CHANGELOG.md
- name: Build Go binaries
run: |
#!/bin/bash
mkdir artifacts
export GOOS=windows
export GOARCH=amd64
export FILENAME=artifacts/go2rtc_win64.zip
go build -ldflags "-s -w" -trimpath && 7z a -mx9 -sdel "$FILENAME" go2rtc.exe
export GOOS=windows
export GOARCH=386
export FILENAME=artifacts/go2rtc_win32.zip
go build -ldflags "-s -w" -trimpath && 7z a -mx9 -sdel "$FILENAME" go2rtc.exe
export GOOS=windows
export GOARCH=arm64
export FILENAME=artifacts/go2rtc_win_arm64.zip
go build -ldflags "-s -w" -trimpath && 7z a -mx9 -sdel "$FILENAME" go2rtc.exe
export GOOS=linux
export GOARCH=amd64
export FILENAME=artifacts/go2rtc_linux_amd64
go build -ldflags "-s -w" -trimpath -o "$FILENAME"
export GOOS=linux
export GOARCH=386
export FILENAME=artifacts/go2rtc_linux_i386
go build -ldflags "-s -w" -trimpath -o "$FILENAME"
export GOOS=linux
export GOARCH=arm64
export FILENAME=artifacts/go2rtc_linux_arm64
go build -ldflags "-s -w" -trimpath -o "$FILENAME"
export GOOS=linux
export GOARCH=arm
export GOARM=7
export FILENAME=artifacts/go2rtc_linux_arm
go build -ldflags "-s -w" -trimpath -o "$FILENAME"
export GOOS=linux
export GOARCH=mipsle
export FILENAME=artifacts/go2rtc_linux_mipsel
go build -ldflags "-s -w" -trimpath -o "$FILENAME"
export GOOS=darwin
export GOARCH=amd64
export FILENAME=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
go build -ldflags "-s -w" -trimpath && 7z a -mx9 -sdel "$FILENAME" go2rtc
parallel --jobs $(nproc) "upx {}" ::: artifacts/go2rtc_linux_*
- name: Setup tmate session
uses: mxschmitt/action-tmate@v3
if: ${{ failure() }}
- name: Set env
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
- name: Create GitHub release
uses: softprops/action-gh-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
files: artifacts/*
generate_release_notes: true
name: Release ${{ env.RELEASE_VERSION }}
body_path: CHANGELOG.md
draft: false
prerelease: false

View File

@@ -9,7 +9,7 @@ Ultimate camera streaming application with support RTSP, WebRTC, HomeKit, FFmpeg
- streaming from [RTSP](#source-rtsp), [RTMP](#source-rtmp), [HTTP](#source-http) (FLV/MJPEG/JPEG), [FFmpeg](#source-ffmpeg), [USB Cameras](#source-ffmpeg-device) and [other sources](#module-streams)
- streaming to [RTSP](#module-rtsp), [WebRTC](#module-webrtc), [MSE/MP4](#module-mp4) or [MJPEG](#module-mjpeg)
- first project in the World with support streaming from [HomeKit Cameras](#source-homekit)
- first project in the World with support H265 for WebRTC in browser ([read more](https://github.com/AlexxIT/Blog/issues/5))
- first project in the World with support H265 for WebRTC in browser (Safari only, [read more](https://github.com/AlexxIT/Blog/issues/5))
- 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
@@ -37,7 +37,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
- install [ffmpeg](#source-ffmpeg) for transcoding
**Developers:**
@@ -50,7 +49,6 @@ Download binary for your OS from [latest release](https://github.com/AlexxIT/go2
- `go2rtc_win64.zip` - Windows 64-bit
- `go2rtc_win32.zip` - Windows 32-bit
- `go2rtc_win_arm64.zip` - Windows ARM 64-bit
- `go2rtc_linux_amd64` - Linux 64-bit
- `go2rtc_linux_i386` - Linux 32-bit
- `go2rtc_linux_arm64` - Linux ARM 64-bit (ex. Raspberry 64-bit OS)
@@ -72,27 +70,17 @@ 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), [Ngrok](#module-ngrok) and [Python](#source-echo).
```yaml
services:
go2rtc:
image: alexxit/go2rtc
network_mode: host
restart: always
volumes:
- "~/go2rtc.yaml:/config/go2rtc.yaml"
```
Container [alexxit/go2rtc](https://hub.docker.com/r/alexxit/go2rtc) with support `amd64`, `386`, `arm64`, `arm`. This container is the same as [Home Assistant Add-on](#go2rtc-home-assistant-add-on), but can be used separately from Home Assistant. Container has preinstalled [FFmpeg](#source-ffmpeg), [Ngrok](#module-ngrok) and [Python](#source-echo).
## Configuration
Create file `go2rtc.yaml` next to the app.
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
- `api` server will start on default **1984 port**
- `rtsp` server will start on default **8554 port**
- `webrtc` will use random UDP port for each connection
- `ffmpeg` will use default transcoding options (you may install it [manually](https://ffmpeg.org/))
- `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
Available modules:
@@ -107,6 +95,8 @@ 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.
@@ -216,7 +206,7 @@ But you can override them via YAML config. You can also add your own formats to
```yaml
ffmpeg:
bin: ffmpeg # path to ffmpeg binary
bin: ffmpeg # path to ffmpeg binary
h264: "-codec:v libx264 -g:v 30 -preset:v superfast -tune:v zerolatency -profile:v main -level:v 4.1"
mycodec: "-any args that support ffmpeg..."
```
@@ -224,8 +214,11 @@ ffmpeg:
- You can use `video` and `audio` params multiple times (ex. `#video=copy#audio=copy#audio=pcmu`)
- You can use go2rtc stream name as ffmpeg input (ex. `ffmpeg:camera1#video=h264`)
- You can use `rotate` params with `90`, `180`, `270` or `-90` values, important with transcoding (ex. `#video=h264#rotate=90`)
- You can use `width` and/or `height` params, important with transcoding (ex. `#video=h264#width=1280`)
- You can use `raw` param for any additional FFmpeg arguments (ex. `#raw=-vf transpose=1`).
Read more about encoding [hardware acceleration](https://github.com/AlexxIT/go2rtc/wiki/Hardware-acceleration).
#### Source: FFmpeg Device
You can get video from any USB-camera or Webcam as RTSP or WebRTC stream. This is part of FFmpeg integration.
@@ -371,13 +364,12 @@ api:
origin: "*" # default "", allow CORS requests (only * supported)
```
**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.
**PS:**
**PS2.** 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)).
**PS3.** MJPEG over WebSocket plays better than native MJPEG because Chrome [bug](https://bugs.chromium.org/p/chromium/issues/detail?id=527446).
**PS4.** MP4 over WebSocket was created only for Apple iOS because it doesn't support MSE and native MP4.
- 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
- 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
### Module: RTSP
@@ -401,45 +393,43 @@ rtsp:
WebRTC usually works without problems in the local network. But external access may require additional settings. It depends on what type of Internet do you have.
- by default, WebRTC use two random UDP ports for each connection (video and audio)
- you can enable one additional TCP port for all connections and use it for external access
- by default, WebRTC uses both TCP and UDP on port 8555 for connections
- you can use this port for external access
- you can change the port in YAML config:
```yaml
webrtc:
listen: ":8555" # address of your local server and port (TCP/UDP)
```
**Static public IP**
- add some TCP port to YAML config (ex. 8555)
- forward this port on your router (you can use same 8555 port or any other)
- forward the port 8555 on your router (you can use same 8555 port or any other as external port)
- add your external IP-address and external port to YAML config
```yaml
webrtc:
listen: ":8555" # address of your local server (TCP)
candidates:
- 216.58.210.174:8555 # if you have static public IP-address
```
**Dynamic public IP**
- add some TCP port to YAML config (ex. 8555)
- forward this port on your router (you can use same 8555 port or any other)
- forward the port 8555 on your router (you can use same 8555 port or any other as the external port)
- add `stun` word and external port to YAML config
- go2rtc automatically detects your external address with STUN-server
```yaml
webrtc:
listen: ":8555" # address of your local server (TCP)
candidates:
- stun:8555 # if you have dynamic public IP-address
```
**Private IP**
- add some TCP port to YAML config (ex. 8555)
- setup integration with [Ngrok service](#module-ngrok)
```yaml
webrtc:
listen: ":8555" # address of your local server (TCP)
ngrok:
command: ...
```
@@ -550,8 +540,13 @@ PS. Default Home Assistant lovelace cards don't support 2-way audio. You can use
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. MP4 "file stream" - bad format for streaming because of high latency, doesn't work in Safari
2. Camera snapshots in MP4 format (single frame), can be sent to [Telegram](https://github.com/AlexxIT/go2rtc/wiki/Snapshot-to-Telegram)
3. MP4 "file stream" - bad format for streaming because of high start delay, doesn't work in Safari
API examples:
- MP4 stream: `http://192.168.1.123:1984/api/stream.mp4?src=camera1`
- MP4 snapshot: `http://192.168.1.123:1984/api/frame.mp4?src=camera1`
### Module: MJPEG
@@ -595,7 +590,7 @@ log:
## Security
By default `go2rtc` start Web interface on port `1984` and RTSP on port `8554`. Both ports are accessible from your local network. So anyone on your local network can watch video from your cameras without authorization. The same rule applies to the Home Assistant Add-on.
By default `go2rtc` starts the Web interface on port `1984` and RTSP on port `8554`, as well as use port `8555` for WebRTC connections. The three ports are accessible from your local network. So anyone on your local network can watch video from your cameras without authorization. The same rule applies to the Home Assistant Add-on.
This is not a problem if you trust your local network as much as I do. But you can change this behaviour with a `go2rtc.yaml` config:
@@ -607,7 +602,7 @@ rtsp:
listen: "127.0.0.1:8554" # localhost
webrtc:
listen: ":8555" # external TCP port
listen: ":8555" # external TCP/UDP port
```
- local access to RTSP is not a problem for [FFmpeg](#source-ffmpeg) integration, because it runs locally on your server
@@ -617,7 +612,7 @@ webrtc:
If you need Web interface protection without Home Assistant Add-on - you need to use reverse proxy, like [Nginx](https://nginx.org/), [Caddy](https://caddyserver.com/), [Ngrok](https://ngrok.com/), etc.
PS. Additionally WebRTC opens a lot of random UDP ports for transmit encrypted media. They work without problems on the local network. And sometimes work for external access, even if you haven't opened ports on your router. But for stable external WebRTC access, you need to configure the TCP port.
PS. Additionally WebRTC will try to use the 8555 UDP port for transmit encrypted media. It works without problems on the local network. And sometimes also works for external access, even if you haven't opened this port on your router ([read more](https://en.wikipedia.org/wiki/UDP_hole_punching)). But for stable external WebRTC access, you need to open the 8555 port on your router for both TCP and UDP.
## Codecs madness
@@ -687,6 +682,10 @@ streams:
- `ffplay -fflags nobuffer -flags low_delay "rtsp://192.168.1.123:8554/camera1"`
- VLC > Preferences > Input / Codecs > Default Caching Level: Lowest Latency
**Snapshots to Telegram**
[read more](https://github.com/AlexxIT/go2rtc/wiki/Snapshot-to-Telegram)
## FAQ
**Q. What's the difference between go2rtc, WebRTC Camera and RTSPtoWebRTC?**

View File

@@ -3,10 +3,12 @@ package api
import (
"encoding/json"
"github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/rs/zerolog"
"net"
"net/http"
"os"
"strconv"
"sync"
)
func Init() {
@@ -35,7 +37,9 @@ func Init() {
initStatic(cfg.Mod.StaticDir)
initWS(cfg.Mod.Origin)
HandleFunc("api/streams", streamsHandler)
HandleFunc("api", apiHandler)
HandleFunc("api/config", configHandler)
HandleFunc("api/exit", exitHandler)
HandleFunc("api/ws", apiWS)
// ensure we can listen without errors
@@ -96,31 +100,25 @@ func middlewareCORS(next http.Handler) http.Handler {
})
}
func streamsHandler(w http.ResponseWriter, r *http.Request) {
src := r.URL.Query().Get("src")
name := r.URL.Query().Get("name")
var mu sync.Mutex
if name == "" {
name = src
func apiHandler(w http.ResponseWriter, r *http.Request) {
mu.Lock()
app.Info["host"] = r.Host
mu.Unlock()
if err := json.NewEncoder(w).Encode(app.Info); err != nil {
log.Warn().Err(err).Caller().Send()
}
switch r.Method {
case "PUT":
streams.New(name, src)
return
case "DELETE":
streams.Delete(src)
return
}
var v interface{}
if src != "" {
v = streams.Get(src)
} else {
v = streams.All()
}
e := json.NewEncoder(w)
e.SetIndent("", " ")
_ = e.Encode(v)
}
func exitHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
http.Error(w, "", http.StatusBadRequest)
return
}
s := r.URL.Query().Get("code")
code, _ := strconv.Atoi(s)
os.Exit(code)
}

97
cmd/api/config.go Normal file
View File

@@ -0,0 +1,97 @@
package api
import (
"github.com/AlexxIT/go2rtc/cmd/app"
"gopkg.in/yaml.v3"
"io"
"net/http"
"os"
)
func configHandler(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case "GET":
data, err := os.ReadFile(app.ConfigPath)
if err != nil {
http.NotFound(w, r)
return
}
if _, err = w.Write(data); err != nil {
log.Warn().Err(err).Caller().Send()
}
case "POST", "PATCH":
data, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if r.Method == "PATCH" {
// no need to validate after merge
data, err = mergeYAML(app.ConfigPath, data)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
} else {
// validate config
var tmp struct{}
if err = yaml.Unmarshal(data, &tmp); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
if err = os.WriteFile(app.ConfigPath, data, 0644); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
}
func mergeYAML(file1 string, yaml2 []byte) ([]byte, error) {
// Read the contents of the first YAML file
data1, err := os.ReadFile(file1)
if err != nil {
return nil, err
}
// Unmarshal the first YAML file into a map
var config1 map[string]interface{}
if err = yaml.Unmarshal(data1, &config1); err != nil {
return nil, err
}
// Unmarshal the second YAML document into a map
var config2 map[string]interface{}
if err = yaml.Unmarshal(yaml2, &config2); err != nil {
return nil, err
}
// Merge the two maps
config1 = merge(config1, config2)
// Marshal the merged map into YAML
return yaml.Marshal(&config1)
}
func merge(dst, src map[string]interface{}) map[string]interface{} {
for k, v := range src {
if vv, ok := dst[k]; ok {
switch vv := vv.(type) {
case map[string]interface{}:
v := v.(map[string]interface{})
dst[k] = merge(vv, v)
case []interface{}:
v := v.([]interface{})
dst[k] = v
default:
dst[k] = v
}
} else {
dst[k] = v
}
}
return dst
}

View File

@@ -2,46 +2,73 @@ package app
import (
"flag"
"github.com/AlexxIT/go2rtc/pkg/shell"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"gopkg.in/yaml.v3"
"io"
"os"
"path"
"runtime"
"strings"
)
var Version = "0.1-rc.8"
var Version = "0.1-rc.9"
var UserAgent = "go2rtc/" + Version
func Init() {
config := flag.String(
"config",
"go2rtc.yaml",
"Path to go2rtc configuration file",
)
var ConfigPath string
var Info = map[string]interface{}{
"version": Version,
}
func Init() {
var confs Config
flag.Var(&confs, "config", "go2rtc config (path to file or raw text), support multiple")
flag.Parse()
data, _ = os.ReadFile(*config)
if confs == nil {
confs = []string{"go2rtc.yaml"}
}
for _, conf := range confs {
if conf[0] != '{' {
// config as file
if ConfigPath == "" {
ConfigPath = conf
}
data, _ := os.ReadFile(conf)
if data == nil {
continue
}
data = []byte(shell.ReplaceEnvVars(string(data)))
configs = append(configs, data)
} else {
// config as raw YAML
configs = append(configs, []byte(conf))
}
}
if ConfigPath != "" {
if cwd, err := os.Getwd(); err == nil {
ConfigPath = path.Join(cwd, ConfigPath)
}
Info["config_path"] = ConfigPath
}
var cfg struct {
Mod map[string]string `yaml:"log"`
}
if data != nil {
if err := yaml.Unmarshal(data, &cfg); err != nil {
println("ERROR: " + err.Error())
}
}
LoadConfig(&cfg)
log.Logger = NewLogger(cfg.Mod["format"], cfg.Mod["level"])
modules = cfg.Mod
log.Info().Msgf("go2rtc version %s %s/%s", Version, runtime.GOOS, runtime.GOARCH)
path, _ := os.Getwd()
log.Debug().Str("cwd", path).Send()
}
func NewLogger(format string, level string) zerolog.Logger {
@@ -65,7 +92,7 @@ func NewLogger(format string, level string) zerolog.Logger {
}
func LoadConfig(v interface{}) {
if data != nil {
for _, data := range configs {
if err := yaml.Unmarshal(data, v); err != nil {
log.Warn().Err(err).Msg("[app] read config")
}
@@ -86,8 +113,18 @@ func GetLogger(module string) zerolog.Logger {
// internal
// data - config content
var data []byte
type Config []string
func (c *Config) String() string {
return strings.Join(*c, " ")
}
func (c *Config) Set(value string) error {
*c = append(*c, value)
return nil
}
var configs [][]byte
// modules log levels
var modules map[string]string

View File

@@ -4,24 +4,14 @@ import (
"github.com/AlexxIT/go2rtc/cmd/api"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"net/http"
"os"
"strconv"
)
func Init() {
api.HandleFunc("api/stack", stackHandler)
api.HandleFunc("api/exit", exitHandler)
streams.HandleFunc("null", nullHandler)
}
func exitHandler(_ http.ResponseWriter, r *http.Request) {
s := r.URL.Query().Get("code")
code, _ := strconv.Atoi(s)
os.Exit(code)
}
func nullHandler(string) (streamer.Producer, error) {
return nil, nil
}

View File

@@ -27,7 +27,10 @@ func handlerKeyframe(w http.ResponseWriter, r *http.Request) {
exit := make(chan []byte)
cons := &mjpeg.Consumer{}
cons := &mjpeg.Consumer{
RemoteAddr: r.RemoteAddr,
UserAgent: r.UserAgent(),
}
cons.Listen(func(msg interface{}) {
switch msg := msg.(type) {
case []byte:
@@ -68,7 +71,10 @@ func handlerStream(w http.ResponseWriter, r *http.Request) {
flusher := w.(http.Flusher)
cons := &mjpeg.Consumer{}
cons := &mjpeg.Consumer{
RemoteAddr: r.RemoteAddr,
UserAgent: r.UserAgent(),
}
cons.Listen(func(msg interface{}) {
switch msg := msg.(type) {
case []byte:
@@ -109,7 +115,10 @@ func handlerWS(tr *api.Transport, _ *api.Message) error {
return errors.New(api.StreamNotFound)
}
cons := &mjpeg.Consumer{}
cons := &mjpeg.Consumer{
RemoteAddr: tr.Request.RemoteAddr,
UserAgent: tr.Request.UserAgent(),
}
cons.Listen(func(msg interface{}) {
if data, ok := msg.([]byte); ok {
tr.Write(data)

View File

@@ -80,7 +80,10 @@ func handlerMP4(w http.ResponseWriter, r *http.Request) {
exit := make(chan error)
cons := &mp4.Consumer{}
cons := &mp4.Consumer{
RemoteAddr: r.RemoteAddr,
UserAgent: r.UserAgent(),
}
cons.Listen(func(msg interface{}) {
if data, ok := msg.([]byte); ok {
if _, err := w.Write(data); err != nil && exit != nil {

View File

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

View File

@@ -14,9 +14,9 @@ import (
func Init() {
var conf struct {
Mod struct {
Listen string `yaml:"listen"`
Username string `yaml:"username"`
Password string `yaml:"password"`
Listen string `yaml:"listen" json:"listen"`
Username string `yaml:"username" json:"-"`
Password string `yaml:"password" json:"-"`
} `yaml:"rtsp"`
}
@@ -24,6 +24,7 @@ func Init() {
conf.Mod.Listen = ":8554"
app.LoadConfig(&conf)
app.Info["rtsp"] = conf.Mod
log = app.GetLogger("rtsp")

15
cmd/streams/consumer.go Normal file
View File

@@ -0,0 +1,15 @@
package streams
import (
"encoding/json"
"github.com/AlexxIT/go2rtc/pkg/streamer"
)
type Consumer struct {
element streamer.Consumer
tracks []*streamer.Track
}
func (c *Consumer) MarshalJSON() ([]byte, error) {
return json.Marshal(c.element)
}

View File

@@ -1,6 +1,7 @@
package streams
import (
"encoding/json"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"strings"
"sync"
@@ -91,6 +92,15 @@ func (p *Producer) GetTrack(media *streamer.Media, codec *streamer.Codec) *strea
return track
}
func (p *Producer) MarshalJSON() ([]byte, error) {
if p.element != nil {
return json.Marshal(p.element)
}
info := streamer.Info{URL: p.url}
return json.Marshal(info)
}
// internals
func (p *Producer) start() {

View File

@@ -10,11 +10,6 @@ import (
"sync/atomic"
)
type Consumer struct {
element streamer.Consumer
tracks []*streamer.Track
}
type Stream struct {
producers []*Producer
consumers []*Consumer
@@ -199,24 +194,19 @@ producers:
func (s *Stream) MarshalJSON() ([]byte, error) {
if !s.mu.TryLock() {
log.Warn().Msgf("[streams] json locked")
return []byte(`null`), nil
return json.Marshal(nil)
}
var v []interface{}
for _, prod := range s.producers {
if prod.element != nil {
v = append(v, prod.element)
}
}
for _, cons := range s.consumers {
// cons.element always not nil
v = append(v, cons.element)
var info struct {
Producers []*Producer `json:"producers"`
Consumers []*Consumer `json:"consumers"`
}
info.Producers = s.producers
info.Consumers = s.consumers
s.mu.Unlock()
if len(v) == 0 {
v = nil
}
return json.Marshal(v)
return json.Marshal(info)
}
func (s *Stream) removeConsumer(i int) {

View File

@@ -1,9 +1,12 @@
package streams
import (
"encoding/json"
"github.com/AlexxIT/go2rtc/cmd/api"
"github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/cmd/app/store"
"github.com/rs/zerolog"
"net/http"
)
func Init() {
@@ -22,6 +25,8 @@ func Init() {
for name, item := range store.GetDict("streams") {
streams[name] = NewStream(item)
}
api.HandleFunc("api/streams", streamsHandler)
}
func Get(name string) *Stream {
@@ -48,19 +53,29 @@ func GetOrNew(src string) *Stream {
return New(src, src)
}
func Delete(name string) {
delete(streams, name)
}
func streamsHandler(w http.ResponseWriter, r *http.Request) {
src := r.URL.Query().Get("src")
func All() map[string]interface{} {
all := map[string]interface{}{}
for name, stream := range streams {
all[name] = stream
//if stream.Active() {
// all[name] = stream
//}
switch r.Method {
case "PUT":
name := r.URL.Query().Get("name")
if name == "" {
name = src
}
New(name, src)
return
case "DELETE":
delete(streams, src)
return
}
if src != "" {
e := json.NewEncoder(w)
e.SetIndent("", " ")
_ = e.Encode(streams[src])
} else {
_ = json.NewEncoder(w).Encode(streams)
}
return all
}
var log zerolog.Logger

View File

@@ -70,7 +70,7 @@ func RTPDepay(track *streamer.Track) streamer.WrapperFunc {
buf = buf[:0]
}
//log.Printf("[AVC] %v, len: %d", Types(payload), len(payload))
//log.Printf("[AVC] %v, len: %d, ts: %10d, seq: %d", Types(payload), len(payload), packet.Timestamp, packet.SequenceNumber)
clone := *packet
clone.Version = RTPPacketVersionAVC

View File

@@ -1,6 +1,7 @@
package homekit
import (
"encoding/json"
"errors"
"fmt"
"github.com/AlexxIT/go2rtc/pkg/hap"
@@ -11,6 +12,7 @@ import (
"github.com/brutella/hap/rtp"
"net"
"net/url"
"sync/atomic"
)
type Client struct {
@@ -263,3 +265,19 @@ func (c *Client) getMedias() []*streamer.Media {
return medias
}
func (c *Client) MarshalJSON() ([]byte, error) {
var recv uint32
for _, session := range c.sessions {
recv += atomic.LoadUint32(&session.Recv)
}
info := &streamer.Info{
Type: "HomeKit source",
URL: c.conn.URL(),
Medias: c.medias,
Tracks: c.tracks,
Recv: recv,
}
return json.Marshal(info)
}

View File

@@ -15,6 +15,7 @@ import (
"net/http"
"strings"
"sync"
"sync/atomic"
"time"
)
@@ -41,6 +42,8 @@ type Client struct {
buffer chan []byte
state State
mu sync.Mutex
recv uint32
}
func NewClient(id string) *Client {
@@ -109,6 +112,7 @@ func (c *Client) Handle() error {
c.mu.Lock()
if c.state == StateHandle {
c.buffer <- data
atomic.AddUint32(&c.recv, uint32(len(data)))
}
c.mu.Unlock()
}
@@ -140,6 +144,7 @@ func (c *Client) Handle() error {
c.mu.Lock()
if c.state == StateHandle {
c.buffer <- data
atomic.AddUint32(&c.recv, uint32(len(data)))
}
c.mu.Unlock()
}

View File

@@ -1,8 +1,10 @@
package ivideon
import (
"encoding/json"
"fmt"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"sync/atomic"
)
func (c *Client) GetMedias() []*streamer.Media {
@@ -29,3 +31,19 @@ func (c *Client) Start() error {
func (c *Client) Stop() error {
return c.Close()
}
func (c *Client) MarshalJSON() ([]byte, error) {
var tracks []*streamer.Track
for _, track := range c.tracks {
tracks = append(tracks, track)
}
info := &streamer.Info{
Type: "Ivideon source",
URL: c.ID,
Medias: c.medias,
Tracks: tracks,
Recv: atomic.LoadUint32(&c.recv),
}
return json.Marshal(info)
}

View File

@@ -2,6 +2,7 @@ package mjpeg
import (
"bufio"
"encoding/json"
"errors"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/AlexxIT/go2rtc/pkg/tcp"
@@ -11,6 +12,7 @@ import (
"net/textproto"
"strconv"
"strings"
"sync/atomic"
"time"
)
@@ -24,6 +26,7 @@ type Client struct {
res *http.Response
track *streamer.Track
recv uint32
}
func NewClient(res *http.Response) *Client {
@@ -70,6 +73,17 @@ func (c *Client) Stop() error {
return nil
}
func (c *Client) MarshalJSON() ([]byte, error) {
info := &streamer.Info{
Type: "MJPEG source",
URL: c.res.Request.URL.String(),
RemoteAddr: c.RemoteAddr,
UserAgent: c.UserAgent,
Recv: atomic.LoadUint32(&c.recv),
}
return json.Marshal(info)
}
func (c *Client) startJPEG() error {
buf, err := io.ReadAll(c.res.Body)
if err != nil {
@@ -79,6 +93,8 @@ func (c *Client) startJPEG() error {
packet := &rtp.Packet{Header: rtp.Header{Timestamp: now()}, Payload: buf}
_ = c.track.WriteRTP(packet)
atomic.AddUint32(&c.recv, uint32(len(buf)))
req := c.res.Request
for !c.closed {
@@ -98,6 +114,8 @@ func (c *Client) startJPEG() error {
packet = &rtp.Packet{Header: rtp.Header{Timestamp: now()}, Payload: buf}
_ = c.track.WriteRTP(packet)
atomic.AddUint32(&c.recv, uint32(len(buf)))
}
return nil
@@ -141,6 +159,8 @@ func (c *Client) startMJPEG(boundary string) error {
packet := &rtp.Packet{Header: rtp.Header{Timestamp: now()}, Payload: buf}
_ = c.track.WriteRTP(packet)
atomic.AddUint32(&c.recv, uint32(len(buf)))
if _, err = r.Discard(2); err != nil {
return err
}

View File

@@ -1,8 +1,10 @@
package mjpeg
import (
"encoding/json"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/pion/rtp"
"sync/atomic"
)
type Consumer struct {
@@ -14,7 +16,7 @@ type Consumer struct {
codecs []*streamer.Codec
start bool
send int
send uint32
}
func (c *Consumer) GetMedias() []*streamer.Media {
@@ -28,6 +30,7 @@ func (c *Consumer) GetMedias() []*streamer.Media {
func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.Track {
push := func(packet *rtp.Packet) error {
c.Fire(packet.Payload)
atomic.AddUint32(&c.send, uint32(len(packet.Payload)))
return nil
}
@@ -38,3 +41,13 @@ func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *strea
return track.Bind(push)
}
func (c *Consumer) MarshalJSON() ([]byte, error) {
info := &streamer.Info{
Type: "MJPEG client",
RemoteAddr: c.RemoteAddr,
UserAgent: c.UserAgent,
Send: atomic.LoadUint32(&c.send),
}
return json.Marshal(info)
}

View File

@@ -7,6 +7,7 @@ import (
"github.com/AlexxIT/go2rtc/pkg/h265"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/pion/rtp"
"sync/atomic"
)
type Consumer struct {
@@ -20,7 +21,7 @@ type Consumer struct {
codecs []*streamer.Codec
wait byte
send int
send uint32
}
const (
@@ -76,7 +77,7 @@ func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *strea
}
buf := c.muxer.Marshal(trackID, packet)
c.send += len(buf)
atomic.AddUint32(&c.send, uint32(len(buf)))
c.Fire(buf)
return nil
@@ -108,7 +109,7 @@ func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *strea
}
buf := c.muxer.Marshal(trackID, packet)
c.send += len(buf)
atomic.AddUint32(&c.send, uint32(len(buf)))
c.Fire(buf)
return nil
@@ -128,7 +129,7 @@ func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *strea
}
buf := c.muxer.Marshal(trackID, packet)
c.send += len(buf)
atomic.AddUint32(&c.send, uint32(len(buf)))
c.Fire(buf)
return nil
@@ -163,12 +164,11 @@ func (c *Consumer) Start() {
//
func (c *Consumer) MarshalJSON() ([]byte, error) {
v := map[string]interface{}{
"type": "MP4 server consumer",
"send": c.send,
"remote_addr": c.RemoteAddr,
"user_agent": c.UserAgent,
info := &streamer.Info{
Type: "MP4 client",
RemoteAddr: c.RemoteAddr,
UserAgent: c.UserAgent,
Send: atomic.LoadUint32(&c.send),
}
return json.Marshal(v)
return json.Marshal(info)
}

View File

@@ -1,7 +1,6 @@
package mp4f
import (
"encoding/json"
"github.com/AlexxIT/go2rtc/pkg/h264"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/deepch/vdk/av"
@@ -149,16 +148,3 @@ func (c *Consumer) Init() ([]byte, error) {
func (c *Consumer) Start() {
c.start = true
}
//
func (c *Consumer) MarshalJSON() ([]byte, error) {
v := map[string]interface{}{
"type": "MSE server consumer",
"send": c.send,
"remote_addr": c.RemoteAddr,
"user_agent": c.UserAgent,
}
return json.Marshal(v)
}

View File

@@ -12,6 +12,7 @@ import (
"github.com/deepch/vdk/format/rtmp"
"github.com/pion/rtp"
"net/http"
"sync/atomic"
"time"
)
@@ -33,7 +34,7 @@ type Client struct {
conn Conn
closed bool
receive int
recv uint32
}
func NewClient(uri string) *Client {
@@ -138,7 +139,7 @@ func (c *Client) Handle() (err error) {
return
}
c.receive += len(pkt.Data)
atomic.AddUint32(&c.recv, uint32(len(pkt.Data)))
track := c.tracks[int(pkt.Idx)]

View File

@@ -4,7 +4,7 @@ import (
"encoding/json"
"fmt"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"strconv"
"sync/atomic"
)
func (c *Client) GetMedias() []*streamer.Media {
@@ -29,19 +29,12 @@ func (c *Client) Stop() error {
}
func (c *Client) MarshalJSON() ([]byte, error) {
v := map[string]interface{}{
streamer.JSONReceive: c.receive,
streamer.JSONType: "RTMP client producer",
//streamer.JSONRemoteAddr: c.conn.NetConn().RemoteAddr().String(),
"url": c.URI,
info := &streamer.Info{
Type: "RTMP source",
URL: c.URI,
Medias: c.medias,
Tracks: c.tracks,
Recv: atomic.LoadUint32(&c.recv),
}
for i, media := range c.medias {
k := "media:" + strconv.Itoa(i)
v[k] = media.String()
}
for i, track := range c.tracks {
k := "track:" + strconv.Itoa(i)
v[k] = track.String()
}
return json.Marshal(v)
return json.Marshal(info)
}

View File

@@ -4,7 +4,6 @@ import (
"encoding/json"
"fmt"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"strconv"
)
// Element Producer
@@ -88,40 +87,30 @@ func (c *Conn) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.
//
func (c *Conn) MarshalJSON() ([]byte, error) {
v := map[string]interface{}{
streamer.JSONReceive: c.receive,
streamer.JSONSend: c.send,
info := &streamer.Info{
UserAgent: c.UserAgent,
Medias: c.Medias,
Tracks: c.tracks,
Recv: uint32(c.receive),
Send: uint32(c.send),
}
switch c.mode {
case ModeUnknown:
v[streamer.JSONType] = "RTSP unknown"
case ModeClientProducer:
v[streamer.JSONType] = "RTSP client producer"
case ModeServerProducer:
v[streamer.JSONType] = "RTSP server producer"
info.Type = "RTSP unknown"
case ModeClientProducer, ModeServerProducer:
info.Type = "RTSP source"
case ModeServerConsumer:
v[streamer.JSONType] = "RTSP server consumer"
info.Type = "RTSP client"
}
//if c.URI != "" {
// v["uri"] = c.URI
//}
if c.URL != nil {
v["url"] = c.URL.String()
info.URL = c.URL.String()
}
if c.conn != nil {
v[streamer.JSONRemoteAddr] = c.conn.RemoteAddr().String()
}
if c.UserAgent != "" {
v[streamer.JSONUserAgent] = c.UserAgent
}
for i, media := range c.Medias {
k := "media:" + strconv.Itoa(i)
v[k] = media.String()
}
for i, track := range c.tracks {
k := "track:" + strconv.Itoa(i)
v[k] = track.String()
info.RemoteAddr = c.conn.RemoteAddr().String()
}
//for i, track := range c.tracks {
// k := "track:" + strconv.Itoa(i+1)
// if track.MimeType() == streamer.MimeTypeH264 {
@@ -130,5 +119,6 @@ func (c *Conn) MarshalJSON() ([]byte, error) {
// v[k] = track.MimeType()
// }
//}
return json.Marshal(v)
return json.Marshal(info)
}

32
pkg/shell/env.go Normal file
View File

@@ -0,0 +1,32 @@
package shell
import (
"os"
"regexp"
"strings"
)
func ReplaceEnvVars(text string) string {
re := regexp.MustCompile(`\${([^}{]+)}`)
return re.ReplaceAllStringFunc(text, func(match string) string {
key := match[2 : len(match)-1]
var def string
var dok bool
if i := strings.IndexByte(key, ':'); i > 0 {
key, def = key[:i], key[i+1:]
dok = true
}
if value, vok := os.LookupEnv(key); vok {
return value
}
if dok {
return def
}
return match
})
}

View File

@@ -3,6 +3,7 @@ package srtp
import (
"encoding/binary"
"net"
"sync/atomic"
)
// Server using same UDP port for SRTP and for SRTCP as the iPhone does
@@ -55,6 +56,8 @@ func (s *Server) Serve(conn net.PacketConn) error {
}
}
atomic.AddUint32(&session.Recv, uint32(n))
if err = session.HandleRTP(buf[:n]); err != nil {
return err
}

View File

@@ -17,6 +17,7 @@ type Session struct {
Write func(b []byte) (int, error)
Track *streamer.Track
Recv uint32
lastSequence uint32
lastTimestamp uint32

View File

@@ -4,13 +4,16 @@ import (
"strings"
)
const (
JSONType = "type"
JSONRemoteAddr = "remote_addr"
JSONUserAgent = "user_agent"
JSONReceive = "receive"
JSONSend = "send"
)
type Info struct {
Type string `json:"type,omitempty"`
URL string `json:"url,omitempty"`
RemoteAddr string `json:"remote_addr,omitempty"`
UserAgent string `json:"user_agent,omitempty"`
Medias []*Media `json:"medias,omitempty"`
Tracks []*Track `json:"tracks,omitempty"`
Recv uint32 `json:"recv,omitempty"`
Send uint32 `json:"send,omitempty"`
}
func Between(s, sub1, sub2 string) string {
i := strings.Index(s, sub1)

View File

@@ -1,6 +1,7 @@
package streamer
import (
"encoding/json"
"fmt"
"github.com/pion/sdp/v3"
"strconv"
@@ -70,6 +71,10 @@ func (m *Media) String() string {
return s
}
func (m *Media) MarshalJSON() ([]byte, error) {
return json.Marshal(m.String())
}
func (m *Media) Clone() *Media {
clone := *m
return &clone

View File

@@ -1,6 +1,7 @@
package streamer
import (
"encoding/json"
"fmt"
"github.com/pion/rtp"
"sync"
@@ -22,12 +23,19 @@ func NewTrack(codec *Codec, direction string) *Track {
func (t *Track) String() string {
s := t.Codec.String()
t.sinkMu.RLock()
s += fmt.Sprintf(", sinks=%d", len(t.sink))
t.sinkMu.RUnlock()
if t.sinkMu.TryRLock() {
s += fmt.Sprintf(", sinks=%d", len(t.sink))
t.sinkMu.RUnlock()
} else {
s += fmt.Sprintf(", sinks=?")
}
return s
}
func (t *Track) MarshalJSON() ([]byte, error) {
return json.Marshal(t.String())
}
func (t *Track) WriteRTP(p *rtp.Packet) error {
t.sinkMu.RLock()
for _, f := range t.sink {

View File

@@ -113,20 +113,12 @@ func (c *Conn) AddCandidate(candidate string) {
}
func (c *Conn) MarshalJSON() ([]byte, error) {
v := map[string]interface{}{
streamer.JSONType: "WebRTC server consumer",
streamer.JSONRemoteAddr: c.remote(),
info := &streamer.Info{
Type: "WebRTC client",
RemoteAddr: c.remote(),
UserAgent: c.UserAgent,
Recv: uint32(c.receive),
Send: uint32(c.send),
}
if c.receive > 0 {
v[streamer.JSONReceive] = c.receive
}
if c.send > 0 {
v[streamer.JSONSend] = c.send
}
if c.UserAgent != "" {
v[streamer.JSONUserAgent] = c.UserAgent
}
return json.Marshal(v)
return json.Marshal(info)
}

View File

@@ -25,13 +25,18 @@ func NewCandidate(network, address string) (string, error) {
return "", err
}
cand, err := ice.NewCandidateHost(&ice.CandidateHostConfig{
config := &ice.CandidateHostConfig{
Network: network,
Address: host,
Port: i,
Component: ice.ComponentRTP,
TCPType: ice.TCPTypePassive,
})
}
if network == "tcp" {
config.TCPType = ice.TCPTypePassive
}
cand, err := ice.NewCandidateHost(config)
if err != nil {
return "", err
}

59
www/editor.html Normal file
View File

@@ -0,0 +1,59 @@
<!DOCTYPE html>
<html>
<head>
<title>File Editor</title>
<meta name="viewport" content="width=device-width, user-scalable=yes, initial-scale=1, maximum-scale=1">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.14.0/ace.min.js"></script>
<style>
body {
font-family: Arial, Helvetica, sans-serif;
}
body {
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
}
html, body, #config {
width: 100%;
height: 100%;
}
</style>
</head>
<body>
<script src="main.js"></script>
<div>
<button id="save">Save & Restart</button>
</div>
<br>
<div id="config"></div>
<script>
ace.config.set('basePath', 'https://cdnjs.cloudflare.com/ajax/libs/ace/1.14.0/');
const editor = ace.edit("config", {
mode: "ace/mode/yaml",
});
document.getElementById('save').addEventListener('click', () => {
fetch('api/config', {
method: 'POST', body: editor.getValue()
}).then(r => {
if (r.ok) {
alert("OK");
fetch('api/exit', {method: 'POST'});
} else {
r.text().then(alert);
}
});
});
window.addEventListener('load', () => {
fetch('api/config').then(r => r.text()).then(data => {
editor.setValue(data);
});
})
</script>
</body>
</html>

View File

@@ -10,6 +10,7 @@
<style>
body {
font-family: Arial, Helvetica, sans-serif;
background-color: white;
}
table {
@@ -61,6 +62,7 @@
</head>
<body>
<script src="main.js"></script>
<div class="info"></div>
<div class="header">
<input id="src" type="text" placeholder="url">
<a id="add" href="#">add</a>
@@ -89,7 +91,6 @@
'<a href="webrtc.html?src={name}">2-way-aud</a>',
'<a href="api/stream.mp4?src={name}">mp4</a>',
'<a href="api/stream.mjpeg?src={name}">mjpeg</a>',
`<a href="rtsp://${location.hostname}:8554/{name}">rtsp</a>`,
'<a href="api/streams?src={name}">info</a>',
'<a href="#" data-name="{name}">delete</a>',
];
@@ -136,7 +137,7 @@
tbody.innerHTML = "";
for (const [name, value] of Object.entries(data)) {
const online = value ? value.length : 0;
const online = value && value.consumers ? value.consumers.length : 0;
const links = templates.map(link => {
return link.replace("{name}", encodeURIComponent(name));
}).join(" ");
@@ -151,7 +152,21 @@
});
}
reload();
const url = new URL("api", location.href);
fetch(url).then(r => r.json()).then(data => {
const info = document.querySelector(".info");
info.innerText = `Version: ${data.version}, Config: ${data.config_path}`;
try {
const host = data.host.match(/^[^:]+/)[0];
const port = data.rtsp.listen.match(/[0-9]+$/)[0];
templates.splice(4, 0, `<a href="rtsp://${host}:${port}/{name}">rtsp</a>`);
} catch (e) {
templates.splice(4, 0, `<a href="rtsp://${location.host}:8554/{name}">rtsp</a>`);
}
reload();
});
</script>
</body>
</html>

View File

@@ -47,6 +47,7 @@ nav li {
<li><a href="index.html">Streams</a></li>
<li><a href="devices.html">Devices</a></li>
<li><a href="homekit.html">HomeKit</a></li>
<li><a href="editor.html">Config</a></li>
</ul>
</nav>
` + document.body.innerHTML;

View File

@@ -228,16 +228,12 @@ export class VideoRTC extends HTMLElement {
this.video.playsInline = true;
this.video.preload = "auto";
this.appendChild(this.video);
// important for second video for mode MP4
this.style.display = "block";
this.style.position = "relative";
this.video.style.display = "block"; // fix bottom margin 4px
this.video.style.width = "100%";
this.video.style.height = "100%"
this.appendChild(this.video);
if (this.background) return;
if ("hidden" in document && this.visibilityCheck) {
@@ -392,21 +388,23 @@ export class VideoRTC extends HTMLElement {
sb.mode = "segments"; // segments or sequence
sb.addEventListener("updateend", () => {
if (sb.updating) return;
if (bufLen > 0) {
try {
sb.appendBuffer(buf.slice(0, bufLen));
} catch (e) {
// console.debug(e);
try {
if (bufLen > 0) {
const data = buf.slice(0, bufLen);
bufLen = 0;
sb.appendBuffer(data);
} else if (sb.buffered && sb.buffered.length) {
const end = sb.buffered.end(sb.buffered.length - 1) - 5;
const start = sb.buffered.start(0);
if (end > start) {
sb.remove(start, end);
ms.setLiveSeekableRange(end, end + 5);
}
// console.debug("VideoRTC.buffered", start, end);
}
bufLen = 0;
} else if (sb.buffered && sb.buffered.length) {
const end = sb.buffered.end(sb.buffered.length - 1) - 5;
const start = sb.buffered.start(0);
if (end > start) {
sb.remove(start, end);
ms.setLiveSeekableRange(end, end + 5);
}
// console.debug("VideoRTC.buffered", start, end);
} catch (e) {
// console.debug(e);
}
});
@@ -504,6 +502,8 @@ export class VideoRTC extends HTMLElement {
* @param ev {Event}
*/
onpcvideo(ev) {
if (!this.pc) return;
/** @type {HTMLVideoElement} */
const video2 = ev.target;
const state = this.pc.connectionState;
@@ -543,46 +543,42 @@ export class VideoRTC extends HTMLElement {
onmjpeg() {
this.ondata = data => {
this.video.controls = false;
this.video.poster = "data:image/jpeg;base64," + VideoRTC.btoa(data);
};
this.send({type: "mjpeg"});
this.video.controls = false;
}
onmp4() {
/** @type {HTMLVideoElement} */
let video2;
/** @type {HTMLCanvasElement} **/
const canvas = document.createElement("canvas");
/** @type {CanvasRenderingContext2D} */
let context;
this.ondata = data => {
// first video with default position (set container size)
// second video with position=absolute and top=0px
if (video2) {
this.removeChild(this.video);
this.video.src = "";
this.video = video2;
video2.style.position = "";
video2.style.top = "";
/** @type {HTMLVideoElement} */
const video2 = document.createElement("video");
video2.autoplay = true;
video2.muted = true;
video2.addEventListener("loadeddata", ev => {
if (!context) {
canvas.width = video2.videoWidth;
canvas.height = video2.videoHeight;
context = canvas.getContext('2d');
}
video2 = this.video.cloneNode();
video2.style.position = "absolute";
video2.style.top = "0px";
this.appendChild(video2);
context.drawImage(video2, 0, 0, canvas.width, canvas.height);
video2.src = "data:video/mp4;base64," + VideoRTC.btoa(data);
video2.play().catch(() => console.log);
};
this.ws.addEventListener("close", () => {
if (!video2) return;
this.removeChild(video2);
video2.src = "";
this.video.controls = false;
this.video.poster = canvas.toDataURL("image/jpeg");
});
this.ondata = data => {
video2.src = "data:video/mp4;base64," + VideoRTC.btoa(data);
};
this.send({type: "mp4", value: this.codecs("mp4")});
this.video.controls = false;
}
static btoa(buffer) {

View File

@@ -22,6 +22,9 @@ class VideoStream extends VideoRTC {
this.innerHTML = `
<style>
video-stream {
position: relative;
}
.info {
position: absolute;
top: 0;