summaryrefslogtreecommitdiffstats
path: root/pica-client
diff options
context:
space:
mode:
Diffstat (limited to 'pica-client')
-rw-r--r--pica-client/pica-client.pro3
-rw-r--r--pica-client/videodevice.cpp61
-rw-r--r--pica-client/videodshow.cpp172
3 files changed, 234 insertions, 2 deletions
diff --git a/pica-client/pica-client.pro b/pica-client/pica-client.pro
index 4973c2e..92430fe 100644
--- a/pica-client/pica-client.pro
+++ b/pica-client/pica-client.pro
@@ -51,7 +51,8 @@ SOURCES += main.cpp\
../PICA_netconf.c \
../PICA_signverify.c \
dialogs/nodesdialog.cpp \
- videodevice.cpp
+ videodevice.cpp \
+ videodshow.cpp
HEADERS += mainwindow.h \
audiodevice.h \
diff --git a/pica-client/videodevice.cpp b/pica-client/videodevice.cpp
index 52367af..8ca5cdc 100644
--- a/pica-client/videodevice.cpp
+++ b/pica-client/videodevice.cpp
@@ -307,6 +307,22 @@ void VideoDevice::Close()
m_queueCond.wakeAll();
}
+// The string to hand avformat_open_input() for a camera.
+//
+// DirectShow namespaces its devices by type, so a camera has to be asked for
+// as "video=<name>" - the same name behind an "audio=" prefix would be a
+// microphone. v4l2 and avfoundation take the device path or index as it comes,
+// so the name is stored unprefixed and decorated here, which also keeps what
+// the settings dialog shows and stores readable.
+static QByteArray cameraOpenUrl(const QString &device)
+{
+#ifdef Q_OS_WIN
+ return (QStringLiteral("video=") + device).toUtf8();
+#else
+ return device.toUtf8();
+#endif
+}
+
void VideoDevice::Capture()
{
m_abort.storeRelaxed(0);
@@ -317,7 +333,7 @@ void VideoDevice::Capture()
avcodec_register_all();
#endif
- const QByteArray deviceUtf8 = m_deviceName.toUtf8();
+ const QByteArray deviceUtf8 = cameraOpenUrl(m_deviceName);
const QByteArray driverUtf8 = VideoDevice::PlatformDriverName().toUtf8();
const AVInputFormat *ifmt = av_find_input_format(driverUtf8.constData());
if (!ifmt)
@@ -370,8 +386,35 @@ void VideoDevice::Capture()
if (ret < 0)
{
+ // Last resort: let the camera pick. DirectShow in particular will
+ // refuse a video_size or framerate it cannot produce exactly, rather
+ // than settling for the nearest it can, so a camera with an unusual
+ // set of modes would otherwise give no picture at all. Whatever comes
+ // back is scaled to the negotiated size by the transcode loop anyway.
+ qWarning() << QString("Camera '%1' would not open at %2x%3 @%4 (%5), letting it choose")
+ .arg(m_deviceName).arg(m_width).arg(m_height).arg(kFrameRate).arg(ff_errstr(ret));
+
+ ret = avformat_open_input(&ifmt_ctx, deviceUtf8.constData(), ifmt, nullptr);
+ }
+
+ if (ret < 0)
+ {
// Not fatal to the call - it simply proceeds without outgoing video.
QString msg = QString("Could not open camera '%1': %2").arg(m_deviceName, ff_errstr(ret));
+
+#ifdef Q_OS_WIN
+ // A camera that enumerated but will not open is usually not a broken
+ // camera. Windows will happily list a device while refusing to
+ // instantiate it, which is what "Let desktop apps access your camera"
+ // being switched off looks like from here - and what another
+ // application already holding the camera looks like too. Neither is
+ // distinguishable from a genuine I/O error at this level, so say so
+ // rather than leaving the user with an error code that suggests
+ // hardware trouble.
+ msg += tr(" - check that \"Let desktop apps access your camera\" is enabled in the "
+ "Windows camera privacy settings, and that no other application is using it");
+#endif
+
qWarning() << msg;
emit errorOccurred(msg);
return;
@@ -991,11 +1034,27 @@ void VideoDevice::Play()
avcodec_free_context(&dec_ctx);
}
+#ifdef Q_OS_WIN
+// Defined in videodshow.cpp - FFmpeg's dshow demuxer can capture but cannot
+// enumerate, so the list comes from DirectShow itself.
+QList<MediaDeviceInfo> pica_enumerate_dshow_video();
+#endif
+
QList<MediaDeviceInfo> VideoDevice::Enumerate(enum MediaDeviceStreamDirection dir)
{
QList<MediaDeviceInfo> result;
int index = 0;
+#ifdef Q_OS_WIN
+ Q_UNUSED(index)
+
+ // Cameras are capture only; nothing renders video to a device here.
+ if (dir != PLAYBACK)
+ result = pica_enumerate_dshow_video();
+
+ return result;
+#endif
+
if (dir == PLAYBACK)
return result;
#ifdef Q_OS_LINUX
diff --git a/pica-client/videodshow.cpp b/pica-client/videodshow.cpp
new file mode 100644
index 0000000..394798a
--- /dev/null
+++ b/pica-client/videodshow.cpp
@@ -0,0 +1,172 @@
+/*
+ (c) Copyright 2012 - 2026 Anton Sviridenko
+ https://picapica.im
+
+ This program is free 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, version 3.
+
+ This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
+*/
+#include "mediadevice.h"
+
+#include <QtGlobal>
+
+#ifdef Q_OS_WIN
+
+#include <QDebug>
+#include <QList>
+#include <QString>
+
+// INITGUID before the DirectShow headers so CLSID_SystemDeviceEnum and the
+// rest get defined here rather than needing strmiids at link time. The GUIDs
+// are emitted with DECLSPEC_SELECTANY, so the ones this shares with
+// nativeaudio_wasapi.cpp merge rather than clashing.
+#define INITGUID
+#include <initguid.h>
+
+#include <windows.h>
+#include <objbase.h>
+#include <dshow.h>
+
+static QString wideToQString(LPCWSTR s)
+{
+ return s ? QString::fromWCharArray(s) : QString();
+}
+
+// Lists the cameras DirectShow knows about.
+//
+// Capture itself goes through FFmpeg's "dshow" demuxer rather than through
+// DirectShow directly - it does the job and there is no equivalent of the
+// audio situation here, where the platform had something we could not reach
+// any other way. But the demuxer has no enumeration API: it can only print
+// device names to the log with -list_devices, which is no use to a settings
+// dialog. So the list is built here, from the same device category the
+// demuxer itself walks, and the names handed back are the ones it matches on.
+QList<MediaDeviceInfo> pica_enumerate_dshow_video()
+{
+ QList<MediaDeviceInfo> result;
+
+ // Qt has already put the GUI thread into an apartment; the reference is
+ // still ours to balance, hence the matching CoUninitialize() below.
+ bool didInit = false;
+ HRESULT hr = CoInitializeEx(nullptr, COINIT_MULTITHREADED);
+ if (SUCCEEDED(hr))
+ didInit = true;
+ else if (hr != RPC_E_CHANGED_MODE)
+ return result;
+
+ ICreateDevEnum *devEnum = nullptr;
+ hr = CoCreateInstance(CLSID_SystemDeviceEnum, nullptr, CLSCTX_INPROC_SERVER,
+ IID_ICreateDevEnum, (void **)&devEnum);
+ if (FAILED(hr) || !devEnum)
+ {
+ qWarning() << "DirectShow: could not create the device enumerator"
+ << QString("0x%1").arg((quint32)hr, 8, 16, QLatin1Char('0'));
+ if (didInit)
+ CoUninitialize();
+ return result;
+ }
+
+ IEnumMoniker *enumMoniker = nullptr;
+ hr = devEnum->CreateClassEnumerator(CLSID_VideoInputDeviceCategory, &enumMoniker, 0);
+
+ // S_FALSE with a null enumerator is how "no cameras at all" comes back.
+ if (hr != S_OK || !enumMoniker)
+ {
+ devEnum->Release();
+ if (didInit)
+ CoUninitialize();
+ return result;
+ }
+
+ int index = 0;
+ QStringList seenNames;
+ IMoniker *moniker = nullptr;
+
+ while (enumMoniker->Next(1, &moniker, nullptr) == S_OK)
+ {
+ IPropertyBag *bag = nullptr;
+ if (FAILED(moniker->BindToStorage(nullptr, nullptr, IID_IPropertyBag, (void **)&bag)) || !bag)
+ {
+ moniker->Release();
+ continue;
+ }
+
+ QString friendlyName;
+ QString uniqueName;
+
+ VARIANT var;
+ VariantInit(&var);
+ if (SUCCEEDED(bag->Read(L"FriendlyName", &var, nullptr)) && var.vt == VT_BSTR)
+ friendlyName = wideToQString(var.bstrVal);
+ VariantClear(&var);
+
+ VariantInit(&var);
+ if (SUCCEEDED(bag->Read(L"DevicePath", &var, nullptr)) && var.vt == VT_BSTR)
+ {
+ // FFmpeg calls this the device's "alternative name" and matches on
+ // it as well as on the friendly one, after replacing colons -
+ // which it uses as a separator in its own device strings.
+ uniqueName = wideToQString(var.bstrVal).replace(QLatin1Char(':'), QLatin1Char('_'));
+ }
+ VariantClear(&var);
+
+ bag->Release();
+ moniker->Release();
+
+ if (friendlyName.isEmpty())
+ continue;
+
+ MediaDeviceInfo d;
+ d.humanReadable = friendlyName;
+
+ // The friendly name is the documented way to name a dshow device and
+ // the one to prefer. It is not required to be unique though, and two
+ // identical cameras would both answer to it - the demuxer would open
+ // whichever came first. Later duplicates therefore fall back to the
+ // device path, which is unique by construction.
+ if (!seenNames.contains(friendlyName))
+ {
+ d.device = friendlyName;
+ seenNames << friendlyName;
+ }
+ else if (!uniqueName.isEmpty())
+ {
+ d.device = uniqueName;
+ d.humanReadable += QString(" (%1)").arg(seenNames.count(friendlyName) + 1);
+ seenNames << friendlyName;
+ }
+ else
+ {
+ // Same name, no device path to tell them apart. Listing it again
+ // would just give the user two entries that open the same camera.
+ continue;
+ }
+
+ d.index = index++;
+
+ // Left empty deliberately. compressedFormats drives the "prefer
+ // compressed formats" setting, and working out what a DirectShow
+ // camera can deliver means walking its output pin's media types
+ // through IAMStreamConfig - a good deal more work than the v4l2 call
+ // that fills this in on Linux, and not needed to get a picture.
+ result << d;
+ }
+
+ enumMoniker->Release();
+ devEnum->Release();
+
+ if (didInit)
+ CoUninitialize();
+
+ return result;
+}
+
+#endif // Q_OS_WIN