VideoReceiver.cc 35.3 KB
Newer Older
1 2
/****************************************************************************
 *
3
 *   (c) 2009-2019 QGROUNDCONTROL PROJECT <http://www.qgroundcontrol.org>
4 5 6 7 8
 *
 * QGroundControl is licensed according to the terms in the file
 * COPYING.md in the root of the source code directory.
 *
 ****************************************************************************/
Gus Grubba's avatar
Gus Grubba committed
9 10 11 12 13 14 15 16 17


/**
 * @file
 *   @brief QGC Video Receiver
 *   @author Gus Grubba <mavlink@grubba.com>
 */

#include "VideoReceiver.h"
18 19
#include "SettingsManager.h"
#include "QGCApplication.h"
20
#include "VideoManager.h"
21 22 23
#ifdef QGC_GST_TAISYNC_ENABLED
#include "TaisyncHandler.h"
#endif
Gus Grubba's avatar
Gus Grubba committed
24
#include <QDebug>
25
#include <QUrl>
26 27
#include <QDir>
#include <QDateTime>
28
#include <QSysInfo>
29

30 31
QGC_LOGGING_CATEGORY(VideoReceiverLog, "VideoReceiverLog")

32 33
#if defined(QGC_GST_STREAMING)

34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
static const char* kVideoExtensions[] =
{
    "mkv",
    "mov",
    "mp4"
};

static const char* kVideoMuxes[] =
{
    "matroskamux",
    "qtmux",
    "mp4mux"
};

#define NUM_MUXES (sizeof(kVideoMuxes) / sizeof(char*))

50 51 52
#endif


Gus Grubba's avatar
Gus Grubba committed
53 54
VideoReceiver::VideoReceiver(QObject* parent)
    : QObject(parent)
55
#if defined(QGC_GST_STREAMING)
56
    , _running(false)
57
    , _recording(false)
58
    , _streaming(false)
59 60
    , _starting(false)
    , _stopping(false)
61
    , _stop(true)
Gus Grubba's avatar
Gus Grubba committed
62 63 64 65 66
    , _sink(nullptr)
    , _tee(nullptr)
    , _pipeline(nullptr)
    , _pipelineStopRec(nullptr)
    , _videoSink(nullptr)
67
    , _restart_time_ms(1389)
68 69 70
    , _socket(nullptr)
    , _serverPresent(false)
    , _tcpTestInterval_ms(5000)
71
    , _udpReconnect_us(5000000)
72
#endif
Gus Grubba's avatar
Gus Grubba committed
73
    , _videoSurface(nullptr)
74 75
    , _videoRunning(false)
    , _showFullScreen(false)
Gus Grubba's avatar
Gus Grubba committed
76
    , _videoSettings(nullptr)
77 78
    , _hwDecoderName(nullptr)
    , _swDecoderName("avdec_h264")
Gus Grubba's avatar
Gus Grubba committed
79
{
80
    _videoSurface = new VideoSurface;
Gus Grubba's avatar
Gus Grubba committed
81
    _videoSettings = qgcApp()->toolbox()->settingsManager()->videoSettings();
82
#if defined(QGC_GST_STREAMING)
83
    setVideoDecoder(H264_SW);
84
    _setVideoSink(_videoSurface->videoSink());
85 86
    _restart_timer.setSingleShot(true);
    connect(&_restart_timer, &QTimer::timeout, this, &VideoReceiver::_restart_timeout);
87 88
    _tcp_timer.setSingleShot(true);
    connect(&_tcp_timer, &QTimer::timeout, this, &VideoReceiver::_tcp_timeout);
89 90 91
    connect(this, &VideoReceiver::msgErrorReceived, this, &VideoReceiver::_handleError);
    connect(this, &VideoReceiver::msgEOSReceived, this, &VideoReceiver::_handleEOS);
    connect(this, &VideoReceiver::msgStateChangedReceived, this, &VideoReceiver::_handleStateChanged);
92 93
    connect(&_frameTimer, &QTimer::timeout, this, &VideoReceiver::_updateTimer);
    _frameTimer.start(1000);
94
#endif
Gus Grubba's avatar
Gus Grubba committed
95 96 97 98
}

VideoReceiver::~VideoReceiver()
{
99
#if defined(QGC_GST_STREAMING)
100
    stop();
101 102 103 104
    if(_socket) {
        delete _socket;
        _socket = nullptr;
    }
105 106 107
    if (_videoSink) {
        gst_object_unref(_videoSink);
    }
108
#endif
109 110
    if(_videoSurface)
        delete _videoSurface;
Gus Grubba's avatar
Gus Grubba committed
111 112
}

113
#if defined(QGC_GST_STREAMING)
114 115
void
VideoReceiver::_setVideoSink(GstElement* sink)
Gus Grubba's avatar
Gus Grubba committed
116 117 118
{
    if (_videoSink) {
        gst_object_unref(_videoSink);
Gus Grubba's avatar
Gus Grubba committed
119
        _videoSink = nullptr;
Gus Grubba's avatar
Gus Grubba committed
120 121 122 123 124 125
    }
    if (sink) {
        _videoSink = sink;
        gst_object_ref_sink(_videoSink);
    }
}
126
#endif
Gus Grubba's avatar
Gus Grubba committed
127

128 129 130 131 132 133 134 135 136
//-----------------------------------------------------------------------------
void
VideoReceiver::grabImage(QString imageFile)
{
    _imageFile = imageFile;
    emit imageFileChanged();
}

//-----------------------------------------------------------------------------
137
#if defined(QGC_GST_STREAMING)
138 139
static void
newPadCB(GstElement* element, GstPad* pad, gpointer data)
140
{
141
    gchar* name = gst_pad_get_name(pad);
142
    //g_print("A new pad %s was created\n", name);
143 144
    GstCaps* p_caps = gst_pad_get_pad_template_caps (pad);
    gchar* description = gst_caps_to_string(p_caps);
145
    qCDebug(VideoReceiverLog) << p_caps << ", " << description;
146
    g_free(description);
147 148
    GstElement* sink = GST_ELEMENT(data);
    if(gst_element_link_pads(element, name, sink, "sink") == false)
149 150 151 152
        qCritical() << "newPadCB : failed to link elements\n";
    g_free(name);
}

