video_input.cpp 12.4 KB
Newer Older
1
/*
2
 *  Copyright (C) 2004-2019 Savoir-faire Linux Inc.
3
 *
4
 *  Author: Tristan Matthews <tristan.matthews@savoirfairelinux.com>
5
 *  Author: Vivien Didelot <vivien.didelot@savoirfairelinux.com>
6
 *  Author: Philippe Gorley <philippe.gorley@savoirfairelinux.com>
7 8 9 10 11 12 13 14 15 16 17 18 19
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
20
 *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA.
21 22
 */

23 24 25 26
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

27
#include "video_input.h"
28

29
#include "media_decoder.h"
30
#include "media_const.h"
31
#include "manager.h"
32
#include "client/videomanager.h"
33
#include "client/ring_signal.h"
34
#include "sinkclient.h"
Tristan Matthews's avatar
Tristan Matthews committed
35
#include "logger.h"
36 37 38
#include "media/media_buffer.h"

#include <libavformat/avio.h>
39

40
#include <string>
41
#include <sstream>
42
#include <cassert>
43
#ifdef _MSC_VER
44 45
#include <io.h> // for access
#else
46
#include <unistd.h>
47
#endif
Adrien Béraud's avatar
Adrien Béraud committed
48 49 50
extern "C" {
#include <libavutil/display.h>
}
51

