LCOV - code coverage report
Current view: top level - src/media - media_encoder.cpp (source / functions) Coverage Total Hit
Test: jami-coverage-filtered.info Lines: 51.4 % 740 380
Test Date: 2026-11-09 10:41:14 Functions: 70.8 % 48 34

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

Generated by: LCOV version 2.0-1