VideoReceiver.cc 35.3 KB
Newer Older
1 2
/****************************************************************************
 *
Gus Grubba's avatar
Gus Grubba committed
3
 * (c) 2009-2020 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
        qCDebug(VideoReceiverLog) << "Stopping _pipeline";
516 517
        GstBus* bus = gst_pipeline_get_bus(GST_PIPELINE(_pipeline));
        gst_bus_disable_sync_message_emission(bus);
518 519
        gst_element_send_event(_pipeline, gst_event_new_eos());
        _stopping = true;
520
        GstMessage* message = gst_bus_timed_pop_filtered(bus, GST_CLOCK_TIME_NONE, (GstMessageType)(GST_MESSAGE_EOS|GST_MESSAGE_ERROR));
521
        gst_object_unref(bus);
522 523 524 525 526 527
        if(GST_MESSAGE_TYPE(message) == GST_MESSAGE_ERROR) {
            _shutdownPipeline();
            qCritical() << "Error stopping pipeline!";
        } else if(GST_MESSAGE_TYPE(message) == GST_MESSAGE_EOS) {
            _handleEOS();
        }
528
        gst_message_unref(message);
Gus Grubba's avatar
Gus Grubba committed
529
    }
530
#endif
Gus Grubba's avatar
Gus Grubba committed
531 532
}

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

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

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

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

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

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

    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
638 639
    return TRUE;
}
640
#endif
641

642
//-----------------------------------------------------------------------------
643
#if defined(QGC_GST_STREAMING)
644 645
void
VideoReceiver::_cleanupOldVideos()
646
{
647
    //-- Only perform cleanup if storage limit is enabled
648
    if(_videoSettings->enableStorageLimit()->rawValue().toBool()) {
649 650 651 652 653 654 655 656
        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]);
657
        }
658 659 660 661 662 663
        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
664
            uint64_t maxSize = (_videoSettings->maxVideoSize()->rawValue().toUInt() * 1024 * 1024);
665 666 667 668 669 670 671 672 673 674 675 676
            //-- 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();
            }
677 678 679
        }
    }
}
680
#endif
681

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

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

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

715
//-----------------------------------------------------------------------------
716 717 718 719 720 721 722 723 724 725 726
// 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 |
//                                        |                                      |
//                                        +--------------------------------------+
727
void
728
VideoReceiver::startRecording(const QString &videoFile)
729
{
730
#if defined(QGC_GST_STREAMING)
731

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

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

    //-- Disk usage maintenance
    _cleanupOldVideos();

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

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

761 762 763 764 765 766 767 768 769 770 771
    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();
772

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

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

781 782
    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);
783 784

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

788 789 790
    // 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
791 792
    // 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.
793
    GstPad* probepad = gst_element_get_static_pad(_sink->queue, "src");
Gus Grubba's avatar
Gus Grubba committed
794
    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?
795 796 797
    gst_object_unref(probepad);

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

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

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

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

828
//-----------------------------------------------------------------------------
829 830 831 832 833 834
// 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)
835 836
void
VideoReceiver::_shutdownRecordingBranch()
837 838
{
    gst_bin_remove(GST_BIN(_pipelineStopRec), _sink->queue);
839
    gst_bin_remove(GST_BIN(_pipelineStopRec), _sink->parse);
840 841 842 843 844
    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
845
    _pipelineStopRec = nullptr;
846

847 848 849 850
    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);
851 852

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

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

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

866
//-----------------------------------------------------------------------------
867 868 869 870 871
// -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)
872 873
void
VideoReceiver::_detachRecordingBranch(GstPadProbeInfo* info)
874 875 876 877
{
    Q_UNUSED(info)

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

    // 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
888 889
    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);
890

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

    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

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

926 927 928 929 930 931
//-----------------------------------------------------------------------------
#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
932
    if(info != nullptr && user_data != nullptr) {
933 934 935 936
        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
937
            VideoReceiver* pThis = static_cast<VideoReceiver*>(user_data);
938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953

            // 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);
954 955 956 957 958 959

            // 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);

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

    return GST_PAD_PROBE_REMOVE;
}
#endif

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