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>
17#include <mmdeviceapi.h>
26 m_durationTimer.setInterval(100);
27 connect(&m_durationTimer, &QTimer::timeout,
this, &QWindowsMediaDeviceReader::updateDuration);
40 ComPtr<IMFAttributes> sourceAttributes;
42 HRESULT hr = MFCreateAttributes(sourceAttributes.GetAddressOf(), 2);
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);
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()));
55 hr = MFCreateDeviceSource(sourceAttributes.Get(), &source);
66 const ComPtr<IMFMediaSource> &secondSource,
67 ComPtr<IMFMediaSource> &aggregateSource,
68 ComPtr<IMFSourceReader> &sourceReader)
70 if (!firstSource && !secondSource)
73 aggregateSource =
nullptr;
74 sourceReader =
nullptr;
76 ComPtr<IMFCollection> sourceCollection;
78 HRESULT hr = MFCreateCollection(sourceCollection.GetAddressOf());
82 sourceCollection->AddElement(firstSource.Get());
85 sourceCollection->AddElement(secondSource.Get());
87 hr = MFCreateAggregateSource(sourceCollection.Get(), &aggregateSource);
90 ComPtr<IMFAttributes> readerAttributes;
92 hr = MFCreateAttributes(readerAttributes.GetAddressOf(), 1);
96 hr = readerAttributes->SetUnknown(MF_SOURCE_READER_ASYNC_CALLBACK,
97 static_cast<IMFSourceReaderCallback*>(
this));
100 hr = MFCreateSourceReaderFromMediaSource(aggregateSource.Get(), readerAttributes.Get(), &sourceReader);
114 if (m_sourceReader && m_videoSource) {
117 ComPtr<IMFMediaType> mediaType;
120 float currFrameRate = 0.0f;
122 while (SUCCEEDED(m_sourceReader->GetNativeMediaType(DWORD(MF_SOURCE_READER_FIRST_VIDEO_STREAM),
123 index, mediaType.ReleaseAndGetAddressOf()))) {
125 GUID subtype = GUID_NULL;
126 if (SUCCEEDED(mediaType->GetGUID(MF_MT_SUBTYPE, &subtype))) {
128 auto pixelFormat = QWMF::pixelFormatFromMediaSubtype(subtype);
129 if (pixelFormat != QVideoFrameFormat::Format_Invalid) {
131 UINT32 width, height;
132 if (SUCCEEDED(MFGetAttributeSize(mediaType.Get(), MF_MT_FRAME_SIZE, &width, &height))) {
135 if (SUCCEEDED(MFGetAttributeRatio(mediaType.Get(), MF_MT_FRAME_RATE, &num, &den))) {
137 UINT32 area = width * height;
138 float frameRate =
float(num) / den;
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) {
148 if ((currFrameRate < 29.9 && currFrameRate < frameRate) ||
149 (currFrameRate == frameRate && currArea < area)) {
151 currFrameRate = frameRate;
178 hr = m_sourceReader->GetCurrentMediaType(DWORD(MF_SOURCE_READER_FIRST_VIDEO_STREAM),
181 hr = m_sourceReader->GetNativeMediaType(DWORD(MF_SOURCE_READER_FIRST_VIDEO_STREAM),
182 mediaTypeIndex, &m_videoMediaType);
184 hr = m_sourceReader->SetCurrentMediaType(DWORD(MF_SOURCE_READER_FIRST_VIDEO_STREAM),
185 nullptr, m_videoMediaType.Get());
190 GUID subtype = GUID_NULL;
191 hr = m_videoMediaType->GetGUID(MF_MT_SUBTYPE, &subtype);
194 m_pixelFormat = QWMF::pixelFormatFromMediaSubtype(subtype);
196 if (m_pixelFormat == QVideoFrameFormat::Format_Invalid) {
201 hr = MFGetAttributeSize(m_videoMediaType.Get(), MF_MT_FRAME_SIZE, &m_frameWidth, &m_frameHeight);
205 hr = MFGetStrideForBitmapInfoHeader(subtype.Data1, m_frameWidth, &m_stride);
207 m_stride = qAbs(m_stride);
208 UINT32 frameRateNum, frameRateDen;
209 hr = MFGetAttributeRatio(m_videoMediaType.Get(), MF_MT_FRAME_RATE, &frameRateNum, &frameRateDen);
212 m_frameRate = qreal(frameRateNum) / frameRateDen;
214 hr = m_sourceReader->SetStreamSelection(DWORD(MF_SOURCE_READER_FIRST_VIDEO_STREAM), TRUE);
216 UINT32 nominalRange = 0;
218 if (SUCCEEDED(m_videoMediaType->GetUINT32(MF_MT_VIDEO_NOMINAL_RANGE, &nominalRange)))
219 m_colorRange = QWMF::colorRangeFromNominalRange(nominalRange);
221 UINT32 yuvMatrix = 0;
223 if (SUCCEEDED(m_videoMediaType->GetUINT32(MF_MT_YUV_MATRIX, &yuvMatrix))) {
224 m_colorSpace = QWMF::colorSpaceFromMatrix(yuvMatrix);
236HRESULT
QWindowsMediaDeviceReader::initAudioType(
const ComPtr<IMFMediaType> &mediaType, UINT32 channels, UINT32 samplesPerSec,
bool flt)
241 HRESULT hr = mediaType->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Audio);
243 hr = mediaType->SetGUID(MF_MT_SUBTYPE, flt ? MFAudioFormat_Float : MFAudioFormat_PCM);
245 hr = mediaType->SetUINT32(MF_MT_AUDIO_NUM_CHANNELS, channels);
247 hr = mediaType->SetUINT32(MF_MT_AUDIO_CHANNEL_MASK,
248 (channels == 1) ? SPEAKER_FRONT_CENTER : (SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT ));
250 hr = mediaType->SetUINT32(MF_MT_AUDIO_SAMPLES_PER_SECOND, samplesPerSec);
252 UINT32 bitsPerSample = flt ? 32 : 16;
253 UINT32 bytesPerFrame = channels * bitsPerSample/8;
254 hr = mediaType->SetUINT32(MF_MT_AUDIO_BITS_PER_SAMPLE, bitsPerSample);
256 hr = mediaType->SetUINT32(MF_MT_AUDIO_BLOCK_ALIGNMENT, bytesPerFrame);
258 hr = mediaType->SetUINT32(MF_MT_AUDIO_AVG_BYTES_PER_SECOND, bytesPerFrame * samplesPerSec);
279 HRESULT hr = m_sourceReader->GetCurrentMediaType(DWORD(MF_SOURCE_READER_FIRST_AUDIO_STREAM),
282 hr = initAudioType(m_audioMediaType, 2, 48000,
true);
284 hr = m_sourceReader->SetCurrentMediaType(DWORD(MF_SOURCE_READER_FIRST_AUDIO_STREAM),
285 nullptr, m_audioMediaType.Get());
287 hr = m_sourceReader->SetStreamSelection(DWORD(MF_SOURCE_READER_FIRST_AUDIO_STREAM), TRUE);
300 m_sourceVideoStreamIndex = MF_SOURCE_READER_INVALID_STREAM_INDEX;
301 m_sourceAudioStreamIndex = MF_SOURCE_READER_INVALID_STREAM_INDEX;
304 BOOL selected = FALSE;
306 ComPtr<IMFMediaType> mediaType;
307 while (m_sourceReader->GetStreamSelection(index, &selected) == S_OK) {
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;
321 if ((m_videoSource && m_sourceVideoStreamIndex == MF_SOURCE_READER_INVALID_STREAM_INDEX) ||
322 (m_audioSource && m_sourceAudioStreamIndex == MF_SOURCE_READER_INVALID_STREAM_INDEX))
329 QMutexLocker locker(&m_mutex);
333 m_audioOutputId = audioOutputId;
335 if (!m_active || m_audioOutputId.isEmpty())
338 HRESULT hr = startMonitoring();
340 return SUCCEEDED(hr);
345 if (m_audioOutputId.isEmpty())
348 ComPtr<IMFAttributes> sinkAttributes;
350 HRESULT hr = MFCreateAttributes(sinkAttributes.GetAddressOf(), 1);
353 hr = sinkAttributes->SetString(MF_AUDIO_RENDERER_ATTRIBUTE_ENDPOINT_ID,
354 reinterpret_cast<LPCWSTR>(m_audioOutputId.utf16()));
357 ComPtr<IMFMediaSink> mediaSink;
358 hr = MFCreateAudioRenderer(sinkAttributes.Get(), mediaSink.GetAddressOf());
361 ComPtr<IMFStreamSink> streamSink;
362 hr = mediaSink->GetStreamSinkByIndex(0, streamSink.GetAddressOf());
365 ComPtr<IMFMediaTypeHandler> typeHandler;
366 hr = streamSink->GetMediaTypeHandler(typeHandler.GetAddressOf());
369 hr = typeHandler->IsMediaTypeSupported(m_audioMediaType.Get(),
nullptr);
372 hr = typeHandler->SetCurrentMediaType(m_audioMediaType.Get());
375 ComPtr<IMFAttributes> writerAttributes;
377 HRESULT hr = MFCreateAttributes(writerAttributes.GetAddressOf(), 1);
380 hr = writerAttributes->SetUINT32(MF_SINK_WRITER_DISABLE_THROTTLING, TRUE);
383 ComPtr<IMFSinkWriter> sinkWriter;
384 hr = MFCreateSinkWriterFromMediaSink(mediaSink.Get(), writerAttributes.Get(), sinkWriter.GetAddressOf());
387 hr = sinkWriter->SetInputMediaType(0, m_audioMediaType.Get(),
nullptr);
390 ComPtr<IMFSimpleAudioVolume> audioVolume;
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);
399 hr = sinkWriter->BeginWriting();
401 m_monitorSink = mediaSink;
402 m_monitorWriter = sinkWriter;
421 m_monitorWriter =
nullptr;
423 m_monitorSink->Shutdown();
424 m_monitorSink =
nullptr;
431 const QCameraFormat &cameraFormat,
432 const QString µphoneId)
434 QMutexLocker locker(&m_mutex);
436 if (cameraId.isEmpty() && microphoneId.isEmpty())
445 if (!cameraId.isEmpty()) {
446 if (!SUCCEEDED(createSource(cameraId,
true, m_videoSource))) {
452 if (!microphoneId.isEmpty()) {
453 if (!SUCCEEDED(createSource(microphoneId,
false, m_audioSource))) {
459 if (!SUCCEEDED(createAggregateReader(m_videoSource, m_audioSource, m_aggregateSource, m_sourceReader))) {
464 DWORD mediaTypeIndex = findMediaTypeIndex(cameraFormat);
466 if (!SUCCEEDED(prepareVideoStream(mediaTypeIndex))) {
471 if (!SUCCEEDED(prepareAudioStream())) {
476 if (!SUCCEEDED(initSourceIndexes())) {
481 updateSinkInputMediaTypes();
485 if (!SUCCEEDED(m_sourceReader->ReadSample(MF_SOURCE_READER_ANY_STREAM, 0,
nullptr,
nullptr,
nullptr,
nullptr))) {
504 QMutexLocker locker(&m_mutex);
511 m_videoMediaType =
nullptr;
512 m_audioMediaType =
nullptr;
513 m_sourceReader =
nullptr;
514 m_aggregateSource =
nullptr;
515 m_videoSource =
nullptr;
516 m_audioSource =
nullptr;
520 UINT32 height, qreal frameRate, ComPtr<IMFMediaType> &mediaType)
523 ComPtr<IMFMediaType> targetMediaType;
525 if (SUCCEEDED(MFCreateMediaType(targetMediaType.GetAddressOf()))) {
527 if (SUCCEEDED(targetMediaType->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Video))) {
529 if (SUCCEEDED(targetMediaType->SetGUID(MF_MT_SUBTYPE, format))) {
531 if (SUCCEEDED(targetMediaType->SetUINT32(MF_MT_AVG_BITRATE, bitRate))) {
533 if (SUCCEEDED(MFSetAttributeSize(targetMediaType.Get(), MF_MT_FRAME_SIZE, width, height))) {
535 if (SUCCEEDED(MFSetAttributeRatio(targetMediaType.Get(), MF_MT_FRAME_RATE,
536 UINT32(frameRate * 1000), 1000))) {
538 if (SUCCEEDED(MFGetAttributeRatio(m_videoMediaType.Get(), MF_MT_PIXEL_ASPECT_RATIO, &t1, &t2))) {
540 if (SUCCEEDED(MFSetAttributeRatio(targetMediaType.Get(), MF_MT_PIXEL_ASPECT_RATIO, t1, t2))) {
542 if (SUCCEEDED(m_videoMediaType->GetUINT32(MF_MT_INTERLACE_MODE, &t1))) {
544 if (SUCCEEDED(targetMediaType->SetUINT32(MF_MT_INTERLACE_MODE, t1))) {
546 mediaType = targetMediaType;
561HRESULT
QWindowsMediaDeviceReader::createAudioMediaType(
const GUID &format, UINT32 bitRate, ComPtr<IMFMediaType> &mediaType)
564 ComPtr<IMFMediaType> targetMediaType;
566 if (SUCCEEDED(MFCreateMediaType(targetMediaType.GetAddressOf()))) {
568 if (SUCCEEDED(targetMediaType->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Audio))) {
570 if (SUCCEEDED(targetMediaType->SetGUID(MF_MT_SUBTYPE, format))) {
572 if (bitRate == 0 || SUCCEEDED(targetMediaType->SetUINT32(MF_MT_AUDIO_AVG_BYTES_PER_SECOND, bitRate / 8))) {
574 mediaType = targetMediaType;
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);
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);
600 const QString &fileName,
const GUID &container,
const GUID &videoFormat, UINT32 videoBitRate,
601 UINT32 width, UINT32 height, qreal frameRate,
const GUID &audioFormat, UINT32 audioBitRate)
603 QMutexLocker locker(&m_mutex);
605 if (!m_active || m_recording || (videoFormat == GUID_NULL && audioFormat == GUID_NULL))
606 return QMediaRecorder::ResourceError;
608 ComPtr<IMFAttributes> writerAttributes;
610 HRESULT hr = MFCreateAttributes(writerAttributes.GetAddressOf(), 2);
612 return QMediaRecorder::ResourceError;
615 hr = writerAttributes->SetUnknown(MF_SINK_WRITER_ASYNC_CALLBACK,
616 static_cast<IMFSinkWriterCallback*>(
this));
618 return QMediaRecorder::ResourceError;
620 hr = writerAttributes->SetGUID(MF_TRANSCODE_CONTAINERTYPE, container);
622 return QMediaRecorder::ResourceError;
624 ComPtr<IMFSinkWriter> sinkWriter;
625 hr = MFCreateSinkWriterFromURL(
reinterpret_cast<LPCWSTR>(fileName.utf16()),
626 nullptr, writerAttributes.Get(), sinkWriter.GetAddressOf());
628 return QMediaRecorder::LocationNotWritable;
630 m_sinkVideoStreamIndex = MF_SINK_WRITER_INVALID_STREAM_INDEX;
631 m_sinkAudioStreamIndex = MF_SINK_WRITER_INVALID_STREAM_INDEX;
633 if (m_videoSource && videoFormat != GUID_NULL) {
634 ComPtr<IMFMediaType> targetMediaType;
636 hr = createVideoMediaType(videoFormat, videoBitRate, width, height, frameRate, targetMediaType);
639 hr = sinkWriter->AddStream(targetMediaType.Get(), &m_sinkVideoStreamIndex);
642 hr = sinkWriter->SetInputMediaType(m_sinkVideoStreamIndex, m_videoMediaType.Get(),
nullptr);
648 if (m_audioSource && audioFormat != GUID_NULL) {
649 ComPtr<IMFMediaType> targetMediaType;
651 hr = createAudioMediaType(audioFormat, audioBitRate, targetMediaType);
654 hr = sinkWriter->AddStream(targetMediaType.Get(), &m_sinkAudioStreamIndex);
657 hr = sinkWriter->SetInputMediaType(m_sinkAudioStreamIndex, m_audioMediaType.Get(),
nullptr);
664 return QMediaRecorder::FormatError;
666 hr = sinkWriter->BeginWriting();
668 return QMediaRecorder::ResourceError;
670 m_sinkWriter = std::move(sinkWriter);
672 m_currentDuration = 0;
674 m_durationTimer.start();
678 m_pauseChanging =
false;
680 return QMediaRecorder::NoError;
685 QMutexLocker locker(&m_mutex);
687 if (m_sinkWriter && m_recording) {
689 HRESULT hr = m_sinkWriter->Finalize();
692 m_hasFinalized.wait(&m_mutex);
694 m_sinkWriter =
nullptr;
696 QMetaObject::invokeMethod(
this, [
this, hr] {
697 emit recordingError(hr);
698 }, Qt::QueuedConnection);
704 m_pauseChanging =
false;
706 m_durationTimer.stop();
708 m_currentDuration = -1;
713 if (!m_recording || m_paused)
715 m_pauseTime = m_lastTimestamp;
717 m_pauseChanging =
true;
723 if (!m_recording || !m_paused)
726 m_pauseChanging =
true;
737 return m_frameHeight;
747 m_inputMuted = muted;
752 m_inputVolume = qBound(0.0, volume, 1.0);
757 QMutexLocker locker(&m_mutex);
759 m_outputMuted = muted;
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);
772 QMutexLocker locker(&m_mutex);
774 m_outputVolume = qBound(0.0, volume, 1.0);
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));
787 if (m_currentDuration >= 0 && m_lastDuration != m_currentDuration) {
788 m_lastDuration = m_currentDuration;
789 emit durationChanged(m_currentDuration);
795 DWORD dwStreamFlags, LONGLONG llTimestamp,
798 QMutexLocker locker(&m_mutex);
800 if (FAILED(hrStatus)) {
801 emit streamingError(hrStatus);
805 m_lastTimestamp = llTimestamp;
807 if ((dwStreamFlags & MF_SOURCE_READERF_ENDOFSTREAM) == MF_SOURCE_READERF_ENDOFSTREAM) {
809 emit streamingStopped();
814 emit streamingStarted();
818 if (m_monitorWriter && dwStreamIndex == m_sourceAudioStreamIndex)
819 m_monitorWriter->WriteSample(0, pSample);
824 m_timeOffset = llTimestamp;
825 m_firstFrame =
false;
826 emit recordingStarted();
829 if (m_pauseChanging) {
832 m_pauseTime = llTimestamp;
834 m_timeOffset += llTimestamp - m_pauseTime;
835 m_pauseChanging =
false;
839 if (m_sinkWriter && !m_paused) {
841 pSample->SetSampleTime(llTimestamp - m_timeOffset);
843 if (dwStreamIndex == m_sourceVideoStreamIndex) {
845 m_sinkWriter->WriteSample(m_sinkVideoStreamIndex, pSample);
847 }
else if (dwStreamIndex == m_sourceAudioStreamIndex) {
849 float volume = m_inputMuted ? 0.0f :
float(m_inputVolume);
852 if (volume != 1.0f) {
853 ComPtr<IMFMediaBuffer> mediaBuffer;
854 if (SUCCEEDED(pSample->ConvertToContiguousBuffer(mediaBuffer.GetAddressOf()))) {
857 BYTE *buffer =
nullptr;
859 if (SUCCEEDED(mediaBuffer->Lock(&buffer,
nullptr, &bufLen))) {
861 float *floatBuffer =
reinterpret_cast<
float*>(buffer);
863 for (DWORD i = 0; i < bufLen/4; ++i)
864 floatBuffer[i] *= volume;
866 mediaBuffer->Unlock();
871 m_sinkWriter->WriteSample(m_sinkAudioStreamIndex, pSample);
873 m_currentDuration = (llTimestamp - m_timeOffset) / 10000;
878 if (dwStreamIndex == m_sourceVideoStreamIndex) {
879 ComPtr<IMFMediaBuffer> mediaBuffer;
880 if (SUCCEEDED(pSample->ConvertToContiguousBuffer(mediaBuffer.GetAddressOf()))) {
883 BYTE *buffer =
nullptr;
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);
891 QVideoFrame frame = QVideoFramePrivate::createFrame(
892 std::make_unique<QMemoryVideoBuffer>(std::move(bytes), m_stride),
896 frame.setStartTime(llTimestamp * 0.1);
898 LONGLONG duration = -1;
899 if (SUCCEEDED(pSample->GetSampleDuration(&duration)))
900 frame.setEndTime((llTimestamp + duration) * 0.1);
902 emit videoFrameChanged(frame);
904 mediaBuffer->Unlock();
911 m_sourceReader->ReadSample(MF_SOURCE_READER_ANY_STREAM,
912 0,
nullptr,
nullptr,
nullptr,
nullptr);
931 QMutexLocker locker(&m_mutex);
932 m_sinkWriter =
nullptr;
933 emit recordingStopped();
934 m_hasFinalized.notify_one();
945#include "moc_qwindowsmediadevicereader_p.cpp"
Combined button and popup list for selecting options.