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

544 lines
16 KiB
C++

/*
Copyright © 2015-2018 by The qTox Project Contributors
This file is part of qTox, a Qt-based graphical interface for Tox.
qTox is libre software: you can redistribute it and/or modify
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,
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.
You should have received a copy of the GNU General Public License
along with qTox. If not, see <http://www.gnu.org/licenses/>.
*/
#include <QApplication>
#include <QDebug>
#include <QDesktopWidget>
#include <QScreen>
extern "C" {
#include <libavdevice/avdevice.h>
#include <libavformat/avformat.h>
}
#include "cameradevice.h"
#include "src/persistence/settings.h"
#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD)
#define USING_V4L 1
#else
#define USING_V4L 0
#endif
#ifdef Q_OS_WIN
#include "src/platform/camera/directshow.h"
#endif
#if USING_V4L
#include "src/platform/camera/v4l2.h"
#endif
#ifdef Q_OS_OSX
#include "src/platform/camera/avfoundation.h"
#endif
/**
* @class CameraDevice
*
* Maintains an FFmpeg context for open camera devices,
* takes care of sharing the context accross users and closing
* the camera device when not in use. The device can be opened
* recursively, and must then be closed recursively
*/
/**
* @var const QString CameraDevice::devName
* @brief Short name of the device
*
* @var AVFormatContext* CameraDevice::context
* @brief Context of the open device, must always be valid
*
* @var std::atomic_int CameraDevice::refcount;
* @brief Number of times the device was opened
*/
QHash<QString, CameraDevice*> CameraDevice::openDevices;
QMutex CameraDevice::openDeviceLock, CameraDevice::iformatLock;
AVInputFormat* CameraDevice::iformat{nullptr};
AVInputFormat* CameraDevice::idesktopFormat{nullptr};
CameraDevice::CameraDevice(const QString& devName, AVFormatContext* context)
: devName{devName}
, context{context}
, refcount{1}
{
}
CameraDevice* CameraDevice::open(QString devName, AVDictionary** options)
{
openDeviceLock.lock();
AVFormatContext* fctx = nullptr;
CameraDevice* dev = openDevices.value(devName);
int aduration;
std::string devString;
if (dev) {
goto out;
}
AVInputFormat* format;
if (devName.startsWith("x11grab#")) {
devName = devName.mid(8);
format = idesktopFormat;
} else if (devName.startsWith("gdigrab#")) {
devName = devName.mid(8);
format = idesktopFormat;
} else {
format = iformat;
}
devString = devName.toStdString();
if (avformat_open_input(&fctx, devString.c_str(), format, options) < 0) {
goto out;
}
// Fix avformat_find_stream_info hanging on garbage input
#if FF_API_PROBESIZE_32
aduration = fctx->max_analyze_duration2 = 0;
#else
aduration = fctx->max_analyze_duration = 0;
#endif
if (avformat_find_stream_info(fctx, nullptr) < 0) {
avformat_close_input(&fctx);
goto out;
}
#if FF_API_PROBESIZE_32
fctx->max_analyze_duration2 = aduration;
#else
fctx->max_analyze_duration = aduration;
#endif
dev = new CameraDevice{devName, fctx};
openDevices[devName] = dev;
out:
openDeviceLock.unlock();
return dev;
}
/**
* @brief Opens a device.
*
* Opens a device, creating a new one if needed
* If the device is alreay open in another mode, the mode
* will be ignored and the existing device is used
* If the mode does not exist, a new device can't be opened.
*
* @param devName Device name to open.
* @param mode Mode of device to open.
* @return CameraDevice if the device could be opened, nullptr otherwise.
*/
CameraDevice* CameraDevice::open(QString devName, VideoMode mode)
{
if (!getDefaultInputFormat())
return nullptr;
if (devName == "none") {
qDebug() << "Tried to open the null device";
return nullptr;
}
float FPS = 5;
if (mode.FPS > 0.0f) {
FPS = mode.FPS;
} else {
qWarning() << "VideoMode could be invalid!";
}
const std::string videoSize = QStringLiteral("%1x%2").arg(mode.width).arg(mode.height).toStdString();
const std::string framerate = QString{}.setNum(FPS).toStdString();
AVDictionary* options = nullptr;
if (!iformat)
;
#if USING_V4L
else if (devName.startsWith("x11grab#")) {
QSize screen;
if (mode.width && mode.height) {
screen.setWidth(mode.width);
screen.setHeight(mode.height);
} else {
QScreen* defaultScreen = QApplication::primaryScreen();
qreal pixRatio = defaultScreen->devicePixelRatio();
screen = defaultScreen->size();
// Workaround https://trac.ffmpeg.org/ticket/4574 by choping 1 px bottom and right
// Actually, let's chop two pixels, toxav hates odd resolutions (off by one stride)
screen.setWidth((screen.width() * pixRatio) - 2);
screen.setHeight((screen.height() * pixRatio) - 2);
}
const std::string screenVideoSize = QStringLiteral("%1x%2").arg(screen.width()).arg(screen.height()).toStdString();
av_dict_set(&options, "video_size", screenVideoSize.c_str(), 0);
devName += QString("+%1,%2").arg(QString().setNum(mode.x), QString().setNum(mode.y));
av_dict_set(&options, "framerate", framerate.c_str(), 0);
} else if (iformat->name == QString("video4linux2,v4l2") && mode) {
av_dict_set(&options, "video_size", videoSize.c_str(), 0);
av_dict_set(&options, "framerate", framerate.c_str(), 0);
const std::string pixelFormatStr = v4l2::getPixelFormatString(mode.pixel_format).toStdString();
// don't try to set a format string that doesn't exist
if (pixelFormatStr != "unknown" && pixelFormatStr != "invalid") {
const char* pixel_format = pixelFormatStr.c_str();
av_dict_set(&options, "pixel_format", pixel_format, 0);
}
}
#endif
#ifdef Q_OS_WIN
else if (devName.startsWith("gdigrab#")) {
const std::string offsetX = QString().setNum(mode.x).toStdString();
const std::string offsetY = QString().setNum(mode.y).toStdString();
av_dict_set(&options, "framerate", framerate.c_str(), 0);
av_dict_set(&options, "offset_x", offsetX.c_str(), 0);
av_dict_set(&options, "offset_y", offsetY.c_str(), 0);
av_dict_set(&options, "video_size", videoSize.c_str(), 0);
} else if (iformat->name == QString("dshow") && mode) {
av_dict_set(&options, "video_size", videoSize.c_str(), 0);
av_dict_set(&options, "framerate", framerate.c_str(), 0);
}
#endif
#ifdef Q_OS_OSX
else if (iformat->name == QString("avfoundation")) {
if (mode) {
av_dict_set(&options, "video_size", videoSize.c_str(), 0);
av_dict_set(&options, "framerate", framerate.c_str(), 0);
} else if (devName.startsWith(avfoundation::CAPTURE_SCREEN)) {
av_dict_set(&options, "framerate", framerate.c_str(), 0);
av_dict_set_int(&options, "capture_cursor", 1, 0);
av_dict_set_int(&options, "capture_mouse_clicks", 1, 0);
}
}
#endif
else if (mode) {
qWarning() << "Video mode-setting not implemented for input " << iformat->name;
Q_UNUSED(mode);
}
CameraDevice* dev = open(devName, &options);
if (options) {
av_dict_free(&options);
}
return dev;
}
/**
* @brief Opens the device again. Never fails
*/
void CameraDevice::open()
{
++refcount;
}
/**
* @brief Closes the device. Never fails.
* @note If returns true, "this" becomes invalid.
* @return True, if device finally deleted (closed last reference),
* false otherwise (if other references exist).
*/
bool CameraDevice::close()
{
if (--refcount > 0)
return false;
openDeviceLock.lock();
openDevices.remove(devName);
openDeviceLock.unlock();
avformat_close_input(&context);
delete this;
return true;
}
/**
* @brief Get raw device list
* @note Uses avdevice_list_devices
* @return Raw device list
*/
QVector<QPair<QString, QString>> CameraDevice::getRawDeviceListGeneric()
{
QVector<QPair<QString, QString>> devices;
if (!getDefaultInputFormat())
return devices;
// Alloc an input device context
AVFormatContext* s;
if (!(s = avformat_alloc_context()))
return devices;
if (!iformat->priv_class || !AV_IS_INPUT_DEVICE(iformat->priv_class->category)) {
avformat_free_context(s);
return devices;
}
s->iformat = iformat;
if (s->iformat->priv_data_size > 0) {
s->priv_data = av_mallocz(s->iformat->priv_data_size);
if (!s->priv_data) {
avformat_free_context(s);
return devices;
}
if (s->iformat->priv_class) {
*(const AVClass**)s->priv_data = s->iformat->priv_class;
av_opt_set_defaults(s->priv_data);
}
} else {
s->priv_data = nullptr;
}
// List the devices for this context
AVDeviceInfoList* devlist = nullptr;
AVDictionary* tmp = nullptr;
av_dict_copy(&tmp, nullptr, 0);
if (av_opt_set_dict2(s, &tmp, AV_OPT_SEARCH_CHILDREN) < 0) {
av_dict_free(&tmp);
avformat_free_context(s);
return devices;
}
avdevice_list_devices(s, &devlist);
av_dict_free(&tmp);
avformat_free_context(s);
if (!devlist) {
qWarning() << "avdevice_list_devices failed";
return devices;
}
// Convert the list to a QVector
devices.resize(devlist->nb_devices);
for (int i = 0; i < devlist->nb_devices; ++i) {
AVDeviceInfo* dev = devlist->devices[i];
devices[i].first = dev->device_name;
devices[i].second = dev->device_description;
}
avdevice_free_list_devices(&devlist);
return devices;
}
/**
* @brief Get device list with desciption
* @return A list of device names and descriptions.
* The names are the first part of the pair and can be passed to open(QString).
*/
QVector<QPair<QString, QString>> CameraDevice::getDeviceList()
{
QVector<QPair<QString, QString>> devices;
devices.append({"none", QObject::tr("None", "No camera device set")});
if (!getDefaultInputFormat())
return devices;
if (!iformat)
;
#ifdef Q_OS_WIN
else if (iformat->name == QString("dshow"))
devices += DirectShow::getDeviceList();
#endif
#if USING_V4L
else if (iformat->name == QString("video4linux2,v4l2"))
devices += v4l2::getDeviceList();
#endif
#ifdef Q_OS_OSX
else if (iformat->name == QString("avfoundation"))
devices += avfoundation::getDeviceList();
#endif
else
devices += getRawDeviceListGeneric();
if (idesktopFormat) {
if (idesktopFormat->name == QString("x11grab")) {
QString dev = "x11grab#";
QByteArray display = qgetenv("DISPLAY");
if (display.isNull())
dev += ":0";
else
dev += display.constData();
devices.push_back(QPair<QString, QString>{
dev, QObject::tr("Desktop", "Desktop as a camera input for screen sharing")});
}
if (idesktopFormat->name == QString("gdigrab"))
devices.push_back(QPair<QString, QString>{
"gdigrab#desktop",
QObject::tr("Desktop", "Desktop as a camera input for screen sharing")});
}
return devices;
}
/**
* @brief Get the default device name.
* @return The short name of the default device
* This is either the device in the settings or the system default.
*/
QString CameraDevice::getDefaultDeviceName()
{
QString defaultdev = Settings::getInstance().getVideoDev();
if (!getDefaultInputFormat())
return defaultdev;
QVector<QPair<QString, QString>> devlist = getDeviceList();
for (const QPair<QString, QString>& device : devlist)
if (defaultdev == device.first)
return defaultdev;
if (devlist.isEmpty())
return defaultdev;
return devlist[0].first;
}
/**
* @brief Checks if a device name specifies a display.
* @param devName Device name to check.
* @return True, if device is screen, false otherwise.
*/
bool CameraDevice::isScreen(const QString& devName)
{
return devName.startsWith("x11grab") || devName.startsWith("gdigrab");
}
/**
* @brief Get list of resolutions and position of screens
* @return Vector of avaliable screen modes with offset
*/
QVector<VideoMode> CameraDevice::getScreenModes()
{
QList<QScreen*> screens = QApplication::screens();
QVector<VideoMode> result;
std::for_each(screens.begin(), screens.end(), [&result](QScreen* s) {
QRect rect = s->geometry();
QPoint p = rect.topLeft();
qreal pixRatio = s->devicePixelRatio();
VideoMode mode(rect.width() * pixRatio, rect.height() * pixRatio, p.x() * pixRatio,
p.y() * pixRatio);
result.push_back(mode);
});
return result;
}
/**
* @brief Get the list of video modes for a device.
* @param devName Device name to get nodes from.
* @return Vector of available modes for the device.
*/
QVector<VideoMode> CameraDevice::getVideoModes(QString devName)
{
Q_UNUSED(devName);
if (!iformat)
;
else if (isScreen(devName))
return getScreenModes();
#ifdef Q_OS_WIN
else if (iformat->name == QString("dshow"))
return DirectShow::getDeviceModes(devName);
#endif
#if USING_V4L
else if (iformat->name == QString("video4linux2,v4l2"))
return v4l2::getDeviceModes(devName);
#endif
#ifdef Q_OS_OSX
else if (iformat->name == QString("avfoundation"))
return avfoundation::getDeviceModes(devName);
#endif
else
qWarning() << "Video mode listing not implemented for input " << iformat->name;
return {};
}
/**
* @brief Get the name of the pixel format of a video mode.
* @param pixel_format Pixel format to get the name from.
* @return Name of the pixel format.
*/
QString CameraDevice::getPixelFormatString(uint32_t pixel_format)
{
#if USING_V4L
return v4l2::getPixelFormatString(pixel_format);
#else
return QString("unknown");
#endif
}
/**
* @brief Compare two pixel formats.
* @param a First pixel format to compare.
* @param b Second pixel format to compare.
* @return True if we prefer format a to b,
* false otherwise (such as if there's no preference).
*/
bool CameraDevice::betterPixelFormat(uint32_t a, uint32_t b)
{
#if USING_V4L
return v4l2::betterPixelFormat(a, b);
#else
return false;
#endif
}
/**
* @brief Sets CameraDevice::iformat to default.
* @return True if success, false if failure.
*/
bool CameraDevice::getDefaultInputFormat()
{
QMutexLocker locker(&iformatLock);
if (iformat)
return true;
avdevice_register_all();
// Desktop capture input formats
#if USING_V4L
idesktopFormat = av_find_input_format("x11grab");
#endif
#ifdef Q_OS_WIN
idesktopFormat = av_find_input_format("gdigrab");
#endif
// Webcam input formats
#if USING_V4L
if ((iformat = av_find_input_format("v4l2")))
return true;
#endif
#ifdef Q_OS_WIN
if ((iformat = av_find_input_format("dshow")))
return true;
if ((iformat = av_find_input_format("vfwcap")))
return true;
#endif
#ifdef Q_OS_OSX
if ((iformat = av_find_input_format("avfoundation")))
return true;
if ((iformat = av_find_input_format("qtkit")))
return true;
#endif
qWarning() << "No valid input format found";
return false;
}