add draft for rtmp

This commit is contained in:
Leandro Moreira
2024-05-12 15:05:35 -03:00
parent 4cb98061df
commit 199c23e9fd
23 changed files with 420 additions and 98 deletions

View File

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

View File

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

View File

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

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

1
go-astiav Submodule

Submodule go-astiav added at 338c4dfcca

4
go.mod generated
View File

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

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

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)
}
@@ -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
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) {
if strings.Contains(strings.ToLower(d.req.StreamURL), "rtmp") {
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
} 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
}

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

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

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

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

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

View 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

View File

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

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

@@ -8,23 +8,21 @@
<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>
<div id="remoteVideos"></div>
<h1>Metadata</h1>
<div id="metadata"></div>
<h1>Logs</h1>
<div id="log"></div>
@@ -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>