Compare commits
40 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9ddbb326b4 | |||
| a2e58d928e | |||
| 3c48fb8bea | |||
| 4b0cbb5a73 | |||
| e28b49ea86 | |||
| 5c17d8fcb6 | |||
| e040fb591f | |||
| 140014f2a6 | |||
| 23f72d111e | |||
| f9d5ab9d0a | |||
| 8628c48db8 | |||
| 6e49d51c33 | |||
| 6a61b5234e | |||
| 7a0091777d | |||
| d23d2a7eff | |||
| cecbe4166c | |||
| dcb457235c | |||
| bc4e032830 | |||
| 8218cda149 | |||
| d1e56feeb6 | |||
| 463d05dfd3 | |||
| a1a73f7b45 | |||
| 39662e10af | |||
| 1c830d6e60 | |||
| 2039aa60b3 | |||
| b7016e798f | |||
| 0b291f5185 | |||
| 395304654a | |||
| e472397705 | |||
| 7c1f48e0ad | |||
| f4346a104f | |||
| 030972b436 | |||
| efddefa123 | |||
| 3c1bdd0dab | |||
| 7e7e15d7c8 | |||
| a1a9f77535 | |||
| a06462729d | |||
| 331c5bbcad | |||
| 58a76efc8a | |||
| 5e0f010885 |
@@ -1,4 +1,4 @@
|
|||||||
name: ci
|
name: docker
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
@@ -19,7 +19,7 @@ jobs:
|
|||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v4
|
uses: docker/metadata-action@v4
|
||||||
with:
|
with:
|
||||||
images: alexxit/go2rtc
|
images: ${{ github.repository }}
|
||||||
tags: |
|
tags: |
|
||||||
type=ref,event=branch
|
type=ref,event=branch
|
||||||
type=semver,pattern={{version}},enable=false
|
type=semver,pattern={{version}},enable=false
|
||||||
@@ -29,7 +29,7 @@ jobs:
|
|||||||
id: meta-hw
|
id: meta-hw
|
||||||
uses: docker/metadata-action@v4
|
uses: docker/metadata-action@v4
|
||||||
with:
|
with:
|
||||||
images: alexxit/go2rtc
|
images: ${{ github.repository }}
|
||||||
flavor: |
|
flavor: |
|
||||||
suffix=-hardware
|
suffix=-hardware
|
||||||
latest=false
|
latest=false
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
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
|
||||||
|
|
||||||
|
esport CGO_ENABLED=0
|
||||||
|
|
||||||
|
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=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=artifacts/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
|
||||||
+4
-5
@@ -33,13 +33,12 @@ FROM scratch AS rootfs
|
|||||||
|
|
||||||
COPY --from=build /build/go2rtc /usr/local/bin/
|
COPY --from=build /build/go2rtc /usr/local/bin/
|
||||||
COPY --from=ngrok /bin/ngrok /usr/local/bin/
|
COPY --from=ngrok /bin/ngrok /usr/local/bin/
|
||||||
COPY ./build/docker/run.sh /
|
|
||||||
|
|
||||||
|
|
||||||
# 3. Final image
|
# 3. Final image
|
||||||
FROM base
|
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.
|
# and other common tools for the echo source.
|
||||||
RUN apk add --no-cache tini ffmpeg bash curl jq
|
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 / /
|
COPY --from=rootfs / /
|
||||||
|
|
||||||
RUN chmod a+x /run.sh && mkdir -p /config
|
|
||||||
|
|
||||||
ENTRYPOINT ["/sbin/tini", "--"]
|
ENTRYPOINT ["/sbin/tini", "--"]
|
||||||
|
VOLUME /config
|
||||||
|
WORKDIR /config
|
||||||
|
|
||||||
CMD ["/run.sh"]
|
CMD ["go2rtc", "-config", "/config/go2rtc.yaml"]
|
||||||
|
|||||||
@@ -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 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)
|
- 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 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)
|
- on the fly transcoding for unsupported codecs via [FFmpeg](#source-ffmpeg)
|
||||||
- multi-source 2-way [codecs negotiation](#codecs-negotiation)
|
- multi-source 2-way [codecs negotiation](#codecs-negotiation)
|
||||||
- mixing tracks from different sources to single stream
|
- 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
|
- add your [streams](#module-streams) to [config](#configuration) file
|
||||||
- setup [external access](#module-webrtc) to webrtc
|
- setup [external access](#module-webrtc) to webrtc
|
||||||
- setup [external access](#module-ngrok) to web interface
|
- setup [external access](#module-ngrok) to web interface
|
||||||
- install [ffmpeg](#source-ffmpeg) for transcoding
|
|
||||||
|
|
||||||
**Developers:**
|
**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_win64.zip` - Windows 64-bit
|
||||||
- `go2rtc_win32.zip` - Windows 32-bit
|
- `go2rtc_win32.zip` - Windows 32-bit
|
||||||
- `go2rtc_win_arm64.zip` - Windows ARM 64-bit
|
|
||||||
- `go2rtc_linux_amd64` - Linux 64-bit
|
- `go2rtc_linux_amd64` - Linux 64-bit
|
||||||
- `go2rtc_linux_i386` - Linux 32-bit
|
- `go2rtc_linux_i386` - Linux 32-bit
|
||||||
- `go2rtc_linux_arm64` - Linux ARM 64-bit (ex. Raspberry 64-bit OS)
|
- `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
|
### 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).
|
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).
|
||||||
|
|
||||||
```yaml
|
|
||||||
services:
|
|
||||||
go2rtc:
|
|
||||||
image: alexxit/go2rtc
|
|
||||||
network_mode: host
|
|
||||||
restart: always
|
|
||||||
volumes:
|
|
||||||
- "~/go2rtc.yaml:/config/go2rtc.yaml"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Configuration
|
## 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
|
- by default, you need to config only your `streams` links
|
||||||
- `api` server will start on default **1984 port**
|
- `api` server will start on default **1984 port** (TCP)
|
||||||
- `rtsp` server will start on default **8554 port**
|
- `rtsp` server will start on default **8554 port** (TCP)
|
||||||
- `webrtc` will use random UDP port for each connection
|
- `webrtc` will use port **8555** (TCP/UDP) for connections
|
||||||
- `ffmpeg` will use default transcoding options (you may install it [manually](https://ffmpeg.org/))
|
- `ffmpeg` will use default transcoding options
|
||||||
|
|
||||||
Available modules:
|
Available modules:
|
||||||
|
|
||||||
@@ -107,6 +95,8 @@ Available modules:
|
|||||||
- [hass](#module-hass) - Home Assistant integration
|
- [hass](#module-hass) - Home Assistant integration
|
||||||
- [log](#module-log) - logs config
|
- [log](#module-log) - logs config
|
||||||
|
|
||||||
|
Full default config [example](https://github.com/AlexxIT/go2rtc/wiki/Configuration).
|
||||||
|
|
||||||
### Module: Streams
|
### Module: Streams
|
||||||
|
|
||||||
**go2rtc** support different stream source types. You can config one or multiple links of any type as stream source.
|
**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
|
```yaml
|
||||||
ffmpeg:
|
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"
|
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..."
|
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 `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 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 `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`).
|
- 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
|
#### Source: FFmpeg Device
|
||||||
|
|
||||||
You can get video from any USB-camera or Webcam as RTSP or WebRTC stream. This is part of FFmpeg integration.
|
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)
|
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)).
|
- 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))
|
||||||
**PS3.** MJPEG over WebSocket plays better than native MJPEG because Chrome [bug](https://bugs.chromium.org/p/chromium/issues/detail?id=527446).
|
- 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
|
||||||
**PS4.** MP4 over WebSocket was created only for Apple iOS because it doesn't support MSE and native MP4.
|
|
||||||
|
|
||||||
### Module: RTSP
|
### 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.
|
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)
|
- by default, WebRTC uses both TCP and UDP on port 8555 for connections
|
||||||
- you can enable one additional TCP port for all connections and use it for external access
|
- 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**
|
**Static public IP**
|
||||||
|
|
||||||
- add some TCP port to YAML config (ex. 8555)
|
- forward the port 8555 on your router (you can use same 8555 port or any other as external port)
|
||||||
- forward this port on your router (you can use same 8555 port or any other)
|
|
||||||
- add your external IP-address and external port to YAML config
|
- add your external IP-address and external port to YAML config
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
webrtc:
|
webrtc:
|
||||||
listen: ":8555" # address of your local server (TCP)
|
|
||||||
candidates:
|
candidates:
|
||||||
- 216.58.210.174:8555 # if you have static public IP-address
|
- 216.58.210.174:8555 # if you have static public IP-address
|
||||||
```
|
```
|
||||||
|
|
||||||
**Dynamic public IP**
|
**Dynamic public IP**
|
||||||
|
|
||||||
- add some TCP port to YAML config (ex. 8555)
|
- forward the port 8555 on your router (you can use same 8555 port or any other as the external port)
|
||||||
- forward this port on your router (you can use same 8555 port or any other)
|
|
||||||
- add `stun` word and external port to YAML config
|
- add `stun` word and external port to YAML config
|
||||||
- go2rtc automatically detects your external address with STUN-server
|
- go2rtc automatically detects your external address with STUN-server
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
webrtc:
|
webrtc:
|
||||||
listen: ":8555" # address of your local server (TCP)
|
|
||||||
candidates:
|
candidates:
|
||||||
- stun:8555 # if you have dynamic public IP-address
|
- stun:8555 # if you have dynamic public IP-address
|
||||||
```
|
```
|
||||||
|
|
||||||
**Private IP**
|
**Private IP**
|
||||||
|
|
||||||
- add some TCP port to YAML config (ex. 8555)
|
|
||||||
- setup integration with [Ngrok service](#module-ngrok)
|
- setup integration with [Ngrok service](#module-ngrok)
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
webrtc:
|
|
||||||
listen: ":8555" # address of your local server (TCP)
|
|
||||||
|
|
||||||
ngrok:
|
ngrok:
|
||||||
command: ...
|
command: ...
|
||||||
```
|
```
|
||||||
@@ -550,8 +540,13 @@ PS. Default Home Assistant lovelace cards don't support 2-way audio. You can use
|
|||||||
Provides several features:
|
Provides several features:
|
||||||
|
|
||||||
1. MSE stream (fMP4 over WebSocket)
|
1. MSE stream (fMP4 over WebSocket)
|
||||||
2. Camera snapshots in MP4 format (single frame), can be sent to [Telegram](https://www.telegram.org/)
|
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 latency, doesn't work in Safari
|
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
|
### Module: MJPEG
|
||||||
|
|
||||||
@@ -595,7 +590,7 @@ log:
|
|||||||
|
|
||||||
## Security
|
## 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:
|
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
|
listen: "127.0.0.1:8554" # localhost
|
||||||
|
|
||||||
webrtc:
|
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
|
- 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.
|
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
|
## Codecs madness
|
||||||
|
|
||||||
@@ -687,6 +682,10 @@ streams:
|
|||||||
- `ffplay -fflags nobuffer -flags low_delay "rtsp://192.168.1.123:8554/camera1"`
|
- `ffplay -fflags nobuffer -flags low_delay "rtsp://192.168.1.123:8554/camera1"`
|
||||||
- VLC > Preferences > Input / Codecs > Default Caching Level: Lowest Latency
|
- VLC > Preferences > Input / Codecs > Default Caching Level: Lowest Latency
|
||||||
|
|
||||||
|
**Snapshots to Telegram**
|
||||||
|
|
||||||
|
[read more](https://github.com/AlexxIT/go2rtc/wiki/Snapshot-to-Telegram)
|
||||||
|
|
||||||
## FAQ
|
## FAQ
|
||||||
|
|
||||||
**Q. What's the difference between go2rtc, WebRTC Camera and RTSPtoWebRTC?**
|
**Q. What's the difference between go2rtc, WebRTC Camera and RTSPtoWebRTC?**
|
||||||
|
|||||||
+54
-34
@@ -3,16 +3,21 @@ package api
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"github.com/AlexxIT/go2rtc/cmd/app"
|
"github.com/AlexxIT/go2rtc/cmd/app"
|
||||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Init() {
|
func Init() {
|
||||||
var cfg struct {
|
var cfg struct {
|
||||||
Mod struct {
|
Mod struct {
|
||||||
Listen string `yaml:"listen"`
|
Listen string `yaml:"listen"`
|
||||||
|
Username string `yaml:"username"`
|
||||||
|
Password string `yaml:"password"`
|
||||||
BasePath string `yaml:"base_path"`
|
BasePath string `yaml:"base_path"`
|
||||||
StaticDir string `yaml:"static_dir"`
|
StaticDir string `yaml:"static_dir"`
|
||||||
Origin string `yaml:"origin"`
|
Origin string `yaml:"origin"`
|
||||||
@@ -35,7 +40,9 @@ func Init() {
|
|||||||
initStatic(cfg.Mod.StaticDir)
|
initStatic(cfg.Mod.StaticDir)
|
||||||
initWS(cfg.Mod.Origin)
|
initWS(cfg.Mod.Origin)
|
||||||
|
|
||||||
HandleFunc("api/streams", streamsHandler)
|
HandleFunc("api", apiHandler)
|
||||||
|
HandleFunc("api/config", configHandler)
|
||||||
|
HandleFunc("api/exit", exitHandler)
|
||||||
HandleFunc("api/ws", apiWS)
|
HandleFunc("api/ws", apiWS)
|
||||||
|
|
||||||
// ensure we can listen without errors
|
// ensure we can listen without errors
|
||||||
@@ -48,14 +55,18 @@ func Init() {
|
|||||||
log.Info().Str("addr", cfg.Mod.Listen).Msg("[api] listen")
|
log.Info().Str("addr", cfg.Mod.Listen).Msg("[api] listen")
|
||||||
|
|
||||||
s := http.Server{}
|
s := http.Server{}
|
||||||
s.Handler = http.DefaultServeMux
|
s.Handler = http.DefaultServeMux // 4th
|
||||||
|
|
||||||
if log.Trace().Enabled() {
|
|
||||||
s.Handler = middlewareLog(s.Handler)
|
|
||||||
}
|
|
||||||
|
|
||||||
if cfg.Mod.Origin == "*" {
|
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() {
|
go func() {
|
||||||
@@ -83,7 +94,22 @@ var log zerolog.Logger
|
|||||||
|
|
||||||
func middlewareLog(next http.Handler) http.Handler {
|
func middlewareLog(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
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)
|
next.ServeHTTP(w, r)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -96,31 +122,25 @@ func middlewareCORS(next http.Handler) http.Handler {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func streamsHandler(w http.ResponseWriter, r *http.Request) {
|
var mu sync.Mutex
|
||||||
src := r.URL.Query().Get("src")
|
|
||||||
name := r.URL.Query().Get("name")
|
|
||||||
|
|
||||||
if name == "" {
|
func apiHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
name = src
|
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":
|
func exitHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
streams.New(name, src)
|
if r.Method != "POST" {
|
||||||
return
|
http.Error(w, "", http.StatusBadRequest)
|
||||||
case "DELETE":
|
return
|
||||||
streams.Delete(src)
|
}
|
||||||
return
|
|
||||||
}
|
s := r.URL.Query().Get("code")
|
||||||
|
code, _ := strconv.Atoi(s)
|
||||||
var v interface{}
|
os.Exit(code)
|
||||||
if src != "" {
|
|
||||||
v = streams.Get(src)
|
|
||||||
} else {
|
|
||||||
v = streams.All()
|
|
||||||
}
|
|
||||||
|
|
||||||
e := json.NewEncoder(w)
|
|
||||||
e.SetIndent("", " ")
|
|
||||||
_ = e.Encode(v)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
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) {
|
||||||
|
if app.ConfigPath == "" {
|
||||||
|
http.Error(w, "", http.StatusGone)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch r.Method {
|
||||||
|
case "GET":
|
||||||
|
data, err := os.ReadFile(app.ConfigPath)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "", http.StatusNotFound)
|
||||||
|
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
|
||||||
|
}
|
||||||
+60
-20
@@ -2,46 +2,76 @@ package app
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"flag"
|
"flag"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/shell"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
var Version = "0.1-rc.8"
|
var Version = "1.0.0"
|
||||||
var UserAgent = "go2rtc/" + Version
|
var UserAgent = "go2rtc/" + Version
|
||||||
|
|
||||||
func Init() {
|
var ConfigPath string
|
||||||
config := flag.String(
|
var Info = map[string]interface{}{
|
||||||
"config",
|
"version": Version,
|
||||||
"go2rtc.yaml",
|
}
|
||||||
"Path to go2rtc configuration file",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
func Init() {
|
||||||
|
var confs Config
|
||||||
|
|
||||||
|
flag.Var(&confs, "config", "go2rtc config (path to file or raw text), support multiple")
|
||||||
flag.Parse()
|
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 !filepath.IsAbs(ConfigPath) {
|
||||||
|
if cwd, err := os.Getwd(); err == nil {
|
||||||
|
ConfigPath = filepath.Join(cwd, ConfigPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Info["config_path"] = ConfigPath
|
||||||
|
}
|
||||||
|
|
||||||
var cfg struct {
|
var cfg struct {
|
||||||
Mod map[string]string `yaml:"log"`
|
Mod map[string]string `yaml:"log"`
|
||||||
}
|
}
|
||||||
|
|
||||||
if data != nil {
|
LoadConfig(&cfg)
|
||||||
if err := yaml.Unmarshal(data, &cfg); err != nil {
|
|
||||||
println("ERROR: " + err.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Logger = NewLogger(cfg.Mod["format"], cfg.Mod["level"])
|
log.Logger = NewLogger(cfg.Mod["format"], cfg.Mod["level"])
|
||||||
|
|
||||||
modules = cfg.Mod
|
modules = cfg.Mod
|
||||||
|
|
||||||
log.Info().Msgf("go2rtc version %s %s/%s", Version, runtime.GOOS, runtime.GOARCH)
|
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 {
|
func NewLogger(format string, level string) zerolog.Logger {
|
||||||
@@ -54,7 +84,7 @@ func NewLogger(format string, level string) zerolog.Logger {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
zerolog.TimeFieldFormat = zerolog.TimeFormatUnixMs
|
zerolog.TimeFieldFormat = time.RFC3339Nano
|
||||||
|
|
||||||
lvl, err := zerolog.ParseLevel(level)
|
lvl, err := zerolog.ParseLevel(level)
|
||||||
if err != nil || lvl == zerolog.NoLevel {
|
if err != nil || lvl == zerolog.NoLevel {
|
||||||
@@ -65,7 +95,7 @@ func NewLogger(format string, level string) zerolog.Logger {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func LoadConfig(v interface{}) {
|
func LoadConfig(v interface{}) {
|
||||||
if data != nil {
|
for _, data := range configs {
|
||||||
if err := yaml.Unmarshal(data, v); err != nil {
|
if err := yaml.Unmarshal(data, v); err != nil {
|
||||||
log.Warn().Err(err).Msg("[app] read config")
|
log.Warn().Err(err).Msg("[app] read config")
|
||||||
}
|
}
|
||||||
@@ -86,8 +116,18 @@ func GetLogger(module string) zerolog.Logger {
|
|||||||
|
|
||||||
// internal
|
// internal
|
||||||
|
|
||||||
// data - config content
|
type Config []string
|
||||||
var data []byte
|
|
||||||
|
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
|
// modules log levels
|
||||||
var modules map[string]string
|
var modules map[string]string
|
||||||
|
|||||||
@@ -4,24 +4,14 @@ import (
|
|||||||
"github.com/AlexxIT/go2rtc/cmd/api"
|
"github.com/AlexxIT/go2rtc/cmd/api"
|
||||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
"github.com/AlexxIT/go2rtc/cmd/streams"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"strconv"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func Init() {
|
func Init() {
|
||||||
api.HandleFunc("api/stack", stackHandler)
|
api.HandleFunc("api/stack", stackHandler)
|
||||||
api.HandleFunc("api/exit", exitHandler)
|
|
||||||
|
|
||||||
streams.HandleFunc("null", nullHandler)
|
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) {
|
func nullHandler(string) (streamer.Producer, error) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-3
@@ -27,7 +27,10 @@ func handlerKeyframe(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
exit := make(chan []byte)
|
exit := make(chan []byte)
|
||||||
|
|
||||||
cons := &mjpeg.Consumer{}
|
cons := &mjpeg.Consumer{
|
||||||
|
RemoteAddr: r.RemoteAddr,
|
||||||
|
UserAgent: r.UserAgent(),
|
||||||
|
}
|
||||||
cons.Listen(func(msg interface{}) {
|
cons.Listen(func(msg interface{}) {
|
||||||
switch msg := msg.(type) {
|
switch msg := msg.(type) {
|
||||||
case []byte:
|
case []byte:
|
||||||
@@ -68,7 +71,10 @@ func handlerStream(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
flusher := w.(http.Flusher)
|
flusher := w.(http.Flusher)
|
||||||
|
|
||||||
cons := &mjpeg.Consumer{}
|
cons := &mjpeg.Consumer{
|
||||||
|
RemoteAddr: r.RemoteAddr,
|
||||||
|
UserAgent: r.UserAgent(),
|
||||||
|
}
|
||||||
cons.Listen(func(msg interface{}) {
|
cons.Listen(func(msg interface{}) {
|
||||||
switch msg := msg.(type) {
|
switch msg := msg.(type) {
|
||||||
case []byte:
|
case []byte:
|
||||||
@@ -109,7 +115,10 @@ func handlerWS(tr *api.Transport, _ *api.Message) error {
|
|||||||
return errors.New(api.StreamNotFound)
|
return errors.New(api.StreamNotFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
cons := &mjpeg.Consumer{}
|
cons := &mjpeg.Consumer{
|
||||||
|
RemoteAddr: tr.Request.RemoteAddr,
|
||||||
|
UserAgent: tr.Request.UserAgent(),
|
||||||
|
}
|
||||||
cons.Listen(func(msg interface{}) {
|
cons.Listen(func(msg interface{}) {
|
||||||
if data, ok := msg.([]byte); ok {
|
if data, ok := msg.([]byte); ok {
|
||||||
tr.Write(data)
|
tr.Write(data)
|
||||||
|
|||||||
+4
-1
@@ -80,7 +80,10 @@ func handlerMP4(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
exit := make(chan error)
|
exit := make(chan error)
|
||||||
|
|
||||||
cons := &mp4.Consumer{}
|
cons := &mp4.Consumer{
|
||||||
|
RemoteAddr: r.RemoteAddr,
|
||||||
|
UserAgent: r.UserAgent(),
|
||||||
|
}
|
||||||
cons.Listen(func(msg interface{}) {
|
cons.Listen(func(msg interface{}) {
|
||||||
if data, ok := msg.([]byte); ok {
|
if data, ok := msg.([]byte); ok {
|
||||||
if _, err := w.Write(data); err != nil && exit != nil {
|
if _, err := w.Write(data); err != nil && exit != nil {
|
||||||
|
|||||||
+9
-2
@@ -18,7 +18,10 @@ func handlerWSMSE(tr *api.Transport, msg *api.Message) error {
|
|||||||
return errors.New(api.StreamNotFound)
|
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.UserAgent = tr.Request.UserAgent()
|
||||||
cons.RemoteAddr = tr.Request.RemoteAddr
|
cons.RemoteAddr = tr.Request.RemoteAddr
|
||||||
|
|
||||||
@@ -68,7 +71,11 @@ func handlerWSMP4(tr *api.Transport, msg *api.Message) error {
|
|||||||
return errors.New(api.StreamNotFound)
|
return errors.New(api.StreamNotFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
cons := &mp4.Segment{}
|
cons := &mp4.Segment{
|
||||||
|
RemoteAddr: tr.Request.RemoteAddr,
|
||||||
|
UserAgent: tr.Request.UserAgent(),
|
||||||
|
OnlyKeyframe: true,
|
||||||
|
}
|
||||||
|
|
||||||
if codecs, ok := msg.Value.(string); ok {
|
if codecs, ok := msg.Value.(string); ok {
|
||||||
log.Trace().Str("codecs", codecs).Msgf("[mp4] new WS/MP4 consumer")
|
log.Trace().Str("codecs", codecs).Msgf("[mp4] new WS/MP4 consumer")
|
||||||
|
|||||||
+6
-3
@@ -14,9 +14,9 @@ import (
|
|||||||
func Init() {
|
func Init() {
|
||||||
var conf struct {
|
var conf struct {
|
||||||
Mod struct {
|
Mod struct {
|
||||||
Listen string `yaml:"listen"`
|
Listen string `yaml:"listen" json:"listen"`
|
||||||
Username string `yaml:"username"`
|
Username string `yaml:"username" json:"-"`
|
||||||
Password string `yaml:"password"`
|
Password string `yaml:"password" json:"-"`
|
||||||
} `yaml:"rtsp"`
|
} `yaml:"rtsp"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,6 +24,7 @@ func Init() {
|
|||||||
conf.Mod.Listen = ":8554"
|
conf.Mod.Listen = ":8554"
|
||||||
|
|
||||||
app.LoadConfig(&conf)
|
app.LoadConfig(&conf)
|
||||||
|
app.Info["rtsp"] = conf.Mod
|
||||||
|
|
||||||
log = app.GetLogger("rtsp")
|
log = app.GetLogger("rtsp")
|
||||||
|
|
||||||
@@ -161,6 +162,8 @@ func tcpHandler(conn *rtsp.Conn) {
|
|||||||
|
|
||||||
log.Debug().Str("stream", name).Msg("[rtsp] new consumer")
|
log.Debug().Str("stream", name).Msg("[rtsp] new consumer")
|
||||||
|
|
||||||
|
conn.SessionName = app.UserAgent
|
||||||
|
|
||||||
initMedias(conn)
|
initMedias(conn)
|
||||||
|
|
||||||
if err := stream.AddConsumer(conn); err != nil {
|
if err := stream.AddConsumer(conn); err != nil {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package streams
|
package streams
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -91,6 +92,15 @@ func (p *Producer) GetTrack(media *streamer.Media, codec *streamer.Codec) *strea
|
|||||||
return track
|
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
|
// internals
|
||||||
|
|
||||||
func (p *Producer) start() {
|
func (p *Producer) start() {
|
||||||
|
|||||||
+9
-19
@@ -10,11 +10,6 @@ import (
|
|||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Consumer struct {
|
|
||||||
element streamer.Consumer
|
|
||||||
tracks []*streamer.Track
|
|
||||||
}
|
|
||||||
|
|
||||||
type Stream struct {
|
type Stream struct {
|
||||||
producers []*Producer
|
producers []*Producer
|
||||||
consumers []*Consumer
|
consumers []*Consumer
|
||||||
@@ -199,24 +194,19 @@ producers:
|
|||||||
func (s *Stream) MarshalJSON() ([]byte, error) {
|
func (s *Stream) MarshalJSON() ([]byte, error) {
|
||||||
if !s.mu.TryLock() {
|
if !s.mu.TryLock() {
|
||||||
log.Warn().Msgf("[streams] json locked")
|
log.Warn().Msgf("[streams] json locked")
|
||||||
return []byte(`null`), nil
|
return json.Marshal(nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
var v []interface{}
|
var info struct {
|
||||||
for _, prod := range s.producers {
|
Producers []*Producer `json:"producers"`
|
||||||
if prod.element != nil {
|
Consumers []*Consumer `json:"consumers"`
|
||||||
v = append(v, prod.element)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, cons := range s.consumers {
|
|
||||||
// cons.element always not nil
|
|
||||||
v = append(v, cons.element)
|
|
||||||
}
|
}
|
||||||
|
info.Producers = s.producers
|
||||||
|
info.Consumers = s.consumers
|
||||||
|
|
||||||
s.mu.Unlock()
|
s.mu.Unlock()
|
||||||
if len(v) == 0 {
|
|
||||||
v = nil
|
return json.Marshal(info)
|
||||||
}
|
|
||||||
return json.Marshal(v)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Stream) removeConsumer(i int) {
|
func (s *Stream) removeConsumer(i int) {
|
||||||
|
|||||||
+26
-11
@@ -1,9 +1,12 @@
|
|||||||
package streams
|
package streams
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"github.com/AlexxIT/go2rtc/cmd/api"
|
||||||
"github.com/AlexxIT/go2rtc/cmd/app"
|
"github.com/AlexxIT/go2rtc/cmd/app"
|
||||||
"github.com/AlexxIT/go2rtc/cmd/app/store"
|
"github.com/AlexxIT/go2rtc/cmd/app/store"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Init() {
|
func Init() {
|
||||||
@@ -22,6 +25,8 @@ func Init() {
|
|||||||
for name, item := range store.GetDict("streams") {
|
for name, item := range store.GetDict("streams") {
|
||||||
streams[name] = NewStream(item)
|
streams[name] = NewStream(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
api.HandleFunc("api/streams", streamsHandler)
|
||||||
}
|
}
|
||||||
|
|
||||||
func Get(name string) *Stream {
|
func Get(name string) *Stream {
|
||||||
@@ -48,19 +53,29 @@ func GetOrNew(src string) *Stream {
|
|||||||
return New(src, src)
|
return New(src, src)
|
||||||
}
|
}
|
||||||
|
|
||||||
func Delete(name string) {
|
func streamsHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
delete(streams, name)
|
src := r.URL.Query().Get("src")
|
||||||
}
|
|
||||||
|
|
||||||
func All() map[string]interface{} {
|
switch r.Method {
|
||||||
all := map[string]interface{}{}
|
case "PUT":
|
||||||
for name, stream := range streams {
|
name := r.URL.Query().Get("name")
|
||||||
all[name] = stream
|
if name == "" {
|
||||||
//if stream.Active() {
|
name = src
|
||||||
// all[name] = stream
|
}
|
||||||
//}
|
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
|
var log zerolog.Logger
|
||||||
|
|||||||
+1
-1
@@ -70,7 +70,7 @@ func RTPDepay(track *streamer.Track) streamer.WrapperFunc {
|
|||||||
buf = buf[:0]
|
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 := *packet
|
||||||
clone.Version = RTPPacketVersionAVC
|
clone.Version = RTPPacketVersionAVC
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package homekit
|
package homekit
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/hap"
|
"github.com/AlexxIT/go2rtc/pkg/hap"
|
||||||
@@ -11,6 +12,7 @@ import (
|
|||||||
"github.com/brutella/hap/rtp"
|
"github.com/brutella/hap/rtp"
|
||||||
"net"
|
"net"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"sync/atomic"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Client struct {
|
type Client struct {
|
||||||
@@ -263,3 +265,19 @@ func (c *Client) getMedias() []*streamer.Media {
|
|||||||
|
|
||||||
return medias
|
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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
+127
-21
@@ -2,8 +2,9 @@ package httpflv
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"errors"
|
"bytes"
|
||||||
"github.com/deepch/vdk/av"
|
"github.com/deepch/vdk/av"
|
||||||
|
"github.com/deepch/vdk/codec/aacparser"
|
||||||
"github.com/deepch/vdk/codec/h264parser"
|
"github.com/deepch/vdk/codec/h264parser"
|
||||||
"github.com/deepch/vdk/format/flv/flvio"
|
"github.com/deepch/vdk/format/flv/flvio"
|
||||||
"github.com/deepch/vdk/utils/bits/pio"
|
"github.com/deepch/vdk/utils/bits/pio"
|
||||||
@@ -41,8 +42,12 @@ func Accept(res *http.Response) (*Conn, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if flags&flvio.FILE_HAS_VIDEO == 0 {
|
if flags&flvio.FILE_HAS_VIDEO != 0 {
|
||||||
return nil, errors.New("not supported")
|
c.videoIdx = -1
|
||||||
|
}
|
||||||
|
|
||||||
|
if flags&flvio.FILE_HAS_AUDIO != 0 {
|
||||||
|
c.audioIdx = -1
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err = c.reader.Discard(n); err != nil {
|
if _, err = c.reader.Discard(n); err != nil {
|
||||||
@@ -56,26 +61,80 @@ type Conn struct {
|
|||||||
conn io.ReadCloser
|
conn io.ReadCloser
|
||||||
reader *bufio.Reader
|
reader *bufio.Reader
|
||||||
buf []byte
|
buf []byte
|
||||||
|
|
||||||
|
videoIdx int8
|
||||||
|
audioIdx int8
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Conn) Streams() ([]av.CodecData, error) {
|
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)
|
tag, _, err := flvio.ReadTag(c.reader, c.buf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if tag.Type != flvio.TAG_VIDEO || tag.AVCPacketType != flvio.AAC_SEQHDR {
|
//log.Printf("[FLV] type=%d avc=%d aac=%d video=%t audio=%t", tag.Type, tag.AVCPacketType, tag.AACPacketType, video != nil, audio != nil)
|
||||||
continue
|
|
||||||
|
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 !waitVideo && !waitAudio {
|
||||||
if err != nil {
|
break
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
|
return []av.CodecData{video}, nil
|
||||||
|
} else if audio != nil {
|
||||||
|
c.audioIdx = 0
|
||||||
|
return []av.CodecData{audio}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Conn) ReadPacket() (av.Packet, error) {
|
func (c *Conn) ReadPacket() (av.Packet, error) {
|
||||||
@@ -85,20 +144,67 @@ func (c *Conn) ReadPacket() (av.Packet, error) {
|
|||||||
return av.Packet{}, err
|
return av.Packet{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if tag.Type != flvio.TAG_VIDEO || tag.AVCPacketType != flvio.AVC_NALU {
|
switch tag.Type {
|
||||||
continue
|
case flvio.TAG_VIDEO:
|
||||||
}
|
if tag.AVCPacketType != flvio.AVC_NALU {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
return av.Packet{
|
return av.Packet{
|
||||||
Idx: 0,
|
Idx: c.videoIdx,
|
||||||
Data: tag.Data,
|
Data: tag.Data,
|
||||||
CompositionTime: flvio.TsToTime(tag.CompositionTime),
|
CompositionTime: flvio.TsToTime(tag.CompositionTime),
|
||||||
IsKeyFrame: tag.FrameType == flvio.FRAME_KEY,
|
IsKeyFrame: tag.FrameType == flvio.FRAME_KEY,
|
||||||
Time: flvio.TsToTime(ts),
|
Time: flvio.TsToTime(ts),
|
||||||
}, nil
|
}, nil
|
||||||
|
|
||||||
|
case flvio.TAG_AUDIO:
|
||||||
|
if 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) {
|
func (c *Conn) Close() (err error) {
|
||||||
return c.conn.Close()
|
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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -41,6 +42,8 @@ type Client struct {
|
|||||||
buffer chan []byte
|
buffer chan []byte
|
||||||
state State
|
state State
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
|
|
||||||
|
recv uint32
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewClient(id string) *Client {
|
func NewClient(id string) *Client {
|
||||||
@@ -109,6 +112,7 @@ func (c *Client) Handle() error {
|
|||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
if c.state == StateHandle {
|
if c.state == StateHandle {
|
||||||
c.buffer <- data
|
c.buffer <- data
|
||||||
|
atomic.AddUint32(&c.recv, uint32(len(data)))
|
||||||
}
|
}
|
||||||
c.mu.Unlock()
|
c.mu.Unlock()
|
||||||
}
|
}
|
||||||
@@ -140,6 +144,7 @@ func (c *Client) Handle() error {
|
|||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
if c.state == StateHandle {
|
if c.state == StateHandle {
|
||||||
c.buffer <- data
|
c.buffer <- data
|
||||||
|
atomic.AddUint32(&c.recv, uint32(len(data)))
|
||||||
}
|
}
|
||||||
c.mu.Unlock()
|
c.mu.Unlock()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
package ivideon
|
package ivideon
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||||
|
"sync/atomic"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (c *Client) GetMedias() []*streamer.Media {
|
func (c *Client) GetMedias() []*streamer.Media {
|
||||||
@@ -29,3 +31,19 @@ func (c *Client) Start() error {
|
|||||||
func (c *Client) Stop() error {
|
func (c *Client) Stop() error {
|
||||||
return c.Close()
|
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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package mjpeg
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
||||||
@@ -11,6 +12,7 @@ import (
|
|||||||
"net/textproto"
|
"net/textproto"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -24,6 +26,7 @@ type Client struct {
|
|||||||
res *http.Response
|
res *http.Response
|
||||||
|
|
||||||
track *streamer.Track
|
track *streamer.Track
|
||||||
|
recv uint32
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewClient(res *http.Response) *Client {
|
func NewClient(res *http.Response) *Client {
|
||||||
@@ -70,6 +73,17 @@ func (c *Client) Stop() error {
|
|||||||
return nil
|
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 {
|
func (c *Client) startJPEG() error {
|
||||||
buf, err := io.ReadAll(c.res.Body)
|
buf, err := io.ReadAll(c.res.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -79,6 +93,8 @@ func (c *Client) startJPEG() error {
|
|||||||
packet := &rtp.Packet{Header: rtp.Header{Timestamp: now()}, Payload: buf}
|
packet := &rtp.Packet{Header: rtp.Header{Timestamp: now()}, Payload: buf}
|
||||||
_ = c.track.WriteRTP(packet)
|
_ = c.track.WriteRTP(packet)
|
||||||
|
|
||||||
|
atomic.AddUint32(&c.recv, uint32(len(buf)))
|
||||||
|
|
||||||
req := c.res.Request
|
req := c.res.Request
|
||||||
|
|
||||||
for !c.closed {
|
for !c.closed {
|
||||||
@@ -98,6 +114,8 @@ func (c *Client) startJPEG() error {
|
|||||||
|
|
||||||
packet = &rtp.Packet{Header: rtp.Header{Timestamp: now()}, Payload: buf}
|
packet = &rtp.Packet{Header: rtp.Header{Timestamp: now()}, Payload: buf}
|
||||||
_ = c.track.WriteRTP(packet)
|
_ = c.track.WriteRTP(packet)
|
||||||
|
|
||||||
|
atomic.AddUint32(&c.recv, uint32(len(buf)))
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -141,6 +159,8 @@ func (c *Client) startMJPEG(boundary string) error {
|
|||||||
packet := &rtp.Packet{Header: rtp.Header{Timestamp: now()}, Payload: buf}
|
packet := &rtp.Packet{Header: rtp.Header{Timestamp: now()}, Payload: buf}
|
||||||
_ = c.track.WriteRTP(packet)
|
_ = c.track.WriteRTP(packet)
|
||||||
|
|
||||||
|
atomic.AddUint32(&c.recv, uint32(len(buf)))
|
||||||
|
|
||||||
if _, err = r.Discard(2); err != nil {
|
if _, err = r.Discard(2); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-1
@@ -1,8 +1,10 @@
|
|||||||
package mjpeg
|
package mjpeg
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||||
"github.com/pion/rtp"
|
"github.com/pion/rtp"
|
||||||
|
"sync/atomic"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Consumer struct {
|
type Consumer struct {
|
||||||
@@ -14,7 +16,7 @@ type Consumer struct {
|
|||||||
codecs []*streamer.Codec
|
codecs []*streamer.Codec
|
||||||
start bool
|
start bool
|
||||||
|
|
||||||
send int
|
send uint32
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Consumer) GetMedias() []*streamer.Media {
|
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 {
|
func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.Track {
|
||||||
push := func(packet *rtp.Packet) error {
|
push := func(packet *rtp.Packet) error {
|
||||||
c.Fire(packet.Payload)
|
c.Fire(packet.Payload)
|
||||||
|
atomic.AddUint32(&c.send, uint32(len(packet.Payload)))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,3 +41,13 @@ func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *strea
|
|||||||
|
|
||||||
return track.Bind(push)
|
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)
|
||||||
|
}
|
||||||
|
|||||||
+11
-11
@@ -7,6 +7,7 @@ import (
|
|||||||
"github.com/AlexxIT/go2rtc/pkg/h265"
|
"github.com/AlexxIT/go2rtc/pkg/h265"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||||
"github.com/pion/rtp"
|
"github.com/pion/rtp"
|
||||||
|
"sync/atomic"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Consumer struct {
|
type Consumer struct {
|
||||||
@@ -20,7 +21,7 @@ type Consumer struct {
|
|||||||
codecs []*streamer.Codec
|
codecs []*streamer.Codec
|
||||||
wait byte
|
wait byte
|
||||||
|
|
||||||
send int
|
send uint32
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -76,7 +77,7 @@ func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *strea
|
|||||||
}
|
}
|
||||||
|
|
||||||
buf := c.muxer.Marshal(trackID, packet)
|
buf := c.muxer.Marshal(trackID, packet)
|
||||||
c.send += len(buf)
|
atomic.AddUint32(&c.send, uint32(len(buf)))
|
||||||
c.Fire(buf)
|
c.Fire(buf)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -108,7 +109,7 @@ func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *strea
|
|||||||
}
|
}
|
||||||
|
|
||||||
buf := c.muxer.Marshal(trackID, packet)
|
buf := c.muxer.Marshal(trackID, packet)
|
||||||
c.send += len(buf)
|
atomic.AddUint32(&c.send, uint32(len(buf)))
|
||||||
c.Fire(buf)
|
c.Fire(buf)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -128,7 +129,7 @@ func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *strea
|
|||||||
}
|
}
|
||||||
|
|
||||||
buf := c.muxer.Marshal(trackID, packet)
|
buf := c.muxer.Marshal(trackID, packet)
|
||||||
c.send += len(buf)
|
atomic.AddUint32(&c.send, uint32(len(buf)))
|
||||||
c.Fire(buf)
|
c.Fire(buf)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -163,12 +164,11 @@ func (c *Consumer) Start() {
|
|||||||
//
|
//
|
||||||
|
|
||||||
func (c *Consumer) MarshalJSON() ([]byte, error) {
|
func (c *Consumer) MarshalJSON() ([]byte, error) {
|
||||||
v := map[string]interface{}{
|
info := &streamer.Info{
|
||||||
"type": "MP4 server consumer",
|
Type: "MP4 client",
|
||||||
"send": c.send,
|
RemoteAddr: c.RemoteAddr,
|
||||||
"remote_addr": c.RemoteAddr,
|
UserAgent: c.UserAgent,
|
||||||
"user_agent": c.UserAgent,
|
Send: atomic.LoadUint32(&c.send),
|
||||||
}
|
}
|
||||||
|
return json.Marshal(info)
|
||||||
return json.Marshal(v)
|
|
||||||
}
|
}
|
||||||
|
|||||||
+21
-1
@@ -1,18 +1,25 @@
|
|||||||
package mp4
|
package mp4
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/h264"
|
"github.com/AlexxIT/go2rtc/pkg/h264"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/h265"
|
"github.com/AlexxIT/go2rtc/pkg/h265"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||||
"github.com/pion/rtp"
|
"github.com/pion/rtp"
|
||||||
|
"sync/atomic"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Segment struct {
|
type Segment struct {
|
||||||
streamer.Element
|
streamer.Element
|
||||||
|
|
||||||
Medias []*streamer.Media
|
Medias []*streamer.Media
|
||||||
|
UserAgent string
|
||||||
|
RemoteAddr string
|
||||||
|
|
||||||
MimeType string
|
MimeType string
|
||||||
OnlyKeyframe bool
|
OnlyKeyframe bool
|
||||||
|
|
||||||
|
send uint32
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Segment) GetMedias() []*streamer.Media {
|
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)
|
buf := muxer.Marshal(0, packet)
|
||||||
|
atomic.AddUint32(&c.send, uint32(len(buf)))
|
||||||
c.Fire(append(init, buf...))
|
c.Fire(append(init, buf...))
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -73,6 +81,7 @@ func (c *Segment) AddTrack(media *streamer.Media, track *streamer.Track) *stream
|
|||||||
buf = append(buf, b...)
|
buf = append(buf, b...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
atomic.AddUint32(&c.send, uint32(len(buf)))
|
||||||
c.Fire(buf)
|
c.Fire(buf)
|
||||||
|
|
||||||
buf = buf[:0]
|
buf = buf[:0]
|
||||||
@@ -106,6 +115,7 @@ func (c *Segment) AddTrack(media *streamer.Media, track *streamer.Track) *stream
|
|||||||
}
|
}
|
||||||
|
|
||||||
buf := muxer.Marshal(0, packet)
|
buf := muxer.Marshal(0, packet)
|
||||||
|
atomic.AddUint32(&c.send, uint32(len(buf)))
|
||||||
c.Fire(append(init, buf...))
|
c.Fire(append(init, buf...))
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -121,3 +131,13 @@ func (c *Segment) AddTrack(media *streamer.Media, track *streamer.Track) *stream
|
|||||||
|
|
||||||
panic("unsupported codec")
|
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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package mp4f
|
package mp4f
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/h264"
|
"github.com/AlexxIT/go2rtc/pkg/h264"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||||
"github.com/deepch/vdk/av"
|
"github.com/deepch/vdk/av"
|
||||||
@@ -149,16 +148,3 @@ func (c *Consumer) Init() ([]byte, error) {
|
|||||||
func (c *Consumer) Start() {
|
func (c *Consumer) Start() {
|
||||||
c.start = true
|
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)
|
|
||||||
}
|
|
||||||
|
|||||||
+3
-2
@@ -12,6 +12,7 @@ import (
|
|||||||
"github.com/deepch/vdk/format/rtmp"
|
"github.com/deepch/vdk/format/rtmp"
|
||||||
"github.com/pion/rtp"
|
"github.com/pion/rtp"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -33,7 +34,7 @@ type Client struct {
|
|||||||
conn Conn
|
conn Conn
|
||||||
closed bool
|
closed bool
|
||||||
|
|
||||||
receive int
|
recv uint32
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewClient(uri string) *Client {
|
func NewClient(uri string) *Client {
|
||||||
@@ -138,7 +139,7 @@ func (c *Client) Handle() (err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.receive += len(pkt.Data)
|
atomic.AddUint32(&c.recv, uint32(len(pkt.Data)))
|
||||||
|
|
||||||
track := c.tracks[int(pkt.Idx)]
|
track := c.tracks[int(pkt.Idx)]
|
||||||
|
|
||||||
|
|||||||
+8
-15
@@ -4,7 +4,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||||
"strconv"
|
"sync/atomic"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (c *Client) GetMedias() []*streamer.Media {
|
func (c *Client) GetMedias() []*streamer.Media {
|
||||||
@@ -29,19 +29,12 @@ func (c *Client) Stop() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) MarshalJSON() ([]byte, error) {
|
func (c *Client) MarshalJSON() ([]byte, error) {
|
||||||
v := map[string]interface{}{
|
info := &streamer.Info{
|
||||||
streamer.JSONReceive: c.receive,
|
Type: "RTMP source",
|
||||||
streamer.JSONType: "RTMP client producer",
|
URL: c.URI,
|
||||||
//streamer.JSONRemoteAddr: c.conn.NetConn().RemoteAddr().String(),
|
Medias: c.medias,
|
||||||
"url": c.URI,
|
Tracks: c.tracks,
|
||||||
|
Recv: atomic.LoadUint32(&c.recv),
|
||||||
}
|
}
|
||||||
for i, media := range c.medias {
|
return json.Marshal(info)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-3
@@ -78,6 +78,7 @@ type Conn struct {
|
|||||||
// public
|
// public
|
||||||
|
|
||||||
Backchannel bool
|
Backchannel bool
|
||||||
|
SessionName string
|
||||||
|
|
||||||
Medias []*streamer.Media
|
Medias []*streamer.Media
|
||||||
Session string
|
Session string
|
||||||
@@ -618,7 +619,7 @@ func (c *Conn) Accept() error {
|
|||||||
medias = append(medias, media)
|
medias = append(medias, media)
|
||||||
}
|
}
|
||||||
|
|
||||||
res.Body, err = streamer.MarshalSDP(medias)
|
res.Body, err = streamer.MarshalSDP(c.SessionName, medias)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -654,6 +655,12 @@ func (c *Conn) Accept() error {
|
|||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
|
|
||||||
|
case MethodTeardown:
|
||||||
|
res := &tcp.Response{Request: req}
|
||||||
|
_ = c.Response(res)
|
||||||
|
c.state = StateNone
|
||||||
|
return c.conn.Close()
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("unsupported method: %s", req.Method)
|
return fmt.Errorf("unsupported method: %s", req.Method)
|
||||||
}
|
}
|
||||||
@@ -792,12 +799,12 @@ func (c *Conn) Handle() (err error) {
|
|||||||
msg := &RTCP{Channel: channelID}
|
msg := &RTCP{Channel: channelID}
|
||||||
|
|
||||||
if err = msg.Header.Unmarshal(buf); err != nil {
|
if err = msg.Header.Unmarshal(buf); err != nil {
|
||||||
return
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
msg.Packets, err = rtcp.Unmarshal(buf)
|
msg.Packets, err = rtcp.Unmarshal(buf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Fire(msg)
|
c.Fire(msg)
|
||||||
|
|||||||
+17
-27
@@ -4,7 +4,6 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||||
"strconv"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Element Producer
|
// Element Producer
|
||||||
@@ -88,40 +87,30 @@ func (c *Conn) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.
|
|||||||
//
|
//
|
||||||
|
|
||||||
func (c *Conn) MarshalJSON() ([]byte, error) {
|
func (c *Conn) MarshalJSON() ([]byte, error) {
|
||||||
v := map[string]interface{}{
|
info := &streamer.Info{
|
||||||
streamer.JSONReceive: c.receive,
|
UserAgent: c.UserAgent,
|
||||||
streamer.JSONSend: c.send,
|
Medias: c.Medias,
|
||||||
|
Tracks: c.tracks,
|
||||||
|
Recv: uint32(c.receive),
|
||||||
|
Send: uint32(c.send),
|
||||||
}
|
}
|
||||||
|
|
||||||
switch c.mode {
|
switch c.mode {
|
||||||
case ModeUnknown:
|
case ModeUnknown:
|
||||||
v[streamer.JSONType] = "RTSP unknown"
|
info.Type = "RTSP unknown"
|
||||||
case ModeClientProducer:
|
case ModeClientProducer, ModeServerProducer:
|
||||||
v[streamer.JSONType] = "RTSP client producer"
|
info.Type = "RTSP source"
|
||||||
case ModeServerProducer:
|
|
||||||
v[streamer.JSONType] = "RTSP server producer"
|
|
||||||
case ModeServerConsumer:
|
case ModeServerConsumer:
|
||||||
v[streamer.JSONType] = "RTSP server consumer"
|
info.Type = "RTSP client"
|
||||||
}
|
}
|
||||||
//if c.URI != "" {
|
|
||||||
// v["uri"] = c.URI
|
|
||||||
//}
|
|
||||||
if c.URL != nil {
|
if c.URL != nil {
|
||||||
v["url"] = c.URL.String()
|
info.URL = c.URL.String()
|
||||||
}
|
}
|
||||||
if c.conn != nil {
|
if c.conn != nil {
|
||||||
v[streamer.JSONRemoteAddr] = c.conn.RemoteAddr().String()
|
info.RemoteAddr = 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()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//for i, track := range c.tracks {
|
//for i, track := range c.tracks {
|
||||||
// k := "track:" + strconv.Itoa(i+1)
|
// k := "track:" + strconv.Itoa(i+1)
|
||||||
// if track.MimeType() == streamer.MimeTypeH264 {
|
// if track.MimeType() == streamer.MimeTypeH264 {
|
||||||
@@ -130,5 +119,6 @@ func (c *Conn) MarshalJSON() ([]byte, error) {
|
|||||||
// v[k] = track.MimeType()
|
// v[k] = track.MimeType()
|
||||||
// }
|
// }
|
||||||
//}
|
//}
|
||||||
return json.Marshal(v)
|
|
||||||
|
return json.Marshal(info)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ package srtp
|
|||||||
import (
|
import (
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"net"
|
"net"
|
||||||
|
"sync/atomic"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Server using same UDP port for SRTP and for SRTCP as the iPhone does
|
// 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 {
|
if err = session.HandleRTP(buf[:n]); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ type Session struct {
|
|||||||
|
|
||||||
Write func(b []byte) (int, error)
|
Write func(b []byte) (int, error)
|
||||||
Track *streamer.Track
|
Track *streamer.Track
|
||||||
|
Recv uint32
|
||||||
|
|
||||||
lastSequence uint32
|
lastSequence uint32
|
||||||
lastTimestamp uint32
|
lastTimestamp uint32
|
||||||
|
|||||||
+10
-7
@@ -4,13 +4,16 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
type Info struct {
|
||||||
JSONType = "type"
|
Type string `json:"type,omitempty"`
|
||||||
JSONRemoteAddr = "remote_addr"
|
URL string `json:"url,omitempty"`
|
||||||
JSONUserAgent = "user_agent"
|
RemoteAddr string `json:"remote_addr,omitempty"`
|
||||||
JSONReceive = "receive"
|
UserAgent string `json:"user_agent,omitempty"`
|
||||||
JSONSend = "send"
|
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 {
|
func Between(s, sub1, sub2 string) string {
|
||||||
i := strings.Index(s, sub1)
|
i := strings.Index(s, sub1)
|
||||||
|
|||||||
+21
-2
@@ -1,6 +1,7 @@
|
|||||||
package streamer
|
package streamer
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/pion/sdp/v3"
|
"github.com/pion/sdp/v3"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -70,6 +71,10 @@ func (m *Media) String() string {
|
|||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *Media) MarshalJSON() ([]byte, error) {
|
||||||
|
return json.Marshal(m.String())
|
||||||
|
}
|
||||||
|
|
||||||
func (m *Media) Clone() *Media {
|
func (m *Media) Clone() *Media {
|
||||||
clone := *m
|
clone := *m
|
||||||
return &clone
|
return &clone
|
||||||
@@ -178,8 +183,22 @@ func UnmarshalSDP(rawSDP []byte) ([]*Media, error) {
|
|||||||
return medias, nil
|
return medias, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func MarshalSDP(medias []*Media) ([]byte, error) {
|
func MarshalSDP(name string, medias []*Media) ([]byte, error) {
|
||||||
sd := &sdp.SessionDescription{}
|
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)
|
payloadType := uint8(96)
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
+11
-3
@@ -1,6 +1,7 @@
|
|||||||
package streamer
|
package streamer
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/pion/rtp"
|
"github.com/pion/rtp"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -22,12 +23,19 @@ func NewTrack(codec *Codec, direction string) *Track {
|
|||||||
|
|
||||||
func (t *Track) String() string {
|
func (t *Track) String() string {
|
||||||
s := t.Codec.String()
|
s := t.Codec.String()
|
||||||
t.sinkMu.RLock()
|
if t.sinkMu.TryRLock() {
|
||||||
s += fmt.Sprintf(", sinks=%d", len(t.sink))
|
s += fmt.Sprintf(", sinks=%d", len(t.sink))
|
||||||
t.sinkMu.RUnlock()
|
t.sinkMu.RUnlock()
|
||||||
|
} else {
|
||||||
|
s += fmt.Sprintf(", sinks=?")
|
||||||
|
}
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *Track) MarshalJSON() ([]byte, error) {
|
||||||
|
return json.Marshal(t.String())
|
||||||
|
}
|
||||||
|
|
||||||
func (t *Track) WriteRTP(p *rtp.Packet) error {
|
func (t *Track) WriteRTP(p *rtp.Packet) error {
|
||||||
t.sinkMu.RLock()
|
t.sinkMu.RLock()
|
||||||
for _, f := range t.sink {
|
for _, f := range t.sink {
|
||||||
|
|||||||
+7
-15
@@ -113,20 +113,12 @@ func (c *Conn) AddCandidate(candidate string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Conn) MarshalJSON() ([]byte, error) {
|
func (c *Conn) MarshalJSON() ([]byte, error) {
|
||||||
v := map[string]interface{}{
|
info := &streamer.Info{
|
||||||
streamer.JSONType: "WebRTC server consumer",
|
Type: "WebRTC client",
|
||||||
streamer.JSONRemoteAddr: c.remote(),
|
RemoteAddr: c.remote(),
|
||||||
|
UserAgent: c.UserAgent,
|
||||||
|
Recv: uint32(c.receive),
|
||||||
|
Send: uint32(c.send),
|
||||||
}
|
}
|
||||||
|
return json.Marshal(info)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,13 +25,18 @@ func NewCandidate(network, address string) (string, error) {
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
cand, err := ice.NewCandidateHost(&ice.CandidateHostConfig{
|
config := &ice.CandidateHostConfig{
|
||||||
Network: network,
|
Network: network,
|
||||||
Address: host,
|
Address: host,
|
||||||
Port: i,
|
Port: i,
|
||||||
Component: ice.ComponentRTP,
|
Component: ice.ComponentRTP,
|
||||||
TCPType: ice.TCPTypePassive,
|
}
|
||||||
})
|
|
||||||
|
if network == "tcp" {
|
||||||
|
config.TCPType = ice.TCPTypePassive
|
||||||
|
}
|
||||||
|
|
||||||
|
cand, err := ice.NewCandidateHost(config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -58,7 +58,7 @@
|
|||||||
0, location.pathname.lastIndexOf("/")
|
0, location.pathname.lastIndexOf("/")
|
||||||
);
|
);
|
||||||
|
|
||||||
fetch(`${baseUrl}/api/devices`)
|
fetch(`${baseUrl}/api/devices`, {cache: 'no-cache'})
|
||||||
.then(r => r.json())
|
.then(r => r.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
document.querySelector("body > table > tbody").innerHTML =
|
document.querySelector("body > table > tbody").innerHTML =
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
<!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', {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>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
+1
-1
@@ -65,7 +65,7 @@
|
|||||||
0, location.pathname.lastIndexOf("/")
|
0, location.pathname.lastIndexOf("/")
|
||||||
);
|
);
|
||||||
|
|
||||||
fetch(`${baseUrl}/api/homekit`)
|
fetch(`${baseUrl}/api/homekit`, {cache: 'no-cache'})
|
||||||
.then(r => r.json())
|
.then(r => r.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
document.querySelector("body > table > tbody").innerHTML =
|
document.querySelector("body > table > tbody").innerHTML =
|
||||||
|
|||||||
+19
-4
@@ -10,6 +10,7 @@
|
|||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
font-family: Arial, Helvetica, sans-serif;
|
font-family: Arial, Helvetica, sans-serif;
|
||||||
|
background-color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
table {
|
table {
|
||||||
@@ -61,6 +62,7 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<script src="main.js"></script>
|
<script src="main.js"></script>
|
||||||
|
<div class="info"></div>
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<input id="src" type="text" placeholder="url">
|
<input id="src" type="text" placeholder="url">
|
||||||
<a id="add" href="#">add</a>
|
<a id="add" href="#">add</a>
|
||||||
@@ -89,7 +91,6 @@
|
|||||||
'<a href="webrtc.html?src={name}">2-way-aud</a>',
|
'<a href="webrtc.html?src={name}">2-way-aud</a>',
|
||||||
'<a href="api/stream.mp4?src={name}">mp4</a>',
|
'<a href="api/stream.mp4?src={name}">mp4</a>',
|
||||||
'<a href="api/stream.mjpeg?src={name}">mjpeg</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="api/streams?src={name}">info</a>',
|
||||||
'<a href="#" data-name="{name}">delete</a>',
|
'<a href="#" data-name="{name}">delete</a>',
|
||||||
];
|
];
|
||||||
@@ -132,11 +133,11 @@
|
|||||||
|
|
||||||
function reload() {
|
function reload() {
|
||||||
const url = new URL("api/streams", location.href);
|
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 = "";
|
tbody.innerHTML = "";
|
||||||
|
|
||||||
for (const [name, value] of Object.entries(data)) {
|
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 => {
|
const links = templates.map(link => {
|
||||||
return link.replace("{name}", encodeURIComponent(name));
|
return link.replace("{name}", encodeURIComponent(name));
|
||||||
}).join(" ");
|
}).join(" ");
|
||||||
@@ -151,7 +152,21 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
reload();
|
const url = new URL("api", location.href);
|
||||||
|
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}`;
|
||||||
|
|
||||||
|
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>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -47,6 +47,7 @@ nav li {
|
|||||||
<li><a href="index.html">Streams</a></li>
|
<li><a href="index.html">Streams</a></li>
|
||||||
<li><a href="devices.html">Devices</a></li>
|
<li><a href="devices.html">Devices</a></li>
|
||||||
<li><a href="homekit.html">HomeKit</a></li>
|
<li><a href="homekit.html">HomeKit</a></li>
|
||||||
|
<li><a href="editor.html">Config</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
` + document.body.innerHTML;
|
` + document.body.innerHTML;
|
||||||
|
|||||||
+42
-46
@@ -228,16 +228,12 @@ export class VideoRTC extends HTMLElement {
|
|||||||
this.video.playsInline = true;
|
this.video.playsInline = true;
|
||||||
this.video.preload = "auto";
|
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.display = "block"; // fix bottom margin 4px
|
||||||
this.video.style.width = "100%";
|
this.video.style.width = "100%";
|
||||||
this.video.style.height = "100%"
|
this.video.style.height = "100%"
|
||||||
|
|
||||||
|
this.appendChild(this.video);
|
||||||
|
|
||||||
if (this.background) return;
|
if (this.background) return;
|
||||||
|
|
||||||
if ("hidden" in document && this.visibilityCheck) {
|
if ("hidden" in document && this.visibilityCheck) {
|
||||||
@@ -392,21 +388,23 @@ export class VideoRTC extends HTMLElement {
|
|||||||
sb.mode = "segments"; // segments or sequence
|
sb.mode = "segments"; // segments or sequence
|
||||||
sb.addEventListener("updateend", () => {
|
sb.addEventListener("updateend", () => {
|
||||||
if (sb.updating) return;
|
if (sb.updating) return;
|
||||||
if (bufLen > 0) {
|
|
||||||
try {
|
try {
|
||||||
sb.appendBuffer(buf.slice(0, bufLen));
|
if (bufLen > 0) {
|
||||||
} catch (e) {
|
const data = buf.slice(0, bufLen);
|
||||||
// console.debug(e);
|
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;
|
} catch (e) {
|
||||||
} else if (sb.buffered && sb.buffered.length) {
|
// console.debug(e);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -504,6 +502,8 @@ export class VideoRTC extends HTMLElement {
|
|||||||
* @param ev {Event}
|
* @param ev {Event}
|
||||||
*/
|
*/
|
||||||
onpcvideo(ev) {
|
onpcvideo(ev) {
|
||||||
|
if (!this.pc) return;
|
||||||
|
|
||||||
/** @type {HTMLVideoElement} */
|
/** @type {HTMLVideoElement} */
|
||||||
const video2 = ev.target;
|
const video2 = ev.target;
|
||||||
const state = this.pc.connectionState;
|
const state = this.pc.connectionState;
|
||||||
@@ -543,46 +543,42 @@ export class VideoRTC extends HTMLElement {
|
|||||||
|
|
||||||
onmjpeg() {
|
onmjpeg() {
|
||||||
this.ondata = data => {
|
this.ondata = data => {
|
||||||
|
this.video.controls = false;
|
||||||
this.video.poster = "data:image/jpeg;base64," + VideoRTC.btoa(data);
|
this.video.poster = "data:image/jpeg;base64," + VideoRTC.btoa(data);
|
||||||
};
|
};
|
||||||
|
|
||||||
this.send({type: "mjpeg"});
|
this.send({type: "mjpeg"});
|
||||||
this.video.controls = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onmp4() {
|
onmp4() {
|
||||||
/** @type {HTMLVideoElement} */
|
/** @type {HTMLCanvasElement} **/
|
||||||
let video2;
|
const canvas = document.createElement("canvas");
|
||||||
|
/** @type {CanvasRenderingContext2D} */
|
||||||
|
let context;
|
||||||
|
|
||||||
this.ondata = data => {
|
/** @type {HTMLVideoElement} */
|
||||||
// first video with default position (set container size)
|
const video2 = document.createElement("video");
|
||||||
// second video with position=absolute and top=0px
|
video2.autoplay = true;
|
||||||
if (video2) {
|
video2.muted = true;
|
||||||
this.removeChild(this.video);
|
|
||||||
this.video.src = "";
|
video2.addEventListener("loadeddata", ev => {
|
||||||
this.video = video2;
|
if (!context) {
|
||||||
video2.style.position = "";
|
canvas.width = video2.videoWidth;
|
||||||
video2.style.top = "";
|
canvas.height = video2.videoHeight;
|
||||||
|
context = canvas.getContext('2d');
|
||||||
}
|
}
|
||||||
|
|
||||||
video2 = this.video.cloneNode();
|
context.drawImage(video2, 0, 0, canvas.width, canvas.height);
|
||||||
video2.style.position = "absolute";
|
|
||||||
video2.style.top = "0px";
|
|
||||||
this.appendChild(video2);
|
|
||||||
|
|
||||||
video2.src = "data:video/mp4;base64," + VideoRTC.btoa(data);
|
this.video.controls = false;
|
||||||
video2.play().catch(() => console.log);
|
this.video.poster = canvas.toDataURL("image/jpeg");
|
||||||
};
|
|
||||||
|
|
||||||
this.ws.addEventListener("close", () => {
|
|
||||||
if (!video2) return;
|
|
||||||
|
|
||||||
this.removeChild(video2);
|
|
||||||
video2.src = "";
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.ondata = data => {
|
||||||
|
video2.src = "data:video/mp4;base64," + VideoRTC.btoa(data);
|
||||||
|
};
|
||||||
|
|
||||||
this.send({type: "mp4", value: this.codecs("mp4")});
|
this.send({type: "mp4", value: this.codecs("mp4")});
|
||||||
this.video.controls = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static btoa(buffer) {
|
static btoa(buffer) {
|
||||||
|
|||||||
@@ -22,6 +22,9 @@ class VideoStream extends VideoRTC {
|
|||||||
|
|
||||||
this.innerHTML = `
|
this.innerHTML = `
|
||||||
<style>
|
<style>
|
||||||
|
video-stream {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
.info {
|
.info {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
|||||||
Reference in New Issue
Block a user