import { grpc } from '@improbable-eng/grpc-web'; // import { // MediaSet as MediaSetPb, // GetRequest, // GetAudioRequest, // GetAudioProgress, // } from './generated/media_set_pb'; import { MediaSet, GrpcWebImpl, MediaSetServiceClientImpl, } from './generated/media_set'; 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'; import './App.css'; import { Duration } from './generated/google/protobuf/duration'; // ported from backend, where should they live? const thumbnailWidth = 177; const thumbnailHeight = 100; const initialViewportSeconds = 10; // Frames represents a selection of audio frames. export interface Frames { start: number; end: number; } function App(): JSX.Element { const [mediaSet, setMediaSet] = useState(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'); if (videoID == null) { return <>; } // fetch mediaset on page load: useEffect(() => { (async function () { const rpc = newRPC(); const service = new MediaSetServiceClientImpl(rpc); const mediaSet = await service.Get({ youtubeId: videoID }); console.log('got media set:', mediaSet); setMediaSet(mediaSet); })(); }, []); // setup player on first page load only: useEffect(() => { setInterval(() => { setPosition(video.currentTime); }, 100); }, []); // load video when MediaSet is loaded: useEffect(() => { if (mediaSet == null) { return; } return; 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; } const numFrames = Math.min( mediaSet.audioSampleRate * initialViewportSeconds, mediaSet.audioFrames ); setViewport({ start: 0, end: numFrames }); }, [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) { setViewport({ start: 0, end: mediaSet.audioFrames }); 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; const offsetPixels = Math.floor(thumbnailWidth / 2); if (mediaSet == null) { // TODO: improve return <>; } return ( <>
{ video.play(); }} onPause={() => { video.pause(); }} /> { console.log('onSelectionStart', x1); }} onSelectionChange={handleOverviewSelectionChange} /> { video.currentTime = position; }} />
); } export default App; function millisFromDuration(dur?: Duration): number { if (dur == undefined) { return 0; } return Math.floor(dur.seconds * 1000.0 + dur.nanos / 1000.0 / 1000.0); } const grpcHost = 'http://localhost:8888'; export function newRPC(): GrpcWebImpl { return new GrpcWebImpl(grpcHost, {}); }