UNPKG

@react-av/editor

Version:

Editor Timeline Components built on React AV.

219 lines 11.3 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.useTimelineSubtitleCueEditor = useTimelineSubtitleCueEditor; exports.useTimelineSubtitleTrack = useTimelineSubtitleTrack; exports.TimelineSubtitlesTrack = TimelineSubtitlesTrack; const jsx_runtime_1 = require("react/jsx-runtime"); const Media = __importStar(require("@react-av/core")); const react_1 = require("react"); const vtt_core_1 = __importStar(require("@react-av/vtt-core")); const vtt_1 = require("@react-av/vtt"); const TimelineTrack_1 = require("./TimelineTrack"); const TimelineEditor_1 = require("./TimelineEditor"); const TimelineEntryLabel_1 = require("./TimelineEntryLabel"); function Captions({ size = 21 }) { return (0, jsx_runtime_1.jsxs)("svg", { xmlns: "http://www.w3.org/2000/svg", width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", children: [(0, jsx_runtime_1.jsx)("rect", { width: "18", height: "14", x: "3", y: "5", rx: "2", ry: "2" }), (0, jsx_runtime_1.jsx)("path", { d: "M7 15h4M15 15h2M7 11h2M13 11h4" })] }); } function useMediaTextTrack2(id) { const media = Media.useMediaElement(); const [activeCues, setActiveCues] = (0, react_1.useState)([]); const [cues, setCues] = (0, react_1.useState)([]); (0, react_1.useEffect)(() => { var _a, _b, _c; if (!media) return; vtt_core_1.default.ref(media); function update() { if (!media) return; const track = vtt_core_1.default.getTrackById(media, id); if (!track) return; const cues = track.cues; if (!cues) return; const orderedCues = cues.sort((a, b) => { if (a.startTime === b.startTime) return b.endTime - a.endTime; return a.startTime - b.startTime; }); const activeCues = orderedCues.filter(cue => cue.startTime <= media.currentTime && cue.endTime > media.currentTime); setCues(orderedCues.map(cue => ({ cue: cue, start: cue.startTime, end: cue.endTime }))); setActiveCues(activeCues.map(cue => ({ cue: cue, start: cue.startTime, end: cue.endTime }))); } (_a = vtt_core_1.default.getContext(media)) === null || _a === void 0 ? void 0 : _a.tracksChanged.addEventListener("change", update); (_b = vtt_core_1.default.getContext(media)) === null || _b === void 0 ? void 0 : _b.tracksChanged.addEventListener("cuechange", update); (_c = vtt_core_1.default.getContext(media)) === null || _c === void 0 ? void 0 : _c.updateRules.add(update); update(); return () => { var _a, _b, _c; (_a = vtt_core_1.default.getContext(media)) === null || _a === void 0 ? void 0 : _a.tracksChanged.removeEventListener("change", update); (_b = vtt_core_1.default.getContext(media)) === null || _b === void 0 ? void 0 : _b.tracksChanged.removeEventListener("cuechange", update); (_c = vtt_core_1.default.getContext(media)) === null || _c === void 0 ? void 0 : _c.updateRules.delete(update); vtt_core_1.default.deref(media); }; }, [media, id]); return [cues, activeCues]; } const TimelineSubtitleCueEditorContext = (0, react_1.createContext)(null); function useTimelineSubtitleCueEditor() { const context = (0, react_1.useContext)(TimelineSubtitleCueEditorContext); if (!context) throw new Error("useTimelineSubtitleCueEditor must be used within a TimelineSubtitlesTrack component"); return context; } const TimelineSubtitleTrackContext = (0, react_1.createContext)(null); function useTimelineSubtitleTrack() { const context = (0, react_1.useContext)(TimelineSubtitleTrackContext); if (!context) throw new Error("useTimelineSubtitleTrack must be used within a TimelineSubtitlesTrack component"); return context; } function TimelineSubtitlesTrack({ id: defaultID, snap, onTrackCuesChanged, children, labelComponent }) { const { timelineInterval: interval } = (0, TimelineEditor_1.useTimelineEditorContext)(); const textTrackList = (0, vtt_1.useMediaTextTrackList)(); const element = Media.useMediaElement(); const id = (0, react_1.useMemo)(() => defaultID !== null && defaultID !== void 0 ? defaultID : "__reactav__draft", [defaultID]); const [cues] = useMediaTextTrack2(id); const [selectedEntry, setSelectedEntry] = (0, react_1.useState)(); const selectedRef = (0, react_1.useRef)(null); const propertiesRef = (0, react_1.useRef)(null); (0, react_1.useEffect)(() => { if (!element) return; vtt_core_1.default.ref(element); return () => { vtt_core_1.default.deref(element); }; }, [element]); const entries = (0, react_1.useMemo)(() => { return cues.map(cue => ({ start: cue.start, end: cue.end, content: cue.cue.text, symbol: cue.cue })); }, [cues]); (0, react_1.useEffect)(() => { if (!element) return; if (!defaultID) { const track = new vtt_core_1.TextTrack("subtitles", "showing", "English", "en", id); vtt_core_1.default.addTrack(element, track); return () => vtt_core_1.default.removeTrack(element, track); } }, [element, defaultID, id]); const currentTrack = (0, react_1.useMemo)(() => { if (!textTrackList) return undefined; return textTrackList.find(track => track.id === id); }, [textTrackList, id]); function forceRefresh() { var _a; element && ((_a = vtt_core_1.default.getContext(element)) === null || _a === void 0 ? void 0 : _a.tracksChanged.dispatchEvent(new CustomEvent("cuechange", { detail: currentTrack }))); vtt_core_1.default.getContext(element).lastTimestamp = undefined; element && currentTrack && vtt_core_1.default.updateTextTrackDisplay(element, [currentTrack]); // TODO: toVTT should be feature complete, i.e. it should include positioning regions etc. onTrackCuesChanged && currentTrack && onTrackCuesChanged(currentTrack); } function handleCueDelete() { if (!selectedEntry) return; const cue = selectedEntry; console.log(currentTrack); currentTrack === null || currentTrack === void 0 ? void 0 : currentTrack.removeCue(cue); setSelectedEntry(undefined); forceRefresh(); } function handleEraseSubtitles() { if (!currentTrack) return; const cues = currentTrack.cues; if (!cues) return; const oldCues = [...cues]; for (const cue of oldCues) { currentTrack.removeCue(cue); } forceRefresh(); } return (0, jsx_runtime_1.jsxs)(TimelineSubtitleTrackContext.Provider, { value: { track: currentTrack, sync: forceRefresh, clear: handleEraseSubtitles }, children: [(0, jsx_runtime_1.jsx)(TimelineTrack_1.TimelineTrack, { labelComponent: labelComponent !== null && labelComponent !== void 0 ? labelComponent : (0, jsx_runtime_1.jsx)(TimelineEntryLabel_1.TimelineEntryLabel, { icon: (0, jsx_runtime_1.jsx)(Captions, { size: 21 }), label: "Subtitles" }), selectedRef: selectedRef, onDraftCreate: entry => { const start = Math.round(entry.start * 1000) / 1000; const end = Math.round(entry.end * 1000) / 1000; // if start is within 20ms of end, do nothing if (Math.abs(start - end) < interval / 20) return; if (!currentTrack) return; const cue = new vtt_core_1.VTTCue(start, end, "New Subtitle"); currentTrack === null || currentTrack === void 0 ? void 0 : currentTrack.addCue(cue); forceRefresh(); setSelectedEntry(cue); }, onEntryMove: delta => { if (!selectedEntry) return; const cue = selectedEntry; cue.startTime += delta; cue.endTime += delta; if (cue.startTime < 0) { const diff = cue.startTime; cue.startTime = 0; cue.endTime -= diff; } forceRefresh(); }, entries: entries, onEntrySelect: entry => { setSelectedEntry(entry && entry.symbol); }, onEntryDelete: handleCueDelete, onEntryEdit: () => { var _a, _b; (_b = (_a = propertiesRef.current) === null || _a === void 0 ? void 0 : _a.querySelector("textarea")) === null || _b === void 0 ? void 0 : _b.focus(); }, onEntryResize: (start, end) => { if (!selectedEntry) return; const cue = selectedEntry; cue.startTime = start; cue.endTime = end; forceRefresh(); }, selectedSymbol: selectedEntry, snap: snap }), (0, jsx_runtime_1.jsx)(TimelineSubtitleCueEditorContext.Provider, { value: { entry: selectedEntry, focusRef: propertiesRef, deselect: () => setSelectedEntry(undefined), delete: () => { var _a, _b; handleCueDelete(); setSelectedEntry(undefined); (_b = (_a = selectedRef.current) === null || _a === void 0 ? void 0 : _a.nextElementSibling) === null || _b === void 0 ? void 0 : _b.focus(); }, focusTimeline: () => { var _a; return (_a = selectedRef.current) === null || _a === void 0 ? void 0 : _a.focus(); }, sync: forceRefresh }, children: children })] }); } //# sourceMappingURL=TimelineSubtitlesTrack.js.map