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 { HLSProvider } from "vidstack";
import { Fragment } from "hls.js";
export enum TimeType {
UTC,
@ -99,3 +101,29 @@ export function timeAgoFromDateTime(dateTime: DateTime): string {
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;
}
media-provider, media-captions, video {
media-provider,
media-captions,
video {
max-width: 100%;
max-height: 50vh;
}

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

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

@ -10,8 +10,14 @@ import {
Suspense,
} from "solid-js";
import { DateTime } from "luxon";
import { HLSProvider } from "vidstack";
import { MediaPlayerElement } from "vidstack/elements";
import styles from "./Restreamer.module.scss";
import { dateTimeFromWubloaderTime, wubloaderTimeFromDateTime } from "../common/convertTime";
import {
dateTimeFromVideoPlayerTime,
dateTimeFromWubloaderTime,
wubloaderTimeFromDateTime,
} from "../common/convertTime";
import {
KeyboardShortcuts,
StreamTimeSettings,
@ -92,6 +98,8 @@ const RestreamerWithDefaults: Component<RestreamerDefaultProps> = (props) => {
streamStartTime: DateTime.utc().minus({ minutes: 10 }),
streamEndTime: null,
});
const [playerTime, setPlayerTime] = createSignal<number>(0);
const [mediaPlayer, setMediaPlayer] = createSignal<MediaPlayerElement>();
const videoURL = () => {
const streamInfo = streamVideoInfo();
@ -109,6 +117,33 @@ const RestreamerWithDefaults: Component<RestreamerDefaultProps> = (props) => {
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 (
<>
<StreamTimeSettings
@ -119,7 +154,16 @@ const RestreamerWithDefaults: Component<RestreamerDefaultProps> = (props) => {
errorList={props.errorList}
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