// ==UserScript==
// @name Disable HTML5 Video AutoPlay and Background Play (Pause On Leave Tab)
// @namespace xeonx1
// @version 1.5
// @description Prevents HTML5 videos from auto-playing in new tabs on any page (not just YouTube) and pauses videos when leave tab. NOTE: On YouTube, click the main video area to play the video so don't have to click the Pause/Play button twice for the first time. From Dan Moorehead (xeonx1), developer of PowerAccess™ (http://PowerAccessDB.com) (https://twitter.com/PowerAccessDB).
// @author Dan Moorehead (xeonx1), developer of PowerAccess™ (http://PowerAccessDB.com) (https://twitter.com/PowerAccessDB) and
// @match http://*/*
// @match https://*/*
// @grant none
// ==/UserScript==
(function () {
'use strict';
// **** User Preferences ****
//whether to pause videos when leaving a tab they are playing on
var pauseOnLeaveTab = true;
// Number of milliseconds after clicking where a video is allowed to autoplay.
var allowAutoPlayWithinMillisecondsOfClick = 500;
//you can add domains (with or without subdomain, must not include http://, etc.) to always allow auto-play
//pause on leave tab will still function however
var autoPlayDomains = [
/*"youtube.com"*/
];
var hasAutoPlayDomains = autoPlayDomains.length > 0;
// **** End Preferences ***
/*
Beyond this point is the logic of the script.
Do not edit unless you know what you are doing.
*/
var lastClickTimeMs = 0;
//determine name of event for switched away from tab, based on the browser
var tabHiddenPropertyName, tabVisibleChangedEventName;
if ("undefined" !== typeof document.hidden) {
tabHiddenPropertyName = "hidden";
tabVisibleChangedEventName = "visibilitychange";
} else if ("undefined" !== typeof document.webkitHidden) {
tabHiddenPropertyName = "webkitHidden";
tabVisibleChangedEventName = "webkitvisibilitychange";
} else if ("undefined" !== typeof document.msHidden) {
tabHiddenPropertyName = "msHidden";
tabVisibleChangedEventName = "msvisibilitychange";
}
function safeAddHandler(element, event, handler) {
element.removeEventListener(event, handler);
element.addEventListener(event, handler);
}
function getVideos() {
//OR: Can also add audio elements
return document.getElementsByTagName("video");
}
function isPlaying(vid) {
return !!(vid.currentTime > 0 && !vid.paused && !vid.ended && vid.readyState > 2);
}
function onTabVisibleChanged() {
//console.log("Tab visibility changed for Video auto-player disabling user script. Document is hidden status: ", document[tabHiddenPropertyName]);
var videos = getVideos();
//if doc is hidden (switched away from that tab), then pause all its videos
if (document[tabHiddenPropertyName]) {
//remember had done this
document.wasPausedOnChangeTab = true;
//pause all videos, since
for (var i = 0; i < videos.length; i++) {
var vid = videos[i];
if (vid.isPlaying) {
vid.wasPausedOnChangeTab = true;
console.log("Paused video playback because switched away from this tab for video with source: ", vid.currentSrc);
}
//always pause just in case isPlaying isn't always reliable
vid.pause();
}
}
//document is now the active tab
else {
document.wasPausedOnChangeTab = false; //reset state (unless need to use this field or delay this)
//TODO-MAYBE: if want to auto-play once switch back to a tab if had paused before, then uncomment below, after changing from forEach() to for loop
// getVideos().forEach( function(vid) {
// if (vid.wasPausedOnChangeTab == true) {
// vid.wasPausedOnChangeTab = false;
// vid.play();
// }
// } );
}
}
//handle active tab change events for this document/tab
if (pauseOnLeaveTab) {
safeAddHandler(document, tabVisibleChangedEventName, onTabVisibleChanged);
}
//returns true if auto-play is always allowed, whitelisted, so should do nothing to it
function isAutoPlayAllowedDomain(s) { // Check if video src is whitelisted.
if (hasAutoPlayDomains) {
for (var i = 0; i < autoPlayDomains.length; i++) {
var reg = new RegExp("https?\:\/\/[a-zA-Z0-9\.\-]*?\.?" + autoPlayDomains[i].replace(/\./, "\.") + "\/", "i");
if (s.match(reg) !== null) {
return true;
}
}
}
return false;
}
//on pause or ended/finished, change playing state back to not-playing, so know to start preventing playback again unless after a click
function onPaused(e)
{
e.target.isPlaying = false;
}
function onPlay(e) { // React when a video begins playing
var msSinceLastClick = Date.now() - lastClickTimeMs;
var vid = e.target;
//exit, do nothing if is already playing (but not if undefined/unknown), in case clicked on seekbar, volume, etc. - don't toggle to paused state on each click
if(vid.isPlaying == true) {
return;
}
vid.isPlaying = true;
//if haven't clicked recently on video, consider it auto-started, so prevent playback by pausing it (unless whitelisted source domain to always play from)
if (msSinceLastClick > allowAutoPlayWithinMillisecondsOfClick && !isAutoPlayAllowedDomain(vid.currentSrc)) {
vid.pause();
//remember video is no longer playing - just in case, though event handler for pause should also set this
vid.isPlaying = false;
console.log("Paused video from source: ", vid.currentSrc);
}
}
function addListenersToVideo(vid)
{
if (vid.hasAutoPlayHandlers != true) {
vid.hasAutoPlayHandlers = true;
safeAddHandler(vid, "play", onPlay);
//NOTE: Seems playing is needed in addition to play event, but isn't this just supposed to occur whenever play, plus after play once buffering is finished?
safeAddHandler(vid, "playing", onPlay);
safeAddHandler(vid, "pause", onPaused);
safeAddHandler(vid, "ended", onPaused);
}
}
function addListeners() {
var videos = getVideos();
//OR: Can get audio elements too
for (var i = 0; i < videos.length; i++) {
// Due to the way some sites dynamically add videos, the "playing" event is not always sufficient.
// Also, in order to handle dynamically added videos, this function may be called on the same elements.
// Must remove any existing instances of this event listener before adding. Prevent duplicate listeners.
var vid = videos[i];
addListenersToVideo(vid);
}
}
//handle click event so can limit auto play until X time after a click
safeAddHandler(document, "click", function () {
lastClickTimeMs = Date.now();
});
var observer = new MutationObserver(function(mutations) {
// Listen for elements being added. Add event listeners when video elements are added.
mutations.forEach(function(mutation) {
if (mutation.type == "attributes" && mutation.target.tagName == "VIDEO") { //&& mutation.attributeName == "src"
videoAdded = true;
addListenersToVideo(mutation.target);
}
if (mutation.addedNodes.length > 0) {
addListeners();
//faster to use getElementsByTagName() for rarely added types vs. iterating over all added elements, checking tagName
// for (var i = 0; i < mutation.addedNodes.length; i++) {
// var added = mutation.addedNodes[i];
// if (added.nodeType == 1 && added.tagName == "VIDEO") {
// videoAdded = true;
// }
// }
}
});
});
//subscribe to documents events for node added and src attribute changed via MutatorObserver, limiting to only src attribute changes
observer.observe(document, { attributes: true, childList: true, subtree: true, characterData: false, attributeFilter: ['src'] });
//don't also need to handle "spfdone" event
//hookup event handlers for all videos that exist now (will still add to any that are inserted later)
addListeners();
})();