diff --git a/client/index.js b/client/index.js index 1772e99b..cea0775f 100644 --- a/client/index.js +++ b/client/index.js @@ -2,7 +2,7 @@ import React from "react"; import ReactDOM from "react-dom"; import Router from "./router"; -import { Config, Log } from "./model/"; +import { Config, Log, Chromecast } from "./model/"; import { http_get, setup_cache } from "./helpers/"; import load from "little-loader"; @@ -150,17 +150,5 @@ function setup_chromecast() { } else if (location.hostname === "localhost" || location.hostname === "127.0.0.1") { return Promise.resolve(); } - return new Promise((done) => { - const script = document.createElement("script"); - script.src = "https://www.gstatic.com/cv/js/sender/v1/cast_sender.js?loadCastFramework=1"; - script.onerror = () => done() - window["__onGCastApiAvailable"] = function(isAvailable) { - if (isAvailable) cast.framework.CastContext.getInstance().setOptions({ - receiverApplicationId: chrome.cast.media.DEFAULT_MEDIA_RECEIVER_APP_ID, - autoJoinPolicy: chrome.cast.AutoJoinPolicy.ORIGIN_SCOPED, - }); - done(); - }; - document.head.appendChild(script) - }); + return Chromecast.init(); } diff --git a/client/model/chromecast.js b/client/model/chromecast.js new file mode 100644 index 00000000..0147f665 --- /dev/null +++ b/client/model/chromecast.js @@ -0,0 +1,73 @@ +"use strict"; + +import { Session } from "./session"; +import { currentShare, objectGet } from "../helpers/"; + +class ChromecastManager { + init() { + return new Promise((done) => { + const script = document.createElement("script"); + script.src = "https://www.gstatic.com/cv/js/sender/v1/cast_sender.js?loadCastFramework=1"; + script.onerror = () => done() + window["__onGCastApiAvailable"] = function(isAvailable) { + if (isAvailable) cast.framework.CastContext.getInstance().setOptions({ + receiverApplicationId: chrome.cast.media.DEFAULT_MEDIA_RECEIVER_APP_ID, + autoJoinPolicy: chrome.cast.AutoJoinPolicy.ORIGIN_SCOPED, + }); + done(); + }; + document.head.appendChild(script) + }); + } + + origin() { + return location.origin; + }; + + createLink(apiPath) { + const shareID = currentShare(); + if (shareID) { + const target = new URL(this.origin() + apiPath); + target.searchParams.append("share", shareID); + return target.toString(); + } + const target = new URL(this.origin() + apiPath) + return target.toString(); + } + + createRequest(mediaInfo) { + let prior = Promise.resolve(); + if (!Session.authorization) prior = Session.currentUser(); + return prior.then(() => { + if (!Session.authorization) throw new Error("Invalid account"); + // TODO: it would be much much nicer to set the authorization from an HTTP header + // but this would require to create a custom web receiver app, setup accounts on + // google, etc,... Until that happens, we're setting the authorization within the + // url. Once we have that app, the authorisation will come from a customData field + // of a chrome.cast.media.LoadRequest + const target = new URL(mediaInfo.contentId); + target.searchParams.append("authorization", Session.authorization); + mediaInfo.contentId = target.toString(); + return new chrome.cast.media.LoadRequest(mediaInfo); + }); + } + + context() { + if (!objectGet(window.chrome, ["cast", "isAvailable"])) { + return; + } + return cast.framework.CastContext.getInstance(); + } + session() { + const context = this.context(); + if (!context) return; + return context.getCurrentSession(); + } + media() { + const session = this.session(); + if (!session) return; + return session.getMediaSession(); + } +} + +export const Chromecast = new ChromecastManager(); diff --git a/client/model/index.js b/client/model/index.js index e298d3b5..8ffc4cdf 100644 --- a/client/model/index.js +++ b/client/model/index.js @@ -6,3 +6,4 @@ export { Log } from "./log"; export { Admin } from "./admin"; export { Audit } from "./audit"; export { Tags } from "./tags"; +export { Chromecast } from "./chromecast"; diff --git a/client/model/session.js b/client/model/session.js index acf8c8be..97d44b7b 100644 --- a/client/model/session.js +++ b/client/model/session.js @@ -4,7 +4,10 @@ class SessionManager { currentUser() { const shareID = currentShare(); return http_get("/api/session" + (shareID && `?share=${shareID}`)) - .then((data) => data.result) + .then((data) => { + this.authorization = data.result.authorization; + return data.result; + }) .catch((err) => { if (err.code === "Unauthorized") { if (location.pathname.indexOf("/files/") !== -1 || location.pathname.indexOf("/view/") !== -1) { @@ -41,6 +44,7 @@ class SessionManager { logout() { const url = "/api/session"; + this.authorization = null; return http_delete(url) .then((data) => data.result); } diff --git a/client/pages/viewerpage.js b/client/pages/viewerpage.js index 8c4462d1..5e1949d3 100644 --- a/client/pages/viewerpage.js +++ b/client/pages/viewerpage.js @@ -4,7 +4,7 @@ import Path from "path"; import "./viewerpage.scss"; import "./error.scss"; -import { Files } from "../model/"; +import { Files, Chromecast } from "../model/"; import { BreadCrumb, Bundle, NgIf, Loader, EventReceiver, LoggedInOnly, ErrorPage, } from "../components/"; @@ -137,12 +137,11 @@ export function ViewerPageComponent({ error, subscribe, unsubscribe, match, loca useEffect(() => { return () => { - if (!objectGet(window.chrome, ["cast", "isAvailable"])) { - return - } - cast.framework.CastContext.getInstance().endCurrentSession(); + const context = Chromecast.context(); + if (!context) return; + context.endCurrentSession(); }; - }, []) + }, []); return (
diff --git a/client/pages/viewerpage/audioplayer.js b/client/pages/viewerpage/audioplayer.js index d1507824..afd8a8bd 100644 --- a/client/pages/viewerpage/audioplayer.js +++ b/client/pages/viewerpage/audioplayer.js @@ -1,9 +1,12 @@ import React, { useState, useEffect, useRef } from "react"; import WaveSurfer from "wavesurfer.js"; +import filepath from "path"; import { MenuBar } from "./menubar"; import { NgIf, Icon } from "../../components/"; -import { settings_get, settings_put } from "../../helpers/"; +import { settings_get, settings_put, notify, getMimeType, basename } from "../../helpers/"; +import { Chromecast } from "../../model/"; +import { t } from "../../locales/"; import "./audioplayer.scss"; export function AudioPlayer({ filename, data }) { @@ -11,6 +14,7 @@ export function AudioPlayer({ filename, data }) { const [isLoading, setIsLoading] = useState(true); const [purcentLoading, setPurcentLoading] = useState(0); const [volume, setVolume] = useState(settings_get("volume") === null ? 50 : settings_get("volume")); + const [isChromecast, setIsChromecast] = useState(false); const [error, setError] = useState(null); const wavesurfer = useRef(null); @@ -29,13 +33,14 @@ export function AudioPlayer({ filename, data }) { let $currentTime = document.getElementById("currentTime"); let $totalDuration = document.getElementById("totalDuration"); wavesurfer.current.on("ready", () => { + setPurcentLoading(100); setIsLoading(false); wavesurfer.current.setVolume(volume / 100); $totalDuration.innerHTML = formatTimecode(wavesurfer.current.getDuration()); }); wavesurfer.current.on("audioprocess", () => { $currentTime.innerHTML = formatTimecode(wavesurfer.current.getCurrentTime()); - }) + }); wavesurfer.current.on("loading", (n) => { setPurcentLoading(n); }); @@ -43,15 +48,113 @@ export function AudioPlayer({ filename, data }) { setIsLoading(false); setError(err); }); + wavesurfer.current.on("seek", (s) => { + const media = Chromecast.media(); + if (!media) return; + const seekRequest = new chrome.cast.media.SeekRequest(); + seekRequest.currentTime = parseInt(s*wavesurfer.current.getDuration()); + media.seek(seekRequest); + }); + return () => wavesurfer.current.destroy(); }, []); useEffect(() => { - if(wavesurfer.current === null) return; window.addEventListener("keypress", onKeyPressHandler); return () => window.removeEventListener("keypress", onKeyPressHandler); }, [isPlaying]) + const chromecastSetup = (event) => { + switch (event.sessionState) { + case cast.framework.SessionState.SESSION_STARTING: + setIsChromecast(true); + setIsLoading(true); + break; + case cast.framework.SessionState.SESSION_START_FAILED: + notify.send(t("Cannot establish a connection"), "error"); + setIsChromecast(false); + setIsLoading(false); + break; + case cast.framework.SessionState.SESSION_STARTED: + chromecastHandler() + break; + case cast.framework.SessionState.SESSION_ENDING: + wavesurfer.current.setMute(false); + setIsChromecast(false); + break; + } + }; + + const chromecastHandler = () => { + setIsLoading(true); + const link = Chromecast.createLink(data); + const media = new chrome.cast.media.MediaInfo( + link, + getMimeType(data), + ); + media.metadata = new chrome.cast.media.MusicTrackMediaMetadata() + media.metadata.title = filename.substr(0, filename.lastIndexOf(filepath.extname(filename))); + media.metadata.subtitle = CONFIG.name; + media.metadata.albumName = CONFIG.name; + media.metadata.images = [ + new chrome.cast.Image(origin + "/assets/icons/music.png"), + ]; + const session = Chromecast.session(); + if (!session) return; + + Chromecast.createRequest(media) + .then((req) => { + req.currentTime = parseInt(wavesurfer.current.getCurrentTime()); + return session.loadMedia(req) + }) + .then(() => { + setIsPlaying(true); + setIsLoading(false); + wavesurfer.current.play(); + wavesurfer.current.setMute(true); + + const media = Chromecast.media(); + if (!media) return; + wavesurfer.current.seekTo(media.getEstimatedTime() / wavesurfer.current.getDuration()); + media.addUpdateListener(chromecastAlive); + }).catch((err) => { + console.error(err); + notify.send(t("Cannot establish a connection"), "error"); + setIsChromecast(false); + setIsLoading(false); + }); + } + + const chromecastAlive = (isAlive) => { + if (isAlive) return; + const session = Chromecast.session(); + if (session) { + session.endSession(); + wavesurfer.current.setMute(false); + } + }; + + useEffect(() => { + const context = Chromecast.context(); + if (!context) return; + chromecastAlive(false); + document.getElementById("chromecast-target").append(document.createElement("google-cast-launcher")); + context.addEventListener( + cast.framework.CastContextEventType.SESSION_STATE_CHANGED, + chromecastSetup, + ); + return () => { + context.removeEventListener( + cast.framework.CastContextEventType.SESSION_STATE_CHANGED, + chromecastSetup, + ); + const media = Chromecast.media(); + if (!media) return + media.removeUpdateListener(chromecastAlive); + chromecastAlive(false); + }; + }, []); + const onKeyPressHandler = (e) => { if(e.code !== "Space") { return @@ -62,22 +165,33 @@ export function AudioPlayer({ filename, data }) { const onPlay = (e) => { e.preventDefault(); e.stopPropagation(); - wavesurfer.current.play(); setIsPlaying(true); + if (wavesurfer.current) wavesurfer.current.play(); + if (isChromecast) { + const media = Chromecast.media(); + if (media) media.play(); + } }; const onPause = (e) => { e.preventDefault(); e.stopPropagation(); - wavesurfer.current.pause(); setIsPlaying(false); + if (wavesurfer.current) wavesurfer.current.pause(); + if (isChromecast) { + const media = Chromecast.media(); + if (media) media.pause(); + } }; const onVolumeChange = (e) => { const v = Number(e.target.value); settings_put("volume", v); setVolume(v); - wavesurfer.current.setVolume(v / 100); + if (isChromecast) { + const session = Chromecast.session() + if (session) session.setVolume(v / 100); + } else wavesurfer.current.setVolume(v / 100); }; const onVolumeClick = () => { onVolumeChange({ target: { value: 0 }}); @@ -95,13 +209,23 @@ export function AudioPlayer({ filename, data }) {
-
- - {purcentLoading}% + { + isChromecast ? ( +
+ +
+ ) : ( + +
+ {purcentLoading}% + +
+ ) + }
-
+
{ isPlaying ? ( @@ -119,7 +243,7 @@ export function AudioPlayer({ filename, data }) {
00:00:00 - / + / 00:00:00
diff --git a/client/pages/viewerpage/audioplayer.scss b/client/pages/viewerpage/audioplayer.scss index ee960d6c..9dd34531 100644 --- a/client/pages/viewerpage/audioplayer.scss +++ b/client/pages/viewerpage/audioplayer.scss @@ -90,16 +90,23 @@ top: 0; left: 0; } + .component_icon[alt="loading"] { + position: absolute; + margin: 50px 0px + } + .chromecast_loader .component_icon[alt="loading"] { + margin: 0; + right: 20px; + top: 20px; + width: 30px; + } + .percent { position: absolute; margin: 100px 0px; width: 120px; font-size: 1.4rem; } - .component_icon[alt="loading"] { - position: absolute; - margin: 50px 0px - } } } } diff --git a/client/pages/viewerpage/imageviewer.js b/client/pages/viewerpage/imageviewer.js index 00090333..d61847f5 100644 --- a/client/pages/viewerpage/imageviewer.js +++ b/client/pages/viewerpage/imageviewer.js @@ -4,8 +4,8 @@ import ReactCSSTransitionGroup from "react-addons-css-transition-group"; import { MenuBar } from "./menubar"; import { Bundle, Icon, NgIf, Loader, EventEmitter, EventReceiver } from "../../components/"; -import { alert, randomString, objectGet, notify, getMimeType, currentShare } from "../../helpers/"; -import { Session } from "../../model/"; +import { alert, randomString, notify, getMimeType, currentShare } from "../../helpers/"; +import { Chromecast } from "../../model/"; import { Pager } from "./pager"; import { t } from "../../locales/"; import "./imageviewer.scss"; @@ -67,45 +67,30 @@ export function ImageViewerComponent({ filename, data, path, subscribe, unsubscr }; const chromecastHandler = (event) => { - const cSession = cast.framework.CastContext.getInstance().getCurrentSession() - if (!cSession) return; + const session = Chromecast.session(); + if (!session) return; - const createLink = () => { - const shareID = currentShare(); - const origin = location.origin; - if (shareID) { - const target = new URL(origin + data); - target.searchParams.append("share", shareID); - return Promise.resolve(target.toString()); - } - return Session.currentUser().then(({ authorization }) => { - const target = new URL(origin + data); - target.searchParams.append("authorization", authorization); - return target.toString() + const link = Chromecast.createLink(data); + const media = new chrome.cast.media.MediaInfo( + link, + getMimeType(filename), + ); + media.metadata = new chrome.cast.media.PhotoMediaMetadata(); + media.metadata.title = filename; + media.metadata.images = [ + new chrome.cast.Image(Chromecast.origin() + "/assets/icons/photo.png"), + ]; + Chromecast.createRequest(media) + .then((req) => session.loadMedia(req)) + .catch((err) => { + console.error(err) + notify.send(t("Cannot establish a connection"), "error"); }); - }; - - return createLink().then((link) => { - const media = new chrome.cast.media.MediaInfo( - link, - getMimeType(filename), - ); - media.metadata = new chrome.cast.media.PhotoMediaMetadata(); - media.metadata.title = filename; - media.metadata.images = [ - new chrome.cast.Image(origin + "/assets/icons/photo.png"), - ]; - return cSession.loadMedia(new chrome.cast.media.LoadRequest(media)); - }).catch((err) => { - notify.send(err && err.message, "error"); - }); }; useEffect(() => { - if (!objectGet(window.chrome, ["cast", "isAvailable"])) { - return; - } - const context = cast.framework.CastContext.getInstance(); + const context = Chromecast.context(); + if (!context) return; document.getElementById("chromecast-target").append(document.createElement("google-cast-launcher")); context.addEventListener( cast.framework.CastContextEventType.SESSION_STATE_CHANGED, @@ -157,8 +142,9 @@ export function ImageViewerComponent({ filename, data, path, subscribe, unsubscr
diff --git a/client/pages/viewerpage/imageviewer.scss b/client/pages/viewerpage/imageviewer.scss index e05c6403..ce743e31 100644 --- a/client/pages/viewerpage/imageviewer.scss +++ b/client/pages/viewerpage/imageviewer.scss @@ -26,8 +26,10 @@ background: #525659; overflow: hidden; padding: 15px 10px 65px 10px; + &.component_image_no_pager { padding-bottom: 15px; } @media screen and (max-height: 410px) { padding: 5px 0px 40px 10px; + &.component_image_no_pager { padding-bottom: 5px; } .component_pager .wrapper{ > span{padding: 2px 5px;} padding: 5px 0; diff --git a/client/pages/viewerpage/videoplayer.scss b/client/pages/viewerpage/videoplayer.scss index 900d7983..53859754 100644 --- a/client/pages/viewerpage/videoplayer.scss +++ b/client/pages/viewerpage/videoplayer.scss @@ -34,7 +34,8 @@ max-height: 500px; .vjs-control-bar{ - background: rgba(255,255,255,0.2); + background: black; + background: linear-gradient(transparent 20%, #00000099); } .vjs-load-progress div{ background: var(--primary);