Skip to content

Commit

Permalink
first test
Browse files Browse the repository at this point in the history
  • Loading branch information
alex-Arc committed Oct 25, 2024
1 parent f2bb803 commit 1faf3b9
Show file tree
Hide file tree
Showing 3 changed files with 80 additions and 161 deletions.
34 changes: 9 additions & 25 deletions apps/client/src/AppRouter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'));
Expand All @@ -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));
Expand Down Expand Up @@ -76,30 +76,6 @@ export default function AppRouter() {

return (
<React.Suspense fallback={null}>
<PopOutTimer
isMirrored={false}
time={{
addedTime: 0,
current: null,
duration: null,
elapsed: null,
expectedFinish: null,
finishedAt: null,
phase: TimerPhase.Default,
playback: Playback.Play,
secondaryTimer: null,
startedAt: null,
clock: 0,
timerType: TimerType.CountDown
}}
viewSettings={{
dangerColor: '',
endMessage: '',
freezeEnd: false,
normalColor: '',
overrideStyles: false,
warningColor: ''
}} />
<SentryRoutes>
<Route path='/' element={<Navigate to='/timer' />} />
<Route
Expand All @@ -110,6 +86,14 @@ export default function AppRouter() {
</ViewLoader>
}
/>
<Route
path='/pop'
element={
<ViewLoader>
<SPopOutTimer />
</ViewLoader>
}
/>
<Route
path='/public'
element={
Expand Down
18 changes: 9 additions & 9 deletions apps/client/src/features/viewers/pop-out-clock/PopOutTimer.scss
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@

.timer {
opacity: 1;
font-family: var(--font-family-bold-override, $timer-bold-font-family) ;
font-size: 20vw;
font-family: var(--font-family-bold-override, $timer-bold-font-family);
font-size: 15vw;
position: relative;
color: var(--timer-color-override, var(--phase-color));
transition: $viewer-transition-time;
Expand All @@ -43,11 +43,11 @@
/* =================== OVERLAY ===================*/

.end-message {
text-align: center;
font-size: 12vw;
line-height: 0.9em;
font-weight: 600;
color: $timer-finished-color;
padding: 0;
}
text-align: center;
font-size: 12vw;
line-height: 0.9em;
font-weight: 600;
color: $timer-finished-color;
padding: 0;
}
}
189 changes: 62 additions & 127 deletions apps/client/src/features/viewers/pop-out-clock/PopOutTimer.tsx
Original file line number Diff line number Diff line change
@@ -1,160 +1,95 @@
import { useEffect, useRef, useState } from 'react';
import { getFormattedTimer, getTimerByType, isStringBoolean } from '../common/viewUtils';
import { Playback, TimerPhase, TimerType, ViewSettings } from 'ontime-types';
import { useCallback, useEffect, useState } from 'react';
import { Button } from '@chakra-ui/react';

import { ViewExtendedTimer } from '../../../common/models/TimeManager.type';
import { useTranslation } from '../../../translation/TranslationProvider';

import { getFormattedTimer, getTimerByType } from '../common/viewUtils';

import './PopOutTimer.scss';

interface MinimalTimerProps {
isMirrored: boolean;
interface PopTimerProps {
time: ViewExtendedTimer;
viewSettings: ViewSettings;

}

export default function PopOutClock(props: MinimalTimerProps) {
const { isMirrored, time, viewSettings } = props;
const [ready, setReady] = useState(false);
const [videoSource, setVideoSource] = useState<string | null>(null);
const canvasRef = useRef<HTMLCanvasElement | null>(null);
const videoRef = useRef<HTMLVideoElement | null>(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 (
<div>
<div>{display}</div>
<canvas
ref={canvasRef}
id="canvas"
width="640"
height="360"
/>
<video
ref={videoRef}
id="pip-video"
loop
controls
>
{videoSource && <source src={videoSource} type="video/webm" />}
</video>
<button onClick={openPip}>
Picture-in-Picture
</button>
<br></br>
<br></br>
<br></br>
<br></br>
<br></br>
<br></br>
<br></br>
<Button isDisabled={pipElement != false} variant='ontime-filled' onClick={openPip}>
Open
</Button>
<Button isDisabled={pipElement === false} variant='ontime-filled' onClick={closePip}>
Close
</Button>
</div>
);
}

0 comments on commit 1faf3b9

Please sign in to comment.