From 9e1b1cbdd8a58b5b260e16f71044ff6321cf9f4c Mon Sep 17 00:00:00 2001 From: Preston Date: Thu, 20 Feb 2020 15:58:21 -0500 Subject: [PATCH] Add Keyboard Shortcuts to ft-video-player component --- package-lock.json | 56 +++-- package.json | 2 +- .../ft-video-player/ft-video-player.js | 235 ++++++++++++++++++ src/renderer/store/modules/settings.js | 4 + src/renderer/views/Watch/Watch.js | 23 +- 5 files changed, 296 insertions(+), 24 deletions(-) diff --git a/package-lock.json b/package-lock.json index dc7c6ca1..e87c8576 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3692,7 +3692,8 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.0.0.tgz", "integrity": "sha512-OElxJ1lUSinuoUnkpOgLmxp0DC4ytEhODEL6QJU0NpxE/mI4rUSh8h1P1Wkvfi3xQEBcxXR2gBIPNYNuaFcAbQ==", - "dev": true + "dev": true, + "optional": true }, "boxen": { "version": "4.2.0", @@ -7476,7 +7477,8 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "aproba": { "version": "1.2.0", @@ -7497,12 +7499,14 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -7517,17 +7521,20 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -7644,7 +7651,8 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -7656,6 +7664,7 @@ "version": "1.0.0", "bundled": true, "dev": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -7670,6 +7679,7 @@ "version": "3.0.4", "bundled": true, "dev": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -7677,12 +7687,14 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "minipass": { "version": "2.3.5", "bundled": true, "dev": true, + "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -7701,6 +7713,7 @@ "version": "0.5.1", "bundled": true, "dev": true, + "optional": true, "requires": { "minimist": "0.0.8" } @@ -7781,7 +7794,8 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -7793,6 +7807,7 @@ "version": "1.4.0", "bundled": true, "dev": true, + "optional": true, "requires": { "wrappy": "1" } @@ -7878,7 +7893,8 @@ "safe-buffer": { "version": "5.1.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -7914,6 +7930,7 @@ "version": "1.0.2", "bundled": true, "dev": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -7933,6 +7950,7 @@ "version": "3.0.1", "bundled": true, "dev": true, + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -7976,12 +7994,14 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "yallist": { "version": "3.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true } } }, @@ -9710,9 +9730,9 @@ } }, "m3u8stream": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/m3u8stream/-/m3u8stream-0.6.4.tgz", - "integrity": "sha512-9WLF1VAtbVij03HWJKbVZ8L0orsoZiP53UljR5EwaDrozQFMsTGRDPe3PbzWV73He8a+j5H/hWZNoI2VkUSsiw==", + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/m3u8stream/-/m3u8stream-0.6.5.tgz", + "integrity": "sha512-QZCzhcfUliZfsOboi68QkNcMejPKTEhxE+s1TApvHubDeR8ythm4ViWuYFqgUwZeoHe8q0nsPxOvA3lQvdSzyg==", "requires": { "miniget": "^1.6.1", "sax": "^1.2.4" @@ -15705,9 +15725,9 @@ } }, "ytdl-core": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/ytdl-core/-/ytdl-core-1.0.7.tgz", - "integrity": "sha512-gECPN5g5JnSy8hIq11xHIGe1T/Xzy0mWxQin3zhlJ3nG/YjPcEVEejrdd2XmA4Vv2Zw3+b1ZyDjmt37XfZri6A==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/ytdl-core/-/ytdl-core-1.0.9.tgz", + "integrity": "sha512-HhFeLfjXU34h0FNHmSkSpKygdaYijSt8VNsC770VYBRFb+dyUKcm11cIKxu2MUSwT9znISZ0k1wFdaV/N5VW+Q==", "requires": { "html-entities": "^1.1.3", "m3u8stream": "^0.6.3", diff --git a/package.json b/package.json index 7a8b001a..e726ee75 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "youtube-comments-task": "^1.3.14", "youtube-suggest": "^1.1.0", "yt-xml2srt": "^1.1.0", - "ytdl-core": "^1.0.7", + "ytdl-core": "^1.0.9", "ytpl": "^0.1.20", "ytsr": "^0.1.10" }, diff --git a/src/renderer/components/ft-video-player/ft-video-player.js b/src/renderer/components/ft-video-player/ft-video-player.js index d6638972..43ff16b0 100644 --- a/src/renderer/components/ft-video-player/ft-video-player.js +++ b/src/renderer/components/ft-video-player/ft-video-player.js @@ -1,6 +1,8 @@ import Vue from 'vue' import FtCard from '../ft-card/ft-card.vue' +import $ from 'jquery' + // I haven't decided which video player I want to use // Need to expirement with both of them to see which one will work best. import videojs from 'video.js' @@ -98,6 +100,10 @@ export default Vue.extend({ videoFormatPreference: function () { return this.$store.getters.getVideoFormatPreference + }, + + autoplay: function () { + return this.$store.getters.getAutoplay } }, mounted: function () { @@ -105,6 +111,12 @@ export default Vue.extend({ this.determineFormatType() }, + beforeDestroy: function () { + if (this.player !== null && !this.player.isInPictureInPicture()) { + this.player.dispose() + this.player = null + } + }, methods: { initializePlayer: function () { const videoPlayer = document.getElementById(this.id) @@ -126,6 +138,15 @@ export default Vue.extend({ this.player.httpSourceSelector() } + + if (this.autoplay) { + // Calling play() won't happen right away, so a quick timeout will make it function properly. + setTimeout(() => { + this.player.play() + }, 100) + } + + $(document).on('keydown', this.keyboardShortcutHandler) } }, @@ -161,6 +182,220 @@ export default Vue.extend({ this.activeSourceList = this.sourceList setTimeout(this.initializePlayer, 100) + }, + + togglePlayPause: function () { + if (this.player.paused()) { + this.player.play() + } else { + this.player.pause() + } + }, + + changeDurationBySeconds: function (seconds) { + const currentTime = this.player.currentTime() + const newTime = currentTime + seconds + + if (newTime < 0) { + this.player.currentTime(0) + } else if (newTime > this.player.duration) { + this.player.currentTime(this.player.duration) + } else { + this.player.currentTime(newTime) + } + }, + + changeDurationByPercentage: function (percentage) { + const duration = this.player.duration() + const newTime = duration * percentage + + this.player.currentTime(newTime) + }, + + changePlayBackRate: function (rate) { + const newPlaybackRate = this.player.playbackRate() + rate + + if (newPlaybackRate >= 0.25 && newPlaybackRate <= 3) { + this.player.playbackRate(newPlaybackRate) + } + }, + + changeVolume: function (volume) { + const currentVolume = this.player.volume() + const newVolume = currentVolume + volume + + if (newVolume < 0) { + this.player.volume(0) + } else if (newVolume > 1) { + this.player.volume(1) + } else { + this.player.volume(newVolume) + } + }, + + toggleMute: function () { + if (this.player.muted()) { + this.player.muted(false) + } else { + this.player.muted(true) + } + }, + + toggleFullscreen: function () { + if (this.player.isFullscreen()) { + this.player.exitFullscreen() + } else { + this.player.requestFullscreen() + } + }, + + toggleCaptions: function () { + const tracks = this.player.textTracks().tracks_ + + if (tracks.length > 1) { + if (tracks[1].mode === 'showing') { + tracks[1].mode = 'disabled' + } else { + tracks[1].mode = 'showing' + } + } + }, + + keyboardShortcutHandler: function (event) { + const activeInputs = $('.ft-input') + + for (let i = 0; i < activeInputs.length; i++) { + if (activeInputs[i] === document.activeElement) { + return + } + } + + if (this.player !== null) { + event.preventDefault() + switch (event.which) { + case 32: + // Space Bar + // Toggle Play/Pause + this.togglePlayPause() + break + case 74: + // J Key + // Rewind by 10 seconds + this.changeDurationBySeconds(-10) + break + case 75: + // K Key + // Toggle Play/Pause + this.togglePlayPause() + break + case 76: + // L Key + // Fast Forward by 10 seconds + this.changeDurationBySeconds(10) + break + case 79: + // O Key + // Decrease playback rate by 0.25x + this.changePlayBackRate(-0.25) + break + case 80: + // P Key + // Increase playback rate by 0.25x + this.changePlayBackRate(0.25) + break + case 70: + // F Key + // Toggle Fullscreen Playback + this.toggleFullscreen() + break + case 77: + // M Key + // Toggle Mute + this.toggleMute() + break + case 67: + // C Key + // Toggle Captions + this.toggleCaptions() + break + case 38: + // Up Arrow Key + // Increase volume + this.changeVolume(0.05) + break + case 40: + // Down Arrow Key + // Descrease Volume + this.changeVolume(-0.05) + break + case 37: + // Left Arrow Key + // Rewind by 5 seconds + this.changeDurationBySeconds(-5) + break + case 39: + // Right Arrow Key + // Fast Foward by 5 seconds + this.changeDurationBySeconds(5) + break + case 49: + // 1 Key + // Jump to 10% in the video + this.changeDurationByPercentage(0.1) + break + case 50: + // 2 Key + // Jump to 20% in the video + this.changeDurationByPercentage(0.2) + break + case 51: + // 3 Key + // Jump to 30% in the video + this.changeDurationByPercentage(0.3) + break + case 52: + // 4 Key + // Jump to 40% in the video + this.changeDurationByPercentage(0.4) + break + case 53: + // 5 Key + // Jump to 50% in the video + this.changeDurationByPercentage(0.5) + break + case 54: + // 6 Key + // Jump to 60% in the video + this.changeDurationByPercentage(0.6) + break + case 55: + // 7 Key + // Jump to 70% in the video + this.changeDurationByPercentage(0.7) + break + case 56: + // 8 Key + // Jump to 80% in the video + this.changeDurationByPercentage(0.8) + break + case 57: + // 9 Key + // Jump to 90% in the video + this.changeDurationByPercentage(0.9) + break + case 48: + // 0 Key + // Jump to 0% in the video (The beginning) + this.changeDurationByPercentage(0) + break + } + } + } + }, + beforeRouteLeave: function () { + if (this.player !== null && !this.player.isInPictureInPicture()) { + this.player.dispose() + this.player = null } } }) diff --git a/src/renderer/store/modules/settings.js b/src/renderer/store/modules/settings.js index 53ffabc3..96af310e 100644 --- a/src/renderer/store/modules/settings.js +++ b/src/renderer/store/modules/settings.js @@ -66,6 +66,10 @@ const getters = { getVideoFormatPreference: () => { return state.videoFormatPreference + }, + + getAutoplay: () => { + return state.autoplay } } diff --git a/src/renderer/views/Watch/Watch.js b/src/renderer/views/Watch/Watch.js index 28e138e4..dd81dc22 100644 --- a/src/renderer/views/Watch/Watch.js +++ b/src/renderer/views/Watch/Watch.js @@ -147,11 +147,14 @@ export default Vue.extend({ // this.videoStoryboardSrc = result.player_response.storyboards.playerStoryboardSpecRenderer.spec this.captionSourceList = result.player_response.captions.playerCaptionsTracklistRenderer.captionTracks - this.captionSourceList = this.captionSourceList.map((caption) => { - caption.baseUrl = `${this.invidiousInstance}/api/v1/captions/${this.videoId}?label=${encodeURI(caption.name.simpleText)}` - return caption - }) + if (typeof (this.captionSourceList) !== 'undefined') { + this.captionSourceList = this.captionSourceList.map((caption) => { + caption.baseUrl = `${this.invidiousInstance}/api/v1/captions/${this.videoId}?label=${encodeURI(caption.name.simpleText)}` + + return caption + }) + } // TODO: The response returns the captions of the video, however they're returned // in XML / TTML. I haven't found a way to properly convert this for use. @@ -234,6 +237,10 @@ export default Vue.extend({ }, enableDashFormat: function () { + if (this.activeFormat === 'dash') { + return + } + this.activeFormat = 'dash' this.hidePlayer = true @@ -241,10 +248,16 @@ export default Vue.extend({ }, enableLegacyFormat: function () { + if (this.activeFormat === 'legacy') { + return + } + this.activeFormat = 'legacy' this.hidePlayer = true - setTimeout(() => { this.hidePlayer = false }, 100) + setTimeout(() => { + this.hidePlayer = false + }, 100) } } })