@tensordoc/prism 0.1.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.
- package/LICENSE +21 -0
- package/README.md +175 -0
- package/dist/backends/milkdrop.d.ts +28 -0
- package/dist/backends/shadertoy.d.ts +22 -0
- package/dist/image-feed.d.ts +32 -0
- package/dist/image-overlay.d.ts +84 -0
- package/dist/index.d.ts +9 -0
- package/dist/player.d.ts +150 -0
- package/dist/prism.cjs +37 -0
- package/dist/prism.cjs.map +1 -0
- package/dist/prism.mjs +1271 -0
- package/dist/prism.mjs.map +1 -0
- package/dist/registry.d.ts +18 -0
- package/dist/runtime.d.ts +25 -0
- package/dist/synth.d.ts +47 -0
- package/dist/types.d.ts +28 -0
- package/package.json +58 -0
- package/src/backends/milkdrop.ts +190 -0
- package/src/backends/shadertoy.ts +290 -0
- package/src/image-feed.ts +185 -0
- package/src/image-overlay.ts +302 -0
- package/src/index.ts +27 -0
- package/src/peer-types.d.ts +18 -0
- package/src/player.ts +395 -0
- package/src/registry.generated.json +2022 -0
- package/src/registry.ts +70 -0
- package/src/runtime.ts +100 -0
- package/src/synth.ts +251 -0
- package/src/types.ts +98 -0
package/src/player.ts
ADDED
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
// player.ts — public PrismPlayer class.
|
|
2
|
+
//
|
|
3
|
+
// Owns a small DOM tree (two stacked <canvas> elements inside `container`),
|
|
4
|
+
// an AudioContext, a SyntheticSignal (the default audio driver), the two
|
|
5
|
+
// rendering backends, and a GraphRuntime that swaps between them.
|
|
6
|
+
//
|
|
7
|
+
// Minimal usage:
|
|
8
|
+
//
|
|
9
|
+
// const player = new PrismPlayer({ container: el, graph });
|
|
10
|
+
//
|
|
11
|
+
// The caller can override the audio source any time via connectAudio(),
|
|
12
|
+
// swap graphs via load(), or pipe a live texture into iChannel1 via
|
|
13
|
+
// setLiveSource(). Backends + runtime + synth + audioCtx are exposed
|
|
14
|
+
// readonly so embedders can drive them directly (read presetName, fire
|
|
15
|
+
// pulseBeat from an external trigger, etc.) without re-creating the API.
|
|
16
|
+
|
|
17
|
+
import { createMilkdropBackground, type MilkdropBg } from "./backends/milkdrop";
|
|
18
|
+
import { createShadertoyBackground, type ShadertoyBg } from "./backends/shadertoy";
|
|
19
|
+
import { HeadlessSlideshow } from "./image-feed";
|
|
20
|
+
import { shortIdToGraph } from "./registry";
|
|
21
|
+
import { GraphRuntime, type ApplyResult } from "./runtime";
|
|
22
|
+
import { SyntheticSignal } from "./synth";
|
|
23
|
+
import type { PrismGraph } from "./types";
|
|
24
|
+
|
|
25
|
+
/** Anything `load()` and the `graph` option accept. A string is treated
|
|
26
|
+
* as a catalog short_id (6-char base62) and resolved against the
|
|
27
|
+
* bundled registry. */
|
|
28
|
+
export type GraphInput = PrismGraph | string;
|
|
29
|
+
|
|
30
|
+
/** Anything the `image` option / `connectImage()` accepts. Strings
|
|
31
|
+
* beyond the "webcam" / "tab" sentinels are treated as static URLs. */
|
|
32
|
+
export type ImageSource =
|
|
33
|
+
| "webcam"
|
|
34
|
+
| "tab"
|
|
35
|
+
| string
|
|
36
|
+
| string[]
|
|
37
|
+
| HTMLCanvasElement
|
|
38
|
+
| HTMLVideoElement
|
|
39
|
+
| MediaStream;
|
|
40
|
+
|
|
41
|
+
export interface PrismPlayerOptions {
|
|
42
|
+
/** Where to mount the visualization canvases. Pass either an element
|
|
43
|
+
* (preferred from frameworks — React refs, Vue template refs, etc.)
|
|
44
|
+
* or a DOM id string for hand-written HTML embeds. The player
|
|
45
|
+
* creates its own canvases inside; it does not touch any children
|
|
46
|
+
* that already exist there. */
|
|
47
|
+
container: HTMLElement | string;
|
|
48
|
+
/** Optional initial graph. Pass a PrismGraph object directly, or a
|
|
49
|
+
* 6-char short_id string (e.g. "7Hq3pK") to look up an entry from
|
|
50
|
+
* the bundled catalog. Without it, cold-opens on a curated milkdrop
|
|
51
|
+
* preset; you can call load() later. */
|
|
52
|
+
graph?: GraphInput;
|
|
53
|
+
/** Audio source. An AudioNode is connected directly; "mic" and "tab"
|
|
54
|
+
* request the matching media stream via getUserMedia /
|
|
55
|
+
* getDisplayMedia and connect that. Undefined → the built-in
|
|
56
|
+
* SyntheticSignal drives both backends. */
|
|
57
|
+
audio?: "mic" | "tab" | MediaStream | AudioNode;
|
|
58
|
+
/** Bring your own AudioContext (useful when the embedder already has
|
|
59
|
+
* one; web pages can only have a small number of them). */
|
|
60
|
+
audioCtx?: AudioContext;
|
|
61
|
+
/** iChannel1 fallback used by shader backends until something more
|
|
62
|
+
* specific is bound by the graph or setLiveSource(). */
|
|
63
|
+
defaultImage?: string;
|
|
64
|
+
/** Image feed for shader backends (bound to iChannel1). Accepts:
|
|
65
|
+
* - A single URL → static image
|
|
66
|
+
* - An array of URLs → built-in crossfading slideshow
|
|
67
|
+
* - "webcam" / "tab" → getUserMedia / getDisplayMedia
|
|
68
|
+
* - a MediaStream / HTMLVideoElement → live video
|
|
69
|
+
* - an HTMLCanvasElement → live canvas (e.g. your own
|
|
70
|
+
* renderer; the player just
|
|
71
|
+
* re-uploads its contents every
|
|
72
|
+
* frame)
|
|
73
|
+
* Use this for "I want pictures showing through the shader" without
|
|
74
|
+
* thinking about iChannel1 or setLiveSource. */
|
|
75
|
+
image?: ImageSource;
|
|
76
|
+
/** Seconds each image is held on-screen before the crossfade to the
|
|
77
|
+
* next begins. Only used when `image` is a URL array. Defaults to 6. */
|
|
78
|
+
holdSeconds?: number;
|
|
79
|
+
/** Milkdrop preset to load on cold-open. Falls back to a random preset
|
|
80
|
+
* from the bundled library if the name isn't found. */
|
|
81
|
+
initialPresetName?: string;
|
|
82
|
+
/** Called once the first frame has painted. */
|
|
83
|
+
onReady?: () => void;
|
|
84
|
+
/** Async failures (audio capture rejection, graph load errors that
|
|
85
|
+
* cannot be surfaced from a synchronous call) land here. */
|
|
86
|
+
onError?: (err: Error) => void;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export class PrismPlayer {
|
|
90
|
+
/** AudioContext driving both backends + the default synthetic signal.
|
|
91
|
+
* Created on construction unless one was passed in `opts.audioCtx`. */
|
|
92
|
+
readonly audioCtx: AudioContext;
|
|
93
|
+
/** Default audio driver: a silent "pink-noise-ish" pad that keeps the
|
|
94
|
+
* visualization animated when no real audio is connected. Kept alive
|
|
95
|
+
* for the lifetime of the player — call `player.synth.stop()` if you
|
|
96
|
+
* want to free it after switching to a real audio source. */
|
|
97
|
+
readonly synth: SyntheticSignal;
|
|
98
|
+
/** Milkdrop (butterchurn) backend handle. Cold-opens on a random
|
|
99
|
+
* preset (or `initialPresetName` if you passed one). */
|
|
100
|
+
readonly milkdrop: MilkdropBg;
|
|
101
|
+
/** Shadertoy backend handle. Idle until a graph with `lf.shadertoy` is
|
|
102
|
+
* loaded (or you call `shadertoy.loadFromUrl` directly). */
|
|
103
|
+
readonly shadertoy: ShadertoyBg;
|
|
104
|
+
/** Graph executor — dispatches to the right backend based on the
|
|
105
|
+
* graph's light-field generator node. */
|
|
106
|
+
readonly runtime: GraphRuntime;
|
|
107
|
+
/** Tracks which backend's canvas is currently visible. */
|
|
108
|
+
activeBackend: "milkdrop" | "shadertoy" = "milkdrop";
|
|
109
|
+
|
|
110
|
+
private readonly milkdropCanvas: HTMLCanvasElement;
|
|
111
|
+
private readonly shadertoyCanvas: HTMLCanvasElement;
|
|
112
|
+
private readonly ownsAudioCtx: boolean;
|
|
113
|
+
/** Default hold per image when `image` is a URL list; set in ctor. */
|
|
114
|
+
private readonly defaultHoldSeconds: number;
|
|
115
|
+
/** Owned media resources (MediaStreams we started, <video> elements
|
|
116
|
+
* we created internally, headless slideshow). Cleaned up by
|
|
117
|
+
* connectImage() before re-binding and by destroy() on teardown. */
|
|
118
|
+
private currentSlideshow: HeadlessSlideshow | null = null;
|
|
119
|
+
private currentVideo: HTMLVideoElement | null = null;
|
|
120
|
+
private currentStream: MediaStream | null = null;
|
|
121
|
+
/** Audio MediaStream the player started itself ("mic" / "tab"). Held
|
|
122
|
+
* so disconnectAudio() can stop the tracks. Null if the caller
|
|
123
|
+
* passed in their own stream/node (we never stop tracks we don't own). */
|
|
124
|
+
private currentAudioOwnedStream: MediaStream | null = null;
|
|
125
|
+
|
|
126
|
+
constructor(opts: PrismPlayerOptions) {
|
|
127
|
+
const container = resolveContainer(opts.container);
|
|
128
|
+
|
|
129
|
+
this.audioCtx = opts.audioCtx ?? new AudioContext();
|
|
130
|
+
this.ownsAudioCtx = opts.audioCtx === undefined;
|
|
131
|
+
this.defaultHoldSeconds = opts.holdSeconds ?? 6;
|
|
132
|
+
this.synth = new SyntheticSignal(this.audioCtx);
|
|
133
|
+
|
|
134
|
+
// Two stacked canvases — opacity-crossfaded by setActiveBackend.
|
|
135
|
+
// Match the legacy id/class pair so existing CSS (cold-open animation,
|
|
136
|
+
// bg-canvas--hidden/--active modifiers) still applies on prism.scott.ai.
|
|
137
|
+
this.milkdropCanvas = createCanvas("milkdrop");
|
|
138
|
+
this.shadertoyCanvas = createCanvas("shadertoy");
|
|
139
|
+
this.shadertoyCanvas.classList.add("bg-canvas--hidden");
|
|
140
|
+
container.appendChild(this.milkdropCanvas);
|
|
141
|
+
container.appendChild(this.shadertoyCanvas);
|
|
142
|
+
|
|
143
|
+
this.milkdrop = createMilkdropBackground(
|
|
144
|
+
this.audioCtx,
|
|
145
|
+
this.milkdropCanvas,
|
|
146
|
+
this.synth.getOutput(),
|
|
147
|
+
opts.onReady,
|
|
148
|
+
{ initialPresetName: opts.initialPresetName },
|
|
149
|
+
);
|
|
150
|
+
this.shadertoy = createShadertoyBackground(
|
|
151
|
+
this.audioCtx,
|
|
152
|
+
this.shadertoyCanvas,
|
|
153
|
+
this.synth.getOutput(),
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
if (opts.defaultImage) {
|
|
157
|
+
void this.shadertoy.bindImage(opts.defaultImage).catch((err: Error) => {
|
|
158
|
+
opts.onError?.(err);
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
this.runtime = new GraphRuntime({
|
|
163
|
+
milkdrop: this.milkdrop,
|
|
164
|
+
shadertoy: this.shadertoy,
|
|
165
|
+
setActiveBackend: (which) => this.setActiveBackend(which),
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
if (opts.graph !== undefined) {
|
|
169
|
+
const result = this.load(opts.graph);
|
|
170
|
+
if (!result.ok && opts.onError) {
|
|
171
|
+
opts.onError(new Error(result.error ?? "graph apply failed"));
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (opts.audio !== undefined) {
|
|
176
|
+
void this.connectAudio(opts.audio).catch((err: Error) => opts.onError?.(err));
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (opts.image !== undefined) {
|
|
180
|
+
void this.connectImage(opts.image).catch((err: Error) => opts.onError?.(err));
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Swap to a new graph. Accepts either a full PrismGraph object or a
|
|
185
|
+
* 6-char short_id string from the bundled registry. Returns the
|
|
186
|
+
* ApplyResult the runtime emits so the caller can react to backend
|
|
187
|
+
* switches / errors. An unknown short_id returns
|
|
188
|
+
* `{ ok: false, error: "unknown short_id ..." }` without touching
|
|
189
|
+
* the running visualization. */
|
|
190
|
+
load(graph: GraphInput, blendSeconds?: number): ApplyResult {
|
|
191
|
+
if (typeof graph === "string") {
|
|
192
|
+
const resolved = shortIdToGraph(graph);
|
|
193
|
+
if (!resolved) {
|
|
194
|
+
return { ok: false, error: `unknown short_id: ${graph}` };
|
|
195
|
+
}
|
|
196
|
+
return this.runtime.apply(resolved, blendSeconds);
|
|
197
|
+
}
|
|
198
|
+
return this.runtime.apply(graph, blendSeconds);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Connect a new audio source. Accepts:
|
|
202
|
+
* - "mic" — getUserMedia({ audio: true })
|
|
203
|
+
* - "tab" — getDisplayMedia({ audio: true })
|
|
204
|
+
* - MediaStream — wrapped in a MediaStreamAudioSourceNode
|
|
205
|
+
* - AudioNode — connected directly
|
|
206
|
+
* Routes the source to both backends so whichever one is active sees
|
|
207
|
+
* reactivity. Tracks any MediaStream the player started itself so
|
|
208
|
+
* disconnectAudio() can stop the tracks (mic indicator off, etc.). */
|
|
209
|
+
async connectAudio(source: "mic" | "tab" | MediaStream | AudioNode): Promise<AudioNode> {
|
|
210
|
+
const { node, owned } = await this.resolveAudioNode(source);
|
|
211
|
+
this.milkdrop.connectAudio(node);
|
|
212
|
+
this.shadertoy.connectAudio(node);
|
|
213
|
+
this.currentAudioOwnedStream = owned;
|
|
214
|
+
return node;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** Disconnect the current audio source and revert both backends to
|
|
218
|
+
* the built-in SyntheticSignal. Stops any MediaStream tracks the
|
|
219
|
+
* player started itself (mic / tab capture indicator goes off).
|
|
220
|
+
* Streams or AudioNodes the caller passed in are left untouched —
|
|
221
|
+
* the caller owns their lifetime. Idempotent. */
|
|
222
|
+
disconnectAudio(): void {
|
|
223
|
+
if (this.currentAudioOwnedStream) {
|
|
224
|
+
for (const track of this.currentAudioOwnedStream.getTracks()) track.stop();
|
|
225
|
+
this.currentAudioOwnedStream = null;
|
|
226
|
+
}
|
|
227
|
+
const fallback = this.synth.getOutput();
|
|
228
|
+
this.milkdrop.connectAudio(fallback);
|
|
229
|
+
this.shadertoy.connectAudio(fallback);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** Pipe a live source (e.g. a slideshow's canvas) into iChannel1 — its
|
|
233
|
+
* contents are re-uploaded each frame. Pass null to disable.
|
|
234
|
+
* Most embedders should prefer connectImage(), which handles
|
|
235
|
+
* webcam/tab/slideshow plumbing for you. */
|
|
236
|
+
setLiveSource(source: HTMLCanvasElement | HTMLVideoElement | null): void {
|
|
237
|
+
this.shadertoy.setLiveSource(source);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** Connect an image source for the shader's iChannel1. Symmetric to
|
|
241
|
+
* connectAudio. Replaces any prior image feed; old MediaStream
|
|
242
|
+
* tracks are stopped and internal slideshow timers are cleared.
|
|
243
|
+
* See ImageSource for the accepted shapes. */
|
|
244
|
+
async connectImage(source: ImageSource): Promise<void> {
|
|
245
|
+
this.teardownImageFeed();
|
|
246
|
+
if (typeof source === "string") {
|
|
247
|
+
if (source === "webcam") {
|
|
248
|
+
const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: false });
|
|
249
|
+
this.bindStream(stream);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
if (source === "tab") {
|
|
253
|
+
const stream = await navigator.mediaDevices.getDisplayMedia({ video: true, audio: false });
|
|
254
|
+
this.bindStream(stream);
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
// Treat any other string as a static image URL.
|
|
258
|
+
this.setLiveSource(null);
|
|
259
|
+
await this.shadertoy.bindImage(source);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
if (Array.isArray(source)) {
|
|
263
|
+
const slideshow = new HeadlessSlideshow(source, { holdSeconds: this.defaultHoldSeconds });
|
|
264
|
+
this.currentSlideshow = slideshow;
|
|
265
|
+
this.setLiveSource(slideshow.canvas);
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
if (source instanceof MediaStream) {
|
|
269
|
+
this.bindStream(source);
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
// Canvas or video element passed in directly — wire it as the
|
|
273
|
+
// live source without owning it (caller manages its lifetime).
|
|
274
|
+
this.setLiveSource(source);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** Stop the current image feed and unbind from the shader. Releases
|
|
278
|
+
* resources the player owned: slideshow timer cleared, MediaStream
|
|
279
|
+
* tracks stopped (webcam light off), internal <video> detached.
|
|
280
|
+
* Canvas/video elements the caller passed in are left untouched.
|
|
281
|
+
* After this, the shader reverts to whatever was last bound via
|
|
282
|
+
* `defaultImage` or `shadertoy.bindImage()` (or the 1×1 placeholder).
|
|
283
|
+
* Idempotent. */
|
|
284
|
+
disconnectImage(): void {
|
|
285
|
+
this.teardownImageFeed();
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** Toggle which backend's canvas is visible. Called by GraphRuntime;
|
|
289
|
+
* callers can flip it manually too (rare). */
|
|
290
|
+
setActiveBackend(which: "milkdrop" | "shadertoy"): void {
|
|
291
|
+
this.activeBackend = which;
|
|
292
|
+
if (which === "milkdrop") {
|
|
293
|
+
this.milkdropCanvas.classList.remove("bg-canvas--hidden");
|
|
294
|
+
this.milkdropCanvas.classList.add("bg-canvas--active");
|
|
295
|
+
this.shadertoyCanvas.classList.add("bg-canvas--hidden");
|
|
296
|
+
this.shadertoyCanvas.classList.remove("bg-canvas--active");
|
|
297
|
+
} else {
|
|
298
|
+
this.shadertoyCanvas.classList.remove("bg-canvas--hidden");
|
|
299
|
+
this.shadertoyCanvas.classList.add("bg-canvas--active");
|
|
300
|
+
this.milkdropCanvas.classList.add("bg-canvas--hidden");
|
|
301
|
+
this.milkdropCanvas.classList.remove("bg-canvas--active");
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** Stop animation, free GL resources, detach canvases, close the
|
|
306
|
+
* AudioContext (only if the player created it itself). */
|
|
307
|
+
destroy(): void {
|
|
308
|
+
this.teardownImageFeed();
|
|
309
|
+
if (this.currentAudioOwnedStream) {
|
|
310
|
+
for (const track of this.currentAudioOwnedStream.getTracks()) track.stop();
|
|
311
|
+
this.currentAudioOwnedStream = null;
|
|
312
|
+
}
|
|
313
|
+
this.milkdrop.destroy();
|
|
314
|
+
this.shadertoy.destroy();
|
|
315
|
+
this.synth.stop();
|
|
316
|
+
this.milkdropCanvas.remove();
|
|
317
|
+
this.shadertoyCanvas.remove();
|
|
318
|
+
if (this.ownsAudioCtx) {
|
|
319
|
+
void this.audioCtx.close();
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/** Wrap a MediaStream in an autoplaying <video> and bind that as the
|
|
324
|
+
* live source. The video element is held internally; teardown is
|
|
325
|
+
* handled by teardownImageFeed(). */
|
|
326
|
+
private bindStream(stream: MediaStream): void {
|
|
327
|
+
const video = document.createElement("video");
|
|
328
|
+
video.srcObject = stream;
|
|
329
|
+
video.autoplay = true;
|
|
330
|
+
video.muted = true;
|
|
331
|
+
video.playsInline = true;
|
|
332
|
+
// Auto-play may reject (Safari, autoplay policy); we ignore — the
|
|
333
|
+
// user gesture that opened the stream usually satisfies the policy.
|
|
334
|
+
void video.play().catch(() => undefined);
|
|
335
|
+
this.currentVideo = video;
|
|
336
|
+
this.currentStream = stream;
|
|
337
|
+
this.setLiveSource(video);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/** Stop the current image feed (slideshow timer, MediaStream tracks,
|
|
341
|
+
* internal <video>) and unbind from the shader. Idempotent. */
|
|
342
|
+
private teardownImageFeed(): void {
|
|
343
|
+
if (this.currentSlideshow) {
|
|
344
|
+
this.currentSlideshow.destroy();
|
|
345
|
+
this.currentSlideshow = null;
|
|
346
|
+
}
|
|
347
|
+
if (this.currentStream) {
|
|
348
|
+
for (const track of this.currentStream.getTracks()) track.stop();
|
|
349
|
+
this.currentStream = null;
|
|
350
|
+
}
|
|
351
|
+
if (this.currentVideo) {
|
|
352
|
+
this.currentVideo.srcObject = null;
|
|
353
|
+
this.currentVideo = null;
|
|
354
|
+
}
|
|
355
|
+
this.setLiveSource(null);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
private async resolveAudioNode(
|
|
359
|
+
source: "mic" | "tab" | MediaStream | AudioNode,
|
|
360
|
+
): Promise<{ node: AudioNode; owned: MediaStream | null }> {
|
|
361
|
+
if (source instanceof AudioNode) return { node: source, owned: null };
|
|
362
|
+
let stream: MediaStream;
|
|
363
|
+
let owned = false;
|
|
364
|
+
if (source instanceof MediaStream) {
|
|
365
|
+
stream = source;
|
|
366
|
+
} else if (source === "mic") {
|
|
367
|
+
stream = await navigator.mediaDevices.getUserMedia({ audio: true, video: false });
|
|
368
|
+
owned = true;
|
|
369
|
+
} else {
|
|
370
|
+
stream = await navigator.mediaDevices.getDisplayMedia({ audio: true, video: false });
|
|
371
|
+
owned = true;
|
|
372
|
+
}
|
|
373
|
+
return {
|
|
374
|
+
node: this.audioCtx.createMediaStreamSource(stream),
|
|
375
|
+
owned: owned ? stream : null,
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function resolveContainer(container: HTMLElement | string): HTMLElement {
|
|
381
|
+
if (typeof container !== "string") return container;
|
|
382
|
+
const el = document.getElementById(container);
|
|
383
|
+
if (!el) {
|
|
384
|
+
throw new Error(`PrismPlayer: no element found with id "${container}"`);
|
|
385
|
+
}
|
|
386
|
+
return el;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function createCanvas(id: string): HTMLCanvasElement {
|
|
390
|
+
const canvas = document.createElement("canvas");
|
|
391
|
+
canvas.id = id;
|
|
392
|
+
canvas.className = "bg-canvas";
|
|
393
|
+
canvas.setAttribute("aria-hidden", "true");
|
|
394
|
+
return canvas;
|
|
395
|
+
}
|