Skip to content
Snippets Groups Projects
Verified Commit 699b9522 authored by Janne Mareike Koschinski's avatar Janne Mareike Koschinski
Browse files

Cleanup subtitle and preview handling with portals

parent 484410c4
Branches
No related tags found
No related merge requests found
Showing
with 209 additions and 150 deletions
import { render } from '@testing-library/react';
import App from './App';
import {App} from './App';
test('renders learn react link', () => {
const { getByText } = render(<App />);
......
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);
......@@ -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);
......
......@@ -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);
......
......@@ -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,6 +51,7 @@ export function Player(
useDebugInfo("audioTracks", audioTracks);
return (
<VideoProvider value={playerApi?.getVideoElement() || null}>
<div>
<Link to="/">Back</Link>
<h2>{name?.name}</h2>
......@@ -65,36 +65,21 @@ export function Player(
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}
ref={setPlayerApi}
/>
)}
</VideoElement>
<TtmlRenderer
className={classes.subtitleCanvas}
trackElement={subtitleTrack}
videoElement={playerApi?.getVideoElement() || null}
subtitle={subtitle}
duration={duration}
/>
</div>
</div>
<p style={{fontVariant: "tabular-nums"}}>{formatDuration(position)} / {formatDuration(duration)}</p>
<button onClick={videoElement?.play}>Play</button>
<button onClick={videoElement?.pause}>Pause</button>
<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 => (
......@@ -117,6 +102,7 @@ export function Player(
<li key={track?.src || "none"}>
<strong>{track?.language || "none"}</strong>
&nbsp;{track?.specifier}
&nbsp;{track?.format}
&nbsp;
<button
disabled={subtitle === track}
......@@ -128,12 +114,15 @@ export function Player(
))}
</ul>
<SeekBar
video={videoElement}
previewTrack={previewTrack}
videoApi={playerApi}
previewSrc={content.preview}
mousePosition={mousePosition}
setMousePosition={setMousePosition}
duration={duration}
position={position}
/>
</div>
</VideoProvider>
);
}
......
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({
......
......@@ -5,19 +5,20 @@ 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}
......
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/>);
}
}
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 (
<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>
);
}
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();
......
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
}
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]);
......
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/>;
}
});
import {createContext, useContext} from "react";
const videoContext = createContext<HTMLVideoElement | null>(null);
export const VideoProvider = videoContext.Provider;
export const useVideo = () => useContext<HTMLVideoElement | null>(videoContext);
......@@ -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) {
......
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);
......
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) {
......
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) {
......
......@@ -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
if (styleClasses.includes(attribute)) {
dom_element.classList.add("ttml-" + attribute);
} else {
dom_element.style.fontFamily = attribute;
}
}
}
),
......
......@@ -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;
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment