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