diff --git a/Dockerfile-dev b/Dockerfile-dev index 7d7ef8a..12b143d 100644 --- a/Dockerfile-dev +++ b/Dockerfile-dev @@ -9,9 +9,10 @@ ENV LD_LIBRARY_PATH="/usr/local/lib:/usr/lib:/usr/lib/x86_64-linux-gnu/" ENV CGO_CFLAGS="-I/usr/local/include/" ENV CGO_LDFLAGS="-L/usr/local/lib" +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 + ENV WD=/usr/src/app -WORKDIR ${WD} - -ENV GOPROXY=direct - -RUN curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.55.2 \ No newline at end of file +WORKDIR ${WD} \ No newline at end of file diff --git a/FAQ.md b/FAQ.md index e4f5638..8bf73b4 100644 --- a/FAQ.md +++ b/FAQ.md @@ -52,76 +52,9 @@ You can try to use the [docker-compose](/README.md#run-using-docker-compose), bu CGO_LDFLAGS="-L$(brew --prefix srt)/lib -lsrt" CGO_CFLAGS="-I$(brew --prefix srt)/include/" go run main.go ``` -## If you're seeing the error "could not determine kind of name for C.AV_CODEC" - -Make sure you're using ffmpeg `"n5.1.2"` (via `make install-ffmpeg`), go-astiav@v0.12.0 only supports ffmpeg 5.0. - -``` -../../go/pkg/mod/github.com/asticode/go-astiav@v0.12.0/codec_context_flag.go:38:50: could not determine kind of name for C.AV_CODEC_FLAG2_DROP_FRAME_TIMECODE -../../go/pkg/mod/github.com/asticode/go-astiav@v0.12.0/codec_context_flag.go:21:51: could not determine kind of name for C.AV_CODEC_FLAG_TRUNCATED -``` - -## If you're seeing the error "issue /usr/bin/ld: skipping incompatible lib.so when searching for -lavdevice" - -Fixing the docker platform fixed the problem. Even though the configured platform is amd64, the final objects are x64, don't know why yet. - -``` -# The tools to check the compiled objects format: -find / -name libsrt.so # to find the objects -objdump -a /opt/srt_lib/lib/libsrt.so -objdump -a /usr/local/lib/libavformat.so -``` - -Fixing the platform. - -Dockerfile -```Dockerfile -FROM --platform=linux/amd64 jrottenberg/ffmpeg:5.1.2-ubuntu2004 AS base -``` - -docker-compose.yml -```yaml -platform: "linux/amd64" -``` - -## If you're seeing the error "checkptr: converted pointer straddles multiple allocations" when using -race - -When the app runs using `go build -race` it stops with the error "converted pointer straddles multiple allocations". I tried to upgrade the golang image but it didn't work, so I remove the `-race` from building. - -``` -srt-1 | connected. -srt-1 | Accepted SRT target connection -app-1 | fatal error: checkptr: converted pointer straddles multiple allocations -app-1 | -app-1 | goroutine 68 [running]: -app-1 | runtime.throw({0xe57eb2?, 0xc00003f17c?}) -app-1 | /usr/local/go/src/runtime/panic.go:1047 +0x5d fp=0xc0001d7700 sp=0xc0001d76d0 pc=0x44febd -app-1 | runtime.checkptrAlignment(0xc00031840d?, 0x3?, 0x7ffffa869c74?) -app-1 | /usr/local/go/src/runtime/checkptr.go:26 +0x6c fp=0xc0001d7720 sp=0xc0001d7700 pc=0x41eacc -app-1 | github.com/asticode/go-astisrt/pkg.(*Socket).Connect(0xc00003e150, {0xc00031840d, 0x3}, 0xc0b0?) -app-1 | /go/pkg/mod/github.com/asticode/go-astisrt@v0.3.0/pkg/socket.go:85 +0x245 fp=0xc0001d77a0 sp=0xc0001d7720 pc=0xc58ce5 -app-1 | github.com/asticode/go-astisrt/pkg.Dial({{0xc000232ba0, 0x4, 0x4}, {0xc00031840d, 0x3}, 0xc0001c4060, 0x9c74}) -app-1 | /go/pkg/mod/github.com/asticode/go-astisrt@v0.3.0/pkg/client.go:53 +0x445 fp=0xc0001d78d8 sp=0xc0001d77a0 pc=0xc55925 -app-1 | github.com/flavioribeiro/donut/internal/controllers/streamers.(*SRTMpegTSStreamer).connect(0xc0002aae40, 0xc0000f4550, 0xc00010c050) -app-1 | /usr/src/app/donut/internal/controllers/streamers/srt_mpegts.go:161 +0x819 fp=0xc0001d7b00 sp=0xc0001d78d8 pc=0xc61bf9 -app-1 | github.com/flavioribeiro/donut/internal/controllers/streamers.(*SRTMpegTSStreamer).Stream(0xc0002aae40, 0xc000100540) -app-1 | /usr/src/app/donut/internal/controllers/streamers/srt_mpegts.go:55 +0xa9 fp=0xc0001d7fa8 sp=0xc0001d7b00 pc=0xc5fa29 -``` - -ref https://github.com/golang/go/issues/54690 - ## If you're seeing the error "At least one invalid signature was encountered ... GPG error: http://security." when running the app -If you see the error "At least one invalid signature was encountered." when running `make run`, please try to run: - -``` -docker-compose down -v --rmi all --remove-orphans && docker volume prune -a -f && docker system prune -a -f && docker builder prune -a -f - -# make sure to check if it was cleaned properly -docker system df -``` - -Then, uncomment the `Makefile#run` commented lines, and try again. +If you see the error "At least one invalid signature was encountered." when running `make run` Or "failed to copy files: userspace copy failed: write": ``` 3.723 W: GPG error: http://deb.debian.org/debian bookworm InRelease: At least one invalid signature was encountered. @@ -134,10 +67,6 @@ Then, uncomment the `Makefile#run` commented lines, and try again. 3.723 W: An error occurred during the signature verification. The repository is not updated and the previous index files will be used. GPG error: http://security.ubuntu.com/ubuntu focal-security InRelease: At least one invalid signature was encountered. ``` -## If you're seeing the error "failed to copy files: userspace copy failed: write" when running the app - -If you see the error "failed to copy files: userspace copy failed: write" when running `make run`, please run `docker system prune` and try again. - ``` => CANCELED [test stage-1 6/6] RUN curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.55.2 4.4s => ERROR [app stage-1 6/8] COPY . ./donut 4.1s @@ -146,4 +75,11 @@ If you see the error "failed to copy files: userspace copy failed: write" when r ------ failed to solve: failed to copy files: userspace copy failed: write /var/lib/docker/overlay2/30zm6uywrtfed4z4wfzbf1ema/merged/usr/src/app/donut/tmp/n5.1.2/src/tests/reference.pnm: no space left on device make: *** [run] Error 17 +``` + +Please try to run: + +``` +# PLEASE be aware that the following command will erase all your docker images, containers, volumes, etc. +make clean-docker ``` \ No newline at end of file diff --git a/HOW_IT_WORKS.md b/HOW_IT_WORKS.md index 5e40096..0ef9fa8 100644 --- a/HOW_IT_WORKS.md +++ b/HOW_IT_WORKS.md @@ -8,25 +8,65 @@ sequenceDiagram participant browser end - User->>+browser: feed protocol, host, port, id, and opts + User->>+browser: input protocol, host, port, id, and opts User->>+browser: click on [Connect] - Note over server,browser: WebRTC connection setup + Note over donut,browser: WebRTC connection setup browser->>+browser: create WebRTC browserOffer - browser->>+server: POST /doSignaling {browserOffer} + browser->>+donut: POST /doSignaling {browserOffer} + + donut->>+browser: reply WebRTC {serverOffer} + + Note over donut,browser: WebRTC connection setup loop Async streaming - server--)streaming server: fetchMedia - server--)server: ffmpeg::libav demux/transcode - server--)browser: sendWebRTCMedia + donut--)streaming server: fetchMedia + donut--)donut: ffmpeg::libav demux/transcode + donut--)browser: sendWebRTCMedia + browser--)browser: render audio/video frames + User--)browser: watch media end - - server->>+browser: reply WebRTC {serverOffer} - - Note over server,browser: WebRTC connection setup - - browser--)User: render audio/video frames ``` -# Architecture \ No newline at end of file +# Core components + +```mermaid +classDiagram + class Signaling{ + +ServeHTTP() + } + + class WebRTC{ + +Setup() + +CreatePeerConnection() + +CreateTrack() + +CreateDataChannel() + +SendMediaSample(track) + +SendMetadata(track) + } + + class DonutEngine{ + +EngineFor(params) + +ServerIngredients() + +ClientIngredients() + +RecipeFor(server, client) + +Serve(donutParams) + +Appetizer() + } + + class Prober { + +StreamInfo(appetizer) + +Match(params) + } + + class Streamer { + +Stream(donutParams) + +Match(params) + } + + DonutEngine *-- Signaling + WebRTC *-- Signaling + Prober *-- DonutEngine + Streamer *-- DonutEngine +``` \ No newline at end of file diff --git a/Makefile b/Makefile index 6f3b08b..f322419 100644 --- a/Makefile +++ b/Makefile @@ -1,30 +1,20 @@ run: - # in case you need to re-build it, uncomment the following line - # docker compose stop && docker compose down && docker compose build && docker compose up origin srt app - docker compose stop && docker compose build app && docker compose up origin srt app + docker compose stop && docker compose up app -test: - # in case you need to re-build it, uncomment the following line - # docker compose stop test && docker compose down test && docker compose build test && docker compose run --rm test - docker compose stop test && docker compose down test && docker compose run --rm test +run-dev: + docker compose stop && docker compose down && docker compose build app && docker compose up app -run-srt: - docker compose stop && docker compose down && docker compose build srt && docker compose up srt +run-dev-total-rebuild: + docker compose stop && docker compose down && docker compose build && docker compose up app -mac-run-local: - ./scripts/mac_local_run.sh +clean-docker: + docker-compose down -v --rmi all --remove-orphans && docker volume prune -a -f && docker system prune -a -f && docker builder prune -a -f -mac-test-local: - ./scripts/mac_local_run_test.sh +run-docker-dev: + docker compose run --rm --service-ports dev -html-local-coverage: - go tool cover -html=coverage.out +run-server-inside-docker: + go run main.go -- --enable-ice-mux=true lint: docker compose stop lint && docker compose down lint && docker compose run --rm lint - -# INCOMPLETE from https://github.com/asticode/go-astiav/blob/master/Makefile -install-ffmpeg: - ./scripts/install_local_ffmpeg.sh - -.PHONY: run lint test run-srt mac-run-local mac-test-local html-local-coverage install-ffmpeg diff --git a/docker-compose.yaml b/docker-compose.yaml index 4e5bebf..f47f71e 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -12,9 +12,86 @@ services: - "8081:8081/udp" - "6060:6060" depends_on: - - srt + - haivision_srt + - nginx_rtmp links: - - srt + - haivision_srt + - nginx_rtmp + + dev: + build: + context: . + dockerfile: Dockerfile-dev + working_dir: "/app" + platform: "linux/amd64" + volumes: + - "./:/app/" + command: "bash" + ports: + - "8080:8080" + - "8081:8081" + - "8081:8081/udp" + - "6060:6060" + depends_on: + - haivision_srt + - nginx_rtmp + links: + - haivision_srt + - nginx_rtmp + + nginx_rtmp: + image: alfg/nginx-rtmp + ports: + - "1935:1935" + volumes: + - ./nginx.conf:/etc/nginx/nginx.conf.template + depends_on: + - origin_rtmp + links: + - origin_rtmp + + origin_rtmp: # simulating an RTMP flv (h264/aac) live transmission + image: jrottenberg/ffmpeg:4.4-alpine + entrypoint: sh + command: "/scripts/ffmpeg_rtmp.sh" + volumes: + - "./scripts:/scripts" + - "./fonts/0xProto:/usr/share/fonts" + environment: + - RTMP_HOST=nginx_rtmp + - RTMP_PORT=1935 + + haivision_srt: + build: + context: . + dockerfile: Dockerfile-srt-live + entrypoint: sh + command: "./srt.sh" + working_dir: "/scripts" + volumes: + - "./scripts:/scripts" + environment: + - SRT_LISTENING_PORT=40052 + - SRT_UDP_TS_INPUT_HOST=0.0.0.0 + - SRT_UDP_TS_INPUT_PORT=1234 + ports: + - "40052:40052/udp" + depends_on: + - origin_srt + links: + - origin_srt + + origin_srt: # simulating an (h264/aac) mpeg-ts upd origin live transmission + image: jrottenberg/ffmpeg:4.4-alpine + entrypoint: sh + command: "/scripts/ffmpeg_mpegts_udp.sh" + volumes: + - "./scripts:/scripts" + - "./fonts/0xProto:/usr/share/fonts" + environment: + - SRT_INPUT_HOST=haivision_srt + - SRT_INPUT_PORT=1234 + - PKT_SIZE=1316 test: build: @@ -34,36 +111,4 @@ services: platform: "linux/amd64" volumes: - "./:/app/" - command: "golangci-lint run -v" - - srt: - build: - context: . - dockerfile: Dockerfile-srt-live - entrypoint: sh - command: "./srt.sh" - working_dir: "/scripts" - volumes: - - "./scripts:/scripts" - environment: - - SRT_LISTENING_PORT=40052 - - SRT_UDP_TS_INPUT_HOST=0.0.0.0 - - SRT_UDP_TS_INPUT_PORT=1234 - ports: - - "40052:40052/udp" - depends_on: - - origin - links: - - origin - - 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 - + command: "golangci-lint run -v" \ No newline at end of file diff --git a/fonts/0xProto/0xProtoNerdFont-Regular.ttf b/fonts/0xProto/0xProtoNerdFont-Regular.ttf new file mode 100644 index 0000000..5089bb4 Binary files /dev/null and b/fonts/0xProto/0xProtoNerdFont-Regular.ttf differ diff --git a/fonts/0xProto/0xProtoNerdFontMono-Regular.ttf b/fonts/0xProto/0xProtoNerdFontMono-Regular.ttf new file mode 100644 index 0000000..b642c07 Binary files /dev/null and b/fonts/0xProto/0xProtoNerdFontMono-Regular.ttf differ diff --git a/fonts/0xProto/0xProtoNerdFontPropo-Regular.ttf b/fonts/0xProto/0xProtoNerdFontPropo-Regular.ttf new file mode 100644 index 0000000..7fd6afa Binary files /dev/null and b/fonts/0xProto/0xProtoNerdFontPropo-Regular.ttf differ diff --git a/fonts/0xProto/LICENSE b/fonts/0xProto/LICENSE new file mode 100644 index 0000000..fefebe8 --- /dev/null +++ b/fonts/0xProto/LICENSE @@ -0,0 +1,92 @@ +Copyright (c) 2023, 0xType Project Authors (https://github.com/0xType) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/fonts/0xProto/README.md b/fonts/0xProto/README.md new file mode 100644 index 0000000..f85d010 --- /dev/null +++ b/fonts/0xProto/README.md @@ -0,0 +1,48 @@ +# Nerd Fonts + +This is an archived font from the Nerd Fonts release v3.2.1. + +For more information see: +* https://github.com/ryanoasis/nerd-fonts/ +* https://github.com/ryanoasis/nerd-fonts/releases/latest/ + +# 0xProto + +An opinionated font for software engineers. + +For more information have a look at the upstream website: https://github.com/0xType/0xProto + +Version: 1.603 + +## Which font? + +### TL;DR + +* Pick your font family: + * If you are limited to monospaced fonts (because of your terminal, etc) then pick a font with `Nerd Font Mono` (or `NFM`). + * If you want to have bigger icons (usually around 1.5 normal letters wide) pick a font without `Mono` i.e. `Nerd Font` (or `NF`). Most terminals support this, but ymmv. + * If you work in a proportional context (GUI elements or edit a presentation etc) pick a font with `Nerd Font Propo` (or `NFP`). + +### Ligatures + +Ligatures are generally preserved in the patched fonts. +Nerd Fonts `v2.0.0` had no ligatures in the `Nerd Font Mono` fonts, this has been dropped with `v2.1.0`. +If you have a ligature-aware terminal and don't want ligatures you can (usually) disable them in the terminal settings. + +### Explanation + +Once you narrow down your font choice of family (`Droid Sans`, `Inconsolata`, etc) and style (`bold`, `italic`, etc) you have 2 main choices: + +#### `Option 1: Download already patched font` + + * For a stable version download a font package from the [release page](https://github.com/ryanoasis/nerd-fonts/releases) + * Direct links for [0xProto.zip](https://github.com/ryanoasis/nerd-fonts/releases/latest/download/0xProto.zip) or [0xProto.tar.xz](https://github.com/ryanoasis/nerd-fonts/releases/latest/download/0xProto.tar.xz) + +#### `Option 2: Patch your own font` + + * Patch your own variations with the various options provided by the font patcher (i.e. not include all symbols for smaller font size) + +For more information see: [The FAQ](https://github.com/ryanoasis/nerd-fonts/wiki/FAQ-and-Troubleshooting#which-font) + +[SIL-RFN]:http://scripts.sil.org/cms/scripts/page.php?item_id=OFL_web_fonts_and_RFNs#14cbfd4a + diff --git a/go.mod b/go.mod index 7bb4c57..412216a 100644 --- a/go.mod +++ b/go.mod @@ -3,8 +3,8 @@ module github.com/flavioribeiro/donut go 1.19 require ( - github.com/asticode/go-astiav v0.12.0 - github.com/asticode/go-astikit v0.36.0 + github.com/asticode/go-astiav v0.14.2-0.20240514161420-d8844951c978 + github.com/asticode/go-astikit v0.42.0 github.com/kelseyhightower/envconfig v1.4.0 github.com/pion/webrtc/v3 v3.1.47 github.com/stretchr/testify v1.8.0 diff --git a/go.sum b/go.sum index 4f0b6d9..807bc40 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,7 @@ -github.com/asticode/go-astiav v0.12.0 h1:tETfPhVpJrSyh3zvUOmDvebFaCoFpeATSaQAA7B50J8= -github.com/asticode/go-astiav v0.12.0/go.mod h1:phvUnSSlV91S/PELeLkDisYiRLOssxWOsj4oDrqM/54= -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-astiav v0.14.2-0.20240514161420-d8844951c978 h1:+xACJz51oNEvxrhrHsvGNn16n/vuLmjtvp93LS6onTQ= +github.com/asticode/go-astiav v0.14.2-0.20240514161420-d8844951c978/go.mod h1:K7D8UC6GeQt85FUxk2KVwYxHnotrxuEnp5evkkudc2s= +github.com/asticode/go-astikit v0.42.0 h1:pnir/2KLUSr0527Tv908iAH6EGYYrYta132vvjXsH5w= +github.com/asticode/go-astikit v0.42.0/go.mod h1:h4ly7idim1tNhaVkdVBeXQZEE3L0xblP7fCWbgwipF0= 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= diff --git a/internal/controllers/engine/donut_engine_controller.go b/internal/controllers/engine/donut_engine_controller.go index a84e5ff..da1511a 100644 --- a/internal/controllers/engine/donut_engine_controller.go +++ b/internal/controllers/engine/donut_engine_controller.go @@ -2,6 +2,7 @@ package engine import ( "fmt" + "strings" "github.com/flavioribeiro/donut/internal/controllers/probers" "github.com/flavioribeiro/donut/internal/controllers/streamers" @@ -11,10 +12,10 @@ import ( ) type DonutEngine interface { - Appetizer() entities.DonutAppetizer + Appetizer() (entities.DonutAppetizer, error) ServerIngredients() (*entities.StreamInfo, error) ClientIngredients() (*entities.StreamInfo, error) - RecipeFor(server, client *entities.StreamInfo) *entities.DonutRecipe + RecipeFor(server, client *entities.StreamInfo) (*entities.DonutRecipe, error) Serve(p *entities.DonutParameters) } @@ -36,12 +37,12 @@ func NewDonutEngineController(p DonutEngineParams) *DonutEngineController { func (c *DonutEngineController) EngineFor(req *entities.RequestParams) (DonutEngine, error) { prober := c.selectProberFor(req) if prober == nil { - return nil, fmt.Errorf("request %v: not fulfilled error %w", req, entities.ErrMissingProber) + return nil, fmt.Errorf("request %v: not fulfilled. error %w", req, entities.ErrMissingProber) } streamer := c.selectStreamerFor(req) if streamer == nil { - return nil, fmt.Errorf("request %v: not fulfilled error %w", req, entities.ErrMissingStreamer) + return nil, fmt.Errorf("request %v: not fulfilled. error %w", req, entities.ErrMissingStreamer) } return &donutEngine{ @@ -80,7 +81,11 @@ type donutEngine struct { } func (d *donutEngine) ServerIngredients() (*entities.StreamInfo, error) { - return d.prober.StreamInfo(d.Appetizer()) + appetizer, err := d.Appetizer() + if err != nil { + return nil, err + } + return d.prober.StreamInfo(appetizer) } func (d *donutEngine) ClientIngredients() (*entities.StreamInfo, error) { @@ -91,7 +96,7 @@ func (d *donutEngine) Serve(p *entities.DonutParameters) { d.streamer.Stream(p) } -func (d *donutEngine) RecipeFor(server, client *entities.StreamInfo) *entities.DonutRecipe { +func (d *donutEngine) RecipeFor(server, client *entities.StreamInfo) (*entities.DonutRecipe, error) { // TODO: implement proper matching // // suggestions: @@ -101,39 +106,57 @@ func (d *donutEngine) RecipeFor(server, client *entities.StreamInfo) *entities.D // preferable = [vp8, opus] // if union(preferable, client.medias) // transcode, preferable + appetizer, err := d.Appetizer() + if err != nil { + return nil, err + } + r := &entities.DonutRecipe{ - Input: d.Appetizer(), + Input: appetizer, Video: entities.DonutMediaTask{ - Action: entities.DonutBypass, - Codec: entities.H264, + Action: entities.DonutBypass, + Codec: entities.H264, + DonutBitStreamFilter: &entities.DonutH264AnnexB, }, Audio: entities.DonutMediaTask{ Action: entities.DonutTranscode, Codec: entities.Opus, // TODO: create method list options per Codec CodecContextOptions: []entities.LibAVOptionsCodecContext{ - // opus specifically works under 48000 Hz entities.SetSampleRate(48000), - // once we changed the sample rate we need to update the time base - entities.SetTimeBase(1, 48000), - // for some reason it's setting "s16" - // entities.SetSampleFormat("fltp"), + entities.SetSampleFormat("fltp"), }, }, } - return r + return r, nil } -func (d *donutEngine) Appetizer() entities.DonutAppetizer { - // TODO: implement input based on param to build proper SRT/RTMP/etc - return entities.DonutAppetizer{ - URL: fmt.Sprintf("srt://%s:%d", d.req.SRTHost, d.req.SRTPort), - Format: "mpegts", // it'll change based on input, i.e. rmtp flv - Options: map[entities.DonutInputOptionKey]string{ - entities.DonutSRTStreamID: d.req.SRTStreamID, - entities.DonutSRTTranstype: "live", - entities.DonutSRTsmoother: "live", - }, +func (d *donutEngine) Appetizer() (entities.DonutAppetizer, error) { + isRTMP := strings.Contains(strings.ToLower(d.req.StreamURL), "rtmp") + isSRT := strings.Contains(strings.ToLower(d.req.StreamURL), "srt") + + if isRTMP { + return entities.DonutAppetizer{ + URL: fmt.Sprintf("%s/%s", d.req.StreamURL, d.req.StreamID), + Options: map[entities.DonutInputOptionKey]string{ + entities.DonutRTMPLive: "live", + }, + Format: "flv", + }, nil } + + if isSRT { + return entities.DonutAppetizer{ + URL: d.req.StreamURL, + Format: "mpegts", // TODO: check how to get format for srt + Options: map[entities.DonutInputOptionKey]string{ + entities.DonutSRTStreamID: d.req.StreamID, + entities.DonutSRTTranstype: "live", + entities.DonutSRTsmoother: "live", + }, + }, nil + } + + return entities.DonutAppetizer{}, entities.ErrUnsupportedStreamURL } diff --git a/internal/controllers/probers/libav_ffmpeg.go b/internal/controllers/probers/libav_ffmpeg.go index b461f5e..f8305fb 100644 --- a/internal/controllers/probers/libav_ffmpeg.go +++ b/internal/controllers/probers/libav_ffmpeg.go @@ -2,6 +2,7 @@ package probers import ( "fmt" + "strings" "github.com/asticode/go-astiav" "github.com/asticode/go-astikit" @@ -39,7 +40,10 @@ func NewLibAVFFmpeg( // Match returns true when the request is for an LibAVFFmpeg prober func (c *LibAVFFmpeg) Match(req *entities.RequestParams) bool { - return req.SRTHost != "" + isRTMP := strings.Contains(strings.ToLower(req.StreamURL), "rtmp") + isSRT := strings.Contains(strings.ToLower(req.StreamURL), "srt") + + return isRTMP || isSRT } // StreamInfo connects to the SRT stream to discovery media properties. @@ -60,7 +64,7 @@ func (c *LibAVFFmpeg) StreamInfo(req entities.DonutAppetizer) (*entities.StreamI inputOptions := c.defineInputOptions(req.Options, closer) if err := inputFormatContext.OpenInput(req.URL, inputFormat, inputOptions); err != nil { - return nil, fmt.Errorf("error while inputFormatContext.OpenInput: %w", err) + return nil, fmt.Errorf("error while inputFormatContext.OpenInput: (%s, %#v, %#v) %w", req.URL, inputFormat, inputOptions, err) } closer.Add(inputFormatContext.CloseInput) diff --git a/internal/controllers/streamers/libav_ffmpeg.go b/internal/controllers/streamers/libav_ffmpeg.go index a60727a..cfbc676 100644 --- a/internal/controllers/streamers/libav_ffmpeg.go +++ b/internal/controllers/streamers/libav_ffmpeg.go @@ -4,8 +4,6 @@ import ( "context" "errors" "fmt" - "reflect" - "runtime" "strconv" "strings" "time" @@ -50,7 +48,10 @@ func NewLibAVFFmpegStreamer(p LibAVFFmpegStreamerParams) ResultLibAVFFmpegStream } func (c *LibAVFFmpegStreamer) Match(req *entities.RequestParams) bool { - return req.SRTHost != "" + isRTMP := strings.Contains(strings.ToLower(req.StreamURL), "rtmp") + isSRT := strings.Contains(strings.ToLower(req.StreamURL), "srt") + + return isRTMP || isSRT } type streamContext struct { @@ -70,6 +71,10 @@ type streamContext struct { encCodec *astiav.Codec encCodecContext *astiav.CodecContext encPkt *astiav.Packet + + // Bit stream filter + bsfContext *astiav.BitStreamFilterContext + bsfPacket *astiav.Packet } type libAVParams struct { @@ -78,7 +83,7 @@ type libAVParams struct { } func (c *LibAVFFmpegStreamer) Stream(donut *entities.DonutParameters) { - c.l.Infow("streaming has started") + c.l.Infof("streaming has started for %#v", donut) closer := astikit.NewCloser() defer closer.Close() @@ -88,26 +93,36 @@ func (c *LibAVFFmpegStreamer) Stream(donut *entities.DonutParameters) { } // it's useful for debugging - astiav.SetLogLevel(astiav.LogLevelDebug) - astiav.SetLogCallback(func(l astiav.LogLevel, fmt, msg, parent string) { + // astiav.SetLogLevel(astiav.LogLevelDebug) + astiav.SetLogLevel(astiav.LogLevelInfo) + astiav.SetLogCallback(func(_ astiav.Classer, l astiav.LogLevel, fmt, msg string) { c.l.Infof("ffmpeg %s: - %s", c.libAVLogToString(l), strings.TrimSpace(msg)) }) + c.l.Infof("preparing input") if err := c.prepareInput(p, closer, donut); err != nil { c.onError(err, donut) return } + c.l.Infof("preparing output") if err := c.prepareOutput(p, closer, donut); err != nil { c.onError(err, donut) return } + c.l.Infof("preparing filters") if err := c.prepareFilters(p, closer, donut); err != nil { c.onError(err, donut) return } + c.l.Infof("preparing bit stream filters") + if err := c.prepareBitStreamFilters(p, closer, donut); err != nil { + c.onError(err, donut) + return + } + inPkt := astiav.AllocPacket() closer.Add(inPkt.Free) @@ -131,63 +146,22 @@ func (c *LibAVFFmpegStreamer) Stream(donut *entities.DonutParameters) { s, ok := p.streams[inPkt.StreamIndex()] if !ok { - c.l.Warnf("cannot find stream id=%d", inPkt.StreamIndex()) + c.l.Warnf("skipping to process stream id=%d", inPkt.StreamIndex()) continue } - inPkt.RescaleTs(s.inputStream.TimeBase(), s.decCodecContext.TimeBase()) - - isVideo := s.decCodecContext.MediaType() == astiav.MediaTypeVideo - isVideoBypass := donut.Recipe.Video.Action == entities.DonutBypass - if isVideo && isVideoBypass { - if donut.OnVideoFrame != nil { - if err := donut.OnVideoFrame(inPkt.Data(), entities.MediaFrameContext{ - PTS: int(inPkt.Pts()), - DTS: int(inPkt.Dts()), - Duration: c.defineVideoDuration(s, inPkt), - }); err != nil { - c.onError(err, donut) - return - } - } - continue - } - - isAudio := s.decCodecContext.MediaType() == astiav.MediaTypeAudio - isAudioBypass := donut.Recipe.Audio.Action == entities.DonutBypass - if isAudio && isAudioBypass { - if donut.OnAudioFrame != nil { - if err := donut.OnAudioFrame(inPkt.Data(), entities.MediaFrameContext{ - PTS: int(inPkt.Pts()), - DTS: int(inPkt.Dts()), - Duration: c.defineAudioDuration(s, inPkt), - }); err != nil { - c.onError(err, donut) - return - } - } - continue - } - - if err := s.decCodecContext.SendPacket(inPkt); err != nil { - c.onError(err, donut) - return - } - - for { - if err := s.decCodecContext.ReceiveFrame(s.decFrame); err != nil { - if errors.Is(err, astiav.ErrEof) || errors.Is(err, astiav.ErrEagain) { - break - } + if s.bsfContext != nil { + if err := c.applyBitStreamFilter(p, inPkt, s, donut); err != nil { c.onError(err, donut) return } - - if err := c.filterAndEncode(s.decFrame, s, donut); err != nil { + } else { + if err := c.processPacket(p, inPkt, s, donut); err != nil { c.onError(err, donut) return } } + inPkt.Unref() } } } @@ -240,6 +214,9 @@ func (c *LibAVFFmpegStreamer) prepareInput(p *libAVParams, closer *astikit.Close return fmt.Errorf("ffmpeg/libav: updating codec context failed %w", err) } + //FFMPEG_NEW + s.decCodecContext.SetTimeBase(s.inputStream.TimeBase()) + if is.CodecParameters().MediaType() == astiav.MediaTypeVideo { s.decCodecContext.SetFramerate(p.inputFormatContext.GuessFrameRate(is, nil)) } @@ -264,17 +241,11 @@ func (c *LibAVFFmpegStreamer) prepareInput(p *libAVParams, closer *astikit.Close return nil } -func functionNameFor(i interface{}) string { - fullName := runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name() - components := strings.Split(fullName, ".") - return components[len(components)-2] -} - func (c *LibAVFFmpegStreamer) prepareOutput(p *libAVParams, closer *astikit.Closer, donut *entities.DonutParameters) error { for _, is := range p.inputFormatContext.Streams() { s, ok := p.streams[is.Index()] if !ok { - c.l.Infof("skipping stream index = %d", is.Index()) + c.l.Infof("skipping absent stream index = %d", is.Index()) continue } @@ -333,10 +304,9 @@ func (c *LibAVFFmpegStreamer) prepareOutput(p *libAVParams, closer *astikit.Clos } s.encCodecContext.SetTimeBase(s.decCodecContext.TimeBase()) - // supplying custom config + // overriding with user provide config if len(donut.Recipe.Audio.CodecContextOptions) > 0 { for _, opt := range donut.Recipe.Audio.CodecContextOptions { - c.l.Infof("overriding av codec context %s", functionNameFor(opt)) opt(s.encCodecContext) } } @@ -352,12 +322,11 @@ func (c *LibAVFFmpegStreamer) prepareOutput(p *libAVParams, closer *astikit.Clos s.encCodecContext.SetTimeBase(s.decCodecContext.TimeBase()) s.encCodecContext.SetHeight(s.decCodecContext.Height()) s.encCodecContext.SetWidth(s.decCodecContext.Width()) - s.encCodecContext.SetFramerate(s.inputStream.AvgFrameRate()) + // s.encCodecContext.SetFramerate(s.inputStream.AvgFrameRate()) - // supplying custom config + // overriding with user provide config if len(donut.Recipe.Video.CodecContextOptions) > 0 { for _, opt := range donut.Recipe.Video.CodecContextOptions { - c.l.Infof("overriding av codec context %s", functionNameFor(opt)) opt(s.encCodecContext) } } @@ -413,9 +382,7 @@ func (c *LibAVFFmpegStreamer) prepareFilters(p *libAVParams, closer *astikit.Clo } closer.Add(inputs.Free) - if s.decCodecContext.MediaType() == astiav.MediaTypeAudio { - // TODO: what's the difference between args and content? - // why args are necessary? + if isAudio { args = astiav.FilterArgs{ "channel_layout": s.decCodecContext.ChannelLayout().String(), "sample_fmt": s.decCodecContext.SampleFormat().Name(), @@ -430,7 +397,7 @@ func (c *LibAVFFmpegStreamer) prepareFilters(p *libAVParams, closer *astikit.Clo ) } - if s.decCodecContext.MediaType() == astiav.MediaTypeVideo { + if isVideo { args = astiav.FilterArgs{ "pix_fmt": strconv.Itoa(int(s.decCodecContext.PixelFormat())), "pixel_aspect": s.decCodecContext.SampleAspectRatio().String(), @@ -483,7 +450,136 @@ func (c *LibAVFFmpegStreamer) prepareFilters(p *libAVParams, closer *astikit.Clo return nil } -func (c *LibAVFFmpegStreamer) filterAndEncode(f *astiav.Frame, s *streamContext, donut *entities.DonutParameters) (err error) { +func (c *LibAVFFmpegStreamer) prepareBitStreamFilters(p *libAVParams, closer *astikit.Closer, donut *entities.DonutParameters) error { + for _, s := range p.streams { + isVideo := s.decCodecContext.MediaType() == astiav.MediaTypeVideo + isAudio := s.decCodecContext.MediaType() == astiav.MediaTypeAudio + var currentMedia *entities.DonutMediaTask + + if isAudio { + currentMedia = &donut.Recipe.Audio + } else if isVideo { + currentMedia = &donut.Recipe.Video + } else { + c.l.Warnf("ignoring bit stream filter for media type %s", s.decCodecContext.MediaType().String()) + continue + } + + if currentMedia.DonutBitStreamFilter == nil { + c.l.Infof("no bit stream filter configured for %s", s.decCodecContext.String()) + continue + } + + bsf := astiav.FindBitStreamFilterByName(string(*currentMedia.DonutBitStreamFilter)) + if bsf == nil { + return fmt.Errorf("can not find the filter %s", string(*currentMedia.DonutBitStreamFilter)) + } + + var err error + s.bsfContext, err = astiav.AllocBitStreamFilterContext(bsf) + if err != nil { + return fmt.Errorf("error while allocating bit stream context %w", err) + } + closer.Add(s.bsfContext.Free) + + s.bsfContext.SetTimeBaseIn(s.inputStream.TimeBase()) + if err := s.inputStream.CodecParameters().Copy(s.bsfContext.CodecParametersIn()); err != nil { + return fmt.Errorf("error while copying codec parameters %w", err) + } + + if err := s.bsfContext.Initialize(); err != nil { + return fmt.Errorf("error while initiating %w", err) + } + s.bsfPacket = astiav.AllocPacket() + closer.Add(s.bsfPacket.Free) + } + return nil +} + +func (c *LibAVFFmpegStreamer) processPacket(p *libAVParams, pkt *astiav.Packet, s *streamContext, donut *entities.DonutParameters) error { + isVideo := s.decCodecContext.MediaType() == astiav.MediaTypeVideo + isAudio := s.decCodecContext.MediaType() == astiav.MediaTypeAudio + var currentMedia *entities.DonutMediaTask + + if isAudio { + currentMedia = &donut.Recipe.Audio + } else if isVideo { + currentMedia = &donut.Recipe.Video + } else { + c.l.Warnf("ignoring to stream for media type %s", s.decCodecContext.MediaType().String()) + return nil + } + + byPass := currentMedia.Action == entities.DonutBypass + if isVideo && byPass { + if donut.OnVideoFrame != nil { + pkt.RescaleTs(s.inputStream.TimeBase(), s.decCodecContext.TimeBase()) + if err := donut.OnVideoFrame(pkt.Data(), entities.MediaFrameContext{ + PTS: int(pkt.Pts()), + DTS: int(pkt.Dts()), + Duration: c.defineVideoDuration(s, pkt), + }); err != nil { + return err + } + } + return nil + } + if isAudio && byPass { + if donut.OnAudioFrame != nil { + pkt.RescaleTs(s.inputStream.TimeBase(), s.decCodecContext.TimeBase()) + if err := donut.OnAudioFrame(pkt.Data(), entities.MediaFrameContext{ + PTS: int(pkt.Pts()), + DTS: int(pkt.Dts()), + Duration: c.defineAudioDuration(s, pkt), + }); err != nil { + return err + } + } + return nil + } + + // if isAudio { + // continue + // } + + if err := s.decCodecContext.SendPacket(pkt); err != nil { + return err + } + + for { + if err := s.decCodecContext.ReceiveFrame(s.decFrame); err != nil { + if errors.Is(err, astiav.ErrEof) || errors.Is(err, astiav.ErrEagain) { + break + } + return err + } + if err := c.filterAndEncode(p, s.decFrame, s, donut); err != nil { + return err + } + } + return nil +} + +func (c *LibAVFFmpegStreamer) applyBitStreamFilter(p *libAVParams, pkt *astiav.Packet, s *streamContext, donut *entities.DonutParameters) error { + if err := s.bsfContext.SendPacket(pkt); err != nil && !errors.Is(err, astiav.ErrEagain) { + return fmt.Errorf("sending bit stream packet failed: %w", err) + } + + for { + if err := s.bsfContext.ReceivePacket(s.bsfPacket); err != nil { + if errors.Is(err, astiav.ErrEof) || errors.Is(err, astiav.ErrEagain) { + break + } + return fmt.Errorf("receiving bit stream packet failed: %w", err) + } + + c.processPacket(p, s.bsfPacket, s, donut) + s.bsfPacket.Unref() + } + return nil +} + +func (c *LibAVFFmpegStreamer) filterAndEncode(p *libAVParams, f *astiav.Frame, s *streamContext, donut *entities.DonutParameters) (err error) { if err = s.buffersrcContext.BuffersrcAddFrame(f, astiav.NewBuffersrcFlags(astiav.BuffersrcFlagKeepRef)); err != nil { return fmt.Errorf("adding frame failed: %w", err) } @@ -499,8 +595,7 @@ func (c *LibAVFFmpegStreamer) filterAndEncode(f *astiav.Frame, s *streamContext, } // TODO: should we avoid setting the picture type for audio? s.filterFrame.SetPictureType(astiav.PictureTypeNone) - - if err = c.encodeFrame(s.filterFrame, s, donut); err != nil { + if err = c.encodeFrame(p, s.filterFrame, s, donut); err != nil { err = fmt.Errorf("main: encoding and writing frame failed: %w", err) return } @@ -508,12 +603,15 @@ func (c *LibAVFFmpegStreamer) filterAndEncode(f *astiav.Frame, s *streamContext, return nil } -func (c *LibAVFFmpegStreamer) encodeFrame(f *astiav.Frame, s *streamContext, donut *entities.DonutParameters) (err error) { +func (c *LibAVFFmpegStreamer) encodeFrame(p *libAVParams, f *astiav.Frame, s *streamContext, donut *entities.DonutParameters) (err error) { s.encPkt.Unref() - // when converting from aac to opus using filters, the np samples are bigger than the frame size + // when converting from aac to opus using filters, + // the np samples are bigger than the frame size // to fix the error "more samples than frame size" - f.SetNbSamples(s.encCodecContext.FrameSize()) + if f != nil { + f.SetNbSamples(s.encCodecContext.FrameSize()) + } if err = s.encCodecContext.SendFrame(f); err != nil { return fmt.Errorf("sending frame failed: %w", err) diff --git a/internal/controllers/webrtc_controller.go b/internal/controllers/webrtc_controller.go index d4555ce..b5e820a 100644 --- a/internal/controllers/webrtc_controller.go +++ b/internal/controllers/webrtc_controller.go @@ -42,14 +42,14 @@ func (c *WebRTCController) Setup(cancel context.CancelFunc, donutRecipe *entitie response.Connection = peer var videoTrack *webrtc.TrackLocalStaticSample - videoTrack, err = c.CreateTrack(peer, donutRecipe.Video.Codec, string(entities.VideoType), params.SRTStreamID) + videoTrack, err = c.CreateTrack(peer, donutRecipe.Video.Codec, string(entities.VideoType), params.StreamID) if err != nil { return nil, err } response.Video = videoTrack var audioTrack *webrtc.TrackLocalStaticSample - audioTrack, err = c.CreateTrack(peer, donutRecipe.Audio.Codec, string(entities.AudioType), params.SRTStreamID) + audioTrack, err = c.CreateTrack(peer, donutRecipe.Audio.Codec, string(entities.AudioType), params.StreamID) if err != nil { return nil, err } diff --git a/internal/entities/entities.go b/internal/entities/entities.go index 00d39ea..c31566d 100644 --- a/internal/entities/entities.go +++ b/internal/entities/entities.go @@ -3,6 +3,7 @@ package entities import ( "context" "fmt" + "strings" "time" "github.com/asticode/go-astiav" @@ -21,12 +22,10 @@ type WebRTCSetupResponse struct { LocalSDP *webrtc.SessionDescription } -// TODO: make it agnostic from streaming protocol when implementing RTMP type RequestParams struct { - SRTHost string - SRTPort uint16 `json:",string"` - SRTStreamID string - Offer webrtc.SessionDescription + StreamURL string + StreamID string + Offer webrtc.SessionDescription } func (p *RequestParams) Valid() error { @@ -34,16 +33,18 @@ func (p *RequestParams) Valid() error { return ErrMissingParamsOffer } - if p.SRTHost == "" { - return ErrMissingSRTHost + if p.StreamID == "" { + return ErrMissingStreamID } - if p.SRTPort == 0 { - return ErrMissingSRTPort + if p.StreamURL == "" { + return ErrMissingStreamURL } + isRTMP := strings.Contains(strings.ToLower(p.StreamURL), "rtmp") + isSRT := strings.Contains(strings.ToLower(p.StreamURL), "srt") - if p.SRTStreamID == "" { - return ErrMissingSRTStreamID + if !(isRTMP || isSRT) { + return ErrUnsupportedStreamURL } return nil @@ -53,7 +54,7 @@ func (p *RequestParams) String() string { if p == nil { return "" } - return fmt.Sprintf("ParamsOffer %v:%v/%v", p.SRTHost, p.SRTPort, p.SRTStreamID) + return fmt.Sprintf("RequestParams {StreamURL: %s, StreamID: %s}", p.StreamURL, p.StreamID) } type MessageType string @@ -151,6 +152,10 @@ type DonutMediaTaskAction string var DonutTranscode DonutMediaTaskAction = "transcode" var DonutBypass DonutMediaTaskAction = "bypass" +type DonutBitStreamFilter string + +var DonutH264AnnexB DonutBitStreamFilter = "h264_mp4toannexb" + // TODO: split entities per domain or files avoiding name collision. // DonutMediaTask is a transformation template to apply over a media. @@ -163,6 +168,9 @@ type DonutMediaTask struct { // If no value is provided ffmpeg will use defaults. // For instance, if one does not provide bit rate, it'll fallback to 64000 bps (opus) CodecContextOptions []LibAVOptionsCodecContext + + // DonutBitStreamFilter is the bitstream filter + DonutBitStreamFilter *DonutBitStreamFilter } type DonutInputOptionKey string @@ -175,6 +183,8 @@ var DonutSRTStreamID DonutInputOptionKey = "srt_streamid" var DonutSRTsmoother DonutInputOptionKey = "smoother" var DonutSRTTranstype DonutInputOptionKey = "transtype" +var DonutRTMPLive DonutInputOptionKey = "rtmp_live" + type DonutInputFormat string func (d DonutInputFormat) String() string { diff --git a/internal/entities/errors.go b/internal/entities/errors.go index 2d12efe..9583915 100644 --- a/internal/entities/errors.go +++ b/internal/entities/errors.go @@ -9,6 +9,10 @@ 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 ErrMissingStreamURL = errors.New("stream URL must not be nil") +var ErrMissingStreamID = errors.New("stream ID must not be nil") +var ErrUnsupportedStreamURL = errors.New("unsupported stream") + 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") diff --git a/internal/web/handlers/signaling.go b/internal/web/handlers/signaling.go index 3707bb3..3cdabe1 100644 --- a/internal/web/handlers/signaling.go +++ b/internal/web/handlers/signaling.go @@ -41,27 +41,33 @@ func (h *SignalingHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) err if err != nil { return err } + h.l.Infof("RequestParams %s", params.String()) donutEngine, err := h.donut.EngineFor(¶ms) if err != nil { return err } + h.l.Infof("DonutEngine %#v", donutEngine) // server side media info serverStreamInfo, err := donutEngine.ServerIngredients() if err != nil { return err } + h.l.Infof("ServerIngredients %#v", serverStreamInfo) + // client side media support clientStreamInfo, err := donutEngine.ClientIngredients() if err != nil { return err } + h.l.Infof("ClientIngredients %#v", clientStreamInfo) - donutRecipe := donutEngine.RecipeFor(serverStreamInfo, clientStreamInfo) - if donutRecipe == nil { - return entities.ErrMissingCompatibleStreams + donutRecipe, err := donutEngine.RecipeFor(serverStreamInfo, clientStreamInfo) + if err != nil { + return err } + h.l.Infof("DonutRecipe %#v", donutRecipe) // We can't defer calling cancel here because it'll live alongside the stream. ctx, cancel := context.WithCancel(context.Background()) @@ -70,6 +76,7 @@ func (h *SignalingHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) err cancel() return err } + h.l.Infof("WebRTCResponse %#v", webRTCResponse) go donutEngine.Serve(&entities.DonutParameters{ Cancel: cancel, @@ -103,6 +110,7 @@ func (h *SignalingHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) err cancel() return err } + h.l.Infof("webRTCResponse %#v", webRTCResponse) return nil } diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..7505f1b --- /dev/null +++ b/nginx.conf @@ -0,0 +1,19 @@ +daemon off; + +error_log /dev/stdout info; + +events { + worker_connections 1024; +} + +rtmp { + server { + listen 1935; + chunk_size 4000; + + application live { + live on; + record off; + } + } +} diff --git a/scripts/ffmpeg_mpegts_udp.sh b/scripts/ffmpeg_mpegts_udp.sh index e9c0d33..84ba2d5 100755 --- a/scripts/ffmpeg_mpegts_udp.sh +++ b/scripts/ffmpeg_mpegts_udp.sh @@ -1,7 +1,8 @@ -ffmpeg -hide_banner -loglevel verbose \ - -re -f lavfi -i testsrc2=size=1280x720:rate=30,format=yuv420p \ +ffmpeg -hide_banner -loglevel info \ + -re -f lavfi -i testsrc2=size=768x432:rate=30,format=yuv420p \ -f lavfi -i sine=frequency=1000:sample_rate=44100 \ -c:v libx264 -preset veryfast -tune zerolatency -profile:v baseline \ + -vf "drawtext=text='SRT streaming':box=1:boxborderw=10:x=(w-text_w)/2:y=(h-text_h)/2:fontsize=64:fontcolor=black" \ -b:v 1000k -bufsize 2000k -x264opts keyint=30:min-keyint=30:scenecut=-1 \ -c:a aac -b:a 128k \ -f mpegts "udp://${SRT_INPUT_HOST}:${SRT_INPUT_PORT}?pkt_size=${PKT_SIZE}" \ No newline at end of file diff --git a/scripts/ffmpeg_rtmp.sh b/scripts/ffmpeg_rtmp.sh new file mode 100755 index 0000000..1848b6f --- /dev/null +++ b/scripts/ffmpeg_rtmp.sh @@ -0,0 +1,9 @@ +#!/bin/bash + ffmpeg -hide_banner -loglevel info \ + -re -f lavfi -i testsrc2=size=768x432:rate=30,format=yuv420p \ + -f lavfi -i sine=frequency=1000:sample_rate=44100 \ + -c:v libx264 -preset veryfast -tune zerolatency -profile:v baseline \ + -vf "drawtext=text='RTMP streaming':box=1:boxborderw=10:x=(w-text_w)/2:y=(h-text_h)/2:fontsize=64:fontcolor=black" \ + -b:v 1000k -bufsize 2000k -x264opts keyint=30:min-keyint=30:scenecut=-1 \ + -c:a aac -b:a 128k \ + -f flv -rtmp_live live "rtmp://${RTMP_HOST}:${RTMP_PORT}/live/app" \ No newline at end of file diff --git a/scripts/install_local_ffmpeg.sh b/scripts/install_local_ffmpeg.sh deleted file mode 100755 index 8e30879..0000000 --- a/scripts/install_local_ffmpeg.sh +++ /dev/null @@ -1,23 +0,0 @@ -#!/bin/bash -set -e -PREFIX="/opt/ffmpeg" - - -# from https://github.com/asticode/go-astiav/blob/master/Makefile -version="n5.1.2" -srcPath="tmp/$(version)/src" -postCheckout="" - -rm -rf $(srcPath) -mkdir -p $(srcPath) -git clone --depth 1 --branch $(version) https://github.com/FFmpeg/FFmpeg $(srcPath) -# TODO: install all required libraries (srt, rtmp, aac, x264...) and enable them. -cd $(srcPath) && ./configure --prefix=.. $(configure) \ - --disable-htmlpages --disable-doc --disable-txtpages --disable-podpages --disable-manpages \ - # --enable-gpl \ - # --disable-ffmpeg --disable-ffplay --disable-ffprobe --enable-libopus \ - # --enable-libsvtav1 --enable-libfdk-aac --enable-libopus \ - # --enable-libfreetype --enable-libsrt --enable-librtmp \ - # --enable-libvorbis --enable-libx265 --enable-libx264 --enable-libvpx -cd $(srcPath) && make -cd $(srcPath) && make install \ No newline at end of file diff --git a/scripts/mac_check_deps.sh b/scripts/mac_check_deps.sh deleted file mode 100755 index c277180..0000000 --- a/scripts/mac_check_deps.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/bash -if ! brew list srt &>/dev/null; then - echo "ERROR you must install srt" - echo "brew install srt" - exit 1 -fi - -if ! ls tmp &>/dev/null; then - echo "ERROR you must install ffmpeg" - echo "make install-ffmpeg" - exit 1 -fi diff --git a/scripts/mac_local_run.sh b/scripts/mac_local_run.sh deleted file mode 100755 index 47473cb..0000000 --- a/scripts/mac_local_run.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash -source ./scripts/mac_check_deps.sh - -# deps -source ./scripts/setup_deps_flags.sh - -go run -race main.go \ No newline at end of file diff --git a/scripts/mac_local_run_test.sh b/scripts/mac_local_run_test.sh deleted file mode 100755 index 84a577d..0000000 --- a/scripts/mac_local_run_test.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/bash -source ./scripts/mac_check_deps.sh - -# deps -source ./scripts/setup_deps_flags.sh - -# For debugging: -# go test -v -p 1 ./... -# ref https://github.com/golang/go/issues/46959#issuecomment-1407594935 - -go test ./... \ No newline at end of file diff --git a/scripts/setup_deps_flags.sh b/scripts/setup_deps_flags.sh deleted file mode 100755 index 4aa1fb1..0000000 --- a/scripts/setup_deps_flags.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash -# SRT deps -export CGO_LDFLAGS="-L$(brew --prefix srt)/lib" -export CGO_CFLAGS="-I$(brew --prefix srt)/include/" -export PKG_CONFIG_PATH="$(brew --prefix srt)/lib/pkgconfig" - -# ffmpeg/libav deps -CGO_LDFLAGS="$CGO_LDFLAGS -L$(pwd)/tmp/n5.1.2/lib/" -CGO_CFLAGS="$CGO_CFLAGS -I$(pwd)/tmp/n5.1.2/include/" -PKG_CONFIG_PATH="$PKG_CONFIG_PATH:$(pwd)/tmp/n5.1.2/lib/pkgconfig" \ No newline at end of file diff --git a/static/demo.css b/static/demo.css index 78566e9..fbc7706 100644 --- a/static/demo.css +++ b/static/demo.css @@ -2,7 +2,70 @@ SPDX-FileCopyrightText: 2023 The Pion community SPDX-License-Identifier: MIT */ -textarea { - width: 500px; - min-height: 75px; + +* { + font-family: "Open Sans", sans-serif; +} +input { + width: 100%; + padding: 12px 20px; + margin: 8px 0; + box-sizing: border-box; +} + +legend { + background-color: #000; + color: #fff; + padding: 3px 6px; +} + +.hint { + color:darkgray; +} + +button { + align-items: center; + background-color: #FFFFFF; + border: 1px solid rgba(0, 0, 0, 0.1); + border-radius: .25rem; + box-shadow: rgba(0, 0, 0, 0.02) 0 1px 3px 0; + box-sizing: border-box; + color: rgba(0, 0, 0, 0.85); + cursor: pointer; + display: inline-flex; + font-family: system-ui,-apple-system,system-ui,"Helvetica Neue",Helvetica,Arial,sans-serif; + font-size: 16px; + font-weight: 600; + justify-content: center; + line-height: 1.25; + margin: 0; + min-height: 3rem; + padding: calc(.875rem - 1px) calc(1.5rem - 1px); + position: relative; + text-decoration: none; + transition: all 250ms; + user-select: none; + -webkit-user-select: none; + touch-action: manipulation; + vertical-align: baseline; + width: auto; +} + +button:hover, +button:focus { + border-color: rgba(0, 0, 0, 0.15); + box-shadow: rgba(0, 0, 0, 0.1) 0 4px 12px; + color: rgba(0, 0, 0, 0.65); +} + +button { + transform: translateY(-1px); +} + +button { + background-color: #F0F0F1; + border-color: rgba(0, 0, 0, 0.15); + box-shadow: rgba(0, 0, 0, 0.06) 0 2px 4px; + color: rgba(0, 0, 0, 0.65); + transform: translateY(0); } \ No newline at end of file diff --git a/static/demo.js b/static/demo.js index dbc489d..4291ff4 100644 --- a/static/demo.js +++ b/static/demo.js @@ -2,15 +2,13 @@ window.metadataMessages = {} window.startSession = () => { - let srtHost = document.getElementById('srt-host').value; - let srtPort = document.getElementById('srt-port').value; - let srtStreamId = document.getElementById('srt-stream-id').value; + let streamURL = document.getElementById('stream-url').value; + let streamID = document.getElementById('stream-id').value; setupWebRTC((pc, offer) => { let srtFullAddress = JSON.stringify({ - "srtHost": srtHost, - "srtPort": srtPort, - "srtStreamId": srtStreamId, + "streamURL": streamURL, + "streamID": streamID, offer }); @@ -142,7 +140,9 @@ const log = (msg, level = "info") => { const el = document.createElement("p") if (typeof(msg) !== "string") { + orig = msg msg = "unknown log msg type " + typeof(msg) + msg = msg + " [" + orig + "]" level = "error" } diff --git a/static/index.html b/static/index.html index c894846..339a746 100644 --- a/static/index.html +++ b/static/index.html @@ -3,31 +3,48 @@ donut - + + + + + -

SRT Config

- SRT Host -
+
+ Remote streaming +

+ + + +

+

+ + + +

+

+ +

+
- SRT Port -
+
+ Video +
+
- SRT Stream ID -
- -

Video

-
- -

Metadata

-
- -

Logs

-
+
+ Metadata +
+
+
+ Logs +
+