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