🏠 返回首頁 

Greasy Fork is available in English.

Netflix keyboard shortcuts

Use similar controls as on YouTube when watching Netflix (f for full screen, k to play/pause, c for captions, j and l to go back and forward 10 seconds, a to change audio, p for picture-in-picture, and many more – all configurable)

  1. // ==UserScript==
  2. // @name Netflix keyboard shortcuts
  3. // @namespace netflix.keyboard
  4. // @version 1.6.2
  5. // @description Use similar controls as on YouTube when watching Netflix (f for full screen, k to play/pause, c for captions, j and l to go back and forward 10 seconds, a to change audio, p for picture-in-picture, and many more – all configurable)
  6. // @include https://netflix.com/*
  7. // @include https://www.netflix.com/*
  8. // @grant none
  9. // @author https://chrome.google.com/webstore/detail/netflix-keyboard-shortcut/mjpponglbellandpimdbmmhbhmakcgji
  10. // ==/UserScript==
  11. (function() {
  12. /* global netflix */
  13. 'use strict';
  14. const debug = false; // set this to true to get debug information as the script processes events, and expose the `player` object as `document.player`.
  15. // change these constants if you prefer to use different keys (or change the letter to uppercase if you want the shortcut to require the use of Shift)
  16. // to disable a particular feature, set the value to null.
  17. const PLAY_PAUSE_KEY = 'k'; // play/pause (same as on YouTube)
  18. const PICTURE_IN_PICTURE_KEY = 'p'; // turns picture-in-picture on or off
  19. const ONE_FRAME_FORWARD_KEY = '.'; // when paused, moves ahead by one frame
  20. const ONE_FRAME_BACKWARD_KEY = ','; // when paused, moves back by one frame
  21. const NEXT_EPISODE_KEY = 'N'; // (capital `n`) – goes to the very end of the current episode, causing the "Play Next Episode" button to appear.
  22. const PLAYBACK_SPEED_FASTER = '>'; // increases the playback speed (see `PLAYBACK_SPEED_INCREMENTS` below)
  23. const PLAYBACK_SPEED_SLOWER = '<'; // decreases the playback speed (see `PLAYBACK_SPEED_INCREMENTS` below)
  24. const SUBTITLES_ON_OFF_KEY = 'c'; // turns subtitles on or off. see `DEFAULT_SUBTITLES_LANGUAGE` below for a way to pick the language of your choice
  25. const SUBTITLES_SIZE_KEY = 's'; // changes the size of subtitles (Netflix has 3 options: small/medium/large – this cycles between them)
  26. const SUBTITLES_NEXT_LANGUAGE_KEY = 'v'; // selects the next subtitles track
  27. const NEXT_AUDIO_TRACK_KEY = 'a'; // switches audio to the next track
  28. const MUTE_UNMUTE_KEY = null; // Netflix sets mute/unmute to 'm'. You can use a different key here.
  29. const VOLUME_UP_KEY = 'PageUp'; // we can't use 'ArrowUp' here since Netflix already handles this event themselves.
  30. const VOLUME_DOWN_KEY = 'PageDown'; // we can't use 'ArrowDown' here since Netflix already handles this event themselves.
  31. const NUMBER_KEYS_ENABLED = true; // press key 0 to jump to 0% (start of the video), 1 for 10%… up to 9 for 90%
  32. const DEBUG_PANEL_KEY = 'd'; // shows the Netflix debug panel
  33. // the following constants control the visual feedback that's briefly displayed for certain actions
  34. const FADING_TEXT_DURATION_SEC = 0.75; // duration of fading-out text on key press, in seconds
  35. const PLAY_PAUSE_SHOW = true; // whether to show the play/pause symbol when `PLAY_PAUSE_KEY` is pressed
  36. const PLAYBACK_SPEED_CHANGE_SHOW = true; // whether to show the new playback speed on-screen
  37. const SUBTITLES_NEXT_LANGUAGE_SHOW = true; // whether to show which subtitles language was switched to
  38. const NEXT_AUDIO_TRACK_SHOW = true; // whether to show which language track audio was switched to
  39. const NUMBER_KEYS_SHOW = true; // whether to show to which percentage we're jumping when pressing 0..9
  40. const TIME_SCRUB_SHOW = true; // whether to show to by how many second we're jumping when pressing one of the keys in `TIME_SCRUB`
  41. // edit the following list to add more time-scrubbing shortcuts.
  42. // each entry has a `key` and a `time`; `key` is the key to press and `time` is by how much to go forward (or backward if negative), expressed in seconds.
  43. // non-integral values are supported (e.g. +2.5), and uppercase letters for `key` work too (e.g. you can have `j` for -10s and `J` for -60s)
  44. const TIME_SCRUB_KEYS = [
  45. {key: 'j', time: -10}, // go back 10 seconds (same as on YouTube)
  46. {key: 'l', time: +10}, // go forward 10 seconds (same as on YouTube)
  47. {key: 'J', time: -60}, // go back 60 seconds
  48. {key: 'L', time: +60}, // go forward 60 seconds
  49. {key: '[', time: -5}, // go back 5 seconds
  50. {key: ']', time: +5}, // go forward 5 seconds
  51. ];
  52. // edit these values to change the available playback speeds. 1.0 is normal speed, 0.5 is half speed, etc.
  53. // pressing `PLAYBACK_SPEED_FASTER` goes to the next higher value, and pressing `PLAYBACK_SPEED_SLOWER` goes to the next lower value
  54. const PLAYBACK_SPEED_INCREMENTS = [
  55. 0.5,
  56. 0.75,
  57. 0.9,
  58. 1.0,
  59. 1.1,
  60. 1.25,
  61. 1.5,
  62. 2.0,
  63. 2.5,
  64. ];
  65. // these constants control the behavior of the shortcut keys above
  66. const VOLUME_DELTA = 0.05; // how much to increase/decrease the volume by (range is 0.0 to 1.0 so 0.05 is 5%)
  67. const DEFAULT_SUBTITLES_LANGUAGE = 'English'; // change this to have the subtitles key pick a different language by default (when you start from "Off" and press "c"). Example values you can use: 'en', 'fr', 'es', 'zh-Hans', 'zh-Hant'...
  68. const DEFAULT_FRAMES_PER_SECOND = 24; // how many frames the script considers to be in one second if this information can't be found in the video metadata (this is used for the "next frame"/"previous frame" shortcut which seeks by one second over this amount)
  69. /***************************************************************************************************************************************************************************************************/
  70. function detectDuplicateKeyBindings() {
  71. // first list all individually-set shortcuts
  72. const mapped = [PLAY_PAUSE_KEY, PICTURE_IN_PICTURE_KEY, ONE_FRAME_FORWARD_KEY, ONE_FRAME_BACKWARD_KEY,
  73. SUBTITLES_ON_OFF_KEY, SUBTITLES_SIZE_KEY, SUBTITLES_NEXT_LANGUAGE_KEY, SUBTITLES_NEXT_LANGUAGE_KEY.toUpperCase(),
  74. NEXT_AUDIO_TRACK_KEY,NEXT_AUDIO_TRACK_KEY.toUpperCase(), MUTE_UNMUTE_KEY, VOLUME_UP_KEY, VOLUME_DOWN_KEY,
  75. PLAYBACK_SPEED_FASTER, PLAYBACK_SPEED_SLOWER, NEXT_EPISODE_KEY, DEBUG_PANEL_KEY];
  76. TIME_SCRUB_KEYS.map(entry => entry.key).forEach(key => mapped.push(key)); // also add the time-scrub ones
  77. const mappedNoNulls = mapped.filter(key => key !== null); // remove null since they don't match actual key presses and mean the shortcut is disabled
  78. const seen = new Set(); // then dedupe and report errors
  79. mapped.forEach(key => {
  80. if (seen.has(key)) {
  81. console.error('Configuration error: the key shortcut "' + key + '" is assigned to at least two different actions');
  82. } else {
  83. seen.add(key);
  84. }
  85. });
  86. }
  87. detectDuplicateKeyBindings(); // called once
  88. /**
  89. * Gets a nested property inside an object.
  90. */
  91. function getDeepProperty(obj, props) {
  92. if (typeof obj === 'undefined') {
  93. return null;
  94. } else if (typeof obj !== 'object') {
  95. return obj;
  96. }
  97. var cur = obj;
  98. for (var key of props.split('.')) {
  99. const isFunction = key.endsWith('()');
  100. const attrName = isFunction ? key.substring(0, key.length-2) : key;
  101. if (!cur[attrName]) {
  102. return null;
  103. }
  104. cur = cur[attrName];
  105. if (isFunction && typeof cur === 'function') {
  106. cur = cur();
  107. }
  108. }
  109. return cur;
  110. }
  111. /**
  112. * Returns the "Player" object used by the Netflix web app to control video playback.
  113. * We get the playerApp, then API, then video player object on which we list the session IDs
  114. * and return the video player for that session ID. Preference is given to a session ID that
  115. * starts with 'watch-' (some start with e.g. 'motion-billboard-' and are not the content).
  116. */
  117. function getPlayer() {
  118. // uses the `netflix` object, a global variable exposed by the web app
  119. const videoPlayer = getDeepProperty(netflix, 'appContext.state.playerApp.getAPI().videoPlayer');
  120. if (videoPlayer && videoPlayer.getVideoPlayerBySessionId && videoPlayer.getAllPlayerSessionIds) {
  121. const allSessionIds = videoPlayer.getAllPlayerSessionIds();
  122. const watchSessionIds = allSessionIds.filter(sid => sid.startsWith('watch-'));
  123. if (watchSessionIds.length > 0) {
  124. return videoPlayer.getVideoPlayerBySessionId(watchSessionIds[0]); // we can't differentiate them though
  125. } else if (allSessionIds.length > 0) {
  126. return videoPlayer.getVideoPlayerBySessionId(allSessionIds[0]); // otherwise just return the first one
  127. }
  128. }
  129. return null;
  130. }
  131. /**
  132. * Returns the `<video>` tag for playing media.
  133. */
  134. function getVideoTag() {
  135. const videos = document.getElementsByTagName('video');
  136. return (videos && videos.length === 1 ? videos[0] : null);
  137. }
  138. function isBoolean(b) {
  139. return b === true || b === false;
  140. }
  141. /**
  142. * Returns the subtitles track for a given language.
  143. * Matches full name (e.g. "English") or a BCP 47 language code (e.g. "en")
  144. */
  145. function findSubtitlesTrack(player, language) {
  146. const tracks = player.getTimedTextTrackList();
  147. var bestTrack = null; // tracks the best choice we've found so far
  148. for (var i = 0; i < tracks.length; i++) {
  149. if (tracks[i].displayName === language || tracks[i].bcp47 === language) { // language matches, that's a good start
  150. if ((bestTrack === null) || // none found yet
  151. (bestTrack !== null && bestTrack.trackType !== 'PRIMARY' && tracks[i].trackType === 'PRIMARY')) { // this one is better (PRIMARY vs ASSISTIVE), replace
  152. bestTrack = tracks[i];
  153. debug && console.log('Best choice so far looking for "' + language + '":', bestTrack);
  154. }
  155. }
  156. }
  157. return bestTrack;
  158. }
  159. /**
  160. * Returns the next size for subtitles
  161. */
  162. function nextSubtitlesSize(currentSize) {
  163. switch(currentSize) {
  164. case 'SMALL': return 'MEDIUM';
  165. case 'MEDIUM': return 'LARGE';
  166. case 'LARGE': return 'SMALL';
  167. default: // not found somehow
  168. return 'MEDIUM';
  169. }
  170. }
  171. var lastSelectedTextTrack = null; // caches the last non-"Off" language to have the `c` key switch between "Off" and that language.
  172. var preferredTextTrack = null; // caches the preferred language track
  173. function switchSubtitles(player) {
  174. // select preferred language, once
  175. if (preferredTextTrack === null) {
  176. preferredTextTrack = findPreferredTextTrack(player);
  177. debug && console.log('Found preferred text track:', preferredTextTrack);
  178. }
  179. // first, get current track to see if subtitles are currently visible
  180. const currentTrack = player.getTimedTextTrack();
  181. const disabledTrack = findSubtitlesTrack(player, 'Off');
  182. const currentlyDisabled = (currentTrack !== null && disabledTrack !== null && currentTrack.displayName === disabledTrack.displayName);
  183. // flip
  184. if (currentlyDisabled) {
  185. // do we have a last selected track? if so, switch back to it.
  186. if (lastSelectedTextTrack && lastSelectedTextTrack.displayName !== 'Off') { // avoid switching from "Off" to "Off"
  187. player.setTimedTextTrack(lastSelectedTextTrack);
  188. } else if (preferredTextTrack) { // otherwise, switch to preferred language
  189. player.setTimedTextTrack(preferredTextTrack);
  190. } else {
  191. console.warn("No last selected subtitles track to go back to, and couldn't find subtitles in the preferred language,", DEFAULT_SUBTITLES_LANGUAGE);
  192. }
  193. } else { // currently enabled, so we're switching to "Off".
  194. player.setTimedTextTrack(disabledTrack);
  195. }
  196. lastSelectedTextTrack = currentTrack; // and remember what we just switched from
  197. }
  198. function findPreferredTextTrack(player) {
  199. var chosenTrack = findSubtitlesTrack(player, DEFAULT_SUBTITLES_LANGUAGE);
  200. if (!chosenTrack) {
  201. console.warn('Could not find subtitles in ' + DEFAULT_SUBTITLES_LANGUAGE + (DEFAULT_SUBTITLES_LANGUAGE !== 'English' ? ', defaulting to English' : ''));
  202. chosenTrack = findSubtitlesTrack(player, 'English');
  203. if (!chosenTrack) {
  204. DEFAULT_SUBTITLES_LANGUAGE !== 'English' && console.warn('Could not find subtitles in English either :-/');
  205. }
  206. }
  207. return chosenTrack; // might be null
  208. }
  209. function nextOffset(curOffset, delta, numElements) {
  210. return (curOffset + delta + numElements) % numElements; // add delta, and then length too so that we don't get a negative modulo
  211. }
  212. /**
  213. * Selects the next track in the list of available audio tracks.
  214. */
  215. function selectNeighborAudioTrack(player, delta) {
  216. const trackList = player.getAudioTrackList();
  217. const currentTrack = player.getAudioTrack();
  218. if (!trackList || !currentTrack) {
  219. console.warn('Could not find the current audio track or the list of audio tracks');
  220. }
  221. for (var i = 0; i < trackList.length; i++) {
  222. if (currentTrack.displayName === trackList[i].displayName) { // found!
  223. const nextTrack = trackList[nextOffset(i, delta, trackList.length)];
  224. debug && console.log('Switching audio track to ' + nextTrack.displayName);
  225. if (NEXT_AUDIO_TRACK_SHOW) {
  226. displayText(player, nextTrack.displayName, false);
  227. }
  228. player.setAudioTrack(nextTrack);
  229. return;
  230. }
  231. }
  232. }
  233. /**
  234. * Selects the next track in the list of available subtitles tracks.
  235. */
  236. function selectNeighborSubtitlesTrack(player, delta) {
  237. const trackList = player.getTimedTextTrackList();
  238. const currentTrack = player.getTimedTextTrack();
  239. if (!trackList || !currentTrack) {
  240. console.warn('Could not find the current subtitles track or the list of subtitles tracks');
  241. }
  242. for (var i = 0; i < trackList.length; i++) {
  243. if (currentTrack.trackId === trackList[i].trackId) { // found!
  244. const nextTrack = trackList[nextOffset(i, delta, trackList.length)];
  245. debug && console.log('Switching subtitles track to ' + nextTrack.displayName);
  246. if (SUBTITLES_NEXT_LANGUAGE_SHOW) {
  247. displayText(player, nextTrack.displayName, false);
  248. }
  249. player.setTimedTextTrack(nextTrack);
  250. return;
  251. }
  252. }
  253. }
  254. /* Debug panel */
  255. function toggleDebugPanel() {
  256. netflix.player.diag.togglePanel('info', null); // also accepts `true` or `false`, but `null` means toggle
  257. }
  258. /* Playback speed */
  259. var savedPlaybackRate = null;
  260. function changePlaybackSpeed(player, video, delta) {
  261. const currentSpeed = video.playbackRate;
  262. var smallestDifference = Number.MAX_VALUE;
  263. var savedOffset = -1;
  264. for (var i = 0; i < PLAYBACK_SPEED_INCREMENTS.length; i++) {
  265. const curDifference = Math.abs(currentSpeed - PLAYBACK_SPEED_INCREMENTS[i]);
  266. if (curDifference < smallestDifference) {
  267. savedOffset = i;
  268. smallestDifference = curDifference;
  269. }
  270. }
  271. // compute new rate, adjust
  272. const newOffset = limitRange(0, PLAYBACK_SPEED_INCREMENTS.length - 1, savedOffset + delta);
  273. const newPlaybackRate = PLAYBACK_SPEED_INCREMENTS[newOffset];
  274. debug && console.log('Found closest rate (', PLAYBACK_SPEED_INCREMENTS[savedOffset], ') to the current rate (', currentSpeed, ')');
  275. debug && console.log('Setting new playback rate:', newPlaybackRate); // not using `debug` to have *some* way to tell what the current playback rate is
  276. // Preserve value, adjust now. Display feedback if needed.
  277. PLAYBACK_SPEED_CHANGE_SHOW && displayText(player, newPlaybackRate + 'x');
  278. savedPlaybackRate = newPlaybackRate;
  279. reapplyPlaybackRate();
  280. }
  281. function reapplyPlaybackRate() {
  282. const video = getVideoTag();
  283. if (video && savedPlaybackRate !== null) {
  284. video.playbackRate = savedPlaybackRate;
  285. }
  286. }
  287. /* Frame-by-frame skips & more precise scrubbing */
  288. var lastExpectedTimeMillis = null; // tracks where we believe we seek()'d to the last time we moved frame by frame
  289. function moveToPosition(player, timeMillis) {
  290. player.seek(timeMillis);
  291. lastExpectedTimeMillis = timeMillis;
  292. }
  293. // if diagnostics give us the number of frames per second, use that; otherwise use the constant `FRAMES_PER_SECOND`.
  294. function getFramesPerSecond(player) {
  295. try {
  296. const fromDiagnostics = parseFloat(player.getDiagnostics().getGroups().filter(group => 'Framerate' in group)[0].Framerate);
  297. if (fromDiagnostics > 0 && fromDiagnostics < 200) {
  298. return fromDiagnostics;
  299. }
  300. } catch (error) {}
  301. return DEFAULT_FRAMES_PER_SECOND;
  302. }
  303. /**
  304. * Skips or goes back one frame (based on factor > 0 or < 0)
  305. */
  306. function skipFrame(player, factor) {
  307. const currentTime = lastExpectedTimeMillis; // use the cached variable that we set when we entered the "paused" state
  308. const fps = getFramesPerSecond(player);
  309. const newPosition = limitRange(0, player.getDuration(), currentTime + factor * 1000.0 / fps); // factor is +1 or -1
  310. debug && console.log('Seek ' + (factor > 0 ? '>' : '<') + ' to:', newPosition, '(assuming',fps,'FPS)');
  311. moveToPosition(player, newPosition);
  312. }
  313. function limitRange(minValue, maxValue, n) {
  314. return Math.max(minValue, Math.min(maxValue, n));
  315. }
  316. /* Play/Pause state change detection. We need this for frame skips to work, since `getCurrentTime()` might not update with a very short seek() so we keep track of the actual time in `lastExpectedTimeMillis`. */
  317. /* Called when the video r###mes playing (from being paused) */
  318. function onPlaybackR###mes(player) {
  319. lastExpectedTimeMillis = null; // clear the current time
  320. }
  321. /* Called when the video is paused (from having been playing) */
  322. function onPlaybackPauses(player) {
  323. lastExpectedTimeMillis = player.getCurrentTime(); // when entering paused state, mark where we think we are and use this variable rather than `getCurrentTime()` to accurately keep track of the position.
  324. }
  325. /**
  326. * Find the `<video>` tag and installs "onPlay" and "onPause" callbacks if needed.
  327. * This is called repeatedly in case the `<video>` tag is replaced (e.g. the next episode starts playing)
  328. */
  329. function installPlayPauseCallbacks() {
  330. const video = getVideoTag();
  331. const player = getPlayer();
  332. if (!video || !player || video._nfkbdInstalled) { // nfkbd for Netflix Keyboard Controls
  333. return;
  334. }
  335. video.addEventListener('play', function() { onPlaybackR###mes(player); });
  336. video.addEventListener('pause', function() { onPlaybackPauses(player); });
  337. video._nfkbdInstalled = true;
  338. debug && console.log('Play/pause callbacks installed');
  339. }
  340. /* Installs `<style>` block for fade-out */
  341. function installStyleBlock() {
  342. const CIRCLE_TEXT_FONT_SIZE_PX = 80;
  343. const CIRCLE_DIAMETER_PX = 260;
  344. const css = '@keyframes fadeOut {' +
  345. ' 0% {' +
  346. ' opacity: 1;' +
  347. ' }' +
  348. ' 100% {' +
  349. ' opacity: 0;' +
  350. ' }' +
  351. '}' +
  352. 'h1.nfkbd-text {' +
  353. ' color: white;' +
  354. ' font-family: sans-serif;' +
  355. ' z-index: 2147483647;' +
  356. ' position: absolute;' +
  357. ' text-align: center;' +
  358. ' transform: translate(0%, -50%);' +
  359. ' animation: fadeOut ease ' + FADING_TEXT_DURATION_SEC + 's;' +
  360. '}' +
  361. 'h1.nfkbd-no-border {' +
  362. ' font-size: 72pt;' +
  363. ' top: 45%;' +
  364. ' left: 0%;' +
  365. ' width: 100%;' +
  366. ' height: 100pt;' +
  367. '}' +
  368. 'h1.nfkbd-cirled {' +
  369. ' top: 45%;' +
  370. ' left: calc(50% - 130px);' +
  371. ' vertical-align: middle;' +
  372. ' transform: translate(0%, -50%);' +
  373. ' border: 3px solid white;' + // white circular border
  374. ' background: transparent;' +
  375. ' padding: 0px;' +
  376. ' padding-top: calc(120px - ' + (CIRCLE_TEXT_FONT_SIZE_PX/2) + 'px);' + // center the text in the circle
  377. ' font-size: ' + CIRCLE_TEXT_FONT_SIZE_PX + 'px;' +
  378. ' border-radius: ' + (CIRCLE_DIAMETER_PX/2) + 'px;' + // half of width and height
  379. ' width: ' + CIRCLE_DIAMETER_PX + 'px;' +
  380. ' height: calc(' + CIRCLE_DIAMETER_PX + 'px - (120px - ' + (CIRCLE_TEXT_FONT_SIZE_PX/2) + 'px)); ' + // remove the padding
  381. '}';
  382. const style = document.createElement('style');
  383. style.innerText = css;
  384. const body = document.querySelector('body');
  385. body.appendChild(style);
  386. }
  387. /**
  388. * Simulates mouse movement to get the player controls to show
  389. */
  390. function simulateMouseEvent(eventType) {
  391. const scrubber = document.querySelector('div.scrubber-container');
  392. if (!scrubber) {
  393. console.warn('Failed to simulate mouse movement');
  394. return;
  395. }
  396. const options = {'bubbles': true, 'button': 0, 'currentTarget': scrubber};
  397. scrubber.dispatchEvent(new MouseEvent(eventType, options));
  398. }
  399. function displayText(player, contents, inCircle) {
  400. const container = document.querySelector('div.controls-full-hit-zone');
  401. if (FADING_TEXT_DURATION_SEC <= 0.0 || !container) {
  402. return; // feature is disabled
  403. }
  404. removeAllCurrentText();
  405. const elt = document.createElement('h1');
  406. elt.classList.add('nfkbd-text', (inCircle ? 'nfkbd-cirled' : 'nfkbd-no-border'));
  407. elt.innerHTML = contents;
  408. // this is kind of hacky, but as far as I know there's no other way to get text to always be visible.
  409. simulateMouseEvent('mouseover'); // simulate mouse moving onto the scrubbing controls
  410. setTimeout(function() {
  411. simulateMouseEvent('mouseout'); // simulate mouse moving out of the scrubbing controls
  412. setTimeout(function() {
  413. container.appendChild(elt);
  414. setTimeout(function() { // schedule removal just before it fades out
  415. removeIfStillExists(elt);
  416. }, 0.9 * (FADING_TEXT_DURATION_SEC * 1000));
  417. }, 20);
  418. }, 20);
  419. }
  420. function removeAllCurrentText() {
  421. document.querySelectorAll('h1.nfkbd-text').forEach(removeIfStillExists);
  422. }
  423. function removeIfStillExists(elt) {
  424. if (elt.parentNode && elt.parentNode.contains(elt)) { // remove only if it hasn't already been removed by the clean-up method
  425. elt.parentNode.removeChild(elt);
  426. }
  427. }
  428. /* called on a timer to install play/pause callbacks or re-adjust the playback rate */
  429. function periodicCallback() {
  430. installPlayPauseCallbacks();
  431. reapplyPlaybackRate();
  432. }
  433. setInterval(periodicCallback, 100);
  434. installStyleBlock(); // installs the CSS `<style>` block the very first time we load the script
  435. /**
  436. * Installs the Netflix player object as `document.player` and a reference to the `<video>` tag as `document.video`.
  437. */
  438. function attachDebugObjects() {
  439. if (debug && (!document.player || !document.video)) {
  440. const player = getPlayer();
  441. const video = getVideoTag();
  442. if (player) {
  443. document.player = player;
  444. }
  445. if (video) {
  446. document.video = video;
  447. }
  448. if (!document.player || !document.video) {
  449. setTimeout(attachDebugObjects, 500); // try again soon
  450. }
  451. }
  452. }
  453. debug && attachDebugObjects();
  454. addEventListener("keydown", function(e) { // we need `keydown` instead of `keypress` to catch arrow presses
  455. if (e.ctrlKey || e.altKey) { // return early if any modifier key like Control or Alt is part of the key press
  456. return;
  457. }
  458. const KEYCODE_ZERO = 48; // keycode for character '0'
  459. const video = getVideoTag();
  460. const player = getPlayer();
  461. if (e.key === PICTURE_IN_PICTURE_KEY) {
  462. if (document.pictureInPictureElement) {
  463. document.exitPictureInPicture();
  464. } else if (video) {
  465. video.requestPictureInPicture();
  466. } else {
  467. console.error('Could not find a <video> tag to start PiP');
  468. }
  469. } else if (!player) {
  470. console.error('/!\\ No player object found, please update this script or report the issue if you are using the latest version');
  471. return;
  472. }
  473. // from now own, we know we have a `player` instance
  474. const scrubIndex = TIME_SCRUB_KEYS.map(o => o.key).indexOf(e.key);
  475. if (e.key === PLAY_PAUSE_KEY) {
  476. PLAY_PAUSE_SHOW && displayText(player, player.getPaused() ? '&#x25B6;' : 'II'); // play/pause
  477. player.getPaused() ? player.play() : player.pause();
  478. } else if (scrubIndex > -1) {
  479. const deltaSec = TIME_SCRUB_KEYS[scrubIndex].time;
  480. const newPosition = player.getCurrentTime() + deltaSec * 1000.0;
  481. TIME_SCRUB_SHOW && displayText(player, (deltaSec > 0 ? '+' : '') + deltaSec + 's');
  482. moveToPosition(player, limitRange(0, player.getDuration(), newPosition));
  483. } else if (e.key === ONE_FRAME_FORWARD_KEY && player.getPaused()) {
  484. skipFrame(player, +1);
  485. } else if (e.key === ONE_FRAME_BACKWARD_KEY && player.getPaused()) {
  486. skipFrame(player, -1);
  487. } else if (e.key === NEXT_EPISODE_KEY) {
  488. moveToPosition(player, 0.9999 * player.getDuration());
  489. } else if (e.key === PLAYBACK_SPEED_FASTER && video) {
  490. changePlaybackSpeed(player, video, +1);
  491. } else if (e.key === PLAYBACK_SPEED_SLOWER && video) {
  492. changePlaybackSpeed(player, video, -1);
  493. } else if (e.key === VOLUME_UP_KEY) {
  494. if (VOLUME_UP_KEY === 'ArrowUp') {
  495. console.warn('Netflix already raises the volume with "arrow up", we can\'t disable their handling');
  496. } else {
  497. player.setVolume(Math.min(1.0, player.getVolume() + VOLUME_DELTA));
  498. }
  499. } else if (e.key === VOLUME_DOWN_KEY) {
  500. if (VOLUME_UP_KEY === 'ArrowDown') {
  501. console.warn('Netflix already lowers the volume with "arrow down", we can\'t disable their handling');
  502. } else {
  503. player.setVolume(Math.max(0.0, player.getVolume() - VOLUME_DELTA));
  504. }
  505. } else if (e.key === MUTE_UNMUTE_KEY) {
  506. if (MUTE_UNMUTE_KEY === 'm') {
  507. console.warn('Netflix already mutes with "m"');
  508. } else {
  509. const muteState = player.getMuted();
  510. if (isBoolean(muteState)) { // make sure we got a valid state back
  511. player.setMuted(!muteState);
  512. }
  513. }
  514. } else if (e.key === NEXT_AUDIO_TRACK_KEY) {
  515. selectNeighborAudioTrack(player, +1);
  516. } else if (e.key === NEXT_AUDIO_TRACK_KEY.toUpperCase()) {
  517. selectNeighborAudioTrack(player, -1);
  518. } else if (e.key === SUBTITLES_NEXT_LANGUAGE_KEY) {
  519. selectNeighborSubtitlesTrack(player, +1);
  520. } else if (e.key === SUBTITLES_NEXT_LANGUAGE_KEY.toUpperCase()) {
  521. selectNeighborSubtitlesTrack(player, -1);
  522. } else if (NUMBER_KEYS_ENABLED && e.keyCode >= KEYCODE_ZERO && e.keyCode <= KEYCODE_ZERO + 9) {
  523. NUMBER_KEYS_SHOW && displayText(player, (e.keyCode - KEYCODE_ZERO) * 10 + '%', true);
  524. player.seek((e.keyCode - KEYCODE_ZERO) * (player.getDuration() / 10.0));
  525. } else if (e.key === SUBTITLES_ON_OFF_KEY) {
  526. switchSubtitles(player); // extracted for readability
  527. } else if (e.key === SUBTITLES_SIZE_KEY) {
  528. const currentSettings = player.getTimedTextSettings();
  529. if (currentSettings && currentSettings.size) {
  530. player.setTimedTextSettings({size: nextSubtitlesSize(currentSettings.size)});
  531. } else {
  532. console.warn('Unable to find current subtitles size');
  533. }
  534. } else if (e.key === DEBUG_PANEL_KEY) {
  535. toggleDebugPanel();
  536. }
  537. });
  538. })();