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