diff --git a/Userland/Libraries/LibWeb/HTML/HTMLMediaElement.cpp b/Userland/Libraries/LibWeb/HTML/HTMLMediaElement.cpp index d71996360a..cca36aacac 100644 --- a/Userland/Libraries/LibWeb/HTML/HTMLMediaElement.cpp +++ b/Userland/Libraries/LibWeb/HTML/HTMLMediaElement.cpp @@ -5,6 +5,7 @@ * SPDX-License-Identifier: BSD-2-Clause */ +#include #include #include #include @@ -20,15 +21,18 @@ #include #include #include +#include #include #include #include #include +#include namespace Web::HTML { HTMLMediaElement::HTMLMediaElement(DOM::Document& document, DOM::QualifiedName qualified_name) : HTMLElement(document, move(qualified_name)) + , m_pending_play_promises(heap()) { } @@ -127,6 +131,27 @@ void HTMLMediaElement::set_duration(double duration) m_duration = duration; } +WebIDL::ExceptionOr> HTMLMediaElement::play() +{ + auto& realm = this->realm(); + auto& vm = realm.vm(); + + // FIXME: 1. If the media element is not allowed to play, then return a promise rejected with a "NotAllowedError" DOMException. + + // FIXME: 2. If the media element's error attribute is not null and its code is MEDIA_ERR_SRC_NOT_SUPPORTED, then return a promise + // rejected with a "NotSupportedError" DOMException. + + // 3. Let promise be a new promise and append promise to the list of pending play promises. + auto promise = WebIDL::create_promise(realm); + TRY_OR_THROW_OOM(vm, m_pending_play_promises.try_append(promise)); + + // 4. Run the internal play steps for the media element. + TRY(play_element()); + + // 5. Return promise. + return JS::NonnullGCPtr { verify_cast(*promise->promise()) }; +} + // https://html.spec.whatwg.org/multipage/media.html#dom-media-pause WebIDL::ExceptionOr HTMLMediaElement::pause() { @@ -192,7 +217,9 @@ WebIDL::ExceptionOr HTMLMediaElement::load_element() // 1. Set the paused attribute to true. set_paused(true); - // FIXME 2. Take pending play promises and reject pending play promises with the result and an "AbortError" DOMException. + // 2. Take pending play promises and reject pending play promises with the result and an "AbortError" DOMException. + auto promises = take_pending_play_promises(); + reject_pending_play_promises(promises, TRY_OR_THROW_OOM(vm, "Media playback was aborted"_fly_string)); } // FIXME: 7. If seeking is true, set it to false. @@ -289,8 +316,8 @@ WebIDL::ExceptionOr HTMLMediaElement::select_resource() // 6. Failed with attribute: Reaching this step indicates that the media resource failed to load or that the given URL could not be parsed. Take // pending play promises and queue a media element task given the media element to run the dedicated media source failure steps with the result. queue_a_media_element_task([this, &ran_media_element_task]() { - // FIXME: Find and pass pending play promises to this AO. - handle_media_source_failure().release_value_but_fixme_should_propagate_errors(); + auto promises = take_pending_play_promises(); + handle_media_source_failure(promises).release_value_but_fixme_should_propagate_errors(); ran_media_element_task = true; }); @@ -671,8 +698,10 @@ WebIDL::ExceptionOr HTMLMediaElement::process_media_data(Function } // https://html.spec.whatwg.org/multipage/media.html#dedicated-media-source-failure-steps -WebIDL::ExceptionOr HTMLMediaElement::handle_media_source_failure() +WebIDL::ExceptionOr HTMLMediaElement::handle_media_source_failure(Span> promises) { + auto& vm = this->vm(); + // FIXME: 1. Set the error attribute to the result of creating a MediaError with MEDIA_ERR_SRC_NOT_SUPPORTED. // 2. Forget the media element's media-resource-specific tracks. @@ -686,7 +715,9 @@ WebIDL::ExceptionOr HTMLMediaElement::handle_media_source_failure() // 5. Fire an event named error at the media element. dispatch_event(TRY(DOM::Event::create(realm(), HTML::EventNames::error))); - // FIXME: 6. Reject pending play promises with promises and a "NotSupportedError" DOMException. + // 6. Reject pending play promises with promises and a "NotSupportedError" DOMException. + reject_pending_play_promises(promises, TRY_OR_THROW_OOM(vm, "Media is not supported"_fly_string)); + // FIXME: 7. Set the element's delaying-the-load-event flag to false. This stops delaying the load event. return {}; @@ -792,6 +823,59 @@ void HTMLMediaElement::set_ready_state(ReadyState ready_state) } } +// https://html.spec.whatwg.org/multipage/media.html#internal-play-steps +WebIDL::ExceptionOr HTMLMediaElement::play_element() +{ + // 1. If the media element's networkState attribute has the value NETWORK_EMPTY, invoke the media element's resource + // selection algorithm. + if (m_network_state == NetworkState::Empty) + TRY(select_resource()); + + // FIXME: 2. If the playback has ended and the direction of playback is forwards, seek to the earliest possible + // position of the media resource. + + // 3. If the media element's paused attribute is true, then: + if (paused()) { + // 1. Change the value of paused to false. + set_paused(false); + + // FIXME: 2. If the show poster flag is true, set the element's show poster flag to false and run the time marches on steps. + + // 3. Queue a media element task given the media element to fire an event named play at the element. + queue_a_media_element_task([this]() { + dispatch_event(DOM::Event::create(realm(), HTML::EventNames::play).release_value_but_fixme_should_propagate_errors()); + }); + + // 4. If the media element's readyState attribute has the value HAVE_NOTHING, HAVE_METADATA, or HAVE_CURRENT_DATA, + // queue a media element task given the media element to fire an event named waiting at the element. + if (m_ready_state == ReadyState::HaveNothing || m_ready_state == ReadyState::HaveMetadata || m_ready_state == ReadyState::HaveCurrentData) { + queue_a_media_element_task([this]() { + dispatch_event(DOM::Event::create(realm(), HTML::EventNames::waiting).release_value_but_fixme_should_propagate_errors()); + }); + } + // Otherwise, the media element's readyState attribute has the value HAVE_FUTURE_DATA or HAVE_ENOUGH_DATA: + // notify about playing for the element. + else { + notify_about_playing(); + } + } + + // 4. Otherwise, if the media element's readyState attribute has the value HAVE_FUTURE_DATA or HAVE_ENOUGH_DATA, take + // pending play promises and queue a media element task given the media element to resolve pending play promises + // with the result. + else if (m_ready_state == ReadyState::HaveFutureData || m_ready_state == ReadyState::HaveEnoughData) { + auto promises = take_pending_play_promises(); + + queue_a_media_element_task([this, promises = move(promises)]() { + resolve_pending_play_promises(promises); + }); + } + + // FIXME: 5. Set the media element's can autoplay flag to false. + + return {}; +} + // https://html.spec.whatwg.org/multipage/media.html#internal-pause-steps WebIDL::ExceptionOr HTMLMediaElement::pause_element() { @@ -802,10 +886,11 @@ WebIDL::ExceptionOr HTMLMediaElement::pause_element() // 1. Change the value of paused to true. set_paused(true); - // FIXME: 2. Take pending play promises and let promises be the result. + // 2. Take pending play promises and let promises be the result. + auto promises = take_pending_play_promises(); // 3. Queue a media element task given the media element and the following steps: - queue_a_media_element_task([this]() { + queue_a_media_element_task([this, promises = move(promises)]() { auto& realm = this->realm(); // 1. Fire an event named timeupdate at the element. @@ -814,7 +899,8 @@ WebIDL::ExceptionOr HTMLMediaElement::pause_element() // 2. Fire an event named pause at the element. dispatch_event(DOM::Event::create(realm, HTML::EventNames::pause).release_value_but_fixme_should_propagate_errors()); - // FIXME: 3. Reject pending play promises with promises and an "AbortError" DOMException. + // 3. Reject pending play promises with promises and an "AbortError" DOMException. + reject_pending_play_promises(promises, "Media playback was paused"_fly_string.release_value_but_fixme_should_propagate_errors()); }); // FIXME: 4. Set the official playback position to the current playback position. @@ -826,14 +912,16 @@ WebIDL::ExceptionOr HTMLMediaElement::pause_element() // https://html.spec.whatwg.org/multipage/media.html#notify-about-playing void HTMLMediaElement::notify_about_playing() { - // FIXME: 1. Take pending play promises and let promises be the result. + // 1. Take pending play promises and let promises be the result. + auto promises = take_pending_play_promises(); // 2. Queue a media element task given the element and the following steps: - queue_a_media_element_task([this]() { + queue_a_media_element_task([this, promises = move(promises)]() { // 1. Fire an event named playing at the element. dispatch_event(DOM::Event::create(realm(), HTML::EventNames::playing).release_value_but_fixme_should_propagate_errors()); - // FIXME: 2. Resolve pending play promises with promises. + // 2. Resolve pending play promises with promises. + resolve_pending_play_promises(promises); }); on_playing(); @@ -850,4 +938,52 @@ void HTMLMediaElement::set_paused(bool paused) on_paused(); } +// https://html.spec.whatwg.org/multipage/media.html#take-pending-play-promises +JS::MarkedVector> HTMLMediaElement::take_pending_play_promises() +{ + // 1. Let promises be an empty list of promises. + // 2. Copy the media element's list of pending play promises to promises. + // 3. Clear the media element's list of pending play promises. + auto promises = move(m_pending_play_promises); + + // 4. Return promises. + return promises; +} + +// https://html.spec.whatwg.org/multipage/media.html#resolve-pending-play-promises +void HTMLMediaElement::resolve_pending_play_promises(ReadonlySpan> promises) +{ + auto& realm = this->realm(); + + // FIXME: This AO runs from the media element task queue, at which point we do not have a running execution + // context. This pushes one to allow the promise resolving hook to run. + auto& environment_settings = document().relevant_settings_object(); + environment_settings.prepare_to_run_script(); + + // To resolve pending play promises for a media element with a list of promises promises, the user agent + // must resolve each promise in promises with undefined. + for (auto const& promise : promises) + WebIDL::resolve_promise(realm, promise, JS::js_undefined()); + + environment_settings.clean_up_after_running_script(); +} + +// https://html.spec.whatwg.org/multipage/media.html#reject-pending-play-promises +void HTMLMediaElement::reject_pending_play_promises(ReadonlySpan> promises, JS::NonnullGCPtr error) +{ + auto& realm = this->realm(); + + // FIXME: This AO runs from the media element task queue, at which point we do not have a running execution + // context. This pushes one to allow the promise rejection hook to run. + auto& environment_settings = document().relevant_settings_object(); + environment_settings.prepare_to_run_script(); + + // To reject pending play promises for a media element with a list of promise promises and an exception name + // error, the user agent must reject each promise in promises with error. + for (auto const& promise : promises) + WebIDL::reject_promise(realm, promise, error); + + environment_settings.clean_up_after_running_script(); +} + } diff --git a/Userland/Libraries/LibWeb/HTML/HTMLMediaElement.h b/Userland/Libraries/LibWeb/HTML/HTMLMediaElement.h index b16127f5e4..93c48fe05a 100644 --- a/Userland/Libraries/LibWeb/HTML/HTMLMediaElement.h +++ b/Userland/Libraries/LibWeb/HTML/HTMLMediaElement.h @@ -9,9 +9,11 @@ #include #include +#include #include #include #include +#include #include namespace Web::HTML { @@ -46,6 +48,7 @@ public: WebIDL::ExceptionOr load(); double duration() const; bool paused() const { return m_paused; } + WebIDL::ExceptionOr> play(); WebIDL::ExceptionOr pause(); JS::NonnullGCPtr video_tracks() const { return *m_video_tracks; } @@ -74,15 +77,30 @@ private: WebIDL::ExceptionOr fetch_resource(AK::URL const&, Function failure_callback); static bool verify_response(JS::NonnullGCPtr, ByteRange const&); WebIDL::ExceptionOr process_media_data(Function failure_callback); - WebIDL::ExceptionOr handle_media_source_failure(); + WebIDL::ExceptionOr handle_media_source_failure(Span> promises); void forget_media_resource_specific_tracks(); void set_ready_state(ReadyState); + WebIDL::ExceptionOr play_element(); WebIDL::ExceptionOr pause_element(); void notify_about_playing(); void set_paused(bool); void set_duration(double); + JS::MarkedVector> take_pending_play_promises(); + void resolve_pending_play_promises(ReadonlySpan> promises); + void reject_pending_play_promises(ReadonlySpan> promises, JS::NonnullGCPtr error); + + // https://html.spec.whatwg.org/multipage/media.html#reject-pending-play-promises + template + void reject_pending_play_promises(ReadonlySpan> promises, FlyString const& message) + { + auto& realm = this->realm(); + + auto error = ErrorType::create(realm, message.to_deprecated_fly_string()); + reject_pending_play_promises(promises, error); + } + // https://html.spec.whatwg.org/multipage/media.html#media-element-event-task-source UniqueTaskSource m_media_element_event_task_source {}; @@ -96,6 +114,9 @@ private: // https://html.spec.whatwg.org/multipage/media.html#dom-media-duration double m_duration { NAN }; + // https://html.spec.whatwg.org/multipage/media.html#list-of-pending-play-promises + JS::MarkedVector> m_pending_play_promises; + // https://html.spec.whatwg.org/multipage/media.html#dom-media-paused bool m_paused { true }; diff --git a/Userland/Libraries/LibWeb/HTML/HTMLMediaElement.idl b/Userland/Libraries/LibWeb/HTML/HTMLMediaElement.idl index 6be3e29bd9..46e9bd5cd7 100644 --- a/Userland/Libraries/LibWeb/HTML/HTMLMediaElement.idl +++ b/Userland/Libraries/LibWeb/HTML/HTMLMediaElement.idl @@ -34,6 +34,7 @@ interface HTMLMediaElement : HTMLElement { readonly attribute boolean paused; [Reflect, CEReactions] attribute boolean autoplay; [Reflect, CEReactions] attribute boolean loop; + Promise play(); undefined pause(); // controls