pixeltris / TwitchAdSolutions

7.75k stars 446 forks source link

Vaft endless buffering Solution? #209

Closed pflanzenandi closed 3 months ago

pflanzenandi commented 10 months ago

Since couple days it seems Twitch has changed the players behaviour when using the vaft script. By accident I ran vaft and another adblock script simultaneously and the problems were gone (Firefox and Violentmonkey).

By itself the other script doesn't work for me and shows ads. But in combination with vaft all ads are blocked and the stream reloads correctly and continuous. The other script forces a proper stream reload after ads finished?

Maybe @pixeltris can get something out of the code.

(function () {
  'use strict';

  const scriptName = 'twitchAdSkip';
  const adTestSel = '[data-test-selector="ad-banner-default-text"]';
  const ffzResetBtnSel = '[data-a-target="ffz-player-reset-button"]';
  const videoPlayerSel = '[data-a-target="video-player"]';
  const videoPlayervolSliderSel = '[data-a-target="player-volume-slider"]';
  const videoNodeSel = 'video';
  const postFixVolWaitTime = 2000;
  const nodeTypesToCheck = [Node.ELEMENT_NODE, Node.DOCUMENT_NODE, Node.DOCUMENT_FRAGMENT_NODE];

  //
  const maxRetriesFindVideoPlayer = 5;
  const maxRetriesVolListener = 5;
  const maxRetriesVideoPlayerObserver = 5;

  // Volume vals
  let videoNodeVolCurrent;
  let adLaunched = false;

  // Helpers //
  const log = function (logType, message) {
    return console[logType](`${scriptName}: ${message}`);
  };
  const getFfzResetButton = function () {
    return document.querySelector(ffzResetBtnSel);
  };
  const getElWithOptContext = function (selStr, context) {
    context = context || document;
    return context.querySelector(selStr);
  };
  const getVideoNodeEl = function (context) {
    return getElWithOptContext(videoNodeSel, context);
  };
  const getVideoPlayerVolSliderEl = function (context) {
    return getElWithOptContext(videoPlayervolSliderSel, context);
  };
  const getVideoPlayerEl = function (context) {
    return getElWithOptContext(videoPlayerSel, context);
  };

  const attachMO = async function (videoPlayerEl) {
    let resetButton = getFfzResetButton();

    const videoPlayerObserver = new MutationObserver(function (mutations) {
      for (const mutation of mutations) {
        for (const node of mutation.addedNodes) {
          const canCheckNode = nodeTypesToCheck.includes(node.nodeType);
          if (!canCheckNode) {
            continue;
          }

          const isAdNode = node.querySelector(adTestSel);
          if (!isAdNode) {
            continue;
          }

          log('info', `Found ad node at: ${adTestSel}`);

          // Is ad //
          adLaunched = true;
          if (!resetButton) {
            log('info', `FFZ reset button not loaded - attempting to load...`);

            // Attempt to load the resetButton now
            resetButton = getFfzResetButton();

            if (!resetButton) {
              log('error', `FFZ reset button could not be loaded - refreshing full page.`);

              // Not loaded for some reason
              window.location.reload();
            }
          }

          // Cache current vol props //
          log('info', 'Finding video node to post-fix volume.');
          // Actual video volume
          const videoNodeEl = getVideoNodeEl(videoPlayerEl);
          log('info', `Volume before reset: ${videoNodeVolCurrent}`);

          // Cosmetic vol slider
          const videoPlayerVolSliderEl = getVideoPlayerVolSliderEl(videoPlayerEl);
          const videoPlayerVolSliderCurrent = parseInt(videoPlayerVolSliderEl.value, 10).toFixed(2);

          log('info', `Triggering FFZ reset button...`);
          resetButton.dispatchEvent(new MouseEvent('dblclick', {
            bubbles: true,
            cancelable: true,
            view: window
          }));d

          log('info', `Fixing volume to original value of '${videoNodeVolCurrent}' after interval of '${postFixVolWaitTime}' ms`);
          setTimeout(() => {
            // Does the video player element still exist after reset?
            if (!videoPlayerEl) {
              log('info', 'Video player element destroyed after reset - sourcing new element...');
              videoPlayerEl = getVideoPlayerEl();
            }

            // Does the video node still exist after reset?
            if (!videoNodeEl) {
              log('info', 'Video node destroyed after reset - sourcing new node...');
              videoNodeEl = getVideoNodeEl(videoPlayerEl);
            }

            // Fix video vol
            const preFixVol = videoNodeEl.volume;
            videoNodeEl.volume = videoNodeVolCurrent;
            log('info', `Post-fixed volume from reset val of '${preFixVol}' -> '${videoNodeVolCurrent}'`);

            // Fix video player vol slider
            // TODO: this may not work due to this input being tied to the js framework component
            if (!videoPlayerVolSliderEl) {
              videoPlayerVolSliderEl = getVideoPlayerVolSliderEl(videoPlayerEl);
            }
            videoPlayerVolSliderEl.value = videoPlayerVolSliderCurrent;

            adLaunched = false;
          }, postFixVolWaitTime);
        }
      }
    });

    videoPlayerObserver.observe(videoPlayerEl, {
      childList: true,
      subtree: true
    });
    log('info', 'Video player observer attached');
  };

  const listenForVolumeChanges = async function (videoPlayerEl) {
    const videoNodeEl = getVideoNodeEl(videoPlayerEl);

    if (!videoNodeEl) {
      throw new Error('Video player element not found.  If it is expected that there is no video on the current page (e.g. Twitch directory), then ignore this error.');
    }

    // Initial load val
    videoNodeVolCurrent = videoNodeEl.volume.toFixed(2);
    log('info', `Initial volume: '${videoNodeVolCurrent}'.`);

    const videoPlayerVolSliderEl = getVideoPlayerVolSliderEl(videoPlayerEl);

    if (!videoPlayerVolSliderEl) {
      throw new Error('Video player volume slider not found.  Perhaps application is in picture-in-picture mode?');
    }

    const setCurrentVolume = (event) => {
      // Ignore any vol changes for ads
      if (document.querySelector(adTestSel) || adLaunched) {
        return;
      }

      // Always find the video node element as Twitch app may have re-created tracked element
      videoNodeVolCurrent = getVideoNodeEl(videoPlayerEl).volume.toFixed(2);
      log('info', `Volume modified to: '${videoNodeVolCurrent}'.`);
    };

    // Standard volume change listeners
    videoPlayerVolSliderEl.addEventListener('keyup', (event) => {
      if (!event.key) {
        return;
      }

      if (!['ArrowUp', 'ArrowDown'].includes(event.key)) {
        return;
      }
      setCurrentVolume(event);
    });

    videoPlayerVolSliderEl.addEventListener('mouseup', setCurrentVolume);
    videoPlayerVolSliderEl.addEventListener('scroll', (event) => _.debounce(setCurrentVolume, 1000));

    // TODO: FFZ scrollup & scrolldown support
  };

  const retryWrap = function(fnToRetry, args, intervalInMs, maxRetries, actionDescription) {
    const retry = (fn, retries = 3) => fn()
      .catch((e) => {
        if (retries <= 0) {
          log('error', `${actionDescription} - failed after ${maxRetries} retries.`)
          return Promise.reject(e);
        }
        log('warn', `${actionDescription} - retrying another ${retries} time(s).`);
        return retry(fn, --retries)
      });

    const delay = ms => new Promise((resolve) => setTimeout(resolve, ms));
    const delayError = (fn, args, ms) => () => fn(...args).catch((e) => delay(ms).then((y) => Promise.reject(e)));
    return retry(delayError(fnToRetry, args, intervalInMs), maxRetries);
  };

  const spawnFindVideoPlayerEl = async function() {
    const actionDescription = 'Finding video player';
    log('info', `${actionDescription}...`);
    const findVideoPlayerEl = async () => {
      const videoPlayerEl = document.querySelector(videoPlayerSel);
      if (!videoPlayerEl) {
        return Promise.reject('Video player not found.');
      }
      return videoPlayerEl;
    };
    return await retryWrap(findVideoPlayerEl, [], 2000, maxRetriesFindVideoPlayer, actionDescription);
  };

  const spawnVolumeChangeListener = async function(videoPlayerEl) {
    const actionDescription = 'Listening for volume changes';
    log('info', `${actionDescription}...`);
    retryWrap(listenForVolumeChanges, [videoPlayerEl], 2000, maxRetriesVolListener, actionDescription);
  };

  const spawnVideoPlayerAdSkipObservers = async function(videoPlayerEl) {
    const actionDescription = 'Attaching MO';
    log('info', `${actionDescription}...`);
    retryWrap(attachMO, [videoPlayerEl], 2000, maxRetriesVideoPlayerObserver, actionDescription);
  };

  const spawnObservers = async function () {
    try {
      const videoPlayerEl = await spawnFindVideoPlayerEl();

      if (!videoPlayerEl) {
        throw new Error('Could not find video player.');
      }
      log('info', 'Success - video player found.');

      spawnVolumeChangeListener(videoPlayerEl);
      spawnVideoPlayerAdSkipObservers(videoPlayerEl);
    } catch (error) {
      log('error', error);
    }
  }

  log('info', 'Page loaded - attempting to spawn observers...');
  spawnObservers();

  log('info', 'Overloading history push state')
  var pushState = history.pushState;
  history.pushState = function () {
    pushState.apply(history, arguments);

    log('info', 'History change - attempting to spawn observers...')
    spawnObservers();
  };
})();
jujar commented 10 months ago

