From 07e77e969d04084f196673e01bc41e8403e29750 Mon Sep 17 00:00:00 2001 From: Mickael Kerjean Date: Tue, 18 Apr 2023 00:01:32 +1000 Subject: [PATCH] feature (chromecast): wip for chromecast support --- client/pages/viewerpage.js | 8 - client/pages/viewerpage/audioplayer.js | 275 +++++++++++++++-------- client/pages/viewerpage/imageviewer.js | 11 +- client/pages/viewerpage/menubar.js | 2 +- client/pages/viewerpage/pager.js | 20 +- client/pages/viewerpage/videoplayer.js | 168 ++++++++------ client/pages/viewerpage/videoplayer.scss | 8 +- 7 files changed, 304 insertions(+), 188 deletions(-) diff --git a/client/pages/viewerpage.js b/client/pages/viewerpage.js index 5e1949d3..a8882c69 100644 --- a/client/pages/viewerpage.js +++ b/client/pages/viewerpage.js @@ -135,14 +135,6 @@ export function ViewerPageComponent({ error, subscribe, unsubscribe, match, loca return history.listen(() => {}) }, [path]); - useEffect(() => { - return () => { - const context = Chromecast.context(); - if (!context) return; - context.endCurrentSession(); - }; - }, []); - return (
diff --git a/client/pages/viewerpage/audioplayer.js b/client/pages/viewerpage/audioplayer.js index 3242e9d9..627bd180 100644 --- a/client/pages/viewerpage/audioplayer.js +++ b/client/pages/viewerpage/audioplayer.js @@ -14,8 +14,11 @@ 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 [currentTime, setCurrentTime] = useState(0); + const [duration, setDuration] = useState(0); const [isChromecast, setIsChromecast] = useState(false); const [error, setError] = useState(null); + const [render, setRender] = useState(0); const wavesurfer = useRef(null); useEffect(() => { @@ -28,18 +31,18 @@ export function AudioPlayer({ filename, data }) { height: 200, barWidth: 1, }); + window.wavesurfer = wavesurfer.current; // TODO: remove this wavesurfer.current.load(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()); + setDuration(wavesurfer.current.getDuration()); }); wavesurfer.current.on("audioprocess", () => { - $currentTime.innerHTML = formatTimecode(wavesurfer.current.getCurrentTime()); + const t = wavesurfer.current.getCurrentTime() + _currentTime = t; + setCurrentTime(t); }); wavesurfer.current.on("loading", (n) => { setPurcentLoading(n); @@ -48,106 +51,53 @@ 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(); - }, []); + }, [data]); useEffect(() => { const onKeyPressHandler = (e) => { if(e.code !== "Space") { return } + // TODO: write shortcut isPlaying ? onPause(e) : onPlay(e); }; window.addEventListener("keypress", onKeyPressHandler); return () => window.removeEventListener("keypress", onKeyPressHandler); - }, [isPlaying, isChromecast]) - - 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: - chromecastHandler() - const session = Chromecast.session(); - if (session) setVolume(session.getVolume() * 100); - break; - case cast.framework.SessionState.SESSION_ENDING: - wavesurfer.current.setMute(false); - setVolume(wavesurfer.current.getVolume() * 100); - 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); - } - }; + }, [isPlaying, isChromecast]); useEffect(() => { const context = Chromecast.context(); if (!context) return; - chromecastAlive(false); document.getElementById("chromecast-target").append(document.createElement("google-cast-launcher")); + _currentTime = 0; + + 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); + // console.log("ENDING seekTo", _currentTime, duration, wavesurfer.current.getDuration(), _currentTime / wavesurfer.current.getDuration()); + wavesurfer.current.seekTo(_currentTime / wavesurfer.current.getDuration()); + // TODO: reset volume setVolume(wavesurfer.current.getVolume() * 100) --> not working + wavesurfer.current.setMute(false); + const media = Chromecast.media(); + if (media && media.playerState === "PLAYING") wavesurfer.current.play(); + else if (media && media.playerState === "PAUSED") wavesurfer.current.pause(); + break; + } + }; context.addEventListener( cast.framework.CastContextEventType.SESSION_STATE_CHANGED, chromecastSetup, @@ -157,13 +107,92 @@ export function AudioPlayer({ filename, data }) { cast.framework.CastContextEventType.SESSION_STATE_CHANGED, chromecastSetup, ); - const media = Chromecast.media(); - if (!media) return - media.removeUpdateListener(chromecastAlive); - chromecastAlive(false); }; }, []); + useEffect(() => { + if (!wavesurfer) return; + const onSeek = (s) => { + // console.log("ON SEEK", isChromecast, isLoading); + if (isChromecast === false) return + else if (s * duration === _currentTime) { + // wavesurfer trigger a seek event when trying to synchronise the remote to the local + // which we want to ignore as we're only interested in user requested seek + return; + } + const media = Chromecast.media(); + if (!media) return; + + wavesurfer.current.pause(); + const seekRequest = new chrome.cast.media.SeekRequest(); + seekRequest.currentTime = s*duration; + media.seek(seekRequest); + } + wavesurfer.current.on("seek", onSeek); + return () => { + wavesurfer.current.un("seek", onSeek); + }; + }, [wavesurfer.current, isChromecast]); + + useEffect(() => { + const media = Chromecast.media(); + if (!media) return; + + const remotePlayer = new cast.framework.RemotePlayer(); + const remotePlayerController = new cast.framework.RemotePlayerController(remotePlayer); + const onPlayerStateChangeHandler = (event) => { + // console.log("PLAYER STATE CHANGE", event) + switch(event.value) { + case "BUFFERING": + wavesurfer.current.pause(); + break + case "PLAYING": + wavesurfer.current.play(); + break; + } + }; + const onPlayerCurrentTimeChangeHandler = (event) => { + _currentTime = event.value; + setCurrentTime(event.value); + if (event.value > 0) wavesurfer.current.seekTo(event.value / wavesurfer.current.getDuration()); + // console.log("time change", event.value, wavesurfer.current.getDuration(), event.value / wavesurfer.current.getDuration()) + }; + 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; + } + }; + + + + remotePlayerController.addEventListener( + cast.framework.RemotePlayerEventType.PLAYER_STATE_CHANGED, + onPlayerStateChangeHandler, + ); + remotePlayerController.addEventListener( + cast.framework.RemotePlayerEventType.CURRENT_TIME_CHANGED, + onPlayerCurrentTimeChangeHandler, + ); + return () => { + remotePlayerController.removeEventListener( + cast.framework.RemotePlayerEventType.PLAYER_STATE_CHANGED, + onPlayerStateChangeHandler, + ); + remotePlayerController.removeEventListener( + cast.framework.RemotePlayerEventType.CURRENT_TIME_CHANGED, + onPlayerCurrentTimeChangeHandler, + ); + }; + }, [isChromecast, isLoading, render]); + const onPlay = (e) => { e.preventDefault(); e.stopPropagation(); @@ -187,13 +216,20 @@ export function AudioPlayer({ filename, data }) { }; const onVolumeChange = (v) => { - settings_put("volume", v); setVolume(v); if (isChromecast) { const session = Chromecast.session() if (session) session.setVolume(v / 100); - } else wavesurfer.current.setVolume(v / 100); + else { + setIsChromecast(false); + notify.send(t("Cannot establish a connection"), "error"); + } + } else { + wavesurfer.current.setVolume(v / 100); + settings_put("volume", v); + } }; + const onVolumeClick = () => { onVolumeChange(0); }; @@ -204,9 +240,52 @@ export function AudioPlayer({ filename, data }) { 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.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"), + ]; + + setIsChromecast(true); + setIsLoading(false); + setIsPlaying(true); + wavesurfer.current.setMute(true); + wavesurfer.current.pause(); + + const session = Chromecast.session(); + if (!session) return; + setVolume(session.getVolume() * 100); + Chromecast.createRequest(media) + .then((req) => { + req.currentTime = _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 (
- + + { + Chromecast.session() && ( + chromecastLoader()} /> + ) + } +
{error} @@ -246,9 +325,9 @@ export function AudioPlayer({ filename, data }) { onVolumeChange(Number(e.target.value) || 0)} type="range" min="0" max="100" value={volume}/>
- { formatTimecode(0) } + { formatTimecode(currentTime) } / - { formatTimecode(0) } + { formatTimecode(duration) }
@@ -257,3 +336,5 @@ export function AudioPlayer({ filename, data }) { ) } + +let _currentTime = 0; // trick to avoid making too many call to the chromecast SDK diff --git a/client/pages/viewerpage/imageviewer.js b/client/pages/viewerpage/imageviewer.js index d61847f5..45e022b1 100644 --- a/client/pages/viewerpage/imageviewer.js +++ b/client/pages/viewerpage/imageviewer.js @@ -61,12 +61,12 @@ export function ImageViewerComponent({ filename, data, path, subscribe, unsubscr const chromecastSetup = (event) => { switch (event.sessionState) { case cast.framework.SessionState.SESSION_STARTED: - chromecastHandler(); + chromecastLoader(); break; } }; - const chromecastHandler = (event) => { + const chromecastLoader = () => { const session = Chromecast.session(); if (!session) return; @@ -96,7 +96,8 @@ export function ImageViewerComponent({ filename, data, path, subscribe, unsubscr cast.framework.CastContextEventType.SESSION_STATE_CHANGED, chromecastSetup, ); - chromecastHandler(); + const media = Chromecast.media(); + if (media && media.media && media.media.mediaCategory === "IMAGE") chromecastLoader(); return () => { context.removeEventListener( cast.framework.CastContextEventType.SESSION_STATE_CHANGED, @@ -119,6 +120,7 @@ export function ImageViewerComponent({ filename, data, path, subscribe, unsubscr } }; const requestFullScreen = () => { + chromecastLoader(); if ("webkitRequestFullscreen" in document.body) { $container.current.webkitRequestFullscreen(); } else if ("mozRequestFullScreen" in document.body) { @@ -168,10 +170,9 @@ export function ImageViewerComponent({ filename, data, path, subscribe, unsubscr setState({ draggable: files.length > 1 ? true : false })} - next={(e) => setState({ preload: e })} /> + /> diff --git a/client/pages/viewerpage/menubar.js b/client/pages/viewerpage/menubar.js index 3b8e0235..67737fd4 100644 --- a/client/pages/viewerpage/menubar.js +++ b/client/pages/viewerpage/menubar.js @@ -16,8 +16,8 @@ export const MenuBar = (props) => {
{props.title}
- {props.children} + { props.download === null ? diff --git a/client/pages/viewerpage/pager.js b/client/pages/viewerpage/pager.js index ec76dae4..5fc1fa3c 100644 --- a/client/pages/viewerpage/pager.js +++ b/client/pages/viewerpage/pager.js @@ -74,9 +74,15 @@ class PagerComponent extends React.Component { if (f === null) return Promise.reject({ code: "NO_DATA" }); return Promise.resolve(f); }) - .then((f) => f.results - .filter((file) => (isImage(file.name) || isVideo(file.name)) && - file.type === "file")) + .then((f) => f.results.filter((file) => { + if (file.type !== "file") return false; + const mType = getMimeType(file.name); + switch(mType.split("/")[0]) { + case "video": return true; + case "image": return true; + } + return false; + })) .then((f) => sort(f, settings_get("filespage_sort") || "type")) .then((f) => findPosition(f, basename(props.path))) .then((res) => { @@ -98,12 +104,6 @@ class PagerComponent extends React.Component { } return [files, i]; }; - const isVideo = (filename) => { - return getMimeType(filename).split("/")[0] === "video"; - }; - const isImage = (filename) => { - return getMimeType(filename).split("/")[0] === "image"; - }; } onFormInputChange(e) { @@ -125,6 +125,8 @@ class PagerComponent extends React.Component { } onKeyPress(e) { + e.preventDefault(); + e.stopPropagation(); if (e.target.classList.contains("prevent")) return; if (e.keyCode === 39) { this.navigatePage(this.calculateNextPageNumber(this.state.n)); diff --git a/client/pages/viewerpage/videoplayer.js b/client/pages/viewerpage/videoplayer.js index 7d4fa487..156738ac 100644 --- a/client/pages/viewerpage/videoplayer.js +++ b/client/pages/viewerpage/videoplayer.js @@ -33,7 +33,6 @@ export function VideoPlayer({ filename, data, path }) { setIsLoading(false); }; const finishHandler = () => { - $video.current.currentTime = 0; setIsPlaying(false); }; const errorHandler = (err) => { @@ -94,8 +93,6 @@ export function VideoPlayer({ filename, data, path }) { case "KeyM": return onVolumeChange(0); case "ArrowUp": return onVolumeChange(Math.min(volume + 10, 100)); case "ArrowDown": return onVolumeChange(Math.max(volume - 10, 0)); - case "ArrowLeft": return onSeek(_currentTime - 5); - case "ArrowRight": return onSeek(_currentTime + 5); case "KeyL": return onSeek(_currentTime + 10); case "KeyJ": return onSeek(_currentTime - 10); case "KeyF": return onRequestFullscreen(); @@ -135,13 +132,22 @@ export function VideoPlayer({ filename, data, path }) { setIsLoading(false); break; case cast.framework.SessionState.SESSION_STARTED: - chromecastLoader() + chromecastLoader(); break; case cast.framework.SessionState.SESSION_ENDING: setIsChromecast(false); + setVolume($video.current.volume * 100); + $video.current.currentTime = _currentTime; + $video.current.muted = false; + 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; - if (isPlaying) $video.current.play(); break; } }; @@ -150,8 +156,6 @@ export function VideoPlayer({ filename, data, path }) { chromecastSetup, ); return () => { - const media = Chromecast.media(); - if (media) media.removeUpdateListener(chromecastMediaHandler); context.removeEventListener( cast.framework.CastContextEventType.SESSION_STATE_CHANGED, chromecastSetup, @@ -160,20 +164,71 @@ export function VideoPlayer({ filename, data, path }) { }, []); 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); + if (isLoading === true) return; + else if (isChromecast === false) { + const interval = setInterval(() => { + _currentTime = $video.current.currentTime; + setCurrentTime(_currentTime); + }, 100); + return () => { + clearInterval(interval); + }; + } + + 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; + } }; - }, [data, isChromecast, isLoading]); + 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 onPlay = () => { setIsPlaying(true); @@ -194,11 +249,10 @@ export function VideoPlayer({ filename, data, path }) { if (isChromecast) { const media = Chromecast.media(); if (!media) return; - setIsLoading(true); + setIsBuffering(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) => { @@ -213,18 +267,24 @@ export function VideoPlayer({ filename, data, path }) { onPause(); n = 0; } - onSeek(n * duration); + _currentTime = n * duration; + setCurrentTime(_currentTime); + onSeek(_currentTime); }; 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 { + setIsChromecast(false); + notify.send(t("Cannot establish a connection"), "error"); + } + } else { + $video.current.volume = n / 100; + settings_put("volume", n); } - else $video.current.volume = n / 100; }; const onProgressHover = (e) => { @@ -232,9 +292,9 @@ export function VideoPlayer({ filename, data, path }) { const width = e.clientX - rec.x; const time = duration * width / rec.width; let posX = width; - posX = Math.max(posX, 30) // min boundary + posX = Math.max(posX, 30); posX = Math.min(posX, e.target.clientWidth - 30); - setHint({ x: `${posX}px`, time }) + setHint({ x: `${posX}px`, time }); }; const onRequestFullscreen = () => { @@ -242,9 +302,7 @@ export function VideoPlayer({ filename, data, path }) { if (!session) { document.querySelector(".video_screen").requestFullscreen(); requestAnimationFrame(() => setRender(render + 1)); - } else { - chromecastLoader(); - } + } else chromecastLoader(); }; const isFullscreen = () => { @@ -290,47 +348,34 @@ export function VideoPlayer({ filename, data, path }) { ]; setIsChromecast(true); - setIsPlaying(true); setIsLoading(false); + setIsPlaying(true); + setIsBuffering(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) + return Chromecast.createRequest(media) .then((req) => { - req.currentTime = parseInt($video.current.currentTime); - setCurrentTime($video.current.currentTime); + req.currentTime = parseInt(_currentTime); return session.loadMedia(req); }) - .then(() => { - const media = session.getMediaSession(); - if (!media) return; - media.addUpdateListener(chromecastMediaHandler); - }) + .then(() => setRender(render + 1)) .catch((err) => { console.error(err); notify.send(t("Cannot establish a connection"), "error"); + setIsChromecast(false); + setIsLoading(false); }); }; - 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 ( -
- +
+ + +
-
+
-
- { - - - - } -
) } diff --git a/client/pages/viewerpage/videoplayer.scss b/client/pages/viewerpage/videoplayer.scss index 3bca5ab3..db50f817 100644 --- a/client/pages/viewerpage/videoplayer.scss +++ b/client/pages/viewerpage/videoplayer.scss @@ -38,7 +38,9 @@ width: 100%; } - &.video-state-pause .videoplayer_control { opacity: 1; transition: 0.1s opacity ease; } + &.is-casting-yes .videoplayer_control, + &.video-state-pause .videoplayer_control, + &.video-state-buffer .videoplayer_control, &.video-state-play:hover .videoplayer_control { opacity: 1; transition: 0.1s opacity ease; } .videoplayer_control { transition: 0.5s opacity ease; @@ -142,10 +144,6 @@ width: 100%; } } - .pull-right { - margin-left: auto; - padding-right: 10px; - } } } video {