diff --git a/apps/client/src/AppRouter.tsx b/apps/client/src/AppRouter.tsx index 68b5fe3698..774fd03e79 100644 --- a/apps/client/src/AppRouter.tsx +++ b/apps/client/src/AppRouter.tsx @@ -17,7 +17,6 @@ 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')); @@ -37,6 +36,7 @@ const StudioClock = React.lazy(() => import('./features/viewers/studio/StudioClo const STimer = withPreset(withData(TimerView)); const SMinimalTimer = withPreset(withData(MinimalTimerView)); +const SPopOutTimer = withPreset(withData(PopOutTimer)); const SClock = withPreset(withData(ClockView)); const SCountdown = withPreset(withData(Countdown)); const SBackstage = withPreset(withData(Backstage)); @@ -76,30 +76,6 @@ export default function AppRouter() { return ( - } /> } /> + + + + } + /> (null); - const canvasRef = useRef(null); - const videoRef = useRef(null); +export default function PopOutClock(props: PopTimerProps) { + const { time } = props; + const [pipElement, setPipElement] = useState<{ timer: HTMLDivElement; pipWindow: Window } | false>(false); 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); + if (pipElement) { + pipElement.timer.innerText = display; } - }, []); + }, [display, pipElement]); - 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 closePip = useCallback(() => { + if (pipElement) { + pipElement.pipWindow.close(); } - }; - - 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); - }; + }, [pipElement]); + + const openPip = useCallback(() => { + // @ts-expect-error - pip is experimental https://wicg.github.io/document-picture-in-picture/#documentpictureinpicture + window.documentPictureInPicture.requestWindow().then((pipWindow: Window) => { + // Copy style sheets over from the initial document + [...document.styleSheets].forEach((styleSheet) => { + try { + const cssRules = [...styleSheet.cssRules].map((rule) => rule.cssText).join(''); + const style = document.createElement('style'); + style.textContent = cssRules; + pipWindow.document.head.appendChild(style); + } catch (e) { + console.log('failed to copy css'); + } + }); - 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[] = []; + // create the backgoind element + const background = document.createElement('div'); + background.classList.add('minimal-timer'); + pipWindow.document.body.append(background); - mediaRecorder.ondataavailable = (event) => { - if (event.data.size > 0) { - chunks.push(event.data); - } - }; + // create the timer element + const timer = document.createElement('div'); + timer.classList.add('timer'); + background.append(timer); - mediaRecorder.onstop = () => { - const blob = new Blob(chunks, { type: 'video/webm' }); - callback(URL.createObjectURL(blob)); - }; + pipWindow.document.title = 'ONTIME'; //TODO: trying to hide or change the title bar - mediaRecorder.start(); - setTimeout(() => { - mediaRecorder.stop(); - }, 100); - }; + setPipElement({ timer, pipWindow }); - 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); - }); + //clear state when the pip is closed + pipWindow.addEventListener( + 'pagehide', + () => { + setPipElement(false); + }, + { once: true }, + ); }); - }; - - 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}
- - - +

+

+

+

+

+

+

+ +
); }