diff options
author | Timothy Flynn <trflynn89@pm.me> | 2023-05-12 15:02:37 -0400 |
---|---|---|
committer | Andreas Kling <kling@serenityos.org> | 2023-05-13 15:51:44 +0200 |
commit | c161a0fc49fa102971b95749280c1dc2c44dc851 (patch) | |
tree | 99d243739fea58bc525f1e0b03561c1b732579f3 | |
parent | 5bc386cc964845911ce0695e50e884eb61c00ec6 (diff) | |
download | serenity-c161a0fc49fa102971b95749280c1dc2c44dc851.zip |
LibWeb: Implement the HTMLMediaElement child <source> selection steps
Rather than setting the src attribute on the HTMLMediaElement, websites
may append a list of HTMLSourceElement nodes to the media element. There
is a series of "try the next source" steps to attempt to fetch/load each
source until we find one that works.
-rw-r--r-- | Userland/Libraries/LibWeb/HTML/HTMLMediaElement.cpp | 268 | ||||
-rw-r--r-- | Userland/Libraries/LibWeb/HTML/HTMLMediaElement.h | 6 |
2 files changed, 216 insertions, 58 deletions
diff --git a/Userland/Libraries/LibWeb/HTML/HTMLMediaElement.cpp b/Userland/Libraries/LibWeb/HTML/HTMLMediaElement.cpp index 635de9e564..92031d0ac7 100644 --- a/Userland/Libraries/LibWeb/HTML/HTMLMediaElement.cpp +++ b/Userland/Libraries/LibWeb/HTML/HTMLMediaElement.cpp @@ -20,6 +20,7 @@ #include <LibWeb/HTML/CORSSettingAttribute.h> #include <LibWeb/HTML/HTMLAudioElement.h> #include <LibWeb/HTML/HTMLMediaElement.h> +#include <LibWeb/HTML/HTMLSourceElement.h> #include <LibWeb/HTML/HTMLVideoElement.h> #include <LibWeb/HTML/MediaError.h> #include <LibWeb/HTML/PotentialCORSRequest.h> @@ -72,9 +73,10 @@ void HTMLMediaElement::visit_edges(Cell::Visitor& visitor) { Base::visit_edges(visitor); visitor.visit(m_error); - visitor.visit(m_fetch_controller); visitor.visit(m_video_tracks); visitor.visit(m_document_observer); + visitor.visit(m_source_element_selector); + visitor.visit(m_fetch_controller); } void HTMLMediaElement::parse_attribute(DeprecatedFlyString const& name, DeprecatedString const& value) @@ -435,10 +437,177 @@ enum class SelectMode { Children, }; +class SourceElementSelector final : public JS::Cell { + JS_CELL(SourceElementSelector, JS::Cell); + +public: + SourceElementSelector(JS::NonnullGCPtr<HTMLMediaElement> media_element, JS::NonnullGCPtr<HTMLSourceElement> candidate) + : m_media_element(media_element) + , m_candidate(candidate) + { + } + + virtual void visit_edges(Cell::Visitor& visitor) override + { + Base::visit_edges(visitor); + visitor.visit(m_media_element); + visitor.visit(m_candidate); + } + + WebIDL::ExceptionOr<void> process_candidate() + { + auto& vm = this->vm(); + + // 2. ⌛ Process candidate: If candidate does not have a src attribute, or if its src attribute's value is the + // empty string, then end the synchronous section, and jump down to the failed with elements step below. + String candiate_src; + if (m_candidate->has_attribute(HTML::AttributeNames::src)) + candiate_src = TRY_OR_THROW_OOM(vm, String::from_utf8(m_candidate->attribute(HTML::AttributeNames::src))); + + if (candiate_src.is_empty()) { + TRY(failed_with_elements()); + return {}; + } + + // 3. ⌛ Let urlString and urlRecord be the resulting URL string and the resulting URL record, respectively, that + // would have resulted from parsing the URL specified by candidate's src attribute's value relative to the + // candidate's node document when the src attribute was last changed. + auto url_record = m_candidate->document().parse_url(candiate_src); + auto url_string = TRY_OR_THROW_OOM(vm, String::from_deprecated_string(url_record.to_deprecated_string())); + + // 4. ⌛ If urlString was not obtained successfully, then end the synchronous section, and jump down to the failed + // with elements step below. + if (!url_record.is_valid()) { + TRY(failed_with_elements()); + return {}; + } + + // FIXME: 5. ⌛ If candidate has a type attribute whose value, when parsed as a MIME type (including any codecs described + // by the codecs parameter, for types that define that parameter), represents a type that the user agent knows + // it cannot render, then end the synchronous section, and jump down to the failed with elements step below. + + // 6. ⌛ Set the currentSrc attribute to urlString. + m_media_element->m_current_src = move(url_string); + + // 7. End the synchronous section, continuing the remaining steps in parallel. + + // 8. Run the resource fetch algorithm with urlRecord. If that algorithm returns without aborting this one, then + // the load failed. + TRY(m_media_element->fetch_resource(url_record, [this](auto) { + failed_with_elements().release_value_but_fixme_should_propagate_errors(); + })); + + return {}; + } + +private: + WebIDL::ExceptionOr<void> failed_with_elements() + { + // 9. Failed with elements: Queue a media element task given the media element to fire an event named error at candidate. + m_media_element->queue_a_media_element_task([this]() { + m_candidate->dispatch_event(DOM::Event::create(m_candidate->realm(), HTML::EventNames::error).release_value_but_fixme_should_propagate_errors()); + }); + + // FIXME: 10. Await a stable state. The synchronous section consists of all the remaining steps of this algorithm until + // the algorithm says the synchronous section has ended. (Steps in synchronous sections are marked with ⌛.) + + // 11. ⌛ Forget the media element's media-resource-specific tracks. + m_media_element->forget_media_resource_specific_tracks(); + + TRY(find_next_candidate(m_candidate)); + return {}; + } + + WebIDL::ExceptionOr<void> find_next_candidate(JS::NonnullGCPtr<DOM::Node> previous_candidate) + { + // 12. ⌛ Find next candidate: Let candidate be null. + JS::GCPtr<HTMLSourceElement> candidate; + + // 13. ⌛ Search loop: If the node after pointer is the end of the list, then jump to the waiting step below. + auto* next_sibling = previous_candidate->next_sibling(); + if (!next_sibling) { + TRY(waiting(previous_candidate)); + return {}; + } + + // 14. ⌛ If the node after pointer is a source element, let candidate be that element. + if (is<HTMLSourceElement>(next_sibling)) + candidate = static_cast<HTMLSourceElement*>(next_sibling); + + // 15. ⌛ Advance pointer so that the node before pointer is now the node that was after pointer, and the node + // after pointer is the node after the node that used to be after pointer, if any. + + // 16. ⌛ If candidate is null, jump back to the search loop step. Otherwise, jump back to the process candidate step. + if (!candidate) { + TRY(find_next_candidate(*next_sibling)); + return {}; + } + + m_candidate = *candidate; + TRY(process_candidate()); + + return {}; + } + + WebIDL::ExceptionOr<void> waiting(JS::NonnullGCPtr<DOM::Node> previous_candidate) + { + // 17. ⌛ Waiting: Set the element's networkState attribute to the NETWORK_NO_SOURCE value. + m_media_element->m_network_state = HTMLMediaElement::NetworkState::NoSource; + + // 18. ⌛ Set the element's show poster flag to true. + m_media_element->set_show_poster(true); + + // 19. ⌛ Queue a media element task given the media element to set the element's delaying-the-load-event flag + // to false. This stops delaying the load event. + m_media_element->queue_a_media_element_task([this]() { + m_media_element->m_delaying_the_load_event.clear(); + }); + + // 20. End the synchronous section, continuing the remaining steps in parallel. + + // 21. Wait until the node after pointer is a node other than the end of the list. (This step might wait forever.) + TRY(wait_for_next_candidate(previous_candidate)); + + return {}; + } + + WebIDL::ExceptionOr<void> wait_for_next_candidate(JS::NonnullGCPtr<DOM::Node> previous_candidate) + { + // FIXME: We implement the "waiting" by constantly queueing a microtask to check if the previous candidate now + // has a sibling. It might be nicer for the DOM tree to just tell us when the sibling becomes available. + if (previous_candidate->next_sibling() == nullptr) { + queue_a_microtask(&m_media_element->document(), [this, previous_candidate]() { + wait_for_next_candidate(previous_candidate).release_value_but_fixme_should_propagate_errors(); + }); + + return {}; + } + + // FIXME: 22. Await a stable state. The synchronous section consists of all the remaining steps of this algorithm until + // the algorithm says the synchronous section has ended. (Steps in synchronous sections are marked with ⌛.) + + // 23. ⌛ Set the element's delaying-the-load-event flag back to true (this delays the load event again, in case + // it hasn't been fired yet). + m_media_element->m_delaying_the_load_event.emplace(m_media_element->document()); + + // 24. ⌛ Set the networkState back to NETWORK_LOADING. + m_media_element->m_network_state = HTMLMediaElement::NetworkState::Loading; + + // 25. ⌛ Jump back to the find next candidate step above. + TRY(find_next_candidate(previous_candidate)); + + return {}; + } + + JS::NonnullGCPtr<HTMLMediaElement> m_media_element; + JS::NonnullGCPtr<HTMLSourceElement> m_candidate; +}; + // https://html.spec.whatwg.org/multipage/media.html#concept-media-load-algorithm WebIDL::ExceptionOr<void> HTMLMediaElement::select_resource() { - auto& vm = this->vm(); + auto& realm = this->realm(); + auto& vm = realm.vm(); // 1. Set the element's networkState attribute to the NETWORK_NO_SOURCE value. m_network_state = NetworkState::NoSource; @@ -454,18 +623,22 @@ WebIDL::ExceptionOr<void> HTMLMediaElement::select_resource() // FIXME: 5. ⌛ If the media element's blocked-on-parser flag is false, then populate the list of pending text tracks. - auto mode = SelectMode::Children; + Optional<SelectMode> mode; + JS::GCPtr<HTMLSourceElement> candidate; // 6. FIXME: ⌛ If the media element has an assigned media provider object, then let mode be object. - // - // ⌛ Otherwise, if the media element has no assigned media provider object but has a src attribute, then let mode be attribute. + + // ⌛ Otherwise, if the media element has no assigned media provider object but has a src attribute, then let mode be attribute. if (has_attribute(HTML::AttributeNames::src)) { mode = SelectMode::Attribute; } - // FIXME: ⌛ Otherwise, if the media element does not have an assigned media provider object and does not have a src attribute, but does have - // a source element child, then let mode be children and let candidate be the first such source element child in tree order. - // - // ⌛ Otherwise the media element has no assigned media provider object and has neither a src attribute nor a source element child: + // ⌛ Otherwise, if the media element does not have an assigned media provider object and does not have a src attribute, but does have + // a source element child, then let mode be children and let candidate be the first such source element child in tree order. + else if (auto* source_element = first_child_of_type<HTMLSourceElement>()) { + mode = SelectMode::Children; + candidate = source_element; + } + // ⌛ Otherwise the media element has no assigned media provider object and has neither a src attribute nor a source element child: else { // 1. ⌛ Set the networkState to NETWORK_EMPTY. m_network_state = NetworkState::Empty; @@ -482,11 +655,11 @@ WebIDL::ExceptionOr<void> HTMLMediaElement::select_resource() // 8. ⌛ Queue a media element task given the media element to fire an event named loadstart at the media element. queue_a_media_element_task([this] { - dispatch_event(DOM::Event::create(realm(), HTML::EventNames::loadstart).release_value_but_fixme_should_propagate_errors()); + dispatch_event(DOM::Event::create(this->realm(), HTML::EventNames::loadstart).release_value_but_fixme_should_propagate_errors()); }); // 9. Run the appropriate steps from the following list: - switch (mode) { + switch (*mode) { // -> If mode is object case SelectMode::Object: // FIXME: 1. ⌛ Set the currentSrc attribute to the empty string. @@ -550,55 +723,31 @@ WebIDL::ExceptionOr<void> HTMLMediaElement::select_resource() // -> Otherwise (mode is children) case SelectMode::Children: - // FIXME: 1. ⌛ Let pointer be a position defined by two adjacent nodes in the media element's child list, treating the start of the list (before the - // first child in the list, if any) and end of the list (after the last child in the list, if any) as nodes in their own right. One node is - // the node before pointer, and the other node is the node after pointer. Initially, let pointer be the position between the candidate node - // and the next node, if there are any, or the end of the list, if it is the last node. + VERIFY(candidate); + + // 1. ⌛ Let pointer be a position defined by two adjacent nodes in the media element's child list, treating the start of the list (before the + // first child in the list, if any) and end of the list (after the last child in the list, if any) as nodes in their own right. One node is + // the node before pointer, and the other node is the node after pointer. Initially, let pointer be the position between the candidate node + // and the next node, if there are any, or the end of the list, if it is the last node. // - // As nodes are inserted and removed into the media element, pointer must be updated as follows: + // As nodes are inserted and removed into the media element, pointer must be updated as follows: // - // If a new node is inserted between the two nodes that define pointer - // Let pointer be the point between the node before pointer and the new node. In other words, insertions at pointer go after pointer. - // If the node before pointer is removed - // Let pointer be the point between the node after pointer and the node before the node after pointer. In other words, pointer doesn't - // move relative to the remaining nodes. - // If the node after pointer is removed - // Let pointer be the point between the node before pointer and the node after the node before pointer. Just as with the previous case, - // pointer doesn't move relative to the remaining nodes. - // Other changes don't affect pointer. - - // FIXME: 2. ⌛ Process candidate: If candidate does not have a src attribute, or if its src attribute's value is the empty string, then end the - // synchronous section, and jump down to the failed with elements step below. - // FIXME: 3. ⌛ Let urlString and urlRecord be the resulting URL string and the resulting URL record, respectively, that would have resulted from parsing - // the URL specified by candidate's src attribute's value relative to the candidate's node document when the src attribute was last changed. - // FIXME: 4. ⌛ If urlString was not obtained successfully, then end the synchronous section, and jump down to the failed with elements step below. - // FIXME: 5. ⌛ If candidate has a type attribute whose value, when parsed as a MIME type (including any codecs described by the codecs parameter, for - // types that define that parameter), represents a type that the user agent knows it cannot render, then end the synchronous section, and - // jump down to the failed with elements step below. - // FIXME: 6. ⌛ Set the currentSrc attribute to urlString. - // FIXME: 7. End the synchronous section, continuing the remaining steps in parallel. - // FIXME: 8. Run the resource fetch algorithm with urlRecord. If that algorithm returns without aborting this one, then the load failed. - // FIXME: 9. Failed with elements: Queue a media element task given the media element to fire an event named error at candidate. - // FIXME: 10. Await a stable state. The synchronous section consists of all the remaining steps of this algorithm until the algorithm says the - // synchronous section has ended. (Steps in synchronous sections are marked with ⌛.) - // FIXME: 11. ⌛ Forget the media element's media-resource-specific tracks. - // FIXME: 12. ⌛ Find next candidate: Let candidate be null. - // FIXME: 13. ⌛ Search loop: If the node after pointer is the end of the list, then jump to the waiting step below. - // FIXME: 14. ⌛ If the node after pointer is a source element, let candidate be that element. - // FIXME: 15. ⌛ Advance pointer so that the node before pointer is now the node that was after pointer, and the node after pointer is the node after - // the node that used to be after pointer, if any. - // FIXME: 16. ⌛ If candidate is null, jump back to the search loop step. Otherwise, jump back to the process candidate step. - // FIXME: 17. ⌛ Waiting: Set the element's networkState attribute to the NETWORK_NO_SOURCE value. - // FIXME: 18. ⌛ Set the element's show poster flag to true. - // FIXME: 19. ⌛ Queue a media element task given the media element to set the element's delaying-the-load-event flag to false. This stops delaying the - // load event. - // FIXME: 20. End the synchronous section, continuing the remaining steps in parallel. - // FIXME: 21. Wait until the node after pointer is a node other than the end of the list. (This step might wait forever.) - // FIXME: 22. Await a stable state. The synchronous section consists of all the remaining steps of this algorithm until the algorithm says the - // synchronous section has ended. (Steps in synchronous sections are marked with ⌛.) - // FIXME: 23. ⌛ Set the element's delaying-the-load-event flag back to true (this delays the load event again, in case it hasn't been fired yet). - // FIXME: 24. ⌛ Set the networkState back to NETWORK_LOADING. - // FIXME: 25. ⌛ Jump back to the find next candidate step above. + // If a new node is inserted between the two nodes that define pointer + // Let pointer be the point between the node before pointer and the new node. In other words, insertions at pointer go after pointer. + // If the node before pointer is removed + // Let pointer be the point between the node after pointer and the node before the node after pointer. In other words, pointer doesn't + // move relative to the remaining nodes. + // If the node after pointer is removed + // Let pointer be the point between the node before pointer and the node after the node before pointer. Just as with the previous case, + // pointer doesn't move relative to the remaining nodes. + // Other changes don't affect pointer. + + // NOTE: We do not bother with maintaining this pointer. We inspect the DOM tree on the fly, rather than dealing + // with the headache of auto-updating this pointer as the DOM changes. + + m_source_element_selector = TRY(vm.heap().allocate<SourceElementSelector>(realm, *this, *candidate)); + TRY(m_source_element_selector->process_candidate()); + break; } @@ -844,6 +993,9 @@ WebIDL::ExceptionOr<void> HTMLMediaElement::process_media_data(Function<void(Str auto event = TRY(TrackEvent::create(realm, HTML::EventNames::addtrack, event_init)); m_video_tracks->dispatch_event(event); + + // AD-HOC: After selecting a track, we do not need the source element selector anymore. + m_source_element_selector = nullptr; } // -> Once enough of the media data has been fetched to determine the duration of the media resource, its dimensions, and other metadata diff --git a/Userland/Libraries/LibWeb/HTML/HTMLMediaElement.h b/Userland/Libraries/LibWeb/HTML/HTMLMediaElement.h index 9638e5464a..766d77e3f0 100644 --- a/Userland/Libraries/LibWeb/HTML/HTMLMediaElement.h +++ b/Userland/Libraries/LibWeb/HTML/HTMLMediaElement.h @@ -26,6 +26,8 @@ enum class MediaSeekMode { ApproximateForSpeed, }; +class SourceElementSelector; + class HTMLMediaElement : public HTMLElement { WEB_PLATFORM_OBJECT(HTMLMediaElement, HTMLElement); @@ -101,6 +103,8 @@ protected: virtual void on_seek(double, MediaSeekMode) { m_seek_in_progress = false; } private: + friend SourceElementSelector; + struct EntireResource { }; using ByteRange = Variant<EntireResource>; // FIXME: This will need to include "until end" and an actual byte range. @@ -210,6 +214,8 @@ private: JS::GCPtr<DOM::DocumentObserver> m_document_observer; + JS::GCPtr<SourceElementSelector> m_source_element_selector; + JS::GCPtr<Fetch::Infrastructure::FetchController> m_fetch_controller; bool m_seek_in_progress = false; |