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