summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorleonklingele <5585491+leonklingele@users.noreply.github.com>2019-08-16 23:01:14 +0200
committerOmar Roth <omarroth@protonmail.com>2019-08-16 16:01:14 -0500
commite6b4e1268945777c5d07dfca4362a1af23f6d970 (patch)
treed2ec039c99ef9ed956e486fc8b7e29cab428e998
parent7eaac995bd549b03f53930fbbfc5d77e2b051362 (diff)
downloadinvidious-e6b4e1268945777c5d07dfca4362a1af23f6d970.tar.gz
invidious-e6b4e1268945777c5d07dfca4362a1af23f6d970.tar.bz2
invidious-e6b4e1268945777c5d07dfca4362a1af23f6d970.zip
js: add support for keydown events (#678)
* 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.
-rw-r--r--assets/js/player.js332
-rw-r--r--assets/js/videojs.hotkeys.min.js2
-rw-r--r--assets/js/watch.js40
-rw-r--r--src/invidious/views/components/player_sources.ecr1
-rw-r--r--src/invidious/views/licenses.ecr14
5 files changed, 291 insertions, 98 deletions
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: "<iframe id='ivplayer' type='text/html' width='640' height='360' src='" + embed_url + "' frameborder='0'></iframe>"
}
-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 continue_autoplay(event) {
- if (event.target.checked) {
- player.on('ended', function () {
- var url = new URL('https://example.com/watch?v=' + video_data.next_video);
+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.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.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.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);
- }
+ 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);
+}
- url.searchParams.set('continue', '1');
- location.assign(url.pathname + url.search);
+function continue_autoplay(event) {
+ if (event.target.checked) {
+ player.on('ended', function () {
+ 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 @@
<script src="/js/video.min.js?v=<%= ASSET_COMMIT %>"></script>
<script src="/js/videojs-contrib-quality-levels.min.js?v=<%= ASSET_COMMIT %>"></script>
<script src="/js/videojs-http-source-selector.min.js?v=<%= ASSET_COMMIT %>"></script>
-<script src="/js/videojs.hotkeys.min.js?v=<%= ASSET_COMMIT %>"></script>
<script src="/js/videojs-markers.min.js?v=<%= ASSET_COMMIT %>"></script>
<script src="/js/videojs-share.min.js?v=<%= ASSET_COMMIT %>"></script>
<script src="/js/videojs-vtt-thumbnails.min.js?v=<%= ASSET_COMMIT %>"></script>
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
@@ -137,20 +137,6 @@
<tr>
<td>
- <a href="/js/videojs.hotkeys.min.js?v=<%= ASSET_COMMIT %>">videojs.hotkeys.min.js</a>
- </td>
-
- <td>
- <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache-2.0-only</a>
- </td>
-
- <td>
- <a href="https://github.com/ctd1500/videojs-hotkeys"><%= translate(locale, "source") %></a>
- </td>
- </tr>
-
- <tr>
- <td>
<a href="/js/videojs-http-source-selector.min.js?v=<%= ASSET_COMMIT %>">videojs-http-source-selector.min.js</a>
</td>