From e6b4e1268945777c5d07dfca4362a1af23f6d970 Mon Sep 17 00:00:00 2001 From: leonklingele <5585491+leonklingele@users.noreply.github.com> Date: Fri, 16 Aug 2019 23:01:14 +0200 Subject: [PATCH] js: add support for keydown events (#678) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * js: add support for keydown events 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 * js: remove unused dependency 'videojs.hotkeys.min.js' Support for controlling the player volume by scrolling over it is still retained by copying over the relevant code part from the aforementioned library. --- assets/js/player.js | 332 ++++++++++++++---- assets/js/videojs.hotkeys.min.js | 2 - assets/js/watch.js | 44 +-- .../views/components/player_sources.ecr | 1 - src/invidious/views/licenses.ecr | 14 - 5 files changed, 293 insertions(+), 100 deletions(-) delete mode 100644 assets/js/videojs.hotkeys.min.js diff --git a/assets/js/player.js b/assets/js/player.js index 25cbb18b..4a61258c 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,273 @@ 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); + +// Add support for controlling the player volume by scrolling over it. Adapted from +// https://github.com/ctd1500/videojs-hotkeys/blob/bb4a158b2e214ccab87c2e7b95f42bc45c6bfd87/videojs.hotkeys.js#L292-L328 +(function() { + const volumeStep = 0.05; + const enableVolumeScroll = true; + const enableHoverScroll = true; + const doc = document; + const pEl = document.getElementById('player'); + + var volumeHover = false; + var volumeSelector = pEl.querySelector('.vjs-volume-menu-button') || pEl.querySelector('.vjs-volume-panel'); + if (volumeSelector != null) { + volumeSelector.onmouseover = function() { volumeHover = true; }; + volumeSelector.onmouseout = function() { volumeHover = false; }; + } + + var mouseScroll = function mouseScroll(event) { + var activeEl = doc.activeElement; + if (enableHoverScroll) { + // If we leave this undefined then it can match non-existent elements below + activeEl = 0; + } + + // When controls are disabled, hotkeys will be disabled as well + if (player.controls()) { + if (volumeHover) { + if (enableVolumeScroll) { + event = window.event || event; + var delta = Math.max(-1, Math.min(1, (event.wheelDelta || -event.detail))); + event.preventDefault(); + + if (delta == 1) { + increase_volume(volumeStep); + } else if (delta == -1) { + increase_volume(-volumeStep); + } + } + } + } + }; + + player.on('mousewheel', mouseScroll); + player.on("DOMMouseScroll", mouseScroll); +}()); + // Since videojs-share can sometimes be blocked, we defer it until last player.share(shareOptions); diff --git a/assets/js/videojs.hotkeys.min.js b/assets/js/videojs.hotkeys.min.js deleted file mode 100644 index a6cfe6e2..00000000 --- a/assets/js/videojs.hotkeys.min.js +++ /dev/null @@ -1,2 +0,0 @@ -/* videojs-hotkeys v0.2.25 - https://github.com/ctd1500/videojs-hotkeys */ -!function(e,n){"undefined"!=typeof window&&window.videojs?n(window.videojs):"function"==typeof define&&define.amd?define("videojs-hotkeys",["video.js"],function(e){return n(e.default||e)}):"undefined"!=typeof module&&module.exports&&(module.exports=n(require("video.js")))}(0,function(e){"use strict";"undefined"!=typeof window&&(window.videojs_hotkeys={version:"0.2.25"});(e.registerPlugin||e.plugin)("hotkeys",function(n){function t(e){return"function"==typeof s?s(e):s}function r(e){null!=e&&"function"==typeof e.then&&e.then(null,function(e){})}var o=this,u=o.el(),l=document,i={volumeStep:.1,seekStep:5,enableMute:!0,enableVolumeScroll:!0,enableHoverScroll:!1,enableFullscreen:!0,enableNumbers:!0,enableJogStyle:!1,alwaysCaptureHotkeys:!1,enableModifiersForNumbers:!0,enableInactiveFocus:!0,skipInitialFocus:!1,playPauseKey:function(e){return 32===e.which||179===e.which},rewindKey:function(e){return 37===e.which||177===e.which},forwardKey:function(e){return 39===e.which||176===e.which},volumeUpKey:function(e){return 38===e.which},volumeDownKey:function(e){return 40===e.which},muteKey:function(e){return 77===e.which},fullscreenKey:function(e){return 70===e.which},customKeys:{}},c=e.mergeOptions||e.util.mergeOptions,a=(n=c(i,n||{})).volumeStep,s=n.seekStep,m=n.enableMute,f=n.enableVolumeScroll,y=n.enableHoverScroll,v=n.enableFullscreen,d=n.enableNumbers,p=n.enableJogStyle,b=n.alwaysCaptureHotkeys,h=n.enableModifiersForNumbers,w=n.enableInactiveFocus,k=n.skipInitialFocus,S=e.VERSION;u.hasAttribute("tabIndex")||u.setAttribute("tabIndex","-1"),u.style.outline="none",!b&&o.autoplay()||k||o.one("play",function(){u.focus()}),w&&o.on("userinactive",function(){var e=function(){clearTimeout(n)},n=setTimeout(function(){o.off("useractive",e);var n=l.activeElement,t=u.querySelector(".vjs-control-bar");n&&n.parentElement==t&&u.focus()},10);o.one("useractive",e)}),o.on("play",function(){var e=u.querySelector(".iframeblocker");e&&""===e.style.display&&(e.style.display="block",e.style.bottom="39px")});var K=!1,q=u.querySelector(".vjs-volume-menu-button")||u.querySelector(".vjs-volume-panel");null!=q&&(q.onmouseover=function(){K=!0},q.onmouseout=function(){K=!1});var j=function(e){if(y)n=0;else var n=l.activeElement;if(o.controls()&&(b||n==u||n==u.querySelector(".vjs-tech")||n==u.querySelector(".iframeblocker")||n==u.querySelector(".vjs-control-bar")||K)&&f){e=window.event||e;var t=Math.max(-1,Math.min(1,e.wheelDelta||-e.detail));e.preventDefault(),1==t?o.volume(o.volume()+a):-1==t&&o.volume(o.volume()-a)}},F=function(e,t){return n.playPauseKey(e,t)?1:n.rewindKey(e,t)?2:n.forwardKey(e,t)?3:n.volumeUpKey(e,t)?4:n.volumeDownKey(e,t)?5:n.muteKey(e,t)?6:n.fullscreenKey(e,t)?7:void 0};return o.on("keydown",function(e){var i,c,s=e.which,f=e.preventDefault,y=o.duration();if(o.controls()){var w=l.activeElement;if(b||w==u||w==u.querySelector(".vjs-tech")||w==u.querySelector(".vjs-control-bar")||w==u.querySelector(".iframeblocker"))switch(F(e,o)){case 1:f(),b&&e.stopPropagation(),o.paused()?r(o.play()):o.pause();break;case 2:i=!o.paused(),f(),i&&o.pause(),(c=o.currentTime()-t(e))<=0&&(c=0),o.currentTime(c),i&&r(o.play());break;case 3:i=!o.paused(),f(),i&&o.pause(),(c=o.currentTime()+t(e))>=y&&(c=i?y-.001:y),o.currentTime(c),i&&r(o.play());break;case 5:f(),p?(c=o.currentTime()-1,o.currentTime()<=1&&(c=0),o.currentTime(c)):o.volume(o.volume()-a);break;case 4:f(),p?((c=o.currentTime()+1)>=y&&(c=y),o.currentTime(c)):o.volume(o.volume()+a);break;case 6:m&&o.muted(!o.muted());break;case 7:v&&(o.isFullscreen()?o.exitFullscreen():o.requestFullscreen());break;default:if((s>47&&s<59||s>95&&s<106)&&(h||!(e.metaKey||e.ctrlKey||e.altKey))&&d){var k=48;s>95&&(k=96);var S=s-k;f(),o.currentTime(o.duration()*S*.1)}for(var K in n.customKeys){var q=n.customKeys[K];q&&q.key&&q.handler&&q.key(e)&&(f(),q.handler(o,n,e))}}}}),o.on("dblclick",function(e){if(null!=S&&S<="7.1.0"&&o.controls()){var n=e.relatedTarget||e.toElement||l.activeElement;n!=u&&n!=u.querySelector(".vjs-tech")&&n!=u.querySelector(".iframeblocker")||v&&(o.isFullscreen()?o.exitFullscreen():o.requestFullscreen())}}),o.on("mousewheel",j),o.on("DOMMouseScroll",j),this})}); \ No newline at end of file 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'); diff --git a/src/invidious/views/components/player_sources.ecr b/src/invidious/views/components/player_sources.ecr index 003d2c3a..d950e0da 100644 --- a/src/invidious/views/components/player_sources.ecr +++ b/src/invidious/views/components/player_sources.ecr @@ -6,7 +6,6 @@ - diff --git a/src/invidious/views/licenses.ecr b/src/invidious/views/licenses.ecr index 7cffb7fc..aae8bb19 100644 --- a/src/invidious/views/licenses.ecr +++ b/src/invidious/views/licenses.ecr @@ -135,20 +135,6 @@ -