summaryrefslogtreecommitdiffstats
path: root/pica-client/videodshow.cpp
blob: 03edbd6850d5daa9fe82ab7c16d3cd7c13cd9d49 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
/*
	(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 "videodevice.h"

#include <QtGlobal>

#ifdef Q_OS_WIN

#include <QDebug>
#include <QList>
#include <QMap>
#include <QPair>
#include <QString>
#include <QStringList>
#include <QVector>
#include <algorithm>
#include <functional>

// 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>
// VIDEOINFOHEADER2 lives here rather than in dshow.h, which only brings in the
// FORMAT_VideoInfo2 GUID that names it.
#include <dvdmedia.h>

static QString wideToQString(LPCWSTR s)
{
	return s ? QString::fromWCharArray(s) : QString();
}

// Compressed formats worth forwarding untouched, best first. Same order and
// the same FFmpeg codec names as kCompressedFormatPreference in
// videodevice.cpp - that table is keyed by v4l2 pixel format, this one by the
// FourCC DirectShow reports in BITMAPINFOHEADER::biCompression, but they mean
// the same thing and should be kept in step.
//
// Cameras are inconsistent about which FourCC they use for the same stream,
// so several map to one codec.
static const struct
{
	const char *fourcc;
	const char *ffmpeg_codec;
} kDshowCompressedFormats[] =
{
	{ "HEVC", "hevc"  },
	{ "hevc", "hevc"  },
	{ "H265", "hevc"  },
	{ "h265", "hevc"  },
	{ "H264", "h264"  },
	{ "h264", "h264"  },
	{ "X264", "h264"  },
	{ "x264", "h264"  },
	{ "AVC1", "h264"  },
	{ "avc1", "h264"  },
	{ "MJPG", "mjpeg" },
	{ "mjpg", "mjpeg" },
	{ "dmb1", "mjpeg" },
	{ "JPEG", "mjpeg" },
};

static QString fourccToString(DWORD fourcc)
{
	char c[5];
	c[0] = (char)(fourcc & 0xff);
	c[1] = (char)((fourcc >> 8) & 0xff);
	c[2] = (char)((fourcc >> 16) & 0xff);
	c[3] = (char)((fourcc >> 24) & 0xff);
	c[4] = 0;

	// Uncompressed types use small numbers rather than four characters -
	// BI_RGB is 0 - which would otherwise come out as an unreadable string.
	for (int i = 0; i < 4; i++)
	{
		if (c[i] < 0x20 || c[i] > 0x7e)
			return QString("0x%1").arg((quint32)fourcc, 8, 16, QLatin1Char('0'));
	}

	return QString::fromLatin1(c);
}

// AM_MEDIA_TYPE owns two allocations of its own. DeleteMediaType() would do
// this, but it lives in the DirectShow base classes rather than in any library
// worth pulling in for one function.
static void freeMediaType(AM_MEDIA_TYPE *mt)
{
	if (!mt)
		return;

	if (mt->cbFormat && mt->pbFormat)
		CoTaskMemFree(mt->pbFormat);
	if (mt->pUnk)
		mt->pUnk->Release();

	CoTaskMemFree(mt);
}

// Finds the camera behind one of the device strings enumeration handed out,
// matching on either name for the same reason FFmpeg's demuxer does: the
// friendly name for the common case, the device path where that was not
// unique. Caller releases the moniker.
static IMoniker *findCamera(const QString &device)
{
	ICreateDevEnum *devEnum = nullptr;
	if (FAILED(CoCreateInstance(CLSID_SystemDeviceEnum, nullptr, CLSCTX_INPROC_SERVER,
	                            IID_ICreateDevEnum, (void **)&devEnum)) || !devEnum)
		return nullptr;

	IEnumMoniker *enumMoniker = nullptr;
	HRESULT hr = devEnum->CreateClassEnumerator(CLSID_VideoInputDeviceCategory, &enumMoniker, 0);
	devEnum->Release();

	if (hr != S_OK || !enumMoniker)
		return nullptr;

	IMoniker *moniker = nullptr;
	IMoniker *found = nullptr;

	while (!found && enumMoniker->Next(1, &moniker, nullptr) == S_OK)
	{
		IPropertyBag *bag = nullptr;
		if (SUCCEEDED(moniker->BindToStorage(nullptr, nullptr, IID_IPropertyBag, (void **)&bag)) && bag)
		{
			VARIANT var;

			VariantInit(&var);
			if (SUCCEEDED(bag->Read(L"FriendlyName", &var, nullptr)) && var.vt == VT_BSTR &&
			    wideToQString(var.bstrVal) == device)
			{
				found = moniker;
			}
			VariantClear(&var);

			if (!found)
			{
				VariantInit(&var);
				if (SUCCEEDED(bag->Read(L"DevicePath", &var, nullptr)) && var.vt == VT_BSTR &&
				    wideToQString(var.bstrVal).replace(QLatin1Char(':'), QLatin1Char('_')) == device)
				{
					found = moniker;
				}
				VariantClear(&var);
			}

			bag->Release();
		}

		if (!found)
			moniker->Release();
	}

	enumMoniker->Release();
	return found;
}

// Which compressed formats a camera can hand over ready-made, best first.
//
// The counterpart of the v4l2 VIDIOC_ENUM_FMT walk in
// VideoDevice::CompressedFormats(). DirectShow has no single call for it: the
// camera has to be instantiated, its output pin found, and the pin's stream
// capabilities enumerated one media type at a time.
//
// Note this instantiates the capture filter, which is also the step that fails
// when Windows is refusing the app access to the camera - so an empty list
// here can mean either "offers nothing compressed" or "cannot be opened at
// all". Both lead to the same place, the caller simply transcodes instead.
QStringList pica_dshow_compressed_formats(const QString &device)
{
	QStringList result;

	if (device.isEmpty())
		return result;

	bool didInit = false;
	HRESULT hr = CoInitializeEx(nullptr, COINIT_MULTITHREADED);
	if (SUCCEEDED(hr))
		didInit = true;
	else if (hr != RPC_E_CHANGED_MODE)
		return result;

	IMoniker *moniker = findCamera(device);
	if (!moniker)
	{
		if (didInit)
			CoUninitialize();
		return result;
	}

	IBaseFilter *filter = nullptr;
	hr = moniker->BindToObject(nullptr, nullptr, IID_IBaseFilter, (void **)&filter);
	moniker->Release();

	if (FAILED(hr) || !filter)
	{
		qWarning() << "DirectShow: could not instantiate" << device
		           << "to ask what formats it offers";
		if (didInit)
			CoUninitialize();
		return result;
	}

	QStringList offered;

	IEnumPins *enumPins = nullptr;
	if (SUCCEEDED(filter->EnumPins(&enumPins)) && enumPins)
	{
		IPin *pin = nullptr;

		while (enumPins->Next(1, &pin, nullptr) == S_OK)
		{
			PIN_DIRECTION dir;
			IAMStreamConfig *config = nullptr;

			if (SUCCEEDED(pin->QueryDirection(&dir)) && dir == PINDIR_OUTPUT &&
			    SUCCEEDED(pin->QueryInterface(IID_IAMStreamConfig, (void **)&config)) && config)
			{
				int count = 0, size = 0;

				if (SUCCEEDED(config->GetNumberOfCapabilities(&count, &size)) && size > 0)
				{
					QVector<quint8> caps(size);

					for (int i = 0; i < count; i++)
					{
						AM_MEDIA_TYPE *mt = nullptr;
						if (FAILED(config->GetStreamCaps(i, &mt, caps.data())) || !mt)
							continue;

						// Only the VIDEOINFOHEADER shapes carry a
						// BITMAPINFOHEADER, which is where the FourCC lives.
						const BITMAPINFOHEADER *bih = nullptr;

						if (mt->formattype == FORMAT_VideoInfo &&
						    mt->cbFormat >= sizeof(VIDEOINFOHEADER))
							bih = &((VIDEOINFOHEADER *)mt->pbFormat)->bmiHeader;
						else if (mt->formattype == FORMAT_VideoInfo2 &&
						         mt->cbFormat >= sizeof(VIDEOINFOHEADER2))
							bih = &((VIDEOINFOHEADER2 *)mt->pbFormat)->bmiHeader;

						if (bih)
						{
							QString fourcc = fourccToString(bih->biCompression);
							if (!offered.contains(fourcc))
								offered << fourcc;
						}

						freeMediaType(mt);
					}
				}
			}

			if (config)
				config->Release();
			pin->Release();
		}

		enumPins->Release();
	}

	filter->Release();

	// Worth logging whole, not just the ones we recognise. A camera offering
	// nothing but uncompressed types here - NV12, YUY2 - while advertising
	// MJPEG on the same hardware under v4l2 is the signature of the Windows
	// Camera Frame Server, which mediates integrated cameras and hands
	// DirectShow the already-decoded output. Nothing to be done about that
	// from here, but it is worth being able to tell it apart from a FourCC we
	// simply failed to map.
	qDebug() << "DirectShow:" << device << "offers"
	         << (offered.isEmpty() ? QStringLiteral("(nothing)") : offered.join(QLatin1String(", ")));

	// Report in our preference order rather than in the order the camera
	// happened to list them.
	for (unsigned int i = 0; i < sizeof(kDshowCompressedFormats) / sizeof(kDshowCompressedFormats[0]); i++)
	{
		QString codec = QLatin1String(kDshowCompressedFormats[i].ffmpeg_codec);

		if (offered.contains(QLatin1String(kDshowCompressedFormats[i].fourcc)) && !result.contains(codec))
			result << codec;
	}

	if (didInit)
		CoUninitialize();

	return result;
}

// Frame rates worth offering out of a pin that reports a range of frame
// intervals rather than one fixed rate. Same list, and the same reasoning, as
// kStandardFrameRates in videodevice.cpp.
static const int kDshowStandardFrameRates[] = { 60, 50, 30, 25, 24, 20, 15, 10, 5 };

// DirectShow measures a frame interval in 100 nanosecond units, and we want
// the rate, so this is a reciprocal like the v4l2 one.
static double intervalToFps(LONGLONG interval)
{
	if (interval <= 0)
		return 0.0;

	return 10000000.0 / (double)interval;
}

// Largest picture first - the same order VideoDevice::CaptureFormats() reports
// on Linux, so the settings dialog's list reads the same on both.
static bool dshowFormatLessThan(const VideoCaptureFormat &a, const VideoCaptureFormat &b)
{
	const qint64 areaA = (qint64)a.width * a.height;
	const qint64 areaB = (qint64)b.width * b.height;

	if (areaA != areaB)
		return areaA > areaB;

	return a.width > b.width;
}

// Every picture size the camera can deliver, with the frame rates and
// compressed formats available at each.
//
// The counterpart of the v4l2 VIDIOC_ENUM_FRAMESIZES/VIDIOC_ENUM_FRAMEINTERVALS
// walk in VideoDevice::CaptureFormats(), and it comes off the same
// IAMStreamConfig enumeration pica_dshow_compressed_formats() uses: each media
// type carries a size and a FourCC, and the VIDEO_STREAM_CONFIG_CAPS alongside
// it carries the range of frame intervals that size can be delivered at.
//
// The same caveat applies as there: this instantiates the capture filter, so
// an empty list can mean "cannot be opened at all" as much as "offers
// nothing". Either way the caller falls back to a set of common sizes.
QList<VideoCaptureFormat> pica_dshow_capture_formats(const QString &device)
{
	QList<VideoCaptureFormat> result;

	if (device.isEmpty())
		return result;

	bool didInit = false;
	HRESULT hr = CoInitializeEx(nullptr, COINIT_MULTITHREADED);
	if (SUCCEEDED(hr))
		didInit = true;
	else if (hr != RPC_E_CHANGED_MODE)
		return result;

	IMoniker *moniker = findCamera(device);
	if (!moniker)
	{
		if (didInit)
			CoUninitialize();
		return result;
	}

	IBaseFilter *filter = nullptr;
	hr = moniker->BindToObject(nullptr, nullptr, IID_IBaseFilter, (void **)&filter);
	moniker->Release();

	if (FAILED(hr) || !filter)
	{
		qWarning() << "DirectShow: could not instantiate" << device
		           << "to ask what sizes it offers";
		if (didInit)
			CoUninitialize();
		return result;
	}

	// Keyed on the size, because a camera lists one media type per size and
	// format combination and the same size comes back once per format.
	QMap<QPair<int, int>, VideoCaptureFormat> bySize;

	IEnumPins *enumPins = nullptr;
	if (SUCCEEDED(filter->EnumPins(&enumPins)) && enumPins)
	{
		IPin *pin = nullptr;

		while (enumPins->Next(1, &pin, nullptr) == S_OK)
		{
			PIN_DIRECTION dir;
			IAMStreamConfig *config = nullptr;

			if (SUCCEEDED(pin->QueryDirection(&dir)) && dir == PINDIR_OUTPUT &&
			    SUCCEEDED(pin->QueryInterface(IID_IAMStreamConfig, (void **)&config)) && config)
			{
				int count = 0, size = 0;

				if (SUCCEEDED(config->GetNumberOfCapabilities(&count, &size)) &&
				    size >= (int)sizeof(VIDEO_STREAM_CONFIG_CAPS))
				{
					QVector<quint8> caps(size);

					for (int i = 0; i < count; i++)
					{
						AM_MEDIA_TYPE *mt = nullptr;
						if (FAILED(config->GetStreamCaps(i, &mt, caps.data())) || !mt)
							continue;

						const BITMAPINFOHEADER *bih = nullptr;
						// The nominal interval of this particular media type,
						// as opposed to the range in the caps below.
						LONGLONG avgTimePerFrame = 0;

						if (mt->formattype == FORMAT_VideoInfo &&
						    mt->cbFormat >= sizeof(VIDEOINFOHEADER))
						{
							const VIDEOINFOHEADER *vih = (const VIDEOINFOHEADER *)mt->pbFormat;
							bih = &vih->bmiHeader;
							avgTimePerFrame = vih->AvgTimePerFrame;
						}
						else if (mt->formattype == FORMAT_VideoInfo2 &&
						         mt->cbFormat >= sizeof(VIDEOINFOHEADER2))
						{
							const VIDEOINFOHEADER2 *vih = (const VIDEOINFOHEADER2 *)mt->pbFormat;
							bih = &vih->bmiHeader;
							avgTimePerFrame = vih->AvgTimePerFrame;
						}

						if (!bih || bih->biWidth <= 0 || bih->biHeight == 0)
						{
							freeMediaType(mt);
							continue;
						}

						// A bottom-up DIB states its height negative; the
						// picture is the same size either way.
						const int width = (int)bih->biWidth;
						const int height = bih->biHeight < 0 ? (int)-bih->biHeight : (int)bih->biHeight;

						VideoCaptureFormat &entry = bySize[qMakePair(width, height)];
						entry.width = width;
						entry.height = height;

						const QString fourcc = fourccToString(bih->biCompression);

						// Empty for an uncompressed media type, which is what
						// pica_merge_capture_rates() wants for one.
						QString codec;

						for (unsigned int f = 0; f < sizeof(kDshowCompressedFormats) / sizeof(kDshowCompressedFormats[0]); f++)
						{
							if (fourcc != QLatin1String(kDshowCompressedFormats[f].fourcc))
								continue;

							codec = QLatin1String(kDshowCompressedFormats[f].ffmpeg_codec);

							if (!entry.compressedFormats.contains(codec))
								entry.compressedFormats << codec;

							break;
						}

						const VIDEO_STREAM_CONFIG_CAPS *scc = (const VIDEO_STREAM_CONFIG_CAPS *)caps.constData();

						// The shortest interval is the highest rate, hence the
						// crossed over names.
						const double maxFps = intervalToFps(scc->MinFrameInterval);
						const double minFps = intervalToFps(scc->MaxFrameInterval);

						// This media type is one size in one format, so the
						// rates it offers belong to that format - an
						// uncompressed pin capping out well below a compressed
						// one at the same size is the whole reason they are
						// kept apart, see VideoPixelFormat.
						QList<int> rates;

						int nominal = qRound(intervalToFps(avgTimePerFrame));

						if (nominal > 0)
							rates << nominal;

						// A pin that accepts a range of intervals will take
						// anything within it, so there is no list to show -
						// the standard rates inside the range stand in for one,
						// exactly as on the v4l2 side.
						for (unsigned int r = 0; r < sizeof(kDshowStandardFrameRates) / sizeof(kDshowStandardFrameRates[0]); r++)
						{
							const int fps = kDshowStandardFrameRates[r];

							if (fps >= minFps && fps <= maxFps && !rates.contains(fps))
								rates << fps;
						}

						pica_merge_capture_rates(entry, codec, rates);

						freeMediaType(mt);
					}
				}
			}

			if (config)
				config->Release();
			pin->Release();
		}

		enumPins->Release();
	}

	filter->Release();

	for (QMap<QPair<int, int>, VideoCaptureFormat>::iterator it = bySize.begin(); it != bySize.end(); ++it)
	{
		VideoCaptureFormat entry = it.value();

		// Report the formats in our preference order rather than in the order
		// the camera happened to list them, the same way
		// pica_dshow_compressed_formats() does.
		QStringList ordered;

		for (unsigned int f = 0; f < sizeof(kDshowCompressedFormats) / sizeof(kDshowCompressedFormats[0]); f++)
		{
			QString codec = QLatin1String(kDshowCompressedFormats[f].ffmpeg_codec);

			if (entry.compressedFormats.contains(codec) && !ordered.contains(codec))
				ordered << codec;
		}

		entry.compressedFormats = ordered;

		pica_finish_capture_format(entry);

		result << entry;
	}

	std::sort(result.begin(), result.end(), dshowFormatLessThan);

	if (didInit)
		CoUninitialize();

	return result;
}

// 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++;

		// Nothing here about what the camera can deliver: that means walking
		// its output pin's media types, which pica_dshow_capture_formats()
		// does once a camera has actually been selected rather than for every
		// camera on the machine.
		result << d;
	}

	enumMoniker->Release();
	devEnum->Release();

	if (didInit)
		CoUninitialize();

	return result;
}

#endif // Q_OS_WIN