import { Accessor, Component, createEffect, createSignal, For, onCleanup, onMount, Setter, Show, } from "solid-js"; import { DateTime } from "luxon"; import { TimeType, wubloaderTimeFromDateTime, busTimeFromDateTime, timeAgoFromDateTime, dateTimeFromWubloaderTime, dateTimeFromBusTime, dateTimeFromTimeAgo, } from "./convertTime"; import styles from "./video.module.scss"; import "./video.scss"; import { HLSProvider } from "vidstack"; import { MediaPlayerElement } from "vidstack/elements"; import "vidstack/icons"; import "vidstack/player/styles/default/theme.css"; import "vidstack/player/styles/default/layouts/video.css"; import "vidstack/player"; import "vidstack/player/layouts/default"; import "vidstack/player/ui"; export const VIDEO_FRAMES_PER_SECOND = 30; export const PLAYBACK_RATES = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2, 4, 8]; export class StreamVideoInfo { streamName: string; streamStartTime: DateTime; streamEndTime: DateTime | null; } export interface StreamTimeSettingsProps { busStartTime: Accessor; streamVideoInfo: Accessor; setStreamVideoInfo: Setter; showTimeRangeLink: boolean; errorList: Accessor; setErrorList: Setter; } export const StreamTimeSettings: Component = (props) => { const [timeType, setTimeType] = createSignal(TimeType.UTC); const submitHandler = (event: SubmitEvent) => { event.preventDefault(); const form = event.currentTarget as HTMLFormElement; const formData = new FormData(form); const streamName = formData.get("stream") as string; const startTimeEntered = formData.get("start-time") as string; const endTimeEntered = formData.get("end-time") as string; const timeType = +formData.get("time-type") as TimeType; let startTime: DateTime | null = null; let endTime: DateTime | null = null; switch (timeType) { case TimeType.UTC: startTime = dateTimeFromWubloaderTime(startTimeEntered); if (endTimeEntered !== "") { endTime = dateTimeFromWubloaderTime(endTimeEntered); } break; case TimeType.BusTime: startTime = dateTimeFromBusTime(props.busStartTime(), startTimeEntered); if (endTimeEntered !== "") { endTime = dateTimeFromBusTime(props.busStartTime(), endTimeEntered); } break; case TimeType.TimeAgo: startTime = dateTimeFromTimeAgo(startTimeEntered); if (endTimeEntered !== "") { endTime = dateTimeFromTimeAgo(endTimeEntered); } break; } if (startTime === null || (endTimeEntered !== "" && endTime === null)) { const error = "A load boundary time could not be parsed. Check the format of your times."; props.setErrorList([...props.errorList(), error]); return; } props.setStreamVideoInfo({ streamName: streamName, streamStartTime: startTime, streamEndTime: endTime, }); }; const startTimeDisplay = () => { const startTime = props.streamVideoInfo().streamStartTime; switch (timeType()) { case TimeType.UTC: return wubloaderTimeFromDateTime(startTime); case TimeType.BusTime: return busTimeFromDateTime(props.busStartTime(), startTime); case TimeType.TimeAgo: return timeAgoFromDateTime(startTime); } }; const endTimeDisplay = () => { const endTime = props.streamVideoInfo().streamEndTime; if (endTime === null) { return ""; } switch (timeType()) { case TimeType.UTC: return wubloaderTimeFromDateTime(endTime); case TimeType.BusTime: return busTimeFromDateTime(props.busStartTime(), endTime); case TimeType.TimeAgo: return timeAgoFromDateTime(endTime); } }; const timeRangeLink = () => { const streamInfo = props.streamVideoInfo(); const startTime = wubloaderTimeFromDateTime(streamInfo.streamStartTime); const query = new URLSearchParams({ stream: streamInfo.streamName, start: startTime, }); if (streamInfo.streamEndTime) { const endTime = wubloaderTimeFromDateTime(streamInfo.streamEndTime); query.append("end", endTime); } return `?${query}`; }; return (
); }; export interface VideoPlayerProps { src: Accessor; setPlayerTime: Setter; mediaPlayer: Accessor; setMediaPlayer: Setter; } export const VideoPlayer: Component = (props) => { createEffect(() => { const player = props.mediaPlayer(); const srcURL = props.src(); player.src = srcURL; }); let [playerTime, setPlayerTime] = createSignal(0); let [duration, setDuration] = createSignal(0); onMount(() => { const player = props.mediaPlayer(); player.subscribe(({ currentTime, duration }) => { setPlayerTime(currentTime); props.setPlayerTime(currentTime); setDuration(duration); }); player.streamType = "on-demand"; }); // The elements provided by vidstack don't show milliseconds, so // we need to run our own for millisecond display. const formatTime = (time: number) => { const hours = Math.floor(time / 3600); const minutes = Math.floor((time / 60) % 60); const milliseconds = Math.floor((time % 1) * 1000); const seconds = Math.floor(time % 60); const minutesDisplay = minutes.toString().padStart(2, "0"); const secondsDisplay = seconds.toString().padStart(2, "0"); const millisecondsDisplay = milliseconds.toString().padStart(3, "0"); if (hours === 0) { return `${minutesDisplay}:${secondsDisplay}.${millisecondsDisplay}`; } return `${hours}:${minutesDisplay}:${secondsDisplay}.${millisecondsDisplay}`; }; return ( { const player = props.mediaPlayer(); if (player.paused) { player.play(event); } else { player.pause(event); } }} /> Play Pause Unmute Mute
{formatTime(playerTime())} / {formatTime(duration())}
Turn Closed Captions Off Turn Closed Captions On Enter Fullscreen Exit Fullscreen
); }; export interface KeyboardShortcutProps { includeEditorShortcuts: boolean; } export const KeyboardShortcuts: Component = ( props: KeyboardShortcutProps, ) => { return (
Keyboard Shortcuts
  • Number keys (0-9): Jump to that 10% interval of the video (0% - 90%)
  • K or Space: Toggle pause
  • M: Toggle mute
  • J: Back 10 seconds
  • L: Forward 10 seconds
  • Left arrow: Back 5 seconds
  • Right arrow: Forward 5 seconds
  • Shift+J: Back 1 second
  • Shift+L: Forward 1 second
  • Comma (,): Back 1 frame
  • Period (.): Forward 1 frame
  • Equals (=): Increase playback speed 1 step
  • Hyphen (-): Decrease playback speed 1 step
  • Shift+=: 2x or maximum playback speed
  • Shift+-: Minimum playback speed
  • Backspace: Reset playback speed to 1x
  • Left bracket ([): Set start point for active range (indicated by arrow) to current video time
  • Right bracket (]): Set end point for active range to current video time
  • O: Set active range one above current active range
  • P: Set active range one below current active range, adding a new range if the current range is the last one
); };