153 154
//-----------------------------------------------------------------------------
void
155
VideoReceiver::_restart_timeout()
156
{
157
    qgcApp()->toolbox()->videoManager()->restartVideo();
158
}
159
#endif
160

161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223
//-----------------------------------------------------------------------------
#if defined(QGC_GST_STREAMING)
void
VideoReceiver::_tcp_timeout()
{
    //-- If socket is live, we got no connection nor a socket error
    if(_socket) {
        delete _socket;
        _socket = nullptr;
    }
    if(_videoSettings->streamEnabled()->rawValue().toBool()) {
        //-- RTSP will try to connect to the server. If it cannot connect,
        //   it will simply give up and never try again. Instead, we keep
        //   attempting a connection on this timer. Once a connection is
        //   found to be working, only then we actually start the stream.
        QUrl url(_uri);
        //-- If RTSP and no port is defined, set default RTSP port (554)
        if(_uri.contains("rtsp://") && url.port() <= 0) {
            url.setPort(554);
        }
        _socket = new QTcpSocket;
        QNetworkProxy tempProxy;
        tempProxy.setType(QNetworkProxy::DefaultProxy);
        _socket->setProxy(tempProxy);
        connect(_socket, static_cast<void (QTcpSocket::*)(QAbstractSocket::SocketError)>(&QTcpSocket::error), this, &VideoReceiver::_socketError);
        connect(_socket, &QTcpSocket::connected, this, &VideoReceiver::_connected);
        _socket->connectToHost(url.host(), static_cast<uint16_t>(url.port()));
        _tcp_timer.start(_tcpTestInterval_ms);
    }
}
#endif

//-----------------------------------------------------------------------------
#if defined(QGC_GST_STREAMING)
void
VideoReceiver::_connected()
{
    //-- Server showed up. Now we start the stream.
    _tcp_timer.stop();
    _socket->deleteLater();
    _socket = nullptr;
    if(_videoSettings->streamEnabled()->rawValue().toBool()) {
        _serverPresent = true;
        start();
    }
}
#endif

//-----------------------------------------------------------------------------
#if defined(QGC_GST_STREAMING)
void
VideoReceiver::_socketError(QAbstractSocket::SocketError socketError)
{
    Q_UNUSED(socketError);
    _socket->deleteLater();
    _socket = nullptr;
    //-- Try again in a while
    if(_videoSettings->streamEnabled()->rawValue().toBool()) {
        _tcp_timer.start(_tcpTestInterval_ms);
    }
}
#endif

