mirror of
https://github.com/flavioribeiro/donut.git
synced 2025-09-26 19:11:11 +08:00
Merge pull request #32 from flavioribeiro/add-rtmp-support
Add rtmp support
This commit is contained in:
@@ -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
|
||||
WORKDIR ${WD}
|
80
FAQ.md
80
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
|
||||
```
|
@@ -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
|
||||
# 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
|
||||
```
|
32
Makefile
32
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
|
||||
|
@@ -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"
|
BIN
fonts/0xProto/0xProtoNerdFont-Regular.ttf
Normal file
BIN
fonts/0xProto/0xProtoNerdFont-Regular.ttf
Normal file
Binary file not shown.
BIN
fonts/0xProto/0xProtoNerdFontMono-Regular.ttf
Normal file
BIN
fonts/0xProto/0xProtoNerdFontMono-Regular.ttf
Normal file
Binary file not shown.
BIN
fonts/0xProto/0xProtoNerdFontPropo-Regular.ttf
Normal file
BIN
fonts/0xProto/0xProtoNerdFontPropo-Regular.ttf
Normal file
Binary file not shown.
92
fonts/0xProto/LICENSE
Normal file
92
fonts/0xProto/LICENSE
Normal file
@@ -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.
|
48
fonts/0xProto/README.md
Normal file
48
fonts/0xProto/README.md
Normal file
@@ -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
|
||||
|
4
go.mod
generated
4
go.mod
generated
@@ -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
|
||||
|
8
go.sum
generated
8
go.sum
generated
@@ -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=
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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)
|
||||
|
||||
|
@@ -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)
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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 {
|
||||
|
@@ -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")
|
||||
|
@@ -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
|
||||
}
|
||||
|
19
nginx.conf
Normal file
19
nginx.conf
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@@ -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}"
|
9
scripts/ffmpeg_rtmp.sh
Executable file
9
scripts/ffmpeg_rtmp.sh
Executable file
@@ -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"
|
@@ -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
|
@@ -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
|
@@ -1,7 +0,0 @@
|
||||
#!/bin/bash
|
||||
source ./scripts/mac_check_deps.sh
|
||||
|
||||
# deps
|
||||
source ./scripts/setup_deps_flags.sh
|
||||
|
||||
go run -race main.go
|
@@ -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 ./...
|
@@ -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"
|
@@ -2,7 +2,70 @@
|
||||
SPDX-FileCopyrightText: 2023 The Pion community <https://pion.ly>
|
||||
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);
|
||||
}
|
@@ -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"
|
||||
}
|
||||
|
||||
|
@@ -3,31 +3,48 @@
|
||||
<head>
|
||||
<title>donut</title>
|
||||
<script src="demo.js"></script>
|
||||
<style src="demo.css"></style>
|
||||
<link rel="stylesheet" href="demo.css">
|
||||
</link>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,300..800;1,300..800&display=swap"
|
||||
rel="stylesheet">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<h1>SRT Config</h1>
|
||||
<b> SRT Host </b>
|
||||
<input type="text" id="srt-host" value="srt"> <br />
|
||||
<fieldset>
|
||||
<legend>Remote streaming</legend>
|
||||
<p>
|
||||
<label for="stream-url">URL: <span aria-label="required">*</span></label>
|
||||
<input id="stream-url" type="text" name="stream-url" required value="srt://haivision_srt:40052" />
|
||||
<label class="hint">rtmp://nginx_rtmp/live</label>
|
||||
</p>
|
||||
<p>
|
||||
<label for="stream-id">ID: <span aria-label="required">*</span></label>
|
||||
<input id="stream-id" type="text" name="stream-id" required value="stream-id" />
|
||||
<label class="hint">app</label>
|
||||
</p>
|
||||
<p>
|
||||
<button onclick="onConnect()"> Connect </button>
|
||||
</p>
|
||||
</fieldset>
|
||||
|
||||
<b> SRT Port </b>
|
||||
<input type="text" id="srt-port" value="40052" /> <br />
|
||||
<fieldset>
|
||||
<legend>Video</legend>
|
||||
<div id="remoteVideos"></div>
|
||||
</fieldset>
|
||||
|
||||
<b> SRT Stream ID </b>
|
||||
<input type="text" id="srt-stream-id" value="stream-id" /> <br />
|
||||
<button onclick="onConnect()"> Connect </button>
|
||||
|
||||
<h1>Video</h1>
|
||||
<div id="remoteVideos"></div>
|
||||
|
||||
<h1>Metadata</h1>
|
||||
<div id="metadata"></div>
|
||||
|
||||
<h1>Logs</h1>
|
||||
<div id="log"></div>
|
||||
<fieldset>
|
||||
<legend>Metadata</legend>
|
||||
<div id="metadata"></div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>Logs</legend>
|
||||
<div id="log"></div>
|
||||
</fieldset>
|
||||
</body>
|
||||
|
||||
<script>
|
||||
@@ -40,26 +57,9 @@
|
||||
}
|
||||
|
||||
docReady(function () {
|
||||
const queryString = window.location.search;
|
||||
const urlParams = new URLSearchParams(queryString);
|
||||
|
||||
window.onConnect = () => {
|
||||
window.startSession();
|
||||
}
|
||||
|
||||
if (urlParams.has('srtHost')) {
|
||||
document.getElementById('srt-host').value = urlParams.get('srtHost');
|
||||
}
|
||||
if (urlParams.has('srtPort')) {
|
||||
document.getElementById('srt-port').value = urlParams.get('srtPort');
|
||||
}
|
||||
if (urlParams.has('srtStreamId')) {
|
||||
document.getElementById('srt-stream-id').value = urlParams.get('srtStreamId');
|
||||
}
|
||||
|
||||
if (urlParams.get('autoplay') === "true") {
|
||||
onConnect();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
|
Reference in New Issue
Block a user