diff options
-rw-r--r-- | src/core/media_capture_devices_dispatcher.cpp | 204 | ||||
-rw-r--r-- | src/core/media_capture_devices_dispatcher.h | 2 | ||||
-rw-r--r-- | src/core/web_contents_adapter_client.h | 2 | ||||
-rw-r--r-- | src/webengine/api/qquickwebengineview.cpp | 20 | ||||
-rw-r--r-- | src/webengine/api/qquickwebengineview_p.h | 4 | ||||
-rw-r--r-- | src/webengine/doc/src/webengineview.qdoc | 5 | ||||
-rw-r--r-- | src/webengine/plugin/plugin.cpp | 1 | ||||
-rw-r--r-- | src/webengine/plugin/plugins.qmltypes | 10 | ||||
-rw-r--r-- | src/webenginewidgets/api/qwebenginepage.cpp | 99 | ||||
-rw-r--r-- | src/webenginewidgets/api/qwebenginepage.h | 4 | ||||
-rw-r--r-- | src/webenginewidgets/doc/src/qwebenginepage_lgpl.qdoc | 5 | ||||
-rw-r--r-- | tests/auto/quick/qmltests/data/tst_getUserMedia.qml | 201 | ||||
-rw-r--r-- | tests/auto/quick/qmltests/qmltests.pro | 1 | ||||
-rw-r--r-- | tests/auto/widgets/qwebenginepage/tst_qwebenginepage.cpp | 120 |
14 files changed, 505 insertions, 173 deletions
diff --git a/src/core/media_capture_devices_dispatcher.cpp b/src/core/media_capture_devices_dispatcher.cpp index 8bdbaadd2..04d4a1924 100644 --- a/src/core/media_capture_devices_dispatcher.cpp +++ b/src/core/media_capture_devices_dispatcher.cpp @@ -121,13 +121,51 @@ std::unique_ptr<content::MediaStreamUI> getDevicesForDesktopCapture(content::Med return std::move(ui); } +content::DesktopMediaID getDefaultScreenId() +{ +#if BUILDFLAG(ENABLE_WEBRTC) + // Source id patterns are different across platforms. + // On Linux, the hardcoded value "0" is used. + // On Windows, the screens are enumerated consecutively in increasing order from 0. + // On macOS the source ids are randomish numbers assigned by the OS. + + // In order to provide a correct screen id, we query for the available screen ids, and + // select the first one as the main display id. + // The code is based on the file + // src/chrome/browser/extensions/api/desktop_capture/desktop_capture_base.cc. + webrtc::DesktopCaptureOptions options = + webrtc::DesktopCaptureOptions::CreateDefault(); + options.set_disable_effects(false); + std::unique_ptr<webrtc::DesktopCapturer> screen_capturer( + webrtc::DesktopCapturer::CreateScreenCapturer(options)); + + if (screen_capturer) { + webrtc::DesktopCapturer::SourceList screens; + if (screen_capturer->GetSourceList(&screens)) { + if (screens.size() > 0) { + return content::DesktopMediaID(content::DesktopMediaID::TYPE_SCREEN, screens[0].id); + } + } + } +#endif + + return content::DesktopMediaID(content::DesktopMediaID::TYPE_SCREEN, 0); +} + WebContentsAdapterClient::MediaRequestFlags mediaRequestFlagsForRequest(const content::MediaStreamRequest &request) { WebContentsAdapterClient::MediaRequestFlags requestFlags = WebContentsAdapterClient::MediaNone; + if (request.audio_type == content::MEDIA_DEVICE_AUDIO_CAPTURE) requestFlags |= WebContentsAdapterClient::MediaAudioCapture; + else if (request.audio_type == content::MEDIA_DESKTOP_AUDIO_CAPTURE) + requestFlags |= WebContentsAdapterClient::MediaDesktopAudioCapture; + if (request.video_type == content::MEDIA_DEVICE_VIDEO_CAPTURE) requestFlags |= WebContentsAdapterClient::MediaVideoCapture; + else if (request.video_type == content::MEDIA_DESKTOP_VIDEO_CAPTURE) + requestFlags |= WebContentsAdapterClient::MediaDesktopVideoCapture; + return requestFlags; } @@ -150,6 +188,7 @@ void MediaCaptureDevicesDispatcher::handleMediaAccessPermissionResponse(content: { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); + std::unique_ptr<content::MediaStreamUI> ui; content::MediaStreamDevices devices; std::map<content::WebContents*, RequestsQueue>::iterator it = m_pendingRequests.find(webContents); @@ -176,16 +215,30 @@ void MediaCaptureDevicesDispatcher::handleMediaAccessPermissionResponse(content: (request.audio_type && authorizationFlags & WebContentsAdapterClient::MediaAudioCapture); bool webcamRequested = (request.video_type && authorizationFlags & WebContentsAdapterClient::MediaVideoCapture); - if (securityOriginsMatch && (microphoneRequested || webcamRequested)) { - switch (request.request_type) { - case content::MEDIA_OPEN_DEVICE_PEPPER_ONLY: - getDefaultDevices("", "", microphoneRequested, webcamRequested, &devices); - break; - case content::MEDIA_DEVICE_ACCESS: - case content::MEDIA_GENERATE_STREAM: - getDefaultDevices(request.requested_audio_device_id, request.requested_video_device_id, - microphoneRequested, webcamRequested, &devices); - break; + bool desktopAudioRequested = + (request.audio_type && authorizationFlags & WebContentsAdapterClient::MediaDesktopAudioCapture); + bool desktopVideoRequested = + (request.video_type && authorizationFlags & WebContentsAdapterClient::MediaDesktopVideoCapture); + + if (securityOriginsMatch) { + if (microphoneRequested || webcamRequested) { + switch (request.request_type) { + case content::MEDIA_OPEN_DEVICE_PEPPER_ONLY: + getDefaultDevices("", "", microphoneRequested, webcamRequested, &devices); + break; + case content::MEDIA_DEVICE_ACCESS: + case content::MEDIA_GENERATE_STREAM: + getDefaultDevices(request.requested_audio_device_id, request.requested_video_device_id, + microphoneRequested, webcamRequested, &devices); + break; + } + } else if (desktopVideoRequested) { + ui = getDevicesForDesktopCapture( + &devices, + getDefaultScreenId(), + desktopAudioRequested, + /* display_notification: */ false, + getContentsUrl(webContents)); } } @@ -200,7 +253,7 @@ void MediaCaptureDevicesDispatcher::handleMediaAccessPermissionResponse(content: BrowserThread::UI, FROM_HERE, base::Bind(&MediaCaptureDevicesDispatcher::ProcessQueuedAccessRequest, base::Unretained(this), webContents)); } - callback.Run(devices, devices.empty() ? content::MEDIA_DEVICE_INVALID_STATE : content::MEDIA_DEVICE_OK, std::unique_ptr<content::MediaStreamUI>()); + callback.Run(devices, devices.empty() ? content::MEDIA_DEVICE_INVALID_STATE : content::MEDIA_DEVICE_OK, std::move(ui)); } @@ -238,22 +291,34 @@ void MediaCaptureDevicesDispatcher::processMediaAccessRequest(WebContentsAdapter , const content::MediaStreamRequest &request , const content::MediaResponseCallback &callback) { - DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); + DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); - // Let's not support tab capture for now. - if (request.video_type == content::MEDIA_TAB_VIDEO_CAPTURE || request.audio_type == content::MEDIA_TAB_AUDIO_CAPTURE) - return; - - if (request.video_type == content::MEDIA_DESKTOP_VIDEO_CAPTURE || request.audio_type == content::MEDIA_DESKTOP_AUDIO_CAPTURE) - // It's still unclear what to make of screen capture. We can rely on existing javascript dialog infrastructure - // to experiment with this without exposing it through our API yet. - processDesktopCaptureAccessRequest(webContents, request, callback); - else { - enqueueMediaAccessRequest(webContents, request, callback); - // We might not require this approval for pepper requests. - adapterClient->runMediaAccessPermissionRequest(toQt(request.security_origin), mediaRequestFlagsForRequest(request)); - } + // Let's not support tab capture for now. + if (request.video_type == content::MEDIA_TAB_VIDEO_CAPTURE || request.audio_type == content::MEDIA_TAB_AUDIO_CAPTURE) { + callback.Run(content::MediaStreamDevices(), content::MEDIA_DEVICE_NOT_SUPPORTED, std::unique_ptr<content::MediaStreamUI>()); + return; + } + + if (request.video_type == content::MEDIA_DESKTOP_VIDEO_CAPTURE || + request.audio_type == content::MEDIA_DESKTOP_AUDIO_CAPTURE) { + const bool screenCaptureEnabled = + adapterClient->webEngineSettings()->testAttribute(WebEngineSettings::ScreenCaptureEnabled); + const bool originIsSecure = content::IsOriginSecure(request.security_origin); + if (!screenCaptureEnabled || !originIsSecure) { + callback.Run(content::MediaStreamDevices(), content::MEDIA_DEVICE_INVALID_STATE, std::unique_ptr<content::MediaStreamUI>()); + return; + } + if (!request.requested_video_device_id.empty()) { + // Non-empty device id from the chooseDesktopMedia() extension API. + processDesktopCaptureAccessRequest(webContents, request, callback); + return; + } + } + + enqueueMediaAccessRequest(webContents, request, callback); + // We might not require this approval for pepper requests. + adapterClient->runMediaAccessPermissionRequest(toQt(request.security_origin), mediaRequestFlagsForRequest(request)); } void MediaCaptureDevicesDispatcher::processDesktopCaptureAccessRequest(content::WebContents *webContents, const content::MediaStreamRequest &request @@ -262,19 +327,12 @@ void MediaCaptureDevicesDispatcher::processDesktopCaptureAccessRequest(content:: content::MediaStreamDevices devices; std::unique_ptr<content::MediaStreamUI> ui; - if (request.video_type != content::MEDIA_DESKTOP_VIDEO_CAPTURE) { + if (request.video_type != content::MEDIA_DESKTOP_VIDEO_CAPTURE || + request.requested_video_device_id.empty()) { callback.Run(devices, content::MEDIA_DEVICE_INVALID_STATE, std::move(ui)); return; } - // If the device id wasn't specified then this is a screen capture request - // (i.e. chooseDesktopMedia() API wasn't used to generate device id). - if (request.requested_video_device_id.empty()) { - processScreenCaptureAccessRequest( - webContents, request, callback); - return; - } - content::WebContents* const web_contents_for_stream = content::WebContents::FromRenderFrameHost( content::RenderFrameHost::FromID(request.render_process_id, request.render_frame_id)); content::RenderFrameHost* const main_frame = web_contents_for_stream ? web_contents_for_stream->GetMainFrame() : NULL; @@ -307,84 +365,6 @@ void MediaCaptureDevicesDispatcher::processDesktopCaptureAccessRequest(content:: callback.Run(devices, devices.empty() ? content::MEDIA_DEVICE_INVALID_STATE : content::MEDIA_DEVICE_OK, std::move(ui)); } -void MediaCaptureDevicesDispatcher::processScreenCaptureAccessRequest(content::WebContents *webContents, const content::MediaStreamRequest &request - ,const content::MediaResponseCallback &callback) -{ - DCHECK_EQ(request.video_type, content::MEDIA_DESKTOP_VIDEO_CAPTURE); - - WebContentsAdapterClient *adapterClient = WebContentsViewQt::from(static_cast<content::WebContentsImpl*>(webContents)->GetView())->client(); - const bool screenCaptureEnabled = adapterClient->webEngineSettings()->testAttribute(WebEngineSettings::ScreenCaptureEnabled); - - const bool originIsSecure = content::IsOriginSecure(request.security_origin); - - if (screenCaptureEnabled && originIsSecure) { - - enqueueMediaAccessRequest(webContents, request, callback); - base::Callback<void(bool, const base::string16&)> dialogCallback = base::Bind(&MediaCaptureDevicesDispatcher::handleScreenCaptureAccessRequest, - base::Unretained(this), base::Unretained(webContents)); - - QUrl securityOrigin(toQt(request.security_origin)); - QString message = QCoreApplication::translate("MediaCaptureDevicesDispatcher", "Do you want %1 to share your screen?").arg(securityOrigin.toString()); - QString title = QCoreApplication::translate("MediaCaptureDevicesDispatcher", "%1 Screen Sharing request").arg(securityOrigin.toString()); - JavaScriptDialogManagerQt::GetInstance()->runDialogForContents(webContents, WebContentsAdapterClient::InternalAuthorizationDialog, message - , QString(), securityOrigin, dialogCallback, title); - } else - callback.Run(content::MediaStreamDevices(), content::MEDIA_DEVICE_INVALID_STATE, std::unique_ptr<content::MediaStreamUI>()); -} - -void MediaCaptureDevicesDispatcher::handleScreenCaptureAccessRequest(content::WebContents *webContents, bool userAccepted, const base::string16 &) -{ - content::MediaStreamDevices devices; - std::unique_ptr<content::MediaStreamUI> ui; -#if BUILDFLAG(ENABLE_WEBRTC) - if (userAccepted) { - // Source id patterns are different across platforms. - // On Linux, the hardcoded value "0" is used. - // On Windows, the screens are enumerated consecutively in increasing order from 0. - // On macOS the source ids are randomish numbers assigned by the OS. - webrtc::DesktopCapturer::SourceId id = 0; - - // In order to provide a correct screen id, we query for the available screen ids, and - // select the first one as the main display id. - // The code is based on the file - // src/chrome/browser/extensions/api/desktop_capture/desktop_capture_base.cc. - webrtc::DesktopCaptureOptions options = - webrtc::DesktopCaptureOptions::CreateDefault(); - options.set_disable_effects(false); - std::unique_ptr<webrtc::DesktopCapturer> screen_capturer( - webrtc::DesktopCapturer::CreateScreenCapturer(options)); - - if (screen_capturer) { - webrtc::DesktopCapturer::SourceList screens; - if (screen_capturer->GetSourceList(&screens)) { - if (screens.size() > 0) { - id = screens[0].id; - } - } - } - - content::DesktopMediaID screenId = content::DesktopMediaID( - content::DesktopMediaID::TYPE_SCREEN, id); - ui = getDevicesForDesktopCapture(&devices, screenId, false/*capture_audio*/, false/*display_notification*/, getContentsUrl(webContents)); - } -#endif - std::map<content::WebContents*, RequestsQueue>::iterator it = - m_pendingRequests.find(webContents); - if (it == m_pendingRequests.end()) { - // WebContents has been destroyed. Don't need to do anything. - return; - } - - RequestsQueue &queue(it->second); - if (queue.empty()) - return; - - content::MediaResponseCallback callback = queue.front().callback; - queue.pop_front(); - - callback.Run(devices, devices.empty() ? content::MEDIA_DEVICE_INVALID_STATE : content::MEDIA_DEVICE_OK, std::move(ui)); -} - void MediaCaptureDevicesDispatcher::enqueueMediaAccessRequest(content::WebContents *webContents, const content::MediaStreamRequest &request ,const content::MediaResponseCallback &callback) { diff --git a/src/core/media_capture_devices_dispatcher.h b/src/core/media_capture_devices_dispatcher.h index c378c327e..579d159a4 100644 --- a/src/core/media_capture_devices_dispatcher.h +++ b/src/core/media_capture_devices_dispatcher.h @@ -116,8 +116,6 @@ class MediaCaptureDevicesDispatcher : public content::MediaObserver, // Helpers for ProcessMediaAccessRequest(). void processDesktopCaptureAccessRequest(content::WebContents *, const content::MediaStreamRequest &, const content::MediaResponseCallback &); - void processScreenCaptureAccessRequest(content::WebContents *,const content::MediaStreamRequest &, const content::MediaResponseCallback &); - void handleScreenCaptureAccessRequest(content::WebContents *, bool userAccepted, const base::string16 &/*unused callback_input*/); void enqueueMediaAccessRequest(content::WebContents *, const content::MediaStreamRequest &, const content::MediaResponseCallback &); void ProcessQueuedAccessRequest(content::WebContents *); diff --git a/src/core/web_contents_adapter_client.h b/src/core/web_contents_adapter_client.h index 8d75f24b7..8b7365342 100644 --- a/src/core/web_contents_adapter_client.h +++ b/src/core/web_contents_adapter_client.h @@ -316,6 +316,8 @@ public: MediaNone = 0, MediaAudioCapture = 0x01, MediaVideoCapture = 0x02, + MediaDesktopAudioCapture = 0x04, + MediaDesktopVideoCapture = 0x08 }; Q_DECLARE_FLAGS(MediaRequestFlags, MediaRequestFlag) diff --git a/src/webengine/api/qquickwebengineview.cpp b/src/webengine/api/qquickwebengineview.cpp index c5c772411..09f22c280 100644 --- a/src/webengine/api/qquickwebengineview.cpp +++ b/src/webengine/api/qquickwebengineview.cpp @@ -703,8 +703,13 @@ void QQuickWebEngineViewPrivate::runMediaAccessPermissionRequest(const QUrl &sec feature = QQuickWebEngineView::MediaAudioVideoCapture; else if (requestFlags.testFlag(WebContentsAdapterClient::MediaAudioCapture)) feature = QQuickWebEngineView::MediaAudioCapture; - else // WebContentsAdapterClient::MediaVideoCapture + else if (requestFlags.testFlag(WebContentsAdapterClient::MediaVideoCapture)) feature = QQuickWebEngineView::MediaVideoCapture; + else if (requestFlags.testFlag(WebContentsAdapterClient::MediaDesktopAudioCapture) && + requestFlags.testFlag(WebContentsAdapterClient::MediaDesktopVideoCapture)) + feature = QQuickWebEngineView::DesktopAudioVideoCapture; + else // if (requestFlags.testFlag(WebContentsAdapterClient::MediaDesktopVideoCapture)) + feature = QQuickWebEngineView::DesktopVideoCapture; Q_EMIT q->featurePermissionRequested(securityOrigin, feature); } @@ -1431,7 +1436,8 @@ void QQuickWebEngineView::grantFeaturePermission(const QUrl &securityOrigin, QQu { if (!d_ptr->adapter) return; - if (!granted && feature >= MediaAudioCapture && feature <= MediaAudioVideoCapture) { + if (!granted && ((feature >= MediaAudioCapture && feature <= MediaAudioVideoCapture) || + (feature >= DesktopVideoCapture && feature <= DesktopAudioVideoCapture))) { d_ptr->adapter->grantMediaAccessPermission(securityOrigin, WebContentsAdapterClient::MediaNone); return; } @@ -1449,6 +1455,16 @@ void QQuickWebEngineView::grantFeaturePermission(const QUrl &securityOrigin, QQu case Geolocation: d_ptr->adapter->runGeolocationRequestCallback(securityOrigin, granted); break; + case DesktopVideoCapture: + d_ptr->adapter->grantMediaAccessPermission(securityOrigin, WebContentsAdapterClient::MediaDesktopVideoCapture); + break; + case DesktopAudioVideoCapture: + d_ptr->adapter->grantMediaAccessPermission( + securityOrigin, + WebContentsAdapterClient::MediaRequestFlags( + WebContentsAdapterClient::MediaDesktopAudioCapture | + WebContentsAdapterClient::MediaDesktopVideoCapture)); + break; default: Q_UNREACHABLE(); } diff --git a/src/webengine/api/qquickwebengineview_p.h b/src/webengine/api/qquickwebengineview_p.h index 32419329a..ae0523460 100644 --- a/src/webengine/api/qquickwebengineview_p.h +++ b/src/webengine/api/qquickwebengineview_p.h @@ -199,7 +199,9 @@ public: MediaAudioCapture, MediaVideoCapture, MediaAudioVideoCapture, - Geolocation + Geolocation, + DesktopVideoCapture, + DesktopAudioVideoCapture }; Q_ENUM(Feature) diff --git a/src/webengine/doc/src/webengineview.qdoc b/src/webengine/doc/src/webengineview.qdoc index e452746d9..1d1c61f03 100644 --- a/src/webengine/doc/src/webengineview.qdoc +++ b/src/webengine/doc/src/webengineview.qdoc @@ -850,6 +850,11 @@ Video devices, such as cameras. \value WebEngineView.MediaAudioVideoCapture Both audio and video capture devices. + \value DesktopVideoCapture + Video output capture, that is, the capture of the user's display. + (Added in Qt 5.10) + \value DesktopAudioVideoCapture + Both audio and video output capture. (Added in Qt 5.10) \sa featurePermissionRequested(), grantFeaturePermission() */ diff --git a/src/webengine/plugin/plugin.cpp b/src/webengine/plugin/plugin.cpp index e58b2ab61..5ab792699 100644 --- a/src/webengine/plugin/plugin.cpp +++ b/src/webengine/plugin/plugin.cpp @@ -85,6 +85,7 @@ public: qmlRegisterType<QQuickWebEngineView, 3>(uri, 1, 3, "WebEngineView"); qmlRegisterType<QQuickWebEngineView, 4>(uri, 1, 4, "WebEngineView"); qmlRegisterType<QQuickWebEngineView, 5>(uri, 1, 5, "WebEngineView"); + qmlRegisterType<QQuickWebEngineView, 6>(uri, 1, 6, "WebEngineView"); qmlRegisterType<QQuickWebEngineProfile>(uri, 1, 1, "WebEngineProfile"); qmlRegisterType<QQuickWebEngineProfile, 1>(uri, 1, 2, "WebEngineProfile"); qmlRegisterType<QQuickWebEngineProfile, 2>(uri, 1, 3, "WebEngineProfile"); diff --git a/src/webengine/plugin/plugins.qmltypes b/src/webengine/plugin/plugins.qmltypes index 0cfa2d25b..321c462b5 100644 --- a/src/webengine/plugin/plugins.qmltypes +++ b/src/webengine/plugin/plugins.qmltypes @@ -576,9 +576,10 @@ Module { "QtWebEngine/WebEngineView 1.2", "QtWebEngine/WebEngineView 1.3", "QtWebEngine/WebEngineView 1.4", - "QtWebEngine/WebEngineView 1.5" + "QtWebEngine/WebEngineView 1.5", + "QtWebEngine/WebEngineView 1.6" ] - exportMetaObjectRevisions: [0, 1, 2, 3, 4, 5] + exportMetaObjectRevisions: [0, 1, 2, 3, 4, 5, 6] Enum { name: "NavigationRequestAction" values: { @@ -633,7 +634,9 @@ Module { "MediaAudioCapture": 0, "MediaVideoCapture": 1, "MediaAudioVideoCapture": 2, - "Geolocation": 3 + "Geolocation": 3, + "DesktopVideoCapture": 4, + "DesktopAudioVideoCapture": 5 } } Enum { @@ -889,6 +892,7 @@ Module { Property { name: "audioMuted"; revision: 3; type: "bool" } Property { name: "recentlyAudible"; revision: 3; type: "bool"; isReadonly: true } Property { name: "webChannelWorld"; revision: 3; type: "uint" } + Property { name: "testSupport"; type: "QQuickWebEngineTestSupport"; isPointer: true } Signal { name: "loadingChanged" Parameter { name: "loadRequest"; type: "QQuickWebEngineLoadRequest"; isPointer: true } diff --git a/src/webenginewidgets/api/qwebenginepage.cpp b/src/webenginewidgets/api/qwebenginepage.cpp index 87af187e3..5b7fa9df2 100644 --- a/src/webenginewidgets/api/qwebenginepage.cpp +++ b/src/webenginewidgets/api/qwebenginepage.cpp @@ -560,16 +560,20 @@ void QWebEnginePagePrivate::showColorDialog(QSharedPointer<ColorChooserControlle void QWebEnginePagePrivate::runMediaAccessPermissionRequest(const QUrl &securityOrigin, WebContentsAdapterClient::MediaRequestFlags requestFlags) { Q_Q(QWebEnginePage); - QWebEnginePage::Feature requestedFeature; - if (requestFlags.testFlag(WebContentsAdapterClient::MediaAudioCapture) && requestFlags.testFlag(WebContentsAdapterClient::MediaVideoCapture)) - requestedFeature = QWebEnginePage::MediaAudioVideoCapture; + QWebEnginePage::Feature feature; + if (requestFlags.testFlag(WebContentsAdapterClient::MediaAudioCapture) && + requestFlags.testFlag(WebContentsAdapterClient::MediaVideoCapture)) + feature = QWebEnginePage::MediaAudioVideoCapture; else if (requestFlags.testFlag(WebContentsAdapterClient::MediaAudioCapture)) - requestedFeature = QWebEnginePage::MediaAudioCapture; + feature = QWebEnginePage::MediaAudioCapture; else if (requestFlags.testFlag(WebContentsAdapterClient::MediaVideoCapture)) - requestedFeature = QWebEnginePage::MediaVideoCapture; - else - return; - Q_EMIT q->featurePermissionRequested(securityOrigin, requestedFeature); + feature = QWebEnginePage::MediaVideoCapture; + else if (requestFlags.testFlag(WebContentsAdapterClient::MediaDesktopAudioCapture) && + requestFlags.testFlag(WebContentsAdapterClient::MediaDesktopVideoCapture)) + feature = QWebEnginePage::DesktopAudioVideoCapture; + else // if (requestFlags.testFlag(WebContentsAdapterClient::MediaDesktopVideoCapture)) + feature = QWebEnginePage::DesktopVideoCapture; + Q_EMIT q->featurePermissionRequested(securityOrigin, feature); } void QWebEnginePagePrivate::runGeolocationPermissionRequest(const QUrl &securityOrigin) @@ -1728,37 +1732,58 @@ void QWebEnginePage::setFeaturePermission(const QUrl &securityOrigin, QWebEngine Q_D(QWebEnginePage); if (policy == PermissionUnknown) return; - WebContentsAdapterClient::MediaRequestFlags flags = WebContentsAdapterClient::MediaNone; - switch (feature) { - case MediaAudioVideoCapture: - case MediaAudioCapture: - case MediaVideoCapture: - if (policy != PermissionUnknown) { - if (policy == PermissionDeniedByUser) - flags = WebContentsAdapterClient::MediaNone; - else { - if (feature == MediaAudioCapture) - flags = WebContentsAdapterClient::MediaAudioCapture; - else if (feature == MediaVideoCapture) - flags = WebContentsAdapterClient::MediaVideoCapture; - else - flags = WebContentsAdapterClient::MediaRequestFlags(WebContentsAdapterClient::MediaVideoCapture | WebContentsAdapterClient::MediaAudioCapture); - } - d->adapter->grantMediaAccessPermission(securityOrigin, flags); - } - d->adapter->grantMediaAccessPermission(securityOrigin, flags); - break; - case QWebEnginePage::Geolocation: - d->adapter->runGeolocationRequestCallback(securityOrigin, (policy == PermissionGrantedByUser) ? true : false); - break; - case MouseLock: - if (policy == PermissionGrantedByUser) + + const WebContentsAdapterClient::MediaRequestFlags audioVideoCaptureFlags( + WebContentsAdapterClient::MediaVideoCapture | + WebContentsAdapterClient::MediaAudioCapture); + const WebContentsAdapterClient::MediaRequestFlags desktopAudioVideoCaptureFlags( + WebContentsAdapterClient::MediaDesktopVideoCapture | + WebContentsAdapterClient::MediaDesktopAudioCapture); + + if (policy == PermissionGrantedByUser) { + switch (feature) { + case MediaAudioVideoCapture: + d->adapter->grantMediaAccessPermission(securityOrigin, audioVideoCaptureFlags); + break; + case MediaAudioCapture: + d->adapter->grantMediaAccessPermission(securityOrigin, WebContentsAdapterClient::MediaAudioCapture); + break; + case MediaVideoCapture: + d->adapter->grantMediaAccessPermission(securityOrigin, WebContentsAdapterClient::MediaVideoCapture); + break; + case DesktopAudioVideoCapture: + d->adapter->grantMediaAccessPermission(securityOrigin, desktopAudioVideoCaptureFlags); + break; + case DesktopVideoCapture: + d->adapter->grantMediaAccessPermission(securityOrigin, WebContentsAdapterClient::MediaDesktopVideoCapture); + break; + case Geolocation: + d->adapter->runGeolocationRequestCallback(securityOrigin, true); + break; + case MouseLock: d->adapter->grantMouseLockPermission(true); - else + break; + case Notifications: + break; + } + } else { // if (policy == PermissionDeniedByUser) + switch (feature) { + case MediaAudioVideoCapture: + case MediaAudioCapture: + case MediaVideoCapture: + case DesktopAudioVideoCapture: + case DesktopVideoCapture: + d->adapter->grantMediaAccessPermission(securityOrigin, WebContentsAdapterClient::MediaNone); + break; + case Geolocation: + d->adapter->runGeolocationRequestCallback(securityOrigin, false); + break; + case MouseLock: d->adapter->grantMouseLockPermission(false); - break; - case Notifications: - break; + break; + case Notifications: + break; + } } } diff --git a/src/webenginewidgets/api/qwebenginepage.h b/src/webenginewidgets/api/qwebenginepage.h index 74ebd0a35..292075827 100644 --- a/src/webenginewidgets/api/qwebenginepage.h +++ b/src/webenginewidgets/api/qwebenginepage.h @@ -188,7 +188,9 @@ public: MediaAudioCapture = 2, MediaVideoCapture, MediaAudioVideoCapture, - MouseLock + MouseLock, + DesktopVideoCapture, + DesktopAudioVideoCapture }; Q_ENUM(Feature) diff --git a/src/webenginewidgets/doc/src/qwebenginepage_lgpl.qdoc b/src/webenginewidgets/doc/src/qwebenginepage_lgpl.qdoc index 621982951..3b9300a4b 100644 --- a/src/webenginewidgets/doc/src/qwebenginepage_lgpl.qdoc +++ b/src/webenginewidgets/doc/src/qwebenginepage_lgpl.qdoc @@ -286,6 +286,11 @@ \value MouseLock Mouse locking, which locks the mouse pointer to the web view and is typically used in games. + \value DesktopVideoCapture + Video output capture, that is, the capture of the user's display, + for screen sharing purposes for example. (Added in Qt 5.10) + \value DesktopAudioVideoCapture + Both audio and video output capture. (Added in Qt 5.10) \sa featurePermissionRequested(), featurePermissionRequestCanceled(), setFeaturePermission(), PermissionPolicy diff --git a/tests/auto/quick/qmltests/data/tst_getUserMedia.qml b/tests/auto/quick/qmltests/data/tst_getUserMedia.qml new file mode 100644 index 000000000..b497542e3 --- /dev/null +++ b/tests/auto/quick/qmltests/data/tst_getUserMedia.qml @@ -0,0 +1,201 @@ +/**************************************************************************** +** +** Copyright (C) 2017 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtWebEngine module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:GPL-EXCEPT$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +import QtQuick 2.2 +import QtTest 1.0 +import QtWebEngine 1.6 + +TestWebEngineView { + id: webEngineView + + settings.screenCaptureEnabled: true + + TestCase { + name: "GetUserMedia" + + function init_data() { + return [ + { + tag: "device audio", + constraints: { audio: true }, + feature: WebEngineView.MediaAudioCapture, + }, + { + tag: "device video", + constraints: { video: true }, + feature: WebEngineView.MediaVideoCapture, + }, + { + tag: "device audio+video", + constraints: { audio: true, video: true }, + feature: WebEngineView.MediaAudioVideoCapture, + }, + { + tag: "desktop video", + constraints: { + video: { + mandatory: { + chromeMediaSource: "desktop" + } + } + }, + feature: WebEngineView.DesktopVideoCapture, + }, + { + tag: "desktop audio+video", + constraints: { + audio: { + mandatory: { + chromeMediaSource: "desktop" + } + }, + video: { + mandatory: { + chromeMediaSource: "desktop" + } + } + }, + feature: WebEngineView.DesktopAudioVideoCapture, + } + ] + } + + function test_getUserMedia(row) { + loadSync(Qt.resolvedUrl("test1.html")) + + // 1. Rejecting request on QML side should reject promise on JS side. + jsGetUserMedia(row.constraints) + tryVerify(function(){ return gotFeatureRequest(row.feature) }) + rejectPendingRequest() + tryVerify(function(){ return !jsPromiseFulfilled() && jsPromiseRejected() }) + + // 2. Accepting request on QML side should either fulfill or reject the + // Promise on JS side. Due to the potential lack of physical media devices + // deeper in the content layer we cannot guarantee that the promise will + // always be fulfilled, however in this case an error should be returned to + // JS instead of leaving the Promise in limbo. + jsGetUserMedia(row.constraints) + tryVerify(function(){ return gotFeatureRequest(row.feature) }) + acceptPendingRequest() + tryVerify(function(){ return jsPromiseFulfilled() || jsPromiseRejected() }); + + // 3. Media feature permissions are not remembered. + jsGetUserMedia(row.constraints); + tryVerify(function(){ return gotFeatureRequest(row.feature) }) + acceptPendingRequest() + tryVerify(function(){ return jsPromiseFulfilled() || jsPromiseRejected() }); + } + } + + //// + // synchronous loading + + signal loadFinished + + SignalSpy { + id: spyOnLoadFinished + target: webEngineView + signalName: "loadFinished" + } + + onLoadingChanged: { + if (loadRequest.status == WebEngineLoadRequest.LoadSucceededStatus) { + loadFinished() + } + } + + function loadSync(url) { + webEngineView.url = url + spyOnLoadFinished.wait() + } + + //// + // synchronous permission requests + + property variant requestedFeature + property variant requestedSecurityOrigin + + onFeaturePermissionRequested: { + requestedFeature = feature + requestedSecurityOrigin = securityOrigin + } + + function gotFeatureRequest(expectedFeature) { + return requestedFeature == expectedFeature + } + + function acceptPendingRequest() { + webEngineView.grantFeaturePermission(requestedSecurityOrigin, requestedFeature, true) + requestedFeature = undefined + requestedSecurityOrigin = undefined + } + + function rejectPendingRequest() { + webEngineView.grantFeaturePermission(requestedSecurityOrigin, requestedFeature, false) + requestedFeature = undefined + requestedSecurityOrigin = undefined + } + + //// + // synchronous JavaScript evaluation + + signal runJavaScriptFinished(variant result) + + SignalSpy { + id: spyOnRunJavaScriptFinished + target: webEngineView + signalName: "runJavaScriptFinished" + } + + function runJavaScriptSync(code) { + spyOnRunJavaScriptFinished.clear() + runJavaScript(code, runJavaScriptFinished) + spyOnRunJavaScriptFinished.wait() + return spyOnRunJavaScriptFinished.signalArguments[0][0] + } + + //// + // JavaScript snippets + + function jsGetUserMedia(constraints) { + runJavaScript( + "var promiseFulfilled = false;" + + "var promiseRejected = false;" + + "navigator.mediaDevices.getUserMedia(" + JSON.stringify(constraints) + ")" + + ".then(stream => { promiseFulfilled = true})" + + ".catch(err => { promiseRejected = true})") + } + + function jsPromiseFulfilled() { + return runJavaScriptSync("promiseFulfilled") + } + + function jsPromiseRejected() { + return runJavaScriptSync("promiseRejected") + } +} diff --git a/tests/auto/quick/qmltests/qmltests.pro b/tests/auto/quick/qmltests/qmltests.pro index d2c9245bd..39b9d0151 100644 --- a/tests/auto/quick/qmltests/qmltests.pro +++ b/tests/auto/quick/qmltests/qmltests.pro @@ -52,6 +52,7 @@ OTHER_FILES += \ $$PWD/data/tst_focusOnNavigation.qml \ $$PWD/data/tst_formValidation.qml \ $$PWD/data/tst_geopermission.qml \ + $$PWD/data/tst_getUserMedia.qml \ $$PWD/data/tst_inputMethod.qml \ $$PWD/data/tst_javaScriptDialogs.qml \ $$PWD/data/tst_linkHovered.qml \ diff --git a/tests/auto/widgets/qwebenginepage/tst_qwebenginepage.cpp b/tests/auto/widgets/qwebenginepage/tst_qwebenginepage.cpp index cd80db9a3..231012c65 100644 --- a/tests/auto/widgets/qwebenginepage/tst_qwebenginepage.cpp +++ b/tests/auto/widgets/qwebenginepage/tst_qwebenginepage.cpp @@ -125,7 +125,10 @@ private Q_SLOTS: void userAgentNewlineStripping(); void undoActionHaveCustomText(); void renderWidgetHostViewNotShowTopLevel(); + void getUserMediaRequest_data(); void getUserMediaRequest(); + void getUserMediaRequestDesktopAudio(); + void getUserMediaRequestSettingDisabled(); void savePage(); void crashTests_LazyInitializationOfMainFrame(); @@ -2605,6 +2608,33 @@ public: : m_gotRequest(false) { connect(this, &QWebEnginePage::featurePermissionRequested, this, &GetUserMediaTestPage::onFeaturePermissionRequested); + + // We need to load content from a resource in order for the securityOrigin to be valid. + QSignalSpy loadSpy(this, SIGNAL(loadFinished(bool))); + load(QUrl("qrc:///resources/content.html")); + QTRY_COMPARE(loadSpy.count(), 1); + } + + void jsGetUserMedia(const QString & constraints) + { + runJavaScript( + QStringLiteral( + "var promiseFulfilled = false;" + "var promiseRejected = false;" + "navigator.mediaDevices.getUserMedia(%1)" + ".then(stream => { promiseFulfilled = true})" + ".catch(err => { promiseRejected = true})") + .arg(constraints)); + } + + bool jsPromiseFulfilled() + { + return evaluateJavaScriptSync(this, QStringLiteral("promiseFulfilled")).toBool(); + } + + bool jsPromiseRejected() + { + return evaluateJavaScriptSync(this, QStringLiteral("promiseRejected")).toBool(); } void rejectPendingRequest() @@ -2623,6 +2653,11 @@ public: return m_gotRequest && m_requestedFeature == feature; } + bool gotFeatureRequest() const + { + return m_gotRequest; + } + private Q_SLOTS: void onFeaturePermissionRequested(const QUrl &securityOrigin, QWebEnginePage::Feature feature) { @@ -2638,28 +2673,83 @@ private: }; +void tst_QWebEnginePage::getUserMediaRequest_data() +{ + QTest::addColumn<QString>("constraints"); + QTest::addColumn<QWebEnginePage::Feature>("feature"); + + QTest::addRow("device audio") + << "{audio: true}" << QWebEnginePage::MediaAudioCapture; + QTest::addRow("device video") + << "{video: true}" << QWebEnginePage::MediaVideoCapture; + QTest::addRow("device audio+video") + << "{audio: true, video: true}" << QWebEnginePage::MediaAudioVideoCapture; + QTest::addRow("desktop video") + << "{video: { mandatory: { chromeMediaSource: 'desktop' }}}" + << QWebEnginePage::DesktopVideoCapture; + QTest::addRow("desktop audio+video") + << "{audio: { mandatory: { chromeMediaSource: 'desktop' }}, video: { mandatory: { chromeMediaSource: 'desktop' }}}" + << QWebEnginePage::DesktopAudioVideoCapture; +} void tst_QWebEnginePage::getUserMediaRequest() { - GetUserMediaTestPage page; + QFETCH(QString, constraints); + QFETCH(QWebEnginePage::Feature, feature); - // We need to load content from a resource in order for the securityOrigin to be valid. - QSignalSpy loadSpy(&page, SIGNAL(loadFinished(bool))); - page.load(QUrl("qrc:///resources/content.html")); - QTRY_COMPARE(loadSpy.count(), 1); + GetUserMediaTestPage page; + page.settings()->setAttribute(QWebEngineSettings::ScreenCaptureEnabled, true); + + // 1. Rejecting request on C++ side should reject promise on JS side. + page.jsGetUserMedia(constraints); + QTRY_VERIFY(page.gotFeatureRequest(feature)); + page.rejectPendingRequest(); + QTRY_VERIFY(!page.jsPromiseFulfilled() && page.jsPromiseRejected()); + + // 2. Accepting request on C++ side should either fulfill or reject the + // Promise on JS side. Due to the potential lack of physical media devices + // deeper in the content layer we cannot guarantee that the promise will + // always be fulfilled, however in this case an error should be returned to + // JS instead of leaving the Promise in limbo. + page.jsGetUserMedia(constraints); + QTRY_VERIFY(page.gotFeatureRequest(feature)); + page.acceptPendingRequest(); + QTRY_VERIFY(page.jsPromiseFulfilled() || page.jsPromiseRejected()); - QVERIFY(evaluateJavaScriptSync(&page, QStringLiteral("!!navigator.webkitGetUserMedia")).toBool()); - evaluateJavaScriptSync(&page, QStringLiteral("navigator.webkitGetUserMedia({audio: true}, function() {}, function(){})")); - QTRY_VERIFY(page.gotFeatureRequest(QWebEnginePage::MediaAudioCapture)); - // Might end up failing due to the lack of physical media devices deeper in the content layer, so the JS callback is not guaranteed to be called, - // but at least we go through that code path, potentially uncovering failing assertions. + // 3. Media feature permissions are not remembered. + page.jsGetUserMedia(constraints); + QTRY_VERIFY(page.gotFeatureRequest(feature)); page.acceptPendingRequest(); + QTRY_VERIFY(page.jsPromiseFulfilled() || page.jsPromiseRejected()); +} + +void tst_QWebEnginePage::getUserMediaRequestDesktopAudio() +{ + GetUserMediaTestPage page; + page.settings()->setAttribute(QWebEngineSettings::ScreenCaptureEnabled, true); + + // Audio-only desktop capture is not supported. JS Promise should be + // rejected immediately. + + page.jsGetUserMedia( + QStringLiteral("{audio: { mandatory: { chromeMediaSource: 'desktop' }}}")); + QTRY_VERIFY(!page.jsPromiseFulfilled() && page.jsPromiseRejected()); + + page.jsGetUserMedia( + QStringLiteral("{audio: { mandatory: { chromeMediaSource: 'desktop' }}, video: true}")); + QTRY_VERIFY(!page.jsPromiseFulfilled() && page.jsPromiseRejected()); +} + +void tst_QWebEnginePage::getUserMediaRequestSettingDisabled() +{ + GetUserMediaTestPage page; + page.settings()->setAttribute(QWebEngineSettings::ScreenCaptureEnabled, false); + + // With the setting disabled, the JS Promise should be rejected without + // asking for permission first. - page.runJavaScript(QStringLiteral("errorCallbackCalled = false;")); - evaluateJavaScriptSync(&page, QStringLiteral("navigator.webkitGetUserMedia({audio: true, video: true}, function() {}, function(){errorCallbackCalled = true;})")); - QTRY_VERIFY(page.gotFeatureRequest(QWebEnginePage::MediaAudioVideoCapture)); - page.rejectPendingRequest(); // Should always end up calling the error callback in JS. - QTRY_VERIFY(evaluateJavaScriptSync(&page, QStringLiteral("errorCallbackCalled;")).toBool()); + page.jsGetUserMedia(QStringLiteral("{video: { mandatory: { chromeMediaSource: 'desktop' }}}")); + QTRY_VERIFY(!page.jsPromiseFulfilled() && page.jsPromiseRejected()); } void tst_QWebEnginePage::savePage() |