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

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址