Merge pull request #32 from flavioribeiro/add-rtmp-support

Add rtmp support
This commit is contained in:
Leandro Moreira
2024-05-19 01:00:47 -03:00
committed by GitHub
30 changed files with 707 additions and 379 deletions

View File

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

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

View File

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

View File

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

View File

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

Binary file not shown.

Binary file not shown.

Binary file not shown.

92
fonts/0xProto/LICENSE Normal file
View 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
View 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
View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +0,0 @@
#!/bin/bash
source ./scripts/mac_check_deps.sh
# deps
source ./scripts/setup_deps_flags.sh
go run -race main.go

View File

@@ -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 ./...

View File

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

View File

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

View File

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

View File

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