Skip to content

Commit

Permalink
js: add support for keydown events (#678)
Browse files Browse the repository at this point in the history
* 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.
  • Loading branch information
leonklingele authored and omarroth committed Aug 16, 2019
1 parent 7eaac99 commit e6b4e12
Show file tree
Hide file tree
Showing 5 changed files with 291 additions and 98 deletions.
332 changes: 269 additions & 63 deletions assets/js/player.js
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -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 changes: 0 additions & 2 deletions assets/js/videojs.hotkeys.min.js

This file was deleted.

Loading

0 comments on commit e6b4e12

Please sign in to comment.