Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve Handy integration #2555

Merged
merged 9 commits into from
May 10, 2022
Merged
15 changes: 9 additions & 6 deletions ui/v2.5/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import * as GQL from "./core/generated-graphql";
import { LoadingIndicator, TITLE_SUFFIX } from "./components/Shared";
import { ConfigurationProvider } from "./hooks/Config";
import { ManualProvider } from "./components/Help/Manual";
import { InteractiveProvider } from "./hooks/Interactive/context";

initPolyfills();

Expand Down Expand Up @@ -150,12 +151,14 @@ export const App: React.FC = () => {
<ToastProvider>
<LightboxProvider>
<ManualProvider>
<Helmet
titleTemplate={`%s ${TITLE_SUFFIX}`}
defaultTitle="Stash"
/>
{maybeRenderNavbar()}
<div className="main container-fluid">{renderContent()}</div>
<InteractiveProvider>
<Helmet
titleTemplate={`%s ${TITLE_SUFFIX}`}
defaultTitle="Stash"
/>
{maybeRenderNavbar()}
<div className="main container-fluid">{renderContent()}</div>
</InteractiveProvider>
</ManualProvider>
</LightboxProvider>
</ToastProvider>
Expand Down
3 changes: 3 additions & 0 deletions ui/v2.5/src/components/Changelog/versions/v0150.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
### ✨ New Features
* Show Handy status on scene player where applicable. ([#2555](https://github.com/stashapp/stash/pull/2555))
* Added recommendations to home page. ([#2571](https://github.com/stashapp/stash/pull/2571))
* Add support for VTT and SRT captions for scenes. ([#2462](https://github.com/stashapp/stash/pull/2462))
* Added option to require a number of scroll attempts before navigating to next/previous image in Lightbox. ([#2544](https://github.com/stashapp/stash/pull/2544))

### 🎨 Improvements
* Added Handy server sync button to Interface settings page. ([#2555](https://github.com/stashapp/stash/pull/2555))
* Changed playback rate options to be the same as those provided by YouTube. ([#2550](https://github.com/stashapp/stash/pull/2550))
* Display error message on fatal error when running stash with double-click in Windows. ([#2543](https://github.com/stashapp/stash/pull/2543))

### 🐛 Bug fixes
* Fix long Handy initialisation delay. ([#2555](https://github.com/stashapp/stash/pull/2555))
* Fix lightbox autoplaying while offscreen. ([#2563](https://github.com/stashapp/stash/pull/2563))
* Fix playback rate resetting when seeking. ([#2550](https://github.com/stashapp/stash/pull/2550))
* Fix video not starting when clicking scene scrubber. ([#2546](https://github.com/stashapp/stash/pull/2546))
Expand Down
193 changes: 143 additions & 50 deletions ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import React, { useContext, useEffect, useRef, useState } from "react";
import React, {
useCallback,
useContext,
useEffect,
useRef,
useState,
} from "react";
import VideoJS, { VideoJsPlayer, VideoJsPlayerOptions } from "video.js";
import "videojs-vtt-thumbnails-freetube";
import "videojs-seek-buttons";
Expand All @@ -15,7 +21,11 @@ import cx from "classnames";
import * as GQL from "src/core/generated-graphql";
import { ScenePlayerScrubber } from "./ScenePlayerScrubber";
import { ConfigurationContext } from "src/hooks/Config";
import { Interactive } from "src/utils/interactive";
import {
ConnectionState,
InteractiveContext,
} from "src/hooks/Interactive/context";
import { SceneInteractiveStatus } from "src/hooks/Interactive/status";
import { languageMap } from "src/utils/caption";

export const VIDEO_PLAYER_ID = "VideoJsPlayer";
Expand Down Expand Up @@ -117,11 +127,18 @@ export const ScenePlayer: React.FC<IScenePlayerProps> = ({

const [time, setTime] = useState(0);

const [interactiveClient] = useState(
new Interactive(config?.handyKey || "", config?.funscriptOffset || 0)
);
const {
interactive: interactiveClient,
uploadScript,
currentScript,
initialised: interactiveInitialised,
state: interactiveState,
} = React.useContext(InteractiveContext);

const [initialTimestamp] = useState(timestamp);
const [ready, setReady] = useState(false);
const started = useRef(false);
const interactiveReady = useRef(false);

const maxLoopDuration = config?.maximumLoopDuration ?? 0;

Expand Down Expand Up @@ -188,10 +205,18 @@ export const ScenePlayer: React.FC<IScenePlayerProps> = ({
}, []);

useEffect(() => {
if (scene?.interactive) {
interactiveClient.uploadScript(scene.paths.funscript || "");
if (scene?.interactive && interactiveInitialised) {
interactiveReady.current = false;
uploadScript(scene.paths.funscript || "").then(() => {
interactiveReady.current = true;
});
}
}, [interactiveClient, scene?.interactive, scene?.paths.funscript]);
}, [
uploadScript,
interactiveInitialised,
scene?.interactive,
scene?.paths.funscript,
]);

useEffect(() => {
if (skipButtonsRef.current) {
Expand Down Expand Up @@ -222,6 +247,24 @@ export const ScenePlayer: React.FC<IScenePlayerProps> = ({
};
}, []);

const start = useCallback(() => {
const player = playerRef.current;
if (player && scene) {
started.current = true;

player
.play()
?.then(() => {
if (initialTimestamp > 0) {
player.currentTime(initialTimestamp);
}
})
.catch(() => {
if (scene.paths.screenshot) player.poster(scene.paths.screenshot);
});
}
}, [scene, initialTimestamp]);

useEffect(() => {
let prevCaptionOffset = 0;

Expand Down Expand Up @@ -374,6 +417,10 @@ export const ScenePlayer: React.FC<IScenePlayerProps> = ({
}
}

// always stop the interactive client on initialisation
interactiveClient.pause();
interactiveReady.current = false;

if (!scene || scene.id === sceneId.current) return;
sceneId.current = scene.id;

Expand Down Expand Up @@ -420,94 +467,137 @@ export const ScenePlayer: React.FC<IScenePlayerProps> = ({

player.currentTime(0);

player.loop(
const looping =
!!scene.file.duration &&
maxLoopDuration !== 0 &&
scene.file.duration < maxLoopDuration
);
maxLoopDuration !== 0 &&
scene.file.duration < maxLoopDuration;
player.loop(looping);
interactiveClient.setLooping(looping);

player.on("loadstart", function (this: VideoJsPlayer) {
function loadstart(this: VideoJsPlayer) {
// handle offset after loading so that we get the correct current source
handleOffset(this);
});
}

player.on("loadstart", loadstart);

player.on("play", function (this: VideoJsPlayer) {
player.poster("");
if (scene.interactive) {
function onPlay(this: VideoJsPlayer) {
this.poster("");
if (scene?.interactive && interactiveReady.current) {
interactiveClient.play(this.currentTime());
}
});
}
player.on("play", onPlay);

player.on("pause", () => {
if (scene.interactive) {
interactiveClient.pause();
}
});
function pause() {
interactiveClient.pause();
}
player.on("pause", pause);

player.on("timeupdate", function (this: VideoJsPlayer) {
if (scene.interactive) {
function timeupdate(this: VideoJsPlayer) {
if (scene?.interactive && interactiveReady.current) {
interactiveClient.ensurePlaying(this.currentTime());
}
setTime(this.currentTime());
});
}
player.on("timeupdate", timeupdate);

player.on("seeking", function (this: VideoJsPlayer) {
function seeking(this: VideoJsPlayer) {
this.play();
});
}
player.on("seeking", seeking);

player.on("error", () => {
function error() {
handleError(true);
});
}
player.on("error", error);

// changing source (eg when seeking) resets the playback rate
// so set the default in addition to the current rate
player.on("ratechange", function (this: VideoJsPlayer) {
function ratechange(this: VideoJsPlayer) {
this.defaultPlaybackRate(this.playbackRate());
});
}
player.on("ratechange", ratechange);

player.on("loadedmetadata", () => {
if (!player.videoWidth() && !player.videoHeight()) {
function loadedmetadata(this: VideoJsPlayer) {
if (!this.videoWidth() && !this.videoHeight()) {
// Occurs during preload when videos with supported audio/unsupported video are preloaded.
// Treat this as a decoding error and try the next source without playing.
// However on Safari we get an media event when m3u8 is loaded which needs to be ignored.
const currentFile = player.currentSrc();
const currentFile = this.currentSrc();
if (currentFile != null && !currentFile.includes("m3u8")) {
// const play = !player.paused();
// handleError(play);
player.error(MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED);
this.error(MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED);
}
}
});
}
player.on("loadedmetadata", loadedmetadata);

player.load();

if (auto) {
player
.play()
?.then(() => {
if (initialTimestamp > 0) {
player.currentTime(initialTimestamp);
}
})
.catch(() => {
if (scene.paths.screenshot) player.poster(scene.paths.screenshot);
});
}

if ((player as any).vttThumbnails?.src)
(player as any).vttThumbnails?.src(scene?.paths.vtt);
else
(player as any).vttThumbnails({
src: scene?.paths.vtt,
showTimestamp: true,
});

setReady(true);
started.current = false;

return () => {
setReady(false);

// stop the interactive client
interactiveClient.pause();

player.off("loadstart", loadstart);
player.off("play", onPlay);
player.off("pause", pause);
player.off("timeupdate", timeupdate);
player.off("seeking", seeking);
player.off("error", error);
player.off("ratechange", ratechange);
player.off("loadedmetadata", loadedmetadata);
};
}, [
scene,
config?.autostartVideo,
maxLoopDuration,
initialTimestamp,
autoplay,
interactiveClient,
start,
]);

useEffect(() => {
if (!ready || started.current) {
return;
}

const auto =
autoplay || (config?.autostartVideo ?? false) || initialTimestamp > 0;

// check if we're waiting for the interactive client
const interactiveWaiting =
scene?.interactive &&
interactiveClient.handyKey &&
currentScript !== scene.paths.funscript;

if (scene && auto && !interactiveWaiting) {
start();
}
}, [
config?.autostartVideo,
initialTimestamp,
scene,
ready,
interactiveClient,
currentScript,
autoplay,
start,
]);

useEffect(() => {
Expand Down Expand Up @@ -550,6 +640,9 @@ export const ScenePlayer: React.FC<IScenePlayerProps> = ({
className="video-js vjs-big-play-centered"
/>
</div>
{scene?.interactive &&
(interactiveState !== ConnectionState.Ready ||
playerRef.current?.paused()) && <SceneInteractiveStatus />}
{scene && (
<ScenePlayerScrubber
scene={scene}
Expand Down
Loading