@gjsify/dom-elements 0.1.15 → 0.3.0

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,11 @@
1
+ const NS_PER_SECOND = 1e9;
2
+ function secondsToGstTime(seconds) {
3
+ return BigInt(Math.round(seconds * NS_PER_SECOND));
4
+ }
5
+ function gstTimeToSeconds(nanoseconds) {
6
+ return Number(nanoseconds) / NS_PER_SECOND;
7
+ }
8
+ export {
9
+ gstTimeToSeconds,
10
+ secondsToGstTime
11
+ };
@@ -1,24 +1,105 @@
1
1
  import { HTMLMediaElement } from "./html-media-element.js";
2
+ import { Event } from "@gjsify/dom-events";
2
3
  import * as PropertySymbol from "./property-symbol.js";
3
4
  import { NamespaceURI } from "./namespace-uri.js";
5
+ import { secondsToGstTime, gstTimeToSeconds } from "./gst-time.js";
6
+ const GST_STATE_PLAYING = 4;
7
+ const GST_STATE_PAUSED = 3;
8
+ const GST_FORMAT_TIME = 3;
9
+ const GST_SEEK_FLAG_FLUSH = 1;
10
+ const GST_SEEK_FLAG_KEY_UNIT = 4;
11
+ const GST_SEEK_TYPE_SET = 1;
12
+ const GST_SEEK_TYPE_NONE = 0;
4
13
  class HTMLVideoElement extends HTMLMediaElement {
5
14
  constructor() {
6
15
  super();
16
+ /** Set by VideoBridge after every pipeline swap. Null when no media is loaded. */
17
+ this._pipeline = null;
7
18
  this._videoWidth = 0;
8
19
  this._videoHeight = 0;
9
20
  this.poster = "";
10
21
  this[PropertySymbol.tagName] = "VIDEO";
11
22
  this[PropertySymbol.localName] = "video";
12
23
  this[PropertySymbol.namespaceURI] = NamespaceURI.html;
24
+ const self = this;
25
+ Object.defineProperty(this, "paused", {
26
+ get() {
27
+ if (!self._pipeline) return true;
28
+ const [, state] = self._pipeline.get_state(0n);
29
+ return state !== GST_STATE_PLAYING;
30
+ },
31
+ configurable: true,
32
+ enumerable: true
33
+ });
34
+ Object.defineProperty(this, "currentTime", {
35
+ get() {
36
+ if (!self._pipeline) return 0;
37
+ const [ok, pos] = self._pipeline.query_position(GST_FORMAT_TIME);
38
+ return ok ? gstTimeToSeconds(pos) : 0;
39
+ },
40
+ set(seconds) {
41
+ self._pipeline?.seek(
42
+ 1,
43
+ GST_FORMAT_TIME,
44
+ GST_SEEK_FLAG_FLUSH | GST_SEEK_FLAG_KEY_UNIT,
45
+ GST_SEEK_TYPE_SET,
46
+ secondsToGstTime(seconds),
47
+ GST_SEEK_TYPE_NONE,
48
+ -1n
49
+ );
50
+ },
51
+ configurable: true,
52
+ enumerable: true
53
+ });
54
+ Object.defineProperty(this, "duration", {
55
+ get() {
56
+ if (!self._pipeline) return NaN;
57
+ const [ok, dur] = self._pipeline.query_duration(GST_FORMAT_TIME);
58
+ return ok && Number(dur) > 0 ? gstTimeToSeconds(dur) : NaN;
59
+ },
60
+ configurable: true,
61
+ enumerable: true
62
+ });
63
+ Object.defineProperty(this, "volume", {
64
+ get() {
65
+ return self._playbin()?.volume ?? 1;
66
+ },
67
+ set(v) {
68
+ const pb = self._playbin();
69
+ if (pb) pb.volume = Math.max(0, Math.min(1, v));
70
+ },
71
+ configurable: true,
72
+ enumerable: true
73
+ });
74
+ Object.defineProperty(this, "muted", {
75
+ get() {
76
+ return self._playbin()?.mute ?? false;
77
+ },
78
+ set(v) {
79
+ const pb = self._playbin();
80
+ if (pb) pb.mute = v;
81
+ },
82
+ configurable: true,
83
+ enumerable: true
84
+ });
13
85
  }
14
- /** Intrinsic width of the video (set by the bridge when media metadata loads). */
86
+ async play() {
87
+ this._pipeline?.set_state(GST_STATE_PLAYING);
88
+ this.dispatchEvent(new Event("play"));
89
+ this.dispatchEvent(new Event("playing"));
90
+ }
91
+ pause() {
92
+ this._pipeline?.set_state(GST_STATE_PAUSED);
93
+ this.dispatchEvent(new Event("pause"));
94
+ }
95
+ /** Intrinsic width of the video (set by bridge when media metadata loads). */
15
96
  get videoWidth() {
16
97
  return this._videoWidth;
17
98
  }
18
99
  set videoWidth(value) {
19
100
  this._videoWidth = value;
20
101
  }
21
- /** Intrinsic height of the video (set by the bridge when media metadata loads). */
102
+ /** Intrinsic height of the video (set by bridge when media metadata loads). */
22
103
  get videoHeight() {
23
104
  return this._videoHeight;
24
105
  }
@@ -28,6 +109,9 @@ class HTMLVideoElement extends HTMLMediaElement {
28
109
  get [Symbol.toStringTag]() {
29
110
  return "HTMLVideoElement";
30
111
  }
112
+ _playbin() {
113
+ return this._pipeline?.get_by_name("playbin") ?? null;
114
+ }
31
115
  }
32
116
  export {
33
117
  HTMLVideoElement
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Convert seconds (number) to GStreamer nanoseconds (bigint).
3
+ * Rounds to the nearest nanosecond to avoid floating-point drift over
4
+ * repeated back-and-forth conversions.
5
+ */
6
+ export declare function secondsToGstTime(seconds: number): bigint;
7
+ /**
8
+ * Convert GStreamer nanoseconds to seconds (number).
9
+ * Accepts both `bigint` (the runtime type from GStreamer queries) and `number`
10
+ * (what the `@girs/gst-1.0` typings currently declare — a known GIR bug for
11
+ * `gint64` return values in `query_position` / `query_duration`).
12
+ */
13
+ export declare function gstTimeToSeconds(nanoseconds: bigint | number): number;
@@ -1,20 +1,29 @@
1
+ import type Gst from '@girs/gst-1.0';
1
2
  import { HTMLMediaElement } from './html-media-element.js';
2
3
  /**
3
4
  * HTML Video Element.
4
5
  *
5
- * Dispatches 'srcobjectchange' when srcObject is set (for bridge containers).
6
- * Dispatches 'srcchange' when src is set.
6
+ * Dispatches 'srcobjectchange' when srcObject is set and 'srcchange' when src is set —
7
+ * bridge containers listen for these to wire up / tear down their pipelines.
8
+ *
9
+ * When a GStreamer pipeline is attached via `_pipeline`, play/pause/seek/volume
10
+ * delegate to it. Without a pipeline the element behaves as a pure DOM stub.
7
11
  */
8
12
  export declare class HTMLVideoElement extends HTMLMediaElement {
13
+ /** Set by VideoBridge after every pipeline swap. Null when no media is loaded. */
14
+ _pipeline: Gst.Pipeline | null;
9
15
  private _videoWidth;
10
16
  private _videoHeight;
11
17
  poster: string;
12
18
  constructor();
13
- /** Intrinsic width of the video (set by the bridge when media metadata loads). */
19
+ play(): Promise<void>;
20
+ pause(): void;
21
+ /** Intrinsic width of the video (set by bridge when media metadata loads). */
14
22
  get videoWidth(): number;
15
23
  set videoWidth(value: number);
16
- /** Intrinsic height of the video (set by the bridge when media metadata loads). */
24
+ /** Intrinsic height of the video (set by bridge when media metadata loads). */
17
25
  get videoHeight(): number;
18
26
  set videoHeight(value: number);
19
27
  get [Symbol.toStringTag](): string;
28
+ private _playbin;
20
29
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/dom-elements",
3
- "version": "0.1.15",
3
+ "version": "0.3.0",
4
4
  "description": "DOM element hierarchy (Node, Element, HTMLElement, HTMLImageElement) for GJS",
5
5
  "type": "module",
6
6
  "module": "lib/esm/index.js",
@@ -51,8 +51,9 @@
51
51
  "build:types": "tsc",
52
52
  "build:test": "yarn build:test:gjs",
53
53
  "build:test:gjs": "gjsify build src/test.mts --app gjs --outfile test.gjs.mjs",
54
+ "build:test:browser": "gjsify build src/test.browser.mts --app browser --outfile dist/test.browser.mjs",
54
55
  "test": "yarn build:gjsify && yarn build:test && yarn test:gjs",
55
- "test:gjs": "gjs -m test.gjs.mjs"
56
+ "test:gjs": "gjsify run test.gjs.mjs"
56
57
  },
57
58
  "keywords": [
58
59
  "gjs",
@@ -61,18 +62,19 @@
61
62
  "node"
62
63
  ],
63
64
  "dependencies": {
64
- "@girs/gdkpixbuf-2.0": "^2.0.0-4.0.0-rc.3",
65
- "@girs/gjs": "^4.0.0-rc.3",
66
- "@girs/glib-2.0": "^2.88.0-4.0.0-rc.3",
67
- "@gjsify/abort-controller": "^0.1.15",
68
- "@gjsify/canvas2d-core": "^0.1.15",
69
- "@gjsify/dom-events": "^0.1.15",
70
- "@gjsify/fetch": "^0.1.15"
65
+ "@girs/gdkpixbuf-2.0": "^2.0.0-4.0.0-rc.9",
66
+ "@girs/gjs": "^4.0.0-rc.9",
67
+ "@girs/glib-2.0": "^2.88.0-4.0.0-rc.9",
68
+ "@gjsify/abort-controller": "^0.3.0",
69
+ "@gjsify/canvas2d-core": "^0.3.0",
70
+ "@gjsify/dom-events": "^0.3.0",
71
+ "@gjsify/fetch": "^0.3.0"
71
72
  },
72
73
  "devDependencies": {
73
- "@gjsify/cli": "^0.1.15",
74
- "@gjsify/unit": "^0.1.15",
74
+ "@girs/gst-1.0": "^1.28.1-4.0.0-rc.9",
75
+ "@gjsify/cli": "^0.3.0",
76
+ "@gjsify/unit": "^0.3.0",
75
77
  "@types/node": "^25.6.0",
76
- "typescript": "^6.0.2"
78
+ "typescript": "^6.0.3"
77
79
  }
78
80
  }
@@ -0,0 +1,26 @@
1
+ // Conversions between seconds (Web video API) and GStreamer's nanosecond
2
+ // `BigInt` timebase (used by `Gst.Format.TIME` throughout GStreamer). Lives
3
+ // in @gjsify/dom-elements so HTMLVideoElement/HTMLAudioElement can use it
4
+ // directly; @gjsify/video re-exports these for consumers of the bridge
5
+ // package. Kept as pure number math — no runtime Gst import required.
6
+
7
+ const NS_PER_SECOND = 1_000_000_000;
8
+
9
+ /**
10
+ * Convert seconds (number) to GStreamer nanoseconds (bigint).
11
+ * Rounds to the nearest nanosecond to avoid floating-point drift over
12
+ * repeated back-and-forth conversions.
13
+ */
14
+ export function secondsToGstTime(seconds: number): bigint {
15
+ return BigInt(Math.round(seconds * NS_PER_SECOND));
16
+ }
17
+
18
+ /**
19
+ * Convert GStreamer nanoseconds to seconds (number).
20
+ * Accepts both `bigint` (the runtime type from GStreamer queries) and `number`
21
+ * (what the `@girs/gst-1.0` typings currently declare — a known GIR bug for
22
+ * `gint64` return values in `query_position` / `query_duration`).
23
+ */
24
+ export function gstTimeToSeconds(nanoseconds: bigint | number): number {
25
+ return Number(nanoseconds) / NS_PER_SECOND;
26
+ }
@@ -1,21 +1,44 @@
1
- // HTMLVideoElement for GJS — video element stub.
1
+ // HTMLVideoElement for GJS — video element with optional GStreamer pipeline wiring.
2
2
  // Reference: https://developer.mozilla.org/en-US/docs/Web/API/HTMLVideoElement
3
- // Reference: refs/happy-dom/packages/happy-dom/src/nodes/html-video-element/HTMLVideoElement.ts
4
3
  //
5
- // Pure DOM class stores video dimensions and poster. The VideoBridge
6
- // listens for 'srcobjectchange' / 'srcchange' events and handles GStreamer.
4
+ // The attached pipeline is set by VideoBridge after each swap. Types come from
5
+ // a type-only Gst import so dom-elements has no runtime dependency on GStreamer
6
+ // (mirrors the HTMLImageElement / GdkPixbuf split). Numeric enum values are
7
+ // still used at runtime because pulling the Gst module eagerly would break the
8
+ // "dom-elements works without gst-init" contract.
7
9
 
10
+ import type Gst from '@girs/gst-1.0';
8
11
  import { HTMLMediaElement } from './html-media-element.js';
12
+ import { Event } from '@gjsify/dom-events';
9
13
  import * as PropertySymbol from './property-symbol.js';
10
14
  import { NamespaceURI } from './namespace-uri.js';
15
+ import { secondsToGstTime, gstTimeToSeconds } from './gst-time.js';
16
+
17
+ // Gst.State / Gst.Format / Gst.SeekFlags / Gst.SeekType numeric values, hardcoded
18
+ // to avoid a runtime `gi://Gst` import. Cross-checked against the GStreamer GIR.
19
+ const GST_STATE_PLAYING = 4;
20
+ const GST_STATE_PAUSED = 3;
21
+ const GST_FORMAT_TIME = 3;
22
+ const GST_SEEK_FLAG_FLUSH = 1;
23
+ const GST_SEEK_FLAG_KEY_UNIT = 4;
24
+ const GST_SEEK_TYPE_SET = 1;
25
+ const GST_SEEK_TYPE_NONE = 0;
26
+
27
+ type Playbin = Gst.Element & { volume?: number; mute?: boolean };
11
28
 
12
29
  /**
13
30
  * HTML Video Element.
14
31
  *
15
- * Dispatches 'srcobjectchange' when srcObject is set (for bridge containers).
16
- * Dispatches 'srcchange' when src is set.
32
+ * Dispatches 'srcobjectchange' when srcObject is set and 'srcchange' when src is set —
33
+ * bridge containers listen for these to wire up / tear down their pipelines.
34
+ *
35
+ * When a GStreamer pipeline is attached via `_pipeline`, play/pause/seek/volume
36
+ * delegate to it. Without a pipeline the element behaves as a pure DOM stub.
17
37
  */
18
38
  export class HTMLVideoElement extends HTMLMediaElement {
39
+ /** Set by VideoBridge after every pipeline swap. Null when no media is loaded. */
40
+ _pipeline: Gst.Pipeline | null = null;
41
+
19
42
  private _videoWidth = 0;
20
43
  private _videoHeight = 0;
21
44
  poster = '';
@@ -25,25 +48,96 @@ export class HTMLVideoElement extends HTMLMediaElement {
25
48
  this[PropertySymbol.tagName] = 'VIDEO';
26
49
  this[PropertySymbol.localName] = 'video';
27
50
  this[PropertySymbol.namespaceURI] = NamespaceURI.html;
28
- }
29
51
 
30
- /** Intrinsic width of the video (set by the bridge when media metadata loads). */
31
- get videoWidth(): number {
32
- return this._videoWidth;
33
- }
34
- set videoWidth(value: number) {
35
- this._videoWidth = value;
52
+ // HTMLMediaElement defines paused/currentTime/duration/volume/muted as plain
53
+ // fields. TypeScript forbids overriding a field with an accessor in the
54
+ // subclass, so we install GStreamer-backed descriptors via defineProperty.
55
+ const self = this;
56
+
57
+ Object.defineProperty(this, 'paused', {
58
+ get(): boolean {
59
+ if (!self._pipeline) return true;
60
+ const [, state] = self._pipeline.get_state(0n);
61
+ return state !== GST_STATE_PLAYING;
62
+ },
63
+ configurable: true,
64
+ enumerable: true,
65
+ });
66
+
67
+ Object.defineProperty(this, 'currentTime', {
68
+ get(): number {
69
+ if (!self._pipeline) return 0;
70
+ const [ok, pos] = self._pipeline.query_position(GST_FORMAT_TIME);
71
+ return ok ? gstTimeToSeconds(pos) : 0;
72
+ },
73
+ set(seconds: number) {
74
+ self._pipeline?.seek(
75
+ 1.0,
76
+ GST_FORMAT_TIME,
77
+ GST_SEEK_FLAG_FLUSH | GST_SEEK_FLAG_KEY_UNIT,
78
+ GST_SEEK_TYPE_SET,
79
+ secondsToGstTime(seconds),
80
+ GST_SEEK_TYPE_NONE,
81
+ -1n,
82
+ );
83
+ },
84
+ configurable: true,
85
+ enumerable: true,
86
+ });
87
+
88
+ Object.defineProperty(this, 'duration', {
89
+ get(): number {
90
+ if (!self._pipeline) return NaN;
91
+ const [ok, dur] = self._pipeline.query_duration(GST_FORMAT_TIME);
92
+ return ok && Number(dur) > 0 ? gstTimeToSeconds(dur) : NaN;
93
+ },
94
+ configurable: true,
95
+ enumerable: true,
96
+ });
97
+
98
+ Object.defineProperty(this, 'volume', {
99
+ get(): number { return self._playbin()?.volume ?? 1.0; },
100
+ set(v: number) {
101
+ const pb = self._playbin();
102
+ if (pb) pb.volume = Math.max(0, Math.min(1, v));
103
+ },
104
+ configurable: true,
105
+ enumerable: true,
106
+ });
107
+
108
+ Object.defineProperty(this, 'muted', {
109
+ get(): boolean { return self._playbin()?.mute ?? false; },
110
+ set(v: boolean) {
111
+ const pb = self._playbin();
112
+ if (pb) pb.mute = v;
113
+ },
114
+ configurable: true,
115
+ enumerable: true,
116
+ });
36
117
  }
37
118
 
38
- /** Intrinsic height of the video (set by the bridge when media metadata loads). */
39
- get videoHeight(): number {
40
- return this._videoHeight;
119
+ override async play(): Promise<void> {
120
+ this._pipeline?.set_state(GST_STATE_PLAYING);
121
+ this.dispatchEvent(new Event('play'));
122
+ this.dispatchEvent(new Event('playing'));
41
123
  }
42
- set videoHeight(value: number) {
43
- this._videoHeight = value;
124
+
125
+ override pause(): void {
126
+ this._pipeline?.set_state(GST_STATE_PAUSED);
127
+ this.dispatchEvent(new Event('pause'));
44
128
  }
45
129
 
46
- get [Symbol.toStringTag](): string {
47
- return 'HTMLVideoElement';
130
+ /** Intrinsic width of the video (set by bridge when media metadata loads). */
131
+ get videoWidth(): number { return this._videoWidth; }
132
+ set videoWidth(value: number) { this._videoWidth = value; }
133
+
134
+ /** Intrinsic height of the video (set by bridge when media metadata loads). */
135
+ get videoHeight(): number { return this._videoHeight; }
136
+ set videoHeight(value: number) { this._videoHeight = value; }
137
+
138
+ get [Symbol.toStringTag](): string { return 'HTMLVideoElement'; }
139
+
140
+ private _playbin(): Playbin | null {
141
+ return (this._pipeline?.get_by_name('playbin') as Playbin | null) ?? null;
48
142
  }
49
143
  }