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.
This commit is contained in:
leonklingele 2019-08-16 23:01:14 +02:00 committed by Omar Roth
parent 7eaac995bd
commit e6b4e12689
5 changed files with 293 additions and 100 deletions

View file

@ -38,69 +38,7 @@ var shareOptions = {
embedCode: "<iframe id='ivplayer' type='text/html' width='640' height='360' src='" + embed_url + "' frameborder='0'></iframe>" embedCode: "<iframe id='ivplayer' type='text/html' width='640' height='360' src='" + embed_url + "' frameborder='0'></iframe>"
} }
var player = videojs('player', options, function () { var player = videojs('player', options);
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]);
}
}
}
});
});
if (location.pathname.startsWith('/embed/')) { if (location.pathname.startsWith('/embed/')) {
player.overlay({ player.overlay({
@ -254,5 +192,273 @@ if (!video_data.params.listen && video_data.params.annotations) {
xhr.send(); 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 // Since videojs-share can sometimes be blocked, we defer it until last
player.share(shareOptions); player.share(shareOptions);

View file

@ -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})});

View file

@ -73,9 +73,7 @@ if (continue_button) {
continue_button.onclick = continue_autoplay; continue_button.onclick = continue_autoplay;
} }
function continue_autoplay(event) { function next_video() {
if (event.target.checked) {
player.on('ended', function () {
var url = new URL('https://example.com/watch?v=' + video_data.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) { if (video_data.params.autoplay || video_data.params.continue_autoplay) {
@ -96,6 +94,12 @@ function continue_autoplay(event) {
url.searchParams.set('continue', '1'); url.searchParams.set('continue', '1');
location.assign(url.pathname + url.search); location.assign(url.pathname + url.search);
}
function continue_autoplay(event) {
if (event.target.checked) {
player.on('ended', function () {
next_video();
}); });
} else { } else {
player.off('ended'); player.off('ended');

View file

@ -6,7 +6,6 @@
<script src="/js/video.min.js?v=<%= ASSET_COMMIT %>"></script> <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-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-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-markers.min.js?v=<%= ASSET_COMMIT %>"></script>
<script src="/js/videojs-share.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> <script src="/js/videojs-vtt-thumbnails.min.js?v=<%= ASSET_COMMIT %>"></script>

View file

@ -135,20 +135,6 @@
</td> </td>
</tr> </tr>
<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> <tr>
<td> <td>
<a href="/js/videojs-http-source-selector.min.js?v=<%= ASSET_COMMIT %>">videojs-http-source-selector.min.js</a> <a href="/js/videojs-http-source-selector.min.js?v=<%= ASSET_COMMIT %>">videojs-http-source-selector.min.js</a>