mirror of
https://github.com/mickael-kerjean/filestash.git
synced 2025-11-03 21:17:33 +08:00
457 lines
18 KiB
JavaScript
457 lines
18 KiB
JavaScript
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 (
|
|
<React.Fragment>
|
|
{
|
|
Array.apply(null, { length: $video.current.buffered.length }).map((_, i) => (
|
|
<div className="progress-buffer" key={i} style={{left: calcLeft(i) + "%", width: calcWidth(i) + "%" }} />
|
|
))
|
|
}
|
|
</React.Fragment>
|
|
);
|
|
};
|
|
|
|
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 (
|
|
<div className="component_videoplayer" >
|
|
<MenuBar title={filename} download={data}>
|
|
<Icon name="fullscreen" onClick={onClickFullscreen} />
|
|
</MenuBar>
|
|
<div className="video_container" ref={$container}>
|
|
<ReactCSSTransitionGroup
|
|
transitionName="video"
|
|
transitionAppear={true}
|
|
transitionLeave={false}
|
|
transitionEnter={true}
|
|
transitionEnterTimeout={300}
|
|
transitionAppearTimeout={300}>
|
|
<div className={
|
|
"video_screen" +
|
|
(isBuffering ? " video-state-buffer" : isPlaying ? " video-state-play" : " video-state-pause") +
|
|
(isChromecast ? " is-casting-yes" : " is-casting-no")
|
|
}>
|
|
<div className="video_wrapper" style={isFullscreen() ? {
|
|
maxHeight: "inherit",
|
|
height: "inherit",
|
|
} : {
|
|
maxHeight: (($container.current || {}).clientHeight - 100) || 0,
|
|
}}>
|
|
<video onClick={() => isPlaying ? onPause() : onPlay()} ref={$video}>
|
|
{
|
|
videoSources.map((d, i) => (
|
|
<source key={i} src={d.src} type={d.type} />
|
|
))
|
|
}
|
|
</video>
|
|
</div>
|
|
{
|
|
isLoading && (
|
|
<div className="loader no-select">
|
|
<Icon name="loading" />
|
|
</div>
|
|
)
|
|
}
|
|
{
|
|
duration > 0 && (
|
|
<div className="videoplayer_control no-select">
|
|
<div className="progress" onClick={onClickSeek} onMouseMove={onHoverProgress} onMouseLeave={() => setHint(null)}>
|
|
{ isChromecast === false && renderBuffer() }
|
|
<div className="progress-active" style={{width: (currentTime * 100 / (duration || 1)) + "%"}}>
|
|
<div className="thumb" />
|
|
</div>
|
|
<div className="progress-placeholder"></div>
|
|
</div>
|
|
{
|
|
isLoading || isBuffering ? (
|
|
<Icon name="loading" />
|
|
) : isPlaying ? (
|
|
<Icon name="pause" onClick={onPause} />
|
|
) : (
|
|
<Icon name="play" onClick={onPlay} />
|
|
)
|
|
}
|
|
<Icon name="volume" onClick={() => onVolume(0)} name={volume === 0 ? "volume_mute" : volume < 50 ? "volume_low" : "volume"}/>
|
|
<input type="range" onChange={(e) => onVolume(Number(e.target.value))} value={volume} min="0" max="100" />
|
|
<span className="timecode">
|
|
{ formatTimecode(currentTime) }
|
|
/
|
|
{ formatTimecode(duration) }
|
|
{
|
|
hint && (
|
|
<div className="hint" style={{left: hint.x}}>{ formatTimecode(hint.time) }</div>
|
|
)
|
|
}
|
|
</span>
|
|
</div>
|
|
)
|
|
}
|
|
</div>
|
|
</ReactCSSTransitionGroup>
|
|
<Pager path={path} />
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
let _currentTime = 0; // trick to avoid making too many call to the chromecast SDK
|