mirror of
https://github.com/qTox/qTox.git
synced 2024-03-22 14:00:36 +08:00
544 lines
16 KiB
C++
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;
|
|
}
|