From 8e235a26eedccd7a97a9aa6a3478620a95a57979 Mon Sep 17 00:00:00 2001 From: CJ <72030708+Teda1@users.noreply.github.com> Date: Mon, 10 Jul 2023 22:47:11 -0500 Subject: [PATCH] Add AirPlay and Chromecast support (#2872) * dynamically load cast_sender.js * add https://www.gstatic.com to connectableOrigins * Add toggle for chromecast --- docker/build/x86_64/Dockerfile | 2 +- internal/api/server.go | 2 +- ui/v2.5/package.json | 2 ++ .../components/ScenePlayer/ScenePlayer.tsx | 18 ++++++++++++++- .../src/components/ScenePlayer/styles.scss | 12 ++++++++++ .../SettingsInterfacePanel.tsx | 6 +++++ ui/v2.5/src/core/config.ts | 2 ++ ui/v2.5/src/hooks/useScript.tsx | 22 +++++++++++++++++++ ui/v2.5/src/locales/en-GB.json | 1 + ui/v2.5/yarn.lock | 16 ++++++++++++++ 10 files changed, 80 insertions(+), 3 deletions(-) create mode 100644 ui/v2.5/src/hooks/useScript.tsx diff --git a/docker/build/x86_64/Dockerfile b/docker/build/x86_64/Dockerfile index 5133eba36..f816d6099 100644 --- a/docker/build/x86_64/Dockerfile +++ b/docker/build/x86_64/Dockerfile @@ -2,7 +2,7 @@ # Build Frontend FROM node:alpine as frontend -RUN apk add --no-cache make +RUN apk add --no-cache make git ## cache node_modules separately COPY ./ui/v2.5/package.json ./ui/v2.5/yarn.lock /stash/ui/v2.5/ WORKDIR /stash diff --git a/internal/api/server.go b/internal/api/server.go index cfc57b3dd..6450a27b4 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -490,7 +490,7 @@ func setPageSecurityHeaders(w http.ResponseWriter, r *http.Request) { // The graphql playground pulls its frontend from a cdn if r.URL.Path == playgroundEndpoint { connectSrc += " https://cdn.jsdelivr.net" - scriptSrc += " https://cdn.jsdelivr.net" + scriptSrc += " https://cdn.jsdelivr.net http://www.gstatic.com https://www.gstatic.com" styleSrc += " https://cdn.jsdelivr.net" } diff --git a/ui/v2.5/package.json b/ui/v2.5/package.json index dd59c944c..b62eb960c 100644 --- a/ui/v2.5/package.json +++ b/ui/v2.5/package.json @@ -29,6 +29,8 @@ "@fortawesome/free-regular-svg-icons": "^6.3.0", "@fortawesome/free-solid-svg-icons": "^6.3.0", "@fortawesome/react-fontawesome": "^0.2.0", + "@silvermine/videojs-airplay": "^1.2.0", + "@silvermine/videojs-chromecast": "^1.4.1", "apollo-upload-client": "^17.0.0", "axios": "^1.3.3", "base64-blob": "^1.4.1", diff --git a/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx b/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx index 06bfb0d4e..631ae55bf 100644 --- a/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx +++ b/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx @@ -8,6 +8,7 @@ import React, { useState, } from "react"; import videojs, { VideoJsPlayer, VideoJsPlayerOptions } from "video.js"; +import useScript from "src/hooks/useScript"; import "videojs-contrib-dash"; import "videojs-mobile-ui"; import "videojs-seek-buttons"; @@ -22,6 +23,12 @@ import "./big-buttons"; import "./track-activity"; import "./vrmode"; import cx from "classnames"; +// @ts-ignore +import airplay from "@silvermine/videojs-airplay"; +// @ts-ignore +import chromecast from "@silvermine/videojs-chromecast"; +import "@silvermine/videojs-chromecast/dist/silvermine-videojs-chromecast.css"; +import "@silvermine/videojs-airplay/dist/silvermine-videojs-airplay.css"; import { useSceneSaveActivity, useSceneIncrementPlayCount, @@ -211,11 +218,15 @@ export const ScenePlayer: React.FC = ({ const started = useRef(false); const auto = useRef(false); const interactiveReady = useRef(false); - const minimumPlayPercent = uiConfig?.minimumPlayPercent ?? 0; const trackActivity = uiConfig?.trackActivity ?? false; const vrTag = uiConfig?.vrTag ?? undefined; + useScript( + "https://www.gstatic.com/cv/js/sender/v1/cast_sender.js?loadCastFramework=1", + uiConfig?.enableChromecast + ); + const file = useMemo( () => (scene.files.length > 0 ? scene.files[0] : undefined), [scene] @@ -267,6 +278,8 @@ export const ScenePlayer: React.FC = ({ // Initialize VideoJS player useEffect(() => { + airplay(videojs); + chromecast(videojs); const options: VideoJsPlayerOptions = { id: VIDEO_PLAYER_ID, controls: true, @@ -300,12 +313,15 @@ export const ScenePlayer: React.FC = ({ inactivityTimeout: 2000, preload: "none", playsinline: true, + techOrder: ["chromecast", "html5"], userActions: { hotkeys: function (this: VideoJsPlayer, event) { handleHotkeys(this, event); }, }, plugins: { + airPlay: {}, + chromecast: {}, vttThumbnails: { showTimestamp: true, }, diff --git a/ui/v2.5/src/components/ScenePlayer/styles.scss b/ui/v2.5/src/components/ScenePlayer/styles.scss index 7fcd6c27b..71a534f0e 100644 --- a/ui/v2.5/src/components/ScenePlayer/styles.scss +++ b/ui/v2.5/src/components/ScenePlayer/styles.scss @@ -62,6 +62,12 @@ $sceneTabWidth: 450px; } } + .vjs-airplay-button .vjs-icon-placeholder, + .vjs-chromecast-button .vjs-icon-placeholder { + height: 1.6em; + width: 1.6em; + } + .vjs-touch-overlay .vjs-play-control { z-index: 1; } @@ -308,6 +314,12 @@ $sceneTabWidth: 450px; font-size: 1.5em; line-height: 2; } + + .vjs-airplay-button .vjs-icon-placeholder, + .vjs-chromecast-button .vjs-icon-placeholder { + height: 1.4em; + width: 1.4em; + } } .vjs-menu-button-popup .vjs-menu { diff --git a/ui/v2.5/src/components/Settings/SettingsInterfacePanel/SettingsInterfacePanel.tsx b/ui/v2.5/src/components/Settings/SettingsInterfacePanel/SettingsInterfacePanel.tsx index c44f3ab78..82343cda4 100644 --- a/ui/v2.5/src/components/Settings/SettingsInterfacePanel/SettingsInterfacePanel.tsx +++ b/ui/v2.5/src/components/Settings/SettingsInterfacePanel/SettingsInterfacePanel.tsx @@ -272,6 +272,12 @@ export const SettingsInterfacePanel: React.FC = () => { + saveUI({ enableChromecast: v })} + /> { + useEffect(() => { + const script = document.createElement("script"); + + script.src = url; + script.async = true; + + if (condition) { + document.head.appendChild(script); + } + + return () => { + if (condition) { + document.head.removeChild(script); + } + }; + }, [url, condition]); +}; + +export default useScript; diff --git a/ui/v2.5/src/locales/en-GB.json b/ui/v2.5/src/locales/en-GB.json index 4e0f3f144..e7df3470d 100644 --- a/ui/v2.5/src/locales/en-GB.json +++ b/ui/v2.5/src/locales/en-GB.json @@ -658,6 +658,7 @@ "description": "Play next scene in queue when video finishes", "heading": "Continue playlist by default" }, + "enable_chromecast": "Enable Chromecast", "show_scrubber": "Show Scrubber", "track_activity": "Track Activity", "vr_tag": { diff --git a/ui/v2.5/yarn.lock b/ui/v2.5/yarn.lock index 6fe09bc33..5cd275c50 100644 --- a/ui/v2.5/yarn.lock +++ b/ui/v2.5/yarn.lock @@ -2161,6 +2161,18 @@ dependencies: dequal "^2.0.2" +"@silvermine/videojs-airplay@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@silvermine/videojs-airplay/-/videojs-airplay-1.2.0.tgz#3ca6464c8e94c97bb9f35c2926f59a5aa662cebd" + integrity sha512-4KzHoM/wJaq3au8IBLxnz8+btAB/M/2AdMoAoOdZRpp9DGG8SGU/UPw2w+CRMknvfbGtvlhlNrNiiXVWA6Cn0A== + +"@silvermine/videojs-chromecast@^1.4.1": + version "1.4.1" + resolved "https://registry.yarnpkg.com/@silvermine/videojs-chromecast/-/videojs-chromecast-1.4.1.tgz#a5763eb85dfd4bc8f47a1b8b150d5dadbb8ea658" + integrity sha512-tXeikkWoNC3WIl2WSkIag1CLMbGsgn+26LM4LoB4qx0TQ8mkg7pgpldCTkvXxLVaDluQ/uEm2uxrgrMmjOc6sw== + dependencies: + webcomponents.js "git+https://git@github.com/webcomponents/webcomponentsjs.git#v0.7.24" + "@tootallnate/once@2": version "2.0.0" resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf" @@ -8143,6 +8155,10 @@ web-streams-polyfill@^3.2.1: resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz#71c2718c52b45fd49dbeee88634b3a60ceab42a6" integrity sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q== +"webcomponents.js@git+https://git@github.com/webcomponents/webcomponentsjs.git#v0.7.24": + version "0.7.24" + resolved "git+https://git@github.com/webcomponents/webcomponentsjs.git#8a2e40557b177e2cca0def2553f84c8269c8f93e" + webcrypto-core@^1.7.4: version "1.7.6" resolved "https://registry.yarnpkg.com/webcrypto-core/-/webcrypto-core-1.7.6.tgz#e32c4a12a13de4251f8f9ef336a6cba7cdec9b55"