LCOV - code coverage report
Current view: top level - foo/src/media - media_recorder.cpp (source / functions) Hit Total Coverage
Test: jami-coverage-filtered.info Lines: 292 442 66.1 %
Date: 2026-02-28 10:41:24 Functions: 37 49 75.5 %

          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             : 
      18             : #include "libav_deps.h" // MUST BE INCLUDED FIRST
      19             : #include "client/jami_signal.h"
      20             : #include "fileutils.h"
      21             : #include "logger.h"
      22             : #include "manager.h"
      23             : #include "media_buffer.h"
      24             : #include "media_io_handle.h"
      25             : #include "media_recorder.h"
      26             : #include "media_filter.h"
      27             : #include "system_codec_container.h"
      28             : #include "video/filter_transpose.h"
      29             : #include "audio/audio_format.h"
      30             : 
      31             : #ifdef ENABLE_VIDEO
      32             : #ifdef ENABLE_HWACCEL
      33             : #include "video/accel.h"
      34             : #endif
      35             : #endif
      36             : 
      37             : #include <opendht/thread_pool.h>
      38             : 
      39             : #include <algorithm>
      40             : #include <iomanip>
      41             : #include <sstream>
      42             : #include <sys/types.h>
      43             : #include <memory>
      44             : #include <ctime>
      45             : 
      46             : namespace jami {
      47             : 
      48             : const constexpr char ROTATION_FILTER_INPUT_NAME[] = "in";
      49             : 
      50             : // Replaces every occurrence of @from with @to in @str
      51             : static std::string
      52           4 : replaceAll(const std::string& str, const std::string& from, const std::string& to)
      53             : {
      54           4 :     if (from.empty())
      55           0 :         return str;
      56           4 :     std::string copy(str);
      57           4 :     size_t startPos = 0;
      58           6 :     while ((startPos = str.find(from, startPos)) != std::string::npos) {
      59           2 :         copy.replace(startPos, from.length(), to);
      60           2 :         startPos += to.length();
      61             :     }
      62           4 :     return copy;
      63           4 : }
      64             : 
      65             : struct MediaRecorder::StreamObserver : public Observer<std::shared_ptr<MediaFrame>>
      66             : {
      67             :     const MediaStream info;
      68             : 
      69          33 :     StreamObserver(const MediaStream& ms, std::function<void(const std::shared_ptr<MediaFrame>&)> func)
      70          66 :         : info(ms)
      71          33 :         , cb_(func) {};
      72             : 
      73          66 :     ~StreamObserver()
      74          66 :     {
      75          34 :         while (observablesFrames_.size() > 0) {
      76           1 :             auto obs = observablesFrames_.begin();
      77           1 :             (*obs)->detach(this);
      78             :             // it should be erased from observablesFrames_ in detach. If it does not happens erase frame to avoid
      79             :             // infinite loop.
      80           1 :             auto it = observablesFrames_.find(*obs);
      81           1 :             if (it != observablesFrames_.end())
      82           0 :                 observablesFrames_.erase(it);
      83             :         }
      84          66 :     };
      85             : 
      86        5255 :     void update(Observable<std::shared_ptr<MediaFrame>>* /*ob*/, const std::shared_ptr<MediaFrame>& m) override
      87             :     {
      88        5255 :         if (not isEnabled)
      89        5255 :             return;
      90             : #ifdef ENABLE_VIDEO
      91           0 :         if (info.isVideo) {
      92           0 :             std::shared_ptr<VideoFrame> framePtr;
      93             : #ifdef ENABLE_HWACCEL
      94           0 :             auto desc = av_pix_fmt_desc_get((AVPixelFormat) (std::static_pointer_cast<VideoFrame>(m))->format());
      95           0 :             if (desc && (desc->flags & AV_PIX_FMT_FLAG_HWACCEL)) {
      96             :                 try {
      97           0 :                     framePtr = jami::video::HardwareAccel::transferToMainMemory(*std::static_pointer_cast<VideoFrame>(m),
      98           0 :                                                                                 AV_PIX_FMT_NV12);
      99           0 :                 } catch (const std::runtime_error& e) {
     100           0 :                     JAMI_ERR("Accel failure: %s", e.what());
     101           0 :                     return;
     102           0 :                 }
     103           0 :             } else
     104             : #endif
     105           0 :                 framePtr = std::static_pointer_cast<VideoFrame>(m);
     106           0 :             int angle = framePtr->getOrientation();
     107           0 :             if (angle != rotation_) {
     108           0 :                 videoRotationFilter_ = jami::video::getTransposeFilter(angle,
     109             :                                                                        ROTATION_FILTER_INPUT_NAME,
     110             :                                                                        framePtr->width(),
     111             :                                                                        framePtr->height(),
     112             :                                                                        framePtr->format(),
     113           0 :                                                                        true);
     114           0 :                 rotation_ = angle;
     115             :             }
     116           0 :             if (videoRotationFilter_) {
     117           0 :                 videoRotationFilter_->feedInput(framePtr->pointer(), ROTATION_FILTER_INPUT_NAME);
     118           0 :                 auto rotated = videoRotationFilter_->readOutput();
     119           0 :                 av_frame_remove_side_data(rotated->pointer(), AV_FRAME_DATA_DISPLAYMATRIX);
     120           0 :                 cb_(std::move(rotated));
     121           0 :             } else {
     122           0 :                 cb_(m);
     123             :             }
     124           0 :         } else {
     125             : #endif
     126           0 :             cb_(m);
     127             : #ifdef ENABLE_VIDEO
     128             :         }
     129             : #endif
     130             :     }
     131             : 
     132          33 :     void attached(Observable<std::shared_ptr<MediaFrame>>* obs) override { observablesFrames_.insert(obs); }
     133             : 
     134          33 :     void detached(Observable<std::shared_ptr<MediaFrame>>* obs) override
     135             :     {
     136          33 :         auto it = observablesFrames_.find(obs);
     137          33 :         if (it != observablesFrames_.end())
     138          33 :             observablesFrames_.erase(it);
     139          33 :     }
     140             : 
     141             :     bool isEnabled = false;
     142             : 
     143             : private:
     144             :     std::function<void(const std::shared_ptr<MediaFrame>&)> cb_;
     145             :     std::unique_ptr<MediaFilter> videoRotationFilter_ {};
     146             :     int rotation_ = 0;
     147             :     std::set<Observable<std::shared_ptr<MediaFrame>>*> observablesFrames_;
     148             : };
     149             : 
     150         396 : MediaRecorder::MediaRecorder() {}
     151             : 
     152         396 : MediaRecorder::~MediaRecorder()
     153             : {
     154         396 :     flush();
     155         395 :     reset();
     156         396 : }
     157             : 
     158             : bool
     159           4 : MediaRecorder::isRecording() const
     160             : {
     161           4 :     return isRecording_;
     162             : }
     163             : 
     164             : std::string
     165          12 : MediaRecorder::getPath() const
     166             : {
     167          12 :     if (audioOnly_)
     168           0 :         return path_ + ".ogg";
     169             :     else
     170          12 :         return path_ + ".webm";
     171             : }
     172             : 
     173             : void
     174           2 : MediaRecorder::audioOnly(bool audioOnly)
     175             : {
     176           2 :     audioOnly_ = audioOnly;
     177           2 : }
     178             : 
     179             : void
     180           2 : MediaRecorder::setPath(const std::string& path)
     181             : {
     182           2 :     path_ = path;
     183           2 : }
     184             : 
     185             : void
     186           1 : MediaRecorder::setMetadata(const std::string& title, const std::string& desc)
     187             : {
     188           1 :     title_ = title;
     189           1 :     description_ = desc;
     190           1 : }
     191             : 
     192             : int
     193           2 : MediaRecorder::startRecording()
     194             : {
     195           2 :     std::time_t t = std::time(nullptr);
     196           2 :     startTime_ = *std::localtime(&t);
     197           2 :     startTimeStamp_ = av_gettime();
     198             : 
     199           2 :     std::lock_guard lk(encoderMtx_);
     200           2 :     encoder_.reset(new MediaEncoder);
     201             : 
     202           8 :     JAMI_LOG("Start recording '{}'", getPath());
     203           2 :     if (initRecord() >= 0) {
     204           2 :         isRecording_ = true;
     205             :         {
     206           2 :             std::lock_guard lk(mutexStreamSetup_);
     207           5 :             for (auto& media : streams_) {
     208           3 :                 if (media.second->info.isVideo) {
     209           2 :                     std::lock_guard lk2(mutexFilterVideo_);
     210           2 :                     setupVideoOutput();
     211           2 :                 } else {
     212           1 :                     std::lock_guard lk2(mutexFilterAudio_);
     213           1 :                     setupAudioOutput();
     214           1 :                 }
     215           3 :                 media.second->isEnabled = true;
     216             :             }
     217           2 :         }
     218             :         // start thread after isRecording_ is set to true
     219           2 :         dht::ThreadPool::computation().run([rec = shared_from_this()] {
     220           2 :             std::lock_guard lk(rec->encoderMtx_);
     221           2 :             while (rec->isRecording()) {
     222           2 :                 std::shared_ptr<MediaFrame> frame;
     223             :                 // get frame from queue
     224             :                 {
     225           2 :                     std::unique_lock lk(rec->mutexFrameBuff_);
     226           6 :                     rec->cv_.wait(lk, [rec] { return rec->interrupted_ or not rec->frameBuff_.empty(); });
     227           2 :                     if (rec->interrupted_) {
     228           2 :                         break;
     229             :                     }
     230           0 :                     frame = std::move(rec->frameBuff_.front());
     231           0 :                     rec->frameBuff_.pop_front();
     232           2 :                 }
     233             :                 try {
     234             :                     // encode frame
     235           0 :                     if (rec->encoder_ && frame && frame->pointer()) {
     236             : #ifdef ENABLE_VIDEO
     237           0 :                         bool isVideo = (frame->pointer()->width > 0 && frame->pointer()->height > 0);
     238           0 :                         rec->encoder_->encode(frame->pointer(), isVideo ? rec->videoIdx_ : rec->audioIdx_);
     239             : #else
     240             :                         rec->encoder_->encode(frame->pointer(), rec->audioIdx_);
     241             : #endif // ENABLE_VIDEO
     242             :                     }
     243           0 :                 } catch (const MediaEncoderException& e) {
     244           0 :                     JAMI_ERR() << "Failed to record frame: " << e.what();
     245           0 :                 }
     246           2 :             }
     247           2 :             rec->flush();
     248           2 :             rec->reset(); // allows recorder to be reused in same call
     249           2 :         });
     250             :     }
     251           2 :     interrupted_ = false;
     252           2 :     return 0;
     253           2 : }
     254             : 
     255             : void
     256           2 : MediaRecorder::stopRecording()
     257             : {
     258           2 :     interrupted_ = true;
     259           2 :     cv_.notify_all();
     260           2 :     if (isRecording_) {
     261           2 :         JAMI_DBG() << "Stop recording '" << getPath() << "'";
     262           2 :         isRecording_ = false;
     263             :         {
     264           2 :             std::lock_guard lk(mutexStreamSetup_);
     265           5 :             for (auto& media : streams_) {
     266           3 :                 media.second->isEnabled = false;
     267             :             }
     268           2 :         }
     269           2 :         emitSignal<libjami::CallSignal::RecordPlaybackStopped>(getPath());
     270             :     }
     271           2 : }
     272             : 
     273             : Observer<std::shared_ptr<MediaFrame>>*
     274          33 : MediaRecorder::addStream(const MediaStream& ms)
     275             : {
     276          33 :     bool streamIsNew = false;
     277          33 :     std::unique_ptr<StreamObserver> oldObserver; // destroy after unlock
     278             : 
     279             :     {
     280          33 :         std::lock_guard lk(mutexStreamSetup_);
     281          33 :         if (audioOnly_ && ms.isVideo) {
     282           0 :             JAMI_ERR() << "Attempting to add video stream to audio only recording";
     283           0 :             return nullptr;
     284             :         }
     285          33 :         if (ms.format < 0 || ms.name.empty()) {
     286           0 :             JAMI_ERR() << "Attempting to add invalid stream to recording";
     287           0 :             return nullptr;
     288             :         }
     289             : 
     290          33 :         auto it = streams_.find(ms.name);
     291          33 :         if (it == streams_.end()) {
     292           0 :             auto streamPtr = std::make_unique<StreamObserver>(ms, [this, ms](const std::shared_ptr<MediaFrame>& frame) {
     293           0 :                 onFrame(ms.name, frame);
     294          32 :             });
     295          32 :             it = streams_.insert(std::make_pair(ms.name, std::move(streamPtr))).first;
     296         128 :             JAMI_LOG("[Recorder: {:p}] Recorder input #{}: {:s}", fmt::ptr(this), streams_.size(), ms.name);
     297          32 :             streamIsNew = true;
     298          32 :         } else {
     299           1 :             if (ms == it->second->info) {
     300           0 :                 JAMI_LOG("[Recorder: {:p}] Recorder already has '{:s}' as input", fmt::ptr(this), ms.name);
     301             :             } else {
     302           1 :                 oldObserver = std::move(it->second);
     303           2 :                 it->second = std::make_unique<StreamObserver>(ms, [this, ms](const std::shared_ptr<MediaFrame>& frame) {
     304           0 :                     onFrame(ms.name, frame);
     305           1 :                 });
     306             :             }
     307           1 :             streamIsNew = false;
     308             :         }
     309             : 
     310          33 :         if (streamIsNew && isRecording_) {
     311           0 :             if (ms.isVideo) {
     312           0 :                 if (!videoFilter_ || videoFilter_->needsReinitForNewStream(ms.name))
     313           0 :                     setupVideoOutput();
     314             :             } else {
     315           0 :                 if (!audioFilter_ || audioFilter_->needsReinitForNewStream(ms.name))
     316           0 :                     setupAudioOutput();
     317             :             }
     318             :         }
     319          33 :         it->second->isEnabled = isRecording_;
     320          33 :         return it->second.get();
     321          33 :     }
     322             : 
     323             :     // oldObserver destroyed here, after mutexStreamSetup_ is released
     324          33 : }
     325             : 
     326             : void
     327          30 : MediaRecorder::removeStream(const MediaStream& ms)
     328             : {
     329          30 :     std::unique_ptr<StreamObserver> oldObserver; // destroy after unlock
     330             : 
     331             :     {
     332          30 :         std::lock_guard lk(mutexStreamSetup_);
     333             : 
     334          30 :         auto it = streams_.find(ms.name);
     335          30 :         if (it == streams_.end()) {
     336           0 :             JAMI_LOG("[Recorder: {:p}] Recorder no stream to remove", fmt::ptr(this));
     337             :         } else {
     338         120 :             JAMI_LOG("[Recorder: {:p}] Recorder removing '{:s}'", fmt::ptr(this), ms.name);
     339          30 :             oldObserver = std::move(it->second);
     340          30 :             streams_.erase(it);
     341          30 :             if (isRecording_) {
     342           0 :                 if (ms.isVideo)
     343           0 :                     setupVideoOutput();
     344             :                 else
     345           0 :                     setupAudioOutput();
     346             :             }
     347             :         }
     348          30 :     }
     349             : 
     350             :     // oldObserver destroyed here, after mutexStreamSetup_ is released
     351          60 :     return;
     352          30 : }
     353             : 
     354             : Observer<std::shared_ptr<MediaFrame>>*
     355         390 : MediaRecorder::getStream(const std::string& name) const
     356             : {
     357         390 :     const auto it = streams_.find(name);
     358         390 :     if (it != streams_.cend())
     359          32 :         return it->second.get();
     360         358 :     return nullptr;
     361             : }
     362             : 
     363             : void
     364           0 : MediaRecorder::onFrame(const std::string& name, const std::shared_ptr<MediaFrame>& frame)
     365             : {
     366           0 :     if (not isRecording_ || interrupted_)
     367           0 :         return;
     368             : 
     369           0 :     std::lock_guard lk(mutexStreamSetup_);
     370             : 
     371             :     // copy frame to not mess with the original frame's pts (does not actually copy frame data)
     372           0 :     std::unique_ptr<MediaFrame> clone;
     373           0 :     const auto& ms = streams_[name]->info;
     374             : #if defined(ENABLE_VIDEO) && defined(ENABLE_HWACCEL)
     375           0 :     if (ms.isVideo) {
     376           0 :         auto desc = av_pix_fmt_desc_get((AVPixelFormat) (std::static_pointer_cast<VideoFrame>(frame))->format());
     377           0 :         if (desc && (desc->flags & AV_PIX_FMT_FLAG_HWACCEL)) {
     378             :             try {
     379           0 :                 clone = video::HardwareAccel::transferToMainMemory(*std::static_pointer_cast<VideoFrame>(frame),
     380           0 :                                                                    static_cast<AVPixelFormat>(ms.format));
     381           0 :             } catch (const std::runtime_error& e) {
     382           0 :                 JAMI_ERR("Accel failure: %s", e.what());
     383           0 :                 return;
     384           0 :             }
     385           0 :         } else {
     386           0 :             clone = std::make_unique<MediaFrame>();
     387           0 :             clone->copyFrom(*frame);
     388             :         }
     389             :     } else {
     390             : #endif // ENABLE_VIDEO && ENABLE_HWACCEL
     391           0 :         clone = std::make_unique<MediaFrame>();
     392           0 :         clone->copyFrom(*frame);
     393             : #if defined(ENABLE_VIDEO) && defined(ENABLE_HWACCEL)
     394             :     }
     395             : #endif // ENABLE_VIDEO && ENABLE_HWACCEL
     396           0 :     clone->pointer()->pts = av_rescale_q_rnd(av_gettime() - startTimeStamp_,
     397             :                                              {1, AV_TIME_BASE},
     398             :                                              ms.timeBase,
     399             :                                              static_cast<AVRounding>(AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX));
     400           0 :     std::vector<std::unique_ptr<MediaFrame>> filteredFrames;
     401             : #ifdef ENABLE_VIDEO
     402           0 :     if (ms.isVideo && videoFilter_ && outputVideoFilter_) {
     403           0 :         std::lock_guard lk(mutexFilterVideo_);
     404           0 :         videoFilter_->feedInput(clone->pointer(), name);
     405           0 :         if (auto filteredVideoOutput = videoFilter_->readOutput()) {
     406           0 :             outputVideoFilter_->feedInput(filteredVideoOutput->pointer(), "input");
     407           0 :             while (auto fFrame = outputVideoFilter_->readOutput()) {
     408           0 :                 filteredFrames.emplace_back(std::move(fFrame));
     409           0 :             }
     410           0 :         }
     411           0 :     } else if (audioFilter_ && outputAudioResampler_ && audioFrameResizer_) {
     412             : #endif // ENABLE_VIDEO
     413           0 :         std::lock_guard lkA(mutexFilterAudio_);
     414           0 :         audioFilter_->feedInput(clone->pointer(), name);
     415           0 :         if (auto filteredAudioOutput = audioFilter_->readOutput()) {
     416           0 :             audioFrameResizer_->enqueue(
     417           0 :                 outputAudioResampler_->resample(std::unique_ptr<AudioFrame>(
     418           0 :                                                     static_cast<AudioFrame*>(filteredAudioOutput.release())),
     419           0 :                                                 AudioFormat(48000, 2)));
     420           0 :         }
     421             : #ifdef ENABLE_VIDEO
     422           0 :     }
     423             : #endif // ENABLE_VIDEO
     424             : 
     425           0 :     for (auto& fFrame : filteredFrames) {
     426           0 :         std::lock_guard lk(mutexFrameBuff_);
     427           0 :         frameBuff_.emplace_back(std::move(fFrame));
     428           0 :         cv_.notify_one();
     429           0 :     }
     430           0 : }
     431             : 
     432             : int
     433           2 : MediaRecorder::initRecord()
     434             : {
     435             :     // need to get encoder parameters before calling openFileOutput
     436             :     // openFileOutput needs to be called before adding any streams
     437             : 
     438           2 :     std::stringstream timestampString;
     439           2 :     timestampString << std::put_time(&startTime_, "%Y-%m-%d %H:%M:%S");
     440             : 
     441           2 :     if (title_.empty()) {
     442           1 :         title_ = "Conversation at %TIMESTAMP";
     443             :     }
     444           2 :     title_ = replaceAll(title_, "%TIMESTAMP", timestampString.str());
     445             : 
     446           2 :     if (description_.empty()) {
     447           2 :         description_ = "Recorded with Jami https://jami.net";
     448             :     }
     449           2 :     description_ = replaceAll(description_, "%TIMESTAMP", timestampString.str());
     450             : 
     451           2 :     encoder_->setMetadata(title_, description_);
     452           2 :     encoder_->openOutput(getPath());
     453             : #ifdef ENABLE_VIDEO
     454             : #ifdef ENABLE_HWACCEL
     455           2 :     encoder_->enableAccel(false); // TODO recorder has problems with hardware encoding
     456             : #endif
     457             : #endif // ENABLE_VIDEO
     458             : 
     459             :     {
     460           2 :         MediaStream audioStream;
     461           2 :         audioStream.name = "audioOutput";
     462           2 :         audioStream.format = 1;
     463           2 :         audioStream.timeBase = rational<int>(1, 48000);
     464           2 :         audioStream.sampleRate = 48000;
     465           2 :         audioStream.nbChannels = 2;
     466           2 :         encoder_->setOptions(audioStream);
     467             : 
     468             :         auto audioCodec = std::static_pointer_cast<jami::SystemAudioCodecInfo>(
     469           4 :             getSystemCodecContainer()->searchCodecByName("opus", jami::MEDIA_AUDIO));
     470           2 :         audioIdx_ = encoder_->addStream(*audioCodec.get());
     471           2 :         if (audioIdx_ < 0) {
     472           0 :             JAMI_ERR() << "Failed to add audio stream to encoder";
     473           0 :             return -1;
     474             :         }
     475           2 :     }
     476             : 
     477             : #ifdef ENABLE_VIDEO
     478           2 :     if (!audioOnly_) {
     479           2 :         MediaStream videoStream;
     480             : 
     481           2 :         videoStream.name = "videoOutput";
     482           2 :         videoStream.format = 0;
     483           2 :         videoStream.isVideo = true;
     484           2 :         videoStream.timeBase = rational<int>(0, 1);
     485           2 :         videoStream.width = 1280;
     486           2 :         videoStream.height = 720;
     487           2 :         videoStream.frameRate = rational<int>(30, 1);
     488           2 :         videoStream.bitrate = Manager::instance().videoPreferences.getRecordQuality();
     489             : 
     490           2 :         MediaDescription args;
     491           2 :         args.mode = RateMode::CQ;
     492           2 :         encoder_->setOptions(videoStream);
     493           2 :         encoder_->setOptions(args);
     494             : 
     495             :         auto videoCodec = std::static_pointer_cast<jami::SystemVideoCodecInfo>(
     496           4 :             getSystemCodecContainer()->searchCodecByName("VP8", jami::MEDIA_VIDEO));
     497           2 :         videoIdx_ = encoder_->addStream(*videoCodec.get());
     498           2 :         if (videoIdx_ < 0) {
     499           0 :             JAMI_ERR() << "Failed to add video stream to encoder";
     500           0 :             return -1;
     501             :         }
     502           2 :     }
     503             : #endif // ENABLE_VIDEO
     504             : 
     505           2 :     encoder_->setIOContext(nullptr);
     506             : 
     507           2 :     JAMI_DBG() << "Recording initialized";
     508           2 :     return 0;
     509           2 : }
     510             : 
     511             : void
     512           2 : MediaRecorder::setupVideoOutput()
     513             : {
     514           2 :     MediaStream encoderStream, peer, local, mixer;
     515           2 :     auto it = std::find_if(streams_.begin(), streams_.end(), [](const auto& pair) {
     516           3 :         return pair.second->info.isVideo && pair.second->info.name.find("remote") != std::string::npos;
     517             :     });
     518           2 :     if (it != streams_.end())
     519           1 :         peer = it->second->info;
     520             : 
     521           2 :     it = std::find_if(streams_.begin(), streams_.end(), [](const auto& pair) {
     522           3 :         return pair.second->info.isVideo && pair.second->info.name.find("local") != std::string::npos;
     523             :     });
     524           2 :     if (it != streams_.end())
     525           0 :         local = it->second->info;
     526             : 
     527           2 :     it = std::find_if(streams_.begin(), streams_.end(), [](const auto& pair) {
     528           3 :         return pair.second->info.isVideo && pair.second->info.name.find("mixer") != std::string::npos;
     529             :     });
     530           2 :     if (it != streams_.end())
     531           1 :         mixer = it->second->info;
     532             : 
     533             :     // vp8 supports only yuv420p
     534           2 :     videoFilter_.reset(new MediaFilter);
     535           2 :     int ret = -1;
     536           2 :     int streams = peer.isValid() + local.isValid() + mixer.isValid();
     537           2 :     switch (streams) {
     538           0 :     case 0: {
     539           0 :         JAMI_WARN() << "Attempting to record a video stream but none is valid";
     540           0 :         return;
     541             :     }
     542           2 :     case 1: {
     543           2 :         MediaStream inputStream;
     544           2 :         if (peer.isValid())
     545           1 :             inputStream = peer;
     546           1 :         else if (local.isValid())
     547           0 :             inputStream = local;
     548           1 :         else if (mixer.isValid())
     549           1 :             inputStream = mixer;
     550             :         else {
     551           0 :             JAMI_ERR("Attempting to record a stream but none is valid");
     552           0 :             break;
     553             :         }
     554             : 
     555           4 :         ret = videoFilter_->initialize(buildVideoFilter({}, inputStream), {inputStream});
     556           2 :         break;
     557           2 :     }
     558           0 :     case 2: // overlay local video over peer video
     559           0 :         ret = videoFilter_->initialize(buildVideoFilter({peer}, local), {peer, local});
     560           0 :         break;
     561           0 :     default:
     562           0 :         JAMI_ERR() << "Recording more than 2 video streams is not supported";
     563           0 :         break;
     564             :     }
     565             : 
     566             : #ifdef ENABLE_VIDEO
     567           2 :     if (ret < 0) {
     568           0 :         JAMI_ERR() << "Failed to initialize video filter";
     569             :     }
     570             : 
     571             :     // setup output filter
     572           2 :     if (!videoFilter_)
     573           0 :         return;
     574           4 :     MediaStream secondaryFilter = videoFilter_->getOutputParams();
     575           2 :     secondaryFilter.name = "input";
     576           2 :     if (outputVideoFilter_) {
     577           0 :         outputVideoFilter_->flush();
     578           0 :         outputVideoFilter_.reset();
     579             :     }
     580             : 
     581           2 :     outputVideoFilter_.reset(new MediaFilter);
     582             : 
     583           2 :     float scaledHeight = 1280 * (float) secondaryFilter.height / (float) secondaryFilter.width;
     584           4 :     std::string scaleFilter = "scale=1280:-2";
     585           2 :     if (scaledHeight > 720)
     586           0 :         scaleFilter += ",scale=-2:720";
     587             : 
     588           4 :     std::ostringstream f;
     589           2 :     f << "[input]" << scaleFilter << ",pad=1280:720:(ow-iw)/2:(oh-ih)/2,format=pix_fmts=yuv420p,fps=30";
     590             : 
     591           4 :     ret = outputVideoFilter_->initialize(f.str(), {secondaryFilter});
     592             : 
     593           2 :     if (ret < 0) {
     594           0 :         JAMI_ERR() << "Failed to initialize output video filter";
     595             :     }
     596             : 
     597             : #endif
     598             : 
     599           2 :     return;
     600           2 : }
     601             : 
     602             : std::string
     603           2 : MediaRecorder::buildVideoFilter(const std::vector<MediaStream>& peers, const MediaStream& local) const
     604             : {
     605           2 :     std::ostringstream v;
     606             : 
     607           2 :     switch (peers.size()) {
     608           2 :     case 0:
     609           2 :         v << "[" << local.name << "] fps=30, format=pix_fmts=yuv420p";
     610           2 :         break;
     611           0 :     case 1: {
     612           0 :         auto p = peers[0];
     613           0 :         const constexpr int minHeight = 720;
     614           0 :         const bool needScale = (p.height < minHeight);
     615           0 :         const int newHeight = (needScale ? minHeight : p.height);
     616             : 
     617             :         // NOTE -2 means preserve aspect ratio and have the new number be even
     618           0 :         if (needScale)
     619           0 :             v << "[" << p.name << "] fps=30, scale=-2:" << newHeight << " [v:m]; ";
     620             :         else
     621           0 :             v << "[" << p.name << "] fps=30 [v:m]; ";
     622             : 
     623           0 :         v << "[" << local.name << "] fps=30, scale=-2:" << newHeight / 5 << " [v:o]; ";
     624             : 
     625           0 :         v << "[v:m] [v:o] overlay=main_w-overlay_w:main_h-overlay_h" << ", format=pix_fmts=yuv420p";
     626           0 :     } break;
     627           0 :     default:
     628           0 :         JAMI_ERR() << "Video recordings with more than 2 video streams are not supported";
     629           0 :         break;
     630             :     }
     631             : 
     632           4 :     return v.str();
     633           2 : }
     634             : 
     635             : void
     636           1 : MediaRecorder::setupAudioOutput()
     637             : {
     638           1 :     MediaStream encoderStream;
     639             : 
     640             :     // resample to common audio format, so any player can play the file
     641           1 :     audioFilter_.reset(new MediaFilter);
     642           1 :     int ret = -1;
     643             : 
     644           1 :     if (streams_.empty()) {
     645           0 :         JAMI_WARN() << "Attempting to record a audio stream but none is valid";
     646           0 :         return;
     647             :     }
     648             : 
     649           1 :     std::vector<MediaStream> peers {};
     650           3 :     for (const auto& media : streams_) {
     651           2 :         if (!media.second->info.isVideo && media.second->info.isValid())
     652           1 :             peers.emplace_back(media.second->info);
     653             :     }
     654             : 
     655           1 :     ret = audioFilter_->initialize(buildAudioFilter(peers), peers);
     656             : 
     657           1 :     if (ret < 0) {
     658           0 :         JAMI_ERR() << "Failed to initialize audio filter";
     659           0 :         return;
     660             :     }
     661             : 
     662             :     // setup output filter
     663           1 :     if (!audioFilter_)
     664           0 :         return;
     665           1 :     MediaStream secondaryFilter = audioFilter_->getOutputParams();
     666           1 :     secondaryFilter.name = "input";
     667             : 
     668           1 :     outputAudioResampler_ = std::make_unique<Resampler>();
     669           1 :     if (!outputAudioResampler_)
     670           0 :         JAMI_ERROR("Failed to initialize audio resampler");
     671             : 
     672           2 :     audioFrameResizer_ = std::make_unique<AudioFrameResizer>(AudioFormat(48000, 2),
     673           1 :                                                              encoder_->getCurrentAudioAVCtxFrameSize(),
     674           0 :                                                              [this](std::shared_ptr<AudioFrame>&& frame) {
     675           0 :                                                                  std::lock_guard lk(mutexFrameBuff_);
     676           0 :                                                                  frameBuff_.emplace_back(
     677           0 :                                                                      std::shared_ptr<MediaFrame>(std::move(frame)));
     678           0 :                                                                  cv_.notify_one();
     679           1 :                                                              });
     680           1 :     if (!audioFrameResizer_)
     681           0 :         JAMI_ERROR("Failed to initialize audio frame resizer");
     682             : 
     683           1 :     return;
     684           1 : }
     685             : 
     686             : std::string
     687           1 : MediaRecorder::buildAudioFilter(const std::vector<MediaStream>& peers) const
     688             : {
     689             :     // resampling is handled by outputAudioResampler_
     690           1 :     std::ostringstream a;
     691             : 
     692           2 :     for (const auto& ms : peers)
     693           1 :         a << "[" << ms.name << "] ";
     694           1 :     a << " amix=inputs=" << peers.size();
     695           2 :     return a.str();
     696           1 : }
     697             : 
     698             : void
     699         398 : MediaRecorder::flush()
     700             : {
     701             :     {
     702         398 :         std::lock_guard lk(mutexFilterVideo_);
     703         398 :         if (videoFilter_)
     704           2 :             videoFilter_->flush();
     705         397 :         if (outputVideoFilter_)
     706           2 :             outputVideoFilter_->flush();
     707         397 :     }
     708             :     {
     709         398 :         std::lock_guard lk(mutexFilterAudio_);
     710         398 :         if (audioFilter_)
     711           1 :             audioFilter_->flush();
     712         398 :     }
     713         397 :     if (encoder_)
     714           2 :         encoder_->flush();
     715         397 : }
     716             : 
     717             : void
     718         397 : MediaRecorder::reset()
     719             : {
     720             :     {
     721         397 :         std::lock_guard lk(mutexFrameBuff_);
     722         398 :         frameBuff_.clear();
     723         398 :     }
     724         397 :     videoIdx_ = audioIdx_ = -1;
     725             :     {
     726         397 :         std::lock_guard lk(mutexStreamSetup_);
     727             :         {
     728         397 :             std::lock_guard lk2(mutexFilterVideo_);
     729         397 :             videoFilter_.reset();
     730         398 :             outputVideoFilter_.reset();
     731         398 :         }
     732             :         {
     733         398 :             std::lock_guard lk2(mutexFilterAudio_);
     734         398 :             audioFilter_.reset();
     735         398 :             outputAudioResampler_.reset();
     736         398 :         }
     737         398 :     }
     738         397 :     encoder_.reset();
     739         398 : }
     740             : 
     741             : } // namespace jami

Generated by: LCOV version 1.14