mirror of
https://github.com/lightpanda-io/browser.git
synced 2025-12-14 15:28:57 +00:00
Media/Audio/Video elements
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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"),
|
||||
|
||||
249
src/browser/tests/element/html/media.html
Normal file
249
src/browser/tests/element/html/media.html
Normal 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>
|
||||
@@ -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
|
||||
|
||||
@@ -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]",
|
||||
};
|
||||
}
|
||||
|
||||
49
src/browser/webapi/element/html/Audio.zig
Normal file
49
src/browser/webapi/element/html/Audio.zig
Normal 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;
|
||||
};
|
||||
};
|
||||
324
src/browser/webapi/element/html/Media.zig
Normal file
324
src/browser/webapi/element/html/Media.zig
Normal 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", .{});
|
||||
}
|
||||
81
src/browser/webapi/element/html/Video.zig
Normal file
81
src/browser/webapi/element/html/Video.zig
Normal 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, .{});
|
||||
};
|
||||
Reference in New Issue
Block a user