clipper/frontend/src/App.tsx

227 lines
5.9 KiB
TypeScript
Raw Normal View History

2021-10-22 19:30:09 +00:00
import { grpc } from '@improbable-eng/grpc-web';
2021-11-02 16:20:47 +00:00
// import {
// MediaSet as MediaSetPb,
// GetRequest,
// GetAudioRequest,
// GetAudioProgress,
// } from './generated/media_set_pb';
2021-10-22 19:30:09 +00:00
import {
2021-11-02 16:20:47 +00:00
MediaSet,
GrpcWebImpl,
MediaSetServiceClientImpl,
} from './generated/media_set';
2021-10-29 12:52:31 +00:00
2021-10-08 14:38:35 +00:00
import { useState, useEffect } from 'react';
import { VideoPreview } from './VideoPreview';
import { Overview } from './Overview';
import { Waveform } from './Waveform';
import { ControlBar } from './ControlBar';
import { SeekBar } from './SeekBar';
2021-09-06 10:17:50 +00:00
import './App.css';
2021-11-02 16:20:47 +00:00
import { Duration } from './generated/google/protobuf/duration';
2021-09-06 14:25:23 +00:00
2021-10-29 12:52:31 +00:00
const grpcHost = 'http://localhost:8888';
2021-11-02 16:20:47 +00:00
// ported from backend, where should they live?
const thumbnailWidth = 177;
const thumbnailHeight = 100;
2021-10-08 14:38:35 +00:00
// Frames represents a selection of audio frames.
export interface Frames {
start: number;
end: number;
}
2021-09-06 10:17:50 +00:00
2021-09-30 19:08:48 +00:00
function App(): JSX.Element {
2021-10-08 14:38:35 +00:00
const [mediaSet, setMediaSet] = useState<MediaSet | null>(null);
const [video, _setVideo] = useState(document.createElement('video'));
const [position, setPosition] = useState(0);
const [viewport, setViewport] = useState({ start: 0, end: 0 });
// effects
// TODO: error handling
const videoID = new URLSearchParams(window.location.search).get('video_id');
2021-10-22 19:30:09 +00:00
if (videoID == null) {
return <></>;
}
2021-10-08 14:38:35 +00:00
// fetch mediaset on page load:
useEffect(() => {
(async function () {
2021-11-02 16:20:47 +00:00
const rpc = new GrpcWebImpl('http://localhost:8888', {});
const service = new MediaSetServiceClientImpl(rpc);
const mediaSet = await service.Get({ youtubeId: videoID });
2021-10-22 19:30:09 +00:00
2021-10-29 12:52:31 +00:00
console.log('got media set:', mediaSet);
2021-11-02 16:20:47 +00:00
setMediaSet(mediaSet);
2021-11-01 05:28:40 +00:00
// const handleProgress = (progress: GetAudioProgress) => {
// console.log('got progress', progress);
// };
2021-10-29 12:52:31 +00:00
2021-11-01 05:28:40 +00:00
// const audioRequest = new GetAudioRequest();
// audioRequest.setId(videoID);
// audioRequest.setNumBins(1000);
// GetMediaSetAudio(grpcHost, audioRequest, handleProgress);
2021-10-22 19:30:09 +00:00
// console.log('fetching media...');
// const resp = await fetch(
// `http://localhost:8888/api/media_sets/${videoID}`
// );
// const respBody = await resp.json();
// if (respBody.error) {
// console.log('error fetching media set:', respBody.error);
// return;
// }
// const mediaSet = {
// id: respBody.id,
// source: respBody.source,
// audio: {
// sampleRate: respBody.audio.sample_rate,
// bytes: respBody.audio.bytes,
// frames: respBody.audio.frames,
// channels: respBody.audio.channels,
// },
// video: {
// bytes: respBody.video.bytes,
// thumbnailWidth: respBody.video.thumbnail_width,
// thumbnailHeight: respBody.video.thumbnail_height,
// durationMillis: Math.floor(respBody.video.duration / 1000 / 1000),
// },
// };
// setMediaSet(mediaSet);
2021-10-08 14:38:35 +00:00
})();
}, []);
// setup player on first page load only:
useEffect(() => {
setInterval(() => {
setPosition(video.currentTime);
}, 100);
}, []);
// load video when MediaSet is loaded:
useEffect(() => {
if (mediaSet == null) {
return;
}
2021-11-02 16:20:47 +00:00
return;
2021-10-08 14:38:35 +00:00
video.src = `http://localhost:8888/api/media_sets/${videoID}/video`;
video.muted = false;
video.volume = 1;
console.log('set video src', video.src);
}, [mediaSet]);
// set viewport when MediaSet is loaded:
useEffect(() => {
if (mediaSet == null) {
return;
}
2021-11-02 16:20:47 +00:00
setViewport({ start: 0, end: mediaSet.audioFrames });
2021-10-08 14:38:35 +00:00
}, [mediaSet]);
useEffect(() => {
console.debug('viewport updated', viewport);
}, [viewport]);
// handlers
const handleOverviewSelectionChange = (selection: Frames) => {
console.log('in handleOverviewSelectionChange', selection);
if (mediaSet == null) {
return;
}
if (selection.start >= selection.end) {
2021-11-02 16:20:47 +00:00
setViewport({ start: 0, end: mediaSet.audioFrames });
2021-10-08 14:38:35 +00:00
return;
}
setViewport({ ...selection });
};
// render component
const containerStyles = {
border: '1px solid black',
width: '90%',
margin: '1em auto',
minHeight: '500px',
height: '700px',
display: 'flex',
flexDirection: 'column',
} as React.CSSProperties;
2021-11-02 16:20:47 +00:00
const offsetPixels = Math.floor(thumbnailWidth / 2);
2021-10-08 14:38:35 +00:00
if (mediaSet == null) {
// TODO: improve
return <></>;
}
2021-09-06 10:17:50 +00:00
return (
2021-10-08 14:38:35 +00:00
<>
<div className="App">
<div style={containerStyles}>
<ControlBar
onPlay={() => {
video.play();
}}
onPause={() => {
video.pause();
}}
/>
<Overview
mediaSet={mediaSet}
offsetPixels={offsetPixels}
height={80}
position={position}
onSelectionStart={(x1: number) => {
console.log('onSelectionStart', x1);
}}
onSelectionChange={handleOverviewSelectionChange}
/>
<Waveform
mediaSet={mediaSet}
position={position}
viewport={viewport}
offsetPixels={offsetPixels}
/>
<SeekBar
position={video.currentTime}
2021-11-02 16:20:47 +00:00
duration={mediaSet.audioFrames / mediaSet.audioSampleRate}
2021-10-08 14:38:35 +00:00
offsetPixels={offsetPixels}
onPositionChanged={(position: number) => {
video.currentTime = position;
}}
/>
<VideoPreview
video={video}
position={position}
2021-11-02 16:20:47 +00:00
duration={millisFromDuration(mediaSet.videoDuration)}
height={thumbnailHeight}
2021-10-08 14:38:35 +00:00
/>
</div>
</div>
</>
2021-09-06 10:17:50 +00:00
);
}
export default App;
2021-11-02 16:20:47 +00:00
function millisFromDuration(dur?: Duration): number {
if (dur == undefined) {
return 0;
}
return Math.floor(dur.seconds * 1000.0 + dur.nanos / 1000.0 / 1000.0);
}