mirror of
https://github.com/mickael-kerjean/filestash.git
synced 2024-04-21 12:32:08 +00:00
feature (video): revamp the video player + chromecast integration
This commit is contained in:
@@ -38,6 +38,7 @@
|
||||
- Manage your files from a browser
|
||||
- Authentication middleware to connect to various source of user
|
||||
- Flexible Share mechanism
|
||||
- Chromecast support for images, music, and videos
|
||||
- Video player
|
||||
- Video transcoding (mov, mkv, avi, mpeg, and more)
|
||||
- Image viewer
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
+8
-1
@@ -143,5 +143,12 @@ function setup_translation() {
|
||||
}
|
||||
|
||||
function setup_chromecast() {
|
||||
return Promise.resolve();
|
||||
if (!CONFIG.enable_chromecast) {
|
||||
return Promise.resolve();
|
||||
} else if (!("chrome" in window)) {
|
||||
return Promise.resolve();
|
||||
} else if (location.hostname === "localhost" || location.hostname === "127.0.0.1") {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Chromecast.init();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use strict";
|
||||
|
||||
import { Session } from "./session";
|
||||
import { currentShare, objectGet } from "../helpers/";
|
||||
|
||||
|
||||
@@ -1,38 +1,319 @@
|
||||
import React, { useEffect, useRef } from "react";
|
||||
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 { getMimeType } from "../../helpers/";
|
||||
import videojs from "video.js";
|
||||
import "videojs-contrib-hls";
|
||||
|
||||
import "video.js/dist/video-js.css";
|
||||
import { Chromecast } from "../../model/"
|
||||
import { getMimeType,settings_get, settings_put, notify } 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();
|
||||
if (!window.overrides["video-map-sources"]) {
|
||||
window.overrides["video-map-sources"] = (s) => (s);
|
||||
}
|
||||
const $container = useRef();
|
||||
const [isPlaying, setIsPlaying] = 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 [isLoading, setIsLoading] = useState(true);
|
||||
const [isBuffering, setIsBuffering] = useState(false);
|
||||
const [render, setRender] = useState(0);
|
||||
const [hint, setHint] = useState(null);
|
||||
const [videoSources, setVideoSources] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
const player = videojs($video.current, {
|
||||
controls: true,
|
||||
sources: window.overrides["video-map-sources"]([{
|
||||
src: data,
|
||||
type: getMimeType(data),
|
||||
}]),
|
||||
});
|
||||
return () => {
|
||||
player.dispose();
|
||||
if (!$video.current) return;
|
||||
const metadataHandler = () => {
|
||||
$video.current.volume = volume / 100;
|
||||
setDuration($video.current.duration);
|
||||
setIsLoading(false);
|
||||
};
|
||||
}, [data]);
|
||||
const finishHandler = () => {
|
||||
$video.current.currentTime = 0;
|
||||
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();
|
||||
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) => {
|
||||
if(e.code !== "Space") {
|
||||
return
|
||||
}
|
||||
isPlaying ? onPause(e) : onPlay(e);
|
||||
};
|
||||
window.addEventListener("resize", resizeHandler);
|
||||
window.addEventListener("keypress", onKeyPressHandler);
|
||||
return () => {
|
||||
window.removeEventListener("resize", resizeHandler);
|
||||
window.removeEventListener("keypress", onKeyPressHandler);
|
||||
};
|
||||
}, [render, isPlaying, isChromecast]);
|
||||
|
||||
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:
|
||||
setIsChromecast(false);
|
||||
$video.current.currentTime = _currentTime;
|
||||
$video.current.muted = false;
|
||||
if (isPlaying) $video.current.play();
|
||||
break;
|
||||
}
|
||||
};
|
||||
context.addEventListener(
|
||||
cast.framework.CastContextEventType.SESSION_STATE_CHANGED,
|
||||
chromecastSetup,
|
||||
);
|
||||
return () => {
|
||||
const media = Chromecast.media();
|
||||
if (media) media.removeUpdateListener(chromecastMediaHandler);
|
||||
context.removeEventListener(
|
||||
cast.framework.CastContextEventType.SESSION_STATE_CHANGED,
|
||||
chromecastSetup,
|
||||
);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
if (isLoading) return;
|
||||
if (isChromecast) {
|
||||
const media = Chromecast.media();
|
||||
if (!media) return;
|
||||
_currentTime = media.getEstimatedTime();
|
||||
setIsBuffering(media.playerState === "BUFFERING");
|
||||
} else _currentTime = $video.current.currentTime;
|
||||
setCurrentTime(_currentTime)
|
||||
}, 100);
|
||||
return () => {
|
||||
clearInterval(interval);
|
||||
};
|
||||
}, [data, isChromecast, isLoading]);
|
||||
|
||||
const onPlay = () => {
|
||||
setIsPlaying(true);
|
||||
if (isChromecast) {
|
||||
const media = Chromecast.media();
|
||||
if (media) media.play();
|
||||
} else $video.current.play();
|
||||
};
|
||||
const onPause = () => {
|
||||
setIsPlaying(false);
|
||||
if (isChromecast) {
|
||||
const media = Chromecast.media();
|
||||
if (media) media.pause();
|
||||
} else $video.current.pause();
|
||||
|
||||
};
|
||||
const onSeek = (newTime) => {
|
||||
if (isChromecast) {
|
||||
const media = Chromecast.media();
|
||||
if (!media) return;
|
||||
setIsLoading(true);
|
||||
const seekRequest = new chrome.cast.media.SeekRequest();
|
||||
seekRequest.currentTime = parseInt(newTime);
|
||||
media.seek(seekRequest);
|
||||
setTimeout(() => setIsLoading(false), 1000);
|
||||
} else $video.current.currentTime = newTime;
|
||||
};
|
||||
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;
|
||||
}
|
||||
onSeek(n * duration);
|
||||
};
|
||||
|
||||
const onVolumeChange = (n) => {
|
||||
settings_put("volume", n);
|
||||
setVolume(n);
|
||||
if (isChromecast) {
|
||||
const session = Chromecast.session()
|
||||
if (session) session.setVolume(n / 100);
|
||||
else notify.send(t("Cannot establish a connection"), "error");
|
||||
}
|
||||
else $video.current.volume = n / 100;
|
||||
};
|
||||
|
||||
const onProgressHover = (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) // min boundary
|
||||
posX = Math.min(posX, e.target.clientWidth - 30);
|
||||
setHint({ x: `${posX}px`, time })
|
||||
};
|
||||
|
||||
const onRequestFullscreen = () => {
|
||||
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 formatTimecode = (seconds) => {
|
||||
return String(parseInt(seconds / 60)).padStart(2, "0") +
|
||||
":"+
|
||||
String(parseInt(seconds % 60)).padStart(2, "0");
|
||||
};
|
||||
|
||||
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);
|
||||
setIsPlaying(true);
|
||||
setIsLoading(false);
|
||||
$video.current.muted = true;
|
||||
$video.current.pause();
|
||||
|
||||
const session = Chromecast.session();
|
||||
if (!session) return;
|
||||
$video.current.pause();
|
||||
setVolume(session.getVolume() * 100);
|
||||
Chromecast.createRequest(media)
|
||||
.then((req) => {
|
||||
req.currentTime = parseInt($video.current.currentTime);
|
||||
setCurrentTime($video.current.currentTime);
|
||||
return session.loadMedia(req);
|
||||
})
|
||||
.then(() => {
|
||||
const media = session.getMediaSession();
|
||||
if (!media) return;
|
||||
media.addUpdateListener(chromecastMediaHandler);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
notify.send(t("Cannot establish a connection"), "error");
|
||||
});
|
||||
};
|
||||
const chromecastMediaHandler = (isAlive) => {
|
||||
if (isAlive) return;
|
||||
const session = Chromecast.session();
|
||||
if (session) {
|
||||
session.endSession();
|
||||
$video.current.muted = false;
|
||||
setVolume($video.current.volume * 100);
|
||||
setIsChromecast(false);
|
||||
onSeek(0);
|
||||
onPause();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="component_videoplayer">
|
||||
<MenuBar title={filename} download={data} />
|
||||
<div className="video_container">
|
||||
<div className="video_container" ref={$container}>
|
||||
<ReactCSSTransitionGroup
|
||||
transitionName="video"
|
||||
transitionAppear={true}
|
||||
@@ -40,16 +321,69 @@ export function VideoPlayer({ filename, data, path }) {
|
||||
transitionEnter={true}
|
||||
transitionEnterTimeout={300}
|
||||
transitionAppearTimeout={300}>
|
||||
<div key={data} data-vjs-player>
|
||||
<video
|
||||
ref={$video}
|
||||
className="video-js vjs-fill vjs-default-skin vjs-big-play-centered"
|
||||
style={{
|
||||
boxShadow: "rgba(0, 0, 0, 0.14) 0px 4px 5px 0px, " +
|
||||
"rgba(0, 0, 0, 0.12) 0px 1px 10px 0px, " +
|
||||
"rgba(0, 0, 0, 0.2) 0px 2px 4px -1px",
|
||||
}}>
|
||||
</video>
|
||||
<div className={"video_screen" + (isPlaying ? " video-state-play" : " video-state-pause")}>
|
||||
<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={onProgressHover} 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={() => onVolumeChange(0)} name={volume === 0 ? "volume_mute" : volume < 50 ? "volume_low" : "volume"}/>
|
||||
<input type="range" onChange={(e) => onVolumeChange(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 className="pull-right">
|
||||
{
|
||||
<React.Fragment>
|
||||
<Icon name="fullscreen" onClick={onRequestFullscreen} />
|
||||
</React.Fragment>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</ReactCSSTransitionGroup>
|
||||
<Pager path={path} />
|
||||
@@ -57,3 +391,5 @@ export function VideoPlayer({ filename, data, path }) {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
let _currentTime = 0; // trick to avoid making too many call to the chromecast SDK
|
||||
|
||||
@@ -1,54 +1,160 @@
|
||||
.component_videoplayer{
|
||||
.component_videoplayer {
|
||||
background: #525659;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
|
||||
.video_container{
|
||||
.video_container {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
|
||||
text-align: center;
|
||||
background: #525659;
|
||||
overflow: hidden;
|
||||
padding: 15px 10px 65px 10px;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
> span{
|
||||
height: 100%;
|
||||
> span {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.video_screen {
|
||||
background: black;
|
||||
box-shadow: rgba(0, 0, 0, 0.14) 0px 4px 5px 0px, rgba(0, 0, 0, 0.12) 0px 1px 10px 0px, rgba(0, 0, 0, 0.2) 0px 2px 4px -1px;
|
||||
position: relative;
|
||||
border-radius: 3px;
|
||||
margin: auto;
|
||||
position: relative;
|
||||
width: 800px;
|
||||
|
||||
.video_wrapper { height: 450px; }
|
||||
.loader {
|
||||
position: absolute;
|
||||
top: 30%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&.video-state-pause .videoplayer_control { opacity: 1; transition: 0.1s opacity ease; }
|
||||
&.video-state-play:hover .videoplayer_control { opacity: 1; transition: 0.1s opacity ease; }
|
||||
.videoplayer_control {
|
||||
transition: 0.5s opacity ease;
|
||||
opacity: 0;
|
||||
|
||||
display: flex;
|
||||
text-align: left;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
border-bottom-left-radius: 3px;
|
||||
border-bottom-right-radius: 3px;
|
||||
background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAADCCAYAAACIaaiTAAAAAXNSR0IArs4c6QAAARJJREFUOE9lyNdHBQAAhfHb3nvvuu2997jNe29TJJEkkkgSSSSJJJJEEkkiifRH5jsP56Xz8PM5gcC/xfCIWBNHiXiTQIlEk0SJZJNCiVRIM+mUyDCZlMgy2ZTIMbmUyDP5lCgwhZQoMsWUKDGllCgz5ZSogEpTRYlqU0OJoKmlRJ2pp0SDaaREk2mmRItppUSbaadEh+mkRBd0mx5K9Jo+SvSbAUoMmiFKDJsRSoyaMUqMmwlKhMwkJabMNCVmYNbMUSJsIpSImnlKLJhFSiyZZWoFVmEN1mEDNmELtmEHdmEP9uEADuEIjuEETuEMzuECLuEKruEGbuEO7uEBHuEJnuEFXuEN3uEDPuELvuEHfv8AoRErEi7Uc8UAAAAASUVORK5CYII=);
|
||||
background-repeat: repeat-x;
|
||||
background-size: contain;
|
||||
padding: 0 0 7px 0;
|
||||
|
||||
img {
|
||||
cursor: pointer;
|
||||
width: 25px;
|
||||
filter: brightness(0) invert(1);
|
||||
padding: 5px 5px 5px 10px;
|
||||
}
|
||||
input[type="range"] {
|
||||
width: 60px;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
-webkit-appearance: none;
|
||||
background: transparent;
|
||||
|
||||
&::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
height: 12px;
|
||||
width: 12px;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
background: white;
|
||||
margin-top: -5px;
|
||||
}
|
||||
&::-webkit-slider-runnable-track {
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background-color: rgba(255,255,255,0.7);
|
||||
border-radius: 2px;
|
||||
}
|
||||
&::-moz-range-track {
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background-color: rgba(255,255,255,0.7);
|
||||
border-radius: 2px;
|
||||
}
|
||||
}
|
||||
.timecode {
|
||||
color: white;
|
||||
margin: auto 0;
|
||||
padding-left: 10px;
|
||||
.hint {
|
||||
position: absolute;
|
||||
top: -40px;
|
||||
margin-left: -23px;
|
||||
font-size: 0.9rem;
|
||||
background: #f1f1f155;
|
||||
border-radius: 3px;
|
||||
padding: 2px 5px;
|
||||
background: var(--dark);
|
||||
}
|
||||
}
|
||||
.progress {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
top: -20px;
|
||||
height: 20px;
|
||||
cursor: pointer;
|
||||
|
||||
.progress-active, .progress-buffer, .progress-placeholder {
|
||||
top: 10px;
|
||||
position: absolute;
|
||||
height: 4px;
|
||||
}
|
||||
.progress-active {
|
||||
background: var(--primary);
|
||||
border-top-right-radius: 2px;
|
||||
border-bottom-right-radius: 2px;
|
||||
.thumb {
|
||||
display: none;
|
||||
float: right;
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
background: rgba(255,255,255,0.1);
|
||||
border-radius: 50%;
|
||||
margin-top: 0px;
|
||||
position: relative;
|
||||
left: 3px;
|
||||
}
|
||||
}
|
||||
.progress-buffer {
|
||||
background: #e2e2e244;
|
||||
}
|
||||
.progress-placeholder {
|
||||
background: #e2e2e222;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
.pull-right {
|
||||
margin-left: auto;
|
||||
padding-right: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
video {
|
||||
width: 100%;
|
||||
> div{margin: auto;}
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* SKIN FOR VIDEOJS: */
|
||||
.video-js{
|
||||
font-size: 14px; /* Chromium bug */
|
||||
max-height: 500px;
|
||||
|
||||
.vjs-control-bar{
|
||||
background: black;
|
||||
background: linear-gradient(transparent 20%, #00000099);
|
||||
}
|
||||
.vjs-load-progress div{
|
||||
background: var(--primary);
|
||||
}
|
||||
.vjs-big-play-button{
|
||||
background-color: var(--primary)!important;
|
||||
border-color: var(--emphasis-primary)!important;
|
||||
border-width: 2px;
|
||||
margin-top: -40px;
|
||||
border-radius: 8px;
|
||||
&:before{ font-size: 45px; color: var(--bg-color); }
|
||||
}
|
||||
}
|
||||
.component_videoplayer .video_container .video_screen .videoplayer_control:hover .progress .progress-active .thumb { display: block; }
|
||||
|
||||
|
||||
.video-enter, .video-appear{
|
||||
|
||||
+1
-3
@@ -18,6 +18,7 @@
|
||||
"codemirror": "^5.26.0",
|
||||
"epubjs": "^0.3.93",
|
||||
"exif-js": "^2.3.0",
|
||||
"hls.js": "^1.4.0",
|
||||
"little-loader": "^0.2.0",
|
||||
"prop-types": "^15.5.10",
|
||||
"react": "^16.5.0",
|
||||
@@ -33,9 +34,6 @@
|
||||
"react-selectable": "git+https://github.com/mickael-kerjean/react-selectable.git",
|
||||
"react-sticky": "^6.0.2",
|
||||
"rxjs": "^5.4.0",
|
||||
"video.js": "^7.14.3",
|
||||
"videojs-contrib-hls": "^5.14.1",
|
||||
"videojs-sublime-skin": "^1.0.3",
|
||||
"wavesurfer.js": "^1.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
Reference in New Issue
Block a user