import { useState, useEffect, useRef, MouseEvent } from 'react'; import { MediaSetServiceClientImpl, MediaSet } from './generated/media_set'; import { Frames, newRPC, VideoPosition } from './App'; import { WaveformCanvas } from './WaveformCanvas'; import { from, Observable } from 'rxjs'; import { map } from 'rxjs/operators'; export interface Selection { start: number; end: number; } interface Props { mediaSet: MediaSet; height: number; offsetPixels: number; position: VideoPosition; onSelectionChange: (selection: Frames) => void; } enum Mode { Normal, Selecting, Dragging, ResizingStart, ResizingEnd, } enum HoverState { Normal, OverSelectionStart, OverSelectionEnd, OverSelection, } const CanvasLogicalWidth = 2_000; const CanvasLogicalHeight = 500; const emptySelection = { start: 0, end: 0 }; export const Overview: React.FC = ({ mediaSet, height, offsetPixels, position, onSelectionChange, }: Props) => { const hudCanvasRef = useRef(null); const [peaks, setPeaks] = useState>(from([])); const [mode, setMode] = useState(Mode.Normal); const [hoverState, setHoverState] = useState(HoverState.Normal); const [newSelection, setNewSelection] = useState({ ...emptySelection, }); const [selection, setSelection] = useState({ start: 0, end: 100 }); const [cursor, setCursor] = useState('auto'); const moveOffsetX = useRef(0); // effects // handle global mouse up. useEffect(() => { window.addEventListener('mouseup', handleMouseUp); return () => { window.removeEventListener('mouseup', handleMouseUp); }; }, [mode, newSelection]); // publish onSelectionChange event useEffect(() => { if (mediaSet == null) { return; } const canvas = hudCanvasRef.current; if (canvas == null) { console.error('no hud canvas ref available'); return; } const ctx = canvas.getContext('2d'); if (ctx == null) { console.error('no hud 2d context available'); return; } const width = canvas.getBoundingClientRect().width; const selectionPercent = { start: (selection.start / width) * 100, end: (selection.end / width) * 100, }; onSelectionChange(selectionPercent); }, [selection]); // load peaks on mediaset change useEffect(() => { (async function () { if (mediaSet == null) { return; } const canvas = hudCanvasRef.current; if (canvas == null) { console.error('no hud canvas ref available'); return; } const ctx = canvas.getContext('2d'); if (ctx == null) { console.error('no hud 2d context available'); return; } console.log('fetching audio...'); const service = new MediaSetServiceClientImpl(newRPC()); const audioProgressStream = service.GetAudio({ id: mediaSet.id, numBins: CanvasLogicalWidth, }); const peaks = audioProgressStream.pipe(map((progress) => progress.peaks)); setPeaks(peaks); })(); }, [mediaSet]); // draw the overview HUD useEffect(() => { requestAnimationFrame(() => { const canvas = hudCanvasRef.current; if (canvas == null) { console.error('no hud canvas ref available'); return; } const ctx = canvas.getContext('2d'); if (ctx == null) { console.error('no hud 2d context available'); return; } ctx.clearRect(0, 0, canvas.width, canvas.height); // draw selection let currentSelection: Selection; if ( mode == Mode.Selecting || mode == Mode.Dragging || mode == Mode.ResizingStart || mode == Mode.ResizingEnd ) { currentSelection = newSelection; } else { currentSelection = selection; } const elementWidth = canvas.getBoundingClientRect().width; const start = (currentSelection.start / elementWidth) * CanvasLogicalWidth; const end = (currentSelection.end / elementWidth) * CanvasLogicalWidth; ctx.beginPath(); ctx.strokeStyle = 'red'; ctx.lineWidth = 4; const alpha = hoverState == HoverState.OverSelection ? '0.15' : '0.13'; ctx.fillStyle = `rgba(255, 255, 255, ${alpha})`; ctx.rect(start, 2, end - start, canvas.height - 10); ctx.fill(); ctx.stroke(); // draw position marker const markerX = canvas.width * (position.percent / 100); ctx.beginPath(); ctx.moveTo(markerX, 0); ctx.lineWidth = 4; ctx.lineTo(markerX, canvas.height - 4); ctx.stroke(); }); }); // handlers const isHoveringSelectionStart = (elementX: number): boolean => { return elementX > selection.start - 10 && elementX < selection.start + 10; }; const isHoveringSelectionEnd = (elementX: number): boolean => { return elementX > selection.end - 10 && elementX < selection.end + 10; }; const isHoveringSelection = (elementX: number): boolean => { return elementX >= selection.start && elementX <= selection.end; }; const handleMouseDown = (evt: MouseEvent) => { if (mode != Mode.Normal) { return; } const elementX = Math.round( evt.clientX - evt.currentTarget.getBoundingClientRect().x ); if (isHoveringSelectionStart(elementX)) { setMode(Mode.ResizingStart); moveOffsetX.current = elementX; return; } else if (isHoveringSelectionEnd(elementX)) { setMode(Mode.ResizingEnd); moveOffsetX.current = elementX; return; } else if (isHoveringSelection(elementX)) { setMode(Mode.Dragging); setCursor('pointer'); moveOffsetX.current = elementX; return; } setMode(Mode.Selecting); setCursor('col-resize'); moveOffsetX.current = elementX; setNewSelection({ start: elementX, end: elementX }); }; const handleMouseMove = (evt: MouseEvent) => { const x = Math.round( evt.clientX - evt.currentTarget.getBoundingClientRect().x ); switch (mode) { case Mode.Normal: { if (isHoveringSelectionStart(x)) { setHoverState(HoverState.OverSelectionStart); setCursor('col-resize'); } else if (isHoveringSelectionEnd(x)) { setHoverState(HoverState.OverSelectionEnd); setCursor('col-resize'); } else if (isHoveringSelection(x)) { setHoverState(HoverState.OverSelection); setCursor('pointer'); } else { setCursor('auto'); } break; } case Mode.ResizingStart: { const diff = x - moveOffsetX.current; const start = selection.start + diff; if (start > selection.end) { setNewSelection({ start: selection.end, end: start }); break; } setNewSelection({ ...newSelection, start: start }); break; } case Mode.ResizingEnd: { const diff = x - moveOffsetX.current; const start = selection.end + diff; if (start < selection.start) { setNewSelection({ start: Math.max(0, start), end: selection.start }); break; } setNewSelection({ ...newSelection, end: start }); break; } case Mode.Dragging: { const diff = x - moveOffsetX.current; const width = selection.end - selection.start; let start = Math.max(0, selection.start + diff); let end = start + width; if (end > evt.currentTarget.getBoundingClientRect().width) { end = evt.currentTarget.getBoundingClientRect().width; start = end - width; } setNewSelection({ start: start, end: end }); break; } case Mode.Selecting: { if (x < moveOffsetX.current) { setNewSelection({ start: x, end: moveOffsetX.current, }); } else { setNewSelection({ start: moveOffsetX.current, end: x }); } break; } } }; const handleMouseUp = () => { if (mode == Mode.Normal) { return; } setMode(Mode.Normal); setCursor('auto'); if (newSelection.start == newSelection.end) { setSelection({ start: newSelection.start, end: newSelection.end + 5 }); return; } if (newSelection.start == newSelection.end) { setSelection({ ...emptySelection }); return; } setSelection({ ...newSelection }); }; const handleMouseLeave = (_evt: MouseEvent) => { setHoverState(HoverState.Normal); }; // render component const containerStyles = { flexGrow: 0, position: 'relative', margin: `0 ${offsetPixels}px`, height: `${height}px`, } as React.CSSProperties; const hudCanvasStyles = { position: 'absolute', width: '100%', height: '100%', display: 'block', zIndex: 2, cursor: cursor, } as React.CSSProperties; return ( <>
); };