diff --git a/apps/client/src/AppRouter.tsx b/apps/client/src/AppRouter.tsx index 0248f7bb45..68b5fe3698 100644 --- a/apps/client/src/AppRouter.tsx +++ b/apps/client/src/AppRouter.tsx @@ -17,6 +17,7 @@ import withData from './features/viewers/ViewWrapper'; import ViewLoader from './views/ViewLoader'; import { ONTIME_VERSION } from './ONTIME_VERSION'; import { sentryDsn, sentryRecommendedIgnore } from './sentry.config'; +import { Playback, TimerPhase, TimerType } from 'ontime-types'; const Editor = React.lazy(() => import('./features/editors/ProtectedEditor')); const Cuesheet = React.lazy(() => import('./features/cuesheet/ProtectedCuesheet')); @@ -24,6 +25,7 @@ const Operator = React.lazy(() => import('./features/operator/OperatorExport')); const TimerView = React.lazy(() => import('./features/viewers/timer/Timer')); const MinimalTimerView = React.lazy(() => import('./features/viewers/minimal-timer/MinimalTimer')); +const PopOutTimer = React.lazy(() => import('./features/viewers/pop-out-clock/PopOutTimer')); const ClockView = React.lazy(() => import('./features/viewers/clock/Clock')); const Countdown = React.lazy(() => import('./features/viewers/countdown/Countdown')); @@ -74,6 +76,30 @@ export default function AppRouter() { return ( + } /> (null); + const canvasRef = useRef(null); + const videoRef = useRef(null); + + const { getLocalizedString } = useTranslation(); + + + + const stageTimer = getTimerByType(false, time); + const display = getFormattedTimer(stageTimer, time.timerType, getLocalizedString('common.minutes'), { + removeSeconds: false, + removeLeadingZero: true, + }); + + let color = "#000000"; + let title = ""; + let clicked = false; + + useEffect(() => { + const canvas = canvasRef.current; + const videoElement = videoRef.current; + if (canvas && videoElement) { + const context = canvas.getContext('2d'); + if (context) { + changeVideo(color, title, context, canvas, videoElement); + } + setReady(true); + } + }, []); + + const openPip = async () => { + if (!videoRef.current) return; + clicked = true; + await videoRef.current.play(); + + if (videoRef.current !== document.pictureInPictureElement) { + try { + await videoRef.current.requestPictureInPicture(); + } catch (error) { + console.error("Error: Unable to enter Picture-in-Picture mode:", error); + } + } else { + try { + await document.exitPictureInPicture(); + } catch (error) { + console.error("Error: Unable to exit Picture-in-Picture mode:", error); + } + } + }; + + const drawFrame = (color: string, text: string, context: CanvasRenderingContext2D, canvas: HTMLCanvasElement) => { + context.fillStyle = color; + context.fillRect(0, 0, canvas.width, canvas.height); + + context.font = "60px Arial"; + context.fillStyle = "white"; + const textWidth = context.measureText(text).width; + const x = (canvas.width - textWidth) / 2; + const y = canvas.height / 2 + 15; + + context.fillText(text, x, y); + }; + + const createVideoBlob = (canvas: HTMLCanvasElement, context: CanvasRenderingContext2D, callback: (url: string) => void) => { + const stream = canvas.captureStream(30); + const mediaRecorder = new MediaRecorder(stream, { mimeType: 'video/webm' }); + const chunks: BlobPart[] = []; + + mediaRecorder.ondataavailable = (event) => { + if (event.data.size > 0) { + chunks.push(event.data); + } + }; + + mediaRecorder.onstop = () => { + const blob = new Blob(chunks, { type: 'video/webm' }); + callback(URL.createObjectURL(blob)); + }; + + mediaRecorder.start(); + setTimeout(() => { + mediaRecorder.stop(); + }, 100); + }; + + const changeVideo = ( + color: string, + text: string, + context: CanvasRenderingContext2D, + canvas: HTMLCanvasElement, + videoElement: HTMLVideoElement + ) => { + drawFrame(color, text, context, canvas); + createVideoBlob(canvas, context, (newVideoSource) => { + if (videoSource) { + URL.revokeObjectURL(videoSource); + } + setVideoSource(newVideoSource); + videoElement.src = newVideoSource; + videoElement.play().catch((error) => { + console.error("Error playing video:", error); + }); + }); + }; + + useEffect(() => { + if (ready && canvasRef.current && videoRef.current) { + const canvas = canvasRef.current; + const context = canvas.getContext('2d'); + let i = 0; + const interval = setInterval(() => { + changeVideo("green", display, context!, canvas, videoRef.current!); + i++; + }, 1000); + return () => clearInterval(interval); // Clean up the interval on component unmount + } + }, [ready]); + + return ( +
+
{display}
+ + + +
+ ); +}