UNPKG

@react-av/editor

Version:

Editor Timeline Components built on React AV.

191 lines 9.39 kB
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; import * as Media from '@react-av/core'; import { createContext, useContext, useEffect, useMemo, useRef, useState } from "react"; import VTT, { TextTrack, VTTCue } from "@react-av/vtt-core"; import { useMediaTextTrackList } from "@react-av/vtt"; import { TimelineTrack } from "./TimelineTrack"; import { useTimelineEditorContext } from './TimelineEditor'; import { TimelineEntryLabel } from './TimelineEntryLabel'; function Captions({ size = 21 }) { return _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: [_jsx("rect", { width: "18", height: "14", x: "3", y: "5", rx: "2", ry: "2" }), _jsx("path", { d: "M7 15h4M15 15h2M7 11h2M13 11h4" })] }); } function useMediaTextTrack2(id) { const media = Media.useMediaElement(); const [activeCues, setActiveCues] = useState([]); const [cues, setCues] = useState([]); useEffect(() => { var _a, _b, _c; if (!media) return; VTT.ref(media); function update() { if (!media) return; const track = VTT.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.getContext(media)) === null || _a === void 0 ? void 0 : _a.tracksChanged.addEventListener("change", update); (_b = VTT.getContext(media)) === null || _b === void 0 ? void 0 : _b.tracksChanged.addEventListener("cuechange", update); (_c = VTT.getContext(media)) === null || _c === void 0 ? void 0 : _c.updateRules.add(update); update(); return () => { var _a, _b, _c; (_a = VTT.getContext(media)) === null || _a === void 0 ? void 0 : _a.tracksChanged.removeEventListener("change", update); (_b = VTT.getContext(media)) === null || _b === void 0 ? void 0 : _b.tracksChanged.removeEventListener("cuechange", update); (_c = VTT.getContext(media)) === null || _c === void 0 ? void 0 : _c.updateRules.delete(update); VTT.deref(media); }; }, [media, id]); return [cues, activeCues]; } const TimelineSubtitleCueEditorContext = createContext(null); export function useTimelineSubtitleCueEditor() { const context = useContext(TimelineSubtitleCueEditorContext); if (!context) throw new Error("useTimelineSubtitleCueEditor must be used within a TimelineSubtitlesTrack component"); return context; } const TimelineSubtitleTrackContext = createContext(null); export function useTimelineSubtitleTrack() { const context = useContext(TimelineSubtitleTrackContext); if (!context) throw new Error("useTimelineSubtitleTrack must be used within a TimelineSubtitlesTrack component"); return context; } export function TimelineSubtitlesTrack({ id: defaultID, snap, onTrackCuesChanged, children, labelComponent }) { const { timelineInterval: interval } = useTimelineEditorContext(); const textTrackList = useMediaTextTrackList(); const element = Media.useMediaElement(); const id = useMemo(() => defaultID !== null && defaultID !== void 0 ? defaultID : "__reactav__draft", [defaultID]); const [cues] = useMediaTextTrack2(id); const [selectedEntry, setSelectedEntry] = useState(); const selectedRef = useRef(null); const propertiesRef = useRef(null); useEffect(() => { if (!element) return; VTT.ref(element); return () => { VTT.deref(element); }; }, [element]); const entries = useMemo(() => { return cues.map(cue => ({ start: cue.start, end: cue.end, content: cue.cue.text, symbol: cue.cue })); }, [cues]); useEffect(() => { if (!element) return; if (!defaultID) { const track = new TextTrack("subtitles", "showing", "English", "en", id); VTT.addTrack(element, track); return () => VTT.removeTrack(element, track); } }, [element, defaultID, id]); const currentTrack = useMemo(() => { if (!textTrackList) return undefined; return textTrackList.find(track => track.id === id); }, [textTrackList, id]); function forceRefresh() { var _a; element && ((_a = VTT.getContext(element)) === null || _a === void 0 ? void 0 : _a.tracksChanged.dispatchEvent(new CustomEvent("cuechange", { detail: currentTrack }))); VTT.getContext(element).lastTimestamp = undefined; element && currentTrack && VTT.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 _jsxs(TimelineSubtitleTrackContext.Provider, { value: { track: currentTrack, sync: forceRefresh, clear: handleEraseSubtitles }, children: [_jsx(TimelineTrack, { labelComponent: labelComponent !== null && labelComponent !== void 0 ? labelComponent : _jsx(TimelineEntryLabel, { icon: _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 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 }), _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