Media/Audio/Video elements

This commit is contained in:
Karl Seguin
2025-12-12 17:34:57 +08:00
parent a4fa40743a
commit 5eb54bbc95
9 changed files with 778 additions and 22 deletions

View File

@@ -239,6 +239,13 @@ pub fn htmlElement(self: *Factory, child: anytype) !*@TypeOf(child) {
).create(allocator, child);
}
pub fn htmlMediaElement(self: *Factory, child: anytype) !*@TypeOf(child) {
const allocator = self._slab.allocator();
return try AutoPrototypeChain(
&.{ EventTarget, Node, Element, Element.Html, Element.Html.Media, @TypeOf(child) },
).create(allocator, child);
}
pub fn svgElement(self: *Factory, tag_name: []const u8, child: anytype) !*@TypeOf(child) {
const allocator = self._slab.allocator();
const ChildT = @TypeOf(child);

View File

@@ -1186,6 +1186,16 @@ pub fn createElement(self: *Page, ns_: ?[]const u8, name: []const u8, attribute_
attribute_iterator,
.{ ._proto = undefined },
),
asUint("audio") => return self.createHtmlMediaElementT(
Element.Html.Media.Audio,
namespace,
attribute_iterator,
),
asUint("video") => return self.createHtmlMediaElementT(
Element.Html.Media.Video,
namespace,
attribute_iterator,
),
else => {},
},
6 => switch (@as(u48, @bitCast(name[0..6].*))) {
@@ -1343,6 +1353,14 @@ fn createHtmlElementT(self: *Page, comptime E: type, namespace: Element.Namespac
return node;
}
fn createHtmlMediaElementT(self: *Page, comptime E: type, namespace: Element.Namespace, attribute_iterator: anytype) !*Node {
const media_element = try self._factory.htmlMediaElement(E{ ._proto = undefined });
const element = media_element.asElement();
element._namespace = namespace;
try self.populateElementAttributes(element, attribute_iterator);
return element.asNode();
}
fn createSvgElementT(self: *Page, comptime E: type, tag_name: []const u8, attribute_iterator: anytype, svg_element: E) !*Node {
const svg_element_ptr = try self._factory.svgElement(tag_name, svg_element);
var element = svg_element_ptr.asElement();

View File

@@ -526,6 +526,7 @@ pub const JsApis = flattenTypes(&.{
@import("../webapi/element/Html.zig"),
@import("../webapi/element/html/IFrame.zig"),
@import("../webapi/element/html/Anchor.zig"),
@import("../webapi/element/html/Audio.zig"),
@import("../webapi/element/html/Body.zig"),
@import("../webapi/element/html/BR.zig"),
@import("../webapi/element/html/Button.zig"),
@@ -544,6 +545,7 @@ pub const JsApis = flattenTypes(&.{
@import("../webapi/element/html/Input.zig"),
@import("../webapi/element/html/LI.zig"),
@import("../webapi/element/html/Link.zig"),
@import("../webapi/element/html/Media.zig"),
@import("../webapi/element/html/Meta.zig"),
@import("../webapi/element/html/OL.zig"),
@import("../webapi/element/html/Option.zig"),
@@ -555,6 +557,7 @@ pub const JsApis = flattenTypes(&.{
@import("../webapi/element/html/Template.zig"),
@import("../webapi/element/html/TextArea.zig"),
@import("../webapi/element/html/Title.zig"),
@import("../webapi/element/html/Video.zig"),
@import("../webapi/element/html/UL.zig"),
@import("../webapi/element/html/Unknown.zig"),
@import("../webapi/element/Svg.zig"),

View File

@@ -0,0 +1,249 @@
<!DOCTYPE html>
<script src="../../testing.js"></script>
<audio id="audio1" src="test.mp3"></audio>
<video id="video1" src="test.mp4"></video>
<script id="canPlayType_audio">
{
const audio = document.getElementById('audio1');
testing.expectEqual('probably', audio.canPlayType('audio/mp3'));
testing.expectEqual('probably', audio.canPlayType('audio/mpeg'));
testing.expectEqual('probably', audio.canPlayType('audio/webm'));
testing.expectEqual('probably', audio.canPlayType('audio/ogg'));
testing.expectEqual('probably', audio.canPlayType('audio/wav'));
testing.expectEqual('maybe', audio.canPlayType('audio/aac'));
testing.expectEqual('maybe', audio.canPlayType('audio/flac'));
testing.expectEqual('', audio.canPlayType('audio/invalid'));
testing.expectEqual('', audio.canPlayType('video/invalid'));
}
</script>
<script id="canPlayType_video">
{
const video = document.getElementById('video1');
testing.expectEqual('probably', video.canPlayType('video/mp4'));
testing.expectEqual('probably', video.canPlayType('video/webm'));
testing.expectEqual('probably', video.canPlayType('video/ogg'));
testing.expectEqual('', video.canPlayType('video/invalid'));
}
</script>
<script id="canPlayType_with_codecs">
{
const audio = document.getElementById('audio1');
testing.expectEqual('probably', audio.canPlayType('audio/mp3; codecs="mp3"'));
testing.expectEqual('probably', audio.canPlayType('video/mp4; codecs="avc1.42E01E"'));
}
</script>
<script id="play_pause">
{
const audio = document.getElementById('audio1');
testing.expectEqual(true, audio.paused);
audio.play();
testing.expectEqual(false, audio.paused);
audio.pause();
testing.expectEqual(true, audio.paused);
}
</script>
<script id="volume_muted">
{
const audio = document.getElementById('audio1');
testing.expectEqual(1.0, audio.volume);
testing.expectEqual(false, audio.muted);
audio.volume = 0.5;
testing.expectEqual(0.5, audio.volume);
audio.volume = 1.5; // Should clamp to 1.0
testing.expectEqual(1.0, audio.volume);
audio.volume = -0.5; // Should clamp to 0.0
testing.expectEqual(0.0, audio.volume);
audio.muted = true;
testing.expectEqual(true, audio.muted);
}
</script>
<script id="playback_rate">
{
const audio = document.getElementById('audio1');
testing.expectEqual(1.0, audio.playbackRate);
audio.playbackRate = 2.0;
testing.expectEqual(2.0, audio.playbackRate);
audio.playbackRate = 0.5;
testing.expectEqual(0.5, audio.playbackRate);
}
</script>
<script id="currentTime">
{
const audio = document.getElementById('audio1');
testing.expectEqual(0, audio.currentTime);
audio.currentTime = 10.5;
testing.expectEqual(10.5, audio.currentTime);
}
</script>
<script id="duration_nan">
{
const audio = document.getElementById('audio1');
testing.expectEqual(true, isNaN(audio.duration));
}
</script>
<script id="ended_seeking">
{
const audio = document.getElementById('audio1');
testing.expectEqual(false, audio.ended);
testing.expectEqual(false, audio.seeking);
}
</script>
<script id="ready_state_network_state">
{
// Create a fresh element to test initial state
const audio = document.createElement('audio');
testing.expectEqual(0, audio.readyState); // HAVE_NOTHING initially
testing.expectEqual(0, audio.networkState); // NETWORK_EMPTY initially
audio.play();
// In headless mode, play() immediately succeeds without actual media loading
testing.expectEqual(4, audio.readyState); // HAVE_ENOUGH_DATA in headless
testing.expectEqual(1, audio.networkState); // NETWORK_IDLE in headless
}
</script>
<script id="load_reset">
{
// Create a fresh element to test load() behavior
const audio = document.createElement('audio');
audio.currentTime = 50;
audio.play();
audio.load();
testing.expectEqual(true, audio.paused);
testing.expectEqual(0, audio.currentTime);
testing.expectEqual(0, audio.readyState);
}
</script>
<script id="attributes_autoplay">
{
const video = document.createElement('video');
testing.expectEqual(false, video.autoplay);
video.setAttribute('autoplay', '');
testing.expectEqual(true, video.autoplay);
video.autoplay = false;
testing.expectEqual(false, video.autoplay);
video.autoplay = true;
testing.expectEqual(true, video.autoplay);
}
</script>
<script id="attributes_controls">
{
const video = document.createElement('video');
testing.expectEqual(false, video.controls);
video.controls = true;
testing.expectEqual(true, video.controls);
video.controls = false;
testing.expectEqual(false, video.controls);
}
</script>
<script id="attributes_loop">
{
const audio = document.createElement('audio');
testing.expectEqual(false, audio.loop);
audio.loop = true;
testing.expectEqual(true, audio.loop);
}
</script>
<script id="attributes_preload">
{
const audio = document.createElement('audio');
testing.expectEqual('auto', audio.preload);
audio.preload = 'none';
testing.expectEqual('none', audio.preload);
audio.preload = 'metadata';
testing.expectEqual('metadata', audio.preload);
}
</script>
<script id="attributes_src">
{
const audio = document.createElement('audio');
testing.expectEqual('', audio.src);
audio.src = 'test.mp3';
testing.expectEqual('http://127.0.0.1:9582/src/browser/tests/element/html/test.mp3', audio.src);
}
</script>
<script id="video_poster">
{
const video = document.createElement('video');
testing.expectEqual('', video.poster);
video.poster = 'poster.jpg';
testing.expectEqual('http://127.0.0.1:9582/src/browser/tests/element/html/poster.jpg', video.poster);
}
</script>
<script id="video_dimensions">
{
const video = document.getElementById('video1');
testing.expectEqual(0, video.videoWidth);
testing.expectEqual(0, video.videoHeight);
}
</script>
<script id="constants">
{
const audio = document.getElementById('audio1');
testing.expectEqual(0, audio.NETWORK_EMPTY);
testing.expectEqual(1, audio.NETWORK_IDLE);
testing.expectEqual(2, audio.NETWORK_LOADING);
testing.expectEqual(3, audio.NETWORK_NO_SOURCE);
testing.expectEqual(0, audio.HAVE_NOTHING);
testing.expectEqual(1, audio.HAVE_METADATA);
testing.expectEqual(2, audio.HAVE_CURRENT_DATA);
testing.expectEqual(3, audio.HAVE_FUTURE_DATA);
testing.expectEqual(4, audio.HAVE_ENOUGH_DATA);
}
</script>
<script id="create_audio_element">
{
const audio = document.createElement('audio');
testing.expectEqual('[object HTMLAudioElement]', audio.toString());
testing.expectEqual(true, audio.paused);
}
</script>
<script id="create_video_element">
{
const video = document.createElement('video');
testing.expectEqual('[object HTMLVideoElement]', video.toString());
testing.expectEqual(true, video.paused);
}
</script>

View File

@@ -188,6 +188,11 @@ pub fn getTagNameLower(self: *const Element) []const u8 {
.input => "input",
.li => "li",
.link => "link",
.media => |m| switch (m._type) {
.audio => "audio",
.video => "video",
.generic => "media",
},
.meta => "meta",
.ol => "ol",
.option => "option",
@@ -236,6 +241,11 @@ pub fn getTagNameSpec(self: *const Element, buf: []u8) []const u8 {
.li => "LI",
.link => "LINK",
.meta => "META",
.media => |m| switch (m._type) {
.audio => "AUDIO",
.video => "VIDEO",
.generic => "MEDIA",
},
.ol => "OL",
.option => "OPTION",
.p => "P",
@@ -1077,6 +1087,11 @@ pub fn getTag(self: *const Element) Tag {
.ul => .ul,
.ol => .ol,
.generic => |g| g._tag,
.media => |m| switch (m._type) {
.audio => .audio,
.video => .video,
.generic => .media,
},
.script => .script,
.select => .select,
.slot => .slot,
@@ -1103,6 +1118,7 @@ pub fn getTag(self: *const Element) Tag {
pub const Tag = enum {
anchor,
audio,
b,
body,
br,
@@ -1137,6 +1153,7 @@ pub const Tag = enum {
link,
main,
meta,
media,
nav,
ol,
option,
@@ -1157,6 +1174,7 @@ pub const Tag = enum {
textarea,
title,
ul,
video,
unknown,
// If the tag is "unknown", we can't use the optimized tag matching, but

View File

@@ -43,6 +43,7 @@ pub const Image = @import("html/Image.zig");
pub const Input = @import("html/Input.zig");
pub const LI = @import("html/LI.zig");
pub const Link = @import("html/Link.zig");
pub const Media = @import("html/Media.zig");
pub const Meta = @import("html/Meta.zig");
pub const OL = @import("html/OL.zig");
pub const Option = @import("html/Option.zig");
@@ -89,6 +90,7 @@ pub const Type = union(enum) {
input: *Input,
li: *LI,
link: *Link,
media: *Media,
meta: *Meta,
ol: *OL,
option: *Option,
@@ -121,37 +123,42 @@ pub fn is(self: *HtmlElement, comptime T: type) ?*T {
pub fn className(self: *const HtmlElement) []const u8 {
return switch (self._type) {
.anchor => "[object HTMLAnchorElement]",
.div => "[object HTMLDivElement]",
.embed => "[object HTMLEmbedElement]",
.form => "[object HTMLFormElement]",
.p => "[object HTMLParagraphElement]",
.body => "[object HTMLBodyElement]",
.br => "[object HTMLBRElement]",
.button => "[object HTMLButtonElement]",
.custom => "[object CUSTOM-TODO]",
.data => "[object HTMLDataElement]",
.dialog => "[object HTMLDialogElement]",
.img => "[object HTMLImageElement]",
.iframe => "[object HTMLIFrameElement]",
.br => "[object HTMLBRElement]",
.button => "[object HTMLButtonElement]",
.heading => "[object HTMLHeadingElement]",
.li => "[object HTMLLIElement]",
.ul => "[object HTMLULElement]",
.ol => "[object HTMLOLElement]",
.div => "[object HTMLDivElement]",
.embed => "[object HTMLEmbedElement]",
.form => "[object HTMLFormElement]",
.generic => "[object HTMLElement]",
.head => "[object HTMLHeadElement]",
.heading => "[object HTMLHeadingElement]",
.hr => "[object HTMLHRElement]",
.html => "[object HTMLHtmlElement]",
.iframe => "[object HTMLIFrameElement]",
.img => "[object HTMLImageElement]",
.input => "[object HTMLInputElement]",
.li => "[object HTMLLIElement]",
.link => "[object HTMLLinkElement]",
.meta => "[object HTMLMetaElement]",
.media => |m| switch (m._type) {
.audio => "[object HTMLAudioElement]",
.video => "[object HTMLVideoElement]",
.generic => "[object HTMLMediaElement]",
},
.ol => "[object HTMLOLElement]",
.option => "[object HTMLOptionElement]",
.p => "[object HTMLParagraphElement]",
.script => "[object HTMLScriptElement]",
.select => "[object HTMLSelectElement]",
.slot => "[object HTMLSlotElement]",
.template => "[object HTMLTemplateElement]",
.option => "[object HTMLOptionElement]",
.text_area => "[object HTMLTextAreaElement]",
.input => "[object HTMLInputElement]",
.link => "[object HTMLLinkElement]",
.meta => "[object HTMLMetaElement]",
.hr => "[object HTMLHRElement]",
.style => "[object HTMLSyleElement]",
.template => "[object HTMLTemplateElement]",
.text_area => "[object HTMLTextAreaElement]",
.title => "[object HTMLTitleElement]",
.body => "[object HTMLBodyElement]",
.html => "[object HTMLHtmlElement]",
.head => "[object HTMLHeadElement]",
.ul => "[object HTMLULElement]",
.unknown => "[object HTMLUnknownElement]",
};
}

View File

@@ -0,0 +1,49 @@
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const js = @import("../../../js/js.zig");
const Node = @import("../../Node.zig");
const Element = @import("../../Element.zig");
const Media = @import("Media.zig");
pub const Audio = @This();
_proto: *Media,
pub fn asMedia(self: *Audio) *Media {
return self._proto;
}
pub fn asElement(self: *Audio) *Element {
return self._proto.asElement();
}
pub fn asNode(self: *Audio) *Node {
return self.asElement().asNode();
}
pub const JsApi = struct {
pub const bridge = js.Bridge(Audio);
pub const Meta = struct {
pub const name = "HTMLAudioElement";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
};
};

View File

@@ -0,0 +1,324 @@
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const js = @import("../../../js/js.zig");
const Page = @import("../../../Page.zig");
const Node = @import("../../Node.zig");
const Element = @import("../../Element.zig");
const HtmlElement = @import("../Html.zig");
pub const Audio = @import("Audio.zig");
pub const Video = @import("Video.zig");
const MediaError = @import("../../media/MediaError.zig");
const Media = @This();
pub const ReadyState = enum(u16) {
HAVE_NOTHING = 0,
HAVE_METADATA = 1,
HAVE_CURRENT_DATA = 2,
HAVE_FUTURE_DATA = 3,
HAVE_ENOUGH_DATA = 4,
};
pub const NetworkState = enum(u16) {
NETWORK_EMPTY = 0,
NETWORK_IDLE = 1,
NETWORK_LOADING = 2,
NETWORK_NO_SOURCE = 3,
};
pub const Type = union(enum) {
generic,
audio: *Audio,
video: *Video,
};
_type: Type,
_proto: *HtmlElement,
_paused: bool = true,
_current_time: f64 = 0,
_volume: f64 = 1.0,
_muted: bool = false,
_playback_rate: f64 = 1.0,
_ready_state: ReadyState = .HAVE_NOTHING,
_network_state: NetworkState = .NETWORK_EMPTY,
_error: ?*MediaError = null,
pub fn asElement(self: *Media) *Element {
return self._proto._proto;
}
pub fn asConstElement(self: *const Media) *const Element {
return self._proto._proto;
}
pub fn asNode(self: *Media) *Node {
return self.asElement().asNode();
}
pub fn is(self: *Media, comptime T: type) ?*T {
const type_name = @typeName(T);
switch (self._type) {
.audio => |a| {
if (T == *Audio) return a;
if (comptime std.mem.startsWith(u8, type_name, "browser.webapi.element.html.Audio")) {
return a;
}
},
.video => |v| {
if (T == *Video) return v;
if (comptime std.mem.startsWith(u8, type_name, "browser.webapi.element.html.Video")) {
return v;
}
},
.generic => {},
}
return null;
}
pub fn as(self: *Media, comptime T: type) *T {
return self.is(T).?;
}
pub fn canPlayType(_: *const Media, mime_type: []const u8, page: *Page) []const u8 {
const pos = std.mem.indexOfScalar(u8, mime_type, ';') orelse mime_type.len;
const base_type = std.mem.trim(u8, mime_type[0..pos], &std.ascii.whitespace);
if (base_type.len > page.buf.len) {
return "";
}
const lower = std.ascii.lowerString(&page.buf, base_type);
if (isProbablySupported(lower)) {
return "probably";
}
if (isMaybeSupported(lower)) {
return "maybe";
}
return "";
}
fn isProbablySupported(mime_type: []const u8) bool {
if (std.mem.eql(u8, mime_type, "video/mp4")) return true;
if (std.mem.eql(u8, mime_type, "video/webm")) return true;
if (std.mem.eql(u8, mime_type, "audio/mp4")) return true;
if (std.mem.eql(u8, mime_type, "audio/webm")) return true;
if (std.mem.eql(u8, mime_type, "audio/mpeg")) return true;
if (std.mem.eql(u8, mime_type, "audio/mp3")) return true;
if (std.mem.eql(u8, mime_type, "audio/ogg")) return true;
if (std.mem.eql(u8, mime_type, "video/ogg")) return true;
if (std.mem.eql(u8, mime_type, "audio/wav")) return true;
if (std.mem.eql(u8, mime_type, "audio/wave")) return true;
if (std.mem.eql(u8, mime_type, "audio/x-wav")) return true;
return false;
}
fn isMaybeSupported(mime_type: []const u8) bool {
if (std.mem.eql(u8, mime_type, "audio/aac")) return true;
if (std.mem.eql(u8, mime_type, "audio/x-m4a")) return true;
if (std.mem.eql(u8, mime_type, "video/x-m4v")) return true;
if (std.mem.eql(u8, mime_type, "audio/flac")) return true;
return false;
}
pub fn play(self: *Media) void {
self._paused = false;
self._ready_state = .HAVE_ENOUGH_DATA;
self._network_state = .NETWORK_IDLE;
// TODO: Could dispatch 'play' and 'playing' events
}
pub fn pause(self: *Media) void {
self._paused = true;
// TODO: Could dispatch 'pause' event
}
pub fn load(self: *Media) void {
self._paused = true;
self._current_time = 0;
self._ready_state = .HAVE_NOTHING;
self._network_state = .NETWORK_LOADING;
self._error = null;
// TODO: Could dispatch events
}
pub fn getPaused(self: *const Media) bool {
return self._paused;
}
pub fn getCurrentTime(self: *const Media) f64 {
return self._current_time;
}
pub fn getDuration(_: *const Media) f64 {
return std.math.nan(f64);
}
pub fn getReadyState(self: *const Media) u16 {
return @intFromEnum(self._ready_state);
}
pub fn getNetworkState(self: *const Media) u16 {
return @intFromEnum(self._network_state);
}
pub fn getEnded(_: *const Media) bool {
return false;
}
pub fn getSeeking(_: *const Media) bool {
return false;
}
pub fn getError(self: *const Media) ?*MediaError {
return self._error;
}
pub fn getVolume(self: *const Media) f64 {
return self._volume;
}
pub fn setVolume(self: *Media, value: f64) void {
self._volume = @max(0.0, @min(1.0, value));
}
pub fn getMuted(self: *const Media) bool {
return self._muted;
}
pub fn setMuted(self: *Media, value: bool) void {
self._muted = value;
}
pub fn getPlaybackRate(self: *const Media) f64 {
return self._playback_rate;
}
pub fn setPlaybackRate(self: *Media, value: f64) void {
self._playback_rate = value;
}
pub fn setCurrentTime(self: *Media, value: f64) void {
self._current_time = value;
}
pub fn getSrc(self: *const Media, page: *Page) ![]const u8 {
const element = self.asConstElement();
const src = element.getAttributeSafe("src") orelse return "";
if (src.len == 0) {
return "";
}
const URL = @import("../../URL.zig");
return URL.resolve(page.call_arena, page.url, src, .{});
}
pub fn setSrc(self: *Media, value: []const u8, page: *Page) !void {
try self.asElement().setAttributeSafe("src", value, page);
}
pub fn getAutoplay(self: *const Media) bool {
return self.asConstElement().getAttributeSafe("autoplay") != null;
}
pub fn setAutoplay(self: *Media, value: bool, page: *Page) !void {
if (value) {
try self.asElement().setAttributeSafe("autoplay", "", page);
} else {
try self.asElement().removeAttribute("autoplay", page);
}
}
pub fn getControls(self: *const Media) bool {
return self.asConstElement().getAttributeSafe("controls") != null;
}
pub fn setControls(self: *Media, value: bool, page: *Page) !void {
if (value) {
try self.asElement().setAttributeSafe("controls", "", page);
} else {
try self.asElement().removeAttribute("controls", page);
}
}
pub fn getLoop(self: *const Media) bool {
return self.asConstElement().getAttributeSafe("loop") != null;
}
pub fn setLoop(self: *Media, value: bool, page: *Page) !void {
if (value) {
try self.asElement().setAttributeSafe("loop", "", page);
} else {
try self.asElement().removeAttribute("loop", page);
}
}
pub fn getPreload(self: *const Media) []const u8 {
return self.asConstElement().getAttributeSafe("preload") orelse "auto";
}
pub fn setPreload(self: *Media, value: []const u8, page: *Page) !void {
try self.asElement().setAttributeSafe("preload", value, page);
}
pub const JsApi = struct {
pub const bridge = js.Bridge(Media);
pub const Meta = struct {
pub const name = "HTMLMediaElement";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
};
pub const NETWORK_EMPTY = bridge.property(@intFromEnum(NetworkState.NETWORK_EMPTY));
pub const NETWORK_IDLE = bridge.property(@intFromEnum(NetworkState.NETWORK_IDLE));
pub const NETWORK_LOADING = bridge.property(@intFromEnum(NetworkState.NETWORK_LOADING));
pub const NETWORK_NO_SOURCE = bridge.property(@intFromEnum(NetworkState.NETWORK_NO_SOURCE));
pub const HAVE_NOTHING = bridge.property(@intFromEnum(ReadyState.HAVE_NOTHING));
pub const HAVE_METADATA = bridge.property(@intFromEnum(ReadyState.HAVE_METADATA));
pub const HAVE_CURRENT_DATA = bridge.property(@intFromEnum(ReadyState.HAVE_CURRENT_DATA));
pub const HAVE_FUTURE_DATA = bridge.property(@intFromEnum(ReadyState.HAVE_FUTURE_DATA));
pub const HAVE_ENOUGH_DATA = bridge.property(@intFromEnum(ReadyState.HAVE_ENOUGH_DATA));
pub const src = bridge.accessor(Media.getSrc, Media.setSrc, .{});
pub const autoplay = bridge.accessor(Media.getAutoplay, Media.setAutoplay, .{});
pub const controls = bridge.accessor(Media.getControls, Media.setControls, .{});
pub const loop = bridge.accessor(Media.getLoop, Media.setLoop, .{});
pub const muted = bridge.accessor(Media.getMuted, Media.setMuted, .{});
pub const preload = bridge.accessor(Media.getPreload, Media.setPreload, .{});
pub const volume = bridge.accessor(Media.getVolume, Media.setVolume, .{});
pub const playbackRate = bridge.accessor(Media.getPlaybackRate, Media.setPlaybackRate, .{});
pub const currentTime = bridge.accessor(Media.getCurrentTime, Media.setCurrentTime, .{});
pub const duration = bridge.accessor(Media.getDuration, null, .{});
pub const paused = bridge.accessor(Media.getPaused, null, .{});
pub const ended = bridge.accessor(Media.getEnded, null, .{});
pub const seeking = bridge.accessor(Media.getSeeking, null, .{});
pub const readyState = bridge.accessor(Media.getReadyState, null, .{});
pub const networkState = bridge.accessor(Media.getNetworkState, null, .{});
pub const @"error" = bridge.accessor(Media.getError, null, .{});
pub const canPlayType = bridge.function(Media.canPlayType, .{});
pub const play = bridge.function(Media.play, .{});
pub const pause = bridge.function(Media.pause, .{});
pub const load = bridge.function(Media.load, .{});
};
const testing = @import("../../../../testing.zig");
test "WebApi: Media" {
try testing.htmlRunner("element/html/media.html", .{});
}

View File

@@ -0,0 +1,81 @@
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const js = @import("../../../js/js.zig");
const Page = @import("../../../Page.zig");
const Node = @import("../../Node.zig");
const Element = @import("../../Element.zig");
const Media = @import("Media.zig");
pub const Video = @This();
_proto: *Media,
pub fn asMedia(self: *Video) *Media {
return self._proto;
}
pub fn asElement(self: *Video) *Element {
return self._proto.asElement();
}
pub fn asConstElement(self: *const Video) *const Element {
return self._proto.asConstElement();
}
pub fn asNode(self: *Video) *Node {
return self.asElement().asNode();
}
pub fn getVideoWidth(_: *const Video) u32 {
return 0;
}
pub fn getVideoHeight(_: *const Video) u32 {
return 0;
}
pub fn getPoster(self: *const Video, page: *Page) ![]const u8 {
const element = self.asConstElement();
const poster = element.getAttributeSafe("poster") orelse return "";
if (poster.len == 0) {
return "";
}
const URL = @import("../../URL.zig");
return URL.resolve(page.call_arena, page.url, poster, .{});
}
pub fn setPoster(self: *Video, value: []const u8, page: *Page) !void {
try self.asElement().setAttributeSafe("poster", value, page);
}
pub const JsApi = struct {
pub const bridge = js.Bridge(Video);
pub const Meta = struct {
pub const name = "HTMLVideoElement";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
};
pub const poster = bridge.accessor(Video.getPoster, Video.setPoster, .{});
pub const videoWidth = bridge.accessor(Video.getVideoWidth, null, .{});
pub const videoHeight = bridge.accessor(Video.getVideoHeight, null, .{});
};