Instagram Video Progressbar + Scrubbing

Adds a small progress bar under Instagram videos and lets you click/drag to seek (scrub). Works on dynamically loaded videos.

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Instagram Video Progressbar + Scrubbing
// @namespace    https://greasyfork.org/
// @version      1.0.0
// @description  Adds a small progress bar under Instagram videos and lets you click/drag to seek (scrub). Works on dynamically loaded videos.
// @match        *://www.instagram.com/*
// @author        X0John
// @run-at       document-end
// ==/UserScript==

(function () {
  'use strict';

  // ------------- CONFIG -------------
  const CONFIG = {
    heightPx: 4,// progress bar height in px
    bgColor: 'rgba(255,255,255,0.16)', // background bar color
    elapsedColor: '#ff2d55',// elapsed (filled) color
    disableLoop: false,// if true, force video.loop = false
    unmute: false,// if true, try to unmute video (may be fragile)
    debug: false // set true to enable console logs
  };
  // ----------------------------------

  function log(...args) { if (CONFIG.debug) console.log('IG-PROG:', ...args); }

  // Add progress UI & handlers for a single <video> element
  function attachProgress(video) {
    if (!video || video.dataset.igprogAttached) return;
    video.dataset.igprogAttached = '1';
    log('attachProgress', video);

    // Find an appropriate container to append the absolute-positioned bar.
    // Usually the video's parent element is usable; make it positioned if needed.
    const container = video.parentElement || video.parentNode || video;
    try {
      const cs = getComputedStyle(container);
      if (cs.position === 'static') {
        // set relative so absolute bar positions correctly
        container.style.position = 'relative';
        // small marker to remember we changed it
        container.dataset.igprogMadeRelative = '1';
      }
    } catch (err) {
      // ignore; fallback below
    }

    // Create bar elements
    const outer = document.createElement('div');
    outer.className = 'ig-progress-outer';
    const inner = document.createElement('div');
    inner.className = 'ig-progress-inner';
    outer.appendChild(inner);

    // Style
    Object.assign(outer.style, {
      position: 'absolute',
      left: '0',
      right: '0',
      bottom: '0',
      height: CONFIG.heightPx + 'px',
      background: CONFIG.bgColor,
      cursor: 'pointer',
      zIndex: '9999',
      pointerEvents: 'auto',
      userSelect: 'none',
      WebkitUserSelect: 'none',
      MozUserSelect: 'none',
      transition: 'opacity .2s',
      opacity: '0.9'
    });
    Object.assign(inner.style, {
      width: '0px',
      height: '100%',
      background: CONFIG.elapsedColor,
      transition: 'width 100ms linear'
    });

    // Insert: put after video so it overlays the bottom of the video
    container.appendChild(outer);

    // Seek logic using pointer events (works for mouse + touch)
    let dragging = false;
    let lastPointerId = null;

    function getRect() {
      // ensure outer.getBoundingClientRect() is used; it follows parent positioning
      return outer.getBoundingClientRect();
    }

    function clamp(n, a, b) { return Math.min(Math.max(n, a), b); }

    function seekByClientX(clientX) {
      const rect = getRect();
      const width = rect.width || outer.offsetWidth || video.offsetWidth;
      if (!width || !video.duration || !isFinite(video.duration)) return;
      const ratio = clamp((clientX - rect.left) / width, 0, 1);
      try { video.currentTime = ratio * video.duration; } catch (e) { /* ignore */ }
      inner.style.width = Math.ceil(ratio * width) + 'px';
    }

    function onPointerDown(e) {
      // Only handle primary button
      if (e.pointerType === 'mouse' && e.button !== 0) return;
      e.preventDefault();
      e.stopPropagation();

      dragging = true;
      lastPointerId = e.pointerId;
      // temporarily disable transition for smoother drag
      inner.style.transition = 'none';
      try { outer.setPointerCapture(e.pointerId); } catch (err) {}
      seekByClientX(e.clientX);
    }

    function onPointerMove(e) {
      if (!dragging) return;
      if (lastPointerId != null && e.pointerId !== lastPointerId) return;
      e.preventDefault();
      e.stopPropagation();
      seekByClientX(e.clientX);
    }

    function onPointerUp(e) {
      if (!dragging) return;
      if (lastPointerId != null && e.pointerId !== lastPointerId) return;
      e.preventDefault();
      e.stopPropagation();
      dragging = false;
      lastPointerId = null;
      // restore transition
      inner.style.transition = 'width 100ms linear';
      try { outer.releasePointerCapture && outer.releasePointerCapture(e.pointerId); } catch (err) {}
    }

    outer.addEventListener('pointerdown', onPointerDown, { passive: false });
    window.addEventListener('pointermove', onPointerMove, { passive: false });
    window.addEventListener('pointerup', onPointerUp, { passive: false });
    window.addEventListener('pointercancel', onPointerUp, { passive: false });

    // Update the bar as video plays (but avoid clobbering while dragging)
    function updateBar() {
      if (dragging) return;
      const dur = video.duration;
      if (!dur || !isFinite(dur)) return;
      const ratio = clamp(video.currentTime / dur, 0, 1);
      const w = outer.getBoundingClientRect().width || outer.offsetWidth || video.offsetWidth || 0;
      inner.style.width = Math.ceil(ratio * w) + 'px';
    }

    // When duration/metadata load, update once
    video.addEventListener('loadedmetadata', updateBar);
    // Real-time updates
    video.addEventListener('timeupdate', updateBar);
    // Also on play/seeked
    video.addEventListener('play', updateBar);
    video.addEventListener('seeked', updateBar);
    // If video ends, fill bar
    video.addEventListener('ended', () => { inner.style.width = '100%'; });

    // Optional: disable loop forcing
    if (CONFIG.disableLoop) {
      try { video.loop = false; } catch (e) {}
    }

    // Optional: attempt to unmute (may or may not work due to site controls)
    if (CONFIG.unmute && video.muted) {
      try { video.muted = false; } catch (e) {}
    }

    // Clean up if video removed from DOM
    const mo = new MutationObserver(() => {
      if (!document.contains(video)) {
        // remove listeners & bar
        outer.removeEventListener('pointerdown', onPointerDown);
        window.removeEventListener('pointermove', onPointerMove);
        window.removeEventListener('pointerup', onPointerUp);
        window.removeEventListener('pointercancel', onPointerUp);
        video.removeEventListener('loadedmetadata', updateBar);
        video.removeEventListener('timeupdate', updateBar);
        video.removeEventListener('play', updateBar);
        video.removeEventListener('seeked', updateBar);
        try { outer.remove(); } catch (ex) {}
        try { mo.disconnect(); } catch (ex) {}
        log('cleaned up for video');
      }
    });
    mo.observe(document.documentElement || document.body, { childList: true, subtree: true });

    // initial update (if metadata already loaded)
    setTimeout(updateBar, 50);
  }

  // Scan existing videos and attach
  function scanAndAttach(root = document) {
    const videos = root.querySelectorAll && root.querySelectorAll('video');
    if (!videos) return;
    videos.forEach(v => attachProgress(v));
  }

  // Observe DOM for added videos
  const observer = new MutationObserver((mutations) => {
    for (const m of mutations) {
      if (m.addedNodes && m.addedNodes.length) {
        m.addedNodes.forEach(node => {
          if (!node) return;
          if (node.nodeType !== 1) return;
          if (node.tagName && node.tagName.toLowerCase() === 'video') {
            attachProgress(node);
          } else {
            // might contain videos deep inside
            try {
              const vids = node.querySelectorAll && node.querySelectorAll('video');
              if (vids && vids.length) {
                vids.forEach(v => attachProgress(v));
              }
            } catch (e) { /* ignore cross-origin etc */ }
          }
        });
      }
    }
  });

  // Start
  scanAndAttach();
  observer.observe(document.documentElement || document.body, { childList: true, subtree: true });

  // Also periodically (safety) rescan a few times in case videos were added in awkward ways
  let tries = 0;
  const rescanInterval = setInterval(() => {
    scanAndAttach();
    tries++;
    if (tries > 20) clearInterval(rescanInterval);
  }, 1000);

  log('Instagram progressbar script loaded');

})();