YouTube Live: Manual Fine Tune Video Time

7/5/2024

// ==UserScript==
// @name        YouTube Live: Manual Fine Tune Video Time
// @namespace   UserScripts
// @match       https://www.youtube.com/*
// @grant       none
// @version     0.1.2
// @author      CY Fung
// @description 7/5/2024
// @run-at      document-start
// @license     MIT
//
// ==/UserScript==

(() => {


  let byPassPlaybackRate = false;
  let tmpPlaybackRate = null;
  let byPassPlaybackRates = null; // [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2]


  if (typeof AbortSignal === 'undefined') return;

  let __requestAnimationFrame__ = typeof webkitRequestAnimationFrame === 'function' ? window.webkitRequestAnimationFrame.bind(window) : window.requestAnimationFrame.bind(window);

  let instance = null;

  /** @type {globalThis.PromiseConstructor} */
  const Promise = (async () => { })().constructor; // YouTube hacks Promise in WaterFox Classic and "Promise.resolve(0)" nevers resolve.


  const PromiseExternal = ((resolve_, reject_) => {
    const h = (resolve, reject) => { resolve_ = resolve; reject_ = reject };
    return class PromiseExternal extends Promise {
      constructor(cb = h) {
        super(cb);
        if (cb === h) {
          /** @type {(value: any) => void} */
          this.resolve = resolve_;
          /** @type {(reason?: any) => void} */
          this.reject = reject_;
        }
      }
    };
  })();


  const observablePromise = (proc, timeoutPromise) => {
    let promise = null;
    return {
      obtain() {
        if (!promise) {
          promise = new Promise(resolve => {
            let mo = null;
            const f = () => {
              let t = proc();
              if (t) {
                mo.disconnect();
                mo.takeRecords();
                mo = null;
                resolve(t);
              }
            }
            mo = new MutationObserver(f);
            mo.observe(document, { subtree: true, childList: true })
            f();
            timeoutPromise && timeoutPromise.then(() => {
              resolve(null)
            });
          });
        }
        return promise
      }
    }
  }

  let fc = 0;

  const pd = Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'playbackRate');

  const isPassiveArgSupport = (typeof IntersectionObserver === 'function');
  const bubblePassive = isPassiveArgSupport ? { capture: false, passive: true } : false;
  const capturePassive = isPassiveArgSupport ? { capture: true, passive: true } : true;

  const insp = o => o ? (o.polymerController || o.inst || o || 0) : (o || 0);

  let pageFetchedDataLocal = null;
  document.addEventListener('yt-page-data-fetched', (evt) => {
    pageFetchedDataLocal = evt.detail;

  }, bubblePassive);


  function getFormatDates() {

    if (!pageFetchedDataLocal) return null;

    const formatDates = {}
    try {
      formatDates.publishDate = pageFetchedDataLocal.pageData.playerResponse.microformat.playerMicroformatRenderer.publishDate
    } catch (e) { }
    // 2022-12-30

    try {
      formatDates.uploadDate = pageFetchedDataLocal.pageData.playerResponse.microformat.playerMicroformatRenderer.uploadDate
    } catch (e) { }
    // 2022-12-30

    try {
      formatDates.publishDate2 = pageFetchedDataLocal.pageData.response.contents.twoColumnWatchNextResults.results.results.contents[0].videoPrimaryInfoRenderer.dateText.simpleText
    } catch (e) { }
    // 2022/12/31

    if (typeof formatDates.publishDate2 === 'string' && formatDates.publishDate2 !== formatDates.publishDate) {
      formatDates.publishDate = formatDates.publishDate2
      formatDates.uploadDate = null
    }

    try {
      formatDates.broadcastBeginAt = pageFetchedDataLocal.pageData.playerResponse.microformat.playerMicroformatRenderer.liveBroadcastDetails.startTimestamp
    } catch (e) { }
    try {
      formatDates.broadcastEndAt = pageFetchedDataLocal.pageData.playerResponse.microformat.playerMicroformatRenderer.liveBroadcastDetails.endTimestamp
    } catch (e) { }
    try {
      formatDates.isLiveNow = pageFetchedDataLocal.pageData.playerResponse.microformat.playerMicroformatRenderer.liveBroadcastDetails.isLiveNow
    } catch (e) { }


    return formatDates;
  }

  const promiseVideoNextFn = async (video, v) => {

    if (typeof video.requestVideoFrameCallback === 'function' && v !== false) {

      return true === await Promise.race([
        new Promise(resolve => video.requestVideoFrameCallback(() => {
          resolve(true);
        })),
        new Promise(resolve => setTimeout(resolve, 1000))
      ]);

    } else {

      return true === await Promise.race([
        new Promise(resolve => video.addEventListener('timeupdate', () => {
          resolve(true);
        }, { once: true, passive: true, capture: false })),
        new Promise(resolve => setTimeout(resolve, 1000))
      ]);

    }



  }


  let videoTarget = null;


  let lastVideoId = null;
  let videoOK = false;

  const updateVideo = () => {

    videoOK = false;
    do {
      if (!videoTarget) break;
      if (!pageFetchedDataLocal) break;
      const dates = getFormatDates();
      if (!dates) break;
      if (dates.broadcastBeginAt && !dates.broadcastEndAt && dates.isLiveNow === true) {
        videoOK = true;
      } else {
        break;
      }
    } while (0);
    if (!videoOK) {
      return;
    }


    const flexy = document.querySelector('ytd-watch-flexy');
    const videoId = flexy.getAttribute('video-id')


    if (lastVideoId !== videoId) {
      lastVideoId = videoId;

      run(videoTarget).then(res => {
        if (res === false) lastVideoId = null;
      });
    }
  }

  document.addEventListener('durationchange', function (evt) {

    const target = (evt || 0).target;
    if (!(target instanceof HTMLVideoElement)) return;

    if (target.classList.contains('video-stream') && target.classList.contains('html5-main-video')) {

      videoTarget = target;

      if (target.duration !== 3600 && target.duration > 120) {

        updateVideo();

      }

    }


  }, true);

  let liveVideo = null;


  async function run(videoTarget) {

    try {

      if (fc > 1e9) fc = 9;
      let tc = ++fc;

      const timeout = new Promise(r => setTimeout(r, 1000));
      const video = videoTarget;
      // const video = watchPage.querySelector('video.video-stream.html5-main-video');
      if (!video) return false;


      const fn = () => {
        if (video.paused || !video.isConnected || video.networkState !== 2 || video.readyState !== 4) return false;
        return video.currentTime > 0.1 && video.duration > 0.1 && instance;
      }
      if (!fn()) {
        await observablePromise(fn, timeout).obtain();
      }

      if (tc !== fc) return false;

      await new Promise(resolve => video.addEventListener('timeupdate', () => {
        resolve();
      }, { once: true, passive: true, capture: false }));

      if (tc !== fc) return false;


      if (!instance || instance.getPlayerState() !== 1) return false;

      liveVideo = video;


    } catch (e) {
      console.log(e)
    }


  }


  let nnId = 0;
  document.addEventListener('yt-navigate-finish', () => {
    const t = ++nnId;
    setTimeout(() => {
      if (t !== nnId) return;
      if ([...document.querySelectorAll('ytd-page-manager#page-manager #movie_player .html5-main-video[src]')].filter(e => !e.closest('[hidden]')).length === 1) {

        updateVideo();
      }
    }, 800);
  }, false);





  const _yt_player_observable = observablePromise(() => {
    return (((window || 0)._yt_player || 0) || 0);
  });

  (async () => {

    const _yt_player = await _yt_player_observable.obtain();

    if (!_yt_player || typeof _yt_player !== 'object') return;

    const addProtoToArr = (parent, key, arr) => {

      let isChildProto = false;
      for (const sr of arr) {
        if (parent[key].prototype instanceof parent[sr]) {
          isChildProto = true;
          break;
        }
      }

      if (isChildProto) return;

      arr = arr.filter(sr => {
        if (parent[sr].prototype instanceof parent[key]) {
          return false;
        }
        return true;
      });

      arr.push(key);

      return arr;


    }

    const getGU = (_yt_player) => {

      const w = 'GU';

      let arr = [];

      for (const [k, v] of Object.entries(_yt_player)) {


        const p = typeof v === 'function' ? v.prototype : 0;

        if (p.getPlaybackRate && p.setPlaybackRate) {
          console.log(p.setPlaybackRate?.length, p.getPlaybackRate?.length, p.getVideoUrl?.length, p.getCurrentTime?.length, p.getDuration?.length)
        }
        if (p
          && typeof p.setPlaybackRate === 'function' && p.setPlaybackRate.length === 2
          && typeof p.getPlaybackRate === 'function' && p.getPlaybackRate.length === 0

          // && typeof p.isAtLiveHead === 'function'

          && typeof p.getVideoUrl === 'function' && p.getVideoUrl.length === 4
          && typeof p.getCurrentTime === 'function' && p.getCurrentTime.length >= 2
          && typeof p.getDuration === 'function' && p.getDuration.length == 2


        ) {
          arr = addProtoToArr(_yt_player, k, arr) || arr;


        }

      }

      /**
       *
    g.k.getCurrentTime = function(a, b, c) {
        var d = this.getPlayerState(a);
        if (this.app.getAppState() === 2 && d === 5) {
            var e;
            return ((e = this.app.getVideoData()) == null ? void 0 : e.startSeconds) || 0
        }
        return this.L("web_player_max_seekable_on_ended") && d === 0 ? zhb(this.app, a) : a ? this.app.getCurrentTime(a, b, c) : this.app.getCurrentTime(a)
    }
       *
       */


      // console.log(1222, arr.map(k=> Object.keys(_yt_player[k].prototype).sort()))

      if (arr.length === 0) {

        console.warn(`Key does not exist. [${w}]`);
      } else {

        console.log(`[${w}]`, arr);
        return arr[0];
      }




    }

    const key = getGU(_yt_player);

    // console.log(1233, key)
    const g = _yt_player;
    const k = key;
    const gk = g[k];
    const gkp = g[k].prototype;


    gkp.getPlaybackRate322 = gkp.getPlaybackRate;
    gkp.getPlaybackRate = function () {

      // console.log(5556, this.getPlaybackRate322)
      instance = this;
      return this.getPlaybackRate322();
    }

    gkp.setPlaybackRate322 = gkp.setPlaybackRate;
    gkp.setPlaybackRate = function (a, b) {
      instance = this;
      if (byPassPlaybackRate) {
        // console.log(5888, 12333,a, b)
        // return;
      }
      // console.log(5388, arguments)
      return this.setPlaybackRate322(a, b);
    }

    if (typeof gkp.getAvailablePlaybackRates === 'function' && typeof gkp.getAvailablePlaybackRates322 !== 'function') {


      gkp.getAvailablePlaybackRates322 = gkp.getAvailablePlaybackRates;
      gkp.getAvailablePlaybackRates = function () {
        instance = this;
        if (byPassPlaybackRates) {
          // console.log(5888, 12333,a, b)
          return byPassPlaybackRates;
        }
        // console.log(5388, arguments)
        return this.getAvailablePlaybackRates322();
      }
      //[0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2]



    }


  })();

  Storage.prototype.setItem322 = Storage.prototype.setItem;
  Storage.prototype.setItem = function (a, b) {

    if (a === 'yt-player-playback-rate') {
      tmpPlaybackRate = b;
      // if(!byPassPlaybackRate) debugger
      if (promisePR1 && b && typeof b === 'string' && b.indexOf('{"data":"1"') >= 0) {

        promisePR1.resolve();
        promisePR1 = null;
      }
      // console.log(5883, a, b,byPassPlaybackRate, 'XX_'+tmpPlaybackRate+'_', location.pathname)
      if (window.location.pathname === '/live_chat') return;
    }

    if (byPassPlaybackRate && a === 'yt-player-playback-rate') return;
    this.setItem322(a, b);

  }


  Storage.prototype.getItem322 = Storage.prototype.getItem;
  Storage.prototype.getItem = function (a) {

    if (a === 'yt-player-playback-rate') {
      if (window.location.pathname === '/live_chat') return null;
      if (typeof tmpPlaybackRate === 'string') return tmpPlaybackRate;
    }

    if (a === 'yt-player-playback-rate' && typeof tmpPlaybackRate === 'string') return tmpPlaybackRate;
    return this.getItem322(a);

  }

  Object.defineProperty(Storage.prototype, 'yt-player-playback-rate', {
    get() {
      return this.getItem('yt-player-playback-rate');
    },
    set(nv) {
      this.setItem('yt-player-playback-rate', nv);
      return true;
    },
    enumerable: true,
    configurable: true
  });

  let onKey = false;

  let lrRates = null;

  const lrRatesSetup = () => {
    if (lrRates || !instance) return;
    const rates = instance.getAvailablePlaybackRates();
    lrRates = [Math.max(...rates.filter(r => r < 1)), Math.min(...rates.filter(r => r > 1))];
  }

  const setRate = (r) => {

    instance && instance.setPlaybackRate(r, r)
  }

  const filterSet = (b) => {
    if (!liveVideo) return;
    b ? (liveVideo.style.filter = 'contrast(0.5)') : (liveVideo.style.filter = '')
  }

  const targetOK = (target) => {
    if (!target || target instanceof Document) return true;
    if (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement) return false;
    if (target instanceof Element && target.closest('[contenteditable]') instanceof Node) return false;
    return true;
  }

  document.addEventListener('keydown', evt => {
    if (evt.code === 'ArrowLeft' && videoOK && instance && !onKey && liveVideo && !liveVideo.paused && targetOK(evt.target)) {
      if (!onKey) {

        onKey = 'left'
        console.log('left')
        lrRatesSetup();
        setRate(lrRates[0]);
        filterSet(1)
      }
      // evt.preventDefault();
      evt.stopImmediatePropagation();
      evt.stopPropagation();
    } else if (evt.code === 'ArrowRight' && videoOK && instance && liveVideo && !liveVideo.paused && targetOK(evt.target)) {
      if (!onKey) {

        onKey = 'right'
        console.log('right')
        lrRatesSetup();
        setRate(lrRates[1]);
        filterSet(1)
      }
      // evt.preventDefault();
      evt.stopImmediatePropagation();
      evt.stopPropagation();
    }
  }, true);


  document.addEventListener('keyup', evt => {
    if (evt.code === 'ArrowLeft' && videoOK && instance && onKey === 'left') {
      console.log('left end')
      // evt.preventDefault();
      evt.stopImmediatePropagation();
      evt.stopPropagation();
      setRate(1);
      filterSet(0)
    } else if (evt.code === 'ArrowRight' && videoOK && instance && onKey === 'right') {
      console.log('right end')
      // evt.preventDefault();
      evt.stopImmediatePropagation();
      evt.stopPropagation();
      setRate(1);
      filterSet(0)
    }
    onKey = false;
  }, true);




})();