import { useState, useEffect, useRef, MouseEvent } from 'react'; import { MediaSetServiceClientImpl, MediaSet } from './generated/media_set'; import { Frames, newRPC } from './App'; import { WaveformCanvas } from './WaveformCanvas'; import { mouseEventToCanvasX } from './Helpers'; import { secsToCanvasX } from './Helpers'; import { from, Observable } from 'rxjs'; import { map } from 'rxjs/operators'; interface Props { mediaSet: MediaSet; height: number; offsetPixels: number; position: number; onSelectionStart: (x1: number) => void; onSelectionChange: (selection: Frames) => void; } enum Mode { Normal, Selecting, Dragging, } const CanvasLogicalWidth = 2_000; const CanvasLogicalHeight = 500; const emptySelection = { start: 0, end: 0 }; // TODO: render position marker during playback export const Overview: React.FC = ({ mediaSet, height, offsetPixels, position, onSelectionStart, onSelectionChange, }: Props) => { const hudCanvasRef = useRef(null); const [peaks, setPeaks] = useState>(from([])); const [mode, setMode] = useState(Mode.Normal); const [selection, setSelection] = useState({ ...emptySelection }); const [newSelection, setNewSelection] = useState({ ...emptySelection }); const [dragStart, setDragStart] = useState(0); // effects // 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(() => { (async function () { 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: Frames; if (mode == Mode.Selecting || mode == Mode.Dragging) { currentSelection = newSelection; } else { currentSelection = selection; } if (currentSelection.start < currentSelection.end) { const x1 = (currentSelection.start / mediaSet.audioFrames) * CanvasLogicalWidth; const x2 = (currentSelection.end / mediaSet.audioFrames) * CanvasLogicalWidth; ctx.beginPath(); ctx.strokeStyle = 'red'; ctx.lineWidth = 4; ctx.fillStyle = 'rgba(255, 255, 255, 0.15)'; ctx.rect(x1, 2, x2 - x1, canvas.height - 10); ctx.fill(); ctx.stroke(); } // draw position marker: const fullSelection = { start: 0, end: mediaSet.audioFrames }; // constantize? const x = secsToCanvasX( position, mediaSet.audioSampleRate, fullSelection ); // should never happen: if (x == null) { return; } ctx.strokeStyle = 'red'; ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineWidth = 4; ctx.lineTo(x, canvas.height - 4); ctx.stroke(); })(); }); // publish event on new selection start useEffect(() => { onSelectionStart(newSelection.start); }, [newSelection]); useEffect(() => { onSelectionChange({ ...selection }); }, [selection]); // handlers const handleMouseDown = (evt: MouseEvent) => { if (mode != Mode.Normal) { return; } const frame = Math.floor( mediaSet.audioFrames * (mouseEventToCanvasX(evt) / evt.currentTarget.width) ); if (frame >= selection.start && frame < selection.end) { setMode(Mode.Dragging); setDragStart(frame); return; } setMode(Mode.Selecting); setNewSelection({ start: frame, end: frame }); }; const handleMouseMove = (evt: MouseEvent) => { if (mode == Mode.Normal) { return; } const frame = Math.floor( mediaSet.audioFrames * (mouseEventToCanvasX(evt) / evt.currentTarget.width) ); if (mode == Mode.Dragging) { const diff = frame - dragStart; const frameCount = selection.end - selection.start; let start = Math.max(0, selection.start + diff); let end = start + frameCount; if (end > mediaSet.audioFrames) { end = mediaSet.audioFrames; start = end - frameCount; } setNewSelection({ start: start, end: end, }); return; } if (frame == newSelection.end) { return; } setNewSelection({ ...newSelection, end: frame }); }; const handleMouseUp = () => { if (mode == Mode.Normal) { return; } setMode(Mode.Normal); setSelection(newSelection); }; // 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, } as React.CSSProperties; return ( <>
); };