YouTube Click To Play

*

目前為 2020-07-03 提交的版本,檢視 最新版本

// ==UserScript==
// @name        YouTube Click To Play
// @name:ja     YouTube Click To Play
// @name:zh-CN  YouTube Click To Play
// @namespace   knoa.jp
// @description *
// @description:ja 動画を自動再生させず、クリックで再生するようにします。
// @description:zh-CN *
// @include     https://www.youtube.com/*
// @noframes
// @run-at      document-start
// @grant       none
// @version     1
// ==/UserScript==

(function(){
  const SCRIPTID = 'YouTubeClickToPlay';
  const SCRIPTNAME = 'YouTube Click To Play';
  const DEBUG = false;/*
[update]

********************************************************************************
Functions of this script:
It displays thumbnail images instead of auto-playing videos on channel pages, individual video pages, etc.
You can play the thumbnail videos by mouse click or [Space] key.
Advertisements and live streamings would automatically start normally.
[image]

Try on YouTube:
https://www.youtube.com/

https://gf.qytechs.cn/en/scripts?set=361295
#YouTubeClickToPlay

Keywords:
kill prevent avoid disable autoplay autostart auto automatically play start
channel home top user video page show thumbnail poster cover image click tap touch space key
********************************************************************************
本スクリプトの機能:
チャンネルページや個別の動画ページなどで、動画を自動再生せず、サムネイル画像を表示します。
サムネイル画像になった動画は、マウスクリックや [スペース] キーなどで再生できます。
広告映像やライブ配信では、通常どおり自動で再生を開始します。
[image]

YouTube で試す:
https://www.youtube.com/

https://gf.qytechs.cn/en/scripts?set=361295
#YouTubeClickToPlay

Keywords:
kill prevent avoid disable autoplay autostart auto automatically play start
channel user video page show thumbnail poster cover image click tap touch space key
自動再生 オートプレイ 勝手に再生 スタート プレイ しない させない 回避 停止 中止 無効化
チャンネルページ ホーム トップ ユーザー 動画 ビデオ サムネイル画像 ポスター カバー イメージ クリック タップ タッチ スペースキー
********************************************************************************
本脚本的功能:
在频道页或单独的视频页面上显示缩略图,而不会自动播放视频。
您可以通过鼠标单击或空格键等播放缩略图视频。
在广告视频和直播中,您可以像往常一样自动开始播放。
[image]

在 YouTube 上尝试:
https://www.youtube.com/

https://gf.qytechs.cn/en/scripts?set=361295
#YouTubeClickToPlay

Keywords:
kill prevent avoid disable autoplay autostart auto automatically play start
channel user video page show thumbnail poster cover image click tap touch space key
自动播放 自动启动 随意播放 开始 播放 不 防止 避免 停止 禁用
频道页 首页 顶部 用户 视频 显示 缩略图图像 海报 封面 图像 单击 点击 轻敲 触摸 空格键
********************************************************************************

[bug]

[todo]

[possible]
channel/ と watch/ は個別に設定可能とか
  => channelだけで動作する別スクリプトがある
document.hidden でのみ作動するオプションとか
0秒で常にサムネに戻る仕様(seekingイベントでよい)

[research]
シアターモードの切り替えで再生してしまう件(そこまで気にしなくてもいい気もする)
t=4 以下で seek 後にサムネイルが消えてしまう問題
たまにぐるぐるが止まらない問題 t 指定とキャッシュに関係ある?

[memo]
本スクリプト仕様:
  サムネになってほしい: チャンネルホーム, ビデオページ
  再生してほしい: LIVE, 広告, 途中広告からの復帰
  要確認: 各ページの行き来, 再生で即停止しないこと, シアターモードの切り替え, 背面タブでの起動
  (YouTubeによるあっぱれなユーザー体験の追究のおかげで、初回読み込み時に限り再生開始済みのvideo要素が即出現する)
YouTube仕様:
  画面更新(URL Enter, S-Reload, Reload に本質的な差異なし)
  新規タブ(開いた直後, 読み込み完了後, title変更後 に本質的な差異なし)
    video:   body ... video ... loadstart ... で必ず play() されるのでダミーと入れ替えておけばよい。
      video要素は #player-api 内に出現した後に ytd-watch-flexy 内に移動する。その際に play() されるようだ。
      t=123 のような時刻指定があると seeking 後にもう一度 play() される。
        thumbnail は t=4 以下だとなぜか消えてしまう。(seekじゃなくてadvanceだとみなされるせい?)
    channel: body ... video ... loadstart で即 pause() 可能。(playは踏まれない)
  画面遷移(動画 <=> LIVE <=> チャンネル)
    video:   yt-navigate-start ... loadstart で即 pause() 可能。(playは踏まれない)
  広告
    冒頭広告: .ad-showing 依存だが判定できる。
    広告明け: 少しだけ泥臭いが、そのURLで一度でも本編が再生されていれば広告明けとみなす。
参考:
  Channelトップの動画でのみ機能するスクリプト
  https://gf.qytechs.cn/ja/scripts/399862-kill-youtube-channel-video-autoplay
  */
  if(window === top && console.time) console.time(SCRIPTID);
  const MS = 1, SECOND = 1000*MS, MINUTE = 60*SECOND, HOUR = 60*MINUTE, DAY = 24*HOUR, WEEK = 7*DAY, MONTH = 30*DAY, YEAR = 365*DAY;
  const FLAGNAME = SCRIPTID.toLowerCase();
  const site = {
    targets: {
      body: () => $('body'),
    },
    get: {
      video: () => $(`video:not([data-${FLAGNAME}])`),
      startTime: () => {
        /* t=1h0m0s or t=3600 */
        let t = (new URL(location)).searchParams.get('t');
        if(t === null) return;
        let [h, m, s] = t.match(/^(?:([0-9]+)h)?(?:([0-9]+)m)?(?:([0-9]+)s?)?$/).slice(1).map(n => parseInt(n || 0));
        return 60*60*h + 60*m + s;
      },
    },
    is: {
      immediate: (video) => ($('#player-api') && $('#player-api').contains(video)),
      live: () => $('.ytp-time-display.ytp-live') !== null,
      ad: () => $('#movie_player.ad-showing') !== null,
    },
  };
  let elements = {}, flags = {}, view;
  const core = {
    initialize: function(){
      elements.html = document.documentElement;
      elements.html.classList.add(SCRIPTID);
      core.ready();
    },
    ready: function(){
      core.getTargets(site.targets).then(() => {
        log("I'm ready.");
        core.findVideo();
      }).catch(e => {
        console.error(`${SCRIPTID}:${e.lineNumber} ${e.name}: ${e.message}`);
      });
    },
    findVideo: function(){
      const found = function(video){
        //log(video);
        if(video.dataset[FLAGNAME]) return;
        video.dataset[FLAGNAME] = 'found';
        core.listenVideo(video);
      };
      /* if a video already exists */
      let video = site.get.video();
      if(video) found(video);
      /* unavoidably observate body for immediate catch */
      observe(elements.body, function(records){
        let video = site.get.video();
        if(video) found(video);
      }, {childList: true, subtree: true});
    },
    listenVideo: function(video){
      /* for the very immediate time */
      //log(video.currentSrc, video.paused, video.currentTime);
      core.stopAutoplay(video);
      core.stopImmediateAutoplay(video);
      /* the video element just changes its src attribute on any case */
      video.addEventListener('loadstart', function(e){
        //log(e.type, video.currentSrc, video.paused, video.currentTime);
        if(site.is.live()) return log('this is a live and should start playing');
        if(site.is.ad()) return log('this is an ad and should start playing.');
        if(flags.playedOnce === location.href) return log('the ad has just closed and video should continue playing.');
        /* then it should be stopped */
        core.stopAutoplay(video);
      });
      /* memorize played status for restarting playing or not on after ads */
      video.addEventListener('playing', function(e){
        //log(e.type, video.currentTime);
        if(site.is.ad()) return;
        if(flags.playedOnce === location.href) return;
        flags.playedOnce = location.href;/* played once on the current location */
      });
      if(flags.listeningNavigation === undefined){
        flags.listeningNavigation = true;
        document.addEventListener('yt-navigate-start', function(e){
          //log(e, location.href);
          delete flags.playedOnce;/* reset the played once status */
        });
      }
    },
    stopAutoplay: function(video){
      //log();
      video.autoplay = false;
      video.pause();
    },
    stopImmediateAutoplay: function(video){
      let count = 0, isImmediate = site.is.immediate(video), startTime = site.get.startTime();
      //log(isImmediate, startTime);
      if(isImmediate) count++;/* for the very first view of the YouTube which plays a video automatically for immediate user experience */
      if(startTime) count++;/* for starting again from middle after seeking with query like t=123 */
      if(count){
        video.originalPlay = video.play;
        video.play = function(){
          log('(play)', count, video.currentTime);
          if(site.is.ad()) return video.originalPlay();
          if(--count === 0) video.play = video.originalPlay;
        };
      }
      /* I don't know why but on t < 5, it'll surely be paused but player UI is remained playing. So... */
      if(startTime && startTime < 5) video.addEventListener('seeked', function(e){
        //log(e.type, video.currentTime);
        video.play();
        video.pause();
      }, {once: true});
    },
    getTarget: function(selector, retry = 10, interval = 1*SECOND){
      const key = selector.name;
      const get = function(resolve, reject){
        let selected = selector();
        if(selected && selected.length > 0) selected.forEach((s) => s.dataset.selector = key);/* elements */
        else if(selected instanceof HTMLElement) selected.dataset.selector = key;/* element */
        else if(--retry) return log(`Not found: ${key}, retrying... (${retry})`), setTimeout(get, interval, resolve, reject);
        else return reject(new Error(`Not found: ${selector.name}, I give up.`));
        elements[key] = selected;
        resolve(selected);
      };
      return new Promise(function(resolve, reject){
        get(resolve, reject);
      });
    },
    getTargets: function(selectors, retry = 10, interval = 1*SECOND){
      return Promise.all(Object.values(selectors).map(selector => core.getTarget(selector, retry, interval)));
    },
  };
  const setTimeout = window.setTimeout.bind(window), clearTimeout = window.clearTimeout.bind(window), setInterval = window.setInterval.bind(window), clearInterval = window.clearInterval.bind(window), requestAnimationFrame = window.requestAnimationFrame.bind(window);
  const alert = window.alert.bind(window), confirm = window.confirm.bind(window), getComputedStyle = window.getComputedStyle.bind(window), fetch = window.fetch.bind(window);
  if(!('isConnected' in Node.prototype)) Object.defineProperty(Node.prototype, 'isConnected', {get: function(){return document.contains(this)}});
  const $ = function(s, f){
    let target = document.querySelector(s);
    if(target === null) return null;
    return f ? f(target) : target;
  };
  const $$ = function(s, f){
    let targets = document.querySelectorAll(s);
    return f ? Array.from(targets).map(t => f(t)) : targets;
  };
  const observe = function(element, callback, options = {childList: true, characterData: false, subtree: false, attributes: false, attributeFilter: undefined}){
    let observer = new MutationObserver(callback.bind(element));
    observer.observe(element, options);
    return observer;
  };
  const log = function(){
    if(!DEBUG) return;
    let l = log.last = log.now || new Date(), n = log.now = new Date();
    let error = new Error(), line = log.format.getLine(error), callers = log.format.getCallers(error);
    //console.log(error.stack);
    console.log(
      SCRIPTID + ':',
      /* 00:00:00.000  */ n.toLocaleTimeString() + '.' + n.getTime().toString().slice(-3),
      /* +0.000s       */ '+' + ((n-l)/1000).toFixed(3) + 's',
      /* :00           */ ':' + line,
      /* caller.caller */ (callers[2] ? callers[2] + '() => ' : '') +
      /* caller        */ (callers[1] || '') + '()',
      ...arguments
    );
  };
  log.formats = [{
      name: 'Firefox Scratchpad',
      detector: /MARKER@Scratchpad/,
      getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1],
      getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
    }, {
      name: 'Firefox Console',
      detector: /MARKER@debugger/,
      getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1],
      getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
    }, {
      name: 'Firefox Greasemonkey 3',
      detector: /\/gm_scripts\//,
      getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1],
      getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
    }, {
      name: 'Firefox Greasemonkey 4+',
      detector: /MARKER@user-script:/,
      getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1] - 500,
      getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
    }, {
      name: 'Firefox Tampermonkey',
      detector: /MARKER@moz-extension:/,
      getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1] - 6,
      getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
    }, {
      name: 'Chrome Console',
      detector: /at MARKER \(<anonymous>/,
      getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)?$/)[1],
      getCallers: (e) => e.stack.match(/[^ ]+(?= \(<anonymous>)/gm),
    }, {
      name: 'Chrome Tampermonkey',
      detector: /at MARKER \(chrome-extension:.*?\/userscript.html\?name=/,
      getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)?$/)[1] - 1,
      getCallers: (e) => e.stack.match(/[^ ]+(?= \(chrome-extension:)/gm),
    }, {
      name: 'Chrome Extension',
      detector: /at MARKER \(chrome-extension:/,
      getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)?$/)[1],
      getCallers: (e) => e.stack.match(/[^ ]+(?= \(chrome-extension:)/gm),
    }, {
      name: 'Edge Console',
      detector: /at MARKER \(eval/,
      getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)$/)[1],
      getCallers: (e) => e.stack.match(/[^ ]+(?= \(eval)/gm),
    }, {
      name: 'Edge Tampermonkey',
      detector: /at MARKER \(Function/,
      getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)$/)[1] - 4,
      getCallers: (e) => e.stack.match(/[^ ]+(?= \(Function)/gm),
    }, {
      name: 'Safari',
      detector: /^MARKER$/m,
      getLine: (e) => 0,/*e.lineが用意されているが最終呼び出し位置のみ*/
      getCallers: (e) => e.stack.split('\n'),
    }, {
      name: 'Default',
      detector: /./,
      getLine: (e) => 0,
      getCallers: (e) => [],
    }];
  log.format = log.formats.find(function MARKER(f){
    if(!f.detector.test(new Error().stack)) return false;
    //console.log('////', f.name, 'wants', 0/*line*/, '\n' + new Error().stack);
    return true;
  });
  core.initialize();
  if(window === top && console.timeEnd) console.timeEnd(SCRIPTID);
})();

QingJ © 2025

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