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