YouTube HTML5 Video Pan And Zoom

Add controls to pan and zoom HTML5 video.

  1. // ==UserScript==
  2. // @name YouTube HTML5 Video Pan And Zoom
  3. // @namespace YouTubeHTML5VideoPanAndZoom
  4. // @version 1.1.9
  5. // @license AGPLv3
  6. // @author jcunews
  7. // @description Add controls to pan and zoom HTML5 video.
  8. // @include https://www.youtube.com/watch*
  9. // @grant none
  10. // ==/UserScript==
  11.  
  12. (() => {
  13. var to = {createHTML: s => s, createScript: s => s}, tp = window.trustedTypes?.createPolicy ? trustedTypes.createPolicy("", to) : to;
  14. var html = s => tp.createHTML(s), script = s => tp.createScript(s);
  15. var ele = document.createElement("SCRIPT");
  16. ele.text = script("(" + (function() {
  17. var resizeUpdateDelay = 300;
  18. var eleVideo, baseWidth, baseHeight, posX, posY, deltaX, deltaY, scaleX, scaleY;
  19. var eleContainer, containerWidth, containerHeight, configs;
  20. var changing = false, timerIdChange = 0, timerIdUpdateAll = 0;
  21. var to = {createHTML: s => s, createScript: s => s}, tp = window.trustedTypes?.createPolicy ? trustedTypes.createPolicy("", to) : to;
  22. var html = s => tp.createHTML(s), script = s => tp.createScript(s);
  23.  
  24. function doneChange() {
  25. changing = false;
  26. clearTimeout(timerIdChange);
  27. timerIdChange = 0;
  28. }
  29.  
  30. function doChange() {
  31. changing = true;
  32. if (timerIdChange) clearTimeout(timerIdChange);
  33. timerIdChange = setTimeout(doneChange, 100);
  34. }
  35.  
  36. function setPos(dx, dy) {
  37. var rw = 1, rh = 1;
  38. if (document.fullscreen) {
  39. rw = screen.width / eleContainer.offsetWidth;
  40. rh = screen.height / eleContainer.offsetHeight;
  41. }
  42. if (dx !== undefined) {
  43. deltaX += dx;
  44. deltaY += dy;
  45. } else {
  46. posX = 0;
  47. posY = 0;
  48. deltaX = 0;
  49. deltaY = 0;
  50. }
  51. doChange();
  52. eleVideo.style.left = ((posX + deltaX) * rw) + "px";
  53. eleVideo.style.top = ((posY + deltaY) * rh) + "px";
  54. eleVideo.style.width = "100%";
  55. }
  56.  
  57. function setSize(dx, dy) {
  58. var rw = 1, rh = 1;
  59. if (document.fullscreen) {
  60. rw = screen.width / eleContainer.offsetWidth;
  61. rh = screen.height / eleContainer.offsetHeight;
  62. }
  63. if (dx !== undefined) {
  64. scaleX += dx;
  65. scaleY += dy;
  66. } else {
  67. scaleX = 1;
  68. scaleY = 1;
  69. }
  70. doChange();
  71. eleVideo.style.MozTransform = eleVideo.style.WebkitTransform =
  72. "scaleX(" + (scaleX*rw).toFixed(2) + ") scaleY(" + (scaleY*rh).toFixed(2) + ")";
  73. }
  74.  
  75. function updateAll() {
  76. var rw = 1, rh = 1, px = posX + deltaX, py = posY + deltaY;
  77. if (document.fullscreen) {
  78. rw = screen.width / eleContainer.offsetWidth;
  79. rh = screen.height / eleContainer.offsetHeight;
  80. }
  81. doChange();
  82. eleVideo.style.left = (px * rw).toFixed(0) + "px";
  83. eleVideo.style.top = (py * rh).toFixed(0) + "px";
  84. eleVideo.style.width = "100%";
  85. eleVideo.style.MozTransform = eleVideo.style.WebkitTransform =
  86. "scaleX(" + (scaleX*rw).toFixed(2) + ") scaleY(" + (scaleY*rh).toFixed(2) + ")";
  87. vpzConfigs.style.top = document.fullscreen ? "-3px" : "";
  88. clearTimeout(timerIdUpdateAll);
  89. timerIdUpdateAll = 0;
  90. }
  91.  
  92. function delayedUpdateAll() {
  93. if (timerIdUpdateAll) clearTimeout(timerIdUpdateAll);
  94. timerIdUpdateAll = setTimeout(updateAll, 100);
  95. }
  96.  
  97. function setup() {
  98. var vpzPanel = window.vpzPanel;
  99. if (!vpzPanel) {
  100. vpzPanel = document.createElement("DIV");
  101. vpzPanel.id = "vpzPanel";
  102. vpzPanel.innerHTML = html(`<style>
  103. #vpzPanel{position:relative;float:left;margin:10px 0 0 20px;white-space:nowrap}
  104. #vpzPanel button{vertical-align:top;border:none;border-radius:3px;padding:0;width:18px;height:18px;line-height:0;font-size:15px;font-weight:bold;cursor:pointer}
  105. #vpzPanel button:hover{background:#bdb}
  106. #vpzMoveLeft{margin-left:0}
  107. #vpzMoveL,#vpzShrink,#vpzShrinkH,#vpzShrinkV,#vpzConfig{margin-left:10px!important}
  108. #vpzCfgContainer{display:none;position:absolute;z-index:99;right:0;bottom:55px;padding:5px;line-height:normal;background:#555}
  109. #vpzCfgContainer button{height:21px;padding:0 5px}
  110. #vpzConfigs{position:relative}
  111. #vpzConfigs~button{width:auto}
  112. </style>
  113. <button id="vpzReset" class="yt-uix-button-default" title="Reset">0</button>
  114. <button id="vpzMoveL" class="yt-uix-button-default" title="Move Left">&#x2190;</button>
  115. <button id="vpzMoveU" class="yt-uix-button-default" title="Move Up">&#x2191;</button>
  116. <button id="vpzMoveD" class="yt-uix-button-default" title="Move Down">&#x2193;</button>
  117. <button id="vpzMoveR" class="yt-uix-button-default" title="Move Right">&#x2192;</button>
  118. <button id="vpzShrink" class="yt-uix-button-default" title="Shrink">&#x2199;</button>
  119. <button id="vpzExpand" class="yt-uix-button-default" title="Expand">&#x2197;</button>
  120. <button id="vpzShrinkH" class="yt-uix-button-default" title="Shrink Horizontal">&#x21C7;</button>
  121. <button id="vpzExpandH" class="yt-uix-button-default" title="Expand Horizontal">&#x21C9;</button>
  122. <button id="vpzShrinkV" class="yt-uix-button-default" title="Shrink Vertical">&#x21CA;</button>
  123. <button id="vpzExpandV" class="yt-uix-button-default" title="Expand Vertical">&#x21C8;</button>
  124. <button id="vpzConfig" class="yt-uix-button-default" title="Show/Hide Profiles Panel">P</button>
  125. <div id="vpzCfgContainer">
  126. Configs: <select id="vpzConfigs"></select>
  127. <button id="vpzSaveCfg" class="yt-uix-button-default">Save</button>
  128. <button id="vpzLoadCfg" class="yt-uix-button-default">Load</button>
  129. <button id="vpzDelCfg" class="yt-uix-button-default">Delete</button>
  130. </div>`);
  131. var a = window["movie_player"].querySelector(".ytp-chrome-controls .ytp-right-controls");
  132. a.parentNode.insertBefore(vpzPanel, a);
  133.  
  134. vpzReset.onclick = function() {
  135. setPos();
  136. setSize();
  137. };
  138.  
  139. vpzMoveL.onclick = function() {
  140. setPos(-8, 0);
  141. };
  142. vpzMoveU.onclick = function() {
  143. setPos(0, -8);
  144. };
  145. vpzMoveD.onclick = function() {
  146. setPos(0, 8);
  147. };
  148. vpzMoveR.onclick = function() {
  149. setPos(8, 0);
  150. };
  151.  
  152. vpzShrink.onclick = function() {
  153. setSize(-0.01, -0.01);
  154. };
  155. vpzExpand.onclick = function() {
  156. setSize(0.01, 0.01);
  157. };
  158.  
  159. vpzShrinkH.onclick = function() {
  160. setSize(-0.01, 0);
  161. };
  162. vpzExpandH.onclick = function() {
  163. setSize(0.01, 0);
  164. };
  165.  
  166. vpzShrinkV.onclick = function() {
  167. setSize(0, -0.01);
  168. };
  169. vpzExpandV.onclick = function() {
  170. setSize(0, 0.01);
  171. };
  172.  
  173. vpzConfig.onclick = function() {
  174. vpzCfgContainer.style.display = vpzCfgContainer.style.display ? "" : "block";
  175. };
  176.  
  177. var i, opt;
  178. for (i = 0; i < configs.length; i++) {
  179. opt = document.createElement("OPTION");
  180. opt.value = i;
  181. opt.textContent = configs[i].name;
  182. vpzConfigs.appendChild(opt);
  183. }
  184.  
  185. function configIndex(cfgName) {
  186. for (var i = configs.length-1; i >= 0; i--) {
  187. if (configs[i].name === cfgName) {
  188. return i;
  189. break;
  190. }
  191. }
  192. return -1;
  193. }
  194. function optionIndex(idx) {
  195. for (var i = configs.length-1; i >= 0; i--) {
  196. if (vpz.options[i].value == idx) {
  197. return i;
  198. break;
  199. }
  200. }
  201. return -1;
  202. }
  203.  
  204. vpzSaveCfg.onclick = function() {
  205. var cfgName, idx, i, opt;
  206. if (vpzConfigs.selectedIndex >= 0) {
  207. cfgName = vpzConfigs.selectedOptions[0].textContent;
  208. } else {
  209. cfgName = "";
  210. }
  211. cfgName = prompt("Enter configuration name.", cfgName);
  212. if (cfgName === null) return;
  213. cfgName = cfgName.trim();
  214. if (!cfgName) return;
  215. idx = configIndex(cfgName);
  216. if (idx >= 0) {
  217. if (!confirm("Replace existing configuration?")) return;
  218. vpzConfigs.options[optionIndex(idx)].textContent = cfgName;
  219. } else {
  220. idx = configs.length;
  221. opt = document.createElement("OPTION");
  222. opt.value = idx;
  223. opt.textContent = cfgName;
  224. vpzConfigs.appendChild(opt);
  225. vpzConfigs.selectedIndex = idx;
  226. }
  227. configs.splice(idx, 1, {
  228. name: cfgName,
  229. data: [deltaX, deltaY, scaleX, scaleY]
  230. });
  231. localStorage.vpzConfigs = JSON.stringify(configs);
  232. };
  233. vpzLoadCfg.onclick = function() {
  234. var idx;
  235. if (vpzConfigs.selectedIndex < 0) return;
  236. idx = parseInt(vpzConfigs.selectedOptions[0].value);
  237. setPos();
  238. setPos(configs[idx].data[0], configs[idx].data[1]);
  239. scaleX = 0; scaleY = 0;
  240. setSize(configs[idx].data[2], configs[idx].data[3]);
  241. };
  242. vpzDelCfg.onclick = function() {
  243. if ((vpzConfigs.selectedIndex < 0) || !confirm("Delete selected configuration?")) return;
  244. configs.splice(vpzConfigs.selectedOptions[0].value, 1);
  245. localStorage.vpzConfigs = JSON.stringify(configs);
  246. vpzConfigs.removeChild(vpzConfigs.selectedOptions[0]);
  247. };
  248.  
  249. }
  250. }
  251.  
  252. function init() {
  253. eleVideo = document.querySelector(".html5-main-video");
  254. if (eleVideo) {
  255. baseWidth = eleVideo.offsetWidth;
  256. baseHeight = eleVideo.offsetHeight;
  257. posX = eleVideo.offsetLeft;
  258. posY = eleVideo.offsetTop;
  259. deltaX = 0;
  260. deltaY = 0;
  261. scaleX = 1;
  262. scaleY = 1;
  263. eleContainer = eleVideo.parentNode.parentNode;
  264. containerWidth = eleContainer.offsetWidth;
  265. containerHeight = eleContainer.offsetHeight;
  266. configs = JSON.parse(localStorage.vpzConfigs || "[]");
  267. if (eleVideo.videoHeight) {
  268. eleVideo.style.left = "0px";
  269. eleVideo.style.top = "0px";
  270. eleVideo.style.width = "100%";
  271. baseWidth = eleVideo.offsetWidth;
  272. baseHeight = eleVideo.offsetHeight;
  273. setup();
  274. } else {
  275. setTimeout(init, 100);
  276. }
  277. (new MutationObserver(function(records, obj) {
  278. if (!changing) delayedUpdateAll();
  279. })).observe(eleVideo, {
  280. attributes: true,
  281. attributeFilter: ["style"]
  282. });
  283. }
  284. }
  285. init();
  286. addEventListener("spfdone", init, false);
  287.  
  288. addEventListener("resize", function() {
  289. if (!eleVideo || !window.vpzConfigs) return;
  290. setTimeout(updateAll, resizeUpdateDelay);
  291. }, false);
  292. }).toString() + ")()");
  293. document.head.appendChild(ele);
  294. })()

QingJ © 2025

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