2021-12-11 16:25:43 +00:00
|
|
|
import { useEffect, useState, useCallback } from 'react';
|
2021-11-25 18:02:37 +00:00
|
|
|
import { Frames, VideoPosition, newRPC } from './App';
|
2021-11-17 17:53:27 +00:00
|
|
|
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';
|
2021-11-17 17:53:27 +00:00
|
|
|
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;
|
2021-12-11 16:25:43 +00:00
|
|
|
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,
|
2021-12-11 16:25:43 +00:00
|
|
|
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([]));
|
2021-12-11 16:25:43 +00:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2021-11-17 17:53:27 +00:00
|
|
|
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);
|
2021-11-17 17:53:27 +00:00
|
|
|
|
|
|
|
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
|
|
|
})();
|
2021-11-17 17:53:27 +00:00
|
|
|
}, [viewport]);
|
2021-10-08 14:38:35 +00:00
|
|
|
|
2021-12-11 16:25:43 +00:00
|
|
|
// convert position to canvas pixels
|
2021-10-08 14:38:35 +00:00
|
|
|
useEffect(() => {
|
2021-12-11 16:25:43 +00:00
|
|
|
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;
|
|
|
|
}
|
2021-12-11 16:25:43 +00:00
|
|
|
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
|
|
|
|
2021-12-11 16:25:43 +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
|
|
|
}
|
2021-12-11 16:25:43 +00:00
|
|
|
}, [viewport]);
|
2021-10-08 14:38:35 +00:00
|
|
|
|
2021-12-11 16:25:43 +00:00
|
|
|
// handlers
|
2021-10-08 14:38:35 +00:00
|
|
|
|
2021-12-11 16:25:43 +00:00
|
|
|
const handleSelectionChange = useCallback(
|
|
|
|
(selection: Selection) => {
|
|
|
|
setSelectedPixels(selection);
|
2021-10-08 14:38:35 +00:00
|
|
|
|
2021-12-11 16:25:43 +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
|
|
|
|
2021-12-11 16:25:43 +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;
|
|
|
|
|
2021-12-11 16:25:43 +00:00
|
|
|
const hudStyles = {
|
|
|
|
borderLineWidth: 0,
|
|
|
|
borderStrokeStyle: 'transparent',
|
|
|
|
positionLineWidth: 6,
|
|
|
|
positionStrokeStyle: 'red',
|
|
|
|
};
|
2021-10-08 14:38:35 +00:00
|
|
|
|
|
|
|
return (
|
|
|
|
<>
|
|
|
|
<div style={containerStyles}>
|
2021-12-04 04:34:17 +00:00
|
|
|
<WaveformCanvas
|
|
|
|
peaks={peaks}
|
|
|
|
channels={mediaSet.audioChannels}
|
|
|
|
width={CanvasLogicalWidth}
|
|
|
|
height={CanvasLogicalHeight}
|
|
|
|
strokeStyle="green"
|
|
|
|
fillStyle="black"
|
|
|
|
zIndex={0}
|
|
|
|
alpha={1}
|
|
|
|
></WaveformCanvas>
|
2021-12-11 16:25:43 +00:00
|
|
|
<HudCanvas
|
2021-10-08 14:38:35 +00:00
|
|
|
width={CanvasLogicalWidth}
|
|
|
|
height={CanvasLogicalHeight}
|
2021-12-11 16:25:43 +00:00
|
|
|
zIndex={1}
|
2021-12-12 10:04:23 +00:00
|
|
|
emptySelectionAction={EmptySelectionAction.SelectNothing}
|
2021-12-11 16:25:43 +00:00
|
|
|
styles={hudStyles}
|
|
|
|
position={positionPixels}
|
|
|
|
selection={selectedPixels}
|
|
|
|
onSelectionChange={handleSelectionChange}
|
|
|
|
/>
|
2021-10-08 14:38:35 +00:00
|
|
|
</div>
|
|
|
|
</>
|
|
|
|
);
|
|
|
|
};
|