Skip to content

Commit

Permalink
pop-out-clock testing (#1247)
Browse files Browse the repository at this point in the history
  • Loading branch information
Haavard15 authored Oct 18, 2024
1 parent ea2330e commit f2bb803
Show file tree
Hide file tree
Showing 4 changed files with 330 additions and 0 deletions.
26 changes: 26 additions & 0 deletions apps/client/src/AppRouter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,15 @@ 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'));
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'));

Expand Down Expand Up @@ -74,6 +76,30 @@ 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 Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { hideTimerSeconds } from '../../../common/components/view-params-editor/constants';
import { ViewOption } from '../../../common/components/view-params-editor/types';

export const MINIMAL_TIMER_OPTIONS: ViewOption[] = [
{ section: 'Timer Options' },
hideTimerSeconds,
{ section: 'Element visibility' },
{
id: 'hideovertime',
title: 'Hide Overtime',
description: 'Whether to suppress overtime styles (red borders and red text)',
type: 'boolean',
defaultValue: false,
},
{
id: 'hideendmessage',
title: 'Hide End Message',
description: 'Whether to hide end message and continue showing the clock if timer is in overtime',
type: 'boolean',
defaultValue: false,
},
{ section: 'View style override' },
{
id: 'key',
title: 'Key Colour',
description: 'Background colour in hexadecimal',
prefix: '#',
type: 'string',
placeholder: '00000000 (default)',
},
{
id: 'text',
title: 'Text Colour',
description: 'Text colour in hexadecimal',
prefix: '#',
type: 'string',
placeholder: 'fffff (default)',
},
{
id: 'textbg',
title: 'Text Background',
description: 'Colour of text background in hexadecimal',
prefix: '#',
type: 'string',
placeholder: '00000000 (default)',
},
{
id: 'font',
title: 'Font',
description: 'Font family, will use the fonts available in the system',
type: 'string',
placeholder: 'Arial Black (default)',
},
{
id: 'size',
title: 'Text Size',
description: 'Scales the current style (0.5 = 50% 1 = 100% 2 = 200%)',
type: 'number',
placeholder: '1 (default)',
},
{
id: 'alignx',
title: 'Align Horizontal',
description: 'Moves the horizontally in page to start = left | center | end = right',
type: 'option',
values: { start: 'Start', center: 'Center', end: 'End' },
defaultValue: 'center',
},
{
id: 'offsetx',
title: 'Offset Horizontal',
description: 'Offsets the timer horizontal position by a given amount in pixels',
type: 'number',
placeholder: '0 (default)',
},
{
id: 'aligny',
title: 'Align Vertical',
description: 'Moves the vertically in page to start = left | center | end = right',
type: 'option',
values: { start: 'Start', center: 'Center', end: 'End' },
defaultValue: 'center',
},
{
id: 'offsety',
title: 'Offset Vertical',
description: 'Offsets the timer vertical position by a given amount in pixels',
type: 'number',
placeholder: '0 (default)',
},
];
53 changes: 53 additions & 0 deletions apps/client/src/features/viewers/pop-out-clock/PopOutTimer.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
@use '../../../theme/viewerDefs' as *;

.minimal-timer {
margin: 0;
box-sizing: border-box; /* reset */
overflow: hidden;
width: 100%; /* restrict the page width to viewport */
height: 100vh;
transition: opacity 0.5s ease-in-out;

background: var(--background-color-override, $viewer-background-color);
color: var(--color-override, $viewer-color);
display: grid;
place-content: center;

&--finished {
outline: clamp(4px, 1vw, 16px) solid $timer-finished-color;
outline-offset: calc(clamp(4px, 1vw, 16px) * -1);
transition: $viewer-transition-time;
}

.timer {
opacity: 1;
font-family: var(--font-family-bold-override, $timer-bold-font-family) ;
font-size: 20vw;
position: relative;
color: var(--timer-color-override, var(--phase-color));
transition: $viewer-transition-time;
transition-property: opacity;
background-color: transparent;
letter-spacing: 0.05em;

&--paused {
opacity: $viewer-opacity-disabled;
transition: $viewer-transition-time;
}

&--finished {
color: $timer-finished-color;
}
}

/* =================== OVERLAY ===================*/

.end-message {
text-align: center;
font-size: 12vw;
line-height: 0.9em;
font-weight: 600;
color: $timer-finished-color;
padding: 0;
}
}
160 changes: 160 additions & 0 deletions apps/client/src/features/viewers/pop-out-clock/PopOutTimer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import { useEffect, useRef, useState } from 'react';

Check failure on line 1 in apps/client/src/features/viewers/pop-out-clock/PopOutTimer.tsx

View workflow job for this annotation

GitHub Actions / unit-test

Run autofix to sort these imports!
import { getFormattedTimer, getTimerByType, isStringBoolean } from '../common/viewUtils';

Check failure on line 2 in apps/client/src/features/viewers/pop-out-clock/PopOutTimer.tsx

View workflow job for this annotation

GitHub Actions / unit-test

'isStringBoolean' is defined but never used. Allowed unused vars must match /^_/u
import { Playback, TimerPhase, TimerType, ViewSettings } from 'ontime-types';

Check failure on line 3 in apps/client/src/features/viewers/pop-out-clock/PopOutTimer.tsx

View workflow job for this annotation

GitHub Actions / unit-test

'Playback' is defined but never used. Allowed unused vars must match /^_/u

Check failure on line 3 in apps/client/src/features/viewers/pop-out-clock/PopOutTimer.tsx

View workflow job for this annotation

GitHub Actions / unit-test

'TimerPhase' is defined but never used. Allowed unused vars must match /^_/u

Check failure on line 3 in apps/client/src/features/viewers/pop-out-clock/PopOutTimer.tsx

View workflow job for this annotation

GitHub Actions / unit-test

'TimerType' is defined but never used. Allowed unused vars must match /^_/u
import { ViewExtendedTimer } from '../../../common/models/TimeManager.type';
import { useTranslation } from '../../../translation/TranslationProvider';


import './PopOutTimer.scss';

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

}

export default function PopOutClock(props: MinimalTimerProps) {
const { isMirrored, time, viewSettings } = props;

Check failure on line 18 in apps/client/src/features/viewers/pop-out-clock/PopOutTimer.tsx

View workflow job for this annotation

GitHub Actions / unit-test

'isMirrored' is assigned a value but never used. Allowed unused vars must match /^_/u

Check failure on line 18 in apps/client/src/features/viewers/pop-out-clock/PopOutTimer.tsx

View workflow job for this annotation

GitHub Actions / unit-test

'viewSettings' is assigned a value but never used. Allowed unused vars must match /^_/u
const [ready, setReady] = useState(false);
const [videoSource, setVideoSource] = useState<string | null>(null);
const canvasRef = useRef<HTMLCanvasElement | null>(null);
const videoRef = useRef<HTMLVideoElement | null>(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";

Check failure on line 34 in apps/client/src/features/viewers/pop-out-clock/PopOutTimer.tsx

View workflow job for this annotation

GitHub Actions / unit-test

'color' is never reassigned. Use 'const' instead
let title = "";

Check failure on line 35 in apps/client/src/features/viewers/pop-out-clock/PopOutTimer.tsx

View workflow job for this annotation

GitHub Actions / unit-test

'title' is never reassigned. Use 'const' instead
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 (
<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>
</div>
);
}

0 comments on commit f2bb803

Please sign in to comment.