您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Add keyboard support to native HTML5 video player.
- // ==UserScript==
- // @name Better-Native-Video
- // @namespace https://tribbe.dev
- // @version 1.0.1
- // @description Add keyboard support to native HTML5 video player.
- // @author Tribbe
- // @include http://*/*
- // @include https://*/*
- // ==/UserScript==
- "use strict";
- const videoAttribute = "betterHtml5VideoType",
- timeoutAttribute = "betterHtml5VideoClickTimeout";
- let toggleChecked, toggleEnabled, observer, dirVideo, settings = {
- firstClick: "focus",
- dblFullScreen: true,
- clickDelay: 0.3,
- skipNormal: 5,
- skipShift: 10,
- skipCtrl: 1,
- allowWOControls: false,
- };
- const shortcutFuncs = {
- toggleCaptions: function(v){
- const validTracks = [];
- for(let i = 0; i < v.textTracks.length; ++i){
- const tt = v.textTracks[i];
- if(tt.mode === "showing"){
- tt.mode = "disabled";
- if(v.textTracks.addEventListener){
- // If text track event listeners are supported
- // (they are on the most recent Chrome), add
- // a marker to remember the old track. Use a
- // listener to delete it if a different track
- // is selected.
- v.cbhtml5vsLastCaptionTrack = tt.label;
- function cleanup(e){
- for(let i = 0; i < v.textTracks.length; ++i){
- const ott = v.textTracks[i];
- if(ott.mode === "showing"){
- delete v.cbhtml5vsLastCaptionTrack;
- v.textTracks.removeEventListener("change", cleanup);
- return;
- }
- }
- }
- v.textTracks.addEventListener("change", cleanup);
- }
- return;
- }else if(tt.mode !== "hidden"){
- validTracks.push(tt);
- }
- }
- // If we got here, none of the tracks were selected.
- if(validTracks.length === 0){
- return true; // Do not prevent default if no UI activated
- }
- // Find the best one and select it.
- validTracks.sort(function(a, b){
- if(v.cbhtml5vsLastCaptionTrack){
- const lastLabel = v.cbhtml5vsLastCaptionTrack;
- if(a.label === lastLabel && b.label !== lastLabel){
- return -1;
- }else if(b.label === lastLabel && a.label !== lastLabel){
- return 1;
- }
- }
- const aLang = a.language.toLowerCase(),
- bLang = b.language.toLowerCase(),
- navLang = navigator.language.toLowerCase();
- if(aLang === navLang && bLang !== navLang){
- return -1;
- }else if(bLang === navLang && aLang !== navLang){
- return 1;
- }
- const aPre = aLang.split("-")[0],
- bPre = bLang.split("-")[0],
- navPre = navLang.split("-")[0];
- if(aPre === navPre && bPre !== navPre){
- return -1;
- }else if(bPre === navPre && aPre !== navPre){
- return 1;
- }
- return 0;
- })[0].mode = "showing";
- },
- togglePlay: function(v){
- if (v.paused) {
- v.play();
- } else {
- v.pause();
- }
- },
- toStart: function(v){
- v.currentTime = 0;
- },
- toEnd: function(v){
- v.currentTime = v.duration;
- },
- skipLeft: function(v,key,shift,ctrl){
- if (shift) {
- v.currentTime -= settings.skipShift;
- } else if(ctrl) {
- v.currentTime -= settings.skipCtrl;
- } else {
- v.currentTime -= settings.skipNormal;
- }
- },
- skipRight: function(v,key,shift,ctrl){
- if (shift) {
- v.currentTime += settings.skipShift;
- } else if (ctrl) {
- v.currentTime += settings.skipCtrl;
- } else {
- v.currentTime += settings.skipNormal;
- }
- },
- increaseVol: function(v){
- if (v.volume <= 0.9) v.volume += 0.1;
- else v.volume = 1;
- },
- decreaseVol: function(v){
- if (v.volume >= 0.1) v.volume -= 0.1;
- else v.volume = 0;
- },
- toggleMute: function(v){
- v.muted = !v.muted;
- },
- toggleFS: function(v){
- if (document.webkitFullscreenElement) {
- document.webkitExitFullscreen();
- } else {
- v.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT);
- }
- },
- reloadVideo: function(v){
- const currTime = v.currentTime;
- v.load();
- v.currentTime = currTime;
- },
- slowOrPrevFrame: function(v,key,shift){
- if (shift) { // Less-Than
- v.playbackRate -= 0.25;
- } else { // Comma
- v.currentTime -= 1/60;
- }
- },
- fastOrNextFrame: function(v,key,shift){
- if (shift) { // Greater-Than
- v.playbackRate += 0.25;
- } else { // Period
- v.currentTime += 1/60;
- }
- },
- normalSpeed: function(v,key,shift){
- if(shift) { // ?
- v.playbackRate = v.defaultPlaybackRate;
- }
- },
- toPercentage: function(v,key){
- v.currentTime = v.duration * (key - 48) / 10.0;
- },
- };
- const keyFuncs = {
- 32 : shortcutFuncs.togglePlay,// Space
- 75 : shortcutFuncs.togglePlay, // K
- 35 : shortcutFuncs.toEnd,// End
- 48 : shortcutFuncs.toStart,// 0
- 36 : shortcutFuncs.toStart,// Home
- 37 : shortcutFuncs.skipLeft,// Left arrow
- 74 : shortcutFuncs.skipLeft,// J
- 39 : shortcutFuncs.skipRight,// Right arrow
- 76 : shortcutFuncs.skipRight,// L
- 38 : shortcutFuncs.increaseVol,// Up arrow
- 40 : shortcutFuncs.decreaseVol,// Down arrow
- 77 : shortcutFuncs.toggleMute,// M
- 70 : shortcutFuncs.toggleFS,// F
- 67 : shortcutFuncs.toggleCaptions,// C
- 82 : shortcutFuncs.reloadVideo,// R
- 188: shortcutFuncs.slowOrPrevFrame,// Comma or Less-Than
- 190: shortcutFuncs.fastOrNextFrame,// Period or Greater-Than
- 191: shortcutFuncs.normalSpeed,// Forward slash or ?
- 49 : shortcutFuncs.toPercentage,// 1
- 50 : shortcutFuncs.toPercentage,// 2
- 51 : shortcutFuncs.toPercentage,// 3
- 52 : shortcutFuncs.toPercentage,// 4
- 53 : shortcutFuncs.toPercentage,// 5
- 54 : shortcutFuncs.toPercentage,// 6
- 55 : shortcutFuncs.toPercentage,// 7
- 56 : shortcutFuncs.toPercentage,// 8
- 57 : shortcutFuncs.toPercentage,// 9
- };
- function registerDirectVideo(v, force){
- ignoreAllIndirectVideos();
- if(dirVideo){
- ignoreDirectVideo();
- }
- if(force !== undefined ? force : v.hasAttribute("controls")){
- dirVideo = v;
- v.dataset[videoAttribute] = "direct";
- }else{
- v.dataset[videoAttribute] = "";
- }
- }
- function ignoreDirectVideo(reregister){
- if(reregister && document.body.contains(dirVideo)){
- registerVideo(dirVideo);
- dirVideo.focus();
- }else{
- dirVideo.dataset[videoAttribute] = "";
- }
- dirVideo = undefined;
- }
- function registerVideo(v, force){
- v.dataset[videoAttribute] =
- (force !== undefined ? force : v.hasAttribute("controls")) ?
- "normal" : "";
- }
- function ignoreVideo(v){
- v.dataset[videoAttribute] = "";
- }
- function registerAllNewVideos(vs){
- for(let i = vs.length - 1; i >= 0; --i){
- if(vs[i].dataset[videoAttribute] === undefined){
- registerVideo(vs[i]);
- }
- }
- }
- function ignoreAllIndirectVideos(){
- const rv = document.getElementsByTagName("video");
- for(let i = rv.length - 1; i >= 0; --i){
- if(rv[i] !== dirVideo) ignoreVideo(rv[i]);
- }
- }
- function isValidTarget(el){
- return (
- (dirVideo && (el === dirVideo
- || el === document.body
- || el === document.documentElement))
- || (el.dataset && el.dataset[videoAttribute])
- );
- }
- function handleClick(e){
- if(!isValidTarget(e.target)){
- return true; // Do not prevent default
- }
- const v = dirVideo || e.target;
- if(settings.firstClick === "play" || dirVideo || document.activeElement === v){
- if(v.dataset[timeoutAttribute]){
- clearTimeout(v.dataset[timeoutAttribute]|0);
- delete v.dataset[timeoutAttribute];
- }
- if(settings.dblFullScreen && settings.clickDelay > 0){
- v.dataset[timeoutAttribute] = setTimeout(function(){
- shortcutFuncs.togglePlay(v);
- delete v.dataset[timeoutAttribute];
- }, settings.clickDelay * 1000);
- }else{
- shortcutFuncs.togglePlay(v);
- }
- }
- v.focus();
- e.preventDefault();
- e.stopPropagation();
- return false
- }
- function handleDblClick(e){
- if(!(settings.dblFullScreen && isValidTarget(e.target))){
- return true; // Do not prevent default
- }
- const v = dirVideo || e.target;
- if(v.dataset[timeoutAttribute]){
- clearTimeout(v.dataset[timeoutAttribute]|0);
- delete v.dataset[timeoutAttribute];
- }
- shortcutFuncs.toggleFS(v);
- e.preventDefault();
- e.stopPropagation();
- return false
- }
- function handleKeyDown(e){
- if(!isValidTarget(e.target) || e.altKey || e.metaKey){
- return true; // Do not activate
- }
- const func = keyFuncs[e.keyCode];
- if(func){
- if((func.length < 3 && e.shiftKey) ||
- (func.length < 4 && e.ctrlKey)){
- return true; // Do not activate
- }
- func(dirVideo || e.target, e.keyCode, e.shiftKey, e.ctrlKey);
- e.preventDefault();
- e.stopPropagation();
- return false;
- }
- return true; // Do not prevent default if no UI activated
- }
- function handleKeyOther(e){
- if(!isValidTarget(e.target) || e.altKey || e.metaKey){
- return true; // Do not prevent default
- }
- const func = keyFuncs[e.keyCode];
- if(func){
- if((func.length < 3 && e.shiftKey) ||
- (func.length < 4 && e.ctrlKey)){
- return true; // Do not prevent default
- }
- e.preventDefault();
- e.stopPropagation();
- return false;
- }
- return true; // Do not prevent default if no UI activated
- }
- function handleFullscreen(){
- if(document.webkitFullscreenElement
- && document.webkitFullscreenElement.dataset[videoAttribute]){
- document.webkitFullscreenElement.focus();
- }
- }
- function handleMutationRecords(mrs){
- for(let i = mrs.length - 1; i >= 0; --i){
- if(mrs[i].attributeName === "controls"){
- const t = mrs[i].target;
- if(!t.hasAttribute("controls")){
- switch(t.dataset[videoAttribute]){
- case "direct":
- ignoreDirectVideo(false);
- break;
- case "normal":
- ignoreVideo(t);
- break;
- }
- }else if(t.tagName.toLowerCase() === "video"){
- if(document.body.children.length === 1
- && document.body.firstElementChild === t){
- registerDirectVideo(t);
- }else{
- registerVideo(t);
- t.focus();
- }
- }
- }else if(mrs[i].type === "childList"){
- if(dirVideo && (document.body.children.length !== 1
- || document.body.firstElementChild !== dirVideo)){
- ignoreDirectVideo(true);
- }
- if(mrs[i].removedNodes){
- for(let j = mrs[i].removedNodes.length - 1; j >= 0; --j){
- if(mrs[i].removedNodes[j] === dirVideo){
- ignoreDirectVideo();
- }
- // No need to ignore other videos currently,
- // as it's just setting an attribute.
- }
- }
- if(document.body.children.length === 1
- && document.body.firstElementChild !== dirVideo
- && document.body.firstElementChild.tagName.toLowerCase() === "video"
- && document.body.firstElementChild.dataset[videoAttribute] !== ""){
- registerDirectVideo(document.body.firstElementChild);
- }else if(mrs[i].addedNodes){
- for(let j = mrs[i].addedNodes.length - 1; j >= 0; --j){
- const an = mrs[i].addedNodes[j];
- if(an.tagName && an.tagName.toLowerCase() === "video"){
- if(an.dataset[videoAttribute] === undefined){
- registerVideo(an);
- }
- }else if(an.getElementsByTagName){
- registerAllNewVideos(an.getElementsByTagName("video"));
- }
- }
- }
- }
- }
- }
- function enableExtension(){
- // useCapture: Handler fired while event is bubbling down instead of up
- document.addEventListener("webkitfullscreenchange", handleFullscreen, true);
- document.addEventListener("click", handleClick, true);
- document.addEventListener("dblclick", handleDblClick, true);
- document.addEventListener("keydown", handleKeyDown, true);
- document.addEventListener("keypress", handleKeyOther, true);
- document.addEventListener("keyup", handleKeyOther, true);
- observer = observer || new MutationObserver(handleMutationRecords);
- observer.observe(document.body, {
- childList: true,
- attributes: true,
- attributeFilter: ["controls"],
- subtree: true
- });
- if(document.body.children.length === 1
- && document.body.firstElementChild.tagName.toLowerCase() === "video"
- && document.body.firstElementChild.dataset[videoAttribute] !== ""){
- registerDirectVideo(document.body.firstElementChild);
- }else{
- registerAllNewVideos(document.getElementsByTagName("video"));
- }
- }
- enableExtension();