VideoReceiver.cc 30.6 KB
Newer Older
1 2 3 4 5 6 7 8
/****************************************************************************
 *
 *   (c) 2009-2016 QGROUNDCONTROL PROJECT <http://www.qgroundcontrol.org>
 *
 * 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

Gus Grubba's avatar
Gus Grubba committed
22
#include <QDebug>
23
#include <QUrl>
24 25
#include <QDir>
#include <QDateTime>
26
#include <QSysInfo>
27

28 29
QGC_LOGGING_CATEGORY(VideoReceiverLog, "VideoReceiverLog")

30 31
#if defined(QGC_GST_STREAMING)

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

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

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

48 49 50
#endif


Gus Grubba's avatar
Gus Grubba committed
51 52
VideoReceiver::VideoReceiver(QObject* parent)
    : QObject(parent)
53
#if defined(QGC_GST_STREAMING)
54
    , _running(false)
55
    , _recording(false)
56
    , _streaming(false)
57 58
    , _starting(false)
    , _stopping(false)
Gus Grubba's avatar
Gus Grubba committed
59 60 61 62 63 64
    , _sink(nullptr)
    , _tee(nullptr)
    , _pipeline(nullptr)
    , _pipelineStopRec(nullptr)
    , _videoSink(nullptr)
    , _socket(nullptr)
65
    , _serverPresent(false)
66
    , _rtspTestInterval_ms(5000)
67
    , _udpReconnect_us(5000000)
68
#endif
Gus Grubba's avatar
Gus Grubba committed
69
    , _videoSurface(nullptr)
70 71
    , _videoRunning(false)
    , _showFullScreen(false)
Gus Grubba's avatar
Gus Grubba committed
72
    , _videoSettings(nullptr)
Gus Grubba's avatar
Gus Grubba committed
73
{
74 75
    _videoSurface = new VideoSurface;
    _videoSettings = qgcApp()->toolbox()->settingsManager()->videoSettings();
76
#if defined(QGC_GST_STREAMING)
77
    _setVideoSink(_videoSurface->videoSink());
78 79
    _timer.setSingleShot(true);
    connect(&_timer, &QTimer::timeout, this, &VideoReceiver::_timeout);
80 81 82
    connect(this, &VideoReceiver::msgErrorReceived, this, &VideoReceiver::_handleError);
    connect(this, &VideoReceiver::msgEOSReceived, this, &VideoReceiver::_handleEOS);
    connect(this, &VideoReceiver::msgStateChangedReceived, this, &VideoReceiver::_handleStateChanged);
83 84
    connect(&_frameTimer, &QTimer::timeout, this, &VideoReceiver::_updateTimer);
    _frameTimer.start(1000);
85
#endif
Gus Grubba's avatar
Gus Grubba committed
86 87 88 89
}

VideoReceiver::~VideoReceiver()
{
90
#if defined(QGC_GST_STREAMING)
91 92 93 94
    stop();
    if(_socket) {
        delete _socket;
    }
95 96 97
    if (_videoSink) {
        gst_object_unref(_videoSink);
    }
98
#endif
99 100
    if(_videoSurface)
        delete _videoSurface;
Gus Grubba's avatar
Gus Grubba committed
101 102
}

103
#if defined(QGC_GST_STREAMING)
104 105
void
VideoReceiver::_setVideoSink(GstElement* sink)
Gus Grubba's avatar
Gus Grubba committed
106 107 108
{
    if (_videoSink) {
        gst_object_unref(_videoSink);
Gus Grubba's avatar
Gus Grubba committed
109
        _videoSink = nullptr;
Gus Grubba's avatar
Gus Grubba committed
110 111 112 113 114 115
    }
    if (sink) {
        _videoSink = sink;
        gst_object_ref_sink(_videoSink);
    }
}
116
#endif
Gus Grubba's avatar
Gus Grubba committed
117

118 119 120 121 122 123 124 125 126
//-----------------------------------------------------------------------------
void
VideoReceiver::grabImage(QString imageFile)
{
    _imageFile = imageFile;
    emit imageFileChanged();
}

//-----------------------------------------------------------------------------
127
#if defined(QGC_GST_STREAMING)
128 129
static void
newPadCB(GstElement* element, GstPad* pad, gpointer data)
130
{
131
    gchar* name;
132
    name = gst_pad_get_name(pad);
133
    //g_print("A new pad %s was created\n", name);
134 135
    GstCaps* p_caps = gst_pad_get_pad_template_caps (pad);
    gchar* description = gst_caps_to_string(p_caps);
136
    qCDebug(VideoReceiverLog) << p_caps << ", " << description;
137
    g_free(description);
138 139
    GstElement* sink = GST_ELEMENT(data);
    if(gst_element_link_pads(element, name, sink, "sink") == false)
140 141 142
        qCritical() << "newPadCB : failed to link elements\n";
    g_free(name);
}
143
#endif
144

145
//-----------------------------------------------------------------------------
146
#if defined(QGC_GST_STREAMING)
147 148
void
VideoReceiver::_connected()
149 150 151
{
    //-- Server showed up. Now we start the stream.
    _timer.stop();
152
    _socket->deleteLater();
Gus Grubba's avatar
Gus Grubba committed
153
    _socket = nullptr;
154
    if(_videoSettings->streamEnabled()->rawValue().toBool()) {
Gus Grubba's avatar
Gus Grubba committed
155 156 157
        _serverPresent = true;
        start();
    }
158 159 160
}
#endif

161
//-----------------------------------------------------------------------------
162
#if defined(QGC_GST_STREAMING)
163 164
void
VideoReceiver::_socketError(QAbstractSocket::SocketError socketError)
165 166
{
    Q_UNUSED(socketError);
167
    _socket->deleteLater();
Gus Grubba's avatar
Gus Grubba committed
168
    _socket = nullptr;
169
    //-- Try again in a while
170
    if(_videoSettings->streamEnabled()->rawValue().toBool()) {
171
        _timer.start(_rtspTestInterval_ms);
Gus Grubba's avatar
Gus Grubba committed
172
    }
173 174 175
}
#endif

176
//-----------------------------------------------------------------------------
177
#if defined(QGC_GST_STREAMING)
178 179
void
VideoReceiver::_timeout()
180 181 182 183
{
    //-- If socket is live, we got no connection nor a socket error
    if(_socket) {
        delete _socket;
Gus Grubba's avatar
Gus Grubba committed
184
        _socket = nullptr;
185
    }
186
    if(_videoSettings->streamEnabled()->rawValue().toBool()) {
Gus Grubba's avatar
Gus Grubba committed
187 188 189 190 191
        //-- 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);
192 193 194 195
        //-- If RTSP and no port is defined, set default RTSP port (554)
        if(_uri.contains("rtsp://") && url.port() <= 0) {
            url.setPort(554);
        }
Gus Grubba's avatar
Gus Grubba committed
196
        _socket = new QTcpSocket;
197 198 199
        QNetworkProxy tempProxy;
        tempProxy.setType(QNetworkProxy::DefaultProxy);
        _socket->setProxy(tempProxy);
Gus Grubba's avatar
Gus Grubba committed
200 201
        connect(_socket, static_cast<void (QTcpSocket::*)(QAbstractSocket::SocketError)>(&QTcpSocket::error), this, &VideoReceiver::_socketError);
        connect(_socket, &QTcpSocket::connected, this, &VideoReceiver::_connected);
Gus Grubba's avatar
Gus Grubba committed
202
        _socket->connectToHost(url.host(), static_cast<uint16_t>(url.port()));
203
        _timer.start(_rtspTestInterval_ms);
Gus Grubba's avatar
Gus Grubba committed
204
    }
205 206 207
}
#endif

208
//-----------------------------------------------------------------------------
209 210 211 212 213 214 215 216 217
// When we finish our pipeline will look like this:
//
//                                   +-->queue-->decoder-->_videosink
//                                   |
//    datasource-->demux-->parser-->tee
//
//                                   ^
//                                   |
//                                   +-Here we will later link elements for recording
218 219
void
VideoReceiver::start()
Gus Grubba's avatar
Gus Grubba committed
220
{
221 222
    if(!_videoSettings->streamEnabled()->rawValue().toBool() ||
       !_videoSettings->streamConfigured()) {
223 224 225
        qCDebug(VideoReceiverLog) << "start() but not enabled/configured";
        return;
    }
226
#if defined(QGC_GST_STREAMING)
227
    _stop = false;
228 229
    qCDebug(VideoReceiverLog) << "start()";

Gus Grubba's avatar
Gus Grubba committed
230 231 232 233
    if (_uri.isEmpty()) {
        qCritical() << "VideoReceiver::start() failed because URI is not specified";
        return;
    }
Gus Grubba's avatar
Gus Grubba committed
234
    if (_videoSink == nullptr) {
Gus Grubba's avatar
Gus Grubba committed
235 236 237
        qCritical() << "VideoReceiver::start() failed because video sink is not set";
        return;
    }
238 239 240 241
    if(_running) {
        qCDebug(VideoReceiverLog) << "Already running!";
        return;
    }
Gus Grubba's avatar
Gus Grubba committed
242

243
    _starting = true;
244

245 246
    bool isUdp  = _uri.contains("udp://");
    bool isRtsp = _uri.contains("rtsp://");
247
    bool isTCP  = _uri.contains("tcp://");
Gus Grubba's avatar
Gus Grubba committed
248

249 250
    //-- For RTSP and TCP, check to see if server is there first
    if(!_serverPresent && (isRtsp || isTCP)) {
251 252 253 254
        _timer.start(100);
        return;
    }

Gus Grubba's avatar
Gus Grubba committed
255
    bool running = false;
256
    bool pipelineUp = false;
Gus Grubba's avatar
Gus Grubba committed
257

Gus Grubba's avatar
Gus Grubba committed
258 259 260 261 262 263 264
    GstElement*     dataSource  = nullptr;
    GstCaps*        caps        = nullptr;
    GstElement*     demux       = nullptr;
    GstElement*     parser      = nullptr;
    GstElement*     queue       = nullptr;
    GstElement*     decoder     = nullptr;
    GstElement*     queue1      = nullptr;
265

Gus Grubba's avatar
Gus Grubba committed
266
    do {
Gus Grubba's avatar
Gus Grubba committed
267
        if ((_pipeline = gst_pipeline_new("receiver")) == nullptr) {
268
            qCritical() << "VideoReceiver::start() failed. Error with gst_pipeline_new()";
Gus Grubba's avatar
Gus Grubba committed
269 270 271
            break;
        }

272 273
        if(isUdp) {
            dataSource = gst_element_factory_make("udpsrc", "udp-source");
274 275
        } else if(isTCP) {
            dataSource = gst_element_factory_make("tcpclientsrc", "tcpclient-source");
276 277
        } else {
            dataSource = gst_element_factory_make("rtspsrc", "rtsp-source");
Gus Grubba's avatar
Gus Grubba committed
278 279
        }

280 281
        if (!dataSource) {
            qCritical() << "VideoReceiver::start() failed. Error with data source for gst_element_factory_make()";
Gus Grubba's avatar
Gus Grubba committed
282 283 284
            break;
        }

285
        if(isUdp) {
Gus Grubba's avatar
Gus Grubba committed
286
            if ((caps = gst_caps_from_string("application/x-rtp, media=(string)video, clock-rate=(int)90000, encoding-name=(string)H264")) == nullptr) {
287 288 289
                qCritical() << "VideoReceiver::start() failed. Error with gst_caps_from_string()";
                break;
            }
Gus Grubba's avatar
Gus Grubba committed
290
            g_object_set(G_OBJECT(dataSource), "uri", qPrintable(_uri), "caps", caps, nullptr);
291 292
        } else if(isTCP) {
            QUrl url(_uri);
Gus Grubba's avatar
Gus Grubba committed
293
            g_object_set(G_OBJECT(dataSource), "host", qPrintable(url.host()), "port", url.port(), nullptr );
294
        } else {
295
            g_object_set(G_OBJECT(dataSource), "location", qPrintable(_uri), "latency", 17, "udp-reconnect", 1, "timeout", _udpReconnect_us, NULL);
296
        }
Gus Grubba's avatar
Gus Grubba committed
297

298 299
        // Currently, we expect H264 when using anything except for TCP.  Long term we may want this to be settable
        if (isTCP) {
Gus Grubba's avatar
Gus Grubba committed
300
            if ((demux = gst_element_factory_make("tsdemux", "mpeg2-ts-demuxer")) == nullptr) {
301 302 303 304
                qCritical() << "VideoReceiver::start() failed. Error with gst_element_factory_make('tsdemux')";
                break;
            }
        } else {
Gus Grubba's avatar
Gus Grubba committed
305
            if ((demux = gst_element_factory_make("rtph264depay", "rtp-h264-depacketizer")) == nullptr) {
306 307 308
                qCritical() << "VideoReceiver::start() failed. Error with gst_element_factory_make('rtph264depay')";
                break;
            }
309 310
        }

Gus Grubba's avatar
Gus Grubba committed
311
        if ((parser = gst_element_factory_make("h264parse", "h264-parser")) == nullptr) {
312
            qCritical() << "VideoReceiver::start() failed. Error with gst_element_factory_make('h264parse')";
Gus Grubba's avatar
Gus Grubba committed
313 314 315
            break;
        }

Gus Grubba's avatar
Gus Grubba committed
316
        if((_tee = gst_element_factory_make("tee", nullptr)) == nullptr)  {
317 318 319
            qCritical() << "VideoReceiver::start() failed. Error with gst_element_factory_make('tee')";
            break;
        }
Gus Grubba's avatar
Gus Grubba committed
320

Gus Grubba's avatar
Gus Grubba committed
321
        if((queue = gst_element_factory_make("queue", nullptr)) == nullptr)  {
322 323
            // 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
324
            qCritical() << "VideoReceiver::start() failed. Error with gst_element_factory_make('queue')";
325 326
            break;
        }
327

Gus Grubba's avatar
Gus Grubba committed
328
        if ((decoder = gst_element_factory_make("avdec_h264", "h264-decoder")) == nullptr) {
329 330 331 332
            qCritical() << "VideoReceiver::start() failed. Error with gst_element_factory_make('avdec_h264')";
            break;
        }

Gus Grubba's avatar
Gus Grubba committed
333
        if ((queue1 = gst_element_factory_make("queue", nullptr)) == nullptr) {
334 335 336 337
            qCritical() << "VideoReceiver::start() failed. Error with gst_element_factory_make('queue') [1]";
            break;
        }

Gus Grubba's avatar
Gus Grubba committed
338
        gst_bin_add_many(GST_BIN(_pipeline), dataSource, demux, parser, _tee, queue, decoder, queue1, _videoSink, nullptr);
339
        pipelineUp = true;
340

341 342
        if(isUdp) {
            // Link the pipeline in front of the tee
Gus Grubba's avatar
Gus Grubba committed
343
            if(!gst_element_link_many(dataSource, demux, parser, _tee, queue, decoder, queue1, _videoSink, nullptr)) {
344 345 346 347 348 349 350 351
                qCritical() << "Unable to link UDP elements.";
                break;
            }
        } else if (isTCP) {
            if(!gst_element_link(dataSource, demux)) {
                qCritical() << "Unable to link TCP dataSource to Demux.";
                break;
            }
Gus Grubba's avatar
Gus Grubba committed
352
            if(!gst_element_link_many(parser, _tee, queue, decoder, queue1, _videoSink, nullptr)) {
353
                qCritical() << "Unable to link TCP pipline to parser.";
354 355
                break;
            }
356
            g_signal_connect(demux, "pad-added", G_CALLBACK(newPadCB), parser);
357
        } else {
358
            g_signal_connect(dataSource, "pad-added", G_CALLBACK(newPadCB), demux);
Gus Grubba's avatar
Gus Grubba committed
359
            if(!gst_element_link_many(demux, parser, _tee, queue, decoder, _videoSink, nullptr)) {
360
                qCritical() << "Unable to link RTSP elements.";
361 362
                break;
            }
363 364
        }

Gus Grubba's avatar
Gus Grubba committed
365
        dataSource = demux = parser = queue = decoder = queue1 = nullptr;
Gus Grubba's avatar
Gus Grubba committed
366

Gus Grubba's avatar
Gus Grubba committed
367
        GstBus* bus = nullptr;
Gus Grubba's avatar
Gus Grubba committed
368

Gus Grubba's avatar
Gus Grubba committed
369
        if ((bus = gst_pipeline_get_bus(GST_PIPELINE(_pipeline))) != nullptr) {
370 371 372
            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
373
            bus = nullptr;
374
        }
Gus Grubba's avatar
Gus Grubba committed
375

376
        GST_DEBUG_BIN_TO_DOT_FILE(GST_BIN(_pipeline), GST_DEBUG_GRAPH_SHOW_ALL, "pipeline-paused");
Gus Grubba's avatar
Gus Grubba committed
377 378 379 380
        running = gst_element_set_state(_pipeline, GST_STATE_PLAYING) != GST_STATE_CHANGE_FAILURE;

    } while(0);

Gus Grubba's avatar
Gus Grubba committed
381
    if (caps != nullptr) {
Gus Grubba's avatar
Gus Grubba committed
382
        gst_caps_unref(caps);
Gus Grubba's avatar
Gus Grubba committed
383
        caps = nullptr;
Gus Grubba's avatar
Gus Grubba committed
384 385 386 387 388
    }

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

389
        // In newer versions, the pipeline will clean up all references that are added to it
Gus Grubba's avatar
Gus Grubba committed
390
        if (_pipeline != nullptr) {
391
            gst_object_unref(_pipeline);
Gus Grubba's avatar
Gus Grubba committed
392
            _pipeline = nullptr;
Gus Grubba's avatar
Gus Grubba committed
393 394
        }

395 396
        // If we failed before adding items to the pipeline, then clean up
        if (!pipelineUp) {
Gus Grubba's avatar
Gus Grubba committed
397
            if (decoder != nullptr) {
398
                gst_object_unref(decoder);
Gus Grubba's avatar
Gus Grubba committed
399
                decoder = nullptr;
400
            }
Gus Grubba's avatar
Gus Grubba committed
401

Gus Grubba's avatar
Gus Grubba committed
402
            if (parser != nullptr) {
403
                gst_object_unref(parser);
Gus Grubba's avatar
Gus Grubba committed
404
                parser = nullptr;
405
            }
Gus Grubba's avatar
Gus Grubba committed
406

Gus Grubba's avatar
Gus Grubba committed
407
            if (demux != nullptr) {
408
                gst_object_unref(demux);
Gus Grubba's avatar
Gus Grubba committed
409
                demux = nullptr;
410
            }
Gus Grubba's avatar
Gus Grubba committed
411

Gus Grubba's avatar
Gus Grubba committed
412
            if (dataSource != nullptr) {
413
                gst_object_unref(dataSource);
Gus Grubba's avatar
Gus Grubba committed
414
                dataSource = nullptr;
415
            }
416

Gus Grubba's avatar
Gus Grubba committed
417
            if (_tee != nullptr) {
418
                gst_object_unref(_tee);
Gus Grubba's avatar
Gus Grubba committed
419
                dataSource = nullptr;
420
            }
421

Gus Grubba's avatar
Gus Grubba committed
422
            if (queue != nullptr) {
423
                gst_object_unref(queue);
Gus Grubba's avatar
Gus Grubba committed
424
                dataSource = nullptr;
425
            }
Gus Grubba's avatar
Gus Grubba committed
426
        }
427 428 429

        _running = false;
    } else {
430
        GST_DEBUG_BIN_TO_DOT_FILE(GST_BIN(_pipeline), GST_DEBUG_GRAPH_SHOW_ALL, "pipeline-playing");
431 432
        _running = true;
        qCDebug(VideoReceiverLog) << "Running";
Gus Grubba's avatar
Gus Grubba committed
433
    }
434
    _starting = false;
435
#endif
Gus Grubba's avatar
Gus Grubba committed
436 437
}

438 439 440
//-----------------------------------------------------------------------------
void
VideoReceiver::stop()
Gus Grubba's avatar
Gus Grubba committed
441
{
442
#if defined(QGC_GST_STREAMING)
443
    _stop = true;
444
    qCDebug(VideoReceiverLog) << "stop()";
445 446
    if(!_streaming) {
        _shutdownPipeline();
Gus Grubba's avatar
Gus Grubba committed
447
    } else if (_pipeline != nullptr && !_stopping) {
448 449 450 451
        qCDebug(VideoReceiverLog) << "Stopping _pipeline";
        gst_element_send_event(_pipeline, gst_event_new_eos());
        _stopping = true;
        GstBus* bus = gst_pipeline_get_bus(GST_PIPELINE(_pipeline));
452
        GstMessage* message = gst_bus_timed_pop_filtered(bus, GST_CLOCK_TIME_NONE, (GstMessageType)(GST_MESSAGE_EOS|GST_MESSAGE_ERROR));
453
        gst_object_unref(bus);
454 455 456 457 458 459
        if(GST_MESSAGE_TYPE(message) == GST_MESSAGE_ERROR) {
            _shutdownPipeline();
            qCritical() << "Error stopping pipeline!";
        } else if(GST_MESSAGE_TYPE(message) == GST_MESSAGE_EOS) {
            _handleEOS();
        }
460
        gst_message_unref(message);
Gus Grubba's avatar
Gus Grubba committed
461
    }
462
#endif
Gus Grubba's avatar
Gus Grubba committed
463 464
}

465 466 467
//-----------------------------------------------------------------------------
void
VideoReceiver::setUri(const QString & uri)
Gus Grubba's avatar
Gus Grubba committed
468 469 470 471
{
    _uri = uri;
}

472
//-----------------------------------------------------------------------------
473
#if defined(QGC_GST_STREAMING)
474 475
void
VideoReceiver::_shutdownPipeline() {
476 477 478 479
    if(!_pipeline) {
        qCDebug(VideoReceiverLog) << "No pipeline";
        return;
    }
Gus Grubba's avatar
Gus Grubba committed
480 481
    GstBus* bus = nullptr;
    if ((bus = gst_pipeline_get_bus(GST_PIPELINE(_pipeline))) != nullptr) {
482 483
        gst_bus_disable_sync_message_emission(bus);
        gst_object_unref(bus);
Gus Grubba's avatar
Gus Grubba committed
484
        bus = nullptr;
485 486 487 488
    }
    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
489
    _pipeline = nullptr;
490
    delete _sink;
Gus Grubba's avatar
Gus Grubba committed
491
    _sink = nullptr;
492 493 494 495 496 497 498
    _serverPresent = false;
    _streaming = false;
    _recording = false;
    _stopping = false;
    _running = false;
    emit recordingChanged();
}
499
#endif
500

501
//-----------------------------------------------------------------------------
502
#if defined(QGC_GST_STREAMING)
503 504
void
VideoReceiver::_handleError() {
505 506 507 508 509
    qCDebug(VideoReceiverLog) << "Gstreamer error!";
    _shutdownPipeline();
}
#endif

510
//-----------------------------------------------------------------------------
511
#if defined(QGC_GST_STREAMING)
512 513
void
VideoReceiver::_handleEOS() {
514 515
    if(_stopping) {
        _shutdownPipeline();
516
        qCDebug(VideoReceiverLog) << "Stopped";
517 518 519
    } else if(_recording && _sink->removing) {
        _shutdownRecordingBranch();
    } else {
520
        qWarning() << "VideoReceiver: Unexpected EOS!";
521
        _shutdownPipeline();
Gus Grubba's avatar
Gus Grubba committed
522 523
    }
}
524
#endif
Gus Grubba's avatar
Gus Grubba committed
525

526
//-----------------------------------------------------------------------------
527
#if defined(QGC_GST_STREAMING)
528 529 530 531 532 533
void
VideoReceiver::_handleStateChanged() {
    if(_pipeline) {
        _streaming = GST_STATE(_pipeline) == GST_STATE_PLAYING;
        qCDebug(VideoReceiverLog) << "State changed, _streaming:" << _streaming;
    }
534 535 536
}
#endif

537
//-----------------------------------------------------------------------------
538
#if defined(QGC_GST_STREAMING)
539 540
gboolean
VideoReceiver::_onBusMessage(GstBus* bus, GstMessage* msg, gpointer data)
Gus Grubba's avatar
Gus Grubba committed
541 542
{
    Q_UNUSED(bus)
Gus Grubba's avatar
Gus Grubba committed
543
    Q_ASSERT(msg != nullptr && data != nullptr);
Gus Grubba's avatar
Gus Grubba committed
544
    VideoReceiver* pThis = (VideoReceiver*)data;
545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566

    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
567 568
    return TRUE;
}
569
#endif
570

571
//-----------------------------------------------------------------------------
572
#if defined(QGC_GST_STREAMING)
573 574
void
VideoReceiver::_cleanupOldVideos()
575
{
576
    //-- Only perform cleanup if storage limit is enabled
577
    if(_videoSettings->enableStorageLimit()->rawValue().toBool()) {
578 579 580 581 582 583 584 585
        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]);
586
        }
587 588 589 590 591 592
        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
593
            uint64_t maxSize = (_videoSettings->maxVideoSize()->rawValue().toUInt() * 1024 * 1024);
594 595 596 597 598 599 600 601 602 603 604 605
            //-- 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();
            }
606 607 608
        }
    }
}
609
#endif
610

611
//-----------------------------------------------------------------------------
612 613 614 615 616 617 618 619 620 621 622
// 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 |
//                                        |                                      |
//                                        +--------------------------------------+
623
void
624
VideoReceiver::startRecording(const QString &videoFile)
625
{
626
#if defined(QGC_GST_STREAMING)
627

628 629
    qCDebug(VideoReceiverLog) << "startRecording()";
    // exit immediately if we are already recording
Gus Grubba's avatar
Gus Grubba committed
630
    if(_pipeline == nullptr || _recording) {
631 632 633 634
        qCDebug(VideoReceiverLog) << "Already recording!";
        return;
    }

635
    uint32_t muxIdx = _videoSettings->recordingFormat()->rawValue().toUInt();
636 637 638 639 640 641 642 643
    if(muxIdx >= NUM_MUXES) {
        qgcApp()->showMessage(tr("Invalid video format defined."));
        return;
    }

    //-- Disk usage maintenance
    _cleanupOldVideos();

644
    _sink           = new Sink();
645
    _sink->teepad   = gst_element_get_request_pad(_tee, "src_%u");
Gus Grubba's avatar
Gus Grubba committed
646 647 648 649
    _sink->queue    = gst_element_factory_make("queue", nullptr);
    _sink->parse    = gst_element_factory_make("h264parse", nullptr);
    _sink->mux      = gst_element_factory_make(kVideoMuxes[muxIdx], nullptr);
    _sink->filesink = gst_element_factory_make("filesink", nullptr);
650 651
    _sink->removing = false;

652
    if(!_sink->teepad || !_sink->queue || !_sink->mux || !_sink->filesink || !_sink->parse) {
653 654 655 656
        qCritical() << "VideoReceiver::startRecording() failed to make _sink elements";
        return;
    }

657 658 659 660 661 662 663 664 665 666 667
    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();
668

Gus Grubba's avatar
Gus Grubba committed
669
    g_object_set(G_OBJECT(_sink->filesink), "location", qPrintable(_videoFile), nullptr);
670
    qCDebug(VideoReceiverLog) << "New video file:" << _videoFile;
671 672

    gst_object_ref(_sink->queue);
673
    gst_object_ref(_sink->parse);
674 675 676
    gst_object_ref(_sink->mux);
    gst_object_ref(_sink->filesink);

Gus Grubba's avatar
Gus Grubba committed
677 678
    gst_bin_add_many(GST_BIN(_pipeline), _sink->queue, _sink->parse, _sink->mux, _sink->filesink, nullptr);
    gst_element_link_many(_sink->queue, _sink->parse, _sink->mux, _sink->filesink, nullptr);
679 680

    gst_element_sync_state_with_parent(_sink->queue);
681
    gst_element_sync_state_with_parent(_sink->parse);
682 683 684
    gst_element_sync_state_with_parent(_sink->mux);
    gst_element_sync_state_with_parent(_sink->filesink);

685 686 687 688
    // 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
    GstPad* probepad = gst_element_get_static_pad(_sink->queue, "src");
Gus Grubba's avatar
Gus Grubba committed
689
    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?
690 691 692
    gst_object_unref(probepad);

    // Link the recording branch to the pipeline
693 694 695 696
    GstPad* sinkpad = gst_element_get_static_pad(_sink->queue, "sink");
    gst_pad_link(_sink->teepad, sinkpad);
    gst_object_unref(sinkpad);

697 698
    GST_DEBUG_BIN_TO_DOT_FILE(GST_BIN(_pipeline), GST_DEBUG_GRAPH_SHOW_ALL, "pipeline-recording");

699 700 701
    _recording = true;
    emit recordingChanged();
    qCDebug(VideoReceiverLog) << "Recording started";
DonLakeFlyer's avatar
DonLakeFlyer committed
702 703
#else
    Q_UNUSED(videoFile)
704 705 706
#endif
}

707 708 709
//-----------------------------------------------------------------------------
void
VideoReceiver::stopRecording(void)
710
{
711
#if defined(QGC_GST_STREAMING)
712 713
    qCDebug(VideoReceiverLog) << "stopRecording()";
    // exit immediately if we are not recording
Gus Grubba's avatar
Gus Grubba committed
714
    if(_pipeline == nullptr || !_recording) {
715 716 717 718
        qCDebug(VideoReceiverLog) << "Not recording!";
        return;
    }
    // Wait for data block before unlinking
Gus Grubba's avatar
Gus Grubba committed
719
    gst_pad_add_probe(_sink->teepad, GST_PAD_PROBE_TYPE_IDLE, _unlinkCallBack, this, nullptr);
720 721 722
#endif
}

723
//-----------------------------------------------------------------------------
724 725 726 727 728 729
// 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)
730 731
void
VideoReceiver::_shutdownRecordingBranch()
732 733
{
    gst_bin_remove(GST_BIN(_pipelineStopRec), _sink->queue);
734
    gst_bin_remove(GST_BIN(_pipelineStopRec), _sink->parse);
735 736 737 738 739
    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
740
    _pipelineStopRec = nullptr;
741

742 743 744 745
    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);
746 747

    gst_object_unref(_sink->queue);
748
    gst_object_unref(_sink->parse);
749 750 751 752
    gst_object_unref(_sink->mux);
    gst_object_unref(_sink->filesink);

    delete _sink;
Gus Grubba's avatar
Gus Grubba committed
753
    _sink = nullptr;
754
    _recording = false;
755

756 757 758 759 760
    emit recordingChanged();
    qCDebug(VideoReceiverLog) << "Recording Stopped";
}
#endif

761
//-----------------------------------------------------------------------------
762 763 764 765 766
// -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)
767 768
void
VideoReceiver::_detachRecordingBranch(GstPadProbeInfo* info)
769 770 771 772
{
    Q_UNUSED(info)

    // Also unlinks and unrefs
Gus Grubba's avatar
Gus Grubba committed
773
    gst_bin_remove_many(GST_BIN(_pipeline), _sink->queue, _sink->parse, _sink->mux, _sink->filesink, nullptr);
774 775 776 777 778 779 780 781 782

    // 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
783 784
    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);
785

786 787 788
    // Add handler for EOS event
    GstBus* bus = gst_pipeline_get_bus(GST_PIPELINE(_pipelineStopRec));
    gst_bus_enable_sync_message_emission(bus);
789
    g_signal_connect(bus, "sync-message", G_CALLBACK(_onBusMessage), this);
790
    gst_object_unref(bus);
791 792 793 794 795 796 797 798 799 800 801 802 803

    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

804
//-----------------------------------------------------------------------------
805
#if defined(QGC_GST_STREAMING)
806 807
GstPadProbeReturn
VideoReceiver::_unlinkCallBack(GstPad* pad, GstPadProbeInfo* info, gpointer user_data)
808 809
{
    Q_UNUSED(pad);
Gus Grubba's avatar
Gus Grubba committed
810 811
    if(info != nullptr && user_data != nullptr) {
        VideoReceiver* pThis = static_cast<VideoReceiver*>(user_data);
812 813 814 815 816
        // We will only act once
        if(g_atomic_int_compare_and_exchange(&pThis->_sink->removing, FALSE, TRUE)) {
            pThis->_detachRecordingBranch(info);
        }
    }
817 818 819
    return GST_PAD_PROBE_REMOVE;
}
#endif
820

821 822 823 824 825 826
//-----------------------------------------------------------------------------
#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
827
    if(info != nullptr && user_data != nullptr) {
828 829 830 831
        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
832
            VideoReceiver* pThis = static_cast<VideoReceiver*>(user_data);
833
            // reset the clock
Jacob Walser's avatar
Jacob Walser committed
834
            GstClock* clock = gst_pipeline_get_clock(GST_PIPELINE(pThis->_pipeline));
835 836 837 838 839 840 841 842 843 844 845 846 847
            GstClockTime time = gst_clock_get_time(clock);
            gst_object_unref(clock);
            gst_element_set_base_time(pThis->_pipeline, time); // offset pipeline timestamps to start at zero again
            buf->dts = 0; // The offset will not apply to this current buffer, our first frame, timestamp is zero
            buf->pts = 0;
            qCDebug(VideoReceiverLog) << "Got keyframe, stop dropping buffers";
        }
    }

    return GST_PAD_PROBE_REMOVE;
}
#endif

848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869
//-----------------------------------------------------------------------------
void
VideoReceiver::_updateTimer()
{
#if defined(QGC_GST_STREAMING)
    if(_videoSurface) {
        if(stopping() || starting()) {
            return;
        }
        if(streaming()) {
            if(!_videoRunning) {
                _videoSurface->setLastFrame(0);
                _videoRunning = true;
                emit videoRunningChanged();
            }
        } else {
            if(_videoRunning) {
                _videoRunning = false;
                emit videoRunningChanged();
            }
        }
        if(_videoRunning) {
870 871
            uint32_t timeout = 1;
            if(qgcApp()->toolbox() && qgcApp()->toolbox()->settingsManager()) {
872
                timeout = _videoSettings->rtspTimeout()->rawValue().toUInt();
873
            }
874 875 876
            time_t elapsed = 0;
            time_t lastFrame = _videoSurface->lastFrame();
            if(lastFrame != 0) {
Gus Grubba's avatar
Gus Grubba committed
877
                elapsed = time(nullptr) - _videoSurface->lastFrame();
878
            }
Gus Grubba's avatar
Gus Grubba committed
879
            if(elapsed > static_cast<time_t>(timeout) && _videoSurface) {
880
                stop();
881 882
                // We want to start it back again with _updateTimer
                _stop = false;
883 884
            }
        } else {
885
            if(!_stop && !running() && !_uri.isEmpty() && _videoSettings->streamEnabled()->rawValue().toBool()) {
886 887 888 889 890 891 892
                start();
            }
        }
    }
#endif
}