Netflix keyboard controls

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 a LOT more)

目前为 2021-01-24 提交的版本。查看 最新版本

// ==UserScript==
// @name         Netflix keyboard controls
// @namespace    netflix.keyboard
// @version      1.1
// @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 a LOT more)
// @match        https://netflix.com/*
// @match        https://www.netflix.com/*
// @grant        none
// @author       https://github.com/nicolasff
// ==/UserScript==

(function() {
    /* global netflix */
    'use strict';
    const debug = false;

    // 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)
    // to disable a particular feature, set the value to null.
    const PLAY_PAUSE_KEY = 'k';
    const PICTURE_IN_PICTURE_KEY = 'p'; // turns picture-in-picture on or off
    const SCRUB_FORWARD_KEY = 'l'; // skips ahead by `SCRUB_DELTA_SECONDS` (10s by default)
    const SCRUB_BACKWARD_KEY = 'j'; // goes back `SCRUB_DELTA_SECONDS` (10s by default)
    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
    const SUBTITLES_SIZE_KEY = 's';
    const SUBTITLES_NEXT_LANGUAGE_KEY = 'v'; // selects the next subtitles track
    const NEXT_AUDIO_TRACK_KEY = 'a'; // switches audio to the next track
    const MUTE_UNMUTE_KEY = null; // Netflix sets mute/unmute to 'm'. You can use a different key here.
    const VOLUME_UP_KEY = 'ArrowUp';
    const VOLUME_DOWN_KEY = 'ArrowDown';
    const NUMBER_KEYS_ENABLED = true; // press key 0 to jump to 0% (start of the video), 1 for 10%… up to 9 for 90%

    // these constants control the behavior of the shortcut keys above
    const SCRUB_DELTA_SECONDS = 10; // how much to skip forward/backward when using the forward/backward keys
    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%)
    const DEFAULT_SUBTITLES_LANGUAGE = 'English'; // change this to have the subtitles key pick a different language. Example values you can use: 'en', 'fr', 'es', 'zh-Hans', 'zh-Hant'...

    /***************************************************************************************************************************************************************************************************/


    /**
     * Gets a nested property inside an object
     */
    function getDeepProperty(obj, props) {
        var cur = obj;
        for (var key of props.split('.')) {
            const isFunction = key.endsWith('()');
            const attrName = isFunction ? key.substring(0, key.length-2) : key;
            if (!cur[attrName]) {
                return null;
            }
            cur = cur[attrName];
            if (isFunction && typeof cur === 'function') {
                cur = cur();
            }
        }
        return cur;
    }

    /**
     * Returns the "Player" object used by the Netflix web app to control video playback.
     */
    function getPlayer() {
        // let's try first the `netflix` object, a global variable exposed by the web app
        const videoPlayer = getDeepProperty(netflix, 'appContext.state.playerApp.getAPI().videoPlayer');
        if (videoPlayer && videoPlayer.getVideoPlayerBySessionId && videoPlayer.getAllPlayerSessionIds) {
            return videoPlayer.getVideoPlayerBySessionId(videoPlayer.getAllPlayerSessionIds()[0]);
        }
        debug && console.log('Player not found in the netflix object, trying React...');

        // or if not found that way, let's try to find it through React...
        const ctrl = document.querySelector('XXX.PlayerControls--control-element.progress-control');
        for (var key in ctrl) {
            if (key.startsWith('__reactInternalInstance$')) { // this is how to access the associated React instance
                const player = getDeepProperty(ctrl[key], 'memoizedProps.children.props.player');
                if (player) {
                    return player;
                }
            }
        }
        debug && console.warn('Player not found with React either :-/');
        return null;
    }

    function isBoolean(b) {
        return b === true || b === false;
    }

    /**
     * Returns the subtitles track for a given language.
     * Matches full name (e.g. "English") or a BCP 47 language code (e.g. "en")
     */
    function findSubtitlesTrack(player, language) {
        const tracks = player.getTimedTextTrackList();
        var selectedTrack = null;
        for (var i = 0; i < tracks.length; i++) {
            if ((tracks[i].displayName === language || tracks[i].bcp47 === language) && tracks[i].trackType === 'PRIMARY') {
                return tracks[i];
            }
        }
        return null;
    }

    /**
     * Returns the next size for subtitles
     */
    function nextSubtitlesSize(currentSize) {
        switch(currentSize) {
            case 'SMALL': return 'MEDIUM';
            case 'MEDIUM': return 'LARGE';
            case 'LARGE': return 'SMALL';
            default: // not found somehow
                return 'MEDIUM';
        }
    }

    var lastSelectedTextTrack = null; // caches the last non-"Off" language to have the `c` key switch between "Off" and that language.
    var preferredTextTrack = null; // caches the preferred language track

    function switchSubtitles(player, debug) {
        // select preferred language, once
        if (preferredTextTrack === null) {
            preferredTextTrack = findPreferredTextTrack(player, debug);
            debug && console.log('Found preferred text track:', preferredTextTrack);
        }

        // first, get current track to see if subtitles are currently visible
        const currentTrack = player.getTimedTextTrack();
        const disabledTrack = findSubtitlesTrack(player, 'Off');
        const currentlyDisabled = (currentTrack !== null && disabledTrack !== null && currentTrack.displayName === disabledTrack.displayName);

        // flip
        if (currentlyDisabled) {
            // do we have a last selected track? if so, switch back to it.
            if (lastSelectedTextTrack && lastSelectedTextTrack.displayName !== 'Off') { // avoid switching from "Off" to "Off"
                player.setTimedTextTrack(lastSelectedTextTrack);
            } else if (preferredTextTrack) { // otherwise, switch to preferred language
                player.setTimedTextTrack(preferredTextTrack);
            } else {
                console.warn("No last selected subtitles track to go back to, and couldn't find subtitles in the preferred language,", DEFAULT_SUBTITLES_LANGUAGE);
            }
        } else { // currently enabled, so we're switching to "Off".
            player.setTimedTextTrack(disabledTrack);
        }
        lastSelectedTextTrack = currentTrack; // and remember what we just switched from
    }

    function findPreferredTextTrack(player, debug) {
        var chosenTrack = findSubtitlesTrack(player, DEFAULT_SUBTITLES_LANGUAGE);
        if (!chosenTrack) {
            console.warn('Could not find subtitles in ' + DEFAULT_SUBTITLES_LANGUAGE + (DEFAULT_SUBTITLES_LANGUAGE !== 'English' ? ', defaulting to English' : ''));
            chosenTrack = findSubtitlesTrack(player, 'English');
            if (!chosenTrack) {
                DEFAULT_SUBTITLES_LANGUAGE !== 'English' && console.warn('Could not find subtitles in English either :-/');
            }
        }
        return chosenTrack; // might be null
    }

    function selectNextAudioTrack(player, debug) {
        const trackList = player.getAudioTrackList();
        const currentTrack = player.getAudioTrack();
        if (!trackList || !currentTrack) {
            console.warn('Could not find the current audio track or the list of audio tracks');
        }

        for (var i = 0; i < trackList.length; i++) {
            if (currentTrack.displayName === trackList[i].displayName) { // found!
                const nextTrack = trackList[(i+1) % trackList.length];
                debug && console.log('Switching audio track to ' + nextTrack.displayName);
                player.setAudioTrack(nextTrack);
                return;
            }
        }
    }

    function selectNextSubtitlesTrack(player, debug) {
        const trackList = player.getTimedTextTrackList();
        const currentTrack = player.getTimedTextTrack();
        if (!trackList || !currentTrack) {
            console.warn('Could not find the current subtitles track or the list of subtitles tracks');
        }

        for (var i = 0; i < trackList.length; i++) {
            if (currentTrack.trackId === trackList[i].trackId) { // found!
                const nextTrack = trackList[(i+1) % trackList.length];
                debug && console.log('Switching subtitles track to ' + nextTrack.displayName);
                player.setTimedTextTrack(nextTrack);
                return;
            }
        }
    }

    addEventListener("keydown", function(e) { // we need `keydown` instead of `keypress` to catch arrow presses
        if (e.ctrlKey || e.altKey) { // return early if any modifier key like Control or Alt is part of the key press
            return;
        }
        const KEYCODE_ZERO = 48; // keycode for character '0'
        const videos = document.getElementsByTagName('video');
        const video = videos && videos.length === 1 ? videos[0] : null;
        const player = getPlayer();
        debug && console.log('Key press:', e);

        if (e.key === PICTURE_IN_PICTURE_KEY) {
            if (document.pictureInPictureElement) {
                document.exitPictureInPicture();
            } else {
                video && video.requestPictureInPicture();
            }
        } else if (!player) {
            console.error('No player object found, please update this script');
            return;
        }
        // from now own, we know we have a `player` instance
        if (e.key === PLAY_PAUSE_KEY) {
            player.getPaused() ? player.play() : player.pause();
        } else if (e.key === SCRUB_FORWARD_KEY) {
            player.seek(Math.min(player.getDuration(), player.getCurrentTime() + SCRUB_DELTA_SECONDS));
        } else if (e.key === SCRUB_BACKWARD_KEY) {
            player.seek(Math.max(0, player.getCurrentTime() - SCRUB_DELTA_SECONDS));
        } else if (e.key === VOLUME_UP_KEY) {
            player.setVolume(Math.min(1.0, player.getVolume() + VOLUME_DELTA));
        } else if (e.key === VOLUME_DOWN_KEY) {
            player.setVolume(Math.max(0.0, player.getVolume() - VOLUME_DELTA));
        } else if (e.key === MUTE_UNMUTE_KEY) {
            if (MUTE_UNMUTE_KEY === 'm') {
                console.warn('Netflix already mutes with "m"');
            } else {
                const muteState = player.getMuted();
                if (isBoolean(muteState)) { // make sure we got a valid state back
                    player.setMuted(!muteState);
                }
            }
        } else if (e.key === NEXT_AUDIO_TRACK_KEY) {
            selectNextAudioTrack(player, debug);
        } else if (e.key === SUBTITLES_NEXT_LANGUAGE_KEY) {
            selectNextSubtitlesTrack(player, debug);
        } else if (NUMBER_KEYS_ENABLED && e.keyCode >= KEYCODE_ZERO && e.keyCode <= KEYCODE_ZERO + 9) {
            player.seek((e.keyCode - KEYCODE_ZERO) * (player.getDuration() / 10.0));
        } else if (e.key === SUBTITLES_ON_OFF_KEY) {
            switchSubtitles(player, debug); // extracted for readability
        } else if (e.key === SUBTITLES_SIZE_KEY) {
            const currentSettings = player.getTimedTextSettings();
            if (currentSettings && currentSettings.size) {
                player.setTimedTextSettings({size: nextSubtitlesSize(currentSettings.size)});
            } else {
                console.warn('Unable to find current subtitles size');
            }
        }
    });
})();

QingJ © 2025

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