Add restreamer links to download video and frames

thrimbletrimmer-solid
ElementalAlchemist 1 week ago
parent f748ad35e0
commit d09bc91de6

@ -1,4 +1,6 @@
import { DateTime } from "luxon"; import { DateTime } from "luxon";
import { HLSProvider } from "vidstack";
import { Fragment } from "hls.js";
export enum TimeType { export enum TimeType {
UTC, UTC,
@ -99,3 +101,29 @@ export function timeAgoFromDateTime(dateTime: DateTime): string {
return `${negative}${hours}:${minutesString}:${secondsString}`; return `${negative}${hours}:${minutesString}:${secondsString}`;
} }
export function dateTimeFromVideoPlayerTime(
videoProvider: HLSProvider,
videoTime: number,
): DateTime | null {
// We do a little bit of cheating our way into private data. The standard way of getting this involves
// handling events and capturing and saving a fragments array from it, which seems overly cumbersome.
const fragments = (videoProvider.instance as any).latencyController.levelDetails
.fragments as Fragment[];
let fragmentStartTime: number | undefined = undefined;
let fragmentStartISOTime: string | undefined = undefined;
for (const fragment of fragments) {
const fragmentEndTime = fragment.start + fragment.duration;
if (videoTime >= fragment.start && videoTime < fragmentEndTime) {
fragmentStartTime = fragment.start;
fragmentStartISOTime = fragment.rawProgramDateTime;
break;
}
}
if (fragmentStartISOTime === undefined) {
return null;
}
const wubloaderTime = DateTime.fromISO(fragmentStartISOTime);
const offset = videoTime - fragmentStartTime;
return wubloaderTime.plus({ seconds: offset });
}

@ -3,7 +3,9 @@ media-player {
flex-direction: column; flex-direction: column;
} }
media-provider, media-captions, video { media-provider,
media-captions,
video {
max-width: 100%; max-width: 100%;
max-height: 50vh; max-height: 50vh;
} }

