Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qwindowsmediadevicereader.cpp
Go to the documentation of this file.
1// Copyright (C) 2021 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
5
6#include "private/qwindowsmultimediautils_p.h"
7#include <qvideosink.h>
8#include <qmediadevices.h>
9#include <qaudiodevice.h>
10#include <private/qmemoryvideobuffer_p.h>
11#include <private/qvideoframe_p.h>
12#include <QtCore/private/qcomptr_p.h>
13#include <QtCore/qdebug.h>
14
15#include <mmdeviceapi.h>
16#include <mfidl.h>
17#include <cguid.h>
18
20
21enum { MEDIA_TYPE_INDEX_DEFAULT = 0xffffffff };
22
23QWindowsMediaDeviceReader::QWindowsMediaDeviceReader(QObject *parent)
24 : QObject(parent)
25{
26 m_durationTimer.setInterval(100);
27 connect(&m_durationTimer, &QTimer::timeout, this, &QWindowsMediaDeviceReader::updateDuration);
28}
29
35
36// Creates a video or audio media source specified by deviceId (symbolic link)
37HRESULT QWindowsMediaDeviceReader::createSource(const QString &deviceId, bool video, IMFMediaSource **source)
38{
39 if (!source)
40 return E_INVALIDARG;
41
42 *source = nullptr;
43 IMFAttributes *sourceAttributes = nullptr;
44
45 HRESULT hr = MFCreateAttributes(&sourceAttributes, 2);
46 if (SUCCEEDED(hr)) {
47
48 hr = sourceAttributes->SetGUID(MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE,
49 video ? MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE_VIDCAP_GUID
50 : MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE_AUDCAP_GUID);
51 if (SUCCEEDED(hr)) {
52
53 hr = sourceAttributes->SetString(video ? MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE_VIDCAP_SYMBOLIC_LINK
54 : MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE_AUDCAP_ENDPOINT_ID,
55 reinterpret_cast<LPCWSTR>(deviceId.utf16()));
56 if (SUCCEEDED(hr)) {
57
58 hr = MFCreateDeviceSource(sourceAttributes, source);
59 }
60 }
61 sourceAttributes->Release();
62 }
63
64 return hr;
65}
66
67// Creates a source/reader aggregating two other sources (video/audio).
68// If one of the sources is null the result will be video-only or audio-only.
69HRESULT QWindowsMediaDeviceReader::createAggregateReader(IMFMediaSource *firstSource,
70 IMFMediaSource *secondSource,
71 IMFMediaSource **aggregateSource,
72 IMFSourceReader **sourceReader)
73{
74 if ((!firstSource && !secondSource) || !aggregateSource || !sourceReader)
75 return E_INVALIDARG;
76
77 *aggregateSource = nullptr;
78 *sourceReader = nullptr;
79
80 IMFCollection *sourceCollection = nullptr;
81
82 HRESULT hr = MFCreateCollection(&sourceCollection);
83 if (SUCCEEDED(hr)) {
84
85 if (firstSource)
86 sourceCollection->AddElement(firstSource);
87
88 if (secondSource)
89 sourceCollection->AddElement(secondSource);
90
91 hr = MFCreateAggregateSource(sourceCollection, aggregateSource);
92 if (SUCCEEDED(hr)) {
93
94 IMFAttributes *readerAttributes = nullptr;
95
96 hr = MFCreateAttributes(&readerAttributes, 1);
97 if (SUCCEEDED(hr)) {
98
99 // Set callback so OnReadSample() is called for each new video frame or audio sample.
100 hr = readerAttributes->SetUnknown(MF_SOURCE_READER_ASYNC_CALLBACK,
101 static_cast<IMFSourceReaderCallback*>(this));
102 if (SUCCEEDED(hr)) {
103
104 hr = MFCreateSourceReaderFromMediaSource(*aggregateSource, readerAttributes, sourceReader);
105 }
106 readerAttributes->Release();
107 }
108 }
109 sourceCollection->Release();
110 }
111 return hr;
112}
113
114// Selects the requested resolution/frame rate (if specified),
115// or chooses a high quality configuration otherwise.
116DWORD QWindowsMediaDeviceReader::findMediaTypeIndex(const QCameraFormat &reqFormat)
117{
118 DWORD mediaIndex = MEDIA_TYPE_INDEX_DEFAULT;
119
120 if (m_sourceReader && m_videoSource) {
121
122 DWORD index = 0;
123 IMFMediaType *mediaType = nullptr;
124
125 UINT32 currArea = 0;
126 float currFrameRate = 0.0f;
127
128 while (SUCCEEDED(m_sourceReader->GetNativeMediaType(DWORD(MF_SOURCE_READER_FIRST_VIDEO_STREAM),
129 index, &mediaType))) {
130
131 GUID subtype = GUID_NULL;
132 if (SUCCEEDED(mediaType->GetGUID(MF_MT_SUBTYPE, &subtype))) {
133
134 auto pixelFormat = QWindowsMultimediaUtils::pixelFormatFromMediaSubtype(subtype);
135 if (pixelFormat != QVideoFrameFormat::Format_Invalid) {
136
137 UINT32 width, height;
138 if (SUCCEEDED(MFGetAttributeSize(mediaType, MF_MT_FRAME_SIZE, &width, &height))) {
139
140 UINT32 num, den;
141 if (SUCCEEDED(MFGetAttributeRatio(mediaType, MF_MT_FRAME_RATE, &num, &den))) {
142
143 UINT32 area = width * height;
144 float frameRate = float(num) / den;
145
146 if (!reqFormat.isNull()
147 && UINT32(reqFormat.resolution().width()) == width
148 && UINT32(reqFormat.resolution().height()) == height
149 && QtPrivate::fuzzyCompare(reqFormat.maxFrameRate(), frameRate)
150 && reqFormat.pixelFormat() == pixelFormat) {
151 mediaType->Release();
152 return index;
153 }
154
155 if ((currFrameRate < 29.9 && currFrameRate < frameRate) ||
156 (currFrameRate == frameRate && currArea < area)) {
157 currArea = area;
158 currFrameRate = frameRate;
159 mediaIndex = index;
160 }
161 }
162 }
163 }
164 }
165 mediaType->Release();
166 ++index;
167 }
168 }
169
170 return mediaIndex;
171}
172
173
174// Prepares the source video stream and gets some metadata.
175HRESULT QWindowsMediaDeviceReader::prepareVideoStream(DWORD mediaTypeIndex)
176{
177 if (!m_sourceReader)
178 return E_FAIL;
179
180 if (!m_videoSource)
181 return S_OK; // It may be audio-only
182
183 HRESULT hr;
184
185 if (mediaTypeIndex == MEDIA_TYPE_INDEX_DEFAULT) {
186 hr = m_sourceReader->GetCurrentMediaType(DWORD(MF_SOURCE_READER_FIRST_VIDEO_STREAM),
187 &m_videoMediaType);
188 } else {
189 hr = m_sourceReader->GetNativeMediaType(DWORD(MF_SOURCE_READER_FIRST_VIDEO_STREAM),
190 mediaTypeIndex, &m_videoMediaType);
191 if (SUCCEEDED(hr))
192 hr = m_sourceReader->SetCurrentMediaType(DWORD(MF_SOURCE_READER_FIRST_VIDEO_STREAM),
193 nullptr, m_videoMediaType);
194 }
195
196 if (SUCCEEDED(hr)) {
197
198 GUID subtype = GUID_NULL;
199 hr = m_videoMediaType->GetGUID(MF_MT_SUBTYPE, &subtype);
200 if (SUCCEEDED(hr)) {
201
202 m_pixelFormat = QWindowsMultimediaUtils::pixelFormatFromMediaSubtype(subtype);
203
204 if (m_pixelFormat == QVideoFrameFormat::Format_Invalid) {
205 hr = E_FAIL;
206 } else {
207
208 // get the frame dimensions
209 hr = MFGetAttributeSize(m_videoMediaType, MF_MT_FRAME_SIZE, &m_frameWidth, &m_frameHeight);
210 if (SUCCEEDED(hr)) {
211
212 // and the stride, which we need to convert the frame later
213 hr = MFGetStrideForBitmapInfoHeader(subtype.Data1, m_frameWidth, &m_stride);
214 if (SUCCEEDED(hr)) {
215 m_stride = qAbs(m_stride);
216 UINT32 frameRateNum, frameRateDen;
217 hr = MFGetAttributeRatio(m_videoMediaType, MF_MT_FRAME_RATE, &frameRateNum, &frameRateDen);
218 if (SUCCEEDED(hr)) {
219
220 m_frameRate = qreal(frameRateNum) / frameRateDen;
221
222 hr = m_sourceReader->SetStreamSelection(DWORD(MF_SOURCE_READER_FIRST_VIDEO_STREAM), TRUE);
223
224 UINT32 nominalRange = 0;
225
226 if (SUCCEEDED(m_videoMediaType->GetUINT32(MF_MT_VIDEO_NOMINAL_RANGE, &nominalRange)))
227 m_colorRange = QWindowsMultimediaUtils::colorRangeFromNominalRange(nominalRange);
228
229 UINT32 yuvMatrix = 0;
230
231 if (SUCCEEDED(m_videoMediaType->GetUINT32(MF_MT_YUV_MATRIX, &yuvMatrix))) {
232 m_colorSpace = QWindowsMultimediaUtils::colorSpaceFromMatrix(yuvMatrix);
233 }
234 }
235 }
236 }
237 }
238 }
239 }
240
241 return hr;
242}
243
244HRESULT QWindowsMediaDeviceReader::initAudioType(IMFMediaType *mediaType, UINT32 channels, UINT32 samplesPerSec, bool flt)
245{
246 if (!mediaType)
247 return E_INVALIDARG;
248
249 HRESULT hr = mediaType->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Audio);
250 if (SUCCEEDED(hr)) {
251 hr = mediaType->SetGUID(MF_MT_SUBTYPE, flt ? MFAudioFormat_Float : MFAudioFormat_PCM);
252 if (SUCCEEDED(hr)) {
253 hr = mediaType->SetUINT32(MF_MT_AUDIO_NUM_CHANNELS, channels);
254 if (SUCCEEDED(hr)) {
255 hr = mediaType->SetUINT32(MF_MT_AUDIO_CHANNEL_MASK,
256 (channels == 1) ? SPEAKER_FRONT_CENTER : (SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT ));
257 if (SUCCEEDED(hr)) {
258 hr = mediaType->SetUINT32(MF_MT_AUDIO_SAMPLES_PER_SECOND, samplesPerSec);
259 if (SUCCEEDED(hr)) {
260 UINT32 bitsPerSample = flt ? 32 : 16;
261 UINT32 bytesPerFrame = channels * bitsPerSample/8;
262 hr = mediaType->SetUINT32(MF_MT_AUDIO_BITS_PER_SAMPLE, bitsPerSample);
263 if (SUCCEEDED(hr)) {
264 hr = mediaType->SetUINT32(MF_MT_AUDIO_BLOCK_ALIGNMENT, bytesPerFrame);
265 if (SUCCEEDED(hr)) {
266 hr = mediaType->SetUINT32(MF_MT_AUDIO_AVG_BYTES_PER_SECOND, bytesPerFrame * samplesPerSec);
267 }
268 }
269 }
270 }
271 }
272 }
273 }
274
275 return hr;
276}
277
278// Prepares the source audio stream.
279HRESULT QWindowsMediaDeviceReader::prepareAudioStream()
280{
281 if (!m_sourceReader)
282 return E_FAIL;
283
284 if (!m_audioSource)
285 return S_OK; // It may be video-only
286
287 HRESULT hr = m_sourceReader->GetCurrentMediaType(DWORD(MF_SOURCE_READER_FIRST_AUDIO_STREAM),
288 &m_audioMediaType);
289 if (SUCCEEDED(hr)) {
290 hr = initAudioType(m_audioMediaType, 2, 48000, true);
291 if (SUCCEEDED(hr)) {
292 hr = m_sourceReader->SetCurrentMediaType(DWORD(MF_SOURCE_READER_FIRST_AUDIO_STREAM),
293 nullptr, m_audioMediaType);
294 if (SUCCEEDED(hr)) {
295 hr = m_sourceReader->SetStreamSelection(DWORD(MF_SOURCE_READER_FIRST_AUDIO_STREAM), TRUE);
296 }
297 }
298 }
299 return hr;
300}
301
302// Retrieves the indexes for selected video/audio streams.
303HRESULT QWindowsMediaDeviceReader::initSourceIndexes()
304{
305 if (!m_sourceReader)
306 return E_FAIL;
307
308 m_sourceVideoStreamIndex = MF_SOURCE_READER_INVALID_STREAM_INDEX;
309 m_sourceAudioStreamIndex = MF_SOURCE_READER_INVALID_STREAM_INDEX;
310
311 DWORD index = 0;
312 BOOL selected = FALSE;
313
314 while (m_sourceReader->GetStreamSelection(index, &selected) == S_OK) {
315 if (selected) {
316 IMFMediaType *mediaType = nullptr;
317 if (SUCCEEDED(m_sourceReader->GetCurrentMediaType(index, &mediaType))) {
318 GUID majorType = GUID_NULL;
319 if (SUCCEEDED(mediaType->GetGUID(MF_MT_MAJOR_TYPE, &majorType))) {
320 if (majorType == MFMediaType_Video)
321 m_sourceVideoStreamIndex = index;
322 else if (majorType == MFMediaType_Audio)
323 m_sourceAudioStreamIndex = index;
324 }
325 mediaType->Release();
326 }
327 }
328 ++index;
329 }
330 if ((m_videoSource && m_sourceVideoStreamIndex == MF_SOURCE_READER_INVALID_STREAM_INDEX) ||
331 (m_audioSource && m_sourceAudioStreamIndex == MF_SOURCE_READER_INVALID_STREAM_INDEX))
332 return E_FAIL;
333 return S_OK;
334}
335
336bool QWindowsMediaDeviceReader::setAudioOutput(const QString &audioOutputId)
337{
338 QMutexLocker locker(&m_mutex);
339
340 stopMonitoring();
341
342 m_audioOutputId = audioOutputId;
343
344 if (!m_active || m_audioOutputId.isEmpty())
345 return true;
346
347 HRESULT hr = startMonitoring();
348
349 return SUCCEEDED(hr);
350}
351
352HRESULT QWindowsMediaDeviceReader::startMonitoring()
353{
354 if (m_audioOutputId.isEmpty())
355 return E_FAIL;
356
357 IMFAttributes *sinkAttributes = nullptr;
358
359 HRESULT hr = MFCreateAttributes(&sinkAttributes, 1);
360 if (SUCCEEDED(hr)) {
361
362 hr = sinkAttributes->SetString(MF_AUDIO_RENDERER_ATTRIBUTE_ENDPOINT_ID,
363 reinterpret_cast<LPCWSTR>(m_audioOutputId.utf16()));
364 if (SUCCEEDED(hr)) {
365
366 IMFMediaSink *mediaSink = nullptr;
367 hr = MFCreateAudioRenderer(sinkAttributes, &mediaSink);
368 if (SUCCEEDED(hr)) {
369
370 IMFStreamSink *streamSink = nullptr;
371 hr = mediaSink->GetStreamSinkByIndex(0, &streamSink);
372 if (SUCCEEDED(hr)) {
373
374 IMFMediaTypeHandler *typeHandler = nullptr;
375 hr = streamSink->GetMediaTypeHandler(&typeHandler);
376 if (SUCCEEDED(hr)) {
377
378 hr = typeHandler->IsMediaTypeSupported(m_audioMediaType, nullptr);
379 if (SUCCEEDED(hr)) {
380
381 hr = typeHandler->SetCurrentMediaType(m_audioMediaType);
382 if (SUCCEEDED(hr)) {
383
384 IMFAttributes *writerAttributes = nullptr;
385
386 HRESULT hr = MFCreateAttributes(&writerAttributes, 1);
387 if (SUCCEEDED(hr)) {
388
389 hr = writerAttributes->SetUINT32(MF_SINK_WRITER_DISABLE_THROTTLING, TRUE);
390 if (SUCCEEDED(hr)) {
391
392 IMFSinkWriter *sinkWriter = nullptr;
393 hr = MFCreateSinkWriterFromMediaSink(mediaSink, writerAttributes, &sinkWriter);
394 if (SUCCEEDED(hr)) {
395
396 hr = sinkWriter->SetInputMediaType(0, m_audioMediaType, nullptr);
397 if (SUCCEEDED(hr)) {
398
399 IMFSimpleAudioVolume *audioVolume = nullptr;
400
401 if (SUCCEEDED(MFGetService(
402 mediaSink, MR_POLICY_VOLUME_SERVICE,
403 IID_PPV_ARGS(&audioVolume)))) {
404 audioVolume->SetMasterVolume(float(m_outputVolume));
405 audioVolume->SetMute(m_outputMuted);
406 audioVolume->Release();
407 }
408
409 hr = sinkWriter->BeginWriting();
410 if (SUCCEEDED(hr)) {
411 m_monitorSink = mediaSink;
412 m_monitorSink->AddRef();
413 m_monitorWriter = sinkWriter;
414 m_monitorWriter->AddRef();
415 }
416 }
417 sinkWriter->Release();
418 }
419 }
420 writerAttributes->Release();
421 }
422 }
423 }
424 typeHandler->Release();
425 }
426 streamSink->Release();
427 }
428 mediaSink->Release();
429 }
430 }
431 sinkAttributes->Release();
432 }
433
434 return hr;
435}
436
437void QWindowsMediaDeviceReader::stopMonitoring()
438{
439 if (m_monitorWriter) {
440 m_monitorWriter->Release();
441 m_monitorWriter = nullptr;
442 }
443 if (m_monitorSink) {
444 m_monitorSink->Shutdown();
445 m_monitorSink->Release();
446 m_monitorSink = nullptr;
447 }
448}
449
450// Activates the requested camera/microphone for streaming.
451// One of the IDs may be empty for video-only/audio-only.
452bool QWindowsMediaDeviceReader::activate(const QString &cameraId,
453 const QCameraFormat &cameraFormat,
454 const QString &microphoneId)
455{
456 QMutexLocker locker(&m_mutex);
457
458 if (cameraId.isEmpty() && microphoneId.isEmpty())
459 return false;
460
461 stopMonitoring();
462 releaseResources();
463
464 m_active = false;
465 m_streaming = false;
466
467 if (!cameraId.isEmpty()) {
468 if (!SUCCEEDED(createSource(cameraId, true, &m_videoSource))) {
469 releaseResources();
470 return false;
471 }
472 }
473
474 if (!microphoneId.isEmpty()) {
475 if (!SUCCEEDED(createSource(microphoneId, false, &m_audioSource))) {
476 releaseResources();
477 return false;
478 }
479 }
480
481 if (!SUCCEEDED(createAggregateReader(m_videoSource, m_audioSource, &m_aggregateSource, &m_sourceReader))) {
482 releaseResources();
483 return false;
484 }
485
486 DWORD mediaTypeIndex = findMediaTypeIndex(cameraFormat);
487
488 if (!SUCCEEDED(prepareVideoStream(mediaTypeIndex))) {
489 releaseResources();
490 return false;
491 }
492
493 if (!SUCCEEDED(prepareAudioStream())) {
494 releaseResources();
495 return false;
496 }
497
498 if (!SUCCEEDED(initSourceIndexes())) {
499 releaseResources();
500 return false;
501 }
502
503 updateSinkInputMediaTypes();
504 startMonitoring();
505
506 // Request the first frame or audio sample.
507 if (!SUCCEEDED(m_sourceReader->ReadSample(MF_SOURCE_READER_ANY_STREAM, 0, nullptr, nullptr, nullptr, nullptr))) {
508 releaseResources();
509 return false;
510 }
511
512 m_active = true;
513 return true;
514}
515
517{
518 stopMonitoring();
519 stopStreaming();
520 m_active = false;
521 m_streaming = false;
522}
523
524void QWindowsMediaDeviceReader::stopStreaming()
525{
526 QMutexLocker locker(&m_mutex);
527 releaseResources();
528}
529
530// Releases allocated streaming stuff.
531void QWindowsMediaDeviceReader::releaseResources()
532{
533 if (m_videoMediaType) {
534 m_videoMediaType->Release();
535 m_videoMediaType = nullptr;
536 }
537 if (m_audioMediaType) {
538 m_audioMediaType->Release();
539 m_audioMediaType = nullptr;
540 }
541 if (m_sourceReader) {
542 m_sourceReader->Release();
543 m_sourceReader = nullptr;
544 }
545 if (m_aggregateSource) {
546 m_aggregateSource->Release();
547 m_aggregateSource = nullptr;
548 }
549 if (m_videoSource) {
550 m_videoSource->Release();
551 m_videoSource = nullptr;
552 }
553 if (m_audioSource) {
554 m_audioSource->Release();
555 m_audioSource = nullptr;
556 }
557}
558
559HRESULT QWindowsMediaDeviceReader::createVideoMediaType(const GUID &format, UINT32 bitRate, UINT32 width,
560 UINT32 height, qreal frameRate, IMFMediaType **mediaType)
561{
562 if (!mediaType)
563 return E_INVALIDARG;
564
565 *mediaType = nullptr;
566 IMFMediaType *targetMediaType = nullptr;
567
568 if (SUCCEEDED(MFCreateMediaType(&targetMediaType))) {
569
570 if (SUCCEEDED(targetMediaType->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Video))) {
571
572 if (SUCCEEDED(targetMediaType->SetGUID(MF_MT_SUBTYPE, format))) {
573
574 if (SUCCEEDED(targetMediaType->SetUINT32(MF_MT_AVG_BITRATE, bitRate))) {
575
576 if (SUCCEEDED(MFSetAttributeSize(targetMediaType, MF_MT_FRAME_SIZE, width, height))) {
577
578 if (SUCCEEDED(MFSetAttributeRatio(targetMediaType, MF_MT_FRAME_RATE,
579 UINT32(frameRate * 1000), 1000))) {
580 UINT32 t1, t2;
581 if (SUCCEEDED(MFGetAttributeRatio(m_videoMediaType, MF_MT_PIXEL_ASPECT_RATIO, &t1, &t2))) {
582
583 if (SUCCEEDED(MFSetAttributeRatio(targetMediaType, MF_MT_PIXEL_ASPECT_RATIO, t1, t2))) {
584
585 if (SUCCEEDED(m_videoMediaType->GetUINT32(MF_MT_INTERLACE_MODE, &t1))) {
586
587 if (SUCCEEDED(targetMediaType->SetUINT32(MF_MT_INTERLACE_MODE, t1))) {
588
589 *mediaType = targetMediaType;
590 return S_OK;
591 }
592 }
593 }
594 }
595 }
596 }
597 }
598 }
599 }
600 targetMediaType->Release();
601 }
602 return E_FAIL;
603}
604
605HRESULT QWindowsMediaDeviceReader::createAudioMediaType(const GUID &format, UINT32 bitRate, IMFMediaType **mediaType)
606{
607 if (!mediaType)
608 return E_INVALIDARG;
609
610 *mediaType = nullptr;
611 IMFMediaType *targetMediaType = nullptr;
612
613 if (SUCCEEDED(MFCreateMediaType(&targetMediaType))) {
614
615 if (SUCCEEDED(targetMediaType->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Audio))) {
616
617 if (SUCCEEDED(targetMediaType->SetGUID(MF_MT_SUBTYPE, format))) {
618
619 if (bitRate == 0 || SUCCEEDED(targetMediaType->SetUINT32(MF_MT_AUDIO_AVG_BYTES_PER_SECOND, bitRate / 8))) {
620
621 *mediaType = targetMediaType;
622 return S_OK;
623 }
624 }
625 }
626 targetMediaType->Release();
627 }
628 return E_FAIL;
629}
630
631HRESULT QWindowsMediaDeviceReader::updateSinkInputMediaTypes()
632{
633 HRESULT hr = S_OK;
634 if (m_sinkWriter) {
635 if (m_videoSource && m_videoMediaType && m_sinkVideoStreamIndex != MF_SINK_WRITER_INVALID_STREAM_INDEX) {
636 hr = m_sinkWriter->SetInputMediaType(m_sinkVideoStreamIndex, m_videoMediaType, nullptr);
637 }
638 if (SUCCEEDED(hr)) {
639 if (m_audioSource && m_audioMediaType && m_sinkAudioStreamIndex != MF_SINK_WRITER_INVALID_STREAM_INDEX) {
640 hr = m_sinkWriter->SetInputMediaType(m_sinkAudioStreamIndex, m_audioMediaType, nullptr);
641 }
642 }
643 }
644 return hr;
645}
646
647QMediaRecorder::Error QWindowsMediaDeviceReader::startRecording(
648 const QString &fileName, const GUID &container, const GUID &videoFormat, UINT32 videoBitRate,
649 UINT32 width, UINT32 height, qreal frameRate, const GUID &audioFormat, UINT32 audioBitRate)
650{
651 QMutexLocker locker(&m_mutex);
652
653 if (!m_active || m_recording || (videoFormat == GUID_NULL && audioFormat == GUID_NULL))
654 return QMediaRecorder::ResourceError;
655
656 ComPtr<IMFAttributes> writerAttributes;
657
658 HRESULT hr = MFCreateAttributes(writerAttributes.GetAddressOf(), 2);
659 if (FAILED(hr))
660 return QMediaRecorder::ResourceError;
661
662 // Set callback so OnFinalize() is called after video is saved.
663 hr = writerAttributes->SetUnknown(MF_SINK_WRITER_ASYNC_CALLBACK,
664 static_cast<IMFSinkWriterCallback*>(this));
665 if (FAILED(hr))
666 return QMediaRecorder::ResourceError;
667
668 hr = writerAttributes->SetGUID(MF_TRANSCODE_CONTAINERTYPE, container);
669 if (FAILED(hr))
670 return QMediaRecorder::ResourceError;
671
672 ComPtr<IMFSinkWriter> sinkWriter;
673 hr = MFCreateSinkWriterFromURL(reinterpret_cast<LPCWSTR>(fileName.utf16()),
674 nullptr, writerAttributes.Get(), sinkWriter.GetAddressOf());
675 if (FAILED(hr))
676 return QMediaRecorder::LocationNotWritable;
677
678 m_sinkVideoStreamIndex = MF_SINK_WRITER_INVALID_STREAM_INDEX;
679 m_sinkAudioStreamIndex = MF_SINK_WRITER_INVALID_STREAM_INDEX;
680
681 if (m_videoSource && videoFormat != GUID_NULL) {
682 IMFMediaType *targetMediaType = nullptr;
683
684 hr = createVideoMediaType(videoFormat, videoBitRate, width, height, frameRate, &targetMediaType);
685 if (SUCCEEDED(hr)) {
686
687 hr = sinkWriter->AddStream(targetMediaType, &m_sinkVideoStreamIndex);
688 if (SUCCEEDED(hr)) {
689
690 hr = sinkWriter->SetInputMediaType(m_sinkVideoStreamIndex, m_videoMediaType, nullptr);
691 }
692 targetMediaType->Release();
693 }
694 }
695
696 if (SUCCEEDED(hr)) {
697 if (m_audioSource && audioFormat != GUID_NULL) {
698 IMFMediaType *targetMediaType = nullptr;
699
700 hr = createAudioMediaType(audioFormat, audioBitRate, &targetMediaType);
701 if (SUCCEEDED(hr)) {
702
703 hr = sinkWriter->AddStream(targetMediaType, &m_sinkAudioStreamIndex);
704 if (SUCCEEDED(hr)) {
705
706 hr = sinkWriter->SetInputMediaType(m_sinkAudioStreamIndex, m_audioMediaType, nullptr);
707 }
708 targetMediaType->Release();
709 }
710 }
711 }
712
713 if (FAILED(hr))
714 return QMediaRecorder::FormatError;
715
716 hr = sinkWriter->BeginWriting();
717 if (FAILED(hr))
718 return QMediaRecorder::ResourceError;
719
720 m_sinkWriter = sinkWriter.Detach();
721 m_lastDuration = -1;
722 m_currentDuration = 0;
723 updateDuration();
724 m_durationTimer.start();
725 m_recording = true;
726 m_firstFrame = true;
727 m_paused = false;
728 m_pauseChanging = false;
729
730 return QMediaRecorder::NoError;
731}
732
734{
735 QMutexLocker locker(&m_mutex);
736
737 if (m_sinkWriter && m_recording) {
738
739 HRESULT hr = m_sinkWriter->Finalize();
740
741 if (SUCCEEDED(hr)) {
742 m_hasFinalized.wait(&m_mutex);
743 } else {
744 m_sinkWriter->Release();
745 m_sinkWriter = nullptr;
746
747 QMetaObject::invokeMethod(this, "recordingError",
748 Qt::QueuedConnection, Q_ARG(int, hr));
749 }
750 }
751
752 m_recording = false;
753 m_paused = false;
754 m_pauseChanging = false;
755
756 m_durationTimer.stop();
757 m_lastDuration = -1;
758 m_currentDuration = -1;
759}
760
762{
763 if (!m_recording || m_paused)
764 return false;
765 m_pauseTime = m_lastTimestamp;
766 m_paused = true;
767 m_pauseChanging = true;
768 return true;
769}
770
772{
773 if (!m_recording || !m_paused)
774 return false;
775 m_paused = false;
776 m_pauseChanging = true;
777 return true;
778}
779
781{
782 return m_frameWidth;
783}
784
786{
787 return m_frameHeight;
788}
789
791{
792 return m_frameRate;
793}
794
796{
797 m_inputMuted = muted;
798}
799
801{
802 m_inputVolume = qBound(0.0, volume, 1.0);
803}
804
806{
807 QMutexLocker locker(&m_mutex);
808
809 m_outputMuted = muted;
810
811 if (m_active && m_monitorSink) {
812 IMFSimpleAudioVolume *audioVolume = nullptr;
813 if (SUCCEEDED(MFGetService(m_monitorSink, MR_POLICY_VOLUME_SERVICE,
814 IID_PPV_ARGS(&audioVolume)))) {
815 audioVolume->SetMute(m_outputMuted);
816 audioVolume->Release();
817 }
818 }
819}
820
822{
823 QMutexLocker locker(&m_mutex);
824
825 m_outputVolume = qBound(0.0, volume, 1.0);
826
827 if (m_active && m_monitorSink) {
828 IMFSimpleAudioVolume *audioVolume = nullptr;
829 if (SUCCEEDED(MFGetService(m_monitorSink, MR_POLICY_VOLUME_SERVICE,
830 IID_PPV_ARGS(&audioVolume)))) {
831 audioVolume->SetMasterVolume(float(m_outputVolume));
832 audioVolume->Release();
833 }
834 }
835}
836
837void QWindowsMediaDeviceReader::updateDuration()
838{
839 if (m_currentDuration >= 0 && m_lastDuration != m_currentDuration) {
840 m_lastDuration = m_currentDuration;
841 emit durationChanged(m_currentDuration);
842 }
843}
844
845//from IMFSourceReaderCallback
846STDMETHODIMP QWindowsMediaDeviceReader::OnReadSample(HRESULT hrStatus, DWORD dwStreamIndex,
847 DWORD dwStreamFlags, LONGLONG llTimestamp,
848 IMFSample *pSample)
849{
850 QMutexLocker locker(&m_mutex);
851
852 if (FAILED(hrStatus)) {
853 emit streamingError(int(hrStatus));
854 return hrStatus;
855 }
856
857 m_lastTimestamp = llTimestamp;
858
859 if ((dwStreamFlags & MF_SOURCE_READERF_ENDOFSTREAM) == MF_SOURCE_READERF_ENDOFSTREAM) {
860 m_streaming = false;
861 emit streamingStopped();
862 } else {
863
864 if (!m_streaming) {
865 m_streaming = true;
866 emit streamingStarted();
867 }
868 if (pSample) {
869
870 if (m_monitorWriter && dwStreamIndex == m_sourceAudioStreamIndex)
871 m_monitorWriter->WriteSample(0, pSample);
872
873 if (m_recording) {
874
875 if (m_firstFrame) {
876 m_timeOffset = llTimestamp;
877 m_firstFrame = false;
878 emit recordingStarted();
879 }
880
881 if (m_pauseChanging) {
882 // Recording time should not pass while paused.
883 if (m_paused)
884 m_pauseTime = llTimestamp;
885 else
886 m_timeOffset += llTimestamp - m_pauseTime;
887 m_pauseChanging = false;
888 }
889
890 // Send the video frame or audio sample to be encoded.
891 if (m_sinkWriter && !m_paused) {
892
893 pSample->SetSampleTime(llTimestamp - m_timeOffset);
894
895 if (dwStreamIndex == m_sourceVideoStreamIndex) {
896
897 m_sinkWriter->WriteSample(m_sinkVideoStreamIndex, pSample);
898
899 } else if (dwStreamIndex == m_sourceAudioStreamIndex) {
900
901 float volume = m_inputMuted ? 0.0f : float(m_inputVolume);
902
903 // Change the volume of the audio sample, if needed.
904 if (volume != 1.0f) {
905 IMFMediaBuffer *mediaBuffer = nullptr;
906 if (SUCCEEDED(pSample->ConvertToContiguousBuffer(&mediaBuffer))) {
907
908 DWORD bufLen = 0;
909 BYTE *buffer = nullptr;
910
911 if (SUCCEEDED(mediaBuffer->Lock(&buffer, nullptr, &bufLen))) {
912
913 float *floatBuffer = reinterpret_cast<float*>(buffer);
914
915 for (DWORD i = 0; i < bufLen/4; ++i)
916 floatBuffer[i] *= volume;
917
918 mediaBuffer->Unlock();
919 }
920 mediaBuffer->Release();
921 }
922 }
923
924 m_sinkWriter->WriteSample(m_sinkAudioStreamIndex, pSample);
925 }
926 m_currentDuration = (llTimestamp - m_timeOffset) / 10000;
927 }
928 }
929
930 // Generate a new QVideoFrame from IMFSample.
931 if (dwStreamIndex == m_sourceVideoStreamIndex) {
932 IMFMediaBuffer *mediaBuffer = nullptr;
933 if (SUCCEEDED(pSample->ConvertToContiguousBuffer(&mediaBuffer))) {
934
935 DWORD bufLen = 0;
936 BYTE *buffer = nullptr;
937
938 if (SUCCEEDED(mediaBuffer->Lock(&buffer, nullptr, &bufLen))) {
939 auto bytes = QByteArray(reinterpret_cast<char*>(buffer), bufLen);
940 QVideoFrameFormat format(QSize(m_frameWidth, m_frameHeight), m_pixelFormat);
941 format.setColorRange(m_colorRange);
942 format.setColorSpace(m_colorSpace);
943
944 QVideoFrame frame = QVideoFramePrivate::createFrame(
945 std::make_unique<QMemoryVideoBuffer>(std::move(bytes), m_stride),
946 std::move(format));
947
948 // WMF uses 100-nanosecond units, Qt uses microseconds
949 frame.setStartTime(llTimestamp * 0.1);
950
951 LONGLONG duration = -1;
952 if (SUCCEEDED(pSample->GetSampleDuration(&duration)))
953 frame.setEndTime((llTimestamp + duration) * 0.1);
954
955 emit videoFrameChanged(frame);
956
957 mediaBuffer->Unlock();
958 }
959 mediaBuffer->Release();
960 }
961 }
962 }
963 // request the next video frame or sound sample
964 if (m_sourceReader)
965 m_sourceReader->ReadSample(MF_SOURCE_READER_ANY_STREAM,
966 0, nullptr, nullptr, nullptr, nullptr);
967 }
968
969 return S_OK;
970}
971
976
978{
979 return S_OK;
980}
981
982//from IMFSinkWriterCallback
984{
985 QMutexLocker locker(&m_mutex);
986 if (m_sinkWriter) {
987 m_sinkWriter->Release();
988 m_sinkWriter = nullptr;
989 }
990 emit recordingStopped();
991 m_hasFinalized.notify_one();
992 return S_OK;
993}
994
999
1000QT_END_NAMESPACE
1001
1002#include "moc_qwindowsmediadevicereader_p.cpp"
bool activate(const QString &cameraId, const QCameraFormat &cameraFormat, const QString &microphoneId)
bool setAudioOutput(const QString &audioOutputId)
STDMETHODIMP OnFlush(DWORD dwStreamIndex) override
STDMETHODIMP OnFinalize(HRESULT hrStatus) override
STDMETHODIMP OnReadSample(HRESULT hrStatus, DWORD dwStreamIndex, DWORD dwStreamFlags, LONGLONG llTimestamp, IMFSample *pSample) override
STDMETHODIMP OnMarker(DWORD dwStreamIndex, LPVOID pvContext) override
STDMETHODIMP OnEvent(DWORD dwStreamIndex, IMFMediaEvent *pEvent) override
Combined button and popup list for selecting options.