2015-05-14 10:46:28 +08:00
|
|
|
/*
|
2015-06-06 09:40:08 +08:00
|
|
|
Copyright © 2015 by The qTox Project
|
|
|
|
|
2015-05-14 10:46:28 +08:00
|
|
|
This file is part of qTox, a Qt-based graphical interface for Tox.
|
|
|
|
|
2015-06-06 09:40:08 +08:00
|
|
|
qTox is libre software: you can redistribute it and/or modify
|
2015-05-14 10:46:28 +08:00
|
|
|
it under the terms of the GNU General Public License as published by
|
|
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
|
|
(at your option) any later version.
|
2015-06-06 09:40:08 +08:00
|
|
|
|
|
|
|
qTox is distributed in the hope that it will be useful,
|
2015-05-14 10:46:28 +08:00
|
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
2015-06-06 09:40:08 +08:00
|
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
GNU General Public License for more details.
|
2015-05-14 10:46:28 +08:00
|
|
|
|
2015-06-06 09:40:08 +08:00
|
|
|
You should have received a copy of the GNU General Public License
|
|
|
|
along with qTox. If not, see <http://www.gnu.org/licenses/>.
|
2015-05-14 10:46:28 +08:00
|
|
|
*/
|
|
|
|
|
|
|
|
extern "C" {
|
|
|
|
#include <libavcodec/avcodec.h>
|
|
|
|
#include <libavdevice/avdevice.h>
|
|
|
|
#include <libavformat/avformat.h>
|
|
|
|
#include <libswscale/swscale.h>
|
|
|
|
}
|
2016-08-04 23:18:37 +08:00
|
|
|
#include <QWriteLocker>
|
|
|
|
#include <QReadLocker>
|
2015-05-14 10:46:28 +08:00
|
|
|
#include <QDebug>
|
|
|
|
#include <QtConcurrent/QtConcurrentRun>
|
|
|
|
#include <memory>
|
|
|
|
#include <functional>
|
2016-11-15 16:00:23 +08:00
|
|
|
#include "persistence/settings.h"
|
2015-05-14 10:46:28 +08:00
|
|
|
#include "camerasource.h"
|
|
|
|
#include "cameradevice.h"
|
|
|
|
#include "videoframe.h"
|
|
|
|
|
2016-07-27 06:18:57 +08:00
|
|
|
/**
|
2016-08-01 16:22:17 +08:00
|
|
|
* @class CameraSource
|
|
|
|
* @brief This class is a wrapper to share a camera's captured video frames
|
|
|
|
*
|
|
|
|
* It allows objects to suscribe and unsuscribe to the stream, starting
|
|
|
|
* the camera and streaming new video frames only when needed.
|
|
|
|
* This is a singleton, since we can only capture from one
|
|
|
|
* camera at the same time without thread-safety issues.
|
|
|
|
* The source is lazy in the sense that it will only keep the video
|
|
|
|
* device open as long as there are subscribers, the source can be
|
|
|
|
* open but the device closed if there are zero subscribers.
|
|
|
|
*/
|
2016-07-27 06:18:57 +08:00
|
|
|
|
|
|
|
/**
|
2016-08-01 16:22:17 +08:00
|
|
|
* @var QVector<std::weak_ptr<VideoFrame>> CameraSource::freelist
|
|
|
|
* @brief Frames that need freeing before we can safely close the device
|
|
|
|
*
|
|
|
|
* @var QFuture<void> CameraSource::streamFuture
|
|
|
|
* @brief Future of the streaming thread
|
|
|
|
*
|
|
|
|
* @var QString CameraSource::deviceName
|
|
|
|
* @brief Short name of the device for CameraDevice's open(QString)
|
|
|
|
*
|
|
|
|
* @var CameraDevice* CameraSource::device
|
|
|
|
* @brief Non-owning pointer to an open CameraDevice, or nullptr. Not atomic, synced with memfences when becomes null.
|
|
|
|
*
|
|
|
|
* @var VideoMode CameraSource::mode
|
|
|
|
* @brief What mode we tried to open the device in, all zeros means default mode
|
|
|
|
*
|
|
|
|
* @var AVCodecContext* CameraSource::cctx
|
|
|
|
* @brief Codec context of the camera's selected video stream
|
|
|
|
*
|
|
|
|
* @var AVCodecContext* CameraSource::cctxOrig
|
|
|
|
* @brief Codec context of the camera's selected video stream
|
|
|
|
*
|
|
|
|
* @var int CameraSource::videoStreamIndex
|
|
|
|
* @brief A camera can have multiple streams, this is the one we're decoding
|
|
|
|
*
|
|
|
|
* @var QMutex CameraSource::biglock
|
|
|
|
* @brief True when locked. Faster than mutexes for video decoding.
|
|
|
|
*
|
|
|
|
* @var QMutex CameraSource::freelistLock
|
|
|
|
* @brief True when locked. Faster than mutexes for video decoding.
|
|
|
|
*
|
|
|
|
* @var std::atomic_bool CameraSource::streamBlocker
|
|
|
|
* @brief Holds the streaming thread still when true
|
|
|
|
*
|
|
|
|
* @var std::atomic_int CameraSource::subscriptions
|
|
|
|
* @brief Remember how many times we subscribed for RAII
|
|
|
|
*/
|
2016-07-27 06:18:57 +08:00
|
|
|
|
2015-06-26 23:38:53 +08:00
|
|
|
CameraSource* CameraSource::instance{nullptr};
|
|
|
|
|
2015-05-14 10:46:28 +08:00
|
|
|
CameraSource::CameraSource()
|
2016-06-19 03:44:31 +08:00
|
|
|
: deviceName{"none"}, device{nullptr}, mode(VideoMode()),
|
2015-06-26 23:38:53 +08:00
|
|
|
cctx{nullptr}, cctxOrig{nullptr}, videoStreamIndex{-1},
|
2015-10-24 11:19:23 +08:00
|
|
|
_isOpen{false}, streamBlocker{false}, subscriptions{0}
|
2015-05-14 10:46:28 +08:00
|
|
|
{
|
2015-10-11 14:38:44 +08:00
|
|
|
subscriptions = 0;
|
2015-06-26 23:38:53 +08:00
|
|
|
av_register_all();
|
|
|
|
avdevice_register_all();
|
2015-05-14 10:46:28 +08:00
|
|
|
}
|
|
|
|
|
2016-07-27 06:18:57 +08:00
|
|
|
/**
|
2016-08-01 16:22:17 +08:00
|
|
|
* @brief Returns the singleton instance.
|
|
|
|
*/
|
2015-06-26 23:38:53 +08:00
|
|
|
CameraSource& CameraSource::getInstance()
|
2015-05-16 10:01:38 +08:00
|
|
|
{
|
2015-06-26 23:38:53 +08:00
|
|
|
if (!instance)
|
|
|
|
instance = new CameraSource();
|
|
|
|
return *instance;
|
2015-05-16 10:01:38 +08:00
|
|
|
}
|
|
|
|
|
2015-06-27 01:04:53 +08:00
|
|
|
void CameraSource::destroyInstance()
|
|
|
|
{
|
|
|
|
if (instance)
|
|
|
|
{
|
|
|
|
delete instance;
|
|
|
|
instance = nullptr;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-07-27 06:18:57 +08:00
|
|
|
/**
|
2016-08-01 16:22:17 +08:00
|
|
|
* @brief Opens the source for the camera device.
|
|
|
|
* @note If a device is already open, the source will seamlessly switch to the new device.
|
|
|
|
*
|
|
|
|
* Opens the source for the camera device in argument, in the settings, or the system default.
|
|
|
|
*/
|
2015-06-26 23:38:53 +08:00
|
|
|
void CameraSource::open()
|
2015-05-14 10:46:28 +08:00
|
|
|
{
|
2015-06-26 23:38:53 +08:00
|
|
|
open(CameraDevice::getDefaultDeviceName());
|
|
|
|
}
|
|
|
|
|
2016-04-14 19:42:16 +08:00
|
|
|
void CameraSource::open(const QString& deviceName)
|
2015-06-26 23:38:53 +08:00
|
|
|
{
|
2016-06-23 07:11:02 +08:00
|
|
|
bool isScreen = CameraDevice::isScreen(deviceName);
|
|
|
|
VideoMode mode = VideoMode(Settings::getInstance().getScreenRegion());
|
|
|
|
if (!isScreen)
|
|
|
|
{
|
|
|
|
mode = VideoMode(Settings::getInstance().getCamVideoRes());
|
|
|
|
mode.FPS = Settings::getInstance().getCamVideoFPS();
|
|
|
|
}
|
|
|
|
|
|
|
|
open(deviceName, mode);
|
2015-06-26 23:38:53 +08:00
|
|
|
}
|
|
|
|
|
2016-04-14 19:42:16 +08:00
|
|
|
void CameraSource::open(const QString& DeviceName, VideoMode Mode)
|
2015-06-26 23:38:53 +08:00
|
|
|
{
|
2016-08-04 23:18:37 +08:00
|
|
|
QWriteLocker locker{&streamMutex};
|
2015-06-26 23:38:53 +08:00
|
|
|
|
|
|
|
if (DeviceName == deviceName && Mode == mode)
|
2015-10-24 11:19:23 +08:00
|
|
|
{
|
2015-06-26 23:38:53 +08:00
|
|
|
return;
|
2015-10-24 11:19:23 +08:00
|
|
|
}
|
2015-06-26 23:38:53 +08:00
|
|
|
|
|
|
|
if (subscriptions)
|
|
|
|
closeDevice();
|
|
|
|
|
|
|
|
deviceName = DeviceName;
|
|
|
|
mode = Mode;
|
2015-09-28 07:04:39 +08:00
|
|
|
_isOpen = (deviceName != "none");
|
2015-06-26 23:38:53 +08:00
|
|
|
|
2015-09-28 07:04:39 +08:00
|
|
|
if (subscriptions && _isOpen)
|
2015-06-26 23:38:53 +08:00
|
|
|
openDevice();
|
|
|
|
}
|
2015-06-22 20:59:55 +08:00
|
|
|
|
2016-07-27 06:18:57 +08:00
|
|
|
/**
|
2016-08-01 16:22:17 +08:00
|
|
|
* @brief Stops streaming.
|
|
|
|
*
|
|
|
|
* Equivalent to opening the source with the video device "none".
|
|
|
|
*/
|
2015-06-26 23:38:53 +08:00
|
|
|
void CameraSource::close()
|
|
|
|
{
|
|
|
|
open("none");
|
2015-05-14 10:46:28 +08:00
|
|
|
}
|
|
|
|
|
2015-09-28 07:04:39 +08:00
|
|
|
bool CameraSource::isOpen()
|
|
|
|
{
|
|
|
|
return _isOpen;
|
|
|
|
}
|
|
|
|
|
2015-05-14 10:46:28 +08:00
|
|
|
CameraSource::~CameraSource()
|
|
|
|
{
|
2016-08-04 23:18:37 +08:00
|
|
|
QWriteLocker locker{&streamMutex};
|
2015-05-14 10:46:28 +08:00
|
|
|
|
2015-09-28 07:04:39 +08:00
|
|
|
if (!_isOpen)
|
2016-08-04 23:18:37 +08:00
|
|
|
{
|
2015-06-22 20:59:55 +08:00
|
|
|
return;
|
2016-08-04 23:18:37 +08:00
|
|
|
}
|
2015-06-22 20:59:55 +08:00
|
|
|
|
2015-05-16 10:01:38 +08:00
|
|
|
// Free all remaining VideoFrame
|
2016-05-02 16:22:08 +08:00
|
|
|
VideoFrame::untrackFrames(id, true);
|
2015-05-16 10:01:38 +08:00
|
|
|
|
2015-05-14 10:46:28 +08:00
|
|
|
if (cctx)
|
|
|
|
avcodec_free_context(&cctx);
|
2015-05-16 10:01:38 +08:00
|
|
|
if (cctxOrig)
|
|
|
|
avcodec_close(cctxOrig);
|
2015-05-14 10:46:28 +08:00
|
|
|
|
2015-10-24 02:30:22 +08:00
|
|
|
if (device)
|
|
|
|
{
|
2016-10-30 06:48:03 +08:00
|
|
|
for (int i = 0; i < subscriptions; ++i)
|
2015-10-24 02:30:22 +08:00
|
|
|
device->close();
|
2016-07-07 17:58:28 +08:00
|
|
|
|
2015-10-24 02:30:22 +08:00
|
|
|
device = nullptr;
|
|
|
|
}
|
2015-10-11 14:38:44 +08:00
|
|
|
|
2016-08-04 23:18:37 +08:00
|
|
|
locker.unlock();
|
2015-05-14 10:46:28 +08:00
|
|
|
|
|
|
|
// Synchronize with our stream thread
|
|
|
|
while (streamFuture.isRunning())
|
|
|
|
QThread::yieldCurrentThread();
|
|
|
|
}
|
|
|
|
|
|
|
|
bool CameraSource::subscribe()
|
|
|
|
{
|
2016-08-04 23:18:37 +08:00
|
|
|
QWriteLocker locker{&streamMutex};
|
2015-05-14 10:46:28 +08:00
|
|
|
|
2015-09-28 07:04:39 +08:00
|
|
|
if (!_isOpen)
|
2015-06-22 20:59:55 +08:00
|
|
|
{
|
2015-06-26 23:38:53 +08:00
|
|
|
++subscriptions;
|
2015-06-22 20:59:55 +08:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2015-06-26 23:38:53 +08:00
|
|
|
if (openDevice())
|
2015-05-14 10:46:28 +08:00
|
|
|
{
|
|
|
|
++subscriptions;
|
|
|
|
return true;
|
|
|
|
}
|
2015-06-26 23:38:53 +08:00
|
|
|
else
|
|
|
|
{
|
|
|
|
while (device && !device->close()) {}
|
|
|
|
device = nullptr;
|
|
|
|
cctx = cctxOrig = nullptr;
|
|
|
|
videoStreamIndex = -1;
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void CameraSource::unsubscribe()
|
|
|
|
{
|
2016-08-04 23:18:37 +08:00
|
|
|
QWriteLocker locker{&streamMutex};
|
2015-06-26 23:38:53 +08:00
|
|
|
|
2015-09-28 07:04:39 +08:00
|
|
|
if (!_isOpen)
|
2015-06-26 23:38:53 +08:00
|
|
|
{
|
2015-10-10 07:24:04 +08:00
|
|
|
--subscriptions;
|
2015-06-26 23:38:53 +08:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!device)
|
|
|
|
{
|
2015-10-15 01:53:46 +08:00
|
|
|
qWarning() << "Unsubscribing with zero subscriber";
|
2015-06-26 23:38:53 +08:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2015-10-11 14:38:44 +08:00
|
|
|
if (subscriptions - 1 == 0)
|
2015-06-26 23:38:53 +08:00
|
|
|
{
|
|
|
|
closeDevice();
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
device->close();
|
|
|
|
}
|
2015-10-11 14:38:44 +08:00
|
|
|
subscriptions--;
|
2015-06-26 23:38:53 +08:00
|
|
|
}
|
|
|
|
|
2016-07-27 06:18:57 +08:00
|
|
|
/**
|
2016-08-01 16:22:17 +08:00
|
|
|
* @brief Opens the video device and starts streaming.
|
|
|
|
* @note Callers must own the biglock.
|
|
|
|
* @return True if success, false otherwise.
|
|
|
|
*/
|
2015-06-26 23:38:53 +08:00
|
|
|
bool CameraSource::openDevice()
|
|
|
|
{
|
2016-06-19 03:44:31 +08:00
|
|
|
qDebug() << "Opening device " << deviceName;
|
2015-10-10 07:24:04 +08:00
|
|
|
|
2015-06-26 23:38:53 +08:00
|
|
|
if (device)
|
|
|
|
{
|
|
|
|
device->open();
|
|
|
|
return true;
|
|
|
|
}
|
2015-05-14 10:46:28 +08:00
|
|
|
|
|
|
|
// We need to create a new CameraDevice
|
|
|
|
AVCodec* codec;
|
2016-06-19 03:44:31 +08:00
|
|
|
device = CameraDevice::open(deviceName, mode);
|
|
|
|
|
2015-05-14 10:46:28 +08:00
|
|
|
if (!device)
|
|
|
|
{
|
2015-06-22 20:59:55 +08:00
|
|
|
qWarning() << "Failed to open device!";
|
2015-05-14 10:46:28 +08:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2015-06-26 23:38:53 +08:00
|
|
|
// We need to open the device as many time as we already have subscribers,
|
|
|
|
// otherwise the device could get closed while we still have subscribers
|
2016-10-30 06:48:03 +08:00
|
|
|
for (int i = 0; i < subscriptions; ++i)
|
2015-06-26 23:38:53 +08:00
|
|
|
device->open();
|
|
|
|
|
2015-05-14 10:46:28 +08:00
|
|
|
// Find the first video stream
|
2016-10-30 06:48:03 +08:00
|
|
|
for (unsigned i = 0; i < device->context->nb_streams; ++i)
|
2015-05-14 10:46:28 +08:00
|
|
|
{
|
2016-07-07 17:58:28 +08:00
|
|
|
if (device->context->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO)
|
2015-05-14 10:46:28 +08:00
|
|
|
{
|
2015-10-11 14:38:44 +08:00
|
|
|
videoStreamIndex = i;
|
2015-05-14 10:46:28 +08:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
2016-06-19 03:44:31 +08:00
|
|
|
|
2015-05-14 10:46:28 +08:00
|
|
|
if (videoStreamIndex == -1)
|
2016-06-19 03:44:31 +08:00
|
|
|
{
|
|
|
|
qWarning() << "Video stream not found";
|
2015-06-26 23:38:53 +08:00
|
|
|
return false;
|
2016-06-19 03:44:31 +08:00
|
|
|
}
|
2015-05-14 10:46:28 +08:00
|
|
|
|
|
|
|
// Get a pointer to the codec context for the video stream
|
2015-10-11 14:38:44 +08:00
|
|
|
cctxOrig = device->context->streams[videoStreamIndex]->codec;
|
|
|
|
codec = avcodec_find_decoder(cctxOrig->codec_id);
|
2016-07-07 17:58:28 +08:00
|
|
|
if (!codec)
|
2016-06-19 03:44:31 +08:00
|
|
|
{
|
|
|
|
qWarning() << "Codec not found";
|
2015-06-26 23:38:53 +08:00
|
|
|
return false;
|
2016-06-19 03:44:31 +08:00
|
|
|
}
|
2015-05-14 10:46:28 +08:00
|
|
|
|
|
|
|
// Copy context, since we apparently aren't allowed to use the original
|
|
|
|
cctx = avcodec_alloc_context3(codec);
|
2016-07-07 17:58:28 +08:00
|
|
|
if (avcodec_copy_context(cctx, cctxOrig) != 0)
|
2016-06-19 03:44:31 +08:00
|
|
|
{
|
|
|
|
qWarning() << "Can't copy context";
|
2015-06-26 23:38:53 +08:00
|
|
|
return false;
|
2016-06-19 03:44:31 +08:00
|
|
|
}
|
2015-10-11 14:38:44 +08:00
|
|
|
|
2015-05-14 10:46:28 +08:00
|
|
|
cctx->refcounted_frames = 1;
|
|
|
|
|
|
|
|
// Open codec
|
2016-07-07 17:58:28 +08:00
|
|
|
if (avcodec_open2(cctx, codec, nullptr)<0)
|
2015-05-14 10:46:28 +08:00
|
|
|
{
|
2016-06-19 03:44:31 +08:00
|
|
|
qWarning() << "Can't open codec";
|
2015-05-14 10:46:28 +08:00
|
|
|
avcodec_free_context(&cctx);
|
2015-06-26 23:38:53 +08:00
|
|
|
return false;
|
2015-05-14 10:46:28 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
if (streamFuture.isRunning())
|
2015-06-26 23:38:53 +08:00
|
|
|
qDebug() << "The stream thread is already running! Keeping the current one open.";
|
2015-05-14 10:46:28 +08:00
|
|
|
else
|
|
|
|
streamFuture = QtConcurrent::run(std::bind(&CameraSource::stream, this));
|
|
|
|
|
|
|
|
// Synchronize with our stream thread
|
|
|
|
while (!streamFuture.isRunning())
|
|
|
|
QThread::yieldCurrentThread();
|
|
|
|
|
2015-07-22 02:38:43 +08:00
|
|
|
emit deviceOpened();
|
|
|
|
|
2015-05-14 10:46:28 +08:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2016-07-27 06:18:57 +08:00
|
|
|
/**
|
2016-08-01 16:22:17 +08:00
|
|
|
* @brief Closes the video device and stops streaming.
|
|
|
|
* @note Callers must own the biglock.
|
|
|
|
*/
|
2015-06-26 23:38:53 +08:00
|
|
|
void CameraSource::closeDevice()
|
2015-05-14 10:46:28 +08:00
|
|
|
{
|
2016-08-04 23:18:37 +08:00
|
|
|
qDebug() << "Closing device " << deviceName;
|
2015-10-10 07:24:04 +08:00
|
|
|
|
2015-06-26 23:38:53 +08:00
|
|
|
// Free all remaining VideoFrame
|
2016-05-02 16:22:08 +08:00
|
|
|
VideoFrame::untrackFrames(id, true);
|
2015-05-14 10:46:28 +08:00
|
|
|
|
2015-06-26 23:38:53 +08:00
|
|
|
// Free our resources and close the device
|
|
|
|
videoStreamIndex = -1;
|
|
|
|
avcodec_free_context(&cctx);
|
|
|
|
avcodec_close(cctxOrig);
|
|
|
|
cctxOrig = nullptr;
|
|
|
|
while (device && !device->close()) {}
|
|
|
|
device = nullptr;
|
2015-05-14 10:46:28 +08:00
|
|
|
}
|
|
|
|
|
2016-07-27 06:18:57 +08:00
|
|
|
/**
|
2016-08-01 16:22:17 +08:00
|
|
|
* @brief Blocking. Decodes video stream and emits new frames.
|
|
|
|
* @note Designed to run in its own thread.
|
|
|
|
*/
|
2015-05-14 10:46:28 +08:00
|
|
|
void CameraSource::stream()
|
|
|
|
{
|
|
|
|
auto streamLoop = [=]()
|
|
|
|
{
|
|
|
|
AVFrame* frame = av_frame_alloc();
|
|
|
|
if (!frame)
|
|
|
|
return;
|
2016-06-19 03:44:31 +08:00
|
|
|
|
2015-05-14 10:46:28 +08:00
|
|
|
AVPacket packet;
|
2016-06-19 03:44:31 +08:00
|
|
|
if (av_read_frame(device->context, &packet) < 0)
|
2015-05-14 10:46:28 +08:00
|
|
|
return;
|
|
|
|
|
|
|
|
// Only keep packets from the right stream;
|
2016-06-19 03:44:31 +08:00
|
|
|
if (packet.stream_index == videoStreamIndex)
|
2015-05-14 10:46:28 +08:00
|
|
|
{
|
|
|
|
// Decode video frame
|
|
|
|
int frameFinished;
|
|
|
|
avcodec_decode_video2(cctx, frame, &frameFinished, &packet);
|
|
|
|
if (!frameFinished)
|
|
|
|
return;
|
|
|
|
|
2016-05-02 16:22:08 +08:00
|
|
|
VideoFrame* vframe = new VideoFrame(id, frame);
|
|
|
|
emit frameAvailable(vframe->trackFrame());
|
2015-05-14 10:46:28 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
// Free the packet that was allocated by av_read_frame
|
2016-04-14 05:33:44 +08:00
|
|
|
av_packet_unref(&packet);
|
2015-05-14 10:46:28 +08:00
|
|
|
};
|
|
|
|
|
2016-06-19 03:44:31 +08:00
|
|
|
forever
|
|
|
|
{
|
2016-08-04 23:18:37 +08:00
|
|
|
QReadLocker locker{&streamMutex};
|
2015-05-14 10:46:28 +08:00
|
|
|
|
2016-08-04 23:18:37 +08:00
|
|
|
// Exit if device is no longer valid
|
|
|
|
if(!device)
|
2015-05-14 10:46:28 +08:00
|
|
|
{
|
2016-08-04 23:18:37 +08:00
|
|
|
break;
|
2015-05-14 10:46:28 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
streamLoop();
|
|
|
|
}
|
|
|
|
}
|