mirror of
https://github.com/stashapp/stash.git
synced 2025-12-06 08:26:00 +01:00
Adds videojs-vr support (#3636)
* Add button for VR mode * fix canvas disapearing * allow user to specify vr tag --------- Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
This commit is contained in:
parent
d0847d1ebf
commit
88179ed54e
11 changed files with 245 additions and 10 deletions
|
|
@ -70,6 +70,7 @@
|
|||
"videojs-contrib-dash": "^5.1.1",
|
||||
"videojs-mobile-ui": "^0.8.0",
|
||||
"videojs-seek-buttons": "^3.0.1",
|
||||
"videojs-vr": "^2.0.0",
|
||||
"videojs-vtt.js": "^0.15.4",
|
||||
"yup": "^1.0.0"
|
||||
},
|
||||
|
|
|
|||
5
ui/v2.5/public/vr.svg
Normal file
5
ui/v2.5/public/vr.svg
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512">
|
||||
<!--! Font Awesome Free 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2023 Fonticons, Inc.
|
||||
Modified from https://github.com/FortAwesome/Font-Awesome/blob/6.x/svgs/solid/vr-cardboard.svg
|
||||
Changed fill style
|
||||
--><path d="M576 64H64C28.7 64 0 92.7 0 128V384c0 35.3 28.7 64 64 64H184.4c24.2 0 46.4-13.7 57.2-35.4l32-64c8.8-17.5 26.7-28.6 46.3-28.6s37.5 11.1 46.3 28.6l32 64c10.8 21.7 33 35.4 57.2 35.4H576c35.3 0 64-28.7 64-64V128c0-35.3-28.7-64-64-64zM96 240a64 64 0 1 1 128 0A64 64 0 1 1 96 240zm384-64a64 64 0 1 1 0 128 64 64 0 1 1 0-128z" style="fill:#ffffff;fill-opacity:1"/></svg>
|
||||
|
After Width: | Height: | Size: 762 B |
|
|
@ -20,6 +20,7 @@ import "./markers";
|
|||
import "./vtt-thumbnails";
|
||||
import "./big-buttons";
|
||||
import "./track-activity";
|
||||
import "./vrmode";
|
||||
import cx from "classnames";
|
||||
import {
|
||||
useSceneSaveActivity,
|
||||
|
|
@ -213,6 +214,7 @@ export const ScenePlayer: React.FC<IScenePlayerProps> = ({
|
|||
|
||||
const minimumPlayPercent = uiConfig?.minimumPlayPercent ?? 0;
|
||||
const trackActivity = uiConfig?.trackActivity ?? false;
|
||||
const vrTag = uiConfig?.vrTag ?? undefined;
|
||||
|
||||
const file = useMemo(
|
||||
() => ((scene?.files.length ?? 0) > 0 ? scene?.files[0] : undefined),
|
||||
|
|
@ -265,6 +267,16 @@ export const ScenePlayer: React.FC<IScenePlayerProps> = ({
|
|||
|
||||
// Initialize VideoJS player
|
||||
useEffect(() => {
|
||||
function isVrScene() {
|
||||
if (!scene?.id || !vrTag) return false;
|
||||
|
||||
return scene?.tags.some((tag) => {
|
||||
if (vrTag == tag.name) {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const options: VideoJsPlayerOptions = {
|
||||
id: VIDEO_PLAYER_ID,
|
||||
controls: true,
|
||||
|
|
@ -318,11 +330,15 @@ export const ScenePlayer: React.FC<IScenePlayerProps> = ({
|
|||
},
|
||||
skipButtons: {},
|
||||
trackActivity: {},
|
||||
vrMenu: {
|
||||
showButton: isVrScene(),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const videoEl = document.createElement("video-js");
|
||||
videoEl.setAttribute("data-vjs-player", "true");
|
||||
videoEl.setAttribute("crossorigin", "anonymous");
|
||||
videoEl.classList.add("vjs-big-play-centered");
|
||||
videoRef.current!.appendChild(videoEl);
|
||||
|
||||
|
|
@ -348,7 +364,7 @@ export const ScenePlayer: React.FC<IScenePlayerProps> = ({
|
|||
// reset sceneId to force reload sources
|
||||
sceneId.current = undefined;
|
||||
};
|
||||
}, []);
|
||||
}, [scene, vrTag]);
|
||||
|
||||
useEffect(() => {
|
||||
const player = getPlayer();
|
||||
|
|
@ -662,6 +678,7 @@ export const ScenePlayer: React.FC<IScenePlayerProps> = ({
|
|||
}, [
|
||||
getPlayer,
|
||||
scene,
|
||||
vrTag,
|
||||
trackActivity,
|
||||
minimumPlayPercent,
|
||||
sceneIncrementPlayCount,
|
||||
|
|
|
|||
|
|
@ -189,6 +189,17 @@ $sceneTabWidth: 450px;
|
|||
}
|
||||
}
|
||||
|
||||
.vjs-vr-selector {
|
||||
.vjs-menu li {
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
.vjs-button {
|
||||
background: url("/vr.svg") center center no-repeat;
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.vjs-marker {
|
||||
background-color: rgba(33, 33, 33, 0.8);
|
||||
bottom: 0;
|
||||
|
|
|
|||
146
ui/v2.5/src/components/ScenePlayer/vrmode.ts
Normal file
146
ui/v2.5/src/components/ScenePlayer/vrmode.ts
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
import videojs, { VideoJsPlayer } from "video.js";
|
||||
import "videojs-vr";
|
||||
|
||||
export interface VRMenuOptions {
|
||||
/**
|
||||
* Whether to show the vr button.
|
||||
* @default false
|
||||
*/
|
||||
showButton?: boolean;
|
||||
}
|
||||
|
||||
enum VRType {
|
||||
Spherical = "360",
|
||||
Off = "Off",
|
||||
}
|
||||
|
||||
const vrTypeProjection = {
|
||||
[VRType.Spherical]: "360",
|
||||
[VRType.Off]: "NONE",
|
||||
};
|
||||
|
||||
function isVrDevice() {
|
||||
return navigator.userAgent.match(/oculusbrowser|\svr\s/i);
|
||||
}
|
||||
|
||||
class VRMenuItem extends videojs.getComponent("MenuItem") {
|
||||
public type: VRType;
|
||||
public isSelected = false;
|
||||
|
||||
constructor(parent: VRMenuButton, type: VRType) {
|
||||
const options = {} as videojs.MenuItemOptions;
|
||||
options.selectable = true;
|
||||
options.multiSelectable = false;
|
||||
options.label = type;
|
||||
|
||||
super(parent.player(), options);
|
||||
|
||||
this.type = type;
|
||||
|
||||
this.addClass("vjs-source-menu-item");
|
||||
}
|
||||
|
||||
selected(selected: boolean): void {
|
||||
super.selected(selected);
|
||||
this.isSelected = selected;
|
||||
}
|
||||
|
||||
handleClick() {
|
||||
if (this.isSelected) return;
|
||||
|
||||
this.trigger("selected");
|
||||
}
|
||||
}
|
||||
|
||||
class VRMenuButton extends videojs.getComponent("MenuButton") {
|
||||
private items: VRMenuItem[] = [];
|
||||
private selectedType: VRType = VRType.Off;
|
||||
|
||||
constructor(player: VideoJsPlayer) {
|
||||
super(player);
|
||||
this.setTypes();
|
||||
}
|
||||
|
||||
private onSelected(item: VRMenuItem) {
|
||||
this.selectedType = item.type;
|
||||
|
||||
this.items.forEach((i) => {
|
||||
i.selected(i.type === this.selectedType);
|
||||
});
|
||||
|
||||
this.trigger("typeselected", item.type);
|
||||
}
|
||||
|
||||
public setTypes() {
|
||||
this.items = Object.values(VRType).map((type) => {
|
||||
const item = new VRMenuItem(this, type);
|
||||
|
||||
item.on("selected", () => {
|
||||
this.onSelected(item);
|
||||
});
|
||||
|
||||
return item;
|
||||
});
|
||||
this.update();
|
||||
}
|
||||
|
||||
createEl() {
|
||||
return videojs.dom.createEl("div", {
|
||||
className:
|
||||
"vjs-vr-selector vjs-menu-button vjs-menu-button-popup vjs-control vjs-button",
|
||||
});
|
||||
}
|
||||
|
||||
createItems() {
|
||||
if (this.items === undefined) return [];
|
||||
|
||||
for (const item of this.items) {
|
||||
item.selected(item.type === this.selectedType);
|
||||
}
|
||||
|
||||
return this.items;
|
||||
}
|
||||
}
|
||||
|
||||
class VRMenuPlugin extends videojs.getPlugin("plugin") {
|
||||
private menu: VRMenuButton;
|
||||
|
||||
constructor(player: VideoJsPlayer, options: VRMenuOptions) {
|
||||
super(player);
|
||||
|
||||
this.menu = new VRMenuButton(player);
|
||||
|
||||
if (isVrDevice() || !options.showButton) return;
|
||||
|
||||
this.menu.on("typeselected", (_, type: VRType) => {
|
||||
const projection = vrTypeProjection[type];
|
||||
player.vr({ projection });
|
||||
player.load();
|
||||
});
|
||||
|
||||
player.on("ready", () => {
|
||||
const { controlBar } = player;
|
||||
const fullscreenToggle = controlBar.getChild("fullscreenToggle")!.el();
|
||||
controlBar.addChild(this.menu);
|
||||
controlBar.el().insertBefore(this.menu.el(), fullscreenToggle);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Register the plugin with video.js.
|
||||
videojs.registerComponent("VRMenuButton", VRMenuButton);
|
||||
videojs.registerPlugin("vrMenu", VRMenuPlugin);
|
||||
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
declare module "video.js" {
|
||||
interface VideoJsPlayer {
|
||||
vrMenu: () => VRMenuPlugin;
|
||||
vr: (options: Object) => void;
|
||||
}
|
||||
interface VideoJsPlayerPluginOptions {
|
||||
vrMenu?: VRMenuOptions;
|
||||
}
|
||||
}
|
||||
|
||||
export default VRMenuPlugin;
|
||||
|
|
@ -743,7 +743,6 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||
/>
|
||||
</Col>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group controlId="date" as={Row}>
|
||||
{FormUtils.renderLabel({
|
||||
title: intl.formatMessage({ id: "date" }),
|
||||
|
|
@ -756,7 +755,6 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||
/>
|
||||
</Col>
|
||||
</Form.Group>
|
||||
|
||||
{renderTextField(
|
||||
"director",
|
||||
intl.formatMessage({ id: "director" })
|
||||
|
|
@ -790,7 +788,6 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||
/>
|
||||
</Col>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group controlId="studio" as={Row}>
|
||||
{FormUtils.renderLabel({
|
||||
title: intl.formatMessage({ id: "studio" }),
|
||||
|
|
@ -811,7 +808,6 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||
/>
|
||||
</Col>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group controlId="performers" as={Row}>
|
||||
{FormUtils.renderLabel({
|
||||
title: intl.formatMessage({ id: "performers" }),
|
||||
|
|
@ -834,7 +830,6 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||
/>
|
||||
</Col>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group controlId="moviesScenes" as={Row}>
|
||||
{FormUtils.renderLabel({
|
||||
title: `${intl.formatMessage({
|
||||
|
|
@ -857,7 +852,6 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||
{renderTableMovies()}
|
||||
</Col>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group controlId="tags" as={Row}>
|
||||
{FormUtils.renderLabel({
|
||||
title: intl.formatMessage({ id: "tags" }),
|
||||
|
|
|
|||
|
|
@ -111,7 +111,9 @@ export const SceneVideoFilterPanel: React.FC<ISceneVideoFilterPanelProps> = (
|
|||
function updateVideoStyle() {
|
||||
const playerVideoContainer = document.getElementById(VIDEO_PLAYER_ID);
|
||||
const videoElements =
|
||||
playerVideoContainer?.getElementsByTagName("video") ?? [];
|
||||
playerVideoContainer?.getElementsByTagName("canvas") ??
|
||||
playerVideoContainer?.getElementsByTagName("video") ??
|
||||
[];
|
||||
const playerVideoElement =
|
||||
videoElements.length > 0 ? videoElements[0] : null;
|
||||
|
||||
|
|
|
|||
|
|
@ -290,6 +290,13 @@ export const SettingsInterfacePanel: React.FC = () => {
|
|||
checked={ui.trackActivity ?? undefined}
|
||||
onChange={(v) => saveUI({ trackActivity: v })}
|
||||
/>
|
||||
<StringSetting
|
||||
id="vr-tag"
|
||||
headingID="config.ui.scene_player.options.vr_tag.heading"
|
||||
subHeadingID="config.ui.scene_player.options.vr_tag.description"
|
||||
value={ui.vrTag ?? undefined}
|
||||
onChange={(v) => saveUI({ vrTag: v })}
|
||||
/>
|
||||
<ModalSetting<number>
|
||||
id="ignore-interval"
|
||||
headingID="config.ui.minimum_play_percent.heading"
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@ export interface IUIConfig {
|
|||
|
||||
lastNoteSeen?: number;
|
||||
|
||||
vrTag?: string;
|
||||
pinnedFilters?: PinnedFilters;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -658,7 +658,11 @@
|
|||
"heading": "Continue playlist by default"
|
||||
},
|
||||
"show_scrubber": "Show Scrubber",
|
||||
"track_activity": "Track Activity"
|
||||
"track_activity": "Track Activity",
|
||||
"vr_tag": {
|
||||
"description": "The VR button will only be displayed for scenes with this tag.",
|
||||
"heading": "VR Tag"
|
||||
}
|
||||
}
|
||||
},
|
||||
"scene_wall": {
|
||||
|
|
|
|||
|
|
@ -1081,7 +1081,7 @@
|
|||
dependencies:
|
||||
regenerator-runtime "^0.13.11"
|
||||
|
||||
"@babel/runtime@^7.8.4":
|
||||
"@babel/runtime@^7.14.5", "@babel/runtime@^7.8.4":
|
||||
version "7.21.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.21.0.tgz#5b55c9d394e5fcf304909a8b00c07dc217b56673"
|
||||
integrity sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw==
|
||||
|
|
@ -3211,6 +3211,15 @@ capital-case@^1.0.4:
|
|||
tslib "^2.0.3"
|
||||
upper-case-first "^2.0.2"
|
||||
|
||||
cardboard-vr-display@^1.0.19:
|
||||
version "1.0.19"
|
||||
resolved "https://registry.yarnpkg.com/cardboard-vr-display/-/cardboard-vr-display-1.0.19.tgz#81dcde1804b329b8228b757ac00e1fd2afa9d748"
|
||||
integrity sha512-+MjcnWKAkb95p68elqZLDPzoiF/dGncQilLGvPBM5ZorABp/ao3lCs7nnRcYBckmuNkg1V/5rdGDKoUaCVsHzQ==
|
||||
dependencies:
|
||||
gl-preserve-state "^1.0.0"
|
||||
nosleep.js "^0.7.0"
|
||||
webvr-polyfill-dpdb "^1.0.17"
|
||||
|
||||
ccount@^1.0.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/ccount/-/ccount-1.1.0.tgz#246687debb6014735131be8abab2d93898f8d043"
|
||||
|
|
@ -4455,6 +4464,11 @@ get-symbol-description@^1.0.0:
|
|||
call-bind "^1.0.2"
|
||||
get-intrinsic "^1.1.1"
|
||||
|
||||
gl-preserve-state@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/gl-preserve-state/-/gl-preserve-state-1.0.0.tgz#4ef710d62873f1470ed015c6546c37dacddd4198"
|
||||
integrity sha512-zQZ25l3haD4hvgJZ6C9+s0ebdkW9y+7U2qxvGu1uWOJh8a4RU+jURIKEQhf8elIlFpMH6CrAY2tH0mYrRjet3Q==
|
||||
|
||||
glob-parent@^5.1.2, glob-parent@~5.1.2:
|
||||
version "5.1.2"
|
||||
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
|
||||
|
|
@ -6002,6 +6016,11 @@ normalize-url@^4.5.1:
|
|||
resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-4.5.1.tgz#0dd90cf1288ee1d1313b87081c9a5932ee48518a"
|
||||
integrity sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA==
|
||||
|
||||
nosleep.js@^0.7.0:
|
||||
version "0.7.0"
|
||||
resolved "https://registry.yarnpkg.com/nosleep.js/-/nosleep.js-0.7.0.tgz#cfd919c25523ca0d0f4a69fb3305c083adaee289"
|
||||
integrity sha512-Z4B1HgvzR+en62ghwZf6BwAR6x4/pjezsiMcbF9KMLh7xoscpoYhaSXfY3lLkqC68AtW+/qLJ1lzvBIj0FGaTA==
|
||||
|
||||
nullthrows@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/nullthrows/-/nullthrows-1.1.1.tgz#7818258843856ae971eae4208ad7d7eb19a431b1"
|
||||
|
|
@ -7471,6 +7490,11 @@ thehandy@^1.0.3:
|
|||
resolved "https://registry.yarnpkg.com/thehandy/-/thehandy-1.0.3.tgz#51c5e9bae5932a6e5c563203711d78610b99d402"
|
||||
integrity sha512-zuuyWKBx/jqku9+MZkdkoK2oLM2mS8byWVR/vkQYq/ygAT6gPAXwiT94rfGuqv+1BLmsyJxm69nhVIzOZjfyIg==
|
||||
|
||||
three@0.125.2:
|
||||
version "0.125.2"
|
||||
resolved "https://registry.yarnpkg.com/three/-/three-0.125.2.tgz#dcba12749a2eb41522e15212b919cd3fbf729b12"
|
||||
integrity sha512-7rIRO23jVKWcAPFdW/HREU2NZMGWPBZ4XwEMt0Ak0jwLUKVJhcKM55eCBWyGZq/KiQbeo1IeuAoo/9l2dzhTXA==
|
||||
|
||||
throttle-debounce@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/throttle-debounce/-/throttle-debounce-5.0.0.tgz#a17a4039e82a2ed38a5e7268e4132d6960d41933"
|
||||
|
|
@ -7975,6 +7999,17 @@ videojs-seek-buttons@^3.0.1:
|
|||
dependencies:
|
||||
global "^4.4.0"
|
||||
|
||||
videojs-vr@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/videojs-vr/-/videojs-vr-2.0.0.tgz#3d86e3fececf7373cfb89b950ed6ab77ca783d2b"
|
||||
integrity sha512-ix4iN8XHaDSEe89Jqybj9DuLKYuK33EIzcSI0IEdnv1KJuH8bd0PYlQEgqIZTOmWruFpW/+rjYFCVUQ9PTypJw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.14.5"
|
||||
global "^4.4.0"
|
||||
three "0.125.2"
|
||||
video.js "^6 || ^7"
|
||||
webvr-polyfill "0.10.12"
|
||||
|
||||
videojs-vtt.js@^0.15.4:
|
||||
version "0.15.4"
|
||||
resolved "https://registry.yarnpkg.com/videojs-vtt.js/-/videojs-vtt.js-0.15.4.tgz#5dc5aabcd82ba40c5595469bd855ea8230ca152c"
|
||||
|
|
@ -8052,6 +8087,18 @@ webidl-conversions@^3.0.0:
|
|||
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"
|
||||
integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==
|
||||
|
||||
webvr-polyfill-dpdb@^1.0.17:
|
||||
version "1.0.18"
|
||||
resolved "https://registry.yarnpkg.com/webvr-polyfill-dpdb/-/webvr-polyfill-dpdb-1.0.18.tgz#258484ce06b057bf18898acc911bd173847bce11"
|
||||
integrity sha512-O0S1ZGEWyPvyZEkS2VbyV7mtir/NM9MNK3EuhbHPoJ8EHTky2pTXehjIl+IiDPr+Lldgx129QGt3NGly7rwRPw==
|
||||
|
||||
webvr-polyfill@0.10.12:
|
||||
version "0.10.12"
|
||||
resolved "https://registry.yarnpkg.com/webvr-polyfill/-/webvr-polyfill-0.10.12.tgz#47ea0b0d558f09e089bc49fa7b47a4ee7e4b8148"
|
||||
integrity sha512-trDJEVUQnRIVAnmImjEQ0BlL1NfuWl8+eaEdu+bs4g59c7OtETi/5tFkgEFDRaWEYwHntXs/uFF3OXZuutNGGA==
|
||||
dependencies:
|
||||
cardboard-vr-display "^1.0.19"
|
||||
|
||||
whatwg-fetch@^3.4.1:
|
||||
version "3.6.2"
|
||||
resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz#dced24f37f2624ed0281725d51d0e2e3fe677f8c"
|
||||
|
|
|
|||
Loading…
Reference in a new issue