From c9d3d1799961749e23da7457bb6bee20a84a0d6e Mon Sep 17 00:00:00 2001 From: egrs Date: Tue, 17 Feb 2026 13:22:32 +0100 Subject: [PATCH 1/5] dispatch play, playing, pause, and emptied events from HTMLMediaElement play(), pause(), and load() now fire the corresponding DOM events, matching the HTMLMediaElement spec behavior. --- src/browser/tests/element/html/media.html | 21 +++++++++++++++++++++ src/browser/webapi/element/html/Media.zig | 20 ++++++++++++++------ 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/src/browser/tests/element/html/media.html b/src/browser/tests/element/html/media.html index 69679b49..97a4d152 100644 --- a/src/browser/tests/element/html/media.html +++ b/src/browser/tests/element/html/media.html @@ -50,6 +50,27 @@ } + + diff --git a/src/browser/webapi/element/html/Media.zig b/src/browser/webapi/element/html/Media.zig index cdfec68a..f5437844 100644 --- a/src/browser/webapi/element/html/Media.zig +++ b/src/browser/webapi/element/html/Media.zig @@ -138,16 +138,21 @@ fn isMaybeSupported(mime_type: []const u8) bool { } pub fn play(self: *Media, page: *Page) !void { + const was_paused = self._paused; self._paused = false; self._ready_state = .HAVE_ENOUGH_DATA; self._network_state = .NETWORK_IDLE; - try self.dispatchEvent("play", page); + if (was_paused) { + try self.dispatchEvent("play", page); + } try self.dispatchEvent("playing", page); } pub fn pause(self: *Media, page: *Page) !void { - self._paused = true; - try self.dispatchEvent("pause", page); + if (!self._paused) { + self._paused = true; + try self.dispatchEvent("pause", page); + } } pub fn load(self: *Media, page: *Page) !void { @@ -160,7 +165,7 @@ pub fn load(self: *Media, page: *Page) !void { } fn dispatchEvent(self: *Media, name: []const u8, page: *Page) !void { - const event = try Event.init(name, null, page); + const event = try Event.init(name, .{ .bubbles = false, .cancelable = false }, page); defer if (!event._v8_handoff) event.deinit(false); try page._event_manager.dispatch(self.asElement().asEventTarget(), event); } From 1b71d1e46d3c09fb828f81a22da321101af83675 Mon Sep 17 00:00:00 2001 From: egrs Date: Tue, 17 Feb 2026 16:10:49 +0100 Subject: [PATCH 3/5] fix playing event: only dispatch on paused-to-playing transition Per MDN, playing fires "after playback is first started, and whenever it is restarted." A second play() while already playing should be a no-op. Both play and playing now only fire on the paused -> playing transition. --- src/browser/tests/element/html/media.html | 10 +++++----- src/browser/webapi/element/html/Media.zig | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/browser/tests/element/html/media.html b/src/browser/tests/element/html/media.html index 80885c6e..ce62ed86 100644 --- a/src/browser/tests/element/html/media.html +++ b/src/browser/tests/element/html/media.html @@ -64,21 +64,21 @@ audio.play(); testing.expectEqual('play,playing', events.join(',')); - // Second play: already playing, fires only playing (not play) + // Second play: already playing, no events audio.play(); - testing.expectEqual('play,playing,playing', events.join(',')); + testing.expectEqual('play,playing', events.join(',')); // Pause: playing -> paused, fires pause audio.pause(); - testing.expectEqual('play,playing,playing,pause', events.join(',')); + testing.expectEqual('play,playing,pause', events.join(',')); // Second pause: already paused, no event audio.pause(); - testing.expectEqual('play,playing,playing,pause', events.join(',')); + testing.expectEqual('play,playing,pause', events.join(',')); // Load: fires emptied audio.load(); - testing.expectEqual('play,playing,playing,pause,emptied', events.join(',')); + testing.expectEqual('play,playing,pause,emptied', events.join(',')); } diff --git a/src/browser/webapi/element/html/Media.zig b/src/browser/webapi/element/html/Media.zig index f5437844..ca9f6088 100644 --- a/src/browser/webapi/element/html/Media.zig +++ b/src/browser/webapi/element/html/Media.zig @@ -144,8 +144,8 @@ pub fn play(self: *Media, page: *Page) !void { self._network_state = .NETWORK_IDLE; if (was_paused) { try self.dispatchEvent("play", page); + try self.dispatchEvent("playing", page); } - try self.dispatchEvent("playing", page); } pub fn pause(self: *Media, page: *Page) !void { From 830eb7472539eb2b72f47c8b56a59e7398864b3f Mon Sep 17 00:00:00 2001 From: egrs Date: Wed, 18 Feb 2026 10:22:49 +0100 Subject: [PATCH 4/5] track playing state: only dispatch playing on first start Per review: playing event should only fire on first start, not on every resume from pause. Add _playing field, reset on load(). --- src/browser/tests/element/html/media.html | 16 ++++++++++++++-- src/browser/webapi/element/html/Media.zig | 7 ++++++- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/browser/tests/element/html/media.html b/src/browser/tests/element/html/media.html index ce62ed86..e6884543 100644 --- a/src/browser/tests/element/html/media.html +++ b/src/browser/tests/element/html/media.html @@ -76,9 +76,21 @@ audio.pause(); testing.expectEqual('play,playing,pause', events.join(',')); - // Load: fires emptied + // Third play: resume from pause, fires play only (not playing) + audio.play(); + testing.expectEqual('play,playing,pause,play', events.join(',')); + + // Pause again + audio.pause(); + testing.expectEqual('play,playing,pause,play,pause', events.join(',')); + + // Load: resets state, fires emptied audio.load(); - testing.expectEqual('play,playing,pause,emptied', events.join(',')); + testing.expectEqual('play,playing,pause,play,pause,emptied', events.join(',')); + + // Play after load: fires play + playing again (fresh start) + audio.play(); + testing.expectEqual('play,playing,pause,play,pause,emptied,play,playing', events.join(',')); } diff --git a/src/browser/webapi/element/html/Media.zig b/src/browser/webapi/element/html/Media.zig index ca9f6088..ab5be73f 100644 --- a/src/browser/webapi/element/html/Media.zig +++ b/src/browser/webapi/element/html/Media.zig @@ -61,6 +61,7 @@ _playback_rate: f64 = 1.0, _ready_state: ReadyState = .HAVE_NOTHING, _network_state: NetworkState = .NETWORK_EMPTY, _error: ?*MediaError = null, +_playing: bool = false, pub fn asElement(self: *Media) *Element { return self._proto._proto; @@ -144,7 +145,10 @@ pub fn play(self: *Media, page: *Page) !void { self._network_state = .NETWORK_IDLE; if (was_paused) { try self.dispatchEvent("play", page); - try self.dispatchEvent("playing", page); + if (!self._playing) { + self._playing = true; + try self.dispatchEvent("playing", page); + } } } @@ -157,6 +161,7 @@ pub fn pause(self: *Media, page: *Page) !void { pub fn load(self: *Media, page: *Page) !void { self._paused = true; + self._playing = false; self._current_time = 0; self._ready_state = .HAVE_NOTHING; self._network_state = .NETWORK_LOADING; From 7675feca91e5049dc899301f726193c2690bff2f Mon Sep 17 00:00:00 2001 From: egrs Date: Wed, 18 Feb 2026 10:49:27 +0100 Subject: [PATCH 5/5] revert _playing flag: playing event fires on every resume Tested in Chrome (headless, --autoplay-policy=no-user-gesture-required): playing fires on every pause-to-play transition, not just the first time. The _playing flag was incorrectly suppressing this. Removed it and updated tests to match verified Chrome behavior. --- src/browser/tests/element/html/media.html | 12 ++++++------ src/browser/webapi/element/html/Media.zig | 7 +------ 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/src/browser/tests/element/html/media.html b/src/browser/tests/element/html/media.html index e6884543..4eba2e76 100644 --- a/src/browser/tests/element/html/media.html +++ b/src/browser/tests/element/html/media.html @@ -76,21 +76,21 @@ audio.pause(); testing.expectEqual('play,playing,pause', events.join(',')); - // Third play: resume from pause, fires play only (not playing) + // Third play: resume from pause, fires play + playing (verified in Chrome) audio.play(); - testing.expectEqual('play,playing,pause,play', events.join(',')); + testing.expectEqual('play,playing,pause,play,playing', events.join(',')); // Pause again audio.pause(); - testing.expectEqual('play,playing,pause,play,pause', events.join(',')); + testing.expectEqual('play,playing,pause,play,playing,pause', events.join(',')); // Load: resets state, fires emptied audio.load(); - testing.expectEqual('play,playing,pause,play,pause,emptied', events.join(',')); + testing.expectEqual('play,playing,pause,play,playing,pause,emptied', events.join(',')); - // Play after load: fires play + playing again (fresh start) + // Play after load: fires play + playing audio.play(); - testing.expectEqual('play,playing,pause,play,pause,emptied,play,playing', events.join(',')); + testing.expectEqual('play,playing,pause,play,playing,pause,emptied,play,playing', events.join(',')); } diff --git a/src/browser/webapi/element/html/Media.zig b/src/browser/webapi/element/html/Media.zig index ab5be73f..ca9f6088 100644 --- a/src/browser/webapi/element/html/Media.zig +++ b/src/browser/webapi/element/html/Media.zig @@ -61,7 +61,6 @@ _playback_rate: f64 = 1.0, _ready_state: ReadyState = .HAVE_NOTHING, _network_state: NetworkState = .NETWORK_EMPTY, _error: ?*MediaError = null, -_playing: bool = false, pub fn asElement(self: *Media) *Element { return self._proto._proto; @@ -145,10 +144,7 @@ pub fn play(self: *Media, page: *Page) !void { self._network_state = .NETWORK_IDLE; if (was_paused) { try self.dispatchEvent("play", page); - if (!self._playing) { - self._playing = true; - try self.dispatchEvent("playing", page); - } + try self.dispatchEvent("playing", page); } } @@ -161,7 +157,6 @@ pub fn pause(self: *Media, page: *Page) !void { pub fn load(self: *Media, page: *Page) !void { self._paused = true; - self._playing = false; self._current_time = 0; self._ready_state = .HAVE_NOTHING; self._network_state = .NETWORK_LOADING;