| // Copyright 2017 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "third_party/blink/renderer/modules/xr/xr_system.h" |
| |
| #include <utility> |
| |
| #include "device/vr/public/mojom/vr_service.mojom-blink.h" |
| #include "services/metrics/public/cpp/ukm_builders.h" |
| #include "third_party/blink/public/common/browser_interface_broker_proxy.h" |
| #include "third_party/blink/public/mojom/feature_policy/feature_policy.mojom-blink.h" |
| #include "third_party/blink/renderer/bindings/core/v8/script_promise_resolver.h" |
| #include "third_party/blink/renderer/bindings/core/v8/v8_fullscreen_options.h" |
| #include "third_party/blink/renderer/bindings/modules/v8/v8_xr_depth_state_init.h" |
| #include "third_party/blink/renderer/bindings/modules/v8/v8_xr_tracked_image_init.h" |
| #include "third_party/blink/renderer/core/dom/document.h" |
| #include "third_party/blink/renderer/core/dom/dom_exception.h" |
| #include "third_party/blink/renderer/core/dom/element.h" |
| #include "third_party/blink/renderer/core/frame/local_dom_window.h" |
| #include "third_party/blink/renderer/core/frame/local_frame.h" |
| #include "third_party/blink/renderer/core/frame/navigator.h" |
| #include "third_party/blink/renderer/core/frame/settings.h" |
| #include "third_party/blink/renderer/core/frame/viewport_data.h" |
| #include "third_party/blink/renderer/core/fullscreen/fullscreen.h" |
| #include "third_party/blink/renderer/core/fullscreen/scoped_allow_fullscreen.h" |
| #include "third_party/blink/renderer/core/html/html_element.h" |
| #include "third_party/blink/renderer/core/imagebitmap/image_bitmap.h" |
| #include "third_party/blink/renderer/core/inspector/console_message.h" |
| #include "third_party/blink/renderer/core/layout/layout_view.h" |
| #include "third_party/blink/renderer/core/loader/document_loader.h" |
| #include "third_party/blink/renderer/modules/event_modules.h" |
| #include "third_party/blink/renderer/modules/event_target_modules.h" |
| #include "third_party/blink/renderer/modules/xr/xr_frame_provider.h" |
| #include "third_party/blink/renderer/modules/xr/xr_session.h" |
| #include "third_party/blink/renderer/modules/xr/xr_session_viewport_scaler.h" |
| #include "third_party/blink/renderer/platform/bindings/v8_throw_exception.h" |
| #include "third_party/blink/renderer/platform/graphics/static_bitmap_image.h" |
| #include "third_party/blink/renderer/platform/heap/heap.h" |
| #include "third_party/blink/renderer/platform/wtf/functional.h" |
| #include "third_party/blink/renderer/platform/wtf/text/string_view.h" |
| #include "third_party/blink/renderer/platform/wtf/text/wtf_string.h" |
| |
| namespace blink { |
| |
| namespace { |
| |
| const char kNavigatorDetachedError[] = |
| "The navigator.xr object is no longer associated with a document."; |
| |
| const char kPageNotVisible[] = "The page is not visible"; |
| |
| const char kFeaturePolicyBlocked[] = |
| "Access to the feature \"xr\" is disallowed by permissions policy."; |
| |
| const char kActiveImmersiveSession[] = |
| "There is already an active, immersive XRSession."; |
| |
| const char kRequestRequiresUserActivation[] = |
| "The requested session requires user activation."; |
| |
| const char kSessionNotSupported[] = |
| "The specified session configuration is not supported."; |
| |
| const char kNoDevicesMessage[] = "No XR hardware found."; |
| |
| const char kImmersiveArModeNotValid[] = |
| "Failed to execute '%s' on 'XRSystem': The provided value 'immersive-ar' " |
| "is not a valid enum value of type XRSessionMode."; |
| |
| const char kTrackedImageWidthInvalid[] = |
| "trackedImages[%d].widthInMeters invalid, must be a positive number."; |
| |
| const char kDepthSensingConfigurationNotSupported[] = |
| "The provided preferences depth sensing usage and format are not " |
| "supported, unable to create the session."; |
| |
| constexpr device::mojom::XRSessionFeature kDefaultImmersiveVrFeatures[] = { |
| device::mojom::XRSessionFeature::REF_SPACE_VIEWER, |
| device::mojom::XRSessionFeature::REF_SPACE_LOCAL, |
| }; |
| |
| constexpr device::mojom::XRSessionFeature kDefaultImmersiveArFeatures[] = { |
| device::mojom::XRSessionFeature::REF_SPACE_VIEWER, |
| device::mojom::XRSessionFeature::REF_SPACE_LOCAL, |
| }; |
| |
| constexpr device::mojom::XRSessionFeature kDefaultInlineFeatures[] = { |
| device::mojom::XRSessionFeature::REF_SPACE_VIEWER, |
| }; |
| |
| device::mojom::blink::XRSessionMode stringToSessionMode( |
| const String& mode_string) { |
| if (mode_string == "inline") { |
| return device::mojom::blink::XRSessionMode::kInline; |
| } |
| if (mode_string == "immersive-vr") { |
| return device::mojom::blink::XRSessionMode::kImmersiveVr; |
| } |
| if (mode_string == "immersive-ar") { |
| return device::mojom::blink::XRSessionMode::kImmersiveAr; |
| } |
| |
| NOTREACHED(); // Only strings in the enum are allowed by IDL. |
| return device::mojom::blink::XRSessionMode::kInline; |
| } |
| |
| const char* SessionModeToString(device::mojom::blink::XRSessionMode mode) { |
| switch (mode) { |
| case device::mojom::blink::XRSessionMode::kInline: |
| return "inline"; |
| case device::mojom::blink::XRSessionMode::kImmersiveVr: |
| return "immersive-vr"; |
| case device::mojom::blink::XRSessionMode::kImmersiveAr: |
| return "immersive-ar"; |
| } |
| |
| NOTREACHED(); |
| return ""; |
| } |
| |
| // TODO(crbug.com/1070871): Drop this #if-else |
| #if defined(USE_BLINK_V8_BINDING_NEW_IDL_DICTIONARY) |
| |
| device::mojom::XRDepthUsage ParseDepthUsage(const V8XRDepthUsage& usage) { |
| switch (usage.AsEnum()) { |
| case V8XRDepthUsage::Enum::kCpuOptimized: |
| return device::mojom::XRDepthUsage::kCPUOptimized; |
| case V8XRDepthUsage::Enum::kGpuOptimized: |
| return device::mojom::XRDepthUsage::kGPUOptimized; |
| } |
| } |
| |
| Vector<device::mojom::XRDepthUsage> ParseDepthUsages( |
| const Vector<V8XRDepthUsage>& usages) { |
| Vector<device::mojom::XRDepthUsage> result; |
| |
| std::transform(usages.begin(), usages.end(), std::back_inserter(result), |
| ParseDepthUsage); |
| |
| return result; |
| } |
| |
| device::mojom::XRDepthDataFormat ParseDepthFormat( |
| const V8XRDepthDataFormat& format) { |
| switch (format.AsEnum()) { |
| case V8XRDepthDataFormat::Enum::kLuminanceAlpha: |
| return device::mojom::XRDepthDataFormat::kLuminanceAlpha; |
| case V8XRDepthDataFormat::Enum::kFloat32: |
| return device::mojom::XRDepthDataFormat::kFloat32; |
| } |
| } |
| |
| Vector<device::mojom::XRDepthDataFormat> ParseDepthFormats( |
| const Vector<V8XRDepthDataFormat>& formats) { |
| Vector<device::mojom::XRDepthDataFormat> result; |
| |
| std::transform(formats.begin(), formats.end(), std::back_inserter(result), |
| ParseDepthFormat); |
| |
| return result; |
| } |
| |
| #else |
| |
| device::mojom::XRDepthUsage ParseDepthUsage(const String& usage) { |
| if (usage == "cpu-optimized") { |
| return device::mojom::XRDepthUsage::kCPUOptimized; |
| } else if (usage == "gpu-optimized") { |
| return device::mojom::XRDepthUsage::kGPUOptimized; |
| } |
| |
| NOTREACHED() << "Only strings in the enum are allowed by IDL"; |
| return device::mojom::XRDepthUsage::kCPUOptimized; |
| } |
| |
| Vector<device::mojom::XRDepthUsage> ParseDepthUsages( |
| const Vector<String>& usages) { |
| Vector<device::mojom::XRDepthUsage> result; |
| |
| std::transform(usages.begin(), usages.end(), std::back_inserter(result), |
| ParseDepthUsage); |
| |
| return result; |
| } |
| |
| device::mojom::XRDepthDataFormat ParseDepthFormat(const String& format) { |
| if (format == "luminance-alpha") { |
| return device::mojom::XRDepthDataFormat::kLuminanceAlpha; |
| } else if (format == "float32") { |
| return device::mojom::XRDepthDataFormat::kFloat32; |
| } |
| |
| NOTREACHED() << "Only strings in the enum are allowed by IDL"; |
| return device::mojom::XRDepthDataFormat::kLuminanceAlpha; |
| } |
| |
| Vector<device::mojom::XRDepthDataFormat> ParseDepthFormats( |
| const Vector<String>& formats) { |
| Vector<device::mojom::XRDepthDataFormat> result; |
| |
| std::transform(formats.begin(), formats.end(), std::back_inserter(result), |
| ParseDepthFormat); |
| |
| return result; |
| } |
| |
| #endif // USE_BLINK_V8_BINDING_NEW_IDL_DICTIONARY |
| |
| // Converts the given string to an XRSessionFeature. If the string is |
| // unrecognized, returns nullopt. Based on the spec: |
| // https://immersive-web.github.io/webxr/#feature-name |
| base::Optional<device::mojom::XRSessionFeature> StringToXRSessionFeature( |
| const ExecutionContext* context, |
| const String& feature_string) { |
| if (feature_string == "viewer") { |
| return device::mojom::XRSessionFeature::REF_SPACE_VIEWER; |
| } else if (feature_string == "local") { |
| return device::mojom::XRSessionFeature::REF_SPACE_LOCAL; |
| } else if (feature_string == "local-floor") { |
| return device::mojom::XRSessionFeature::REF_SPACE_LOCAL_FLOOR; |
| } else if (feature_string == "bounded-floor") { |
| return device::mojom::XRSessionFeature::REF_SPACE_BOUNDED_FLOOR; |
| } else if (feature_string == "unbounded") { |
| return device::mojom::XRSessionFeature::REF_SPACE_UNBOUNDED; |
| } else if (RuntimeEnabledFeatures::WebXRHitTestEnabled(context) && |
| feature_string == "hit-test") { |
| return device::mojom::XRSessionFeature::HIT_TEST; |
| } else if (RuntimeEnabledFeatures::WebXRAnchorsEnabled(context) && |
| feature_string == "anchors") { |
| return device::mojom::XRSessionFeature::ANCHORS; |
| } else if (feature_string == "dom-overlay") { |
| return device::mojom::XRSessionFeature::DOM_OVERLAY; |
| } else if (RuntimeEnabledFeatures::WebXRLightEstimationEnabled(context) && |
| feature_string == "light-estimation") { |
| return device::mojom::XRSessionFeature::LIGHT_ESTIMATION; |
| } else if (RuntimeEnabledFeatures::WebXRCameraAccessEnabled(context) && |
| feature_string == "camera-access") { |
| return device::mojom::XRSessionFeature::CAMERA_ACCESS; |
| } else if (RuntimeEnabledFeatures::WebXRPlaneDetectionEnabled(context) && |
| feature_string == "plane-detection") { |
| return device::mojom::XRSessionFeature::PLANE_DETECTION; |
| } else if (RuntimeEnabledFeatures::WebXRDepthEnabled(context) && |
| feature_string == "depth-sensing") { |
| return device::mojom::XRSessionFeature::DEPTH; |
| } else if (RuntimeEnabledFeatures::WebXRImageTrackingEnabled(context) && |
| feature_string == "image-tracking") { |
| return device::mojom::XRSessionFeature::IMAGE_TRACKING; |
| } else if (RuntimeEnabledFeatures::WebXRHandInputEnabled(context) && |
| feature_string == "hand-tracking") { |
| return device::mojom::XRSessionFeature::HAND_INPUT; |
| } |
| |
| return base::nullopt; |
| } |
| |
| bool IsFeatureValidForMode(device::mojom::XRSessionFeature feature, |
| device::mojom::blink::XRSessionMode mode, |
| XRSessionInit* session_init, |
| ExecutionContext* execution_context, |
| mojom::blink::ConsoleMessageLevel error_level) { |
| switch (feature) { |
| case device::mojom::XRSessionFeature::REF_SPACE_VIEWER: |
| case device::mojom::XRSessionFeature::REF_SPACE_LOCAL: |
| case device::mojom::XRSessionFeature::REF_SPACE_LOCAL_FLOOR: |
| return true; |
| case device::mojom::XRSessionFeature::REF_SPACE_BOUNDED_FLOOR: |
| case device::mojom::XRSessionFeature::REF_SPACE_UNBOUNDED: |
| case device::mojom::XRSessionFeature::HIT_TEST: |
| case device::mojom::XRSessionFeature::ANCHORS: |
| case device::mojom::XRSessionFeature::HAND_INPUT: |
| return mode == device::mojom::blink::XRSessionMode::kImmersiveVr || |
| mode == device::mojom::blink::XRSessionMode::kImmersiveAr; |
| case device::mojom::XRSessionFeature::DOM_OVERLAY: |
| if (mode != device::mojom::blink::XRSessionMode::kImmersiveAr) |
| return false; |
| if (!session_init->hasDomOverlay()) { |
| execution_context->AddConsoleMessage(MakeGarbageCollected< |
| ConsoleMessage>( |
| mojom::blink::ConsoleMessageSource::kJavaScript, error_level, |
| "Must specify a valid domOverlay.root element in XRSessionInit")); |
| return false; |
| } |
| return true; |
| case device::mojom::XRSessionFeature::IMAGE_TRACKING: |
| if (mode != device::mojom::blink::XRSessionMode::kImmersiveAr) |
| return false; |
| if (!session_init->hasTrackedImages()) { |
| execution_context->AddConsoleMessage( |
| MakeGarbageCollected<ConsoleMessage>( |
| mojom::blink::ConsoleMessageSource::kJavaScript, error_level, |
| "Must specify trackedImages in XRSessionInit")); |
| return false; |
| } |
| return true; |
| case device::mojom::XRSessionFeature::LIGHT_ESTIMATION: |
| case device::mojom::XRSessionFeature::CAMERA_ACCESS: |
| case device::mojom::XRSessionFeature::PLANE_DETECTION: |
| // Fallthrough - light estimation, camera access, and plane detection are |
| // all valid only for immersive AR mode for now. |
| return mode == device::mojom::blink::XRSessionMode::kImmersiveAr; |
| case device::mojom::XRSessionFeature::DEPTH: |
| if (!session_init->hasDepthSensing()) { |
| execution_context->AddConsoleMessage( |
| MakeGarbageCollected<ConsoleMessage>( |
| mojom::blink::ConsoleMessageSource::kJavaScript, error_level, |
| "Must provide a depthSensing dictionary in XRSessionInit")); |
| return false; |
| } |
| return mode == device::mojom::blink::XRSessionMode::kImmersiveAr; |
| } |
| } |
| |
| bool HasRequiredFeaturePolicy(const ExecutionContext* context, |
| device::mojom::XRSessionFeature feature) { |
| if (!context) |
| return false; |
| |
| switch (feature) { |
| case device::mojom::XRSessionFeature::REF_SPACE_VIEWER: |
| return true; |
| case device::mojom::XRSessionFeature::REF_SPACE_LOCAL: |
| case device::mojom::XRSessionFeature::REF_SPACE_LOCAL_FLOOR: |
| case device::mojom::XRSessionFeature::REF_SPACE_BOUNDED_FLOOR: |
| case device::mojom::XRSessionFeature::REF_SPACE_UNBOUNDED: |
| case device::mojom::XRSessionFeature::DOM_OVERLAY: |
| case device::mojom::XRSessionFeature::HIT_TEST: |
| case device::mojom::XRSessionFeature::LIGHT_ESTIMATION: |
| case device::mojom::XRSessionFeature::ANCHORS: |
| case device::mojom::XRSessionFeature::CAMERA_ACCESS: |
| case device::mojom::XRSessionFeature::PLANE_DETECTION: |
| case device::mojom::XRSessionFeature::DEPTH: |
| case device::mojom::XRSessionFeature::IMAGE_TRACKING: |
| case device::mojom::XRSessionFeature::HAND_INPUT: |
| return context->IsFeatureEnabled( |
| mojom::blink::FeaturePolicyFeature::kWebXr, |
| ReportOptions::kReportOnFailure); |
| } |
| } |
| |
| // Ensure that the immersive session request is allowed, if not |
| // return which security error occurred. |
| // https://immersive-web.github.io/webxr/#immersive-session-request-is-allowed |
| const char* CheckImmersiveSessionRequestAllowed(LocalDOMWindow* window) { |
| // Ensure that the session was initiated by a user gesture |
| if (!LocalFrame::HasTransientUserActivation(window->GetFrame())) { |
| return kRequestRequiresUserActivation; |
| } |
| |
| // Check that the document is "trustworthy" |
| // https://immersive-web.github.io/webxr/#trustworthy |
| if (!window->document()->IsPageVisible()) { |
| return kPageNotVisible; |
| } |
| |
| // Consent occurs in the Browser process. |
| |
| return nullptr; |
| } |
| |
| // Helper method to convert the mojom error code into text for displaying in the |
| // console. The console message will have the format of: |
| // "Could not create a session because: <this value>" |
| const char* GetConsoleMessage(device::mojom::RequestSessionError error) { |
| switch (error) { |
| case device::mojom::RequestSessionError::EXISTING_IMMERSIVE_SESSION: |
| return "There is already an existing immersive session"; |
| case device::mojom::RequestSessionError::INVALID_CLIENT: |
| return "An error occurred while querying for runtime support"; |
| case device::mojom::RequestSessionError::USER_DENIED_CONSENT: |
| return "The user denied some part of the requested configuration"; |
| case device::mojom::RequestSessionError::NO_RUNTIME_FOUND: |
| return "No runtimes supported the requested configuration"; |
| case device::mojom::RequestSessionError::UNKNOWN_RUNTIME_ERROR: |
| return "Something went wrong initializing the session in the runtime"; |
| case device::mojom::RequestSessionError::RUNTIME_INSTALL_FAILURE: |
| return "The runtime for this configuration could not be installed"; |
| case device::mojom::RequestSessionError::RUNTIMES_CHANGED: |
| return "The supported runtimes changed while initializing the session"; |
| case device::mojom::RequestSessionError::FULLSCREEN_ERROR: |
| return "An error occurred while initializing fullscreen support"; |
| case device::mojom::RequestSessionError::UNKNOWN_FAILURE: |
| return "An unknown error occurred"; |
| } |
| } |
| |
| bool IsFeatureRequested( |
| device::mojom::XRSessionFeatureRequestStatus requestStatus) { |
| switch (requestStatus) { |
| case device::mojom::XRSessionFeatureRequestStatus::kOptionalAccepted: |
| case device::mojom::XRSessionFeatureRequestStatus::kRequired: |
| return true; |
| case device::mojom::XRSessionFeatureRequestStatus::kNotRequested: |
| case device::mojom::XRSessionFeatureRequestStatus::kOptionalRejected: |
| return false; |
| } |
| } |
| |
| bool IsImmersiveArAllowedBySettings(LocalDOMWindow* window) { |
| // If we're unable to get the settings for any reason, we'll treat the AR as |
| // enabled. |
| if (!window->GetFrame()) { |
| return true; |
| } |
| |
| return window->GetFrame()->GetSettings()->GetWebXRImmersiveArAllowed(); |
| } |
| |
| } // namespace |
| |
| // Ensure that the inline session request is allowed, if not |
| // return which security error occurred. |
| // https://immersive-web.github.io/webxr/#inline-session-request-is-allowed |
| const char* XRSystem::CheckInlineSessionRequestAllowed( |
| LocalFrame* frame, |
| const PendingRequestSessionQuery& query) { |
| // Without user activation, we must reject the session if *any* features |
| // (optional or required) were present, whether or not they were recognized. |
| // The only exception to this is the 'viewer' feature. |
| if (!LocalFrame::HasTransientUserActivation(frame)) { |
| if (query.InvalidOptionalFeatures() || query.InvalidRequiredFeatures()) { |
| return kRequestRequiresUserActivation; |
| } |
| |
| // If any required features (besides 'viewer') were requested, reject. |
| for (auto feature : query.RequiredFeatures()) { |
| if (feature != device::mojom::XRSessionFeature::REF_SPACE_VIEWER) { |
| return kRequestRequiresUserActivation; |
| } |
| } |
| |
| // If any optional features (besides 'viewer') were requested, reject. |
| for (auto feature : query.OptionalFeatures()) { |
| if (feature != device::mojom::XRSessionFeature::REF_SPACE_VIEWER) { |
| return kRequestRequiresUserActivation; |
| } |
| } |
| } |
| |
| return nullptr; |
| } |
| |
| XRSystem::PendingSupportsSessionQuery::PendingSupportsSessionQuery( |
| ScriptPromiseResolver* resolver, |
| device::mojom::blink::XRSessionMode session_mode, |
| bool throw_on_unsupported) |
| : resolver_(resolver), |
| mode_(session_mode), |
| throw_on_unsupported_(throw_on_unsupported) {} |
| |
| void XRSystem::PendingSupportsSessionQuery::Trace(Visitor* visitor) const { |
| visitor->Trace(resolver_); |
| } |
| |
| void XRSystem::PendingSupportsSessionQuery::Resolve( |
| bool supported, |
| ExceptionState* exception_state) { |
| if (throw_on_unsupported_) { |
| if (supported) { |
| resolver_->Resolve(); |
| } else { |
| DVLOG(2) << __func__ << ": session is unsupported - throwing exception"; |
| RejectWithDOMException(DOMExceptionCode::kNotSupportedError, |
| kSessionNotSupported, exception_state); |
| } |
| } else { |
| resolver_->Resolve(supported); |
| } |
| } |
| |
| void XRSystem::PendingSupportsSessionQuery::RejectWithDOMException( |
| DOMExceptionCode exception_code, |
| const String& message, |
| ExceptionState* exception_state) { |
| DCHECK_NE(exception_code, DOMExceptionCode::kSecurityError); |
| |
| if (exception_state) { |
| // The generated bindings will reject the returned promise for us. |
| // Detaching the resolver prevents it from thinking we abandoned |
| // the promise. |
| exception_state->ThrowDOMException(exception_code, message); |
| resolver_->Detach(); |
| } else { |
| resolver_->Reject( |
| MakeGarbageCollected<DOMException>(exception_code, message)); |
| } |
| } |
| |
| void XRSystem::PendingSupportsSessionQuery::RejectWithSecurityError( |
| const String& sanitized_message, |
| ExceptionState* exception_state) { |
| if (exception_state) { |
| // The generated V8 bindings will reject the returned promise for us. |
| // Detaching the resolver prevents it from thinking we abandoned |
| // the promise. |
| exception_state->ThrowSecurityError(sanitized_message); |
| resolver_->Detach(); |
| } else { |
| resolver_->Reject(MakeGarbageCollected<DOMException>( |
| DOMExceptionCode::kSecurityError, sanitized_message)); |
| } |
| } |
| |
| void XRSystem::PendingSupportsSessionQuery::RejectWithTypeError( |
| const String& message, |
| ExceptionState* exception_state) { |
| if (exception_state) { |
| // The generated bindings will reject the returned promise for us. |
| // Detaching the resolver prevents it from thinking we abandoned |
| // the promise. |
| exception_state->ThrowTypeError(message); |
| resolver_->Detach(); |
| } else { |
| resolver_->Reject(V8ThrowException::CreateTypeError( |
| resolver_->GetScriptState()->GetIsolate(), message)); |
| } |
| } |
| |
| device::mojom::blink::XRSessionMode |
| XRSystem::PendingSupportsSessionQuery::mode() const { |
| return mode_; |
| } |
| |
| XRSystem::PendingRequestSessionQuery::PendingRequestSessionQuery( |
| int64_t ukm_source_id, |
| ScriptPromiseResolver* resolver, |
| device::mojom::blink::XRSessionMode session_mode, |
| RequestedXRSessionFeatureSet required_features, |
| RequestedXRSessionFeatureSet optional_features) |
| : resolver_(resolver), |
| mode_(session_mode), |
| required_features_(std::move(required_features)), |
| optional_features_(std::move(optional_features)), |
| ukm_source_id_(ukm_source_id) { |
| ParseSensorRequirement(); |
| } |
| |
| void XRSystem::PendingRequestSessionQuery::Resolve( |
| XRSession* session, |
| mojo::PendingRemote<device::mojom::blink::XRSessionMetricsRecorder> |
| metrics_recorder) { |
| resolver_->Resolve(session); |
| ReportRequestSessionResult(SessionRequestStatus::kSuccess, session, |
| std::move(metrics_recorder)); |
| } |
| |
| void XRSystem::PendingRequestSessionQuery::RejectWithDOMException( |
| DOMExceptionCode exception_code, |
| const String& message, |
| ExceptionState* exception_state) { |
| DCHECK_NE(exception_code, DOMExceptionCode::kSecurityError); |
| |
| if (exception_state) { |
| exception_state->ThrowDOMException(exception_code, message); |
| resolver_->Detach(); |
| } else { |
| resolver_->Reject( |
| MakeGarbageCollected<DOMException>(exception_code, message)); |
| } |
| |
| ReportRequestSessionResult(SessionRequestStatus::kOtherError); |
| } |
| |
| void XRSystem::PendingRequestSessionQuery::RejectWithSecurityError( |
| const String& sanitized_message, |
| ExceptionState* exception_state) { |
| if (exception_state) { |
| exception_state->ThrowSecurityError(sanitized_message); |
| resolver_->Detach(); |
| } else { |
| resolver_->Reject(MakeGarbageCollected<DOMException>( |
| DOMExceptionCode::kSecurityError, sanitized_message)); |
| } |
| |
| ReportRequestSessionResult(SessionRequestStatus::kOtherError); |
| } |
| |
| void XRSystem::PendingRequestSessionQuery::RejectWithTypeError( |
| const String& message, |
| ExceptionState* exception_state) { |
| if (exception_state) { |
| exception_state->ThrowTypeError(message); |
| resolver_->Detach(); |
| } else { |
| resolver_->Reject(V8ThrowException::CreateTypeError( |
| GetScriptState()->GetIsolate(), message)); |
| } |
| |
| ReportRequestSessionResult(SessionRequestStatus::kOtherError); |
| } |
| |
| device::mojom::XRSessionFeatureRequestStatus |
| XRSystem::PendingRequestSessionQuery::GetFeatureRequestStatus( |
| device::mojom::XRSessionFeature feature, |
| const XRSession* session) const { |
| using device::mojom::XRSessionFeatureRequestStatus; |
| |
| if (RequiredFeatures().Contains(feature)) { |
| // In the case of required features, accepted/rejected state is |
| // the same as the entire session. |
| return XRSessionFeatureRequestStatus::kRequired; |
| } |
| |
| if (OptionalFeatures().Contains(feature)) { |
| if (!session || !session->IsFeatureEnabled(feature)) { |
| return XRSessionFeatureRequestStatus::kOptionalRejected; |
| } |
| |
| return XRSessionFeatureRequestStatus::kOptionalAccepted; |
| } |
| |
| return XRSessionFeatureRequestStatus::kNotRequested; |
| } |
| |
| void XRSystem::PendingRequestSessionQuery::ReportRequestSessionResult( |
| SessionRequestStatus status, |
| XRSession* session, |
| mojo::PendingRemote<device::mojom::blink::XRSessionMetricsRecorder> |
| metrics_recorder) { |
| using device::mojom::XRSessionFeature; |
| |
| if (!resolver_->DomWindow()) |
| return; |
| |
| auto feature_request_viewer = |
| GetFeatureRequestStatus(XRSessionFeature::REF_SPACE_VIEWER, session); |
| auto feature_request_local = |
| GetFeatureRequestStatus(XRSessionFeature::REF_SPACE_LOCAL, session); |
| auto feature_request_local_floor = |
| GetFeatureRequestStatus(XRSessionFeature::REF_SPACE_LOCAL_FLOOR, session); |
| auto feature_request_bounded_floor = GetFeatureRequestStatus( |
| XRSessionFeature::REF_SPACE_BOUNDED_FLOOR, session); |
| auto feature_request_unbounded = |
| GetFeatureRequestStatus(XRSessionFeature::REF_SPACE_UNBOUNDED, session); |
| auto feature_request_dom_overlay = |
| GetFeatureRequestStatus(XRSessionFeature::DOM_OVERLAY, session); |
| auto feature_request_depth_sensing = |
| GetFeatureRequestStatus(XRSessionFeature::DEPTH, session); |
| |
| ukm::builders::XR_WebXR_SessionRequest(ukm_source_id_) |
| .SetMode(static_cast<int64_t>(mode_)) |
| .SetStatus(static_cast<int64_t>(status)) |
| .SetFeature_Viewer(static_cast<int64_t>(feature_request_viewer)) |
| .SetFeature_Local(static_cast<int64_t>(feature_request_local)) |
| .SetFeature_LocalFloor(static_cast<int64_t>(feature_request_local_floor)) |
| .SetFeature_BoundedFloor( |
| static_cast<int64_t>(feature_request_bounded_floor)) |
| .SetFeature_Unbounded(static_cast<int64_t>(feature_request_unbounded)) |
| .Record(resolver_->DomWindow()->UkmRecorder()); |
| |
| // If the session was successfully created and DOM overlay was requested, |
| // count this as a use of the DOM overlay feature. |
| if (session && status == SessionRequestStatus::kSuccess && |
| IsFeatureRequested(feature_request_dom_overlay)) { |
| DVLOG(2) << __func__ << ": DOM overlay was requested, logging a UseCounter"; |
| UseCounter::Count(session->GetExecutionContext(), |
| WebFeature::kXRDOMOverlay); |
| } |
| |
| // If the session was successfully created and depth-sensing was requested, |
| // count this as a use of depth sensing feature. |
| if (session && status == SessionRequestStatus::kSuccess && |
| IsFeatureRequested(feature_request_depth_sensing)) { |
| DVLOG(2) << __func__ |
| << ": depth sensing was requested, logging a UseCounter"; |
| UseCounter::Count(session->GetExecutionContext(), |
| WebFeature::kXRDepthSensing); |
| } |
| |
| if (session && metrics_recorder) { |
| mojo::Remote<device::mojom::blink::XRSessionMetricsRecorder> recorder( |
| std::move(metrics_recorder)); |
| session->SetMetricsReporter( |
| std::make_unique<XRSession::MetricsReporter>(std::move(recorder))); |
| } |
| } |
| |
| device::mojom::blink::XRSessionMode XRSystem::PendingRequestSessionQuery::mode() |
| const { |
| return mode_; |
| } |
| |
| const XRSessionFeatureSet& |
| XRSystem::PendingRequestSessionQuery::RequiredFeatures() const { |
| return required_features_.valid_features; |
| } |
| |
| const XRSessionFeatureSet& |
| XRSystem::PendingRequestSessionQuery::OptionalFeatures() const { |
| return optional_features_.valid_features; |
| } |
| |
| bool XRSystem::PendingRequestSessionQuery::HasFeature( |
| device::mojom::XRSessionFeature feature) const { |
| return RequiredFeatures().Contains(feature) || |
| OptionalFeatures().Contains(feature); |
| } |
| |
| bool XRSystem::PendingRequestSessionQuery::InvalidRequiredFeatures() const { |
| return required_features_.invalid_features; |
| } |
| |
| bool XRSystem::PendingRequestSessionQuery::InvalidOptionalFeatures() const { |
| return optional_features_.invalid_features; |
| } |
| |
| ScriptState* XRSystem::PendingRequestSessionQuery::GetScriptState() const { |
| return resolver_->GetScriptState(); |
| } |
| |
| void XRSystem::PendingRequestSessionQuery::ParseSensorRequirement() { |
| // All modes other than inline require sensors. |
| if (mode_ != device::mojom::blink::XRSessionMode::kInline) { |
| sensor_requirement_ = SensorRequirement::kRequired; |
| return; |
| } |
| |
| // If any required features require sensors, then sensors are required. |
| for (const auto& feature : RequiredFeatures()) { |
| if (feature != device::mojom::XRSessionFeature::REF_SPACE_VIEWER) { |
| sensor_requirement_ = SensorRequirement::kRequired; |
| return; |
| } |
| } |
| |
| // If any optional features require sensors, then sensors are optional. |
| for (const auto& feature : OptionalFeatures()) { |
| if (feature != device::mojom::XRSessionFeature::REF_SPACE_VIEWER) { |
| sensor_requirement_ = SensorRequirement::kOptional; |
| return; |
| } |
| } |
| |
| // By this point any situation that requires sensors should have returned. |
| sensor_requirement_ = kNone; |
| } |
| |
| void XRSystem::PendingRequestSessionQuery::Trace(Visitor* visitor) const { |
| visitor->Trace(resolver_); |
| visitor->Trace(dom_overlay_element_); |
| } |
| |
| XRSystem::OverlayFullscreenEventManager::OverlayFullscreenEventManager( |
| XRSystem* xr, |
| XRSystem::PendingRequestSessionQuery* query, |
| device::mojom::blink::RequestSessionResultPtr result) |
| : xr_(xr), query_(query), result_(std::move(result)) { |
| DVLOG(2) << __func__; |
| } |
| |
| XRSystem::OverlayFullscreenEventManager::~OverlayFullscreenEventManager() = |
| default; |
| |
| void XRSystem::OverlayFullscreenEventManager::Invoke( |
| ExecutionContext* execution_context, |
| Event* event) { |
| DVLOG(2) << __func__ << ": event type=" << event->type(); |
| |
| // This handler should only be called once, it's unregistered after use. |
| DCHECK(query_); |
| DCHECK(result_); |
| |
| Element* element = query_->DOMOverlayElement(); |
| element->GetDocument().removeEventListener( |
| event_type_names::kFullscreenchange, this, true); |
| element->GetDocument().removeEventListener(event_type_names::kFullscreenerror, |
| this, true); |
| |
| if (event->type() == event_type_names::kFullscreenchange) { |
| // Succeeded, proceed with session creation. |
| element->GetDocument().GetViewportData().SetExpandIntoDisplayCutout(true); |
| xr_->OnRequestSessionReturned(query_, std::move(result_)); |
| } |
| |
| if (event->type() == event_type_names::kFullscreenerror) { |
| // Failed, reject the session |
| xr_->OnRequestSessionReturned( |
| query_, device::mojom::blink::RequestSessionResult::NewFailureReason( |
| device::mojom::RequestSessionError::FULLSCREEN_ERROR)); |
| } |
| } |
| |
| void XRSystem::OverlayFullscreenEventManager::RequestFullscreen() { |
| Element* element = query_->DOMOverlayElement(); |
| DCHECK(element); |
| |
| bool wait_for_fullscreen_change = true; |
| |
| if (element == Fullscreen::FullscreenElementFrom(element->GetDocument())) { |
| // It's possible that the requested element is already fullscreen, in which |
| // case we must not wait for a fullscreenchange event since it won't arrive. |
| // This can happen if the site used Fullscreen API to place the element into |
| // fullscreen mode before requesting the session, and if the session can |
| // proceed without needing a permission prompt. (Showing a dialog exits |
| // fullscreen mode.) |
| // |
| // We still need to do the RequestFullscreen call to apply the kForXrOverlay |
| // property which sets the background transparent. |
| DVLOG(2) << __func__ << ": requested element already fullscreen"; |
| wait_for_fullscreen_change = false; |
| } |
| |
| if (wait_for_fullscreen_change) { |
| // Set up event listeners for success and failure. |
| element->GetDocument().addEventListener(event_type_names::kFullscreenchange, |
| this, true); |
| element->GetDocument().addEventListener(event_type_names::kFullscreenerror, |
| this, true); |
| } |
| |
| // Use the event-generating unprefixed version of RequestFullscreen to ensure |
| // that the fullscreen event listener is informed once this completes. |
| FullscreenOptions* options = FullscreenOptions::Create(); |
| options->setNavigationUI("hide"); |
| |
| // Grant fullscreen API permission for the following call. Requesting the |
| // immersive session had required a user activation state, but that may have |
| // expired by now due to the user taking time to respond to the consent |
| // prompt. |
| ScopedAllowFullscreen scope(ScopedAllowFullscreen::kXrOverlay); |
| |
| Fullscreen::RequestFullscreen(*element, options, |
| FullscreenRequestType::kUnprefixed | |
| FullscreenRequestType::kForXrOverlay); |
| |
| if (!wait_for_fullscreen_change) { |
| // Element was already fullscreen, proceed with session creation. |
| xr_->OnRequestSessionReturned(query_, std::move(result_)); |
| } |
| } |
| |
| void XRSystem::OverlayFullscreenEventManager::Trace(Visitor* visitor) const { |
| visitor->Trace(xr_); |
| visitor->Trace(query_); |
| EventListener::Trace(visitor); |
| } |
| |
| XRSystem::OverlayFullscreenExitObserver::OverlayFullscreenExitObserver( |
| XRSystem* xr) |
| : xr_(xr) { |
| DVLOG(2) << __func__; |
| } |
| |
| XRSystem::OverlayFullscreenExitObserver::~OverlayFullscreenExitObserver() = |
| default; |
| |
| void XRSystem::OverlayFullscreenExitObserver::Invoke( |
| ExecutionContext* execution_context, |
| Event* event) { |
| DVLOG(2) << __func__ << ": event type=" << event->type(); |
| |
| document_->removeEventListener(event_type_names::kFullscreenchange, this, |
| true); |
| |
| if (event->type() == event_type_names::kFullscreenchange) { |
| // Succeeded, proceed with session shutdown. Expanding into the fullscreen |
| // cutout is only valid for fullscreen mode which we just exited (cf. |
| // MediaControlsDisplayCutoutDelegate::DidExitFullscreen), so we can |
| // unconditionally turn this off here. |
| document_->GetViewportData().SetExpandIntoDisplayCutout(false); |
| xr_->ExitPresent(std::move(on_exited_)); |
| } |
| } |
| |
| void XRSystem::OverlayFullscreenExitObserver::ExitFullscreen( |
| Document* document, |
| base::OnceClosure on_exited) { |
| DVLOG(2) << __func__; |
| document_ = document; |
| on_exited_ = std::move(on_exited); |
| |
| document->addEventListener(event_type_names::kFullscreenchange, this, true); |
| // "ua_originated" means that the browser process already exited |
| // fullscreen. Set it to false because we need the browser process |
| // to get notified that it needs to exit fullscreen. Use |
| // FullyExitFullscreen to ensure that we return to non-fullscreen mode. |
| // ExitFullscreen only unfullscreens a single element, potentially |
| // leaving others in fullscreen mode. |
| constexpr bool kUaOriginated = false; |
| |
| Fullscreen::FullyExitFullscreen(*document, kUaOriginated); |
| } |
| |
| void XRSystem::OverlayFullscreenExitObserver::Trace(Visitor* visitor) const { |
| visitor->Trace(xr_); |
| visitor->Trace(document_); |
| EventListener::Trace(visitor); |
| } |
| |
| device::mojom::blink::XRSessionOptionsPtr XRSystem::XRSessionOptionsFromQuery( |
| const PendingRequestSessionQuery& query) { |
| device::mojom::blink::XRSessionOptionsPtr session_options = |
| device::mojom::blink::XRSessionOptions::New(); |
| session_options->mode = query.mode(); |
| |
| CopyToVector(query.RequiredFeatures(), session_options->required_features); |
| CopyToVector(query.OptionalFeatures(), session_options->optional_features); |
| |
| session_options->tracked_images.resize(query.TrackedImages().size()); |
| for (unsigned i = 0; i < query.TrackedImages().size(); ++i) { |
| session_options->tracked_images[i] = |
| device::mojom::blink::XRTrackedImage::New(); |
| *session_options->tracked_images[i] = query.TrackedImages()[i]; |
| } |
| |
| if (query.HasFeature(device::mojom::XRSessionFeature::DEPTH)) { |
| session_options->depth_options = |
| device::mojom::blink::XRDepthOptions::New(); |
| session_options->depth_options->usage_preferences = query.PreferredUsage(); |
| session_options->depth_options->data_format_preferences = |
| query.PreferredFormat(); |
| } |
| |
| return session_options; |
| } |
| |
| const char XRSystem::kSupplementName[] = "XRSystem"; |
| |
| XRSystem* XRSystem::FromIfExists(Document& document) { |
| if (!document.domWindow()) |
| return nullptr; |
| return Supplement<Navigator>::From<XRSystem>( |
| document.domWindow()->navigator()); |
| } |
| |
| XRSystem* XRSystem::From(Document& document) { |
| DVLOG(2) << __func__; |
| |
| return document.domWindow() ? xr(*document.domWindow()->navigator()) |
| : nullptr; |
| } |
| |
| XRSystem* XRSystem::xr(Navigator& navigator) { |
| DVLOG(2) << __func__; |
| |
| LocalDOMWindow* window = navigator.DomWindow(); |
| if (!window) |
| return nullptr; |
| |
| XRSystem* xr = Supplement<Navigator>::From<XRSystem>(navigator); |
| if (!xr) { |
| xr = MakeGarbageCollected<XRSystem>(navigator); |
| ProvideTo(navigator, xr); |
| |
| ukm::builders::XR_WebXR(window->UkmSourceID()) |
| .SetDidUseNavigatorXR(1) |
| .Record(window->UkmRecorder()); |
| } |
| return xr; |
| } |
| |
| XRSystem::XRSystem(Navigator& navigator) |
| : Supplement<Navigator>(navigator), |
| ExecutionContextLifecycleObserver(navigator.DomWindow()), |
| FocusChangedObserver(navigator.DomWindow()->GetFrame()->GetPage()), |
| service_(navigator.DomWindow()), |
| environment_provider_(navigator.DomWindow()), |
| receiver_(this, navigator.DomWindow()), |
| navigation_start_(navigator.DomWindow() |
| ->document() |
| ->Loader() |
| ->GetTiming() |
| .NavigationStart()), |
| feature_handle_for_scheduler_( |
| navigator.DomWindow() |
| ->GetFrame() |
| ->GetFrameScheduler() |
| ->RegisterFeature( |
| SchedulingPolicy::Feature::kWebXR, |
| {SchedulingPolicy::DisableBackForwardCache()})) {} |
| |
| void XRSystem::FocusedFrameChanged() { |
| // Tell all sessions that focus changed. |
| // Since this eventually dispatches an event to the page, the page could |
| // create a new session which would invalidate our iterators; so iterate over |
| // a copy of the session map. |
| HeapHashSet<WeakMember<XRSession>> processing_sessions = sessions_; |
| for (const auto& session : processing_sessions) { |
| session->OnFocusChanged(); |
| } |
| |
| if (frame_provider_) |
| frame_provider_->OnFocusChanged(); |
| } |
| |
| bool XRSystem::IsFrameFocused() { |
| return FocusChangedObserver::IsFrameFocused( |
| DomWindow() ? DomWindow()->GetFrame() : nullptr); |
| } |
| |
| ExecutionContext* XRSystem::GetExecutionContext() const { |
| return ExecutionContextLifecycleObserver::GetExecutionContext(); |
| } |
| |
| const AtomicString& XRSystem::InterfaceName() const { |
| return event_target_names::kXR; |
| } |
| |
| XRFrameProvider* XRSystem::frameProvider() { |
| if (!frame_provider_) { |
| frame_provider_ = MakeGarbageCollected<XRFrameProvider>(this); |
| } |
| |
| return frame_provider_; |
| } |
| |
| device::mojom::blink::XREnvironmentIntegrationProvider* |
| XRSystem::xrEnvironmentProviderRemote() { |
| return environment_provider_.get(); |
| } |
| |
| device::mojom::blink::VRService* XRSystem::BrowserService() { |
| return service_.get(); |
| } |
| |
| void XRSystem::AddEnvironmentProviderErrorHandler( |
| EnvironmentProviderErrorCallback callback) { |
| environment_provider_error_callbacks_.push_back(std::move(callback)); |
| } |
| |
| void XRSystem::ExitPresent(base::OnceClosure on_exited) { |
| DVLOG(1) << __func__; |
| |
| // If the document was potentially being shown in a DOM overlay via |
| // fullscreened elements, make sure to clear any fullscreen states on exiting |
| // the session. This avoids a race condition: |
| // - browser side ends session and exits fullscreen (i.e. back button) |
| // - renderer processes WebViewImpl::ExitFullscreen via ChromeClient |
| // - JS application sets a new element to fullscreen, this is allowed |
| // because doc->IsXrOverlay() is still true at this point |
| // - renderer processes XR session shutdown (this method) |
| // - browser re-enters fullscreen unexpectedly |
| if (LocalDOMWindow* window = DomWindow()) { |
| Document* doc = window->document(); |
| DVLOG(3) << __func__ << ": doc->IsXrOverlay()=" << doc->IsXrOverlay(); |
| if (doc->IsXrOverlay()) { |
| Element* fullscreen_element = Fullscreen::FullscreenElementFrom(*doc); |
| DVLOG(3) << __func__ << ": fullscreen_element=" << fullscreen_element; |
| if (fullscreen_element) { |
| fullscreen_exit_observer_ = |
| MakeGarbageCollected<OverlayFullscreenExitObserver>(this); |
| fullscreen_exit_observer_->ExitFullscreen(doc, std::move(on_exited)); |
| return; |
| } |
| } |
| } |
| |
| if (service_.is_bound()) { |
| service_->ExitPresent(std::move(on_exited)); |
| } else { |
| // The service was already shut down, run the callback immediately. |
| std::move(on_exited).Run(); |
| } |
| } |
| |
| void XRSystem::SetFramesThrottled(const XRSession* session, bool throttled) { |
| // The service only cares if the immersive session is throttling frames. |
| if (session->immersive()) { |
| // If we have an immersive session, we should have a service. |
| DCHECK(service_.is_bound()); |
| service_->SetFramesThrottled(throttled); |
| } |
| } |
| |
| ScriptPromise XRSystem::supportsSession(ScriptState* script_state, |
| const String& mode, |
| ExceptionState& exception_state) { |
| return InternalIsSessionSupported(script_state, mode, exception_state, true); |
| } |
| |
| ScriptPromise XRSystem::isSessionSupported(ScriptState* script_state, |
| const String& mode, |
| ExceptionState& exception_state) { |
| return InternalIsSessionSupported(script_state, mode, exception_state, false); |
| } |
| |
| void XRSystem::AddConsoleMessage(mojom::blink::ConsoleMessageLevel error_level, |
| const String& message) { |
| DVLOG(2) << __func__ << ": error_level=" << error_level |
| << ", message=" << message; |
| |
| GetExecutionContext()->AddConsoleMessage(MakeGarbageCollected<ConsoleMessage>( |
| mojom::blink::ConsoleMessageSource::kJavaScript, error_level, message)); |
| } |
| |
| ScriptPromise XRSystem::InternalIsSessionSupported( |
| ScriptState* script_state, |
| const String& mode, |
| ExceptionState& exception_state, |
| bool throw_on_unsupported) { |
| if (!GetExecutionContext()) { |
| // Reject if the context is inaccessible. |
| exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError, |
| kNavigatorDetachedError); |
| return ScriptPromise(); // Will be rejected by generated bindings |
| } |
| |
| auto* resolver = MakeGarbageCollected<ScriptPromiseResolver>(script_state); |
| ScriptPromise promise = resolver->Promise(); |
| |
| device::mojom::blink::XRSessionMode session_mode = stringToSessionMode(mode); |
| PendingSupportsSessionQuery* query = |
| MakeGarbageCollected<PendingSupportsSessionQuery>(resolver, session_mode, |
| throw_on_unsupported); |
| |
| if (session_mode == device::mojom::blink::XRSessionMode::kImmersiveAr && |
| !IsImmersiveArAllowed()) { |
| DVLOG(2) << __func__ |
| << ": Immersive AR session is only supported if WebXRARModule " |
| "feature is enabled by a runtime feature and web settings"; |
| query->Resolve(false); |
| return promise; |
| } |
| |
| if (session_mode == device::mojom::blink::XRSessionMode::kInline) { |
| // inline sessions are always supported. |
| query->Resolve(true); |
| return promise; |
| } |
| |
| if (!GetExecutionContext()->IsFeatureEnabled( |
| mojom::blink::FeaturePolicyFeature::kWebXr, |
| ReportOptions::kReportOnFailure)) { |
| // Only allow the call to be made if the appropriate feature policy is in |
| // place. |
| query->RejectWithSecurityError(kFeaturePolicyBlocked, &exception_state); |
| return promise; |
| } |
| |
| // If TryEnsureService() doesn't set |service_|, then we don't have any WebXR |
| // hardware, so we need to reject as being unsupported. |
| TryEnsureService(); |
| if (!service_.is_bound()) { |
| query->Resolve(false, &exception_state); |
| return promise; |
| } |
| |
| device::mojom::blink::XRSessionOptionsPtr session_options = |
| device::mojom::blink::XRSessionOptions::New(); |
| session_options->mode = query->mode(); |
| |
| outstanding_support_queries_.insert(query); |
| service_->SupportsSession( |
| std::move(session_options), |
| WTF::Bind(&XRSystem::OnSupportsSessionReturned, WrapPersistent(this), |
| WrapPersistent(query))); |
| |
| return promise; |
| } |
| |
| void XRSystem::RequestImmersiveSession(PendingRequestSessionQuery* query, |
| ExceptionState* exception_state) { |
| DVLOG(2) << __func__; |
| // Log an immersive session request if we haven't already |
| if (!did_log_request_immersive_session_) { |
| ukm::builders::XR_WebXR(DomWindow()->UkmSourceID()) |
| .SetDidRequestPresentation(1) |
| .Record(DomWindow()->UkmRecorder()); |
| did_log_request_immersive_session_ = true; |
| } |
| |
| // Make sure the request is allowed |
| auto* immersive_session_request_error = |
| CheckImmersiveSessionRequestAllowed(DomWindow()); |
| if (immersive_session_request_error) { |
| DVLOG(2) << __func__ |
| << ": rejecting session - immersive session not allowed, reason: " |
| << immersive_session_request_error; |
| query->RejectWithSecurityError(immersive_session_request_error, |
| exception_state); |
| return; |
| } |
| |
| // Ensure there are no other immersive sessions currently pending or active |
| if (has_outstanding_immersive_request_ || |
| frameProvider()->immersive_session()) { |
| DVLOG(2) << __func__ |
| << ": rejecting session - immersive session request is already " |
| "pending or an immersive session is already active"; |
| query->RejectWithDOMException(DOMExceptionCode::kInvalidStateError, |
| kActiveImmersiveSession, exception_state); |
| return; |
| } |
| |
| // If TryEnsureService() doesn't set |service_|, then we don't have any WebXR |
| // hardware. |
| TryEnsureService(); |
| if (!service_.is_bound()) { |
| DVLOG(2) << __func__ << ": rejecting session - service is not bound"; |
| query->RejectWithDOMException(DOMExceptionCode::kNotSupportedError, |
| kNoDevicesMessage, exception_state); |
| return; |
| } |
| |
| // Reject session if any of the required features were invalid. |
| if (query->InvalidRequiredFeatures()) { |
| DVLOG(2) << __func__ << ": rejecting session - invalid required features"; |
| query->RejectWithDOMException(DOMExceptionCode::kNotSupportedError, |
| kSessionNotSupported, exception_state); |
| return; |
| } |
| |
| // Reworded from spec 'pending immersive session' |
| has_outstanding_immersive_request_ = true; |
| |
| // Submit the request to VrServiceImpl in the Browser process |
| outstanding_request_queries_.insert(query); |
| auto session_options = XRSessionOptionsFromQuery(*query); |
| |
| // If using DOM Overlay, if we're already in fullscreen mode, and if this |
| // frame has a remote ancestor (OOPIF with the ancestor in another process), |
| // we need to exit and re-enter fullscreen mode to properly apply the |
| // is_xr_overlay property. Request a fullscreen exit, and continue with |
| // the session request once that completes. |
| Document* doc = DomWindow()->document(); |
| if (query->DOMOverlayElement() && Fullscreen::FullscreenElementFrom(*doc)) { |
| bool has_remote_ancestor = false; |
| for (Frame* f = DomWindow()->GetFrame(); f; f = f->Tree().Parent()) { |
| if (f->IsRemoteFrame()) { |
| has_remote_ancestor = true; |
| break; |
| } |
| } |
| DVLOG(2) << __func__ << ": has_remote_ancestor=" << has_remote_ancestor; |
| if (has_remote_ancestor) { |
| fullscreen_exit_observer_ = |
| MakeGarbageCollected<OverlayFullscreenExitObserver>(this); |
| |
| base::OnceClosure callback = |
| WTF::Bind(&XRSystem::DoRequestSession, WrapWeakPersistent(this), |
| WrapPersistent(query), std::move(session_options)); |
| fullscreen_exit_observer_->ExitFullscreen(doc, std::move(callback)); |
| return; |
| } |
| } |
| DoRequestSession(std::move(query), std::move(session_options)); |
| } |
| |
| void XRSystem::DoRequestSession( |
| PendingRequestSessionQuery* query, |
| device::mojom::blink::XRSessionOptionsPtr session_options) { |
| // In DOM overlay mode, there's an additional step before an immersive-ar |
| // session can start, we need to enter fullscreen mode by setting the |
| // appropriate element as fullscreen from the Renderer, then waiting for the |
| // browser side to send an event indicating success or failure. |
| auto callback = |
| query->DOMOverlayElement() |
| ? WTF::Bind(&XRSystem::OnRequestSessionSetupForDomOverlay, |
| WrapWeakPersistent(this), WrapPersistent(query)) |
| : WTF::Bind(&XRSystem::OnRequestSessionReturned, |
| WrapWeakPersistent(this), WrapPersistent(query)); |
| service_->RequestSession(std::move(session_options), std::move(callback)); |
| } |
| |
| void XRSystem::RequestInlineSession(PendingRequestSessionQuery* query, |
| ExceptionState* exception_state) { |
| DVLOG(2) << __func__; |
| // Make sure the inline session request was allowed |
| auto* inline_session_request_error = |
| CheckInlineSessionRequestAllowed(DomWindow()->GetFrame(), *query); |
| if (inline_session_request_error) { |
| query->RejectWithSecurityError(inline_session_request_error, |
| exception_state); |
| return; |
| } |
| |
| // Reject session if any of the required features were invalid. |
| if (query->InvalidRequiredFeatures()) { |
| DVLOG(2) << __func__ << ": rejecting session - invalid required features"; |
| query->RejectWithDOMException(DOMExceptionCode::kNotSupportedError, |
| kSessionNotSupported, exception_state); |
| return; |
| } |
| |
| auto sensor_requirement = query->GetSensorRequirement(); |
| |
| // Try to get the service now. If we can't get it, then we know that we can |
| // only support a sensorless session. But if we *can* get it, then we need to |
| // check if we have any hardware that supports the requested features. |
| TryEnsureService(); |
| |
| // If no sensors are requested, or if we don't have a service and sensors are |
| // not required, then just create a sensorless session. |
| if (sensor_requirement == SensorRequirement::kNone || |
| (!service_.is_bound() && |
| sensor_requirement != SensorRequirement::kRequired)) { |
| query->Resolve(CreateSensorlessInlineSession()); |
| return; |
| } |
| |
| // If we don't have a service, then we don't have any WebXR hardware. |
| // If we didn't already create a sensorless session, we can't create a session |
| // without hardware, so just reject now. |
| if (!service_.is_bound()) { |
| DVLOG(2) << __func__ << ": rejecting session - no service"; |
| query->RejectWithDOMException(DOMExceptionCode::kNotSupportedError, |
| kSessionNotSupported, exception_state); |
| return; |
| } |
| |
| // Submit the request to VrServiceImpl in the Browser process |
| outstanding_request_queries_.insert(query); |
| auto session_options = XRSessionOptionsFromQuery(*query); |
| service_->RequestSession( |
| std::move(session_options), |
| WTF::Bind(&XRSystem::OnRequestSessionReturned, WrapWeakPersistent(this), |
| WrapPersistent(query))); |
| } |
| |
| XRSystem::RequestedXRSessionFeatureSet XRSystem::ParseRequestedFeatures( |
| const HeapVector<ScriptValue>& features, |
| const device::mojom::blink::XRSessionMode& session_mode, |
| XRSessionInit* session_init, |
| mojom::blink::ConsoleMessageLevel error_level) { |
| DVLOG(2) << __func__ << ": features.size()=" << features.size() |
| << ", session_mode=" << session_mode; |
| RequestedXRSessionFeatureSet result; |
| |
| // Iterate over all requested features, even if intermediate |
| // elements are found to be invalid. |
| for (const auto& feature : features) { |
| String feature_string; |
| if (feature.ToString(feature_string)) { |
| auto feature_enum = |
| StringToXRSessionFeature(GetExecutionContext(), feature_string); |
| |
| if (!feature_enum) { |
| AddConsoleMessage(error_level, |
| "Unrecognized feature requested: " + feature_string); |
| result.invalid_features = true; |
| } else if (!IsFeatureValidForMode(feature_enum.value(), session_mode, |
| session_init, GetExecutionContext(), |
| error_level)) { |
| AddConsoleMessage(error_level, "Feature '" + feature_string + |
| "' is not supported for mode: " + |
| SessionModeToString(session_mode)); |
| result.invalid_features = true; |
| } else if (!HasRequiredFeaturePolicy(GetExecutionContext(), |
| feature_enum.value())) { |
| AddConsoleMessage(error_level, |
| "Feature '" + feature_string + |
| "' is not permitted by permissions policy"); |
| result.invalid_features = true; |
| } else { |
| DVLOG(3) << __func__ << ": Adding feature " << feature_string |
| << " to valid_features."; |
| result.valid_features.insert(feature_enum.value()); |
| } |
| } else { |
| AddConsoleMessage(error_level, "Unrecognized feature value"); |
| result.invalid_features = true; |
| } |
| } |
| |
| DVLOG(2) << __func__ |
| << ": result.invalid_features=" << result.invalid_features |
| << ", result.valid_features.size()=" << result.valid_features.size(); |
| return result; |
| } |
| |
| ScriptPromise XRSystem::requestSession(ScriptState* script_state, |
| const String& mode, |
| XRSessionInit* session_init, |
| ExceptionState& exception_state) { |
| DVLOG(2) << __func__; |
| // TODO(https://crbug.com/968622): Make sure we don't forget to call |
| // metrics-related methods when the promise gets resolved/rejected. |
| if (!DomWindow()) { |
| // Reject if the window is inaccessible. |
| |
| // Do *not* record an UKM event in this case (we won't be able to access the |
| // Document to get UkmRecorder anyway). |
| exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError, |
| kNavigatorDetachedError); |
| return ScriptPromise(); // Will be rejected by generated bindings |
| } |
| |
| device::mojom::blink::XRSessionMode session_mode = stringToSessionMode(mode); |
| |
| // If the request is for immersive-ar, ensure that feature is enabled. |
| if (session_mode == device::mojom::blink::XRSessionMode::kImmersiveAr && |
| !IsImmersiveArAllowed()) { |
| exception_state.ThrowTypeError( |
| String::Format(kImmersiveArModeNotValid, "requestSession")); |
| |
| // We haven't created the query yet, so we can't use it to implicitly log |
| // our metrics for us, so explicitly log it here, as the query requires the |
| // features to be parsed before it can be built. |
| ukm::builders::XR_WebXR_SessionRequest(DomWindow()->UkmSourceID()) |
| .SetMode(static_cast<int64_t>(session_mode)) |
| .SetStatus(static_cast<int64_t>(SessionRequestStatus::kOtherError)) |
| .Record(DomWindow()->UkmRecorder()); |
| return ScriptPromise(); |
| } |
| |
| // Parse required feature strings |
| RequestedXRSessionFeatureSet required_features; |
| if (session_init && session_init->hasRequiredFeatures()) { |
| required_features = ParseRequestedFeatures( |
| session_init->requiredFeatures(), session_mode, session_init, |
| mojom::blink::ConsoleMessageLevel::kError); |
| } |
| |
| // Parse optional feature strings |
| RequestedXRSessionFeatureSet optional_features; |
| if (session_init && session_init->hasOptionalFeatures()) { |
| optional_features = ParseRequestedFeatures( |
| session_init->optionalFeatures(), session_mode, session_init, |
| mojom::blink::ConsoleMessageLevel::kWarning); |
| } |
| |
| // Certain session modes imply default features. |
| // Add those default features as required features now. |
| base::span<const device::mojom::XRSessionFeature> default_features; |
| switch (session_mode) { |
| case device::mojom::blink::XRSessionMode::kImmersiveVr: |
| default_features = kDefaultImmersiveVrFeatures; |
| break; |
| case device::mojom::blink::XRSessionMode::kImmersiveAr: |
| default_features = kDefaultImmersiveArFeatures; |
| break; |
| case device::mojom::blink::XRSessionMode::kInline: |
| default_features = kDefaultInlineFeatures; |
| break; |
| } |
| |
| for (const auto& feature : default_features) { |
| if (HasRequiredFeaturePolicy(GetExecutionContext(), feature)) { |
| required_features.valid_features.insert(feature); |
| } else { |
| DVLOG(2) << __func__ |
| << ": feature policy not satisfied for a default feature: " |
| << feature; |
| required_features.invalid_features = true; |
| } |
| } |
| |
| auto* resolver = MakeGarbageCollected<ScriptPromiseResolver>(script_state); |
| ScriptPromise promise = resolver->Promise(); |
| |
| PendingRequestSessionQuery* query = |
| MakeGarbageCollected<PendingRequestSessionQuery>( |
| DomWindow()->UkmSourceID(), resolver, session_mode, |
| std::move(required_features), std::move(optional_features)); |
| |
| if (query->HasFeature(device::mojom::XRSessionFeature::DOM_OVERLAY)) { |
| // Prerequisites were checked by IsFeatureValidForMode and IDL. |
| DCHECK(session_init); |
| DCHECK(session_init->hasDomOverlay()); |
| DCHECK(session_init->domOverlay()->hasRoot()) << "required in IDL"; |
| query->SetDOMOverlayElement(session_init->domOverlay()->root()); |
| } |
| |
| if (query->HasFeature(device::mojom::XRSessionFeature::IMAGE_TRACKING)) { |
| // Prerequisites were checked by IsFeatureValidForMode. |
| DCHECK(session_init); |
| DCHECK(session_init->hasTrackedImages()); |
| DVLOG(3) << __func__ << ": set up trackedImages"; |
| Vector<device::mojom::blink::XRTrackedImage> images; |
| int index = 0; |
| for (auto& image : session_init->trackedImages()) { |
| DCHECK(image->hasImage()) << "required in IDL"; |
| DCHECK(image->hasWidthInMeters()) << "required in IDL"; |
| if (std::isnan(image->widthInMeters()) || |
| image->widthInMeters() <= 0.0f) { |
| String message = String::Format(kTrackedImageWidthInvalid, index); |
| query->RejectWithTypeError(message, &exception_state); |
| return promise; |
| } |
| // Extract an SkBitmap snapshot for each image. |
| scoped_refptr<StaticBitmapImage> static_bitmap_image = |
| image->image()->BitmapImage(); |
| SkBitmap sk_bitmap = static_bitmap_image->AsSkBitmapForCurrentFrame( |
| kRespectImageOrientation); |
| IntSize int_size = static_bitmap_image->Size(); |
| gfx::Size size(int_size.Width(), int_size.Height()); |
| images.emplace_back(sk_bitmap, size, image->widthInMeters()); |
| ++index; |
| } |
| query->SetTrackedImages(images); |
| } |
| |
| if (query->HasFeature(device::mojom::XRSessionFeature::DEPTH)) { |
| // Prerequisites were checked by IsFeatureValidForMode and IDL. |
| DCHECK(session_init); |
| DCHECK(session_init->hasDepthSensing()); |
| DCHECK(session_init->depthSensing()->hasUsagePreference()) |
| << "required in IDL"; |
| DCHECK(session_init->depthSensing()->hasDataFormatPreference()) |
| << "required in IDL"; |
| |
| Vector<device::mojom::XRDepthUsage> preferred_usage = |
| ParseDepthUsages(session_init->depthSensing()->usagePreference()); |
| Vector<device::mojom::XRDepthDataFormat> preferred_format = |
| ParseDepthFormats(session_init->depthSensing()->dataFormatPreference()); |
| |
| // If the depth API is required and either preferred usages or preferred |
| // formats are empty, we already know that the session creation will fail |
| // (as we won't be able to pick a supported usage & format combination), so |
| // let's fail it already: |
| if (query->RequiredFeatures().Contains( |
| device::mojom::XRSessionFeature::DEPTH) && |
| (preferred_usage.IsEmpty() || preferred_format.IsEmpty())) { |
| query->RejectWithDOMException(DOMExceptionCode::kNotSupportedError, |
| kDepthSensingConfigurationNotSupported, |
| &exception_state); |
| return promise; |
| } |
| |
| query->SetDepthSensingConfiguration(preferred_usage, preferred_format); |
| } |
| |
| // The various session request methods may have other checks that would reject |
| // before needing to create the vr service, so we don't try to create it here. |
| switch (session_mode) { |
| case device::mojom::blink::XRSessionMode::kImmersiveVr: |
| case device::mojom::blink::XRSessionMode::kImmersiveAr: |
| RequestImmersiveSession(query, &exception_state); |
| break; |
| case device::mojom::blink::XRSessionMode::kInline: |
| RequestInlineSession(query, &exception_state); |
| break; |
| } |
| |
| return promise; |
| } |
| |
| void XRSystem::MakeXrCompatibleAsync( |
| device::mojom::blink::VRService::MakeXrCompatibleCallback callback) { |
| if (!GetExecutionContext()->IsFeatureEnabled( |
| mojom::blink::FeaturePolicyFeature::kWebXr)) { |
| std::move(callback).Run( |
| device::mojom::XrCompatibleResult::kWebXrFeaturePolicyBlocked); |
| return; |
| } |
| |
| TryEnsureService(); |
| if (service_.is_bound()) { |
| service_->MakeXrCompatible(std::move(callback)); |
| } else { |
| std::move(callback).Run( |
| device::mojom::XrCompatibleResult::kNoDeviceAvailable); |
| } |
| } |
| |
| void XRSystem::MakeXrCompatibleSync( |
| device::mojom::XrCompatibleResult* xr_compatible_result) { |
| if (!GetExecutionContext()->IsFeatureEnabled( |
| mojom::blink::FeaturePolicyFeature::kWebXr)) { |
| *xr_compatible_result = |
| device::mojom::XrCompatibleResult::kWebXrFeaturePolicyBlocked; |
| return; |
| } |
| *xr_compatible_result = device::mojom::XrCompatibleResult::kNoDeviceAvailable; |
| |
| TryEnsureService(); |
| if (service_.is_bound()) |
| service_->MakeXrCompatible(xr_compatible_result); |
| } |
| |
| // This will be called when the XR hardware or capabilities have potentially |
| // changed. For example, if a new physical device was connected to the system, |
| // it might be able to support immersive sessions, where it couldn't before. |
| void XRSystem::OnDeviceChanged() { |
| ExecutionContext* context = GetExecutionContext(); |
| if (context && |
| context->IsFeatureEnabled(mojom::blink::FeaturePolicyFeature::kWebXr)) { |
| DispatchEvent(*blink::Event::Create(event_type_names::kDevicechange)); |
| } |
| } |
| |
| void XRSystem::OnSupportsSessionReturned(PendingSupportsSessionQuery* query, |
| bool supports_session) { |
| // The session query has returned and we're about to resolve or reject the |
| // promise, so remove it from our outstanding list. |
| DCHECK(outstanding_support_queries_.Contains(query)); |
| outstanding_support_queries_.erase(query); |
| query->Resolve(supports_session); |
| } |
| |
| void XRSystem::OnRequestSessionSetupForDomOverlay( |
| PendingRequestSessionQuery* query, |
| device::mojom::blink::RequestSessionResultPtr result) { |
| DCHECK(query->DOMOverlayElement()); |
| if (result->is_success()) { |
| // Success. Now request fullscreen mode and continue with |
| // OnRequestSessionReturned once that completes. |
| fullscreen_event_manager_ = |
| MakeGarbageCollected<OverlayFullscreenEventManager>(this, query, |
| std::move(result)); |
| fullscreen_event_manager_->RequestFullscreen(); |
| } else { |
| // Session request failed, continue processing that normally. |
| OnRequestSessionReturned(query, std::move(result)); |
| } |
| } |
| |
| void XRSystem::OnRequestSessionReturned( |
| PendingRequestSessionQuery* query, |
| device::mojom::blink::RequestSessionResultPtr result) { |
| DVLOG(2) << __func__; |
| // The session query has returned and we're about to resolve or reject the |
| // promise, so remove it from our outstanding list. |
| DCHECK(outstanding_request_queries_.Contains(query)); |
| outstanding_request_queries_.erase(query); |
| if (query->mode() == device::mojom::blink::XRSessionMode::kImmersiveVr || |
| query->mode() == device::mojom::blink::XRSessionMode::kImmersiveAr) { |
| DCHECK(has_outstanding_immersive_request_); |
| has_outstanding_immersive_request_ = false; |
| } |
| |
| // Clean up the fullscreen event manager which may have been added for |
| // DOM overlay setup. We're done with it, and it contains a reference |
| // to the query and the DOM overlay element. |
| fullscreen_event_manager_ = nullptr; |
| |
| if (!result->is_success()) { |
| // |service_| does not support the requested mode. Attempt to create a |
| // sensorless session. |
| if (query->GetSensorRequirement() != SensorRequirement::kRequired) { |
| DVLOG(2) << __func__ << ": session creation failed - creating sensorless"; |
| XRSession* session = CreateSensorlessInlineSession(); |
| query->Resolve(session); |
| return; |
| } |
| |
| String error_message = |
| String::Format("Could not create a session because: %s", |
| GetConsoleMessage(result->get_failure_reason())); |
| AddConsoleMessage(mojom::blink::ConsoleMessageLevel::kError, error_message); |
| query->RejectWithDOMException(DOMExceptionCode::kNotSupportedError, |
| kSessionNotSupported, nullptr); |
| return; |
| } |
| |
| auto session_ptr = std::move(result->get_success()->session); |
| auto metrics_recorder = std::move(result->get_success()->metrics_recorder); |
| |
| // immersive sessions must supply display info. |
| DCHECK(session_ptr->display_info); |
| |
| XRSessionFeatureSet enabled_features; |
| for (const auto& feature : session_ptr->enabled_features) { |
| DVLOG(2) << __func__ << ": feature " << feature << " will be enabled"; |
| enabled_features.insert(feature); |
| } |
| |
| XRSession* session = CreateSession( |
| query->mode(), session_ptr->enviroment_blend_mode, |
| session_ptr->interaction_mode, std::move(session_ptr->client_receiver), |
| std::move(session_ptr->display_info), |
| std::move(session_ptr->device_config), enabled_features); |
| |
| frameProvider()->OnSessionStarted(session, std::move(session_ptr)); |
| |
| if (query->mode() == device::mojom::blink::XRSessionMode::kImmersiveVr || |
| query->mode() == device::mojom::blink::XRSessionMode::kImmersiveAr) { |
| const bool anchors_enabled = base::Contains( |
| enabled_features, device::mojom::XRSessionFeature::ANCHORS); |
| const bool hit_test_enabled = base::Contains( |
| enabled_features, device::mojom::XRSessionFeature::HIT_TEST); |
| const bool environment_integration = hit_test_enabled || anchors_enabled; |
| if (environment_integration) { |
| // See Task Sources spreadsheet for more information: |
| // https://docs.google.com/spreadsheets/d/1b-dus1Ug3A8y0lX0blkmOjJILisUASdj8x9YN_XMwYc/view |
| frameProvider() |
| ->GetImmersiveDataProvider() |
| ->GetEnvironmentIntegrationProvider( |
| environment_provider_.BindNewEndpointAndPassReceiver( |
| GetExecutionContext()->GetTaskRunner( |
| TaskType::kMiscPlatformAPI))); |
| environment_provider_.set_disconnect_handler( |
| WTF::Bind(&XRSystem::OnEnvironmentProviderDisconnect, |
| WrapWeakPersistent(this))); |
| |
| session->OnEnvironmentProviderCreated(); |
| } |
| |
| if (query->mode() == device::mojom::blink::XRSessionMode::kImmersiveAr) { |
| DCHECK(DomWindow()); |
| if (query->HasFeature(device::mojom::XRSessionFeature::DOM_OVERLAY)) { |
| DCHECK(query->DOMOverlayElement()); |
| // The session is using DOM overlay mode. At this point the overlay |
| // element is already in fullscreen mode, and the session can |
| // proceed. |
| session->SetDOMOverlayElement(query->DOMOverlayElement()); |
| } |
| } |
| |
| if (query->mode() == device::mojom::blink::XRSessionMode::kImmersiveVr && |
| session->UsesInputEventing()) { |
| frameProvider()->GetImmersiveDataProvider()->SetInputSourceButtonListener( |
| session->GetInputClickListener()); |
| } |
| } |
| |
| UseCounter::Count(ExecutionContext::From(query->GetScriptState()), |
| WebFeature::kWebXrSessionCreated); |
| |
| query->Resolve(session, std::move(metrics_recorder)); |
| } |
| |
| void XRSystem::AddedEventListener( |
| const AtomicString& event_type, |
| RegisteredEventListener& registered_listener) { |
| EventTargetWithInlineData::AddedEventListener(event_type, |
| registered_listener); |
| |
| // If we're adding an event listener we should spin up the service, if we can, |
| // so that we can actually register for notifications. |
| TryEnsureService(); |
| if (!service_.is_bound()) |
| return; |
| |
| if (event_type == event_type_names::kDevicechange) { |
| // Register for notifications if we haven't already. |
| // |
| // See https://bit.ly/2S0zRAS for task types. |
| auto task_runner = |
| GetExecutionContext()->GetTaskRunner(TaskType::kMiscPlatformAPI); |
| if (!receiver_.is_bound()) |
| service_->SetClient(receiver_.BindNewPipeAndPassRemote(task_runner)); |
| } |
| } |
| |
| void XRSystem::ContextDestroyed() { |
| Dispose(DisposeType::kContextDestroyed); |
| } |
| |
| // A session is always created and returned. |
| XRSession* XRSystem::CreateSession( |
| device::mojom::blink::XRSessionMode mode, |
| device::mojom::blink::XREnvironmentBlendMode blend_mode, |
| device::mojom::blink::XRInteractionMode interaction_mode, |
| mojo::PendingReceiver<device::mojom::blink::XRSessionClient> |
| client_receiver, |
| device::mojom::blink::VRDisplayInfoPtr display_info, |
| device::mojom::blink::XRSessionDeviceConfigPtr device_config, |
| XRSessionFeatureSet enabled_features, |
| bool sensorless_session) { |
| XRSession* session = MakeGarbageCollected<XRSession>( |
| this, std::move(client_receiver), mode, blend_mode, interaction_mode, |
| std::move(device_config), sensorless_session, |
| std::move(enabled_features)); |
| if (display_info) |
| session->SetXRDisplayInfo(std::move(display_info)); |
| sessions_.insert(session); |
| return session; |
| } |
| |
| XRSession* XRSystem::CreateSensorlessInlineSession() { |
| // TODO(https://crbug.com/944936): The blend mode could be "additive". |
| device::mojom::blink::XREnvironmentBlendMode blend_mode = |
| device::mojom::blink::XREnvironmentBlendMode::kOpaque; |
| device::mojom::blink::XRInteractionMode interaction_mode = |
| device::mojom::blink::XRInteractionMode::kScreenSpace; |
| device::mojom::blink::XRSessionDeviceConfigPtr device_config = |
| device::mojom::blink::XRSessionDeviceConfig::New(); |
| return CreateSession(device::mojom::blink::XRSessionMode::kInline, blend_mode, |
| interaction_mode, |
| mojo::NullReceiver() /* client receiver */, |
| nullptr /* display_info */, std::move(device_config), |
| {device::mojom::XRSessionFeature::REF_SPACE_VIEWER}, |
| true /* sensorless_session */); |
| } |
| |
| void XRSystem::Dispose(DisposeType dispose_type) { |
| switch (dispose_type) { |
| case DisposeType::kContextDestroyed: |
| is_context_destroyed_ = true; |
| break; |
| case DisposeType::kDisconnected: |
| did_service_ever_disconnect_ = true; |
| break; |
| } |
| |
| // If the document context was destroyed, shut down the client connection |
| // and never call the mojo service again. |
| service_.reset(); |
| receiver_.reset(); |
| |
| // Shutdown frame provider, which manages the message pipes. |
| if (frame_provider_) |
| frame_provider_->Dispose(); |
| |
| HeapHashSet<Member<PendingSupportsSessionQuery>> support_queries = |
| outstanding_support_queries_; |
| for (const auto& query : support_queries) { |
| OnSupportsSessionReturned(query, false); |
| } |
| DCHECK(outstanding_support_queries_.IsEmpty()); |
| |
| HeapHashSet<Member<PendingRequestSessionQuery>> request_queries = |
| outstanding_request_queries_; |
| for (const auto& query : request_queries) { |
| OnRequestSessionReturned( |
| query, device::mojom::blink::RequestSessionResult::NewFailureReason( |
| device::mojom::RequestSessionError::INVALID_CLIENT)); |
| } |
| DCHECK(outstanding_support_queries_.IsEmpty()); |
| } |
| |
| void XRSystem::OnEnvironmentProviderDisconnect() { |
| for (auto& callback : environment_provider_error_callbacks_) { |
| std::move(callback).Run(); |
| } |
| |
| environment_provider_error_callbacks_.clear(); |
| environment_provider_.reset(); |
| } |
| |
| void XRSystem::TryEnsureService() { |
| DVLOG(2) << __func__; |
| |
| // If we already have a service, there's nothing to do. |
| if (service_.is_bound()) { |
| DVLOG(2) << __func__ << ": service already bound"; |
| return; |
| } |
| |
| // If the service has been disconnected in the past or our context has been |
| // destroyed, don't try to get the service again. |
| if (did_service_ever_disconnect_ || is_context_destroyed_) { |
| DVLOG(2) << __func__ |
| << ": service disconnected or context destroyed, " |
| "did_service_ever_disconnect_=" |
| << did_service_ever_disconnect_ |
| << ", is_context_destroyed_=" << is_context_destroyed_; |
| return; |
| } |
| |
| // If the current frame isn't attached, don't try to get the service. |
| if (!DomWindow()) { |
| DVLOG(2) << ": current frame is not attached"; |
| return; |
| } |
| |
| // See https://bit.ly/2S0zRAS for task types. |
| DomWindow()->GetBrowserInterfaceBroker().GetInterface( |
| service_.BindNewPipeAndPassReceiver( |
| DomWindow()->GetTaskRunner(TaskType::kMiscPlatformAPI))); |
| service_.set_disconnect_handler(WTF::Bind(&XRSystem::Dispose, |
| WrapWeakPersistent(this), |
| DisposeType::kDisconnected)); |
| } |
| |
| bool XRSystem::IsImmersiveArAllowed() { |
| const bool ar_allowed_in_settings = |
| IsImmersiveArAllowedBySettings(DomWindow()); |
| const bool ar_enabled = |
| ar_allowed_in_settings && |
| RuntimeEnabledFeatures::WebXRARModuleEnabled(GetExecutionContext()); |
| |
| DVLOG(2) << __func__ << ": ar_allowed_in_settings=" << ar_allowed_in_settings |
| << ", ar_enabled=" << ar_enabled; |
| |
| return ar_enabled; |
| } |
| |
| void XRSystem::Trace(Visitor* visitor) const { |
| visitor->Trace(frame_provider_); |
| visitor->Trace(sessions_); |
| visitor->Trace(service_); |
| visitor->Trace(environment_provider_); |
| visitor->Trace(receiver_); |
| visitor->Trace(outstanding_support_queries_); |
| visitor->Trace(outstanding_request_queries_); |
| visitor->Trace(fullscreen_event_manager_); |
| visitor->Trace(fullscreen_exit_observer_); |
| Supplement<Navigator>::Trace(visitor); |
| ExecutionContextLifecycleObserver::Trace(visitor); |
| EventTargetWithInlineData::Trace(visitor); |
| } |
| |
| } // namespace blink |