mirror of
https://github.com/flavioribeiro/donut.git
synced 2025-09-27 03:15:54 +08:00
add draft for rtmp
This commit is contained in:
@@ -20,6 +20,7 @@ RUN apt-get clean && apt-get update && \
|
|||||||
ENV GOPROXY=direct
|
ENV GOPROXY=direct
|
||||||
|
|
||||||
COPY . ./donut
|
COPY . ./donut
|
||||||
|
COPY ./go-astiav/ /Users/leandro.moreira/src/go-astiav/
|
||||||
WORKDIR ${WD}/donut
|
WORKDIR ${WD}/donut
|
||||||
RUN go build .
|
RUN go build .
|
||||||
CMD ["/usr/src/app/donut/donut", "--enable-ice-mux=true"]
|
CMD ["/usr/src/app/donut/donut", "--enable-ice-mux=true"]
|
29
Makefile
29
Makefile
@@ -1,30 +1,19 @@
|
|||||||
run:
|
run:
|
||||||
# in case you need to re-build it, uncomment the following line
|
docker compose stop && docker compose up origin srt app
|
||||||
# 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
|
|
||||||
|
|
||||||
test:
|
run-dev:
|
||||||
# in case you need to re-build it, uncomment the following line
|
docker compose stop && docker compose down && docker compose build app && docker compose up origin srt app
|
||||||
# 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-total-rebuild:
|
||||||
|
docker compose stop && docker compose down && docker compose build && docker compose up origin srt app
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
run-srt:
|
run-srt:
|
||||||
docker compose stop && docker compose down && docker compose build srt && docker compose up srt
|
docker compose stop && docker compose down && docker compose build srt && docker compose up srt
|
||||||
|
|
||||||
mac-run-local:
|
|
||||||
./scripts/mac_local_run.sh
|
|
||||||
|
|
||||||
mac-test-local:
|
|
||||||
./scripts/mac_local_run_test.sh
|
|
||||||
|
|
||||||
html-local-coverage:
|
|
||||||
go tool cover -html=coverage.out
|
|
||||||
|
|
||||||
lint:
|
lint:
|
||||||
docker compose stop lint && docker compose down lint && docker compose run --rm 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
|
.PHONY: run lint test run-srt mac-run-local mac-test-local html-local-coverage install-ffmpeg
|
||||||
|
@@ -13,8 +13,10 @@ services:
|
|||||||
- "6060:6060"
|
- "6060:6060"
|
||||||
depends_on:
|
depends_on:
|
||||||
- srt
|
- srt
|
||||||
|
- rtmp
|
||||||
links:
|
links:
|
||||||
- srt
|
- srt
|
||||||
|
- rtmp
|
||||||
|
|
||||||
test:
|
test:
|
||||||
build:
|
build:
|
||||||
@@ -36,6 +38,19 @@ services:
|
|||||||
- "./:/app/"
|
- "./:/app/"
|
||||||
command: "golangci-lint run -v"
|
command: "golangci-lint run -v"
|
||||||
|
|
||||||
|
rtmp: # simulating an RTMP 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=0.0.0.0
|
||||||
|
- RTMP_PORT=1935
|
||||||
|
ports:
|
||||||
|
- "1935:1935"
|
||||||
|
|
||||||
srt:
|
srt:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
@@ -62,8 +77,8 @@ services:
|
|||||||
command: "/scripts/ffmpeg_mpegts_udp.sh"
|
command: "/scripts/ffmpeg_mpegts_udp.sh"
|
||||||
volumes:
|
volumes:
|
||||||
- "./scripts:/scripts"
|
- "./scripts:/scripts"
|
||||||
|
- "./fonts/0xProto:/usr/share/fonts"
|
||||||
environment:
|
environment:
|
||||||
- SRT_INPUT_HOST=srt
|
- SRT_INPUT_HOST=srt
|
||||||
- SRT_INPUT_PORT=1234
|
- SRT_INPUT_PORT=1234
|
||||||
- PKT_SIZE=1316
|
- PKT_SIZE=1316
|
||||||
|
|
||||||
|
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
|
||||||
|
|
1
go-astiav
Submodule
1
go-astiav
Submodule
Submodule go-astiav added at 338c4dfcca
4
go.mod
generated
4
go.mod
generated
@@ -2,9 +2,11 @@ module github.com/flavioribeiro/donut
|
|||||||
|
|
||||||
go 1.19
|
go 1.19
|
||||||
|
|
||||||
|
replace github.com/asticode/go-astiav => /Users/leandro.moreira/src/go-astiav
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/asticode/go-astiav v0.12.0
|
github.com/asticode/go-astiav v0.12.0
|
||||||
github.com/asticode/go-astikit v0.36.0
|
github.com/asticode/go-astikit v0.42.0
|
||||||
github.com/kelseyhightower/envconfig v1.4.0
|
github.com/kelseyhightower/envconfig v1.4.0
|
||||||
github.com/pion/webrtc/v3 v3.1.47
|
github.com/pion/webrtc/v3 v3.1.47
|
||||||
github.com/stretchr/testify v1.8.0
|
github.com/stretchr/testify v1.8.0
|
||||||
|
6
go.sum
generated
6
go.sum
generated
@@ -1,7 +1,5 @@
|
|||||||
github.com/asticode/go-astiav v0.12.0 h1:tETfPhVpJrSyh3zvUOmDvebFaCoFpeATSaQAA7B50J8=
|
github.com/asticode/go-astikit v0.42.0 h1:pnir/2KLUSr0527Tv908iAH6EGYYrYta132vvjXsH5w=
|
||||||
github.com/asticode/go-astiav v0.12.0/go.mod h1:phvUnSSlV91S/PELeLkDisYiRLOssxWOsj4oDrqM/54=
|
github.com/asticode/go-astikit v0.42.0/go.mod h1:h4ly7idim1tNhaVkdVBeXQZEE3L0xblP7fCWbgwipF0=
|
||||||
github.com/asticode/go-astikit v0.36.0 h1:WHSY88YT76D/XRbdp0lMLwfjyUGw8dygnbKKtbGNIG8=
|
|
||||||
github.com/asticode/go-astikit v0.36.0/go.mod h1:h4ly7idim1tNhaVkdVBeXQZEE3L0xblP7fCWbgwipF0=
|
|
||||||
github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A=
|
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.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
@@ -2,6 +2,7 @@ package engine
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/flavioribeiro/donut/internal/controllers/probers"
|
"github.com/flavioribeiro/donut/internal/controllers/probers"
|
||||||
"github.com/flavioribeiro/donut/internal/controllers/streamers"
|
"github.com/flavioribeiro/donut/internal/controllers/streamers"
|
||||||
@@ -11,10 +12,10 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type DonutEngine interface {
|
type DonutEngine interface {
|
||||||
Appetizer() entities.DonutAppetizer
|
Appetizer() (entities.DonutAppetizer, error)
|
||||||
ServerIngredients() (*entities.StreamInfo, error)
|
ServerIngredients() (*entities.StreamInfo, error)
|
||||||
ClientIngredients() (*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)
|
Serve(p *entities.DonutParameters)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,7 +81,11 @@ type donutEngine struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *donutEngine) ServerIngredients() (*entities.StreamInfo, error) {
|
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) {
|
func (d *donutEngine) ClientIngredients() (*entities.StreamInfo, error) {
|
||||||
@@ -91,7 +96,7 @@ func (d *donutEngine) Serve(p *entities.DonutParameters) {
|
|||||||
d.streamer.Stream(p)
|
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
|
// TODO: implement proper matching
|
||||||
//
|
//
|
||||||
// suggestions:
|
// suggestions:
|
||||||
@@ -101,10 +106,16 @@ func (d *donutEngine) RecipeFor(server, client *entities.StreamInfo) *entities.D
|
|||||||
// preferable = [vp8, opus]
|
// preferable = [vp8, opus]
|
||||||
// if union(preferable, client.medias)
|
// if union(preferable, client.medias)
|
||||||
// transcode, preferable
|
// transcode, preferable
|
||||||
|
appetizer, err := d.Appetizer()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
r := &entities.DonutRecipe{
|
r := &entities.DonutRecipe{
|
||||||
Input: d.Appetizer(),
|
Input: appetizer,
|
||||||
Video: entities.DonutMediaTask{
|
Video: entities.DonutMediaTask{
|
||||||
Action: entities.DonutBypass,
|
// Action: entities.DonutBypass,
|
||||||
|
Action: entities.DonutTranscode,
|
||||||
Codec: entities.H264,
|
Codec: entities.H264,
|
||||||
},
|
},
|
||||||
Audio: entities.DonutMediaTask{
|
Audio: entities.DonutMediaTask{
|
||||||
@@ -122,18 +133,29 @@ func (d *donutEngine) RecipeFor(server, client *entities.StreamInfo) *entities.D
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
return r
|
return r, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *donutEngine) Appetizer() entities.DonutAppetizer {
|
func (d *donutEngine) Appetizer() (entities.DonutAppetizer, error) {
|
||||||
// TODO: implement input based on param to build proper SRT/RTMP/etc
|
if strings.Contains(strings.ToLower(d.req.StreamURL), "rtmp") {
|
||||||
return entities.DonutAppetizer{
|
return entities.DonutAppetizer{
|
||||||
URL: fmt.Sprintf("srt://%s:%d", d.req.SRTHost, d.req.SRTPort),
|
URL: fmt.Sprintf("%s/%s", d.req.StreamURL, d.req.StreamID),
|
||||||
Format: "mpegts", // it'll change based on input, i.e. rmtp flv
|
Options: map[entities.DonutInputOptionKey]string{
|
||||||
Options: map[entities.DonutInputOptionKey]string{
|
entities.DonutRTMPLive: "live",
|
||||||
entities.DonutSRTStreamID: d.req.SRTStreamID,
|
},
|
||||||
entities.DonutSRTTranstype: "live",
|
// Format: "flv",
|
||||||
entities.DonutSRTsmoother: "live",
|
}, nil
|
||||||
},
|
} else if strings.Contains(strings.ToLower(d.req.StreamURL), "srt") {
|
||||||
|
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 (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/asticode/go-astiav"
|
"github.com/asticode/go-astiav"
|
||||||
"github.com/asticode/go-astikit"
|
"github.com/asticode/go-astikit"
|
||||||
@@ -39,7 +40,10 @@ func NewLibAVFFmpeg(
|
|||||||
|
|
||||||
// Match returns true when the request is for an LibAVFFmpeg prober
|
// Match returns true when the request is for an LibAVFFmpeg prober
|
||||||
func (c *LibAVFFmpeg) Match(req *entities.RequestParams) bool {
|
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.
|
// 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)
|
inputOptions := c.defineInputOptions(req.Options, closer)
|
||||||
|
|
||||||
if err := inputFormatContext.OpenInput(req.URL, inputFormat, inputOptions); err != nil {
|
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)
|
closer.Add(inputFormatContext.CloseInput)
|
||||||
|
|
||||||
|
@@ -50,7 +50,10 @@ func NewLibAVFFmpegStreamer(p LibAVFFmpegStreamerParams) ResultLibAVFFmpegStream
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *LibAVFFmpegStreamer) Match(req *entities.RequestParams) bool {
|
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 {
|
type streamContext struct {
|
||||||
@@ -78,7 +81,7 @@ type libAVParams struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *LibAVFFmpegStreamer) Stream(donut *entities.DonutParameters) {
|
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()
|
closer := astikit.NewCloser()
|
||||||
defer closer.Close()
|
defer closer.Close()
|
||||||
@@ -89,20 +92,26 @@ func (c *LibAVFFmpegStreamer) Stream(donut *entities.DonutParameters) {
|
|||||||
|
|
||||||
// it's useful for debugging
|
// it's useful for debugging
|
||||||
astiav.SetLogLevel(astiav.LogLevelDebug)
|
astiav.SetLogLevel(astiav.LogLevelDebug)
|
||||||
astiav.SetLogCallback(func(l astiav.LogLevel, fmt, msg, parent string) {
|
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("ffmpeg %s: - %s", c.libAVLogToString(l), strings.TrimSpace(msg))
|
||||||
})
|
})
|
||||||
|
// 138.1 internal/controllers/streamers/libav_ffmpeg.go:95:24:
|
||||||
|
// cannot use func(l astiav.LogLevel, fmt, msg, parent string) {…}
|
||||||
|
// (value of type func(l astiav.LogLevel, fmt string, msg string, parent string)) as astiav.LogCallback value in argument to astiav.SetLogCallback
|
||||||
|
|
||||||
|
c.l.Infof("preparing input")
|
||||||
if err := c.prepareInput(p, closer, donut); err != nil {
|
if err := c.prepareInput(p, closer, donut); err != nil {
|
||||||
c.onError(err, donut)
|
c.onError(err, donut)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
c.l.Infof("preparing output")
|
||||||
if err := c.prepareOutput(p, closer, donut); err != nil {
|
if err := c.prepareOutput(p, closer, donut); err != nil {
|
||||||
c.onError(err, donut)
|
c.onError(err, donut)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
c.l.Infof("preparing filters")
|
||||||
if err := c.prepareFilters(p, closer, donut); err != nil {
|
if err := c.prepareFilters(p, closer, donut); err != nil {
|
||||||
c.onError(err, donut)
|
c.onError(err, donut)
|
||||||
return
|
return
|
||||||
@@ -121,6 +130,7 @@ func (c *LibAVFFmpegStreamer) Stream(donut *entities.DonutParameters) {
|
|||||||
c.onError(donut.Ctx.Err(), donut)
|
c.onError(donut.Ctx.Err(), donut)
|
||||||
return
|
return
|
||||||
default:
|
default:
|
||||||
|
c.l.Infof("started reading frame")
|
||||||
if err := p.inputFormatContext.ReadFrame(inPkt); err != nil {
|
if err := p.inputFormatContext.ReadFrame(inPkt); err != nil {
|
||||||
if errors.Is(err, astiav.ErrEof) {
|
if errors.Is(err, astiav.ErrEof) {
|
||||||
c.l.Info("streaming has ended")
|
c.l.Info("streaming has ended")
|
||||||
@@ -141,6 +151,63 @@ func (c *LibAVFFmpegStreamer) Stream(donut *entities.DonutParameters) {
|
|||||||
isVideoBypass := donut.Recipe.Video.Action == entities.DonutBypass
|
isVideoBypass := donut.Recipe.Video.Action == entities.DonutBypass
|
||||||
if isVideo && isVideoBypass {
|
if isVideo && isVideoBypass {
|
||||||
if donut.OnVideoFrame != nil {
|
if donut.OnVideoFrame != nil {
|
||||||
|
// The SRT(mpegts[h264]) bitstream format is Annex B 0x0, 0x0, 0x0, 0x1 [Start Code]
|
||||||
|
// [start code]--[NAL]--[start code]--[NAL] etc
|
||||||
|
//
|
||||||
|
// The RTMP(flv[h264]) bitstream format is AVCC (mp4) 0xY, 0xZ, 0xK, 0xW [Length]
|
||||||
|
// [SIZE (4 bytes)]--[NAL]--[SIZE (4 bytes)]--[NAL] etc
|
||||||
|
//
|
||||||
|
// ref: https://stackoverflow.com/questions/28421375/usage-of-start-code-for-h264-video/29103276#29103276
|
||||||
|
//
|
||||||
|
// To convert from AVCC to AnnexB:
|
||||||
|
//
|
||||||
|
// Remove length, insert start code, insert SPS for each I-frame, insert PPS for each frame, insert AU delimiter for each GOP.
|
||||||
|
//
|
||||||
|
// https://ffmpeg.org/doxygen/trunk/h264__mp4toannexb__bsf_8c.html#a773e34981d7642d499348d1ae72fd02e
|
||||||
|
|
||||||
|
// av_bsf_send_packet(bsfContext, pkt)
|
||||||
|
// av_bsf_receive_packet(bsfContext, pkt)
|
||||||
|
|
||||||
|
// for {
|
||||||
|
// c.l.Infof("start receiving packet")
|
||||||
|
// if err := s.decCodecContext.ReceiveFrame(s.decFrame); err != nil {
|
||||||
|
// if errors.Is(err, astiav.ErrEof) || errors.Is(err, astiav.ErrEagain) {
|
||||||
|
// break
|
||||||
|
// }
|
||||||
|
// c.onError(err, donut)
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
// c.l.Infof("start filtering")
|
||||||
|
// if err := c.filterAndEncode(s.decFrame, s, donut); err != nil {
|
||||||
|
// c.onError(err, donut)
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
bistreamFilter := astiav.FindBitStreamFilterByName("h264_mp4toannexb")
|
||||||
|
if bistreamFilter == nil {
|
||||||
|
c.l.Info("cannot find bit stream filter")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
bsfCtx, err := astiav.AllocBitStreamContext(bistreamFilter)
|
||||||
|
if err != nil {
|
||||||
|
c.l.Info("error while AllocBitStreamContext", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := bsfCtx.Init(); err != nil {
|
||||||
|
c.l.Info("error while init", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := bsfCtx.SendPacket(inPkt); err != nil {
|
||||||
|
c.l.Info("error while SendPacket", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if bsfCtx.ReceivePacket(inPkt) != nil {
|
||||||
|
c.l.Info("error while ReceivePacket", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if err := donut.OnVideoFrame(inPkt.Data(), entities.MediaFrameContext{
|
if err := donut.OnVideoFrame(inPkt.Data(), entities.MediaFrameContext{
|
||||||
PTS: int(inPkt.Pts()),
|
PTS: int(inPkt.Pts()),
|
||||||
DTS: int(inPkt.Dts()),
|
DTS: int(inPkt.Dts()),
|
||||||
@@ -169,12 +236,19 @@ func (c *LibAVFFmpegStreamer) Stream(donut *entities.DonutParameters) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if isAudio {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
c.l.Infof("start sending packet")
|
||||||
|
// c.processPacket(inPkt, s, donut)
|
||||||
if err := s.decCodecContext.SendPacket(inPkt); err != nil {
|
if err := s.decCodecContext.SendPacket(inPkt); err != nil {
|
||||||
c.onError(err, donut)
|
c.onError(err, donut)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
for {
|
for {
|
||||||
|
c.l.Infof("start receiving packet")
|
||||||
if err := s.decCodecContext.ReceiveFrame(s.decFrame); err != nil {
|
if err := s.decCodecContext.ReceiveFrame(s.decFrame); err != nil {
|
||||||
if errors.Is(err, astiav.ErrEof) || errors.Is(err, astiav.ErrEagain) {
|
if errors.Is(err, astiav.ErrEof) || errors.Is(err, astiav.ErrEagain) {
|
||||||
break
|
break
|
||||||
@@ -182,7 +256,7 @@ func (c *LibAVFFmpegStreamer) Stream(donut *entities.DonutParameters) {
|
|||||||
c.onError(err, donut)
|
c.onError(err, donut)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
c.l.Infof("start filtering")
|
||||||
if err := c.filterAndEncode(s.decFrame, s, donut); err != nil {
|
if err := c.filterAndEncode(s.decFrame, s, donut); err != nil {
|
||||||
c.onError(err, donut)
|
c.onError(err, donut)
|
||||||
return
|
return
|
||||||
@@ -414,8 +488,6 @@ func (c *LibAVFFmpegStreamer) prepareFilters(p *libAVParams, closer *astikit.Clo
|
|||||||
closer.Add(inputs.Free)
|
closer.Add(inputs.Free)
|
||||||
|
|
||||||
if s.decCodecContext.MediaType() == astiav.MediaTypeAudio {
|
if s.decCodecContext.MediaType() == astiav.MediaTypeAudio {
|
||||||
// TODO: what's the difference between args and content?
|
|
||||||
// why args are necessary?
|
|
||||||
args = astiav.FilterArgs{
|
args = astiav.FilterArgs{
|
||||||
"channel_layout": s.decCodecContext.ChannelLayout().String(),
|
"channel_layout": s.decCodecContext.ChannelLayout().String(),
|
||||||
"sample_fmt": s.decCodecContext.SampleFormat().Name(),
|
"sample_fmt": s.decCodecContext.SampleFormat().Name(),
|
||||||
@@ -483,6 +555,29 @@ func (c *LibAVFFmpegStreamer) prepareFilters(p *libAVParams, closer *astikit.Clo
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *LibAVFFmpegStreamer) processPacket(pkt *astiav.Packet, s *streamContext, donut *entities.DonutParameters) {
|
||||||
|
if err := s.decCodecContext.SendPacket(pkt); err != nil {
|
||||||
|
c.onError(err, donut)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
c.l.Infof("start receiving packet")
|
||||||
|
if err := s.decCodecContext.ReceiveFrame(s.decFrame); err != nil {
|
||||||
|
if errors.Is(err, astiav.ErrEof) || errors.Is(err, astiav.ErrEagain) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
c.onError(err, donut)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.l.Infof("start filtering")
|
||||||
|
if err := c.filterAndEncode(s.decFrame, s, donut); err != nil {
|
||||||
|
c.onError(err, donut)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (c *LibAVFFmpegStreamer) filterAndEncode(f *astiav.Frame, s *streamContext, donut *entities.DonutParameters) (err error) {
|
func (c *LibAVFFmpegStreamer) filterAndEncode(f *astiav.Frame, s *streamContext, donut *entities.DonutParameters) (err error) {
|
||||||
if err = s.buffersrcContext.BuffersrcAddFrame(f, astiav.NewBuffersrcFlags(astiav.BuffersrcFlagKeepRef)); err != nil {
|
if err = s.buffersrcContext.BuffersrcAddFrame(f, astiav.NewBuffersrcFlags(astiav.BuffersrcFlagKeepRef)); err != nil {
|
||||||
return fmt.Errorf("adding frame failed: %w", err)
|
return fmt.Errorf("adding frame failed: %w", err)
|
||||||
@@ -499,7 +594,7 @@ func (c *LibAVFFmpegStreamer) filterAndEncode(f *astiav.Frame, s *streamContext,
|
|||||||
}
|
}
|
||||||
// TODO: should we avoid setting the picture type for audio?
|
// TODO: should we avoid setting the picture type for audio?
|
||||||
s.filterFrame.SetPictureType(astiav.PictureTypeNone)
|
s.filterFrame.SetPictureType(astiav.PictureTypeNone)
|
||||||
|
c.l.Infof("start encoding")
|
||||||
if err = c.encodeFrame(s.filterFrame, s, donut); err != nil {
|
if err = c.encodeFrame(s.filterFrame, s, donut); err != nil {
|
||||||
err = fmt.Errorf("main: encoding and writing frame failed: %w", err)
|
err = fmt.Errorf("main: encoding and writing frame failed: %w", err)
|
||||||
return
|
return
|
||||||
@@ -520,6 +615,7 @@ func (c *LibAVFFmpegStreamer) encodeFrame(f *astiav.Frame, s *streamContext, don
|
|||||||
}
|
}
|
||||||
|
|
||||||
for {
|
for {
|
||||||
|
c.l.Infof("start receiving packet")
|
||||||
if err = s.encCodecContext.ReceivePacket(s.encPkt); err != nil {
|
if err = s.encCodecContext.ReceivePacket(s.encPkt); err != nil {
|
||||||
if errors.Is(err, astiav.ErrEof) || errors.Is(err, astiav.ErrEagain) {
|
if errors.Is(err, astiav.ErrEof) || errors.Is(err, astiav.ErrEagain) {
|
||||||
err = nil
|
err = nil
|
||||||
@@ -535,6 +631,7 @@ func (c *LibAVFFmpegStreamer) encodeFrame(f *astiav.Frame, s *streamContext, don
|
|||||||
isVideo := s.decCodecContext.MediaType() == astiav.MediaTypeVideo
|
isVideo := s.decCodecContext.MediaType() == astiav.MediaTypeVideo
|
||||||
if isVideo {
|
if isVideo {
|
||||||
if donut.OnVideoFrame != nil {
|
if donut.OnVideoFrame != nil {
|
||||||
|
c.l.Infof("sending transcoded video")
|
||||||
if err := donut.OnVideoFrame(s.encPkt.Data(), entities.MediaFrameContext{
|
if err := donut.OnVideoFrame(s.encPkt.Data(), entities.MediaFrameContext{
|
||||||
PTS: int(s.encPkt.Pts()),
|
PTS: int(s.encPkt.Pts()),
|
||||||
DTS: int(s.encPkt.Dts()),
|
DTS: int(s.encPkt.Dts()),
|
||||||
@@ -548,6 +645,7 @@ func (c *LibAVFFmpegStreamer) encodeFrame(f *astiav.Frame, s *streamContext, don
|
|||||||
isAudio := s.decCodecContext.MediaType() == astiav.MediaTypeAudio
|
isAudio := s.decCodecContext.MediaType() == astiav.MediaTypeAudio
|
||||||
if isAudio {
|
if isAudio {
|
||||||
if donut.OnAudioFrame != nil {
|
if donut.OnAudioFrame != nil {
|
||||||
|
c.l.Infof("sending transcoded audio")
|
||||||
if err := donut.OnAudioFrame(s.encPkt.Data(), entities.MediaFrameContext{
|
if err := donut.OnAudioFrame(s.encPkt.Data(), entities.MediaFrameContext{
|
||||||
PTS: int(s.encPkt.Pts()),
|
PTS: int(s.encPkt.Pts()),
|
||||||
DTS: int(s.encPkt.Dts()),
|
DTS: int(s.encPkt.Dts()),
|
||||||
|
@@ -42,14 +42,14 @@ func (c *WebRTCController) Setup(cancel context.CancelFunc, donutRecipe *entitie
|
|||||||
response.Connection = peer
|
response.Connection = peer
|
||||||
|
|
||||||
var videoTrack *webrtc.TrackLocalStaticSample
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
response.Video = videoTrack
|
response.Video = videoTrack
|
||||||
|
|
||||||
var audioTrack *webrtc.TrackLocalStaticSample
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@@ -3,6 +3,7 @@ package entities
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/asticode/go-astiav"
|
"github.com/asticode/go-astiav"
|
||||||
@@ -27,6 +28,9 @@ type RequestParams struct {
|
|||||||
SRTPort uint16 `json:",string"`
|
SRTPort uint16 `json:",string"`
|
||||||
SRTStreamID string
|
SRTStreamID string
|
||||||
Offer webrtc.SessionDescription
|
Offer webrtc.SessionDescription
|
||||||
|
|
||||||
|
StreamURL string
|
||||||
|
StreamID string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *RequestParams) Valid() error {
|
func (p *RequestParams) Valid() error {
|
||||||
@@ -34,16 +38,18 @@ func (p *RequestParams) Valid() error {
|
|||||||
return ErrMissingParamsOffer
|
return ErrMissingParamsOffer
|
||||||
}
|
}
|
||||||
|
|
||||||
if p.SRTHost == "" {
|
if p.StreamID == "" {
|
||||||
return ErrMissingSRTHost
|
return ErrMissingStreamID
|
||||||
}
|
}
|
||||||
|
|
||||||
if p.SRTPort == 0 {
|
if p.StreamURL == "" {
|
||||||
return ErrMissingSRTPort
|
return ErrMissingStreamURL
|
||||||
}
|
}
|
||||||
|
isRTMP := strings.Contains(strings.ToLower(p.StreamURL), "rtmp")
|
||||||
|
isSRT := strings.Contains(strings.ToLower(p.StreamURL), "srt")
|
||||||
|
|
||||||
if p.SRTStreamID == "" {
|
if !(isRTMP || isSRT) {
|
||||||
return ErrMissingSRTStreamID
|
return ErrUnsupportedStreamURL
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -53,7 +59,7 @@ func (p *RequestParams) String() string {
|
|||||||
if p == nil {
|
if p == nil {
|
||||||
return ""
|
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
|
type MessageType string
|
||||||
@@ -175,6 +181,8 @@ var DonutSRTStreamID DonutInputOptionKey = "srt_streamid"
|
|||||||
var DonutSRTsmoother DonutInputOptionKey = "smoother"
|
var DonutSRTsmoother DonutInputOptionKey = "smoother"
|
||||||
var DonutSRTTranstype DonutInputOptionKey = "transtype"
|
var DonutSRTTranstype DonutInputOptionKey = "transtype"
|
||||||
|
|
||||||
|
var DonutRTMPLive DonutInputOptionKey = "rtmp_live"
|
||||||
|
|
||||||
type DonutInputFormat string
|
type DonutInputFormat string
|
||||||
|
|
||||||
func (d DonutInputFormat) String() 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 ErrHTTPPostOnly = errors.New("you must use http POST verb")
|
||||||
var ErrMissingParamsOffer = errors.New("ParamsOffer must not be nil")
|
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 ErrMissingSRTHost = errors.New("SRTHost must not be nil")
|
||||||
var ErrMissingSRTPort = errors.New("SRTPort must be valid")
|
var ErrMissingSRTPort = errors.New("SRTPort must be valid")
|
||||||
var ErrMissingSRTStreamID = errors.New("SRTStreamID must not be empty")
|
var ErrMissingSRTStreamID = errors.New("SRTStreamID must not be empty")
|
||||||
|
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/flavioribeiro/donut/internal/controllers"
|
"github.com/flavioribeiro/donut/internal/controllers"
|
||||||
"github.com/flavioribeiro/donut/internal/controllers/engine"
|
"github.com/flavioribeiro/donut/internal/controllers/engine"
|
||||||
@@ -41,11 +42,13 @@ func (h *SignalingHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) err
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
h.l.Infof("createAndValidateParams %s", params.String())
|
||||||
|
|
||||||
donutEngine, err := h.donut.EngineFor(¶ms)
|
donutEngine, err := h.donut.EngineFor(¶ms)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
h.l.Infof("EngineFor %#v", donutEngine)
|
||||||
|
|
||||||
// server side media info
|
// server side media info
|
||||||
serverStreamInfo, err := donutEngine.ServerIngredients()
|
serverStreamInfo, err := donutEngine.ServerIngredients()
|
||||||
@@ -57,20 +60,30 @@ func (h *SignalingHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) err
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
h.l.Infof("ServerIngredients %#v", serverStreamInfo)
|
||||||
|
h.l.Infof("ClientIngredients %#v", clientStreamInfo)
|
||||||
|
|
||||||
donutRecipe := donutEngine.RecipeFor(serverStreamInfo, clientStreamInfo)
|
donutRecipe, err := donutEngine.RecipeFor(serverStreamInfo, clientStreamInfo)
|
||||||
if donutRecipe == nil {
|
h.l.Info("after RecipeFor")
|
||||||
return entities.ErrMissingCompatibleStreams
|
h.l.Info("after RecipeFor err", err)
|
||||||
|
h.l.Info("after RecipeFor donutRecipe", donutRecipe)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
h.l.Infof("RecipeFor %#v", donutRecipe)
|
||||||
|
|
||||||
// We can't defer calling cancel here because it'll live alongside the stream.
|
// We can't defer calling cancel here because it'll live alongside the stream.
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
webRTCResponse, err := h.webRTCController.Setup(cancel, donutRecipe, params)
|
webRTCResponse, err := h.webRTCController.Setup(cancel, donutRecipe, params)
|
||||||
|
h.l.Infof("webRTCController.Setup %#v, err=%#v", webRTCResponse, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cancel()
|
cancel()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
//tODO: add explan
|
||||||
|
h.l.Info("before sleeping")
|
||||||
|
time.Sleep(5 * time.Second)
|
||||||
|
h.l.Info("after sleeping")
|
||||||
go donutEngine.Serve(&entities.DonutParameters{
|
go donutEngine.Serve(&entities.DonutParameters{
|
||||||
Cancel: cancel,
|
Cancel: cancel,
|
||||||
Ctx: ctx,
|
Ctx: ctx,
|
||||||
@@ -85,13 +98,18 @@ func (h *SignalingHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) err
|
|||||||
h.l.Errorw("error while streaming", "error", err)
|
h.l.Errorw("error while streaming", "error", err)
|
||||||
},
|
},
|
||||||
OnStream: func(st *entities.Stream) error {
|
OnStream: func(st *entities.Stream) error {
|
||||||
|
h.l.Infof("onstream %#v", st)
|
||||||
return h.webRTCController.SendMetadata(webRTCResponse.Data, st)
|
return h.webRTCController.SendMetadata(webRTCResponse.Data, st)
|
||||||
},
|
},
|
||||||
OnVideoFrame: func(data []byte, c entities.MediaFrameContext) error {
|
OnVideoFrame: func(data []byte, c entities.MediaFrameContext) error {
|
||||||
|
// sl[len(sl)-1]
|
||||||
|
h.l.Infof("OnVideoFrame %#v < %d > First %#v Last %#v", c, len(data), data[0:7], data[len(data)-7:])
|
||||||
return h.webRTCController.SendMediaSample(webRTCResponse.Video, data, c)
|
return h.webRTCController.SendMediaSample(webRTCResponse.Video, data, c)
|
||||||
},
|
},
|
||||||
OnAudioFrame: func(data []byte, c entities.MediaFrameContext) error {
|
OnAudioFrame: func(data []byte, c entities.MediaFrameContext) error {
|
||||||
return h.webRTCController.SendMediaSample(webRTCResponse.Audio, data, c)
|
h.l.Infof("OnAudioFrame %#v", c)
|
||||||
|
return nil
|
||||||
|
// return h.webRTCController.SendMediaSample(webRTCResponse.Audio, data, c)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -99,6 +117,7 @@ func (h *SignalingHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) err
|
|||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
|
|
||||||
err = json.NewEncoder(w).Encode(*webRTCResponse.LocalSDP)
|
err = json.NewEncoder(w).Encode(*webRTCResponse.LocalSDP)
|
||||||
|
h.l.Infof("webRTCResponse %#v", webRTCResponse)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cancel()
|
cancel()
|
||||||
return err
|
return err
|
||||||
|
26
notes_bitstream_filter.txt
Normal file
26
notes_bitstream_filter.txt
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
docker run -it --rm --entrypoint bash jrottenberg/ffmpeg:5.1.4-ubuntu2204
|
||||||
|
|
||||||
|
ffmpeg -bsfs | grep annex
|
||||||
|
# h264_mp4toannexb
|
||||||
|
# hevc_mp4toannexb
|
||||||
|
|
||||||
|
|
||||||
|
live_flv : In case of live network streams, if you force format, you may use live_flv option instead of flv to survive timestamp discontinuities.
|
||||||
|
|
||||||
|
-f live_flv
|
||||||
|
|
||||||
|
ff_live_flv_demuxer
|
||||||
|
|
||||||
|
ff_flv_demuxer
|
||||||
|
|
||||||
|
ffmpeg -h demuxer=live_flv
|
||||||
|
ffmpeg -h bsf=h264_mp4toannexb
|
||||||
|
|
||||||
|
example:
|
||||||
|
|
||||||
|
https://github.com/FFmpeg/FFmpeg/blob/9c6c4f3d476d7a8d423ec3b954254c6a67ebc792/libavformat/mux.c#L1351
|
||||||
|
https://github.com/search?q=repo%3AFFmpeg%2FFFmpeg%20ff_stream_add_bitstream_filter&type=code
|
||||||
|
|
||||||
|
Good explanaation
|
||||||
|
https://stackoverflow.com/questions/24884827/possible-locations-for-sequence-picture-parameter-sets-for-h-264-stream/24890903#24890903
|
||||||
|
Filter impl https://github.com/FFmpeg/FFmpeg/blob/9c6c4f3d476d7a8d423ec3b954254c6a67ebc792/libavcodec/bsf/h264_mp4toannexb.c#L276
|
@@ -2,6 +2,7 @@ ffmpeg -hide_banner -loglevel verbose \
|
|||||||
-re -f lavfi -i testsrc2=size=1280x720:rate=30,format=yuv420p \
|
-re -f lavfi -i testsrc2=size=1280x720:rate=30,format=yuv420p \
|
||||||
-f lavfi -i sine=frequency=1000:sample_rate=44100 \
|
-f lavfi -i sine=frequency=1000:sample_rate=44100 \
|
||||||
-c:v libx264 -preset veryfast -tune zerolatency -profile:v baseline \
|
-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=128:fontcolor=black" \
|
||||||
-b:v 1000k -bufsize 2000k -x264opts keyint=30:min-keyint=30:scenecut=-1 \
|
-b:v 1000k -bufsize 2000k -x264opts keyint=30:min-keyint=30:scenecut=-1 \
|
||||||
-c:a aac -b:a 128k \
|
-c:a aac -b:a 128k \
|
||||||
-f mpegts "udp://${SRT_INPUT_HOST}:${SRT_INPUT_PORT}?pkt_size=${PKT_SIZE}"
|
-f mpegts "udp://${SRT_INPUT_HOST}:${SRT_INPUT_PORT}?pkt_size=${PKT_SIZE}"
|
13
scripts/ffmpeg_rtmp.sh
Executable file
13
scripts/ffmpeg_rtmp.sh
Executable file
@@ -0,0 +1,13 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
while true
|
||||||
|
do
|
||||||
|
ffmpeg -hide_banner -loglevel debug \
|
||||||
|
-re -f lavfi -i testsrc2=size=1280x720:rate=30,format=yuv420p \
|
||||||
|
-f lavfi -i sine=frequency=1000:sample_rate=44100 \
|
||||||
|
-c:v libx264 -preset veryfast -tune zerolatency -profile:v baseline \
|
||||||
|
-vf "drawtext=text='RTMP streaming':box=1:boxborderw=10:x=(w-text_w)/2:y=(h-text_h)/2:fontsize=128:fontcolor=black" \
|
||||||
|
-bsf:v h264_mp4toannexb \
|
||||||
|
-b:v 1000k -bufsize 2000k -x264opts keyint=30:min-keyint=30:scenecut=-1 \
|
||||||
|
-c:a aac -b:a 128k \
|
||||||
|
-f flv -listen 1 -rtmp_live live "rtmp://${RTMP_HOST}:${RTMP_PORT}/live/app"
|
||||||
|
done
|
@@ -2,15 +2,13 @@
|
|||||||
window.metadataMessages = {}
|
window.metadataMessages = {}
|
||||||
|
|
||||||
window.startSession = () => {
|
window.startSession = () => {
|
||||||
let srtHost = document.getElementById('srt-host').value;
|
let streamURL = document.getElementById('stream-url').value;
|
||||||
let srtPort = document.getElementById('srt-port').value;
|
let streamID = document.getElementById('stream-id').value;
|
||||||
let srtStreamId = document.getElementById('srt-stream-id').value;
|
|
||||||
|
|
||||||
setupWebRTC((pc, offer) => {
|
setupWebRTC((pc, offer) => {
|
||||||
let srtFullAddress = JSON.stringify({
|
let srtFullAddress = JSON.stringify({
|
||||||
"srtHost": srtHost,
|
"streamURL": streamURL,
|
||||||
"srtPort": srtPort,
|
"streamID": streamID,
|
||||||
"srtStreamId": srtStreamId,
|
|
||||||
offer
|
offer
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -142,7 +140,9 @@ const log = (msg, level = "info") => {
|
|||||||
const el = document.createElement("p")
|
const el = document.createElement("p")
|
||||||
|
|
||||||
if (typeof(msg) !== "string") {
|
if (typeof(msg) !== "string") {
|
||||||
|
orig = msg
|
||||||
msg = "unknown log msg type " + typeof(msg)
|
msg = "unknown log msg type " + typeof(msg)
|
||||||
|
msg = msg + " [" + orig + "]"
|
||||||
level = "error"
|
level = "error"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -8,23 +8,21 @@
|
|||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<h1>SRT Config</h1>
|
<h1>Remote streaming</h1>
|
||||||
<b> SRT Host </b>
|
<b> URL </b>
|
||||||
<input type="text" id="srt-host" value="srt"> <br />
|
<input type="text" id="stream-url" value="srt://srt:40052"> <br />
|
||||||
|
|
||||||
<b> SRT Port </b>
|
<b> ID </b>
|
||||||
<input type="text" id="srt-port" value="40052" /> <br />
|
<input type="text" id="stream-id" value="stream-id" /> <br />
|
||||||
|
|
||||||
<b> SRT Stream ID </b>
|
|
||||||
<input type="text" id="srt-stream-id" value="stream-id" /> <br />
|
|
||||||
<button onclick="onConnect()"> Connect </button>
|
<button onclick="onConnect()"> Connect </button>
|
||||||
|
|
||||||
<h1>Video</h1>
|
<h1>Video</h1>
|
||||||
<div id="remoteVideos"></div>
|
<div id="remoteVideos"></div>
|
||||||
|
|
||||||
<h1>Metadata</h1>
|
<h1>Metadata</h1>
|
||||||
<div id="metadata"></div>
|
<div id="metadata"></div>
|
||||||
|
|
||||||
<h1>Logs</h1>
|
<h1>Logs</h1>
|
||||||
<div id="log"></div>
|
<div id="log"></div>
|
||||||
|
|
||||||
@@ -40,26 +38,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
docReady(function () {
|
docReady(function () {
|
||||||
const queryString = window.location.search;
|
|
||||||
const urlParams = new URLSearchParams(queryString);
|
|
||||||
|
|
||||||
window.onConnect = () => {
|
window.onConnect = () => {
|
||||||
window.startSession();
|
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>
|
</script>
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user