256 lines
6.3 KiB
TypeScript
256 lines
6.3 KiB
TypeScript
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<Props> = ({
|
|
mediaSet,
|
|
height,
|
|
offsetPixels,
|
|
position,
|
|
onSelectionStart,
|
|
onSelectionChange,
|
|
}: Props) => {
|
|
const hudCanvasRef = useRef<HTMLCanvasElement>(null);
|
|
const [peaks, setPeaks] = useState<Observable<number[]>>(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<HTMLCanvasElement>) => {
|
|
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<HTMLCanvasElement>) => {
|
|
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 (
|
|
<>
|
|
<div style={containerStyles}>
|
|
<WaveformCanvas
|
|
peaks={peaks}
|
|
channels={mediaSet.audioChannels}
|
|
width={CanvasLogicalWidth}
|
|
height={CanvasLogicalHeight}
|
|
strokeStyle="black"
|
|
fillStyle="#003300"
|
|
zIndex={1}
|
|
alpha={1}
|
|
></WaveformCanvas>
|
|
<canvas
|
|
ref={hudCanvasRef}
|
|
width={CanvasLogicalWidth}
|
|
height={CanvasLogicalHeight}
|
|
style={hudCanvasStyles}
|
|
onMouseDown={handleMouseDown}
|
|
onMouseMove={handleMouseMove}
|
|
onMouseUp={handleMouseUp}
|
|
></canvas>
|
|
</div>
|
|
</>
|
|
);
|
|
};
|