diff --git a/Dockerfile b/Dockerfile index de2e683..e4673ad 100644 --- a/Dockerfile +++ b/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_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 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"] \ No newline at end of file diff --git a/Dockerfile-srt-live b/Dockerfile-srt-live new file mode 100644 index 0000000..476e5e2 --- /dev/null +++ b/Dockerfile-srt-live @@ -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} diff --git a/FAQ.md b/FAQ.md new file mode 100644 index 0000000..d94658f --- /dev/null +++ b/FAQ.md @@ -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 + ^~~~~~~~~~~ +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 +``` diff --git a/README.md b/README.md index 0d222e2..9b8571a 100644 --- a/README.md +++ b/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` - -### 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 - ^~~~~~~~~~~ -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. \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml index 0d56f69..66c6671 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -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 + diff --git a/go.mod b/go.mod index 45ac6c6..7e9baac 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 611583d..1bfc9ae 100644 --- a/go.sum +++ b/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.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= diff --git a/h264/h264.go b/h264/h264.go deleted file mode 100644 index 72bdec0..0000000 --- a/h264/h264.go +++ /dev/null @@ -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 -} diff --git a/helpers.go b/helpers.go deleted file mode 100644 index 0cd07fc..0000000 --- a/helpers.go +++ /dev/null @@ -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") - } -} diff --git a/eia608/eia608.go b/internal/controllers/eia608_controller.go similarity index 82% rename from eia608/eia608.go rename to internal/controllers/eia608_controller.go index 3ec2873..dd86823 100644 --- a/eia608/eia608.go +++ b/internal/controllers/eia608_controller.go @@ -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", diff --git a/internal/controllers/h264_controller.go b/internal/controllers/h264_controller.go new file mode 100644 index 0000000..0306918 --- /dev/null +++ b/internal/controllers/h264_controller.go @@ -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 +} diff --git a/internal/controllers/srt_controller.go b/internal/controllers/srt_controller.go new file mode 100644 index 0000000..d846f12 --- /dev/null +++ b/internal/controllers/srt_controller.go @@ -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 +} diff --git a/internal/controllers/streaming_controller.go b/internal/controllers/streaming_controller.go new file mode 100644 index 0000000..05be21d --- /dev/null +++ b/internal/controllers/streaming_controller.go @@ -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 + } + } +} diff --git a/internal/controllers/webrtc_controller.go b/internal/controllers/webrtc_controller.go new file mode 100644 index 0000000..beb0d0e --- /dev/null +++ b/internal/controllers/webrtc_controller.go @@ -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 +} diff --git a/internal/entities/entities.go b/internal/entities/entities.go new file mode 100644 index 0000000..0afb91e --- /dev/null +++ b/internal/entities/entities.go @@ -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"` +} diff --git a/internal/entities/errors.go b/internal/entities/errors.go new file mode 100644 index 0000000..6b1512c --- /dev/null +++ b/internal/entities/errors.go @@ -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") diff --git a/h264/types.go b/internal/entities/h264.go similarity index 78% rename from h264/types.go rename to internal/entities/h264.go index 43b277e..a69a95c 100644 --- a/h264/types.go +++ b/internal/entities/h264.go @@ -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 +} diff --git a/internal/mapper/mapper.go b/internal/mapper/mapper.go new file mode 100644 index 0000000..10ee19c --- /dev/null +++ b/internal/mapper/mapper.go @@ -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 +} diff --git a/internal/web/demo/demo.css b/internal/web/demo/demo.css new file mode 100644 index 0000000..78566e9 --- /dev/null +++ b/internal/web/demo/demo.css @@ -0,0 +1,8 @@ +/* + SPDX-FileCopyrightText: 2023 The Pion community + SPDX-License-Identifier: MIT +*/ +textarea { + width: 500px; + min-height: 75px; +} \ No newline at end of file diff --git a/internal/web/demo/demo.js b/internal/web/demo/demo.js new file mode 100644 index 0000000..1ea6c5d --- /dev/null +++ b/internal/web/demo/demo.js @@ -0,0 +1,67 @@ +/* eslint-env browser */ + +// SPDX-FileCopyrightText: 2023 The Pion community +// SPDX-License-Identifier: MIT + +const pc = new RTCPeerConnection({ + iceServers: [{ + urls: 'stun:stun.l.google.com:19302' + }] + }) + const log = msg => { + document.getElementById('div').innerHTML += msg + '
' + } + + 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) + } + } \ No newline at end of file diff --git a/internal/web/demo/index.html b/internal/web/demo/index.html new file mode 100644 index 0000000..d73d0e1 --- /dev/null +++ b/internal/web/demo/index.html @@ -0,0 +1,30 @@ + +Browser Session Description +
+ +
+ + + +
+
+
+ +Remote Session Description +
+ +
+ +
+
+ +Video +
+

+ +Logs +
+
\ No newline at end of file diff --git a/internal/web/demo/readme.txt b/internal/web/demo/readme.txt new file mode 100644 index 0000000..b29ccca --- /dev/null +++ b/internal/web/demo/readme.txt @@ -0,0 +1 @@ +Initially copied from https://github.com/pion/webrtc/tree/master/examples/play-from-disk/jsfiddle \ No newline at end of file diff --git a/internal/web/handlers/index.go b/internal/web/handlers/index.go new file mode 100644 index 0000000..1c828e0 --- /dev/null +++ b/internal/web/handlers/index.go @@ -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)) +} diff --git a/index.html b/internal/web/handlers/index.html similarity index 89% rename from index.html rename to internal/web/handlers/index.html index 4c13ac1..fd3c681 100644 --- a/index.html +++ b/internal/web/handlers/index.html @@ -7,13 +7,13 @@ SRT Host -
+
SRT Port -
+
SRT Stream ID -
+
diff --git a/internal/web/handlers/signaling.go b/internal/web/handlers/signaling.go new file mode 100644 index 0000000..17f054f --- /dev/null +++ b/internal/web/handlers/signaling.go @@ -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 +} diff --git a/internal/web/router.go b/internal/web/router.go new file mode 100644 index 0000000..b708486 --- /dev/null +++ b/internal/web/router.go @@ -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 + } + }) +} diff --git a/internal/web/server.go b/internal/web/server.go new file mode 100644 index 0000000..8fbe036 --- /dev/null +++ b/internal/web/server.go @@ -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 +} diff --git a/main.go b/main.go index 630af56..8f09eac 100644 --- a/main.go +++ b/main.go @@ -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() } diff --git a/scripts/ffmpeg_mpegts_udp.sh b/scripts/ffmpeg_mpegts_udp.sh new file mode 100755 index 0000000..a601c4c --- /dev/null +++ b/scripts/ffmpeg_mpegts_udp.sh @@ -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}" \ No newline at end of file diff --git a/scripts/ffmpeg_srt_live_listener.sh b/scripts/ffmpeg_srt_live_listener.sh deleted file mode 100755 index 0631f6e..0000000 --- a/scripts/ffmpeg_srt_live_listener.sh +++ /dev/null @@ -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}" - diff --git a/scripts/srt.sh b/scripts/srt.sh new file mode 100755 index 0000000..638e375 --- /dev/null +++ b/scripts/srt.sh @@ -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 \ No newline at end of file