diff --git a/src/browser/Factory.zig b/src/browser/Factory.zig
index 69cf0f52..6a0de803 100644
--- a/src/browser/Factory.zig
+++ b/src/browser/Factory.zig
@@ -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);
diff --git a/src/browser/Page.zig b/src/browser/Page.zig
index aa4cd9e1..c05fcb14 100644
--- a/src/browser/Page.zig
+++ b/src/browser/Page.zig
@@ -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();
diff --git a/src/browser/js/bridge.zig b/src/browser/js/bridge.zig
index b79622d0..4059d6a8 100644
--- a/src/browser/js/bridge.zig
+++ b/src/browser/js/bridge.zig
@@ -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"),
diff --git a/src/browser/tests/element/html/media.html b/src/browser/tests/element/html/media.html
new file mode 100644
index 00000000..cb6c1523
--- /dev/null
+++ b/src/browser/tests/element/html/media.html
@@ -0,0 +1,249 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/browser/webapi/Element.zig b/src/browser/webapi/Element.zig
index fd5cd75c..7de5f14f 100644
--- a/src/browser/webapi/Element.zig
+++ b/src/browser/webapi/Element.zig
@@ -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
diff --git a/src/browser/webapi/element/Html.zig b/src/browser/webapi/element/Html.zig
index e0220504..8016c8be 100644
--- a/src/browser/webapi/element/Html.zig
+++ b/src/browser/webapi/element/Html.zig
@@ -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]",
};
}
diff --git a/src/browser/webapi/element/html/Audio.zig b/src/browser/webapi/element/html/Audio.zig
new file mode 100644
index 00000000..929d6aca
--- /dev/null
+++ b/src/browser/webapi/element/html/Audio.zig
@@ -0,0 +1,49 @@
+// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
+//
+// Francis Bouvier
+// Pierre Tachoire
+//
+// 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 .
+
+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;
+ };
+};
diff --git a/src/browser/webapi/element/html/Media.zig b/src/browser/webapi/element/html/Media.zig
new file mode 100644
index 00000000..dc29e160
--- /dev/null
+++ b/src/browser/webapi/element/html/Media.zig
@@ -0,0 +1,324 @@
+// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
+//
+// Francis Bouvier
+// Pierre Tachoire
+//
+// 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 .
+
+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", .{});
+}
diff --git a/src/browser/webapi/element/html/Video.zig b/src/browser/webapi/element/html/Video.zig
new file mode 100644
index 00000000..66eb3f77
--- /dev/null
+++ b/src/browser/webapi/element/html/Video.zig
@@ -0,0 +1,81 @@
+// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
+//
+// Francis Bouvier
+// Pierre Tachoire
+//
+// 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 .
+
+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, .{});
+};