LCOV - code coverage report
Current view: top level - src/media - media_recorder.cpp (source / functions) Coverage Total Hit
Test: jami-coverage-filtered.info Lines: 65.8 % 442 291
Test Date: 2026-06-13 09:18:46 Functions: 49.4 % 83 41

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

Generated by: LCOV version 2.0-1