1
0
mirror of https://github.com/qTox/qTox.git synced 2024-03-22 14:00:36 +08:00
qTox/src/video/camerasource.cpp

467 lines
12 KiB
C++
Raw Normal View History

2015-05-14 10:46:28 +08:00
/*
Copyright © 2015 by The qTox Project Contributors
2015-05-14 10:46:28 +08:00
This file is part of qTox, a Qt-based graphical interface for Tox.
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.
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
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
2015-05-14 10:46:28 +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>
}
#include "cameradevice.h"
#include "camerasource.h"
#include "videoframe.h"
#include "src/persistence/settings.h"
2015-05-14 10:46:28 +08:00
#include <QDebug>
#include <QReadLocker>
#include <QWriteLocker>
2015-05-14 10:46:28 +08:00
#include <QtConcurrent/QtConcurrentRun>
#include <functional>
#include <memory>
2015-05-14 10:46:28 +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.
*/
/**
* @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
* @deprecated
*
* @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
*/
CameraSource* CameraSource::instance{nullptr};
2015-05-14 10:46:28 +08:00
CameraSource::CameraSource()
2017-06-26 18:58:22 +08:00
: deviceThread{new QThread}
, deviceName{"none"}
, device{nullptr}
, mode(VideoMode())
2017-06-01 15:50:59 +08:00
// clang-format off
, cctx{nullptr}
#if LIBAVCODEC_VERSION_INT < 3747941
, cctxOrig{nullptr}
#endif
, videoStreamIndex{-1}
, _isNone{true}
, subscriptions{0}
2015-05-14 10:46:28 +08:00
{
qRegisterMetaType<VideoMode>("VideoMode");
2017-06-26 18:58:22 +08:00
deviceThread->setObjectName("Device thread");
deviceThread->start();
moveToThread(deviceThread);
subscriptions = 0;
// TODO(sudden6): remove code when minimum ffmpeg version >= 4.0
#if LIBAVFORMAT_VERSION_INT < AV_VERSION_INT(58, 9, 100)
av_register_all();
#endif
avdevice_register_all();
2015-05-14 10:46:28 +08:00
}
// clang-format on
/**
* @brief Returns the singleton instance.
*/
CameraSource& CameraSource::getInstance()
2015-05-16 10:01:38 +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) {
2015-06-27 01:04:53 +08:00
delete instance;
instance = nullptr;
}
}
/**
* @brief Setup default device
* @note If a device is already open, the source will seamlessly switch to the new device.
*/
void CameraSource::setupDefault()
2015-05-14 10:46:28 +08:00
{
2017-06-26 18:27:06 +08:00
QString deviceName = CameraDevice::getDefaultDeviceName();
bool isScreen = CameraDevice::isScreen(deviceName);
VideoMode mode = VideoMode(Settings::getInstance().getScreenRegion());
if (!isScreen) {
mode = VideoMode(Settings::getInstance().getCamVideoRes());
mode.FPS = Settings::getInstance().getCamVideoFPS();
}
setupDevice(deviceName, mode);
}
/**
* @brief Change the device and mode.
* @note If a device is already open, the source will seamlessly switch to the new device.
*/
void CameraSource::setupDevice(const QString& DeviceName, const VideoMode& Mode)
{
if (QThread::currentThread() != deviceThread) {
QMetaObject::invokeMethod(this, "setupDevice", Q_ARG(const QString&, DeviceName),
Q_ARG(const VideoMode&, Mode));
return;
}
QWriteLocker locker{&deviceMutex};
if (DeviceName == deviceName && Mode == mode) {
return;
2015-10-24 11:19:23 +08:00
}
if (subscriptions) {
// To force close, ignoring optimization
int subs = subscriptions;
subscriptions = 0;
closeDevice();
subscriptions = subs;
}
deviceName = DeviceName;
mode = Mode;
_isNone = (deviceName == "none");
if (subscriptions && !_isNone) {
openDevice();
}
}
2015-06-22 20:59:55 +08:00
bool CameraSource::isNone() const
2015-09-28 07:04:39 +08:00
{
return _isNone;
2015-09-28 07:04:39 +08:00
}
2015-05-14 10:46:28 +08:00
CameraSource::~CameraSource()
{
QWriteLocker locker{&streamMutex};
QWriteLocker locker2{&deviceMutex};
2015-05-14 10:46:28 +08:00
// Stop the device thread
deviceThread->exit(0);
deviceThread->wait();
delete deviceThread;
if (_isNone) {
2015-06-22 20:59:55 +08:00
return;
}
2015-06-22 20:59:55 +08:00
2015-05-16 10:01:38 +08:00
// Free all remaining VideoFrame
VideoFrame::untrackFrames(id, true);
2015-05-16 10:01:38 +08:00
if (cctx) {
2015-05-14 10:46:28 +08:00
avcodec_free_context(&cctx);
}
#if LIBAVCODEC_VERSION_INT < 3747941
if (cctxOrig) {
2015-05-16 10:01:38 +08:00
avcodec_close(cctxOrig);
}
#endif
2015-05-14 10:46:28 +08:00
if (device) {
for (int i = 0; i < subscriptions; ++i)
device->close();
2016-07-07 17:58:28 +08:00
device = nullptr;
}
locker.unlock();
2015-05-14 10:46:28 +08:00
// Synchronize with our stream thread
while (streamFuture.isRunning())
QThread::yieldCurrentThread();
}
2017-06-26 18:51:44 +08:00
void CameraSource::subscribe()
2015-05-14 10:46:28 +08:00
{
QWriteLocker locker{&deviceMutex};
2015-05-14 10:46:28 +08:00
++subscriptions;
2017-06-26 18:51:44 +08:00
openDevice();
}
void CameraSource::unsubscribe()
{
QWriteLocker locker{&deviceMutex};
--subscriptions;
if (subscriptions == 0) {
closeDevice();
}
}
/**
* @brief Opens the video device and starts streaming.
* @note Callers must own the biglock.
*/
2017-06-26 18:51:44 +08:00
void CameraSource::openDevice()
{
if (QThread::currentThread() != deviceThread) {
QMetaObject::invokeMethod(this, "openDevice");
return;
}
QWriteLocker locker{&streamMutex};
if (subscriptions == 0) {
return;
}
qDebug() << "Opening device " << deviceName;
if (device) {
device->open();
2017-06-26 18:51:44 +08:00
emit openFailed();
return;
}
2015-05-14 10:46:28 +08:00
// We need to create a new CameraDevice
AVCodec* codec;
device = CameraDevice::open(deviceName, mode);
if (!device) {
2015-06-22 20:59:55 +08:00
qWarning() << "Failed to open device!";
2017-06-26 18:51:44 +08:00
emit openFailed();
return;
2015-05-14 10:46:28 +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
for (int i = 0; i < subscriptions; ++i) {
device->open();
}
// Find the first video stream, if any
for (unsigned i = 0; i < device->context->nb_streams; ++i) {
AVMediaType type;
#if LIBAVCODEC_VERSION_INT < 3747941
type = device->context->streams[i]->codec->codec_type;
#else
type = device->context->streams[i]->codecpar->codec_type;
#endif
if (type == AVMEDIA_TYPE_VIDEO) {
videoStreamIndex = i;
2015-05-14 10:46:28 +08:00
break;
}
}
if (videoStreamIndex == -1) {
qWarning() << "Video stream not found";
2017-06-26 18:51:44 +08:00
emit openFailed();
return;
}
2015-05-14 10:46:28 +08:00
AVCodecID codecId;
#if LIBAVCODEC_VERSION_INT < 3747941
cctxOrig = device->context->streams[videoStreamIndex]->codec;
codecId = cctxOrig->codec_id;
#else
// Get the stream's codec's parameters and find a matching decoder
AVCodecParameters* cparams = device->context->streams[videoStreamIndex]->codecpar;
codecId = cparams->codec_id;
#endif
codec = avcodec_find_decoder(codecId);
if (!codec) {
qWarning() << "Codec not found";
2017-06-26 18:51:44 +08:00
emit openFailed();
return;
}
2015-05-14 10:46:28 +08:00
#if LIBAVCODEC_VERSION_INT < 3747941
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);
if (avcodec_copy_context(cctx, cctxOrig) != 0) {
qWarning() << "Can't copy context";
2017-06-26 18:51:44 +08:00
emit openFailed();
return;
}
cctx->refcounted_frames = 1;
#else
// Create a context for our codec, using the existing parameters
cctx = avcodec_alloc_context3(codec);
if (avcodec_parameters_to_context(cctx, cparams) < 0) {
qWarning() << "Can't create AV context from parameters";
2017-06-26 18:51:44 +08:00
emit openFailed();
return;
}
#endif
2015-05-14 10:46:28 +08:00
// Open codec
if (avcodec_open2(cctx, codec, nullptr) < 0) {
qWarning() << "Can't open codec";
2015-05-14 10:46:28 +08:00
avcodec_free_context(&cctx);
2017-06-26 18:51:44 +08:00
emit openFailed();
return;
2015-05-14 10:46:28 +08:00
}
if (streamFuture.isRunning())
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();
emit deviceOpened();
2015-05-14 10:46:28 +08:00
}
/**
* @brief Closes the video device and stops streaming.
* @note Callers must own the biglock.
*/
void CameraSource::closeDevice()
2015-05-14 10:46:28 +08:00
{
if (QThread::currentThread() != deviceThread) {
QMetaObject::invokeMethod(this, "closeDevice");
return;
}
QWriteLocker locker{&streamMutex};
if (subscriptions != 0) {
return;
}
qDebug() << "Closing device " << deviceName;
// Free all remaining VideoFrame
VideoFrame::untrackFrames(id, true);
2015-05-14 10:46:28 +08:00
// Free our resources and close the device
videoStreamIndex = -1;
avcodec_free_context(&cctx);
#if LIBAVCODEC_VERSION_INT < 3747941
avcodec_close(cctxOrig);
cctxOrig = nullptr;
#endif
while (device && !device->close()) {
}
device = nullptr;
2015-05-14 10:46:28 +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 = [=]() {
AVPacket packet;
if (av_read_frame(device->context, &packet) != 0) {
2015-05-14 10:46:28 +08:00
return;
}
#if LIBAVCODEC_VERSION_INT < 3747941
AVFrame* frame = av_frame_alloc();
if (!frame) {
2015-05-14 10:46:28 +08:00
return;
}
2015-05-14 10:46:28 +08:00
// Only keep packets from the right stream;
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) {
2015-05-14 10:46:28 +08:00
return;
}
2015-05-14 10:46:28 +08:00
VideoFrame* vframe = new VideoFrame(id, frame);
emit frameAvailable(vframe->trackFrame());
2015-05-14 10:46:28 +08:00
}
#else
// Forward packets to the decoder and grab the decoded frame
bool isVideo = packet.stream_index == videoStreamIndex;
bool readyToRecive = isVideo && !avcodec_send_packet(cctx, &packet);
if (readyToRecive) {
AVFrame* frame = av_frame_alloc();
if (frame && !avcodec_receive_frame(cctx, frame)) {
VideoFrame* vframe = new VideoFrame(id, frame);
emit frameAvailable(vframe->trackFrame());
} else {
av_frame_free(&frame);
}
}
#endif
2015-05-14 10:46:28 +08:00
av_packet_unref(&packet);
2015-05-14 10:46:28 +08:00
};
forever
{
QReadLocker locker{&streamMutex};
2015-05-14 10:46:28 +08:00
// Exit if device is no longer valid
if (!device) {
break;
2015-05-14 10:46:28 +08:00
}
streamLoop();
}
}