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 "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 33 : StreamObserver(const MediaStream& ms, std::function<void(const std::shared_ptr<MediaFrame>&)> func)
70 66 : : info(ms)
71 33 : , cb_(func) {};
72 :
73 66 : ~StreamObserver()
74 66 : {
75 34 : 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 66 : };
85 :
86 5255 : void update(Observable<std::shared_ptr<MediaFrame>>* /*ob*/, const std::shared_ptr<MediaFrame>& m) override
87 : {
88 5255 : if (not isEnabled)
89 5255 : 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 33 : void attached(Observable<std::shared_ptr<MediaFrame>>* obs) override { observablesFrames_.insert(obs); }
133 :
134 33 : void detached(Observable<std::shared_ptr<MediaFrame>>* obs) override
135 : {
136 33 : auto it = observablesFrames_.find(obs);
137 33 : if (it != observablesFrames_.end())
138 33 : observablesFrames_.erase(it);
139 33 : }
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 396 : MediaRecorder::MediaRecorder() {}
151 :
152 396 : MediaRecorder::~MediaRecorder()
153 : {
154 396 : flush();
155 395 : reset();
156 396 : }
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 33 : MediaRecorder::addStream(const MediaStream& ms)
275 : {
276 33 : bool streamIsNew = false;
277 33 : std::unique_ptr<StreamObserver> oldObserver; // destroy after unlock
278 :
279 : {
280 33 : std::lock_guard lk(mutexStreamSetup_);
281 33 : if (audioOnly_ && ms.isVideo) {
282 0 : JAMI_ERR() << "Attempting to add video stream to audio only recording";
283 0 : return nullptr;
284 : }
285 33 : if (ms.format < 0 || ms.name.empty()) {
286 0 : JAMI_ERR() << "Attempting to add invalid stream to recording";
287 0 : return nullptr;
288 : }
289 :
290 33 : auto it = streams_.find(ms.name);
291 33 : if (it == streams_.end()) {
292 0 : auto streamPtr = std::make_unique<StreamObserver>(ms, [this, ms](const std::shared_ptr<MediaFrame>& frame) {
293 0 : onFrame(ms.name, frame);
294 32 : });
295 32 : it = streams_.insert(std::make_pair(ms.name, std::move(streamPtr))).first;
296 128 : JAMI_LOG("[Recorder: {:p}] Recorder input #{}: {:s}", fmt::ptr(this), streams_.size(), ms.name);
297 32 : streamIsNew = true;
298 32 : } else {
299 1 : if (ms == it->second->info) {
300 0 : JAMI_LOG("[Recorder: {:p}] Recorder already has '{:s}' as input", fmt::ptr(this), ms.name);
301 : } else {
302 1 : oldObserver = std::move(it->second);
303 2 : it->second = std::make_unique<StreamObserver>(ms, [this, ms](const std::shared_ptr<MediaFrame>& frame) {
304 0 : onFrame(ms.name, frame);
305 1 : });
306 : }
307 1 : streamIsNew = false;
308 : }
309 :
310 33 : if (streamIsNew && isRecording_) {
311 0 : if (ms.isVideo) {
312 0 : if (!videoFilter_ || videoFilter_->needsReinitForNewStream(ms.name))
313 0 : setupVideoOutput();
314 : } else {
315 0 : if (!audioFilter_ || audioFilter_->needsReinitForNewStream(ms.name))
316 0 : setupAudioOutput();
317 : }
318 : }
319 33 : it->second->isEnabled = isRecording_;
320 33 : return it->second.get();
321 33 : }
322 :
323 : // oldObserver destroyed here, after mutexStreamSetup_ is released
324 33 : }
325 :
326 : void
327 30 : MediaRecorder::removeStream(const MediaStream& ms)
328 : {
329 30 : std::unique_ptr<StreamObserver> oldObserver; // destroy after unlock
330 :
331 : {
332 30 : std::lock_guard lk(mutexStreamSetup_);
333 :
334 30 : auto it = streams_.find(ms.name);
335 30 : if (it == streams_.end()) {
336 0 : JAMI_LOG("[Recorder: {:p}] Recorder no stream to remove", fmt::ptr(this));
337 : } else {
338 120 : JAMI_LOG("[Recorder: {:p}] Recorder removing '{:s}'", fmt::ptr(this), ms.name);
339 30 : oldObserver = std::move(it->second);
340 30 : streams_.erase(it);
341 30 : if (isRecording_) {
342 0 : if (ms.isVideo)
343 0 : setupVideoOutput();
344 : else
345 0 : setupAudioOutput();
346 : }
347 : }
348 30 : }
349 :
350 : // oldObserver destroyed here, after mutexStreamSetup_ is released
351 60 : return;
352 30 : }
353 :
354 : Observer<std::shared_ptr<MediaFrame>>*
355 390 : MediaRecorder::getStream(const std::string& name) const
356 : {
357 390 : const auto it = streams_.find(name);
358 390 : if (it != streams_.cend())
359 32 : return it->second.get();
360 358 : return nullptr;
361 : }
362 :
363 : void
364 0 : MediaRecorder::onFrame(const std::string& name, const std::shared_ptr<MediaFrame>& frame)
365 : {
366 0 : if (not isRecording_ || interrupted_)
367 0 : return;
368 :
369 0 : std::lock_guard lk(mutexStreamSetup_);
370 :
371 : // copy frame to not mess with the original frame's pts (does not actually copy frame data)
372 0 : std::unique_ptr<MediaFrame> clone;
373 0 : const auto& ms = streams_[name]->info;
374 : #if defined(ENABLE_VIDEO) && defined(ENABLE_HWACCEL)
375 0 : if (ms.isVideo) {
376 0 : auto desc = av_pix_fmt_desc_get((AVPixelFormat) (std::static_pointer_cast<VideoFrame>(frame))->format());
377 0 : if (desc && (desc->flags & AV_PIX_FMT_FLAG_HWACCEL)) {
378 : try {
379 0 : clone = video::HardwareAccel::transferToMainMemory(*std::static_pointer_cast<VideoFrame>(frame),
380 0 : static_cast<AVPixelFormat>(ms.format));
381 0 : } catch (const std::runtime_error& e) {
382 0 : JAMI_ERR("Accel failure: %s", e.what());
383 0 : return;
384 0 : }
385 0 : } else {
386 0 : clone = std::make_unique<MediaFrame>();
387 0 : clone->copyFrom(*frame);
388 : }
389 : } else {
390 : #endif // ENABLE_VIDEO && ENABLE_HWACCEL
391 0 : clone = std::make_unique<MediaFrame>();
392 0 : clone->copyFrom(*frame);
393 : #if defined(ENABLE_VIDEO) && defined(ENABLE_HWACCEL)
394 : }
395 : #endif // ENABLE_VIDEO && ENABLE_HWACCEL
396 0 : clone->pointer()->pts = av_rescale_q_rnd(av_gettime() - startTimeStamp_,
397 : {1, AV_TIME_BASE},
398 : ms.timeBase,
399 : static_cast<AVRounding>(AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX));
400 0 : std::vector<std::unique_ptr<MediaFrame>> filteredFrames;
401 : #ifdef ENABLE_VIDEO
402 0 : if (ms.isVideo && videoFilter_ && outputVideoFilter_) {
403 0 : std::lock_guard lk(mutexFilterVideo_);
404 0 : videoFilter_->feedInput(clone->pointer(), name);
405 0 : if (auto filteredVideoOutput = videoFilter_->readOutput()) {
406 0 : outputVideoFilter_->feedInput(filteredVideoOutput->pointer(), "input");
407 0 : while (auto fFrame = outputVideoFilter_->readOutput()) {
408 0 : filteredFrames.emplace_back(std::move(fFrame));
409 0 : }
410 0 : }
411 0 : } else if (audioFilter_ && outputAudioResampler_ && audioFrameResizer_) {
412 : #endif // ENABLE_VIDEO
413 0 : std::lock_guard lkA(mutexFilterAudio_);
414 0 : audioFilter_->feedInput(clone->pointer(), name);
415 0 : if (auto filteredAudioOutput = audioFilter_->readOutput()) {
416 0 : audioFrameResizer_->enqueue(
417 0 : outputAudioResampler_->resample(std::unique_ptr<AudioFrame>(
418 0 : static_cast<AudioFrame*>(filteredAudioOutput.release())),
419 0 : AudioFormat(48000, 2)));
420 0 : }
421 : #ifdef ENABLE_VIDEO
422 0 : }
423 : #endif // ENABLE_VIDEO
424 :
425 0 : for (auto& fFrame : filteredFrames) {
426 0 : std::lock_guard lk(mutexFrameBuff_);
427 0 : frameBuff_.emplace_back(std::move(fFrame));
428 0 : cv_.notify_one();
429 0 : }
430 0 : }
431 :
432 : int
433 2 : MediaRecorder::initRecord()
434 : {
435 : // need to get encoder parameters before calling openFileOutput
436 : // openFileOutput needs to be called before adding any streams
437 :
438 2 : std::stringstream timestampString;
439 2 : timestampString << std::put_time(&startTime_, "%Y-%m-%d %H:%M:%S");
440 :
441 2 : if (title_.empty()) {
442 1 : title_ = "Conversation at %TIMESTAMP";
443 : }
444 2 : title_ = replaceAll(title_, "%TIMESTAMP", timestampString.str());
445 :
446 2 : if (description_.empty()) {
447 2 : description_ = "Recorded with Jami https://jami.net";
448 : }
449 2 : description_ = replaceAll(description_, "%TIMESTAMP", timestampString.str());
450 :
451 2 : encoder_->setMetadata(title_, description_);
452 2 : encoder_->openOutput(getPath());
453 : #ifdef ENABLE_VIDEO
454 : #ifdef ENABLE_HWACCEL
455 2 : encoder_->enableAccel(false); // TODO recorder has problems with hardware encoding
456 : #endif
457 : #endif // ENABLE_VIDEO
458 :
459 : {
460 2 : MediaStream audioStream;
461 2 : audioStream.name = "audioOutput";
462 2 : audioStream.format = 1;
463 2 : audioStream.timeBase = rational<int>(1, 48000);
464 2 : audioStream.sampleRate = 48000;
465 2 : audioStream.nbChannels = 2;
466 2 : encoder_->setOptions(audioStream);
467 :
468 : auto audioCodec = std::static_pointer_cast<jami::SystemAudioCodecInfo>(
469 4 : getSystemCodecContainer()->searchCodecByName("opus", jami::MEDIA_AUDIO));
470 2 : audioIdx_ = encoder_->addStream(*audioCodec.get());
471 2 : if (audioIdx_ < 0) {
472 0 : JAMI_ERR() << "Failed to add audio stream to encoder";
473 0 : return -1;
474 : }
475 2 : }
476 :
477 : #ifdef ENABLE_VIDEO
478 2 : if (!audioOnly_) {
479 2 : MediaStream videoStream;
480 :
481 2 : videoStream.name = "videoOutput";
482 2 : videoStream.format = 0;
483 2 : videoStream.isVideo = true;
484 2 : videoStream.timeBase = rational<int>(0, 1);
485 2 : videoStream.width = 1280;
486 2 : videoStream.height = 720;
487 2 : videoStream.frameRate = rational<int>(30, 1);
488 2 : videoStream.bitrate = Manager::instance().videoPreferences.getRecordQuality();
489 :
490 2 : MediaDescription args;
491 2 : args.mode = RateMode::CQ;
492 2 : encoder_->setOptions(videoStream);
493 2 : encoder_->setOptions(args);
494 :
495 : auto videoCodec = std::static_pointer_cast<jami::SystemVideoCodecInfo>(
496 4 : getSystemCodecContainer()->searchCodecByName("VP8", jami::MEDIA_VIDEO));
497 2 : videoIdx_ = encoder_->addStream(*videoCodec.get());
498 2 : if (videoIdx_ < 0) {
499 0 : JAMI_ERR() << "Failed to add video stream to encoder";
500 0 : return -1;
501 : }
502 2 : }
503 : #endif // ENABLE_VIDEO
504 :
505 2 : encoder_->setIOContext(nullptr);
506 :
507 2 : JAMI_DBG() << "Recording initialized";
508 2 : return 0;
509 2 : }
510 :
511 : void
512 2 : MediaRecorder::setupVideoOutput()
513 : {
514 2 : MediaStream encoderStream, peer, local, mixer;
515 2 : auto it = std::find_if(streams_.begin(), streams_.end(), [](const auto& pair) {
516 3 : return pair.second->info.isVideo && pair.second->info.name.find("remote") != std::string::npos;
517 : });
518 2 : if (it != streams_.end())
519 1 : peer = it->second->info;
520 :
521 2 : it = std::find_if(streams_.begin(), streams_.end(), [](const auto& pair) {
522 3 : return pair.second->info.isVideo && pair.second->info.name.find("local") != std::string::npos;
523 : });
524 2 : if (it != streams_.end())
525 0 : local = it->second->info;
526 :
527 2 : it = std::find_if(streams_.begin(), streams_.end(), [](const auto& pair) {
528 3 : return pair.second->info.isVideo && pair.second->info.name.find("mixer") != std::string::npos;
529 : });
530 2 : if (it != streams_.end())
531 1 : mixer = it->second->info;
532 :
533 : // vp8 supports only yuv420p
534 2 : videoFilter_.reset(new MediaFilter);
535 2 : int ret = -1;
536 2 : int streams = peer.isValid() + local.isValid() + mixer.isValid();
537 2 : switch (streams) {
538 0 : case 0: {
539 0 : JAMI_WARN() << "Attempting to record a video stream but none is valid";
540 0 : return;
541 : }
542 2 : case 1: {
543 2 : MediaStream inputStream;
544 2 : if (peer.isValid())
545 1 : inputStream = peer;
546 1 : else if (local.isValid())
547 0 : inputStream = local;
548 1 : else if (mixer.isValid())
549 1 : inputStream = mixer;
550 : else {
551 0 : JAMI_ERR("Attempting to record a stream but none is valid");
552 0 : break;
553 : }
554 :
555 4 : ret = videoFilter_->initialize(buildVideoFilter({}, inputStream), {inputStream});
556 2 : break;
557 2 : }
558 0 : case 2: // overlay local video over peer video
559 0 : ret = videoFilter_->initialize(buildVideoFilter({peer}, local), {peer, local});
560 0 : break;
561 0 : default:
562 0 : JAMI_ERR() << "Recording more than 2 video streams is not supported";
563 0 : break;
564 : }
565 :
566 : #ifdef ENABLE_VIDEO
567 2 : if (ret < 0) {
568 0 : JAMI_ERR() << "Failed to initialize video filter";
569 : }
570 :
571 : // setup output filter
572 2 : if (!videoFilter_)
573 0 : return;
574 4 : MediaStream secondaryFilter = videoFilter_->getOutputParams();
575 2 : secondaryFilter.name = "input";
576 2 : if (outputVideoFilter_) {
577 0 : outputVideoFilter_->flush();
578 0 : outputVideoFilter_.reset();
579 : }
580 :
581 2 : outputVideoFilter_.reset(new MediaFilter);
582 :
583 2 : float scaledHeight = 1280 * (float) secondaryFilter.height / (float) secondaryFilter.width;
584 4 : std::string scaleFilter = "scale=1280:-2";
585 2 : if (scaledHeight > 720)
586 0 : scaleFilter += ",scale=-2:720";
587 :
588 4 : std::ostringstream f;
589 2 : f << "[input]" << scaleFilter << ",pad=1280:720:(ow-iw)/2:(oh-ih)/2,format=pix_fmts=yuv420p,fps=30";
590 :
591 4 : ret = outputVideoFilter_->initialize(f.str(), {secondaryFilter});
592 :
593 2 : if (ret < 0) {
594 0 : JAMI_ERR() << "Failed to initialize output video filter";
595 : }
596 :
597 : #endif
598 :
599 2 : return;
600 2 : }
601 :
602 : std::string
603 2 : MediaRecorder::buildVideoFilter(const std::vector<MediaStream>& peers, const MediaStream& local) const
604 : {
605 2 : std::ostringstream v;
606 :
607 2 : switch (peers.size()) {
608 2 : case 0:
609 2 : v << "[" << local.name << "] fps=30, format=pix_fmts=yuv420p";
610 2 : break;
611 0 : case 1: {
612 0 : auto p = peers[0];
613 0 : const constexpr int minHeight = 720;
614 0 : const bool needScale = (p.height < minHeight);
615 0 : const int newHeight = (needScale ? minHeight : p.height);
616 :
617 : // NOTE -2 means preserve aspect ratio and have the new number be even
618 0 : if (needScale)
619 0 : v << "[" << p.name << "] fps=30, scale=-2:" << newHeight << " [v:m]; ";
620 : else
621 0 : v << "[" << p.name << "] fps=30 [v:m]; ";
622 :
623 0 : v << "[" << local.name << "] fps=30, scale=-2:" << newHeight / 5 << " [v:o]; ";
624 :
625 0 : v << "[v:m] [v:o] overlay=main_w-overlay_w:main_h-overlay_h" << ", format=pix_fmts=yuv420p";
626 0 : } break;
627 0 : default:
628 0 : JAMI_ERR() << "Video recordings with more than 2 video streams are not supported";
629 0 : break;
630 : }
631 :
632 4 : return v.str();
633 2 : }
634 :
635 : void
636 1 : MediaRecorder::setupAudioOutput()
637 : {
638 1 : MediaStream encoderStream;
639 :
640 : // resample to common audio format, so any player can play the file
641 1 : audioFilter_.reset(new MediaFilter);
642 1 : int ret = -1;
643 :
644 1 : if (streams_.empty()) {
645 0 : JAMI_WARN() << "Attempting to record a audio stream but none is valid";
646 0 : return;
647 : }
648 :
649 1 : std::vector<MediaStream> peers {};
650 3 : for (const auto& media : streams_) {
651 2 : if (!media.second->info.isVideo && media.second->info.isValid())
652 1 : peers.emplace_back(media.second->info);
653 : }
654 :
655 1 : ret = audioFilter_->initialize(buildAudioFilter(peers), peers);
656 :
657 1 : if (ret < 0) {
658 0 : JAMI_ERR() << "Failed to initialize audio filter";
659 0 : return;
660 : }
661 :
662 : // setup output filter
663 1 : if (!audioFilter_)
664 0 : return;
665 1 : MediaStream secondaryFilter = audioFilter_->getOutputParams();
666 1 : secondaryFilter.name = "input";
667 :
668 1 : outputAudioResampler_ = std::make_unique<Resampler>();
669 1 : if (!outputAudioResampler_)
670 0 : JAMI_ERROR("Failed to initialize audio resampler");
671 :
672 2 : audioFrameResizer_ = std::make_unique<AudioFrameResizer>(AudioFormat(48000, 2),
673 1 : encoder_->getCurrentAudioAVCtxFrameSize(),
674 0 : [this](std::shared_ptr<AudioFrame>&& frame) {
675 0 : std::lock_guard lk(mutexFrameBuff_);
676 0 : frameBuff_.emplace_back(
677 0 : std::shared_ptr<MediaFrame>(std::move(frame)));
678 0 : cv_.notify_one();
679 1 : });
680 1 : if (!audioFrameResizer_)
681 0 : JAMI_ERROR("Failed to initialize audio frame resizer");
682 :
683 1 : return;
684 1 : }
685 :
686 : std::string
687 1 : MediaRecorder::buildAudioFilter(const std::vector<MediaStream>& peers) const
688 : {
689 : // resampling is handled by outputAudioResampler_
690 1 : std::ostringstream a;
691 :
692 2 : for (const auto& ms : peers)
693 1 : a << "[" << ms.name << "] ";
694 1 : a << " amix=inputs=" << peers.size();
695 2 : return a.str();
696 1 : }
697 :
698 : void
699 398 : MediaRecorder::flush()
700 : {
701 : {
702 398 : std::lock_guard lk(mutexFilterVideo_);
703 398 : if (videoFilter_)
704 2 : videoFilter_->flush();
705 397 : if (outputVideoFilter_)
706 2 : outputVideoFilter_->flush();
707 397 : }
708 : {
709 398 : std::lock_guard lk(mutexFilterAudio_);
710 398 : if (audioFilter_)
711 1 : audioFilter_->flush();
712 398 : }
713 397 : if (encoder_)
714 2 : encoder_->flush();
715 397 : }
716 :
717 : void
718 397 : MediaRecorder::reset()
719 : {
720 : {
721 397 : std::lock_guard lk(mutexFrameBuff_);
722 398 : frameBuff_.clear();
723 398 : }
724 397 : videoIdx_ = audioIdx_ = -1;
725 : {
726 397 : std::lock_guard lk(mutexStreamSetup_);
727 : {
728 397 : std::lock_guard lk2(mutexFilterVideo_);
729 397 : videoFilter_.reset();
730 398 : outputVideoFilter_.reset();
731 398 : }
732 : {
733 398 : std::lock_guard lk2(mutexFilterAudio_);
734 398 : audioFilter_.reset();
735 398 : outputAudioResampler_.reset();
736 398 : }
737 398 : }
738 397 : encoder_.reset();
739 398 : }
740 :
741 : } // namespace jami
|