From 699b95221e474bc01db8ec57fd4fa958f92ee0b1 Mon Sep 17 00:00:00 2001
From: Janne Koschinski <janne@kuschku.de>
Date: Fri, 27 Nov 2020 19:05:30 +0100
Subject: [PATCH] Cleanup subtitle and preview handling with portals

---
 src/App.test.tsx                              |   2 +-
 src/api/ApiClientContext.ts                   |  12 +-
 src/routes/ContentRoute.tsx                   |   2 +-
 src/routes/main/MainPage.tsx                  |   2 +-
 src/routes/player/Player.tsx                  | 169 ++++++++----------
 src/routes/player/PreviewBar.tsx              |  18 +-
 src/routes/player/SeekBar.tsx                 |  25 +--
 .../player/subtitles/SubtitleRenderer.tsx     |  22 +++
 src/routes/player/subtitles/TtmlRenderer.tsx  |  32 ++--
 src/routes/player/video/DashVideoElement.tsx  |   9 +-
 .../video/{VideoApi.ts => PlayerApi.ts}       |   4 +-
 src/routes/player/video/RawVideoElement.tsx   |   7 +-
 src/routes/player/video/Track.tsx             |  17 ++
 src/routes/player/video/VideoContext.tsx      |   5 +
 src/routes/player/video/VideoElement.tsx      |   4 +-
 src/util/media/useAudioTracks.ts              |   4 +-
 src/util/media/useDuration.ts                 |   4 +-
 src/util/media/usePosition.ts                 |   4 +-
 src/util/ttml/html.js                         |  14 +-
 src/util/ttml/ismc.ts                         |   3 +
 20 files changed, 209 insertions(+), 150 deletions(-)
 create mode 100644 src/routes/player/subtitles/SubtitleRenderer.tsx
 rename src/routes/player/video/{VideoApi.ts => PlayerApi.ts} (89%)
 create mode 100644 src/routes/player/video/Track.tsx
 create mode 100644 src/routes/player/video/VideoContext.tsx

diff --git a/src/App.test.tsx b/src/App.test.tsx
index 9a9be2e..13224db 100644
--- a/src/App.test.tsx
+++ b/src/App.test.tsx
@@ -1,5 +1,5 @@
 import { render } from '@testing-library/react';
