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 : #ifdef HAVE_CONFIG_H
19 : #include "config.h"
20 : #endif
21 :
22 : #include "video_input.h"
23 :
24 : #include "media_decoder.h"
25 : #include "media_const.h"
26 : #include "manager.h"
27 : #include "client/videomanager.h"
28 : #include "client/jami_signal.h"
29 : #include "logger.h"
30 : #include "media/media_buffer.h"
31 :
32 : #include <libavformat/avio.h>
33 :
34 : #include <cassert>
35 : #include <stdexcept>
36 : #include <string>
37 : #ifdef _MSC_VER
38 : #include <io.h> // for access
39 : #else
40 : #include <sys/syscall.h>
41 : #include <unistd.h>
42 : #endif
43 : extern "C" {
44 : #include <libavutil/display.h>
45 : }
46 :
47 : namespace jami {
48 : namespace video {
49 :
50 : static constexpr unsigned default_grab_width = 640;
51 : static constexpr unsigned default_grab_height = 480;
52 :
53 : // How long, and how often, to retry opening a capture device that is reported
54 : // busy before giving up.
55 : static constexpr auto EBUSY_RETRY_DELAY = std::chrono::milliseconds(100);
56 : static constexpr unsigned EBUSY_MAX_ATTEMPTS = 50;
57 :
58 72 : VideoInput::VideoInput(VideoInputMode inputMode, const std::string& resource, const std::string& sink)
59 : : VideoGenerator::VideoGenerator()
60 144 : , loop_(std::bind(&VideoInput::setup, this),
61 144 : std::bind(&VideoInput::process, this),
62 144 : std::bind(&VideoInput::cleanup, this))
63 : {
64 72 : inputMode_ = inputMode;
65 72 : if (inputMode_ == VideoInputMode::Undefined) {
66 : #if (defined(__ANDROID__) || (defined(TARGET_OS_IOS) && TARGET_OS_IOS))
67 : inputMode_ = VideoInputMode::ManagedByClient;
68 : #else
69 61 : inputMode_ = VideoInputMode::ManagedByDaemon;
70 : #endif
71 : }
72 72 : sink_ = Manager::instance().createSinkClient(sink.empty() ? resource : sink);
73 72 : switchInput(resource);
74 72 : }
75 :
76 143 : VideoInput::~VideoInput()
77 : {
78 72 : isStopped_ = true;
79 72 : if (videoManagedByClient()) {
80 : // Stop the video sink for videos that are managed by the client.
81 0 : cleanup();
82 0 : emitSignal<libjami::VideoSignal::StopCapture>(decOpts_.input);
83 0 : capturing_ = false;
84 0 : return;
85 : }
86 72 : loop_.join();
87 143 : }
88 :
89 : void
90 62 : VideoInput::startLoop()
91 : {
92 62 : if (videoManagedByClient()) {
93 0 : switchDevice();
94 0 : return;
95 : }
96 62 : if (!loop_.isRunning())
97 62 : loop_.start();
98 : }
99 :
100 : void
101 0 : VideoInput::switchDevice()
102 : {
103 0 : if (switchPending_.exchange(false)) {
104 0 : JAMI_LOG("Switching input to '{}'", decOpts_.input);
105 0 : if (decOpts_.input.empty()) {
106 0 : capturing_ = false;
107 0 : return;
108 : }
109 :
110 0 : emitSignal<libjami::VideoSignal::StartCapture>(decOpts_.input);
111 0 : capturing_ = true;
112 : }
113 : }
114 :
115 : int
116 165 : VideoInput::getWidth() const
117 : {
118 165 : if (videoManagedByClient()) {
119 0 : return static_cast<int>(decOpts_.width);
120 : }
121 165 : return decoder_ ? decoder_->getWidth() : 0;
122 : }
123 :
124 : int
125 165 : VideoInput::getHeight() const
126 : {
127 165 : if (videoManagedByClient()) {
128 0 : return static_cast<int>(decOpts_.height);
129 : }
130 165 : return decoder_ ? decoder_->getHeight() : 0;
131 : }
132 :
133 : AVPixelFormat
134 0 : VideoInput::getPixelFormat() const
135 : {
136 0 : if (!videoManagedByClient()) {
137 0 : return decoder_->getPixelFormat();
138 : }
139 0 : return (AVPixelFormat) std::stoi(decOpts_.format);
140 : }
141 :
142 : void
143 0 : VideoInput::setRotation(int angle)
144 : {
145 0 : std::shared_ptr<AVBufferRef> displayMatrix {av_buffer_alloc(sizeof(int32_t) * 9), [](AVBufferRef* buf) {
146 0 : av_buffer_unref(&buf);
147 0 : }};
148 0 : if (displayMatrix) {
149 0 : av_display_rotation_set(reinterpret_cast<int32_t*>(displayMatrix->data), angle);
150 0 : displayMatrix_ = std::move(displayMatrix);
151 : }
152 0 : }
153 :
154 : bool
155 72 : VideoInput::setup()
156 : {
157 72 : if (not attach(sink_.get())) {
158 0 : JAMI_ERROR("attach sink failed");
159 0 : return false;
160 : }
161 :
162 72 : if (!sink_->start())
163 0 : JAMI_ERROR("start sink failed");
164 :
165 72 : JAMI_LOG("VideoInput ready to capture");
166 :
167 72 : return true;
168 : }
169 :
170 : void
171 5514 : VideoInput::process()
172 : {
173 5514 : if (playingFile_) {
174 5452 : if (paused_ || !decoder_->emitFrame(false)) {
175 5452 : std::this_thread::sleep_for(std::chrono::milliseconds(20));
176 : }
177 5452 : return;
178 : }
179 62 : if (switchPending_)
180 62 : createDecoder();
181 :
182 62 : if (not captureFrame()) {
183 62 : loop_.stop();
184 62 : return;
185 : }
186 : }
187 :
188 : void
189 9 : VideoInput::setSeekTime(int64_t time)
190 : {
191 9 : if (decoder_) {
192 9 : decoder_->setSeekTime(time);
193 : }
194 9 : }
195 :
196 : void
197 72 : VideoInput::cleanup()
198 : {
199 72 : deleteDecoder(); // do it first to let a chance to last frame to be displayed
200 72 : stopSink();
201 72 : JAMI_LOG("VideoInput closed");
202 72 : }
203 :
204 : bool
205 62 : VideoInput::captureFrame()
206 : {
207 : // Return true if capture could continue, false if must be stop
208 62 : if (not decoder_)
209 62 : return false;
210 :
211 0 : switch (decoder_->decode()) {
212 0 : case MediaDemuxer::Status::EndOfFile:
213 0 : createDecoder();
214 0 : return static_cast<bool>(decoder_);
215 0 : case MediaDemuxer::Status::ReadError:
216 0 : JAMI_ERROR("Failed to decode frame");
217 0 : return false;
218 0 : default:
219 0 : return true;
220 : }
221 : }
222 : void
223 9 : VideoInput::flushBuffers()
224 : {
225 9 : if (decoder_) {
226 9 : decoder_->flushBuffers();
227 : }
228 9 : }
229 :
230 : void
231 10 : VideoInput::configureFilePlayback(const std::string& path, std::shared_ptr<MediaDemuxer>& demuxer, int index)
232 : {
233 10 : deleteDecoder();
234 10 : clearOptions();
235 :
236 0 : auto decoder = std::make_unique<MediaDecoder>(demuxer, index, [this](std::shared_ptr<MediaFrame>&& frame) {
237 0 : publishFrame(std::static_pointer_cast<VideoFrame>(frame));
238 10 : });
239 10 : if (!decoder->isReady()) {
240 0 : throw std::runtime_error("video decoder setup failed for " + path);
241 : }
242 10 : decoder->setInterruptCallback([](void* data) -> int { return not static_cast<VideoInput*>(data)->isCapturing(); },
243 : this);
244 10 : decoder->emulateRate();
245 :
246 10 : decoder_ = std::move(decoder);
247 10 : playingFile_ = true;
248 :
249 : // For DBUS it is imperative that we start the sink before setting the frame size
250 10 : sink_->start();
251 : /* Signal the client about readable sink */
252 10 : sink_->setFrameSize(decoder_->getWidth(), decoder_->getHeight());
253 :
254 10 : loop_.start();
255 :
256 10 : decOpts_.width = ((decoder_->getWidth() >> 3) << 3);
257 10 : decOpts_.height = ((decoder_->getHeight() >> 3) << 3);
258 10 : decOpts_.framerate = decoder_->getFps();
259 10 : AVPixelFormat fmt = decoder_->getPixelFormat();
260 10 : if (fmt != AV_PIX_FMT_NONE) {
261 10 : decOpts_.pixel_format = av_get_pix_fmt_name(fmt);
262 : } else {
263 0 : JAMI_WARNING("Unable to determine pixel format, using default");
264 0 : decOpts_.pixel_format = av_get_pix_fmt_name(AV_PIX_FMT_YUV420P);
265 : }
266 :
267 10 : if (onSuccessfulSetup_)
268 0 : onSuccessfulSetup_(MEDIA_VIDEO, 0);
269 10 : foundDecOpts(decOpts_);
270 10 : futureDecOpts_ = foundDecOpts_.get_future().share();
271 10 : }
272 :
273 : void
274 117 : VideoInput::setRecorderCallback(const std::function<void(const MediaStream& ms)>& cb)
275 : {
276 117 : recorderCallback_ = cb;
277 117 : if (decoder_)
278 9 : decoder_->setContextCallback([this]() {
279 0 : if (recorderCallback_)
280 0 : recorderCallback_(getInfo());
281 0 : });
282 117 : }
283 :
284 : void
285 62 : VideoInput::createDecoder()
286 : {
287 62 : deleteDecoder();
288 :
289 62 : switchPending_ = false;
290 :
291 62 : if (decOpts_.input.empty()) {
292 62 : foundDecOpts(decOpts_);
293 62 : return;
294 : }
295 :
296 0 : auto decoder = std::make_unique<MediaDecoder>([this](const std::shared_ptr<MediaFrame>& frame) mutable {
297 0 : publishFrame(std::static_pointer_cast<VideoFrame>(frame));
298 0 : });
299 :
300 0 : if (emulateRate_)
301 0 : decoder->emulateRate();
302 :
303 0 : decoder->setInterruptCallback([](void* data) -> int { return not static_cast<VideoInput*>(data)->isCapturing(); },
304 : this);
305 :
306 0 : bool ready = false, restartSink = false;
307 0 : if ((decOpts_.format == "x11grab" || decOpts_.format == "dxgigrab" || decOpts_.format == "pipewiregrab")
308 0 : && !decOpts_.is_area) {
309 0 : decOpts_.width = 0;
310 0 : decOpts_.height = 0;
311 : }
312 0 : unsigned busyAttempts = 0;
313 0 : while (!ready && !isStopped_) {
314 : // Retry to open the video till the input is opened
315 0 : int ret = decoder->openInput(decOpts_);
316 0 : ready = ret >= 0;
317 0 : if (ret < 0 && -ret != EBUSY) {
318 0 : JAMI_ERROR("Unable to open input \"{}\" with status {}", decOpts_.input, ret);
319 0 : foundDecOpts(decOpts_);
320 0 : return;
321 0 : } else if (-ret == EBUSY) {
322 : // If the device is busy, this means that it can be used by another call.
323 : // If this is the case, cleanup() can occurs and this will erase shmPath_
324 : // So, be sure to regenerate a correct shmPath for clients.
325 0 : restartSink = true;
326 : // Give the other user a chance to release the device, but don't retry
327 : // forever: hammering a capture device that never frees up can wedge
328 : // the driver, and some webcams drop off the USB bus entirely.
329 0 : if (++busyAttempts >= EBUSY_MAX_ATTEMPTS) {
330 0 : JAMI_ERROR("Unable to open input \"{}\": still busy after {} attempts",
331 : decOpts_.input,
332 : EBUSY_MAX_ATTEMPTS);
333 0 : foundDecOpts(decOpts_);
334 0 : return;
335 : }
336 0 : std::this_thread::sleep_for(EBUSY_RETRY_DELAY);
337 : }
338 : }
339 :
340 0 : if (isStopped_)
341 0 : return;
342 :
343 0 : if (restartSink && !isStopped_) {
344 0 : sink_->start();
345 : }
346 :
347 : /* Data available, finish the decoding */
348 0 : if (decoder->setupVideo() < 0) {
349 0 : JAMI_ERROR("decoder IO startup failed");
350 0 : foundDecOpts(decOpts_);
351 0 : return;
352 : }
353 :
354 0 : auto ret = decoder->decode(); // Populate AVCodecContext fields
355 0 : if (ret == MediaDemuxer::Status::ReadError) {
356 0 : JAMI_LOG("Decoder error");
357 0 : return;
358 : }
359 :
360 0 : decOpts_.width = ((decoder->getWidth() >> 3) << 3);
361 0 : decOpts_.height = ((decoder->getHeight() >> 3) << 3);
362 0 : decOpts_.framerate = decoder->getFps();
363 0 : AVPixelFormat fmt = decoder->getPixelFormat();
364 0 : if (fmt != AV_PIX_FMT_NONE) {
365 0 : decOpts_.pixel_format = av_get_pix_fmt_name(fmt);
366 : } else {
367 0 : JAMI_WARNING("Unable to determine pixel format, using default");
368 0 : decOpts_.pixel_format = av_get_pix_fmt_name(AV_PIX_FMT_YUV420P);
369 : }
370 :
371 0 : JAMI_LOG("created decoder with video params : size={}X{}, fps={} pix={}",
372 : decOpts_.width,
373 : decOpts_.height,
374 : decOpts_.framerate.real(),
375 : decOpts_.pixel_format);
376 0 : if (onSuccessfulSetup_)
377 0 : onSuccessfulSetup_(MEDIA_VIDEO, 0);
378 :
379 0 : decoder_ = std::move(decoder);
380 :
381 0 : foundDecOpts(decOpts_);
382 :
383 : /* Signal the client about readable sink */
384 0 : sink_->setFrameSize(decoder_->getWidth(), decoder_->getHeight());
385 :
386 0 : decoder_->setContextCallback([this]() {
387 0 : if (recorderCallback_)
388 0 : recorderCallback_(getInfo());
389 0 : });
390 0 : }
391 :
392 : void
393 144 : VideoInput::deleteDecoder()
394 : {
395 144 : if (not decoder_)
396 134 : return;
397 10 : flushFrames();
398 10 : decoder_.reset();
399 : }
400 :
401 : void
402 72 : VideoInput::clearOptions()
403 : {
404 72 : decOpts_ = {};
405 72 : emulateRate_ = false;
406 72 : }
407 :
408 : bool
409 0 : VideoInput::isCapturing() const noexcept
410 : {
411 0 : if (videoManagedByClient()) {
412 0 : return capturing_;
413 : }
414 0 : return loop_.isRunning();
415 : }
416 :
417 : bool
418 0 : VideoInput::initCamera(const std::string& device)
419 : {
420 0 : if (auto* dm = jami::getVideoDeviceMonitor()) {
421 0 : decOpts_ = dm->getDeviceParams(device);
422 0 : return true;
423 : }
424 0 : return false;
425 : }
426 :
427 : static constexpr unsigned
428 0 : round2pow(unsigned i, unsigned n)
429 : {
430 0 : return (i >> n) << n;
431 : }
432 :
433 : #if !defined(WIN32) && !defined(__APPLE__)
434 : bool
435 0 : VideoInput::initLinuxGrab(const std::string& display)
436 : {
437 0 : auto* deviceMonitor = jami::getVideoDeviceMonitor();
438 0 : if (!deviceMonitor)
439 0 : return false;
440 : // Patterns (all platforms except Linux with Wayland)
441 : // full screen sharing: :1+0,0 2560x1440 (SCREEN 1, POSITION 0X0, RESOLUTION 2560X1440)
442 : // area sharing: :1+882,211 1532x779 (SCREEN 1, POSITION 882x211, RESOLUTION 1532x779)
443 : // window sharing: :+1,0 0x0 window-id:0x0340021e (POSITION 0X0)
444 : //
445 : // Pattern (Linux with Wayland)
446 : // full screen or window sharing: pipewire pid:2861 fd:23 node:68
447 0 : size_t space = display.find(' ');
448 0 : std::string windowIdStr = "window-id:";
449 0 : size_t winIdPos = display.find(windowIdStr);
450 :
451 0 : DeviceParams p = deviceMonitor->getDeviceParams(DEVICE_DESKTOP);
452 0 : if (winIdPos != std::string::npos) {
453 0 : size_t endPos = display.find(' ', winIdPos + windowIdStr.size());
454 0 : p.window_id = display.substr(winIdPos + windowIdStr.size(),
455 0 : endPos - (winIdPos + windowIdStr.size())); // "0x0340021e";
456 0 : p.is_area = 0;
457 : }
458 0 : std::string fpsStr = "fps:";
459 0 : if (display.find(fpsStr) != std::string::npos) {
460 0 : size_t fpsPos = display.find(fpsStr) + fpsStr.size();
461 0 : int fps = std::stoi(display.substr(fpsPos));
462 0 : p.framerate = fps;
463 0 : JAMI_LOG("Custom framerate set to {} fps", fps);
464 : }
465 0 : if (display.find("pipewire") != std::string::npos) {
466 0 : std::string pidStr = "pid:";
467 0 : std::string fdStr = "fd:";
468 0 : std::string nodeStr = "node:";
469 :
470 0 : size_t pidPos = display.find(pidStr) + pidStr.size();
471 0 : size_t fdPos = display.find(fdStr) + fdStr.size();
472 0 : size_t nodePos = display.find(nodeStr) + nodeStr.size();
473 :
474 0 : pid_t pid = static_cast<pid_t>(std::stol(display.substr(pidPos)));
475 0 : int fd = std::stoi(display.substr(fdPos));
476 0 : if (pid != getpid()) {
477 : #ifdef SYS_pidfd_getfd
478 : // We are unable to directly use a file descriptor that was opened in a different
479 : // process, so we try to duplicate it in the current process.
480 0 : int pidfd = static_cast<int>(syscall(SYS_pidfd_open, pid, 0));
481 0 : if (pidfd < 0) {
482 0 : JAMI_ERROR("Unable to duplicate PipeWire fd: call to pidfd_open failed (errno = {})", errno);
483 0 : return false;
484 : }
485 0 : fd = static_cast<int>(syscall(SYS_pidfd_getfd, pidfd, fd, 0));
486 0 : if (fd < 0) {
487 0 : JAMI_ERROR("Unable to duplicate PipeWire fd: call to pidfd_getfd failed (errno = {})", errno);
488 0 : return false;
489 : }
490 : #else
491 : JAMI_ERROR("Unable to duplicate PipeWire fd: pidfd_getfd syscall not available");
492 : return false;
493 : #endif
494 : }
495 0 : p.fd = fd;
496 0 : p.node = display.substr(nodePos);
497 0 : } else if (space != std::string::npos) {
498 0 : p.input = display.substr(1, space);
499 0 : if (p.window_id.empty()) {
500 0 : p.input = display.substr(0, space);
501 0 : auto splits = jami::split_string_to_unsigned(display.substr(space + 1), 'x');
502 : // round to 8 pixel block
503 0 : p.width = round2pow(splits[0], 3);
504 0 : p.height = round2pow(splits[1], 3);
505 0 : p.is_area = 1;
506 0 : }
507 : } else {
508 0 : p.input = display;
509 0 : p.width = default_grab_width;
510 0 : p.height = default_grab_height;
511 0 : p.is_area = 1;
512 : }
513 :
514 0 : decOpts_ = p;
515 0 : emulateRate_ = false;
516 :
517 0 : return true;
518 0 : }
519 : #endif
520 :
521 : #ifdef __APPLE__
522 : bool
523 : VideoInput::initAVFoundation(const std::string& display)
524 : {
525 : auto deviceMonitor = jami::getVideoDeviceMonitor();
526 : if (!deviceMonitor)
527 : return false;
528 :
529 : size_t space = display.find(' ');
530 :
531 : clearOptions();
532 : decOpts_.format = "avfoundation";
533 : decOpts_.pixel_format = "nv12";
534 : decOpts_.name = "Capture screen 0";
535 : decOpts_.input = "Capture screen 0";
536 : decOpts_.framerate = deviceMonitor->getDeviceParams(DEVICE_DESKTOP).framerate;
537 :
538 : if (space != std::string::npos) {
539 : std::istringstream iss(display.substr(space + 1));
540 : char sep;
541 : unsigned w, h;
542 : iss >> w >> sep >> h;
543 : decOpts_.width = round2pow(w, 3);
544 : decOpts_.height = round2pow(h, 3);
545 : } else {
546 : decOpts_.width = default_grab_width;
547 : decOpts_.height = default_grab_height;
548 : }
549 : return true;
550 : }
551 : #endif
552 :
553 : #ifdef WIN32
554 : bool
555 : VideoInput::initWindowsGrab(const std::string& display)
556 : {
557 : auto deviceMonitor = jami::getVideoDeviceMonitor();
558 : if (!deviceMonitor)
559 : return false;
560 :
561 : // Patterns
562 : // full screen sharing : :1+0,0 2560x1440 - SCREEN 1, POSITION 0X0, RESOLUTION 2560X1440
563 : // area sharing : :1+882,211 1532x779 - SCREEN 1, POSITION 882x211, RESOLUTION 1532x779
564 : // window sharing : :+1,0 0x0 window-id:HANDLE - POSITION 0X0
565 : size_t space = display.find(' ');
566 : std::string windowIdStr = "window-id:";
567 : size_t winHandlePos = display.find(windowIdStr);
568 :
569 : DeviceParams p = deviceMonitor->getDeviceParams(DEVICE_DESKTOP);
570 : if (winHandlePos != std::string::npos) {
571 : size_t endPos = display.find(' ', winHandlePos + windowIdStr.size());
572 : p.input = display.substr(winHandlePos + windowIdStr.size(),
573 : endPos - (winHandlePos + windowIdStr.size())); // "HANDLE";
574 : p.name = display.substr(winHandlePos + windowIdStr.size(),
575 : endPos - (winHandlePos + windowIdStr.size())); // "HANDLE";
576 : p.is_area = 0;
577 : } else {
578 : p.input = display.substr(1);
579 : p.name = display.substr(1);
580 : p.is_area = 1;
581 : if (space != std::string::npos) {
582 : auto splits = jami::split_string_to_unsigned(display.substr(space + 1), 'x');
583 : if (splits.size() != 2)
584 : return false;
585 :
586 : // round to 8 pixel block
587 : p.width = splits[0];
588 : p.height = splits[1];
589 :
590 : size_t plus = display.find('+');
591 : auto position = display.substr(plus + 1, space - plus - 1);
592 : splits = jami::split_string_to_unsigned(position, ',');
593 : if (splits.size() != 2)
594 : return false;
595 : p.offset_x = splits[0];
596 : p.offset_y = splits[1];
597 : } else {
598 : p.width = default_grab_width;
599 : p.height = default_grab_height;
600 : }
601 : }
602 : std::string fpsStr = "fps:";
603 : if (display.find(fpsStr) != std::string::npos) {
604 : size_t fpsPos = display.find(fpsStr) + fpsStr.size();
605 : int fps = std::stoi(display.substr(fpsPos));
606 : p.framerate = fps;
607 : JAMI_LOG("Custom framerate set to {} fps", fps);
608 : }
609 :
610 : auto dec = std::make_unique<MediaDecoder>();
611 : if (dec->openInput(p) < 0 || dec->setupVideo() < 0)
612 : return initCamera(deviceMonitor->getDefaultDevice());
613 :
614 : clearOptions();
615 : decOpts_ = p;
616 : decOpts_.width = dec->getStream().width;
617 : decOpts_.height = dec->getStream().height;
618 :
619 : return true;
620 : }
621 : #endif
622 :
623 : bool
624 0 : VideoInput::initFile(const std::string& path)
625 : {
626 0 : auto* deviceMonitor = jami::getVideoDeviceMonitor();
627 0 : if (!deviceMonitor)
628 0 : return false;
629 :
630 0 : size_t dot = path.find_last_of('.');
631 0 : std::string ext = dot == std::string::npos ? "" : path.substr(dot + 1);
632 :
633 : /* File exists? */
634 0 : if (access(path.c_str(), R_OK) != 0) {
635 0 : JAMI_ERROR("file '{}' unavailable", path);
636 0 : return false;
637 : }
638 :
639 : // check if file has video, fall back to default device if none
640 : // FIXME the way this is done is hackish, but it is unable to be done in createDecoder
641 : // because that would break the promise returned in switchInput
642 0 : DeviceParams p;
643 0 : p.input = path;
644 0 : p.name = path;
645 0 : auto dec = std::make_unique<MediaDecoder>();
646 0 : if (dec->openInput(p) < 0 || dec->setupVideo() < 0) {
647 0 : return initCamera(deviceMonitor->getDefaultDevice());
648 : }
649 :
650 0 : clearOptions();
651 0 : emulateRate_ = true;
652 0 : decOpts_.input = path;
653 0 : decOpts_.name = path;
654 0 : decOpts_.loop = "1";
655 :
656 : // Force 1fps for static image
657 0 : if (ext == "jpeg" || ext == "jpg" || ext == "png") {
658 0 : decOpts_.format = "image2";
659 0 : decOpts_.framerate = 1;
660 : } else {
661 0 : JAMI_WARNING("Guessing file type for {}", path);
662 : }
663 :
664 0 : return false;
665 0 : }
666 :
667 : void
668 30 : VideoInput::restart()
669 : {
670 30 : if (loop_.isStopping())
671 14 : switchInput(resource_);
672 30 : }
673 :
674 : std::shared_future<DeviceParams>
675 86 : VideoInput::switchInput(const std::string& resource)
676 : {
677 86 : JAMI_LOG("MRL: '{}'", resource);
678 :
679 86 : if (switchPending_.exchange(true)) {
680 0 : JAMI_ERROR("Video switch already requested");
681 0 : return {};
682 : }
683 :
684 86 : resource_ = resource;
685 86 : decOptsFound_ = false;
686 :
687 86 : std::promise<DeviceParams> p;
688 86 : foundDecOpts_.swap(p);
689 :
690 : // Switch off video input?
691 86 : if (resource_.empty()) {
692 62 : clearOptions();
693 62 : futureDecOpts_ = foundDecOpts_.get_future();
694 62 : startLoop();
695 62 : return futureDecOpts_;
696 : }
697 :
698 : // Supported MRL schemes
699 24 : static const std::string sep = libjami::Media::VideoProtocolPrefix::SEPARATOR;
700 :
701 24 : const auto pos = resource_.find(sep);
702 24 : if (pos == std::string::npos)
703 24 : return {};
704 :
705 0 : const auto prefix = resource_.substr(0, pos);
706 0 : if ((pos + sep.size()) >= resource_.size())
707 0 : return {};
708 :
709 0 : const auto suffix = resource_.substr(pos + sep.size());
710 :
711 0 : bool ready = false;
712 :
713 0 : if (prefix == libjami::Media::VideoProtocolPrefix::CAMERA) {
714 : /* Video4Linux2 */
715 0 : ready = initCamera(suffix);
716 0 : } else if (prefix == libjami::Media::VideoProtocolPrefix::DISPLAY) {
717 : /* X11 display name */
718 : #ifdef __APPLE__
719 : ready = initAVFoundation(suffix);
720 : #elif defined(WIN32)
721 : ready = initWindowsGrab(suffix);
722 : #else
723 0 : ready = initLinuxGrab(suffix);
724 : #endif
725 0 : } else if (prefix == libjami::Media::VideoProtocolPrefix::FILE) {
726 : /* Pathname */
727 0 : ready = initFile(suffix);
728 : }
729 :
730 0 : if (ready) {
731 0 : foundDecOpts(decOpts_);
732 : }
733 0 : futureDecOpts_ = foundDecOpts_.get_future().share();
734 0 : startLoop();
735 0 : return futureDecOpts_;
736 0 : }
737 :
738 : MediaStream
739 5 : VideoInput::getInfo() const
740 : {
741 5 : if (!videoManagedByClient()) {
742 5 : if (decoder_)
743 6 : return decoder_->getStream("v:local");
744 : }
745 3 : auto opts = futureDecOpts_.get();
746 3 : rational<int> fr(static_cast<int>(opts.framerate.numerator()), static_cast<int>(opts.framerate.denominator()));
747 : return MediaStream("v:local",
748 3 : av_get_pix_fmt(opts.pixel_format.c_str()),
749 : 1 / fr,
750 3 : static_cast<int>(opts.width),
751 3 : static_cast<int>(opts.height),
752 : 0,
753 6 : fr);
754 3 : }
755 :
756 : void
757 72 : VideoInput::foundDecOpts(const DeviceParams& params)
758 : {
759 72 : if (not decOptsFound_) {
760 72 : decOptsFound_ = true;
761 72 : foundDecOpts_.set_value(params);
762 : }
763 72 : }
764 :
765 : void
766 0 : VideoInput::setSink(const std::string& sinkId)
767 : {
768 0 : sink_ = Manager::instance().createSinkClient(sinkId);
769 0 : }
770 :
771 : void
772 0 : VideoInput::setupSink(const int width, const int height)
773 : {
774 0 : setup();
775 : /* Signal the client about readable sink */
776 0 : sink_->setFrameSize(width, height);
777 0 : }
778 :
779 : void
780 72 : VideoInput::stopSink()
781 : {
782 72 : detach(sink_.get());
783 72 : sink_->stop();
784 72 : }
785 :
786 : void
787 36 : VideoInput::updateStartTime(int64_t startTime)
788 : {
789 36 : if (decoder_) {
790 36 : decoder_->updateStartTime(startTime);
791 : }
792 36 : }
793 :
794 : } // namespace video
795 : } // namespace jami
|