Greasy Fork镜像 支持简体中文。

ExtTube

Replaces YouTube player with Invidious player and adds a "Watch on Invidious" button.

  1. // ==UserScript==
  2. // @name ExtTube
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.2
  5. // @description Replaces YouTube player with Invidious player and adds a "Watch on Invidious" button.
  6. // @author sypcerr
  7. // @match https://*.youtube.com/watch?*
  8. // @icon https://cdn-icons-png.flaticon.com/256/1384/1384060.png
  9. // @run-at document-start
  10. // @license MIT
  11. // ==/UserScript==
  12.  
  13.  
  14. const configs = ytcfg.d();
  15. configs['WEB_PLAYER_CONTEXT_CONFIGS'] = {};
  16. ytcfg.set(configs);
  17.  
  18. (function() {
  19. 'use strict';
  20.  
  21. const CONFIG = {
  22. PLAYER_SELECTOR: '#player-container[role="complementary"]',
  23. RETRY_ATTEMPTS: 3,
  24. RETRY_DELAY: 1000,
  25. DEFAULT_QUALITY: 'hd1080',
  26. IFRAME_STYLES: {
  27. width: '100%',
  28. height: '100%',
  29. borderRadius: '15px',
  30. border: 'none'
  31. }
  32. };
  33.  
  34. function getVideoId() {
  35. try {
  36. const url = new URL(window.location.href);
  37. return url.searchParams.get('v');
  38. } catch (error) {
  39. return null;
  40. }
  41. }
  42.  
  43. function createEnhancedIframe(videoId) {
  44. const iframe = document.createElement('iframe');
  45. const params = new URLSearchParams({
  46. autoplay: 1,
  47. rel: 0,
  48. modestbranding: 1,
  49. enablejsapi: 1,
  50. origin: window.location.origin,
  51. widget_referrer: window.location.href,
  52. hl: document.documentElement.lang || 'en',
  53. controls: 1,
  54. fs: 1,
  55. quality: CONFIG.DEFAULT_QUALITY
  56. });
  57.  
  58. iframe.src = `https://www.youtube.com/embed/${videoId}?${params.toString()}`;
  59. iframe.title = 'YouTube video player';
  60. iframe.allow = 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share';
  61. iframe.referrerpolicy = 'strict-origin-when-cross-origin';
  62. iframe.allowFullscreen = true;
  63.  
  64. Object.assign(iframe.style, CONFIG.IFRAME_STYLES);
  65.  
  66. return iframe;
  67. }
  68.  
  69. async function replacePlayer(attempts = CONFIG.RETRY_ATTEMPTS) {
  70. const videoId = getVideoId();
  71. if (!videoId) return;
  72.  
  73. const findPlayerContainer = () => document.querySelector(CONFIG.PLAYER_SELECTOR);
  74. let playerContainer = findPlayerContainer();
  75.  
  76. if (!playerContainer && attempts > 0) {
  77. console.log(`⏳ Retrying player replacement... (${attempts} attempts left)`);
  78. await new Promise(resolve => setTimeout(resolve, CONFIG.RETRY_DELAY));
  79. return replacePlayer(attempts - 1);
  80. }
  81.  
  82. if (!playerContainer) {
  83. console.error('❌ Player container not found after all attempts');
  84. return;
  85. }
  86.  
  87. try {
  88. while (playerContainer.firstChild) {
  89. playerContainer.firstChild.remove();
  90. }
  91.  
  92. const iframe = createEnhancedIframe(videoId);
  93. playerContainer.appendChild(iframe);
  94. console.log('✅ Player replaced successfully');
  95. } catch (error) {
  96. console.error('❌ Error replacing player:', error);
  97. }
  98. }
  99.  
  100. function stopMediaElements() {
  101. const mediaElements = document.querySelectorAll('video, audio');
  102. mediaElements.forEach((media) => {
  103. media.pause();
  104. media.currentTime = 0;
  105. media.remove();
  106. });
  107. if (window.AudioContext || window.webkitAudioContext) {
  108. const AudioContext = window.AudioContext || window.webkitAudioContext;
  109. const originalAudioContext = AudioContext.prototype;
  110. AudioContext.prototype.createBufferSource = function() {
  111. const source = originalAudioContext.createBufferSource.call(this);
  112. source.stop = function() {
  113. console.log('🔇 Stopped Web Audio API source');
  114. originalAudioContext.stop.call(this);
  115. };
  116. return source;
  117. };
  118. const contexts = [];
  119. const originalCreateContext = AudioContext.prototype.constructor;
  120. AudioContext.prototype.constructor = function() {
  121. const context = new originalCreateContext();
  122. contexts.push(context);
  123. return context;
  124. };
  125. contexts.forEach((context) => {
  126. context.close().then(() => {
  127. });
  128. });
  129. }
  130. if (window.MediaSource) {
  131. const originalMediaSource = window.MediaSource;
  132. window.MediaSource = function() {
  133. const mediaSource = new originalMediaSource();
  134. mediaSource.endOfStream = function() {
  135. originalMediaSource.prototype.endOfStream.call(this);
  136. };
  137. return mediaSource;
  138. };
  139. }
  140. console.log('🔇 Stopped all audio and video elements, Web Audio API, and MediaSource');
  141. }
  142.  
  143. function init() {
  144. stopMediaElements();
  145. window.addEventListener('yt-navigate-finish', () => replacePlayer());
  146. }
  147.  
  148. init();
  149. })();

QingJ © 2025

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