yzwone / t.me: fix popping sounds on vertical videos

// ==UserScript==
// @namespace     https://openuserjs.org/users/yzwone
// @name          t.me: fix popping sounds on vertical videos
// @description   Fix for popping sounds when a vertical video plays on https://t.me/*
// @license       GPL-3.0-only; http://www.gnu.org/licenses/gpl-3.0.txt
// @version       1.0.0
// @match         https://t.me/*
// @grant         none
// @run-at        document-end
// ==/UserScript==

// ==OpenUserJS==
// @author yzwone
// ==/OpenUserJS==

(function () {
  "strict"

  /**
   * TLDR;
   * Popping sounds when playing vertical videos in t.me happen because of blurred `<video>` elements.
   * A blurred video plays simultaneously with a normal video (they actually use the same video url) and
   * is used as "borders" for the normal video.
   * The popping sounds occur because the two videos desynchronize:
   * 1) at first we hear echo
   * 2) then we hear a popping sound - when the code periodically sets `currentTime` of the blurred video
   * to `currentTime` of the normal video.
   */

  // Parameters

  /**
   * The threshold value in seconds for writes to `<video>.currentTime`: the new value is only written
   * if it exceeds the current value by this threshold.
   */
  const CURRENT_TIME_SETTER_THRESHOLD_IN_SECONDS = 1;

  function main() {
    // process videos in the current frame: this is for urls like 'https://t.me/s/...' when multiple posts are shown
    processNestedVideos(document);

    // process videos in other frames: single posts are shown inside an `iframe`
    for (let i = 0; i < window.frames.length; i++) {
      processNestedVideos(window.frames[i].document);
    }

    // process videos, which are added dynamically when we scroll page
    const observer = new MutationObserver(mutationsList => {
      mutationsList.forEach(mutation => {
        if (mutation.addedNodes) {
          mutation.addedNodes.forEach(el => processNestedVideos(el));
        }
      });
    });
    observer.observe(document.body, {
      childList: true,
      subtree: true
    });
  }

  /**
   * @param {Node} rootEl
   */
  function processNestedVideos(rootEl) {
    if (!rootEl.querySelectorAll) {
      return;
    }
    // popping sounds are caused by blurred videos
    // (there are 2 `<video>` elements for every video post: one normal, and one blurred)
    rootEl
      .querySelectorAll('video.tgme_widget_message_video.js-message_video_blured')
      .forEach(videoBlurredEl => stopPoppingSounds(videoBlurredEl));
  }

  /**
   * Stop popping sounds caused by the given blurred video
   *
   * @param {HTMLVideoElement} videoBlurredEl
   */
  function stopPoppingSounds(videoBlurredEl) {
    // mute the blurred video (only the normal video will play sound)
    muteVideo(videoBlurredEl);

    // Unfortunately, the above is not enough: the normal video has a "timeupdate" event handler with this logic:
    //   if (videoBluredEl && videoBluredEl.currentTime != videoEl.currentTime) {
    //     videoBluredEl.currentTime = videoEl.currentTime;
    //   }
    // When "currentTime" is assigned a new value, then you hear a popping sound, even if the video is muted.
    // To fix that we override "currentTime" property so that it is only written if the
    // difference between the new value and the current value is bigger then the threshold
    addThresholdToCurrentTimeSetter(videoBlurredEl);
  }

  /**
   * @param {HTMLVideoElement} videoEl
   */
  function muteVideo(videoEl) {
    videoEl.muted = true;
    videoEl.defaultMuted = true;
    videoEl.volume = 0;
  }

  /**
   * Make `video.currentTime = newValue` to work only when the difference between
   * the current value and the `newValue` is greater than the given threshold
   *
   * @param {HTMLVideoElement} videoEl
   * @param {number} thresholdInSeconds
   */
  function addThresholdToCurrentTimeSetter(videoEl) {
    let currentDesc = null;
    for (let obj = videoEl; obj && (!currentDesc); obj = Object.getPrototypeOf(obj)) {
      currentDesc = Object.getOwnPropertyDescriptor(obj, 'currentTime');
    }
    if (!(currentDesc && currentDesc.get && currentDesc.set)) {
      console.warn("t.me video popping: property descriptor for video.currentTime wasn't found");
      return;
    }
    const oldGetter = currentDesc.get;
    const oldSetter = currentDesc.set;
    Object.defineProperty(videoEl, 'currentTime', {
      ...currentDesc,
      set: function (newVal) {
        const curVal = oldGetter.call(this);
        if (Math.abs(newVal - curVal) >= CURRENT_TIME_SETTER_THRESHOLD_IN_SECONDS) {
          oldSetter.call(this, newVal);
        }
      }
    });
  }

  main();
})();