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:
Flavio Ribeiro
2024-02-04 10:09:31 -08:00
committed by GitHub
31 changed files with 1207 additions and 512 deletions

View File

@@ -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_FOLDER="/opt/srt_lib"
WORKDIR ${WD}
RUN apt-get clean && apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get install -y \
@@ -12,27 +14,20 @@ RUN \
mkdir -p "${SRT_FOLDER}" && \
git clone --depth 1 --branch "${SRT_VERSION}" https://github.com/Haivision/srt && \
cd srt && \
./configure --prefix=. $(configure) && \
./configure --prefix=${SRT_FOLDER} $(configure) && \
make && \
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
# find / -name srt.h
# find / -name libsrt.so
# inside the container docker run -it --rm -t <TAG_YOU_BUILT> bash
ENV GOPROXY=direct
ENV LD_LIBRARY_PATH="$LD_LIBRARY_PATH:/opt/srt/lib/"
ENV CGO_CFLAGS="-I/opt/srt/include/"
ENV CGO_LDFLAGS="-L/opt/srt/lib/"
ENV LD_LIBRARY_PATH="$LD_LIBRARY_PATH:${SRT_FOLDER}/lib/"
ENV CGO_CFLAGS="-I${SRT_FOLDER}/include/"
ENV CGO_LDFLAGS="-L${SRT_FOLDER}/lib/"
COPY . ./donut
WORKDIR ${WD}/donut
RUN go build .
CMD ["/usr/src/app/donut/donut", "--enable-ice-mux=true"]
CMD ["/usr/src/app/donut/donut", "--enable-ice-mux=true"]

22
Dockerfile-srt-live Normal file
View 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
View 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
```

View File

@@ -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`
### 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
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
@@ -39,57 +26,10 @@ Open [http://localhost:8080](http://localhost:8080). You will see three text box
![donut docker-compose setup](/.github/docker-compose-donut-setup.webp "donut docker-compose setup")
### 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:
```
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'
```
Please check the [FAQ](/FAQ.md) if you're facing any trouble.

View File

@@ -12,16 +12,19 @@ services:
- "8081:8081"
- "8081:8081/udp"
origin: # simulating an srt origin live transmission
image: jrottenberg/ffmpeg:4.4-alpine
srt:
build:
context: .
dockerfile: Dockerfile-srt-live
entrypoint: sh
command: "/scripts/ffmpeg_srt_live_listener.sh"
command: "./srt.sh"
working_dir: "/scripts"
volumes:
- "./scripts:/scripts"
environment:
- SRT_LISTENING_PORT=40052
- SRT_LISTENING_HOST=0.0.0.0
- SRT_LISTENING_LATENCY_US=400000
- SRT_UDP_TS_INPUT_HOST=0.0.0.0
- SRT_UDP_TS_INPUT_PORT=1234
ports:
- "40052:40052/udp"
depends_on:
@@ -29,3 +32,18 @@ services:
links:
- 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
View File

@@ -5,8 +5,10 @@ go 1.19
require (
github.com/asticode/go-astisrt v0.3.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/szatmary/gocaption v0.0.0-20220607192049-fdd59655f0c3
go.uber.org/fx v1.20.1
)
require (
@@ -28,6 +30,10 @@ require (
github.com/pion/transport v0.13.1 // indirect
github.com/pion/turn/v2 v2.0.8 // 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/net v0.2.0 // indirect
golang.org/x/sys v0.2.0 // indirect

24
go.sum
View File

@@ -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.36.0 h1:WHSY88YT76D/XRbdp0lMLwfjyUGw8dygnbKKtbGNIG8=
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/go.mod h1:tP5Dx+MXyaICUeF0gz4nwyav3RDI609e0en3QQkrxKE=
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/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.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
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/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
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/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
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/ice/v2 v2.2.11 h1:wiAy7TSrVZ4KdyjC0CcNTkwltz9ywetbe4wbHLKUbIg=
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.12 h1:CslaNriCFUItiXS5o+hh5lpL0t0ytQkFnUcbbCs2Zq8=
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/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.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.3 h1:LWcciN2ptLkw9Ugp/Ks2E76fiWy7yk3Wm79D6oFbFNo=
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/webrtc/v3 v3.1.47 h1:2dFEKRI1rzFvehXDq43hK9OGGyTGJSusUi3j6QKHC5s=
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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
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/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.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
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/go.mod h1:l9r7RYKHGLuHbXpKJhJgASvi8xT+Uqxnz9B26uVU73c=
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-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-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.2.0 h1:BRXPfhNivWL5Yq0BGQ39a2sW6t44aODpfxkWjYdzewE=
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-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-20221004154528-8021a29435af h1:wv66FM3rLZGPdxpYL+ApnDe2HzHcTFta3z5nsc13wI4=
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/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-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-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.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=

View File

@@ -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
}

View File

@@ -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")
}
}

View File

