A-B looper (Audio and Video)

Replays a segment back and forth between two user-defined markers

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name        A-B looper (Audio and Video)
// @namespace   Continuously replays a user-defined segment from A to B
// @match       *://*/*
// @grant       none
// @version     Alpha-v1
// @author      JesusisLord
// @description Replays a segment back and forth between two user-defined markers
// @license     MIT
// ==/UserScript==
// Variables for looping functionality, initialized for clarity
let timeA = null; // Time positions for looping
let timeB = null;
let click = 0; // Counter for click-based actions
let looper_video = null; // Reference to the video element

// Helper function to check for text field focus
function isAnyTextFieldFocused() {
  const activeElement = document.activeElement;

  // Early return for null or undefined activeElement
  if (!activeElement) {
    return false;
  }

  // Optimized matching for text fields
  const tagName = activeElement.tagName.toLowerCase();
  return (
    tagName === "textarea" || // Direct check for textareas
    (tagName === "input" && activeElement.type === "text") // Text input validation
  );
}

// Function to retrieve the currently playing video
function getVideo() {
  // Directly select all videos using querySelectorAll
  const videos = document.querySelectorAll('video');

  // Check if any videos are found
  if (videos.length > 0) {
    // Find the currently playing video based on paused state
    let currentVideo = Array.from(videos).find(video => !video.paused);

    // If no video is currently playing, find the one with the highest currentTime
    if (!currentVideo) {
      currentVideo = Array.from(videos).reduce((currentVid, nextVid) => {
        return nextVid.currentTime > currentVid.currentTime ? nextVid : currentVid;
      }, videos[0]); // Start with the first video as the initial value
    }

    return currentVideo;
  } else {
    // Return null if no videos are found
    return null;
  }
}

// Initialize MutationObserver to detect video changes
const observer = new MutationObserver(() => {
  getVideo(); // Refresh video detection on changes
});
// Configure the observer to watch for changes in video elements
observer.observe(document.body, { childList: true, subtree: true });

// Core looping algorithm
function loopingAlgorithm() {
  click++;

  if (click === 1) {
    pointA(looper_video);
  } else if (click === 2) {
    pointB(looper_video);
  } else {
    // Reset click counter after the third click
    click = 0;
  }
}

// Functions for setting loop points
function pointA(vid) {
  timeA = vid.currentTime;
}

function pointB(vid) {
  timeB = vid.currentTime;
  vid.ontimeupdate = function () { gotoPointA(vid) };
}

function gotoPointA(vid) {
  if (vid.currentTime >= timeB && click == 2) {
    vid.currentTime = timeA;
  }
}

function run() {
  looper_video = getVideo();

  if (looper_video && looper_video.src) {
      loopingAlgorithm();
  }
}


// Event listener for key press
document.addEventListener("keydown", (event) => {
  if (!isAnyTextFieldFocused() && event.code === "KeyA") {
    run();
  }
});