avplumber is a graph-based modular environment for processing video & audio streams, raw or encoded. Frames or packets can be processed using nodes. Nodes for common tasks are provided, most of them based on FFmpeg libraries (libav). It's also fairly easy to write custom ones in C++.
Make sure to clone this repo with --recursive
option.
git clone --recursive https://github.com/amagimedia/avplumber
docker build -t avplumber .
docker run -p 20200:20200 avplumber -p 20200
or if you don't want to use Docker but have Ubuntu:
apt install git gcc pkg-config make cmake libavcodec-dev libavdevice-dev libavfilter-dev libavformat-dev libavutil-dev libswresample-dev libcurl4-openssl-dev libboost-thread-dev libboost-system-dev libssl-dev
make -j`nproc`
./avplumber
and in a different terminal:
nc localhost 20200
and you can type some commands (see Control protocol) or paste a script (e.g. from examples/
directory)
To quickly run demo with FFmpeg test source, use the provided Docker Compose file:
script=remux_analyze_audio.avplumber docker compose -f examples/compose/rtmp_test_source.yml up
After Docker pulls and builds everything, you should see stream statistics JSON lines, once per second.
Output stream will be available at rtmp://localhost/live/output
Change script to complicated_transcoder.avplumber
to test transcoding.
This demo uses MediaMTX as streaming server.
brew install docker docker-compose colima
colima start
avplumber can be built as a static library: make static_library
will make libavplumber.a
which your app or library can link to. library_examples/obs-avplumber-source/CMakeLists.txt
is an example of CMake integration.
Public API is contained in src/avplumber.hpp
.
Example: library_examples/obs-avplumber-source
- source plugin for OBS supporting video decoder to texture direct VRAM copy.
Nodes in the graph are connected by edges. Edge is implemented as a queue. queue.plan_capacity
can be used to change its size. Type of data inside queue is determinated automatically when the queue is created.
Data types:
av::Packet
- encoded media packetav::VideoFrame
- raw video frameav::AudioSamples
- raw audio frame (usually 1024 samples of all channels)
Some nodes support multiple input/output types - they work like templates/generics in programming languages (and are implemented this way). If the data type can be deduced from source or sink edges, there is no need to provide it explicitly. But if it can't be, use template syntax in type
field of the node JSON object:
node_type<data_type>
for example:
split<av::VideoFrame>
avplumber is controlled using text commands on TCP socket, so it can be controlled manually using netcat
or telnet
. --port
argument specifies the port to listen on.
--script
argument specifies commands to execute on startup.
Control protocol loosely follows MVCP.
On new connection, server (avplumber) sends a line: 100 VTR READY
Client issues a command followed by arguments separated by spaces and ending with the new line character. The last argument may contain spaces.
Server (avplumber) responds with line with status code and information:
200 OK
- command accepted, empty response201 OK
- command accepted, response will follow and an empty line marks the end of response400 Unknown command: ...
500 ERROR: ...
BYE
and connection close - special response forbye
command
Commands:
hello
Replies with HELLO
version
Replies with app version and build date
bye
Closes the connection
node.add { ...json object... }
Add node
node.add_create { ...json object... }
Add and create node (without starting it right now)
node.add_start { ...json object... }
Add, create and start node
node.delete name
Delete node
node.start name
Start node
node.stop name
Stop node (auto_restart
action is inhibited)
node.stop_wait name
Stop node, return reply when node really stopped
node.auto_restart name
Stop node and trigger its auto_restart
action. This command is probably most useful for restarting input after changing its URL, with confidence that it will be handled the same way as if the previous input stream finished. And unlike retry group.restart ...
, nothing blocks! See also more experimental node.interrupt
.
node.interrupt name
Stop node even if it is being constructed right now. Also, bypass any locks. Currently only input
node supports this command. After interruption, auto_restart
action will be triggered.
node.param.set node_name param_name new_json_value
Change node parameter. Equivalent in JavaScript: node['param_name'] = JSON.parse('new_json_value')
. WARNING: Node won't accept new parameters until restarted.
node.param.get node_name
Get whole node JSON object, for example:
node.param.get encode720
{"codec":"libx264","dst":"venc1","group":"out","name":"encode720","options":{"b":"4M","bufsize":"8M","flags":"+cgop","g":25,"level":"3.2","maxrate":"5M","minrate":"3M","muxdelay":0,"muxrate":0,"preset":"ultrafast","profile":"baseline","sc_threshold":0,"x264opts":"no-scenecut"},"src":"vs1o","type":"enc_video"}
OK
node.param.get node_name param_name
Get single parameter as JSON. Example:
node.param.get encode720 group
"out"
OK
node.object.get node_name object_name
Get object from node. Example:
node.object.get input streams
201 OK
[{"codec":"h264","index":0,"type":"V"},{"codec":"h264","index":1,"type":"V"},{"codec":"h264","index":2,"type":"V"},{"codec":"aac","index":3,"type":"A"},{"codec":"aac","index":4,"type":"A"},{"codec":"aac","index":5,"type":"A"}]
node.object.get input programs
201 OK
[{"index":0,"streams":[0,3,4,5]},{"index":1,"streams":[1,3,4,5]},{"index":2,"streams":[2,3,4,5]}]
queue.plan_capacity queue_name capacity
Plan capacity of queue (which must be created after issuing this command). capacity
is positive integer. Warning: moodycamel::ReaderWriterQueue (the library we use for queues) forces the queue size to be the smallest 2^n-1 (n=integer) larger or equal to specified capacity.
Default capacity can also be changed - use *
as a queue_name, e.g. queue.plan_capacity * 7
queues.stats
Print (human-readable) statistics of queues (graph edges). Example:
[#...................] 1821.1 videoin
[....................] 243265 aenc1
[....................] 243265 aenc0
[....................] 243265 aenc2
[....................] 243265 mux2
[#####...............] 243265 aenc
[....................] 1821.03 audioin
^ ^ ^
queue fill last PTS queue name
queue.drain queue_name
Wait until queue is empty.
group.restart group
group.stop group
group.start group
output.start output_group
output.stop output_group
hwaccel.init { "name": "name", "type": "type" }
Init hardware accelerator which may be used for encoding, decoding or filtering video frames.
- name - identifier, supports global objects syntax (
@
). If accelerator with given name already exists, it isn't touched and no error is returned. - type - currently only
cuda
is supported
stats.subscribe { ... json object ... }
Subscribe to statistics. JSON object structure:
{
"url":"", // URL to put statistics using HTTP POST. Empty to write them to console.
"name":"qwerty", // opaque
"interval":1, // refresh interval, in seconds
"streams":{
"audio":[
{
"q_pre_dec":"audioin", // name of queue before decoder (kbitrate, speed, AV_diff are taken from it)
"q_post_dec":"a10", // name of queue after decoder (fps, width, height, pix_fmt, field_order, samplerate, samplerate_md, channel_layout are taken from it)
"decoder":"Audio_Decode" // name of decoder node (codec, type are taken from it)
}
],
"video":[
{
"q_pre_dec":"videoin",
"q_post_dec":"v05",
"decoder":"Video_Decode"
}
]
},
"sentinel":"Video_Sentinel" // name of sentinel node (card flag is taken from it)
}
event.on.node.finished event_name node_name
When node finishes, signal (wake up) an event event_name
event.wait event_name
Wait for event event_name
retry command arguments ...
Repeat command until it succeeds. Intended for startup scripts (--script
). Not recommended for remote control.
detach command arguments ...
Return 200 OK immediately, run command in background thread.
Each node is described by a JSON object consisting of the following fields:
name
(string without spaces) - optional, specifies identifier that can be later used for controlling the node- if specified, must be unique within the instance
- if unspecified, the string
type@memory_address
will be generated and used
type
(string) - mandatorygroup
(string) - used for grouping together nearby nodes. Example: transcoder that will have separate input and output groups so that when input URL is changed, only demuxer and decoders will be restarted, not encoders and muxer.auto_restart
(string) - optional:off
(default) - let the node stop without restartingon
- restart single node when it finishes/crashesgroup
- restart the whole group to which the node belongspanic
- when the node finishes/crashes, shutdown the whole avplumber instance
src
(string for single-input nodes, list of strings for multi-input nodes) - source edgedst
(string for single-output nodes, list of strings for multi-output nodes) - sink edgeoptional
(bool) - optional: when creating the node fails:true
- ignore exceptions (return 20x) and pretend nothing bad happenedfalse
(default) - fail the whole operation (e.g. starting a group)
Most nodes have also their specific parameters which are specified on the same level as the fields above.
1 output: av::Packet
url
(string of URL)options
(dictionary) - options for libavformat
Rate limit output packets/frames to wallclock. This way, DTS (in packets) or PTS (in frames) differences will equal wallclock differences at this node's sink.
1 input, 1 output: anything
leak_after
(float, seconds) - if specified, bypass rate limiting after having input packets available immediately (in other words, at least one packet was always enqueued) for specified time. Intended for segmented inputs for which realtime node is useful to prevent bursts, while we don't want clock drift problems and missed segments in long running streams.speed
(float) - default 1, implemented by scaling wallclock's timebase (millisecond precision) so values between ~0.9995 and ~1.0006 are treated as 1.negative_time_tolerance
(float, seconds) - default0.25
. Do not resync if newly arrived packet should have been emitted at most that much time in the past. 0 to disable and always resync in such situation - effectively increasing latency until sufficient buffering for smooth output is achieved.negative_time_discard
(float, seconds) - if specified, if newly arrived packet should have been emitted at least that much time in the past, discard this packet. Discarding has lower priority than resyncing (negative_time_tolerance
), so the value must be less thannegative_time_tolerance
to make sense, equal or higher values disable discarding.discontinuity_threshold
(float, seconds) - default1
. If we need to wait for more than specified time, treat as discontinuity and resync. Default value may be unsuitable (too small) for multiple source synchronization in case more data is buffered.jitter_margin
(float, seconds) - default0
. When (re)syncing, add this value to the time to be waited. This prevents frequent resyncing and visible jitter at the cost of higher latency. It makes sense only with unbuffered output (e.g. display)initial_jitter_margin
(float, seconds) - default =jitter_margin
.jitter_margin
to use for the first frame received after node start, after discontinuity or afterleak_after
-triggered bypass, but not after "negative time to wait (...), resyncing"team
(string, name of instance-shared object) - if specified, realtime nodes with the same team will cooperate to have their output synchronized. Use only if timestamps are synchronized.master
(bool) - defaulttrue
. Only masters are allowed to resync in case of discontinuity. A team can have multiple masters.
For each passing packet, time to wait is computed (how long should we sleep before outputting that packet, to maintain realtime output rate) and different actions are performed based on its value
- if timeToWait <= 0:
- if timeToWait < -negative_time_tolerance:
- resync and emit packet
- else if timeToWait < -negative_time_discard:
- discard this packet
- else: /* -negative_time_discard <= timeToWait <= 0
*/
- emit packet immediately
- if timeToWait < -negative_time_tolerance:
- else: /* timeToWait > 0 */
- if timeToWait < discontinuity_threshold:
- normal behavior - wait timeToWait and emit packet
- else: /* timeToWait >= discontinuity_threshold */
- resync and emit packet
- if timeToWait < discontinuity_threshold:
Note: This pseudocode omits leak_after
logic.
Recommended options for encoding from segmented input (HLS, DASH):
- speed: 1.01
- leak_after: segment length * 2
Recommended options for displaying live video:
- negative_time_tolerance: 0.001 - 0.02 depending on clock precision of your system
- jitter_margin: 0.1 or some more
1 input, many outputs: av::Packet
streams_filter
(string): if present, filter input streams according to ffmpeg syntax (for example "p:1
" to select Program 1 - useful for HLSes) before parsing routing keysrouting
(dictionary of string => string): stream mapping, keys are streams in input file, values are queues, like{ "v:0": "videoin", "a:0": "audioin" }
, used instead ofdst
- keys may be prefixed with
?
to indicate optional input (ignore the route if stream not found)
- keys may be prefixed with
wait_for_keyframe
(bool): default false, discard packets until a keyframe appears in any video stream
1 input: av::Packet
, 1 output: av::VideoFrame
or av::AudioSamples
respectively
codec
(string) - optional, codec name, auto-detected by libavcodec if not specifiedcodec_map
(dictionary of string => string) - optional, use specified decoder for matching stream's codec, for example to use cuvid for h264 and hevc streams:"codec_map": {"h264": "h264_cuvid", "hevc": "hevc_cuvid"}
pixel_format
(string) - optional, if unspecified, libavcodec and/or codec will select best possible pixel format for given input stream. As seen inpix_fmt
field of FFmpeg'sAVPacket
, so it doesn't have to be any real pixel format (e.g.yuv420p
) but can also be hardware acceleration specification (e.g.cuda
)- if starts with
?
, prefer specified pixel format, e.g.?cuda
, but allow use of any - if doesn't start with
?
, force specified pixel format, fail if it's incompatible with codec or stream
- if starts with
hwaccel
(string, name of instance-shared object) - optional, name of hwaccel previously created withhwaccel.init
hwaccel_only_for_codecs
(list of strings) - use hwaccel only for specified input stream codecs, useful because apparently settinghw_device_ctx
in normally-software libavcodecs triggers frame corruption bugsoptions
(dictionary) - optional, options passed to libavcodec
Set PTS to timecode in video frame's side data.
1 input, 1 output: av::VideoFrame
-
team
(string, name of instance-shared object) - if specified, multipleextract_timestamps
andextract_timestamps_slave
nodes within the same team will share the same offset, so streams not containing timecode side data (e.g. audio) will be synchronized to the same timecode, too (as long as their PTSes are synchronized to each other). -
passthrough_before_available
(bool) - defaultfalse
, meaning that processing will be blocked (and input queue will grow) as long as timecode isn't available. If true, frames will be passed through with PTS unchanged in such case. -
drop_before_available
(bool) - discard incoming packets before timecode is available. Disabled by default. Has lower priority thanpassthrough_before_available
. -
timecodes
(list of strings), default\["S12M"\]
- side data to get timecodes from. If specified timecode doesn't exist, next one in the list is tried. Possible items:S12M.1
orS12M
- SMPTE 12M = SEIS12M.2
S12M.3
GOP
-
liveu
(bool, default false) - workaround for LiveU-encoded SMPTE 12M. Treat drop bit as a part of frames field. -
frame_rate_source
(string) - timecodes have frame numbers, so to calculate PTS from them, number of frames per second must be known. Possible values:fps
- default. Use FPS from nearest node implementing IFrameRateSource (decoder, filter orforce_fps
)timebase
- use 1/timebase from nearest node implementing ITimeBaseSource (decoder, filter orforce_fps
)
both values may yield the same or different results depending on input stream. Some video streams are generated by skipping every second frame from higher FPS stream, with S12M side-data preserved in remaining frames. In such cases
fps
is wrong andtimebase
is correct.
Set PTS to timecode extracted by extract_timestamps
node.
1 input, 1 output: av::VideoFrame
or av::AudioSamples
Supports parameters working the same as in extract_timestamps
node:
team
- mandatorypassthrough_before_available
drop_before_available
1 input, 1 output: av::VideoFrame
or av::AudioSamples
, respectively
graph
(string) - FFmpeg filter graphhwaccel
(string, name of instance-shared object) - optional (mandatory for some filters), name of hwaccel previously created withhwaccel.init
Duplicate and drop frames to achieve requested FPS
1 input, 1 output: av::VideoFrame
fps
(string of rational) - target FPS as a string, e.g.25
or30000/1001
Set initial metadata to allow nodes that rely on them to start when real metadata aren't available yet.
1 input, 1 output: av::VideoFrame
or av::AudioSamples
Parameters for video:
width
(int) - default 1920height
(int) - default 1080pixel_format
(string) - defaultyuv420p
real_pixel_format
(string) - specify only ifpixel_format
is hardware-accelerated (e.g.cuda
)
Parameters for audio:
sample_rate
(int) - default 48000sample_format
(string) - defaults32p
channel_layout
(string) - defaultstereo
A sentinel
- guards streams against wild timestamps - PTSes jumping forward or backward, or repeating timestamps
- inserts backup frames when input signal is not available for specified time:
- in audio stream: silence
- in video stream, for maximum time of
freeze
parameter: repeated last frame - in video stream: custom slate, can be used for adding "we'll be back shortly" card
Sentinel's output has "ideal" timestamps with tolerance specified in sentinel's parameters. In other words, it ensures output stream continuity.
1 input, 1 output: av::VideoFrame
/av::AudioSamples
-
timeout
(float) - default 1, seconds to wait for input frame before inserting frozen or backup frame -
correction_group
(string, name of instance-shared object) - optional, defaults to"default"
, used for sharing the clock between streams -
forward_start_shift
(bool):true
: if input streams are present when starting the sentinels, forward relative shifts of their first packets (i.e. A-V offset) to output.false
(default): start all output streams at exact PTS = 10 seconds (hardcoded inPTSCorrectorCommon
class)
-
max_streams_diff
(float) - default0.001
, tolerance in seconds -
lock_timeshift
(bool) - after receiving first PTS, maintain constant input-output PTS difference. Disabled by default. Enable only if you're sure that input timestamps are synchronized to real-time clock. -
reporting_url
(optional, string of URL) - if specified, correction time shift changes will be reported to this URL as HTTP POST with JSON body:{"changed_at":128.1,"input_pts_offset":126.75999999999999,"output_pts_offset":10.0}
changed_at
- output timestamp of the change relative to first output PTS (output_pts_offset
)input_pts_offset
- what sentinel needs to add to input timestamp to achieve output PTS, minusoutput_pts_offset
output_pts_offset
= first output PTS, constant through processing, hardcoded in PTSCorrectorCommon class
For video only:
freeze
(float) - default 5, seconds to duplicate last good frame before outputting backup framebackup_frame
(string of URL) - backup frame (slate) imagebackup_picture_buffer
(string, name of instance-shared object) - read backup frame (slate) from this buffer. Usepicture_buffer_sink
to write frame to the buffer. Sentinel will reload the slate from the picture buffer every 64 frames and on every input signal break triggering slate insertion.initial_picture_buffer
(string, name of instance-shared object) - initialize last frame buffer with this buffer, so that at the beginning of stream it will be used for at mostfreeze
duration. Useful to insert black frame instead of slate at the beginning whenforward_start_shift
is set to false. If unspecified, regularbackup_frame
orbackup_picture_buffer
will be used.
For sentinel_video
, either backup_frame
or backup_picture_buffer
must be provided.
Dynamic video scaler, maintains constant output dimensions and pixel
format even if input stream changes parameters. Does not resample
FPS (see force_fps
node)
1 input, 1 output: av::VideoFrame
dst_width
(int)dst_height
(int)dst_pixel_format
(string)flags
(list of strings) - list of possible flags: https://www.ffmpeg.org/doxygen/3.2/swscale_8h_source.html#l00057
Dynamic audio resampler, maintains continuity of output stream even if input stream changes parameters.
1 input, 1 output: av::AudioSamples
dst_sample_rate
(int)dst_channel_layout
(string)dst_sample_format
(string)compensation
(float)0
(default) means brutal compensation using built-in avplumber's sample dropping algorithm- any negative value means brutal compensation using libswresample, may not work correctly
- positive value between 0 and 1 means soft compensation using libswresample, value means fraction of samples to compensate, may not work correctly
1 input, multi outputs: anything
drop
(bool) - drop packets if output queue is full, disabled by default
Set keyframe flag in frame to make encoder output keyframe. Unlike -g
encoder option in FFmpeg, works with non-integer FPS.
1 input, 1 output: av::VideoFrame
interval_sec
(int / float / string of rational) - keyframe interval, in seconds
Encodes video or audio frames.
1 input: av::VideoFrame
or av::AudioSamples
, 1 output: av::Packet
codec
(string) - mandatoryoptions
(dictionary) - options passed to libavcodechwaccel
(string, name of instance-shared object) - optional (mandatory for some encoders), name of hwaccel previously created withhwaccel.init
timestamps_passthrough
(bool) - defaultfalse
, intended for codecs that don't buffer data (otherwise bad things like repeated timestamps may happen), replace PTS & DTS in outgoing packet with incoming PTS
Insert it between demuxer and muxer to remux packets without transcoding.
1 input, 1 output: av::Packet
no parameters
1 input, 1 output: av::Packet
bsf
(string) - name of bsf to use
multiple inputs, 1 output: av::Packet
fix_timestamps
(bool) - shift PTSes and DTSes so that DTSes are always increasing and (PTS >= DTS). Disabled by default.ts_sort_wait
(float, seconds) - default2.5
, maximum time to wait for all streams to select the packet with least DTS. Set to0
to emit packets as soon as they arrive.
1 input: av::Packet
format
(string) - mandatoryurl
(string of URL) - mandatoryoptions
(dictionary) - format options that will be passed to libavformat
Write stream of raw packets or frames to file or pipe. Unlike output
node, can be used anywhere in graph. Outputs whatever will be thrown on
it. Use nodes rescale_video
or resample_audio
to convert to required
format.
1 input: anything
path
(string) - file/pipe name, not URL, must be reachable via Unix file structure, libavformat protocols liketcp://
aren't supportedoutput_group
(string) - default "default
". Name of output group for control (seeoutput.start
andoutput.stop
commands) and synchronization. Since raw output doesn't have PTSes, avplumber will try to synchronize audio with video by cutting first audio frame to make it start together with first video frame.
Take a frame and write it to picture buffer that can be later used by sentinel_video
.
1 input: av::VideoFrame
buffer
(string, name of instance-shared object) - mandatory, picture buffer nameonce
(bool) - default true, finish after getting a single frame
Discards incoming packets just like /dev/null
.
1 input: anything
no parameters
Get video frames from CUDA IPC memory. Frame pointer and parameters are read from named pipe. See src/nodes/cuda/ipc_cuda_source.cpp
for structure.
1 output: av::VideoFrame
(frame's pixel format will always be cuda
)
pipe
(string) - mandatory, path to named pipehwaccel
(string, name of instance-shared object) - mandatory, name of hwaccel previously created withhwaccel.init
Get audio frames from named pipe. See src/nodes/ipc_audio_source.cpp
for header structure. Header must be followed by interleaved audio samples.
1 output: av::AudioSamples
pipe
(string) - mandatory, path to named pipe
Enabled only if avplumber is compiled with BUILD_TYPE=Debug
. Delay packets
or frames for a random time. Timestamps aren't modified. Delay will be
gradually decreased down to 1ms if a congestion is detected.
1 input, 1 output: anything
Enabled only if avplumber is compiled with BUILD_TYPE=Debug
. Delay packets or frames for specified time. Timestamps aren't modified.
1 input, 1 output: anything
delay
(float) - mandatory, delay in seconds
Some nodes (sentinel
, realtime
) can have shared state. It's stored in
instance-shared objects. Other nodes (encoder
, filter
) need the
instance-shared object created (hwaccel.init
) before it's used in them.
If a name of an instance-shared object starts with @
, it is global in
process address space. If not, its scope is limited to avplumber instance.
In case of avplumber launched as a standalone process, instance==process and using global objects doesn't have any benefit.
In case of avplumber used as a library, each AVPlumber object is an avplumber instance. Global objects can be used to share state between nodes of different instances as long as they're within the same operating system's process.
node.interrupt input
node.param.set input url "rtmp://new.stream/url"
Important: Execute the second command immediately after the first.
The first command stops input close to immediately (even if it's being
restarted right now). Input (if configured properly by auto_restart
policy) will restart itself (or the whole group) after a second. So we
issue the second command within that second, before internal lock on
nodes manager is acquired.
Note that if input is running normally (i.e. not starting right now), the following commands will do effectively the same:
node.param.set input url "rtmp://new.stream/url"
node.auto_restart input
sed -e 's/^.\+\[control\] Executing: \(.\+\)$/\1/; t; d' < log
grep -Ev '^(EXT-X-MEDIA-SEQUENCE:[0-9]+|\[AVIOContext @ 0x[a-f0-9]+\] Statistics: [0-9]+ seeks, [0-9]+ writeouts|\[hls @ 0x[a-f0-9]+\] Opening '\''.+\.tmp'\'' for writing)$' logfile | less
watch -n0.1 "echo 'queues.stats' | nc localhost 20200"
In some versions of netcat it doesn't work. Try this:
watch -n0.1 "echo 'queues.stats\nbye\n\n' | nc localhost 20200"
open log file in less, press /
or ?
and use this regular expression:
[1-9]0?/[0-9]{1,3},
Created by Teodor Wozniak teodor.wozniak@amagi.com https://lumifaza.org
Copyright (c) 2018-2023 Amagi Media Labs Pvt. Ltd https://amagi.com
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License along with this program. If not, see https://www.gnu.org/licenses/.
This program uses FFmpeg libraries.
FFmpeg codebase is mainly LGPL-licensed with optional components licensed under GPL. Please refer to its LICENSE file for detailed information.
This program uses AvCpp - C++ wrapper for FFmpeg dual-licensed under the GNU Lesser General Public License, version 2.1 or a BSD-Style License
This program uses C++ Requests (cpr) library.
Copyright (c) 2017-2021 Huu Nguyen
Copyright (c) 2022 libcpr and many other contributors
This program uses Flags.hh command line parser header.
Copyright (c) 2015, Song Gao
This program uses ReaderWriterQueue.
Copyright (c) 2013-2021, Cameron Desrochers
This program uses JSON for Modern C++ library licensed under the MIT License
Copyright © 2013-2022 Niels Lohmann
This program uses CUDA loader taken from NVIDIA's CUDA samples.
Copyright (c) 2022, NVIDIA CORPORATION. All rights reserved.