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