From 3d2058791143a0485e5b36b19602f0628ac87be8 Mon Sep 17 00:00:00 2001 From: Tim Ledbetter Date: Mon, 13 Jan 2025 22:51:48 +0000 Subject: [PATCH] LibWeb: Fetch source file when HTMLTrackElement src attribute changes This commit begins to implement the track processing model. When the `src` attribute is updated, we now fetch the given source file. Currently, we always fire an `error` event once fetching is completed, as we don't support processing the fetched data. --- Libraries/LibWeb/HTML/HTMLMediaElement.h | 2 + Libraries/LibWeb/HTML/HTMLTrackElement.cpp | 168 +++++++++++++++++- Libraries/LibWeb/HTML/HTMLTrackElement.h | 15 ++ .../track-load-error-readyState.txt | 6 + .../track-load-error-readyState.html | 15 ++ 5 files changed, 205 insertions(+), 1 deletion(-) create mode 100644 Tests/LibWeb/Text/expected/wpt-import/html/semantics/embedded-content/media-elements/track/track-element/track-load-error-readyState.txt create mode 100644 Tests/LibWeb/Text/input/wpt-import/html/semantics/embedded-content/media-elements/track/track-element/track-load-error-readyState.html diff --git a/Libraries/LibWeb/HTML/HTMLMediaElement.h b/Libraries/LibWeb/HTML/HTMLMediaElement.h index 70eebf3cd3d..fa330e09ce2 100644 --- a/Libraries/LibWeb/HTML/HTMLMediaElement.h +++ b/Libraries/LibWeb/HTML/HTMLMediaElement.h @@ -147,6 +147,8 @@ public: }; CachedLayoutBoxes& cached_layout_boxes(Badge) const { return m_layout_boxes; } + CORSSettingAttribute crossorigin() const { return m_crossorigin; } + protected: HTMLMediaElement(DOM::Document&, DOM::QualifiedName); diff --git a/Libraries/LibWeb/HTML/HTMLTrackElement.cpp b/Libraries/LibWeb/HTML/HTMLTrackElement.cpp index 7c82f4a0899..c977bf62ca9 100644 --- a/Libraries/LibWeb/HTML/HTMLTrackElement.cpp +++ b/Libraries/LibWeb/HTML/HTMLTrackElement.cpp @@ -1,6 +1,7 @@ /* * Copyright (c) 2020, the SerenityOS developers. * Copyright (c) 2024, Jamie Mansfield + * Copyright (c) 2025, Tim Ledbetter * * SPDX-License-Identifier: BSD-2-Clause */ @@ -8,8 +9,15 @@ #include #include #include +#include +#include +#include +#include +#include #include +#include #include +#include namespace Web::HTML { @@ -33,6 +41,8 @@ void HTMLTrackElement::visit_edges(Cell::Visitor& visitor) { Base::visit_edges(visitor); visitor.visit(m_track); + visitor.visit(m_fetch_algorithms); + visitor.visit(m_fetch_controller); } void HTMLTrackElement::attribute_changed(FlyString const& name, Optional const& old_value, Optional const& value, Optional const& namespace_) @@ -47,8 +57,27 @@ void HTMLTrackElement::attribute_changed(FlyString const& name, Optional m_track->set_label(value.value_or({})); } else if (name.equals_ignoring_ascii_case(HTML::AttributeNames::srclang)) { m_track->set_language(value.value_or({})); - } + } else if (name.equals_ignoring_ascii_case(HTML::AttributeNames::src)) { + // https://html.spec.whatwg.org/multipage/media.html#sourcing-out-of-band-text-tracks:attr-track-src + // FIXME: Whenever a track element has its src attribute set, changed, or removed, the user agent must immediately empty the element's text track's text track list of cues. + // (This also causes the algorithm above to stop adding cues from the resource being obtained using the previously given URL, if any.) + if (!value.has_value()) + return; + + // https://html.spec.whatwg.org/multipage/media.html#attr-track-src + // When the element's src attribute is set, run these steps: + // 1. Let trackURL be failure. + Optional track_url; + + // 2. Let value be the element's src attribute value. + // 3. If value is not the empty string, then set trackURL to the result of encoding-parsing-and-serializing a URL given value, relative to the element's node document. + if (!value->is_empty()) + track_url = document().encoding_parse_and_serialize_url(value.value_or({})); + + // 4. Set the element's track URL to trackURL if it is not failure; otherwise to the empty string. + set_track_url(track_url.value_or({})); + } // https://html.spec.whatwg.org/multipage/media.html#dom-texttrack-id // For tracks that correspond to track elements, the track's identifier is the value of the element's id attribute, if any. if (name.equals_ignoring_ascii_case(HTML::AttributeNames::id)) { @@ -56,6 +85,18 @@ void HTMLTrackElement::attribute_changed(FlyString const& name, Optional } } +void HTMLTrackElement::inserted() +{ + HTMLElement::inserted(); + + // AD-HOC: This is a hack to allow tracks to start loading, without needing to implement the entire + // "honor user preferences for automatic text track selection" AO detailed here: + // https://html.spec.whatwg.org/multipage/media.html#honor-user-preferences-for-automatic-text-track-selection + m_track->set_mode(Bindings::TextTrackMode::Hidden); + + start_the_track_processing_model(); +} + // https://html.spec.whatwg.org/multipage/media.html#dom-track-readystate WebIDL::UnsignedShort HTMLTrackElement::ready_state() { @@ -82,4 +123,129 @@ WebIDL::UnsignedShort HTMLTrackElement::ready_state() VERIFY_NOT_REACHED(); } +void HTMLTrackElement::set_track_url(String track_url) +{ + if (m_track_url == track_url) + return; + + m_track_url = move(track_url); + + // https://html.spec.whatwg.org/multipage/media.html#start-the-track-processing-model + if (m_loading && m_fetch_controller && first_is_one_of(m_track->mode(), Bindings::TextTrackMode::Hidden, Bindings::TextTrackMode::Showing)) { + m_loading = false; + m_fetch_controller->abort(realm(), {}); + } +} + +// https://html.spec.whatwg.org/multipage/media.html#start-the-track-processing-model +void HTMLTrackElement::start_the_track_processing_model() +{ + // 1. If another occurrence of this algorithm is already running for this text track and its track element, return, + // letting that other algorithm take care of this element. + if (m_loading) + return; + + // 2. If the text track's text track mode is not set to one of hidden or showing, then return. + if (!first_is_one_of(m_track->mode(), Bindings::TextTrackMode::Hidden, Bindings::TextTrackMode::Showing)) + return; + + // 3. If the text track's track element does not have a media element as a parent, return. + if (!is(parent_element())) + return; + + // 4. Run the remainder of these steps in parallel, allowing whatever caused these steps to run to continue. + auto& realm = this->realm(); + Platform::EventLoopPlugin::the().deferred_invoke(GC::create_function(realm.heap(), [this, &realm] { + m_loading = true; + start_the_track_processing_model_parallel_steps(realm); + })); +} + +void HTMLTrackElement::start_the_track_processing_model_parallel_steps(JS::Realm& realm) +{ + // 5. Top: Await a stable state. The synchronous section consists of the following steps. + + // 6. ⌛ Set the text track readiness state to loading. + m_track->set_readiness_state(TextTrack::ReadinessState::Loading); + + // 7. ⌛ Let URL be the track URL of the track element. + auto url = track_url(); + + // 8. ⌛ If the track element's parent is a media element then let corsAttributeState be the state of the + // parent media element's crossorigin content attribute. Otherwise, let corsAttributeState be No CORS. + auto cors_attribute_state = CORSSettingAttribute::NoCORS; + if (is(parent())) { + cors_attribute_state = verify_cast(parent())->crossorigin(); + } + + // 9. End the synchronous section, continuing the remaining steps in parallel. + + // 10. If URL is not the empty string, then: + if (!url.is_empty()) { + // 1. Let request be the result of creating a potential-CORS request given URL, "track", and corsAttributeState, + // and with the same-origin fallback flag set. + auto request = create_potential_CORS_request(realm.vm(), url, Fetch::Infrastructure::Request::Destination::Track, cors_attribute_state, SameOriginFallbackFlag::Yes); + + // 2. Set request's client to the track element's node document's relevant settings object. + request->set_client(&document().relevant_settings_object()); + + // 3. Set request's initiator type to "track". + request->set_initiator_type(Fetch::Infrastructure::Request::InitiatorType::Track); + + Fetch::Infrastructure::FetchAlgorithms::Input fetch_algorithms_input {}; + fetch_algorithms_input.process_response_consume_body = [this, &realm](auto response, auto body_bytes) { + m_loading = false; + + // If fetching fails for any reason (network error, the server returns an error code, CORS fails, etc.), + // or if URL is the empty string, then queue an element task on the DOM manipulation task source given the media element + // to first change the text track readiness state to failed to load and then fire an event named error at the track element. + if (!response->url().has_value() || body_bytes.template has() || body_bytes.template has() || !Fetch::Infrastructure::is_ok_status(response->status()) || response->is_network_error()) { + queue_an_element_task(Task::Source::DOMManipulation, [this, &realm]() { + m_track->set_readiness_state(TextTrack::ReadinessState::FailedToLoad); + dispatch_event(DOM::Event::create(realm, HTML::EventNames::error)); + }); + return; + } + + // If fetching does not fail, but the type of the resource is not a supported text track format, or the file was not successfully processed + // (e.g., the format in question is an XML format and the file contained a well-formedness error that XML requires be detected and reported to the application), + // then the task that is queued on the networking task source in which the aforementioned problem is found must change the text track readiness state to failed to + // load and fire an event named error at the track element. + // FIXME: Currently we always fail here, since we don't support loading any track formats. + queue_an_element_task(Task::Source::Networking, [this, &realm]() { + m_track->set_readiness_state(TextTrack::ReadinessState::FailedToLoad); + dispatch_event(DOM::Event::create(realm, HTML::EventNames::error)); + }); + + // If fetching does not fail, and the file was successfully processed, then the final task that is queued by the networking task source, + // after it has finished parsing the data, must change the text track readiness state to loaded, and fire an event named load at the track element. + // FIXME: Enable this once we support processing track files + if (false) { + queue_an_element_task(Task::Source::Networking, [this, &realm]() { + m_track->set_readiness_state(TextTrack::ReadinessState::Loaded); + dispatch_event(DOM::Event::create(realm, HTML::EventNames::load)); + }); + } + }; + + // 4. Fetch request. + m_fetch_algorithms = Fetch::Infrastructure::FetchAlgorithms::create(vm(), move(fetch_algorithms_input)); + m_fetch_controller = MUST(Fetch::Fetching::fetch(realm, request, *m_fetch_algorithms)); + return; + } + + // 11. Wait until the text track readiness state is no longer set to loading. + HTML::main_thread_event_loop().spin_until(GC::create_function(realm.heap(), [this] { + return m_track->readiness_state() != TextTrack::ReadinessState::Loading; + })); + + // 12. Wait until the track URL is no longer equal to URL, at the same time as the text track mode is set to hidden or showing. + HTML::main_thread_event_loop().spin_until(GC::create_function(realm.heap(), [this, url = move(url)] { + return track_url() != url && first_is_one_of(m_track->mode(), Bindings::TextTrackMode::Hidden, Bindings::TextTrackMode::Showing); + })); + + // 13. Jump to the step labeled top. + start_the_track_processing_model_parallel_steps(realm); +} + } diff --git a/Libraries/LibWeb/HTML/HTMLTrackElement.h b/Libraries/LibWeb/HTML/HTMLTrackElement.h index 630d1ab0f07..80f8ae147af 100644 --- a/Libraries/LibWeb/HTML/HTMLTrackElement.h +++ b/Libraries/LibWeb/HTML/HTMLTrackElement.h @@ -29,10 +29,25 @@ private: virtual void initialize(JS::Realm&) override; virtual void visit_edges(Cell::Visitor&) override; + String track_url() const { return m_track_url; } + void set_track_url(String); + + void start_the_track_processing_model(); + void start_the_track_processing_model_parallel_steps(JS::Realm& realm); + // ^DOM::Element virtual void attribute_changed(FlyString const& name, Optional const& old_value, Optional const& value, Optional const& namespace_) override; + virtual void inserted() override; GC::Ptr m_track; + + // https://html.spec.whatwg.org/multipage/media.html#track-url + String m_track_url {}; + + GC::Ptr m_fetch_algorithms; + GC::Ptr m_fetch_controller; + + bool m_loading { false }; }; } diff --git a/Tests/LibWeb/Text/expected/wpt-import/html/semantics/embedded-content/media-elements/track/track-element/track-load-error-readyState.txt b/Tests/LibWeb/Text/expected/wpt-import/html/semantics/embedded-content/media-elements/track/track-element/track-load-error-readyState.txt new file mode 100644 index 00000000000..771c7e4d98d --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/html/semantics/embedded-content/media-elements/track/track-element/track-load-error-readyState.txt @@ -0,0 +1,6 @@ +Harness status: OK + +Found 1 tests + +1 Pass +Pass Error event on HTMLTrackElement and ERROR readyState on TextTrack \ No newline at end of file diff --git a/Tests/LibWeb/Text/input/wpt-import/html/semantics/embedded-content/media-elements/track/track-element/track-load-error-readyState.html b/Tests/LibWeb/Text/input/wpt-import/html/semantics/embedded-content/media-elements/track/track-element/track-load-error-readyState.html new file mode 100644 index 00000000000..8e4c1b345e5 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/html/semantics/embedded-content/media-elements/track/track-element/track-load-error-readyState.html @@ -0,0 +1,15 @@ + +Error event on HTMLTrackElement and ERROR readyState on TextTrack + + +