clipper/frontend/src/Waveform.tsx

180 lines
4.9 KiB
TypeScript
Raw Normal View History

import { useEffect, useState, useCallback } from 'react';
2021-11-25 18:02:37 +00:00
import { Frames, VideoPosition, newRPC } from './App';
import { MediaSetServiceClientImpl, MediaSet } from './generated/media_set';
2021-10-08 14:38:35 +00:00
import { WaveformCanvas } from './WaveformCanvas';
2021-12-12 10:04:23 +00:00
import { Selection, HudCanvas, EmptySelectionAction } from './HudCanvas';
2021-11-06 20:52:47 +00:00
import { from, Observable } from 'rxjs';
import { bufferCount } from 'rxjs/operators';
2021-10-08 14:38:35 +00:00
interface Props {
mediaSet: MediaSet;
2021-11-25 18:02:37 +00:00
position: VideoPosition;
2021-10-08 14:38:35 +00:00
viewport: Frames;
offsetPixels: number;
onSelectionChange: (selection: Selection) => void;
2021-10-08 14:38:35 +00:00
}
export const CanvasLogicalWidth = 2000;
export const CanvasLogicalHeight = 500;
export const Waveform: React.FC<Props> = ({
mediaSet,
position,
viewport,
offsetPixels,
onSelectionChange,
2021-10-08 14:38:35 +00:00
}: Props) => {
2021-11-06 20:52:47 +00:00
const [peaks, setPeaks] = useState<Observable<number[]>>(from([]));
const [selectedFrames, setSelectedFrames] = useState({ start: 0, end: 0 });
const [selectedPixels, setSelectedPixels] = useState({
start: 0,
end: 0,
});
const [positionPixels, setPositionPixels] = useState<number | null>(0);
2021-10-08 14:38:35 +00:00
// effects
// load peaks on MediaSet change
useEffect(() => {
(async function () {
if (mediaSet == null) {
return;
}
if (viewport.start >= viewport.end) {
return;
2021-10-08 14:38:35 +00:00
}
2021-11-30 19:41:34 +00:00
console.log('fetch audio segment, frames', viewport);
const service = new MediaSetServiceClientImpl(newRPC());
const segment = await service.GetAudioSegment({
id: mediaSet.id,
numBins: CanvasLogicalWidth,
startFrame: viewport.start,
endFrame: viewport.end,
});
console.log('got segment', segment);
const peaks = from(segment.peaks).pipe(
bufferCount(mediaSet.audioChannels)
);
setPeaks(peaks);
2021-10-08 14:38:35 +00:00
})();
}, [viewport]);
2021-10-08 14:38:35 +00:00
// convert position to canvas pixels
2021-10-08 14:38:35 +00:00
useEffect(() => {
const frame = Math.round(position.currentTime * mediaSet.audioSampleRate);
if (frame < viewport.start || frame > viewport.end) {
setPositionPixels(null);
2021-10-08 14:38:35 +00:00
return;
}
const logicalPixelsPerFrame =
CanvasLogicalWidth / (viewport.end - viewport.start);
const positionPixels = (frame - viewport.start) * logicalPixelsPerFrame;
setPositionPixels(positionPixels);
}, [mediaSet, position, viewport]);
2021-10-08 14:38:35 +00:00
// update selectedPixels on viewport change
useEffect(() => {
const start = frameToCanvasX(selectedFrames.start);
const end = frameToCanvasX(selectedFrames.end);
// more verbose than it has to be to make TypeScript happy
if (start == null && end == null) {
setSelectedPixels({ start: 0, end: 0 });
} else if (start == null && end != null) {
setSelectedPixels({ start: 0, end: end });
} else if (start != null && end == null) {
setSelectedPixels({ start: 0, end: CanvasLogicalWidth });
} else if (start != null && end != null) {
setSelectedPixels({ start, end });
} else {
console.error('unreachable');
2021-10-08 14:38:35 +00:00
}
}, [viewport]);
2021-10-08 14:38:35 +00:00
// handlers
2021-10-08 14:38:35 +00:00
const handleSelectionChange = useCallback(
(selection: Selection) => {
setSelectedPixels(selection);
2021-10-08 14:38:35 +00:00
const framesPerPixel =
(viewport.end - viewport.start) / CanvasLogicalWidth;
const selectedFrames = {
start: Math.round(viewport.start + selection.start * framesPerPixel),
end: Math.round(viewport.start + selection.end * framesPerPixel),
};
setSelectedFrames(selectedFrames);
onSelectionChange(selectedFrames);
},
[viewport]
);
2021-10-08 14:38:35 +00:00
// helpers
const frameToCanvasX = useCallback(
(frame: number): number | null => {
if (mediaSet == null) {
return null;
}
if (frame < viewport.start || frame > viewport.end) {
return null;
}
const pixelsPerFrame =
CanvasLogicalWidth / (viewport.end - viewport.start);
return (frame - viewport.start) * pixelsPerFrame;
},
[mediaSet, viewport]
);
2021-10-08 14:38:35 +00:00
// render component
const containerStyles = {
background: 'black',
margin: '0 ' + offsetPixels + 'px',
flexGrow: 1,
position: 'relative',
} as React.CSSProperties;
const hudStyles = {
borderLineWidth: 0,
borderStrokeStyle: 'transparent',
positionLineWidth: 6,
positionStrokeStyle: 'red',
};
2021-10-08 14:38:35 +00:00
return (
<>
<div style={containerStyles}>
<WaveformCanvas
peaks={peaks}
channels={mediaSet.audioChannels}
width={CanvasLogicalWidth}
height={CanvasLogicalHeight}
strokeStyle="green"
fillStyle="black"
zIndex={0}
alpha={1}
></WaveformCanvas>
<HudCanvas
2021-10-08 14:38:35 +00:00
width={CanvasLogicalWidth}
height={CanvasLogicalHeight}
zIndex={1}
2021-12-12 10:04:23 +00:00
emptySelectionAction={EmptySelectionAction.SelectNothing}
styles={hudStyles}
position={positionPixels}
selection={selectedPixels}
onSelectionChange={handleSelectionChange}
/>
2021-10-08 14:38:35 +00:00
</div>
</>
);
};