import React, { useEffect, useState, useRef, useMemo } from "react"; import ReactCSSTransitionGroup from "react-addons-css-transition-group"; import filepath from "path"; import { Pager } from "./pager"; import { MenuBar } from "./menubar"; import { Chromecast } from "../../model/" import { getMimeType,settings_get, settings_put, notify, formatTimecode } from "../../helpers/"; import { t } from "../../locales/"; import { Icon } from "../../components/"; import hls from "hls.js"; import "./videoplayer.scss"; export function VideoPlayer({ filename, data, path }) { const $video = useRef(); const $container = useRef(); const [isPlaying, setIsPlaying] = useState(false); const [isLoading, setIsLoading] = useState(true); const [isBuffering, setIsBuffering] = useState(false); const [volume, setVolume] = useState(settings_get("volume") === null ? 50 : settings_get("volume")); const [currentTime, setCurrentTime] = useState(0); const [duration, setDuration] = useState(0); const [isChromecast, setIsChromecast] = useState(false); const [render, setRender] = useState(0); const [hint, setHint] = useState(null); const [videoSources, setVideoSources] = useState([]); useEffect(() => { if (!$video.current) return; const metadataHandler = () => { $video.current.volume = volume / 100; setDuration($video.current.duration); setIsLoading(false); }; const finishHandler = () => { setIsPlaying(false); }; const errorHandler = (err) => { console.error(err); notify.send(t("Not supported"), "error"); setIsPlaying(false); setIsLoading(false); }; const waitingHandler = (e) => { setIsBuffering(true); } const playingHandler = (e) => { setIsBuffering(false); } if (!window.overrides["video-map-sources"]) { window.overrides["video-map-sources"] = (s) => (s); } const sources = window.overrides["video-map-sources"]([{ src: data, type: getMimeType(data), }]); setVideoSources(sources.map((source) => { if (source.type !== "application/x-mpegURL" && source.type !== "application/vnd.apple.mpegurl") return source; const h = new hls({ enableWorker: false, // until https://github.com/video-dev/hls.js/issues/5107 is fixed }); h.loadSource(source.src); h.attachMedia($video.current); return source; })); $video.current.addEventListener("loadeddata", metadataHandler); $video.current.addEventListener("ended", finishHandler); $video.current.addEventListener("error", errorHandler); $video.current.addEventListener("waiting", waitingHandler); $video.current.addEventListener("playing", playingHandler); let $sources = $video.current.querySelectorAll("source") for (let i=0; i<$sources.length; i++) { $sources[i].addEventListener("error", errorHandler); } return () => { $video.current.removeEventListener("loadeddata", metadataHandler); $video.current.removeEventListener("ended", finishHandler); $video.current.removeEventListener("error", errorHandler); $video.current.removeEventListener("waiting", waitingHandler); $video.current.removeEventListener("playing", playingHandler); for (let i=0; i<$sources.length; i++) { $sources[i].removeEventListener("error", errorHandler); } }; }, [$video, data]); useEffect(() => { const resizeHandler = () => setRender(render + 1); const onKeyPressHandler = (e) => { switch(e.code) { case "Space": case "KeyK": return isPlaying ? onPause(e) : onPlay(e); case "KeyM": return onVolume(0); case "ArrowUp": return onVolume(Math.min(volume + 10, 100)); case "ArrowDown": return onVolume(Math.max(volume - 10, 0)); case "KeyL": return onSeek(_currentTime + 10); case "KeyJ": return onSeek(_currentTime - 10); case "KeyF": return onRequestFullscreen(); case "Digit0": return onSeek(0); case "Digit1": return onSeek(duration / 10); case "Digit2": return onSeek(2 * duration / 10); case "Digit3": return onSeek(3 * duration / 10); case "Digit4": return onSeek(4 * duration / 10); case "Digit5": return onSeek(5 * duration / 10); case "Digit6": return onSeek(6 * duration / 10); case "Digit7": return onSeek(7 * duration / 10); case "Digit8": return onSeek(8 * duration / 10); case "Digit9": return onSeek(9 * duration / 10); } }; window.addEventListener("resize", resizeHandler); window.addEventListener("keydown", onKeyPressHandler); return () => { window.removeEventListener("resize", resizeHandler); window.removeEventListener("keydown", onKeyPressHandler); }; }, [render, isPlaying, isChromecast, volume]); useEffect(() => { const context = Chromecast.context(); if (!context) return; document.getElementById("chromecast-target").append(document.createElement("google-cast-launcher")); const chromecastSetup = (event) => { switch (event.sessionState) { case cast.framework.SessionState.SESSION_STARTING: setIsChromecast(true); setIsLoading(true); break; case cast.framework.SessionState.SESSION_START_FAILED: setIsChromecast(false); setIsLoading(false); break; case cast.framework.SessionState.SESSION_STARTED: chromecastLoader(); break; case cast.framework.SessionState.SESSION_ENDING: $video.current.currentTime = _currentTime; $video.current.muted = false; setIsChromecast(false); setVolume($video.current.volume * 100); const media = Chromecast.media(); if (media && media.playerState === "PLAYING") $video.current.play(); else if (media && media.playerState === "PAUSED") $video.current.pause(); break; case cast.framework.SessionState.SESSION_ENDED: setIsChromecast(false); setVolume($video.current.volume * 100); $video.current.currentTime = _currentTime; $video.current.muted = false; break; } }; context.addEventListener( cast.framework.CastContextEventType.SESSION_STATE_CHANGED, chromecastSetup, ); return () => { context.removeEventListener( cast.framework.CastContextEventType.SESSION_STATE_CHANGED, chromecastSetup, ); }; }, []); useEffect(() => { if (isLoading === true) return; else if (isChromecast === false) { const onPlayerTimeChangeHandler = (event) => { _currentTime = $video.current.currentTime; setCurrentTime(_currentTime); }; $video.current.addEventListener("timeupdate", onPlayerTimeChangeHandler); return () => $video.current.removeEventListener("timeupdate", onPlayerTimeChangeHandler); } const media = Chromecast.media(); if (!media) return; const remotePlayer = new cast.framework.RemotePlayer(); const remotePlayerController = new cast.framework.RemotePlayerController(remotePlayer); const onPlayerStateChangeHandler = (event) => { switch(event.value) { case "BUFFERING": setIsBuffering(true); break case "PLAYING": setIsBuffering(false); break; } }; const onPlayerCurrentTimeChangeHandler = (event) => { _currentTime = event.value; setCurrentTime(event.value); }; const onMediaChange = (isAlive) => { if (media.playerState !== chrome.cast.media.PlayerState.IDLE) return; switch(media.idleReason) { case chrome.cast.media.IdleReason.FINISHED: setIsPlaying(false); setIsChromecast(false); setVolume($video.current.volume * 100); $video.current.currentTime = _currentTime; $video.current.muted = false; break; } }; media.addUpdateListener(onMediaChange); remotePlayerController.addEventListener( cast.framework.RemotePlayerEventType.PLAYER_STATE_CHANGED, onPlayerStateChangeHandler, ); remotePlayerController.addEventListener( cast.framework.RemotePlayerEventType.CURRENT_TIME_CHANGED, onPlayerCurrentTimeChangeHandler, ); return () => { media.removeUpdateListener(onMediaChange); remotePlayerController.removeEventListener( cast.framework.RemotePlayerEventType.PLAYER_STATE_CHANGED, onPlayerStateChangeHandler, ); remotePlayerController.removeEventListener( cast.framework.RemotePlayerEventType.CURRENT_TIME_CHANGED, onPlayerCurrentTimeChangeHandler, ); }; }, [isChromecast, isLoading, render]); const onVolume = (n) => { setVolume(n); if (!isChromecast) { $video.current.volume = n / 100; settings_put("volume", n); } else { const session = Chromecast.session() if (session) session.setVolume(n / 100); else { setIsChromecast(false); notify.send(t("Cannot establish a connection"), "error"); } } }; const onPlay = () => { setIsPlaying(true); if (!isChromecast) $video.current.play(); else { const media = Chromecast.media(); if (media) media.play(); } }; const onPause = () => { setIsPlaying(false); if (!isChromecast) $video.current.pause(); else { const media = Chromecast.media(); if (media) media.pause(); } }; const onSeek = (newTime) => { if (!isChromecast) $video.current.currentTime = newTime; else { const media = Chromecast.media(); if (!media) return; setIsBuffering(true); const seekRequest = new chrome.cast.media.SeekRequest(); seekRequest.currentTime = parseInt(newTime); media.seek(seekRequest); } }; const onClickSeek = (e) => { let $progress = e.target; if (e.target.classList.contains("progress") == false) { $progress = e.target.parentElement; } const rec = $progress.getBoundingClientRect(); e.persist(); let n = (e.clientX - rec.x) / rec.width; if (n < 2/100) { onPause(); n = 0; } _currentTime = n * duration; setCurrentTime(_currentTime); onSeek(_currentTime); }; const onHoverProgress = (e) => { const rec = e.target.getBoundingClientRect(); const width = e.clientX - rec.x; const time = duration * width / rec.width; let posX = width; posX = Math.max(posX, 30); posX = Math.min(posX, e.target.clientWidth - 30); setHint({ x: `${posX}px`, time }); }; const onClickFullscreen = () => { const session = Chromecast.session(); if (!session) { document.querySelector(".video_screen").requestFullscreen(); requestAnimationFrame(() => setRender(render + 1)); } else chromecastLoader(); }; const isFullscreen = () => { if (!$container.current) return false return window.innerHeight === screen.height; }; const renderBuffer = () => { if (!$video.current) return null; const calcWidth = (i) => { return ($video.current.buffered.end(i) - $video.current.buffered.start(i)) / duration * 100; }; const calcLeft = (i) => { return $video.current.buffered.start(i) / duration * 100; }; return ( { Array.apply(null, { length: $video.current.buffered.length }).map((_, i) => (
)) } ); }; const chromecastLoader = () => { const link = Chromecast.createLink(data); const media = new chrome.cast.media.MediaInfo( link, getMimeType(data), ); media.metadata = new chrome.cast.media.MovieMediaMetadata() media.metadata.title = filename.substr(0, filename.lastIndexOf(filepath.extname(filename))); media.metadata.subtitle = CONFIG.name; media.metadata.images = [ new chrome.cast.Image(origin + "/assets/icons/video.png"), ]; setIsChromecast(true); setIsLoading(false); setIsPlaying(true); setIsBuffering(false); $video.current.muted = true; $video.current.pause(); const session = Chromecast.session(); if (!session) return; setVolume(session.getVolume() * 100); return Chromecast.createRequest(media) .then((req) => { req.currentTime = parseInt(_currentTime); return session.loadMedia(req); }) .then(() => setRender(render + 1)) .catch((err) => { console.error(err); notify.send(t("Cannot establish a connection"), "error"); setIsChromecast(false); setIsLoading(false); }); }; return (
{ isLoading && (
) } { duration > 0 && (
setHint(null)}> { isChromecast === false && renderBuffer() }
{ isLoading || isBuffering ? ( ) : isPlaying ? ( ) : ( ) } onVolume(0)} name={volume === 0 ? "volume_mute" : volume < 50 ? "volume_low" : "volume"}/> onVolume(Number(e.target.value))} value={volume} min="0" max="100" /> { formatTimecode(currentTime) }   /   { formatTimecode(duration) } { hint && (
{ formatTimecode(hint.time) }
) }
) }
); } let _currentTime = 0; // trick to avoid making too many call to the chromecast SDK