Better XVideos.com

Always expand video to large size, scroll down to video, use w a s d to rotate video, q e ` 1 2 3 4 5 z x to seek, v V to frame advance, allow seeking to 0:00, allow clicking anywhere to start video, infinite volume up and down - 2025-05-30, 14:44

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name        Better XVideos.com
// @namespace   Violentmonkey Scripts
// @match       https://www.xvideos.com/video*
// @grant       none
// @version     3.3
// @author      -
// @description Always expand video to large size, scroll down to video, use w a s d to rotate video, q e ` 1 2 3 4 5 z x to seek, v V to frame advance, allow seeking to 0:00, allow clicking anywhere to start video, infinite volume up and down - 2025-05-30, 14:44
// @require https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js
// ==/UserScript==

const gainStep = 0.8; // gain when stepping the volume down. volume up will be the inverse. configure to your liking.

this.$ = jQuery.noConflict(true);

const scrollToVideo = () => {
  $("div#video-player-bg")[0].scrollIntoView({
    behavior: 'instant',
    block: 'center',
    inline: 'center'
  });
};
// Poll until the video has the desired class
const scrollToVideoAfterFullWidth = () => {
  var host = {};
  const scrollPoll = (host) => {
    if ($("div#content")[0].classList.contains("player-enlarged")) {
      scrollToVideo();
      clearInterval(host.id);
      setTimeout(scrollToVideo, 500); // one more, for Jesus
    }
  }
  host.id = setInterval(scrollPoll.bind(null, host), 5);
};

$("div#video-player-bg").ready(() => {
  setTimeout(() => {
    if (! $("div#content")[0].classList.contains("player-enlarged")) {
      $("span.pif-full-width")[0].click();
    }
    scrollToVideoAfterFullWidth();
  }, 100);
});

// Poll until the video has changed its full screen status and then run callback multiple times to make sure it hits right.
const waitFullScreenChange = (callback) => {
  const initial = html5player.isFullScreen;
  var host = {};
  const fullScreenPoll = (host) => {
    if (html5player.isFullScreen != initial) {
      setTimeout(callback, 10);
      setTimeout(callback, 50);
      setTimeout(callback, 75);
      setTimeout(callback, 100);
      setTimeout(callback, 200);
      clearInterval(host.id);
    }
  }
  host.id = setInterval(fullScreenPoll.bind(null, host), 5);
};

// Somehow, the below didn't work - the button got added, but when clicking it,
// it showed the menu for the next button to the right (originally from xvideos),
// and that button showed the menu for the next one, etc.
//
//$("button#v-actions-overflow-menu").ready(function () {
//  var rotationButtons = `<button class="tab-button rotate-video-right"><span>Rotate video right (E)</span></button>`
//  $(rotationButtons).insertAfter("button#v-actions-overflow-menu")
//})

const video = $("div#video-player-bg video")[0];

