mirror of
https://github.com/AlexxIT/go2rtc.git
synced 2025-09-27 20:52:08 +08:00
Compare commits
76 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
368562c540 | ||
![]() |
6d6e7010b4 | ||
![]() |
4157a53dd8 | ||
![]() |
bdf5654c01 | ||
![]() |
66f729aa0e | ||
![]() |
96d1ef2d2c | ||
![]() |
9739f7f416 | ||
![]() |
654fa32b3a | ||
![]() |
db2263c7fe | ||
![]() |
e6c36f1cf7 | ||
![]() |
110f90cb34 | ||
![]() |
aca3bab238 | ||
![]() |
4df44645d7 | ||
![]() |
097fdfbbb8 | ||
![]() |
dc21a04da7 | ||
![]() |
db255b476a | ||
![]() |
464ea417ef | ||
![]() |
c1fac66329 | ||
![]() |
a6057a2eca | ||
![]() |
7c69ba13b0 | ||
![]() |
2b8bfe8bd9 | ||
![]() |
0bd54da456 | ||
![]() |
9f6af1c9e4 | ||
![]() |
c9dd0e37e4 | ||
![]() |
562872beb8 | ||
![]() |
46a278c067 | ||
![]() |
270fc7c1b6 | ||
![]() |
6feb635522 | ||
![]() |
6f48131e4d | ||
![]() |
f120db71a3 | ||
![]() |
72823af9d0 | ||
![]() |
15d9d4ebf4 | ||
![]() |
b09bbd79c4 | ||
![]() |
1830273f02 | ||
![]() |
07f3972794 | ||
![]() |
4c2ebd20bc | ||
![]() |
440c7bd6e1 | ||
![]() |
74c3510a10 | ||
![]() |
c2748fc77b | ||
![]() |
d334551591 | ||
![]() |
cfe20925ac | ||
![]() |
5b39f78ace | ||
![]() |
b965c191b7 | ||
![]() |
7057b4846f | ||
![]() |
a746b96adc | ||
![]() |
b7718b33b8 | ||
![]() |
69b17230f3 | ||
![]() |
e2ecd909ab | ||
![]() |
ea79da0d53 | ||
![]() |
e64919838c | ||
![]() |
162b11213d | ||
![]() |
d27acbd7e3 | ||
![]() |
a692ecd7c1 | ||
![]() |
98c5366ba9 | ||
![]() |
3eaaa3fcfa | ||
![]() |
7409b32836 | ||
![]() |
e2cfdf8419 | ||
![]() |
4c0929d854 | ||
![]() |
258a0ffb91 | ||
![]() |
999e81c2dd | ||
![]() |
8c6729027b | ||
![]() |
d3bd5eeab5 | ||
![]() |
dbbf2ea310 | ||
![]() |
b8234e0c76 | ||
![]() |
96cd753e27 | ||
![]() |
c522e5bb08 | ||
![]() |
a16d8acc30 | ||
![]() |
684878b4b1 | ||
![]() |
140a742cee | ||
![]() |
1b518b94fd | ||
![]() |
5678121c50 | ||
![]() |
4915f12bde | ||
![]() |
bb91240b95 | ||
![]() |
b1d5d53832 | ||
![]() |
31fbbf91bb | ||
![]() |
a3f72fbab9 |
59
.github/workflows/builder.yml
vendored
59
.github/workflows/builder.yml
vendored
@@ -1,59 +0,0 @@
|
|||||||
# https://github.com/home-assistant/builder
|
|
||||||
name: 'Builder'
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags: [ 'v*' ]
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
hassio:
|
|
||||||
name: Hassio Addon
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout the repository
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Login to DockerHub
|
|
||||||
uses: docker/login-action@v2
|
|
||||||
with:
|
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Branch name
|
|
||||||
run: |
|
|
||||||
VERSION="${GITHUB_REF#refs/tags/v}"
|
|
||||||
echo "REPO=alexxit/go2rtc" >> $GITHUB_ENV
|
|
||||||
echo "TAG=${VERSION}" >> $GITHUB_ENV
|
|
||||||
echo "IMAGE=alexxit/go2rtc:${VERSION}" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: Build amd64
|
|
||||||
uses: home-assistant/builder@master
|
|
||||||
with:
|
|
||||||
args: --amd64 --target build/hassio --version $TAG-amd64 --no-latest --docker-hub-check
|
|
||||||
|
|
||||||
- name: Build i386
|
|
||||||
uses: home-assistant/builder@master
|
|
||||||
with:
|
|
||||||
args: --i386 --target build/hassio --version $TAG-i386 --no-latest --docker-hub-check
|
|
||||||
|
|
||||||
- name: Build aarch64
|
|
||||||
uses: home-assistant/builder@master
|
|
||||||
with:
|
|
||||||
args: --aarch64 --target build/hassio --version $TAG-aarch64 --no-latest --docker-hub-check
|
|
||||||
|
|
||||||
- name: Build armv7
|
|
||||||
uses: home-assistant/builder@master
|
|
||||||
with:
|
|
||||||
args: --armv7 --target build/hassio --version $TAG-armv7 --no-latest --docker-hub-check
|
|
||||||
|
|
||||||
- name: Docker manifest
|
|
||||||
run: |
|
|
||||||
# thanks to https://github.com/aler9/rtsp-simple-server/blob/main/Makefile
|
|
||||||
docker manifest create "${IMAGE}" \
|
|
||||||
"${IMAGE}-amd64" "${IMAGE}-i386" "${IMAGE}-aarch64" "${IMAGE}-armv7"
|
|
||||||
docker manifest push "${IMAGE}"
|
|
||||||
|
|
||||||
docker manifest create "${REPO}:latest" \
|
|
||||||
"${IMAGE}-amd64" "${IMAGE}-i386" "${IMAGE}-aarch64" "${IMAGE}-armv7"
|
|
||||||
docker manifest push "${REPO}:latest"
|
|
75
.github/workflows/ci.yml
vendored
Normal file
75
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
name: ci
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- 'master'
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-push:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Docker meta
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v4
|
||||||
|
with:
|
||||||
|
images: alexxit/go2rtc
|
||||||
|
tags: |
|
||||||
|
type=ref,event=branch
|
||||||
|
type=semver,pattern={{version}},enable=false
|
||||||
|
type=match,pattern=v(.*),group=1
|
||||||
|
|
||||||
|
- name: Docker meta Hardware
|
||||||
|
id: meta-hw
|
||||||
|
uses: docker/metadata-action@v4
|
||||||
|
with:
|
||||||
|
images: alexxit/go2rtc
|
||||||
|
flavor: |
|
||||||
|
suffix=-hardware
|
||||||
|
latest=false
|
||||||
|
tags: |
|
||||||
|
type=ref,event=branch
|
||||||
|
type=semver,pattern={{version}},enable=false
|
||||||
|
type=match,pattern=v(.*),group=1
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v2
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v2
|
||||||
|
|
||||||
|
- name: Login to DockerHub
|
||||||
|
if: github.event_name != 'pull_request'
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build and push
|
||||||
|
uses: docker/build-push-action@v3
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
platforms: |
|
||||||
|
linux/amd64
|
||||||
|
linux/386
|
||||||
|
linux/arm/v7
|
||||||
|
linux/arm64/v8
|
||||||
|
push: ${{ github.event_name != 'pull_request' }}
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
|
||||||
|
- name: Build and push Hardware
|
||||||
|
uses: docker/build-push-action@v3
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: hardware.Dockerfile
|
||||||
|
platforms: linux/amd64
|
||||||
|
push: ${{ github.event_name != 'pull_request' }}
|
||||||
|
tags: ${{ steps.meta-hw.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta-hw.outputs.labels }}
|
55
Dockerfile
Normal file
55
Dockerfile
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
# 0. Prepare images
|
||||||
|
ARG PYTHON_VERSION="3.11"
|
||||||
|
ARG GO_VERSION="1.19"
|
||||||
|
ARG NGROK_VERSION="3"
|
||||||
|
|
||||||
|
FROM python:${PYTHON_VERSION}-alpine AS base
|
||||||
|
FROM golang:${GO_VERSION}-alpine AS go
|
||||||
|
FROM ngrok/ngrok:${NGROK_VERSION}-alpine AS ngrok
|
||||||
|
|
||||||
|
|
||||||
|
# 1. Build go2rtc binary
|
||||||
|
FROM go AS build
|
||||||
|
|
||||||
|
WORKDIR /build
|
||||||
|
|
||||||
|
# Cache dependencies
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
RUN go mod download
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
RUN CGO_ENABLED=0 go build -ldflags "-s -w" -trimpath
|
||||||
|
|
||||||
|
|
||||||
|
# 2. Collect all files
|
||||||
|
FROM scratch AS rootfs
|
||||||
|
|
||||||
|
COPY --from=build /build/go2rtc /usr/local/bin/
|
||||||
|
COPY --from=ngrok /bin/ngrok /usr/local/bin/
|
||||||
|
COPY ./build/docker/run.sh /
|
||||||
|
|
||||||
|
|
||||||
|
# 3. Final image
|
||||||
|
FROM base
|
||||||
|
|
||||||
|
# Install ffmpeg, bash (for run.sh), tini (for signal handling),
|
||||||
|
# and other common tools for the echo source.
|
||||||
|
RUN apk add --no-cache tini ffmpeg bash curl jq
|
||||||
|
|
||||||
|
# Hardware Acceleration for Intel CPU (+50MB)
|
||||||
|
ARG TARGETARCH
|
||||||
|
|
||||||
|
RUN if [ "${TARGETARCH}" = "amd64" ]; then apk add --no-cache libva-intel-driver intel-media-driver; fi
|
||||||
|
|
||||||
|
# Hardware: AMD and NVidia VAAPI (not sure about this)
|
||||||
|
# RUN libva-glx mesa-va-gallium
|
||||||
|
# Hardware: AMD and NVidia VDPAU (not sure about this)
|
||||||
|
# RUN libva-vdpau-driver mesa-vdpau-gallium (+150MB total)
|
||||||
|
|
||||||
|
COPY --from=rootfs / /
|
||||||
|
|
||||||
|
RUN chmod a+x /run.sh && mkdir -p /config
|
||||||
|
|
||||||
|
ENTRYPOINT ["/sbin/tini", "--"]
|
||||||
|
|
||||||
|
CMD ["/run.sh"]
|
136
README.md
136
README.md
@@ -6,10 +6,10 @@ Ultimate camera streaming application with support RTSP, WebRTC, HomeKit, FFmpeg
|
|||||||
|
|
||||||
- zero-dependency and zero-config [small app](#go2rtc-binary) for all OS (Windows, macOS, Linux, ARM)
|
- zero-dependency and zero-config [small app](#go2rtc-binary) for all OS (Windows, macOS, Linux, ARM)
|
||||||
- zero-delay for many supported protocols (lowest possible streaming latency)
|
- zero-delay for many supported protocols (lowest possible streaming latency)
|
||||||
- streaming from [RTSP](#source-rtsp), [RTMP](#source-rtmp), [MJPEG](#source-ffmpeg), [HLS/HTTP](#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 (only Safari)
|
- first project in the World with support H265 for WebRTC in browser ([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
|
||||||
@@ -50,13 +50,14 @@ 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)
|
||||||
- `go2rtc_linux_arm` - Linux ARM 32-bit (ex. Raspberry 32-bit OS)
|
- `go2rtc_linux_arm` - Linux ARM 32-bit (ex. Raspberry 32-bit OS)
|
||||||
- `go2rtc_linux_mipsel` - Linux on MIPS (ex. [Xiaomi Gateway 3](https://github.com/AlexxIT/XiaomiGateway3))
|
- `go2rtc_linux_mipsel` - Linux MIPS (ex. [Xiaomi Gateway 3](https://github.com/AlexxIT/XiaomiGateway3))
|
||||||
- `go2rtc_mac_amd64` - Mac with Intel
|
- `go2rtc_mac_amd64.zip` - Mac Intel 64-bit
|
||||||
- `go2rtc_mac_arm64` - Mac with M1
|
- `go2rtc_mac_arm64.zip` - Mac ARM 64-bit
|
||||||
|
|
||||||
Don't forget to fix the rights `chmod +x go2rtc_xxx_xxx` on Linux and Mac.
|
Don't forget to fix the rights `chmod +x go2rtc_xxx_xxx` on Linux and Mac.
|
||||||
|
|
||||||
@@ -99,7 +100,8 @@ Available modules:
|
|||||||
- [api](#module-api) - HTTP API (important for WebRTC support)
|
- [api](#module-api) - HTTP API (important for WebRTC support)
|
||||||
- [rtsp](#module-rtsp) - RTSP Server (important for FFmpeg support)
|
- [rtsp](#module-rtsp) - RTSP Server (important for FFmpeg support)
|
||||||
- [webrtc](#module-webrtc) - WebRTC Server
|
- [webrtc](#module-webrtc) - WebRTC Server
|
||||||
- [mp4](#module-mp4) - MSE, MP4 stream and MP4 shapshot
|
- [mp4](#module-mp4) - MSE, MP4 stream and MP4 shapshot Server
|
||||||
|
- [mjpeg](#module-mjpeg) - MJPEG Server
|
||||||
- [ffmpeg](#source-ffmpeg) - FFmpeg integration
|
- [ffmpeg](#source-ffmpeg) - FFmpeg integration
|
||||||
- [ngrok](#module-ngrok) - Ngrok integration (external access for private network)
|
- [ngrok](#module-ngrok) - Ngrok integration (external access for private network)
|
||||||
- [hass](#module-hass) - Home Assistant integration
|
- [hass](#module-hass) - Home Assistant integration
|
||||||
@@ -112,8 +114,9 @@ Available modules:
|
|||||||
Available source types:
|
Available source types:
|
||||||
|
|
||||||
- [rtsp](#source-rtsp) - `RTSP` and `RTSPS` cameras
|
- [rtsp](#source-rtsp) - `RTSP` and `RTSPS` cameras
|
||||||
- [rtmp](#source-rtmp) - `RTMP` and `HTTP-FLV` streams
|
- [rtmp](#source-rtmp) - `RTMP` streams
|
||||||
- [ffmpeg](#source-ffmpeg) - FFmpeg integration (`MJPEG`, `HLS`, `files` and others)
|
- [http](#source-http) - `HTTP-FLV`, `JPEG` (snapshots), `MJPEG` streams
|
||||||
|
- [ffmpeg](#source-ffmpeg) - FFmpeg integration (`HLS`, `files` and many others)
|
||||||
- [ffmpeg:device](#source-ffmpeg-device) - local USB Camera or Webcam
|
- [ffmpeg:device](#source-ffmpeg-device) - local USB Camera or Webcam
|
||||||
- [exec](#source-exec) - advanced FFmpeg and GStreamer integration
|
- [exec](#source-exec) - advanced FFmpeg and GStreamer integration
|
||||||
- [echo](#source-echo) - get stream link from bash or python
|
- [echo](#source-echo) - get stream link from bash or python
|
||||||
@@ -148,13 +151,35 @@ streams:
|
|||||||
|
|
||||||
#### Source: RTMP
|
#### Source: RTMP
|
||||||
|
|
||||||
You can get stream from RTMP server, for example [Frigate](https://docs.frigate.video/configuration/rtmp). Support ONLY `H264` video codec without audio.
|
You can get stream from RTMP server, for example [Frigate](https://docs.frigate.video/configuration/rtmp).
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
streams:
|
streams:
|
||||||
rtmp_stream: rtmp://192.168.1.123/live/camera1
|
rtmp_stream: rtmp://192.168.1.123/live/camera1
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Source: HTTP
|
||||||
|
|
||||||
|
Support Content-Type:
|
||||||
|
|
||||||
|
- **HTTP-FLV** (`video/x-flv`) - same as RTMP, but over HTTP
|
||||||
|
- **HTTP-JPEG** (`image/jpeg`) - camera snapshot link, can be converted by go2rtc to MJPEG stream
|
||||||
|
- **HTTP-MJPEG** (`multipart/x`) - simple MJPEG stream over HTTP
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
streams:
|
||||||
|
# [HTTP-FLV] stream in video/x-flv format
|
||||||
|
http_flv: http://192.168.1.123:20880/api/camera/stream/780900131155/657617
|
||||||
|
|
||||||
|
# [JPEG] snapshots from Dahua camera, will be converted to MJPEG stream
|
||||||
|
dahua_snap: http://admin:password@192.168.1.123/cgi-bin/snapshot.cgi?channel=1
|
||||||
|
|
||||||
|
# [MJPEG] stream will be proxied without modification
|
||||||
|
http_mjpeg: https://mjpeg.sanford.io/count.mjpeg
|
||||||
|
```
|
||||||
|
|
||||||
|
**PS.** Dahua camera has bug: if you select MJPEG codec for RTSP second stream - snapshot won't work.
|
||||||
|
|
||||||
#### Source: FFmpeg
|
#### Source: FFmpeg
|
||||||
|
|
||||||
You can get any stream or file or device via FFmpeg and push it to go2rtc. The app will automatically start FFmpeg with the proper arguments when someone starts watching the stream.
|
You can get any stream or file or device via FFmpeg and push it to go2rtc. The app will automatically start FFmpeg with the proper arguments when someone starts watching the stream.
|
||||||
@@ -305,7 +330,34 @@ More cameras, like [Tuya](https://www.home-assistant.io/integrations/tuya/), [ON
|
|||||||
|
|
||||||
The HTTP API is the main part for interacting with the application. Default address: `http://127.0.0.1:1984/`.
|
The HTTP API is the main part for interacting with the application. Default address: `http://127.0.0.1:1984/`.
|
||||||
|
|
||||||
- you can use WebRTC only when HTTP API enabled
|
go2rtc has its own JS video player (`video-rtc.js`) with:
|
||||||
|
|
||||||
|
- support technologies:
|
||||||
|
- WebRTC over UDP or TCP
|
||||||
|
- MSE or MP4 or MJPEG over WebSocket
|
||||||
|
- automatic selection best technology according on:
|
||||||
|
- codecs inside your stream
|
||||||
|
- current browser capabilities
|
||||||
|
- current network configuration
|
||||||
|
- automatic stop stream while browser or page not active
|
||||||
|
- automatic stop stream while player not inside page viewport
|
||||||
|
- automatic reconnection
|
||||||
|
|
||||||
|
Technology selection based on priorities:
|
||||||
|
|
||||||
|
1. Video and Audio better than just Video
|
||||||
|
2. H265 better than H264
|
||||||
|
3. WebRTC better than MSE, than MP4, than MJPEG
|
||||||
|
|
||||||
|
go2rtc has simple HTML page (`stream.html`) with support params in URL:
|
||||||
|
|
||||||
|
- multiple streams on page `src=camera1&src=camera2...`
|
||||||
|
- stream technology autoselection `mode=webrtc,mse,mp4,mjpeg`
|
||||||
|
- stream technology comparison `src=camera1&mode=webrtc&mode=mse&mode=mp4`
|
||||||
|
- player width setting in pixels `width=320px` or percents `width=50%`
|
||||||
|
|
||||||
|
**Module config**
|
||||||
|
|
||||||
- you can disable HTTP API with `listen: ""` and use, for example, only RTSP client/server protocol
|
- you can disable HTTP API with `listen: ""` and use, for example, only RTSP client/server protocol
|
||||||
- you can enable HTTP API only on localhost with `listen: "127.0.0.1:1984"` setting
|
- you can enable HTTP API only on localhost with `listen: "127.0.0.1:1984"` setting
|
||||||
- you can change API `base_path` and host go2rtc on your main app webserver suburl
|
- you can change API `base_path` and host go2rtc on your main app webserver suburl
|
||||||
@@ -313,16 +365,20 @@ The HTTP API is the main part for interacting with the application. Default addr
|
|||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
api:
|
api:
|
||||||
listen: ":1984" # HTTP API port ("" - disabled)
|
listen: ":1984" # default ":1984", HTTP API port ("" - disabled)
|
||||||
base_path: "/rtc" # API prefix for serve on suburl (/api => /rtc/api)
|
base_path: "/rtc" # default "", API prefix for serve on suburl (/api => /rtc/api)
|
||||||
static_dir: "www" # folder for static files (custom web interface)
|
static_dir: "www" # default "", folder for static files (custom web interface)
|
||||||
origin: "*" # 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. 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.
|
||||||
|
|
||||||
**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)).
|
**PS2.** You can access microphone (for 2-way audio) only with HTTPS ([read more](https://stackoverflow.com/questions/52759992/how-to-access-camera-and-microphone-in-chrome-without-https)).
|
||||||
|
|
||||||
|
**PS3.** MJPEG over WebSocket plays better than native MJPEG because Chrome [bug](https://bugs.chromium.org/p/chromium/issues/detail?id=527446).
|
||||||
|
|
||||||
|
**PS4.** MP4 over WebSocket was created only for Apple iOS because it doesn't support MSE and native MP4.
|
||||||
|
|
||||||
### Module: RTSP
|
### Module: RTSP
|
||||||
|
|
||||||
You can get any stream as RTSP-stream: `rtsp://192.168.1.123:8554/{stream_name}`
|
You can get any stream as RTSP-stream: `rtsp://192.168.1.123:8554/{stream_name}`
|
||||||
@@ -330,10 +386,15 @@ You can get any stream as RTSP-stream: `rtsp://192.168.1.123:8554/{stream_name}`
|
|||||||
- you can omit the codec filters, so one first video and one first audio will be selected
|
- you can omit the codec filters, so one first video and one first audio will be selected
|
||||||
- you can set `?video=copy` or just `?video`, so only one first video without audio will be selected
|
- you can set `?video=copy` or just `?video`, so only one first video without audio will be selected
|
||||||
- you can set multiple video or audio, so all of them will be selected
|
- you can set multiple video or audio, so all of them will be selected
|
||||||
|
- you can enable external password protection for your RTSP streams
|
||||||
|
|
||||||
|
Password protection always disabled for localhost calls (ex. FFmpeg or Hass on same server)
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
rtsp:
|
rtsp:
|
||||||
listen: ":8554"
|
listen: ":8554" # RTSP Server TCP port, default - 8554
|
||||||
|
username: admin # optional, default - disabled
|
||||||
|
password: pass # optional, default - disabled
|
||||||
```
|
```
|
||||||
|
|
||||||
### Module: WebRTC
|
### Module: WebRTC
|
||||||
@@ -472,7 +533,8 @@ View almost any Hass camera using `WebRTC` technology, supported codecs `H264`/`
|
|||||||
When the stream starts - the camera `entity_id` will be added to go2rtc "on the fly". You don't need to add cameras manually to [go2rtc config](#configuration). Some cameras (like [Nest](https://www.home-assistant.io/integrations/nest/)) have a dynamic link to the stream, it will be updated each time a stream is started from the Hass interface.
|
When the stream starts - the camera `entity_id` will be added to go2rtc "on the fly". You don't need to add cameras manually to [go2rtc config](#configuration). Some cameras (like [Nest](https://www.home-assistant.io/integrations/nest/)) have a dynamic link to the stream, it will be updated each time a stream is started from the Hass interface.
|
||||||
|
|
||||||
1. Hass > Settings > Integrations > Add Integration > [RTSPtoWebRTC](https://my.home-assistant.io/redirect/config_flow_start/?domain=rtsp_to_webrtc) > `http://127.0.0.1:1984/`
|
1. Hass > Settings > Integrations > Add Integration > [RTSPtoWebRTC](https://my.home-assistant.io/redirect/config_flow_start/?domain=rtsp_to_webrtc) > `http://127.0.0.1:1984/`
|
||||||
2. Use Picture Entity or Picture Glance lovelace card
|
2. RTSPtoWebRTC > Configure > STUN server: `stun.l.google.com:19302`
|
||||||
|
3. Use Picture Entity or Picture Glance lovelace card
|
||||||
|
|
||||||
You can add camera `entity_id` to [go2rtc config](#configuration) if you need transcoding:
|
You can add camera `entity_id` to [go2rtc config](#configuration) if you need transcoding:
|
||||||
|
|
||||||
@@ -489,20 +551,32 @@ 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://www.telegram.org/)
|
||||||
3. Progressive MP4 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 latency, doesn't work in Safari
|
||||||
|
|
||||||
### Module: MJPEG
|
### Module: MJPEG
|
||||||
|
|
||||||
**Important.** For stream as MJPEG format, your source MUST contain the MJPEG codec. If your camera outputs H264/H265 - you SHOULD use transcoding. With this example, your stream will have both H264 and MJPEG codecs:
|
**Important.** For stream as MJPEG format, your source MUST contain the MJPEG codec. If your stream has a MJPEG codec - you can receive **MJPEG stream** or **JPEG snapshots** via API.
|
||||||
|
|
||||||
|
You can receive an MJPEG stream in several ways:
|
||||||
|
|
||||||
|
- some cameras support MJPEG codec inside [RTSP stream](#source-rtsp) (ex. second stream for Dahua cameras)
|
||||||
|
- some cameras has HTTP link with [MJPEG stream](#source-http)
|
||||||
|
- some cameras has HTTP link with snapshots - go2rtc can convert them to [MJPEG stream](#source-http)
|
||||||
|
- you can convert H264/H265 stream from your camera via [FFmpeg integraion](#source-ffmpeg)
|
||||||
|
|
||||||
|
With this example, your stream will have both H264 and MJPEG codecs:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
streams:
|
streams:
|
||||||
camera1:
|
camera1:
|
||||||
- rtsp://rtsp:12345678@192.168.1.123/av_stream/ch0
|
- rtsp://rtsp:12345678@192.168.1.123/av_stream/ch0
|
||||||
- ffmpeg:rtsp://rtsp:12345678@192.168.1.123/av_stream/ch0#video=mjpeg
|
- ffmpeg:camera1#video=mjpeg
|
||||||
```
|
```
|
||||||
|
|
||||||
Example link to MJPEG: `http://192.168.1.123:1984/api/stream.mjpeg?src=camera1`
|
API examples:
|
||||||
|
|
||||||
|
- MJPEG stream: `http://192.168.1.123:1984/api/stream.mjpeg?src=camera1`
|
||||||
|
- JPEG snapshots: `http://192.168.1.123:1984/api/frame.jpeg?src=camera1`
|
||||||
|
|
||||||
### Module: Log
|
### Module: Log
|
||||||
|
|
||||||
@@ -549,17 +623,17 @@ PS. Additionally WebRTC opens a lot of random UDP ports for transmit encrypted m
|
|||||||
|
|
||||||
`AVC/H.264` codec can be played almost anywhere. But `HEVC/H.265` has a lot of limitations in supporting with different devices and browsers. It's all about patents and money, you can't do anything about it.
|
`AVC/H.264` codec can be played almost anywhere. But `HEVC/H.265` has a lot of limitations in supporting with different devices and browsers. It's all about patents and money, you can't do anything about it.
|
||||||
|
|
||||||
Device | WebRTC | MSE | MP4
|
| Device | WebRTC | MSE | MP4 |
|
||||||
-------|--------|-----|----
|
|---------------------|-------------|-------------|-------------|
|
||||||
*latency* | best | medium | bad
|
| *latency* | best | medium | bad |
|
||||||
Desktop Chrome 107+ | H264 | H264, H265* | H264, H265*
|
| Desktop Chrome 107+ | H264 | H264, H265* | H264, H265* |
|
||||||
Desktop Safari | H264, H265* | H264, H265 | **no!**
|
| Desktop Edge | H264 | H264, H265* | H264, H265* |
|
||||||
Desktop Edge | H264 | H264, H265* | H264, H265*
|
| Desktop Safari | H264, H265* | H264, H265 | **no!** |
|
||||||
Desktop Firefox | H264 | H264 | H264
|
| Desktop Firefox | H264 | H264 | H264 |
|
||||||
iPad Safari 13+ | H264, H265* | H264, H265 | **no!**
|
| Android Chrome 107+ | H264 | H264, H265* | H264 |
|
||||||
iPhone Safari 13+ | H264, H265* | **no!** | **no!**
|
| iPad Safari 13+ | H264, H265* | H264, H265 | **no!** |
|
||||||
Android Chrome 107+ | H264 | H264, H265* | H264
|
| iPhone Safari 13+ | H264, H265* | **no!** | **no!** |
|
||||||
masOS Hass App | no | no | no
|
| masOS Hass App | no | no | no |
|
||||||
|
|
||||||
- Chrome H265: [read this](https://chromestatus.com/feature/5186511939567616) and [read this](https://github.com/StaZhu/enable-chromium-hevc-hardware-decoding)
|
- Chrome H265: [read this](https://chromestatus.com/feature/5186511939567616) and [read this](https://github.com/StaZhu/enable-chromium-hevc-hardware-decoding)
|
||||||
- Edge H265: [read this](https://www.reddit.com/r/MicrosoftEdge/comments/v9iw8k/enable_hevc_support_in_edge/)
|
- Edge H265: [read this](https://www.reddit.com/r/MicrosoftEdge/comments/v9iw8k/enable_hevc_support_in_edge/)
|
||||||
|
19
build/docker/run.sh
Normal file
19
build/docker/run.sh
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
echo "Starting go2rtc..." >&2
|
||||||
|
|
||||||
|
readonly config_path="/config"
|
||||||
|
|
||||||
|
if [[ -x "${config_path}/go2rtc" ]]; then
|
||||||
|
readonly binary_path="${config_path}/go2rtc"
|
||||||
|
echo "Using go2rtc binary from '${binary_path}' instead of the embedded one" >&2
|
||||||
|
else
|
||||||
|
readonly binary_path="/usr/local/bin/go2rtc"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# set cwd for go2rtc (for config file, Hass integration, etc)
|
||||||
|
cd "${config_path}" || echo "Could not change working directory to '${config_path}'" >&2
|
||||||
|
|
||||||
|
exec "${binary_path}"
|
@@ -1,41 +0,0 @@
|
|||||||
ARG BUILD_FROM
|
|
||||||
|
|
||||||
FROM $BUILD_FROM as build
|
|
||||||
|
|
||||||
# 1. Build go2rtc
|
|
||||||
RUN apk add --no-cache git go
|
|
||||||
|
|
||||||
RUN git clone https://github.com/AlexxIT/go2rtc \
|
|
||||||
&& cd go2rtc \
|
|
||||||
&& CGO_ENABLED=0 go build -ldflags "-s -w" -trimpath
|
|
||||||
|
|
||||||
# 2. Download ngrok
|
|
||||||
ARG BUILD_ARCH
|
|
||||||
|
|
||||||
# https://github.com/home-assistant/docker-base/blob/master/alpine/Dockerfile
|
|
||||||
RUN if [ "${BUILD_ARCH}" = "aarch64" ]; then BUILD_ARCH="arm64"; \
|
|
||||||
elif [ "${BUILD_ARCH}" = "armv7" ]; then BUILD_ARCH="arm"; fi \
|
|
||||||
&& cd go2rtc \
|
|
||||||
&& curl $(curl -s "https://raw.githubusercontent.com/ngrok/docker-ngrok/main/releases.json" | jq -r ".${BUILD_ARCH}.url") -o ngrok.zip \
|
|
||||||
&& unzip ngrok
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# https://devopscube.com/reduce-docker-image-size/
|
|
||||||
FROM $BUILD_FROM
|
|
||||||
|
|
||||||
# 3. Copy go2rtc and ngrok to release
|
|
||||||
COPY --from=build /go2rtc/go2rtc /usr/local/bin
|
|
||||||
COPY --from=build /go2rtc/ngrok /usr/local/bin
|
|
||||||
|
|
||||||
# 4. Install ffmpeg
|
|
||||||
# apk base OK: 22 MiB in 40 packages
|
|
||||||
# ffmpeg OK: 113 MiB in 110 packages
|
|
||||||
# python3 OK: 161 MiB in 114 packages
|
|
||||||
RUN apk add --no-cache ffmpeg python3
|
|
||||||
|
|
||||||
# 5. Copy run to release
|
|
||||||
COPY run.sh /
|
|
||||||
RUN chmod a+x /run.sh
|
|
||||||
|
|
||||||
CMD [ "/run.sh" ]
|
|
@@ -1,6 +0,0 @@
|
|||||||
# https://github.com/home-assistant/builder/blob/master/builder.sh
|
|
||||||
name: go2rtc
|
|
||||||
description: Ultimate camera streaming application
|
|
||||||
url: https://github.com/AlexxIT/go2rtc
|
|
||||||
image: alexxit/go2rtc
|
|
||||||
arch: [ amd64, aarch64, i386, armv7 ]
|
|
@@ -1,14 +0,0 @@
|
|||||||
#!/usr/bin/with-contenv bashio
|
|
||||||
|
|
||||||
set +e
|
|
||||||
|
|
||||||
# set cwd for go2rtc (for config file, Hass integration, etc)
|
|
||||||
cd /config
|
|
||||||
|
|
||||||
# add the feature to override go2rtc binary from Hass config folder
|
|
||||||
export PATH="/config:$PATH"
|
|
||||||
|
|
||||||
while true; do
|
|
||||||
go2rtc
|
|
||||||
sleep 5
|
|
||||||
done
|
|
@@ -4,8 +4,6 @@ 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/AlexxIT/go2rtc/cmd/streams"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
|
||||||
"github.com/gorilla/websocket"
|
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -78,13 +76,10 @@ func HandleFunc(pattern string, handler http.HandlerFunc) {
|
|||||||
http.HandleFunc(pattern, handler)
|
http.HandleFunc(pattern, handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
func HandleWS(msgType string, handler WSHandler) {
|
const StreamNotFound = "stream not found"
|
||||||
wsHandlers[msgType] = handler
|
|
||||||
}
|
|
||||||
|
|
||||||
var basePath string
|
var basePath string
|
||||||
var log zerolog.Logger
|
var log zerolog.Logger
|
||||||
var wsHandlers = make(map[string]WSHandler)
|
|
||||||
|
|
||||||
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) {
|
||||||
@@ -129,29 +124,3 @@ func streamsHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
e.SetIndent("", " ")
|
e.SetIndent("", " ")
|
||||||
_ = e.Encode(v)
|
_ = e.Encode(v)
|
||||||
}
|
}
|
||||||
|
|
||||||
func apiWS(w http.ResponseWriter, r *http.Request) {
|
|
||||||
ctx := new(Context)
|
|
||||||
if err := ctx.Upgrade(w, r); err != nil {
|
|
||||||
log.Error().Err(err).Msg("[api.ws] upgrade")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer ctx.Close()
|
|
||||||
|
|
||||||
for {
|
|
||||||
msg := new(streamer.Message)
|
|
||||||
if err := ctx.Conn.ReadJSON(msg); err != nil {
|
|
||||||
if websocket.IsUnexpectedCloseError(
|
|
||||||
err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure,
|
|
||||||
) {
|
|
||||||
log.Error().Err(err).Msg("[api.ws] readJSON")
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
handler := wsHandlers[msg.Type]
|
|
||||||
if handler != nil {
|
|
||||||
handler(ctx, msg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
137
cmd/api/ws.go
137
cmd/api/ws.go
@@ -1,69 +1,138 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
|
||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Message - struct for data exchange in Web API
|
||||||
|
type Message struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Value interface{} `json:"value,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type WSHandler func(tr *Transport, msg *Message) error
|
||||||
|
|
||||||
|
func HandleWS(msgType string, handler WSHandler) {
|
||||||
|
wsHandlers[msgType] = handler
|
||||||
|
}
|
||||||
|
|
||||||
|
var wsHandlers = make(map[string]WSHandler)
|
||||||
|
|
||||||
func initWS(origin string) {
|
func initWS(origin string) {
|
||||||
wsUp = &websocket.Upgrader{
|
wsUp = &websocket.Upgrader{
|
||||||
ReadBufferSize: 1024,
|
ReadBufferSize: 1024,
|
||||||
WriteBufferSize: 512000,
|
WriteBufferSize: 2028,
|
||||||
}
|
}
|
||||||
|
|
||||||
if origin == "*" {
|
switch origin {
|
||||||
|
case "":
|
||||||
|
// same origin + ignore port
|
||||||
|
wsUp.CheckOrigin = func(r *http.Request) bool {
|
||||||
|
origin := r.Header["Origin"]
|
||||||
|
if len(origin) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
o, err := url.Parse(origin[0])
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if o.Host == r.Host {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
log.Trace().Msgf("[api.ws] origin=%s, host=%s", o.Host, r.Host)
|
||||||
|
// https://github.com/AlexxIT/go2rtc/issues/118
|
||||||
|
if i := strings.IndexByte(o.Host, ':'); i > 0 {
|
||||||
|
return o.Host[:i] == r.Host
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
case "*":
|
||||||
|
// any origin
|
||||||
wsUp.CheckOrigin = func(r *http.Request) bool {
|
wsUp.CheckOrigin = func(r *http.Request) bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func apiWS(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ws, err := wsUp.Upgrade(w, r, nil)
|
||||||
|
if err != nil {
|
||||||
|
origin := r.Header.Get("Origin")
|
||||||
|
log.Error().Err(err).Caller().Msgf("host=%s origin=%s", r.Host, origin)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tr := &Transport{Request: r}
|
||||||
|
tr.OnWrite(func(msg interface{}) {
|
||||||
|
if data, ok := msg.([]byte); ok {
|
||||||
|
_ = ws.WriteMessage(websocket.BinaryMessage, data)
|
||||||
|
} else {
|
||||||
|
_ = ws.WriteJSON(msg)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
for {
|
||||||
|
msg := new(Message)
|
||||||
|
if err = ws.ReadJSON(msg); err != nil {
|
||||||
|
log.Trace().Err(err).Caller().Send()
|
||||||
|
_ = ws.Close()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if handler := wsHandlers[msg.Type]; handler != nil {
|
||||||
|
go func() {
|
||||||
|
if err = handler(tr, msg); err != nil {
|
||||||
|
tr.Write(&Message{Type: "error", Value: msg.Type + ": " + err.Error()})
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tr.Close()
|
||||||
|
}
|
||||||
|
|
||||||
var wsUp *websocket.Upgrader
|
var wsUp *websocket.Upgrader
|
||||||
|
|
||||||
type WSHandler func(ctx *Context, msg *streamer.Message)
|
type Transport struct {
|
||||||
|
|
||||||
type Context struct {
|
|
||||||
Conn *websocket.Conn
|
|
||||||
Request *http.Request
|
Request *http.Request
|
||||||
Consumer interface{} // TODO: rewrite
|
Consumer interface{} // TODO: rewrite
|
||||||
|
|
||||||
onClose []func()
|
mx sync.Mutex
|
||||||
mu sync.Mutex
|
|
||||||
|
onChange func()
|
||||||
|
onWrite func(msg interface{})
|
||||||
|
onClose []func()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ctx *Context) Upgrade(w http.ResponseWriter, r *http.Request) (err error) {
|
func (t *Transport) OnWrite(f func(msg interface{})) {
|
||||||
ctx.Conn, err = wsUp.Upgrade(w, r, nil)
|
t.mx.Lock()
|
||||||
ctx.Request = r
|
if t.onChange != nil {
|
||||||
return
|
t.onChange()
|
||||||
|
}
|
||||||
|
t.onWrite = f
|
||||||
|
t.mx.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ctx *Context) Close() {
|
func (t *Transport) Write(msg interface{}) {
|
||||||
for _, f := range ctx.onClose {
|
t.mx.Lock()
|
||||||
|
t.onWrite(msg)
|
||||||
|
t.mx.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Transport) Close() {
|
||||||
|
for _, f := range t.onClose {
|
||||||
f()
|
f()
|
||||||
}
|
}
|
||||||
_ = ctx.Conn.Close()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ctx *Context) Write(msg interface{}) {
|
func (t *Transport) OnChange(f func()) {
|
||||||
ctx.mu.Lock()
|
t.onChange = f
|
||||||
|
|
||||||
if data, ok := msg.([]byte); ok {
|
|
||||||
_ = ctx.Conn.WriteMessage(websocket.BinaryMessage, data)
|
|
||||||
} else {
|
|
||||||
_ = ctx.Conn.WriteJSON(msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.mu.Unlock()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ctx *Context) Error(err error) {
|
func (t *Transport) OnClose(f func()) {
|
||||||
ctx.Write(&streamer.Message{
|
t.onClose = append(t.onClose, f)
|
||||||
Type: "error", Value: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ctx *Context) OnClose(f func()) {
|
|
||||||
ctx.onClose = append(ctx.onClose, f)
|
|
||||||
}
|
}
|
||||||
|
@@ -10,7 +10,7 @@ import (
|
|||||||
"runtime"
|
"runtime"
|
||||||
)
|
)
|
||||||
|
|
||||||
var Version = "0.1-rc.4"
|
var Version = "0.1-rc.6"
|
||||||
var UserAgent = "go2rtc/" + Version
|
var UserAgent = "go2rtc/" + Version
|
||||||
|
|
||||||
func Init() {
|
func Init() {
|
||||||
|
@@ -1,6 +1,8 @@
|
|||||||
package ffmpeg
|
package ffmpeg
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
"github.com/AlexxIT/go2rtc/cmd/app"
|
"github.com/AlexxIT/go2rtc/cmd/app"
|
||||||
"github.com/AlexxIT/go2rtc/cmd/exec"
|
"github.com/AlexxIT/go2rtc/cmd/exec"
|
||||||
"github.com/AlexxIT/go2rtc/cmd/ffmpeg/device"
|
"github.com/AlexxIT/go2rtc/cmd/ffmpeg/device"
|
||||||
@@ -17,190 +19,226 @@ func Init() {
|
|||||||
Mod map[string]string `yaml:"ffmpeg"`
|
Mod map[string]string `yaml:"ffmpeg"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// defaults
|
cfg.Mod = defaults // will be overriden from yaml
|
||||||
|
|
||||||
cfg.Mod = map[string]string{
|
|
||||||
"bin": "ffmpeg",
|
|
||||||
|
|
||||||
// inputs
|
|
||||||
"file": "-re -stream_loop -1 -i {input}",
|
|
||||||
"http": "-fflags nobuffer -flags low_delay -i {input}",
|
|
||||||
"rtsp": "-fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_transport tcp -i {input}",
|
|
||||||
|
|
||||||
// output
|
|
||||||
"output": "-user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}",
|
|
||||||
|
|
||||||
// `-g 30` - group of picture, GOP, keyframe interval
|
|
||||||
// `-preset superfast` - we can't use ultrafast because it doesn't support `-profile main -level 4.1`
|
|
||||||
// `-tune zerolatency` - for minimal latency
|
|
||||||
// `-profile main -level 4.1` - most used streaming profile
|
|
||||||
// `-pix_fmt yuv420p` - if input pix format 4:2:2
|
|
||||||
"h264": "-c:v libx264 -g:v 30 -preset:v superfast -tune:v zerolatency -profile:v main -level:v 4.1 -pix_fmt:v yuv420p",
|
|
||||||
"h264/ultra": "-c:v libx264 -g:v 30 -preset:v ultrafast -tune:v zerolatency",
|
|
||||||
"h264/high": "-c:v libx264 -g:v 30 -preset:v superfast -tune:v zerolatency",
|
|
||||||
"h265": "-c:v libx265 -g:v 30 -preset:v superfast -tune:v zerolatency -profile:v main -level:v 5.1 -pix_fmt:v yuv420p",
|
|
||||||
"mjpeg": "-c:v mjpeg -force_duplicated_matrix:v 1 -huffman:v 0 -pix_fmt:v yuvj420p",
|
|
||||||
"opus": "-c:a libopus -ar:a 48000 -ac:a 2",
|
|
||||||
"pcmu": "-c:a pcm_mulaw -ar:a 8000 -ac:a 1",
|
|
||||||
"pcmu/16000": "-c:a pcm_mulaw -ar:a 16000 -ac:a 1",
|
|
||||||
"pcmu/48000": "-c:a pcm_mulaw -ar:a 48000 -ac:a 1",
|
|
||||||
"pcma": "-c:a pcm_alaw -ar:a 8000 -ac:a 1",
|
|
||||||
"pcma/16000": "-c:a pcm_alaw -ar:a 16000 -ac:a 1",
|
|
||||||
"pcma/48000": "-c:a pcm_alaw -ar:a 48000 -ac:a 1",
|
|
||||||
"aac": "-c:a aac", // keep sample rate and channels
|
|
||||||
"aac/16000": "-c:a aac -ar:a 16000 -ac:a 1",
|
|
||||||
}
|
|
||||||
|
|
||||||
app.LoadConfig(&cfg)
|
app.LoadConfig(&cfg)
|
||||||
|
|
||||||
tpl := cfg.Mod
|
|
||||||
|
|
||||||
cmd := "exec:" + tpl["bin"] + " -hide_banner "
|
|
||||||
|
|
||||||
if app.GetLogger("exec").GetLevel() >= 0 {
|
if app.GetLogger("exec").GetLevel() >= 0 {
|
||||||
cmd += "-v error "
|
defaults["global"] += " -v error"
|
||||||
}
|
}
|
||||||
|
|
||||||
streams.HandleFunc("ffmpeg", func(s string) (streamer.Producer, error) {
|
streams.HandleFunc("ffmpeg", func(url string) (streamer.Producer, error) {
|
||||||
s = s[7:] // remove `ffmpeg:`
|
args := parseArgs(url[7:]) // remove `ffmpeg:`
|
||||||
|
if args == nil {
|
||||||
var query url.Values
|
return nil, errors.New("can't generate ffmpeg command")
|
||||||
var queryVideo, queryAudio bool
|
|
||||||
|
|
||||||
if i := strings.IndexByte(s, '#'); i > 0 {
|
|
||||||
query = parseQuery(s[i+1:])
|
|
||||||
queryVideo = query["video"] != nil
|
|
||||||
queryAudio = query["audio"] != nil
|
|
||||||
s = s[:i]
|
|
||||||
} else {
|
|
||||||
// by default query both video and audio
|
|
||||||
queryVideo = true
|
|
||||||
queryAudio = true
|
|
||||||
}
|
}
|
||||||
|
return exec.Handle("exec:" + args.String())
|
||||||
var input string
|
|
||||||
if i := strings.Index(s, "://"); i > 0 {
|
|
||||||
switch s[:i] {
|
|
||||||
case "http", "https", "rtmp":
|
|
||||||
input = strings.Replace(tpl["http"], "{input}", s, 1)
|
|
||||||
case "rtsp", "rtsps":
|
|
||||||
// https://ffmpeg.org/ffmpeg-protocols.html#rtsp
|
|
||||||
// skip unnecessary input tracks
|
|
||||||
switch {
|
|
||||||
case queryVideo && queryAudio:
|
|
||||||
input = "-allowed_media_types video+audio "
|
|
||||||
case queryVideo:
|
|
||||||
input = "-allowed_media_types video "
|
|
||||||
case queryAudio:
|
|
||||||
input = "-allowed_media_types audio "
|
|
||||||
}
|
|
||||||
|
|
||||||
input += strings.Replace(tpl["rtsp"], "{input}", s, 1)
|
|
||||||
default:
|
|
||||||
input = "-i " + s
|
|
||||||
}
|
|
||||||
} else if streams.Get(s) != nil {
|
|
||||||
s = "rtsp://localhost:" + rtsp.Port + "/" + s
|
|
||||||
switch {
|
|
||||||
case queryVideo && !queryAudio:
|
|
||||||
s += "?video"
|
|
||||||
case queryAudio && !queryVideo:
|
|
||||||
s += "?audio"
|
|
||||||
}
|
|
||||||
input = strings.Replace(tpl["rtsp"], "{input}", s, 1)
|
|
||||||
} else if strings.HasPrefix(s, "device?") {
|
|
||||||
var err error
|
|
||||||
input, err = device.GetInput(s)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
input = strings.Replace(tpl["file"], "{input}", s, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, ok := query["async"]; ok {
|
|
||||||
input = "-use_wallclock_as_timestamps 1 -async 1 " + input
|
|
||||||
}
|
|
||||||
|
|
||||||
s = cmd + input
|
|
||||||
|
|
||||||
if query != nil {
|
|
||||||
for _, raw := range query["raw"] {
|
|
||||||
s += " " + raw
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, rotate := range query["rotate"] {
|
|
||||||
switch rotate {
|
|
||||||
case "90":
|
|
||||||
s += " -vf transpose=1" // 90 degrees clockwise
|
|
||||||
case "180":
|
|
||||||
s += " -vf transpose=1,transpose=1"
|
|
||||||
case "-90", "270":
|
|
||||||
s += " -vf transpose=2" // 90 degrees counterclockwise
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
switch len(query["video"]) {
|
|
||||||
case 0:
|
|
||||||
s += " -vn"
|
|
||||||
case 1:
|
|
||||||
if len(query["audio"]) > 1 {
|
|
||||||
s += " -map 0:v:0?"
|
|
||||||
}
|
|
||||||
for _, video := range query["video"] {
|
|
||||||
if video == "copy" {
|
|
||||||
s += " -c:v copy"
|
|
||||||
} else {
|
|
||||||
s += " " + tpl[video]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
for i, video := range query["video"] {
|
|
||||||
if video == "copy" {
|
|
||||||
s += " -map 0:v:0? -c:v:" + strconv.Itoa(i) + " copy"
|
|
||||||
} else {
|
|
||||||
s += " -map 0:v:0? " + strings.ReplaceAll(tpl[video], ":v ", ":v:"+strconv.Itoa(i)+" ")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
switch len(query["audio"]) {
|
|
||||||
case 0:
|
|
||||||
s += " -an"
|
|
||||||
case 1:
|
|
||||||
if len(query["video"]) > 1 {
|
|
||||||
s += " -map 0:a:0?"
|
|
||||||
}
|
|
||||||
for _, audio := range query["audio"] {
|
|
||||||
if audio == "copy" {
|
|
||||||
s += " -c:a copy"
|
|
||||||
} else {
|
|
||||||
s += " " + tpl[audio]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
for i, audio := range query["audio"] {
|
|
||||||
if audio == "copy" {
|
|
||||||
s += " -map 0:a:0? -c:a:" + strconv.Itoa(i) + " copy"
|
|
||||||
} else {
|
|
||||||
s += " -map 0:a:0? " + strings.ReplaceAll(tpl[audio], ":a ", ":a:"+strconv.Itoa(i)+" ")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
s += " -c copy"
|
|
||||||
}
|
|
||||||
|
|
||||||
s += " " + tpl["output"]
|
|
||||||
|
|
||||||
return exec.Handle(s)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
device.Bin = cfg.Mod["bin"]
|
device.Bin = defaults["bin"]
|
||||||
device.Init()
|
device.Init()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var defaults = map[string]string{
|
||||||
|
"bin": "ffmpeg",
|
||||||
|
"global": "-hide_banner",
|
||||||
|
|
||||||
|
// inputs
|
||||||
|
"file": "-re -stream_loop -1 -i {input}",
|
||||||
|
"http": "-fflags nobuffer -flags low_delay -i {input}",
|
||||||
|
"rtsp": "-fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_transport tcp -i {input}",
|
||||||
|
|
||||||
|
// output
|
||||||
|
"output": "-user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}",
|
||||||
|
|
||||||
|
// `-preset superfast` - we can't use ultrafast because it doesn't support `-profile main -level 4.1`
|
||||||
|
// `-tune zerolatency` - for minimal latency
|
||||||
|
// `-profile high -level 4.1` - most used streaming profile
|
||||||
|
"h264": "-c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency",
|
||||||
|
"h265": "-c:v libx265 -g 50 -profile:v high -level:v 5.1 -preset:v superfast -tune:v zerolatency",
|
||||||
|
"mjpeg": "-c:v mjpeg -force_duplicated_matrix:v 1 -huffman:v 0 -pix_fmt:v yuvj420p",
|
||||||
|
|
||||||
|
"opus": "-c:a libopus -ar:a 48000 -ac:a 2",
|
||||||
|
"pcmu": "-c:a pcm_mulaw -ar:a 8000 -ac:a 1",
|
||||||
|
"pcmu/16000": "-c:a pcm_mulaw -ar:a 16000 -ac:a 1",
|
||||||
|
"pcmu/48000": "-c:a pcm_mulaw -ar:a 48000 -ac:a 1",
|
||||||
|
"pcma": "-c:a pcm_alaw -ar:a 8000 -ac:a 1",
|
||||||
|
"pcma/16000": "-c:a pcm_alaw -ar:a 16000 -ac:a 1",
|
||||||
|
"pcma/48000": "-c:a pcm_alaw -ar:a 48000 -ac:a 1",
|
||||||
|
"aac": "-c:a aac", // keep sample rate and channels
|
||||||
|
"aac/16000": "-c:a aac -ar:a 16000 -ac:a 1",
|
||||||
|
|
||||||
|
// hardware Intel and AMD on Linux
|
||||||
|
// better not to set `-async_depth:v 1` like for QSV, because framedrops
|
||||||
|
// `-bf 0` - disable B-frames is very important
|
||||||
|
"h264/vaapi": "-c:v h264_vaapi -g 50 -bf 0 -profile:v high -level:v 4.1 -sei:v 0",
|
||||||
|
"h265/vaapi": "-c:v hevc_vaapi -g 50 -bf 0 -profile:v high -level:v 5.1 -sei:v 0",
|
||||||
|
"mjpeg/vaapi": "-c:v mjpeg_vaapi",
|
||||||
|
|
||||||
|
// hardware Raspberry
|
||||||
|
"h264/v4l2m2m": "-c:v h264_v4l2m2m -g 50 -bf 0",
|
||||||
|
"h265/v4l2m2m": "-c:v hevc_v4l2m2m -g 50 -bf 0",
|
||||||
|
|
||||||
|
// hardware NVidia on Linux and Windows
|
||||||
|
// preset=p2 - faster, tune=ll - low latency
|
||||||
|
"h264/cuda": "-c:v h264_nvenc -g 50 -profile:v high -level:v auto -preset:v p2 -tune:v ll",
|
||||||
|
"h265/cuda": "-c:v hevc_nvenc -g 50 -profile:v high -level:v auto",
|
||||||
|
|
||||||
|
// hardware Intel on Windows
|
||||||
|
"h264/dxva2": "-c:v h264_qsv -g 50 -bf 0 -profile:v high -level:v 4.1 -async_depth:v 1",
|
||||||
|
"h265/dxva2": "-c:v hevc_qsv -g 50 -bf 0 -profile:v high -level:v 5.1 -async_depth:v 1",
|
||||||
|
"mjpeg/dxva2": "-c:v mjpeg_qsv -profile:v high -level:v 5.1",
|
||||||
|
|
||||||
|
// hardware macOS
|
||||||
|
"h264/videotoolbox": "-c:v h264_videotoolbox -g 50 -bf 0 -profile:v high -level:v 4.1",
|
||||||
|
"h265/videotoolbox": "-c:v hevc_videotoolbox -g 50 -bf 0 -profile:v high -level:v 5.1",
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseArgs(s string) *Args {
|
||||||
|
// init FFmpeg arguments
|
||||||
|
args := &Args{
|
||||||
|
bin: defaults["bin"],
|
||||||
|
global: defaults["global"],
|
||||||
|
output: defaults["output"],
|
||||||
|
}
|
||||||
|
|
||||||
|
var query url.Values
|
||||||
|
if i := strings.IndexByte(s, '#'); i > 0 {
|
||||||
|
query = parseQuery(s[i+1:])
|
||||||
|
args.video = len(query["video"])
|
||||||
|
args.audio = len(query["audio"])
|
||||||
|
s = s[:i]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse input:
|
||||||
|
// 1. Input as xxxx:// link (http or rtsp or any other)
|
||||||
|
// 2. Input as stream name
|
||||||
|
// 3. Input as FFmpeg device (local USB camera)
|
||||||
|
if i := strings.Index(s, "://"); i > 0 {
|
||||||
|
switch s[:i] {
|
||||||
|
case "http", "https", "rtmp":
|
||||||
|
args.input = strings.Replace(defaults["http"], "{input}", s, 1)
|
||||||
|
case "rtsp", "rtsps":
|
||||||
|
// https://ffmpeg.org/ffmpeg-protocols.html#rtsp
|
||||||
|
// skip unnecessary input tracks
|
||||||
|
switch {
|
||||||
|
case (args.video > 0 && args.audio > 0) || (args.video == 0 && args.audio == 0):
|
||||||
|
args.input = "-allowed_media_types video+audio "
|
||||||
|
case args.video > 0:
|
||||||
|
args.input = "-allowed_media_types video "
|
||||||
|
case args.audio > 0:
|
||||||
|
args.input = "-allowed_media_types audio "
|
||||||
|
}
|
||||||
|
|
||||||
|
args.input += strings.Replace(defaults["rtsp"], "{input}", s, 1)
|
||||||
|
default:
|
||||||
|
args.input = "-i " + s
|
||||||
|
}
|
||||||
|
} else if streams.Get(s) != nil {
|
||||||
|
s = "rtsp://localhost:" + rtsp.Port + "/" + s
|
||||||
|
switch {
|
||||||
|
case args.video > 0 && args.audio == 0:
|
||||||
|
s += "?video"
|
||||||
|
case args.audio > 0 && args.video == 0:
|
||||||
|
s += "?audio"
|
||||||
|
}
|
||||||
|
args.input = strings.Replace(defaults["rtsp"], "{input}", s, 1)
|
||||||
|
} else if strings.HasPrefix(s, "device?") {
|
||||||
|
var err error
|
||||||
|
args.input, err = device.GetInput(s)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
args.input = strings.Replace(defaults["file"], "{input}", s, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if query["async"] != nil {
|
||||||
|
args.input = "-use_wallclock_as_timestamps 1 -async 1 " + args.input
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse query params:
|
||||||
|
// 1. `width`/`height` params
|
||||||
|
// 2. `rotate` param
|
||||||
|
// 3. `video` params (support multiple)
|
||||||
|
// 4. `audio` params (support multiple)
|
||||||
|
// 5. `hardware` param
|
||||||
|
if query != nil {
|
||||||
|
// 1. Process raw params for FFmpeg
|
||||||
|
for _, raw := range query["raw"] {
|
||||||
|
args.AddCodec(raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Process video filters (resize and rotation)
|
||||||
|
if query["width"] != nil || query["height"] != nil {
|
||||||
|
filter := "scale="
|
||||||
|
if query["width"] != nil {
|
||||||
|
filter += query["width"][0]
|
||||||
|
} else {
|
||||||
|
filter += "-1"
|
||||||
|
}
|
||||||
|
filter += ":"
|
||||||
|
if query["height"] != nil {
|
||||||
|
filter += query["height"][0]
|
||||||
|
} else {
|
||||||
|
filter += "-1"
|
||||||
|
}
|
||||||
|
args.AddFilter(filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
if query["rotate"] != nil {
|
||||||
|
var filter string
|
||||||
|
switch query["rotate"][0] {
|
||||||
|
case "90":
|
||||||
|
filter = "transpose=1" // 90 degrees clockwise
|
||||||
|
case "180":
|
||||||
|
filter = "transpose=1,transpose=1"
|
||||||
|
case "-90", "270":
|
||||||
|
filter = "transpose=2" // 90 degrees counterclockwise
|
||||||
|
}
|
||||||
|
if filter != "" {
|
||||||
|
args.AddFilter(filter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Process video codecs
|
||||||
|
if args.video > 0 {
|
||||||
|
for _, video := range query["video"] {
|
||||||
|
if video != "copy" {
|
||||||
|
args.AddCodec(defaults[video])
|
||||||
|
} else {
|
||||||
|
args.AddCodec("-c:v copy")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
args.AddCodec("-vn")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Process audio codecs
|
||||||
|
if args.audio > 0 {
|
||||||
|
for _, audio := range query["audio"] {
|
||||||
|
if audio != "copy" {
|
||||||
|
args.AddCodec(defaults[audio])
|
||||||
|
} else {
|
||||||
|
args.AddCodec("-c:a copy")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
args.AddCodec("-an")
|
||||||
|
}
|
||||||
|
|
||||||
|
if query["hardware"] != nil {
|
||||||
|
MakeHardware(args, query["hardware"][0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if args.codecs == nil {
|
||||||
|
args.AddCodec("-c copy")
|
||||||
|
}
|
||||||
|
|
||||||
|
return args
|
||||||
|
}
|
||||||
|
|
||||||
func parseQuery(s string) map[string][]string {
|
func parseQuery(s string) map[string][]string {
|
||||||
query := map[string][]string{}
|
query := map[string][]string{}
|
||||||
for _, key := range strings.Split(s, "#") {
|
for _, key := range strings.Split(s, "#") {
|
||||||
@@ -213,3 +251,76 @@ func parseQuery(s string) map[string][]string {
|
|||||||
}
|
}
|
||||||
return query
|
return query
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Args struct {
|
||||||
|
bin string // ffmpeg
|
||||||
|
global string // -hide_banner -v error
|
||||||
|
input string // -re -stream_loop -1 -i /media/bunny.mp4
|
||||||
|
codecs []string // -c:v libx264 -g:v 30 -preset:v ultrafast -tune:v zerolatency
|
||||||
|
filters []string // scale=1920:1080
|
||||||
|
output string // -f rtsp {output}
|
||||||
|
|
||||||
|
video, audio int // count of video and audio params
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Args) AddCodec(codec string) {
|
||||||
|
a.codecs = append(a.codecs, codec)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Args) AddFilter(filter string) {
|
||||||
|
a.filters = append(a.filters, filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Args) InsertFilter(filter string) {
|
||||||
|
a.filters = append([]string{filter}, a.filters...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Args) String() string {
|
||||||
|
b := bytes.NewBuffer(make([]byte, 0, 512))
|
||||||
|
|
||||||
|
b.WriteString(a.bin)
|
||||||
|
|
||||||
|
if a.global != "" {
|
||||||
|
b.WriteByte(' ')
|
||||||
|
b.WriteString(a.global)
|
||||||
|
}
|
||||||
|
|
||||||
|
b.WriteByte(' ')
|
||||||
|
b.WriteString(a.input)
|
||||||
|
|
||||||
|
multimode := a.video > 1 || a.audio > 1
|
||||||
|
var iv, ia int
|
||||||
|
|
||||||
|
for _, codec := range a.codecs {
|
||||||
|
// support multiple video and/or audio codecs
|
||||||
|
if multimode && len(codec) >= 5 {
|
||||||
|
switch codec[:5] {
|
||||||
|
case "-c:v ":
|
||||||
|
codec = "-map 0:v:0? " + strings.ReplaceAll(codec, ":v ", ":v:"+strconv.Itoa(iv)+" ")
|
||||||
|
iv++
|
||||||
|
case "-c:a ":
|
||||||
|
codec = "-map 0:a:0? " + strings.ReplaceAll(codec, ":a ", ":a:"+strconv.Itoa(ia)+" ")
|
||||||
|
ia++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
b.WriteByte(' ')
|
||||||
|
b.WriteString(codec)
|
||||||
|
}
|
||||||
|
|
||||||
|
if a.filters != nil {
|
||||||
|
for i, filter := range a.filters {
|
||||||
|
if i == 0 {
|
||||||
|
b.WriteString(" -vf ")
|
||||||
|
} else {
|
||||||
|
b.WriteByte(',')
|
||||||
|
}
|
||||||
|
b.WriteString(filter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
b.WriteByte(' ')
|
||||||
|
b.WriteString(a.output)
|
||||||
|
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
112
cmd/ffmpeg/hardware.go
Normal file
112
cmd/ffmpeg/hardware.go
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
package ffmpeg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
EngineSoftware = "software"
|
||||||
|
EngineVAAPI = "vaapi" // Intel iGPU and AMD GPU
|
||||||
|
EngineV4L2M2M = "v4l2m2m" // Raspberry Pi 3 and 4
|
||||||
|
EngineCUDA = "cuda" // NVidia on Windows and Linux
|
||||||
|
EngineDXVA2 = "dxva2" // Intel on Windows
|
||||||
|
EngineVideoToolbox = "videotoolbox" // macOS
|
||||||
|
)
|
||||||
|
|
||||||
|
var cache = map[string]string{}
|
||||||
|
|
||||||
|
// MakeHardware converts software FFmpeg args to hardware args
|
||||||
|
// empty engine for autoselect
|
||||||
|
func MakeHardware(args *Args, engine string) {
|
||||||
|
for i, codec := range args.codecs {
|
||||||
|
if len(codec) < 12 {
|
||||||
|
continue // skip short line (-c:v libx264...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// get current codec name
|
||||||
|
name := cut(codec, ' ', 1)
|
||||||
|
switch name {
|
||||||
|
case "libx264":
|
||||||
|
name = "h264"
|
||||||
|
case "libx265":
|
||||||
|
name = "h265"
|
||||||
|
case "mjpeg":
|
||||||
|
default:
|
||||||
|
continue // skip unsupported codec
|
||||||
|
}
|
||||||
|
|
||||||
|
// temporary disable probe for H265 and MJPEG
|
||||||
|
if engine == "" && name == "h264" {
|
||||||
|
if engine = cache[name]; engine == "" {
|
||||||
|
engine = ProbeHardware(name)
|
||||||
|
cache[name] = engine
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch engine {
|
||||||
|
case EngineVAAPI:
|
||||||
|
args.input = "-hwaccel vaapi -hwaccel_output_format vaapi " + args.input
|
||||||
|
args.codecs[i] = defaults[name+"/"+engine]
|
||||||
|
|
||||||
|
for i, filter := range args.filters {
|
||||||
|
if strings.HasPrefix(filter, "scale=") {
|
||||||
|
args.filters[i] = "scale_vaapi=" + filter[6:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// fix if input doesn't support hwaccel, do nothing when support
|
||||||
|
args.InsertFilter("format=vaapi|nv12,hwupload")
|
||||||
|
|
||||||
|
case EngineCUDA:
|
||||||
|
args.input = "-hwaccel cuda -hwaccel_output_format cuda -extra_hw_frames 2 " + args.input
|
||||||
|
args.codecs[i] = defaults[name+"/"+engine]
|
||||||
|
|
||||||
|
for i, filter := range args.filters {
|
||||||
|
if strings.HasPrefix(filter, "scale=") {
|
||||||
|
args.filters[i] = "scale_cuda=" + filter[6:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case EngineDXVA2:
|
||||||
|
args.input = "-hwaccel dxva2 -hwaccel_output_format dxva2_vld " + args.input
|
||||||
|
args.codecs[i] = defaults[name+"/"+engine]
|
||||||
|
|
||||||
|
for i, filter := range args.filters {
|
||||||
|
if strings.HasPrefix(filter, "scale=") {
|
||||||
|
args.filters[i] = "scale_qsv=" + filter[6:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
args.InsertFilter("hwmap=derive_device=qsv,format=qsv")
|
||||||
|
|
||||||
|
case EngineVideoToolbox:
|
||||||
|
args.input = "-hwaccel videotoolbox -hwaccel_output_format videotoolbox_vld " + args.input
|
||||||
|
args.codecs[i] = defaults[name+"/"+engine]
|
||||||
|
|
||||||
|
case EngineV4L2M2M:
|
||||||
|
args.codecs[i] = defaults[name+"/"+engine]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func run(arg ...string) bool {
|
||||||
|
err := exec.Command(defaults["bin"], arg...).Run()
|
||||||
|
log.Printf("%v %v", arg, err)
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func cut(s string, sep byte, pos int) string {
|
||||||
|
for n := 0; n < pos; n++ {
|
||||||
|
if i := strings.IndexByte(s, sep); i > 0 {
|
||||||
|
s = s[i+1:]
|
||||||
|
} else {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if i := strings.IndexByte(s, sep); i > 0 {
|
||||||
|
return s[:i]
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
21
cmd/ffmpeg/hardware_darwin.go
Normal file
21
cmd/ffmpeg/hardware_darwin.go
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
package ffmpeg
|
||||||
|
|
||||||
|
func ProbeHardware(name string) string {
|
||||||
|
switch name {
|
||||||
|
case "h264":
|
||||||
|
if run(
|
||||||
|
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
|
||||||
|
"-c", "h264_videotoolbox", "-f", "null", "-") {
|
||||||
|
return EngineVideoToolbox
|
||||||
|
}
|
||||||
|
|
||||||
|
case "h265":
|
||||||
|
if run(
|
||||||
|
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
|
||||||
|
"-c", "hevc_videotoolbox", "-f", "null", "-") {
|
||||||
|
return EngineVideoToolbox
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return EngineSoftware
|
||||||
|
}
|
67
cmd/ffmpeg/hardware_linux.go
Normal file
67
cmd/ffmpeg/hardware_linux.go
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
package ffmpeg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ProbeHardware(name string) string {
|
||||||
|
if runtime.GOARCH == "arm64" || runtime.GOARCH == "arm" {
|
||||||
|
switch name {
|
||||||
|
case "h264":
|
||||||
|
if run(
|
||||||
|
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
|
||||||
|
"-c", "h264_v4l2m2m", "-f", "null", "-") {
|
||||||
|
return EngineV4L2M2M
|
||||||
|
}
|
||||||
|
|
||||||
|
case "h265":
|
||||||
|
if run(
|
||||||
|
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
|
||||||
|
"-c", "hevc_v4l2m2m", "-f", "null", "-") {
|
||||||
|
return EngineV4L2M2M
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return EngineSoftware
|
||||||
|
}
|
||||||
|
|
||||||
|
switch name {
|
||||||
|
case "h264":
|
||||||
|
if run("-init_hw_device", "cuda",
|
||||||
|
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
|
||||||
|
"-c", "h264_nvenc", "-f", "null", "-") {
|
||||||
|
return EngineCUDA
|
||||||
|
}
|
||||||
|
|
||||||
|
if run("-init_hw_device", "vaapi",
|
||||||
|
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
|
||||||
|
"-vf", "format=nv12,hwupload",
|
||||||
|
"-c", "h264_vaapi", "-f", "null", "-") {
|
||||||
|
return EngineVAAPI
|
||||||
|
}
|
||||||
|
|
||||||
|
case "h265":
|
||||||
|
if run("-init_hw_device", "cuda",
|
||||||
|
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
|
||||||
|
"-c", "hevc_nvenc", "-f", "null", "-") {
|
||||||
|
return EngineCUDA
|
||||||
|
}
|
||||||
|
|
||||||
|
if run("-init_hw_device", "vaapi",
|
||||||
|
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
|
||||||
|
"-vf", "format=nv12,hwupload",
|
||||||
|
"-c", "hevc_vaapi", "-f", "null", "-") {
|
||||||
|
return EngineVAAPI
|
||||||
|
}
|
||||||
|
|
||||||
|
case "mjpeg":
|
||||||
|
if run("-init_hw_device", "vaapi",
|
||||||
|
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
|
||||||
|
"-vf", "format=nv12,hwupload",
|
||||||
|
"-c", "mjpeg_vaapi", "-f", "null", "-") {
|
||||||
|
return EngineVAAPI
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return EngineSoftware
|
||||||
|
}
|
40
cmd/ffmpeg/hardware_windows.go
Normal file
40
cmd/ffmpeg/hardware_windows.go
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
package ffmpeg
|
||||||
|
|
||||||
|
func ProbeHardware(name string) string {
|
||||||
|
switch name {
|
||||||
|
case "h264":
|
||||||
|
if run("-init_hw_device", "cuda",
|
||||||
|
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
|
||||||
|
"-c", "h264_nvenc", "-f", "null", "-") {
|
||||||
|
return EngineCUDA
|
||||||
|
}
|
||||||
|
|
||||||
|
if run("-init_hw_device", "dxva2",
|
||||||
|
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
|
||||||
|
"-c", "h264_qsv", "-f", "null", "-") {
|
||||||
|
return EngineDXVA2
|
||||||
|
}
|
||||||
|
|
||||||
|
case "h265":
|
||||||
|
if run("-init_hw_device", "cuda",
|
||||||
|
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
|
||||||
|
"-c", "hevc_nvenc", "-f", "null", "-") {
|
||||||
|
return EngineCUDA
|
||||||
|
}
|
||||||
|
|
||||||
|
if run("-init_hw_device", "dxva2",
|
||||||
|
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
|
||||||
|
"-c", "hevc_qsv", "-f", "null", "-") {
|
||||||
|
return EngineDXVA2
|
||||||
|
}
|
||||||
|
|
||||||
|
case "mjpeg":
|
||||||
|
if run("-init_hw_device", "dxva2",
|
||||||
|
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
|
||||||
|
"-c", "mjpeg_qsv", "-f", "null", "-") {
|
||||||
|
return EngineDXVA2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return EngineSoftware
|
||||||
|
}
|
56
cmd/http/http.go
Normal file
56
cmd/http/http.go
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/AlexxIT/go2rtc/cmd/streams"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/mjpeg"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/rtmp"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Init() {
|
||||||
|
streams.HandleFunc("http", handle)
|
||||||
|
streams.HandleFunc("https", handle)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handle(url string) (streamer.Producer, error) {
|
||||||
|
// first we get the Content-Type to define supported producer
|
||||||
|
req, err := http.NewRequest("GET", url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := tcp.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if res.StatusCode != http.StatusOK {
|
||||||
|
return nil, errors.New(res.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
ct := res.Header.Get("Content-Type")
|
||||||
|
if i := strings.IndexByte(ct, ';'); i > 0 {
|
||||||
|
ct = ct[:i]
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ct {
|
||||||
|
case "image/jpeg", "multipart/x-mixed-replace":
|
||||||
|
return mjpeg.NewClient(res), nil
|
||||||
|
case "video/x-flv":
|
||||||
|
var conn *rtmp.Client
|
||||||
|
if conn, err = rtmp.Accept(res); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err = conn.Describe(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return conn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("unsupported Content-Type: %s", ct)
|
||||||
|
}
|
@@ -1,6 +1,7 @@
|
|||||||
package mjpeg
|
package mjpeg
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"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/mjpeg"
|
"github.com/AlexxIT/go2rtc/pkg/mjpeg"
|
||||||
@@ -12,12 +13,15 @@ import (
|
|||||||
func Init() {
|
func Init() {
|
||||||
api.HandleFunc("api/frame.jpeg", handlerKeyframe)
|
api.HandleFunc("api/frame.jpeg", handlerKeyframe)
|
||||||
api.HandleFunc("api/stream.mjpeg", handlerStream)
|
api.HandleFunc("api/stream.mjpeg", handlerStream)
|
||||||
|
|
||||||
|
api.HandleWS("mjpeg", handlerWS)
|
||||||
}
|
}
|
||||||
|
|
||||||
func handlerKeyframe(w http.ResponseWriter, r *http.Request) {
|
func handlerKeyframe(w http.ResponseWriter, r *http.Request) {
|
||||||
src := r.URL.Query().Get("src")
|
src := r.URL.Query().Get("src")
|
||||||
stream := streams.GetOrNew(src)
|
stream := streams.GetOrNew(src)
|
||||||
if stream == nil {
|
if stream == nil {
|
||||||
|
http.Error(w, api.StreamNotFound, http.StatusNotFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,8 +44,12 @@ func handlerKeyframe(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
stream.RemoveConsumer(cons)
|
stream.RemoveConsumer(cons)
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "image/jpeg")
|
h := w.Header()
|
||||||
w.Header().Set("Content-Length", strconv.Itoa(len(data)))
|
h.Set("Content-Type", "image/jpeg")
|
||||||
|
h.Set("Content-Length", strconv.Itoa(len(data)))
|
||||||
|
h.Set("Cache-Control", "no-cache")
|
||||||
|
h.Set("Connection", "close")
|
||||||
|
h.Set("Pragma", "no-cache")
|
||||||
|
|
||||||
if _, err := w.Write(data); err != nil {
|
if _, err := w.Write(data); err != nil {
|
||||||
log.Error().Err(err).Caller().Send()
|
log.Error().Err(err).Caller().Send()
|
||||||
@@ -54,23 +62,25 @@ func handlerStream(w http.ResponseWriter, r *http.Request) {
|
|||||||
src := r.URL.Query().Get("src")
|
src := r.URL.Query().Get("src")
|
||||||
stream := streams.GetOrNew(src)
|
stream := streams.GetOrNew(src)
|
||||||
if stream == nil {
|
if stream == nil {
|
||||||
|
http.Error(w, api.StreamNotFound, http.StatusNotFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
exit := make(chan struct{})
|
flusher := w.(http.Flusher)
|
||||||
|
|
||||||
cons := &mjpeg.Consumer{}
|
cons := &mjpeg.Consumer{}
|
||||||
cons.Listen(func(msg interface{}) {
|
cons.Listen(func(msg interface{}) {
|
||||||
switch msg := msg.(type) {
|
switch msg := msg.(type) {
|
||||||
case []byte:
|
case []byte:
|
||||||
data := []byte(header + strconv.Itoa(len(msg)))
|
data := []byte(header + strconv.Itoa(len(msg)))
|
||||||
data = append(data, 0x0D, 0x0A, 0x0D, 0x0A)
|
data = append(data, '\r', '\n', '\r', '\n')
|
||||||
data = append(data, msg...)
|
data = append(data, msg...)
|
||||||
data = append(data, 0x0D, 0x0A)
|
data = append(data, '\r', '\n')
|
||||||
|
|
||||||
if _, err := w.Write(data); err != nil {
|
// Chrome bug: mjpeg image always shows the second to last image
|
||||||
exit <- struct{}{}
|
// https://bugs.chromium.org/p/chromium/issues/detail?id=527446
|
||||||
}
|
_, _ = w.Write(data)
|
||||||
|
flusher.Flush()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -79,11 +89,43 @@ func handlerStream(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", `multipart/x-mixed-replace; boundary=frame`)
|
h := w.Header()
|
||||||
|
h.Set("Content-Type", "multipart/x-mixed-replace; boundary=frame")
|
||||||
|
h.Set("Cache-Control", "no-cache")
|
||||||
|
h.Set("Connection", "close")
|
||||||
|
h.Set("Pragma", "no-cache")
|
||||||
|
|
||||||
<-exit
|
<-r.Context().Done()
|
||||||
|
|
||||||
stream.RemoveConsumer(cons)
|
stream.RemoveConsumer(cons)
|
||||||
|
|
||||||
//log.Trace().Msg("[api.mjpeg] close")
|
//log.Trace().Msg("[api.mjpeg] close")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func handlerWS(tr *api.Transport, _ *api.Message) error {
|
||||||
|
src := tr.Request.URL.Query().Get("src")
|
||||||
|
stream := streams.GetOrNew(src)
|
||||||
|
if stream == nil {
|
||||||
|
return errors.New(api.StreamNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
cons := &mjpeg.Consumer{}
|
||||||
|
cons.Listen(func(msg interface{}) {
|
||||||
|
if data, ok := msg.([]byte); ok {
|
||||||
|
tr.Write(data)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := stream.AddConsumer(cons); err != nil {
|
||||||
|
log.Error().Err(err).Caller().Send()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
tr.Write(&api.Message{Type: "mjpeg"})
|
||||||
|
|
||||||
|
tr.OnClose(func() {
|
||||||
|
stream.RemoveConsumer(cons)
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
@@ -15,7 +15,8 @@ import (
|
|||||||
func Init() {
|
func Init() {
|
||||||
log = app.GetLogger("mp4")
|
log = app.GetLogger("mp4")
|
||||||
|
|
||||||
api.HandleWS(MsgTypeMSE, handlerWS)
|
api.HandleWS("mse", handlerWSMSE)
|
||||||
|
api.HandleWS("mp4", handlerWSMP4)
|
||||||
|
|
||||||
api.HandleFunc("api/frame.mp4", handlerKeyframe)
|
api.HandleFunc("api/frame.mp4", handlerKeyframe)
|
||||||
api.HandleFunc("api/stream.mp4", handlerMP4)
|
api.HandleFunc("api/stream.mp4", handlerMP4)
|
||||||
@@ -31,12 +32,13 @@ func handlerKeyframe(w http.ResponseWriter, r *http.Request) {
|
|||||||
src := r.URL.Query().Get("src")
|
src := r.URL.Query().Get("src")
|
||||||
stream := streams.GetOrNew(src)
|
stream := streams.GetOrNew(src)
|
||||||
if stream == nil {
|
if stream == nil {
|
||||||
|
http.Error(w, api.StreamNotFound, http.StatusNotFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
exit := make(chan []byte)
|
exit := make(chan []byte)
|
||||||
|
|
||||||
cons := &mp4.Keyframe{}
|
cons := &mp4.Segment{OnlyKeyframe: true}
|
||||||
cons.Listen(func(msg interface{}) {
|
cons.Listen(func(msg interface{}) {
|
||||||
if data, ok := msg.([]byte); ok && exit != nil {
|
if data, ok := msg.([]byte); ok && exit != nil {
|
||||||
exit <- data
|
exit <- data
|
||||||
@@ -63,7 +65,7 @@ func handlerKeyframe(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func handlerMP4(w http.ResponseWriter, r *http.Request) {
|
func handlerMP4(w http.ResponseWriter, r *http.Request) {
|
||||||
log.Trace().Msgf("[api.mp4] %s %+v", r.Method, r.Header)
|
log.Trace().Msgf("[mp4] %s %+v", r.Method, r.Header)
|
||||||
|
|
||||||
if isChromeFirst(w, r) || isSafari(w, r) {
|
if isChromeFirst(w, r) || isSafari(w, r) {
|
||||||
return
|
return
|
||||||
@@ -72,6 +74,7 @@ func handlerMP4(w http.ResponseWriter, r *http.Request) {
|
|||||||
src := r.URL.Query().Get("src")
|
src := r.URL.Query().Get("src")
|
||||||
stream := streams.GetOrNew(src)
|
stream := streams.GetOrNew(src)
|
||||||
if stream == nil {
|
if stream == nil {
|
||||||
|
http.Error(w, api.StreamNotFound, http.StatusNotFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
package mp4
|
package mp4
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"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/mp4"
|
"github.com/AlexxIT/go2rtc/pkg/mp4"
|
||||||
@@ -8,60 +9,93 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
const MsgTypeMSE = "mse" // fMP4
|
const packetSize = 1400
|
||||||
|
|
||||||
const packetSize = 8192
|
func handlerWSMSE(tr *api.Transport, msg *api.Message) error {
|
||||||
|
src := tr.Request.URL.Query().Get("src")
|
||||||
func handlerWS(ctx *api.Context, msg *streamer.Message) {
|
|
||||||
src := ctx.Request.URL.Query().Get("src")
|
|
||||||
stream := streams.GetOrNew(src)
|
stream := streams.GetOrNew(src)
|
||||||
if stream == nil {
|
if stream == nil {
|
||||||
return
|
return errors.New(api.StreamNotFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
cons := &mp4.Consumer{}
|
cons := &mp4.Consumer{}
|
||||||
cons.UserAgent = ctx.Request.UserAgent()
|
cons.UserAgent = tr.Request.UserAgent()
|
||||||
cons.RemoteAddr = ctx.Request.RemoteAddr
|
cons.RemoteAddr = tr.Request.RemoteAddr
|
||||||
|
|
||||||
if codecs, ok := msg.Value.(string); ok {
|
if codecs, ok := msg.Value.(string); ok {
|
||||||
cons.Medias = parseMedias(codecs)
|
log.Trace().Str("codecs", codecs).Msgf("[mp4] new WS/MSE consumer")
|
||||||
|
cons.Medias = parseMedias(codecs, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
cons.Listen(func(msg interface{}) {
|
cons.Listen(func(msg interface{}) {
|
||||||
if data, ok := msg.([]byte); ok {
|
if data, ok := msg.([]byte); ok {
|
||||||
for len(data) > packetSize {
|
for len(data) > packetSize {
|
||||||
ctx.Write(data[:packetSize])
|
tr.Write(data[:packetSize])
|
||||||
data = data[packetSize:]
|
data = data[packetSize:]
|
||||||
}
|
}
|
||||||
ctx.Write(data)
|
tr.Write(data)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
if err := stream.AddConsumer(cons); err != nil {
|
if err := stream.AddConsumer(cons); err != nil {
|
||||||
log.Warn().Err(err).Caller().Send()
|
log.Warn().Err(err).Caller().Send()
|
||||||
ctx.Error(err)
|
return err
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.OnClose(func() {
|
tr.OnClose(func() {
|
||||||
stream.RemoveConsumer(cons)
|
stream.RemoveConsumer(cons)
|
||||||
})
|
})
|
||||||
|
|
||||||
ctx.Write(&streamer.Message{Type: MsgTypeMSE, Value: cons.MimeType()})
|
tr.Write(&api.Message{Type: "mse", Value: cons.MimeType()})
|
||||||
|
|
||||||
data, err := cons.Init()
|
data, err := cons.Init()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn().Err(err).Caller().Send()
|
log.Warn().Err(err).Caller().Send()
|
||||||
ctx.Error(err)
|
return err
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.Write(data)
|
tr.Write(data)
|
||||||
|
|
||||||
cons.Start()
|
cons.Start()
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseMedias(codecs string) (medias []*streamer.Media) {
|
func handlerWSMP4(tr *api.Transport, msg *api.Message) error {
|
||||||
|
src := tr.Request.URL.Query().Get("src")
|
||||||
|
stream := streams.GetOrNew(src)
|
||||||
|
if stream == nil {
|
||||||
|
return errors.New(api.StreamNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
cons := &mp4.Segment{}
|
||||||
|
|
||||||
|
if codecs, ok := msg.Value.(string); ok {
|
||||||
|
log.Trace().Str("codecs", codecs).Msgf("[mp4] new WS/MP4 consumer")
|
||||||
|
cons.Medias = parseMedias(codecs, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
cons.Listen(func(msg interface{}) {
|
||||||
|
if data, ok := msg.([]byte); ok {
|
||||||
|
tr.Write(data)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := stream.AddConsumer(cons); err != nil {
|
||||||
|
log.Error().Err(err).Caller().Send()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
tr.Write(&api.Message{Type: "mp4", Value: cons.MimeType})
|
||||||
|
|
||||||
|
tr.OnClose(func() {
|
||||||
|
stream.RemoveConsumer(cons)
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseMedias(codecs string, parseAudio bool) (medias []*streamer.Media) {
|
||||||
var videos []*streamer.Codec
|
var videos []*streamer.Codec
|
||||||
var audios []*streamer.Codec
|
var audios []*streamer.Codec
|
||||||
|
|
||||||
@@ -88,7 +122,7 @@ func parseMedias(codecs string) (medias []*streamer.Media) {
|
|||||||
medias = append(medias, media)
|
medias = append(medias, media)
|
||||||
}
|
}
|
||||||
|
|
||||||
if audios != nil {
|
if audios != nil && parseAudio {
|
||||||
media := &streamer.Media{
|
media := &streamer.Media{
|
||||||
Kind: streamer.KindAudio,
|
Kind: streamer.KindAudio,
|
||||||
Direction: streamer.DirectionRecvonly,
|
Direction: streamer.DirectionRecvonly,
|
@@ -8,8 +8,6 @@ import (
|
|||||||
|
|
||||||
func Init() {
|
func Init() {
|
||||||
streams.HandleFunc("rtmp", handle)
|
streams.HandleFunc("rtmp", handle)
|
||||||
streams.HandleFunc("http", handle)
|
|
||||||
streams.HandleFunc("https", handle)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func handle(url string) (streamer.Producer, error) {
|
func handle(url string) (streamer.Producer, error) {
|
||||||
@@ -17,5 +15,8 @@ func handle(url string) (streamer.Producer, error) {
|
|||||||
if err := conn.Dial(); err != nil {
|
if err := conn.Dial(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if err := conn.Describe(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
return conn, nil
|
return conn, nil
|
||||||
}
|
}
|
||||||
|
@@ -14,7 +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"`
|
||||||
|
Username string `yaml:"username"`
|
||||||
|
Password string `yaml:"password"`
|
||||||
} `yaml:"rtsp"`
|
} `yaml:"rtsp"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,7 +54,13 @@ func Init() {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
go tcpHandler(conn)
|
|
||||||
|
c := rtsp.NewServer(conn)
|
||||||
|
// skip check auth for localhost
|
||||||
|
if conf.Mod.Username != "" && !conn.RemoteAddr().(*net.TCPAddr).IP.IsLoopback() {
|
||||||
|
c.Auth(conf.Mod.Username, conf.Mod.Password)
|
||||||
|
}
|
||||||
|
go tcpHandler(c)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
@@ -121,13 +129,12 @@ func rtspHandler(url string) (streamer.Producer, error) {
|
|||||||
return conn, nil
|
return conn, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func tcpHandler(c net.Conn) {
|
func tcpHandler(conn *rtsp.Conn) {
|
||||||
var name string
|
var name string
|
||||||
var closer func()
|
var closer func()
|
||||||
|
|
||||||
trace := log.Trace().Enabled()
|
trace := log.Trace().Enabled()
|
||||||
|
|
||||||
conn := rtsp.NewServer(c)
|
|
||||||
conn.Listen(func(msg interface{}) {
|
conn.Listen(func(msg interface{}) {
|
||||||
if trace {
|
if trace {
|
||||||
switch msg := msg.(type) {
|
switch msg := msg.(type) {
|
||||||
|
@@ -24,6 +24,7 @@ type Producer struct {
|
|||||||
template string
|
template string
|
||||||
|
|
||||||
element streamer.Producer
|
element streamer.Producer
|
||||||
|
lastErr error
|
||||||
tracks []*streamer.Track
|
tracks []*streamer.Track
|
||||||
|
|
||||||
state state
|
state state
|
||||||
@@ -45,10 +46,9 @@ func (p *Producer) GetMedias() []*streamer.Media {
|
|||||||
if p.state == stateNone {
|
if p.state == stateNone {
|
||||||
log.Debug().Msgf("[streams] probe producer url=%s", p.url)
|
log.Debug().Msgf("[streams] probe producer url=%s", p.url)
|
||||||
|
|
||||||
var err error
|
p.element, p.lastErr = GetProducer(p.url)
|
||||||
p.element, err = GetProducer(p.url)
|
if p.lastErr != nil || p.element == nil {
|
||||||
if err != nil || p.element == nil {
|
log.Error().Err(p.lastErr).Caller().Send()
|
||||||
log.Error().Err(err).Caller().Send()
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -3,7 +3,9 @@ package streams
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -55,6 +57,8 @@ func (s *Stream) AddConsumer(cons streamer.Consumer) (err error) {
|
|||||||
consumer := &Consumer{element: cons}
|
consumer := &Consumer{element: cons}
|
||||||
var producers []*Producer // matched producers for consumer
|
var producers []*Producer // matched producers for consumer
|
||||||
|
|
||||||
|
var codecs string
|
||||||
|
|
||||||
// Step 1. Get consumer medias
|
// Step 1. Get consumer medias
|
||||||
for icc, consMedia := range cons.GetMedias() {
|
for icc, consMedia := range cons.GetMedias() {
|
||||||
log.Trace().Stringer("media", consMedia).
|
log.Trace().Stringer("media", consMedia).
|
||||||
@@ -67,6 +71,8 @@ func (s *Stream) AddConsumer(cons streamer.Consumer) (err error) {
|
|||||||
log.Trace().Stringer("media", prodMedia).
|
log.Trace().Stringer("media", prodMedia).
|
||||||
Msgf("[streams] producer=%d candidate=%d", ip, ipc)
|
Msgf("[streams] producer=%d candidate=%d", ip, ipc)
|
||||||
|
|
||||||
|
collectCodecs(prodMedia, &codecs)
|
||||||
|
|
||||||
// Step 3. Match consumer/producer codecs list
|
// Step 3. Match consumer/producer codecs list
|
||||||
prodCodec := prodMedia.MatchMedia(consMedia)
|
prodCodec := prodMedia.MatchMedia(consMedia)
|
||||||
if prodCodec != nil {
|
if prodCodec != nil {
|
||||||
@@ -93,7 +99,18 @@ func (s *Stream) AddConsumer(cons streamer.Consumer) (err error) {
|
|||||||
|
|
||||||
if len(producers) == 0 {
|
if len(producers) == 0 {
|
||||||
s.stopProducers()
|
s.stopProducers()
|
||||||
return errors.New("couldn't find the matching tracks")
|
|
||||||
|
if len(codecs) > 0 {
|
||||||
|
return errors.New("codecs not match: " + codecs)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, producer := range s.producers {
|
||||||
|
if producer.lastErr != nil {
|
||||||
|
return fmt.Errorf("source %d error: %w", i, producer.lastErr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("sources unavailable: %d", len(s.producers))
|
||||||
}
|
}
|
||||||
|
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
@@ -216,3 +233,19 @@ func (s *Stream) removeProducer(i int) {
|
|||||||
s.producers = append(s.producers[:i], s.producers[i+1:]...)
|
s.producers = append(s.producers[:i], s.producers[i+1:]...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func collectCodecs(media *streamer.Media, codecs *string) {
|
||||||
|
for _, codec := range media.Codecs {
|
||||||
|
name := codec.Name
|
||||||
|
if name == streamer.CodecAAC {
|
||||||
|
name = "AAC"
|
||||||
|
}
|
||||||
|
if strings.Contains(*codecs, name) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if len(*codecs) > 0 {
|
||||||
|
*codecs += ","
|
||||||
|
}
|
||||||
|
*codecs += name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -2,7 +2,6 @@ package webrtc
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/AlexxIT/go2rtc/cmd/api"
|
"github.com/AlexxIT/go2rtc/cmd/api"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/webrtc"
|
"github.com/AlexxIT/go2rtc/pkg/webrtc"
|
||||||
"github.com/pion/sdp/v3"
|
"github.com/pion/sdp/v3"
|
||||||
)
|
)
|
||||||
@@ -13,7 +12,7 @@ func AddCandidate(address string) {
|
|||||||
candidates = append(candidates, address)
|
candidates = append(candidates, address)
|
||||||
}
|
}
|
||||||
|
|
||||||
func asyncCandidates(ctx *api.Context) {
|
func asyncCandidates(tr *api.Transport) {
|
||||||
for _, address := range candidates {
|
for _, address := range candidates {
|
||||||
address, err := webrtc.LookupIP(address)
|
address, err := webrtc.LookupIP(address)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -29,7 +28,7 @@ func asyncCandidates(ctx *api.Context) {
|
|||||||
|
|
||||||
log.Trace().Str("candidate", cand).Msg("[webrtc] config")
|
log.Trace().Str("candidate", cand).Msg("[webrtc] config")
|
||||||
|
|
||||||
ctx.Write(&streamer.Message{Type: webrtc.MsgTypeCandidate, Value: cand})
|
tr.Write(&api.Message{Type: "webrtc/candidate", Value: cand})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,12 +78,14 @@ func syncCanditates(answer string) (string, error) {
|
|||||||
return string(data), nil
|
return string(data), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func candidateHandler(ctx *api.Context, msg *streamer.Message) {
|
func candidateHandler(tr *api.Transport, msg *api.Message) error {
|
||||||
if ctx.Consumer == nil {
|
if tr.Consumer == nil {
|
||||||
return
|
return nil
|
||||||
}
|
}
|
||||||
if conn := ctx.Consumer.(*webrtc.Conn); conn != nil {
|
if conn := tr.Consumer.(*webrtc.Conn); conn != nil {
|
||||||
log.Trace().Str("candidate", msg.Value.(string)).Msg("[webrtc] remote")
|
s := msg.Value.(string)
|
||||||
conn.Push(msg)
|
log.Trace().Str("candidate", s).Msg("[webrtc] remote")
|
||||||
|
conn.AddCandidate(s)
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
@@ -1,14 +1,14 @@
|
|||||||
package webrtc
|
package webrtc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"github.com/AlexxIT/go2rtc/cmd/api"
|
"github.com/AlexxIT/go2rtc/cmd/api"
|
||||||
"github.com/AlexxIT/go2rtc/cmd/app"
|
"github.com/AlexxIT/go2rtc/cmd/app"
|
||||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
"github.com/AlexxIT/go2rtc/cmd/streams"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/webrtc"
|
"github.com/AlexxIT/go2rtc/pkg/webrtc"
|
||||||
pion "github.com/pion/webrtc/v3"
|
pion "github.com/pion/webrtc/v3"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
"io/ioutil"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
@@ -55,8 +55,8 @@ func Init() {
|
|||||||
|
|
||||||
candidates = cfg.Mod.Candidates
|
candidates = cfg.Mod.Candidates
|
||||||
|
|
||||||
api.HandleWS(webrtc.MsgTypeOffer, asyncHandler)
|
api.HandleWS("webrtc/offer", asyncHandler)
|
||||||
api.HandleWS(webrtc.MsgTypeCandidate, candidateHandler)
|
api.HandleWS("webrtc/candidate", candidateHandler)
|
||||||
|
|
||||||
api.HandleFunc("api/webrtc", syncHandler)
|
api.HandleFunc("api/webrtc", syncHandler)
|
||||||
}
|
}
|
||||||
@@ -66,11 +66,11 @@ var log zerolog.Logger
|
|||||||
|
|
||||||
var NewPConn func() (*pion.PeerConnection, error)
|
var NewPConn func() (*pion.PeerConnection, error)
|
||||||
|
|
||||||
func asyncHandler(ctx *api.Context, msg *streamer.Message) {
|
func asyncHandler(tr *api.Transport, msg *api.Message) error {
|
||||||
src := ctx.Request.URL.Query().Get("src")
|
src := tr.Request.URL.Query().Get("src")
|
||||||
stream := streams.Get(src)
|
stream := streams.GetOrNew(src)
|
||||||
if stream == nil {
|
if stream == nil {
|
||||||
return
|
return errors.New(api.StreamNotFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debug().Str("url", src).Msg("[webrtc] new consumer")
|
log.Debug().Str("url", src).Msg("[webrtc] new consumer")
|
||||||
@@ -81,21 +81,23 @@ func asyncHandler(ctx *api.Context, msg *streamer.Message) {
|
|||||||
conn := new(webrtc.Conn)
|
conn := new(webrtc.Conn)
|
||||||
conn.Conn, err = NewPConn()
|
conn.Conn, err = NewPConn()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Caller().Msg("NewPConn")
|
log.Error().Err(err).Caller().Send()
|
||||||
return
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
conn.UserAgent = ctx.Request.UserAgent()
|
conn.UserAgent = tr.Request.UserAgent()
|
||||||
conn.Listen(func(msg interface{}) {
|
conn.Listen(func(msg interface{}) {
|
||||||
switch msg := msg.(type) {
|
switch msg := msg.(type) {
|
||||||
case pion.PeerConnectionState:
|
case pion.PeerConnectionState:
|
||||||
if msg == pion.PeerConnectionStateClosed {
|
if msg == pion.PeerConnectionStateClosed {
|
||||||
stream.RemoveConsumer(conn)
|
stream.RemoveConsumer(conn)
|
||||||
}
|
}
|
||||||
case *streamer.Message:
|
case *pion.ICECandidate:
|
||||||
// subscribe on webrtc server candidates
|
if msg != nil {
|
||||||
log.Trace().Str("candidate", msg.Value.(string)).Msg("[webrtc] local")
|
s := msg.ToJSON().Candidate
|
||||||
ctx.Write(msg)
|
log.Trace().Str("candidate", s).Msg("[webrtc] local")
|
||||||
|
tr.Write(&api.Message{Type: "webrtc/candidate", Value: s})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -104,36 +106,35 @@ func asyncHandler(ctx *api.Context, msg *streamer.Message) {
|
|||||||
log.Trace().Msgf("[webrtc] offer:\n%s", offer)
|
log.Trace().Msgf("[webrtc] offer:\n%s", offer)
|
||||||
|
|
||||||
if err = conn.SetOffer(offer); err != nil {
|
if err = conn.SetOffer(offer); err != nil {
|
||||||
log.Warn().Err(err).Caller().Msg("conn.SetOffer")
|
log.Warn().Err(err).Caller().Send()
|
||||||
ctx.Error(err)
|
return err
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. AddConsumer, so we get new tracks
|
// 2. AddConsumer, so we get new tracks
|
||||||
if err = stream.AddConsumer(conn); err != nil {
|
if err = stream.AddConsumer(conn); err != nil {
|
||||||
log.Warn().Err(err).Caller().Msg("stream.AddConsumer")
|
log.Warn().Err(err).Caller().Send()
|
||||||
_ = conn.Conn.Close()
|
_ = conn.Conn.Close()
|
||||||
ctx.Error(err)
|
return err
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
conn.Init()
|
conn.Init()
|
||||||
|
|
||||||
// exchange sdp without waiting all candidates
|
// 3. Exchange SDP without waiting all candidates
|
||||||
answer, err := conn.GetAnswer()
|
answer, err := conn.GetAnswer()
|
||||||
log.Trace().Msgf("[webrtc] answer\n%s", answer)
|
log.Trace().Msgf("[webrtc] answer\n%s", answer)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Caller().Msg("conn.GetAnswer")
|
log.Error().Err(err).Caller().Send()
|
||||||
ctx.Error(err)
|
return err
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.Consumer = conn
|
tr.Consumer = conn
|
||||||
|
|
||||||
ctx.Write(&streamer.Message{Type: webrtc.MsgTypeAnswer, Value: answer})
|
tr.Write(&api.Message{Type: "webrtc/answer", Value: answer})
|
||||||
|
|
||||||
asyncCandidates(ctx)
|
asyncCandidates(tr)
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func syncHandler(w http.ResponseWriter, r *http.Request) {
|
func syncHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -144,7 +145,7 @@ func syncHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// get offer
|
// get offer
|
||||||
offer, err := ioutil.ReadAll(r.Body)
|
offer, err := io.ReadAll(r.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Caller().Msg("ioutil.ReadAll")
|
log.Error().Err(err).Caller().Msg("ioutil.ReadAll")
|
||||||
return
|
return
|
||||||
|
10
go.mod
10
go.mod
@@ -1,6 +1,6 @@
|
|||||||
module github.com/AlexxIT/go2rtc
|
module github.com/AlexxIT/go2rtc
|
||||||
|
|
||||||
go 1.17
|
go 1.19
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/brutella/hap v0.0.17
|
github.com/brutella/hap v0.0.17
|
||||||
@@ -26,6 +26,7 @@ require (
|
|||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/go-chi/chi v1.5.4 // indirect
|
github.com/go-chi/chi v1.5.4 // indirect
|
||||||
github.com/google/uuid v1.3.0 // indirect
|
github.com/google/uuid v1.3.0 // indirect
|
||||||
|
github.com/kr/pretty v0.2.1 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.12 // indirect
|
github.com/mattn/go-colorable v0.1.12 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.14 // indirect
|
github.com/mattn/go-isatty v0.0.14 // indirect
|
||||||
github.com/miekg/dns v1.1.50 // indirect
|
github.com/miekg/dns v1.1.50 // indirect
|
||||||
@@ -41,12 +42,11 @@ require (
|
|||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/xiam/to v0.0.0-20200126224905-d60d31e03561 // indirect
|
github.com/xiam/to v0.0.0-20200126224905-d60d31e03561 // indirect
|
||||||
golang.org/x/crypto v0.0.0-20220516162934-403b01795ae8 // indirect
|
golang.org/x/crypto v0.0.0-20220516162934-403b01795ae8 // indirect
|
||||||
golang.org/x/mod v0.4.2 // indirect
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
|
||||||
golang.org/x/net v0.0.0-20220630215102-69896b714898 // indirect
|
golang.org/x/net v0.0.0-20220630215102-69896b714898 // indirect
|
||||||
golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664 // indirect
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab // indirect
|
||||||
golang.org/x/text v0.3.7 // indirect
|
golang.org/x/text v0.3.7 // indirect
|
||||||
golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2 // indirect
|
golang.org/x/tools v0.1.11 // indirect
|
||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
|
|
||||||
)
|
)
|
||||||
|
|
||||||
replace (
|
replace (
|
||||||
|
55
go.sum
55
go.sum
@@ -4,7 +4,6 @@ github.com/AlexxIT/vdk v0.0.18-0.20221108193131-6168555b4f92 h1:cIeYMGaAirSZnrKR
|
|||||||
github.com/AlexxIT/vdk v0.0.18-0.20221108193131-6168555b4f92/go.mod h1:7ydHfSkflMZxBXfWR79dMjrT54xzvLxnPaByOa9Jpzg=
|
github.com/AlexxIT/vdk v0.0.18-0.20221108193131-6168555b4f92/go.mod h1:7ydHfSkflMZxBXfWR79dMjrT54xzvLxnPaByOa9Jpzg=
|
||||||
github.com/brutella/dnssd v1.2.3 h1:4fBLjZjPH7SbcHhEcIJhZcC9nOhIDZ0m3rn9bjl1/i0=
|
github.com/brutella/dnssd v1.2.3 h1:4fBLjZjPH7SbcHhEcIJhZcC9nOhIDZ0m3rn9bjl1/i0=
|
||||||
github.com/brutella/dnssd v1.2.3/go.mod h1:JoW2sJUrmVIef25G6lrLj7HS6Xdwh6q8WUIvMkkBYXs=
|
github.com/brutella/dnssd v1.2.3/go.mod h1:JoW2sJUrmVIef25G6lrLj7HS6Xdwh6q8WUIvMkkBYXs=
|
||||||
github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ=
|
|
||||||
github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
@@ -15,7 +14,6 @@ github.com/go-chi/chi v1.5.4 h1:QHdzF2szwjqVV4wmByUnTcsbIg7UGaQ0tPF2t5GcAIs=
|
|||||||
github.com/go-chi/chi v1.5.4/go.mod h1:uaf8YgoFazUOkPBG7fxPftUylNumIev9awIWOENIuEg=
|
github.com/go-chi/chi v1.5.4/go.mod h1:uaf8YgoFazUOkPBG7fxPftUylNumIev9awIWOENIuEg=
|
||||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
|
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
|
||||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
|
||||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||||
@@ -29,7 +27,6 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
|
|||||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
|
||||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||||
@@ -37,13 +34,12 @@ github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad
|
|||||||
github.com/hashicorp/mdns v1.0.5 h1:1M5hW1cunYeoXOqHwEb/GBDDHAFo0Yqb/uz/beC6LbE=
|
github.com/hashicorp/mdns v1.0.5 h1:1M5hW1cunYeoXOqHwEb/GBDDHAFo0Yqb/uz/beC6LbE=
|
||||||
github.com/hashicorp/mdns v1.0.5/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc=
|
github.com/hashicorp/mdns v1.0.5/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc=
|
||||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
|
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
|
||||||
|
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
github.com/lucas-clemente/quic-go v0.7.1-0.20190401152353-907071221cf9/go.mod h1:PpMmPfPKO9nKJ/psF49ESTAGQSdfXxlg1otPbEB2nOw=
|
|
||||||
github.com/marten-seemann/qtls v0.2.3/go.mod h1:xzjG7avBwGGbdZ8dTGxlBnLArsVKLvwmjgmPuiQEcYk=
|
|
||||||
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
|
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
|
||||||
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||||
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
|
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
|
||||||
@@ -54,74 +50,49 @@ github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7Xn
|
|||||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||||
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
||||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
|
||||||
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
||||||
github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
|
github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
|
||||||
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
|
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
|
||||||
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
|
||||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||||
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||||
github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
|
github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
|
||||||
github.com/pion/datachannel v1.4.21/go.mod h1:oiNyP4gHx2DIwRzX/MFyH0Rz/Gz05OgBlayAI2hAWjg=
|
|
||||||
github.com/pion/datachannel v1.5.2 h1:piB93s8LGmbECrpO84DnkIVWasRMk3IimbcXkTQLE6E=
|
github.com/pion/datachannel v1.5.2 h1:piB93s8LGmbECrpO84DnkIVWasRMk3IimbcXkTQLE6E=
|
||||||
github.com/pion/datachannel v1.5.2/go.mod h1:FTGQWaHrdCwIJ1rw6xBIfZVkslikjShim5yr05XFuCQ=
|
github.com/pion/datachannel v1.5.2/go.mod h1:FTGQWaHrdCwIJ1rw6xBIfZVkslikjShim5yr05XFuCQ=
|
||||||
github.com/pion/dtls/v2 v2.0.1/go.mod h1:uMQkz2W0cSqY00xav7WByQ4Hb+18xeQh2oH2fRezr5U=
|
|
||||||
github.com/pion/dtls/v2 v2.0.2/go.mod h1:27PEO3MDdaCfo21heT59/vsdmZc0zMt9wQPcSlLu/1I=
|
|
||||||
github.com/pion/dtls/v2 v2.1.3/go.mod h1:o6+WvyLDAlXF7YiPB/RlskRoeK+/JtuaZa5emwQcWus=
|
github.com/pion/dtls/v2 v2.1.3/go.mod h1:o6+WvyLDAlXF7YiPB/RlskRoeK+/JtuaZa5emwQcWus=
|
||||||
github.com/pion/dtls/v2 v2.1.5 h1:jlh2vtIyUBShchoTDqpCCqiYCyRFJ/lvf/gQ8TALs+c=
|
github.com/pion/dtls/v2 v2.1.5 h1:jlh2vtIyUBShchoTDqpCCqiYCyRFJ/lvf/gQ8TALs+c=
|
||||||
github.com/pion/dtls/v2 v2.1.5/go.mod h1:BqCE7xPZbPSubGasRoDFJeTsyJtdD1FanJYL0JGheqY=
|
github.com/pion/dtls/v2 v2.1.5/go.mod h1:BqCE7xPZbPSubGasRoDFJeTsyJtdD1FanJYL0JGheqY=
|
||||||
github.com/pion/ice v0.7.18 h1:KbAWlzWRUdX9SmehBh3gYpIFsirjhSQsCw6K2MjYMK0=
|
|
||||||
github.com/pion/ice v0.7.18/go.mod h1:+Bvnm3nYC6Nnp7VV6glUkuOfToB/AtMRZpOU8ihuf4c=
|
|
||||||
github.com/pion/ice/v2 v2.2.6 h1:R/vaLlI1J2gCx141L5PEwtuGAGcyS6e7E0hDeJFq5Ig=
|
github.com/pion/ice/v2 v2.2.6 h1:R/vaLlI1J2gCx141L5PEwtuGAGcyS6e7E0hDeJFq5Ig=
|
||||||
github.com/pion/ice/v2 v2.2.6/go.mod h1:SWuHiOGP17lGromHTFadUe1EuPgFh/oCU6FCMZHooVE=
|
github.com/pion/ice/v2 v2.2.6/go.mod h1:SWuHiOGP17lGromHTFadUe1EuPgFh/oCU6FCMZHooVE=
|
||||||
github.com/pion/interceptor v0.1.11 h1:00U6OlqxA3FFB50HSg25J/8cWi7P6FbSzw4eFn24Bvs=
|
github.com/pion/interceptor v0.1.11 h1:00U6OlqxA3FFB50HSg25J/8cWi7P6FbSzw4eFn24Bvs=
|
||||||
github.com/pion/interceptor v0.1.11/go.mod h1:tbtKjZY14awXd7Bq0mmWvgtHB5MDaRN7HV3OZ/uy7s8=
|
github.com/pion/interceptor v0.1.11/go.mod h1:tbtKjZY14awXd7Bq0mmWvgtHB5MDaRN7HV3OZ/uy7s8=
|
||||||
github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
|
github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
|
||||||
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
|
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
|
||||||
github.com/pion/mdns v0.0.4/go.mod h1:R1sL0p50l42S5lJs91oNdUL58nm0QHrhxnSegr++qC0=
|
|
||||||
github.com/pion/mdns v0.0.5 h1:Q2oj/JB3NqfzY9xGZ1fPzZzK7sDSD8rZPOvcIQ10BCw=
|
github.com/pion/mdns v0.0.5 h1:Q2oj/JB3NqfzY9xGZ1fPzZzK7sDSD8rZPOvcIQ10BCw=
|
||||||
github.com/pion/mdns v0.0.5/go.mod h1:UgssrvdD3mxpi8tMxAXbsppL3vJ4Jipw1mTCW+al01g=
|
github.com/pion/mdns v0.0.5/go.mod h1:UgssrvdD3mxpi8tMxAXbsppL3vJ4Jipw1mTCW+al01g=
|
||||||
github.com/pion/quic v0.1.1/go.mod h1:zEU51v7ru8Mp4AUBJvj6psrSth5eEFNnVQK5K48oV3k=
|
|
||||||
github.com/pion/randutil v0.0.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
|
|
||||||
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
|
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
|
||||||
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
|
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
|
||||||
github.com/pion/rtcp v1.2.3/go.mod h1:zGhIv0RPRF0Z1Wiij22pUt5W/c9fevqSzT4jje/oK7I=
|
|
||||||
github.com/pion/rtcp v1.2.9 h1:1ujStwg++IOLIEoOiIQ2s+qBuJ1VN81KW+9pMPsif+U=
|
github.com/pion/rtcp v1.2.9 h1:1ujStwg++IOLIEoOiIQ2s+qBuJ1VN81KW+9pMPsif+U=
|
||||||
github.com/pion/rtcp v1.2.9/go.mod h1:qVPhiCzAm4D/rxb6XzKeyZiQK69yJpbUDJSF7TgrqNo=
|
github.com/pion/rtcp v1.2.9/go.mod h1:qVPhiCzAm4D/rxb6XzKeyZiQK69yJpbUDJSF7TgrqNo=
|
||||||
github.com/pion/rtp v1.6.0/go.mod h1:QgfogHsMBVE/RFNno467U/KBqfUywEH+HK+0rtnwsdI=
|
|
||||||
github.com/pion/rtp v1.7.13 h1:qcHwlmtiI50t1XivvoawdCGTP4Uiypzfrsap+bijcoA=
|
github.com/pion/rtp v1.7.13 h1:qcHwlmtiI50t1XivvoawdCGTP4Uiypzfrsap+bijcoA=
|
||||||
github.com/pion/rtp v1.7.13/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko=
|
github.com/pion/rtp v1.7.13/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko=
|
||||||
github.com/pion/sctp v1.7.10/go.mod h1:EhpTUQu1/lcK3xI+eriS6/96fWetHGCvBi9MSsnaBN0=
|
|
||||||
github.com/pion/sctp v1.8.0/go.mod h1:xFe9cLMZ5Vj6eOzpyiKjT9SwGM4KpK/8Jbw5//jc+0s=
|
github.com/pion/sctp v1.8.0/go.mod h1:xFe9cLMZ5Vj6eOzpyiKjT9SwGM4KpK/8Jbw5//jc+0s=
|
||||||
github.com/pion/sctp v1.8.2 h1:yBBCIrUMJ4yFICL3RIvR4eh/H2BTTvlligmSTy+3kiA=
|
github.com/pion/sctp v1.8.2 h1:yBBCIrUMJ4yFICL3RIvR4eh/H2BTTvlligmSTy+3kiA=
|
||||||
github.com/pion/sctp v1.8.2/go.mod h1:xFe9cLMZ5Vj6eOzpyiKjT9SwGM4KpK/8Jbw5//jc+0s=
|
github.com/pion/sctp v1.8.2/go.mod h1:xFe9cLMZ5Vj6eOzpyiKjT9SwGM4KpK/8Jbw5//jc+0s=
|
||||||
github.com/pion/sdp/v2 v2.4.0/go.mod h1:L2LxrOpSTJbAns244vfPChbciR/ReU1KWfG04OpkR7E=
|
|
||||||
github.com/pion/sdp/v3 v3.0.5 h1:ouvI7IgGl+V4CrqskVtr3AaTrPvPisEOxwgpdktctkU=
|
github.com/pion/sdp/v3 v3.0.5 h1:ouvI7IgGl+V4CrqskVtr3AaTrPvPisEOxwgpdktctkU=
|
||||||
github.com/pion/sdp/v3 v3.0.5/go.mod h1:iiFWFpQO8Fy3S5ldclBkpXqmWy02ns78NOKoLLL0YQw=
|
github.com/pion/sdp/v3 v3.0.5/go.mod h1:iiFWFpQO8Fy3S5ldclBkpXqmWy02ns78NOKoLLL0YQw=
|
||||||
github.com/pion/srtp v1.5.1 h1:9Q3jAfslYZBt+C69SI/ZcONJh9049JUHZWYRRf5KEKw=
|
|
||||||
github.com/pion/srtp v1.5.1/go.mod h1:B+QgX5xPeQTNc1CJStJPHzOlHK66ViMDWTT0HZTCkcA=
|
|
||||||
github.com/pion/srtp/v2 v2.0.9/go.mod h1:5TtM9yw6lsH0ppNCehB/EjEUli7VkUgKSPJqWVqbhQ4=
|
|
||||||
github.com/pion/srtp/v2 v2.0.10 h1:b8ZvEuI+mrL8hbr/f1YiJFB34UMrOac3R3N1yq2UN0w=
|
github.com/pion/srtp/v2 v2.0.10 h1:b8ZvEuI+mrL8hbr/f1YiJFB34UMrOac3R3N1yq2UN0w=
|
||||||
github.com/pion/srtp/v2 v2.0.10/go.mod h1:XEeSWaK9PfuMs7zxXyiN252AHPbH12NX5q/CFDWtUuA=
|
github.com/pion/srtp/v2 v2.0.10/go.mod h1:XEeSWaK9PfuMs7zxXyiN252AHPbH12NX5q/CFDWtUuA=
|
||||||
github.com/pion/stun v0.3.5 h1:uLUCBCkQby4S1cf6CGuR9QrVOKcvUwFeemaC865QHDg=
|
github.com/pion/stun v0.3.5 h1:uLUCBCkQby4S1cf6CGuR9QrVOKcvUwFeemaC865QHDg=
|
||||||
github.com/pion/stun v0.3.5/go.mod h1:gDMim+47EeEtfWogA37n6qXZS88L5V6LqFcf+DZA2UA=
|
github.com/pion/stun v0.3.5/go.mod h1:gDMim+47EeEtfWogA37n6qXZS88L5V6LqFcf+DZA2UA=
|
||||||
github.com/pion/transport v0.6.0/go.mod h1:iWZ07doqOosSLMhZ+FXUTq+TamDoXSllxpbGcfkCmbE=
|
|
||||||
github.com/pion/transport v0.8.10/go.mod h1:tBmha/UCjpum5hqTWhfAEs3CO4/tHSg0MYRhSzR+CZ8=
|
|
||||||
github.com/pion/transport v0.10.0/go.mod h1:BnHnUipd0rZQyTVB2SBGojFHT9CBt5C5TcsJSQGkvSE=
|
|
||||||
github.com/pion/transport v0.10.1/go.mod h1:PBis1stIILMiis0PewDw91WJeLJkyIMcEk+DwKOzf4A=
|
|
||||||
github.com/pion/transport v0.12.2/go.mod h1:N3+vZQD9HlDP5GWkZ85LohxNsDcNgofQmyL6ojX5d8Q=
|
github.com/pion/transport v0.12.2/go.mod h1:N3+vZQD9HlDP5GWkZ85LohxNsDcNgofQmyL6ojX5d8Q=
|
||||||
github.com/pion/transport v0.12.3/go.mod h1:OViWW9SP2peE/HbwBvARicmAVnesphkNkCVZIWJ6q9A=
|
github.com/pion/transport v0.12.3/go.mod h1:OViWW9SP2peE/HbwBvARicmAVnesphkNkCVZIWJ6q9A=
|
||||||
github.com/pion/transport v0.13.0/go.mod h1:yxm9uXpK9bpBBWkITk13cLo1y5/ur5VQpG22ny6EP7g=
|
github.com/pion/transport v0.13.0/go.mod h1:yxm9uXpK9bpBBWkITk13cLo1y5/ur5VQpG22ny6EP7g=
|
||||||
github.com/pion/transport v0.13.1 h1:/UH5yLeQtwm2VZIPjxwnNFxjS4DFhyLfS4GlfuKUzfA=
|
github.com/pion/transport v0.13.1 h1:/UH5yLeQtwm2VZIPjxwnNFxjS4DFhyLfS4GlfuKUzfA=
|
||||||
github.com/pion/transport v0.13.1/go.mod h1:EBxbqzyv+ZrmDb82XswEE0BjfQFtuw1Nu6sjnjWCsGg=
|
github.com/pion/transport v0.13.1/go.mod h1:EBxbqzyv+ZrmDb82XswEE0BjfQFtuw1Nu6sjnjWCsGg=
|
||||||
github.com/pion/turn/v2 v2.0.4/go.mod h1:1812p4DcGVbYVBTiraUmP50XoKye++AMkbfp+N27mog=
|
|
||||||
github.com/pion/turn/v2 v2.0.8 h1:KEstL92OUN3k5k8qxsXHpr7WWfrdp7iJZHx99ud8muw=
|
github.com/pion/turn/v2 v2.0.8 h1:KEstL92OUN3k5k8qxsXHpr7WWfrdp7iJZHx99ud8muw=
|
||||||
github.com/pion/turn/v2 v2.0.8/go.mod h1:+y7xl719J8bAEVpSXBXvTxStjJv3hbz9YFflvkpcGPw=
|
github.com/pion/turn/v2 v2.0.8/go.mod h1:+y7xl719J8bAEVpSXBXvTxStjJv3hbz9YFflvkpcGPw=
|
||||||
github.com/pion/udp v0.1.0/go.mod h1:BPELIjbwE9PRbd/zxI/KYBnbo7B6+oA6YuEaNE8lths=
|
|
||||||
github.com/pion/udp v0.1.1 h1:8UAPvyqmsxK8oOjloDk4wUt63TzFe9WEJkg5lChlj7o=
|
github.com/pion/udp v0.1.1 h1:8UAPvyqmsxK8oOjloDk4wUt63TzFe9WEJkg5lChlj7o=
|
||||||
github.com/pion/udp v0.1.1/go.mod h1:6AFo+CMdKQm7UiA0eUPA8/eVCTx8jBIITLZHc9DWX5M=
|
github.com/pion/udp v0.1.1/go.mod h1:6AFo+CMdKQm7UiA0eUPA8/eVCTx8jBIITLZHc9DWX5M=
|
||||||
github.com/pion/webrtc/v2 v2.2.26/go.mod h1:XMZbZRNHyPDe1gzTIHFcQu02283YO45CbiwFgKvXnmc=
|
|
||||||
github.com/pion/webrtc/v3 v3.1.42/go.mod h1:ffD9DulDrPxyWvDPUIPAOSAWx9GUlOExiJPf7cCcMLA=
|
|
||||||
github.com/pion/webrtc/v3 v3.1.43 h1:YT3ZTO94UT4kSBvZnRAH82+0jJPUruiKr9CEstdlQzk=
|
github.com/pion/webrtc/v3 v3.1.43 h1:YT3ZTO94UT4kSBvZnRAH82+0jJPUruiKr9CEstdlQzk=
|
||||||
github.com/pion/webrtc/v3 v3.1.43/go.mod h1:G/J8k0+grVsjC/rjCZ24AKoCCxcFFODgh7zThNZGs0M=
|
github.com/pion/webrtc/v3 v3.1.43/go.mod h1:G/J8k0+grVsjC/rjCZ24AKoCCxcFFODgh7zThNZGs0M=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
@@ -132,7 +103,6 @@ github.com/rs/zerolog v1.27.0 h1:1T7qCieN22GVc8S4Q2yuexzBb1EqjbgjSH9RohbMjKs=
|
|||||||
github.com/rs/zerolog v1.27.0/go.mod h1:7frBqO0oezxmnO7GF86FY++uy8I0Tk/If5ni1G9Qc0U=
|
github.com/rs/zerolog v1.27.0/go.mod h1:7frBqO0oezxmnO7GF86FY++uy8I0Tk/If5ni1G9Qc0U=
|
||||||
github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw=
|
github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
|
||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
@@ -145,28 +115,21 @@ github.com/xiam/to v0.0.0-20200126224905-d60d31e03561 h1:SVoNK97S6JlaYlHcaC+79tg
|
|||||||
github.com/xiam/to v0.0.0-20200126224905-d60d31e03561/go.mod h1:cqbG7phSzrbdg3aj+Kn63bpVruzwDZi58CpxlZkjwzw=
|
github.com/xiam/to v0.0.0-20200126224905-d60d31e03561/go.mod h1:cqbG7phSzrbdg3aj+Kn63bpVruzwDZi58CpxlZkjwzw=
|
||||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||||
golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.0.0-20200602180216-279210d13fed/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
|
||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
|
||||||
golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
golang.org/x/crypto v0.0.0-20220516162934-403b01795ae8 h1:y+mHpWoQJNAHt26Nhh6JP7hvM71IRZureyvZhoVALIs=
|
golang.org/x/crypto v0.0.0-20220516162934-403b01795ae8 h1:y+mHpWoQJNAHt26Nhh6JP7hvM71IRZureyvZhoVALIs=
|
||||||
golang.org/x/crypto v0.0.0-20220516162934-403b01795ae8/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
golang.org/x/crypto v0.0.0-20220516162934-403b01795ae8/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo=
|
|
||||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s=
|
||||||
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
|
||||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
|
||||||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||||
golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
|
||||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
|
||||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
|
||||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||||
golang.org/x/net v0.0.0-20201201195509-5d6afe98e0b7/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
golang.org/x/net v0.0.0-20201201195509-5d6afe98e0b7/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||||
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
@@ -190,13 +153,11 @@ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cO
|
|||||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190228124157-a34e9553db1e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200724161237-0e2f3a69832c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
@@ -210,8 +171,9 @@ golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220608164250-635b8c9b7f68/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220608164250-635b8c9b7f68/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664 h1:wEZYwx+kK+KlZ0hpvP2Ls1Xr4+RWnlzGFwPP0aiDjIU=
|
|
||||||
golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab h1:2QkjZIsXupsJbJIdSjjUOgWK3aEtzyuh2mPt3l/CkeU=
|
||||||
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
@@ -222,12 +184,12 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
|||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||||
golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2 h1:BonxutuHCTL0rBDnZlKjpGIQFTjyUVTexFOdWkB6Fg0=
|
|
||||||
golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||||
|
golang.org/x/tools v0.1.11 h1:loJ25fNOEhSXfHrpoGj91eCUThwdNX6u24rO1xnNteY=
|
||||||
|
golang.org/x/tools v0.1.11/go.mod h1:SgwaegtQh8clINPpECJMqnxLv9I09HLqnW3RMqW0CA4=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
|
||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||||
@@ -242,7 +204,6 @@ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogR
|
|||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
52
hardware.Dockerfile
Normal file
52
hardware.Dockerfile
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
# 0. Prepare images
|
||||||
|
# only debian 12 (bookworm) has latest ffmpeg
|
||||||
|
ARG DEBIAN_VERSION="bookworm-slim"
|
||||||
|
ARG GO_VERSION="1.19-buster"
|
||||||
|
ARG NGROK_VERSION="3"
|
||||||
|
|
||||||
|
FROM debian:${DEBIAN_VERSION} AS base
|
||||||
|
FROM golang:${GO_VERSION} AS go
|
||||||
|
FROM ngrok/ngrok:${NGROK_VERSION} AS ngrok
|
||||||
|
|
||||||
|
|
||||||
|
# 1. Build go2rtc binary
|
||||||
|
FROM go AS build
|
||||||
|
|
||||||
|
WORKDIR /build
|
||||||
|
|
||||||
|
# Cache dependencies
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
RUN go mod download
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
RUN CGO_ENABLED=0 go build -ldflags "-s -w" -trimpath
|
||||||
|
|
||||||
|
|
||||||
|
# 2. Collect all files
|
||||||
|
FROM scratch AS rootfs
|
||||||
|
|
||||||
|
COPY --from=build /build/go2rtc /usr/local/bin/
|
||||||
|
COPY --from=ngrok /bin/ngrok /usr/local/bin/
|
||||||
|
COPY ./build/docker/run.sh /
|
||||||
|
|
||||||
|
|
||||||
|
# 3. Final image
|
||||||
|
FROM base
|
||||||
|
|
||||||
|
# Install ffmpeg, bash (for run.sh), tini (for signal handling),
|
||||||
|
# and other common tools for the echo source.
|
||||||
|
# non-free for Intel QSV support (not used by go2rtc, just for tests)
|
||||||
|
RUN echo 'deb http://deb.debian.org/debian bookworm non-free' > /etc/apt/sources.list.d/debian-non-free.list && \
|
||||||
|
apt-get -y update && apt-get -y install tini ffmpeg python3 curl jq intel-media-va-driver-non-free
|
||||||
|
|
||||||
|
COPY --from=rootfs / /
|
||||||
|
|
||||||
|
RUN chmod a+x /run.sh && mkdir -p /config
|
||||||
|
|
||||||
|
ENTRYPOINT ["/usr/bin/tini", "--"]
|
||||||
|
|
||||||
|
# https://github.com/NVIDIA/nvidia-docker/wiki/Installation-(Native-GPU-Support)
|
||||||
|
ENV NVIDIA_VISIBLE_DEVICES all
|
||||||
|
ENV NVIDIA_DRIVER_CAPABILITIES compute,video,utility
|
||||||
|
|
||||||
|
CMD ["/run.sh"]
|
2
main.go
2
main.go
@@ -9,6 +9,7 @@ import (
|
|||||||
"github.com/AlexxIT/go2rtc/cmd/ffmpeg"
|
"github.com/AlexxIT/go2rtc/cmd/ffmpeg"
|
||||||
"github.com/AlexxIT/go2rtc/cmd/hass"
|
"github.com/AlexxIT/go2rtc/cmd/hass"
|
||||||
"github.com/AlexxIT/go2rtc/cmd/homekit"
|
"github.com/AlexxIT/go2rtc/cmd/homekit"
|
||||||
|
"github.com/AlexxIT/go2rtc/cmd/http"
|
||||||
"github.com/AlexxIT/go2rtc/cmd/ivideon"
|
"github.com/AlexxIT/go2rtc/cmd/ivideon"
|
||||||
"github.com/AlexxIT/go2rtc/cmd/mjpeg"
|
"github.com/AlexxIT/go2rtc/cmd/mjpeg"
|
||||||
"github.com/AlexxIT/go2rtc/cmd/mp4"
|
"github.com/AlexxIT/go2rtc/cmd/mp4"
|
||||||
@@ -40,6 +41,7 @@ func main() {
|
|||||||
webrtc.Init()
|
webrtc.Init()
|
||||||
mp4.Init()
|
mp4.Init()
|
||||||
mjpeg.Init()
|
mjpeg.Init()
|
||||||
|
http.Init()
|
||||||
|
|
||||||
srtp.Init()
|
srtp.Init()
|
||||||
homekit.Init()
|
homekit.Init()
|
||||||
|
@@ -36,26 +36,26 @@ func RTPDepay(track *streamer.Track) streamer.WrapperFunc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(buf) == 0 {
|
if len(buf) == 0 {
|
||||||
// Amcrest IP4M-1051: 9, 7, 8, 6, 28...
|
for {
|
||||||
// Amcrest IP4M-1051: 9, 6, 1
|
// Amcrest IP4M-1051: 9, 7, 8, 6, 28...
|
||||||
switch NALUType(payload) {
|
// Amcrest IP4M-1051: 9, 6, 1
|
||||||
case NALUTypeIFrame:
|
switch NALUType(payload) {
|
||||||
// fix IFrame without SPS,PPS
|
case NALUTypeIFrame:
|
||||||
buf = append(buf, ps...)
|
// fix IFrame without SPS,PPS
|
||||||
case NALUTypeSEI, NALUTypeAUD:
|
|
||||||
// fix ffmpeg with transcoding first frame
|
|
||||||
i := int(4 + binary.BigEndian.Uint32(payload))
|
|
||||||
|
|
||||||
// check if only one NAL (fix ffmpeg transcoding for Reolink RLC-510A)
|
|
||||||
if i == len(payload) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
payload = payload[i:]
|
|
||||||
|
|
||||||
if NALUType(payload) == NALUTypeIFrame {
|
|
||||||
buf = append(buf, ps...)
|
buf = append(buf, ps...)
|
||||||
|
case NALUTypeSEI, NALUTypeAUD:
|
||||||
|
// fix ffmpeg with transcoding first frame
|
||||||
|
i := int(4 + binary.BigEndian.Uint32(payload))
|
||||||
|
|
||||||
|
// check if only one NAL (fix ffmpeg transcoding for Reolink RLC-510A)
|
||||||
|
if i == len(payload) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
payload = payload[i:]
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -4,7 +4,6 @@ import (
|
|||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"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/codec/h265parser"
|
|
||||||
"github.com/pion/rtp"
|
"github.com/pion/rtp"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -69,68 +68,80 @@ func RTPDepay(track *streamer.Track) streamer.WrapperFunc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SafariPay - generate Safari friendly payload for H265
|
// SafariPay - generate Safari friendly payload for H265
|
||||||
|
// https://github.com/AlexxIT/Blog/issues/5
|
||||||
func SafariPay(mtu uint16) streamer.WrapperFunc {
|
func SafariPay(mtu uint16) streamer.WrapperFunc {
|
||||||
sequencer := rtp.NewRandomSequencer()
|
sequencer := rtp.NewRandomSequencer()
|
||||||
size := int(mtu - 12) // rtp.Header size
|
size := int(mtu - 12) // rtp.Header size
|
||||||
|
|
||||||
var buffer []byte
|
|
||||||
|
|
||||||
return func(push streamer.WriterFunc) streamer.WriterFunc {
|
return func(push streamer.WriterFunc) streamer.WriterFunc {
|
||||||
return func(packet *rtp.Packet) error {
|
return func(packet *rtp.Packet) error {
|
||||||
if packet.Version != h264.RTPPacketVersionAVC {
|
if packet.Version != h264.RTPPacketVersionAVC {
|
||||||
return push(packet)
|
return push(packet)
|
||||||
}
|
}
|
||||||
|
|
||||||
data := packet.Payload
|
// protect original packets from modification
|
||||||
data[0] = 0
|
au := make([]byte, len(packet.Payload))
|
||||||
data[1] = 0
|
copy(au, packet.Payload)
|
||||||
data[2] = 0
|
|
||||||
data[3] = 1
|
|
||||||
|
|
||||||
var start byte
|
var start byte
|
||||||
|
|
||||||
nuType := (data[4] >> 1) & 0b111111
|
for i := 0; i < len(au); {
|
||||||
//fmt.Printf("[H265] nut: %2d, size: %6d, data: %16x\n", nut, len(data), data[4:20])
|
size := int(binary.BigEndian.Uint32(au[i:])) + 4
|
||||||
switch {
|
|
||||||
case nuType >= h265parser.NAL_UNIT_VPS && nuType <= h265parser.NAL_UNIT_PPS:
|
// convert AVC to Annex-B
|
||||||
buffer = append(buffer, data...)
|
au[i] = 0
|
||||||
return nil
|
au[i+1] = 0
|
||||||
case nuType >= h265parser.NAL_UNIT_CODED_SLICE_BLA_W_LP && nuType <= h265parser.NAL_UNIT_CODED_SLICE_CRA:
|
au[i+2] = 0
|
||||||
buffer = append([]byte{3}, buffer...)
|
au[i+3] = 1
|
||||||
data = append(buffer, data...)
|
|
||||||
start = 1
|
switch NALUType(au[i:]) {
|
||||||
default:
|
case NALUTypeIFrame, NALUTypeIFrame2, NALUTypeIFrame3:
|
||||||
data = append([]byte{2}, data...)
|
start = 3
|
||||||
start = 0
|
default:
|
||||||
|
if start == 0 {
|
||||||
|
start = 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
i += size
|
||||||
}
|
}
|
||||||
|
|
||||||
for len(data) > size {
|
// rtp.Packet payload
|
||||||
|
b := make([]byte, 1, size)
|
||||||
|
size-- // minus header byte
|
||||||
|
|
||||||
|
for au != nil {
|
||||||
|
b[0] = start
|
||||||
|
|
||||||
|
if start > 1 {
|
||||||
|
start -= 2
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(au) > size {
|
||||||
|
b = append(b, au[:size]...)
|
||||||
|
au = au[size:]
|
||||||
|
} else {
|
||||||
|
b = append(b, au...)
|
||||||
|
au = nil
|
||||||
|
}
|
||||||
|
|
||||||
clone := rtp.Packet{
|
clone := rtp.Packet{
|
||||||
Header: rtp.Header{
|
Header: rtp.Header{
|
||||||
Version: 2,
|
Version: 2,
|
||||||
Marker: false,
|
Marker: au == nil,
|
||||||
SequenceNumber: sequencer.NextSequenceNumber(),
|
SequenceNumber: sequencer.NextSequenceNumber(),
|
||||||
Timestamp: packet.Timestamp,
|
Timestamp: packet.Timestamp,
|
||||||
},
|
},
|
||||||
Payload: data[:size],
|
Payload: b,
|
||||||
}
|
}
|
||||||
if err := push(&clone); err != nil {
|
if err := push(&clone); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
data = append([]byte{start}, data[size:]...)
|
b = b[:1] // clear buffer
|
||||||
}
|
}
|
||||||
|
|
||||||
clone := rtp.Packet{
|
return nil
|
||||||
Header: rtp.Header{
|
|
||||||
Version: 2,
|
|
||||||
Marker: true,
|
|
||||||
SequenceNumber: sequencer.NextSequenceNumber(),
|
|
||||||
Timestamp: packet.Timestamp,
|
|
||||||
},
|
|
||||||
Payload: data,
|
|
||||||
}
|
|
||||||
return push(&clone)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -22,13 +22,17 @@ func Dial(uri string) (*Conn, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return Accept(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Accept(res *http.Response) (*Conn, error) {
|
||||||
c := Conn{
|
c := Conn{
|
||||||
conn: res.Body,
|
conn: res.Body,
|
||||||
reader: bufio.NewReaderSize(res.Body, pio.RecommendBufioSize),
|
reader: bufio.NewReaderSize(res.Body, pio.RecommendBufioSize),
|
||||||
buf: make([]byte, 256),
|
buf: make([]byte, 256),
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err = io.ReadFull(c.reader, c.buf[:flvio.FileHeaderLength]); err != nil {
|
if _, err := io.ReadFull(c.reader, c.buf[:flvio.FileHeaderLength]); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,9 +53,9 @@ func Dial(uri string) (*Conn, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Conn struct {
|
type Conn struct {
|
||||||
conn io.ReadCloser
|
conn io.ReadCloser
|
||||||
reader *bufio.Reader
|
reader *bufio.Reader
|
||||||
buf []byte
|
buf []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Conn) Streams() ([]av.CodecData, error) {
|
func (c *Conn) Streams() ([]av.CodecData, error) {
|
||||||
|
@@ -165,7 +165,7 @@ func (c *Client) getTracks() error {
|
|||||||
Name: streamer.CodecH264,
|
Name: streamer.CodecH264,
|
||||||
ClockRate: 90000,
|
ClockRate: 90000,
|
||||||
FmtpLine: "profile-level-id=" + msg.CodecString[i+1:],
|
FmtpLine: "profile-level-id=" + msg.CodecString[i+1:],
|
||||||
PayloadType: streamer.PayloadTypeMP4,
|
PayloadType: streamer.PayloadTypeRAW,
|
||||||
}
|
}
|
||||||
|
|
||||||
i = bytes.Index(msg.Data, []byte("avcC")) - 4
|
i = bytes.Index(msg.Data, []byte("avcC")) - 4
|
||||||
|
@@ -2,3 +2,4 @@
|
|||||||
|
|
||||||
- https://www.rfc-editor.org/rfc/rfc2435
|
- https://www.rfc-editor.org/rfc/rfc2435
|
||||||
- https://github.com/GStreamer/gst-plugins-good/blob/master/gst/rtp/gstrtpjpegdepay.c
|
- https://github.com/GStreamer/gst-plugins-good/blob/master/gst/rtp/gstrtpjpegdepay.c
|
||||||
|
- https://mjpeg.sanford.io/
|
||||||
|
152
pkg/mjpeg/client.go
Normal file
152
pkg/mjpeg/client.go
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
package mjpeg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"errors"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
||||||
|
"github.com/pion/rtp"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/textproto"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Client struct {
|
||||||
|
streamer.Element
|
||||||
|
|
||||||
|
UserAgent string
|
||||||
|
RemoteAddr string
|
||||||
|
|
||||||
|
closed bool
|
||||||
|
res *http.Response
|
||||||
|
|
||||||
|
track *streamer.Track
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewClient(res *http.Response) *Client {
|
||||||
|
codec := &streamer.Codec{
|
||||||
|
Name: streamer.CodecJPEG, ClockRate: 90000, PayloadType: streamer.PayloadTypeRAW,
|
||||||
|
}
|
||||||
|
return &Client{
|
||||||
|
res: res,
|
||||||
|
track: streamer.NewTrack(codec, streamer.DirectionSendonly),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) GetMedias() []*streamer.Media {
|
||||||
|
return []*streamer.Media{{
|
||||||
|
Kind: streamer.KindVideo,
|
||||||
|
Direction: streamer.DirectionSendonly,
|
||||||
|
Codecs: []*streamer.Codec{c.track.Codec},
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) GetTrack(media *streamer.Media, codec *streamer.Codec) *streamer.Track {
|
||||||
|
return c.track
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Start() error {
|
||||||
|
ct := c.res.Header.Get("Content-Type")
|
||||||
|
|
||||||
|
if ct == "image/jpeg" {
|
||||||
|
return c.startJPEG()
|
||||||
|
}
|
||||||
|
|
||||||
|
// added in go1.18
|
||||||
|
if _, s, ok := strings.Cut(ct, "boundary="); ok {
|
||||||
|
return c.startMJPEG(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors.New("wrong Content-Type: " + ct)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Stop() error {
|
||||||
|
c.closed = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) startJPEG() error {
|
||||||
|
buf, err := io.ReadAll(c.res.Body)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
packet := &rtp.Packet{Header: rtp.Header{Timestamp: now()}, Payload: buf}
|
||||||
|
_ = c.track.WriteRTP(packet)
|
||||||
|
|
||||||
|
req := c.res.Request
|
||||||
|
|
||||||
|
for !c.closed {
|
||||||
|
res, err := tcp.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if res.StatusCode != http.StatusOK {
|
||||||
|
return errors.New("wrong status: " + res.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf, err = io.ReadAll(res.Body)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
packet = &rtp.Packet{Header: rtp.Header{Timestamp: now()}, Payload: buf}
|
||||||
|
_ = c.track.WriteRTP(packet)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) startMJPEG(boundary string) error {
|
||||||
|
boundary = "--" + boundary
|
||||||
|
|
||||||
|
r := bufio.NewReader(c.res.Body)
|
||||||
|
tp := textproto.NewReader(r)
|
||||||
|
|
||||||
|
for !c.closed {
|
||||||
|
s, err := tp.ReadLine()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if s != boundary {
|
||||||
|
return errors.New("wrong boundary: " + s)
|
||||||
|
}
|
||||||
|
|
||||||
|
header, err := tp.ReadMIMEHeader()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
s = header.Get("Content-Length")
|
||||||
|
if s == "" {
|
||||||
|
return errors.New("no content length")
|
||||||
|
}
|
||||||
|
|
||||||
|
size, err := strconv.Atoi(s)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := make([]byte, size)
|
||||||
|
if _, err = io.ReadFull(r, buf); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
packet := &rtp.Packet{Header: rtp.Header{Timestamp: now()}, Payload: buf}
|
||||||
|
_ = c.track.WriteRTP(packet)
|
||||||
|
|
||||||
|
if _, err = r.Discard(2); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func now() uint32 {
|
||||||
|
return uint32(time.Now().UnixMilli() * 90)
|
||||||
|
}
|
@@ -26,70 +26,15 @@ 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 {
|
||||||
var header, payload []byte
|
|
||||||
|
|
||||||
push := func(packet *rtp.Packet) error {
|
push := func(packet *rtp.Packet) error {
|
||||||
//fmt.Printf(
|
c.Fire(packet.Payload)
|
||||||
// "[RTP] codec: %s, size: %6d, ts: %10d, pt: %2d, ssrc: %d, seq: %d, mark: %v\n",
|
|
||||||
// track.Codec.Name, len(packet.Payload), packet.Timestamp,
|
|
||||||
// packet.PayloadType, packet.SSRC, packet.SequenceNumber, packet.Marker,
|
|
||||||
//)
|
|
||||||
|
|
||||||
// https://www.rfc-editor.org/rfc/rfc2435#section-3.1
|
|
||||||
b := packet.Payload
|
|
||||||
|
|
||||||
// 3.1. JPEG header
|
|
||||||
t := b[4]
|
|
||||||
|
|
||||||
// 3.1.7. Restart Marker header
|
|
||||||
if 64 <= t && t <= 127 {
|
|
||||||
b = b[12:] // skip it
|
|
||||||
} else {
|
|
||||||
b = b[8:]
|
|
||||||
}
|
|
||||||
|
|
||||||
if header == nil {
|
|
||||||
var lqt, cqt []byte
|
|
||||||
|
|
||||||
// 3.1.8. Quantization Table header
|
|
||||||
q := packet.Payload[5]
|
|
||||||
if q >= 128 {
|
|
||||||
lqt = b[4:68]
|
|
||||||
cqt = b[68:132]
|
|
||||||
b = b[132:]
|
|
||||||
} else {
|
|
||||||
lqt, cqt = MakeTables(q)
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://www.rfc-editor.org/rfc/rfc2435#section-3.1.5
|
|
||||||
// The maximum width is 2040 pixels.
|
|
||||||
w := uint16(packet.Payload[6]) << 3
|
|
||||||
h := uint16(packet.Payload[7]) << 3
|
|
||||||
|
|
||||||
// fix 2560x1920 and 2560x1440
|
|
||||||
if w == 512 && (h == 1920 || h == 1440) {
|
|
||||||
w = 2560
|
|
||||||
}
|
|
||||||
|
|
||||||
//fmt.Printf("t: %d, q: %d, w: %d, h: %d\n", t, q, w, h)
|
|
||||||
header = MakeHeaders(t, w, h, lqt, cqt)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3.1.9. JPEG Payload
|
|
||||||
payload = append(payload, b...)
|
|
||||||
|
|
||||||
if packet.Marker {
|
|
||||||
b = append(header, payload...)
|
|
||||||
if end := b[len(b)-2:]; end[0] != 0xFF && end[1] != 0xD9 {
|
|
||||||
b = append(b, 0xFF, 0xD9)
|
|
||||||
}
|
|
||||||
c.Fire(b)
|
|
||||||
|
|
||||||
header = nil
|
|
||||||
payload = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if track.Codec.IsRTP() {
|
||||||
|
wrapper := RTPDepay(track)
|
||||||
|
push = wrapper(push)
|
||||||
|
}
|
||||||
|
|
||||||
return track.Bind(push)
|
return track.Bind(push)
|
||||||
}
|
}
|
||||||
|
@@ -138,9 +138,9 @@ var chm_ac_symbols = []byte{
|
|||||||
0xf9, 0xfa,
|
0xf9, 0xfa,
|
||||||
}
|
}
|
||||||
|
|
||||||
func MakeHeaders(t byte, w, h uint16, lqt, cqt []byte) []byte {
|
func MakeHeaders(p []byte, t byte, w, h uint16, lqt, cqt []byte) []byte {
|
||||||
// Appendix A from https://www.rfc-editor.org/rfc/rfc2435
|
// Appendix A from https://www.rfc-editor.org/rfc/rfc2435
|
||||||
p := []byte{0xFF, 0xD8}
|
p = append(p, 0xFF, 0xD8)
|
||||||
|
|
||||||
p = MakeQuantHeader(p, lqt, 0)
|
p = MakeQuantHeader(p, lqt, 0)
|
||||||
p = MakeQuantHeader(p, cqt, 1)
|
p = MakeQuantHeader(p, cqt, 1)
|
||||||
|
212
pkg/mjpeg/rtp.go
Normal file
212
pkg/mjpeg/rtp.go
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
package mjpeg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||||
|
"github.com/pion/rtp"
|
||||||
|
)
|
||||||
|
|
||||||
|
func RTPDepay(track *streamer.Track) streamer.WrapperFunc {
|
||||||
|
buf := make([]byte, 0, 512*1024) // 512K
|
||||||
|
|
||||||
|
return func(push streamer.WriterFunc) streamer.WriterFunc {
|
||||||
|
return func(packet *rtp.Packet) error {
|
||||||
|
//log.Printf("[RTP] codec: %s, size: %6d, ts: %10d, pt: %2d, ssrc: %d, seq: %d, mark: %v", track.Codec.Name, len(packet.Payload), packet.Timestamp, packet.PayloadType, packet.SSRC, packet.SequenceNumber, packet.Marker)
|
||||||
|
|
||||||
|
// https://www.rfc-editor.org/rfc/rfc2435#section-3.1
|
||||||
|
b := packet.Payload
|
||||||
|
|
||||||
|
// 3.1. JPEG header
|
||||||
|
t := b[4]
|
||||||
|
|
||||||
|
// 3.1.7. Restart Marker header
|
||||||
|
if 64 <= t && t <= 127 {
|
||||||
|
b = b[12:] // skip it
|
||||||
|
} else {
|
||||||
|
b = b[8:]
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(buf) == 0 {
|
||||||
|
var lqt, cqt []byte
|
||||||
|
|
||||||
|
// 3.1.8. Quantization Table header
|
||||||
|
q := packet.Payload[5]
|
||||||
|
if q >= 128 {
|
||||||
|
lqt = b[4:68]
|
||||||
|
cqt = b[68:132]
|
||||||
|
b = b[132:]
|
||||||
|
} else {
|
||||||
|
lqt, cqt = MakeTables(q)
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://www.rfc-editor.org/rfc/rfc2435#section-3.1.5
|
||||||
|
// The maximum width is 2040 pixels.
|
||||||
|
w := uint16(packet.Payload[6]) << 3
|
||||||
|
h := uint16(packet.Payload[7]) << 3
|
||||||
|
|
||||||
|
// fix 2560x1920 and 2560x1440
|
||||||
|
if w == 512 && (h == 1920 || h == 1440) {
|
||||||
|
w = 2560
|
||||||
|
}
|
||||||
|
|
||||||
|
//fmt.Printf("t: %d, q: %d, w: %d, h: %d\n", t, q, w, h)
|
||||||
|
buf = MakeHeaders(buf, t, w, h, lqt, cqt)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3.1.9. JPEG Payload
|
||||||
|
buf = append(buf, b...)
|
||||||
|
|
||||||
|
if !packet.Marker {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if end := buf[len(buf)-2:]; end[0] != 0xFF && end[1] != 0xD9 {
|
||||||
|
buf = append(buf, 0xFF, 0xD9)
|
||||||
|
}
|
||||||
|
|
||||||
|
clone := *packet
|
||||||
|
clone.Payload = buf
|
||||||
|
|
||||||
|
buf = buf[:0] // clear buffer
|
||||||
|
|
||||||
|
return push(&clone)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func RTPPay() streamer.WrapperFunc {
|
||||||
|
return func(push streamer.WriterFunc) streamer.WriterFunc {
|
||||||
|
return func(packet *rtp.Packet) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//func RTPPay() streamer.WrapperFunc {
|
||||||
|
// const packetSize = 1436
|
||||||
|
//
|
||||||
|
// sequencer := rtp.NewRandomSequencer()
|
||||||
|
//
|
||||||
|
// return func(push streamer.WriterFunc) streamer.WriterFunc {
|
||||||
|
// return func(packet *rtp.Packet) error {
|
||||||
|
// // reincode image to more common form
|
||||||
|
// img, err := jpeg.Decode(bytes.NewReader(packet.Payload))
|
||||||
|
// if err != nil {
|
||||||
|
// return err
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// wh := img.Bounds().Size()
|
||||||
|
// w := wh.X
|
||||||
|
// h := wh.Y
|
||||||
|
//
|
||||||
|
// if w > 2040 {
|
||||||
|
// w = 2040
|
||||||
|
// } else if w&3 > 0 {
|
||||||
|
// w &= 3
|
||||||
|
// }
|
||||||
|
// if h > 2040 {
|
||||||
|
// h = 2040
|
||||||
|
// } else if h&3 > 0 {
|
||||||
|
// h &= 3
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// if w != wh.X || h != wh.Y {
|
||||||
|
// x0 := (wh.X - w) / 2
|
||||||
|
// y0 := (wh.Y - h) / 2
|
||||||
|
// rect := image.Rect(x0, y0, x0+w, y0+h)
|
||||||
|
// img = img.(*image.YCbCr).SubImage(rect)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// buf := bytes.NewBuffer(nil)
|
||||||
|
// if err = jpeg.Encode(buf, img, nil); err != nil {
|
||||||
|
// return err
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// h1 := make([]byte, 8)
|
||||||
|
// h1[4] = 1 // Type
|
||||||
|
// h1[5] = 255 // Q
|
||||||
|
//
|
||||||
|
// // MBZ=0, Precision=0, Length=128
|
||||||
|
// h2 := make([]byte, 4, 132)
|
||||||
|
// h2[3] = 128
|
||||||
|
//
|
||||||
|
// var jpgData []byte
|
||||||
|
//
|
||||||
|
// p := buf.Bytes()
|
||||||
|
//
|
||||||
|
// for jpgData == nil {
|
||||||
|
// // 2 bytes h1
|
||||||
|
// if p[0] != 0xFF {
|
||||||
|
// return nil
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// size := binary.BigEndian.Uint16(p[2:]) + 2
|
||||||
|
//
|
||||||
|
// // 2 bytes payload size (include 2 bytes)
|
||||||
|
// switch p[1] {
|
||||||
|
// case 0xD8: // 0. Start Of Image (size=0)
|
||||||
|
// p = p[2:]
|
||||||
|
// continue
|
||||||
|
// case 0xDB: // 1. Define Quantization Table (size=130)
|
||||||
|
// for i := uint16(4 + 1); i < size; i += 1 + 64 {
|
||||||
|
// h2 = append(h2, p[i:i+64]...)
|
||||||
|
// }
|
||||||
|
// case 0xC0: // 2. Start Of Frame (size=15)
|
||||||
|
// if p[4] != 8 {
|
||||||
|
// return nil
|
||||||
|
// }
|
||||||
|
// h := binary.BigEndian.Uint16(p[5:])
|
||||||
|
// w := binary.BigEndian.Uint16(p[7:])
|
||||||
|
// h1[6] = uint8(w >> 3)
|
||||||
|
// h1[7] = uint8(h >> 3)
|
||||||
|
// case 0xC4: // 3. Define Huffman Table (size=416)
|
||||||
|
// case 0xDA: // 4. Start Of Scan (size=10)
|
||||||
|
// jpgData = p[size:]
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// p = p[size:]
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// offset := 0
|
||||||
|
// p = make([]byte, 0)
|
||||||
|
//
|
||||||
|
// for jpgData != nil {
|
||||||
|
// p = p[:0]
|
||||||
|
//
|
||||||
|
// if offset > 0 {
|
||||||
|
// h1[1] = byte(offset >> 16)
|
||||||
|
// h1[2] = byte(offset >> 8)
|
||||||
|
// h1[3] = byte(offset)
|
||||||
|
// p = append(p, h1...)
|
||||||
|
// } else {
|
||||||
|
// p = append(p, h1...)
|
||||||
|
// p = append(p, h2...)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// dataLen := packetSize - len(p)
|
||||||
|
// if dataLen < len(jpgData) {
|
||||||
|
// p = append(p, jpgData[:dataLen]...)
|
||||||
|
// jpgData = jpgData[dataLen:]
|
||||||
|
// offset += dataLen
|
||||||
|
// } else {
|
||||||
|
// p = append(p, jpgData...)
|
||||||
|
// jpgData = nil
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// clone := rtp.Packet{
|
||||||
|
// Header: rtp.Header{
|
||||||
|
// Version: 2,
|
||||||
|
// Marker: jpgData == nil,
|
||||||
|
// SequenceNumber: sequencer.NextSequenceNumber(),
|
||||||
|
// Timestamp: packet.Timestamp,
|
||||||
|
// },
|
||||||
|
// Payload: p,
|
||||||
|
// }
|
||||||
|
// if err := push(&clone); err != nil {
|
||||||
|
// return err
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// return nil
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//}
|
@@ -18,11 +18,17 @@ type Consumer struct {
|
|||||||
|
|
||||||
muxer *Muxer
|
muxer *Muxer
|
||||||
codecs []*streamer.Codec
|
codecs []*streamer.Codec
|
||||||
start bool
|
wait byte
|
||||||
|
|
||||||
send int
|
send int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
waitNone byte = iota
|
||||||
|
waitKeyframe
|
||||||
|
waitInit
|
||||||
|
)
|
||||||
|
|
||||||
func (c *Consumer) GetMedias() []*streamer.Media {
|
func (c *Consumer) GetMedias() []*streamer.Media {
|
||||||
if c.Medias != nil {
|
if c.Medias != nil {
|
||||||
return c.Medias
|
return c.Medias
|
||||||
@@ -55,13 +61,18 @@ func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *strea
|
|||||||
codec := track.Codec
|
codec := track.Codec
|
||||||
switch codec.Name {
|
switch codec.Name {
|
||||||
case streamer.CodecH264:
|
case streamer.CodecH264:
|
||||||
|
c.wait = waitInit
|
||||||
|
|
||||||
push := func(packet *rtp.Packet) error {
|
push := func(packet *rtp.Packet) error {
|
||||||
if packet.Version != h264.RTPPacketVersionAVC {
|
if packet.Version != h264.RTPPacketVersionAVC {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if !c.start {
|
if c.wait != waitNone {
|
||||||
return nil
|
if c.wait == waitInit || !h264.IsKeyframe(packet.Payload) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
c.wait = waitNone
|
||||||
}
|
}
|
||||||
|
|
||||||
buf := c.muxer.Marshal(trackID, packet)
|
buf := c.muxer.Marshal(trackID, packet)
|
||||||
@@ -72,23 +83,28 @@ func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *strea
|
|||||||
}
|
}
|
||||||
|
|
||||||
var wrapper streamer.WrapperFunc
|
var wrapper streamer.WrapperFunc
|
||||||
if codec.IsMP4() {
|
if codec.IsRTP() {
|
||||||
wrapper = h264.RepairAVC(track)
|
|
||||||
} else {
|
|
||||||
wrapper = h264.RTPDepay(track)
|
wrapper = h264.RTPDepay(track)
|
||||||
|
} else {
|
||||||
|
wrapper = h264.RepairAVC(track)
|
||||||
}
|
}
|
||||||
push = wrapper(push)
|
push = wrapper(push)
|
||||||
|
|
||||||
return track.Bind(push)
|
return track.Bind(push)
|
||||||
|
|
||||||
case streamer.CodecH265:
|
case streamer.CodecH265:
|
||||||
|
c.wait = waitInit
|
||||||
|
|
||||||
push := func(packet *rtp.Packet) error {
|
push := func(packet *rtp.Packet) error {
|
||||||
if packet.Version != h264.RTPPacketVersionAVC {
|
if packet.Version != h264.RTPPacketVersionAVC {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if !c.start {
|
if c.wait != waitNone {
|
||||||
return nil
|
if c.wait == waitInit || !h265.IsKeyframe(packet.Payload) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
c.wait = waitNone
|
||||||
}
|
}
|
||||||
|
|
||||||
buf := c.muxer.Marshal(trackID, packet)
|
buf := c.muxer.Marshal(trackID, packet)
|
||||||
@@ -98,7 +114,7 @@ func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *strea
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if !codec.IsMP4() {
|
if codec.IsRTP() {
|
||||||
wrapper := h265.RTPDepay(track)
|
wrapper := h265.RTPDepay(track)
|
||||||
push = wrapper(push)
|
push = wrapper(push)
|
||||||
}
|
}
|
||||||
@@ -107,7 +123,7 @@ func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *strea
|
|||||||
|
|
||||||
case streamer.CodecAAC:
|
case streamer.CodecAAC:
|
||||||
push := func(packet *rtp.Packet) error {
|
push := func(packet *rtp.Packet) error {
|
||||||
if !c.start {
|
if c.wait != waitNone {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,7 +134,7 @@ func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *strea
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if !codec.IsMP4() {
|
if codec.IsRTP() {
|
||||||
wrapper := aac.RTPDepay(track)
|
wrapper := aac.RTPDepay(track)
|
||||||
push = wrapper(push)
|
push = wrapper(push)
|
||||||
}
|
}
|
||||||
@@ -139,7 +155,9 @@ func (c *Consumer) Init() ([]byte, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Consumer) Start() {
|
func (c *Consumer) Start() {
|
||||||
c.start = true
|
if c.wait == waitInit {
|
||||||
|
c.wait = waitKeyframe
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
|
@@ -19,8 +19,6 @@ type Muxer struct {
|
|||||||
fragIndex uint32
|
fragIndex uint32
|
||||||
dts []uint64
|
dts []uint64
|
||||||
pts []uint32
|
pts []uint32
|
||||||
//data []byte
|
|
||||||
//total int
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Muxer) MimeType(codecs []*streamer.Codec) string {
|
func (m *Muxer) MimeType(codecs []*streamer.Codec) string {
|
||||||
@@ -185,10 +183,13 @@ func (m *Muxer) GetInit(codecs []*streamer.Codec) ([]byte, error) {
|
|||||||
return append(FTYP(), data...), nil
|
return append(FTYP(), data...), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
//func (m *Muxer) Rewind() {
|
func (m *Muxer) Reset() {
|
||||||
// m.dts = 0
|
m.fragIndex = 0
|
||||||
// m.pts = 0
|
for i := range m.dts {
|
||||||
//}
|
m.dts[i] = 0
|
||||||
|
m.pts[i] = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (m *Muxer) Marshal(trackID byte, packet *rtp.Packet) []byte {
|
func (m *Muxer) Marshal(trackID byte, packet *rtp.Packet) []byte {
|
||||||
run := &mp4fio.TrackFragRun{
|
run := &mp4fio.TrackFragRun{
|
||||||
@@ -218,15 +219,16 @@ func (m *Muxer) Marshal(trackID byte, packet *rtp.Packet) []byte {
|
|||||||
}
|
}
|
||||||
|
|
||||||
entry := mp4io.TrackFragRunEntry{
|
entry := mp4io.TrackFragRunEntry{
|
||||||
//Duration: 90000,
|
|
||||||
Size: uint32(len(packet.Payload)),
|
Size: uint32(len(packet.Payload)),
|
||||||
}
|
}
|
||||||
|
|
||||||
newTime := packet.Timestamp
|
newTime := packet.Timestamp
|
||||||
if m.pts[trackID] > 0 {
|
if m.pts[trackID] > 0 {
|
||||||
//m.dts += uint64(newTime - m.pts)
|
|
||||||
entry.Duration = newTime - m.pts[trackID]
|
entry.Duration = newTime - m.pts[trackID]
|
||||||
m.dts[trackID] += uint64(entry.Duration)
|
m.dts[trackID] += uint64(entry.Duration)
|
||||||
|
} else {
|
||||||
|
// important, or Safari will fail with first frame
|
||||||
|
entry.Duration = 1
|
||||||
}
|
}
|
||||||
m.pts[trackID] = newTime
|
m.pts[trackID] = newTime
|
||||||
|
|
||||||
|
@@ -7,13 +7,20 @@ import (
|
|||||||
"github.com/pion/rtp"
|
"github.com/pion/rtp"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Keyframe struct {
|
type Segment struct {
|
||||||
streamer.Element
|
streamer.Element
|
||||||
|
|
||||||
MimeType string
|
Medias []*streamer.Media
|
||||||
|
MimeType string
|
||||||
|
OnlyKeyframe bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Keyframe) GetMedias() []*streamer.Media {
|
func (c *Segment) GetMedias() []*streamer.Media {
|
||||||
|
if c.Medias != nil {
|
||||||
|
return c.Medias
|
||||||
|
}
|
||||||
|
|
||||||
|
// default medias
|
||||||
return []*streamer.Media{
|
return []*streamer.Media{
|
||||||
{
|
{
|
||||||
Kind: streamer.KindVideo,
|
Kind: streamer.KindVideo,
|
||||||
@@ -26,7 +33,7 @@ func (c *Keyframe) GetMedias() []*streamer.Media {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Keyframe) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.Track {
|
func (c *Segment) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.Track {
|
||||||
muxer := &Muxer{}
|
muxer := &Muxer{}
|
||||||
|
|
||||||
codecs := []*streamer.Codec{track.Codec}
|
codecs := []*streamer.Codec{track.Codec}
|
||||||
@@ -40,22 +47,53 @@ func (c *Keyframe) AddTrack(media *streamer.Media, track *streamer.Track) *strea
|
|||||||
|
|
||||||
switch track.Codec.Name {
|
switch track.Codec.Name {
|
||||||
case streamer.CodecH264:
|
case streamer.CodecH264:
|
||||||
push := func(packet *rtp.Packet) error {
|
var push streamer.WriterFunc
|
||||||
if !h264.IsKeyframe(packet.Payload) {
|
|
||||||
|
if c.OnlyKeyframe {
|
||||||
|
push = func(packet *rtp.Packet) error {
|
||||||
|
if !h264.IsKeyframe(packet.Payload) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := muxer.Marshal(0, packet)
|
||||||
|
c.Fire(append(init, buf...))
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
var buf []byte
|
||||||
|
|
||||||
buf := muxer.Marshal(0, packet)
|
push = func(packet *rtp.Packet) error {
|
||||||
c.Fire(append(init, buf...))
|
if h264.IsKeyframe(packet.Payload) {
|
||||||
|
// fist frame - send only IFrame
|
||||||
|
// other frames - send IFrame and all PFrames
|
||||||
|
if buf == nil {
|
||||||
|
buf = append(buf, init...)
|
||||||
|
b := muxer.Marshal(0, packet)
|
||||||
|
buf = append(buf, b...)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
c.Fire(buf)
|
||||||
|
|
||||||
|
buf = buf[:0]
|
||||||
|
buf = append(buf, init...)
|
||||||
|
muxer.Reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
if buf != nil {
|
||||||
|
b := muxer.Marshal(0, packet)
|
||||||
|
buf = append(buf, b...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var wrapper streamer.WrapperFunc
|
var wrapper streamer.WrapperFunc
|
||||||
if track.Codec.IsMP4() {
|
if track.Codec.IsRTP() {
|
||||||
wrapper = h264.RepairAVC(track)
|
|
||||||
} else {
|
|
||||||
wrapper = h264.RTPDepay(track)
|
wrapper = h264.RTPDepay(track)
|
||||||
|
} else {
|
||||||
|
wrapper = h264.RepairAVC(track)
|
||||||
}
|
}
|
||||||
push = wrapper(push)
|
push = wrapper(push)
|
||||||
|
|
||||||
@@ -73,7 +111,7 @@ func (c *Keyframe) AddTrack(media *streamer.Media, track *streamer.Track) *strea
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if !track.Codec.IsMP4() {
|
if track.Codec.IsRTP() {
|
||||||
wrapper := h265.RTPDepay(track)
|
wrapper := h265.RTPDepay(track)
|
||||||
push = wrapper(push)
|
push = wrapper(push)
|
||||||
}
|
}
|
@@ -89,7 +89,7 @@ func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *strea
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if !codec.IsMP4() {
|
if !codec.IsRAW() {
|
||||||
wrapper := h264.RTPDepay(track)
|
wrapper := h264.RTPDepay(track)
|
||||||
push = wrapper(push)
|
push = wrapper(push)
|
||||||
}
|
}
|
||||||
@@ -146,7 +146,7 @@ func (c *Consumer) Init() ([]byte, error) {
|
|||||||
return data, nil
|
return data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Consumer) Start() {
|
func (c *Consumer) Start() {
|
||||||
c.start = true
|
c.start = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -11,7 +11,7 @@ import (
|
|||||||
"github.com/deepch/vdk/codec/h264parser"
|
"github.com/deepch/vdk/codec/h264parser"
|
||||||
"github.com/deepch/vdk/format/rtmp"
|
"github.com/deepch/vdk/format/rtmp"
|
||||||
"github.com/pion/rtp"
|
"github.com/pion/rtp"
|
||||||
"strings"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -41,16 +41,20 @@ func NewClient(uri string) *Client {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) Dial() (err error) {
|
func (c *Client) Dial() (err error) {
|
||||||
if strings.HasPrefix(c.URI, "http") {
|
c.conn, err = rtmp.Dial(c.URI)
|
||||||
c.conn, err = httpflv.Dial(c.URI)
|
return
|
||||||
} else {
|
}
|
||||||
c.conn, err = rtmp.Dial(c.URI)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Accept - convert http.Response to Client
|
||||||
|
func Accept(res *http.Response) (*Client, error) {
|
||||||
|
conn, err := httpflv.Accept(res)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return nil, err
|
||||||
}
|
}
|
||||||
|
return &Client{URI: res.Request.URL.String(), conn: conn}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Describe() (err error) {
|
||||||
// important to get SPS/PPS
|
// important to get SPS/PPS
|
||||||
streams, err := c.conn.Streams()
|
streams, err := c.conn.Streams()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -73,7 +77,7 @@ func (c *Client) Dial() (err error) {
|
|||||||
Name: streamer.CodecH264,
|
Name: streamer.CodecH264,
|
||||||
ClockRate: 90000,
|
ClockRate: 90000,
|
||||||
FmtpLine: fmtp,
|
FmtpLine: fmtp,
|
||||||
PayloadType: streamer.PayloadTypeMP4,
|
PayloadType: streamer.PayloadTypeRAW,
|
||||||
}
|
}
|
||||||
|
|
||||||
media := &streamer.Media{
|
media := &streamer.Media{
|
||||||
@@ -96,7 +100,7 @@ func (c *Client) Dial() (err error) {
|
|||||||
Channels: uint16(cd.Config.ChannelConfig),
|
Channels: uint16(cd.Config.ChannelConfig),
|
||||||
// a=fmtp:97 streamtype=5;profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=1588
|
// a=fmtp:97 streamtype=5;profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=1588
|
||||||
FmtpLine: "streamtype=5;profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=" + hex.EncodeToString(cd.ConfigBytes),
|
FmtpLine: "streamtype=5;profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=" + hex.EncodeToString(cd.ConfigBytes),
|
||||||
PayloadType: streamer.PayloadTypeMP4,
|
PayloadType: streamer.PayloadTypeRAW,
|
||||||
}
|
}
|
||||||
|
|
||||||
media := &streamer.Media{
|
media := &streamer.Media{
|
||||||
|
@@ -9,6 +9,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/aac"
|
"github.com/AlexxIT/go2rtc/pkg/aac"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/h264"
|
"github.com/AlexxIT/go2rtc/pkg/h264"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/mjpeg"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
||||||
"github.com/pion/rtcp"
|
"github.com/pion/rtcp"
|
||||||
@@ -99,6 +100,11 @@ func NewServer(conn net.Conn) *Conn {
|
|||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Conn) Auth(username, password string) {
|
||||||
|
info := url.UserPassword(username, password)
|
||||||
|
c.auth = tcp.NewAuth(info)
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Conn) parseURI() (err error) {
|
func (c *Conn) parseURI() (err error) {
|
||||||
c.URL, err = url.Parse(c.uri)
|
c.URL, err = url.Parse(c.uri)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -398,6 +404,12 @@ func (c *Conn) SetupMedia(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// in case the track has already been setup before
|
||||||
|
if codec == nil {
|
||||||
|
c.state = StateSetup
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
// we send our `interleaved`, but camera can answer with another
|
// we send our `interleaved`, but camera can answer with another
|
||||||
|
|
||||||
// Transport: RTP/AVP/TCP;unicast;interleaved=10-11;ssrc=10117CB7
|
// Transport: RTP/AVP/TCP;unicast;interleaved=10-11;ssrc=10117CB7
|
||||||
@@ -490,6 +502,17 @@ func (c *Conn) Accept() error {
|
|||||||
|
|
||||||
c.Fire(req)
|
c.Fire(req)
|
||||||
|
|
||||||
|
if !c.auth.Validate(req) {
|
||||||
|
res := &tcp.Response{
|
||||||
|
Status: "401 Unauthorized",
|
||||||
|
Header: map[string][]string{"Www-Authenticate": {`Basic realm="go2rtc"`}},
|
||||||
|
}
|
||||||
|
if err = c.Response(res); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
// Receiver: OPTIONS > DESCRIBE > SETUP... > PLAY > TEARDOWN
|
// Receiver: OPTIONS > DESCRIBE > SETUP... > PLAY > TEARDOWN
|
||||||
// Sender: OPTIONS > ANNOUNCE > SETUP... > RECORD > TEARDOWN
|
// Sender: OPTIONS > ANNOUNCE > SETUP... > RECORD > TEARDOWN
|
||||||
switch req.Method {
|
switch req.Method {
|
||||||
@@ -760,6 +783,8 @@ func (c *Conn) bindTrack(
|
|||||||
|
|
||||||
size := packet.MarshalSize()
|
size := packet.MarshalSize()
|
||||||
|
|
||||||
|
//log.Printf("[RTP] codec: %s, size: %6d, ts: %10d, pt: %2d, ssrc: %d, seq: %d, mark: %v", track.Codec.Name, len(packet.Payload), packet.Timestamp, packet.PayloadType, packet.SSRC, packet.SequenceNumber, packet.Marker)
|
||||||
|
|
||||||
data := make([]byte, 4+size)
|
data := make([]byte, 4+size)
|
||||||
data[0] = '$'
|
data[0] = '$'
|
||||||
data[1] = channel
|
data[1] = channel
|
||||||
@@ -778,7 +803,7 @@ func (c *Conn) bindTrack(
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if track.Codec.IsMP4() {
|
if !track.Codec.IsRTP() {
|
||||||
switch track.Codec.Name {
|
switch track.Codec.Name {
|
||||||
case streamer.CodecH264:
|
case streamer.CodecH264:
|
||||||
wrapper := h264.RTPPay(1500)
|
wrapper := h264.RTPPay(1500)
|
||||||
@@ -786,6 +811,9 @@ func (c *Conn) bindTrack(
|
|||||||
case streamer.CodecAAC:
|
case streamer.CodecAAC:
|
||||||
wrapper := aac.RTPPay(1500)
|
wrapper := aac.RTPPay(1500)
|
||||||
push = wrapper(push)
|
push = wrapper(push)
|
||||||
|
case streamer.CodecJPEG:
|
||||||
|
wrapper := mjpeg.RTPPay()
|
||||||
|
push = wrapper(push)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -20,9 +20,10 @@ func (c *Conn) GetTrack(media *streamer.Media, codec *streamer.Codec) *streamer.
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// can't setup new tracks from play state
|
// can't setup new tracks from play state - forcing a reconnection feature
|
||||||
if c.state == StatePlay {
|
if c.state == StatePlay {
|
||||||
return nil
|
go c.Close()
|
||||||
|
return streamer.NewTrack(codec, media.Direction)
|
||||||
}
|
}
|
||||||
|
|
||||||
track, err := c.SetupMedia(media, codec)
|
track, err := c.SetupMedia(media, codec)
|
||||||
|
@@ -12,14 +12,6 @@ const (
|
|||||||
JSONSend = "send"
|
JSONSend = "send"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Message - struct for data exchange in Web API
|
|
||||||
type Message struct {
|
|
||||||
Type string `json:"type"`
|
|
||||||
Value interface{} `json:"value,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// other
|
|
||||||
|
|
||||||
func Between(s, sub1, sub2 string) string {
|
func Between(s, sub1, sub2 string) string {
|
||||||
i := strings.Index(s, sub1)
|
i := strings.Index(s, sub1)
|
||||||
if i < 0 {
|
if i < 0 {
|
||||||
|
@@ -37,7 +37,7 @@ const (
|
|||||||
CodecELD = "ELD" // AAC-ELD
|
CodecELD = "ELD" // AAC-ELD
|
||||||
)
|
)
|
||||||
|
|
||||||
const PayloadTypeMP4 byte = 255
|
const PayloadTypeRAW byte = 255
|
||||||
|
|
||||||
func GetKind(name string) string {
|
func GetKind(name string) string {
|
||||||
switch name {
|
switch name {
|
||||||
@@ -139,8 +139,8 @@ func (c *Codec) String() string {
|
|||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Codec) IsMP4() bool {
|
func (c *Codec) IsRTP() bool {
|
||||||
return c.PayloadType == PayloadTypeMP4
|
return c.PayloadType != PayloadTypeRAW
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Codec) Clone() *Codec {
|
func (c *Codec) Clone() *Codec {
|
||||||
|
@@ -80,6 +80,24 @@ func (a *Auth) Write(req *Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *Auth) Validate(req *Request) bool {
|
||||||
|
if a == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
header := req.Header.Get("Authorization")
|
||||||
|
if header == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if a.Method == AuthUnknown {
|
||||||
|
a.Method = AuthBasic
|
||||||
|
a.header = "Basic " + B64(a.user, a.pass)
|
||||||
|
}
|
||||||
|
|
||||||
|
return header == a.header
|
||||||
|
}
|
||||||
|
|
||||||
func Between(s, sub1, sub2 string) string {
|
func Between(s, sub1, sub2 string) string {
|
||||||
i := strings.Index(s, sub1)
|
i := strings.Index(s, sub1)
|
||||||
if i < 0 {
|
if i < 0 {
|
||||||
|
68
pkg/tcp/request.go
Normal file
68
pkg/tcp/request.go
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
package tcp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Do - http.Client with support Digest Authorization
|
||||||
|
func Do(req *http.Request) (*http.Response, error) {
|
||||||
|
// need to create new client each time to reset timeout
|
||||||
|
client := http.Client{Timeout: time.Second * 5000}
|
||||||
|
res, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if res.StatusCode == http.StatusUnauthorized && req.URL.User != nil {
|
||||||
|
auth := res.Header.Get("WWW-Authenticate")
|
||||||
|
if !strings.HasPrefix(auth, "Digest") {
|
||||||
|
return nil, errors.New("unsupported auth: " + auth)
|
||||||
|
}
|
||||||
|
|
||||||
|
realm := Between(auth, `realm="`, `"`)
|
||||||
|
nonce := Between(auth, `nonce="`, `"`)
|
||||||
|
qop := Between(auth, `qop="`, `"`)
|
||||||
|
|
||||||
|
user := req.URL.User
|
||||||
|
username := user.Username()
|
||||||
|
password, _ := user.Password()
|
||||||
|
ha1 := HexMD5(username, realm, password)
|
||||||
|
|
||||||
|
uri := req.URL.RequestURI()
|
||||||
|
ha2 := HexMD5(req.Method, uri)
|
||||||
|
|
||||||
|
var header string
|
||||||
|
|
||||||
|
switch qop {
|
||||||
|
case "":
|
||||||
|
response := HexMD5(ha1, nonce, ha2)
|
||||||
|
header = fmt.Sprintf(
|
||||||
|
`Digest username="%s", realm="%s", nonce="%s", uri="%s", response="%s"`,
|
||||||
|
user, realm, nonce, uri, response,
|
||||||
|
)
|
||||||
|
case "auth":
|
||||||
|
nc := "00000001"
|
||||||
|
cnonce := "00000001" // TODO: random...
|
||||||
|
response := HexMD5(ha1, nonce, nc, cnonce, qop, ha2)
|
||||||
|
header = fmt.Sprintf(
|
||||||
|
`Digest username="%s", realm="%s", nonce="%s", uri="%s", qop=%s, nc=%s, cnonce="%s", response="%s"`,
|
||||||
|
username, realm, nonce, uri, qop, nc, cnonce, response,
|
||||||
|
)
|
||||||
|
default:
|
||||||
|
return nil, errors.New("unsupported qop: " + auth)
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Authorization", header)
|
||||||
|
|
||||||
|
res, err = client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res, nil
|
||||||
|
}
|
@@ -5,13 +5,6 @@ import (
|
|||||||
"github.com/pion/webrtc/v3"
|
"github.com/pion/webrtc/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
MsgTypeOffer = "webrtc/offer"
|
|
||||||
MsgTypeOfferComplete = "webrtc/offer-complete"
|
|
||||||
MsgTypeAnswer = "webrtc/answer"
|
|
||||||
MsgTypeCandidate = "webrtc/candidate"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Conn struct {
|
type Conn struct {
|
||||||
streamer.Element
|
streamer.Element
|
||||||
|
|
||||||
@@ -28,11 +21,7 @@ type Conn struct {
|
|||||||
|
|
||||||
func (c *Conn) Init() {
|
func (c *Conn) Init() {
|
||||||
c.Conn.OnICECandidate(func(candidate *webrtc.ICECandidate) {
|
c.Conn.OnICECandidate(func(candidate *webrtc.ICECandidate) {
|
||||||
if candidate != nil {
|
c.Fire(candidate)
|
||||||
c.Fire(&streamer.Message{
|
|
||||||
Type: MsgTypeCandidate, Value: candidate.ToJSON().Candidate,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
c.Conn.OnTrack(func(remote *webrtc.TrackRemote, receiver *webrtc.RTPReceiver) {
|
c.Conn.OnTrack(func(remote *webrtc.TrackRemote, receiver *webrtc.RTPReceiver) {
|
||||||
@@ -60,6 +49,10 @@ func (c *Conn) Init() {
|
|||||||
//fmt.Printf("TODO: webrtc ontrack %+v\n", remote)
|
//fmt.Printf("TODO: webrtc ontrack %+v\n", remote)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
c.Conn.OnDataChannel(func(channel *webrtc.DataChannel) {
|
||||||
|
c.Fire(channel)
|
||||||
|
})
|
||||||
|
|
||||||
// OK connection:
|
// OK connection:
|
||||||
// 15:01:46 ICE connection state changed: checking
|
// 15:01:46 ICE connection state changed: checking
|
||||||
// 15:01:46 peer connection state changed: connected
|
// 15:01:46 peer connection state changed: connected
|
||||||
|
@@ -57,10 +57,10 @@ func (c *Conn) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.
|
|||||||
wrapper := h264.RTPPay(1200)
|
wrapper := h264.RTPPay(1200)
|
||||||
push = wrapper(push)
|
push = wrapper(push)
|
||||||
|
|
||||||
if codec.IsMP4() {
|
if codec.IsRTP() {
|
||||||
wrapper = h264.RepairAVC(track)
|
|
||||||
} else {
|
|
||||||
wrapper = h264.RTPDepay(track)
|
wrapper = h264.RTPDepay(track)
|
||||||
|
} else {
|
||||||
|
wrapper = h264.RepairAVC(track)
|
||||||
}
|
}
|
||||||
push = wrapper(push)
|
push = wrapper(push)
|
||||||
|
|
||||||
@@ -108,14 +108,8 @@ func (c *Conn) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.
|
|||||||
|
|
||||||
//
|
//
|
||||||
|
|
||||||
func (c *Conn) Push(msg interface{}) {
|
func (c *Conn) AddCandidate(candidate string) {
|
||||||
if msg := msg.(*streamer.Message); msg != nil {
|
_ = c.Conn.AddICECandidate(webrtc.ICECandidateInit{Candidate: candidate})
|
||||||
if msg.Type == MsgTypeCandidate {
|
|
||||||
_ = c.Conn.AddICECandidate(webrtc.ICECandidateInit{
|
|
||||||
Candidate: msg.Value.(string),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Conn) MarshalJSON() ([]byte, error) {
|
func (c *Conn) MarshalJSON() ([]byte, error) {
|
||||||
|
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
- UPX-3.96 pack broken bin for `linux_mipsel`
|
- UPX-3.96 pack broken bin for `linux_mipsel`
|
||||||
- UPX-3.95 pack broken bin for `mac_amd64`
|
- UPX-3.95 pack broken bin for `mac_amd64`
|
||||||
|
- UPX pack broken bin for `mac_arm64`
|
||||||
- UPX windows pack is recognised by anti-viruses as malicious
|
- UPX windows pack is recognised by anti-viruses as malicious
|
||||||
- `aarch64` = `arm64`
|
- `aarch64` = `arm64`
|
||||||
- `armv7` = `arm`
|
- `armv7` = `arm`
|
||||||
|
@@ -3,45 +3,50 @@
|
|||||||
@SET GOOS=windows
|
@SET GOOS=windows
|
||||||
@SET GOARCH=amd64
|
@SET GOARCH=amd64
|
||||||
@SET FILENAME=go2rtc_win64.zip
|
@SET FILENAME=go2rtc_win64.zip
|
||||||
go build -ldflags "-s -w" -trimpath && 7z a -sdel %FILENAME% go2rtc.exe
|
go build -ldflags "-s -w" -trimpath && 7z a -mx9 -sdel %FILENAME% go2rtc.exe
|
||||||
|
|
||||||
@SET GOOS=windows
|
@SET GOOS=windows
|
||||||
@SET GOARCH=386
|
@SET GOARCH=386
|
||||||
@SET FILENAME=go2rtc_win32.zip
|
@SET FILENAME=go2rtc_win32.zip
|
||||||
go build -ldflags "-s -w" -trimpath && 7z a -sdel %FILENAME% go2rtc.exe
|
go build -ldflags "-s -w" -trimpath && 7z a -mx9 -sdel %FILENAME% go2rtc.exe
|
||||||
|
|
||||||
|
@SET GOOS=windows
|
||||||
|
@SET GOARCH=arm64
|
||||||
|
@SET FILENAME=go2rtc_win_arm64.zip
|
||||||
|
go build -ldflags "-s -w" -trimpath && 7z a -mx9 -sdel %FILENAME% go2rtc.exe
|
||||||
|
|
||||||
@SET GOOS=linux
|
@SET GOOS=linux
|
||||||
@SET GOARCH=amd64
|
@SET GOARCH=amd64
|
||||||
@SET FILENAME=go2rtc_linux_amd64
|
@SET FILENAME=go2rtc_linux_amd64
|
||||||
go build -ldflags "-s -w" -trimpath -o %FILENAME% && upx-3.96 %FILENAME%
|
go build -ldflags "-s -w" -trimpath -o %FILENAME% && upx %FILENAME%
|
||||||
|
|
||||||
@SET GOOS=linux
|
@SET GOOS=linux
|
||||||
@SET GOARCH=386
|
@SET GOARCH=386
|
||||||
@SET FILENAME=go2rtc_linux_i386
|
@SET FILENAME=go2rtc_linux_i386
|
||||||
go build -ldflags "-s -w" -trimpath -o %FILENAME% && upx-3.96 %FILENAME%
|
go build -ldflags "-s -w" -trimpath -o %FILENAME% && upx %FILENAME%
|
||||||
|
|
||||||
@SET GOOS=linux
|
@SET GOOS=linux
|
||||||
@SET GOARCH=arm64
|
@SET GOARCH=arm64
|
||||||
@SET FILENAME=go2rtc_linux_arm64
|
@SET FILENAME=go2rtc_linux_arm64
|
||||||
go build -ldflags "-s -w" -trimpath -o %FILENAME% && upx-3.96 %FILENAME%
|
go build -ldflags "-s -w" -trimpath -o %FILENAME% && upx %FILENAME%
|
||||||
|
|
||||||
@SET GOOS=linux
|
@SET GOOS=linux
|
||||||
@SET GOARCH=arm
|
@SET GOARCH=arm
|
||||||
@SET GOARM=7
|
@SET GOARM=7
|
||||||
@SET FILENAME=go2rtc_linux_arm
|
@SET FILENAME=go2rtc_linux_arm
|
||||||
go build -ldflags "-s -w" -trimpath -o %FILENAME% && upx-3.96 %FILENAME%
|
go build -ldflags "-s -w" -trimpath -o %FILENAME% && upx %FILENAME%
|
||||||
|
|
||||||
@SET GOOS=linux
|
@SET GOOS=linux
|
||||||
@SET GOARCH=mipsle
|
@SET GOARCH=mipsle
|
||||||
@SET FILENAME=go2rtc_linux_mipsel
|
@SET FILENAME=go2rtc_linux_mipsel
|
||||||
go build -ldflags "-s -w" -trimpath -o %FILENAME% && upx-3.95 %FILENAME%
|
go build -ldflags "-s -w" -trimpath -o %FILENAME% && upx %FILENAME%
|
||||||
|
|
||||||
@SET GOOS=darwin
|
@SET GOOS=darwin
|
||||||
@SET GOARCH=amd64
|
@SET GOARCH=amd64
|
||||||
@SET FILENAME=go2rtc_mac_amd64
|
@SET FILENAME=go2rtc_mac_amd64.zip
|
||||||
go build -ldflags "-s -w" -trimpath -o %FILENAME% && upx-3.96 %FILENAME%
|
go build -ldflags "-s -w" -trimpath && 7z a -mx9 -sdel %FILENAME% go2rtc
|
||||||
|
|
||||||
@SET GOOS=darwin
|
@SET GOOS=darwin
|
||||||
@SET GOARCH=arm64
|
@SET GOARCH=arm64
|
||||||
@SET FILENAME=go2rtc_mac_arm64
|
@SET FILENAME=go2rtc_mac_arm64.zip
|
||||||
go build -ldflags "-s -w" -trimpath -o %FILENAME% && upx-3.96 %FILENAME%
|
go build -ldflags "-s -w" -trimpath && 7z a -mx9 -sdel %FILENAME% go2rtc
|
||||||
|
@@ -1,5 +0,0 @@
|
|||||||
@SET GOOS=linux
|
|
||||||
@SET GOARCH=amd64
|
|
||||||
@cd ..
|
|
||||||
del go2rtc
|
|
||||||
go build -ldflags "-s -w" -trimpath
|
|
@@ -1,5 +0,0 @@
|
|||||||
@SET GOOS=linux
|
|
||||||
@SET GOARCH=arm
|
|
||||||
@cd ..
|
|
||||||
del go2rtc
|
|
||||||
go build -ldflags "-s -w" -trimpath
|
|
@@ -1,5 +0,0 @@
|
|||||||
@ECHO OFF
|
|
||||||
@SET GOOS=linux
|
|
||||||
@SET GOARCH=mipsle
|
|
||||||
cd ..
|
|
||||||
go build -ldflags "-s -w" -trimpath && upx-3.95 go2rtc
|
|
@@ -1,5 +0,0 @@
|
|||||||
@SET GOOS=darwin
|
|
||||||
@SET GOARCH=amd64
|
|
||||||
@cd ..
|
|
||||||
del go2rtc
|
|
||||||
go build -ldflags "-s -w" -trimpath
|
|
@@ -1,4 +0,0 @@
|
|||||||
@SET GOOS=windows
|
|
||||||
@SET GOARCH=amd64
|
|
||||||
cd ..
|
|
||||||
go build -ldflags "-w -s" -trimpath
|
|
114
www/index.html
114
www/index.html
@@ -8,6 +8,10 @@
|
|||||||
<title>go2rtc</title>
|
<title>go2rtc</title>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial, Helvetica, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
table {
|
table {
|
||||||
background-color: white;
|
background-color: white;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
@@ -36,9 +40,23 @@
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
padding: 5px 5px;
|
padding: 5px 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
display: flex;
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls > label {
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -47,6 +65,13 @@
|
|||||||
<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>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="controls">
|
||||||
|
<button>stream</button>
|
||||||
|
<label><input type="checkbox" name="webrtc" checked>webrtc</label>
|
||||||
|
<label><input type="checkbox" name="mse" checked>mse</label>
|
||||||
|
<label><input type="checkbox" name="mp4" checked>mp4</label>
|
||||||
|
<label><input type="checkbox" name="mjpeg" checked>mjpeg</label>
|
||||||
|
</div>
|
||||||
<table id="streams">
|
<table id="streams">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -59,50 +84,71 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<script>
|
<script>
|
||||||
const baseUrl = location.origin + location.pathname.substr(
|
const templates = [
|
||||||
0, location.pathname.lastIndexOf("/")
|
'<a href="stream.html?src={name}">stream</a>',
|
||||||
);
|
'<a href="webrtc.html?src={name}">2-way-aud</a>',
|
||||||
|
|
||||||
const links = [
|
|
||||||
'<a href="webrtc.html?src={name}">webrtc</a>',
|
|
||||||
'<a href="mse.html?src={name}">mse</a>',
|
|
||||||
// '<a href="video.html?src={name}">video</a>',
|
|
||||||
'<a href="api/stream.mp4?src={name}">mp4</a>',
|
'<a href="api/stream.mp4?src={name}">mp4</a>',
|
||||||
'<a href="api/frame.mp4?src={name}">frame</a>',
|
|
||||||
`<a href="rtsp://${location.hostname}:8554/{name}">rtsp</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>',
|
||||||
];
|
];
|
||||||
|
|
||||||
function reload() {
|
document.querySelector("#add")
|
||||||
fetch(`${baseUrl}/api/streams`).then(r => {
|
.addEventListener("click", () => {
|
||||||
r.json().then(data => {
|
const src = document.querySelector("#src");
|
||||||
let html = '';
|
const url = new URL("api/streams", location.href);
|
||||||
|
url.searchParams.set("src", src.value);
|
||||||
|
fetch(url, {method: "PUT"}).then(reload);
|
||||||
|
});
|
||||||
|
|
||||||
for (const [name, value] of Object.entries(data)) {
|
document.querySelector(".controls > button")
|
||||||
const online = value !== null ? value.length : 0
|
.addEventListener("click", () => {
|
||||||
html += `<tr><td>${name || 'default'}</td><td>${online}</td><td>`;
|
const url = new URL("stream.html", location.href);
|
||||||
links.forEach(link => {
|
|
||||||
html += link.replace('{name}', encodeURIComponent(name)) + ' ';
|
|
||||||
})
|
|
||||||
html += `<a href="#" onclick="deleteStream('${name}')">delete</a>`;
|
|
||||||
html += `</td></tr>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
let content = document.getElementById('streams').getElementsByTagName('tbody')[0];
|
const streams = document.querySelectorAll("#streams input");
|
||||||
content.innerHTML = html
|
streams.forEach(i => {
|
||||||
|
if (i.checked) url.searchParams.append("src", i.name);
|
||||||
});
|
});
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function deleteStream(src) {
|
if (!url.searchParams.has("src")) return;
|
||||||
fetch(`${baseUrl}/api/streams?src=${encodeURIComponent(src)}`, {method: 'DELETE'}).then(reload);
|
|
||||||
}
|
|
||||||
|
|
||||||
const addButton = document.querySelector('a#add');
|
let mode = document.querySelectorAll(".controls input");
|
||||||
addButton.onclick = () => {
|
mode = Array.from(mode).filter(i => i.checked).map(i => i.name).join(",");
|
||||||
let src = document.querySelector('input#src');
|
|
||||||
fetch(`${baseUrl}/api/streams?src=${encodeURIComponent(src.value)}`, {method: 'PUT'}).then(reload);
|
window.location.href = `${url}&mode=${mode}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
const tbody = document.querySelector("#streams > tbody");
|
||||||
|
tbody.addEventListener("click", ev => {
|
||||||
|
if (ev.target.innerText !== "delete") return;
|
||||||
|
|
||||||
|
ev.preventDefault();
|
||||||
|
|
||||||
|
const url = new URL("api/streams", location.href);
|
||||||
|
url.searchParams.set("src", ev.target.dataset.name);
|
||||||
|
fetch(url, {method: "DELETE"}).then(reload);
|
||||||
|
});
|
||||||
|
|
||||||
|
function reload() {
|
||||||
|
const url = new URL("api/streams", location.href);
|
||||||
|
fetch(url).then(r => r.json()).then(data => {
|
||||||
|
tbody.innerHTML = "";
|
||||||
|
|
||||||
|
for (const [name, value] of Object.entries(data)) {
|
||||||
|
const online = value ? value.length : 0;
|
||||||
|
const links = templates.map(link => {
|
||||||
|
return link.replace("{name}", encodeURIComponent(name));
|
||||||
|
}).join(" ");
|
||||||
|
|
||||||
|
const tr = document.createElement("tr");
|
||||||
|
tr.dataset["id"] = name;
|
||||||
|
tr.innerHTML =
|
||||||
|
`<td><label><input type="checkbox" name="${name}">${name}</label></td>` +
|
||||||
|
`<td>${online}</td><td>${links}</td>`;
|
||||||
|
tbody.appendChild(tr);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
reload();
|
reload();
|
||||||
|
28
www/mse.html
28
www/mse.html
@@ -1,28 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
<title>go2rtc - MSE</title>
|
|
||||||
<script src="video-rtc.js"></script>
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
background: black;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
html, body, video {
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<script>
|
|
||||||
const url = new URL("api/ws" + location.search, location.href);
|
|
||||||
const video = document.createElement("video-rtc");
|
|
||||||
video.src = url.toString();
|
|
||||||
document.body.appendChild(video);
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
62
www/stream.html
Normal file
62
www/stream.html
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>go2rtc - Stream</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
background: black;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
font-family: Arial, Helvetica, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-content: flex-start;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script type="module" src="./video-stream.js"></script>
|
||||||
|
<script type="module">
|
||||||
|
const params = new URLSearchParams(location.search);
|
||||||
|
|
||||||
|
// support multiple streams and multiple modes
|
||||||
|
const streams = params.getAll("src");
|
||||||
|
const modes = params.getAll("mode");
|
||||||
|
if (modes.length === 0) modes.push("");
|
||||||
|
|
||||||
|
while (modes.length > streams.length) {
|
||||||
|
streams.push(streams[0]);
|
||||||
|
}
|
||||||
|
while (streams.length > modes.length) {
|
||||||
|
modes.push(modes[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (streams.length > 1) {
|
||||||
|
document.body.className = "flex";
|
||||||
|
}
|
||||||
|
|
||||||
|
const background = params.get("background") !== "false";
|
||||||
|
const width = "1 0 " + (params.get("width") || "320px");
|
||||||
|
|
||||||
|
for (let i = 0; i < streams.length; i++) {
|
||||||
|
/** @type {VideoStream} */
|
||||||
|
const video = document.createElement("video-stream");
|
||||||
|
video.background = background;
|
||||||
|
video.mode = modes[i] || video.mode;
|
||||||
|
video.style.flex = width;
|
||||||
|
video.src = new URL("api/ws?src=" + encodeURIComponent(streams[i]), location.href);
|
||||||
|
document.body.appendChild(video);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
743
www/video-rtc.js
743
www/video-rtc.js
@@ -1,182 +1,150 @@
|
|||||||
/**
|
/**
|
||||||
* Common function for processing MSE and MSE2 data.
|
* Video player for go2rtc streaming application.
|
||||||
* @param ms MediaSource
|
|
||||||
*/
|
|
||||||
function MediaSourceHandler(ms) {
|
|
||||||
let sb, qb = [];
|
|
||||||
|
|
||||||
return ev => {
|
|
||||||
if (typeof ev.data === "string") {
|
|
||||||
const msg = JSON.parse(ev.data);
|
|
||||||
if (msg.type === "mse") {
|
|
||||||
if (!MediaSource.isTypeSupported(msg.value)) {
|
|
||||||
console.warn("Not supported: " + msg.value)
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
sb = ms.addSourceBuffer(msg.value);
|
|
||||||
sb.mode = "segments"; // segments or sequence
|
|
||||||
sb.addEventListener("updateend", () => {
|
|
||||||
if (!sb.updating && qb.length > 0) {
|
|
||||||
try {
|
|
||||||
sb.appendBuffer(qb.shift());
|
|
||||||
} catch (e) {
|
|
||||||
// console.warn(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else if (sb.updating || qb.length > 0) {
|
|
||||||
qb.push(ev.data);
|
|
||||||
// console.debug("buffer:", qb.length);
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
sb.appendBuffer(ev.data);
|
|
||||||
} catch (e) {
|
|
||||||
// console.warn(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dedicated Worker Handler for MSE2 https://chromestatus.com/feature/5177263249162240
|
|
||||||
*/
|
|
||||||
if (typeof importScripts == "function") {
|
|
||||||
// protect below code (class VideoRTC) from fail inside Worker
|
|
||||||
HTMLElement = Object;
|
|
||||||
customElements = {define: Function()};
|
|
||||||
|
|
||||||
const ms = new MediaSource();
|
|
||||||
ms.addEventListener("sourceopen", ev => {
|
|
||||||
postMessage({type: ev.type});
|
|
||||||
}, {once: true});
|
|
||||||
|
|
||||||
onmessage = MediaSourceHandler(ms);
|
|
||||||
|
|
||||||
postMessage({type: "handle", value: ms.handle}, [ms.handle]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Video player for MSE and WebRTC connections.
|
|
||||||
*
|
*
|
||||||
* All modern web technologies are supported in almost any browser except Apple Safari.
|
* All modern web technologies are supported in almost any browser except Apple Safari.
|
||||||
*
|
*
|
||||||
* Support:
|
* Support:
|
||||||
* - RTCPeerConnection for Safari iOS 11.0+
|
* - RTCPeerConnection for Safari iOS 11.0+
|
||||||
* - IntersectionObserver for Safari iOS 12.2+
|
* - IntersectionObserver for Safari iOS 12.2+
|
||||||
* - MediaSource in Workers for Chrome 108+
|
|
||||||
*
|
*
|
||||||
* Doesn't support:
|
* Doesn't support:
|
||||||
* - MediaSource for Safari iOS all
|
* - MediaSource for Safari iOS all
|
||||||
* - Customized built-in elements (extends HTMLVideoElement) because all Safari
|
* - Customized built-in elements (extends HTMLVideoElement) because all Safari
|
||||||
|
* - Public class fields because old Safari (before 14.0)
|
||||||
*/
|
*/
|
||||||
class VideoRTC extends HTMLElement {
|
export class VideoRTC extends HTMLElement {
|
||||||
DISCONNECT_TIMEOUT = 5000;
|
|
||||||
RECONNECT_TIMEOUT = 30000;
|
|
||||||
|
|
||||||
CODECS = [
|
|
||||||
"avc1.640029", // H.264 high 4.1 (Chromecast 1st and 2nd Gen)
|
|
||||||
"avc1.64002A", // H.264 high 4.2 (Chromecast 3rd Gen)
|
|
||||||
"hvc1.1.6.L153.B0", // H.265 main 5.1 (Chromecast Ultra)
|
|
||||||
"mp4a.40.2", // AAC LC
|
|
||||||
"mp4a.40.5", // AAC HE
|
|
||||||
"mp4a.69", // MP3
|
|
||||||
"mp4a.6B", // MP3
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Enable MediaSource in Workers mode.
|
|
||||||
* @type {boolean}
|
|
||||||
*/
|
|
||||||
MSE2 = true;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Run stream when not displayed on the screen. Default `false`.
|
|
||||||
* @type {boolean}
|
|
||||||
*/
|
|
||||||
background = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Run stream only when player in the viewport. Stop when user scroll out player.
|
|
||||||
* Value is percentage of visibility from `0` (not visible) to `1` (full visible).
|
|
||||||
* Default `0` - disable;
|
|
||||||
* @type {number}
|
|
||||||
*/
|
|
||||||
intersectionThreshold = 0;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Run stream only when browser page on the screen. Stop when user change browser
|
|
||||||
* tab or minimise browser windows.
|
|
||||||
* @type {boolean}
|
|
||||||
*/
|
|
||||||
visibilityCheck = true;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @type {HTMLVideoElement}
|
|
||||||
*/
|
|
||||||
video = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @type {RTCPeerConnection}
|
|
||||||
*/
|
|
||||||
pc = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @type {WebSocket}
|
|
||||||
*/
|
|
||||||
ws = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Internal WebSocket connection state. Values: CLOSED, CONNECTING, OPEN
|
|
||||||
* @type {number}
|
|
||||||
*/
|
|
||||||
wsState = WebSocket.CLOSED;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Internal WebSocket URL.
|
|
||||||
* @type {string}
|
|
||||||
*/
|
|
||||||
wsURL = "";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Internal disconnect TimeoutID.
|
|
||||||
* @type {number}
|
|
||||||
*/
|
|
||||||
disconnectTimeout = 0;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Internal reconnect TimeoutID.
|
|
||||||
* @type {number}
|
|
||||||
*/
|
|
||||||
reconnectTimeout = 0;
|
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
console.debug("this.constructor");
|
this.DISCONNECT_TIMEOUT = 5000;
|
||||||
|
this.RECONNECT_TIMEOUT = 30000;
|
||||||
|
|
||||||
this.video = document.createElement("video");
|
this.CODECS = [
|
||||||
this.video.controls = true;
|
"avc1.640029", // H.264 high 4.1 (Chromecast 1st and 2nd Gen)
|
||||||
this.video.playsInline = true;
|
"avc1.64002A", // H.264 high 4.2 (Chromecast 3rd Gen)
|
||||||
|
"avc1.640033", // H.264 high 5.1 (Chromecast with Google TV)
|
||||||
|
"hvc1.1.6.L153.B0", // H.265 main 5.1 (Chromecast Ultra)
|
||||||
|
"mp4a.40.2", // AAC LC
|
||||||
|
"mp4a.40.5", // AAC HE
|
||||||
|
"mp4a.69", // MP3
|
||||||
|
"mp4a.6B", // MP3
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [config] Supported modes (webrtc, mse, mp4, mjpeg).
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
this.mode = "webrtc,mse,mp4,mjpeg";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [config] Run stream when not displayed on the screen. Default `false`.
|
||||||
|
* @type {boolean}
|
||||||
|
*/
|
||||||
|
this.background = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [config] Run stream only when player in the viewport. Stop when user scroll out player.
|
||||||
|
* Value is percentage of visibility from `0` (not visible) to `1` (full visible).
|
||||||
|
* Default `0` - disable;
|
||||||
|
* @type {number}
|
||||||
|
*/
|
||||||
|
this.visibilityThreshold = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [config] Run stream only when browser page on the screen. Stop when user change browser
|
||||||
|
* tab or minimise browser windows.
|
||||||
|
* @type {boolean}
|
||||||
|
*/
|
||||||
|
this.visibilityCheck = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [config] WebRTC configuration
|
||||||
|
* @type {RTCConfiguration}
|
||||||
|
*/
|
||||||
|
this.pcConfig = {iceServers: [{urls: "stun:stun.l.google.com:19302"}]};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [info] WebSocket connection state. Values: CONNECTING, OPEN, CLOSED
|
||||||
|
* @type {number}
|
||||||
|
*/
|
||||||
|
this.wsState = WebSocket.CLOSED;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [info] WebRTC connection state.
|
||||||
|
* @type {number}
|
||||||
|
*/
|
||||||
|
this.pcState = WebSocket.CLOSED;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {HTMLVideoElement}
|
||||||
|
*/
|
||||||
|
this.video = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {WebSocket}
|
||||||
|
*/
|
||||||
|
this.ws = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {string|URL}
|
||||||
|
*/
|
||||||
|
this.wsURL = "";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {RTCPeerConnection}
|
||||||
|
*/
|
||||||
|
this.pc = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {number}
|
||||||
|
*/
|
||||||
|
this.connectTS = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
this.mseCodecs = "";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [internal] Disconnect TimeoutID.
|
||||||
|
* @type {number}
|
||||||
|
*/
|
||||||
|
this.disconnectTID = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [internal] Reconnect TimeoutID.
|
||||||
|
* @type {number}
|
||||||
|
*/
|
||||||
|
this.reconnectTID = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [internal] Handler for receiving Binary from WebSocket.
|
||||||
|
* @type {Function}
|
||||||
|
*/
|
||||||
|
this.ondata = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [internal] Handlers list for receiving JSON from WebSocket
|
||||||
|
* @type {Object.<string,Function>}}
|
||||||
|
*/
|
||||||
|
this.onmessage = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** public properties **/
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set video source (WebSocket URL). Support relative path.
|
* Set video source (WebSocket URL). Support relative path.
|
||||||
* @param value
|
* @param {string|URL} value
|
||||||
*/
|
*/
|
||||||
set src(value) {
|
set src(value) {
|
||||||
if (value.startsWith("/")) {
|
if (typeof value !== "string") value = value.toString();
|
||||||
value = "ws" + location.origin.substr(4) + value;
|
if (value.startsWith("http")) {
|
||||||
} else if (value.startsWith("http")) {
|
value = "ws" + value.substring(4);
|
||||||
value = "ws" + value.substr(4);
|
} else if (value.startsWith("/")) {
|
||||||
|
value = "ws" + location.origin.substring(4) + value;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.wsURL = value;
|
this.wsURL = value;
|
||||||
|
|
||||||
if (this.isConnected) this.connectedCallback();
|
this.onconnect();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -187,15 +155,24 @@ class VideoRTC extends HTMLElement {
|
|||||||
this.video.play().catch(er => {
|
this.video.play().catch(er => {
|
||||||
if (er.name === "NotAllowedError" && !this.video.muted) {
|
if (er.name === "NotAllowedError" && !this.video.muted) {
|
||||||
this.video.muted = true;
|
this.video.muted = true;
|
||||||
this.video.play();
|
this.video.play().catch(() => console.debug);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
get codecs() {
|
/**
|
||||||
return this.CODECS.filter(value => {
|
* Send message to server via WebSocket
|
||||||
return MediaSource.isTypeSupported(`video/mp4; codecs="${value}"`);
|
* @param {Object} value
|
||||||
}).join();
|
*/
|
||||||
|
send(value) {
|
||||||
|
if (this.ws) this.ws.send(JSON.stringify(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
codecs(type) {
|
||||||
|
const test = type === "mse"
|
||||||
|
? codec => MediaSource.isTypeSupported(`video/mp4; codecs="${codec}"`)
|
||||||
|
: codec => this.video.canPlayType(`video/mp4; codecs="${codec}"`);
|
||||||
|
return this.CODECS.filter(test).join();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -203,26 +180,23 @@ class VideoRTC extends HTMLElement {
|
|||||||
* document-connected element.
|
* document-connected element.
|
||||||
*/
|
*/
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
console.debug("this.connectedCallback", this.wsState);
|
if (this.disconnectTID) {
|
||||||
if (this.disconnectTimeout) {
|
clearTimeout(this.disconnectTID);
|
||||||
clearTimeout(this.disconnectTimeout);
|
this.disconnectTID = 0;
|
||||||
this.disconnectTimeout = 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// because video autopause on disconnected from DOM
|
// because video autopause on disconnected from DOM
|
||||||
const seek = this.video.seekable;
|
if (this.video) {
|
||||||
if (seek.length > 0) {
|
const seek = this.video.seekable;
|
||||||
this.video.currentTime = seek.end(seek.length - 1);
|
if (seek.length > 0) {
|
||||||
this.play();
|
this.video.currentTime = seek.end(seek.length - 1);
|
||||||
|
this.play();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.oninit();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.wsURL || this.wsState !== WebSocket.CLOSED) return;
|
this.onconnect();
|
||||||
|
|
||||||
// CLOSED => CONNECTING
|
|
||||||
this.wsState = WebSocket.CONNECTING;
|
|
||||||
|
|
||||||
this.internalInit();
|
|
||||||
this.internalConnect();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -230,32 +204,40 @@ class VideoRTC extends HTMLElement {
|
|||||||
* document's DOM.
|
* document's DOM.
|
||||||
*/
|
*/
|
||||||
disconnectedCallback() {
|
disconnectedCallback() {
|
||||||
console.debug("this.disconnectedCallback", this.wsState);
|
if (this.background || this.disconnectTID) return;
|
||||||
if (this.background || this.disconnectTimeout ||
|
if (this.wsState === WebSocket.CLOSED && this.pcState === WebSocket.CLOSED) return;
|
||||||
this.wsState === WebSocket.CLOSED) return;
|
|
||||||
|
|
||||||
this.disconnectTimeout = setTimeout(() => {
|
this.disconnectTID = setTimeout(() => {
|
||||||
if (this.reconnectTimeout) {
|
if (this.reconnectTID) {
|
||||||
clearTimeout(this.reconnectTimeout);
|
clearTimeout(this.reconnectTID);
|
||||||
this.reconnectTimeout = 0;
|
this.reconnectTID = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.disconnectTimeout = 0;
|
this.disconnectTID = 0;
|
||||||
// CONNECTING, OPEN => CLOSED
|
|
||||||
this.wsState = WebSocket.CLOSED;
|
|
||||||
|
|
||||||
if (this.ws) {
|
this.ondisconnect();
|
||||||
this.ws.close();
|
|
||||||
this.ws = null;
|
|
||||||
}
|
|
||||||
}, this.DISCONNECT_TIMEOUT);
|
}, this.DISCONNECT_TIMEOUT);
|
||||||
}
|
}
|
||||||
|
|
||||||
internalInit() {
|
/**
|
||||||
if (this.childElementCount) return;
|
* Creates child DOM elements. Called automatically once on `connectedCallback`.
|
||||||
|
*/
|
||||||
|
oninit() {
|
||||||
|
this.video = document.createElement("video");
|
||||||
|
this.video.controls = true;
|
||||||
|
this.video.playsInline = true;
|
||||||
|
this.video.preload = "auto";
|
||||||
|
|
||||||
this.appendChild(this.video);
|
this.appendChild(this.video);
|
||||||
|
|
||||||
|
// important for second video for mode MP4
|
||||||
|
this.style.display = "block";
|
||||||
|
this.style.position = "relative";
|
||||||
|
|
||||||
|
this.video.style.display = "block"; // fix bottom margin 4px
|
||||||
|
this.video.style.width = "100%";
|
||||||
|
this.video.style.height = "100%"
|
||||||
|
|
||||||
if (this.background) return;
|
if (this.background) return;
|
||||||
|
|
||||||
if ("hidden" in document && this.visibilityCheck) {
|
if ("hidden" in document && this.visibilityCheck) {
|
||||||
@@ -268,7 +250,7 @@ class VideoRTC extends HTMLElement {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if ("IntersectionObserver" in window && this.intersectionThreshold) {
|
if ("IntersectionObserver" in window && this.visibilityThreshold) {
|
||||||
const observer = new IntersectionObserver(entries => {
|
const observer = new IntersectionObserver(entries => {
|
||||||
entries.forEach(entry => {
|
entries.forEach(entry => {
|
||||||
if (!entry.isIntersecting) {
|
if (!entry.isIntersecting) {
|
||||||
@@ -277,98 +259,339 @@ class VideoRTC extends HTMLElement {
|
|||||||
this.connectedCallback();
|
this.connectedCallback();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, {threshold: this.intersectionThreshold});
|
}, {threshold: this.visibilityThreshold});
|
||||||
observer.observe(this);
|
observer.observe(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internalConnect() {
|
/**
|
||||||
if (this.wsState !== WebSocket.CONNECTING) return;
|
* Connect to WebSocket. Called automatically on `connectedCallback`.
|
||||||
if (this.ws) throw "connect with non null WebSocket";
|
* @return {boolean} true if the connection has started.
|
||||||
|
*/
|
||||||
|
onconnect() {
|
||||||
|
if (!this.isConnected || !this.wsURL || this.ws || this.pc) return false;
|
||||||
|
|
||||||
const ts = Date.now();
|
// CLOSED or CONNECTING => CONNECTING
|
||||||
|
this.wsState = WebSocket.CONNECTING;
|
||||||
|
|
||||||
|
this.connectTS = Date.now();
|
||||||
|
|
||||||
this.ws = new WebSocket(this.wsURL);
|
this.ws = new WebSocket(this.wsURL);
|
||||||
this.ws.binaryType = "arraybuffer";
|
this.ws.binaryType = "arraybuffer";
|
||||||
|
this.ws.addEventListener("open", ev => this.onopen(ev));
|
||||||
|
this.ws.addEventListener("close", ev => this.onclose(ev));
|
||||||
|
|
||||||
this.ws.addEventListener("open", () => {
|
return true;
|
||||||
console.debug("ws.open", this.wsState);
|
}
|
||||||
if (this.wsState !== WebSocket.CONNECTING) return;
|
|
||||||
|
|
||||||
// CONNECTING => OPEN
|
ondisconnect() {
|
||||||
this.wsState = WebSocket.OPEN;
|
this.wsState = WebSocket.CLOSED;
|
||||||
});
|
if (this.ws) {
|
||||||
this.ws.addEventListener("close", () => {
|
this.ws.close();
|
||||||
console.debug("ws.close", this.wsState);
|
|
||||||
if (this.wsState === WebSocket.CLOSED) return;
|
|
||||||
|
|
||||||
// CONNECTING, OPEN => CONNECTING
|
|
||||||
this.wsState = WebSocket.CONNECTING;
|
|
||||||
this.ws = null;
|
this.ws = null;
|
||||||
|
|
||||||
// reconnect no more than once every X seconds
|
|
||||||
const delay = Math.max(this.RECONNECT_TIMEOUT - (Date.now() - ts), 0);
|
|
||||||
|
|
||||||
this.reconnectTimeout = setTimeout(() => {
|
|
||||||
this.reconnectTimeout = 0;
|
|
||||||
this.internalConnect();
|
|
||||||
}, delay);
|
|
||||||
});
|
|
||||||
|
|
||||||
if ("MediaSource" in window && this.MSE2) {
|
|
||||||
if (MediaSource.canConstructInDedicatedWorker) {
|
|
||||||
this.internalMSE2();
|
|
||||||
} else {
|
|
||||||
this.internalMSE();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: this.internalRTC();
|
this.pcState = WebSocket.CLOSED;
|
||||||
|
if (this.pc) {
|
||||||
|
this.pc.close();
|
||||||
|
this.pc = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internalMSE() {
|
/**
|
||||||
console.debug("this.internalMSE");
|
* @returns {Array.<string>} of modes (mse, webrtc, etc.)
|
||||||
this.ws.addEventListener("open", () => {
|
*/
|
||||||
const ms = new MediaSource();
|
onopen() {
|
||||||
ms.addEventListener("sourceopen", () => {
|
// CONNECTING => OPEN
|
||||||
URL.revokeObjectURL(this.video.src);
|
this.wsState = WebSocket.OPEN;
|
||||||
this.ws.send(JSON.stringify({type: "mse", value: this.codecs}));
|
|
||||||
}, {once: true});
|
|
||||||
|
|
||||||
this.video.src = URL.createObjectURL(ms);
|
|
||||||
this.play();
|
|
||||||
|
|
||||||
this.ws.addEventListener("message", MediaSourceHandler(ms));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
internalMSE2() {
|
|
||||||
console.debug("this.internalMSE2");
|
|
||||||
const worker = new Worker("video-rtc.js");
|
|
||||||
worker.addEventListener("message", ev => {
|
|
||||||
if (ev.data.type === "handle") {
|
|
||||||
this.video.srcObject = ev.data.value;
|
|
||||||
this.play();
|
|
||||||
} else if (ev.data.type === "sourceopen") {
|
|
||||||
this.ws.send(JSON.stringify({type: "mse", value: this.codecs}));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.ws.addEventListener("message", ev => {
|
this.ws.addEventListener("message", ev => {
|
||||||
if (typeof ev.data === "string") {
|
if (typeof ev.data === "string") {
|
||||||
worker.postMessage(ev.data);
|
const msg = JSON.parse(ev.data);
|
||||||
|
for (const mode in this.onmessage) {
|
||||||
|
this.onmessage[mode](msg);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
worker.postMessage(ev.data, [ev.data]);
|
this.ondata(ev.data);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.ws.addEventListener("close", () => {
|
|
||||||
worker.terminate();
|
this.ondata = null;
|
||||||
});
|
this.onmessage = {};
|
||||||
|
|
||||||
|
const modes = [];
|
||||||
|
|
||||||
|
if (this.mode.indexOf("mse") >= 0 && "MediaSource" in window) { // iPhone
|
||||||
|
modes.push("mse");
|
||||||
|
this.onmse();
|
||||||
|
} else if (this.mode.indexOf("mp4") >= 0) {
|
||||||
|
modes.push("mp4");
|
||||||
|
this.onmp4();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.mode.indexOf("webrtc") >= 0 && "RTCPeerConnection" in window) { // macOS Desktop app
|
||||||
|
modes.push("webrtc");
|
||||||
|
this.onwebrtc();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.mode.indexOf("mjpeg") >= 0) {
|
||||||
|
if (modes.length) {
|
||||||
|
this.onmessage["mjpeg"] = msg => {
|
||||||
|
if (msg.type !== "error" || msg.value.indexOf(modes[0]) !== 0) return;
|
||||||
|
this.onmjpeg();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
modes.push("mjpeg");
|
||||||
|
this.onmjpeg();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return modes;
|
||||||
}
|
}
|
||||||
|
|
||||||
internalRTC() {
|
/**
|
||||||
if (!("RTCPeerConnection" in window)) return; // macOS Desktop app
|
* @return {boolean} true if reconnection has started.
|
||||||
|
*/
|
||||||
|
onclose() {
|
||||||
|
if (this.wsState === WebSocket.CLOSED) return false;
|
||||||
|
|
||||||
|
// CONNECTING, OPEN => CONNECTING
|
||||||
|
this.wsState = WebSocket.CONNECTING;
|
||||||
|
this.ws = null;
|
||||||
|
|
||||||
|
// reconnect no more than once every X seconds
|
||||||
|
const delay = Math.max(this.RECONNECT_TIMEOUT - (Date.now() - this.connectTS), 0);
|
||||||
|
|
||||||
|
this.reconnectTID = setTimeout(() => {
|
||||||
|
this.reconnectTID = 0;
|
||||||
|
this.onconnect();
|
||||||
|
}, delay);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
onmse() {
|
||||||
|
const ms = new MediaSource();
|
||||||
|
ms.addEventListener("sourceopen", () => {
|
||||||
|
URL.revokeObjectURL(this.video.src);
|
||||||
|
this.send({type: "mse", value: this.codecs("mse")});
|
||||||
|
}, {once: true});
|
||||||
|
|
||||||
|
this.video.src = URL.createObjectURL(ms);
|
||||||
|
this.video.srcObject = null;
|
||||||
|
this.play();
|
||||||
|
|
||||||
|
this.mseCodecs = "";
|
||||||
|
|
||||||
|
this.onmessage["mse"] = msg => {
|
||||||
|
if (msg.type !== "mse") return;
|
||||||
|
|
||||||
|
this.mseCodecs = msg.value;
|
||||||
|
|
||||||
|
const sb = ms.addSourceBuffer(msg.value);
|
||||||
|
sb.mode = "segments"; // segments or sequence
|
||||||
|
sb.addEventListener("updateend", () => {
|
||||||
|
if (sb.updating) return;
|
||||||
|
if (bufLen > 0) {
|
||||||
|
try {
|
||||||
|
sb.appendBuffer(buf.slice(0, bufLen));
|
||||||
|
} catch (e) {
|
||||||
|
// console.debug(e);
|
||||||
|
}
|
||||||
|
bufLen = 0;
|
||||||
|
} else if (sb.buffered && sb.buffered.length) {
|
||||||
|
const end = sb.buffered.end(sb.buffered.length - 1) - 5;
|
||||||
|
const start = sb.buffered.start(0);
|
||||||
|
if (end > start) {
|
||||||
|
sb.remove(start, end);
|
||||||
|
ms.setLiveSeekableRange(end, end + 5);
|
||||||
|
}
|
||||||
|
// console.debug("VideoRTC.buffered", start, end);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const buf = new Uint8Array(2 * 1024 * 1024);
|
||||||
|
let bufLen = 0;
|
||||||
|
|
||||||
|
this.ondata = data => {
|
||||||
|
if (sb.updating || bufLen > 0) {
|
||||||
|
const b = new Uint8Array(data);
|
||||||
|
buf.set(b, bufLen);
|
||||||
|
bufLen += b.byteLength;
|
||||||
|
// console.debug("VideoRTC.buffer", b.byteLength, bufLen);
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
sb.appendBuffer(data);
|
||||||
|
} catch (e) {
|
||||||
|
// console.debug(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onwebrtc() {
|
||||||
|
const pc = new RTCPeerConnection(this.pcConfig);
|
||||||
|
|
||||||
|
/** @type {HTMLVideoElement} */
|
||||||
|
const video2 = document.createElement("video");
|
||||||
|
video2.addEventListener("loadeddata", ev => this.onpcvideo(ev), {once: true});
|
||||||
|
|
||||||
|
pc.addEventListener("icecandidate", ev => {
|
||||||
|
const candidate = ev.candidate ? ev.candidate.toJSON().candidate : "";
|
||||||
|
this.send({type: "webrtc/candidate", value: candidate});
|
||||||
|
});
|
||||||
|
|
||||||
|
pc.addEventListener("track", ev => {
|
||||||
|
// when stream already init
|
||||||
|
if (video2.srcObject !== null) return;
|
||||||
|
|
||||||
|
// when audio track not exist in Chrome
|
||||||
|
if (ev.streams.length === 0) return;
|
||||||
|
|
||||||
|
// when audio track not exist in Firefox
|
||||||
|
if (ev.streams[0].id[0] === '{') return;
|
||||||
|
|
||||||
|
video2.srcObject = ev.streams[0];
|
||||||
|
});
|
||||||
|
|
||||||
|
pc.addEventListener("connectionstatechange", () => {
|
||||||
|
if (pc.connectionState === "failed" || pc.connectionState === "disconnected") {
|
||||||
|
pc.close(); // stop next events
|
||||||
|
|
||||||
|
this.pcState = WebSocket.CLOSED;
|
||||||
|
this.pc = null;
|
||||||
|
|
||||||
|
this.onconnect();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.onmessage["webrtc"] = msg => {
|
||||||
|
switch (msg.type) {
|
||||||
|
case "webrtc/candidate":
|
||||||
|
pc.addIceCandidate({
|
||||||
|
candidate: msg.value,
|
||||||
|
sdpMid: "0"
|
||||||
|
}).catch(() => console.debug);
|
||||||
|
break;
|
||||||
|
case "webrtc/answer":
|
||||||
|
pc.setRemoteDescription({
|
||||||
|
type: "answer",
|
||||||
|
sdp: msg.value
|
||||||
|
}).catch(() => console.debug);
|
||||||
|
break;
|
||||||
|
case "error":
|
||||||
|
if (msg.value.indexOf("webrtc/offer") < 0) return;
|
||||||
|
pc.close();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Safari doesn't support "offerToReceiveVideo"
|
||||||
|
pc.addTransceiver("video", {direction: "recvonly"});
|
||||||
|
pc.addTransceiver("audio", {direction: "recvonly"});
|
||||||
|
|
||||||
|
pc.createOffer().then(offer => {
|
||||||
|
pc.setLocalDescription(offer).then(() => {
|
||||||
|
this.send({type: "webrtc/offer", value: offer.sdp});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
this.pcState = WebSocket.CONNECTING;
|
||||||
|
this.pc = pc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param ev {Event}
|
||||||
|
*/
|
||||||
|
onpcvideo(ev) {
|
||||||
|
/** @type {HTMLVideoElement} */
|
||||||
|
const video2 = ev.target;
|
||||||
|
const state = this.pc.connectionState;
|
||||||
|
|
||||||
|
// Firefox doesn't support pc.connectionState
|
||||||
|
if (state === "connected" || state === "connecting" || !state) {
|
||||||
|
// Video+Audio > Video, H265 > H264, Video > Audio, WebRTC > MSE
|
||||||
|
let rtcPriority = 0, msePriority = 0;
|
||||||
|
|
||||||
|
/** @type {MediaStream} */
|
||||||
|
const ms = video2.srcObject;
|
||||||
|
if (ms.getVideoTracks().length > 0) rtcPriority += 0x220;
|
||||||
|
if (ms.getAudioTracks().length > 0) rtcPriority += 0x102;
|
||||||
|
|
||||||
|
if (this.mseCodecs.indexOf("hvc1.") >= 0) msePriority += 0x230;
|
||||||
|
if (this.mseCodecs.indexOf("avc1.") >= 0) msePriority += 0x210;
|
||||||
|
if (this.mseCodecs.indexOf("mp4a.") >= 0) msePriority += 0x101;
|
||||||
|
|
||||||
|
if (rtcPriority >= msePriority) {
|
||||||
|
this.video.srcObject = ms;
|
||||||
|
this.play();
|
||||||
|
|
||||||
|
this.pcState = WebSocket.OPEN;
|
||||||
|
|
||||||
|
this.wsState = WebSocket.CLOSED;
|
||||||
|
this.ws.close();
|
||||||
|
this.ws = null;
|
||||||
|
} else {
|
||||||
|
this.pcState = WebSocket.CLOSED;
|
||||||
|
this.pc.close();
|
||||||
|
this.pc = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
video2.srcObject = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
onmjpeg() {
|
||||||
|
this.ondata = data => {
|
||||||
|
this.video.poster = "data:image/jpeg;base64," + VideoRTC.btoa(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
this.send({type: "mjpeg"});
|
||||||
|
this.video.controls = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
onmp4() {
|
||||||
|
/** @type {HTMLVideoElement} */
|
||||||
|
let video2;
|
||||||
|
|
||||||
|
this.ondata = data => {
|
||||||
|
// first video with default position (set container size)
|
||||||
|
// second video with position=absolute and top=0px
|
||||||
|
if (video2) {
|
||||||
|
this.removeChild(this.video);
|
||||||
|
this.video.src = "";
|
||||||
|
this.video = video2;
|
||||||
|
video2.style.position = "";
|
||||||
|
video2.style.top = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
video2 = this.video.cloneNode();
|
||||||
|
video2.style.position = "absolute";
|
||||||
|
video2.style.top = "0px";
|
||||||
|
this.appendChild(video2);
|
||||||
|
|
||||||
|
video2.src = "data:video/mp4;base64," + VideoRTC.btoa(data);
|
||||||
|
video2.play().catch(() => console.log);
|
||||||
|
};
|
||||||
|
|
||||||
|
this.ws.addEventListener("close", () => {
|
||||||
|
if (!video2) return;
|
||||||
|
|
||||||
|
this.removeChild(video2);
|
||||||
|
video2.src = "";
|
||||||
|
});
|
||||||
|
|
||||||
|
this.send({type: "mp4", value: this.codecs("mp4")});
|
||||||
|
this.video.controls = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static btoa(buffer) {
|
||||||
|
const bytes = new Uint8Array(buffer);
|
||||||
|
const len = bytes.byteLength;
|
||||||
|
let binary = "";
|
||||||
|
for (let i = 0; i < len; i++) {
|
||||||
|
binary += String.fromCharCode(bytes[i]);
|
||||||
|
}
|
||||||
|
return window.btoa(binary);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
customElements.define("video-rtc", VideoRTC);
|
|
||||||
|
85
www/video-stream.js
Normal file
85
www/video-stream.js
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import {VideoRTC} from "./video-rtc.js";
|
||||||
|
|
||||||
|
class VideoStream extends VideoRTC {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
|
||||||
|
/** @type {HTMLDivElement} */
|
||||||
|
this.divMode = null;
|
||||||
|
/** @type {HTMLDivElement} */
|
||||||
|
this.divStatus = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom GUI
|
||||||
|
*/
|
||||||
|
oninit() {
|
||||||
|
super.oninit();
|
||||||
|
|
||||||
|
this.innerHTML = `
|
||||||
|
<style>
|
||||||
|
.info {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
padding: 12px;
|
||||||
|
color: white;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<div class="info">
|
||||||
|
<div class="status"></div>
|
||||||
|
<div class="mode"></div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
this.divStatus = this.querySelector(".status");
|
||||||
|
this.divMode = this.querySelector(".mode");
|
||||||
|
|
||||||
|
const info = this.querySelector(".info")
|
||||||
|
this.insertBefore(this.video, info);
|
||||||
|
}
|
||||||
|
|
||||||
|
onconnect() {
|
||||||
|
const result = super.onconnect();
|
||||||
|
if (result) {
|
||||||
|
this.divMode.innerText = "loading";
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
onopen() {
|
||||||
|
const result = super.onopen();
|
||||||
|
|
||||||
|
this.onmessage["stream"] = msg => {
|
||||||
|
switch (msg.type) {
|
||||||
|
case "error":
|
||||||
|
this.divMode.innerText = "error";
|
||||||
|
this.divStatus.innerText = msg.value;
|
||||||
|
break;
|
||||||
|
case "mse":
|
||||||
|
case "mp4":
|
||||||
|
case "mjpeg":
|
||||||
|
this.divMode.innerText = msg.type.toUpperCase();
|
||||||
|
this.divStatus.innerText = "";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
onpcvideo(ev) {
|
||||||
|
super.onpcvideo(ev);
|
||||||
|
|
||||||
|
if (this.pcState !== WebSocket.CLOSED) {
|
||||||
|
this.divMode.innerText = "RTC";
|
||||||
|
this.divStatus.innerText = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define("video-stream", VideoStream);
|
@@ -1,53 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
<title>go2rtc - WebRTC</title>
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
html, body {
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
#video {
|
|
||||||
/* video "container" size */
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background: black;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<video id="video" autoplay controls playsinline muted></video>
|
|
||||||
<!--<video id="video" preload="auto" controls playsinline muted></video>-->
|
|
||||||
<script>
|
|
||||||
const baseUrl = location.origin + location.pathname.substr(
|
|
||||||
0, location.pathname.lastIndexOf("/")
|
|
||||||
);
|
|
||||||
const video = document.getElementById('video');
|
|
||||||
|
|
||||||
video.oncanplay = ev => console.log(ev.type, ev);
|
|
||||||
video.onplaying = ev => console.log(ev.type, ev);
|
|
||||||
video.onwaiting = ev => console.log(ev.type, ev);
|
|
||||||
video.onseeking = ev => console.log(ev.type, ev);
|
|
||||||
video.onloadeddata = ev => console.log(ev.type, ev);
|
|
||||||
video.oncanplaythrough = ev => console.log(ev.type, ev);
|
|
||||||
// video.ondurationchange = ev => console.log(ev.type, ev);
|
|
||||||
// video.ontimeupdate = ev => console.log(ev.type, ev);
|
|
||||||
video.onplay = ev => console.log(ev.type, ev);
|
|
||||||
video.onpause = ev => console.log(ev.type, ev);
|
|
||||||
video.onsuspended = ev => console.log(ev.type, ev);
|
|
||||||
video.onemptied = ev => console.log(ev.type, ev);
|
|
||||||
video.onstalled = ev => console.log(ev.type, ev);
|
|
||||||
|
|
||||||
console.log("start");
|
|
||||||
|
|
||||||
video.src = baseUrl + "/api/stream.mp4" + location.search;
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@@ -48,7 +48,7 @@
|
|||||||
console.debug('ws.onmessage', msg);
|
console.debug('ws.onmessage', msg);
|
||||||
|
|
||||||
if (msg.type === 'webrtc/candidate') {
|
if (msg.type === 'webrtc/candidate') {
|
||||||
pc.addIceCandidate({candidate: msg.value, sdpMid: ''});
|
pc.addIceCandidate({candidate: msg.value, sdpMid: '0'});
|
||||||
} else if (msg.type === 'webrtc/answer') {
|
} else if (msg.type === 'webrtc/answer') {
|
||||||
pc.setRemoteDescription({type: 'answer', sdp: msg.value});
|
pc.setRemoteDescription({type: 'answer', sdp: msg.value});
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user