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 (