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