LibWeb: Resolve FIXME in media select resource algorithm

Fixes at least three WPT test that were previously timing out:
- html/semantics/embedded-content/media-elements/error-codes/error.html
- html/semantics/embedded-content/media-elements/location-of-the-media-resource/currentSrc.html
- html/semantics/embedded-content/the-video-element/video_crash_empty_src.html
This commit is contained in:
Prajjwal 2025-06-02 04:26:02 +05:30 committed by Shannon Booth
commit e1d2582680
Notes: github-actions[bot] 2025-06-16 11:29:21 +00:00
8 changed files with 334 additions and 115 deletions

View file

@ -40,6 +40,7 @@
#include <LibWeb/MimeSniff/MimeType.h> #include <LibWeb/MimeSniff/MimeType.h>
#include <LibWeb/Page/Page.h> #include <LibWeb/Page/Page.h>
#include <LibWeb/Painting/Paintable.h> #include <LibWeb/Painting/Paintable.h>
#include <LibWeb/Platform/EventLoopPlugin.h>
#include <LibWeb/WebIDL/Promise.h> #include <LibWeb/WebIDL/Promise.h>
namespace Web::HTML { namespace Web::HTML {
@ -813,138 +814,140 @@ WebIDL::ExceptionOr<void> HTMLMediaElement::select_resource()
// 3. Set the media element's delaying-the-load-event flag to true (this delays the load event). // 3. Set the media element's delaying-the-load-event flag to true (this delays the load event).
m_delaying_the_load_event.emplace(document()); m_delaying_the_load_event.emplace(document());
// FIXME: 4. Await a stable state, allowing the task that invoked this algorithm to continue. The synchronous section consists of all the remaining // 4. Await a stable state, allowing the task that invoked this algorithm to continue. 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 ⌛.) // steps of this algorithm until the algorithm says the synchronous section has ended. (Steps in synchronous sections are marked with ⌛.)
// FIXME: 5. ⌛ If the media element's blocked-on-parser flag is false, then populate the list of pending text tracks. queue_a_microtask(&document(), GC::create_function(realm.heap(), [this, &realm]() {
// FIXME: 5. ⌛ If the media element's blocked-on-parser flag is false, then populate the list of pending text tracks.
Optional<SelectMode> mode; Optional<SelectMode> mode;
GC::Ptr<HTMLSourceElement> candidate; GC::Ptr<HTMLSourceElement> candidate;
// 6. FIXME: ⌛ If the media element has an assigned media provider object, then let mode be object. // 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)) { if (has_attribute(HTML::AttributeNames::src)) {
mode = SelectMode::Attribute; mode = SelectMode::Attribute;
} }
// ⌛ Otherwise, if the media element does not have an assigned media provider object and does not have a src attribute, but does have // ⌛ 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. // 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>()) { else if (auto* source_element = first_child_of_type<HTMLSourceElement>()) {
mode = SelectMode::Children; mode = SelectMode::Children;
candidate = source_element; candidate = source_element;
} }
// ⌛ Otherwise the media element has no assigned media provider object and has neither a src attribute nor a source element child: // ⌛ Otherwise the media element has no assigned media provider object and has neither a src attribute nor a source element child:
else { else {
// 1. ⌛ Set the networkState to NETWORK_EMPTY. // 1. ⌛ Set the networkState to NETWORK_EMPTY.
m_network_state = NetworkState::Empty; m_network_state = NetworkState::Empty;
// 2. ⌛ Set the element's delaying-the-load-event flag to false. This stops delaying the load event. // 2. ⌛ Set the element's delaying-the-load-event flag to false. This stops delaying the load event.
m_delaying_the_load_event.clear(); m_delaying_the_load_event.clear();
// 3. End the synchronous section and return. // 3. End the synchronous section and return.
return {}; return;
}
// 7. ⌛ Set the media element's networkState to NETWORK_LOADING.
m_network_state = NetworkState::Loading;
// 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(this->realm(), HTML::EventNames::loadstart));
});
// 9. Run the appropriate steps from the following list:
switch (*mode) {
// -> If mode is object
case SelectMode::Object:
// FIXME: 1. ⌛ Set the currentSrc attribute to the empty string.
// FIXME: 2. End the synchronous section, continuing the remaining steps in parallel.
// FIXME: 3. Run the resource fetch algorithm with the assigned media provider object. If that algorithm returns without aborting this one,
// then theload failed.
// FIXME: 4. Failed with media provider: Reaching this step indicates that the media resource failed to load. 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.
// FIXME: 5. Wait for the task queued by the previous step to have executed.
// 6. Return. The element won't attempt to load another resource until this algorithm is triggered again.
return {};
// -> If mode is attribute
case SelectMode::Attribute: {
auto failed_with_attribute = [this](auto error_message) {
IGNORE_USE_IN_ESCAPING_LAMBDA bool ran_media_element_task = false;
// 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, error_message = move(error_message)]() mutable {
auto promises = take_pending_play_promises();
handle_media_source_failure(promises, move(error_message)).release_value_but_fixme_should_propagate_errors();
ran_media_element_task = true;
});
// 7. Wait for the task queued by the previous step to have executed.
HTML::main_thread_event_loop().spin_until(GC::create_function(heap(), [&]() { return ran_media_element_task; }));
};
// 1. ⌛ If the src attribute's value is the empty string, then end the synchronous section, and jump down to the failed with attribute step below.
auto source = get_attribute_value(HTML::AttributeNames::src);
if (source.is_empty()) {
failed_with_attribute("The 'src' attribute is empty"_string);
return {};
} }
// 2. ⌛ Let urlString and urlRecord be the resulting URL string and the resulting URL record, respectively, that would have resulted from parsing // 7. ⌛ Set the media element's networkState to NETWORK_LOADING.
// the URL specified by the src attribute's value relative to the media element's node document when the src attribute was last changed. m_network_state = NetworkState::Loading;
auto url_record = document().parse_url(source);
// 3. ⌛ If urlString was obtained successfully, set the currentSrc attribute to urlString. // 8. ⌛ Queue a media element task given the media element to fire an event named loadstart at the media element.
if (url_record.has_value()) queue_a_media_element_task([this] {
m_current_src = url_record->to_string(); dispatch_event(DOM::Event::create(this->realm(), HTML::EventNames::loadstart));
});
// 4. End the synchronous section, continuing the remaining steps in parallel. // 9. Run the appropriate steps from the following list:
switch (*mode) {
// -> If mode is object
case SelectMode::Object:
// FIXME: 1. ⌛ Set the currentSrc attribute to the empty string.
// FIXME: 2. End the synchronous section, continuing the remaining steps in parallel.
// FIXME: 3. Run the resource fetch algorithm with the assigned media provider object. If that algorithm returns without aborting this one,
// then theload failed.
// FIXME: 4. Failed with media provider: Reaching this step indicates that the media resource failed to load. 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.
// FIXME: 5. Wait for the task queued by the previous step to have executed.
// 5. If urlRecord was obtained successfully, run the resource fetch algorithm with urlRecord. If that algorithm returns without aborting this one, // 6. Return. The element won't attempt to load another resource until this algorithm is triggered again.
// then the load failed. return;
if (url_record.has_value()) {
TRY(fetch_resource(*url_record, move(failed_with_attribute))); // -> If mode is attribute
return {}; case SelectMode::Attribute: {
auto failed_with_attribute = [this](auto error_message) {
IGNORE_USE_IN_ESCAPING_LAMBDA bool ran_media_element_task = false;
// 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, error_message = move(error_message)]() mutable {
auto promises = take_pending_play_promises();
handle_media_source_failure(promises, move(error_message)).release_value_but_fixme_should_propagate_errors();
ran_media_element_task = true;
});
// 7. Wait for the task queued by the previous step to have executed.
HTML::main_thread_event_loop().spin_until(GC::create_function(heap(), [&]() { return ran_media_element_task; }));
};
// 1. ⌛ If the src attribute's value is the empty string, then end the synchronous section, and jump down to the failed with attribute step below.
auto source = get_attribute_value(HTML::AttributeNames::src);
if (source.is_empty()) {
failed_with_attribute("The 'src' attribute is empty"_string);
return;
}
// 2. ⌛ 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 the src attribute's value relative to the media element's node document when the src attribute was last changed.
auto url_record = document().parse_url(source);
// 3. ⌛ If urlString was obtained successfully, set the currentSrc attribute to urlString.
if (url_record.has_value())
m_current_src = url_record->to_string();
// 4. End the synchronous section, continuing the remaining steps in parallel.
// 5. If urlRecord was obtained successfully, run the resource fetch algorithm with urlRecord. If that algorithm returns without aborting this one,
// then the load failed.
Platform::EventLoopPlugin::the().deferred_invoke(GC::create_function(realm.heap(), [this, url_record = move(url_record), failed_with_attribute = move(failed_with_attribute)]() mutable {
if (url_record.has_value()) {
fetch_resource(*url_record, move(failed_with_attribute)).release_value_but_fixme_should_propagate_errors();
return;
}
}));
// 8. Return. The element won't attempt to load another resource until this algorithm is triggered again.
return;
} }
failed_with_attribute("Failed to parse 'src' attribute as a URL"_string); // -> Otherwise (mode is children)
case SelectMode::Children:
VERIFY(candidate);
// 8. Return. The element won't attempt to load another resource until this algorithm is triggered again. // 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
return {}; // 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:
//
// 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.
// -> Otherwise (mode is children) // NOTE: We do not bother with maintaining this pointer. We inspect the DOM tree on the fly, rather than dealing
case SelectMode::Children: // with the headache of auto-updating this pointer as the DOM changes.
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 m_source_element_selector = realm.create<SourceElementSelector>(*this, *candidate);
// 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 m_source_element_selector->process_candidate().release_value_but_fixme_should_propagate_errors();
// 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:
//
// 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 break;
// with the headache of auto-updating this pointer as the DOM changes. }
}));
m_source_element_selector = realm.create<SourceElementSelector>(*this, *candidate);
TRY(m_source_element_selector->process_candidate());
break;
}
return {}; return {};
} }

