156 lines
4.3 KiB
TypeScript
156 lines
4.3 KiB
TypeScript
import { MediaSet, MediaSetServiceClientImpl } from './generated/media_set';
|
|
import { newRPC } from './App';
|
|
import { PlayState } from './AppState';
|
|
import { useEffect, useRef } from 'react';
|
|
import millisFromDuration from './helpers/millisFromDuration';
|
|
|
|
interface Props {
|
|
mediaSet: MediaSet;
|
|
playState: PlayState;
|
|
audioSrc: string;
|
|
videoSrc: string;
|
|
// used to jump to a new position:
|
|
currentTime?: number;
|
|
onPositionChanged: (currentTime: number) => void;
|
|
}
|
|
|
|
const triggerCallbackIntervalMillis = 20;
|
|
|
|
// eslint is complaining about prop validation which doesn't make much sense.
|
|
/* eslint-disable react/prop-types */
|
|
export const Player: React.FC<Props> = ({
|
|
mediaSet,
|
|
playState,
|
|
audioSrc,
|
|
videoSrc,
|
|
currentTime,
|
|
onPositionChanged,
|
|
}) => {
|
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
|
|
|
const audioRef = useRef(new Audio());
|
|
const videoRef = useRef(document.createElement('video'));
|
|
|
|
useEffect(() => {
|
|
setInterval(() => {
|
|
if (audioRef.current.paused) {
|
|
return;
|
|
}
|
|
|
|
onPositionChanged(audioRef.current.currentTime);
|
|
}, triggerCallbackIntervalMillis);
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (playState == PlayState.Paused && !audioRef.current.paused) {
|
|
audioRef.current.pause();
|
|
videoRef.current.pause();
|
|
return;
|
|
}
|
|
|
|
if (playState == PlayState.Playing && audioRef.current.paused) {
|
|
audioRef.current.play();
|
|
videoRef.current.play();
|
|
return;
|
|
}
|
|
}, [playState]);
|
|
|
|
useEffect(() => {
|
|
if (audioSrc == '') {
|
|
return;
|
|
}
|
|
audioRef.current.src = audioSrc;
|
|
console.log('set audio src', audioSrc);
|
|
}, [audioSrc]);
|
|
|
|
useEffect(() => {
|
|
if (videoSrc == '') {
|
|
return;
|
|
}
|
|
videoRef.current.src = videoSrc;
|
|
console.log('set video src', videoSrc);
|
|
}, [videoSrc]);
|
|
|
|
useEffect(() => {
|
|
if (currentTime == undefined) {
|
|
return;
|
|
}
|
|
audioRef.current.currentTime = currentTime;
|
|
videoRef.current.currentTime = currentTime;
|
|
onPositionChanged(currentTime);
|
|
}, [currentTime]);
|
|
|
|
// render canvas
|
|
useEffect(() => {
|
|
// TODO: not sure if requestAnimationFrame is recommended here.
|
|
requestAnimationFrame(() => {
|
|
(async function () {
|
|
if (!mediaSet) {
|
|
return;
|
|
}
|
|
|
|
const canvas = canvasRef.current;
|
|
if (canvas == null) {
|
|
console.error('no canvas ref available');
|
|
return;
|
|
}
|
|
|
|
const ctx = canvas.getContext('2d');
|
|
if (ctx == null) {
|
|
console.error('no 2d context available');
|
|
return;
|
|
}
|
|
|
|
// Set aspect ratio.
|
|
canvas.width =
|
|
canvas.height * (canvas.clientWidth / canvas.clientHeight);
|
|
|
|
// If the required position is 0, display the thumbnail instead of
|
|
// trying to render the video. The most important use case is before a
|
|
// click event has happened, when autoplay restrictions will prevent
|
|
// the video being rendered to canvas.
|
|
if (videoRef.current.currentTime == 0) {
|
|
const service = new MediaSetServiceClientImpl(newRPC());
|
|
const thumbnail = await service.GetVideoThumbnail({
|
|
id: mediaSet.id,
|
|
});
|
|
|
|
// TODO: avoid fetching the image every re-render:
|
|
const url = URL.createObjectURL(
|
|
new Blob([thumbnail.image], { type: 'image/jpeg' })
|
|
);
|
|
const img = new Image(thumbnail.width, thumbnail.height);
|
|
|
|
img.src = url;
|
|
img.onload = () => ctx.drawImage(img, 0, 0, 177, 100);
|
|
return;
|
|
}
|
|
|
|
// otherwise, render the video, which (should) work now.
|
|
const duration = millisFromDuration(mediaSet.videoDuration);
|
|
const durSecs = duration / 1000;
|
|
const ratio = videoRef.current.currentTime / durSecs;
|
|
const x = (canvas.width - 177) * ratio;
|
|
ctx.clearRect(0, 0, x, canvas.height);
|
|
ctx.clearRect(x + 177, 0, canvas.width - 177 - x, canvas.height);
|
|
ctx.drawImage(videoRef.current, x, 0, 177, 100);
|
|
})();
|
|
});
|
|
}, [mediaSet, videoRef.current.currentTime]);
|
|
|
|
// render component
|
|
|
|
return (
|
|
<>
|
|
<div className={`relative grow-0 h-[100px]`}>
|
|
<canvas
|
|
className="absolute block w-full h-full"
|
|
width="500"
|
|
height="100"
|
|
ref={canvasRef}
|
|
></canvas>
|
|
</div>
|
|
</>
|
|
);
|
|
};
|