Files
stash/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx
2020-06-10 10:15:02 +10:00

226 lines
5.9 KiB
TypeScript

import React from "react";
import ReactJWPlayer from "react-jw-player";
import * as GQL from "src/core/generated-graphql";
import { useConfiguration } from "src/core/StashService";
import { JWUtils } from "src/utils";
import { ScenePlayerScrubber } from "./ScenePlayerScrubber";
interface IScenePlayerProps {
scene: GQL.SceneDataFragment;
timestamp: number;
autoplay?: boolean;
onReady?: () => void;
onSeeked?: () => void;
onTime?: () => void;
config?: GQL.ConfigInterfaceDataFragment;
}
interface IScenePlayerState {
scrubberPosition: number;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
config: Record<string, any>;
}
export class ScenePlayerImpl extends React.Component<
IScenePlayerProps,
IScenePlayerState
> {
// Typings for jwplayer are, unfortunately, very lacking
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private player: any;
private lastTime = 0;
constructor(props: IScenePlayerProps) {
super(props);
this.onReady = this.onReady.bind(this);
this.onSeeked = this.onSeeked.bind(this);
this.onTime = this.onTime.bind(this);
this.onScrubberSeek = this.onScrubberSeek.bind(this);
this.onScrubberScrolled = this.onScrubberScrolled.bind(this);
this.state = {
scrubberPosition: 0,
config: this.makeJWPlayerConfig(props.scene),
};
}
public UNSAFE_componentWillReceiveProps(props: IScenePlayerProps) {
if (props.scene !== this.props.scene) {
this.setState((state) => ({
...state,
config: this.makeJWPlayerConfig(this.props.scene),
}));
}
}
public componentDidUpdate(prevProps: IScenePlayerProps) {
if (prevProps.timestamp !== this.props.timestamp) {
this.player.seek(this.props.timestamp);
}
}
onIncrease() {
const currentPlaybackRate = this.player ? this.player.getPlaybackRate() : 1;
this.player.setPlaybackRate(currentPlaybackRate + 0.5);
}
onDecrease() {
const currentPlaybackRate = this.player ? this.player.getPlaybackRate() : 1;
this.player.setPlaybackRate(currentPlaybackRate - 0.5);
}
onReset() {
this.player.setPlaybackRate(1);
}
onPause() {
if (this.player.getState().paused) this.player.play();
else this.player.pause();
}
private onReady() {
this.player = JWUtils.getPlayer();
if (this.props.timestamp > 0) {
this.player.seek(this.props.timestamp);
}
}
private onSeeked() {
const position = this.player.getPosition();
this.setState({ scrubberPosition: position });
this.player.play();
}
private onTime() {
const position = this.player.getPosition();
const difference = Math.abs(position - this.lastTime);
if (difference > 1) {
this.lastTime = position;
this.setState({ scrubberPosition: position });
}
}
private onScrubberSeek(seconds: number) {
this.player.seek(seconds);
}
private onScrubberScrolled() {
this.player.pause();
}
private shouldRepeat(scene: GQL.SceneDataFragment) {
const maxLoopDuration = this.props?.config?.maximumLoopDuration ?? 0;
return (
!!scene.file.duration &&
!!maxLoopDuration &&
scene.file.duration < maxLoopDuration
);
}
private makeJWPlayerConfig(scene: GQL.SceneDataFragment) {
if (!scene.paths.stream) {
return {};
}
const repeat = this.shouldRepeat(scene);
let getDurationHook: (() => GQL.Maybe<number>) | undefined;
let seekHook:
| ((seekToPosition: number, _videoTag: HTMLVideoElement) => void)
| undefined;
let getCurrentTimeHook:
| ((_videoTag: HTMLVideoElement) => number)
| undefined;
if (!this.props.scene.is_streamable) {
getDurationHook = () => {
return this.props.scene.file.duration ?? null;
};
seekHook = (seekToPosition: number, _videoTag: HTMLVideoElement) => {
/* eslint-disable no-param-reassign */
_videoTag.dataset.start = seekToPosition.toString();
_videoTag.src = `${this.props.scene.paths.stream}?start=${seekToPosition}`;
/* eslint-enable no-param-reassign */
_videoTag.play();
};
getCurrentTimeHook = (_videoTag: HTMLVideoElement) => {
const start = Number.parseInt(_videoTag.dataset?.start ?? "0", 10);
return _videoTag.currentTime + start;
};
}
const ret = {
file: scene.paths.stream,
image: scene.paths.screenshot,
tracks: [
{
file: scene.paths.vtt,
kind: "thumbnails",
},
{
file: scene.paths.chapters_vtt,
kind: "chapters",
},
],
aspectratio: "16:9",
width: "100%",
floating: {
dismissible: true,
},
cast: {},
primary: "html5",
autostart:
this.props.autoplay ||
(this.props.config ? this.props.config.autostartVideo : false),
repeat,
playbackRateControls: true,
playbackRates: [0.75, 1, 1.5, 2, 3, 4],
getDurationHook,
seekHook,
getCurrentTimeHook,
};
return ret;
}
public render() {
return (
<div
id="jwplayer-container"
className="w-100 col-sm-9 m-sm-auto no-gutter"
>
<ReactJWPlayer
playerId={JWUtils.playerID}
playerScript="/jwplayer/jwplayer.js"
customProps={this.state.config}
onReady={this.onReady}
onSeeked={this.onSeeked}
onTime={this.onTime}
/>
<ScenePlayerScrubber
scene={this.props.scene}
position={this.state.scrubberPosition}
onSeek={this.onScrubberSeek}
onScrolled={this.onScrubberScrolled}
/>
</div>
);
}
}
export const ScenePlayer: React.FC<IScenePlayerProps> = (
props: IScenePlayerProps
) => {
const config = useConfiguration();
return (
<ScenePlayerImpl
{...props}
config={
config.data && config.data.configuration
? config.data.configuration.interface
: undefined
}
/>
);
};