YouTube HTML5 Video Pan And Zoom

Add controls to pan and zoom HTML5 video.

目前為 2024-08-07 提交的版本,檢視 最新版本

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