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:
parent
7eaac995bd
commit
e6b4e12689
5 changed files with 293 additions and 100 deletions
|
@ -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);
|
||||
|
|
2
assets/js/videojs.hotkeys.min.js
vendored
2
assets/js/videojs.hotkeys.min.js
vendored
|
@ -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})});
|
|
@ -73,9 +73,7 @@ if (continue_button) {
|
|||
continue_button.onclick = continue_autoplay;
|
||||
}
|
||||
|
||||
function continue_autoplay(event) {
|
||||
if (event.target.checked) {
|
||||
player.on('ended', function () {
|
||||
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) {
|
||||
|
@ -96,6 +94,12 @@ function continue_autoplay(event) {
|
|||
|
||||
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');
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -135,20 +135,6 @@
|
|||
</td>
|
||||
</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>
|
||||
<td>
|
||||
<a href="/js/videojs-http-source-selector.min.js?v=<%= ASSET_COMMIT %>">videojs-http-source-selector.min.js</a>
|
||||
|
|
Loading…
Reference in a new issue