(null);
21 |
22 | const { width = 0 } = useResizeObserver({
23 | ref: i_ref
24 | });
25 | const { width: containerWidth = 0 } = useResizeObserver({
26 | ref: root
27 | });
28 |
29 | useEffect(() => {
30 | if (onShowChange) {
31 | onShowChange(show);
32 | }
33 | }, [show]);
34 |
35 | return {
38 | if (typeof ref === "function") ref(current);
39 | // @ts-ignore
40 | else if (ref) ref.current = current;
41 | // @ts-ignore
42 | i_ref.current = current;
43 | }}
44 | data-tooltip-state={show ? "showing" : "hidden"}
45 | style={{
46 | ...(style || {}),
47 | left:
48 | position === 'center' ?
49 | `max(min(${percentage * 100}%, ${(containerWidth ?? 0) - ((width ?? 0) / 2)}px), ${(width ?? 0) / 2}px)` :
50 | position === 'left' ?
51 | `max(min(${percentage * 100}%, ${(containerWidth ?? 0) - (width ?? 0)}px), 0px)` :
52 | `max(min(${percentage * 100}%, ${(containerWidth ?? 0)}px), ${(width ?? 0)}px)`
53 | }}
54 | className={`${className} ${show ? showingClassName : ""}`.trim() || undefined}
55 | >
56 | {children}
57 |
58 | });
59 |
--------------------------------------------------------------------------------
/packages/sliders/src/VolumeRoot.tsx:
--------------------------------------------------------------------------------
1 | import * as Slider from '@radix-ui/react-slider';
2 | const SliderRoot = Slider.Root;
3 | import React, { ComponentProps, ComponentPropsWithoutRef, forwardRef, RefAttributes } from 'react';
4 | import { useMediaVolume } from '@react-av/core';
5 |
6 | export type VolumeRootProps = Omit, "onValueChange" | "value" | "max" | "min" | "step"> & ComponentPropsWithoutRef<'span'>;
7 |
8 | export const VolumeRoot: React.ForwardRefExoticComponent> = forwardRef(function VolumeRoot({ children, ...props }, ref) {
9 | const [volume, setVolume] = useMediaVolume();
10 |
11 | // TODO: I have no idea why this is throwing typescript errors, it works fine in the ProgressBarRoot file
12 | return value[0] && setVolume(value[0])} value={[volume]}
15 | min={0} max={1} step={0.0001}
16 | >
17 | {children}
18 | ;
19 | });
20 |
--------------------------------------------------------------------------------
/packages/sliders/src/index.ts:
--------------------------------------------------------------------------------
1 | export { default as ProgressBarBufferedRanges } from './ProgressBarBufferedRanges';
2 | export * from './ProgressBarRoot';
3 | export * from './ProgressBarTooltip';
4 | export * from './VolumeRoot';
5 |
--------------------------------------------------------------------------------
/packages/sliders/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es2016",
4 | "lib": ["DOM", "DOM.Iterable", "ESNext"],
5 | "jsx": "react-jsx",
6 | "module": "esnext",
7 | "moduleResolution": "node",
8 | "declaration": true,
9 | "sourceMap": true,
10 | "outDir": "./dist",
11 | "esModuleInterop": true,
12 | "forceConsistentCasingInFileNames": true,
13 | "strict": true,
14 | "skipLibCheck": true
15 | },
16 | "include": [
17 | "src/**/*"
18 | ],
19 | "exclude": [
20 | "node_modules",
21 | "dist",
22 | ]
23 | }
24 |
--------------------------------------------------------------------------------
/packages/vtt-controls/.npmignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | .turbo
3 | dist/
4 | .next/
5 | build/
6 | .DS_Store/
7 | .turbo/
8 |
--------------------------------------------------------------------------------
/packages/vtt-controls/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # @react-av/vtt-controls
2 |
3 | ## 0.0.9
4 |
5 | ### Patch Changes
6 |
7 | - Updated dependencies
8 | - @react-av/vtt@0.0.9
9 |
10 | ## 0.0.8
11 |
12 | ### Patch Changes
13 |
14 | - Updated dependencies
15 | - @react-av/vtt@0.0.8
16 |
17 | ## 0.0.7
18 |
19 | ### Patch Changes
20 |
21 | - Updated dependencies
22 | - @react-av/vtt@0.0.7
23 |
24 | ## 0.0.6
25 |
26 | ### Patch Changes
27 |
28 | - Updated dependencies
29 | - @react-av/vtt@0.0.6
30 |
31 | ## 0.0.5
32 |
33 | ### Patch Changes
34 |
35 | - Updated dependencies
36 | - @react-av/vtt@0.0.5
37 |
38 | ## 0.0.4
39 |
40 | ### Patch Changes
41 |
42 | - Updated dependencies
43 | - @react-av/vtt@0.0.4
44 | - @react-av/core@0.0.4
45 |
46 | ## 0.0.3
47 |
48 | ### Patch Changes
49 |
50 | - @react-av/core@0.0.3
51 | - @react-av/vtt@0.0.3
52 |
53 | ## 0.0.2
54 |
55 | ### Patch Changes
56 |
57 | - Updated dependencies
58 | - @react-av/core@0.0.2
59 | - @react-av/vtt@0.0.2
60 |
61 | ## 0.0.1
62 |
63 | ### Patch Changes
64 |
65 | - e36e0fb: Initial experimental release
66 | - Updated dependencies [e36e0fb]
67 | - @react-av/core@0.0.1
68 | - @react-av/vtt@0.0.1
69 |
--------------------------------------------------------------------------------
/packages/vtt-controls/README.md:
--------------------------------------------------------------------------------
1 | # React AV WebVTT Controls
2 |
3 | This package adds components for use with the WebVTT module of React AV.
4 |
5 | ## Installation
6 |
7 | ```bash
8 | npm i @react-av/core @react-av/vtt-core @react-av/vtt @react-av/vtt-controls
9 | yarn add @react-av/core @react-av/vtt-core @react-av/vtt @react-av/vtt-controls
10 | pnpm i @react-av/core @react-av/vtt-core @react-av/vtt @react-av/vtt-controls
11 | ```
12 |
13 | ## Usage
14 |
15 | See the [documentation](https://react-av.wykerd.dev) for more information.
16 |
--------------------------------------------------------------------------------
/packages/vtt-controls/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@react-av/vtt-controls",
3 | "version": "0.0.9",
4 | "private": false,
5 | "description": "WebVTT controls for use with React AV.",
6 | "main": "dist/cjs/index.js",
7 | "module": "dist/esm/index.js",
8 | "scripts": {
9 | "build:cjs": "tsc --outDir dist/cjs --module commonjs",
10 | "build:esm": "tsc --outDir dist/esm --module esnext",
11 | "build": "pnpm build:cjs && pnpm build:esm",
12 | "dev": "tsc --watch"
13 | },
14 | "devDependencies": {
15 | "@react-av/core": "workspace:^",
16 | "@react-av/vtt": "workspace:^",
17 | "@types/react": "^18.3.5",
18 | "@types/react-dom": "^18.3.0",
19 | "react": "^18.3.1",
20 | "react-dom": "^18.3.1",
21 | "tslib": "^2.7.0",
22 | "typescript": "^5.5.4"
23 | },
24 | "peerDependencies": {
25 | "@react-av/core": "workspace:^",
26 | "@react-av/vtt": "workspace:^",
27 | "react": "^18.3.1",
28 | "react-dom": "^18.3.1"
29 | },
30 | "keywords": [],
31 | "author": "Daniel Wykerd ",
32 | "license": "MIT",
33 | "repository": {
34 | "directory": "packages/vtt-controls",
35 | "type": "git",
36 | "url": "https://github.com/Wykerd/react-av.git"
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/packages/vtt-controls/src/StoryboardThumbnail.tsx:
--------------------------------------------------------------------------------
1 | import { ComponentPropsWithRef, forwardRef, ForwardRefExoticComponent, RefAttributes, useEffect, useState } from "react";
2 | import { useMediaTextTrack } from "@react-av/vtt";
3 |
4 | export type StoryboardThumbnailProps = Omit, "src" | "srcSet"> & {
5 | timestamp: number,
6 | storyboardId: string
7 | }
8 |
9 | const StoryboardThumbnail = forwardRef(function StoryboardThumbnail({ timestamp, storyboardId, ...props }, ref) {
10 | const [cues] = useMediaTextTrack(storyboardId);
11 |
12 | const [blob, setBlob] = useState();
13 |
14 | const [lastImage, setLastImage] = useState();
15 | const [lastImageUrl, setLastImageUrl] = useState();
16 | const [lastXYWH, setLastXYWH] = useState();
17 |
18 | useEffect(() => {
19 | const cue = cues.find(cue => cue.startTime <= timestamp && cue.endTime >= timestamp);
20 | if (!cue) return;
21 |
22 | const url = new URL(cue.text);
23 |
24 | const hash = url.hash.substring(1);
25 |
26 | const params = new URLSearchParams(hash);
27 |
28 | if (!params.has("xywh")) return;
29 |
30 | const [x, y, w, h] = params.get("xywh")!.split(",").map(s => parseInt(s));
31 |
32 | url.hash = "";
33 |
34 | if (lastXYWH === params.get("xywh")! && lastImageUrl === url.href) return;
35 | else URL.revokeObjectURL(lastImageUrl!);
36 |
37 | setLastXYWH(params.get("xywh")!);
38 |
39 | let blobUrl: string | undefined;
40 |
41 | async function extractImage(image: HTMLImageElement) {
42 | if (x === undefined || Number.isNaN(x) || y === undefined || Number.isNaN(y) || w === undefined || Number.isNaN(w) || h === undefined || Number.isNaN(h)) return;
43 |
44 | const canvas = document.createElement("canvas");
45 | canvas.width = w;
46 | canvas.height = h;
47 | const ctx = canvas.getContext("2d");
48 | if (!ctx) return;
49 | ctx.drawImage(image, x, y, w, h, 0, 0, w, h);
50 | const blob = await new Promise(resolve => canvas.toBlob(resolve));
51 | if (!blob) return;
52 | blobUrl = URL.createObjectURL(blob);
53 | setBlob(blobUrl);
54 | }
55 |
56 | if (lastImage && lastImageUrl === url.href) {
57 | extractImage(lastImage);
58 | } else {
59 | const img = new Image();
60 | img.crossOrigin = "anonymous";
61 | img.onload = async () => {
62 | setLastImage(img);
63 | setLastImageUrl(url.href);
64 | extractImage(img);
65 | }
66 | img.src = url.href;
67 | }
68 | }, [cues, timestamp]);
69 |
70 | useEffect(() => {
71 | return () => {
72 | if (blob) URL.revokeObjectURL(blob);
73 | }
74 | }, []);
75 |
76 | return
;
77 | });
78 |
79 | export default StoryboardThumbnail;
80 |
--------------------------------------------------------------------------------
/packages/vtt-controls/src/index.ts:
--------------------------------------------------------------------------------
1 | export { default as StoryboardThumbnail } from './StoryboardThumbnail';
--------------------------------------------------------------------------------
/packages/vtt-controls/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es2016",
4 | "lib": ["DOM", "DOM.Iterable", "ESNext"],
5 | "jsx": "react-jsx",
6 | "module": "esnext",
7 | "moduleResolution": "node",
8 | "declaration": true,
9 | "sourceMap": true,
10 | "outDir": "./dist",
11 | "esModuleInterop": true,
12 | "forceConsistentCasingInFileNames": true,
13 | "strict": true,
14 | "skipLibCheck": true
15 | },
16 | "include": [
17 | "src/**/*"
18 | ],
19 | "exclude": [
20 | "node_modules",
21 | "dist",
22 | ]
23 | }
24 |
--------------------------------------------------------------------------------
/packages/vtt-core/.npmignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | .turbo
3 | dist/
4 | .next/
5 | build/
6 | .DS_Store/
7 | .turbo/
8 |
--------------------------------------------------------------------------------
/packages/vtt-core/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # @react-av/vtt-core
2 |
3 | ## 0.0.7
4 |
5 | ### Patch Changes
6 |
7 | - Minor fixes for AABB and hooks of VTT
8 |
9 | ## 0.0.6
10 |
11 | ### Patch Changes
12 |
13 | - General refactor of VTT libraries. Brings our implementation more inline with the standard HTML implementations.
14 |
15 | ## 0.0.5
16 |
17 | ### Patch Changes
18 |
19 | - Refactor VTT core global state
20 |
21 | ## 0.0.4
22 |
23 | ### Patch Changes
24 |
25 | - Add resize observer to recompute text track cue positions when video container size changes
26 |
27 | ## 0.0.3
28 |
29 | ### Patch Changes
30 |
31 | - Fix text track container alignment.
32 |
33 | ## 0.0.2
34 |
35 | ### Patch Changes
36 |
37 | - Fix WebVTT renderer reset behaviour
38 |
39 | ## 0.0.1
40 |
41 | ### Patch Changes
42 |
43 | - e36e0fb: Initial experimental release
44 |
--------------------------------------------------------------------------------
/packages/vtt-core/README.md:
--------------------------------------------------------------------------------
1 | # React AV WebVTT Implementation
2 |
3 | A compliant, standalone browser implementation of W3C WebVTT specification.
4 |
5 | ## Installation
6 |
7 | ```bash
8 | npm i @react-av/vtt-core
9 | yarn add @react-av/vtt-core
10 | pnpm i @react-av/vtt-core
11 | ```
12 |
13 | ## Usage
14 |
15 | See the [documentation](https://react-av.wykerd.dev) for more information.
16 |
--------------------------------------------------------------------------------
/packages/vtt-core/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@react-av/vtt-core",
3 | "version": "0.0.7",
4 | "private": false,
5 | "description": "W3C specification compliant WebVTT implementation.",
6 | "main": "dist/cjs/index.js",
7 | "module": "dist/esm/index.js",
8 | "scripts": {
9 | "build:cjs": "tsc --outDir dist/cjs --module commonjs",
10 | "build:esm": "tsc --outDir dist/esm --module esnext",
11 | "build": "pnpm build:cjs && pnpm build:esm",
12 | "dev": "tsc --watch"
13 | },
14 | "keywords": [],
15 | "author": "Daniel Wykerd ",
16 | "license": "MIT",
17 | "repository": {
18 | "directory": "packages/vtt-core",
19 | "type": "git",
20 | "url": "https://github.com/Wykerd/react-av.git"
21 | },
22 | "devDependencies": {
23 | "typescript": "^5.5.4"
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/packages/vtt-core/src/TextTrack.ts:
--------------------------------------------------------------------------------
1 | import { getCueContext } from "./GlobalState";
2 | import { TextTrackCue } from "./VTTCue";
3 |
4 | export type TextTrackKind = "subtitles" | "captions" | "descriptions" | "chapters" | "metadata";
5 | export type TextTrackMode = "disabled" | "hidden" | "showing";
6 |
7 | // TODO: make this spec compliant
8 | export default class TextTrack extends EventTarget {
9 | #kind: TextTrackKind;
10 | get kind() {
11 | return this.#kind;
12 | }
13 | #label: string;
14 | get label() {
15 | return this.#label;
16 | }
17 | #language: string;
18 | get language() {
19 | return this.#language;
20 | }
21 | #id?: string;
22 | get id() {
23 | return this.#id || "";
24 | }
25 | #isBandMetadataTrackDispatchType: boolean;
26 | get inBandMetadataTrackDispatchType() {
27 | return this.#isBandMetadataTrackDispatchType;
28 | }
29 | #mode: TextTrackMode;
30 | get mode() {
31 | return this.#mode;
32 | }
33 | set mode(mode: TextTrackMode) {
34 | if (!["disabled", "hidden", "showing"].includes(mode)) return;
35 | this.#mode = mode;
36 | }
37 | #cues?: TextTrackCue[];
38 | get cues() {
39 | return this.#cues;
40 | }
41 | get activeCues() {
42 | return this.#cues?.filter(cue => getCueContext(cue)?.active);
43 | }
44 |
45 | #regions: VTTRegion[] = [];
46 | // XXX: this is non-standard
47 | get _regions() {
48 | return this.#regions;
49 | }
50 |
51 | addCue(cue: TextTrackCue) {
52 | if (this.#cues) {
53 | if (this.#cues.includes(cue)) {
54 | // remove the old cue
55 | this.removeCue(cue);
56 | }
57 | this.#cues.push(cue);
58 | } else {
59 | this.#cues = [cue];
60 | }
61 | }
62 |
63 | removeCue(cue: TextTrackCue) {
64 | if (this.#cues) {
65 | const index = this.#cues.indexOf(cue);
66 | if (index > -1) {
67 | this.#cues.splice(index, 1);
68 | } else {
69 | throw new DOMException("The cue does not exist in the track", "NotFoundError");
70 | }
71 | }
72 | }
73 |
74 | _addRegion(region: VTTRegion) {
75 | this.#regions.push(region);
76 | }
77 |
78 | _removeRegion(region: VTTRegion) {
79 | const index = this.#regions.indexOf(region);
80 | if (index > -1) {
81 | this.#regions.splice(index, 1);
82 | }
83 | }
84 |
85 | constructor(kind: TextTrackKind, mode?: TextTrackMode, label?: string, language?: string, id?: string, isBandMetadataTrackDispatchType?: boolean, regions?: VTTRegion[]) {
86 | super();
87 | this.#kind = kind ? ["subtitles", "captions", "descriptions", "chapters", "metadata"].includes(kind) ? kind : "metadata" : "subtitles";
88 | this.#label = label || "";
89 | this.#language = language || "";
90 | this.#isBandMetadataTrackDispatchType = !!isBandMetadataTrackDispatchType;
91 | this.#mode = mode || "hidden";
92 | this.#id = id;
93 | this.#regions = regions || [];
94 | }
95 |
96 | // TODO: oncuechange
97 | }
--------------------------------------------------------------------------------
/packages/vtt-core/src/Track.ts:
--------------------------------------------------------------------------------
1 | import { addTrack, getContext, init, removeTrack, updateTextTrackDisplay } from "./GlobalState";
2 | import TextTrack from "./TextTrack";
3 | import VTTParser from "./VTTParser";
4 |
5 | export interface HTMLTrackElementAttributes {
6 | kind?: TextTrackKind;
7 | src: string;
8 | srclang?: string;
9 | label?: string;
10 | default?: boolean;
11 | id?: string;
12 | }
13 |
14 | /**
15 | * Similar to the HTMLTrackElement, but for use in JS only. (Thus not a DOM element.)
16 | */
17 | export class Track {
18 | kind: TextTrackKind;
19 | src: URL;
20 | srclang: string;
21 | label: string;
22 | default: boolean;
23 | #track: TextTrack;
24 | #readyState: number = 0;
25 | #element: HTMLMediaElement;
26 | get readyState() {
27 | return this.#readyState;
28 | }
29 | #abortController = new AbortController();
30 | static NONE = 0;
31 | static LOADING = 1;
32 | static LOADED = 2;
33 | static ERROR = 3;
34 | constructor(element: HTMLMediaElement, attrs: HTMLTrackElementAttributes) {
35 | this.src = new URL(attrs.src.trim(), location.href);
36 | // XXX: we don't verify language tag
37 | this.srclang = attrs.srclang ? attrs.srclang.trim() : "";
38 | this.label = attrs.label ? attrs.label.trim() : "";
39 | this.default = !!attrs.default;
40 | this.#element = element;
41 | this.#track = new TextTrack(attrs.kind || "subtitles", this.default ? "showing" : "hidden", this.label, this.srclang, attrs.id);
42 | this.kind = this.#track.kind;
43 | addTrack(element, this.#track);
44 | this.#fetch();
45 | }
46 | async #fetch() {
47 | this.#readyState = Track.LOADING;
48 | try {
49 | const response = await fetch(this.src.href, {
50 | signal: this.#abortController.signal,
51 | });
52 | const text = await response.text();
53 | const parser = new VTTParser(text);
54 | for (const cue of parser.cues) {
55 | cue.track = this.#track;
56 | this.#track.addCue(cue);
57 | const context = getContext(this.#element);
58 | context?.newlyIntroducedCues.add(cue);
59 | context?.tracksChanged.dispatchEvent(new CustomEvent("cuechange", {
60 | detail: this.#track
61 | }))
62 | }
63 | for (const region of parser.regions) {
64 | this.#track._addRegion(region);
65 | }
66 | this.#readyState = Track.LOADED;
67 | } catch (e) {
68 | console.error(e);
69 | this.#readyState = Track.ERROR;
70 | }
71 | }
72 | remove() {
73 | if (this.#readyState === Track.LOADING)
74 | this.#abortController.abort();
75 | removeTrack(this.#element, this.#track);
76 | }
77 | }
78 |
79 | Object.defineProperty(Track, "NONE", { value: 0, writable: false, enumerable: true, configurable: false });
80 | Object.defineProperty(Track, "LOADING", { value: 1, writable: false, enumerable: true, configurable: false });
81 | Object.defineProperty(Track, "LOADED", { value: 2, writable: false, enumerable: true, configurable: false });
82 | Object.defineProperty(Track, "ERROR", { value: 3, writable: false, enumerable: true, configurable: false });
83 |
--------------------------------------------------------------------------------
/packages/vtt-core/src/VTTRegion.ts:
--------------------------------------------------------------------------------
1 | export type ScrollSetting = "" | "up";
2 |
3 | export default interface VTTRegion {
4 | id: string;
5 | width: number;
6 | lines: number;
7 | regionAnchorX: number;
8 | regionAnchorY: number;
9 | viewportAnchorX: number;
10 | viewportAnchorY: number;
11 | scroll: ScrollSetting;
12 | }
13 |
14 | export default class VTTRegion implements VTTRegion {
15 | id: string = "";
16 | width: number = 100;
17 | lines: number = 3;
18 | regionAnchorX: number = 0;
19 | regionAnchorY: number = 100;
20 | viewportAnchorX: number = 0;
21 | viewportAnchorY: number = 100;
22 | scroll: ScrollSetting = "";
23 | }
24 |
--------------------------------------------------------------------------------
/packages/vtt-core/src/index.ts:
--------------------------------------------------------------------------------
1 | import * as VTT from './GlobalState';
2 | export default VTT;
3 | export {default as TextTrack} from './TextTrack';
4 | export * from './TextTrack';
5 | export {default as VTTCue} from './VTTCue';
6 | export * from './VTTCue';
7 | export {default as WebVTTParseCueText} from './VTTCueTextParser';
8 | export * from './VTTCueTextParser';
9 | export {default as VTTParser} from './VTTParser';
10 | export {default as VTTRegion} from './VTTRegion';
11 | export * from './VTTRegion';
12 | export * as Renderer from './VTTRenderer';
13 | export {default as WebVTTUpdateTextTracksDisplay} from './VTTRenderer';
14 | export * from './Track';
15 |
--------------------------------------------------------------------------------
/packages/vtt-core/src/renderer/Cues.ts:
--------------------------------------------------------------------------------
1 | if (globalThis.document) {
2 | class VTTNodeList extends HTMLElement {
3 | constructor() {
4 | super();
5 | this.applyStyles();
6 | }
7 |
8 | applyStyles() {
9 | const vertical = this.getAttribute('vertical') || "";
10 | const align = this.getAttribute('align') || "center";
11 | this.style.writingMode = vertical === "" ?
12 | "horizontal-tb" :
13 | vertical === "lr" ?
14 | "vertical-lr" : "vertical-rl";
15 | this.style.textAlign = align;
16 | }
17 |
18 | static get observedAttributes() {
19 | return ['vertical', 'align'];
20 | }
21 |
22 | attributeChangedCallback() {
23 | this.applyStyles();
24 | }
25 | }
26 |
27 | class VTTVoiceElement extends HTMLElement {
28 | constructor() {
29 | super();
30 | }
31 | }
32 |
33 | class VTTCueBackgroundBox extends HTMLElement {
34 | constructor() {
35 | super();
36 | }
37 | }
38 |
39 | class VTTRegionElement extends HTMLElement {
40 | constructor() {
41 | super();
42 | }
43 | }
44 |
45 | window.customElements.define('vtt-cue-root', VTTNodeList);
46 | window.customElements.define('v', VTTVoiceElement);
47 | window.customElements.define('vtt-cue-bgbox', VTTCueBackgroundBox);
48 | window.customElements.define('vtt-region', VTTRegionElement);
49 |
50 | }
51 |
--------------------------------------------------------------------------------
/packages/vtt-core/src/renderer/TextTrackContainer.ts:
--------------------------------------------------------------------------------
1 | if (globalThis.document) {
2 |
3 | const template = document.createElement("template");
4 |
5 | template.innerHTML = `
6 |
132 |
133 | `;
134 | class TextTrackContainer extends HTMLElement {
135 | #hostStyles: HTMLStyleElement;
136 | constructor() {
137 | super();
138 | this.attachShadow({ mode: "open" });
139 | this.#hostStyles = document.createElement('style');
140 | this.shadowRoot!.appendChild(this.#hostStyles);
141 | this.shadowRoot!.appendChild(template.content.cloneNode(true));
142 |
143 | this.applyStyles();
144 | }
145 |
146 | applyStyles() {
147 | const viewHeight = parseFloat(this.getAttribute('vheight')||"0");
148 | this.#hostStyles.textContent = `:host {
149 | --vtt-view-height: ${viewHeight}px;
150 | }`;
151 | }
152 |
153 | attributeChangedCallback() {
154 | this.applyStyles();
155 | }
156 |
157 | static get observedAttributes() {
158 | return ['vheight']
159 | }
160 |
161 | appendChild(node: T): T {
162 | return this.shadowRoot!.querySelector('slot')!.appendChild(node);
163 | }
164 |
165 | append(...nodes: (string | Node)[]): void {
166 | this.shadowRoot!.querySelector('slot')!.append(...nodes);
167 | }
168 |
169 | removeChild(child: T): T {
170 | return this.shadowRoot!.querySelector('slot')!.removeChild(child);
171 | }
172 |
173 | get innerHTML(): string {
174 | return this.shadowRoot!.querySelector('slot')!.innerHTML;
175 | }
176 |
177 | set innerHTML(value: string) {
178 | this.shadowRoot!.querySelector('slot')!.innerHTML = value;
179 | }
180 |
181 | get children() {
182 | return this.shadowRoot!.querySelector('slot')!.children;
183 | }
184 | }
185 |
186 | window.customElements.define('vtt-texttrackcontainer', TextTrackContainer);
187 |
188 | }
189 |
--------------------------------------------------------------------------------
/packages/vtt/.npmignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | .turbo
3 | dist/
4 | .next/
5 | build/
6 | .DS_Store/
7 | .turbo/
8 |
--------------------------------------------------------------------------------
/packages/vtt/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # @react-av/vtt
2 |
3 | ## 0.0.9
4 |
5 | ### Patch Changes
6 |
7 | - Minor bug fixes
8 |
9 | ## 0.0.8
10 |
11 | ### Patch Changes
12 |
13 | - Minor fixes for AABB and hooks of VTT
14 | - Updated dependencies
15 | - @react-av/vtt-core@0.0.7
16 |
17 | ## 0.0.7
18 |
19 | ### Patch Changes
20 |
21 | - General refactor of VTT libraries. Brings our implementation more inline with the standard HTML implementations.
22 | - Updated dependencies
23 | - @react-av/vtt-core@0.0.6
24 |
25 | ## 0.0.6
26 |
27 | ### Patch Changes
28 |
29 | - Refactor VTT core global state
30 | - Updated dependencies
31 | - @react-av/vtt-core@0.0.5
32 |
33 | ## 0.0.5
34 |
35 | ### Patch Changes
36 |
37 | - Add useMediaTextTrackList hook for consuming list of all text tracks of the media element
38 |
39 | ## 0.0.4
40 |
41 | ### Patch Changes
42 |
43 | - Add resize observer to recompute text track cue positions when video container size changes
44 | - Updated dependencies
45 | - @react-av/vtt-core@0.0.4
46 | - @react-av/core@0.0.4
47 |
48 | ## 0.0.3
49 |
50 | ### Patch Changes
51 |
52 | - Updated dependencies
53 | - @react-av/vtt-core@0.0.3
54 | - @react-av/core@0.0.3
55 |
56 | ## 0.0.2
57 |
58 | ### Patch Changes
59 |
60 | - Fix WebVTT renderer reset behaviour
61 | - Updated dependencies
62 | - @react-av/core@0.0.2
63 | - @react-av/vtt-core@0.0.2
64 |
65 | ## 0.0.1
66 |
67 | ### Patch Changes
68 |
69 | - e36e0fb: Initial experimental release
70 | - Updated dependencies [e36e0fb]
71 | - @react-av/core@0.0.1
72 | - @react-av/vtt-core@0.0.1
73 |
--------------------------------------------------------------------------------
/packages/vtt/README.md:
--------------------------------------------------------------------------------
1 | # React AV WebVTT
2 |
3 | This package adds WebVTT support to React AV.
4 |
5 | ## Installation
6 |
7 | ```bash
8 | npm i @react-av/core @react-av/vtt-core @react-av/vtt
9 | yarn add @react-av/core @react-av/vtt-core @react-av/vtt
10 | pnpm i @react-av/core @react-av/vtt-core @react-av/vtt
11 | ```
12 |
13 | ## Usage
14 |
15 | See the [documentation](https://react-av.wykerd.dev) for more information.
16 |
--------------------------------------------------------------------------------
/packages/vtt/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@react-av/vtt",
3 | "private": false,
4 | "version": "0.0.9",
5 | "description": "WebVTT support for React AV.",
6 | "main": "dist/cjs/index.js",
7 | "module": "dist/esm/index.js",
8 | "scripts": {
9 | "build:cjs": "tsc --outDir dist/cjs --module commonjs",
10 | "build:esm": "tsc --outDir dist/esm --module esnext",
11 | "build": "pnpm build:cjs && pnpm build:esm",
12 | "dev": "tsc --watch"
13 | },
14 | "dependencies": {},
15 | "keywords": [],
16 | "author": "Daniel Wykerd ",
17 | "license": "MIT",
18 | "repository": {
19 | "directory": "packages/vtt",
20 | "type": "git",
21 | "url": "https://github.com/Wykerd/react-av.git"
22 | },
23 | "devDependencies": {
24 | "@react-av/core": "workspace:^",
25 | "@react-av/vtt-core": "workspace:^",
26 | "@types/react": "^18.3.5",
27 | "@types/react-dom": "^18.3.0",
28 | "react": "^18.3.1",
29 | "react-dom": "^18.3.1",
30 | "tslib": "^2.7.0",
31 | "typescript": "^5.5.4"
32 | },
33 | "peerDependencies": {
34 | "@react-av/core": "workspace:^",
35 | "@react-av/vtt-core": "workspace:^",
36 | "react": "^18.3.1",
37 | "react-dom": "^18.3.1"
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/packages/vtt/src/InterfaceOverlay.tsx:
--------------------------------------------------------------------------------
1 | import React, { ComponentPropsWithoutRef, forwardRef, useEffect, useMemo, useRef, useState } from "react";
2 | import { Renderer, WebVTTUpdateTextTracksDisplay } from '@react-av/vtt-core';
3 | import { useMediaElement, useMediaPlaying, useMediaViewportHover } from "@react-av/core";
4 |
5 | const InterfaceOverlay = forwardRef, "tabIndex"> & { persistent?: boolean, inactiveClassName?: string }>(function InterfaceOverlay({ children, persistent, inactiveClassName, className, onMouseEnter, onMouseLeave, onFocus, onBlur, ...props }, f_ref) {
6 | const element = useMediaElement();
7 | const hover = useMediaViewportHover();
8 | const ref = useRef(null);
9 | const [playing] = useMediaPlaying();
10 | const [active, setActive] = useState(false);
11 | const isHidden = useMemo(() => {
12 | return hover !== undefined && !hover && !persistent && playing && !active;
13 | }, [hover, persistent, playing, active]);
14 |
15 | useEffect(() => {
16 | if (!element || !ref.current || isHidden) return;
17 | Renderer.addUIContainer(element as HTMLVideoElement, ref.current);
18 | element?.nodeName === "VIDEO" && WebVTTUpdateTextTracksDisplay(element as HTMLVideoElement);
19 | return () => {
20 | Renderer.removeUIContainer(element as HTMLVideoElement);
21 | element?.nodeName === "VIDEO" && WebVTTUpdateTextTracksDisplay(element as HTMLVideoElement, true);
22 | }
23 | }, [element, isHidden]);
24 |
25 | return {
35 | // @ts-ignore
36 | ref.current = current;
37 | if (typeof f_ref === 'function') f_ref(current);
38 | // @ts-ignore
39 | else if (f_ref) f_ref.current = current;
40 | }}
41 | >
42 | {children}
43 |
44 | });
45 |
46 | export default InterfaceOverlay;
47 |
--------------------------------------------------------------------------------
/packages/vtt/src/TextTrack.tsx:
--------------------------------------------------------------------------------
1 | import React, { ComponentPropsWithoutRef, forwardRef, useEffect, useRef, useState } from "react";
2 | import VTT, { TextTrack, VTTCue } from '@react-av/vtt-core';
3 | import { useMediaElement } from "@react-av/core";
4 |
5 | export function useMediaTextTrack(id: string) {
6 | const media = useMediaElement();
7 |
8 | const [activeCues, setActiveCues] = useState([]);
9 | const [cues, setCues] = useState([]);
10 |
11 | useEffect(() => {
12 | if (!media) return;
13 | VTT.ref(media);
14 | function update() {
15 | if (!media) return;
16 | const track = VTT.getTrackById(media, id);
17 | if (!track) return;
18 | const cues = track.cues;
19 | if (!cues) return;
20 | const orderedCues = cues.sort((a, b) => {
21 | if (a.startTime === b.startTime)
22 | return b.endTime - a.endTime;
23 | return a.startTime - b.startTime;
24 | });
25 | const activeCues = orderedCues.filter(cue => cue.startTime <= media.currentTime && cue.endTime > media.currentTime);
26 | setCues(orderedCues as VTTCue[]);
27 | setActiveCues(activeCues as VTTCue[]);
28 | }
29 |
30 | VTT.getContext(media)?.tracksChanged.addEventListener("change", update);
31 | VTT.getContext(media)?.tracksChanged.addEventListener("cuechange", update);
32 | VTT.getContext(media)?.updateRules.add(update);
33 | update();
34 |
35 | return () => {
36 | VTT.getContext(media)?.tracksChanged.removeEventListener("change", update);
37 | VTT.getContext(media)?.tracksChanged.removeEventListener("cuechange", update);
38 | VTT.getContext(media)?.updateRules.delete(update);
39 | VTT.deref(media);
40 | }
41 | }, [media, id]);
42 |
43 | return [cues, activeCues] as const;
44 | }
45 |
46 | export function useMediaTextTrackList() {
47 | const media = useMediaElement();
48 |
49 | const [tracks, setTracks] = useState();
50 |
51 | useEffect(() => {
52 | if (!media) return;
53 | VTT.ref(media);
54 | function update() {
55 | if (!media) return;
56 | const list = VTT.getContext(media)?.tracks;
57 | if (!list) return;
58 | setTracks([...list]);
59 | }
60 |
61 | VTT.getContext(media)?.tracksChanged.addEventListener("change", update);
62 | update();
63 |
64 | return () => {
65 | VTT.getContext(media)?.tracksChanged.removeEventListener("change", update);
66 | VTT.deref(media);
67 | }
68 | }, [media]);
69 |
70 | return tracks;
71 | }
72 |
73 | // TODO: this should be redone
74 | // see: https://www.benmvp.com/blog/forwarding-refs-polymorphic-react-component-typescript/
75 | // see: https://stackoverflow.com/questions/62238716/using-ref-current-in-react-forwardref (useImperativeHandle)
76 |
77 | export const Cue = forwardRef(function Cue({ as, cue, ...props }: { as: T, cue: VTTCue } & Omit, "children">, ref: React.Ref) {
78 | const i_ref = useRef();
79 | useEffect(() => {
80 | if (!i_ref.current) return;
81 | i_ref.current.innerHTML = "";
82 | i_ref.current.append(cue.getCueAsHTML());
83 | }, [cue]);
84 | const As = (as) as string;
85 | // @ts-ignore
86 | return {
87 | i_ref.current = current;
88 | if (typeof ref === "function") ref(current);
89 | // @ts-ignore
90 | else if (ref) ref.current = current;
91 | }} />;
92 | });
93 |
--------------------------------------------------------------------------------
/packages/vtt/src/Track.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect } from "react";
2 | import VTT, { Track as TextTrack, HTMLTrackElementAttributes } from '@react-av/vtt-core';
3 | import { useMediaElement } from "@react-av/core";
4 |
5 | export default function Track({ kind, srclang, label, src, id, ...props } : HTMLTrackElementAttributes) {
6 | const element = useMediaElement();
7 |
8 | useEffect(() => {
9 | if (!element) return;
10 | VTT.ref(element);
11 | VTT.observeResize(element);
12 |
13 | const track = new TextTrack(element, {
14 | kind,
15 | srclang,
16 | label,
17 | src,
18 | id,
19 | default: props.default
20 | });
21 |
22 | return () => {
23 | VTT.deref(element);
24 | track.remove();
25 | }
26 | }, [element, kind, srclang, label, src, id, props.default]);
27 |
28 | return null;
29 | }
--------------------------------------------------------------------------------
/packages/vtt/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './TextTrack';
2 | export { default as Track } from './Track';
3 | export { default as InterfaceOverlay } from './InterfaceOverlay';
4 |
--------------------------------------------------------------------------------
/packages/vtt/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es2016",
4 | "lib": ["DOM", "DOM.Iterable", "ESNext"],
5 | "jsx": "react-jsx",
6 | "module": "esnext",
7 | "moduleResolution": "node",
8 | "declaration": true,
9 | "sourceMap": true,
10 | "outDir": "./dist",
11 | "esModuleInterop": true,
12 | "forceConsistentCasingInFileNames": true,
13 | "strict": true,
14 | "skipLibCheck": true
15 | },
16 | "include": [
17 | "src/**/*"
18 | ],
19 | "exclude": [
20 | "node_modules",
21 | "dist",
22 | ]
23 | }
24 |
--------------------------------------------------------------------------------
/pnpm-workspace.yaml:
--------------------------------------------------------------------------------
1 | packages:
2 | - packages/*
3 | - apps/*
--------------------------------------------------------------------------------
/turbo.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://turborepo.org/schema.json",
3 | "pipeline": {
4 | "build": {
5 | "dependsOn": [
6 | "^build"
7 | ],
8 | "outputs": [
9 | ".next/**",
10 | "dist/**"
11 | ]
12 | },
13 | "dev": {
14 | "dependsOn": [
15 | "build"
16 | ],
17 | "cache": false
18 | }
19 | }
20 | }
--------------------------------------------------------------------------------