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/Page/Page.h>
#include <LibWeb/Painting/Paintable.h>
#include <LibWeb/Platform/EventLoopPlugin.h>
#include <LibWeb/WebIDL/Promise.h>
namespace Web::HTML {
@ -813,9 +814,10 @@ WebIDL::ExceptionOr<void> HTMLMediaElement::select_resource()
// 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());
// 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 ⌛.)
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;
@ -842,7 +844,7 @@ WebIDL::ExceptionOr<void> HTMLMediaElement::select_resource()
m_delaying_the_load_event.clear();
// 3. End the synchronous section and return.
return {};
return;
}
// 7. ⌛ Set the media element's networkState to NETWORK_LOADING.
@ -866,7 +868,7 @@ WebIDL::ExceptionOr<void> HTMLMediaElement::select_resource()
// 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 {};
return;
// -> If mode is attribute
case SelectMode::Attribute: {
@ -890,7 +892,7 @@ WebIDL::ExceptionOr<void> HTMLMediaElement::select_resource()
auto source = get_attribute_value(HTML::AttributeNames::src);
if (source.is_empty()) {
failed_with_attribute("The 'src' attribute is empty"_string);
return {};
return;
}
// 2. ⌛ Let urlString and urlRecord be the resulting URL string and the resulting URL record, respectively, that would have resulted from parsing
@ -905,15 +907,15 @@ WebIDL::ExceptionOr<void> HTMLMediaElement::select_resource()
// 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()) {
TRY(fetch_resource(*url_record, move(failed_with_attribute)));
return {};
fetch_resource(*url_record, move(failed_with_attribute)).release_value_but_fixme_should_propagate_errors();
return;
}
failed_with_attribute("Failed to parse 'src' attribute as a URL"_string);
}));
// 8. Return. The element won't attempt to load another resource until this algorithm is triggered again.
return {};
return;
}
// -> Otherwise (mode is children)
@ -941,10 +943,11 @@ WebIDL::ExceptionOr<void> HTMLMediaElement::select_resource()
// 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());
m_source_element_selector->process_candidate().release_value_but_fixme_should_propagate_errors();
break;
}
}));
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>