I am also experiencing the endless buffering problem since today, I'm using the uBlock origin VAFT script.

EDIT: And forgot to mention that using the second script in conjunction with VAFT does fix the issue.

PunchEnergyFTW commented 10 months ago

I can also verify that twitch keeps buffering the Video as long as the ad is running. After that, the stream picks up again. I have not tested the second script in conjunction with VAFT yet.

pflanzenandi commented 10 months ago

I can also verify that twitch keeps buffering the Video as long as the ad is running. After that, the stream picks up again. I have not tested the second script in conjunction with VAFT yet.

That's not what I meant. The vaft script buffers endlessly while blocking ads. You have to manually click pause then play to resume the stream.

Squtzy commented 10 months ago

I can also verify that twitch keeps buffering the Video as long as the ad is running. After that, the stream picks up again. I have not tested the second script in conjunction with VAFT yet.

That's not what I meant. The vaft script buffers endlessly while blocking ads. You have to manually click pause then play to resume the stream.

This started happening to me the other day as well. Really annoying as the player is not unpausing on it's own and will freeze.

Herdrage commented 9 months ago

I've been using Vaft+uBlock and AdBlock Plus to make it work but some streams (not all) still freeze when it starts blocking ads. It's weird that it seems to depend on the channel and not something else. Maybe it's related to autoran ads vs. manually ran ads?

Edit: I added the op's mentioned script as well and even the streams that would freeze also work better now with only a quick buffer after the ads are done running.

Squtzy commented 9 months ago

I've been using Vaft+uBlock and AdBlock Plus to make it work but some streams (not all) still freeze when it starts blocking ads. It's weird that it seems to depend on the channel and not something else. Maybe it's related to autoran ads vs. manually ran ads?

Edit: I added the op's mentioned script as well and even the streams that would freeze also work better now with only a quick buffer after the ads are done running.

I changed from the VAFT to the Video-Swap-New script and haven't had any issues since. Idk why VAFT started doing that for me :/

pixeltris commented 3 months ago

I'll make a note to take a look at this, thanks. Closing this as I'm consolidating all freezing / pausing / playback issues for vaft into #164