mirror of
https://github.com/flavioribeiro/donut.git
synced 2025-10-05 23:16:53 +08:00
Merge pull request #25 from leandromoreira/main
Large refactoring + cancel when either SRT or WebRTC fails/disconnects + ease of use
This commit is contained in:
19
Dockerfile
19
Dockerfile
@@ -1,7 +1,9 @@
|
|||||||
FROM ubuntu:20.04 AS builder
|
FROM golang:1.19
|
||||||
|
|
||||||
|
ENV WD=/usr/src/app
|
||||||
ENV SRT_VERSION="v1.5.3"
|
ENV SRT_VERSION="v1.5.3"
|
||||||
ENV SRT_FOLDER="/opt/srt_lib"
|
ENV SRT_FOLDER="/opt/srt_lib"
|
||||||
|
WORKDIR ${WD}
|
||||||
|
|
||||||
RUN apt-get clean && apt-get update && \
|
RUN apt-get clean && apt-get update && \
|
||||||
DEBIAN_FRONTEND=noninteractive apt-get install -y \
|
DEBIAN_FRONTEND=noninteractive apt-get install -y \
|
||||||
@@ -12,25 +14,18 @@ RUN \
|
|||||||
mkdir -p "${SRT_FOLDER}" && \
|
mkdir -p "${SRT_FOLDER}" && \
|
||||||
git clone --depth 1 --branch "${SRT_VERSION}" https://github.com/Haivision/srt && \
|
git clone --depth 1 --branch "${SRT_VERSION}" https://github.com/Haivision/srt && \
|
||||||
cd srt && \
|
cd srt && \
|
||||||
./configure --prefix=. $(configure) && \
|
./configure --prefix=${SRT_FOLDER} $(configure) && \
|
||||||
make && \
|
make && \
|
||||||
make install
|
make install
|
||||||
|
|
||||||
FROM golang:1.19
|
|
||||||
ENV WD=/usr/src/app
|
|
||||||
WORKDIR ${WD}
|
|
||||||
|
|
||||||
RUN mkdir srt-lib
|
|
||||||
COPY --from=builder /srt /opt/srt
|
|
||||||
|
|
||||||
# To find where the srt.h and libsrt.so were you can
|
# To find where the srt.h and libsrt.so were you can
|
||||||
# find / -name srt.h
|
# find / -name srt.h
|
||||||
# find / -name libsrt.so
|
# find / -name libsrt.so
|
||||||
# inside the container docker run -it --rm -t <TAG_YOU_BUILT> bash
|
# inside the container docker run -it --rm -t <TAG_YOU_BUILT> bash
|
||||||
ENV GOPROXY=direct
|
ENV GOPROXY=direct
|
||||||
ENV LD_LIBRARY_PATH="$LD_LIBRARY_PATH:/opt/srt/lib/"
|
ENV LD_LIBRARY_PATH="$LD_LIBRARY_PATH:${SRT_FOLDER}/lib/"
|
||||||
ENV CGO_CFLAGS="-I/opt/srt/include/"
|
ENV CGO_CFLAGS="-I${SRT_FOLDER}/include/"
|
||||||
ENV CGO_LDFLAGS="-L/opt/srt/lib/"
|
ENV CGO_LDFLAGS="-L${SRT_FOLDER}/lib/"
|
||||||
|
|
||||||
COPY . ./donut
|
COPY . ./donut
|
||||||
WORKDIR ${WD}/donut
|
WORKDIR ${WD}/donut
|
||||||
|
22
Dockerfile-srt-live
Normal file
22
Dockerfile-srt-live
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
FROM golang:1.19
|
||||||
|
|
||||||
|
ENV WD=/usr/src/app
|
||||||
|
ENV SRT_VERSION="v1.5.3"
|
||||||
|
ENV SRT_FOLDER="/opt/srt_lib"
|
||||||
|
WORKDIR ${WD}
|
||||||
|
|
||||||
|
RUN apt-get clean && apt-get update && \
|
||||||
|
DEBIAN_FRONTEND=noninteractive apt-get install -y \
|
||||||
|
tclsh pkg-config cmake libssl-dev build-essential git \
|
||||||
|
&& apt-get clean
|
||||||
|
|
||||||
|
RUN \
|
||||||
|
mkdir -p "${SRT_FOLDER}" && \
|
||||||
|
git clone --depth 1 --branch "${SRT_VERSION}" https://github.com/Haivision/srt && \
|
||||||
|
cd srt && \
|
||||||
|
./configure --prefix=${SRT_FOLDER} $(configure) && \
|
||||||
|
make && \
|
||||||
|
make install
|
||||||
|
|
||||||
|
ENV PATH="${PATH}:/usr/src/app/srt"
|
||||||
|
WORKDIR ${WD}
|
58
FAQ.md
Normal file
58
FAQ.md
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
# FAQ
|
||||||
|
|
||||||
|
## I can't connect two tabs or browser at the same for the SRT
|
||||||
|
|
||||||
|
It doesn't work When I try to connect in another browser or tab, or even when I try to refresh the current page. It raises an seemingly timeout error.
|
||||||
|
|
||||||
|
```
|
||||||
|
astisrt: connecting failed: astisrt: connecting failed: astisrt: Connection setup failure: connection timed out
|
||||||
|
```
|
||||||
|
|
||||||
|
Apparently both `ffmpeg` and `srt-live-transmit` won't allow multiple persistent connections.
|
||||||
|
|
||||||
|
ref1 https://github.com/Haivision/srt/blob/master/docs/apps/srt-live-transmit.md#medium-srt
|
||||||
|
ref2 https://github.com/asticode/go-astisrt/issues/6#issuecomment-1917076767
|
||||||
|
|
||||||
|
## It's not working on Firefox/Chrome/Edge.
|
||||||
|
|
||||||
|
[WebRTC establishes a baseline set of codecs which all compliant browsers are required to support. Some browsers may choose to allow other codecs as well.](https://developer.mozilla.org/en-US/docs/Web/Media/Formats/WebRTC_codecs#supported_video_codecs)
|
||||||
|
|
||||||
|
You might also want to check the general [support for codecs by containers](https://en.wikipedia.org/wiki/Comparison_of_video_container_formats).
|
||||||
|
|
||||||
|
## If you're facing issues while trying to run or compile it locally, such as:
|
||||||
|
|
||||||
|
```
|
||||||
|
mod/github.com/asticode/go-astisrt@v0.3.0/pkg/callbacks.go:4:11: fatal error: 'srt/srt.h' file not found
|
||||||
|
#include <srt/srt.h>
|
||||||
|
^~~~~~~~~~~
|
||||||
|
1 error generated.
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
./main.go:117:2: undefined: setCors
|
||||||
|
./main.go:135:3: undefined: errorToHTTP
|
||||||
|
./main.go:147:3: undefined: errorToHTTP
|
||||||
|
./main.go:154:3: undefined: errorToHTTP
|
||||||
|
./main.go:158:3: undefined: errorToHTTP
|
||||||
|
./main.go:165:3: undefined: errorToHTTP
|
||||||
|
./main.go:174:18: undefined: assertSignalingCorrect
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
/opt/homebrew/Cellar/go/1.21.6/libexec/pkg/tool/darwin_arm64/link: running cc failed: exit status 1
|
||||||
|
ld: warning: ignoring duplicate libraries: '-lsrt'
|
||||||
|
ld: library 'srt' not found
|
||||||
|
clang: error: linker command failed with exit code 1 (use -v to see invocation)
|
||||||
|
```
|
||||||
|
|
||||||
|
You can try to use the [docker-compose](/README.md#run-using-docker-compose), but if you want to run it locally you must provide path to the linker.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Find where the headers and libraries files are located at. If you can't find, install them with brew or apt-get.
|
||||||
|
# Feel free to replace the path after the find to roo[/] or any other suspicious place.
|
||||||
|
sudo find /opt/homebrew/ -name srt.h
|
||||||
|
sudo find /opt/ -name libsrt.a # libsrt.so for linux
|
||||||
|
|
||||||
|
# Add the required flags so the compiler/linker can find the needed files.
|
||||||
|
CGO_LDFLAGS="-L/opt/homebrew/Cellar/srt/1.5.3/lib -lsrt" CGO_CFLAGS="-I/opt/homebrew//Cellar/srt/1.5.3/include/" go run main.go helpers.go
|
||||||
|
```
|
70
README.md
70
README.md
@@ -13,22 +13,9 @@ $ go install github.com/flavioribeiro/donut@latest
|
|||||||
```
|
```
|
||||||
Once installed, execute `donut`. This will be in your `$GOPATH/bin`. The default will be `~/go/bin/donut`
|
Once installed, execute `donut`. This will be in your `$GOPATH/bin`. The default will be `~/go/bin/donut`
|
||||||
|
|
||||||
|
|
||||||
### Install & Run using Docker
|
|
||||||
|
|
||||||
Alternatively, you can build a docker image. Docker will take care of downloading the dependencies (including the libsrt) and compiling donut for you.
|
|
||||||
|
|
||||||
```
|
|
||||||
$ docker build -t donut .
|
|
||||||
$ docker run -it -p 8080:8080 donut
|
|
||||||
```
|
|
||||||
|
|
||||||
### Open the Web UI
|
|
||||||
Open [http://localhost:8080](http://localhost:8080). You will see three text boxes. Fill in your details for your SRT listener configuration and hit connect.
|
|
||||||
|
|
||||||
### Run using docker-compose
|
### Run using docker-compose
|
||||||
|
|
||||||
Docker-compose can simulate an SRT live transmission and run the donut in separate containers.
|
Alternatively, you can use `docker-compose` to simulate an SRT live transmission and run the donut effortless.
|
||||||
|
|
||||||
```
|
```
|
||||||
$ make run
|
$ make run
|
||||||
@@ -39,57 +26,10 @@ Open [http://localhost:8080](http://localhost:8080). You will see three text box
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
### How it works
|
||||||
|
|
||||||
### Troubleshooting locally
|
Please check the [How it works](/HOW_IT_WORKS.md) section.
|
||||||
|
|
||||||
#### Mac
|
### FAQ
|
||||||
|
|
||||||
If you're facing issues while trying to run it locally, such as:
|
Please check the [FAQ](/FAQ.md) if you're facing any trouble.
|
||||||
|
|
||||||
```
|
|
||||||
mod/github.com/asticode/go-astisrt@v0.3.0/pkg/callbacks.go:4:11: fatal error: 'srt/srt.h' file not found
|
|
||||||
#include <srt/srt.h>
|
|
||||||
^~~~~~~~~~~
|
|
||||||
1 error generated.
|
|
||||||
```
|
|
||||||
|
|
||||||
```
|
|
||||||
./main.go:117:2: undefined: setCors
|
|
||||||
./main.go:135:3: undefined: errorToHTTP
|
|
||||||
./main.go:147:3: undefined: errorToHTTP
|
|
||||||
./main.go:154:3: undefined: errorToHTTP
|
|
||||||
./main.go:158:3: undefined: errorToHTTP
|
|
||||||
./main.go:165:3: undefined: errorToHTTP
|
|
||||||
./main.go:174:18: undefined: assertSignalingCorrect
|
|
||||||
```
|
|
||||||
|
|
||||||
```
|
|
||||||
/opt/homebrew/Cellar/go/1.21.6/libexec/pkg/tool/darwin_arm64/link: running cc failed: exit status 1
|
|
||||||
ld: warning: ignoring duplicate libraries: '-lsrt'
|
|
||||||
ld: library 'srt' not found
|
|
||||||
clang: error: linker command failed with exit code 1 (use -v to see invocation)
|
|
||||||
```
|
|
||||||
|
|
||||||
You can try to use the [docker-compose](#run-using-docker-compose), but if you want to run it locally you might must provide path to the linker.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
|
|
||||||
# Find where the headers and libraries files are located at. If you can't find, install them with brew.
|
|
||||||
sudo find /opt/homebrew/ -name srt.h
|
|
||||||
sudo find /opt/ -name libsrt.a
|
|
||||||
|
|
||||||
# Add the required flags so the compiler/linker can find the needed files.
|
|
||||||
CGO_LDFLAGS="-L/opt/homebrew/Cellar/srt/1.5.3/lib -lsrt" CGO_CFLAGS="-I/opt/homebrew//Cellar/srt/1.5.3/include/" go run main.go helpers.go
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
You can run ffmpeg locally to simulate an SRT live transmission:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
ffmpeg -hide_banner -loglevel verbose \
|
|
||||||
-re -f lavfi -i "testsrc2=size=1280x720:rate=30,format=yuv420p" \
|
|
||||||
-f lavfi -i "sine=frequency=1000:sample_rate=44100" \
|
|
||||||
-c:v libx264 -preset veryfast -tune zerolatency -profile:v baseline \
|
|
||||||
-b:v 1400k -bufsize 2800k -x264opts keyint=30:min-keyint=30:scenecut=-1 \
|
|
||||||
-c:a aac -b:a 128k -f mpegts 'srt://0.0.0.0:40052?mode=listener&latency=400000'
|
|
||||||
```
|
|
@@ -12,16 +12,19 @@ services:
|
|||||||
- "8081:8081"
|
- "8081:8081"
|
||||||
- "8081:8081/udp"
|
- "8081:8081/udp"
|
||||||
|
|
||||||
origin: # simulating an srt origin live transmission
|
srt:
|
||||||
image: jrottenberg/ffmpeg:4.4-alpine
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile-srt-live
|
||||||
entrypoint: sh
|
entrypoint: sh
|
||||||
command: "/scripts/ffmpeg_srt_live_listener.sh"
|
command: "./srt.sh"
|
||||||
|
working_dir: "/scripts"
|
||||||
volumes:
|
volumes:
|
||||||
- "./scripts:/scripts"
|
- "./scripts:/scripts"
|
||||||
environment:
|
environment:
|
||||||
- SRT_LISTENING_PORT=40052
|
- SRT_LISTENING_PORT=40052
|
||||||
- SRT_LISTENING_HOST=0.0.0.0
|
- SRT_UDP_TS_INPUT_HOST=0.0.0.0
|
||||||
- SRT_LISTENING_LATENCY_US=400000
|
- SRT_UDP_TS_INPUT_PORT=1234
|
||||||
ports:
|
ports:
|
||||||
- "40052:40052/udp"
|
- "40052:40052/udp"
|
||||||
depends_on:
|
depends_on:
|
||||||
@@ -29,3 +32,18 @@ services:
|
|||||||
links:
|
links:
|
||||||
- app
|
- app
|
||||||
|
|
||||||
|
origin: # simulating an mpeg-ts upd origin live transmission
|
||||||
|
image: jrottenberg/ffmpeg:4.4-alpine
|
||||||
|
entrypoint: sh
|
||||||
|
command: "/scripts/ffmpeg_mpegts_udp.sh"
|
||||||
|
volumes:
|
||||||
|
- "./scripts:/scripts"
|
||||||
|
environment:
|
||||||
|
- SRT_INPUT_HOST=srt
|
||||||
|
- SRT_INPUT_PORT=1234
|
||||||
|
- PKT_SIZE=1316
|
||||||
|
depends_on:
|
||||||
|
- srt
|
||||||
|
links:
|
||||||
|
- srt
|
||||||
|
|
||||||
|
6
go.mod
6
go.mod
@@ -5,8 +5,10 @@ go 1.19
|
|||||||
require (
|
require (
|
||||||
github.com/asticode/go-astisrt v0.3.0
|
github.com/asticode/go-astisrt v0.3.0
|
||||||
github.com/asticode/go-astits v1.11.0
|
github.com/asticode/go-astits v1.11.0
|
||||||
|
github.com/kelseyhightower/envconfig v1.4.0
|
||||||
github.com/pion/webrtc/v3 v3.1.47
|
github.com/pion/webrtc/v3 v3.1.47
|
||||||
github.com/szatmary/gocaption v0.0.0-20220607192049-fdd59655f0c3
|
github.com/szatmary/gocaption v0.0.0-20220607192049-fdd59655f0c3
|
||||||
|
go.uber.org/fx v1.20.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@@ -28,6 +30,10 @@ require (
|
|||||||
github.com/pion/transport v0.13.1 // indirect
|
github.com/pion/transport v0.13.1 // indirect
|
||||||
github.com/pion/turn/v2 v2.0.8 // indirect
|
github.com/pion/turn/v2 v2.0.8 // indirect
|
||||||
github.com/pion/udp v0.1.1 // indirect
|
github.com/pion/udp v0.1.1 // indirect
|
||||||
|
go.uber.org/atomic v1.7.0 // indirect
|
||||||
|
go.uber.org/dig v1.17.0 // indirect
|
||||||
|
go.uber.org/multierr v1.6.0 // indirect
|
||||||
|
go.uber.org/zap v1.23.0 // indirect
|
||||||
golang.org/x/crypto v0.2.0 // indirect
|
golang.org/x/crypto v0.2.0 // indirect
|
||||||
golang.org/x/net v0.2.0 // indirect
|
golang.org/x/net v0.2.0 // indirect
|
||||||
golang.org/x/sys v0.2.0 // indirect
|
golang.org/x/sys v0.2.0 // indirect
|
||||||
|
24
go.sum
24
go.sum
@@ -1,13 +1,11 @@
|
|||||||
github.com/asticode/go-astikit v0.30.0 h1:DkBkRQRIxYcknlaU7W7ksNfn4gMFsB0tqMJflxkRsZA=
|
|
||||||
github.com/asticode/go-astikit v0.30.0/go.mod h1:h4ly7idim1tNhaVkdVBeXQZEE3L0xblP7fCWbgwipF0=
|
github.com/asticode/go-astikit v0.30.0/go.mod h1:h4ly7idim1tNhaVkdVBeXQZEE3L0xblP7fCWbgwipF0=
|
||||||
github.com/asticode/go-astikit v0.36.0 h1:WHSY88YT76D/XRbdp0lMLwfjyUGw8dygnbKKtbGNIG8=
|
github.com/asticode/go-astikit v0.36.0 h1:WHSY88YT76D/XRbdp0lMLwfjyUGw8dygnbKKtbGNIG8=
|
||||||
github.com/asticode/go-astikit v0.36.0/go.mod h1:h4ly7idim1tNhaVkdVBeXQZEE3L0xblP7fCWbgwipF0=
|
github.com/asticode/go-astikit v0.36.0/go.mod h1:h4ly7idim1tNhaVkdVBeXQZEE3L0xblP7fCWbgwipF0=
|
||||||
github.com/asticode/go-astisrt v0.2.0 h1:pfaSfW2BfW2O2NFtzlktzIf5gHc4Pue1q1scTuj7uhc=
|
|
||||||
github.com/asticode/go-astisrt v0.2.0/go.mod h1:tP5Dx+MXyaICUeF0gz4nwyav3RDI609e0en3QQkrxKE=
|
|
||||||
github.com/asticode/go-astisrt v0.3.0 h1:LpvqOc17qfMr2suLZPzMs9wYLozxXYu/PE9CA1tH88c=
|
github.com/asticode/go-astisrt v0.3.0 h1:LpvqOc17qfMr2suLZPzMs9wYLozxXYu/PE9CA1tH88c=
|
||||||
github.com/asticode/go-astisrt v0.3.0/go.mod h1:tP5Dx+MXyaICUeF0gz4nwyav3RDI609e0en3QQkrxKE=
|
github.com/asticode/go-astisrt v0.3.0/go.mod h1:tP5Dx+MXyaICUeF0gz4nwyav3RDI609e0en3QQkrxKE=
|
||||||
github.com/asticode/go-astits v1.11.0 h1:GTHUXht0ZXAJXsVbsLIcyfHr1Bchi4QQwMARw2ZWAng=
|
github.com/asticode/go-astits v1.11.0 h1:GTHUXht0ZXAJXsVbsLIcyfHr1Bchi4QQwMARw2ZWAng=
|
||||||
github.com/asticode/go-astits v1.11.0/go.mod h1:QSHmknZ51pf6KJdHKZHJTLlMegIrhega3LPWz3ND/iI=
|
github.com/asticode/go-astits v1.11.0/go.mod h1:QSHmknZ51pf6KJdHKZHJTLlMegIrhega3LPWz3ND/iI=
|
||||||
|
github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A=
|
||||||
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=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
@@ -30,6 +28,8 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
|||||||
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/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||||
|
github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8=
|
||||||
|
github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg=
|
||||||
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/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/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
@@ -48,7 +48,6 @@ 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/v2 v2.2.11 h1:wiAy7TSrVZ4KdyjC0CcNTkwltz9ywetbe4wbHLKUbIg=
|
github.com/pion/ice/v2 v2.2.11 h1:wiAy7TSrVZ4KdyjC0CcNTkwltz9ywetbe4wbHLKUbIg=
|
||||||
github.com/pion/ice/v2 v2.2.11/go.mod h1:NqUDUao6SjSs1+4jrqpexDmFlptlVhGxQjcymXLaVvE=
|
github.com/pion/ice/v2 v2.2.11/go.mod h1:NqUDUao6SjSs1+4jrqpexDmFlptlVhGxQjcymXLaVvE=
|
||||||
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/interceptor v0.1.12 h1:CslaNriCFUItiXS5o+hh5lpL0t0ytQkFnUcbbCs2Zq8=
|
github.com/pion/interceptor v0.1.12 h1:CslaNriCFUItiXS5o+hh5lpL0t0ytQkFnUcbbCs2Zq8=
|
||||||
github.com/pion/interceptor v0.1.12/go.mod h1:bDtgAD9dRkBZpWHGKaoKb42FhDHTG2rX8Ii9LRALLVA=
|
github.com/pion/interceptor v0.1.12/go.mod h1:bDtgAD9dRkBZpWHGKaoKb42FhDHTG2rX8Ii9LRALLVA=
|
||||||
@@ -64,7 +63,6 @@ github.com/pion/rtcp v1.2.10/go.mod h1:ztfEwXZNLGyF1oQDttz/ZKIBaeeg/oWbRYqzBM9TL
|
|||||||
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.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/go.mod h1:xFe9cLMZ5Vj6eOzpyiKjT9SwGM4KpK/8Jbw5//jc+0s=
|
github.com/pion/sctp v1.8.2/go.mod h1:xFe9cLMZ5Vj6eOzpyiKjT9SwGM4KpK/8Jbw5//jc+0s=
|
||||||
github.com/pion/sctp v1.8.3 h1:LWcciN2ptLkw9Ugp/Ks2E76fiWy7yk3Wm79D6oFbFNo=
|
github.com/pion/sctp v1.8.3 h1:LWcciN2ptLkw9Ugp/Ks2E76fiWy7yk3Wm79D6oFbFNo=
|
||||||
github.com/pion/sctp v1.8.3/go.mod h1:OHbDjdk7kg+L+7TJim9q/qGVefdEJohuA2SZyihccgI=
|
github.com/pion/sctp v1.8.3/go.mod h1:OHbDjdk7kg+L+7TJim9q/qGVefdEJohuA2SZyihccgI=
|
||||||
@@ -85,11 +83,13 @@ 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/v3 v3.1.47 h1:2dFEKRI1rzFvehXDq43hK9OGGyTGJSusUi3j6QKHC5s=
|
github.com/pion/webrtc/v3 v3.1.47 h1:2dFEKRI1rzFvehXDq43hK9OGGyTGJSusUi3j6QKHC5s=
|
||||||
github.com/pion/webrtc/v3 v3.1.47/go.mod h1:8U39MYZCLVV4sIBn01htASVNkWQN2zDa/rx5xisEXWs=
|
github.com/pion/webrtc/v3 v3.1.47/go.mod h1:8U39MYZCLVV4sIBn01htASVNkWQN2zDa/rx5xisEXWs=
|
||||||
|
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||||
github.com/pkg/profile v1.4.0/go.mod h1:NWz/XGvpEW1FyYQ7fCx4dqYBLlfTcE+A9FLAkNKqjFE=
|
github.com/pkg/profile v1.4.0/go.mod h1:NWz/XGvpEW1FyYQ7fCx4dqYBLlfTcE+A9FLAkNKqjFE=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
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=
|
||||||
@@ -99,11 +99,21 @@ github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PK
|
|||||||
github.com/szatmary/gocaption v0.0.0-20220607192049-fdd59655f0c3 h1:j8SVIV6YZreqjOPGjxM48tB4XgS8oUZdgy0cyN7YrBg=
|
github.com/szatmary/gocaption v0.0.0-20220607192049-fdd59655f0c3 h1:j8SVIV6YZreqjOPGjxM48tB4XgS8oUZdgy0cyN7YrBg=
|
||||||
github.com/szatmary/gocaption v0.0.0-20220607192049-fdd59655f0c3/go.mod h1:l9r7RYKHGLuHbXpKJhJgASvi8xT+Uqxnz9B26uVU73c=
|
github.com/szatmary/gocaption v0.0.0-20220607192049-fdd59655f0c3/go.mod h1:l9r7RYKHGLuHbXpKJhJgASvi8xT+Uqxnz9B26uVU73c=
|
||||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
|
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
|
||||||
|
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||||
|
go.uber.org/dig v1.17.0 h1:5Chju+tUvcC+N7N6EV08BJz41UZuO3BmHcN4A287ZLI=
|
||||||
|
go.uber.org/dig v1.17.0/go.mod h1:rTxpf7l5I0eBTlE6/9RL+lDybC7WFwY2QH55ZSjy1mU=
|
||||||
|
go.uber.org/fx v1.20.1 h1:zVwVQGS8zYvhh9Xxcu4w1M6ESyeMzebzj2NbSayZ4Mk=
|
||||||
|
go.uber.org/fx v1.20.1/go.mod h1:iSYNbHf2y55acNCwCXKx7LbWb5WG1Bnue5RDXz1OREg=
|
||||||
|
go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI=
|
||||||
|
go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4=
|
||||||
|
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
|
||||||
|
go.uber.org/zap v1.23.0 h1:OjGQ5KQDEUawVHxNwQgPpiypGHOxo2mNZsOqTak4fFY=
|
||||||
|
go.uber.org/zap v1.23.0/go.mod h1:D+nX8jyLsMHMYrln8A0rJjFt/T/9/bGgIhAqxv5URuY=
|
||||||
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-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-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-20221010152910-d6f0a8c073c2 h1:x8vtB3zMecnlqZIwJNUUpwYKYSqCz5jXbiyv0ZJJZeI=
|
|
||||||
golang.org/x/crypto v0.0.0-20221010152910-d6f0a8c073c2/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
golang.org/x/crypto v0.0.0-20221010152910-d6f0a8c073c2/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
golang.org/x/crypto v0.2.0 h1:BRXPfhNivWL5Yq0BGQ39a2sW6t44aODpfxkWjYdzewE=
|
golang.org/x/crypto v0.2.0 h1:BRXPfhNivWL5Yq0BGQ39a2sW6t44aODpfxkWjYdzewE=
|
||||||
golang.org/x/crypto v0.2.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
golang.org/x/crypto v0.2.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
||||||
@@ -121,7 +131,6 @@ golang.org/x/net v0.0.0-20211201190559-0a0e4e1bb54c/go.mod h1:9nx3DQGgdP8bBQD5qx
|
|||||||
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||||
golang.org/x/net v0.0.0-20220531201128-c960675eff93/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
golang.org/x/net v0.0.0-20220531201128-c960675eff93/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
golang.org/x/net v0.0.0-20221002022538-bcab6841153b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
|
golang.org/x/net v0.0.0-20221002022538-bcab6841153b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
|
||||||
golang.org/x/net v0.0.0-20221004154528-8021a29435af h1:wv66FM3rLZGPdxpYL+ApnDe2HzHcTFta3z5nsc13wI4=
|
|
||||||
golang.org/x/net v0.0.0-20221004154528-8021a29435af/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
|
golang.org/x/net v0.0.0-20221004154528-8021a29435af/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
|
||||||
golang.org/x/net v0.2.0 h1:sZfSu1wtKLGlWI4ZZayP0ck9Y73K1ynO6gqzTdBVdPU=
|
golang.org/x/net v0.2.0 h1:sZfSu1wtKLGlWI4ZZayP0ck9Y73K1ynO6gqzTdBVdPU=
|
||||||
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
||||||
@@ -145,7 +154,6 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||||||
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/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-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20221010170243-090e33056c14 h1:k5II8e6QD8mITdi+okbbmR/cIyEbeXLBhy5Ha4nevyc=
|
|
||||||
golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A=
|
golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A=
|
||||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
114
h264/h264.go
114
h264/h264.go
@@ -1,114 +0,0 @@
|
|||||||
package h264
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
)
|
|
||||||
|
|
||||||
func ParseNALUs(data []byte) (NALUs, error) {
|
|
||||||
var nalus NALUs
|
|
||||||
|
|
||||||
rawNALUs := bytes.Split(data, []byte{0x00, 0x00, 0x01})
|
|
||||||
|
|
||||||
for _, rawNALU := range rawNALUs[1:] {
|
|
||||||
nal, err := ParseNAL(rawNALU)
|
|
||||||
if err != nil {
|
|
||||||
return NALUs{}, err
|
|
||||||
|
|
||||||
}
|
|
||||||
nalus.Units = append(nalus.Units, nal)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nalus, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func ParseNAL(data []byte) (NAL, error) {
|
|
||||||
index := 0
|
|
||||||
n := NAL{}
|
|
||||||
if data[index]>>7&0x01 != 0 {
|
|
||||||
return NAL{}, fmt.Errorf("forbidden_zero_bit is not 0")
|
|
||||||
}
|
|
||||||
n.RefIDC = (data[index] >> 5) & 0x03
|
|
||||||
n.UnitType = NALUnitType(data[index] & 0x1f)
|
|
||||||
numBytesInRBSP := 0
|
|
||||||
nalUnitHeaderBytes := 1
|
|
||||||
n.HeaderBytes = data[:nalUnitHeaderBytes]
|
|
||||||
|
|
||||||
index += nalUnitHeaderBytes
|
|
||||||
|
|
||||||
n.RBSPByte = make([]byte, 0, 16)
|
|
||||||
i := 0
|
|
||||||
for i = index; i < len(data); i++ {
|
|
||||||
if (i+2) < len(data) && (data[i] == 0x00 && data[i+1] == 0x00 && data[i+2] == 0x03) {
|
|
||||||
n.RBSPByte = append(n.RBSPByte, data[i], data[i+1])
|
|
||||||
i += 2
|
|
||||||
numBytesInRBSP += 2
|
|
||||||
// 0x03
|
|
||||||
} else {
|
|
||||||
n.RBSPByte = append(n.RBSPByte, data[i])
|
|
||||||
numBytesInRBSP++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
index += numBytesInRBSP
|
|
||||||
|
|
||||||
n.ParseRBSP()
|
|
||||||
|
|
||||||
return n, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *NAL) ParseRBSP() error {
|
|
||||||
switch n.UnitType {
|
|
||||||
case SupplementalEnhancementInformation:
|
|
||||||
err := n.parseSEI()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *NAL) parseSEI() error {
|
|
||||||
numBits := 0
|
|
||||||
byteOffset := 0
|
|
||||||
n.SEI.PayloadType = 0
|
|
||||||
n.SEI.PayloadSize = 0
|
|
||||||
nextBits := n.RBSPByte[byteOffset]
|
|
||||||
|
|
||||||
for {
|
|
||||||
if nextBits == 0xff {
|
|
||||||
n.PayloadType += 255
|
|
||||||
numBits += 8
|
|
||||||
byteOffset += numBits / 8
|
|
||||||
numBits = numBits % 8
|
|
||||||
nextBits = n.RBSPByte[byteOffset]
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
n.PayloadType += int(nextBits)
|
|
||||||
numBits += 8
|
|
||||||
byteOffset += numBits / 8
|
|
||||||
numBits = numBits % 8
|
|
||||||
nextBits = n.RBSPByte[byteOffset]
|
|
||||||
|
|
||||||
// read size
|
|
||||||
for {
|
|
||||||
if nextBits == 0xff {
|
|
||||||
n.PayloadSize += 255
|
|
||||||
numBits += 8
|
|
||||||
byteOffset += numBits / 8
|
|
||||||
numBits = numBits % 8
|
|
||||||
nextBits = n.RBSPByte[byteOffset]
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
n.PayloadSize += int(nextBits)
|
|
||||||
numBits += 8
|
|
||||||
byteOffset += numBits / 8
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
35
helpers.go
35
helpers.go
@@ -1,35 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
)
|
|
||||||
|
|
||||||
func assertSignalingCorrect(SRTHost, SRTPort, SRTStreamID string) (int, error) {
|
|
||||||
switch {
|
|
||||||
case SRTHost == "":
|
|
||||||
return 0, errors.New("SRTHost must not be nil")
|
|
||||||
case SRTPort == "":
|
|
||||||
return 0, errors.New("SRTPort must not be empty")
|
|
||||||
case SRTStreamID == "":
|
|
||||||
return 0, errors.New("SRTStreamID must not be empty")
|
|
||||||
}
|
|
||||||
|
|
||||||
return strconv.Atoi(SRTPort)
|
|
||||||
}
|
|
||||||
|
|
||||||
func errorToHTTP(w http.ResponseWriter, err error) {
|
|
||||||
w.WriteHeader(500)
|
|
||||||
w.Write([]byte(err.Error()))
|
|
||||||
}
|
|
||||||
|
|
||||||
func setCors(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if origin := r.Header.Get("Origin"); origin != "" {
|
|
||||||
allowedHeaders := "Accept, Content-Type, Content-Length, Accept-Encoding, Authorization,X-CSRF-Token"
|
|
||||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
|
||||||
w.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE")
|
|
||||||
w.Header().Set("Access-Control-Allow-Headers", allowedHeaders)
|
|
||||||
w.Header().Set("Access-Control-Expose-Headers", "Authorization")
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,11 +1,10 @@
|
|||||||
package eia608
|
package controllers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
|
||||||
"github.com/flavioribeiro/donut/h264"
|
|
||||||
|
|
||||||
"github.com/asticode/go-astits"
|
"github.com/asticode/go-astits"
|
||||||
|
"github.com/flavioribeiro/donut/internal/entities"
|
||||||
gocaption "github.com/szatmary/gocaption"
|
gocaption "github.com/szatmary/gocaption"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -13,25 +12,19 @@ type EIA608Reader struct {
|
|||||||
frame gocaption.EIA608Frame
|
frame gocaption.EIA608Frame
|
||||||
}
|
}
|
||||||
|
|
||||||
type Cue struct {
|
|
||||||
Type string
|
|
||||||
StartTime int64
|
|
||||||
Text string
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewEIA608Reader() (r *EIA608Reader) {
|
func NewEIA608Reader() (r *EIA608Reader) {
|
||||||
return &EIA608Reader{}
|
return &EIA608Reader{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *EIA608Reader) Parse(PES *astits.PESData) (string, error) {
|
func (r *EIA608Reader) Parse(PES *astits.PESData) (string, error) {
|
||||||
nalus, err := h264.ParseNALUs(PES.Data)
|
nalus, err := ParseNALUs(PES.Data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
for _, nal := range nalus.Units {
|
for _, nal := range nalus.Units {
|
||||||
// ANSI/SCTE 128-1 2020
|
// ANSI/SCTE 128-1 2020
|
||||||
// Note that SEI payload is a SEI payloadType of 4 which contains the itu_t_t35_payload_byte for the terminal provider
|
// Note that SEI payload is a SEI payloadType of 4 which contains the itu_t_t35_payload_byte for the terminal provider
|
||||||
if nal.UnitType == h264.SupplementalEnhancementInformation && nal.SEI.PayloadType == 4 {
|
if nal.UnitType == entities.SupplementalEnhancementInformation && nal.SEI.PayloadType == 4 {
|
||||||
// ANSI/SCTE 128-1 2020
|
// ANSI/SCTE 128-1 2020
|
||||||
// Caption, AFD and bar data shall be carried in the SEI raw byte sequence payload (RBSP)
|
// Caption, AFD and bar data shall be carried in the SEI raw byte sequence payload (RBSP)
|
||||||
// syntax of the video Elementary Stream.
|
// syntax of the video Elementary Stream.
|
||||||
@@ -55,7 +48,7 @@ func (r *EIA608Reader) Parse(PES *astits.PESData) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func BuildCaptionsMessage(pts *astits.ClockReference, captions string) (string, error) {
|
func BuildCaptionsMessage(pts *astits.ClockReference, captions string) (string, error) {
|
||||||
cue := Cue{
|
cue := entities.Cue{
|
||||||
StartTime: pts.Base,
|
StartTime: pts.Base,
|
||||||
Text: captions,
|
Text: captions,
|
||||||
Type: "captions",
|
Type: "captions",
|
59
internal/controllers/h264_controller.go
Normal file
59
internal/controllers/h264_controller.go
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
package controllers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/flavioribeiro/donut/internal/entities"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ParseNALUs(data []byte) (entities.NALUs, error) {
|
||||||
|
var nalus entities.NALUs
|
||||||
|
|
||||||
|
rawNALUs := bytes.Split(data, []byte{0x00, 0x00, 0x01})
|
||||||
|
|
||||||
|
for _, rawNALU := range rawNALUs[1:] {
|
||||||
|
nal, err := ParseNAL(rawNALU)
|
||||||
|
if err != nil {
|
||||||
|
return entities.NALUs{}, err
|
||||||
|
|
||||||
|
}
|
||||||
|
nalus.Units = append(nalus.Units, nal)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nalus, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseNAL(data []byte) (entities.NAL, error) {
|
||||||
|
index := 0
|
||||||
|
n := entities.NAL{}
|
||||||
|
if data[index]>>7&0x01 != 0 {
|
||||||
|
return entities.NAL{}, fmt.Errorf("forbidden_zero_bit is not 0")
|
||||||
|
}
|
||||||
|
n.RefIDC = (data[index] >> 5) & 0x03
|
||||||
|
n.UnitType = entities.NALUnitType(data[index] & 0x1f)
|
||||||
|
numBytesInRBSP := 0
|
||||||
|
nalUnitHeaderBytes := 1
|
||||||
|
n.HeaderBytes = data[:nalUnitHeaderBytes]
|
||||||
|
|
||||||
|
index += nalUnitHeaderBytes
|
||||||
|
|
||||||
|
n.RBSPByte = make([]byte, 0, 16)
|
||||||
|
i := 0
|
||||||
|
for i = index; i < len(data); i++ {
|
||||||
|
if (i+2) < len(data) && (data[i] == 0x00 && data[i+1] == 0x00 && data[i+2] == 0x03) {
|
||||||
|
n.RBSPByte = append(n.RBSPByte, data[i], data[i+1])
|
||||||
|
i += 2
|
||||||
|
numBytesInRBSP += 2
|
||||||
|
// 0x03
|
||||||
|
} else {
|
||||||
|
n.RBSPByte = append(n.RBSPByte, data[i])
|
||||||
|
numBytesInRBSP++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
index += numBytesInRBSP
|
||||||
|
|
||||||
|
n.ParseRBSP()
|
||||||
|
|
||||||
|
return n, nil
|
||||||
|
}
|
91
internal/controllers/srt_controller.go
Normal file
91
internal/controllers/srt_controller.go
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
package controllers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
astisrt "github.com/asticode/go-astisrt/pkg"
|
||||||
|
"github.com/flavioribeiro/donut/internal/entities"
|
||||||
|
"go.uber.org/fx"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SRTController struct {
|
||||||
|
c *entities.Config
|
||||||
|
l *zap.SugaredLogger
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSRTController(c *entities.Config, l *zap.SugaredLogger, lc fx.Lifecycle) (*SRTController, error) {
|
||||||
|
// Handle logs
|
||||||
|
astisrt.SetLogLevel(astisrt.LogLevel(astisrt.LogLevelNotice))
|
||||||
|
astisrt.SetLogHandler(func(ll astisrt.LogLevel, file, area, msg string, line int) {
|
||||||
|
l.Infow("SRT",
|
||||||
|
"ll", ll,
|
||||||
|
"msg", msg,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Startup srt
|
||||||
|
if err := astisrt.Startup(); err != nil {
|
||||||
|
l.Errorw("failed to start up srt",
|
||||||
|
"error", err,
|
||||||
|
)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
lc.Append(fx.Hook{
|
||||||
|
OnStop: func(ctx context.Context) error {
|
||||||
|
// Clean up
|
||||||
|
if err := astisrt.CleanUp(); err != nil {
|
||||||
|
l.Errorw("failed to clean up srt",
|
||||||
|
"error", err,
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return &SRTController{
|
||||||
|
c: c,
|
||||||
|
l: l,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *SRTController) Connect(cancel context.CancelFunc, params entities.RequestParams) (*astisrt.Connection, error) {
|
||||||
|
c.l.Infow("trying to connect srt")
|
||||||
|
|
||||||
|
if err := params.Valid(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
c.l.Infow("Connecting to SRT ",
|
||||||
|
"offer", params.String(),
|
||||||
|
)
|
||||||
|
|
||||||
|
conn, err := astisrt.Dial(astisrt.DialOptions{
|
||||||
|
ConnectionOptions: []astisrt.ConnectionOption{
|
||||||
|
astisrt.WithLatency(c.c.SRTConnectionLatencyMS),
|
||||||
|
astisrt.WithStreamid(params.SRTStreamID),
|
||||||
|
astisrt.WithCongestion("live"),
|
||||||
|
astisrt.WithTranstype(astisrt.Transtype(astisrt.TranstypeLive)),
|
||||||
|
},
|
||||||
|
|
||||||
|
OnDisconnect: func(conn *astisrt.Connection, err error) {
|
||||||
|
c.l.Infow("Canceling SRT",
|
||||||
|
"error", err,
|
||||||
|
)
|
||||||
|
cancel()
|
||||||
|
},
|
||||||
|
|
||||||
|
Host: params.SRTHost,
|
||||||
|
Port: params.SRTPort,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
c.l.Errorw("failed to connect srt",
|
||||||
|
"error", err,
|
||||||
|
)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
c.l.Infow("Connected to SRT")
|
||||||
|
return conn, nil
|
||||||
|
}
|
155
internal/controllers/streaming_controller.go
Normal file
155
internal/controllers/streaming_controller.go
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
package controllers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
astisrt "github.com/asticode/go-astisrt/pkg"
|
||||||
|
"github.com/asticode/go-astits"
|
||||||
|
"github.com/flavioribeiro/donut/internal/entities"
|
||||||
|
"github.com/pion/webrtc/v3"
|
||||||
|
"github.com/pion/webrtc/v3/pkg/media"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
type StreamingController struct {
|
||||||
|
c *entities.Config
|
||||||
|
l *zap.SugaredLogger
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewStreamingController(c *entities.Config, l *zap.SugaredLogger) *StreamingController {
|
||||||
|
return &StreamingController{
|
||||||
|
c: c,
|
||||||
|
l: l,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *StreamingController) Stream(sp entities.StreamParameters) {
|
||||||
|
r, w := io.Pipe()
|
||||||
|
|
||||||
|
defer r.Close()
|
||||||
|
defer w.Close()
|
||||||
|
defer sp.SRTConnection.Close()
|
||||||
|
defer sp.WebRTCConn.Close()
|
||||||
|
defer sp.Cancel()
|
||||||
|
|
||||||
|
// TODO: pick the proper transport? is it possible to get rtp instead?
|
||||||
|
go c.readFromSRTIntoWriterPipe(sp.SRTConnection, w)
|
||||||
|
|
||||||
|
// reading from reader pipe to the mpeg-ts demuxer
|
||||||
|
mpegTSDemuxer := astits.NewDemuxer(sp.Ctx, r)
|
||||||
|
eia608Reader := NewEIA608Reader()
|
||||||
|
h264PID := uint16(0)
|
||||||
|
|
||||||
|
c.l.Infow("streaming has started")
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-sp.Ctx.Done():
|
||||||
|
c.l.Errorw("streaming has stopped")
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
// fetching mpeg-ts data
|
||||||
|
// ref https://tsduck.io/download/docs/mpegts-introduction.pdf
|
||||||
|
mpegTSDemuxData, err := mpegTSDemuxer.NextData()
|
||||||
|
if err != nil {
|
||||||
|
c.l.Errorw("failed to demux mpeg-ts",
|
||||||
|
"error", err,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if mpegTSDemuxData.PMT != nil {
|
||||||
|
// writing mpeg-ts media codec info to the metadata webrtc channel
|
||||||
|
h264PID = c.captureMediaInfoAndSendToWebRTC(mpegTSDemuxData, sp.MetadataTrack, h264PID)
|
||||||
|
c.captureBitrateAndSendToWebRTC(mpegTSDemuxData, sp.MetadataTrack)
|
||||||
|
}
|
||||||
|
|
||||||
|
// writing mpeg-ts video/captions to webrtc channels
|
||||||
|
err = c.writeMpegtsToWebRTC(mpegTSDemuxData, h264PID, err, sp, eia608Reader)
|
||||||
|
if err != nil {
|
||||||
|
c.l.Errorw("failed to write an mpeg-ts to web rtc",
|
||||||
|
"error", err,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *StreamingController) writeMpegtsToWebRTC(mpegTSDemuxData *astits.DemuxerData, h264PID uint16, err error, sp entities.StreamParameters, eia608Reader *EIA608Reader) error {
|
||||||
|
if mpegTSDemuxData.PID == h264PID && mpegTSDemuxData.PES != nil {
|
||||||
|
|
||||||
|
if err = sp.VideoTrack.WriteSample(media.Sample{Data: mpegTSDemuxData.PES.Data, Duration: time.Second / 30}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
captions, err := eia608Reader.Parse(mpegTSDemuxData.PES)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if captions != "" {
|
||||||
|
captionsMsg, err := BuildCaptionsMessage(mpegTSDemuxData.PES.Header.OptionalHeader.PTS, captions)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
sp.MetadataTrack.SendText(captionsMsg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*StreamingController) captureBitrateAndSendToWebRTC(d *astits.DemuxerData, metadataTrack *webrtc.DataChannel) {
|
||||||
|
for _, d := range d.PMT.ProgramDescriptors {
|
||||||
|
if d.MaximumBitrate != nil {
|
||||||
|
bitrateInMbitsPerSecond := float32(d.MaximumBitrate.Bitrate) / float32(125000)
|
||||||
|
msg, _ := json.Marshal(entities.Message{
|
||||||
|
Type: entities.MessageTypeMetadata,
|
||||||
|
Message: fmt.Sprintf("Bitrate %.2fMbps", bitrateInMbitsPerSecond),
|
||||||
|
})
|
||||||
|
metadataTrack.SendText(string(msg))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*StreamingController) captureMediaInfoAndSendToWebRTC(d *astits.DemuxerData, metadataTrack *webrtc.DataChannel, h264PID uint16) uint16 {
|
||||||
|
for _, es := range d.PMT.ElementaryStreams {
|
||||||
|
|
||||||
|
msg, _ := json.Marshal(entities.Message{
|
||||||
|
Type: entities.MessageTypeMetadata,
|
||||||
|
Message: es.StreamType.String(),
|
||||||
|
})
|
||||||
|
metadataTrack.SendText(string(msg))
|
||||||
|
|
||||||
|
if es.StreamType == astits.StreamTypeH264Video {
|
||||||
|
h264PID = es.ElementaryPID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return h264PID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *StreamingController) readFromSRTIntoWriterPipe(srtConnection *astisrt.Connection, w *io.PipeWriter) {
|
||||||
|
defer srtConnection.Close()
|
||||||
|
|
||||||
|
inboundMpegTsPacket := make([]byte, c.c.SRTReadBufferSizeBytes)
|
||||||
|
|
||||||
|
for {
|
||||||
|
n, err := srtConnection.Read(inboundMpegTsPacket)
|
||||||
|
if err != nil {
|
||||||
|
c.l.Errorw("str conn failed to write data to buffer",
|
||||||
|
"error", err,
|
||||||
|
)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := w.Write(inboundMpegTsPacket[:n]); err != nil {
|
||||||
|
c.l.Errorw("failed to write mpeg-ts into the pipe",
|
||||||
|
"error", err,
|
||||||
|
)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
163
internal/controllers/webrtc_controller.go
Normal file
163
internal/controllers/webrtc_controller.go
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
package controllers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net"
|
||||||
|
|
||||||
|
"github.com/flavioribeiro/donut/internal/entities"
|
||||||
|
"github.com/flavioribeiro/donut/internal/mapper"
|
||||||
|
"github.com/pion/webrtc/v3"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
type WebRTCController struct {
|
||||||
|
c *entities.Config
|
||||||
|
l *zap.SugaredLogger
|
||||||
|
api *webrtc.API
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewWebRTCController(
|
||||||
|
c *entities.Config,
|
||||||
|
l *zap.SugaredLogger,
|
||||||
|
api *webrtc.API,
|
||||||
|
) *WebRTCController {
|
||||||
|
return &WebRTCController{
|
||||||
|
c: c,
|
||||||
|
l: l,
|
||||||
|
api: api,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *WebRTCController) CreatePeerConnection(cancel context.CancelFunc) (*webrtc.PeerConnection, error) {
|
||||||
|
c.l.Infow("trying to set up web rtc conn")
|
||||||
|
|
||||||
|
peerConnectionConfiguration := webrtc.Configuration{}
|
||||||
|
if !c.c.EnableICEMux {
|
||||||
|
peerConnectionConfiguration.ICEServers = []webrtc.ICEServer{
|
||||||
|
{
|
||||||
|
URLs: c.c.StunServers,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
peerConnection, err := c.api.NewPeerConnection(peerConnectionConfiguration)
|
||||||
|
if err != nil {
|
||||||
|
c.l.Errorw("error while creating a new peer connection",
|
||||||
|
"error", err,
|
||||||
|
)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
peerConnection.OnICEConnectionStateChange(func(connectionState webrtc.ICEConnectionState) {
|
||||||
|
finished := connectionState == webrtc.ICEConnectionStateClosed ||
|
||||||
|
connectionState == webrtc.ICEConnectionStateDisconnected ||
|
||||||
|
connectionState == webrtc.ICEConnectionStateCompleted ||
|
||||||
|
connectionState == webrtc.ICEConnectionStateFailed
|
||||||
|
|
||||||
|
if finished {
|
||||||
|
c.l.Infow("Canceling webrtc",
|
||||||
|
"status", connectionState.String(),
|
||||||
|
)
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
c.l.Infow("OnICEConnectionStateChange",
|
||||||
|
"status", connectionState.String(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
return peerConnection, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *WebRTCController) CreateTrack(peer *webrtc.PeerConnection, track entities.Track, id string, streamId string) (*webrtc.TrackLocalStaticSample, error) {
|
||||||
|
codecCapability := mapper.FromTrackToRTPCodecCapability(track)
|
||||||
|
webRTCtrack, err := webrtc.NewTrackLocalStaticSample(codecCapability, id, streamId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := peer.AddTrack(webRTCtrack); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return webRTCtrack, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *WebRTCController) CreateDataChannel(peer *webrtc.PeerConnection, channelID string) (*webrtc.DataChannel, error) {
|
||||||
|
metadataSender, err := peer.CreateDataChannel(channelID, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return metadataSender, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *WebRTCController) SetRemoteDescription(peer *webrtc.PeerConnection, desc webrtc.SessionDescription) error {
|
||||||
|
err := peer.SetRemoteDescription(desc)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *WebRTCController) GatheringWebRTC(peer *webrtc.PeerConnection) (*webrtc.SessionDescription, error) {
|
||||||
|
|
||||||
|
c.l.Infow("Gathering WebRTC Candidates")
|
||||||
|
gatherComplete := webrtc.GatheringCompletePromise(peer)
|
||||||
|
answer, err := peer.CreateAnswer(nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if err = peer.SetLocalDescription(answer); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
<-gatherComplete
|
||||||
|
c.l.Infow("Gathering WebRTC Candidates Complete")
|
||||||
|
|
||||||
|
return peer.LocalDescription(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewWebRTCSettingsEngine(c *entities.Config, tcpListener net.Listener, udpListener net.PacketConn) webrtc.SettingEngine {
|
||||||
|
settingEngine := webrtc.SettingEngine{}
|
||||||
|
|
||||||
|
settingEngine.SetNAT1To1IPs(c.ICEExternalIPsDNAT, webrtc.ICECandidateTypeHost)
|
||||||
|
settingEngine.SetICETCPMux(webrtc.NewICETCPMux(nil, tcpListener, c.ICEReadBufferSize))
|
||||||
|
settingEngine.SetICEUDPMux(webrtc.NewICEUDPMux(nil, udpListener))
|
||||||
|
|
||||||
|
return settingEngine
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewWebRTCMediaEngine() (*webrtc.MediaEngine, error) {
|
||||||
|
mediaEngine := &webrtc.MediaEngine{}
|
||||||
|
if err := mediaEngine.RegisterDefaultCodecs(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return mediaEngine, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewWebRTCAPI(mediaEngine *webrtc.MediaEngine, settingEngine webrtc.SettingEngine) *webrtc.API {
|
||||||
|
return webrtc.NewAPI(
|
||||||
|
webrtc.WithSettingEngine(settingEngine),
|
||||||
|
webrtc.WithMediaEngine(mediaEngine),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTCPICEServer(c *entities.Config) (net.Listener, error) {
|
||||||
|
tcpListener, err := net.ListenTCP("tcp", &net.TCPAddr{
|
||||||
|
IP: net.IP{0, 0, 0, 0},
|
||||||
|
Port: c.TCPICEPort,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return tcpListener, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUDPICEServer(c *entities.Config) (net.PacketConn, error) {
|
||||||
|
udpListener, err := net.ListenUDP("udp", &net.UDPAddr{
|
||||||
|
IP: net.IP{0, 0, 0, 0},
|
||||||
|
Port: c.UDPICEPort,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return udpListener, nil
|
||||||
|
}
|
98
internal/entities/entities.go
Normal file
98
internal/entities/entities.go
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
package entities
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
astisrt "github.com/asticode/go-astisrt/pkg"
|
||||||
|
"github.com/pion/webrtc/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
MetadataChannelID string = "metadata"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RequestParams struct {
|
||||||
|
SRTHost string
|
||||||
|
SRTPort uint16 `json:",string"`
|
||||||
|
SRTStreamID string
|
||||||
|
Offer webrtc.SessionDescription
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *RequestParams) Valid() error {
|
||||||
|
if p == nil {
|
||||||
|
return ErrMissingParamsOffer
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.SRTHost == "" {
|
||||||
|
return ErrMissingSRTHost
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.SRTPort == 0 {
|
||||||
|
return ErrMissingSRTPort
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.SRTStreamID == "" {
|
||||||
|
return ErrMissingSRTStreamID
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *RequestParams) String() string {
|
||||||
|
if p == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("ParamsOffer %v:%v/%v", p.SRTHost, p.SRTPort, p.SRTStreamID)
|
||||||
|
}
|
||||||
|
|
||||||
|
type MessageType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
MessageTypeMetadata MessageType = "metadata"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Message struct {
|
||||||
|
Type MessageType
|
||||||
|
Message string
|
||||||
|
}
|
||||||
|
|
||||||
|
type TrackType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
H264 TrackType = "h264"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Track struct {
|
||||||
|
Type TrackType
|
||||||
|
}
|
||||||
|
|
||||||
|
type Cue struct {
|
||||||
|
Type string
|
||||||
|
StartTime int64
|
||||||
|
Text string
|
||||||
|
}
|
||||||
|
|
||||||
|
type StreamParameters struct {
|
||||||
|
WebRTCConn *webrtc.PeerConnection
|
||||||
|
Cancel context.CancelFunc
|
||||||
|
Ctx context.Context
|
||||||
|
SRTConnection *astisrt.Connection
|
||||||
|
VideoTrack *webrtc.TrackLocalStaticSample
|
||||||
|
MetadataTrack *webrtc.DataChannel
|
||||||
|
}
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
HTTPPort int32 `required:"true" default:"8080"`
|
||||||
|
HTTPHost string `required:"true" default:"0.0.0.0"`
|
||||||
|
|
||||||
|
TCPICEPort int `required:"true" default:"8081"`
|
||||||
|
UDPICEPort int `required:"true" default:"8081"`
|
||||||
|
ICEReadBufferSize int `required:"true" default:"8"`
|
||||||
|
ICEExternalIPsDNAT []string `required:"true" default:"127.0.0.1"`
|
||||||
|
EnableICEMux bool `require:"true" default:"false"`
|
||||||
|
StunServers []string `required:"true" default:"stun:stun4.l.google.com:19302"`
|
||||||
|
|
||||||
|
SRTConnectionLatencyMS int32 `required:"true" default:"300"`
|
||||||
|
SRTReadBufferSizeBytes int `required:"true" default:"1316"`
|
||||||
|
}
|
12
internal/entities/errors.go
Normal file
12
internal/entities/errors.go
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
package entities
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
var ErrHTTPGetOnly = errors.New("you must use http GET verb")
|
||||||
|
var ErrHTTPPostOnly = errors.New("you must use http POST verb")
|
||||||
|
var ErrMissingParamsOffer = errors.New("ParamsOffer must not be nil")
|
||||||
|
var ErrMissingSRTHost = errors.New("SRTHost must not be nil")
|
||||||
|
var ErrMissingSRTPort = errors.New("SRTPort must be valid")
|
||||||
|
var ErrMissingSRTStreamID = errors.New("SRTStreamID must not be empty")
|
||||||
|
var ErrMissingWebRTCSetup = errors.New("WebRTCController.SetupPeerConnection must be called first")
|
||||||
|
var ErrMissingRemoteOffer = errors.New("nil offer, in order to connect one must pass a valid offer")
|
@@ -1,4 +1,4 @@
|
|||||||
package h264
|
package entities
|
||||||
|
|
||||||
type NALUs struct {
|
type NALUs struct {
|
||||||
Units []NAL
|
Units []NAL
|
||||||
@@ -56,3 +56,60 @@ const (
|
|||||||
Unspecified31 = NALUnitType(31) // Unspecified
|
Unspecified31 = NALUnitType(31) // Unspecified
|
||||||
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func (n *NAL) ParseRBSP() error {
|
||||||
|
switch n.UnitType {
|
||||||
|
case SupplementalEnhancementInformation:
|
||||||
|
err := n.parseSEI()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NAL) parseSEI() error {
|
||||||
|
numBits := 0
|
||||||
|
byteOffset := 0
|
||||||
|
n.SEI.PayloadType = 0
|
||||||
|
n.SEI.PayloadSize = 0
|
||||||
|
nextBits := n.RBSPByte[byteOffset]
|
||||||
|
|
||||||
|
for {
|
||||||
|
if nextBits == 0xff {
|
||||||
|
n.PayloadType += 255
|
||||||
|
numBits += 8
|
||||||
|
byteOffset += numBits / 8
|
||||||
|
numBits = numBits % 8
|
||||||
|
nextBits = n.RBSPByte[byteOffset]
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
n.PayloadType += int(nextBits)
|
||||||
|
numBits += 8
|
||||||
|
byteOffset += numBits / 8
|
||||||
|
numBits = numBits % 8
|
||||||
|
nextBits = n.RBSPByte[byteOffset]
|
||||||
|
|
||||||
|
// read size
|
||||||
|
for {
|
||||||
|
if nextBits == 0xff {
|
||||||
|
n.PayloadSize += 255
|
||||||
|
numBits += 8
|
||||||
|
byteOffset += numBits / 8
|
||||||
|
numBits = numBits % 8
|
||||||
|
nextBits = n.RBSPByte[byteOffset]
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
n.PayloadSize += int(nextBits)
|
||||||
|
numBits += 8
|
||||||
|
byteOffset += numBits / 8
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
16
internal/mapper/mapper.go
Normal file
16
internal/mapper/mapper.go
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
package mapper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/flavioribeiro/donut/internal/entities"
|
||||||
|
"github.com/pion/webrtc/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func FromTrackToRTPCodecCapability(track entities.Track) webrtc.RTPCodecCapability {
|
||||||
|
response := webrtc.RTPCodecCapability{}
|
||||||
|
|
||||||
|
if track.Type == entities.H264 {
|
||||||
|
response.MimeType = webrtc.MimeTypeH264
|
||||||
|
}
|
||||||
|
|
||||||
|
return response
|
||||||
|
}
|
8
internal/web/demo/demo.css
Normal file
8
internal/web/demo/demo.css
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
/*
|
||||||
|
SPDX-FileCopyrightText: 2023 The Pion community <https://pion.ly>
|
||||||
|
SPDX-License-Identifier: MIT
|
||||||
|
*/
|
||||||
|
textarea {
|
||||||
|
width: 500px;
|
||||||
|
min-height: 75px;
|
||||||
|
}
|
67
internal/web/demo/demo.js
Normal file
67
internal/web/demo/demo.js
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
/* eslint-env browser */
|
||||||
|
|
||||||
|
// SPDX-FileCopyrightText: 2023 The Pion community <https://pion.ly>
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
const pc = new RTCPeerConnection({
|
||||||
|
iceServers: [{
|
||||||
|
urls: 'stun:stun.l.google.com:19302'
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
const log = msg => {
|
||||||
|
document.getElementById('div').innerHTML += msg + '<br>'
|
||||||
|
}
|
||||||
|
|
||||||
|
pc.ontrack = function (event) {
|
||||||
|
const el = document.createElement(event.track.kind)
|
||||||
|
el.srcObject = event.streams[0]
|
||||||
|
el.autoplay = true
|
||||||
|
el.controls = true
|
||||||
|
|
||||||
|
document.getElementById('remoteVideos').appendChild(el)
|
||||||
|
}
|
||||||
|
|
||||||
|
pc.oniceconnectionstatechange = e => log(pc.iceConnectionState)
|
||||||
|
pc.onicecandidate = event => {
|
||||||
|
if (event.candidate === null) {
|
||||||
|
document.getElementById('localSessionDescription').value = btoa(JSON.stringify(pc.localDescription))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Offer to receive 1 audio, and 1 video track
|
||||||
|
pc.addTransceiver('video', {
|
||||||
|
direction: 'sendrecv'
|
||||||
|
})
|
||||||
|
pc.addTransceiver('audio', {
|
||||||
|
direction: 'sendrecv'
|
||||||
|
})
|
||||||
|
|
||||||
|
pc.createOffer().then(d => pc.setLocalDescription(d)).catch(log)
|
||||||
|
|
||||||
|
window.startSession = () => {
|
||||||
|
const sd = document.getElementById('remoteSessionDescription').value
|
||||||
|
if (sd === '') {
|
||||||
|
return alert('Session Description must not be empty')
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
pc.setRemoteDescription(JSON.parse(atob(sd)))
|
||||||
|
} catch (e) {
|
||||||
|
alert(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.copySessionDescription = () => {
|
||||||
|
const browserSessionDescription = document.getElementById('localSessionDescription')
|
||||||
|
|
||||||
|
browserSessionDescription.focus()
|
||||||
|
browserSessionDescription.select()
|
||||||
|
|
||||||
|
try {
|
||||||
|
const successful = document.execCommand('copy')
|
||||||
|
const msg = successful ? 'successful' : 'unsuccessful'
|
||||||
|
log('Copying SessionDescription was ' + msg)
|
||||||
|
} catch (err) {
|
||||||
|
log('Oops, unable to copy SessionDescription ' + err)
|
||||||
|
}
|
||||||
|
}
|
30
internal/web/demo/index.html
Normal file
30
internal/web/demo/index.html
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<!--
|
||||||
|
SPDX-FileCopyrightText: 2023 The Pion community <https://pion.ly>
|
||||||
|
SPDX-License-Identifier: MIT
|
||||||
|
-->
|
||||||
|
Browser Session Description
|
||||||
|
<br/>
|
||||||
|
<textarea id="localSessionDescription" readonly="true"></textarea>
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
<button onclick="window.copySessionDescription()">Copy browser Session Description to clipboard</button>
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
<br/>
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
Remote Session Description
|
||||||
|
<br/>
|
||||||
|
<textarea id="remoteSessionDescription"></textarea>
|
||||||
|
<br/>
|
||||||
|
<button onclick="window.startSession()">Start Session</button>
|
||||||
|
<br/>
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
Video
|
||||||
|
<br/>
|
||||||
|
<div id="remoteVideos"></div> <br />
|
||||||
|
|
||||||
|
Logs
|
||||||
|
<br/>
|
||||||
|
<div id="div"></div>
|
1
internal/web/demo/readme.txt
Normal file
1
internal/web/demo/readme.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Initially copied from https://github.com/pion/webrtc/tree/master/examples/play-from-disk/jsfiddle
|
20
internal/web/handlers/index.go
Normal file
20
internal/web/handlers/index.go
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "embed"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed index.html
|
||||||
|
var indexHTML string
|
||||||
|
|
||||||
|
type IndexHandler struct{}
|
||||||
|
|
||||||
|
func NewIndexHandler() *IndexHandler {
|
||||||
|
return &IndexHandler{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *IndexHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write([]byte(indexHTML))
|
||||||
|
}
|
@@ -7,13 +7,13 @@
|
|||||||
|
|
||||||
<body>
|
<body>
|
||||||
<b> SRT Host </b>
|
<b> SRT Host </b>
|
||||||
<input type="text" id="srt-host"> <br />
|
<input type="text" id="srt-host" value="srt"> <br />
|
||||||
|
|
||||||
<b> SRT Port </b>
|
<b> SRT Port </b>
|
||||||
<input type="text" id="srt-port" /> <br />
|
<input type="text" id="srt-port" value="40052"/> <br />
|
||||||
|
|
||||||
<b> SRT Stream ID </b>
|
<b> SRT Stream ID </b>
|
||||||
<input type="text" id="srt-stream-id" /> <br />
|
<input type="text" id="srt-stream-id" value="stream-id" /> <br />
|
||||||
<button onclick="onConnect()"> Connect </button>
|
<button onclick="onConnect()"> Connect </button>
|
||||||
|
|
||||||
<donut-video server="http://localhost:8080" controls />
|
<donut-video server="http://localhost:8080" controls />
|
134
internal/web/handlers/signaling.go
Normal file
134
internal/web/handlers/signaling.go
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/flavioribeiro/donut/internal/controllers"
|
||||||
|
"github.com/flavioribeiro/donut/internal/entities"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SignalingHandler struct {
|
||||||
|
c *entities.Config
|
||||||
|
l *zap.SugaredLogger
|
||||||
|
webRTCController *controllers.WebRTCController
|
||||||
|
srtController *controllers.SRTController
|
||||||
|
streamingController *controllers.StreamingController
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSignalingHandler(
|
||||||
|
c *entities.Config,
|
||||||
|
log *zap.SugaredLogger,
|
||||||
|
webRTCController *controllers.WebRTCController,
|
||||||
|
srtController *controllers.SRTController,
|
||||||
|
streamingController *controllers.StreamingController,
|
||||||
|
) *SignalingHandler {
|
||||||
|
return &SignalingHandler{
|
||||||
|
c: c,
|
||||||
|
l: log,
|
||||||
|
webRTCController: webRTCController,
|
||||||
|
srtController: srtController,
|
||||||
|
streamingController: streamingController,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *SignalingHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) error {
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
h.l.Errorw("unexpected method")
|
||||||
|
return entities.ErrHTTPPostOnly
|
||||||
|
}
|
||||||
|
|
||||||
|
params := entities.RequestParams{}
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil {
|
||||||
|
h.l.Errorw("error while decoding request params json",
|
||||||
|
"error", err,
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := params.Valid(); err != nil {
|
||||||
|
h.l.Errorw("invalid params",
|
||||||
|
"error", err,
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
|
||||||
|
peer, err := h.webRTCController.CreatePeerConnection(cancel)
|
||||||
|
if err != nil {
|
||||||
|
h.l.Errorw("error while setting up web rtc connection",
|
||||||
|
"error", err,
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: create tracks according with SRT available streams
|
||||||
|
// Create a video track
|
||||||
|
videoTrack, err := h.webRTCController.CreateTrack(
|
||||||
|
peer,
|
||||||
|
entities.Track{
|
||||||
|
Type: entities.H264,
|
||||||
|
}, "video", params.SRTStreamID,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
h.l.Errorw("error while creating a web rtc track",
|
||||||
|
"error", err,
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
metadataSender, err := h.webRTCController.CreateDataChannel(peer, entities.MetadataChannelID)
|
||||||
|
if err != nil {
|
||||||
|
h.l.Errorw("error while createing a web rtc data channel",
|
||||||
|
"error", err,
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = h.webRTCController.SetRemoteDescription(peer, params.Offer); err != nil {
|
||||||
|
h.l.Errorw("error while setting a remote web rtc description",
|
||||||
|
"error", err,
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
localDescription, err := h.webRTCController.GatheringWebRTC(peer)
|
||||||
|
if err != nil {
|
||||||
|
h.l.Errorw("error while preparing a local web rtc description",
|
||||||
|
"error", err,
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
srtConnection, err := h.srtController.Connect(cancel, params)
|
||||||
|
if err != nil {
|
||||||
|
h.l.Errorw("error while connecting to an srt server",
|
||||||
|
"error", err,
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
go h.streamingController.Stream(entities.StreamParameters{
|
||||||
|
Cancel: cancel,
|
||||||
|
Ctx: ctx,
|
||||||
|
WebRTCConn: peer,
|
||||||
|
SRTConnection: srtConnection,
|
||||||
|
VideoTrack: videoTrack,
|
||||||
|
MetadataTrack: metadataSender,
|
||||||
|
})
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
|
||||||
|
err = json.NewEncoder(w).Encode(*localDescription)
|
||||||
|
if err != nil {
|
||||||
|
h.l.Errorw("error while encoding a local web rtc description",
|
||||||
|
"error", err,
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
53
internal/web/router.go
Normal file
53
internal/web/router.go
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
package web
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/flavioribeiro/donut/internal/web/handlers"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ErrorHTTPHandler interface {
|
||||||
|
ServeHTTP(w http.ResponseWriter, r *http.Request) error
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewServeMux(
|
||||||
|
index *handlers.IndexHandler,
|
||||||
|
signaling *handlers.SignalingHandler,
|
||||||
|
l *zap.SugaredLogger,
|
||||||
|
) *http.ServeMux {
|
||||||
|
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
|
||||||
|
mux.Handle("/", index)
|
||||||
|
mux.Handle("/doSignaling", setCors(errorHandler(l, signaling)))
|
||||||
|
mux.Handle("/demo", http.FileServer(http.Dir("./demo")))
|
||||||
|
|
||||||
|
return mux
|
||||||
|
}
|
||||||
|
|
||||||
|
func setCors(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if origin := r.Header.Get("Origin"); origin != "" {
|
||||||
|
allowedHeaders := "Accept, Content-Type, Content-Length, Accept-Encoding, Authorization,X-CSRF-Token"
|
||||||
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
|
w.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE")
|
||||||
|
w.Header().Set("Access-Control-Allow-Headers", allowedHeaders)
|
||||||
|
w.Header().Set("Access-Control-Expose-Headers", "Authorization")
|
||||||
|
}
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func errorHandler(l *zap.SugaredLogger, next ErrorHTTPHandler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
err := next.ServeHTTP(w, r)
|
||||||
|
if err != nil {
|
||||||
|
l.Errorw("error on handler",
|
||||||
|
"err", err,
|
||||||
|
)
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
41
internal/web/server.go
Normal file
41
internal/web/server.go
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
package web
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/flavioribeiro/donut/internal/entities"
|
||||||
|
"go.uber.org/fx"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewHTTPServer(
|
||||||
|
c *entities.Config,
|
||||||
|
mux *http.ServeMux,
|
||||||
|
log *zap.SugaredLogger,
|
||||||
|
lc fx.Lifecycle,
|
||||||
|
) *http.Server {
|
||||||
|
srv := &http.Server{
|
||||||
|
Addr: fmt.Sprintf("%s:%d", c.HTTPHost, c.HTTPPort),
|
||||||
|
Handler: mux,
|
||||||
|
}
|
||||||
|
lc.Append(fx.Hook{
|
||||||
|
OnStart: func(ctx context.Context) error {
|
||||||
|
ln, err := net.Listen("tcp", srv.Addr)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.Infow(fmt.Sprintf("Starting HTTP server. Open http://%s to access the demo", srv.Addr),
|
||||||
|
"addr", srv.Addr,
|
||||||
|
)
|
||||||
|
go srv.Serve(ln)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
OnStop: func(ctx context.Context) error {
|
||||||
|
return srv.Shutdown(ctx)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return srv
|
||||||
|
}
|
299
main.go
299
main.go
@@ -4,269 +4,66 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
_ "embed"
|
|
||||||
"encoding/json"
|
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"log"
|
"log"
|
||||||
"net"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/flavioribeiro/donut/eia608"
|
"github.com/flavioribeiro/donut/internal/controllers"
|
||||||
|
"github.com/flavioribeiro/donut/internal/entities"
|
||||||
|
"github.com/flavioribeiro/donut/internal/web"
|
||||||
|
"github.com/flavioribeiro/donut/internal/web/handlers"
|
||||||
|
|
||||||
astisrt "github.com/asticode/go-astisrt/pkg"
|
"github.com/kelseyhightower/envconfig"
|
||||||
"github.com/asticode/go-astits"
|
"go.uber.org/fx"
|
||||||
"github.com/pion/webrtc/v3"
|
"go.uber.org/zap"
|
||||||
"github.com/pion/webrtc/v3/pkg/media"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
|
||||||
//go:embed index.html
|
|
||||||
indexHTML string
|
|
||||||
|
|
||||||
api *webrtc.API //nolint
|
|
||||||
|
|
||||||
enableICEMux = false
|
|
||||||
)
|
|
||||||
|
|
||||||
func srtToWebRTC(srtConnection *astisrt.Connection, videoTrack *webrtc.TrackLocalStaticSample, metadataTrack *webrtc.DataChannel) {
|
|
||||||
r, w := io.Pipe()
|
|
||||||
defer r.Close()
|
|
||||||
defer w.Close()
|
|
||||||
defer srtConnection.Close()
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
defer srtConnection.Close()
|
|
||||||
inboundMpegTsPacket := make([]byte, 1316) // SRT Read Size
|
|
||||||
|
|
||||||
for {
|
|
||||||
n, err := srtConnection.Read(inboundMpegTsPacket)
|
|
||||||
if err != nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := w.Write(inboundMpegTsPacket[:n]); err != nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
dmx := astits.NewDemuxer(context.Background(), r)
|
|
||||||
eia608Reader := eia608.NewEIA608Reader()
|
|
||||||
h264PID := uint16(0)
|
|
||||||
for {
|
|
||||||
d, err := dmx.NextData()
|
|
||||||
if err != nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
if d.PMT != nil {
|
|
||||||
for _, es := range d.PMT.ElementaryStreams {
|
|
||||||
msg, _ := json.Marshal(struct {
|
|
||||||
Type string
|
|
||||||
Message string
|
|
||||||
}{
|
|
||||||
Type: "metadata",
|
|
||||||
Message: es.StreamType.String(),
|
|
||||||
})
|
|
||||||
metadataTrack.SendText(string(msg))
|
|
||||||
if es.StreamType == astits.StreamTypeH264Video {
|
|
||||||
h264PID = es.ElementaryPID
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, d := range d.PMT.ProgramDescriptors {
|
|
||||||
if d.MaximumBitrate != nil {
|
|
||||||
bitrateInMbitsPerSecond := float32(d.MaximumBitrate.Bitrate) / float32(125000)
|
|
||||||
msg, _ := json.Marshal(struct {
|
|
||||||
Type string
|
|
||||||
Message string
|
|
||||||
}{
|
|
||||||
Type: "metadata",
|
|
||||||
Message: fmt.Sprintf("Bitrate %.2fMbps", bitrateInMbitsPerSecond),
|
|
||||||
})
|
|
||||||
metadataTrack.SendText(string(msg))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if d.PID == h264PID && d.PES != nil {
|
|
||||||
if err = videoTrack.WriteSample(media.Sample{Data: d.PES.Data, Duration: time.Second / 30}); err != nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
captions, err := eia608Reader.Parse(d.PES)
|
|
||||||
if err != nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if captions != "" {
|
|
||||||
captionsMsg, err := eia608.BuildCaptionsMessage(d.PES.Header.OptionalHeader.PTS, captions)
|
|
||||||
if err != nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
metadataTrack.SendText(captionsMsg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func doSignaling(w http.ResponseWriter, r *http.Request) {
|
|
||||||
setCors(w, r)
|
|
||||||
if r.Method != http.MethodPost {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
peerConnectionConfiguration := webrtc.Configuration{}
|
|
||||||
if !enableICEMux {
|
|
||||||
peerConnectionConfiguration.ICEServers = []webrtc.ICEServer{
|
|
||||||
{
|
|
||||||
URLs: []string{
|
|
||||||
"stun:stun4.l.google.com:19302",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
peerConnection, err := api.NewPeerConnection(peerConnectionConfiguration)
|
|
||||||
if err != nil {
|
|
||||||
errorToHTTP(w, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
offer := struct {
|
|
||||||
SRTHost string
|
|
||||||
SRTPort string
|
|
||||||
SRTStreamID string
|
|
||||||
Offer webrtc.SessionDescription
|
|
||||||
}{"", "", "", webrtc.SessionDescription{}}
|
|
||||||
|
|
||||||
if err = json.NewDecoder(r.Body).Decode(&offer); err != nil {
|
|
||||||
errorToHTTP(w, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a video track
|
|
||||||
videoTrack, err := webrtc.NewTrackLocalStaticSample(webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeH264}, "video", offer.SRTStreamID)
|
|
||||||
if err != nil {
|
|
||||||
errorToHTTP(w, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if _, err := peerConnection.AddTrack(videoTrack); err != nil {
|
|
||||||
errorToHTTP(w, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create data channel for metadata transmission
|
|
||||||
metadataSender, err := peerConnection.CreateDataChannel("metadata", nil)
|
|
||||||
if err != nil {
|
|
||||||
errorToHTTP(w, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set the handler for ICE connection state
|
|
||||||
// This will notify you when the peer has connected/disconnected
|
|
||||||
peerConnection.OnICEConnectionStateChange(func(connectionState webrtc.ICEConnectionState) {
|
|
||||||
log.Printf("ICE Connection State has changed: %s\n", connectionState.String())
|
|
||||||
})
|
|
||||||
|
|
||||||
srtPort, err := assertSignalingCorrect(offer.SRTHost, offer.SRTPort, offer.SRTStreamID)
|
|
||||||
if err != nil {
|
|
||||||
errorToHTTP(w, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = peerConnection.SetRemoteDescription(offer.Offer); err != nil {
|
|
||||||
errorToHTTP(w, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Println("Gathering WebRTC Candidates")
|
|
||||||
gatherComplete := webrtc.GatheringCompletePromise(peerConnection)
|
|
||||||
answer, err := peerConnection.CreateAnswer(nil)
|
|
||||||
if err != nil {
|
|
||||||
errorToHTTP(w, err)
|
|
||||||
return
|
|
||||||
} else if err = peerConnection.SetLocalDescription(answer); err != nil {
|
|
||||||
errorToHTTP(w, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
<-gatherComplete
|
|
||||||
log.Println("Gathering WebRTC Candidates Complete")
|
|
||||||
|
|
||||||
response, err := json.Marshal(*peerConnection.LocalDescription())
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Println("Connecting to SRT ", offer.SRTHost, srtPort, offer.SRTStreamID)
|
|
||||||
srtConnection, err := astisrt.Dial(astisrt.DialOptions{
|
|
||||||
ConnectionOptions: []astisrt.ConnectionOption{
|
|
||||||
astisrt.WithLatency(300),
|
|
||||||
astisrt.WithStreamid(offer.SRTStreamID),
|
|
||||||
},
|
|
||||||
|
|
||||||
// Callback when the connection is disconnected
|
|
||||||
OnDisconnect: func(c *astisrt.Connection, err error) { log.Fatal("Disconnected from SRT") },
|
|
||||||
|
|
||||||
Host: offer.SRTHost,
|
|
||||||
Port: uint16(srtPort),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
errorToHTTP(w, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
log.Println("Connected to SRT")
|
|
||||||
|
|
||||||
go srtToWebRTC(srtConnection, videoTrack, metadataSender)
|
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
if _, err := w.Write(response); err != nil {
|
|
||||||
errorToHTTP(w, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
enableICEMux := false
|
||||||
flag.BoolVar(&enableICEMux, "enable-ice-mux", false, "Enable ICE Mux on :8081")
|
flag.BoolVar(&enableICEMux, "enable-ice-mux", false, "Enable ICE Mux on :8081")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
mediaEngine := &webrtc.MediaEngine{}
|
var c entities.Config
|
||||||
settingEngine := webrtc.SettingEngine{}
|
err := envconfig.Process("donut", &c)
|
||||||
if err := mediaEngine.RegisterDefaultCodecs(); err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if enableICEMux {
|
|
||||||
tcpListener, err := net.ListenTCP("tcp", &net.TCPAddr{
|
|
||||||
IP: net.IP{0, 0, 0, 0},
|
|
||||||
Port: 8081,
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err.Error())
|
||||||
}
|
}
|
||||||
|
c.EnableICEMux = enableICEMux
|
||||||
|
|
||||||
udpListener, err := net.ListenUDP("udp", &net.UDPAddr{
|
fx.New(
|
||||||
IP: net.IP{0, 0, 0, 0},
|
// HTTP Server
|
||||||
Port: 8081,
|
fx.Provide(web.NewHTTPServer),
|
||||||
})
|
|
||||||
if err != nil {
|
// HTTP router
|
||||||
log.Fatal(err)
|
fx.Provide(web.NewServeMux),
|
||||||
}
|
|
||||||
|
// HTTP handlers
|
||||||
settingEngine.SetNAT1To1IPs([]string{"127.0.0.1"}, webrtc.ICECandidateTypeHost)
|
fx.Provide(handlers.NewSignalingHandler),
|
||||||
settingEngine.SetICETCPMux(webrtc.NewICETCPMux(nil, tcpListener, 8))
|
fx.Provide(handlers.NewIndexHandler),
|
||||||
settingEngine.SetICEUDPMux(webrtc.NewICEUDPMux(nil, udpListener))
|
|
||||||
}
|
// ICE mux servers
|
||||||
api = webrtc.NewAPI(webrtc.WithSettingEngine(settingEngine), webrtc.WithMediaEngine(mediaEngine))
|
fx.Provide(controllers.NewTCPICEServer),
|
||||||
|
fx.Provide(controllers.NewUDPICEServer),
|
||||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.WriteHeader(200)
|
// Controllers
|
||||||
w.Write([]byte(indexHTML))
|
fx.Provide(controllers.NewSRTController),
|
||||||
})
|
fx.Provide(controllers.NewStreamingController),
|
||||||
http.HandleFunc("/doSignaling", doSignaling)
|
|
||||||
|
fx.Provide(controllers.NewWebRTCController),
|
||||||
log.Println("Open http://localhost:8080 to access this demo")
|
fx.Provide(controllers.NewWebRTCSettingsEngine),
|
||||||
log.Fatal(http.ListenAndServe(":8080", nil))
|
fx.Provide(controllers.NewWebRTCMediaEngine),
|
||||||
|
fx.Provide(controllers.NewWebRTCAPI),
|
||||||
|
|
||||||
|
// Logging, Config constructors
|
||||||
|
fx.Provide(func() *zap.SugaredLogger {
|
||||||
|
logger, _ := zap.NewProduction()
|
||||||
|
return logger.Sugar()
|
||||||
|
}),
|
||||||
|
fx.Provide(func() *entities.Config {
|
||||||
|
return &c
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Forcing the lifecycle initiation with NewHTTPServer
|
||||||
|
fx.Invoke(func(*http.Server) {}),
|
||||||
|
).Run()
|
||||||
}
|
}
|
||||||
|
6
scripts/ffmpeg_mpegts_udp.sh
Executable file
6
scripts/ffmpeg_mpegts_udp.sh
Executable file
@@ -0,0 +1,6 @@
|
|||||||
|
ffmpeg -hide_banner -loglevel verbose \
|
||||||
|
-re -f lavfi -i testsrc2=size=1280x720:rate=30,format=yuv420p \
|
||||||
|
-f lavfi -i sine=frequency=1000:sample_rate=44100 \
|
||||||
|
-c:v libx264 -preset veryfast -tune zerolatency -profile:v baseline \
|
||||||
|
-b:v 1000k -bufsize 2000k -x264opts keyint=30:min-keyint=30:scenecut=-1 \
|
||||||
|
-f mpegts "udp://${SRT_INPUT_HOST}:${SRT_INPUT_PORT}?pkt_size=${PKT_SIZE}"
|
@@ -1,8 +0,0 @@
|
|||||||
ffmpeg -hide_banner -loglevel verbose \
|
|
||||||
-re -f lavfi -i "testsrc2=size=1280x720:rate=30,format=yuv420p" \
|
|
||||||
-f lavfi -i "sine=frequency=1000:sample_rate=44100" \
|
|
||||||
-c:v libx264 -preset veryfast -tune zerolatency -profile:v baseline \
|
|
||||||
-b:v 1400k -bufsize 2800k -x264opts keyint=30:min-keyint=30:scenecut=-1 \
|
|
||||||
-c:a aac -b:a 128k \
|
|
||||||
-f mpegts "srt://${SRT_LISTENING_HOST}:${SRT_LISTENING_PORT}?mode=listener&latency=${SRT_LISTENING_LATENCY_US}"
|
|
||||||
|
|
4
scripts/srt.sh
Executable file
4
scripts/srt.sh
Executable file
@@ -0,0 +1,4 @@
|
|||||||
|
# ref https://github.com/Haivision/srt/blob/master/docs/apps/srt-live-transmit.md
|
||||||
|
srt-live-transmit \
|
||||||
|
"udp://${SRT_UDP_TS_INPUT_HOST}:${SRT_UDP_TS_INPUT_PORT}" \
|
||||||
|
"srt://:${SRT_LISTENING_PORT}?congestion=live&transtype=live" -v
|
Reference in New Issue
Block a user