mirror of
				https://github.com/mickael-kerjean/filestash.git
				synced 2025-10-31 18:16:00 +08:00 
			
		
		
		
	feature (chromecast): improve support for chromecast
This commit is contained in:
		| @ -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(); | ||||
| } | ||||
|  | ||||
							
								
								
									
										73
									
								
								client/model/chromecast.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								client/model/chromecast.js
									
									
									
									
									
										Normal file
									
								
							| @ -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(); | ||||
| @ -6,3 +6,4 @@ export { Log } from "./log"; | ||||
| export { Admin } from "./admin"; | ||||
| export { Audit } from "./audit"; | ||||
| export { Tags } from "./tags"; | ||||
| export { Chromecast } from "./chromecast"; | ||||
|  | ||||
| @ -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); | ||||
|     } | ||||
|  | ||||
| @ -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 ( | ||||
|         <div className="component_page_viewerpage"> | ||||
|  | ||||
| @ -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 }) { | ||||
|                 <NgIf cond={error === null}> | ||||
|                     <div className="audioplayer_box"> | ||||
|                         <NgIf cond={isLoading}> | ||||
|                             { | ||||
|                                 isChromecast ? ( | ||||
|                                     <div className="chromecast_loader"> | ||||
|                                         <Icon name="loading" /> | ||||
|                                     </div> | ||||
|                                 ) : ( | ||||
|                                     <React.Fragment> | ||||
|                                         <div className="audioplayer_loader" style={{width: purcentLoading + "%"}}></div> | ||||
|                             <Icon name="loading"/> | ||||
|                                         <span className="percent">{purcentLoading}%</span> | ||||
|                                         <Icon name="loading" /> | ||||
|                                     </React.Fragment> | ||||
|                                 ) | ||||
|                             } | ||||
|                         </NgIf> | ||||
|                         <div id="waveform"></div> | ||||
|                         <div className="audioplayer_control" style={{ opacity: isLoading? 0 : 1 }}> | ||||
|                             <div className="buttons"> | ||||
|                             <div className="buttons no-select"> | ||||
|                                 { | ||||
|                                     isPlaying ? ( | ||||
|                                         <span onClick={onPause}> | ||||
| @ -119,7 +243,7 @@ export function AudioPlayer({ filename, data }) { | ||||
|  | ||||
|                             <div className="timecode"> | ||||
|                                 <span id="currentTime">00:00:00</span> | ||||
|                                 <span id="separator">/</span> | ||||
|                                 <span id="separator" className="no-select">/</span> | ||||
|                                 <span id="totalDuration">00:00:00</span> | ||||
|                             </div> | ||||
|                         </div> | ||||
|  | ||||
| @ -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 | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -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,25 +67,10 @@ 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() | ||||
|             }); | ||||
|         }; | ||||
|  | ||||
|         return createLink().then((link) => { | ||||
|         const link = Chromecast.createLink(data); | ||||
|         const media = new chrome.cast.media.MediaInfo( | ||||
|             link, | ||||
|             getMimeType(filename), | ||||
| @ -93,19 +78,19 @@ export function ImageViewerComponent({ filename, data, path, subscribe, unsubscr | ||||
|         media.metadata = new chrome.cast.media.PhotoMediaMetadata(); | ||||
|         media.metadata.title = filename; | ||||
|         media.metadata.images = [ | ||||
|                 new chrome.cast.Image(origin + "/assets/icons/photo.png"), | ||||
|             new chrome.cast.Image(Chromecast.origin() + "/assets/icons/photo.png"), | ||||
|         ]; | ||||
|             return cSession.loadMedia(new chrome.cast.media.LoadRequest(media)); | ||||
|         }).catch((err) => { | ||||
|             notify.send(err && err.message, "error"); | ||||
|         Chromecast.createRequest(media) | ||||
|             .then((req) => session.loadMedia(req)) | ||||
|             .catch((err) => { | ||||
|                 console.error(err) | ||||
|                 notify.send(t("Cannot establish a connection"), "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 | ||||
|             <div | ||||
|                 ref={$container} | ||||
|                 className={ | ||||
|                     "component_image_container " + | ||||
|                         (document.webkitIsFullScreen || document.mozFullScreen ? "fullscreen" : "") | ||||
|                     "component_image_container" + | ||||
|                         (state.draggable ? "" : " component_image_no_pager") + | ||||
|                         (document.webkitIsFullScreen || document.mozFullScreen ? " fullscreen" : "") | ||||
|                 } | ||||
|             > | ||||
|                 <div className="images_wrapper"> | ||||
|  | ||||
| @ -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; | ||||
|  | ||||
| @ -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); | ||||
|  | ||||
		Reference in New Issue
	
	Block a user
	 Mickael Kerjean
					Mickael Kerjean