View file

@ -0,0 +1,12 @@
Harness status: OK
Found 6 tests
4 Pass
2 Fail
Pass audio.error initial value
Fail audio.error after successful load
Pass audio.error after setting src to the empty string
Pass video.error initial value
Fail video.error after successful load
Pass video.error after setting src to the empty string

View file

@ -0,0 +1,23 @@
Harness status: OK
Found 18 tests
18 Pass
Pass audio.currentSrc initial value
Pass audio.currentSrc after setting src attribute ""
Pass audio.currentSrc after adding source element with src attribute ""
Pass audio.currentSrc after setting src attribute "."
Pass audio.currentSrc after adding source element with src attribute "."
Pass audio.currentSrc after setting src attribute " "
Pass audio.currentSrc after adding source element with src attribute " "
Pass audio.currentSrc after setting src attribute "data:,"
Pass audio.currentSrc after adding source element with src attribute "data:,"
Pass video.currentSrc initial value
Pass video.currentSrc after setting src attribute ""
Pass video.currentSrc after adding source element with src attribute ""
Pass video.currentSrc after setting src attribute "."
Pass video.currentSrc after adding source element with src attribute "."
Pass video.currentSrc after setting src attribute " "
Pass video.currentSrc after adding source element with src attribute " "
Pass video.currentSrc after setting src attribute "data:,"
Pass video.currentSrc after adding source element with src attribute "data:,"

