mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 12:24:38 +03:00
Replace JW Player with video.js (#2100)
* Replace JW Player with video.js * Move HLS stream to bottom of list HLS doesn't work very well on non-ios devices. Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
This commit is contained in:
@@ -1,447 +1,344 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import React from "react";
|
||||
import ReactJWPlayer from "react-jw-player";
|
||||
import React, { useContext, useEffect, useRef, useState } from "react";
|
||||
import VideoJS, { VideoJsPlayer, VideoJsPlayerOptions } from "video.js";
|
||||
import "videojs-vtt-thumbnails-freetube";
|
||||
import "videojs-seek-buttons";
|
||||
import "videojs-landscape-fullscreen";
|
||||
import "./live";
|
||||
import "./PlaylistButtons";
|
||||
import cx from "classnames";
|
||||
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { JWUtils, ScreenUtils } from "src/utils";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
import { ScenePlayerScrubber } from "./ScenePlayerScrubber";
|
||||
import { Interactive } from "../../utils/interactive";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
import { Interactive } from "src/utils/interactive";
|
||||
|
||||
/*
|
||||
fast-forward svg derived from https://github.com/jwplayer/jwplayer/blob/master/src/assets/SVG/rewind-10.svg
|
||||
Flipped horizontally, then flipped '10' numerals horizontally.
|
||||
|
||||
Creative Common License: https://github.com/jwplayer/jwplayer/blob/master/LICENSE
|
||||
*/
|
||||
const ffSVG = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="jw-svg-icon jw-svg-icon-rewind" viewBox="0 0 240 240" focusable="false">
|
||||
<path d="M185,135.6c-3.7-6.3-10.4-10.3-17.7-10.6c-7.3,0.3-14,4.3-17.7,10.6c-8.6,14.2-8.6,32.1,0,46.3c3.7,6.3,10.4,10.3,17.7,10.6
|
||||
c7.3-0.3,14-4.3,17.7-10.6C193.6,167.6,193.6,149.8,185,135.6z M167.3,182.8c-7.8,0-14.4-11-14.4-24.1s6.6-24.1,14.4-24.1
|
||||
s14.4,11,14.4,24.1S175.2,182.8,167.3,182.8z M123.9,192.5v-51l-4.8,4.8l-6.8-6.8l13-13c1.9-1.9,4.9-1.9,6.8,0
|
||||
c0.9,0.9,1.4,2.1,1.4,3.4v62.7L123.9,192.5z M22.7,57.4h130.1V38.1c0-5.3,3.6-7.2,8-4.3l41.8,27.9c1.2,0.6,2.1,1.5,2.7,2.7
|
||||
c1.4,3,0.2,6.5-2.7,8l-41.8,27.9c-4.4,2.9-8,1-8-4.3V76.7H37.1v96.4h48.2v19.3H22.6c-2.6,0-4.8-2.2-4.8-4.8V62.3
|
||||
C17.8,59.6,20,57.4,22.7,57.4z">
|
||||
</path>
|
||||
</svg>
|
||||
`;
|
||||
export const VIDEO_PLAYER_ID = "VideoJsPlayer";
|
||||
|
||||
interface IScenePlayerProps {
|
||||
className?: string;
|
||||
scene: GQL.SceneDataFragment;
|
||||
sceneStreams: GQL.SceneStreamEndpoint[];
|
||||
scene: GQL.SceneDataFragment | undefined | null;
|
||||
timestamp: number;
|
||||
autoplay?: boolean;
|
||||
onReady?: () => void;
|
||||
onSeeked?: () => void;
|
||||
onTime?: () => void;
|
||||
onComplete?: () => void;
|
||||
config?: GQL.ConfigInterfaceDataFragment;
|
||||
onNext?: () => void;
|
||||
onPrevious?: () => void;
|
||||
}
|
||||
interface IScenePlayerState {
|
||||
scrubberPosition: number;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
config: Record<string, any>;
|
||||
interactiveClient: Interactive;
|
||||
}
|
||||
export class ScenePlayerImpl extends React.Component<
|
||||
IScenePlayerProps,
|
||||
IScenePlayerState
|
||||
> {
|
||||
private static isDirectStream(src?: string) {
|
||||
if (!src) {
|
||||
|
||||
export const ScenePlayer: React.FC<IScenePlayerProps> = ({
|
||||
className,
|
||||
autoplay,
|
||||
scene,
|
||||
timestamp,
|
||||
onComplete,
|
||||
onNext,
|
||||
onPrevious,
|
||||
}) => {
|
||||
const { configuration } = useContext(ConfigurationContext);
|
||||
const config = configuration?.interface;
|
||||
const videoRef = useRef<HTMLVideoElement>(null);
|
||||
const playerRef = useRef<VideoJsPlayer | undefined>();
|
||||
const skipButtonsRef = useRef<any>();
|
||||
|
||||
const [time, setTime] = useState(0);
|
||||
|
||||
const [interactiveClient] = useState(
|
||||
new Interactive(config?.handyKey || "", config?.funscriptOffset || 0)
|
||||
);
|
||||
|
||||
const [initialTimestamp] = useState(timestamp);
|
||||
|
||||
const maxLoopDuration = config?.maximumLoopDuration ?? 0;
|
||||
|
||||
useEffect(() => {
|
||||
if (playerRef.current && timestamp >= 0) {
|
||||
const player = playerRef.current;
|
||||
player.play()?.then(() => {
|
||||
player.currentTime(timestamp);
|
||||
});
|
||||
}
|
||||
}, [timestamp]);
|
||||
|
||||
useEffect(() => {
|
||||
const videoElement = videoRef.current;
|
||||
if (!videoElement) return;
|
||||
|
||||
const options: VideoJsPlayerOptions = {
|
||||
controls: true,
|
||||
controlBar: {
|
||||
pictureInPictureToggle: false,
|
||||
volumePanel: {
|
||||
inline: false,
|
||||
},
|
||||
},
|
||||
nativeControlsForTouch: false,
|
||||
playbackRates: [0.75, 1, 1.5, 2, 3, 4],
|
||||
inactivityTimeout: 2000,
|
||||
preload: "none",
|
||||
userActions: {
|
||||
hotkeys: true,
|
||||
},
|
||||
};
|
||||
|
||||
const player = VideoJS(videoElement, options);
|
||||
|
||||
(player as any).landscapeFullscreen({
|
||||
fullscreen: {
|
||||
enterOnRotate: true,
|
||||
exitOnRotate: true,
|
||||
alwaysInLandscapeMode: true,
|
||||
iOS: true,
|
||||
},
|
||||
});
|
||||
|
||||
(player as any).offset();
|
||||
|
||||
player.focus();
|
||||
playerRef.current = player;
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (scene?.interactive) {
|
||||
interactiveClient.uploadScript(scene.paths.funscript || "");
|
||||
}
|
||||
}, [interactiveClient, scene?.interactive, scene?.paths.funscript]);
|
||||
|
||||
useEffect(() => {
|
||||
if (skipButtonsRef.current) {
|
||||
skipButtonsRef.current.setForwardHandler(onNext);
|
||||
skipButtonsRef.current.setBackwardHandler(onPrevious);
|
||||
}
|
||||
}, [onNext, onPrevious]);
|
||||
|
||||
useEffect(() => {
|
||||
const player = playerRef.current;
|
||||
if (player) {
|
||||
player.seekButtons({
|
||||
forward: 10,
|
||||
back: 10,
|
||||
});
|
||||
|
||||
skipButtonsRef.current = player.skipButtons() ?? undefined;
|
||||
|
||||
player.focus();
|
||||
}
|
||||
|
||||
// Video player destructor
|
||||
return () => {
|
||||
if (playerRef.current) {
|
||||
playerRef.current.dispose();
|
||||
playerRef.current = undefined;
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
function handleOffset(player: VideoJsPlayer) {
|
||||
if (!scene) return;
|
||||
|
||||
const currentSrc = player.currentSrc();
|
||||
|
||||
const isDirect =
|
||||
currentSrc.endsWith("/stream") || currentSrc.endsWith("/stream.m3u8");
|
||||
if (!isDirect) {
|
||||
(player as any).setOffsetDuration(scene.file.duration);
|
||||
} else {
|
||||
(player as any).clearOffsetDuration();
|
||||
}
|
||||
}
|
||||
|
||||
function handleError(play: boolean) {
|
||||
const player = playerRef.current;
|
||||
if (!player) return;
|
||||
|
||||
const currentFile = player.currentSource();
|
||||
if (currentFile) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`Source failed: ${currentFile.src}`);
|
||||
player.focus();
|
||||
}
|
||||
|
||||
if (tryNextStream()) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`Trying next source in playlist: ${player.currentSrc()}`);
|
||||
player.load();
|
||||
if (play) {
|
||||
player.play();
|
||||
}
|
||||
} else {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log("No more sources in playlist.");
|
||||
}
|
||||
}
|
||||
|
||||
function tryNextStream() {
|
||||
const player = playerRef.current;
|
||||
if (!player) return;
|
||||
|
||||
const sources = player.currentSources();
|
||||
|
||||
if (sources.length > 1) {
|
||||
sources.shift();
|
||||
player.src(sources);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
const url = new URL(src);
|
||||
return url.pathname.endsWith("/stream");
|
||||
}
|
||||
if (!scene) return;
|
||||
|
||||
// Typings for jwplayer are, unfortunately, very lacking
|
||||
private player: any;
|
||||
private playlist: any;
|
||||
private lastTime = 0;
|
||||
const player = playerRef.current;
|
||||
if (!player) return;
|
||||
|
||||
constructor(props: IScenePlayerProps) {
|
||||
super(props);
|
||||
this.onReady = this.onReady.bind(this);
|
||||
this.onSeeked = this.onSeeked.bind(this);
|
||||
this.onTime = this.onTime.bind(this);
|
||||
const auto =
|
||||
autoplay || (config?.autostartVideo ?? false) || initialTimestamp > 0;
|
||||
if (!auto && scene.paths?.screenshot) player.poster(scene.paths.screenshot);
|
||||
else player.poster("");
|
||||
|
||||
this.onScrubberSeek = this.onScrubberSeek.bind(this);
|
||||
this.onScrubberScrolled = this.onScrubberScrolled.bind(this);
|
||||
this.state = {
|
||||
scrubberPosition: 0,
|
||||
config: this.makeJWPlayerConfig(props.scene),
|
||||
interactiveClient: new Interactive(
|
||||
this.props.config?.handyKey || "",
|
||||
this.props.config?.funscriptOffset || 0
|
||||
),
|
||||
};
|
||||
// clear the offset before loading anything new.
|
||||
// otherwise, the offset will be applied to the next file when
|
||||
// currentTime is called.
|
||||
(player as any).clearOffsetDuration();
|
||||
player.src(
|
||||
scene.sceneStreams.map((stream) => ({
|
||||
src: stream.url,
|
||||
type: stream.mime_type ?? undefined,
|
||||
label: stream.label ?? undefined,
|
||||
}))
|
||||
);
|
||||
player.currentTime(0);
|
||||
|
||||
// Default back to Direct Streaming
|
||||
localStorage.removeItem("jwplayer.qualityLabel");
|
||||
}
|
||||
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 addForwardButton() {
|
||||
// add forward button: https://github.com/jwplayer/jwplayer/issues/3894
|
||||
const playerContainer = document.querySelector(
|
||||
`#${JWUtils.playerID}`
|
||||
) as HTMLElement;
|
||||
|
||||
// display icon
|
||||
const rewindContainer = playerContainer.querySelector(
|
||||
".jw-display-icon-rewind"
|
||||
) as HTMLElement;
|
||||
const forwardContainer = rewindContainer.cloneNode(true) as HTMLElement;
|
||||
const forwardDisplayButton = forwardContainer.querySelector(
|
||||
".jw-icon-rewind"
|
||||
) as HTMLElement;
|
||||
forwardDisplayButton.innerHTML = ffSVG;
|
||||
forwardDisplayButton.ariaLabel = "Forward 10 Seconds";
|
||||
const nextContainer = playerContainer.querySelector(
|
||||
".jw-display-icon-next"
|
||||
) as HTMLElement;
|
||||
(nextContainer.parentNode as HTMLElement).insertBefore(
|
||||
forwardContainer,
|
||||
nextContainer
|
||||
player.loop(
|
||||
!!scene.file.duration &&
|
||||
maxLoopDuration !== 0 &&
|
||||
scene.file.duration < maxLoopDuration
|
||||
);
|
||||
|
||||
// control bar icon
|
||||
const buttonContainer = playerContainer.querySelector(
|
||||
".jw-button-container"
|
||||
) as HTMLElement;
|
||||
const rewindControlBarButton = buttonContainer.querySelector(
|
||||
".jw-icon-rewind"
|
||||
) as HTMLElement;
|
||||
const forwardControlBarButton = rewindControlBarButton.cloneNode(
|
||||
true
|
||||
) as HTMLElement;
|
||||
forwardControlBarButton.innerHTML = ffSVG;
|
||||
forwardControlBarButton.ariaLabel = "Forward 10 Seconds";
|
||||
(rewindControlBarButton.parentNode as HTMLElement).insertBefore(
|
||||
forwardControlBarButton,
|
||||
rewindControlBarButton.nextElementSibling
|
||||
);
|
||||
|
||||
// add onclick handlers
|
||||
[forwardDisplayButton, forwardControlBarButton].forEach((button) => {
|
||||
button.onclick = () => {
|
||||
this.player.seek(this.player.getPosition() + 10);
|
||||
};
|
||||
player.on("loadstart", function (this: VideoJsPlayer) {
|
||||
// handle offset after loading so that we get the correct current source
|
||||
handleOffset(this);
|
||||
});
|
||||
}
|
||||
|
||||
private onReady() {
|
||||
this.player = JWUtils.getPlayer();
|
||||
this.addForwardButton();
|
||||
|
||||
this.player.on("error", (err: any) => {
|
||||
if (err && err.code === 224003) {
|
||||
// When jwplayer has been requested to play but the browser doesn't support the video format.
|
||||
this.handleError(true);
|
||||
player.on("play", function (this: VideoJsPlayer) {
|
||||
if (scene.interactive) {
|
||||
interactiveClient.play(this.currentTime());
|
||||
}
|
||||
});
|
||||
|
||||
//
|
||||
this.player.on("meta", (metadata: any) => {
|
||||
if (
|
||||
metadata.metadataType === "media" &&
|
||||
!metadata.width &&
|
||||
!metadata.height
|
||||
) {
|
||||
player.on("pause", () => {
|
||||
if (scene.interactive) {
|
||||
interactiveClient.pause();
|
||||
}
|
||||
});
|
||||
|
||||
player.on("timeupdate", function (this: VideoJsPlayer) {
|
||||
if (scene.interactive) {
|
||||
interactiveClient.ensurePlaying(this.currentTime());
|
||||
}
|
||||
|
||||
setTime(this.currentTime());
|
||||
});
|
||||
|
||||
player.on("seeking", function (this: VideoJsPlayer) {
|
||||
// backwards compatibility - may want to remove this in future
|
||||
this.play();
|
||||
});
|
||||
|
||||
player.on("error", () => {
|
||||
handleError(true);
|
||||
});
|
||||
|
||||
player.on("loadedmetadata", () => {
|
||||
if (!player.videoWidth() && !player.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 = this.player.getPlaylistItem().file;
|
||||
const currentFile = player.currentSrc();
|
||||
if (currentFile != null && !currentFile.includes("m3u8")) {
|
||||
const state = this.player.getState();
|
||||
const play = state === "buffering" || state === "playing";
|
||||
this.handleError(play);
|
||||
// const play = !player.paused();
|
||||
// handleError(play);
|
||||
player.error(MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.player.on("firstFrame", () => {
|
||||
if (this.props.timestamp > 0) {
|
||||
this.player.seek(this.props.timestamp);
|
||||
}
|
||||
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,
|
||||
});
|
||||
}, [
|
||||
scene,
|
||||
config?.autostartVideo,
|
||||
maxLoopDuration,
|
||||
initialTimestamp,
|
||||
autoplay,
|
||||
interactiveClient,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
// Attach handler for onComplete event
|
||||
const player = playerRef.current;
|
||||
if (!player) return;
|
||||
|
||||
player.on("ended", () => {
|
||||
onComplete?.();
|
||||
});
|
||||
|
||||
this.player.on("play", () => {
|
||||
if (this.props.scene.interactive) {
|
||||
this.state.interactiveClient.play(this.player.getPosition());
|
||||
}
|
||||
});
|
||||
return () => player.off("ended");
|
||||
}, [onComplete]);
|
||||
|
||||
this.player.on("pause", () => {
|
||||
if (this.props.scene.interactive) {
|
||||
this.state.interactiveClient.pause();
|
||||
}
|
||||
});
|
||||
const onScrubberScrolled = () => {
|
||||
playerRef.current?.pause();
|
||||
};
|
||||
const onScrubberSeek = (seconds: number) => {
|
||||
playerRef.current?.currentTime(seconds);
|
||||
};
|
||||
|
||||
if (this.props.scene.interactive) {
|
||||
this.state.interactiveClient.uploadScript(
|
||||
this.props.scene.paths.funscript || ""
|
||||
);
|
||||
}
|
||||
|
||||
this.player.getContainer().focus();
|
||||
}
|
||||
|
||||
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 });
|
||||
if (this.props.scene.interactive) {
|
||||
this.state.interactiveClient.ensurePlaying(position);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private onComplete() {
|
||||
if (this.props?.onComplete) {
|
||||
this.props.onComplete();
|
||||
}
|
||||
}
|
||||
|
||||
private onScrubberSeek(seconds: number) {
|
||||
this.player.seek(seconds);
|
||||
}
|
||||
|
||||
private onScrubberScrolled() {
|
||||
this.player.pause();
|
||||
}
|
||||
|
||||
private handleError(play: boolean) {
|
||||
const currentFile = this.player.getPlaylistItem();
|
||||
if (currentFile) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`Source failed: ${currentFile.file}`);
|
||||
}
|
||||
|
||||
if (this.tryNextStream()) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(
|
||||
`Trying next source in playlist: ${this.playlist.sources[0].file}`
|
||||
);
|
||||
this.player.load(this.playlist);
|
||||
if (play) {
|
||||
this.player.play();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private shouldRepeat(scene: GQL.SceneDataFragment) {
|
||||
const maxLoopDuration = this.props?.config?.maximumLoopDuration ?? 0;
|
||||
return (
|
||||
!!scene.file.duration &&
|
||||
!!maxLoopDuration &&
|
||||
scene.file.duration < maxLoopDuration
|
||||
);
|
||||
}
|
||||
|
||||
private tryNextStream() {
|
||||
if (this.playlist.sources.length > 1) {
|
||||
this.playlist.sources.shift();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private makePlaylist() {
|
||||
const { scene } = this.props;
|
||||
|
||||
return {
|
||||
image: scene.paths.screenshot,
|
||||
tracks: [
|
||||
{
|
||||
file: scene.paths.vtt,
|
||||
kind: "thumbnails",
|
||||
},
|
||||
{
|
||||
file: scene.paths.chapters_vtt,
|
||||
kind: "chapters",
|
||||
},
|
||||
],
|
||||
sources: this.props.sceneStreams.map((s) => {
|
||||
return {
|
||||
file: s.url,
|
||||
type: s.mime_type,
|
||||
label: s.label,
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
private makeJWPlayerConfig(scene: GQL.SceneDataFragment) {
|
||||
if (!scene.paths.stream) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const repeat = this.shouldRepeat(scene);
|
||||
const getDurationHook = () => {
|
||||
return this.props.scene.file.duration ?? null;
|
||||
};
|
||||
|
||||
const seekHook = (seekToPosition: number, _videoTag: HTMLVideoElement) => {
|
||||
if (!_videoTag.src || _videoTag.src.endsWith(".m3u8")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ScenePlayerImpl.isDirectStream(_videoTag.src)) {
|
||||
if (_videoTag.dataset.start) {
|
||||
/* eslint-disable-next-line no-param-reassign */
|
||||
_videoTag.dataset.start = "0";
|
||||
}
|
||||
|
||||
// direct stream - fall back to default
|
||||
return false;
|
||||
}
|
||||
|
||||
// remove the start parameter
|
||||
const srcUrl = new URL(_videoTag.src);
|
||||
srcUrl.searchParams.delete("start");
|
||||
|
||||
/* eslint-disable no-param-reassign */
|
||||
_videoTag.dataset.start = seekToPosition.toString();
|
||||
srcUrl.searchParams.append("start", seekToPosition.toString());
|
||||
_videoTag.src = srcUrl.toString();
|
||||
/* eslint-enable no-param-reassign */
|
||||
|
||||
_videoTag.play();
|
||||
|
||||
// return true to indicate not to fall through to default
|
||||
return true;
|
||||
};
|
||||
|
||||
const getCurrentTimeHook = (_videoTag: HTMLVideoElement) => {
|
||||
const start = Number.parseFloat(_videoTag.dataset?.start ?? "0");
|
||||
return _videoTag.currentTime + start;
|
||||
};
|
||||
|
||||
this.playlist = this.makePlaylist();
|
||||
|
||||
// TODO: leverage the floating.mode option after upgrading JWPlayer
|
||||
const extras: any = {};
|
||||
|
||||
if (!ScreenUtils.isMobile()) {
|
||||
extras.floating = {
|
||||
dismissible: true,
|
||||
};
|
||||
}
|
||||
|
||||
const ret = {
|
||||
playlist: this.playlist,
|
||||
image: scene.paths.screenshot,
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
cast: {},
|
||||
primary: "html5",
|
||||
preload: "none",
|
||||
autostart:
|
||||
this.props.autoplay ||
|
||||
(this.props.config ? this.props.config.autostartVideo : false) ||
|
||||
this.props.timestamp > 0,
|
||||
repeat,
|
||||
playbackRateControls: true,
|
||||
playbackRates: [0.75, 1, 1.5, 2, 3, 4],
|
||||
getDurationHook,
|
||||
seekHook,
|
||||
getCurrentTimeHook,
|
||||
...extras,
|
||||
};
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
public render() {
|
||||
let className =
|
||||
this.props.className ?? "w-100 col-sm-9 m-sm-auto no-gutter";
|
||||
const sceneFile = this.props.scene.file;
|
||||
|
||||
if (
|
||||
sceneFile.height &&
|
||||
sceneFile.width &&
|
||||
sceneFile.height > sceneFile.width
|
||||
) {
|
||||
className += " portrait";
|
||||
}
|
||||
|
||||
return (
|
||||
<div id="jwplayer-container" className={className}>
|
||||
<ReactJWPlayer
|
||||
playerId={JWUtils.playerID}
|
||||
playerScript="jwplayer/jwplayer.js"
|
||||
customProps={this.state.config}
|
||||
onReady={this.onReady}
|
||||
onSeeked={this.onSeeked}
|
||||
onTime={this.onTime}
|
||||
onOneHundredPercent={() => this.onComplete()}
|
||||
className="video-wrapper"
|
||||
/>
|
||||
<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 { configuration } = React.useContext(ConfigurationContext);
|
||||
const isPortrait =
|
||||
scene &&
|
||||
scene.file.height &&
|
||||
scene.file.width &&
|
||||
scene.file.height > scene.file.width;
|
||||
|
||||
return (
|
||||
<ScenePlayerImpl
|
||||
{...props}
|
||||
config={configuration ? configuration.interface : undefined}
|
||||
/>
|
||||
<div className={cx("VideoPlayer", { portrait: isPortrait })}>
|
||||
<div data-vjs-player className={cx("video-wrapper", className)}>
|
||||
<video
|
||||
ref={videoRef}
|
||||
id={VIDEO_PLAYER_ID}
|
||||
className="video-js vjs-big-play-centered"
|
||||
/>
|
||||
</div>
|
||||
{scene && (
|
||||
<ScenePlayerScrubber
|
||||
scene={scene}
|
||||
position={time}
|
||||
onSeek={onScrubberSeek}
|
||||
onScrolled={onScrubberScrolled}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const getPlayerPosition = () =>
|
||||
VideoJS.getPlayer(VIDEO_PLAYER_ID).currentTime();
|
||||
|
||||
Reference in New Issue
Block a user