LCOV - code coverage report
Current view: top level - src/media - media_recorder.cpp (source / functions) Hit Total Coverage
Test: jami-coverage-filtered.info Lines: 297 437 68.0 %
Date: 2024-04-23 08:02:50 Functions: 37 44 84.1 %

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

Generated by: LCOV version 1.14