Skip to content

Instantly share code, notes, and snippets.

@marioloncarek
Created June 22, 2020 15:32
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save marioloncarek/637e86d126a514f448513852ffcd02be to your computer and use it in GitHub Desktop.
Save marioloncarek/637e86d126a514f448513852ffcd02be to your computer and use it in GitHub Desktop.
custom video player
<!--CUSTOM VIDEO PLAYER-->
<div class="c-custom-video-player is-paused js-custom-video-player">
<!--video source-->
<video class="c-custom-video-player__video js-custom-video-player-media"
preload="metadata"
poster="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.jpg">
<source src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-576p.mp4"
type="video/mp4"/>
</video>
<!--end video source-->
<!--playback state indicator-->
<div class="c-custom-video-player__state-indicator js-custom-video-player-state">
<!--icon play-->
<svg class="c-custom-video-player__play"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 320.001 320.001">
<path d="M295.84 146.049l-256-144a16.026 16.026 0 00-15.904.128A15.986 15.986 0 0016 16.001v288a15.986 15.986 0 007.936 13.824A16.144 16.144 0 0032 320.001c2.688 0 5.408-.672 7.84-2.048l256-144c5.024-2.848 8.16-8.16 8.16-13.952s-3.136-11.104-8.16-13.952z"/>
</svg>
<!--end icon play-->
<!--icon pause-->
<svg class="c-custom-video-player__pause"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 320 320">
<path d="M112 0H16C7.168 0 0 7.168 0 16v288c0 8.832 7.168 16 16 16h96c8.832 0 16-7.168 16-16V16c0-8.832-7.168-16-16-16zM304 0h-96c-8.832 0-16 7.168-16 16v288c0 8.832 7.168 16 16 16h96c8.832 0 16-7.168 16-16V16c0-8.832-7.168-16-16-16z"/>
</svg>
<!--end icon pause-->
</div>
<!--end playback state indicator-->
<!--controls-->
<div class="c-custom-video-player__controls">
<!--progress-->
<div class="c-custom-video-player__progress">
<!--progress bar-->
<progress class="c-custom-video-player__progress-bar js-custom-video-player-progress-bar"
value="0">
</progress>
<!--end progress bar-->
<!--time input-->
<input class="c-custom-video-player__time-input js-custom-video-player-time-input"
value="0"
min="0"
max="100"
type="range"
step="0.01">
<!--end time input-->
</div>
<!--end progress-->
<!--actions-->
<div class="c-custom-video-player__actions">
<!--actions left-->
<div class="c-custom-video-player__actions-left">
<!--play button-->
<button class="c-custom-video-player__action-button js-custom-video-player-play">
<!--icon play-->
<svg class="c-custom-video-player__play"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 320.001 320.001">
<path d="M295.84 146.049l-256-144a16.026 16.026 0 00-15.904.128A15.986 15.986 0 0016 16.001v288a15.986 15.986 0 007.936 13.824A16.144 16.144 0 0032 320.001c2.688 0 5.408-.672 7.84-2.048l256-144c5.024-2.848 8.16-8.16 8.16-13.952s-3.136-11.104-8.16-13.952z"/>
</svg>
<!--end icon play-->
<!--icon pause-->
<svg class="c-custom-video-player__pause"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 320 320">
<path d="M112 0H16C7.168 0 0 7.168 0 16v288c0 8.832 7.168 16 16 16h96c8.832 0 16-7.168 16-16V16c0-8.832-7.168-16-16-16zM304 0h-96c-8.832 0-16 7.168-16 16v288c0 8.832 7.168 16 16 16h96c8.832 0 16-7.168 16-16V16c0-8.832-7.168-16-16-16z"/>
</svg>
<!--end icon pause-->
</button>
<!--end play button-->
<!--volume-->
<div class="c-custom-video-player__volume">
<!--mute button-->
<button class="c-custom-video-player__action-button js-custom-video-player-volume-button">
<!--icon mute-->
<svg class="c-custom-video-player__mute"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 448.075 448.075">
<path d="M352.021 16.075c0-6.08-3.52-11.84-8.96-14.4-5.76-2.88-12.16-1.92-16.96 1.92l-141.76 112.96 167.68 167.68V16.075zM443.349 420.747l-416-416c-6.24-6.24-16.384-6.24-22.624 0s-6.24 16.384 0 22.624l100.672 100.704h-9.376c-9.92 0-18.56 4.48-24.32 11.52-4.8 5.44-7.68 12.8-7.68 20.48v128c0 17.6 14.4 32 32 32h74.24l155.84 124.48c2.88 2.24 6.4 3.52 9.92 3.52 2.24 0 4.8-.64 7.04-1.6 5.44-2.56 8.96-8.32 8.96-14.4v-57.376l68.672 68.672c3.136 3.136 7.232 4.704 11.328 4.704s8.192-1.568 11.328-4.672c6.24-6.272 6.24-16.384 0-22.656z"/>
</svg>
<!--end icon mute-->
<!--icon sound-->
<svg class="c-custom-video-player__sound"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 480 480">
<path d="M278.944 17.577c-5.568-2.656-12.128-1.952-16.928 1.92L106.368 144.009H32c-17.632 0-32 14.368-32 32v128c0 17.664 14.368 32 32 32h74.368l155.616 124.512A16.158 16.158 0 00272 464.009c2.368 0 4.736-.544 6.944-1.6a15.968 15.968 0 009.056-14.4v-416a16.05 16.05 0 00-9.056-14.432zM368.992 126.857c-6.304-6.208-16.416-6.112-22.624.128-6.208 6.304-6.144 16.416.128 22.656C370.688 173.513 384 205.609 384 240.009s-13.312 66.496-37.504 90.368c-6.272 6.176-6.336 16.32-.128 22.624a15.943 15.943 0 0011.36 4.736c4.064 0 8.128-1.536 11.264-4.64C399.328 323.241 416 283.049 416 240.009s-16.672-83.232-47.008-113.152z"/>
<path d="M414.144 81.769c-6.304-6.24-16.416-6.176-22.656.096-6.208 6.272-6.144 16.416.096 22.624C427.968 140.553 448 188.681 448 240.009s-20.032 99.424-56.416 135.488c-6.24 6.24-6.304 16.384-.096 22.656 3.168 3.136 7.264 4.704 11.36 4.704 4.064 0 8.16-1.536 11.296-4.64C456.64 356.137 480 299.945 480 240.009s-23.36-116.128-65.856-158.24z"/>
</svg>
<!--end icon sound-->
</button>
<!--end mute button-->
<!--volume input-->
<input class="c-custom-video-player__volume-input js-custom-video-player-volume-input"
value="1"
type="range"
max="1"
min="0"
step="0.01">
<!--end volume input-->
</div>
<!--end volume-->
<!--time-->
<div class="c-custom-video-player__time">
<time class="js-custom-video-player-current-time">00:00</time>
<span> / </span>
<time class="js-custom-video-player-video-duration">00:00</time>
</div>
<!--end time-->
</div>
<!--end actions left-->
<!--actions right-->
<div class="c-custom-video-player__actions-right">
<!--full screen button-->
<button class="c-custom-video-player__action-button js-custom-video-player-full-screen">
<!--icon fullscreen-->
<svg class="c-custom-video-player__full-screen"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 512 512">
<path d="M486.396 336.393l-43.184 43.184-104.019-104.033c-5.856-5.856-15.349-5.862-21.211 0l-42.437 42.437c-5.868 5.868-5.844 15.382 0 21.226l104.034 104.004-43.184 43.184C326.986 495.803 333.635 512 347 512h150c8.291 0 15-6.709 15-15V347c0-13.301-16.126-20.053-25.604-10.607zM236.454 172.808L132.421 68.789l43.184-43.184C185.014 16.197 178.365 0 165 0H15C6.709 0 0 6.709 0 15v150c0 13.269 16.088 20.09 25.606 10.606l43.198-43.169 104.004 104.018c5.856 5.856 15.351 5.859 21.209.001l42.437-42.437c5.832-5.832 5.88-15.331 0-21.211z"/>
</svg>
<!--end icon fullscreen-->
<!--icon exit full screen-->
<svg class="c-custom-video-player__exit-full-screen"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 511.999 511.999">
<path d="M507.604 443.959L403.571 339.94l43.184-43.184c9.409-9.408 2.76-25.605-10.605-25.605h-150c-8.291 0-15 6.709-15 15v150c0 13.324 16.158 20.052 25.605 10.605l43.198-43.169 104.004 104.018c5.856 5.856 15.352 5.859 21.21.002l42.437-42.437c5.832-5.832 5.88-15.331 0-21.211zM215.244 65.243l-43.184 43.184L68.041 4.394c-5.856-5.856-15.349-5.862-21.211 0L4.394 46.83c-5.868 5.868-5.844 15.382 0 21.226L108.428 172.06l-43.184 43.184c-9.409 9.408-2.76 25.605 10.605 25.605h150c8.291 0 15-6.709 15-15.001v-150c0-13.283-16.12-20.09-25.605-10.605z"/>
</svg>
<!--end icon exit full screen-->
</button>
<!--end full screen button-->
</div>
<!--end actions right-->
</div>
<!--end actions-->
</div>
<!--end controls-->
</div>
<!--end CUSTOM VIDEO PLAYER-->
/**
* Custom video player
*/
export default class CustomVideoPlayer {
constructor() {
/**
* Custom video player DOM selectors
* @type {{volumeInput: string, videoDurationEl: string, volumeButton: string, timeInput: string, fullScreenButton: string, playButton: string, videoEl: string, videoPlayer: string, videoProgressBarEl: string, videoCurrentTimeEl: string, states: {paused: string, fullscreen: string, playing: string, mute: string}}}
*/
this.DOM = {
videoPlayer: ".js-custom-video-player",
videoEl: ".js-custom-video-player-media",
videoProgressBarEl: ".js-custom-video-player-progress-bar",
videoDurationEl: ".js-custom-video-player-video-duration",
videoCurrentTimeEl: ".js-custom-video-player-current-time",
timeInput: ".js-custom-video-player-time-input",
volumeInput: ".js-custom-video-player-volume-input",
volumeButton: ".js-custom-video-player-volume-button",
playButton: ".js-custom-video-player-play",
fullScreenButton: ".js-custom-video-player-full-screen",
states: {
playing: "is-playing",
paused: "is-paused",
mute: "is-muted",
fullscreen: "is-full-screen",
},
};
/**
*
* @type {NodeListOf<HTMLElement>}
*/
this.videoPlayer = document.querySelectorAll(this.DOM.videoPlayer);
}
/**
* Init
*/
init() {
if (this.videoPlayer.length < 1) {
return;
}
this.customVideoPlayer();
}
/**
* Main player method
* Get all elements from each player in DOM
* Init events for elements from each player
*/
customVideoPlayer() {
console.log("Custom video player init()");
for (let i = 0; i < this.videoPlayer.length; i++) {
let videoContainer = this.videoPlayer[i];
let videoEl = videoContainer.querySelector(this.DOM.videoEl);
let videoProgressBarEl = videoContainer.querySelector(this.DOM.videoProgressBarEl);
let videoDurationEl = videoContainer.querySelector(this.DOM.videoDurationEl);
let videoCurrentTimeEl = videoContainer.querySelector(this.DOM.videoCurrentTimeEl);
let timeInput = videoContainer.querySelector(this.DOM.timeInput);
let volumeInput = videoContainer.querySelector(this.DOM.volumeInput);
let playButton = videoContainer.querySelector(this.DOM.playButton);
let volumeButton = videoContainer.querySelector(this.DOM.volumeButton);
let fullScreenButton = videoContainer.querySelector(this.DOM.fullScreenButton);
/**
* Await meta data
*/
this.awaitMeta(videoEl, timeInput, videoProgressBarEl, videoDurationEl);
/**
* Video timeupdate event
*/
videoEl.addEventListener("timeupdate", () => {
this.updateCurrentVideoTime(videoEl, videoCurrentTimeEl);
this.updateProgress(videoEl, timeInput, videoProgressBarEl);
});
/**
* Click on video event
*/
videoEl.addEventListener("click", () => {
this.togglePlay(videoEl, videoContainer);
});
/**
* Change time slider event
*/
timeInput.addEventListener("input", (event) => {
this.goToTime(event, videoEl, videoProgressBarEl, timeInput);
});
/**
* Change volume slider event
*/
volumeInput.addEventListener("input", () => {
this.updateVolume(videoEl, volumeInput, videoContainer);
});
/**
* Click on play button event
*/
playButton.addEventListener("click", () => {
this.togglePlay(videoEl, videoContainer);
});
/**
* Click on volume button event
*/
volumeButton.addEventListener("click", () => {
this.toggleMute(videoEl, volumeInput, videoContainer);
});
/**
* Click on full screen button event
*/
fullScreenButton.addEventListener("click", () => {
this.toggleFullScreen(videoContainer);
});
}
}
/**
* Recursive function that checks if video metadata is loaded
* If video duration is NaN that indicates that video meta is not loaded
* @param videoEl
* @param timeInput
* @param videoProgressBarEl
* @param videoDurationEl
*/
awaitMeta(videoEl, timeInput, videoProgressBarEl, videoDurationEl) {
if (!isNaN(videoEl.duration)) {
this.videoSetup(videoEl, timeInput, videoProgressBarEl, videoDurationEl);
console.log("Video meta loaded");
} else {
setTimeout(() => {
this.awaitMeta(videoEl, timeInput, videoProgressBarEl, videoDurationEl);
}, 10);
}
}
/**
* Setup video from preloaded metadata
* @param videoEl
* @param timeInput
* @param videoProgressBarEl
* @param videoDurationEl
*/
videoSetup(videoEl, timeInput, videoProgressBarEl, videoDurationEl) {
timeInput.setAttribute("max", videoEl.duration);
videoProgressBarEl.setAttribute("max", videoEl.duration);
let time = this.formatTime(videoEl.duration);
videoDurationEl.innerText = `${time.minutes}:${time.seconds}`;
}
/**
* Update video volume
* @param videoEl
* @param volumeInput
* @param videoContainer
*/
updateVolume(videoEl, volumeInput, videoContainer) {
videoEl.volume = volumeInput.value;
if (videoEl.muted || videoEl.volume === 0) {
videoContainer.classList.add(this.DOM.states.mute);
} else {
videoContainer.classList.remove(this.DOM.states.mute);
}
}
/**
* Update current video time
* @param videoEl
* @param videoCurrentTimeEl
*/
updateCurrentVideoTime(videoEl, videoCurrentTimeEl) {
let time = this.formatTime(videoEl.currentTime);
videoCurrentTimeEl.innerText = `${time.minutes}:${time.seconds}`;
}
/**
* Update video progress
* @param videoEl
* @param timeInput
* @param videoProgressBarEl
*/
updateProgress(videoEl, timeInput, videoProgressBarEl) {
timeInput.value = videoEl.currentTime;
videoProgressBarEl.value = videoEl.currentTime;
timeInput.setAttribute("data-bla", timeInput.value);
}
/**
* Go to time when user uses range slider
* @param event
* @param videoEl
* @param videoProgressBarEl
* @param timeInput
*/
goToTime(event, videoEl, videoProgressBarEl, timeInput) {
let time = event.target.dataset.time ? event.target.dataset.time : event.target.value;
videoEl.currentTime = time;
videoProgressBarEl.value = time;
timeInput.value = time;
}
/**
* Toggle play or pause states
* @param videoEl
* @param videoContainer
*/
togglePlay(videoEl, videoContainer) {
if (videoEl.paused || videoEl.ended) {
videoEl.play();
videoContainer.classList.remove(this.DOM.states.paused);
videoContainer.classList.add(this.DOM.states.playing);
} else {
videoEl.pause();
videoContainer.classList.remove(this.DOM.states.playing);
videoContainer.classList.add(this.DOM.states.paused);
}
}
/**
* Toggle fullscreen mode
* Requested on wrapper so default browser player is not showed
* @param videoContainer
*/
toggleFullScreen(videoContainer) {
if (!document.fullscreenElement) {
videoContainer.requestFullscreen().catch((err) => {
alert(`Error attempting to enable full-screen mode: ${err.message} (${err.name})`);
});
videoContainer.classList.add(this.DOM.states.fullscreen);
} else {
document.exitFullscreen();
videoContainer.classList.remove(this.DOM.states.fullscreen);
}
}
/**
* Toggle video mute
* @param videoEl
* @param volumeInput
* @param videoContainer
*/
toggleMute(videoEl, volumeInput, videoContainer) {
if (volumeInput.value > 0) {
volumeInput.setAttribute("data-previous-volume", volumeInput.value);
volumeInput.value = 0;
this.updateVolume(videoEl, volumeInput, videoContainer);
} else {
volumeInput.value = volumeInput.dataset.previousVolume;
this.updateVolume(videoEl, volumeInput, videoContainer);
}
}
/**
* Get time in minutes and seconds
* @param time
* @returns {{seconds: string, minutes: string}}
*/
formatTime(time) {
let formattedTime = new Date(time * 1000).toISOString().substr(11, 8);
return {
minutes: formattedTime.substr(3, 2),
seconds: formattedTime.substr(6, 2),
};
}
}
/**
* Custom video player
* 1. Settings
* 2. Time input track styles
* 3. Time input thumb styles
* 4. Volume input track styles
* 5. Volume input thumb styles
* 6. Custom video player wrapper styles
* 7. Indicator for playback state
* 8. HTML Video styles
* 9. Controls wrapper
* 10. Progress wrapper
* 11. Progress bar styles - cross browser progress element for displaying elapsed time
* 12. Time range input styles - cross browser range input for navigating in time
* 13. Actions wrapper - contains all action buttons
* 14. Actions left area
* 15. Volume wrapper
* 16. Volume range input styles - cross browser range input for volume
* 17. Action buttons - accessible buttons that do action
* 18. Icon manipulation
* 19. Reset SVG styles inside player
*/
/* 1 */
$time-track-w: 100%;
$time-track-h: 3px;
$time-thumb-d: 12px;
$time-thumb-bg: $electric-blue;
$time-elapsed-bg: $electric-blue;
$volume-track-w: 100%;
$volume-track-h: 3px;
$volume-thumb-d: 11px;
$volume-track-bg: $white;
$volume-thumb-bg: $white;
/* 2 */
@mixin time-track() {
box-sizing: border-box;
border: none;
width: $time-track-w;
height: $time-track-h;
background: transparent;
}
/* 3 */
@mixin time-thumb() {
box-sizing: border-box;
border: none;
width: $time-thumb-d;
height: $time-thumb-d;
border-radius: 50%;
background: $time-thumb-bg;
}
/* 4 */
@mixin volume-track() {
box-sizing: border-box;
border: none;
width: $volume-track-w;
height: $volume-track-h;
background: $volume-track-bg;
}
/* 5 */
@mixin volume-thumb() {
box-sizing: border-box;
border: none;
width: $volume-thumb-d;
height: $volume-thumb-d;
border-radius: 50%;
background: $volume-thumb-bg;
}
/* 6 */
.c-custom-video-player {
position: relative;
overflow: hidden;
color: $white;
font-family: $font-primary;
$font-size: (
$breakpoint-sm: 13px,
$breakpoint-xl: 14px,
$breakpoint-xxl: 16px,
);
@include poly-fluid-sizing("font-size", $font-size);
$line-height: (
$breakpoint-sm: 19px,
$breakpoint-xl: 20px,
$breakpoint-xxl: 22px,
);
@include poly-fluid-sizing("line-height", $line-height);
&:hover {
.c-custom-video-player__controls {
transform: translateY(0%);
}
}
/* 7 */
&__state-indicator {
pointer-events: none;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: rem(50px);
height: rem(50px);
border-radius: 100%;
background-color: $black-50;
display: flex;
justify-content: center;
align-items: center;
opacity: 0;
visibility: hidden;
transition: getTransition();
.is-paused & {
opacity: 1;
visibility: visible;
}
}
/* 8 */
&__video {
width: 100%;
height: 100%;
}
/* 9 */
&__controls {
right: 0;
left: 0;
padding: rem(35px) rem(10px) rem(10px);
position: absolute;
bottom: 0;
transition: getTransition("slide");
background: linear-gradient(180deg, transparent 0%, $black-70 80%);
transform: translateY(100%);
.is-paused & {
transform: translateY(0%);
}
}
/* 10 */
&__progress {
position: relative;
height: $time-track-h;
}
/* 11 */
&__progress-bar {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
border: none;
outline: none;
padding: 0;
margin: 0;
width: 100%;
height: $time-track-h;
pointer-events: none;
position: absolute;
top: 50%;
left: 0;
transform: translateY(-50%);
&::-webkit-progress-bar {
background-color: rgba($white, 0.3);
}
&::-webkit-progress-value {
background: $time-elapsed-bg;
}
&::-moz-progress-bar {
background: $time-elapsed-bg;
}
&::-ms-fill {
background: $time-elapsed-bg;
}
}
/* 12 */
&__time-input {
position: absolute;
top: 50%;
left: 0;
transform: translateY(-50%);
-webkit-appearance: none;
margin: 0;
padding: 0;
width: 100%;
box-shadow: none;
outline: none;
transition: getTransition();
cursor: pointer;
min-height: $time-thumb-d;
background: transparent;
font: inherit;
&::-webkit-slider-thumb {
-webkit-appearance: none;
}
&::-webkit-slider-runnable-track {
@include time-track();
}
&::-moz-range-track {
@include time-track;
}
&::-ms-track {
@include time-track;
}
&::-webkit-slider-thumb {
margin-top: 0.5 * ($time-track-h - $time-thumb-d);
@include time-thumb;
}
&::-moz-range-thumb {
@include time-thumb;
}
&::-ms-thumb {
margin-top: 0;
@include time-thumb;
}
&::-ms-tooltip {
display: none;
}
}
/* 13 */
&__actions {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: rem(15px);
}
/* 14 */
&__actions-left {
display: flex;
align-items: center;
flex: 1 0 0;
}
/* 15 */
&__volume {
display: flex;
align-items: center;
}
/* 16 */
&__volume-input {
-webkit-appearance: none;
margin: 0 rem(10px);
padding: 0;
width: rem(100px);
box-shadow: none;
outline: none;
transition: getTransition();
cursor: pointer;
min-height: $volume-thumb-d;
background: transparent;
font: inherit;
&::-webkit-slider-thumb {
-webkit-appearance: none;
}
&::-webkit-slider-runnable-track {
@include volume-track();
}
&::-moz-range-track {
@include volume-track;
}
&::-ms-track {
@include volume-track;
}
&::-webkit-slider-thumb {
margin-top: 0.5 * ($volume-track-h - $volume-thumb-d);
@include volume-thumb;
}
&::-moz-range-thumb {
@include volume-thumb;
}
&::-ms-thumb {
margin-top: 0;
@include volume-thumb;
}
&::-ms-tooltip {
display: none;
}
}
/* 17 */
&__action-button {
background: transparent;
text-decoration: none;
cursor: pointer;
padding: 5px rem(10px);
outline: none;
border: none;
vertical-align: top;
-webkit-appearance: none;
-moz-appearance: none;
border-radius: 0;
position: relative;
}
/* 18 */
&__play {
display: none;
.is-paused & {
display: inline-block;
}
}
&__pause {
display: inline-block;
.is-paused & {
display: none;
}
}
&__sound {
display: inline-block;
.is-muted & {
display: none;
}
}
&__mute {
display: none;
.is-muted & {
display: inline-block;
}
}
&__full-screen {
display: inline-block;
.is-full-screen & {
display: none;
}
}
&__exit-full-screen {
display: none;
.is-full-screen & {
display: inline-block;
}
}
/* 19 */
svg {
width: rem(12px);
height: rem(12px);
stroke-width: 0;
stroke: currentColor;
fill: currentColor;
color: $white;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment