From a7cd2e51557cceb1b0895a4600d92333c448dc5d Mon Sep 17 00:00:00 2001 From: Leon Klingele Date: Thu, 8 Aug 2019 10:55:19 +0200 Subject: [PATCH] js: add support for keydown events MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This will modify the player behavior even if the player element is unfocused. Based on the YouTube key bindings, allow to - toggle playback with space and 'k' key - increase and decrease player volume with up / down arrow key - mute and unmute player with 'm' key - jump forwards and backwards by 5 seconds with right / left arrow key - jump forwards and backwards by 10 seconds with 'l' / 'j' key - set video progress with number keys 0–9 - toggle captions with 'c' key - toggle fullscreen mode with 'f' key - play next video with 'N' key - increase and decrease playback speed with '>' / '<' key --- assets/js/player.js | 287 ++++++++++++++++++++++++++++++++++---------- assets/js/watch.js | 44 ++++--- 2 files changed, 248 insertions(+), 83 deletions(-) diff --git a/assets/js/player.js b/assets/js/player.js index 25cbb18b..c8377902 100644 --- a/assets/js/player.js +++ b/assets/js/player.js @@ -38,69 +38,7 @@ var shareOptions = { embedCode: "" } -var player = videojs('player', options, function () { - this.hotkeys({ - volumeStep: 0.1, - seekStep: 5, - enableModifiersForNumbers: false, - enableHoverScroll: true, - customKeys: { - // Toggle play with K Key - play: { - key: function (e) { - return e.which === 75; - }, - handler: function (player, options, e) { - if (player.paused()) { - player.play(); - } else { - player.pause(); - } - } - }, - // Go backward 10 seconds - backward: { - key: function (e) { - return e.which === 74; - }, - handler: function (player, options, e) { - player.currentTime(player.currentTime() - 10); - } - }, - // Go forward 10 seconds - forward: { - key: function (e) { - return e.which === 76; - }, - handler: function (player, options, e) { - player.currentTime(player.currentTime() + 10); - } - }, - // Increase speed - increase_speed: { - key: function (e) { - return (e.which === 190 && e.shiftKey); - }, - handler: function (player, _, e) { - size = options.playbackRates.length; - index = options.playbackRates.indexOf(player.playbackRate()); - player.playbackRate(options.playbackRates[(index + 1) % size]); - } - }, - // Decrease speed - decrease_speed: { - key: function (e) { - return (e.which === 188 && e.shiftKey); - }, - handler: function (player, _, e) { - size = options.playbackRates.length; - index = options.playbackRates.indexOf(player.playbackRate()); - player.playbackRate(options.playbackRates[(size + index - 1) % size]); - } - } - } - }); -}); +var player = videojs('player', options); if (location.pathname.startsWith('/embed/')) { player.overlay({ @@ -254,5 +192,228 @@ if (!video_data.params.listen && video_data.params.annotations) { xhr.send(); } +function increase_volume(delta) { + const curVolume = player.volume(); + let newVolume = curVolume + delta; + if (newVolume > 1) { + newVolume = 1; + } else if (newVolume < 0) { + newVolume = 0; + } + player.volume(newVolume); +} + +function toggle_muted() { + const isMuted = player.muted(); + player.muted(!isMuted); +} + +function skip_seconds(delta) { + const duration = player.duration(); + const curTime = player.currentTime(); + let newTime = curTime + delta; + if (newTime > duration) { + newTime = duration; + } else if (newTime < 0) { + newTime = 0; + } + player.currentTime(newTime); +} + +function set_time_percent(percent) { + const duration = player.duration(); + const newTime = duration * (percent / 100); + player.currentTime(newTime); +} + +function toggle_play() { + if (player.paused()) { + player.play(); + } else { + player.pause(); + } +} + +const toggle_captions = (function() { + let toggledTrack = null; + const onChange = function(e) { + toggledTrack = null; + }; + const bindChange = function(onOrOff) { + player.textTracks()[onOrOff]('change', onChange); + }; + // Wrapper function to ignore our own emitted events and only listen + // to events emitted by Video.js on click on the captions menu items. + const setMode = function(track, mode) { + bindChange('off'); + track.mode = mode; + window.setTimeout(function() { + bindChange('on'); + }, 0); + }; + bindChange('on'); + return function() { + if (toggledTrack !== null) { + if (toggledTrack.mode !== 'showing') { + setMode(toggledTrack, 'showing'); + } else { + setMode(toggledTrack, 'disabled'); + } + toggledTrack = null; + return; + } + + // Used as a fallback if no captions are currently active. + // TODO: Make this more intelligent by e.g. relying on browser language. + let fallbackCaptionsTrack = null; + + const tracks = player.textTracks(); + for (let i = 0; i < tracks.length; i++) { + const track = tracks[i]; + if (track.kind !== 'captions') { + continue; + } + + if (fallbackCaptionsTrack === null) { + fallbackCaptionsTrack = track; + } + if (track.mode === 'showing') { + setMode(track, 'disabled'); + toggledTrack = track; + return; + } + } + + // Fallback if no captions are currently active. + if (fallbackCaptionsTrack !== null) { + setMode(fallbackCaptionsTrack, 'showing'); + toggledTrack = fallbackCaptionsTrack; + } + }; +})(); + +function toggle_fullscreen() { + if (player.isFullscreen()) { + player.exitFullscreen(); + } else { + player.requestFullscreen(); + } +} + +function increase_playback_rate(steps) { + const maxIndex = options.playbackRates.length - 1; + const curIndex = options.playbackRates.indexOf(player.playbackRate()); + let newIndex = curIndex + steps; + if (newIndex > maxIndex) { + newIndex = maxIndex; + } else if (newIndex < 0) { + newIndex = 0; + } + player.playbackRate(options.playbackRates[newIndex]); +} + +window.addEventListener('keydown', e => { + if (e.target.tagName.toLowerCase() === 'input') { + // Ignore input when focus is on certain elements, e.g. form fields. + return; + } + // See https://github.com/ctd1500/videojs-hotkeys/blob/bb4a158b2e214ccab87c2e7b95f42bc45c6bfd87/videojs.hotkeys.js#L310-L313 + const isPlayerFocused = false + || e.target === document.querySelector('.video-js') + || e.target === document.querySelector('.vjs-tech') + || e.target === document.querySelector('.iframeblocker') + || e.target === document.querySelector('.vjs-control-bar') + ; + let action = null; + + const code = e.keyCode; + const key = e.key; + switch (key) { + case ' ': + case 'k': + action = toggle_play; + break; + + case 'ArrowUp': + if (isPlayerFocused) { + action = increase_volume.bind(this, 0.1); + } + break; + case 'ArrowDown': + if (isPlayerFocused) { + action = increase_volume.bind(this, -0.1); + } + break; + + case 'm': + action = toggle_muted; + break; + + case 'ArrowRight': + action = skip_seconds.bind(this, 5); + break; + case 'ArrowLeft': + action = skip_seconds.bind(this, -5); + break; + case 'l': + action = skip_seconds.bind(this, 10); + break; + case 'j': + action = skip_seconds.bind(this, -10); + break; + + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + const percent = (code - 48) * 10; + action = set_time_percent.bind(this, percent); + break; + + case 'c': + action = toggle_captions; + break; + case 'f': + action = toggle_fullscreen; + break; + + case 'N': + action = next_video; + break; + case 'P': + // TODO: Add support to play back previous video. + break; + + case '.': + // TODO: Add support for next-frame-stepping. + break; + case ',': + // TODO: Add support for previous-frame-stepping. + break; + + case '>': + action = increase_playback_rate.bind(this, 1); + break; + case '<': + action = increase_playback_rate.bind(this, -1); + break; + + default: + console.info('Unhandled key down event: %s:', key, e); + break; + } + + if (action) { + e.preventDefault(); + action(); + } +}, false); + // Since videojs-share can sometimes be blocked, we defer it until last player.share(shareOptions); diff --git a/assets/js/watch.js b/assets/js/watch.js index 05e3b7e2..0f3e8123 100644 --- a/assets/js/watch.js +++ b/assets/js/watch.js @@ -73,29 +73,33 @@ if (continue_button) { continue_button.onclick = continue_autoplay; } +function next_video() { + var url = new URL('https://example.com/watch?v=' + video_data.next_video); + + if (video_data.params.autoplay || video_data.params.continue_autoplay) { + url.searchParams.set('autoplay', '1'); + } + + if (video_data.params.listen !== video_data.preferences.listen) { + url.searchParams.set('listen', video_data.params.listen); + } + + if (video_data.params.speed !== video_data.preferences.speed) { + url.searchParams.set('speed', video_data.params.speed); + } + + if (video_data.params.local !== video_data.preferences.local) { + url.searchParams.set('local', video_data.params.local); + } + + url.searchParams.set('continue', '1'); + location.assign(url.pathname + url.search); +} + function continue_autoplay(event) { if (event.target.checked) { player.on('ended', function () { - var url = new URL('https://example.com/watch?v=' + video_data.next_video); - - if (video_data.params.autoplay || video_data.params.continue_autoplay) { - url.searchParams.set('autoplay', '1'); - } - - if (video_data.params.listen !== video_data.preferences.listen) { - url.searchParams.set('listen', video_data.params.listen); - } - - if (video_data.params.speed !== video_data.preferences.speed) { - url.searchParams.set('speed', video_data.params.speed); - } - - if (video_data.params.local !== video_data.preferences.local) { - url.searchParams.set('local', video_data.params.local); - } - - url.searchParams.set('continue', '1'); - location.assign(url.pathname + url.search); + next_video(); }); } else { player.off('ended');