-import App from './App';
+import {App} from './App';
 
 test('renders learn react link', () => {
   const { getByText } = render(<App />);
diff --git a/src/api/ApiClientContext.ts b/src/api/ApiClientContext.ts
index f8f2618..44218d5 100644
--- a/src/api/ApiClientContext.ts
+++ b/src/api/ApiClientContext.ts
@@ -1,17 +1,13 @@
 import {createContext, useContext} from "react";
 import {ApiClient} from "./ApiClient";
 
-interface ApiClientContext {
-    apiClient: ApiClient
-}
-
-const ApiClientContext = createContext<ApiClientContext>({
-    apiClient: new ApiClient(
+const ApiClientContext = createContext<ApiClient>(
+    new ApiClient(
         localStorage.getItem("API_ENDPOINT") ||
         new URL("/", window.location.href).toString()
     )
-});
+);
 
 export const ApiClientProvider = ApiClientContext.Provider;
 export const ApiClientConsumer = ApiClientContext.Consumer;
-export const useApiClient = () => useContext<ApiClientContext>(ApiClientContext);
+export const useApiClient = () => useContext<ApiClient>(ApiClientContext);
diff --git a/src/routes/ContentRoute.tsx b/src/routes/ContentRoute.tsx
index 5d5b36a..805a29c 100644
--- a/src/routes/ContentRoute.tsx
+++ b/src/routes/ContentRoute.tsx
@@ -7,7 +7,7 @@ import {CurrentContentProvider} from "../util/CurrentContentContext";
 export function ContentRoute(props: PropsWithChildren<{}>) {
     const {children} = props;
     const {contentId} = useParams<{ contentId: string }>();
-    const {apiClient} = useApiClient();
+    const apiClient = useApiClient();
     const [meta, setMeta] = useState<ContentMeta | null>(null);
     useEffect(() => {
         apiClient.getContent(contentId).then(setMeta);
diff --git a/src/routes/main/MainPage.tsx b/src/routes/main/MainPage.tsx
index 03d69bc..ddd6cb5 100644
--- a/src/routes/main/MainPage.tsx
+++ b/src/routes/main/MainPage.tsx
@@ -12,7 +12,7 @@ interface Props {
 }
 
 export function MainPage(props: Props) {
-    const {apiClient} = useApiClient();
+    const apiClient = useApiClient();
     const [data, setData] = useState<Content[]>();
     useEffect(() => {
         apiClient.listContent().then(setData);
diff --git a/src/routes/player/Player.tsx b/src/routes/player/Player.tsx
index a497aaa..7a5b1f5 100644
--- a/src/routes/player/Player.tsx
+++ b/src/routes/player/Player.tsx
@@ -10,11 +10,13 @@ import {useDuration} from "../../util/media/useDuration";
 import {usePosition} from "../../util/media/usePosition";
 import {SeekBar} from "./SeekBar";
 import {VideoElement} from "./video/VideoElement";
-import {VideoApi} from "./video/VideoApi";
+import {PlayerApi} from "./video/PlayerApi";
 import {useAudioTracks} from "../../util/media/useAudioTracks";
 import {useDebugInfo} from "../../util/media/useDebugInfo";
 import {Subtitle} from "../../api/models/Subtitle";
 import {TtmlRenderer} from "./subtitles/TtmlRenderer";
+import {MousePosition} from "../../util/mouse/MousePosition";
+import {VideoProvider} from "./video/VideoContext";
 
 interface Props {
     meta: ContentMeta,
@@ -34,17 +36,14 @@ export function Player(
 
     const [subtitle, setSubtitle] = useState<Subtitle | null>(null);
 
-    const [videoElement, setVideoElement] = useState<VideoApi | null>(null);
-    const [previewTrack, setPreviewTrack] = useState<HTMLTrackElement | null>(null);
-    const [subtitleTrack, setSubtitleTrack] = useState<HTMLTrackElement | null>(null);
+    const [playerApi, setPlayerApi] = useState<PlayerApi | null>(null);
+    const [mousePosition, setMousePosition] = useState<MousePosition | null>(null);
 
-    const position = usePosition(videoElement);
-    const duration = useDuration(videoElement);
-    const [audioTracks, currentTrack, setCurrentTrack] = useAudioTracks(videoElement);
+    const position = usePosition(playerApi);
+    const duration = useDuration(playerApi);
+    const [audioTracks, currentTrack, setCurrentTrack] = useAudioTracks(playerApi);
 
-    useDebugInfo("playerEl", videoElement);
-    useDebugInfo("previewTrackEl", previewTrack);
-    useDebugInfo("subtitleTrackEl", subtitleTrack);
+    useDebugInfo("player", playerApi);
     useDebugInfo("content", content);
     useDebugInfo("subtitle", subtitle);
     useDebugInfo("position", position);
@@ -52,88 +51,78 @@ export function Player(
     useDebugInfo("audioTracks", audioTracks);
 
     return (
-        <div>
-            <Link to="/">Back</Link>
-            <h2>{name?.name}</h2>
-            <p>{rating?.certification}</p>
-            <strong>{description?.tagline}</strong>
-            <p>{description?.overview}</p>
-            <p>{instalment?.content && getLocalizedName(instalment?.content, locale)?.name}</p>
-            <div className={classes.player}>
-                <div className={classes.playerCanvas}>
-                    <VideoElement
-                        className={classes.video}
-                        media={media}
-                        autoPlay={true}
-                        ref={setVideoElement}
-                    >
-                        {content.preview && (
-                            <track
-                                ref={setPreviewTrack}
-                                kind="metadata"
-                                label="previews"
-                                src={content.preview}
-                            />
-                        )}
-                        {subtitle && (
-                            <track
-                                ref={setSubtitleTrack}
-                                kind="captions"
-                                label={`${subtitle.language} (${subtitle.specifier})`}
-                                src={subtitle.src}
-                            />
-                        )}
-                    </VideoElement>
-                    <TtmlRenderer
-                        className={classes.subtitleCanvas}
-                        trackElement={subtitleTrack}
-                        subtitle={subtitle}
-                        duration={duration}
-                    />
+        <VideoProvider value={playerApi?.getVideoElement() || null}>
+            <div>
+                <Link to="/">Back</Link>
+                <h2>{name?.name}</h2>
+                <p>{rating?.certification}</p>
+                <strong>{description?.tagline}</strong>
+                <p>{description?.overview}</p>
+                <p>{instalment?.content && getLocalizedName(instalment?.content, locale)?.name}</p>
+                <div className={classes.player}>
+                    <div className={classes.playerCanvas}>
+                        <VideoElement
+                            className={classes.video}
+                            media={media}
+                            autoPlay={true}
+                            ref={setPlayerApi}
+                        />
+                        <TtmlRenderer
+                            className={classes.subtitleCanvas}
+                            videoElement={playerApi?.getVideoElement() || null}
+                            subtitle={subtitle}
+                            duration={duration}
+                        />
+                    </div>
                 </div>
+                <p style={{fontVariant: "tabular-nums"}}>
+                    {formatDuration(mousePosition ? (mousePosition.relative * duration) : position)} / {formatDuration(duration)}
+                </p>
+                <button onClick={playerApi?.play}>Play</button>
+                <button onClick={playerApi?.pause}>Pause</button>
+                <h3>Audio</h3>
+                <ul>
+                    {audioTracks.map(track => (
+                        <li key={track.index}>
+                            <strong>{track.lang}</strong>
+                            &nbsp;{track.labels}
+                            &nbsp;
+                            <button
+                                disabled={currentTrack === track}
+                                onClick={() => setCurrentTrack(track)}
+                            >
+                                Choose
+                            </button>
+                        </li>
+                    ))}
+                </ul>
+                <h3>Subtitles</h3>
+                <ul>
+                    {[null, ...content.subtitles].map(track => (
+                        <li key={track?.src || "none"}>
+                            <strong>{track?.language || "none"}</strong>
+                            &nbsp;{track?.specifier}
+                            &nbsp;{track?.format}
+                            &nbsp;
+                            <button
+                                disabled={subtitle === track}
+                                onClick={() => setSubtitle(track)}
+                            >
+                                Choose
+                            </button>
+                        </li>
+                    ))}
+                </ul>
+                <SeekBar
+                    videoApi={playerApi}
+                    previewSrc={content.preview}
+                    mousePosition={mousePosition}
+                    setMousePosition={setMousePosition}
+                    duration={duration}
+                    position={position}
+                />
             </div>
-            <p style={{fontVariant: "tabular-nums"}}>{formatDuration(position)} / {formatDuration(duration)}</p>
-            <button onClick={videoElement?.play}>Play</button>
-            <button onClick={videoElement?.pause}>Pause</button>
-            <h3>Audio</h3>
-            <ul>
-                {audioTracks.map(track => (
-                    <li key={track.index}>
-                        <strong>{track.lang}</strong>
-                        &nbsp;{track.labels}
-                        &nbsp;
-                        <button
-                            disabled={currentTrack === track}
-                            onClick={() => setCurrentTrack(track)}
-                        >
-                            Choose
-                        </button>
-                    </li>
-                ))}
-            </ul>
-            <h3>Subtitles</h3>
-            <ul>
-                {[null, ...content.subtitles].map(track => (
-                    <li key={track?.src || "none"}>
-                        <strong>{track?.language || "none"}</strong>
-                        &nbsp;{track?.specifier}
-                        &nbsp;
-                        <button
-                            disabled={subtitle === track}
-                            onClick={() => setSubtitle(track)}
-                        >
-                            Choose
-                        </button>
-                    </li>
-                ))}
-            </ul>
-            <SeekBar
-                video={videoElement}
-                previewTrack={previewTrack}
-                duration={duration}
-                position={position}
-            />
-        </div>
+        </VideoProvider>
     );
 }
 
diff --git a/src/routes/player/PreviewBar.tsx b/src/routes/player/PreviewBar.tsx
index 9c5a7fe..a3dee23 100644
--- a/src/routes/player/PreviewBar.tsx
+++ b/src/routes/player/PreviewBar.tsx
@@ -1,22 +1,24 @@
-import { useMemo } from "react";
+import {useState} from "react";
 import {createUseStyles} from "react-jss";
 import {MousePosition} from "../../util/mouse/MousePosition";
 import {useOffsetAbsoluteRef} from "../../util/offset/useOffsetAbsoluteRef";
 import {PreviewViewer} from "./PreviewViewer";
+import {Track} from "./video/Track";
 
 interface Props {
-    previewTrack: HTMLTrackElement | null,
+    previewSrc: string | null,
     duration: number,
     position: MousePosition | null,
     hidden: boolean,
 }
 
-export function PreviewBar({previewTrack, duration, position, hidden}: Props) {
+export function PreviewBar({previewSrc, duration, position, hidden}: Props) {
     const classes = useStyles();
 
+    const [previewTrack, setPreviewTrack] = useState<HTMLTrackElement | null>(null);
     const [previewBarRef, previewHeadRef, offset] = useOffsetAbsoluteRef(position?.absolute || 0);
 
-    return useMemo(() => (
+    return (
         <div
             ref={previewBarRef}
             className={classes.previewBar}
@@ -29,13 +31,19 @@ export function PreviewBar({previewTrack, duration, position, hidden}: Props) {
                     opacity: hidden ? 0 : 1,
                 }}
             >
+                <Track
+                    ref={setPreviewTrack}
+                    kind="metadata"
+                    label="previews"
+                    src={previewSrc || undefined}
+                />
                 <PreviewViewer
                     previewTrack={previewTrack}
                     position={(position?.relative || 0) * duration}
                 />
             </div>
         </div>
-    ), [previewBarRef, classes.previewBar, classes.previewHead, previewHeadRef, offset, hidden, previewTrack, position, duration]);
+    );
 }
 
 const useStyles = createUseStyles({
diff --git a/src/routes/player/SeekBar.tsx b/src/routes/player/SeekBar.tsx
index b77d4de..2a2de12 100644
--- a/src/routes/player/SeekBar.tsx
+++ b/src/routes/player/SeekBar.tsx
@@ -1,23 +1,24 @@
-import { Fragment, MouseEvent, useCallback, useMemo, useState } from "react";
+import {Fragment, MouseEvent, useCallback, useMemo, useState} from "react";
 import {createUseStyles} from "react-jss";
 import {getMousePosition} from "../../util/mouse/getMousePosition";
 import {MousePosition} from "../../util/mouse/MousePosition";
 import {useOffsetAbsolute} from "../../util/offset/useOffsetAbsolute";
 import {useOffsetRelative} from "../../util/offset/useOffsetRelative";
 import {PreviewBar} from "./PreviewBar";
-import {VideoApi} from "./video/VideoApi";
+import {PlayerApi} from "./video/PlayerApi";
 
 interface Props {
-    previewTrack: HTMLTrackElement | null,
-    video: VideoApi | null,
+    videoApi: PlayerApi | null,
+    previewSrc: string | null,
+    mousePosition: MousePosition | null,
+    setMousePosition: (position: MousePosition | null) => void,
     duration: number,
     position: number,
 }
 
-export function SeekBar({video, previewTrack, duration, position}: Props) {
+export function SeekBar({videoApi, previewSrc, mousePosition, setMousePosition, duration, position}: Props) {
     const classes = useStyles();
 
-    const [mousePosition, setMousePosition] = useState<MousePosition | null>(null);
     const isVisible = mousePosition !== null;
 
     const [seekBarRef, setSeekBarRef] = useState<HTMLDivElement | null>(null);
@@ -29,20 +30,20 @@ export function SeekBar({video, previewTrack, duration, position}: Props) {
 
     const onMouseLeave = useCallback(() => {
         setMousePosition(null)
-    }, []);
+    }, [setMousePosition]);
 
     const onClick = useCallback((event: MouseEvent<HTMLDivElement>) => {
         const position = getMousePosition(event);
         setMousePosition(position);
-        if (video && position) {
-            video.setCurrentTime(position.relative * duration);
+        if (videoApi && position) {
+            videoApi.setCurrentTime(position.relative * duration);
         }
-    }, [duration, video]);
+    }, [duration, setMousePosition, videoApi]);
 
     const onMouseMove = useCallback((event: MouseEvent<HTMLDivElement>) => {
         const position = getMousePosition(event);
         setMousePosition(position);
-    }, []);
+    }, [setMousePosition]);
 
     const seekHead = useMemo(() => (
         <div
@@ -81,7 +82,7 @@ export function SeekBar({video, previewTrack, duration, position}: Props) {
     return (
         <Fragment>
             <PreviewBar
-                previewTrack={previewTrack}
+                previewSrc={previewSrc}
                 duration={duration}
                 position={mousePosition}
                 hidden={mousePosition === null}
diff --git a/src/routes/player/subtitles/SubtitleRenderer.tsx b/src/routes/player/subtitles/SubtitleRenderer.tsx
new file mode 100644
index 0000000..dc33205
--- /dev/null
+++ b/src/routes/player/subtitles/SubtitleRenderer.tsx
@@ -0,0 +1,22 @@
+import {Subtitle} from "../../../api/models/Subtitle";
+import {Fragment} from "react";
+import {TtmlRenderer} from "./TtmlRenderer";
+
+interface Props {
+    videoElement: HTMLVideoElement | null,
+    subtitle: Subtitle | null,
+    duration: number,
+    className?: string,
+}
+
+export function SubtitleRenderer(
+    props: Props
+) {
+    switch (props.subtitle?.format) {
+        case "ttml":
+            return (<TtmlRenderer {...props} />);
+        //case "ass": return (<AssRendered {...props} />);
+        default:
+            return (<Fragment/>);
+    }
+}
diff --git a/src/routes/player/subtitles/TtmlRenderer.tsx b/src/routes/player/subtitles/TtmlRenderer.tsx
index 06c2112..5e94fd8 100644
--- a/src/routes/player/subtitles/TtmlRenderer.tsx
+++ b/src/routes/player/subtitles/TtmlRenderer.tsx
@@ -1,18 +1,20 @@
-import {useEffect, useState} from "react";
+import {Fragment, useEffect, useState} from "react";
 import TtmlHelper from "./TtmlHelper";
 import {Subtitle} from "../../../api/models/Subtitle";
+import {Track} from "../video/Track";
 
 interface Props {
-    trackElement: HTMLTrackElement | null,
+    videoElement: HTMLVideoElement | null,
     subtitle: Subtitle | null,
     duration: number,
     className?: string,
 }
 
 export function TtmlRenderer(
-    {trackElement, subtitle, duration, className}: Props
-) {
+    {subtitle, duration, className}: Props
+): JSX.Element {
     const [subtitleCanvas, setSubtitleCanvas] = useState<HTMLElement | null>(null);
+    const [trackElement, setTrackElement] = useState<HTMLTrackElement | null>(null);
 
     useEffect(() => {
         if (subtitleCanvas && trackElement && subtitle) {
@@ -21,10 +23,20 @@ export function TtmlRenderer(
     }, [subtitleCanvas, subtitle, trackElement, duration]);
 
     return (
-        <div
-            ref={setSubtitleCanvas}
-            lang={subtitle?.language || undefined}
-            className={className}
-        />
-    )
+        <Fragment>
+            {subtitle && (
+                <Track
+                    ref={setTrackElement}
+                    kind="captions"
+                    label={`${subtitle.language} (${subtitle.specifier})`}
+                    src={subtitle.src}
+                />
+            )}
+            <div
+                ref={setSubtitleCanvas}
+                lang={subtitle?.language || undefined}
+                className={className}
+            />
+        </Fragment>
+    );
 }
diff --git a/src/routes/player/video/DashVideoElement.tsx b/src/routes/player/video/DashVideoElement.tsx
index aa7dce5..afa2f81 100644
--- a/src/routes/player/video/DashVideoElement.tsx
+++ b/src/routes/player/video/DashVideoElement.tsx
@@ -1,7 +1,7 @@
 import {forwardRef, PropsWithChildren, useEffect, useImperativeHandle, useMemo, useState,} from "react";
 import {Media} from "../../../api/models/Media";
 import dashjs, {MediaInfo} from "dashjs";
-import {VideoApi} from "./VideoApi";
+import {PlayerApi} from "./PlayerApi";
 
 interface Props {
     media: Media,
@@ -9,7 +9,7 @@ interface Props {
     className?: string,
 }
 
-export const DashVideoElement = forwardRef<VideoApi, PropsWithChildren<Props>>(function (
+export const DashVideoElement = forwardRef<PlayerApi, PropsWithChildren<Props>>(function (
     {media, autoPlay, className, children},
     ref
 ) {
@@ -105,8 +105,11 @@ export const DashVideoElement = forwardRef<VideoApi, PropsWithChildren<Props>>(f
         },
         debug(): any {
             return player;
+        },
+        getVideoElement(): HTMLVideoElement | null {
+            return videoElement;
         }
-    }), [player]);
+    }), [player, videoElement]);
 
     useEffect(() => {
         player.initialize();
diff --git a/src/routes/player/video/VideoApi.ts b/src/routes/player/video/PlayerApi.ts
similarity index 89%
rename from src/routes/player/video/VideoApi.ts
rename to src/routes/player/video/PlayerApi.ts
index 24c7fb1..bf11227 100644
--- a/src/routes/player/video/VideoApi.ts
+++ b/src/routes/player/video/PlayerApi.ts
@@ -1,6 +1,6 @@
 import {MediaInfo} from "dashjs";
 
-export interface VideoApi {
+export interface PlayerApi {
     METADATA_EVENT: string
     TIMECHANGE_EVENT: string
 
@@ -34,4 +34,6 @@ export interface VideoApi {
 
     debug(): any
 
+    getVideoElement(): HTMLVideoElement | null
+
 }
diff --git a/src/routes/player/video/RawVideoElement.tsx b/src/routes/player/video/RawVideoElement.tsx
index 0d1c630..b378ebd 100644
--- a/src/routes/player/video/RawVideoElement.tsx
+++ b/src/routes/player/video/RawVideoElement.tsx
@@ -1,6 +1,6 @@
 import {forwardRef, PropsWithChildren, useImperativeHandle, useState} from "react";
 import {Media} from "../../../api/models/Media";
-import {VideoApi} from "./VideoApi";
+import {PlayerApi} from "./PlayerApi";
 import {MediaInfo} from "dashjs";
 
 interface Props {
@@ -9,7 +9,7 @@ interface Props {
     className?: string,
 }
 
-export const RawVideoElement = forwardRef<VideoApi, PropsWithChildren<Props>>(function (
+export const RawVideoElement = forwardRef<PlayerApi, PropsWithChildren<Props>>(function (
     {media, autoPlay, className, children},
     ref
 ) {
@@ -105,6 +105,9 @@ export const RawVideoElement = forwardRef<VideoApi, PropsWithChildren<Props>>(fu
         },
         debug(): any {
             return videoElement;
+        },
+        getVideoElement(): HTMLVideoElement | null {
+            return videoElement;
         }
     }), [videoElement]);
 
diff --git a/src/routes/player/video/Track.tsx b/src/routes/player/video/Track.tsx
new file mode 100644
index 0000000..c3a922f
--- /dev/null
+++ b/src/routes/player/video/Track.tsx
@@ -0,0 +1,17 @@
+import {forwardRef, Fragment, HTMLProps} from "react";
+import {createPortal} from "react-dom";
+import {useVideo} from "./VideoContext";
+
+export const Track = forwardRef<HTMLTrackElement, HTMLProps<HTMLTrackElement>>(function (
+    props, ref
+) {
+    const video = useVideo();
+    if (video) {
+        return createPortal(
+            <track ref={ref} {...props} />,
+            video
+        );
+    } else {
+        return <Fragment/>;
+    }
+});
diff --git a/src/routes/player/video/VideoContext.tsx b/src/routes/player/video/VideoContext.tsx
new file mode 100644
index 0000000..af8de15
--- /dev/null
+++ b/src/routes/player/video/VideoContext.tsx
@@ -0,0 +1,5 @@
+import {createContext, useContext} from "react";
+
+const videoContext = createContext<HTMLVideoElement | null>(null);
+export const VideoProvider = videoContext.Provider;
+export const useVideo = () => useContext<HTMLVideoElement | null>(videoContext);
diff --git a/src/routes/player/video/VideoElement.tsx b/src/routes/player/video/VideoElement.tsx
index 526b40a..1de8562 100644
--- a/src/routes/player/video/VideoElement.tsx
+++ b/src/routes/player/video/VideoElement.tsx
@@ -2,7 +2,7 @@ import { forwardRef, PropsWithChildren } from "react";
 import {Media} from "../../../api/models/Media";
 import {DashVideoElement} from "./DashVideoElement";
 import {RawVideoElement} from "./RawVideoElement";
-import {VideoApi} from "./VideoApi";
+import {PlayerApi} from "./PlayerApi";
 
 interface Props {
     media: Media,
@@ -10,7 +10,7 @@ interface Props {
     className?: string,
 }
 
-export const VideoElement = forwardRef<VideoApi, PropsWithChildren<Props>>(function (
+export const VideoElement = forwardRef<PlayerApi, PropsWithChildren<Props>>(function (
     props: Props, ref
 ) {
     switch (props.media.mime) {
diff --git a/src/util/media/useAudioTracks.ts b/src/util/media/useAudioTracks.ts
index d22fb0e..ca9ee8f 100644
--- a/src/util/media/useAudioTracks.ts
+++ b/src/util/media/useAudioTracks.ts
@@ -1,8 +1,8 @@
 import {useCallback, useEffect, useState} from "react";
-import {VideoApi} from "../../routes/player/video/VideoApi";
+import {PlayerApi} from "../../routes/player/video/PlayerApi";
 import {MediaInfo} from "dashjs";
 
-export function useAudioTracks(video: VideoApi | null):
+export function useAudioTracks(video: PlayerApi | null):
     [MediaInfo[], MediaInfo | null, (track: MediaInfo) => void] {
     const [audioTracks, setAudioTracks] = useState<MediaInfo[]>([]);
     const [currentTrack, setCurrentTrack] = useState<MediaInfo | null>(null);
diff --git a/src/util/media/useDuration.ts b/src/util/media/useDuration.ts
index 8b2d42b..c0259a3 100644
--- a/src/util/media/useDuration.ts
+++ b/src/util/media/useDuration.ts
@@ -1,7 +1,7 @@
 import {useEffect, useState} from "react";
-import {VideoApi} from "../../routes/player/video/VideoApi";
+import {PlayerApi} from "../../routes/player/video/PlayerApi";
 
-export const useDuration = (video: VideoApi | null) => {
+export const useDuration = (video: PlayerApi | null) => {
     const [duration, setDuration] = useState<number>(0);
     useEffect(() => {
         if (video !== null) {
diff --git a/src/util/media/usePosition.ts b/src/util/media/usePosition.ts
index 28db74d..d8a3b8a 100644
--- a/src/util/media/usePosition.ts
+++ b/src/util/media/usePosition.ts
@@ -1,7 +1,7 @@
 import {useEffect, useState} from "react";
-import {VideoApi} from "../../routes/player/video/VideoApi";
+import {PlayerApi} from "../../routes/player/video/PlayerApi";
 
-export const usePosition = (video: VideoApi | null) => {
+export const usePosition = (video: PlayerApi | null) => {
     const [position, setPosition] = useState<number>(0);
     useEffect(() => {
         if (video !== null) {
diff --git a/src/util/ttml/html.js b/src/util/ttml/html.js
index fa42a24..a76ee4a 100644
--- a/src/util/ttml/html.js
+++ b/src/util/ttml/html.js
@@ -705,15 +705,13 @@ let STYLING_MAP_DEFS = [
         "http://www.w3.org/ns/ttml#styling fontFamily",
         function (context, dom_element, isd_element, attr) {
             /* per IMSC1 */
+            const styleClasses = ["monospaceSerif", "proportionalSerif", "monospace", "sansSerif", "serif", "monospaceSansSerif", "proportionalSansSerif"];
             for (let attribute of attr) {
-                // monospaceSerif
-                // proportionalSansSerif
-                // monospace
-                // sansSerif
-                // serif
-                // monospaceSansSerif
-                // proportionalSerif
-                dom_element.classList.add("ttml-" + attribute);
+                if (styleClasses.includes(attribute)) {
+                    dom_element.classList.add("ttml-" + attribute);
+                } else {
+                    dom_element.style.fontFamily = attribute;
+                }
             }
         }
     ),
diff --git a/src/util/ttml/ismc.ts b/src/util/ttml/ismc.ts
index fae12ca..8a33d89 100644
--- a/src/util/ttml/ismc.ts
+++ b/src/util/ttml/ismc.ts
@@ -144,6 +144,9 @@ type renderHTML = (
     enableRollUp?: boolean
 ) => ISDState;
 
+// eslint-disable-next-line @typescript-eslint/no-redeclare
 export const generateISD: generateISD = isd.generateISD;
+// eslint-disable-next-line @typescript-eslint/no-redeclare
 export const fromXML: fromXML = doc.fromXML;
+// eslint-disable-next-line @typescript-eslint/no-redeclare
 export const renderHTML: renderHTML = html.render;
-- 
GitLab