@ -21,6 +21,7 @@ import {
} from "./convertTime"; } from "./convertTime";
import styles from "./video.module.scss"; import styles from "./video.module.scss";
import "./video.scss"; import "./video.scss";
import { HLSProvider } from "vidstack";
import { MediaPlayerElement } from "vidstack/elements"; import { MediaPlayerElement } from "vidstack/elements";
import "vidstack/icons"; import "vidstack/icons";
@ -200,12 +201,14 @@ export const StreamTimeSettings: Component<StreamTimeSettingsProps> = (props) =>
export interface VideoPlayerProps { export interface VideoPlayerProps {
src: Accessor<string>; src: Accessor<string>;
setPlayerTime: Setter<number>;
mediaPlayer: Accessor<MediaPlayerElement>;
setMediaPlayer: Setter<MediaPlayerElement>;
} }
export const VideoPlayer: Component<VideoPlayerProps> = (props) => { export const VideoPlayer: Component<VideoPlayerProps> = (props) => {
let [mediaPlayer, setMediaPlayer] = createSignal<MediaPlayerElement>();
createEffect(() => { createEffect(() => {
const player = mediaPlayer(); const player = props.mediaPlayer();
const srcURL = props.src(); const srcURL = props.src();
player.src = srcURL; player.src = srcURL;
}); });
@ -214,9 +217,10 @@ export const VideoPlayer: Component<VideoPlayerProps> = (props) => {
let [duration, setDuration] = createSignal(0); let [duration, setDuration] = createSignal(0);
onMount(() => { onMount(() => {
const player = mediaPlayer(); const player = props.mediaPlayer();
player.subscribe(({ currentTime, duration }) => { player.subscribe(({ currentTime, duration }) => {
setPlayerTime(currentTime); setPlayerTime(currentTime);
props.setPlayerTime(currentTime);
setDuration(duration); setDuration(duration);
}); });
player.streamType = "on-demand"; player.streamType = "on-demand";
@ -243,14 +247,14 @@ export const VideoPlayer: Component<VideoPlayerProps> = (props) => {
return ( return (
<media-player <media-player
src={props.src()} src={props.src()}
ref={setMediaPlayer} ref={props.setMediaPlayer}
preload="auto" preload="auto"
controlsDelay={0} controlsDelay={0}
storage="thrimbletrimmer" storage="thrimbletrimmer"
> >
<media-provider <media-provider
onClick={(event) => { onClick={(event) => {
const player = mediaPlayer(); const player = props.mediaPlayer();
if (player.paused) { if (player.paused) {
player.play(event); player.play(event);
} else { } else {

@ -19,3 +19,7 @@
top: 0; top: 0;
right: 0; right: 0;
} }
.videoLinks a {
margin: 2px;
}

@ -10,8 +10,14 @@ import {
Suspense, Suspense,
} from "solid-js"; } from "solid-js";
import { DateTime } from "luxon"; import { DateTime } from "luxon";
import { HLSProvider } from "vidstack";
import { MediaPlayerElement } from "vidstack/elements";
import styles from "./Restreamer.module.scss"; import styles from "./Restreamer.module.scss";
import { dateTimeFromWubloaderTime, wubloaderTimeFromDateTime } from "../common/convertTime"; import {
dateTimeFromVideoPlayerTime,
dateTimeFromWubloaderTime,
wubloaderTimeFromDateTime,
} from "../common/convertTime";
import { import {
KeyboardShortcuts, KeyboardShortcuts,
StreamTimeSettings, StreamTimeSettings,
@ -92,6 +98,8 @@ const RestreamerWithDefaults: Component<RestreamerDefaultProps> = (props) => {
streamStartTime: DateTime.utc().minus({ minutes: 10 }), streamStartTime: DateTime.utc().minus({ minutes: 10 }),
streamEndTime: null, streamEndTime: null,
}); });
const [playerTime, setPlayerTime] = createSignal<number>(0);
const [mediaPlayer, setMediaPlayer] = createSignal<MediaPlayerElement>();
const videoURL = () => { const videoURL = () => {
const streamInfo = streamVideoInfo(); const streamInfo = streamVideoInfo();
@ -109,6 +117,33 @@ const RestreamerWithDefaults: Component<RestreamerDefaultProps> = (props) => {
return url; return url;
}; };
const downloadVideoURL = () => {
const streamInfo = streamVideoInfo();
const startTime = wubloaderTimeFromDateTime(streamInfo.streamStartTime);
const params = new URLSearchParams({ type: "smart", start: encodeURIComponent(startTime) });
if (streamInfo.streamEndTime) {
const endTime = wubloaderTimeFromDateTime(streamInfo.streamEndTime);
params.append("end", endTime);
}
return `/cut/${streamInfo.streamName}/source.ts?${params}`;
};
const downloadFrameURL = () => {
const streamInfo = streamVideoInfo();
const player = mediaPlayer();
const videoTime = playerTime();
const provider = player.provider as HLSProvider;
if (!provider) {
return "";
}
const currentTime = dateTimeFromVideoPlayerTime(provider, videoTime);
if (currentTime === null) {
return "";
}
const wubloaderTime = wubloaderTimeFromDateTime(currentTime);
return `/frame/${streamInfo.streamName}/source.png?timestamp=${wubloaderTime}`;
};
return ( return (
<> <>
<StreamTimeSettings <StreamTimeSettings
@ -119,7 +154,16 @@ const RestreamerWithDefaults: Component<RestreamerDefaultProps> = (props) => {
errorList={props.errorList} errorList={props.errorList}
setErrorList={props.setErrorList} setErrorList={props.setErrorList}
/> />
<VideoPlayer src={videoURL} /> <VideoPlayer
src={videoURL}
setPlayerTime={setPlayerTime}
mediaPlayer={mediaPlayer}
setMediaPlayer={setMediaPlayer}
/>
<div class={styles.videoLinks}>
<a href={downloadVideoURL()}>Download Video</a>
<a href={downloadFrameURL()}>Download Current Frame as Image</a>
</div>
</> </>
); );
}; };

Loading…
Cancel
Save