224
//-----------------------------------------------------------------------------
225 226 227 228 229 230 231 232
// When we finish our pipeline will look like this:
//
//                                   +-->queue-->decoder-->_videosink
//                                   |
//    datasource-->demux-->parser-->tee
//                                   ^
//                                   |
//                                   +-Here we will later link elements for recording
233 234
void
VideoReceiver::start()
Gus Grubba's avatar
Gus Grubba committed
235
{
236 237 238 239
    if (_uri.isEmpty()) {
        return;
    }
    qCDebug(VideoReceiverLog) << "start():" << _uri;
Gus Grubba's avatar
Gus Grubba committed
240
    if(qgcApp()->runningUnitTests()) {
241 242
        return;
    }
243 244
    if(!_videoSettings->streamEnabled()->rawValue().toBool() ||
       !_videoSettings->streamConfigured()) {
245 246 247
        qCDebug(VideoReceiverLog) << "start() but not enabled/configured";
        return;
    }
248

249
#if defined(QGC_GST_STREAMING)
250
    _stop = false;
251

252
#if defined(QGC_GST_TAISYNC_ENABLED) && (defined(__android__) || defined(__ios__))
253
    //-- Taisync on iOS or Android sends a raw h.264 stream
254 255 256 257
    bool isTaisyncUSB = qgcApp()->toolbox()->videoManager()->isTaisync();
#else
    bool isTaisyncUSB = false;
#endif
Gus Grubba's avatar
Gus Grubba committed
258
    bool isUdp264   = _uri.contains("udp://")  && !isTaisyncUSB;
259
    bool isRtsp     = _uri.contains("rtsp://") && !isTaisyncUSB;
Gus Grubba's avatar
Gus Grubba committed
260
    bool isUdp265   = _uri.contains("udp265://")  && !isTaisyncUSB;
261 262
    bool isTCP      = _uri.contains("tcp://")  && !isTaisyncUSB;
    bool isMPEGTS   = _uri.contains("mpegts://")  && !isTaisyncUSB;
263 264

    if (!isTaisyncUSB && _uri.isEmpty()) {
Gus Grubba's avatar
Gus Grubba committed
265 266 267
        qCritical() << "VideoReceiver::start() failed because URI is not specified";
        return;
    }
Gus Grubba's avatar
Gus Grubba committed
268
    if (_videoSink == nullptr) {
Gus Grubba's avatar
Gus Grubba committed
269 270 271
        qCritical() << "VideoReceiver::start() failed because video sink is not set";
        return;
    }
272 273 274 275
    if(_running) {
        qCDebug(VideoReceiverLog) << "Already running!";
        return;
    }
276 277 278 279 280
    if (isUdp264) {
        setVideoDecoder(H264_HW);
    } else if (isUdp265) {
        setVideoDecoder(H265_HW);
    }
Gus Grubba's avatar
Gus Grubba committed
281

282
    _starting = true;
283

284 285 286 287 288 289
    //-- For RTSP and TCP, check to see if server is there first
    if(!_serverPresent && (isRtsp || isTCP)) {
        _tcp_timer.start(100);
        return;
    }

290
    bool running    = false;
291
    bool pipelineUp = false;
Gus Grubba's avatar
Gus Grubba committed
292

Gus Grubba's avatar
Gus Grubba committed
293 294 295 296 297 298 299
    GstElement*     dataSource  = nullptr;
    GstCaps*        caps        = nullptr;
    GstElement*     demux       = nullptr;
    GstElement*     parser      = nullptr;
    GstElement*     queue       = nullptr;
    GstElement*     decoder     = nullptr;
    GstElement*     queue1      = nullptr;
300

Gus Grubba's avatar
Gus Grubba committed
301
    do {
Gus Grubba's avatar
Gus Grubba committed
302
        if ((_pipeline = gst_pipeline_new("receiver")) == nullptr) {
303
            qCritical() << "VideoReceiver::start() failed. Error with gst_pipeline_new()";
Gus Grubba's avatar
Gus Grubba committed
304 305 306
            break;
        }

Gus Grubba's avatar
Gus Grubba committed
307
        if(isUdp264 || isUdp265 || isMPEGTS || isTaisyncUSB) {
308
            dataSource = gst_element_factory_make("udpsrc", "udp-source");
309 310
        } else if(isTCP) {
            dataSource = gst_element_factory_make("tcpclientsrc", "tcpclient-source");
311 312
        } else {
            dataSource = gst_element_factory_make("rtspsrc", "rtsp-source");
Gus Grubba's avatar
Gus Grubba committed
313 314
        }

315 316
        if (!dataSource) {
            qCritical() << "VideoReceiver::start() failed. Error with data source for gst_element_factory_make()";
Gus Grubba's avatar
Gus Grubba committed
317 318 319
            break;
        }

Gus Grubba's avatar
Gus Grubba committed
320
        if(isUdp264) {
Gus Grubba's avatar
Gus Grubba committed
321
            if ((caps = gst_caps_from_string("application/x-rtp, media=(string)video, clock-rate=(int)90000, encoding-name=(string)H264")) == nullptr) {
322 323 324
                qCritical() << "VideoReceiver::start() failed. Error with gst_caps_from_string()";
                break;
            }
325
            g_object_set(static_cast<gpointer>(dataSource), "uri", qPrintable(_uri), "caps", caps, nullptr);
Gus Grubba's avatar
Gus Grubba committed
326
        } else if(isUdp265) {
327 328 329 330 331
            if ((caps = gst_caps_from_string("application/x-rtp, media=(string)video, clock-rate=(int)90000, encoding-name=(string)H265")) == nullptr) {
                qCritical() << "VideoReceiver::start() failed. Error with gst_caps_from_string()";
                break;
            }
            g_object_set(static_cast<gpointer>(dataSource), "uri", qPrintable(_uri.replace("udp265", "udp")), "caps", caps, nullptr);
332
#if  defined(QGC_GST_TAISYNC_ENABLED) && (defined(__android__) || defined(__ios__))
333 334 335 336 337
        } else if(isTaisyncUSB) {
            QString uri = QString("0.0.0.0:%1").arg(TAISYNC_VIDEO_UDP_PORT);
            qCDebug(VideoReceiverLog) << "Taisync URI:" << uri;
            g_object_set(static_cast<gpointer>(dataSource), "port", TAISYNC_VIDEO_UDP_PORT, nullptr);
#endif
338 339
        } else if(isTCP) {
            QUrl url(_uri);
340
            g_object_set(static_cast<gpointer>(dataSource), "host", qPrintable(url.host()), "port", url.port(), nullptr );
341 342 343
        } else if(isMPEGTS) {
            QUrl url(_uri);
            g_object_set(static_cast<gpointer>(dataSource), "port", url.port(), nullptr);
344
        } else {
345
            g_object_set(static_cast<gpointer>(dataSource), "location", qPrintable(_uri), "latency", 17, "udp-reconnect", 1, "timeout", _udpReconnect_us, NULL);
346
        }
Gus Grubba's avatar
Gus Grubba committed
347

348 349
        if (isTCP || isMPEGTS) {
            if ((demux = gst_element_factory_make("tsdemux", "mpeg-ts-demuxer")) == nullptr) {
350 351 352 353
                qCritical() << "VideoReceiver::start() failed. Error with gst_element_factory_make('tsdemux')";
                break;
            }
        } else {
354
            if(!isTaisyncUSB) {
355 356
                if ((demux = gst_element_factory_make(_depayName, "rtp-depacketizer")) == nullptr) {
                   qCritical() << "VideoReceiver::start() failed. Error with gst_element_factory_make('" << _depayName << "')";
357 358
                    break;
                }
359
            }
360 361
        }

362 363
        if ((parser = gst_element_factory_make(_parserName, "parser")) == nullptr) {
            qCritical() << "VideoReceiver::start() failed. Error with gst_element_factory_make('" << _parserName << "')";
Gus Grubba's avatar
Gus Grubba committed
364 365 366
            break;
        }

Gus Grubba's avatar
Gus Grubba committed
367
        if((_tee = gst_element_factory_make("tee", nullptr)) == nullptr)  {
368 369 370
            qCritical() << "VideoReceiver::start() failed. Error with gst_element_factory_make('tee')";
            break;
        }
Gus Grubba's avatar
Gus Grubba committed
371

Gus Grubba's avatar
Gus Grubba committed
372
        if((queue = gst_element_factory_make("queue", nullptr)) == nullptr)  {
373 374
            // TODO: We may want to add queue2 max-size-buffers=1 to get lower latency
            //       We should compare gstreamer scripts to QGroundControl to determine the need
375
            qCritical() << "VideoReceiver::start() failed. Error with gst_element_factory_make('queue')";
376 377
            break;
        }
378

379 380 381 382 383 384
        if (!_hwDecoderName || (decoder = gst_element_factory_make(_hwDecoderName, "decoder")) == nullptr) {
            qWarning() << "VideoReceiver::start() hardware decoding not available " << ((_hwDecoderName) ? _hwDecoderName : "");
            if ((decoder = gst_element_factory_make(_swDecoderName, "decoder")) == nullptr) {
                qCritical() << "VideoReceiver::start() failed. Error with gst_element_factory_make('" << _swDecoderName << "')";
                break;
            }
385 386
        }

Gus Grubba's avatar
Gus Grubba committed
387
        if ((queue1 = gst_element_factory_make("queue", nullptr)) == nullptr) {
388 389 390 391
            qCritical() << "VideoReceiver::start() failed. Error with gst_element_factory_make('queue') [1]";
            break;
        }

392 393 394 395 396
        if(isTaisyncUSB) {
            gst_bin_add_many(GST_BIN(_pipeline), dataSource, parser, _tee, queue, decoder, queue1, _videoSink, nullptr);
        } else {
            gst_bin_add_many(GST_BIN(_pipeline), dataSource, demux, parser, _tee, queue, decoder, queue1, _videoSink, nullptr);
        }
397
        pipelineUp = true;
398

Gus Grubba's avatar
Gus Grubba committed
399
        if(isUdp264 || isUdp265) {
400
            // Link the pipeline in front of the tee
Gus Grubba's avatar
Gus Grubba committed
401
            if(!gst_element_link_many(dataSource, demux, parser, _tee, queue, decoder, queue1, _videoSink, nullptr)) {
402 403 404
                qCritical() << "Unable to link UDP elements.";
                break;
            }
405 406 407 408 409 410
        } else if(isTaisyncUSB) {
            // Link the pipeline in front of the tee
            if(!gst_element_link_many(dataSource, parser, _tee, queue, decoder, queue1, _videoSink, nullptr)) {
                qCritical() << "Unable to link Taisync USB elements.";
                break;
            }
411
        } else if (isTCP || isMPEGTS) {
412
            if(!gst_element_link(dataSource, demux)) {
413
                qCritical() << "Unable to link TCP/MPEG-TS dataSource to Demux.";
414 415
                break;
            }
Gus Grubba's avatar
Gus Grubba committed
416
            if(!gst_element_link_many(parser, _tee, queue, decoder, queue1, _videoSink, nullptr)) {
417
                qCritical() << "Unable to link TCP/MPEG-TS pipline to parser.";
418 419
                break;
            }
420
            g_signal_connect(demux, "pad-added", G_CALLBACK(newPadCB), parser);
421
        } else {
422
            g_signal_connect(dataSource, "pad-added", G_CALLBACK(newPadCB), demux);
Gus Grubba's avatar
Gus Grubba committed
423
            if(!gst_element_link_many(demux, parser, _tee, queue, decoder, _videoSink, nullptr)) {
424
                qCritical() << "Unable to link RTSP elements.";
425 426
                break;
            }
427 428
        }

Gus Grubba's avatar
Gus Grubba committed
429
        dataSource = demux = parser = queue = decoder = queue1 = nullptr;
Gus Grubba's avatar
Gus Grubba committed
430

Gus Grubba's avatar
Gus Grubba committed
431
        GstBus* bus = nullptr;
Gus Grubba's avatar
Gus Grubba committed
432

Gus Grubba's avatar
Gus Grubba committed
433
        if ((bus = gst_pipeline_get_bus(GST_PIPELINE(_pipeline))) != nullptr) {
434 435 436
            gst_bus_enable_sync_message_emission(bus);
            g_signal_connect(bus, "sync-message", G_CALLBACK(_onBusMessage), this);
            gst_object_unref(bus);
Gus Grubba's avatar
Gus Grubba committed
437
            bus = nullptr;
438
        }
Gus Grubba's avatar
Gus Grubba committed
439

440
        GST_DEBUG_BIN_TO_DOT_FILE(GST_BIN(_pipeline), GST_DEBUG_GRAPH_SHOW_ALL, "pipeline-paused");
Gus Grubba's avatar
Gus Grubba committed
441 442 443 444
        running = gst_element_set_state(_pipeline, GST_STATE_PLAYING) != GST_STATE_CHANGE_FAILURE;

    } while(0);

Gus Grubba's avatar
Gus Grubba committed
445
    if (caps != nullptr) {
Gus Grubba's avatar
Gus Grubba committed
446
        gst_caps_unref(caps);
Gus Grubba's avatar
Gus Grubba committed
447
        caps = nullptr;
Gus Grubba's avatar
Gus Grubba committed
448 449 450 451 452
    }

    if (!running) {
        qCritical() << "VideoReceiver::start() failed";

453
        // In newer versions, the pipeline will clean up all references that are added to it
Gus Grubba's avatar
Gus Grubba committed
454
        if (_pipeline != nullptr) {
455
            gst_object_unref(_pipeline);
Gus Grubba's avatar
Gus Grubba committed
456
            _pipeline = nullptr;
Gus Grubba's avatar
Gus Grubba committed
457 458
        }

459 460
        // If we failed before adding items to the pipeline, then clean up
        if (!pipelineUp) {
Gus Grubba's avatar
Gus Grubba committed
461
            if (decoder != nullptr) {
462
                gst_object_unref(decoder);
Gus Grubba's avatar
Gus Grubba committed
463
                decoder = nullptr;
464
            }
Gus Grubba's avatar
Gus Grubba committed
465

Gus Grubba's avatar
Gus Grubba committed
466
            if (parser != nullptr) {
467
                gst_object_unref(parser);
Gus Grubba's avatar
Gus Grubba committed
468
                parser = nullptr;
469
            }
Gus Grubba's avatar
Gus Grubba committed
470

Gus Grubba's avatar
Gus Grubba committed
471
            if (demux != nullptr) {
472
                gst_object_unref(demux);
Gus Grubba's avatar
Gus Grubba committed
473
                demux = nullptr;
474
            }
Gus Grubba's avatar
Gus Grubba committed
475

Gus Grubba's avatar
Gus Grubba committed
476
            if (dataSource != nullptr) {
477
                gst_object_unref(dataSource);
Gus Grubba's avatar
Gus Grubba committed
478
                dataSource = nullptr;
479
            }
480

Gus Grubba's avatar
Gus Grubba committed
481
            if (_tee != nullptr) {
482
                gst_object_unref(_tee);
Gus Grubba's avatar
Gus Grubba committed
483
                dataSource = nullptr;
484
            }
485

Gus Grubba's avatar
Gus Grubba committed
486
            if (queue != nullptr) {
487
                gst_object_unref(queue);
Gus Grubba's avatar
Gus Grubba committed
488
                dataSource = nullptr;
489
            }
Gus Grubba's avatar
Gus Grubba committed
490
        }
491 492 493

        _running = false;
    } else {
494
        GST_DEBUG_BIN_TO_DOT_FILE(GST_BIN(_pipeline), GST_DEBUG_GRAPH_SHOW_ALL, "pipeline-playing");
495 496
        _running = true;
        qCDebug(VideoReceiverLog) << "Running";
Gus Grubba's avatar
Gus Grubba committed
497
    }
498
    _starting = false;
499
#endif
Gus Grubba's avatar
Gus Grubba committed
500 501
}

