@boyernick/standard-ui-react 0.4.2-canary.61 → 0.4.2-canary.63
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/package.json +1 -1
- package/src/lib/lens-glass/renderer.ts +148 -40
- package/src/video-player.tsx +33 -0
package/package.json
CHANGED
|
@@ -149,6 +149,21 @@ function drawable(element: Element): element is HTMLImageElement | HTMLVideoElem
|
|
|
149
149
|
return element instanceof HTMLCanvasElement && element.width > 0 && element.height > 0;
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
+
/** Whether drawing this source would taint a canvas (its pixels are cross-origin). */
|
|
153
|
+
function unreadable(source: CanvasImageSource) {
|
|
154
|
+
const probe = document.createElement("canvas");
|
|
155
|
+
probe.width = probe.height = 1;
|
|
156
|
+
const context = probe.getContext("2d");
|
|
157
|
+
if (!context) return true;
|
|
158
|
+
try {
|
|
159
|
+
context.drawImage(source, 0, 0, 1, 1);
|
|
160
|
+
context.getImageData(0, 0, 1, 1);
|
|
161
|
+
return false;
|
|
162
|
+
} catch {
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
152
167
|
function naturalSize(element: HTMLImageElement | HTMLVideoElement | HTMLCanvasElement) {
|
|
153
168
|
if (element instanceof HTMLImageElement) return { width: element.naturalWidth, height: element.naturalHeight };
|
|
154
169
|
if (element instanceof HTMLVideoElement) return { width: element.videoWidth, height: element.videoHeight };
|
|
@@ -196,6 +211,13 @@ type Pane = {
|
|
|
196
211
|
element: HTMLElement;
|
|
197
212
|
canvas: HTMLCanvasElement;
|
|
198
213
|
context: CanvasRenderingContext2D;
|
|
214
|
+
/** This pane's own ground canvas, kept between frames rather than resized. */
|
|
215
|
+
scene: HTMLCanvasElement;
|
|
216
|
+
sceneContext: CanvasRenderingContext2D;
|
|
217
|
+
/** When a playing video behind it last redrew this pane. */
|
|
218
|
+
lastVideoFrame: number;
|
|
219
|
+
/** Whether the pane was drawable last time it was checked. */
|
|
220
|
+
shown: boolean;
|
|
199
221
|
restorePosition: string | null;
|
|
200
222
|
hover: number;
|
|
201
223
|
press: number;
|
|
@@ -219,11 +241,6 @@ export interface LensGlass {
|
|
|
219
241
|
export function createLensGlass({ root, glassElements, defaults = {} }: LensGlassOptions): LensGlass {
|
|
220
242
|
getShared(); // throws if WebGL is unavailable, so the root can fall back
|
|
221
243
|
|
|
222
|
-
const scene = document.createElement("canvas");
|
|
223
|
-
const maybeSceneContext = scene.getContext("2d");
|
|
224
|
-
if (!maybeSceneContext) throw new Error("Glass: 2D canvas is unavailable.");
|
|
225
|
-
const sceneContext: CanvasRenderingContext2D = maybeSceneContext;
|
|
226
|
-
|
|
227
244
|
// If anything below throws, take back what was already added to the DOM so
|
|
228
245
|
// a failed start leaves the panes as they were.
|
|
229
246
|
const injected: Array<{ canvas: HTMLCanvasElement; element: HTMLElement; position: string | null }> = [];
|
|
@@ -249,6 +266,9 @@ export function createLensGlass({ root, glassElements, defaults = {} }: LensGlas
|
|
|
249
266
|
injected.push(record);
|
|
250
267
|
const context = canvas.getContext("2d");
|
|
251
268
|
if (!context) throw new Error("Glass: 2D canvas is unavailable.");
|
|
269
|
+
const scene = document.createElement("canvas");
|
|
270
|
+
const sceneContext = scene.getContext("2d");
|
|
271
|
+
if (!sceneContext) throw new Error("Glass: 2D canvas is unavailable.");
|
|
252
272
|
|
|
253
273
|
let restorePosition: string | null = null;
|
|
254
274
|
if (getComputedStyle(element).position === "static") {
|
|
@@ -258,7 +278,7 @@ export function createLensGlass({ root, glassElements, defaults = {} }: LensGlas
|
|
|
258
278
|
}
|
|
259
279
|
|
|
260
280
|
const pane: Pane = {
|
|
261
|
-
element, canvas, context, restorePosition,
|
|
281
|
+
element, canvas, context, scene, sceneContext, lastVideoFrame: 0, shown: false, restorePosition,
|
|
262
282
|
hover: 0, press: 0, hoverTarget: 0, pressTarget: 0, signature: "", cleanup: () => {},
|
|
263
283
|
};
|
|
264
284
|
const enter = () => { pane.hoverTarget = 1; wake(); };
|
|
@@ -317,10 +337,19 @@ export function createLensGlass({ root, glassElements, defaults = {} }: LensGlas
|
|
|
317
337
|
return getComputedStyle(document.documentElement).backgroundColor || "#000";
|
|
318
338
|
};
|
|
319
339
|
|
|
340
|
+
// `forced`: something may have changed, so check every pane this frame.
|
|
341
|
+
// `redraw`: draw every pane even if nothing it depends on looks different
|
|
342
|
+
// (markChanged, a restored WebGL context).
|
|
320
343
|
let forced = true;
|
|
344
|
+
let redraw = true;
|
|
321
345
|
let visible = true;
|
|
322
346
|
let frame = 0;
|
|
323
347
|
let destroyed = false;
|
|
348
|
+
// Frames since anything last moved or changed; the loop stops a few frames
|
|
349
|
+
// after the page settles and waits for an event to wake it.
|
|
350
|
+
let quietFrames = 0;
|
|
351
|
+
let lastLayout = "";
|
|
352
|
+
let transitions = 0;
|
|
324
353
|
|
|
325
354
|
const intersection = new IntersectionObserver((entries) => {
|
|
326
355
|
visible = entries.some((entry) => entry.isIntersecting);
|
|
@@ -330,57 +359,118 @@ export function createLensGlass({ root, glassElements, defaults = {} }: LensGlas
|
|
|
330
359
|
|
|
331
360
|
const mutations = new MutationObserver((records) => {
|
|
332
361
|
// Our own canvases change on every frame we draw; they are not the ground.
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
)
|
|
362
|
+
// A pane's own contents (a progress bar, a label) are drawn over the glass,
|
|
363
|
+
// not refracted by it; only its configuration matters.
|
|
364
|
+
const relevant = records.filter((record) => {
|
|
365
|
+
const target = record.target as HTMLElement;
|
|
366
|
+
if (target.dataset?.glassCanvas !== undefined) return false;
|
|
367
|
+
if (record.attributeName === "data-config") return true;
|
|
368
|
+
return !(target instanceof Element && !paneSet.has(target as HTMLElement) && insidePane(target));
|
|
369
|
+
});
|
|
336
370
|
if (!relevant.length) return;
|
|
337
|
-
// Inline style changes (a carousel's transform while dragging) move
|
|
338
|
-
// things, which the
|
|
339
|
-
//
|
|
371
|
+
// Inline style changes (a carousel's transform while dragging) only move
|
|
372
|
+
// things, which the position check sees; a change in what exists or how
|
|
373
|
+
// it is styled means re-listing what paints.
|
|
340
374
|
if (relevant.some((record) => record.type === "childList" || record.attributeName !== "style")) {
|
|
341
375
|
collectMedia();
|
|
342
376
|
}
|
|
377
|
+
// An inline style can change opacity or clipping without moving anything.
|
|
343
378
|
forced = true;
|
|
344
379
|
wake();
|
|
345
380
|
});
|
|
346
381
|
mutations.observe(root, { childList: true, subtree: true, attributes: true, attributeFilter: ["src", "data-config", "class", "style"] });
|
|
347
382
|
|
|
348
|
-
//
|
|
349
|
-
const
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
383
|
+
// Everything else that can change what a pane shows arrives as an event.
|
|
384
|
+
const onChange = () => { forced = true; wake(); };
|
|
385
|
+
const onMove = () => wake();
|
|
386
|
+
const onTransitionStart = () => { transitions += 1; forced = true; wake(); };
|
|
387
|
+
const onTransitionEnd = () => { transitions = Math.max(0, transitions - 1); forced = true; wake(); };
|
|
388
|
+
const rootEvents: Array<[string, EventListener]> = [
|
|
389
|
+
["load", onChange], ["loadeddata", onChange], ["seeked", onChange], ["play", onChange],
|
|
390
|
+
["pause", onChange], ["ended", onChange],
|
|
391
|
+
["transitionrun", onTransitionStart], ["transitionend", onTransitionEnd], ["transitioncancel", onTransitionEnd],
|
|
392
|
+
["animationstart", onTransitionStart], ["animationend", onTransitionEnd], ["animationcancel", onTransitionEnd],
|
|
393
|
+
["pointerover", onChange], ["pointerout", onChange], ["pointermove", onMove], ["pointerdown", onMove],
|
|
394
|
+
];
|
|
395
|
+
for (const [type, listener] of rootEvents) root.addEventListener(type, listener, true);
|
|
396
|
+
window.addEventListener("resize", onChange);
|
|
397
|
+
window.addEventListener("scroll", onMove, { capture: true, passive: true });
|
|
354
398
|
// A root that started in a background tab draws when the tab is shown.
|
|
355
|
-
document.addEventListener("visibilitychange",
|
|
399
|
+
document.addEventListener("visibilitychange", onChange);
|
|
356
400
|
|
|
357
401
|
function wake() {
|
|
402
|
+
quietFrames = 0;
|
|
358
403
|
if (!frame && !destroyed) frame = requestAnimationFrame(tick);
|
|
359
404
|
}
|
|
360
405
|
|
|
361
|
-
|
|
406
|
+
const rectKey = (element: Element) => {
|
|
407
|
+
const r = element.getBoundingClientRect();
|
|
408
|
+
return `${r.x.toFixed(1)},${r.y.toFixed(1)},${r.width.toFixed(1)},${r.height.toFixed(1)}`;
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
/** Whether a pane can be seen at all: not faded out, hidden or clipped away. */
|
|
412
|
+
function paneShown(pane: Pane) {
|
|
413
|
+
const seen = visibility(pane.element, root);
|
|
414
|
+
if (!seen || seen.opacity < 0.02) return false;
|
|
415
|
+
return Boolean(intersect(seen.clip, { x: 0, y: 0, width: innerWidth, height: innerHeight }));
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function tick(now: number) {
|
|
362
419
|
frame = 0;
|
|
363
420
|
// Hidden tabs get no animation frames anyway; no need to check here.
|
|
364
421
|
if (destroyed || !visible) return;
|
|
365
|
-
|
|
366
|
-
|
|
422
|
+
|
|
423
|
+
// Cheap: where the panes and media are. Anything else that matters
|
|
424
|
+
// arrives as an event and sets `forced`.
|
|
425
|
+
const layout = panes.map((pane) => rectKey(pane.element)).join(";") + "|" + media.map(rectKey).join(";");
|
|
426
|
+
const moved = layout !== lastLayout || transitions > 0;
|
|
427
|
+
lastLayout = layout;
|
|
428
|
+
const check = forced || moved;
|
|
429
|
+
|
|
430
|
+
const ground = check ? groundColor() : "";
|
|
367
431
|
const dpr = Math.min(window.devicePixelRatio || 1, 2);
|
|
368
|
-
const playing = media.
|
|
369
|
-
(element)
|
|
432
|
+
const playing = media.filter(
|
|
433
|
+
(element): element is HTMLVideoElement =>
|
|
434
|
+
element instanceof HTMLVideoElement && !element.paused && !element.ended,
|
|
370
435
|
);
|
|
436
|
+
let animating = false;
|
|
437
|
+
let videoBehindShownPane = false;
|
|
371
438
|
|
|
372
439
|
for (const pane of panes) {
|
|
373
440
|
pane.hover += (pane.hoverTarget - pane.hover) * 0.25;
|
|
374
441
|
pane.press += (pane.pressTarget - pane.press) * 0.35;
|
|
375
442
|
if (Math.abs(pane.hoverTarget - pane.hover) < 0.01) pane.hover = pane.hoverTarget;
|
|
376
443
|
if (Math.abs(pane.pressTarget - pane.press) < 0.01) pane.press = pane.pressTarget;
|
|
377
|
-
|
|
378
|
-
|
|
444
|
+
const paneAnimating = pane.hover !== pane.hoverTarget || pane.press !== pane.pressTarget;
|
|
445
|
+
if (paneAnimating) animating = true;
|
|
446
|
+
|
|
447
|
+
if (check || paneAnimating) pane.shown = paneShown(pane);
|
|
448
|
+
if (!pane.shown) {
|
|
449
|
+
// Draw afresh when it next appears.
|
|
450
|
+
pane.signature = "";
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// A playing video behind a shown pane changes every frame; follow it
|
|
455
|
+
// at up to 30fps.
|
|
456
|
+
const paneBox = toBox(pane.element.getBoundingClientRect());
|
|
457
|
+
const overVideo = playing.some((video) => intersect(toBox(video.getBoundingClientRect()), paneBox));
|
|
458
|
+
let videoFrame = false;
|
|
459
|
+
if (overVideo) {
|
|
460
|
+
videoBehindShownPane = true;
|
|
461
|
+
if (now - pane.lastVideoFrame >= 33) {
|
|
462
|
+
pane.lastVideoFrame = now;
|
|
463
|
+
videoFrame = true;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
if (!check && !paneAnimating && !videoFrame) continue;
|
|
468
|
+
renderPane(pane, ground || groundColor(), dpr, redraw);
|
|
379
469
|
}
|
|
380
470
|
forced = false;
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
if (animating ||
|
|
471
|
+
redraw = false;
|
|
472
|
+
quietFrames = moved || animating ? 0 : quietFrames + 1;
|
|
473
|
+
if (animating || videoBehindShownPane || quietFrames < 12) frame = requestAnimationFrame(tick);
|
|
384
474
|
}
|
|
385
475
|
|
|
386
476
|
function renderPane(pane: Pane, ground: string, dpr: number, force: boolean) {
|
|
@@ -424,14 +514,20 @@ export function createLensGlass({ root, glassElements, defaults = {} }: LensGlas
|
|
|
424
514
|
});
|
|
425
515
|
}
|
|
426
516
|
|
|
517
|
+
// Relative to the pane: a pane that moves together with what is behind it
|
|
518
|
+
// (a video player's controls as its slide pages) shows the same thing, and
|
|
519
|
+
// its canvas moves with it, so it needs no redraw.
|
|
520
|
+
const ox = rect.left;
|
|
521
|
+
const oy = rect.top;
|
|
522
|
+
const px = (value: number) => value.toFixed(1);
|
|
427
523
|
const signature = [
|
|
428
|
-
|
|
524
|
+
px(region.width), px(region.height),
|
|
429
525
|
dpr, ground, element.dataset.config ?? "", pane.hover.toFixed(2), pane.press.toFixed(2),
|
|
430
526
|
...drawn.map(({ element: item, clip, opacity, filter, color }) => {
|
|
431
527
|
const r = item.getBoundingClientRect();
|
|
432
528
|
const frameKey = item instanceof HTMLVideoElement ? item.currentTime.toFixed(3)
|
|
433
529
|
: item instanceof HTMLImageElement ? item.currentSrc : "";
|
|
434
|
-
return `${r.x
|
|
530
|
+
return `${px(r.x - ox)},${px(r.y - oy)},${px(r.width)},${px(r.height)}|${px(clip.x - ox)},${px(clip.y - oy)},${px(clip.width)},${px(clip.height)}|${opacity.toFixed(2)}|${filter}|${color ?? ""}|${frameKey}`;
|
|
435
531
|
}),
|
|
436
532
|
].join(";");
|
|
437
533
|
if (!force && signature === pane.signature) return;
|
|
@@ -442,8 +538,16 @@ export function createLensGlass({ root, glassElements, defaults = {} }: LensGlas
|
|
|
442
538
|
if (region.width * region.height * scale * scale > MAX_SCENE_PIXELS) {
|
|
443
539
|
scale = Math.sqrt(MAX_SCENE_PIXELS / (region.width * region.height));
|
|
444
540
|
}
|
|
445
|
-
|
|
446
|
-
|
|
541
|
+
const { scene, sceneContext } = pane;
|
|
542
|
+
const sceneWidth = Math.max(1, Math.round(region.width * scale));
|
|
543
|
+
const sceneHeight = Math.max(1, Math.round(region.height * scale));
|
|
544
|
+
// Resizing a canvas reallocates it; only do so when the size changes.
|
|
545
|
+
if (scene.width !== sceneWidth || scene.height !== sceneHeight) {
|
|
546
|
+
scene.width = sceneWidth;
|
|
547
|
+
scene.height = sceneHeight;
|
|
548
|
+
}
|
|
549
|
+
sceneContext.setTransform(1, 0, 0, 1, 0, 0);
|
|
550
|
+
sceneContext.clearRect(0, 0, sceneWidth, sceneHeight);
|
|
447
551
|
sceneContext.setTransform(scale, 0, 0, scale, -region.x * scale, -region.y * scale);
|
|
448
552
|
sceneContext.globalAlpha = 1;
|
|
449
553
|
sceneContext.fillStyle = ground;
|
|
@@ -489,8 +593,12 @@ export function createLensGlass({ root, glassElements, defaults = {} }: LensGlas
|
|
|
489
593
|
try {
|
|
490
594
|
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, scene);
|
|
491
595
|
} catch {
|
|
492
|
-
//
|
|
493
|
-
|
|
596
|
+
// Something we drew was cross-origin after all — a canvas, or media
|
|
597
|
+
// behind a same-origin address that redirects elsewhere (a video served
|
|
598
|
+
// through a signed-link redirect). Find it and leave it out from now on.
|
|
599
|
+
const media = drawn.filter(({ element: item }) => item instanceof HTMLImageElement || item instanceof HTMLVideoElement || item instanceof HTMLCanvasElement);
|
|
600
|
+
const culprits = media.filter(({ element: item }) => unreadable(item as CanvasImageSource));
|
|
601
|
+
for (const { element: item } of culprits.length ? culprits : media) tainted.add(item);
|
|
494
602
|
pane.signature = "";
|
|
495
603
|
forced = true;
|
|
496
604
|
return;
|
|
@@ -558,6 +666,7 @@ export function createLensGlass({ root, glassElements, defaults = {} }: LensGlas
|
|
|
558
666
|
const instance: LensGlass = {
|
|
559
667
|
markChanged() {
|
|
560
668
|
forced = true;
|
|
669
|
+
redraw = true;
|
|
561
670
|
wake();
|
|
562
671
|
},
|
|
563
672
|
destroy() {
|
|
@@ -565,11 +674,10 @@ export function createLensGlass({ root, glassElements, defaults = {} }: LensGlas
|
|
|
565
674
|
if (frame) cancelAnimationFrame(frame);
|
|
566
675
|
intersection.disconnect();
|
|
567
676
|
mutations.disconnect();
|
|
568
|
-
root.removeEventListener(
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
document.removeEventListener("visibilitychange", onMediaEvent);
|
|
677
|
+
for (const [type, listener] of rootEvents) root.removeEventListener(type, listener, true);
|
|
678
|
+
window.removeEventListener("resize", onChange);
|
|
679
|
+
window.removeEventListener("scroll", onMove, { capture: true });
|
|
680
|
+
document.removeEventListener("visibilitychange", onChange);
|
|
573
681
|
for (const pane of panes) {
|
|
574
682
|
pane.cleanup();
|
|
575
683
|
pane.canvas.remove();
|
package/src/video-player.tsx
CHANGED
|
@@ -45,6 +45,15 @@ export type VideoPlayerProps = ComponentProps<"div"> & {
|
|
|
45
45
|
title?: string
|
|
46
46
|
/** Accessible name for the video. Defaults to title. */
|
|
47
47
|
"aria-label"?: string
|
|
48
|
+
/**
|
|
49
|
+
* CORS mode for the video. The glass controls can only refract frames the
|
|
50
|
+
* page may read, which needs the host to allow cross-origin reads. By
|
|
51
|
+
* default the player asks with `anonymous` and, if the host refuses and the
|
|
52
|
+
* video fails to load, loads it once more without CORS — the video plays
|
|
53
|
+
* either way, and the controls fall back to the ground colour. Pass a mode
|
|
54
|
+
* to fix it, or `null` never to ask.
|
|
55
|
+
*/
|
|
56
|
+
crossOrigin?: "anonymous" | "use-credentials" | null
|
|
48
57
|
}
|
|
49
58
|
|
|
50
59
|
export const VideoPlayer = ({
|
|
@@ -53,6 +62,7 @@ export const VideoPlayer = ({
|
|
|
53
62
|
title,
|
|
54
63
|
className,
|
|
55
64
|
"aria-label": ariaLabel,
|
|
65
|
+
crossOrigin,
|
|
56
66
|
...props
|
|
57
67
|
}: VideoPlayerProps) => {
|
|
58
68
|
const playerRef = useRef<HTMLDivElement>(null)
|
|
@@ -65,6 +75,13 @@ export const VideoPlayer = ({
|
|
|
65
75
|
const [buffering, setBuffering] = useState(false)
|
|
66
76
|
const [ended, setEnded] = useState(false)
|
|
67
77
|
const [failed, setFailed] = useState(false)
|
|
78
|
+
// Automatic CORS: ask first, fall back once if the host refuses. Kept per
|
|
79
|
+
// source, so a new src gets a fresh attempt.
|
|
80
|
+
const automaticCors = crossOrigin === undefined
|
|
81
|
+
const [corsRefusedSrc, setCorsRefusedSrc] = useState<string | null>(null)
|
|
82
|
+
const videoCrossOrigin = automaticCors
|
|
83
|
+
? corsRefusedSrc === src ? undefined : "anonymous"
|
|
84
|
+
: crossOrigin ?? undefined
|
|
68
85
|
const [fullscreen, setFullscreen] = useState(false)
|
|
69
86
|
const pipSupported = useSyncExternalStore(
|
|
70
87
|
subscribePipSupport,
|
|
@@ -181,6 +198,11 @@ export const VideoPlayer = ({
|
|
|
181
198
|
setEnded(true)
|
|
182
199
|
}
|
|
183
200
|
const handleError = () => {
|
|
201
|
+
// Refused with CORS: try once more without it, rather than fail.
|
|
202
|
+
if (video.dataset.cors === "auto" && video.crossOrigin) {
|
|
203
|
+
setCorsRefusedSrc(video.getAttribute("src"))
|
|
204
|
+
return
|
|
205
|
+
}
|
|
184
206
|
setPlaying(false)
|
|
185
207
|
setLoading(false)
|
|
186
208
|
setBuffering(false)
|
|
@@ -233,6 +255,15 @@ export const VideoPlayer = ({
|
|
|
233
255
|
}
|
|
234
256
|
}, [])
|
|
235
257
|
|
|
258
|
+
// Changing the crossorigin attribute alone does not refetch; reload so the
|
|
259
|
+
// fallback takes effect.
|
|
260
|
+
const appliedCrossOrigin = useRef(videoCrossOrigin)
|
|
261
|
+
useEffect(() => {
|
|
262
|
+
if (appliedCrossOrigin.current === videoCrossOrigin) return
|
|
263
|
+
appliedCrossOrigin.current = videoCrossOrigin
|
|
264
|
+
videoRef.current?.load()
|
|
265
|
+
}, [videoCrossOrigin])
|
|
266
|
+
|
|
236
267
|
useEffect(() => {
|
|
237
268
|
const handleFullscreenChange = () => {
|
|
238
269
|
setFullscreen(document.fullscreenElement === playerRef.current)
|
|
@@ -299,6 +330,8 @@ export const VideoPlayer = ({
|
|
|
299
330
|
ref={videoRef}
|
|
300
331
|
src={src}
|
|
301
332
|
poster={poster}
|
|
333
|
+
crossOrigin={videoCrossOrigin}
|
|
334
|
+
data-cors={automaticCors ? "auto" : undefined}
|
|
302
335
|
preload="auto"
|
|
303
336
|
playsInline
|
|
304
337
|
className="aspect-video w-full cursor-pointer object-cover group-data-[fullscreen]:h-full group-data-[fullscreen]:w-full group-data-[fullscreen]:aspect-auto group-data-[fullscreen]:object-cover"
|