Skip to content

Commit

Permalink
Improve Handy integration (#2555)
Browse files Browse the repository at this point in the history
* Refactor interactive into context
* Stop the interactive device when leaving page
* Show interactive state if not ready
* Handle navigation and looping
  • Loading branch information
WithoutPants authored May 10, 2022
1 parent bc85614 commit ea2fcd9
Show file tree
Hide file tree
Showing 10 changed files with 575 additions and 65 deletions.
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

0 comments on commit ea2fcd9

Please sign in to comment.