View file

@ -0,0 +1,7 @@
Harness status: OK
Found 2 tests
2 Pass
Pass src="about:blank" does not crash.
Pass src="" does not crash.

View file

@ -0,0 +1,57 @@
/**
* Returns the URL of a supported video source based on the user agent
* @param {string} base - media URL without file extension
* @returns {string}
*/
function getVideoURI(base)
{
var extension = '.mp4';
var videotag = document.createElement("video");
if ( videotag.canPlayType )
{
if (videotag.canPlayType('video/webm; codecs="vp9, opus"') )
{
extension = '.webm';
}
}
return base + extension;
}
/**
* Returns the URL of a supported audio source based on the user agent
* @param {string} base - media URL without file extension
* @returns {string}
*/
function getAudioURI(base)
{
var extension = '.mp3';
var audiotag = document.createElement("audio");
if ( audiotag.canPlayType &&
audiotag.canPlayType('audio/ogg') )
{
extension = '.oga';
}
return base + extension;
}
/**
* Returns the MIME type for a media URL based on the file extension.
* @param {string} url
* @returns {string}
*/
function getMediaContentType(url) {
var extension = new URL(url, location).pathname.split(".").pop();
var map = {
"mp4" : "video/mp4",
"webm": "video/webm",
"mp3" : "audio/mp3",
"oga" : "application/ogg",
};
return map[extension];
}

