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-09-06 14:25:23 +00:00
|
|
|
|
2021-10-08 14:38:35 +00:00
|
|
|
// Audio corresponds to media.Audio.
|
|
|
|
export interface Audio {
|
|
|
|
bytes: number;
|
|
|
|
channels: number;
|
|
|
|
frames: number;
|
|
|
|
sampleRate: number;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Video corresponds to media.Video.
|
|
|
|
export interface Video {
|
|
|
|
bytes: number;
|
|
|
|
thumbnailWidth: number;
|
|
|
|
thumbnailHeight: number;
|
|
|
|
durationMillis: number;
|
|
|
|
}
|
|
|
|
|
|
|
|
// MediaSet corresponds to media.MediaSet.
|
|
|
|
export interface MediaSet {
|
|
|
|
id: string;
|
|
|
|
source: string;
|
|
|
|
audio: Audio;
|
|
|
|
video: Video;
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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');
|
|
|
|
|
|
|
|
// fetch mediaset on page load:
|
|
|
|
useEffect(() => {
|
|
|
|
(async function () {
|
|
|
|
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);
|
|
|
|
})();
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
// setup player on first page load only:
|
|
|
|
useEffect(() => {
|
|
|
|
setInterval(() => {
|
|
|
|
setPosition(video.currentTime);
|
|
|
|
}, 100);
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
// load video when MediaSet is loaded:
|
|
|
|
useEffect(() => {
|
|
|
|
if (mediaSet == null) {
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
|
|
|
setViewport({ start: 0, end: mediaSet.audio.frames });
|
|
|
|
}, [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.audio.frames });
|
|
|
|
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;
|
|
|
|
|
|
|
|
let offsetPixels = 75;
|
|
|
|
if (mediaSet != null) {
|
|
|
|
offsetPixels = Math.floor(mediaSet.video.thumbnailWidth / 2);
|
|
|
|
}
|
|
|
|
|
|
|
|
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}
|
|
|
|
duration={mediaSet.audio.frames / mediaSet.audio.sampleRate}
|
|
|
|
offsetPixels={offsetPixels}
|
|
|
|
onPositionChanged={(position: number) => {
|
|
|
|
video.currentTime = position;
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
|
|
|
|
<VideoPreview
|
|
|
|
video={video}
|
|
|
|
position={position}
|
|
|
|
duration={mediaSet.video.durationMillis}
|
|
|
|
height={mediaSet.video.thumbnailHeight}
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</>
|
2021-09-06 10:17:50 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
export default App;
|