@@ -1,11 +1,10 @@
package eia608
package controllers
import (
"encoding/json"
"github.com/flavioribeiro/donut/h264"
"github.com/asticode/go-astits"
"github.com/flavioribeiro/donut/internal/entities"
gocaption "github.com/szatmary/gocaption"
)
@@ -13,25 +12,19 @@ type EIA608Reader struct {
frame gocaption.EIA608Frame
}
type Cue struct {
Type string
StartTime int64
Text string
}
func NewEIA608Reader() (r *EIA608Reader) {
return &EIA608Reader{}
}
func (r *EIA608Reader) Parse(PES *astits.PESData) (string, error) {
nalus, err := h264.ParseNALUs(PES.Data)
nalus, err := ParseNALUs(PES.Data)
if err != nil {
return "", err
}
for _, nal := range nalus.Units {
// 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
if nal.UnitType == h264.SupplementalEnhancementInformation && nal.SEI.PayloadType == 4 {
if nal.UnitType == entities.SupplementalEnhancementInformation && nal.SEI.PayloadType == 4 {
// ANSI/SCTE 128-1 2020
// Caption, AFD and bar data shall be carried in the SEI raw byte sequence payload (RBSP)
// 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) {
cue := Cue{
cue := entities.Cue{
StartTime: pts.Base,
Text: captions,
Type: "captions",

View 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
}

View 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
}

View 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
}
}
}

View 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
}

View 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"`
}

View 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")

View File

@@ -1,4 +1,4 @@
package h264
package entities
type NALUs struct {
Units []NAL
@@ -56,3 +56,60 @@ const (
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
View 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
}

View 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
View 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)
}
}

View 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>

View File

@@ -0,0 +1 @@
Initially copied from https://github.com/pion/webrtc/tree/master/examples/play-from-disk/jsfiddle

View 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))
}

View File

@@ -7,13 +7,13 @@
<body>
<b> SRT Host </b>
<input type="text" id="srt-host"> <br />
<input type="text" id="srt-host" value="srt"> <br />
<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>
<input type="text" id="srt-stream-id" /> <br />
<input type="text" id="srt-stream-id" value="stream-id" /> <br />
<button onclick="onConnect()"> Connect </button>
<donut-video server="http://localhost:8080" controls />

View 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(&params); 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
View 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
View 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
}

293
main.go
View File

@@ -4,269 +4,66 @@
package main
import (
"context"
_ "embed"
"encoding/json"
"flag"
"fmt"
"io"
"log"
"net"
"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/asticode/go-astits"
"github.com/pion/webrtc/v3"
"github.com/pion/webrtc/v3/pkg/media"
"github.com/kelseyhightower/envconfig"
"go.uber.org/fx"
"go.uber.org/zap"
)
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() {
enableICEMux := false
flag.BoolVar(&enableICEMux, "enable-ice-mux", false, "Enable ICE Mux on :8081")
flag.Parse()
mediaEngine := &webrtc.MediaEngine{}
settingEngine := webrtc.SettingEngine{}
if err := mediaEngine.RegisterDefaultCodecs(); err != nil {
log.Fatal(err)
var c entities.Config
err := envconfig.Process("donut", &c)
if err != nil {
log.Fatal(err.Error())
}
c.EnableICEMux = enableICEMux
if enableICEMux {
tcpListener, err := net.ListenTCP("tcp", &net.TCPAddr{
IP: net.IP{0, 0, 0, 0},
Port: 8081,
})
if err != nil {
log.Fatal(err)
}
fx.New(
// HTTP Server
fx.Provide(web.NewHTTPServer),
udpListener, err := net.ListenUDP("udp", &net.UDPAddr{
IP: net.IP{0, 0, 0, 0},
Port: 8081,
})
if err != nil {
log.Fatal(err)
}
// HTTP router
fx.Provide(web.NewServeMux),
settingEngine.SetNAT1To1IPs([]string{"127.0.0.1"}, webrtc.ICECandidateTypeHost)
settingEngine.SetICETCPMux(webrtc.NewICETCPMux(nil, tcpListener, 8))
settingEngine.SetICEUDPMux(webrtc.NewICEUDPMux(nil, udpListener))
}
api = webrtc.NewAPI(webrtc.WithSettingEngine(settingEngine), webrtc.WithMediaEngine(mediaEngine))
// HTTP handlers
fx.Provide(handlers.NewSignalingHandler),
fx.Provide(handlers.NewIndexHandler),
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
w.Write([]byte(indexHTML))
})
http.HandleFunc("/doSignaling", doSignaling)
// ICE mux servers
fx.Provide(controllers.NewTCPICEServer),
fx.Provide(controllers.NewUDPICEServer),
log.Println("Open http://localhost:8080 to access this demo")
log.Fatal(http.ListenAndServe(":8080", nil))
// Controllers
fx.Provide(controllers.NewSRTController),
fx.Provide(controllers.NewStreamingController),
fx.Provide(controllers.NewWebRTCController),
fx.Provide(controllers.NewWebRTCSettingsEngine),
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
View 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}"

View File

@@ -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
View 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