View file

@ -0,0 +1,40 @@
<!doctype html>
<title>error</title>
<script src="../../../../../resources/testharness.js"></script>
<script src="../../../../../resources/testharnessreport.js"></script>
<script src="../../../../../common/media.js"></script>
<div id="log"></div>
<script>
function error_test(tagName, src) {
test(function() {
assert_equals(document.createElement(tagName).error, null);
}, tagName + '.error initial value');
async_test(function(t) {
var e = document.createElement(tagName);
e.src = src;
e.onerror = t.unreached_func();
e.onloadeddata = t.step_func(function() {
assert_equals(e.error, null);
t.done();
});
}, tagName + '.error after successful load');
// TODO: MEDIA_ERR_ABORTED, MEDIA_ERR_NETWORK, MEDIA_ERR_DECODE
async_test(function(t) {
var e = document.createElement(tagName);
e.src = '';
e.onerror = t.step_func(function() {
assert_true(e.error instanceof MediaError);
assert_equals(e.error.code, 4);
assert_equals(e.error.code, e.error.MEDIA_ERR_SRC_NOT_SUPPORTED);
assert_equals(typeof e.error.message, 'string', 'error.message type');
t.done();
});
}, tagName + '.error after setting src to the empty string');
}
error_test('audio', getAudioURI('/media/sound_5'));
error_test('video', getVideoURI('/media/movie_5'));
</script>

View file

@ -0,0 +1,48 @@
<!doctype html>
<title>currentSrc</title>
<script src="../../../../../resources/testharness.js"></script>
<script src="../../../../../resources/testharnessreport.js"></script>
<div id="log"></div>
<script>
['audio', 'video'].forEach(function(tagName) {
test(function() {
assert_equals(document.createElement(tagName).currentSrc, '');
}, tagName + '.currentSrc initial value');
['', '.', ' ', 'data:,'].forEach(function(src) {
async_test(function(t) {
var e = document.createElement(tagName);
e.src = src;
assert_equals(e.currentSrc, '');
e.addEventListener('loadstart', function () {
t.step_timeout(function () {
if (src == '') {
assert_equals(e.currentSrc, '');
} else {
assert_equals(e.currentSrc, e.src);
}
t.done();
}, 0);
})
}, tagName + '.currentSrc after setting src attribute "' + src + '"');
async_test(function(t) {
var e = document.createElement(tagName);
var s = document.createElement('source');
s.src = src;
e.appendChild(s);
assert_equals(e.currentSrc, '');
e.addEventListener('loadstart', function() {
t.step_timeout(function () {
if (src == '') {
assert_equals(e.currentSrc, '');
} else {
assert_equals(e.currentSrc, s.src);
}
t.done();
}, 0);
});
}, tagName + '.currentSrc after adding source element with src attribute "' + src + '"');
});
});
</script>

View file

@ -0,0 +1,29 @@
<!DOCTYPE HTML>
<html>
<head>
<title>HTML5 Media Elements: An empty src should not crash the player.</title>
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type">
<link rel="author" title="Alicia Boya García" href="mailto:aboya@igalia.com"/>
<script src="../../../../resources/testharness.js"></script>
<script src="../../../../resources/testharnessreport.js"></script>
</head>
<body>
<script>
function makeCrashTest(src) {
async_test((test) => {
const video = document.createElement("video");
video.src = src;
video.controls = true;
video.addEventListener("error", () => {
document.body.removeChild(video);
test.done();
});
document.body.appendChild(video);
}, `src="${src}" does not crash.`);
}
makeCrashTest("about:blank");
makeCrashTest("");
</script>
</body>
</html>