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
|
||||
|
||||
COPY . ./donut
|
||||
COPY ./go-astiav/ /Users/leandro.moreira/src/go-astiav/
|
||||
WORKDIR ${WD}/donut
|
||||
RUN go build .
|
||||
CMD ["/usr/src/app/donut/donut", "--enable-ice-mux=true"]
|
29
Makefile
29
Makefile
@@ -1,30 +1,19 @@
|
||||
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 origin srt 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 origin srt app
|
||||
|
||||
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:
|
||||
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:
|
||||
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
|
||||
|
@@ -13,8 +13,10 @@ services:
|
||||
- "6060:6060"
|
||||
depends_on:
|
||||
- srt
|
||||
- rtmp
|
||||
links:
|
||||
- srt
|
||||
- rtmp
|
||||
|
||||
test:
|
||||
build:
|
||||
@@ -36,6 +38,19 @@ services:
|
||||
- "./:/app/"
|
||||
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:
|
||||
build:
|
||||
context: .
|
||||
@@ -62,8 +77,8 @@ services:
|
||||
command: "/scripts/ffmpeg_mpegts_udp.sh"
|
||||
volumes:
|
||||
- "./scripts:/scripts"
|
||||
- "./fonts/0xProto:/usr/share/fonts"
|
||||
environment:
|
||||
- SRT_INPUT_HOST=srt
|
||||
- SRT_INPUT_PORT=1234
|
||||
- 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
|
||||
|
||||
replace github.com/asticode/go-astiav => /Users/leandro.moreira/src/go-astiav
|
||||
|
||||
require (
|
||||
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/pion/webrtc/v3 v3.1.47
|
||||
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-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-astikit v0.42.0 h1:pnir/2KLUSr0527Tv908iAH6EGYYrYta132vvjXsH5w=
|
||||
github.com/asticode/go-astikit v0.42.0/go.mod h1:h4ly7idim1tNhaVkdVBeXQZEE3L0xblP7fCWbgwipF0=
|
||||
github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
|
@@ -2,6 +2,7 @@ package engine
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/flavioribeiro/donut/internal/controllers/probers"
|
||||
"github.com/flavioribeiro/donut/internal/controllers/streamers"
|
||||
@@ -11,10 +12,10 @@ import (
|
||||
)
|
||||
|
||||
type DonutEngine interface {
|
||||
Appetizer() entities.DonutAppetizer
|
||||
Appetizer() (entities.DonutAppetizer, error)
|
||||
ServerIngredients() (*entities.StreamInfo, error)
|
||||
ClientIngredients() (*entities.StreamInfo, error)
|
||||
RecipeFor(server, client *entities.StreamInfo) *entities.DonutRecipe
|
||||
RecipeFor(server, client *entities.StreamInfo) (*entities.DonutRecipe, error)
|
||||
Serve(p *entities.DonutParameters)
|
||||
}
|
||||
|
||||
@@ -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,10 +106,16 @@ 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,
|
||||
// Action: entities.DonutBypass,
|
||||
Action: entities.DonutTranscode,
|
||||
Codec: entities.H264,
|
||||
},
|
||||
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 {
|
||||
// TODO: implement input based on param to build proper SRT/RTMP/etc
|
||||
func (d *donutEngine) Appetizer() (entities.DonutAppetizer, error) {
|
||||
if strings.Contains(strings.ToLower(d.req.StreamURL), "rtmp") {
|
||||
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
|
||||
URL: fmt.Sprintf("%s/%s", d.req.StreamURL, d.req.StreamID),
|
||||
Options: map[entities.DonutInputOptionKey]string{
|
||||
entities.DonutSRTStreamID: d.req.SRTStreamID,
|
||||
entities.DonutRTMPLive: "live",
|
||||
},
|
||||
// Format: "flv",
|
||||
}, 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 (
|
||||
"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)
|
||||
|
||||
|
@@ -50,7 +50,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 {
|
||||
@@ -78,7 +81,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()
|
||||
@@ -89,20 +92,26 @@ 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.SetLogCallback(func(_ astiav.Classer, l astiav.LogLevel, fmt, msg string) {
|
||||
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 {
|
||||
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
|
||||
@@ -121,6 +130,7 @@ func (c *LibAVFFmpegStreamer) Stream(donut *entities.DonutParameters) {
|
||||
c.onError(donut.Ctx.Err(), donut)
|
||||
return
|
||||
default:
|
||||
c.l.Infof("started reading frame")
|
||||
if err := p.inputFormatContext.ReadFrame(inPkt); err != nil {
|
||||
if errors.Is(err, astiav.ErrEof) {
|
||||
c.l.Info("streaming has ended")
|
||||
@@ -141,6 +151,63 @@ func (c *LibAVFFmpegStreamer) Stream(donut *entities.DonutParameters) {
|
||||
isVideoBypass := donut.Recipe.Video.Action == entities.DonutBypass
|
||||
if isVideo && isVideoBypass {
|
||||
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{
|
||||
PTS: int(inPkt.Pts()),
|
||||
DTS: int(inPkt.Dts()),
|
||||
@@ -169,12 +236,19 @@ func (c *LibAVFFmpegStreamer) Stream(donut *entities.DonutParameters) {
|
||||
continue
|
||||
}
|
||||
|
||||
if isAudio {
|
||||
continue
|
||||
}
|
||||
|
||||
c.l.Infof("start sending packet")
|
||||
// c.processPacket(inPkt, s, donut)
|
||||
if err := s.decCodecContext.SendPacket(inPkt); 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
|
||||
@@ -182,7 +256,7 @@ func (c *LibAVFFmpegStreamer) Stream(donut *entities.DonutParameters) {
|
||||
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
|
||||
@@ -414,8 +488,6 @@ 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?
|
||||
args = astiav.FilterArgs{
|
||||
"channel_layout": s.decCodecContext.ChannelLayout().String(),
|
||||
"sample_fmt": s.decCodecContext.SampleFormat().Name(),
|
||||
@@ -483,6 +555,29 @@ func (c *LibAVFFmpegStreamer) prepareFilters(p *libAVParams, closer *astikit.Clo
|
||||
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) {
|
||||
if err = s.buffersrcContext.BuffersrcAddFrame(f, astiav.NewBuffersrcFlags(astiav.BuffersrcFlagKeepRef)); err != nil {
|
||||
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?
|
||||
s.filterFrame.SetPictureType(astiav.PictureTypeNone)
|
||||
|
||||
c.l.Infof("start encoding")
|
||||
if err = c.encodeFrame(s.filterFrame, s, donut); err != nil {
|
||||
err = fmt.Errorf("main: encoding and writing frame failed: %w", err)
|
||||
return
|
||||
@@ -520,6 +615,7 @@ func (c *LibAVFFmpegStreamer) encodeFrame(f *astiav.Frame, s *streamContext, don
|
||||
}
|
||||
|
||||
for {
|
||||
c.l.Infof("start receiving packet")
|
||||
if err = s.encCodecContext.ReceivePacket(s.encPkt); err != nil {
|
||||
if errors.Is(err, astiav.ErrEof) || errors.Is(err, astiav.ErrEagain) {
|
||||
err = nil
|
||||
@@ -535,6 +631,7 @@ func (c *LibAVFFmpegStreamer) encodeFrame(f *astiav.Frame, s *streamContext, don
|
||||
isVideo := s.decCodecContext.MediaType() == astiav.MediaTypeVideo
|
||||
if isVideo {
|
||||
if donut.OnVideoFrame != nil {
|
||||
c.l.Infof("sending transcoded video")
|
||||
if err := donut.OnVideoFrame(s.encPkt.Data(), entities.MediaFrameContext{
|
||||
PTS: int(s.encPkt.Pts()),
|
||||
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
|
||||
if isAudio {
|
||||
if donut.OnAudioFrame != nil {
|
||||
c.l.Infof("sending transcoded audio")
|
||||
if err := donut.OnAudioFrame(s.encPkt.Data(), entities.MediaFrameContext{
|
||||
PTS: int(s.encPkt.Pts()),
|
||||
DTS: int(s.encPkt.Dts()),
|
||||
|
@@ -42,14 +42,14 @@ func (c *WebRTCController) Setup(cancel context.CancelFunc, donutRecipe *entitie
|
||||
response.Connection = peer
|
||||
|
||||
var videoTrack *webrtc.TrackLocalStaticSample
|
||||
videoTrack, err = c.CreateTrack(peer, donutRecipe.Video.Codec, string(entities.VideoType), params.SRTStreamID)
|
||||
videoTrack, err = c.CreateTrack(peer, donutRecipe.Video.Codec, string(entities.VideoType), params.StreamID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
response.Video = videoTrack
|
||||
|
||||
var audioTrack *webrtc.TrackLocalStaticSample
|
||||
audioTrack, err = c.CreateTrack(peer, donutRecipe.Audio.Codec, string(entities.AudioType), params.SRTStreamID)
|
||||
audioTrack, err = c.CreateTrack(peer, donutRecipe.Audio.Codec, string(entities.AudioType), params.StreamID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@@ -3,6 +3,7 @@ package entities
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/asticode/go-astiav"
|
||||
@@ -27,6 +28,9 @@ type RequestParams struct {
|
||||
SRTPort uint16 `json:",string"`
|
||||
SRTStreamID string
|
||||
Offer webrtc.SessionDescription
|
||||
|
||||
StreamURL string
|
||||
StreamID string
|
||||
}
|
||||
|
||||
func (p *RequestParams) Valid() error {
|
||||
@@ -34,16 +38,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 +59,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
|
||||
@@ -175,6 +181,8 @@ var DonutSRTStreamID DonutInputOptionKey = "srt_streamid"
|
||||
var DonutSRTsmoother DonutInputOptionKey = "smoother"
|
||||
var DonutSRTTranstype DonutInputOptionKey = "transtype"
|
||||
|
||||
var DonutRTMPLive DonutInputOptionKey = "rtmp_live"
|
||||
|
||||
type DonutInputFormat string
|
||||
|
||||
func (d DonutInputFormat) String() string {
|
||||
|
@@ -9,6 +9,10 @@ var ErrHTTPGetOnly = errors.New("you must use http GET verb")
|
||||
var ErrHTTPPostOnly = errors.New("you must use http POST verb")
|
||||
var ErrMissingParamsOffer = errors.New("ParamsOffer must not be nil")
|
||||
|
||||
var ErrMissingStreamURL = errors.New("stream URL must not be nil")
|
||||
var ErrMissingStreamID = errors.New("stream ID must not be nil")
|
||||
var ErrUnsupportedStreamURL = errors.New("unsupported stream")
|
||||
|
||||
var ErrMissingSRTHost = errors.New("SRTHost must not be nil")
|
||||
var ErrMissingSRTPort = errors.New("SRTPort must be valid")
|
||||
var ErrMissingSRTStreamID = errors.New("SRTStreamID must not be empty")
|
||||
|
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/flavioribeiro/donut/internal/controllers"
|
||||
"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 {
|
||||
return err
|
||||
}
|
||||
h.l.Infof("createAndValidateParams %s", params.String())
|
||||
|
||||
donutEngine, err := h.donut.EngineFor(¶ms)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
h.l.Infof("EngineFor %#v", donutEngine)
|
||||
|
||||
// server side media info
|
||||
serverStreamInfo, err := donutEngine.ServerIngredients()
|
||||
@@ -57,20 +60,30 @@ func (h *SignalingHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) err
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
h.l.Infof("ServerIngredients %#v", serverStreamInfo)
|
||||
h.l.Infof("ClientIngredients %#v", clientStreamInfo)
|
||||
|
||||
donutRecipe := donutEngine.RecipeFor(serverStreamInfo, clientStreamInfo)
|
||||
if donutRecipe == nil {
|
||||
return entities.ErrMissingCompatibleStreams
|
||||
donutRecipe, err := donutEngine.RecipeFor(serverStreamInfo, clientStreamInfo)
|
||||
h.l.Info("after RecipeFor")
|
||||
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.
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
webRTCResponse, err := h.webRTCController.Setup(cancel, donutRecipe, params)
|
||||
h.l.Infof("webRTCController.Setup %#v, err=%#v", webRTCResponse, err)
|
||||
if err != nil {
|
||||
cancel()
|
||||
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{
|
||||
Cancel: cancel,
|
||||
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)
|
||||
},
|
||||
OnStream: func(st *entities.Stream) error {
|
||||
h.l.Infof("onstream %#v", st)
|
||||
return h.webRTCController.SendMetadata(webRTCResponse.Data, st)
|
||||
},
|
||||
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)
|
||||
},
|
||||
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)
|
||||
|
||||
err = json.NewEncoder(w).Encode(*webRTCResponse.LocalSDP)
|
||||
h.l.Infof("webRTCResponse %#v", webRTCResponse)
|
||||
if err != nil {
|
||||
cancel()
|
||||
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 \
|
||||
-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=128: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}"
|
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.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"
|
||||
}
|
||||
|
||||
|
@@ -8,15 +8,13 @@
|
||||
|
||||
<body>
|
||||
|
||||
<h1>SRT Config</h1>
|
||||
<b> SRT Host </b>
|
||||
<input type="text" id="srt-host" value="srt"> <br />
|
||||
<h1>Remote streaming</h1>
|
||||
<b> URL </b>
|
||||
<input type="text" id="stream-url" value="srt://srt:40052"> <br />
|
||||
|
||||
<b> SRT Port </b>
|
||||
<input type="text" id="srt-port" value="40052" /> <br />
|
||||
<b> ID </b>
|
||||
<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>
|
||||
|
||||
<h1>Video</h1>
|
||||
@@ -40,26 +38,9 @@
|
||||
}
|
||||
|
||||
docReady(function () {
|
||||
const queryString = window.location.search;
|
||||
const urlParams = new URLSearchParams(queryString);
|
||||
|
||||
window.onConnect = () => {
|
||||
window.startSession();
|
||||
}
|
||||
|
||||
if (urlParams.has('srtHost')) {
|
||||
document.getElementById('srt-host').value = urlParams.get('srtHost');
|
||||
}
|
||||
if (urlParams.has('srtPort')) {
|
||||
document.getElementById('srt-port').value = urlParams.get('srtPort');
|
||||
}
|
||||
if (urlParams.has('srtStreamId')) {
|
||||
document.getElementById('srt-stream-id').value = urlParams.get('srtStreamId');
|
||||
}
|
||||
|
||||
if (urlParams.get('autoplay') === "true") {
|
||||
onConnect();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
|
Reference in New Issue
Block a user