Adrien Béraud's avatar
Adrien Béraud committed
52
namespace jami { namespace video {
53

54 55 56
static constexpr unsigned default_grab_width = 640;
static constexpr unsigned default_grab_height = 480;

57 58
VideoInput::VideoInput()
    : VideoGenerator::VideoGenerator()
59
#if !VIDEO_CLIENT_INPUT
60
    , sink_ {Manager::instance().createSinkClient("local")}
61 62 63
    , loop_(std::bind(&VideoInput::setup, this),
            std::bind(&VideoInput::process, this),
            std::bind(&VideoInput::cleanup, this))
64
#endif
65
{}
66

67
VideoInput::~VideoInput()
68
{
69 70 71 72
#if VIDEO_CLIENT_INPUT
    emitSignal<DRing::VideoSignal::StopCapture>();
    capturing_ = false;
#else
73
    loop_.join();
74
#endif
75 76
}

77 78
void
VideoInput::startLoop()
79
{
80 81 82 83 84 85
#if VIDEO_CLIENT_INPUT
    switchDevice();
#else
    if (!loop_.isRunning())
        loop_.start();
#endif
86 87
}

88 89 90
#if VIDEO_CLIENT_INPUT
void
VideoInput::switchDevice()
91 92
{
    if (switchPending_.exchange(false)) {
Adrien Béraud's avatar
Adrien Béraud committed
93
        JAMI_DBG("Switching input to '%s'", decOpts_.input.c_str());
94
        if (decOpts_.input.empty()) {
95
            capturing_ = false;
96 97 98 99 100
            return;
        }

        emitSignal<DRing::VideoSignal::StopCapture>();
        emitSignal<DRing::VideoSignal::StartCapture>(decOpts_.input);
101
        capturing_ = true;
102
    }
103
}
104

105 106
int VideoInput::getWidth() const
{ return decOpts_.width; }
107

108 109
int VideoInput::getHeight() const
{ return decOpts_.height; }
Adrien Béraud's avatar
Adrien Béraud committed
110

111 112 113 114 115 116 117
AVPixelFormat VideoInput::getPixelFormat() const
{
    int format;
    std::stringstream ss;
    ss << decOpts_.format;
    ss >> format;
    return (AVPixelFormat)format;
118 119
}

120 121
#else

Adrien Béraud's avatar
Adrien Béraud committed
122 123 124
void
VideoInput::setRotation(int angle)
{
125 126 127 128 129 130 131 132
    std::shared_ptr<AVBufferRef> displayMatrix {
        av_buffer_alloc(sizeof(int32_t) * 9),
        [](AVBufferRef* buf){ av_buffer_unref(&buf); }
    };
    if (displayMatrix) {
        av_display_rotation_set(reinterpret_cast<int32_t*>(displayMatrix->data), angle);
        displayMatrix_ = std::move(displayMatrix);
    }
Adrien Béraud's avatar
Adrien Béraud committed
133 134
}

135
bool VideoInput::setup()
136
{
137 138 139 140
    if (not attach(sink_.get())) {
        JAMI_ERR("attach sink failed");
        return false;
    }
141

142 143
    if (!sink_->start())
        JAMI_ERR("start sink failed");
144

145
    JAMI_DBG("VideoInput ready to capture");
Adrien Béraud's avatar
Adrien Béraud committed
146

147
    return true;
148
}
149

150 151
void
VideoInput::process()
152
{
153
    if (switchPending_)
154 155
        createDecoder();

156
    if (not captureFrame()) {
157 158 159
        loop_.stop();
        return;
    }
160 161
}

162 163
void
VideoInput::cleanup()
164
{
165 166 167
    deleteDecoder(); // do it first to let a chance to last frame to be displayed
    detach(sink_.get());
    sink_->stop();
Adrien Béraud's avatar
Adrien Béraud committed
168
    JAMI_DBG("VideoInput closed");
169 170
}

171
bool
172
VideoInput::captureFrame()
173
{
174 175 176 177
    // Return true if capture could continue, false if must be stop
    if (not decoder_)
        return false;

178 179 180 181 182 183 184 185 186
    switch (decoder_->decode()) {
    case MediaDemuxer::Status::EndOfFile:
        createDecoder();
        return static_cast<bool>(decoder_);
    case MediaDemuxer::Status::ReadError:
        JAMI_ERR() << "Failed to decode frame";
        return false;
    default:
        return true;
187
    }
188 189 190 191 192
}

void
VideoInput::createDecoder()
{
193 194 195 196
    deleteDecoder();

    switchPending_ = false;

197
    if (decOpts_.input.empty()) {
198
        foundDecOpts(decOpts_);
199
        return;
200
    }
201

202 203 204
    auto decoder = std::make_unique<MediaDecoder>([this](const std::shared_ptr<MediaFrame>& frame) mutable {
        publishFrame(std::static_pointer_cast<VideoFrame>(frame));
    });
205

206
    if (emulateRate_)
207
        decoder->emulateRate();
208

209 210 211
    decoder->setInterruptCallback(
        [](void* data) -> int { return not static_cast<VideoInput*>(data)->isCapturing(); },
        this);
212

213
    if (decoder->openInput(decOpts_) < 0) {
Adrien Béraud's avatar
Adrien Béraud committed
214
        JAMI_ERR("Could not open input \"%s\"", decOpts_.input.c_str());
215
        foundDecOpts(decOpts_);
216 217
        return;
    }
218 219

    /* Data available, finish the decoding */
220
    if (decoder->setupVideo() < 0) {
Adrien Béraud's avatar
Adrien Béraud committed
221
        JAMI_ERR("decoder IO startup failed");
222
        foundDecOpts(decOpts_);
223 224
        return;
    }
225

226 227
    decoder->decode(); // Populate AVCodecContext fields

228 229 230
    decOpts_.width = decoder->getWidth();
    decOpts_.height = decoder->getHeight();
    decOpts_.framerate = decoder->getFps();
231 232 233 234 235 236 237
    AVPixelFormat fmt = decoder->getPixelFormat();
    if (fmt != AV_PIX_FMT_NONE) {
        decOpts_.pixel_format = av_get_pix_fmt_name(fmt);
    } else {
        JAMI_WARN("Could not determine pixel format, using default");
        decOpts_.pixel_format = av_get_pix_fmt_name(AV_PIX_FMT_YUV420P);
    }
238

239 240 241
    JAMI_DBG("created decoder with video params : size=%dX%d, fps=%lf pix=%s",
             decOpts_.width, decOpts_.height, decOpts_.framerate.real(),
             decOpts_.pixel_format.c_str());
242 243

    decoder_ = std::move(decoder);
244
    foundDecOpts(decOpts_);
245 246 247

    /* Signal the client about readable sink */
    sink_->setFrameSize(decoder_->getWidth(), decoder_->getHeight());
248 249
}

250 251
void
VideoInput::deleteDecoder()
252
{
253 254
    if (not decoder_)
        return;
255
    flushFrames();
256
    decoder_.reset();
257 258
}

259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285
int VideoInput::getWidth() const
{ return decoder_->getWidth(); }

int VideoInput::getHeight() const
{ return decoder_->getHeight(); }

AVPixelFormat VideoInput::getPixelFormat() const
{ return decoder_->getPixelFormat(); }

#endif

void VideoInput::clearOptions()
{
    decOpts_ = {};
    emulateRate_ = false;
}

bool
VideoInput::isCapturing() const noexcept
{
#if VIDEO_CLIENT_INPUT
    return capturing_;
#else
    return loop_.isRunning();
#endif
}

286 287
bool
VideoInput::initCamera(const std::string& device)
288
{
Adrien Béraud's avatar
Adrien Béraud committed
289
    decOpts_ = jami::getVideoDeviceMonitor().getDeviceParams(device);
290
    return true;
291 292
}

293 294 295 296 297 298
static constexpr unsigned
round2pow(unsigned i, unsigned n)
{
    return (i >> n) << n;
}

299 300
bool
VideoInput::initX11(std::string display)
301
{
302
    size_t space = display.find(' ');
303

304
    clearOptions();
Guillaume Roguez's avatar
Guillaume Roguez committed
305 306
    decOpts_.format = "x11grab";
    decOpts_.framerate = 25;
307

308
    if (space != std::string::npos) {
309 310
        std::istringstream iss(display.substr(space + 1));
        char sep;
311 312 313 314 315
        unsigned w, h;
        iss >> w >> sep >> h;
        // round to 8 pixel block
        decOpts_.width = round2pow(w, 3);
        decOpts_.height = round2pow(h, 3);
Guillaume Roguez's avatar
Guillaume Roguez committed
316
        decOpts_.input = display.erase(space);
317
    } else {
Guillaume Roguez's avatar
Guillaume Roguez committed
318
        decOpts_.input = display;
319
        //decOpts_.video_size = "vga";
320 321 322 323 324 325 326
        decOpts_.width = default_grab_width;
        decOpts_.height = default_grab_height;
    }

    return true;
}

327 328 329 330 331 332 333
bool
VideoInput::initAVFoundation(const std::string& display)
{
    size_t space = display.find(' ');

    clearOptions();
    decOpts_.format = "avfoundation";
334
    decOpts_.pixel_format = "nv12";
335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351
    decOpts_.input = "Capture screen 0";
    decOpts_.framerate = 30;

    if (space != std::string::npos) {
        std::istringstream iss(display.substr(space + 1));
        char sep;
        unsigned w, h;
        iss >> w >> sep >> h;
        decOpts_.width = round2pow(w, 3);
        decOpts_.height = round2pow(h, 3);
    } else {
        decOpts_.width = default_grab_width;
        decOpts_.height = default_grab_height;
    }
    return true;
}

352
bool
353
VideoInput::initGdiGrab(const std::string& params)
354 355 356 357 358 359 360 361 362 363 364 365 366 367
{
    size_t space = params.find(' ');
    clearOptions();
    decOpts_.format = "gdigrab";
    decOpts_.input = "desktop";
    decOpts_.framerate = 30;

    if (space != std::string::npos) {
        std::istringstream iss(params.substr(space + 1));
        char sep;
        unsigned w, h;
        iss >> w >> sep >> h;
        decOpts_.width = round2pow(w, 3);
        decOpts_.height = round2pow(h, 3);
368 369 370 371

        size_t plus = params.find('+');
        std::istringstream dss(params.substr(plus + 1, space - plus));
        dss >> decOpts_.offset_x >> sep >> decOpts_.offset_y;
372 373 374
    } else {
        decOpts_.width = default_grab_width;
        decOpts_.height = default_grab_height;
375 376 377 378 379 380 381 382 383 384 385 386 387
    }

    return true;
}

bool
VideoInput::initFile(std::string path)
{
    size_t dot = path.find_last_of('.');
    std::string ext = dot == std::string::npos ? "" : path.substr(dot + 1);

    /* File exists? */
    if (access(path.c_str(), R_OK) != 0) {
Adrien Béraud's avatar
Adrien Béraud committed
388
        JAMI_ERR("file '%s' unavailable\n", path.c_str());
389 390 391
        return false;
    }

392 393 394 395 396 397
    // check if file has video, fall back to default device if none
    // FIXME the way this is done is hackish, but it can't be done in createDecoder because that
    // would break the promise returned in switchInput
    DeviceParams p;
    p.input = path;
    auto dec = std::make_unique<MediaDecoder>();
398
    if (dec->openInput(p) < 0 || dec->setupVideo() < 0) {
Adrien Béraud's avatar
Adrien Béraud committed
399
        return initCamera(jami::getVideoDeviceMonitor().getDefaultDevice());
400 401
    }

402 403
    clearOptions();
    emulateRate_ = true;
Guillaume Roguez's avatar
Guillaume Roguez committed
404 405
    decOpts_.input = path;
    decOpts_.loop = "1";
406

407 408
    // Force 1fps for static image
    if (ext == "jpeg" || ext == "jpg" || ext == "png") {
Guillaume Roguez's avatar
Guillaume Roguez committed
409 410
        decOpts_.format = "image2";
        decOpts_.framerate = 1;
411
    } else {
Adrien Béraud's avatar
Adrien Béraud committed
412
        JAMI_WARN("Guessing file type for %s", path.c_str());
413 414
    }

415
    return false;
416 417
}

418
std::shared_future<DeviceParams>
419 420
VideoInput::switchInput(const std::string& resource)
{
421 422 423
    if (resource == currentResource_)
        return futureDecOpts_;

Adrien Béraud's avatar
Adrien Béraud committed
424
    JAMI_DBG("MRL: '%s'", resource.c_str());
425

426
    if (switchPending_.exchange(true)) {
Adrien Béraud's avatar
Adrien Béraud committed
427
        JAMI_ERR("Video switch already requested");
428
        return {};
429 430
    }

431
    currentResource_ = resource;
432
    decOptsFound_ = false;
433

434 435 436
    std::promise<DeviceParams> p;
    foundDecOpts_.swap(p);

437
    // Switch off video input?
438
    if (resource.empty()) {
439
        clearOptions();
440 441
        futureDecOpts_  = foundDecOpts_.get_future();
        startLoop();
442
        return futureDecOpts_;
443 444 445
    }

    // Supported MRL schemes
446
    static const std::string sep = DRing::Media::VideoProtocolPrefix::SEPARATOR;
447

448 449
    const auto pos = resource.find(sep);
    if (pos == std::string::npos)
450
        return {};
451 452 453

    const auto prefix = resource.substr(0, pos);
    if ((pos + sep.size()) >= resource.size())
454
        return {};
455

456
    const auto suffix = resource.substr(pos + sep.size());
457

458
    bool ready = false;
459

460
    if (prefix == DRing::Media::VideoProtocolPrefix::CAMERA) {
461
        /* Video4Linux2 */
462
        ready = initCamera(suffix);
463
    } else if (prefix == DRing::Media::VideoProtocolPrefix::DISPLAY) {
464
        /* X11 display name */
465 466 467
#ifdef __APPLE__
        ready = initAVFoundation(suffix);
#elif defined(_WIN32)
468
        ready = initGdiGrab(suffix);
469 470
#else
        ready = initX11(suffix);
471
#endif
472
    } else if (prefix == DRing::Media::VideoProtocolPrefix::FILE) {
473
        /* Pathname */
474
        ready = initFile(suffix);
475
    }
476

477 478
    if (ready) {
        foundDecOpts(decOpts_);
479 480
    }
    futureDecOpts_ = foundDecOpts_.get_future().share();
481
    startLoop();
482
    return futureDecOpts_;
483 484
}

485 486 487
DeviceParams VideoInput::getParams() const
{ return decOpts_; }

488
MediaStream
489
VideoInput::getInfo() const
490
{
491
#if !VIDEO_CLIENT_INPUT
492 493
    if (decoder_)
        return decoder_->getStream("v:local");
494
#endif
495 496 497 498
    auto opts = futureDecOpts_.get();
    rational<int> fr(opts.framerate.numerator(), opts.framerate.denominator());
    return MediaStream("v:local", av_get_pix_fmt(opts.pixel_format.c_str()),
        1 / fr, opts.width, opts.height, 0, fr);
499 500
}

501 502 503 504 505 506 507 508 509
void
VideoInput::foundDecOpts(const DeviceParams& params)
{
    if (not decOptsFound_) {
        decOptsFound_ = true;
        foundDecOpts_.set_value(params);
    }
}

Adrien Béraud's avatar
Adrien Béraud committed
510
}} // namespace jami::video