@react-av/editor
Version:
Editor Timeline Components built on React AV.
191 lines • 9.39 kB
JavaScript
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