]>
Commit | Line | Data |
---|---|---|
d7b052ff C |
1 | import videojs from 'video.js' |
2 | ||
3 | type KeyHandler = { accept: (event: KeyboardEvent) => boolean, cb: (e: KeyboardEvent) => void } | |
4 | ||
5 | const Plugin = videojs.getPlugin('plugin') | |
6 | ||
7 | class PeerTubeHotkeysPlugin extends Plugin { | |
8 | private static readonly VOLUME_STEP = 0.1 | |
9 | private static readonly SEEK_STEP = 5 | |
10 | ||
11 | private readonly handleKeyFunction: (event: KeyboardEvent) => void | |
12 | ||
13 | private readonly handlers: KeyHandler[] | |
14 | ||
15 | constructor (player: videojs.Player, options: videojs.PlayerOptions) { | |
16 | super(player, options) | |
17 | ||
18 | this.handlers = this.buildHandlers() | |
19 | ||
20 | this.handleKeyFunction = (event: KeyboardEvent) => this.onKeyDown(event) | |
21 | document.addEventListener('keydown', this.handleKeyFunction) | |
22 | } | |
23 | ||
24 | dispose () { | |
25 | document.removeEventListener('keydown', this.handleKeyFunction) | |
26 | } | |
27 | ||
28 | private onKeyDown (event: KeyboardEvent) { | |
29 | if (!this.isValidKeyTarget(event.target as HTMLElement)) return | |
30 | ||
31 | for (const handler of this.handlers) { | |
32 | if (handler.accept(event)) { | |
33 | handler.cb(event) | |
34 | return | |
35 | } | |
36 | } | |
37 | } | |
38 | ||
39 | private buildHandlers () { | |
40 | const handlers: KeyHandler[] = [ | |
41 | // Play | |
42 | { | |
43 | accept: e => (e.key === ' ' || e.key === 'MediaPlayPause'), | |
44 | cb: e => { | |
45 | e.preventDefault() | |
46 | e.stopPropagation() | |
47 | ||
48 | if (this.player.paused()) this.player.play() | |
49 | else this.player.pause() | |
50 | } | |
51 | }, | |
52 | ||
53 | // Increase volume | |
54 | { | |
55 | accept: e => this.isNaked(e, 'ArrowUp'), | |
56 | cb: e => { | |
57 | e.preventDefault() | |
58 | this.player.volume(this.player.volume() + PeerTubeHotkeysPlugin.VOLUME_STEP) | |
59 | } | |
60 | }, | |
61 | ||
62 | // Decrease volume | |
63 | { | |
64 | accept: e => this.isNaked(e, 'ArrowDown'), | |
65 | cb: e => { | |
66 | e.preventDefault() | |
67 | this.player.volume(this.player.volume() - PeerTubeHotkeysPlugin.VOLUME_STEP) | |
68 | } | |
69 | }, | |
70 | ||
71 | // Rewind | |
72 | { | |
73 | accept: e => this.isNaked(e, 'ArrowLeft') || this.isNaked(e, 'MediaRewind'), | |
74 | cb: e => { | |
75 | e.preventDefault() | |
76 | ||
77 | const target = Math.max(0, this.player.currentTime() - PeerTubeHotkeysPlugin.SEEK_STEP) | |
78 | this.player.currentTime(target) | |
79 | } | |
80 | }, | |
81 | ||
82 | // Forward | |
83 | { | |
84 | accept: e => this.isNaked(e, 'ArrowRight') || this.isNaked(e, 'MediaForward'), | |
85 | cb: e => { | |
86 | e.preventDefault() | |
87 | ||
88 | const target = Math.min(this.player.duration(), this.player.currentTime() + PeerTubeHotkeysPlugin.SEEK_STEP) | |
89 | this.player.currentTime(target) | |
90 | } | |
91 | }, | |
92 | ||
93 | // Fullscreen | |
94 | { | |
95 | // f key or Ctrl + Enter | |
96 | accept: e => this.isNaked(e, 'f') || (!e.altKey && e.ctrlKey && e.key === 'Enter'), | |
97 | cb: e => { | |
98 | e.preventDefault() | |
99 | ||
100 | if (this.player.isFullscreen()) this.player.exitFullscreen() | |
101 | else this.player.requestFullscreen() | |
102 | } | |
103 | }, | |
104 | ||
105 | // Mute | |
106 | { | |
107 | accept: e => this.isNaked(e, 'm'), | |
108 | cb: e => { | |
109 | e.preventDefault() | |
110 | ||
111 | this.player.muted(!this.player.muted()) | |
112 | } | |
113 | }, | |
114 | ||
115 | // Increase playback rate | |
116 | { | |
117 | accept: e => e.key === '>', | |
118 | cb: () => { | |
119 | const target = Math.min(this.player.playbackRate() + 0.1, 5) | |
120 | ||
121 | this.player.playbackRate(parseFloat(target.toFixed(2))) | |
122 | } | |
123 | }, | |
124 | ||
125 | // Decrease playback rate | |
126 | { | |
127 | accept: e => e.key === '<', | |
128 | cb: () => { | |
129 | const target = Math.max(this.player.playbackRate() - 0.1, 0.10) | |
130 | ||
131 | this.player.playbackRate(parseFloat(target.toFixed(2))) | |
132 | } | |
133 | }, | |
134 | ||
135 | // Previous frame | |
136 | { | |
137 | accept: e => e.key === ',', | |
138 | cb: () => { | |
139 | this.player.pause() | |
140 | ||
141 | // Calculate movement distance (assuming 30 fps) | |
142 | const dist = 1 / 30 | |
143 | this.player.currentTime(this.player.currentTime() - dist) | |
144 | } | |
145 | }, | |
146 | ||
147 | // Next frame | |
148 | { | |
149 | accept: e => e.key === '.', | |
150 | cb: () => { | |
151 | this.player.pause() | |
152 | ||
153 | // Calculate movement distance (assuming 30 fps) | |
154 | const dist = 1 / 30 | |
155 | this.player.currentTime(this.player.currentTime() + dist) | |
156 | } | |
157 | } | |
158 | ] | |
159 | ||
160 | // 0-9 key handlers | |
161 | for (let i = 0; i < 10; i++) { | |
162 | handlers.push({ | |
163 | accept: e => e.key === i + '', | |
164 | cb: e => { | |
165 | e.preventDefault() | |
166 | ||
167 | this.player.currentTime(this.player.duration() * i * 0.1) | |
168 | } | |
169 | }) | |
170 | } | |
171 | ||
172 | return handlers | |
173 | } | |
174 | ||
175 | private isValidKeyTarget (eventEl: HTMLElement) { | |
176 | const playerEl = this.player.el() | |
177 | const activeEl = document.activeElement | |
178 | const currentElTagName = eventEl.tagName.toLowerCase() | |
179 | ||
180 | return ( | |
181 | activeEl === playerEl || | |
182 | activeEl === playerEl.querySelector('.vjs-tech') || | |
183 | activeEl === playerEl.querySelector('.vjs-control-bar') || | |
184 | eventEl.id === 'content' || | |
185 | currentElTagName === 'body' || | |
186 | currentElTagName === 'video' | |
187 | ) | |
188 | } | |
189 | ||
190 | private isNaked (event: KeyboardEvent, key: string) { | |
191 | return (!event.ctrlKey && !event.altKey && !event.metaKey && !event.shiftKey && event.key === key) | |
192 | } | |
193 | } | |
194 | ||
195 | videojs.registerPlugin('peerTubeHotkeysPlugin', PeerTubeHotkeysPlugin) | |
196 | export { PeerTubeHotkeysPlugin } |