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, .{}); +};