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