//keyboard handler for various things
$("html").ready(() => {


  // make whole player clickable to start playing
  $("div.video-pic").click(() => { $("span.pif-play").trigger("click") });


  // define our keyboard handler

  var orientation = 0; // 0, 90, 180, 270

  const setVideoOrientation = (angle) => { // set absolute rotation for video
    if (0 === angle) { // no rotation
      $("div.video-bg-pic > video").css("transform", "");
      return true;
    } else if(90 === angle) { // rotated to the right
      const fitScale = Math.min(video.videoWidth / video.videoHeight, video.videoHeight / video.videoWidth);
      const transform = `rotate(${angle}deg) scale(${fitScale})`;
      $("div.video-bg-pic > video").css("transform", transform);
      return true;
    } else if (180 === angle) { // upside down
      $("div.video-bg-pic > video").css("transform", "scaleX(-1) scaleY(-1)");
      return true;
    } else if (270 === angle) { // rotated left
      const fitScale = Math.min(video.videoWidth / video.videoHeight, video.videoHeight / video.videoWidth);
      const transform = `rotate(${angle}deg) scale(${fitScale})`;
      $("div.video-bg-pic > video").css("transform", transform);
      return true;
    }
    return false;
  };

  const cycleOrientationRight = () => {
    const desiredOrientation = (orientation % 360) + 90;
    const moduloOrientation = desiredOrientation % 360; // make sure it's 0...359
    orientation = moduloOrientation;
  };

  const cycleOrientationLeft = () => {
    const desiredOrientation = (orientation % 360) - 90;
    const moduloOrientation = (desiredOrientation + 360) % 360; // make sure it's 0...359. note x % 360 clamps to (-359.999... ... 359.999)
    orientation = moduloOrientation;
  };

  const cycleOrientation180 = () => {
    const desiredOrientation = (orientation % 360) + 180;
    const moduloOrientation = desiredOrientation % 360; // make sure it's 0...359
    orientation = moduloOrientation;
  };

  // Seek to fraction of video between 0 and 1, meaning start and end.
  const seekFraction = (fraction) => {
    if((fraction > 1) || (fraction < 0)) {
      return false;
    }
    video.currentTime = video.duration * fraction;
    return true;
  };

  const videoExtraGain = (mediaElem) => {
    // Chrome compat
    var context = new(window.AudioContext || window.webkitAudioContext);
    var result = {
      context: context,
      source: context.createMediaElementSource(mediaElem),
      gain: context.createGain(),
      media: mediaElem,
      amplify: function(multiplier) {
        result.gain.gain.value = multiplier;
      },
      getAmpLevel: function() {
        return result.gain.gain.value;
      }
    };
    result.source.connect(result.gain);
    result.gain.connect(context.destination);
    result.amplify(1);
    return result;
  };

  const vidGain = videoExtraGain(video);
  vidGain.amplify(1);

  var totalGain = video.volume;

  const gainToDb = (gain) => 20 * Math.log10(gain);

  const volumeSet = (desiredGain) => {
    const dBString = gainToDb(desiredGain).toFixed(1);
    if (desiredGain <= 1) {
      vidGain.amplify(1);
      html5player.setVolume(desiredGain);
    } else {
      html5player.setVolume(1);
      vidGain.amplify(desiredGain);
    }
    console.log(`Set gain to ${dBString} dB.`); // logging at the end to make it show up after website spam
  };

  const volumeUp = () => {
    if (totalGain <= 0) {
      totalGain == 0.0000001; // 0 multiplied by anything is always zero, so we need to be able to escape it.
    }
    const desiredGain = totalGain / gainStep;
    totalGain = desiredGain;
    volumeSet(desiredGain);
  };

  const volumeDown = () => {
    const desiredGain = totalGain * gainStep;
    totalGain = desiredGain;
    volumeSet(desiredGain);
  };

  const playPause = () => {
    if (video.paused) {
      video.play();
    } else {
      video.pause();
    }
  };

  const fullScreen = () => {
    waitFullScreenChange(() => { setVideoOrientation(orientation); });
    html5player.fullscreen();
    return true;
  };

  const keyboardHandlerInner = (event) => {
    if ("w" == event.key) { // restore upright rotation (no rotation)
      orientation = 0;
      setVideoOrientation(orientation);
      return true;
    }
    if ("a" == event.key) { // rotate head 90 degrees to the left
      cycleOrientationRight();
      setVideoOrientation(orientation);
      return true;
    }
    if ("s" == event.key) { // turn upside down
      cycleOrientation180();
      setVideoOrientation(orientation);
      return true;
    }
    if ("d" == event.key) { // rotate head 90 degrees to the right
      cycleOrientationLeft();
      setVideoOrientation(orientation);
      return true;
    }
    if ( ("ArrowLeft" == event.key) || ("q" == event.key) ) { // seek left
      video.currentTime = Math.max(0, video.currentTime - 10);
      return true;
    }
    if ( ("ArrowRight" == event.key) || ("e" == event.key) ) { // seek right
      video.currentTime = Math.min(video.currentTime + 10, Math.floor(video.duration));
      return true;
    }
    if ("z" == event.key) { // seek left long
      video.currentTime = Math.max(0, video.currentTime - 60);
      return true;
    }
    if ("x" == event.key) { // seek right long
      video.currentTime = Math.min(video.currentTime + 60, Math.floor(video.duration));
      return true;
    }
    if ("v" == event.key) { // frame advance (we don't know the FPS so we'll just assume 30 fps)
      video.pause();
      video.currentTime = Math.min(video.currentTime + 1/30, Math.floor(video.duration));
      return true;
    }
    if ("V" == event.key) { // frame advance backwards (we don't know the FPS so we'll just assume 30 fps)
      video.pause();
      video.currentTime = Math.max(0, video.currentTime - 1/30);
      return true;
    }
    if (" " == event.key) { // space - play/pause
      playPause();
      return true;
    }

    if ("`" == event.key) { seekFraction(  0); return true; }
    if ("1" == event.key) { seekFraction(0.1); return true; }
    if ("2" == event.key) { seekFraction(0.2); return true; }
    if ("3" == event.key) { seekFraction(0.4); return true; }
    if ("4" == event.key) { seekFraction(0.6); return true; }
    if ("5" == event.key) { seekFraction(0.8); return true; }

    if ("ArrowUp"   == event.key) { volumeUp();   return true; }
    if ("ArrowDown" == event.key) { volumeDown(); return true; }

    if ("f" == event.key) { fullScreen(); return true; }

    return false;
  };

  const handlerStopsEvents = (handler) => {
    const wrapper = (event) => {
      // console.log('handler: event.target: ', event.target);
      // console.log('handler: event.currentTarget: ', event.currentTarget);

      const ret = handler(event);
      if (ret) {
        event.stopImmediatePropagation();
        event.preventDefault();
        return true;
      }
      return null; // pass to xvideos own handler and the browser. only happens on null, and never on false etc.
    };
    return wrapper;
  };

  videoClickHandlerDiv = document.querySelector('div.video-click-handler');

  const isOverNavBar = (event) => {
    return ( (event.offsetY > videoClickHandlerDiv.clientHeight * 0.8) || (event.offsetY > videoClickHandlerDiv.clientHeight - 140) );
  };

  const mouseMoveHandlerInner = (event) => {
    // stop the nav bar from popping up on mouse move unless the mouse cursor is close to it.
    if (! isOverNavBar(event) ) {
      return true;
    }
    return false; // pass to xvideos own handler and the browser
  };

  const clickHandlerInner = (event) => {
    // stop the nav bar from popping up on click unless the mouse cursor is close to it.
    if (! isOverNavBar(event) ) {
      $("div.video-click-handler").trigger('mouseup');
      playPause();
      return true;
    }
    return false; // pass to xvideos own handler and the browser
  }

  const keyboardHandler = handlerStopsEvents(keyboardHandlerInner);

  $("html").keydown(keyboardHandler);

  const mouseMoveHandler = handlerStopsEvents(mouseMoveHandlerInner);

  $("div.video-click-handler").mousemove(mouseMoveHandler);

  // const clickHandler = handlerStopsEvents( (event) => { console.log('click handler: event:', event); return false; });

  const clickHandler = handlerStopsEvents(clickHandlerInner);

  // TODO: doesn't work currently for some reason. it triggers the drag-to-seek functionality.
  // $("div.video-click-handler").click(clickHandler);




  /* actually this whole thing with finding the old handler doesn't do anything anymore, the handler always comes up null.

  // attach our keyboard handler - step 1: find old keyboard handler

  const getHandlerOnElm = (handler, element) => {
    var ret = null;
    $.each($._data(element, "events"), function(i, event) {
      // i is the event type, like "click"
      if (i != handler) return;
      $.each(event, function(j, h) {
        // h.handler is the function being called
        ret = h.handler;
      });
    });
    return ret;
  }

  const xvKeyboardHandler = getHandlerOnElm("keydown", $("html")[0]);

  // const xvMouseOverHandler = getHandlerOnElm("mouseover", $("div.video-click-handler")[0]);

  // attach our keyboard handler - step 2: override keyboard handler with our pass-through

  // $("html").off("keydown", "**") // BROKEN - currently doesn't detach the original listener

  $("html").keydown((event) => {
    if((keyboardHandler(event) === false) && (xvKeyboardHandler !== null)) {
      xvKeyboardHandler(event); // BROKEN - the original listener fires no matter what
    }
  });
  */




});