502 503 504
//-----------------------------------------------------------------------------
void
VideoReceiver::stop()
Gus Grubba's avatar
Gus Grubba committed
505
{
506
    if(qgcApp() && qgcApp()->runningUnitTests()) {
507 508
        return;
    }
509
#if defined(QGC_GST_STREAMING)
510
    _stop = true;
511
    qCDebug(VideoReceiverLog) << "stop()";
512 513
    if(!_streaming) {
        _shutdownPipeline();
Gus Grubba's avatar
Gus Grubba committed
514
    } else if (_pipeline != nullptr && !_stopping) {
515 516 517 518
        qCDebug(VideoReceiverLog) << "Stopping _pipeline";
        gst_element_send_event(_pipeline, gst_event_new_eos());
        _stopping = true;
        GstBus* bus = gst_pipeline_get_bus(GST_PIPELINE(_pipeline));
519
        GstMessage* message = gst_bus_timed_pop_filtered(bus, GST_CLOCK_TIME_NONE, (GstMessageType)(GST_MESSAGE_EOS|GST_MESSAGE_ERROR));
520
        gst_object_unref(bus);
521 522 523 524 525 526
        if(GST_MESSAGE_TYPE(message) == GST_MESSAGE_ERROR) {
            _shutdownPipeline();
            qCritical() << "Error stopping pipeline!";
        } else if(GST_MESSAGE_TYPE(message) == GST_MESSAGE_EOS) {
            _handleEOS();
        }
527
        gst_message_unref(message);
Gus Grubba's avatar
Gus Grubba committed
528
    }
529
#endif
Gus Grubba's avatar
Gus Grubba committed
530 531
}

