mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 20:34:37 +03:00
226 lines
5.9 KiB
TypeScript
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
|
|
}
|
|
/>
|
|
);
|
|
};
|