├── poster.jpg ├── video.mp4 ├── custom-video-player-ss.jpg ├── index.js ├── index.html ├── styles.css └── video.js /poster.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Navaneethakrishnan2004/Video-Player/HEAD/poster.jpg -------------------------------------------------------------------------------- /video.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Navaneethakrishnan2004/Video-Player/HEAD/video.mp4 -------------------------------------------------------------------------------- /custom-video-player-ss.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Navaneethakrishnan2004/Video-Player/HEAD/custom-video-player-ss.jpg -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const video = new Video({ 4 | wrapperID: "video-wrapper", 5 | videoSrc: "static/video.mp4", 6 | posterSrc: "static/poster.jpg", 7 | absolute: true, 8 | hideControlsOnPlay: true, 9 | progressColor: "white" 10 | }); 11 | 12 | console.log(video); 13 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Custom Video Player 8 | 9 | 10 |
11 |

Custom Video Player

12 |
13 |
14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /styles.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | .m-video { 6 | position: relative; 7 | display: flex; 8 | flex-direction: column; 9 | justify-content: center; 10 | align-items: center; 11 | 12 | --progress-color: red; 13 | } 14 | 15 | .m-video video { 16 | width: 100%; 17 | max-width: 100%; 18 | } 19 | 20 | .m-video video::-webkit-media-controls-enclosure, 21 | .m-video video::-webkit-media-controls { 22 | display: none !important; 23 | } 24 | 25 | .m-video input[type="range"] { 26 | height: 4px; 27 | -webkit-appearance: none; 28 | outline: none; 29 | background: transparent; 30 | overflow: hidden; 31 | cursor: pointer; 32 | border-radius: 4px; 33 | } 34 | 35 | .m-video .v-controls { 36 | background: rgba(0, 0, 0, .6); 37 | padding: .5rem; 38 | color: #fff; 39 | display: flex; 40 | flex-direction: column; 41 | flex-wrap: nowrap; 42 | width: 100%; 43 | } 44 | 45 | .m-video .v-controls.--fs-abs, 46 | .m-video .v-controls.--absolute { 47 | position: absolute; 48 | width: 100%; 49 | left: 0; 50 | bottom: 0; 51 | } 52 | 53 | .m-video.playing .v-controls.--absolute.--auto-hide, 54 | .m-video .v-controls:not(.--absolute).--fs-abs { 55 | opacity: 0; 56 | transition: .3s; 57 | } 58 | 59 | .m-video .v-controls:hover.--absolute.--auto-hide, 60 | .m-video .v-controls:hover.--fs-abs { 61 | opacity: 1; 62 | } 63 | 64 | 65 | .m-video .v-controls svg { 66 | width: 24px; 67 | height: 24px; 68 | fill: none; 69 | stroke: #fff; 70 | stroke-width: 2; 71 | stroke-linecap: round; 72 | stroke-linejoin: round; 73 | } 74 | 75 | .m-video button { 76 | outline: none; 77 | background: transparent; 78 | border: none; 79 | cursor: pointer; 80 | width: 30px; 81 | height: 30px; 82 | padding: 0; 83 | margin: 0; 84 | display: inline-flex; 85 | justify-content: center; 86 | align-items: center; 87 | box-shadow: none; 88 | } 89 | 90 | .m-video button:focus, 91 | .m-video button::-moz-focus-inner { 92 | border: 0; 93 | outline: none; 94 | } 95 | 96 | /* Slider in Chrome, Firefox, and Opera */ 97 | .m-video input[type="range"]::-webkit-slider-runnable-track { 98 | background: rgba(255, 255, 255, 0.6); 99 | height: 4px; 100 | border-radius: 4px; 101 | border: 0; 102 | width: 100%; 103 | } 104 | 105 | .m-video input[type="range"]::-moz-range-track { 106 | background: rgba(255, 255, 255, 0.6); 107 | height: 4px; 108 | border-radius: 4px; 109 | border: 0; 110 | width: 100%; 111 | } 112 | 113 | .m-video input[type="range"]::-ms-track { 114 | background: rgba(255, 255, 255, 0.6); 115 | height: 4px; 116 | border-radius: 4px; 117 | border: 0; 118 | width: 100%; 119 | } 120 | 121 | .m-video input[type="range"]::-ms-fill-lower { 122 | background: var(--progress-color, red); 123 | } 124 | 125 | .m-video input[type="range"]::-moz-range-progress { 126 | background: var(--progress-color, red); 127 | } 128 | 129 | .m-video input[type="range"]::-webkit-slider-thumb { 130 | -webkit-appearance: none; 131 | background: var(--progress-color, red); 132 | width: 4px; 133 | height: 4px; 134 | margin-top: 0; 135 | border-radius: 50%; 136 | cursor: pointer; 137 | } 138 | 139 | .m-video input[type="range"]::-moz-range-thumb { 140 | -moz-appearance: none; 141 | background: var(--progress-color, red); 142 | width: 4px; 143 | height: 4px; 144 | margin-top: 0; 145 | border-radius: 50%; 146 | cursor: pointer; 147 | } 148 | 149 | .m-video input[type="range"]::-ms-thumb { 150 | background: var(--progress-color, red); 151 | width: 4px; 152 | height: 4px; 153 | margin-top: 0; 154 | border-radius: 50%; 155 | cursor: pointer; 156 | } 157 | 158 | .m-video input[type="range"]::-webkit-slider-thumb { 159 | box-shadow: -1200px 0 0 1200px var(--progress-color, red); 160 | outline: none; 161 | border: 0; 162 | } 163 | 164 | .m-video .v-controls__btns { 165 | display: flex; 166 | flex-wrap: nowrap; 167 | justify-content: space-between; 168 | align-items: center; 169 | } 170 | 171 | .m-video .v-controls__btns .v-controls__btns__sound { 172 | width: 30%; 173 | flex: 0 0 30%; 174 | display: flex; 175 | justify-content: flex-start; 176 | align-items: center; 177 | } 178 | 179 | .m-video .v-controls__btns .v-controls__btns__sound input[type="range"] { 180 | max-width: 90px; 181 | } 182 | 183 | .m-video .v-controls__btns .v-controls__btns_play { 184 | width: 40%; 185 | flex: 0 0 40%; 186 | display: flex; 187 | justify-content: center; 188 | align-items: center; 189 | } 190 | 191 | .m-video .v-controls__btns .v-controls__btns_fs { 192 | width: 30%; 193 | flex: 0 0 30%; 194 | display: flex; 195 | justify-content: flex-end; 196 | } 197 | 198 | .m-video .v-controls .v-controls__timing { 199 | display: flex; 200 | justify-content: center; 201 | align-items: center; 202 | margin-top: .5rem; 203 | } 204 | 205 | .m-video .v-controls .v-controls__timing input[type="range"] { 206 | width: 100%; 207 | margin: 0 1rem; 208 | } 209 | 210 | .m-video .v-controls .v-controls__timing span { 211 | text-align: center; 212 | width: 58px; 213 | display: inline-block; 214 | } 215 | -------------------------------------------------------------------------------- /video.js: -------------------------------------------------------------------------------- 1 | const defaultOptions = { 2 | wrapperID: "video-wrapper", 3 | videoSrc: null, 4 | posterSrc: null, 5 | absolute: false, 6 | hideControlsOnPlay: true, 7 | progressColor: "red" 8 | }; 9 | 10 | function Video(options = defaultOptions) { 11 | 12 | // check options and its validity 13 | if (!options || typeof options !== "object" || !options.wrapperID) 14 | throw new Error("Options not passed to the Video functions."); 15 | this.options = Object.assign(defaultOptions, options); 16 | 17 | // check the existence of video wrapper 18 | let wrapper = document.getElementById(this.options.wrapperID); 19 | if (!wrapper && (!wrapper instanceof Element || !wrapper instanceof HTMLDocument)) 20 | throw new Error("Wrapper must be a valid node."); 21 | this.wrapper = wrapper; 22 | 23 | // initialize the video 24 | this.init(); 25 | } 26 | 27 | Video.prototype.init = function () { 28 | if (this.video) 29 | return; 30 | 31 | // init the icons 32 | this.icons = { 33 | stop: '', 34 | play: '', 35 | pause: '', 36 | muted: '', 37 | volume_0: '', 38 | volume_1: '', 39 | volume_2: '', 40 | fullscreen: '', 41 | exitFullscreen: '', 42 | }; 43 | 44 | // add special class to the wrapper to active all styles 45 | this.wrapper.classList.add("m-video"); 46 | 47 | // check the existence of video tag in wrapper. 48 | this.video = this.wrapper.querySelector("video"); 49 | if (!this.video) { 50 | if (!this.options.videoSrc) 51 | throw new Error("Can not find video tag in the wrapper or videoSrc in options"); 52 | 53 | // create video tag with source and append it to wrapper 54 | this.generateVideoTag(); 55 | } 56 | 57 | // se default controls to false; 58 | this.video.controls = false; 59 | 60 | // create custom controls element and append it to wrapper 61 | this.generateControls(); 62 | 63 | // initialize the listeners. 64 | this.initListeners(); 65 | }; 66 | 67 | Video.prototype.generateVideoTag = function () { 68 | if (this.video) 69 | return; 70 | 71 | this.video = document.createElement("video"); 72 | this.video.src = this.options.videoSrc; 73 | this.video.poster = this.options.posterSrc; 74 | 75 | this.wrapper.append(this.video); 76 | }; 77 | 78 | Video.prototype.generateControls = function () { 79 | if (!this.video || this.controls) 80 | return; 81 | 82 | this.controls = document.createElement("div"); 83 | this.controls.classList.add("v-controls"); 84 | if (this.options.absolute) 85 | this.controls.classList.add("--absolute"); 86 | 87 | if (this.options.hideControlsOnPlay) 88 | this.controls.classList.add("--auto-hide"); 89 | 90 | this.controls.innerHTML = ` 91 |
92 |
93 | 94 | 95 |
96 |
97 | 98 | 99 |
100 |
101 | 102 |
103 |
104 |
105 | 00:00 106 | 107 | 00:00 108 |
`; 109 | 110 | 111 | this.elapsedTimeSpan = this.controls.querySelector(".v-controls__timing .elapsed-time"); 112 | this.remainingTimeSpan = this.controls.querySelector(".v-controls__timing .remaining-time"); 113 | 114 | this.wrapper.style.setProperty('--progress-color', this.options.progressColor); 115 | this.wrapper.append(this.controls); 116 | 117 | this.videoPlayBtn = this.wrapper.querySelector(".v-controls__btns #video-play"); 118 | this.videoStopBtn = this.wrapper.querySelector(".v-controls__btns #video-stop"); 119 | this.soundToggleBtn = this.wrapper.querySelector(".v-controls__btns #sound-toggle"); 120 | this.videoFullscreenBtn = this.wrapper.querySelector(".v-controls__btns #video-fs"); 121 | this.videoProgress = this.wrapper.querySelector(".v-controls__timing input[type=range]"); 122 | this.soundRange = this.wrapper.querySelector(".v-controls__btns__sound input[type=range]"); 123 | 124 | }; 125 | 126 | Video.prototype.initListeners = function () { 127 | if (!this.video || !this.controls) 128 | return; 129 | 130 | this.video.addEventListener("loadeddata", this.calcProgress.bind(this)); 131 | this.video.addEventListener("play", this.onPlay.bind(this)); 132 | this.video.addEventListener("pause", this.onPause.bind(this)); 133 | this.video.addEventListener("timeupdate", this.calcProgress.bind(this)); 134 | 135 | this.videoPlayBtn.addEventListener("click", this.togglePlay.bind(this)); 136 | this.videoStopBtn.addEventListener("click", this.stop.bind(this)); 137 | this.soundToggleBtn.addEventListener("click", this.soundToggle.bind(this)); 138 | this.videoFullscreenBtn.addEventListener("click", this.toggleFullscreen.bind(this)); 139 | this.videoProgress.addEventListener("change", this.setCurrentTime.bind(this)); 140 | this.soundRange.addEventListener("change", this.updateVolume.bind(this)); 141 | 142 | }; 143 | 144 | Video.prototype.onPlay = function () { 145 | this.wrapper.classList.add("playing"); 146 | this.videoPlayBtn.innerHTML = this.icons.pause; 147 | }; 148 | 149 | Video.prototype.onPause = function () { 150 | this.wrapper.classList.remove("playing"); 151 | this.videoPlayBtn.innerHTML = this.icons.play; 152 | }; 153 | 154 | Video.prototype.togglePlay = function () { 155 | if (!this.video) 156 | return; 157 | 158 | if (this.video.paused) { 159 | this.video.play(); 160 | } else { 161 | this.video.pause(); 162 | } 163 | }; 164 | 165 | Video.prototype.stop = function () { 166 | if (!this.video) 167 | return; 168 | 169 | this.video.currentTime = 0; 170 | this.video.pause(); 171 | }; 172 | 173 | Video.prototype.soundToggle = function () { 174 | if (!this.video) 175 | return; 176 | 177 | this.video.muted = !this.video.muted; 178 | 179 | // handle sound range animation when mute clicked 180 | const animateVolumeRange = () => { 181 | if (this.video.muted) { 182 | if (+this.soundRange.value > 0) { 183 | this.soundRange.value = +this.soundRange.value - .1; 184 | requestAnimationFrame(animateVolumeRange) 185 | } 186 | } else { 187 | if (+this.soundRange.value < this.volumeMemorize) { 188 | this.soundRange.value = +this.soundRange.value + .1; 189 | requestAnimationFrame(animateVolumeRange) 190 | } 191 | } 192 | }; 193 | 194 | if (this.video.muted) { 195 | this.soundToggleBtn.innerHTML = this.icons.muted; 196 | this.volumeMemorize = this.soundRange.value; 197 | } else { 198 | this.soundToggleBtn.innerHTML = this.icons.volume_2; 199 | } 200 | 201 | animateVolumeRange(); 202 | 203 | }; 204 | 205 | Video.prototype.updateVolume = function (e) { 206 | if (!this.video || !e) 207 | return; 208 | 209 | this.video.volume = e.target.value; 210 | if (+e.target.value === 0) { 211 | this.soundToggleBtn.innerHTML = this.icons.volume_0; 212 | } else if (+e.target.value < .5) { 213 | this.soundToggleBtn.innerHTML = this.icons.volume_1; 214 | } else { 215 | this.soundToggleBtn.innerHTML = this.icons.volume_2; 216 | } 217 | }; 218 | 219 | Video.prototype.toggleFullscreen = function () { 220 | if (!this.video) 221 | return; 222 | 223 | if (!this.fullscreen) { 224 | this.fullscreen = true; 225 | this.videoFullscreenBtn.innerHTML = this.icons.exitFullscreen; 226 | this.controls.classList.add("--fs-abs"); 227 | 228 | if (this.wrapper.requestFullscreen) { 229 | this.wrapper.requestFullscreen(); 230 | } else if (this.wrapper.mozRequestFullScreen) { /* Firefox */ 231 | this.wrapper.mozRequestFullScreen(); 232 | } else if (this.wrapper.webkitRequestFullscreen) { /* Chrome, Safari and Opera */ 233 | this.wrapper.webkitRequestFullscreen(); 234 | } else if (this.wrapper.msRequestFullscreen) { /* IE/Edge */ 235 | this.wrapper.msRequestFullscreen(); 236 | } 237 | } else { 238 | this.fullscreen = false; 239 | this.videoFullscreenBtn.innerHTML = this.icons.fullscreen; 240 | this.controls.classList.remove("--fs-abs"); 241 | if (document.exitFullscreen) { 242 | document.exitFullscreen(); 243 | } else if (document.mozCancelFullScreen) { /* Firefox */ 244 | document.mozCancelFullScreen(); 245 | } else if (document.webkitExitFullscreen) { /* Chrome, Safari and Opera */ 246 | document.webkitExitFullscreen(); 247 | } else if (document.msExitFullscreen) { /* IE/Edge */ 248 | document.msExitFullscreen(); 249 | } 250 | } 251 | }; 252 | 253 | Video.prototype.setCurrentTime = function (e) { 254 | if (!this.video || !e) 255 | return; 256 | 257 | const rangeValue = e.target.value || 0; 258 | 259 | this.video.currentTime = (rangeValue * this.video.duration) / 100; 260 | }; 261 | 262 | // calculate the real time progress for the video and 263 | // set the value of times spans 264 | Video.prototype.calcProgress = function () { 265 | if (!this.video) 266 | return; 267 | 268 | this.videoProgress.value = (this.video.currentTime / this.video.duration) * 100; 269 | 270 | this.elapsedTimeSpan.innerHTML = secToTimeStr(this.video.currentTime); 271 | this.remainingTimeSpan.innerHTML = secToTimeStr(this.video.duration - this.video.currentTime); 272 | }; 273 | 274 | /** 275 | * Convert numbers in second to time string like 00:00 276 | * 277 | * @param seconds 278 | * @returns {string} 279 | */ 280 | function secToTimeStr(seconds) { 281 | 282 | let timeInHour = Math.floor(seconds / 3600); 283 | let timeInMin = Math.floor((seconds % 3600) / 60); 284 | let timeInSec = Math.floor(seconds % 60); 285 | 286 | if (timeInHour < 10) 287 | timeInHour = `0${timeInHour}`; 288 | 289 | if (timeInMin < 10) 290 | timeInMin = `0${timeInMin}`; 291 | 292 | if (timeInSec < 10) 293 | timeInSec = `0${timeInSec}`; 294 | 295 | let timeStr = `${timeInMin}:${timeInSec}`; 296 | if (parseInt(timeInHour)) 297 | timeStr = `${timeInHour}:${timeStr}`; 298 | 299 | return timeStr; 300 | } 301 | --------------------------------------------------------------------------------