532 533 534
//-----------------------------------------------------------------------------
void
VideoReceiver::setUri(const QString & uri)
Gus Grubba's avatar
Gus Grubba committed
535 536 537 538
{
    _uri = uri;
}

539
//-----------------------------------------------------------------------------
540
#if defined(QGC_GST_STREAMING)
541 542
void
VideoReceiver::_shutdownPipeline() {
543 544 545 546
    if(!_pipeline) {
        qCDebug(VideoReceiverLog) << "No pipeline";
        return;
    }
Gus Grubba's avatar
Gus Grubba committed
547 548
    GstBus* bus = nullptr;
    if ((bus = gst_pipeline_get_bus(GST_PIPELINE(_pipeline))) != nullptr) {
549 550
        gst_bus_disable_sync_message_emission(bus);
        gst_object_unref(bus);
Gus Grubba's avatar
Gus Grubba committed
551
        bus = nullptr;
552 553 554 555
    }
    gst_element_set_state(_pipeline, GST_STATE_NULL);
    gst_bin_remove(GST_BIN(_pipeline), _videoSink);
    gst_object_unref(_pipeline);
Gus Grubba's avatar
Gus Grubba committed
556
    _pipeline = nullptr;
557
    delete _sink;
Gus Grubba's avatar
Gus Grubba committed
558
    _sink = nullptr;
559
    _serverPresent = false;
560 561 562 563 564 565
    _streaming = false;
    _recording = false;
    _stopping = false;
    _running = false;
    emit recordingChanged();
}
566
#endif
567

568
//-----------------------------------------------------------------------------
569
#if defined(QGC_GST_STREAMING)
570 571
void
VideoReceiver::_handleError() {
572
    qCDebug(VideoReceiverLog) << "Gstreamer error!";
573 574
    // If there was an error we switch to software decoding only
    _tryWithHardwareDecoding = false;
575
    stop();
576
    _restart_timer.start(_restart_time_ms);
577 578 579
}
#endif

580
//-----------------------------------------------------------------------------
581
#if defined(QGC_GST_STREAMING)
582 583
void
VideoReceiver::_handleEOS() {
584 585
    if(_stopping) {
        _shutdownPipeline();
586
        qCDebug(VideoReceiverLog) << "Stopped";
587 588 589
    } else if(_recording && _sink->removing) {
        _shutdownRecordingBranch();
    } else {
590
        qWarning() << "VideoReceiver: Unexpected EOS!";
591
        _handleError();
Gus Grubba's avatar
Gus Grubba committed
592 593
    }
}
594
#endif
Gus Grubba's avatar
Gus Grubba committed
595

596
//-----------------------------------------------------------------------------
597
#if defined(QGC_GST_STREAMING)
598 599 600 601
void
VideoReceiver::_handleStateChanged() {
    if(_pipeline) {
        _streaming = GST_STATE(_pipeline) == GST_STATE_PLAYING;
602
        //qCDebug(VideoReceiverLog) << "State changed, _streaming:" << _streaming;
603
    }
604 605 606
}
#endif

607
//-----------------------------------------------------------------------------
608
#if defined(QGC_GST_STREAMING)
609 610
gboolean
VideoReceiver::_onBusMessage(GstBus* bus, GstMessage* msg, gpointer data)
Gus Grubba's avatar
Gus Grubba committed
611 612
{
    Q_UNUSED(bus)
Gus Grubba's avatar
Gus Grubba committed
613
    Q_ASSERT(msg != nullptr && data != nullptr);
Gus Grubba's avatar
Gus Grubba committed
614
    VideoReceiver* pThis = (VideoReceiver*)data;
615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636

    switch(GST_MESSAGE_TYPE(msg)) {
    case(GST_MESSAGE_ERROR): {
        gchar* debug;
        GError* error;
        gst_message_parse_error(msg, &error, &debug);
        g_free(debug);
        qCritical() << error->message;
        g_error_free(error);
        pThis->msgErrorReceived();
    }
        break;
    case(GST_MESSAGE_EOS):
        pThis->msgEOSReceived();
        break;
    case(GST_MESSAGE_STATE_CHANGED):
        pThis->msgStateChangedReceived();
        break;
    default:
        break;
    }

Gus Grubba's avatar
Gus Grubba committed
637 638
    return TRUE;
}
639
#endif
640

