@gjsify/video 0.1.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,28 @@
1
+ import Gst from "gi://Gst?version=1.0";
2
+ let initialized = false;
3
+ function ensureGstInit() {
4
+ if (initialized) return;
5
+ Gst.init(null);
6
+ initialized = true;
7
+ }
8
+ function ensurePaintableSinkAvailable() {
9
+ ensureGstInit();
10
+ const factory = Gst.ElementFactory.find("gtk4paintablesink");
11
+ if (!factory) {
12
+ throwNotSupported(
13
+ 'GStreamer element "gtk4paintablesink" not available. Install gst-plugins-rs:\n Fedora: dnf install gstreamer1-plugin-gtk4\n Ubuntu/Debian: apt install gstreamer1.0-gtk4\n Verify with: gst-inspect-1.0 gtk4paintablesink'
14
+ );
15
+ }
16
+ }
17
+ function throwNotSupported(message) {
18
+ const DOMExceptionCtor = globalThis.DOMException;
19
+ if (DOMExceptionCtor) {
20
+ throw new DOMExceptionCtor(message, "NotSupportedError");
21
+ }
22
+ throw new Error(message);
23
+ }
24
+ export {
25
+ Gst,
26
+ ensureGstInit,
27
+ ensurePaintableSinkAvailable
28
+ };
@@ -0,0 +1,14 @@
1
+ import { VideoBridge } from "./video-bridge.js";
2
+ import { buildMediaStreamPipeline, buildUriPipeline, createPaintableSink } from "./pipeline-builder.js";
3
+ import { HTMLVideoElement } from "@gjsify/dom-elements";
4
+ Object.defineProperty(globalThis, "HTMLVideoElement", {
5
+ value: HTMLVideoElement,
6
+ writable: true,
7
+ configurable: true
8
+ });
9
+ export {
10
+ VideoBridge,
11
+ buildMediaStreamPipeline,
12
+ buildUriPipeline,
13
+ createPaintableSink
14
+ };
@@ -0,0 +1,87 @@
1
+ import { ensureGstInit, ensurePaintableSinkAvailable, Gst } from "./gst-init.js";
2
+ function createPaintableSink() {
3
+ ensurePaintableSinkAvailable();
4
+ const paintableSink = Gst.ElementFactory.make("gtk4paintablesink", "videosink");
5
+ if (!paintableSink) {
6
+ throw new Error("Failed to create gtk4paintablesink element");
7
+ }
8
+ const paintable = paintableSink.paintable;
9
+ if (!paintable) {
10
+ throw new Error("gtk4paintablesink has no paintable property");
11
+ }
12
+ let glSink = null;
13
+ const glContext = paintable.gl_context;
14
+ if (glContext) {
15
+ glSink = Gst.ElementFactory.make("glsinkbin", "glsink");
16
+ if (glSink) {
17
+ glSink.sink = paintableSink;
18
+ }
19
+ }
20
+ return {
21
+ sink: glSink ?? paintableSink,
22
+ paintable,
23
+ glSink
24
+ };
25
+ }
26
+ function buildMediaStreamPipeline(gstSource, gstPipeline) {
27
+ ensureGstInit();
28
+ const { sink, paintable } = createPaintableSink();
29
+ if (gstPipeline) {
30
+ gstPipeline.set_state(Gst.State.NULL);
31
+ gstPipeline.remove(gstSource);
32
+ }
33
+ const pipeline = new Gst.Pipeline({ name: "video-bridge-pipeline" });
34
+ const tee = Gst.ElementFactory.make("tee", "source-tee");
35
+ if (!tee) {
36
+ throw new Error("Failed to create tee element");
37
+ }
38
+ tee.allow_not_linked = true;
39
+ const queue = Gst.ElementFactory.make("queue", "preview-queue");
40
+ if (!queue) {
41
+ throw new Error("Failed to create queue element");
42
+ }
43
+ const videoconvert = Gst.ElementFactory.make("videoconvert", "convert");
44
+ if (!videoconvert) {
45
+ throw new Error("Failed to create videoconvert element");
46
+ }
47
+ pipeline.add(gstSource);
48
+ pipeline.add(tee);
49
+ pipeline.add(queue);
50
+ pipeline.add(videoconvert);
51
+ pipeline.add(sink);
52
+ if (!gstSource.link(tee)) {
53
+ throw new Error("Failed to link source \u2192 tee");
54
+ }
55
+ const teeSrcPad = tee.request_pad_simple ? tee.request_pad_simple("src_%u") : tee.get_request_pad("src_%u");
56
+ const queueSinkPad = queue.get_static_pad("sink");
57
+ if (teeSrcPad && queueSinkPad) {
58
+ teeSrcPad.link(queueSinkPad);
59
+ } else {
60
+ throw new Error("Failed to link tee \u2192 queue");
61
+ }
62
+ if (!queue.link(videoconvert)) {
63
+ throw new Error("Failed to link queue \u2192 videoconvert");
64
+ }
65
+ if (!videoconvert.link(sink)) {
66
+ throw new Error("Failed to link videoconvert \u2192 sink");
67
+ }
68
+ return { pipeline, paintable, tee };
69
+ }
70
+ function buildUriPipeline(uri) {
71
+ ensureGstInit();
72
+ const { sink, paintable } = createPaintableSink();
73
+ const playbin = Gst.ElementFactory.make("playbin", "playbin");
74
+ if (!playbin) {
75
+ throw new Error("Failed to create playbin element");
76
+ }
77
+ playbin.uri = uri;
78
+ playbin.video_sink = sink;
79
+ const pipeline = new Gst.Pipeline({ name: "video-bridge-uri-pipeline" });
80
+ pipeline.add(playbin);
81
+ return { pipeline, paintable };
82
+ }
83
+ export {
84
+ buildMediaStreamPipeline,
85
+ buildUriPipeline,
86
+ createPaintableSink
87
+ };
@@ -0,0 +1,174 @@
1
+ import GObject from "gi://GObject";
2
+ import GLib from "gi://GLib?version=2.0";
3
+ import Gtk from "gi://Gtk?version=4.0";
4
+ import { HTMLVideoElement } from "@gjsify/dom-elements";
5
+ import { attachEventControllers } from "@gjsify/event-bridge";
6
+ import { Event } from "@gjsify/dom-events";
7
+ import { BridgeEnvironment } from "@gjsify/bridge-types";
8
+ import { buildMediaStreamPipeline, buildUriPipeline } from "./pipeline-builder.js";
9
+ import { Gst } from "./gst-init.js";
10
+ const VideoBridge = GObject.registerClass(
11
+ { GTypeName: "GjsifyVideoBridge" },
12
+ class VideoBridge2 extends Gtk.Box {
13
+ constructor(params) {
14
+ super({
15
+ ...params,
16
+ orientation: Gtk.Orientation.VERTICAL
17
+ });
18
+ this._pipeline = null;
19
+ // Gst.Pipeline
20
+ this._readyCallbacks = [];
21
+ this._resizeCallbacks = [];
22
+ this._timeOrigin = GLib.get_monotonic_time();
23
+ this._ready = false;
24
+ this._picture = new Gtk.Picture();
25
+ this._picture.set_hexpand(true);
26
+ this._picture.set_vexpand(true);
27
+ this.append(this._picture);
28
+ this._video = new HTMLVideoElement();
29
+ const host = {
30
+ performanceNow: () => (GLib.get_monotonic_time() - this._timeOrigin) / 1e3,
31
+ getWidth: () => this.get_allocated_width(),
32
+ getHeight: () => this.get_allocated_height(),
33
+ getDevicePixelRatio: () => {
34
+ const display = this.get_display();
35
+ const surface = this.get_native()?.get_surface();
36
+ if (surface) return surface.get_scale_factor();
37
+ if (display) return display.get_scale?.() ?? 1;
38
+ return 1;
39
+ }
40
+ };
41
+ this._environment = new BridgeEnvironment(host);
42
+ this._environment.document.body.appendChild(this._video);
43
+ attachEventControllers(this, () => this._video);
44
+ this._video.addEventListener("srcobjectchange", () => this._onSrcObjectChange());
45
+ this._video.addEventListener("srcchange", () => this._onSrcChange());
46
+ this.connect("realize", () => {
47
+ if (this._ready) return;
48
+ this._ready = true;
49
+ for (const cb of this._readyCallbacks) {
50
+ cb(this._video);
51
+ }
52
+ this._readyCallbacks = [];
53
+ });
54
+ let lastWidth = 0;
55
+ let lastHeight = 0;
56
+ const checkResize = () => {
57
+ const width = this.get_allocated_width();
58
+ const height = this.get_allocated_height();
59
+ if (width !== lastWidth || height !== lastHeight) {
60
+ lastWidth = width;
61
+ lastHeight = height;
62
+ this._video.dispatchEvent(new Event("resize"));
63
+ for (const cb of this._resizeCallbacks) {
64
+ cb(width, height);
65
+ }
66
+ }
67
+ };
68
+ this.connect("notify::width-request", checkResize);
69
+ this.connect("notify::height-request", checkResize);
70
+ this.connect("map", checkResize);
71
+ this.connect("unrealize", () => {
72
+ this._destroyPipeline();
73
+ });
74
+ }
75
+ /** The HTMLVideoElement backing this bridge. */
76
+ get element() {
77
+ return this._video;
78
+ }
79
+ /** Alias for element — matches browser naming. */
80
+ get videoElement() {
81
+ return this._video;
82
+ }
83
+ /** The isolated browser environment for this bridge. */
84
+ get environment() {
85
+ return this._environment;
86
+ }
87
+ /**
88
+ * Register a callback to be invoked once the video element is ready.
89
+ * If already ready, the callback fires synchronously.
90
+ */
91
+ onReady(cb) {
92
+ if (this._ready) {
93
+ cb(this._video);
94
+ return;
95
+ }
96
+ this._readyCallbacks.push(cb);
97
+ }
98
+ /** Register a callback invoked whenever the widget is resized. */
99
+ onResize(cb) {
100
+ this._resizeCallbacks.push(cb);
101
+ }
102
+ /** Sets browser globals for video support. */
103
+ installGlobals() {
104
+ globalThis.HTMLVideoElement = HTMLVideoElement;
105
+ const timeOrigin = this._timeOrigin;
106
+ if (typeof globalThis.performance === "undefined") {
107
+ globalThis.performance = {
108
+ now: () => (GLib.get_monotonic_time() - timeOrigin) / 1e3,
109
+ timeOrigin: Date.now()
110
+ };
111
+ }
112
+ }
113
+ /** @internal Handle srcObject change (MediaStream) */
114
+ _onSrcObjectChange() {
115
+ this._destroyPipeline();
116
+ const stream = this._video.srcObject;
117
+ if (!stream) return;
118
+ const videoTracks = stream.getVideoTracks?.() ?? [];
119
+ const track = videoTracks.find((t) => t._gstSource != null);
120
+ if (!track?._gstSource) {
121
+ console.warn("VideoBridge: MediaStream has no video track with GStreamer source");
122
+ return;
123
+ }
124
+ try {
125
+ const { pipeline, paintable, tee } = buildMediaStreamPipeline(track._gstSource, track._gstPipeline);
126
+ this._pipeline = pipeline;
127
+ this._picture.set_paintable(paintable);
128
+ track._gstPipeline = pipeline;
129
+ track._gstTee = tee;
130
+ pipeline.set_state(Gst.State.PLAYING);
131
+ this._video.readyState = 4;
132
+ this._video.paused = false;
133
+ this._video.dispatchEvent(new Event("loadedmetadata"));
134
+ this._video.dispatchEvent(new Event("canplay"));
135
+ this._video.dispatchEvent(new Event("playing"));
136
+ } catch (err) {
137
+ console.error("VideoBridge: Failed to build MediaStream pipeline:", err);
138
+ }
139
+ }
140
+ /** @internal Handle src change (URI) */
141
+ _onSrcChange() {
142
+ this._destroyPipeline();
143
+ const src = this._video.src;
144
+ if (!src) return;
145
+ try {
146
+ const { pipeline, paintable } = buildUriPipeline(src);
147
+ this._pipeline = pipeline;
148
+ this._picture.set_paintable(paintable);
149
+ pipeline.set_state(Gst.State.PLAYING);
150
+ this._video.readyState = 4;
151
+ this._video.paused = false;
152
+ this._video.dispatchEvent(new Event("loadedmetadata"));
153
+ this._video.dispatchEvent(new Event("canplay"));
154
+ this._video.dispatchEvent(new Event("playing"));
155
+ } catch (err) {
156
+ console.error("VideoBridge: Failed to build URI pipeline:", err);
157
+ }
158
+ }
159
+ /** @internal Tear down the GStreamer pipeline */
160
+ _destroyPipeline() {
161
+ if (this._pipeline) {
162
+ try {
163
+ this._pipeline.set_state(Gst.State.NULL);
164
+ } catch {
165
+ }
166
+ this._pipeline = null;
167
+ }
168
+ this._picture.set_paintable(null);
169
+ }
170
+ }
171
+ );
172
+ export {
173
+ VideoBridge
174
+ };
@@ -0,0 +1,5 @@
1
+ import Gst from 'gi://Gst?version=1.0';
2
+ export declare function ensureGstInit(): void;
3
+ /** Throws if the gtk4paintablesink element is not registered. */
4
+ export declare function ensurePaintableSinkAvailable(): void;
5
+ export { Gst };
@@ -0,0 +1,2 @@
1
+ export { VideoBridge } from './video-bridge.js';
2
+ export { buildMediaStreamPipeline, buildUriPipeline, createPaintableSink } from './pipeline-builder.js';
@@ -0,0 +1,37 @@
1
+ import Gdk from 'gi://Gdk?version=4.0';
2
+ import { Gst } from './gst-init.js';
3
+ export interface PaintableSinkResult {
4
+ sink: Gst.Element;
5
+ paintable: Gdk.Paintable;
6
+ glSink: Gst.Element | null;
7
+ }
8
+ /**
9
+ * Create a gtk4paintablesink and extract its Gdk.Paintable.
10
+ * Optionally wraps in glsinkbin for GL-accelerated rendering
11
+ * (following the showtime pattern).
12
+ */
13
+ export declare function createPaintableSink(): PaintableSinkResult;
14
+ export interface MediaStreamPipelineResult {
15
+ pipeline: Gst.Pipeline;
16
+ paintable: Gdk.Paintable;
17
+ /** The tee element inserted after the source for fan-out to other consumers (e.g. WebRTC). Only present for MediaStream pipelines. */
18
+ tee?: Gst.Element;
19
+ }
20
+ /**
21
+ * Build a pipeline for rendering a MediaStream video track.
22
+ *
23
+ * Expects the track's _gstSource element (from getUserMedia) and the
24
+ * track's _gstPipeline. The source is removed from its original pipeline
25
+ * (created by getUserMedia) and re-parented into a new pipeline with:
26
+ * source → tee → queue → videoconvert → gtk4paintablesink
27
+ *
28
+ * The tee element allows other consumers (e.g. RTCPeerConnection.addTrack)
29
+ * to request additional branches without moving the source across pipelines.
30
+ */
31
+ export declare function buildMediaStreamPipeline(gstSource: any, gstPipeline: any): MediaStreamPipelineResult;
32
+ /**
33
+ * Build a pipeline for playing a URI (video.src = "file:///..." or URL).
34
+ *
35
+ * Uses GStreamer playbin with gtk4paintablesink as video-sink.
36
+ */
37
+ export declare function buildUriPipeline(uri: string): MediaStreamPipelineResult;