35#include <opendht/thread_pool.h>
51replaceAll(
const std::string&
str,
const std::string& from,
const std::string& to)
74 while (observablesFrames_.size() > 0) {
75 auto obs = observablesFrames_.begin();
79 auto it = observablesFrames_.find(*
obs);
80 if (
it != observablesFrames_.end())
81 observablesFrames_.erase(
it);
85 void update(
Observable<std::shared_ptr<MediaFrame>>* ,
const std::shared_ptr<MediaFrame>&
m)
override
91 std::shared_ptr<VideoFrame>
framePtr;
98 }
catch (
const std::runtime_error&
e) {
104 framePtr = std::static_pointer_cast<VideoFrame>(
m);
106 if (
angle != rotation_) {
115 if (videoRotationFilter_) {
117 auto rotated = videoRotationFilter_->readOutput();
135 auto it = observablesFrames_.find(
obs);
136 if (
it != observablesFrames_.end())
137 observablesFrames_.erase(
it);
143 std::function<
void(
const std::shared_ptr<MediaFrame>&)> cb_;
144 std::unique_ptr<MediaFilter> videoRotationFilter_ {};
146 std::set<Observable<std::shared_ptr<MediaFrame>>*> observablesFrames_;
167 return path_ +
".ogg";
169 return path_ +
".webm";
194 std::time_t t = std::time(
nullptr);
195 startTime_ = *std::localtime(&t);
198 std::lock_guard
lk(encoderMtx_);
202 if (initRecord() >= 0) {
205 std::lock_guard
lk(mutexStreamSetup_);
206 for (
auto&
media : streams_) {
207 if (
media.second->info.isVideo) {
208 std::lock_guard
lk2(mutexFilterVideo_);
211 std::lock_guard
lk2(mutexFilterAudio_);
214 media.second->isEnabled =
true;
219 std::lock_guard
lk(
rec->encoderMtx_);
220 while (
rec->isRecording()) {
221 std::shared_ptr<MediaFrame>
frame;
224 std::unique_lock
lk(
rec->mutexFrameBuff_);
226 if (
rec->interrupted_) {
229 frame = std::move(
rec->frameBuff_.front());
230 rec->frameBuff_.pop_front();
236 bool isVideo = (
frame->pointer()->width > 0 &&
frame->pointer()->height > 0);
237 rec->encoder_->encode(
frame->pointer(), isVideo ?
rec->videoIdx_ :
rec->audioIdx_);
239 rec->encoder_->encode(
frame->pointer(),
rec->audioIdx_);
243 JAMI_ERR() <<
"Failed to record frame: " <<
e.what();
250 interrupted_ =
false;
261 isRecording_ =
false;
263 std::lock_guard
lk(mutexStreamSetup_);
264 for (
auto&
media : streams_) {
265 media.second->isEnabled =
false;
279 std::lock_guard
lk(mutexStreamSetup_);
280 if (audioOnly_ && ms.
isVideo) {
281 JAMI_ERR() <<
"Attempting to add video stream to audio only recording";
285 JAMI_ERR() <<
"Attempting to add invalid stream to recording";
289 auto it = streams_.find(ms.
name);
290 if (
it == streams_.end()) {
291 auto streamPtr = std::make_unique<StreamObserver>(ms, [
this, ms](
const std::shared_ptr<MediaFrame>&
frame) {
295 JAMI_LOG(
"[Recorder: {:p}] Recorder input #{}: {:s}", fmt::ptr(
this), streams_.size(), ms.
name);
298 if (ms ==
it->second->info) {
299 JAMI_LOG(
"[Recorder: {:p}] Recorder already has '{:s}' as input", fmt::ptr(
this), ms.
name);
302 it->second = std::make_unique<StreamObserver>(ms, [
this, ms](
const std::shared_ptr<MediaFrame>&
frame) {
311 if (!videoFilter_ || videoFilter_->needsReinitForNewStream(ms.
name))
314 if (!audioFilter_ || audioFilter_->needsReinitForNewStream(ms.
name))
318 it->second->isEnabled = isRecording_;
319 return it->second.get();
331 std::lock_guard
lk(mutexStreamSetup_);
333 auto it = streams_.find(ms.
name);
334 if (
it == streams_.end()) {
335 JAMI_LOG(
"[Recorder: {:p}] Recorder no stream to remove", fmt::ptr(
this));
337 JAMI_LOG(
"[Recorder: {:p}] Recorder removing '{:s}'", fmt::ptr(
this), ms.
name);
356 const auto it = streams_.find(name);
357 if (
it != streams_.cend())
358 return it->second.get();
363MediaRecorder::onFrame(
const std::string& name,
const std::shared_ptr<MediaFrame>&
frame)
365 if (
not isRecording_ || interrupted_)
368 std::lock_guard
lk(mutexStreamSetup_);
371 std::unique_ptr<MediaFrame>
clone;
372 const auto& ms = streams_[name]->info;
373#if defined(ENABLE_VIDEO) && defined(ENABLE_HWACCEL)
380 }
catch (
const std::runtime_error&
e) {
385 clone = std::make_unique<MediaFrame>();
390 clone = std::make_unique<MediaFrame>();
392#if defined(ENABLE_VIDEO) && defined(ENABLE_HWACCEL)
401 if (ms.isVideo && videoFilter_ && outputVideoFilter_) {
402 std::lock_guard
lk(mutexFilterVideo_);
403 videoFilter_->feedInput(
clone->pointer(), name);
406 while (
auto fFrame = outputVideoFilter_->readOutput()) {
410 }
else if (audioFilter_ && outputAudioResampler_ && audioFrameResizer_) {
412 std::lock_guard
lkA(mutexFilterAudio_);
413 audioFilter_->feedInput(
clone->pointer(), name);
415 audioFrameResizer_->enqueue(
416 outputAudioResampler_->resample(std::unique_ptr<AudioFrame>(
418 AudioFormat(48000, 2)));
425 std::lock_guard
lk(mutexFrameBuff_);
426 frameBuff_.emplace_back(std::move(
fFrame));
432MediaRecorder::initRecord()
440 if (title_.empty()) {
441 title_ =
"Conversation at %TIMESTAMP";
445 if (description_.empty()) {
446 description_ =
"Recorded with Jami https://jami.net";
450 encoder_->setMetadata(title_, description_);
451 encoder_->openOutput(
getPath());
454 encoder_->enableAccel(
false);
467 auto audioCodec = std::static_pointer_cast<jami::SystemAudioCodecInfo>(
469 audioIdx_ = encoder_->addStream(*
audioCodec.get());
471 JAMI_ERR() <<
"Failed to add audio stream to encoder";
489 MediaDescription
args;
492 encoder_->setOptions(
args);
494 auto videoCodec = std::static_pointer_cast<jami::SystemVideoCodecInfo>(
496 videoIdx_ = encoder_->addStream(*
videoCodec.get());
498 JAMI_ERR() <<
"Failed to add video stream to encoder";
504 encoder_->setIOContext(
nullptr);
506 JAMI_DBG() <<
"Recording initialized";
511MediaRecorder::setupVideoOutput()
514 auto it = std::find_if(streams_.begin(), streams_.end(), [](
const auto&
pair) {
515 return pair.second->info.isVideo && pair.second->info.name.find(
"remote") != std::string::npos;
517 if (
it != streams_.end())
518 peer =
it->second->info;
520 it = std::find_if(streams_.begin(), streams_.end(), [](
const auto&
pair) {
521 return pair.second->info.isVideo && pair.second->info.name.find(
"local") != std::string::npos;
523 if (
it != streams_.end())
526 it = std::find_if(streams_.begin(), streams_.end(), [](
const auto&
pair) {
527 return pair.second->info.isVideo && pair.second->info.name.find(
"mixer") != std::string::npos;
529 if (
it != streams_.end())
533 videoFilter_.reset(
new MediaFilter);
538 JAMI_WARN() <<
"Attempting to record a video stream but none is valid";
545 else if (
local.isValid())
547 else if (
mixer.isValid())
550 JAMI_ERR(
"Attempting to record a stream but none is valid");
558 ret = videoFilter_->initialize(buildVideoFilter({peer},
local), {peer,
local});
561 JAMI_ERR() <<
"Recording more than 2 video streams is not supported";
567 JAMI_ERR() <<
"Failed to initialize video filter";
575 if (outputVideoFilter_) {
576 outputVideoFilter_->flush();
577 outputVideoFilter_.reset();
580 outputVideoFilter_.reset(
new MediaFilter);
587 std::ostringstream
f;
588 f <<
"[input]" <<
scaleFilter <<
",pad=1280:720:(ow-iw)/2:(oh-ih)/2,format=pix_fmts=yuv420p,fps=30";
593 JAMI_ERR() <<
"Failed to initialize output video filter";
602MediaRecorder::buildVideoFilter(
const std::vector<MediaStream>& peers,
const MediaStream&
local)
const
604 std::ostringstream v;
606 switch (peers.size()) {
608 v <<
"[" <<
local.name <<
"] fps=30, format=pix_fmts=yuv420p";
611 const auto& p = peers[0];
618 v <<
"[" << p.name <<
"] fps=30, scale=-2:" <<
newHeight <<
" [v:m]; ";
620 v <<
"[" << p.name <<
"] fps=30 [v:m]; ";
622 v <<
"[" <<
local.name <<
"] fps=30, scale=-2:" <<
newHeight / 5 <<
" [v:o]; ";
624 v <<
"[v:m] [v:o] overlay=main_w-overlay_w:main_h-overlay_h" <<
", format=pix_fmts=yuv420p";
627 JAMI_ERR() <<
"Video recordings with more than 2 video streams are not supported";
635MediaRecorder::setupAudioOutput()
640 audioFilter_.reset(
new MediaFilter);
643 if (streams_.empty()) {
644 JAMI_WARN() <<
"Attempting to record a audio stream but none is valid";
648 std::vector<MediaStream> peers {};
649 for (
const auto&
media : streams_) {
650 if (!
media.second->info.isVideo &&
media.second->info.isValid())
651 peers.emplace_back(
media.second->info);
654 ret = audioFilter_->initialize(buildAudioFilter(peers), peers);
657 JAMI_ERR() <<
"Failed to initialize audio filter";
667 outputAudioResampler_ = std::make_unique<Resampler>();
668 if (!outputAudioResampler_)
669 JAMI_ERROR(
"Failed to initialize audio resampler");
671 audioFrameResizer_ = std::make_unique<AudioFrameResizer>(AudioFormat(48000, 2),
672 encoder_->getCurrentAudioAVCtxFrameSize(),
673 [
this](std::shared_ptr<AudioFrame>&&
frame) {
674 std::lock_guard lk(mutexFrameBuff_);
675 frameBuff_.emplace_back(
676 std::shared_ptr<MediaFrame>(std::move(frame)));
679 if (!audioFrameResizer_)
680 JAMI_ERROR(
"Failed to initialize audio frame resizer");
686MediaRecorder::buildAudioFilter(
const std::vector<MediaStream>& peers)
const
689 std::ostringstream
a;
691 for (
const auto& ms : peers)
692 a <<
"[" << ms.name <<
"] ";
693 a <<
" amix=inputs=" << peers.size();
698MediaRecorder::flush()
701 std::lock_guard
lk(mutexFilterVideo_);
703 videoFilter_->flush();
704 if (outputVideoFilter_)
705 outputVideoFilter_->flush();
708 std::lock_guard
lk(mutexFilterAudio_);
710 audioFilter_->flush();
717MediaRecorder::reset()
720 std::lock_guard
lk(mutexFrameBuff_);
723 videoIdx_ = audioIdx_ = -1;
725 std::lock_guard
lk(mutexStreamSetup_);
727 std::lock_guard
lk2(mutexFilterVideo_);
728 videoFilter_.reset();
729 outputVideoFilter_.reset();
732 std::lock_guard
lk2(mutexFilterAudio_);
733 audioFilter_.reset();
734 outputAudioResampler_.reset();
static LIBJAMI_TEST_EXPORT Manager & instance()
static std::unique_ptr< VideoFrame > transferToMainMemory(const VideoFrame &frame, AVPixelFormat desiredFormat)
Transfers hardware frame to main memory.
#define JAMI_ERROR(formatstr,...)
#define JAMI_LOG(formatstr,...)
std::unique_ptr< MediaFilter > getTransposeFilter(int rotation, std::string inputName, int width, int height, int format, bool rescale)
decltype(getGlobalInstance< SystemCodecContainer >) & getSystemCodecContainer
const constexpr char ROTATION_FILTER_INPUT_NAME[]
void emitSignal(Args... args)
static std::string replaceAll(const std::string &str, const std::string &from, const std::string &to)
libjami::AudioFrame AudioFrame