641
//-----------------------------------------------------------------------------
642
#if defined(QGC_GST_STREAMING)
643 644
void
VideoReceiver::_cleanupOldVideos()
645
{
646
    //-- Only perform cleanup if storage limit is enabled
647
    if(_videoSettings->enableStorageLimit()->rawValue().toBool()) {
648 649 650 651 652 653 654 655
        QString savePath = qgcApp()->toolbox()->settingsManager()->appSettings()->videoSavePath();
        QDir videoDir = QDir(savePath);
        videoDir.setFilter(QDir::Files | QDir::Readable | QDir::NoSymLinks | QDir::Writable);
        videoDir.setSorting(QDir::Time);
        //-- All the movie extensions we support
        QStringList nameFilters;
        for(uint32_t i = 0; i < NUM_MUXES; i++) {
            nameFilters << QString("*.") + QString(kVideoExtensions[i]);
656
        }
657 658 659 660 661 662
        videoDir.setNameFilters(nameFilters);
        //-- get the list of videos stored
        QFileInfoList vidList = videoDir.entryInfoList();
        if(!vidList.isEmpty()) {
            uint64_t total   = 0;
            //-- Settings are stored using MB
663
            uint64_t maxSize = (_videoSettings->maxVideoSize()->rawValue().toUInt() * 1024 * 1024);
664 665 666 667 668 669 670 671 672 673 674 675
            //-- Compute total used storage
            for(int i = 0; i < vidList.size(); i++) {
                total += vidList[i].size();
            }
            //-- Remove old movies until max size is satisfied.
            while(total >= maxSize && !vidList.isEmpty()) {
                total -= vidList.last().size();
                qCDebug(VideoReceiverLog) << "Removing old video file:" << vidList.last().filePath();
                QFile file (vidList.last().filePath());
                file.remove();
                vidList.removeLast();
            }
676 677 678
        }
    }
}
679
#endif
680

681 682 683 684
//-----------------------------------------------------------------------------
void
VideoReceiver::setVideoDecoder(VideoEncoding encoding)
{
685 686 687 688 689 690 691 692
    /*
    #if defined(Q_OS_MAC)
        _hwDecoderName = "vtdec";
    #else
        _hwDecoderName = "vaapidecode";
    #endif
    */

693
    if (encoding == H265_HW || encoding == H265_SW) {
694
        _depayName  = "rtph265depay";
695
        _parserName = "h265parse";
696 697 698 699
#if defined(__android__)
        _hwDecoderName = "amcviddec-omxgooglehevcdecoder";
#endif
        _swDecoderName = "avdec_h265";
700
    } else {
701
        _depayName  = "rtph264depay";
702
        _parserName = "h264parse";
703 704 705 706
#if defined(__android__)
        _hwDecoderName = "amcviddec-omxgoogleh264decoder";
#endif
        _swDecoderName = "avdec_h264";
707 708
    }

709 710 711 712
    if (!_tryWithHardwareDecoding) {
        _hwDecoderName = nullptr;
    }
}
713

714
//-----------------------------------------------------------------------------
715 716 717 718 719 720 721 722 723 724 725
// When we finish our pipeline will look like this:
//
//                                   +-->queue-->decoder-->_videosink
//                                   |
//    datasource-->demux-->parser-->tee
//                                   |
//                                   |    +--------------_sink-------------------+
//                                   |    |                                      |
//   we are adding these elements->  +->teepad-->queue-->matroskamux-->_filesink |
//                                        |                                      |
//                                        +--------------------------------------+
726
void
727
VideoReceiver::startRecording(const QString &videoFile)
728
{
729
#if defined(QGC_GST_STREAMING)
730

731 732
    qCDebug(VideoReceiverLog) << "startRecording()";
    // exit immediately if we are already recording
Gus Grubba's avatar
Gus Grubba committed
733
    if(_pipeline == nullptr || _recording) {
734 735 736 737
        qCDebug(VideoReceiverLog) << "Already recording!";
        return;
    }

738
    uint32_t muxIdx = _videoSettings->recordingFormat()->rawValue().toUInt();
739 740 741 742 743 744 745 746
    if(muxIdx >= NUM_MUXES) {
        qgcApp()->showMessage(tr("Invalid video format defined."));
        return;
    }

    //-- Disk usage maintenance
    _cleanupOldVideos();

747
    _sink           = new Sink();
748
    _sink->teepad   = gst_element_get_request_pad(_tee, "src_%u");
Gus Grubba's avatar
Gus Grubba committed
749
    _sink->queue    = gst_element_factory_make("queue", nullptr);
750
    _sink->parse    = gst_element_factory_make(_parserName, nullptr);
Gus Grubba's avatar
Gus Grubba committed
751 752
    _sink->mux      = gst_element_factory_make(kVideoMuxes[muxIdx], nullptr);
    _sink->filesink = gst_element_factory_make("filesink", nullptr);
753 754
    _sink->removing = false;

755
    if(!_sink->teepad || !_sink->queue || !_sink->mux || !_sink->filesink || !_sink->parse) {
756 757 758 759
        qCritical() << "VideoReceiver::startRecording() failed to make _sink elements";
        return;
    }

760 761 762 763 764 765 766 767 768 769 770
    if(videoFile.isEmpty()) {
        QString savePath = qgcApp()->toolbox()->settingsManager()->appSettings()->videoSavePath();
        if(savePath.isEmpty()) {
            qgcApp()->showMessage(tr("Unabled to record video. Video save path must be specified in Settings."));
            return;
        }
        _videoFile = savePath + "/" + QDateTime::currentDateTime().toString("yyyy-MM-dd_hh.mm.ss") + "." + kVideoExtensions[muxIdx];
    } else {
        _videoFile = videoFile;
    }
    emit videoFileChanged();
771

772
    g_object_set(static_cast<gpointer>(_sink->filesink), "location", qPrintable(_videoFile), nullptr);
773
    qCDebug(VideoReceiverLog) << "New video file:" << _videoFile;
774 775

    gst_object_ref(_sink->queue);
776
    gst_object_ref(_sink->parse);
777 778 779
    gst_object_ref(_sink->mux);
    gst_object_ref(_sink->filesink);

780 781
    gst_bin_add_many(GST_BIN(_pipeline), _sink->queue, _sink->parse, _sink->mux, nullptr);
    gst_element_link_many(_sink->queue, _sink->parse, _sink->mux, nullptr);
782 783

    gst_element_sync_state_with_parent(_sink->queue);
784
    gst_element_sync_state_with_parent(_sink->parse);
785 786
    gst_element_sync_state_with_parent(_sink->mux);

787 788 789
    // Install a probe on the recording branch to drop buffers until we hit our first keyframe
    // When we hit our first keyframe, we can offset the timestamps appropriately according to the first keyframe time
    // This will ensure the first frame is a keyframe at t=0, and decoding can begin immediately on playback
790 791
    // Once we have this valid frame, we attach the filesink.
    // Attaching it here would cause the filesink to fail to preroll and to stall the pipeline for a few seconds.
792
    GstPad* probepad = gst_element_get_static_pad(_sink->queue, "src");
Gus Grubba's avatar
Gus Grubba committed
793
    gst_pad_add_probe(probepad, (GstPadProbeType)(GST_PAD_PROBE_TYPE_BUFFER /* | GST_PAD_PROBE_TYPE_BLOCK */), _keyframeWatch, this, nullptr); // to drop the buffer or to block the buffer?
794 795 796
    gst_object_unref(probepad);

    // Link the recording branch to the pipeline
797 798 799 800
    GstPad* sinkpad = gst_element_get_static_pad(_sink->queue, "sink");
    gst_pad_link(_sink->teepad, sinkpad);
    gst_object_unref(sinkpad);

801 802
    GST_DEBUG_BIN_TO_DOT_FILE(GST_BIN(_pipeline), GST_DEBUG_GRAPH_SHOW_ALL, "pipeline-recording");

803 804 805
    _recording = true;
    emit recordingChanged();
    qCDebug(VideoReceiverLog) << "Recording started";
DonLakeFlyer's avatar
DonLakeFlyer committed
806 807
#else
    Q_UNUSED(videoFile)
808 809 810
#endif
}

811 812 813
//-----------------------------------------------------------------------------
void
VideoReceiver::stopRecording(void)
814
{
815
#if defined(QGC_GST_STREAMING)
816 817
    qCDebug(VideoReceiverLog) << "stopRecording()";
    // exit immediately if we are not recording
Gus Grubba's avatar
Gus Grubba committed
818
    if(_pipeline == nullptr || !_recording) {
819 820 821 822
        qCDebug(VideoReceiverLog) << "Not recording!";
        return;
    }
    // Wait for data block before unlinking
Gus Grubba's avatar
Gus Grubba committed
823
    gst_pad_add_probe(_sink->teepad, GST_PAD_PROBE_TYPE_IDLE, _unlinkCallBack, this, nullptr);
824 825 826
#endif
}

827
//-----------------------------------------------------------------------------
828 829 830 831 832 833
// This is only installed on the transient _pipelineStopRec in order
// to finalize a video file. It is not used for the main _pipeline.
// -EOS has appeared on the bus of the temporary pipeline
// -At this point all of the recoring elements have been flushed, and the video file has been finalized
// -Now we can remove the temporary pipeline and its elements
#if defined(QGC_GST_STREAMING)
834 835
void
VideoReceiver::_shutdownRecordingBranch()
836 837
{
    gst_bin_remove(GST_BIN(_pipelineStopRec), _sink->queue);
838
    gst_bin_remove(GST_BIN(_pipelineStopRec), _sink->parse);
839 840 841 842 843
    gst_bin_remove(GST_BIN(_pipelineStopRec), _sink->mux);
    gst_bin_remove(GST_BIN(_pipelineStopRec), _sink->filesink);

    gst_element_set_state(_pipelineStopRec, GST_STATE_NULL);
    gst_object_unref(_pipelineStopRec);
Gus Grubba's avatar
Gus Grubba committed
844
    _pipelineStopRec = nullptr;
845

846 847 848 849
    gst_element_set_state(_sink->filesink,  GST_STATE_NULL);
    gst_element_set_state(_sink->parse,     GST_STATE_NULL);
    gst_element_set_state(_sink->mux,       GST_STATE_NULL);
    gst_element_set_state(_sink->queue,     GST_STATE_NULL);
850 851

    gst_object_unref(_sink->queue);
852
    gst_object_unref(_sink->parse);
853 854 855 856
    gst_object_unref(_sink->mux);
    gst_object_unref(_sink->filesink);

    delete _sink;
Gus Grubba's avatar
Gus Grubba committed
857
    _sink = nullptr;
858
    _recording = false;
859

860 861 862 863 864
    emit recordingChanged();
    qCDebug(VideoReceiverLog) << "Recording Stopped";
}
#endif

865
//-----------------------------------------------------------------------------
866 867 868 869 870
// -Unlink the recording branch from the tee in the main _pipeline
// -Create a second temporary pipeline, and place the recording branch elements into that pipeline
// -Setup watch and handler for EOS event on the temporary pipeline's bus
// -Send an EOS event at the beginning of that pipeline
#if defined(QGC_GST_STREAMING)
871 872
void
VideoReceiver::_detachRecordingBranch(GstPadProbeInfo* info)
873 874 875 876
{
    Q_UNUSED(info)

    // Also unlinks and unrefs
Gus Grubba's avatar
Gus Grubba committed
877
    gst_bin_remove_many(GST_BIN(_pipeline), _sink->queue, _sink->parse, _sink->mux, _sink->filesink, nullptr);
878 879 880 881 882 883 884 885 886

    // Give tee its pad back
    gst_element_release_request_pad(_tee, _sink->teepad);
    gst_object_unref(_sink->teepad);

    // Create temporary pipeline
    _pipelineStopRec = gst_pipeline_new("pipeStopRec");

    // Put our elements from the recording branch into the temporary pipeline
Gus Grubba's avatar
Gus Grubba committed
887 888
    gst_bin_add_many(GST_BIN(_pipelineStopRec), _sink->queue, _sink->parse, _sink->mux, _sink->filesink, nullptr);
    gst_element_link_many(_sink->queue, _sink->parse, _sink->mux, _sink->filesink, nullptr);
889

890 891 892
    // Add handler for EOS event
    GstBus* bus = gst_pipeline_get_bus(GST_PIPELINE(_pipelineStopRec));
    gst_bus_enable_sync_message_emission(bus);
893
    g_signal_connect(bus, "sync-message", G_CALLBACK(_onBusMessage), this);
894
    gst_object_unref(bus);
895 896 897 898 899 900 901 902 903 904 905 906 907

    if(gst_element_set_state(_pipelineStopRec, GST_STATE_PLAYING) == GST_STATE_CHANGE_FAILURE) {
        qCDebug(VideoReceiverLog) << "problem starting _pipelineStopRec";
    }

    // Send EOS at the beginning of the pipeline
    GstPad* sinkpad = gst_element_get_static_pad(_sink->queue, "sink");
    gst_pad_send_event(sinkpad, gst_event_new_eos());
    gst_object_unref(sinkpad);
    qCDebug(VideoReceiverLog) << "Recording branch unlinked";
}
#endif

908
//-----------------------------------------------------------------------------
909
#if defined(QGC_GST_STREAMING)
910 911
GstPadProbeReturn
VideoReceiver::_unlinkCallBack(GstPad* pad, GstPadProbeInfo* info, gpointer user_data)
912 913
{
    Q_UNUSED(pad);
Gus Grubba's avatar
Gus Grubba committed
914 915
    if(info != nullptr && user_data != nullptr) {
        VideoReceiver* pThis = static_cast<VideoReceiver*>(user_data);
916 917 918 919 920
        // We will only act once
        if(g_atomic_int_compare_and_exchange(&pThis->_sink->removing, FALSE, TRUE)) {
            pThis->_detachRecordingBranch(info);
        }
    }
921 922 923
    return GST_PAD_PROBE_REMOVE;
}
#endif
924

925 926 927 928 929 930
//-----------------------------------------------------------------------------
#if defined(QGC_GST_STREAMING)
GstPadProbeReturn
VideoReceiver::_keyframeWatch(GstPad* pad, GstPadProbeInfo* info, gpointer user_data)
{
    Q_UNUSED(pad);
Gus Grubba's avatar
Gus Grubba committed
931
    if(info != nullptr && user_data != nullptr) {
932 933 934 935
        GstBuffer* buf = gst_pad_probe_info_get_buffer(info);
        if(GST_BUFFER_FLAG_IS_SET(buf, GST_BUFFER_FLAG_DELTA_UNIT)) { // wait for a keyframe
            return GST_PAD_PROBE_DROP;
        } else {
Gus Grubba's avatar
Gus Grubba committed
936
            VideoReceiver* pThis = static_cast<VideoReceiver*>(user_data);
937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952

            // set media file '0' offset to current timeline position - we don't want to touch other elements in the graph, except these which are downstream!

            gint64 position;

            if (gst_element_query_position(pThis->_pipeline, GST_FORMAT_TIME, &position) != TRUE) {
                qCDebug(VideoReceiverLog) << "Unable to get timeline position, let's hope that downstream elements will survive";

                if (buf->pts != GST_CLOCK_TIME_NONE) {
                    position = buf->pts;
                } else {
                    position = gst_pad_get_offset(pad);
                }
            }

            gst_pad_set_offset(pad, position);
953 954 955 956 957 958

            // Add the filesink once we have a valid I-frame
            gst_bin_add_many(GST_BIN(pThis->_pipeline), pThis->_sink->filesink, nullptr);
            gst_element_link_many(pThis->_sink->mux, pThis->_sink->filesink, nullptr);
            gst_element_sync_state_with_parent(pThis->_sink->filesink);

959
            qCDebug(VideoReceiverLog) << "Got keyframe, stop dropping buffers";
960
            pThis->gotFirstRecordingKeyFrame();
961 962 963 964 965 966 967
        }
    }

    return GST_PAD_PROBE_REMOVE;
}
#endif

968 969 970 971 972 973
//-----------------------------------------------------------------------------
void
VideoReceiver::_updateTimer()
{
#if defined(QGC_GST_STREAMING)
    if(_videoSurface) {
974
        if(_stopping || _starting) {
975 976
            return;
        }
977
        if(_streaming) {
978 979 980 981 982 983 984 985 986 987 988 989
            if(!_videoRunning) {
                _videoSurface->setLastFrame(0);
                _videoRunning = true;
                emit videoRunningChanged();
            }
        } else {
            if(_videoRunning) {
                _videoRunning = false;
                emit videoRunningChanged();
            }
        }
        if(_videoRunning) {
990 991
            uint32_t timeout = 1;
            if(qgcApp()->toolbox() && qgcApp()->toolbox()->settingsManager()) {
992
                timeout = _videoSettings->rtspTimeout()->rawValue().toUInt();
993
            }
994 995 996
            time_t elapsed = 0;
            time_t lastFrame = _videoSurface->lastFrame();
            if(lastFrame != 0) {
Gus Grubba's avatar
Gus Grubba committed
997
                elapsed = time(nullptr) - _videoSurface->lastFrame();
998
            }
Gus Grubba's avatar
Gus Grubba committed
999
            if(elapsed > static_cast<time_t>(timeout) && _videoSurface) {
1000
                stop();
1001 1002
                // We want to start it back again with _updateTimer
                _stop = false;
1003 1004
            }
        } else {
1005
            if(!_stop && _running && !_uri.isEmpty() && _videoSettings->streamEnabled()->rawValue().toBool()) {
1006 1007 1008 1009 1010 1011 1012
                start();
            }
        }
    }
#endif
}