Ring Daemon 16.0.0
Loading...
Searching...
No Matches
media_encoder.cpp
Go to the documentation of this file.
1/*
2 * Copyright (C) 2004-2025 Savoir-faire Linux Inc.
3 *
4 * This program is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, either version 3 of the License, or
7 * (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <https://www.gnu.org/licenses/>.
16 */
17#include "libav_deps.h" // MUST BE INCLUDED FIRST
18#include "media_codec.h"
19#include "media_encoder.h"
20#include "media_buffer.h"
21
22#include "client/ring_signal.h"
23#include "fileutils.h"
24#include "logger.h"
25#include "manager.h"
26#include "string_utils.h"
28
29#ifdef RING_ACCEL
30#include "video/accel.h"
31#endif
32
33extern "C" {
34#include <libavutil/parseutils.h>
35}
36
37#include <algorithm>
38#include <fstream>
39#include <iostream>
40#include <json/json.h>
41#include <sstream>
42#include <thread> // hardware_concurrency
43#include <string_view>
44#include <cmath>
45
46// Define following line if you need to debug libav SDP
47//#define DEBUG_SDP 1
48
49using namespace std::literals;
50
51namespace jami {
52
53constexpr double LOGREG_PARAM_A {101};
54constexpr double LOGREG_PARAM_B {-5.};
55
56constexpr double LOGREG_PARAM_A_HEVC {96};
57constexpr double LOGREG_PARAM_B_HEVC {-5.};
58
60 : outputCtx_(avformat_alloc_context())
61{
62 JAMI_DBG("[%p] New instance created", this);
63}
64
66{
67 if (outputCtx_) {
68 if (outputCtx_->priv_data && outputCtx_->pb)
69 av_write_trailer(outputCtx_);
70 if (fileIO_) {
71 avio_close(outputCtx_->pb);
72 }
73 for (auto encoderCtx : encoders_) {
74 if (encoderCtx) {
75#ifndef _MSC_VER
77#else
79#endif
80 }
81 }
82 avformat_free_context(outputCtx_);
83 }
85
86 JAMI_DBG("[%p] Instance destroyed", this);
87}
88
89void
91{
92 if (!opts.isValid()) {
93 JAMI_ERR() << "Invalid options";
94 return;
95 }
96
97 if (opts.isVideo) {
99 // Make sure width and height are even (required by x264)
100 // This is especially for image/gif streaming, as video files and cameras usually have even
101 // resolutions
102 videoOpts_.width = ((videoOpts_.width >> 3) << 3);
103 videoOpts_.height = ((videoOpts_.height >> 3) << 3);
106 if (!videoOpts_.bitrate) {
108 }
109 } else {
111 }
112}
113
114void
116{
117 int ret;
118 if (args.payload_type
119 and (ret = av_opt_set_int(reinterpret_cast<void*>(outputCtx_),
120 "payload_type",
121 args.payload_type,
123 < 0))
124 JAMI_ERR() << "Failed to set payload type: " << libav_utils::getError(ret);
125
126 if (not args.parameters.empty())
127 libav_utils::setDictValue(&options_, "parameters", args.parameters);
128
129 mode_ = args.mode;
130 linkableHW_ = args.linkableHW;
131 fecEnabled_ = args.fecEnabled;
132}
133
134void
135MediaEncoder::setMetadata(const std::string& title, const std::string& description)
136{
137 if (not title.empty())
138 libav_utils::setDictValue(&outputCtx_->metadata, "title", title);
139 if (not description.empty())
140 libav_utils::setDictValue(&outputCtx_->metadata, "description", description);
141}
142
143void
145{
146 // only set not default value (!=0)
147 if (seqVal != 0)
148 av_opt_set_int(outputCtx_, "seq", seqVal, AV_OPT_SEARCH_CHILDREN);
149}
150
153{
155 if (av_opt_get_int(outputCtx_, "seq", AV_OPT_SEARCH_CHILDREN, &retVal) >= 0)
156 return (uint16_t) retVal;
157 else
158 return 0;
159}
160
161void
162MediaEncoder::openOutput(const std::string& filename, const std::string& format)
163{
164 avformat_free_context(outputCtx_);
165 int result = avformat_alloc_output_context2(&outputCtx_,
166 nullptr,
167 format.empty() ? nullptr : format.c_str(),
168 filename.c_str());
169 if (result < 0)
170 JAMI_ERR() << "Unable to open " << filename << ": " << libav_utils::getError(-result);
171}
172
173int
175{
176 if (systemCodecInfo.mediaType == MEDIA_AUDIO) {
178 } else {
180 }
181
182 auto stream = avformat_new_stream(outputCtx_, outputCodec_);
183
184 if (stream == nullptr) {
185 JAMI_ERR("[%p] Failed to create coding instance for %s", this, systemCodecInfo.name.c_str());
186 return -1;
187 }
188
189 JAMI_DBG("[%p] Created new coding instance for %s @ index %d",
190 this,
191 systemCodecInfo.name.c_str(),
192 stream->index);
193 // Only init audio now, video will be intialized when
194 // encoding the first frame.
195 if (systemCodecInfo.mediaType == MEDIA_AUDIO) {
196 return initStream(systemCodecInfo);
197 }
198
199 // If audio options are valid, it means this session is used
200 // for both audio and video streams, thus the video will be
201 // at index 1, otherwise it will be at index 0.
202 // TODO. Hacky, needs better solution.
203 if (audioOpts_.isValid())
204 return 1;
205 else
206 return 0;
207}
208
209int
210MediaEncoder::initStream(const std::string& codecName, AVBufferRef* framesCtx)
211{
212 const auto codecInfo = getSystemCodecContainer()->searchCodecByName(codecName, MEDIA_ALL);
213 if (codecInfo)
214 return initStream(*codecInfo, framesCtx);
215 else
216 return -1;
217}
218
219int
220MediaEncoder::initStream(const SystemCodecInfo& systemCodecInfo, AVBufferRef* framesCtx)
221{
222 JAMI_DBG("[%p] Initializing stream: codec type %d, name %s, lib %s",
223 this,
224 systemCodecInfo.codecType,
225 systemCodecInfo.name.c_str(),
226 systemCodecInfo.libName.c_str());
227
228 std::lock_guard lk(encMutex_);
229
230 if (!outputCtx_)
231 throw MediaEncoderException("Unable to allocate stream");
232
233 // Must already have codec instance(s)
234 if (outputCtx_->nb_streams == 0) {
235 JAMI_ERR("[%p] Unable to init, output context has no coding sessions!", this);
236 throw MediaEncoderException("Unable to init, output context has no coding sessions!");
237 }
238
239 AVCodecContext* encoderCtx = nullptr;
240 AVMediaType mediaType;
241
242 if (systemCodecInfo.mediaType == MEDIA_VIDEO)
243 mediaType = AVMEDIA_TYPE_VIDEO;
244 else if (systemCodecInfo.mediaType == MEDIA_AUDIO)
245 mediaType = AVMEDIA_TYPE_AUDIO;
246 else
247 throw MediaEncoderException("Unsuported media type");
248
249 AVStream* stream {nullptr};
250
251 // Only supports one audio and one video streams at most per instance.
252 for (unsigned i = 0; i < outputCtx_->nb_streams; i++) {
253 stream = outputCtx_->streams[i];
254 if (stream->codecpar->codec_type == mediaType) {
255 if (mediaType == AVMEDIA_TYPE_VIDEO) {
256 stream->codecpar->width = videoOpts_.width;
257 stream->codecpar->height = videoOpts_.height;
258 }
259 break;
260 }
261 }
262
263 if (stream == nullptr) {
264 JAMI_ERR("[%p] Unable to init, output context has no coding sessions for %s",
265 this,
266 systemCodecInfo.name.c_str());
267 throw MediaEncoderException("Unable to allocate stream");
268 }
269
270 currentStreamIdx_ = stream->index;
271#ifdef RING_ACCEL
272 // Get compatible list of Hardware API
273 if (enableAccel_ && mediaType == AVMEDIA_TYPE_VIDEO) {
275 systemCodecInfo.avcodecId),
279 for (const auto& it : APIs) {
280 accel_ = std::make_unique<video::HardwareAccel>(it); // save accel
281 // Init codec need accel_ to init encoderCtx accelerated
282 encoderCtx = initCodec(mediaType,
283 static_cast<AVCodecID>(systemCodecInfo.avcodecId),
285 encoderCtx->opaque = accel_.get();
286 // Check if pixel format from encoder match pixel format from decoder frame context
287 // if it mismatch, it means that we are using two different hardware API (nvenc and
288 // vaapi for example) in this case we don't want link the APIs
289 if (framesCtx) {
290 auto hw = reinterpret_cast<AVHWFramesContext*>(framesCtx->data);
291 if (encoderCtx->pix_fmt != hw->format)
292 linkableHW_ = false;
293 }
294 auto ret = accel_->initAPI(linkableHW_, framesCtx);
295 if (ret < 0) {
296 accel_.reset();
297 encoderCtx = nullptr;
298 continue;
299 }
300 accel_->setDetails(encoderCtx);
301 if (avcodec_open2(encoderCtx, outputCodec_, &options_) < 0) {
302 // Failed to open codec
303 JAMI_WARN("Fail to open hardware encoder %s with %s ",
304 avcodec_get_name(static_cast<AVCodecID>(systemCodecInfo.avcodecId)),
305 it.getName().c_str());
307 encoderCtx = nullptr;
308 accel_ = nullptr;
309 continue;
310 } else {
311 // Succeed to open codec
312 JAMI_WARN("Using hardware encoding for %s with %s ",
313 avcodec_get_name(static_cast<AVCodecID>(systemCodecInfo.avcodecId)),
314 it.getName().c_str());
315 encoders_.emplace_back(encoderCtx);
316 break;
317 }
318 }
319 }
320#endif
321
322 if (!encoderCtx) {
323 JAMI_WARN("Not using hardware encoding for %s",
324 avcodec_get_name(static_cast<AVCodecID>(systemCodecInfo.avcodecId)));
325 encoderCtx = initCodec(mediaType,
326 static_cast<AVCodecID>(systemCodecInfo.avcodecId),
329 encoders_.emplace_back(encoderCtx);
330 if (avcodec_open2(encoderCtx, outputCodec_, &options_) < 0)
331 throw MediaEncoderException("Unable to open encoder");
332 }
333
335
336 // framerate is not copied from encoderCtx to stream
337 stream->avg_frame_rate = encoderCtx->framerate;
338#ifdef ENABLE_VIDEO
339 if (systemCodecInfo.mediaType == MEDIA_VIDEO) {
340 // allocate buffers for both scaled (pre-encoder) and encoded frames
341 const int width = encoderCtx->width;
342 const int height = encoderCtx->height;
343 int format = encoderCtx->pix_fmt;
344#ifdef RING_ACCEL
345 if (accel_) {
346 // hardware encoders require a specific pixel format
347 auto desc = av_pix_fmt_desc_get(encoderCtx->pix_fmt);
348 if (desc && (desc->flags & AV_PIX_FMT_FLAG_HWACCEL))
349 format = accel_->getSoftwareFormat();
350 }
351#endif
352 scaledFrameBufferSize_ = videoFrameSize(format, width, height);
353 if (scaledFrameBufferSize_ < 0)
354 throw MediaEncoderException(
355 ("Unable to compute buffer size: " + libav_utils::getError(scaledFrameBufferSize_))
356 .c_str());
357 else if (scaledFrameBufferSize_ <= AV_INPUT_BUFFER_MIN_SIZE)
358 throw MediaEncoderException("buffer too small");
359
360 scaledFrameBuffer_.resize(scaledFrameBufferSize_);
361 scaledFrame_ = std::make_shared<VideoFrame>();
362 scaledFrame_->setFromMemory(scaledFrameBuffer_.data(), format, width, height);
363 }
364#endif // ENABLE_VIDEO
365
366 return stream->index;
367}
368
369void
370MediaEncoder::openIOContext()
371{
372 if (ioCtx_) {
373 outputCtx_->pb = ioCtx_;
374 outputCtx_->packet_size = outputCtx_->pb->buffer_size;
375 } else {
376 int ret = 0;
377#if LIBAVFORMAT_VERSION_INT >= AV_VERSION_INT(58, 7, 100)
378 const char* filename = outputCtx_->url;
379#else
380 const char* filename = outputCtx_->filename;
381#endif
382 if (!(outputCtx_->oformat->flags & AVFMT_NOFILE)) {
383 fileIO_ = true;
384 if ((ret = avio_open(&outputCtx_->pb, filename, AVIO_FLAG_WRITE)) < 0) {
385 throw MediaEncoderException(fmt::format("Unable to open IO context for '{}': {}", filename, libav_utils::getError(ret)));
386 }
387 }
388 }
389}
390
391void
392MediaEncoder::startIO()
393{
394 if (!outputCtx_->pb)
395 openIOContext();
396 if (avformat_write_header(outputCtx_, options_ ? &options_ : nullptr)) {
397 JAMI_ERR("Unable to write header for output file... check codec parameters");
398 throw MediaEncoderException("Failed to write output file header");
399 }
400
401#if LIBAVFORMAT_VERSION_INT >= AV_VERSION_INT(58, 7, 100)
402 av_dump_format(outputCtx_, 0, outputCtx_->url, 1);
403#else
404 av_dump_format(outputCtx_, 0, outputCtx_->filename, 1);
405#endif
406 initialized_ = true;
407}
408
409#ifdef ENABLE_VIDEO
410int
411MediaEncoder::encode(const std::shared_ptr<VideoFrame>& input,
412 bool is_keyframe,
414{
415 auto width = (input->width() >> 3) << 3;
416 auto height = (input->height() >> 3) << 3;
417 if (initialized_ && (getWidth() != width || getHeight() != height)) {
418 resetStreams(width, height);
419 is_keyframe = true;
420 }
421
422 if (!initialized_) {
423 initStream(videoCodec_, input->pointer()->hw_frames_ctx);
424 startIO();
425 }
426
427 std::shared_ptr<VideoFrame> output;
428#ifdef RING_ACCEL
429 if (getHWFrame(input, output) < 0) {
430 JAMI_ERR("Fail to get hardware frame");
431 return -1;
432 }
433#else
434 output = getScaledSWFrame(*input.get());
435#endif // RING_ACCEL
436
437 if (!output) {
438 JAMI_ERR("Fail to get frame");
439 return -1;
440 }
441 auto avframe = output->pointer();
442
443 AVCodecContext* enc = encoders_[currentStreamIdx_];
444 avframe->pts = frame_number;
445 if (enc->framerate.num != enc->time_base.den || enc->framerate.den != enc->time_base.num)
446 avframe->pts /= (rational<int64_t>(enc->framerate) * rational<int64_t>(enc->time_base))
447 .real<int64_t>();
448
449 if (is_keyframe) {
450 avframe->pict_type = AV_PICTURE_TYPE_I;
451 avframe->key_frame = 1;
452 } else {
453 avframe->pict_type = AV_PICTURE_TYPE_NONE;
454 avframe->key_frame = 0;
455 }
456
457 return encode(avframe, currentStreamIdx_);
458}
459#endif // ENABLE_VIDEO
460
461int
463{
464 if (!initialized_) {
465 // Initialize on first video frame, or first audio frame if no video stream
466 if (not videoOpts_.isValid())
467 startIO();
468 else
469 return 0;
470 }
471 frame.pointer()->pts = sent_samples;
472 sent_samples += frame.pointer()->nb_samples;
473 encode(frame.pointer(), currentStreamIdx_);
474 return 0;
475}
476
477int
479{
480 if (!initialized_ && frame) {
481 // Initialize on first video frame, or first audio frame if no video stream
482 bool isVideo = (frame->width > 0 && frame->height > 0);
483 if (isVideo and videoOpts_.isValid()) {
484 // Has video stream, so init with video frame
485 streamIdx = initStream(videoCodec_, frame->hw_frames_ctx);
486 startIO();
487 } else if (!isVideo and !videoOpts_.isValid()) {
488 // Only audio, for MediaRecorder, which doesn't use encodeAudio
489 startIO();
490 } else {
491 return 0;
492 }
493 }
494 int ret = 0;
495 if (static_cast<size_t>(streamIdx) >= encoders_.size())
496 return -1;
497 AVCodecContext* encoderCtx = encoders_[streamIdx];
500 pkt.data = nullptr; // packet data will be allocated by the encoder
501 pkt.size = 0;
502
503 if (!encoderCtx)
504 return -1;
505
507 if (ret < 0)
508 return -1;
509
510 while (ret >= 0) {
512 if (ret == AVERROR(EAGAIN))
513 break;
514 if (ret < 0 && ret != AVERROR_EOF) { // we still want to write our frame on EOF
515 JAMI_ERR() << "Failed to encode frame: " << libav_utils::getError(ret);
516 return ret;
517 }
518
519 if (pkt.size) {
520 if (send(pkt, streamIdx))
521 break;
522 }
523 }
524
526 return 0;
527}
528
529bool
531{
532 if (!initialized_) {
533 streamIdx = initStream(videoCodec_);
534 startIO();
535 }
536 if (streamIdx < 0)
537 streamIdx = currentStreamIdx_;
538 if (streamIdx >= 0 and static_cast<size_t>(streamIdx) < encoders_.size()
539 and static_cast<unsigned int>(streamIdx) < outputCtx_->nb_streams) {
540 auto encoderCtx = encoders_[streamIdx];
541 pkt.stream_index = streamIdx;
542 if (pkt.pts != AV_NOPTS_VALUE)
543 pkt.pts = av_rescale_q(pkt.pts,
544 encoderCtx->time_base,
545 outputCtx_->streams[streamIdx]->time_base);
546 if (pkt.dts != AV_NOPTS_VALUE)
547 pkt.dts = av_rescale_q(pkt.dts,
548 encoderCtx->time_base,
549 outputCtx_->streams[streamIdx]->time_base);
550 }
551 // write the compressed frame
552 auto ret = av_write_frame(outputCtx_, &pkt);
553 if (ret < 0) {
554 JAMI_ERR() << "av_write_frame failed: " << libav_utils::getError(ret);
555 }
556 return ret >= 0;
557}
558
559int
561{
562 int ret = 0;
563 for (size_t i = 0; i < outputCtx_->nb_streams; ++i) {
564 if (encode(nullptr, i) < 0) {
565 JAMI_ERR() << "Unable to flush stream #" << i;
566 ret |= 1u << i; // provide a way for caller to know which streams failed
567 }
568 }
569 return -ret;
570}
571
572std::string
574{
575 /* theora sdp can be huge */
576 const auto sdp_size = outputCtx_->streams[currentStreamIdx_]->codecpar->extradata_size + 2048;
577 std::string sdp(sdp_size, '\0');
578 av_sdp_create(&outputCtx_, 1, &(*sdp.begin()), sdp_size);
579
580 std::string result;
581 result.reserve(sdp_size);
582
583 std::string_view steam(sdp), line;
584 while (jami::getline(steam, line)) {
585 /* strip windows line ending */
586 result += line.substr(0, line.length() - 1);
587 result += "\n"sv;
588 }
589#ifdef DEBUG_SDP
590 JAMI_DBG("Sending SDP:\n%s", result.c_str());
591#endif
592 return result;
593}
594
596MediaEncoder::prepareEncoderContext(const AVCodec* outputCodec, bool is_video)
597{
599
600 auto encoderName = outputCodec->name; // guaranteed to be non null if AVCodec is not null
601
602 encoderCtx->thread_count = std::min(std::thread::hardware_concurrency(), is_video ? 16u : 4u);
603 JAMI_DBG("[%s] Using %d threads", encoderName, encoderCtx->thread_count);
604
605 if (is_video) {
606 // resolution must be a multiple of two
607 encoderCtx->width = videoOpts_.width;
608 encoderCtx->height = videoOpts_.height;
609
610 // satisfy ffmpeg: denominator must be 16bit or less value
611 // time base = 1/FPS
612 av_reduce(&encoderCtx->framerate.num,
613 &encoderCtx->framerate.den,
616 (1U << 16) - 1);
617 encoderCtx->time_base = av_inv_q(encoderCtx->framerate);
618
619 // emit one intra frame every gop_size frames
620 encoderCtx->max_b_frames = 0;
622 // Keep YUV format for macOS
623#ifdef RING_ACCEL
624#if defined(TARGET_OS_IOS) && TARGET_OS_IOS
625 if (accel_)
626 encoderCtx->pix_fmt = accel_->getSoftwareFormat();
627#elif !defined(__APPLE__)
628 if (accel_)
629 encoderCtx->pix_fmt = accel_->getFormat();
630#endif
631#endif
632
633 // Fri Jul 22 11:37:59 EDT 2011:tmatth:XXX: DON'T set this, we want our
634 // pps and sps to be sent in-band for RTP
635 // This is to place global headers in extradata instead of every
636 // keyframe.
637 // encoderCtx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
638 } else {
641 encoderCtx->sample_rate = std::max(8000, audioOpts_.sampleRate);
642 encoderCtx->time_base = AVRational {1, encoderCtx->sample_rate};
644 audioOpts_.nbChannels = std::clamp(audioOpts_.nbChannels, 1, 2);
645 JAMI_ERR() << "[" << encoderName
646 << "] Clamping invalid channel count: " << audioOpts_.nbChannels;
647 }
649 if (audioOpts_.frameSize) {
650 encoderCtx->frame_size = audioOpts_.frameSize;
651 JAMI_DBG() << "[" << encoderName << "] Frame size " << encoderCtx->frame_size;
652 } else {
653 JAMI_WARN() << "[" << encoderName << "] Frame size not set";
654 }
655 }
656
657 return encoderCtx;
658}
659
660void
661MediaEncoder::forcePresetX2645(AVCodecContext* encoderCtx)
662{
663#ifdef RING_ACCEL
664 if (accel_ && accel_->getName() == "nvenc") {
665 if (av_opt_set(encoderCtx, "preset", "fast", AV_OPT_SEARCH_CHILDREN))
666 JAMI_WARN("Failed to set preset to 'fast'");
667 if (av_opt_set(encoderCtx, "level", "auto", AV_OPT_SEARCH_CHILDREN))
668 JAMI_WARN("Failed to set level to 'auto'");
669 if (av_opt_set_int(encoderCtx, "zerolatency", 1, AV_OPT_SEARCH_CHILDREN))
670 JAMI_WARN("Failed to set zerolatency to '1'");
671 } else
672#endif
673 {
674#if (defined(TARGET_OS_IOS) && TARGET_OS_IOS)
675 const char* speedPreset = "ultrafast";
676#else
677 const char* speedPreset = "veryfast";
678#endif
680 JAMI_WARN("Failed to set preset '%s'", speedPreset);
681 const char* tune = "zerolatency";
683 JAMI_WARN("Failed to set tune '%s'", tune);
684 }
685}
686
687void
688MediaEncoder::extractProfileLevelID(const std::string& parameters, AVCodecContext* ctx)
689{
690 // From RFC3984:
691 // If no profile-level-id is present, the Baseline Profile without
692 // additional constraints at Level 1 MUST be implied.
694 ctx->level = 0x0d;
695 // ctx->level = 0x0d; // => 13 aka 1.3
696 if (parameters.empty())
697 return;
698
699 const std::string target("profile-level-id=");
700 size_t needle = parameters.find(target);
701 if (needle == std::string::npos)
702 return;
703
704 needle += target.length();
705 const size_t id_length = 6; /* digits */
706 const std::string profileLevelID(parameters.substr(needle, id_length));
707 if (profileLevelID.length() != id_length)
708 return;
709
710 int result;
711 std::stringstream ss;
713 ss >> std::hex >> result;
714 // profile-level id consists of three bytes
715 const unsigned char profile_idc = result >> 16; // 42xxxx -> 42
716 const unsigned char profile_iop = ((result >> 8) & 0xff); // xx80xx -> 80
717 ctx->level = result & 0xff; // xxxx0d -> 0d
718 switch (profile_idc) {
720 // check constraint_set_1_flag
721 if ((profile_iop & 0x40) >> 6)
722 ctx->profile |= FF_PROFILE_H264_CONSTRAINED;
723 break;
727 // check constraint_set_3_flag
728 if ((profile_iop & 0x10) >> 4)
729 ctx->profile |= FF_PROFILE_H264_INTRA;
730 break;
731 }
732 JAMI_DBG("Using profile %s (%x) and level %d",
734 ctx->profile,
735 ctx->level);
736}
737
738#ifdef RING_ACCEL
739void
740MediaEncoder::enableAccel(bool enableAccel)
741{
744 if (!enableAccel_) {
745 accel_.reset();
746 for (auto enc : encoders_)
747 enc->opaque = nullptr;
748 }
749}
750#endif
751
752unsigned
757
759MediaEncoder::getStream(const std::string& name, int streamIdx) const
760{
761 // if streamIdx is negative, use currentStreamIdx_
762 if (streamIdx < 0)
763 streamIdx = currentStreamIdx_;
764 // make sure streamIdx is valid
765 if (getStreamCount() <= 0 || streamIdx < 0 || encoders_.size() < (unsigned) (streamIdx + 1))
766 return {};
767 auto enc = encoders_[streamIdx];
768 // TODO set firstTimestamp
769 auto ms = MediaStream(name, enc);
770#ifdef RING_ACCEL
771 if (accel_)
772 ms.format = accel_->getSoftwareFormat();
773#endif
774 return ms;
775}
776
778MediaEncoder::initCodec(AVMediaType mediaType, AVCodecID avcodecId, uint64_t br)
779{
780 outputCodec_ = nullptr;
781#ifdef RING_ACCEL
782 if (mediaType == AVMEDIA_TYPE_VIDEO) {
783 if (enableAccel_) {
784 if (accel_) {
785 outputCodec_ = avcodec_find_encoder_by_name(accel_->getCodecName().c_str());
786 }
787 } else {
788 JAMI_WARN() << "Hardware encoding disabled";
789 }
790 }
791#endif
792
793 if (!outputCodec_) {
794 /* find the video encoder */
795 if (avcodecId == AV_CODEC_ID_H263)
796 // For H263 encoding, we force the use of AV_CODEC_ID_H263P (H263-1998)
797 // H263-1998 can manage all frame sizes while H263 don't
798 // AV_CODEC_ID_H263 decoder will be used for decoding
800 else
801 outputCodec_ = avcodec_find_encoder(static_cast<AVCodecID>(avcodecId));
802 if (!outputCodec_) {
803 throw MediaEncoderException("No output encoder");
804 }
805 }
806
807 AVCodecContext* encoderCtx = prepareEncoderContext(outputCodec_,
808 mediaType == AVMEDIA_TYPE_VIDEO);
809
810 // Only clamp video bitrate
811 if (mediaType == AVMEDIA_TYPE_VIDEO && br > 0) {
813 JAMI_WARNING("Requested bitrate {:d} too low, setting to {:d}",
814 br,
818 JAMI_WARNING("Requested bitrate {:d} too high, setting to {:d}",
819 br,
822 }
823 }
824
825 /* Enable libopus FEC encoding support */
826 if (mediaType == AVMEDIA_TYPE_AUDIO) {
827 if (avcodecId == AV_CODEC_ID_OPUS) {
828 initOpus(encoderCtx);
829 }
830 }
831
832 /* let x264 preset override our encoder settings */
833 if (avcodecId == AV_CODEC_ID_H264) {
835 extractProfileLevelID(profileLevelId, encoderCtx);
836 forcePresetX2645(encoderCtx);
837 initH264(encoderCtx, br);
838 } else if (avcodecId == AV_CODEC_ID_HEVC) {
840 forcePresetX2645(encoderCtx);
841 initH265(encoderCtx, br);
843 } else if (avcodecId == AV_CODEC_ID_VP8) {
844 initVP8(encoderCtx, br);
845 } else if (avcodecId == AV_CODEC_ID_MPEG4) {
846 initMPEG4(encoderCtx, br);
847 } else if (avcodecId == AV_CODEC_ID_H263) {
848 initH263(encoderCtx, br);
849 }
850 initAccel(encoderCtx, br);
851 return encoderCtx;
852}
853
854int
856{
857 std::lock_guard lk(encMutex_);
858 AVCodecContext* encoderCtx = getCurrentVideoAVCtx();
859 if (not encoderCtx)
860 return -1; // NOK
861
862 AVCodecID codecId = encoderCtx->codec_id;
863
864 if (not isDynBitrateSupported(codecId))
865 return 0; // Restart needed
866
867 // No need to restart encoder for h264, h263 and MPEG4
868 // Change parameters on the fly
870 initH264(encoderCtx, br);
872 initH265(encoderCtx, br);
873 else if (codecId == AV_CODEC_ID_H263P)
874 initH263(encoderCtx, br);
875 else if (codecId == AV_CODEC_ID_MPEG4)
876 initMPEG4(encoderCtx, br);
877 else {
878 // restart encoder on runtime doesn't work for VP8
879 // stopEncoder();
880 // encoderCtx = initCodec(codecType, codecId, br);
881 // if (avcodec_open2(encoderCtx, outputCodec_, &options_) < 0)
882 // throw MediaEncoderException("Unable to open encoder");
883 }
884 initAccel(encoderCtx, br);
885 return 1; // OK
886}
887
888int
890{
891 std::lock_guard lk(encMutex_);
892 AVCodecContext* encoderCtx = getCurrentAudioAVCtx();
893 if (not encoderCtx)
894 return -1; // NOK
895
896 AVCodecID codecId = encoderCtx->codec_id;
897
898 if (not isDynPacketLossSupported(codecId))
899 return 0; // Restart needed
900
901 // Cap between 0 and 100
902 pl = std::clamp((int) pl, 0, 100);
903
904 // Change parameters on the fly
907 return 1; // OK
908}
909
910void
911MediaEncoder::initH264(AVCodecContext* encoderCtx, uint64_t br)
912{
913 uint64_t maxBitrate = 1000 * br;
914 // 200 Kbit/s -> CRF40
915 // 6 Mbit/s -> CRF23
916 uint8_t crf = (uint8_t) std::round(LOGREG_PARAM_A + LOGREG_PARAM_B * std::log(maxBitrate));
917 // bufsize parameter impact the variation of the bitrate, reduce to half the maxrate to limit
918 // peak and congestion
919 // https://trac.ffmpeg.org/wiki/Limiting%20the%20output%20bitrate
920 uint64_t bufSize = maxBitrate / 2;
921
922 // If auto quality disabled use CRF mode
923 if (mode_ == RateMode::CRF_CONSTRAINED) {
925 av_opt_set_int(encoderCtx, "maxrate", maxBitrate, AV_OPT_SEARCH_CHILDREN);
927 JAMI_DEBUG("H264 encoder setup: crf={:d}, maxrate={:d} kbit/s, bufsize={:d} kbit",
928 crf,
929 maxBitrate / 1000,
930 bufSize / 1000);
931 } else if (mode_ == RateMode::CBR) {
933 av_opt_set_int(encoderCtx, "maxrate", maxBitrate, AV_OPT_SEARCH_CHILDREN);
934 av_opt_set_int(encoderCtx, "minrate", maxBitrate, AV_OPT_SEARCH_CHILDREN);
937
938 JAMI_DEBUG("H264 encoder setup cbr: bitrate={:d} kbit/s", br);
939 }
940}
941
942void
943MediaEncoder::initH265(AVCodecContext* encoderCtx, uint64_t br)
944{
945 // If auto quality disabled use CRF mode
946 if (mode_ == RateMode::CRF_CONSTRAINED) {
947 uint64_t maxBitrate = 1000 * br;
948 // H265 use 50% less bitrate compared to H264 (half bitrate is equivalent to a change 6 for
949 // CRF) https://slhck.info/video/2017/02/24/crf-guide.html
950 // 200 Kbit/s -> CRF35
951 // 6 Mbit/s -> CRF18
953 + LOGREG_PARAM_B_HEVC * std::log(maxBitrate));
954 uint64_t bufSize = maxBitrate / 2;
956 av_opt_set_int(encoderCtx, "maxrate", maxBitrate, AV_OPT_SEARCH_CHILDREN);
958 JAMI_DEBUG("H265 encoder setup: crf={:d}, maxrate={:d} kbit/s, bufsize={:d} kbit",
959 crf,
960 maxBitrate / 1000,
961 bufSize / 1000);
962 } else if (mode_ == RateMode::CBR) {
968 JAMI_DEBUG("H265 encoder setup cbr: bitrate={:d} kbit/s", br);
969 }
970}
971
972void
973MediaEncoder::initVP8(AVCodecContext* encoderCtx, uint64_t br)
974{
975 if (mode_ == RateMode::CQ) {
978 av_opt_set(encoderCtx, "deadline", "good", AV_OPT_SEARCH_CHILDREN);
985 JAMI_DEBUG("VP8 encoder setup: crf=18");
986 } else {
987 // 1- if quality is set use it
988 // bitrate need to be set. The target bitrate becomes the maximum allowed bitrate
989 // 2- otherwise set rc_max_rate and rc_buffer_size
990 // Using information given on this page:
991 // http://www.webmproject.org/docs/encoder-parameters/
992 uint64_t maxBitrate = 1000 * br;
993 // 200 Kbit/s -> CRF40
994 // 6 Mbit/s -> CRF23
995 uint8_t crf = (uint8_t) std::round(LOGREG_PARAM_A + LOGREG_PARAM_B * std::log(maxBitrate));
996 uint64_t bufSize = maxBitrate / 2;
997
998 av_opt_set(encoderCtx, "quality", "realtime", AV_OPT_SEARCH_CHILDREN);
999 av_opt_set_int(encoderCtx, "error-resilient", 1, AV_OPT_SEARCH_CHILDREN);
1001 "cpu-used",
1002 7,
1003 AV_OPT_SEARCH_CHILDREN); // value obtained from testing
1004 av_opt_set_int(encoderCtx, "lag-in-frames", 0, AV_OPT_SEARCH_CHILDREN);
1005 // allow encoder to drop frames if buffers are full and
1006 // to undershoot target bitrate to lessen strain on resources
1008 av_opt_set_int(encoderCtx, "undershoot-pct", 95, AV_OPT_SEARCH_CHILDREN);
1009 // don't set encoderCtx->gop_size: let libvpx decide when to insert a keyframe
1010 av_opt_set_int(encoderCtx, "slices", 2, AV_OPT_SEARCH_CHILDREN); // VP8E_SET_TOKEN_PARTITIONS
1013 crf = std::clamp((int) crf, 4, 56);
1016 av_opt_set_int(encoderCtx, "maxrate", maxBitrate, AV_OPT_SEARCH_CHILDREN);
1018 JAMI_DEBUG("VP8 encoder setup: crf={:d}, maxrate={:d}, bufsize={:d}",
1019 crf,
1020 maxBitrate / 1000,
1021 bufSize / 1000);
1022 }
1023}
1024
1025void
1026MediaEncoder::initMPEG4(AVCodecContext* encoderCtx, uint64_t br)
1027{
1028 uint64_t maxBitrate = 1000 * br;
1029 uint64_t bufSize = maxBitrate / 2;
1030
1031 // Use CBR (set bitrate)
1032 encoderCtx->rc_buffer_size = bufSize;
1033 encoderCtx->bit_rate = encoderCtx->rc_min_rate = encoderCtx->rc_max_rate = maxBitrate;
1034 JAMI_DEBUG("MPEG4 encoder setup: maxrate={:d}, bufsize={:d}", maxBitrate, bufSize);
1035}
1036
1037void
1038MediaEncoder::initH263(AVCodecContext* encoderCtx, uint64_t br)
1039{
1040 uint64_t maxBitrate = 1000 * br;
1041 uint64_t bufSize = maxBitrate / 2;
1042
1043 // Use CBR (set bitrate)
1044 encoderCtx->rc_buffer_size = bufSize;
1045 encoderCtx->bit_rate = encoderCtx->rc_min_rate = encoderCtx->rc_max_rate = maxBitrate;
1046 JAMI_DEBUG("H263 encoder setup: maxrate={:d}, bufsize={:d}", maxBitrate, bufSize);
1047}
1048
1049void
1050MediaEncoder::initOpus(AVCodecContext* encoderCtx)
1051{
1052 // Enable FEC support by default with 10% packet loss
1053 av_opt_set_int(encoderCtx, "fec", fecEnabled_ ? 1 : 0, AV_OPT_SEARCH_CHILDREN);
1055}
1056
1057void
1058MediaEncoder::initAccel(AVCodecContext* encoderCtx, uint64_t br)
1059{
1060#ifdef RING_ACCEL
1061 if (not accel_)
1062 return;
1063 if (accel_->getName() == "nvenc"sv) {
1064 // Use same parameters as software
1065 } else if (accel_->getName() == "vaapi"sv) {
1066 // Use VBR encoding with bitrate target set to 80% of the maxrate
1068 av_opt_set_int(encoderCtx, "b", br * 1000 * 0.8f, AV_OPT_SEARCH_CHILDREN);
1069 } else if (accel_->getName() == "videotoolbox"sv) {
1070 av_opt_set_int(encoderCtx, "b", br * 1000 * 0.8f, AV_OPT_SEARCH_CHILDREN);
1071 } else if (accel_->getName() == "qsv"sv) {
1072 // Use Video Conferencing Mode
1074 av_opt_set_int(encoderCtx, "b", br * 1000 * 0.8f, AV_OPT_SEARCH_CHILDREN);
1075 }
1076#endif
1077}
1078
1080MediaEncoder::getCurrentVideoAVCtx()
1081{
1082 for (auto it : encoders_) {
1083 if (it->codec_type == AVMEDIA_TYPE_VIDEO)
1084 return it;
1085 }
1086 return nullptr;
1087}
1088
1090MediaEncoder::getCurrentAudioAVCtx()
1091{
1092 for (auto it : encoders_) {
1093 if (it->codec_type == AVMEDIA_TYPE_AUDIO)
1094 return it;
1095 }
1096 return nullptr;
1097}
1098
1099void
1100MediaEncoder::stopEncoder()
1101{
1102 flush();
1103 for (auto it = encoders_.begin(); it != encoders_.end(); it++) {
1104 if ((*it)->codec_type == AVMEDIA_TYPE_VIDEO) {
1105 encoders_.erase(it);
1106 break;
1107 }
1108 }
1109 AVCodecContext* encoderCtx = getCurrentVideoAVCtx();
1113}
1114
1115bool
1116MediaEncoder::isDynBitrateSupported(AVCodecID codecid)
1117{
1118#ifdef RING_ACCEL
1119 if (accel_) {
1120 return accel_->dynBitrate();
1121 }
1122#endif
1123 if (codecid != AV_CODEC_ID_VP8)
1124 return true;
1125
1126 return false;
1127}
1128
1129bool
1130MediaEncoder::isDynPacketLossSupported(AVCodecID codecid)
1131{
1133 return true;
1134
1135 return false;
1136}
1137
1138void
1140{
1141 auto path = fileutils::get_config_dir() / "encoder.json";
1142 std::string name = encoderCtx->codec->name;
1143 std::error_code ec;
1144 if (std::filesystem::is_regular_file(path, ec)) {
1145 JAMI_WARN("encoder.json file found, default settings will be erased");
1146 try {
1147 Json::Value root;
1148 std::ifstream file(path);
1149 file >> root;
1150 if (!root.isObject()) {
1151 JAMI_ERR() << "Invalid encoder configuration: root is not an object";
1152 return;
1153 }
1154 const auto& config = root[name];
1155 if (config.isNull()) {
1156 JAMI_WARN() << "Encoder '" << name << "' not found in configuration file";
1157 return;
1158 }
1159 if (!config.isObject()) {
1160 JAMI_ERR() << "Invalid encoder configuration: '" << name << "' is not an object";
1161 return;
1162 }
1163 for (Json::Value::const_iterator it = config.begin(); it != config.end(); ++it) {
1164 Json::Value v = *it;
1165 if (!it.key().isConvertibleTo(Json::ValueType::stringValue)
1166 || !v.isConvertibleTo(Json::ValueType::stringValue)) {
1167 JAMI_ERR() << "Invalid configuration for '" << name << "'";
1168 return;
1169 }
1170 const auto& key = it.key().asString();
1171 const auto& value = v.asString();
1172 int ret = av_opt_set(reinterpret_cast<void*>(encoderCtx),
1173 key.c_str(),
1174 value.c_str(),
1176 if (ret < 0) {
1177 JAMI_ERR() << "Failed to set option " << key << " in " << name
1178 << " context: " << libav_utils::getError(ret) << "\n";
1179 }
1180 }
1181 } catch (const Json::Exception& e) {
1182 JAMI_ERR() << "Failed to load encoder configuration file: " << e.what();
1183 }
1184 }
1185}
1186
1187std::string
1189{
1190#ifdef RING_ACCEL
1191 if (jami::Manager::instance().videoPreferences.getEncodingAccelerated()) {
1192 // Get compatible list of Hardware API
1194 1280,
1195 720,
1197
1198 std::unique_ptr<video::HardwareAccel> accel;
1199
1200 for (const auto& it : APIs) {
1201 accel = std::make_unique<video::HardwareAccel>(it); // save accel
1202 // Init codec need accel to init encoderCtx accelerated
1203 auto outputCodec = avcodec_find_encoder_by_name(accel->getCodecName().c_str());
1204
1206 encoderCtx->thread_count = std::min(std::thread::hardware_concurrency(), 16u);
1207 encoderCtx->width = 1280;
1208 encoderCtx->height = 720;
1209 AVRational framerate;
1210 framerate.num = 30;
1211 framerate.den = 1;
1212 encoderCtx->time_base = av_inv_q(framerate);
1213 encoderCtx->pix_fmt = accel->getFormat();
1215 encoderCtx->opaque = accel.get();
1216
1223
1224 auto ret = accel->initAPI(false, nullptr);
1225 if (ret < 0) {
1226 accel.reset();
1227 ;
1228 encoderCtx = nullptr;
1229 continue;
1230 }
1231 accel->setDetails(encoderCtx);
1232 if (avcodec_open2(encoderCtx, outputCodec, nullptr) < 0) {
1233 // Failed to open codec
1234 JAMI_WARN("Fail to open hardware encoder H265 with %s ", it.getName().c_str());
1236 encoderCtx = nullptr;
1237 accel = nullptr;
1238 continue;
1239 } else {
1240 // Succeed to open codec
1242 encoderCtx = nullptr;
1243 accel = nullptr;
1244 return it.getName();
1245 }
1246 }
1247 }
1248#endif
1249 return "";
1250}
1251
1252#ifdef ENABLE_VIDEO
1253int
1254MediaEncoder::getHWFrame(const std::shared_ptr<VideoFrame>& input,
1255 std::shared_ptr<VideoFrame>& output)
1256{
1257 try {
1258#if defined(TARGET_OS_IOS) && TARGET_OS_IOS
1259 // iOS
1260 if (accel_) {
1261 auto pix = accel_->getSoftwareFormat();
1262 if (input->format() != pix) {
1263 output = scaler_.convertFormat(*input.get(), pix);
1264 } else {
1265 // Fully accelerated pipeline, skip main memory
1266 output = input;
1267 }
1268 } else {
1269 output = getScaledSWFrame(*input.get());
1270 }
1271#elif !defined(__APPLE__) && defined(RING_ACCEL)
1272 // Other Platforms
1273 auto desc = av_pix_fmt_desc_get(static_cast<AVPixelFormat>(input->format()));
1274 bool isHardware = desc && (desc->flags & AV_PIX_FMT_FLAG_HWACCEL);
1275 if (accel_ && accel_->isLinked() && isHardware) {
1276 // Fully accelerated pipeline, skip main memory
1277 output = input;
1278 } else if (isHardware) {
1279 // Hardware decoded frame, transfer back to main memory
1280 // Transfer to GPU if we have a hardware encoder
1281 // Hardware decoders decode to NV12, but Jami's supported software encoders want YUV420P
1282 output = getUnlinkedHWFrame(*input.get());
1283 } else if (accel_) {
1284 // Software decoded frame with a hardware encoder, convert to accepted format first
1285 output = getHWFrameFromSWFrame(*input.get());
1286 } else {
1287 output = getScaledSWFrame(*input.get());
1288 }
1289#else
1290 // macOS
1291 output = getScaledSWFrame(*input.get());
1292#endif
1293 } catch (const std::runtime_error& e) {
1294 JAMI_ERR("Accel failure: %s", e.what());
1295 return -1;
1296 }
1297
1298 return 0;
1299}
1300
1301#ifdef RING_ACCEL
1302std::shared_ptr<VideoFrame>
1303MediaEncoder::getUnlinkedHWFrame(const VideoFrame& input)
1304{
1305 AVPixelFormat pix = (accel_ ? accel_->getSoftwareFormat() : AV_PIX_FMT_NV12);
1306 std::shared_ptr<VideoFrame> framePtr = video::HardwareAccel::transferToMainMemory(input, pix);
1307 if (!accel_) {
1308 framePtr = scaler_.convertFormat(*framePtr, AV_PIX_FMT_YUV420P);
1309 } else {
1310 framePtr = accel_->transfer(*framePtr);
1311 }
1312 return framePtr;
1313}
1314
1315std::shared_ptr<VideoFrame>
1316MediaEncoder::getHWFrameFromSWFrame(const VideoFrame& input)
1317{
1318 std::shared_ptr<VideoFrame> framePtr;
1319 auto pix = accel_->getSoftwareFormat();
1320 if (input.format() != pix) {
1321 framePtr = scaler_.convertFormat(input, pix);
1322 framePtr = accel_->transfer(*framePtr);
1323 } else {
1324 framePtr = accel_->transfer(input);
1325 }
1326 return framePtr;
1327}
1328#endif
1329
1330std::shared_ptr<VideoFrame>
1331MediaEncoder::getScaledSWFrame(const VideoFrame& input)
1332{
1334 scaler_.scale_with_aspect(input, *scaledFrame_);
1335 return scaledFrame_;
1336}
1337#endif
1338
1339void
1340MediaEncoder::resetStreams(int width, int height)
1341{
1342 videoOpts_.width = width;
1343 videoOpts_.height = height;
1344
1345 try {
1346 flush();
1347 initialized_ = false;
1348 if (outputCtx_) {
1349 for (auto encoderCtx : encoders_) {
1350 if (encoderCtx) {
1351#ifndef _MSC_VER
1353#else
1355#endif
1356 }
1357 }
1358 encoders_.clear();
1359 }
1360 } catch (...) {
1361 }
1362}
1363
1364} // namespace jami
static LIBJAMI_TEST_EXPORT Manager & instance()
Definition manager.cpp:676
std::string videoCodec_
unsigned getStreamCount() const
MediaStream audioOpts_
void setInitSeqVal(uint16_t seqVal)
int getWidth() const
int encodeAudio(AudioFrame &frame)
void readConfig(AVCodecContext *encoderCtx)
void resetStreams(int width, int height)
int addStream(const SystemCodecInfo &codec)
static std::string testH265Accel()
MediaStream getStream(const std::string &name, int streamIdx=-1) const
int encode(AVFrame *frame, int streamIdx)
AVDictionary * options_
int setPacketLoss(uint64_t pl)
void setMetadata(const std::string &title, const std::string &description)
int getHeight() const
MediaStream videoOpts_
std::string audioCodec_
void openOutput(const std::string &filename, const std::string &format="")
std::string print_sdp()
int setBitrate(uint64_t br)
void setOptions(const MediaStream &opts)
bool send(AVPacket &packet, int streamIdx=-1)
constexpr I denominator() const
Definition rational.h:78
constexpr I numerator() const
Definition rational.h:77
static std::list< HardwareAccel > getCompatibleAccel(AVCodecID id, int width, int height, CodecType type)
Definition accel.cpp:424
static std::unique_ptr< VideoFrame > transferToMainMemory(const VideoFrame &frame, AVPixelFormat desiredFormat)
Transfers hardware frame to main memory.
Definition accel.cpp:372
#define JAMI_ERR(...)
Definition logger.h:218
#define JAMI_DBG(...)
Definition logger.h:216
#define JAMI_DEBUG(formatstr,...)
Definition logger.h:226
#define JAMI_WARN(...)
Definition logger.h:217
#define JAMI_WARNING(formatstr,...)
Definition logger.h:227
const std::filesystem::path & get_config_dir()
void fillWithBlack(AVFrame *frame)
const char * getDictValue(const AVDictionary *d, const std::string &key, int flags)
void setDictValue(AVDictionary **d, const std::string &key, const std::string &value, int flags)
std::string getError(int err)
constexpr double LOGREG_PARAM_A
decltype(getGlobalInstance< SystemCodecContainer >) & getSystemCodecContainer
constexpr double LOGREG_PARAM_B
void emitSignal(Args... args)
Definition ring_signal.h:64
libjami::VideoFrame VideoFrame
Definition video_base.h:53
@ CODEC_ENCODER
Definition media_codec.h:40
bool getline(std::string_view &str, std::string_view &line, char delim='\n')
Similar to @getline_full but skips empty results.
@ MEDIA_AUDIO
Definition media_codec.h:47
@ MEDIA_ALL
Definition media_codec.h:49
@ MEDIA_VIDEO
Definition media_codec.h:48
constexpr double LOGREG_PARAM_A_HEVC
constexpr double LOGREG_PARAM_B_HEVC
MediaDescription Negotiated RTP media slot.
bool isValid() const
rational< int > frameRate
static constexpr unsigned DEFAULT_MAX_BITRATE
Definition media_codec.h:73
static constexpr unsigned DEFAULT_MIN_BITRATE
Definition media_codec.h:72
static constexpr unsigned DEFAULT_VIDEO_BITRATE
Definition media_codec.h:74