PiPifier

PiPifier is an extension that lets you use every HTML5 video in Picture in Picture mode

目前为 2018-11-09 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name PiPifier
  3. // @namespace https://github.com/Willian-Zhang/PiPifier
  4. // @version 0.2
  5. // @description PiPifier is an extension that lets you use every HTML5 video in Picture in Picture mode
  6. // @author @arno_app <https://twitter.com/arno_app>, @Cacauu_de <https://twitter.com/Cacauu_de>, @Willian <https://github.com/willian-zhang>
  7. // @match */*
  8. // @grant none
  9. // ==/UserScript==
  10.  
  11. //image URLs
  12. var whiteSVG_Icon = `data:image/svg+xml;utf8,<?xml version="1.0" encoding="UTF-8"?>
  13. <svg width="671px" height="441px" viewBox="0 0 671 441" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
  14. <!-- Generator: Sketch 40.3 (33839) - http://www.bohemiancoding.com/sketch -->
  15. <title>PiP_Toolbar_Icon_white</title>
  16. <desc>Created with Sketch.</desc>
  17. <defs>
  18. <polyline id="path-1" points="617.200445 188.359322 617.200445 0 0 0 0 366.254237 252.55902 366.254237"></polyline>
  19. <mask id="mask-2" maskContentUnits="userSpaceOnUse" maskUnits="objectBoundingBox" x="0" y="0" width="617.200445" height="366.254237" fill="white">
  20. <use xlink:href="#path-1"></use>
  21. </mask>
  22. </defs>
  23. <g id="UI" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
  24. <g id="PiP_Toolbar_Icon_white" transform="translate(-15.000000, -130.000000)">
  25. <g id="Toolbar-Icon" transform="translate(15.000000, 130.000000)">
  26. <use id="Combined-Shape" stroke="#FFFFFF" mask="url(#mask-2)" stroke-width="54" xlink:href="#path-1"></use>
  27. <rect id="Rectangle" fill="#FFFFFF" x="263.020045" y="197.328814" width="407.979955" height="243.671186"></rect>
  28. <path d="M166.149412,103.362155 L166.149412,245.485858 L131.742911,245.485858 L131.742911,103.706611 L89.1651635,115.230311 L149.582508,5.46510893 L209.999853,115.230311 L166.149412,103.362155 Z" id="Combined-Shape" fill="#FFFFFF" transform="translate(149.582508, 125.475483) rotate(-236.000000) translate(-149.582508, -125.475483) "></path>
  29. </g>
  30. </g>
  31. </g>
  32. </svg>`;
  33. var blackSVG_Icon = `data:image/svg+xml;utf8,<?xml version="1.0" encoding="UTF-8"?>
  34. <svg width="701px" height="701px" viewBox="0 0 701 701" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
  35. <!-- Generator: Sketch 40.3 (33839) - http://www.bohemiancoding.com/sketch -->
  36. <title>PiP_Toolbar_Icon</title>
  37. <desc>Created with Sketch.</desc>
  38. <defs>
  39. <polyline id="path-1" points="617.200445 188.359322 617.200445 0 0 0 0 366.254237 252.55902 366.254237"></polyline>
  40. <mask id="mask-2" maskContentUnits="userSpaceOnUse" maskUnits="objectBoundingBox" x="0" y="0" width="617.200445" height="366.254237" fill="white">
  41. <use xlink:href="#path-1"></use>
  42. </mask>
  43. </defs>
  44. <g id="UI" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
  45. <g id="PiP_Toolbar_Icon">
  46. <g id="Toolbar-Icon" transform="translate(15.000000, 130.000000)">
  47. <use id="Combined-Shape" stroke="#000000" mask="url(#mask-2)" stroke-width="54" xlink:href="#path-1"></use>
  48. <rect id="Rectangle" fill="#000000" x="263.020045" y="197.328814" width="407.979955" height="243.671186"></rect>
  49. <path d="M166.149412,103.362155 L166.149412,245.485858 L131.742911,245.485858 L131.742911,103.706611 L89.1651635,115.230311 L149.582508,5.46510893 L209.999853,115.230311 L166.149412,103.362155 Z" id="Combined-Shape" fill="#000000" transform="translate(149.582508, 125.475483) rotate(-236.000000) translate(-149.582508, -125.475483) "></path>
  50. </g>
  51. </g>
  52. </g>
  53. </svg>`;
  54.  
  55. //safari.self.addEventListener("message", messageHandler); // Message recieved from Swift code
  56. window.onfocus = function() {
  57. previousResult = null;
  58. checkForVideo();
  59. }; // Tab selected
  60. new MutationObserver(checkForVideo).observe(document, {subtree: true, childList: true}); // DOM changed
  61.  
  62. //function dispatchMessage(messageName, parameters) {
  63. // safari.extension.dispatchMessage(messageName, parameters);
  64. //}
  65.  
  66. function messageHandler(event) {
  67. if (event.name === "enablePiP" && getVideo() != null) {
  68. enablePiP();
  69. } else if (event.name === "addCustomPiPButtonToPlayer") {
  70. addCustomPiPButtonToPlayer(event.message)
  71. }
  72. }
  73. function addCustomPiPButtonToPlayer(message){
  74. message.callback();
  75. }
  76.  
  77. var previousResult = null;
  78. var videoCheck = {found: true}
  79. function checkForVideo() {
  80. if (getVideo() != null) {
  81. addCustomPiPButtons();
  82. if (previousResult === null || previousResult === false) {
  83. //dispatchMessage("videoCheck", {found: true});
  84. console.warn("videoCheck", {found: true})
  85. videoCheck = {found: true}
  86. enablePiP();
  87. }
  88. previousResult = true;
  89. } else if (window == window.top) {
  90. if (previousResult === null || previousResult === true) {
  91. //dispatchMessage("videoCheck", {found: false});
  92. console.warn("videoCheck", {found: false})
  93. videoCheck = {found: false}
  94. }
  95. previousResult = false;
  96. }
  97. }
  98.  
  99. function getVideo() {
  100. return document.getElementsByTagName('video')[0];
  101. }
  102.  
  103. async function action(video) {
  104. if (video.hasAttribute('__pip__')) {
  105. await document.exitPictureInPicture();
  106. } else {
  107. await video.requestPictureInPicture();
  108. video.setAttribute('__pip__', true);
  109. video.addEventListener('leavepictureinpicture', event => {
  110. video.removeAttribute('__pip__');
  111. }, {
  112. once: true
  113. });
  114. }
  115. }
  116.  
  117. function enablePiP() {
  118. let video = getVideo()
  119. if(video.webkitSetPresentationMode){
  120. // safari
  121. video.webkitSetPresentationMode('picture-in-picture');
  122. }else{
  123. //chrome
  124. action(video);
  125. }
  126. }
  127.  
  128. //----------------- Custom Button Methods -----------------
  129.  
  130. var players = [
  131. {name: "YouTube", shouldAddButton: shouldAddYouTubeButton, addButton: addYouTubeButton},
  132. {name: "VideoJS", shouldAddButton: shouldAddVideoJSButton, addButton: addVideoJSButton},
  133. {name: "Netflix", shouldAddButton: shouldAddNetflixButton, addButton: addNetflixButton},
  134. {name: "Wistia", shouldAddButton: shouldAddWistiaButton, addButton: addWistiaButton},
  135. //TODO: add other players here
  136. ];
  137.  
  138. let pipCheck = function pipCheck(message){
  139. addCustomPiPButtonToPlayer(message);
  140. }
  141. function addCustomPiPButtons() {
  142. for (const player of players) {
  143. if (player.shouldAddButton()) {
  144. //dispatchMessage("pipCheck", {callback: player.addButton.name}) //Sets the callback to the player's addButton
  145. pipCheck({callback: player.addButton});
  146. }
  147. }
  148. }
  149.  
  150. //----------------- Player Implementations -------------------------
  151.  
  152. function shouldAddYouTubeButton() {
  153. //check if on youtube or player is embedded
  154. return (location.hostname.match(/^(www\.)?youtube\.com$/)
  155. || document.getElementsByClassName("ytp-right-controls").length > 0)
  156. && document.getElementsByClassName('PiPifierButton').length == 0;
  157. }
  158.  
  159. function addYouTubeButton() {
  160. if (!shouldAddYouTubeButton()) return;
  161. var button = document.createElement("button");
  162. button.className = "ytp-button PiPifierButton";
  163. button.title = "PiP (by PiPifier)";
  164. button.onclick = enablePiP;
  165. //TODO add style
  166. //button.style.backgroundImage = 'url('+ whiteSVG_Icon + ')';
  167. var buttonImage = document.createElement("img");
  168. buttonImage.src = whiteSVG_Icon;
  169. buttonImage.width = 22;
  170. buttonImage.height = 36;
  171. button.appendChild(buttonImage);
  172.  
  173. document.getElementsByClassName("ytp-right-controls")[0].appendChild(button);
  174. }
  175.  
  176.  
  177. function shouldAddVideoJSButton() {
  178. return document.getElementsByClassName('vjs-control-bar').length > 0
  179. && document.getElementsByClassName('PiPifierButton').length == 0;
  180. }
  181.  
  182.  
  183. function addVideoJSButton() {
  184. if (!shouldAddVideoJSButton()) return;
  185. var button = document.createElement("button");
  186. button.className = "PiPifierButton vjs-control vjs-button";
  187. button.title = "PiP (by PiPifier)";
  188. button.onclick = enablePiP;
  189. var buttonImage = document.createElement("img");
  190. buttonImage.src = whiteSVG_Icon;
  191. buttonImage.width = 16;
  192. buttonImage.height = 30;
  193. button.appendChild(buttonImage);
  194. var fullscreenButton = document.getElementsByClassName("vjs-fullscreen-control")[0];
  195. fullscreenButton.parentNode.insertBefore(button, fullscreenButton);
  196. }
  197.  
  198. function shouldAddWistiaButton() {
  199. return document.getElementsByClassName('wistia_playbar').length > 0
  200. && document.getElementsByClassName('PiPifierButton').length == 0;
  201. }
  202.  
  203. function addWistiaButton() {
  204. if (!shouldAddWistiaButton()) return;
  205. var button = document.createElement("button");
  206. button.className = "PiPifierButton w-control w-control--fullscreen w-is-visible";
  207. button.alt = "Picture in Picture";
  208. button.title = "PiP (by PiPifier)";
  209. button.onclick = enablePiP;
  210. var buttonImage = document.createElement("img");
  211. buttonImage.src = whiteSVG_Icon;
  212. buttonImage.width = 28;
  213. buttonImage.height = 18;
  214. buttonImage.style.verticalAlign = "middle";
  215. button.appendChild(buttonImage);
  216. document.getElementsByClassName("w-control-bar__region--airplay")[0].appendChild(button);
  217. }
  218.  
  219.  
  220. function shouldAddNetflixButton() {
  221. return location.hostname.match('netflix')
  222. && document.getElementsByClassName('PiPifierButton').length == 0;
  223. }
  224.  
  225. function addNetflixButton(timeOutCounter) {
  226. if (!shouldAddNetflixButton()) return;
  227. if (timeOutCounter == null) timeOutCounter = 0;
  228. var button = document.createElement("button");
  229. button.className = "PiPifierButton";
  230. button.title = "PiP (by PiPifier)";
  231. button.onclick = enablePiP;
  232. button.style.backgroundColor = "transparent";
  233. button.style.border = "none";
  234. button.style.maxHeight = "inherit";
  235. button.style.width = "70px";
  236. button.style.marginRight = "2px";
  237. var buttonImage = document.createElement("img");
  238. buttonImage.src = whiteSVG_Icon;
  239. buttonImage.style.verticalAlign = "middle";
  240. buttonImage.style.maxHeight = "40%";
  241. button.appendChild(buttonImage);
  242. var playerStatusDiv = document.getElementsByClassName("player-status")[0];
  243. if (playerStatusDiv == null && timeOutCounter < 3) {
  244. //this is needed because the div is sometimes not reachable on the first load
  245. //also necessary to count up and stop at some time to avoid endless loop on main netflix page
  246. setTimeout(function() {addNetflixButton(timeOutCounter+1);}, 3000);
  247. return;
  248. }
  249. playerStatusDiv.insertBefore(button, playerStatusDiv.firstChild);
  250. }