Easy Picture-in-Picture

Picture in Picture を簡単に利用できるようにポップアウト ボタンを追加します。

  1. // ==UserScript==
  2. // @name Easy Picture-in-Picture
  3. // @namespace easy-picture-in-picture.user.js
  4. // @version 1.10
  5. // @description Picture in Picture を簡単に利用できるようにポップアウト ボタンを追加します。
  6. // @author nafumofu
  7. // @match *://*/*
  8. // @grant GM_addStyle
  9. // @license MIT License
  10. // ==/UserScript==
  11.  
  12. class EasyPictureInPicture {
  13. constructor() {
  14. if (document.pictureInPictureEnabled) {
  15. document.body.addEventListener('mousemove', (evt) => this.event(evt), {passive: true});
  16. document.body.addEventListener('touchstart', (evt) => this.event(evt), {passive: true});
  17. }
  18. }
  19. event(evt) {
  20. if (!this.eventLocked) {
  21. this.eventLocked = !!setTimeout(() => {
  22. this.eventLocked = false;
  23. }, 50);
  24. var posX = evt.clientX || evt.changedTouches[0].clientX;
  25. var posY = evt.clientY || evt.changedTouches[0].clientY;
  26. var elems = document.elementsFromPoint(posX, posY);
  27. for (let elem of elems) {
  28. if (elem.tagName === 'VIDEO' && elem.readyState) {
  29. this.showButton(elem);
  30. break;
  31. }
  32. }
  33. }
  34. }
  35. popOut() {
  36. if (document.pictureInPictureElement === this.epipTarget) {
  37. document.exitPictureInPicture();
  38. return
  39. }
  40. this.epipTarget.requestPictureInPicture();
  41. }
  42. showButton(target) {
  43. if (!this.epipButton) {
  44. this.epipButton = this.createButton();
  45. }
  46. if (!target.disablePictureInPicture) {
  47. this.epipTarget = target;
  48. var style = this.epipButton.style;
  49. var compStyle = getComputedStyle(this.epipButton);
  50. var rect =this.epipTarget.getBoundingClientRect();
  51. var posY = window.scrollY + rect.top;
  52. var posX = window.scrollX + rect.left + (rect.width / 2 - parseInt(compStyle.width) / 2);
  53. style.setProperty('top', `${posY}px`, 'important');
  54. style.setProperty('left', `${posX}px`, 'important');
  55. style.setProperty('opacity', '1', 'important');
  56. style.setProperty('pointer-events', 'auto', 'important');
  57. clearTimeout(this.epipTimer);
  58. this.epipTimer = setTimeout(() => {
  59. style.setProperty('opacity', '0', 'important');
  60. style.setProperty('pointer-events', 'none', 'important');
  61. }, 3000);
  62. }
  63. }
  64. createButton() {
  65. // https://material.io/resources/icons/?icon=picture_in_picture_alt&style=round
  66. var resIcon = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0V0z"/><path d="M18 11h-6c-.55 0-1 .45-1 1v4c0 .55.45 1 1 1h6c.55 0 1-.45 1-1v-4c0-.55-.45-1-1-1zm5 8V4.98C23 3.88 22.1 3 21 3H3c-1.1 0-2 .88-2 1.98V19c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2zm-3 .02H4c-.55 0-1-.45-1-1V5.97c0-.55.45-1 1-1h16c.55 0 1 .45 1 1v12.05c0 .55-.45 1-1 1z"/></svg>`;
  67. GM_addStyle(`
  68. #epip-button {
  69. all: unset !important;
  70. z-index: 2147483647 !important;
  71. position: absolute !important;
  72. pointer-events: none !important;
  73. opacity: 0 !important;
  74. transition: opacity 0.3s !important;
  75. margin-top: 4px !important;
  76. }
  77. #epip-button > .epip-icon {
  78. all: unset !important;
  79. fill: rgba(255,255,255,0.95) !important;
  80. background: rgba(0,0,0,0.5) !important;
  81. width: 20px !important;
  82. height: 20px !important;
  83. padding: 6px !important;
  84. border-radius: 50% !important;
  85. }
  86. `);
  87. var button = document.createElement('button');
  88. button.id = 'epip-button';
  89. button.tabIndex = -1;
  90. button.addEventListener('click', () => this.popOut());
  91. button.insertAdjacentHTML('afterbegin', resIcon);
  92. button.firstChild.classList.add('epip-icon');
  93. document.documentElement.append(button);
  94. return button;
  95. }
  96. }
  97.  
  98. new EasyPictureInPicture();