AutoPlay Disabled for HTML5 Videos + Pause on Switch Tab

Prevents auto-play HTML5 videos in new tabs on any page (not just YouTube) and pauses videos when leave/switch/change the current tab, to prevent any video playing in the background. This auto play blocker is an alternative to Click-to-Play / click to play. On some times like YouTube, click twice to begin first playback — From Dan Moorehead (xeonx1), developer of PowerAccess™ (http://PowerAccessDB.com)

// ==UserScript==
// @name         AutoPlay Disabled for HTML5 Videos + Pause on Switch Tab
// @namespace    xeonx1
// @version      1.83
// @description  Prevents auto-play HTML5 videos in new tabs on any page (not just YouTube) and pauses videos when leave/switch/change the current tab, to prevent any video playing in the background. This auto play blocker is an alternative to Click-to-Play / click to play. On some times like YouTube, click twice to begin first playback — From Dan Moorehead (xeonx1), developer of PowerAccess™ (http://PowerAccessDB.com)
// @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;

    // For websites you won't to disable this script on, you can add domains (with or without subdomain, must not include http://, etc.) to always allow auto-play (doesn't affect pause on leave tab)
    var autoPlaySitesWhitelist = [
        // "youtube.com"
    ];
    // For video hosting sources (eg. YouTube used for videos embedded on other sites), you can add domains (with or without subdomain, must not include http://, etc.) to always allow auto-play (doesn't affect pause on leave tab)
    var autoPlaySourcesWhitelist = [
        // "youtube.com"
    ];
  
    //Advanced preferences from controlling side compatibility / testing:
    //seems required for: 
    var handlePlayingInAdditionToPlayEvent = false;
    var allowPauseAgainAfterFirstFound = false;
    var treatPlayingLikeOnPlay = false;
  
    // ******** End Preferences ********

    /* Test Pages:
       https://www.youtube.com/watch?v=OMOVFvcNfvE
       http://www.imdb.com/title/tt2527336/videoplayer/vi1488632089
       https://trailers.apple.com/trailers/lucasfilm/star-wars-the-last-jedi/
       https://www.theguardian.com/film/video/2017/apr/14/star-wars-last-jedi-trailer-film-current-sequel-trilogy-video
       http://www.politico.com/video
       http://www.nfl.com/videos
       
       Known Issues:
         Have to click twice the first time to start:
             https://www.youtube.com/watch?v=OMOVFvcNfvE
             https://www.theguardian.com/film/video/2017/apr/14/star-wars-last-jedi-trailer-film-current-sequel-trilogy-video
         Clicking anywhere except Play button causes to pause (so seeking, or click on video itself to unpause)
             https://www.usatoday.com/story/life/movies/2017/04/14/star-wars-the-last-jedi-trailer-analysis/100466154/
         
       Still Auto Plays:
           http://www.cnn.com/videos  (eventually auto-plays, stops for long time first, seems to switch between a few videos)
        
         
       Test JS Snippets:
       document.querySelector("video.html5-main-video").pause()  //for pause on YouTube
       
       TODO (MAYBE):
         Allow pressing play after pressing spacebar?
         Delay YouTube pausing to avoid having to double click first time to play?
    */
  
    //TODO-MAYBE: We could add support for click anywhere on video to play/pause, but some video players may not update their play button status and therefore could be out-of-sync and many of them already support that (though not Apple Trailers, etc.)
  
    var hasAutoPlaySourcesWhitelist = autoPlaySourcesWhitelist.length > 0;
    var hasAutoPlaySitesWhitelist = autoPlaySitesWhitelist.length > 0;
    var lastClickTimeMs = 0;
  
  
    function isUrlMatch(url, pattern) {
        var regex = "https?\:\/\/[a-zA-Z0-9\.\-]*?\.?" + pattern.replace(/\./, "\.") + "\/";
        var reg = new RegExp(regex, "i");
        return url.match(reg) !== null;
    }
  
    //returns true if auto-play is always allowed for the *website* video is shown on (not source its hosted on)
    function isAutoPlayAllowedForSite(url) { // Check if video src is whitelisted.

        if (hasAutoPlaySitesWhitelist) {
            for (var i = 0; i < autoPlaySitesWhitelist.length; i++) {
                if (isUrlMatch(url, autoPlaySitesWhitelist[i]))
                  return true;
            }
        }
        return false;
    }
  
    //exit, if the page shown in tab is whitelisted (regardless of where video is hosted / embedded from)
    if (isAutoPlayAllowedForSite(document.url)) {
        return;
    }

    //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];
                
                pauseVideo(vid, true);
            }
        }
        //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 for the *website* video is shown on (not source its hosted on)
    //so YouTube videos embedded onto other sites will be blocked if YouTube is blocked here
    function isAutoPlayAllowedForSource(url) { // Check if video src is whitelisted.
        //NOTE: URL can start with blob: like on YouTube
        if (hasAutoPlaySourcesWhitelist) {
            for (var i = 0; i < autoPlaySitesWhitelist.length; i++) {
                if (isUrlMatch(url, hasAutoPlaySourcesWhitelist[i]))
                  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 pauseVideo(vid, isLeavingTab) {
        
        var eventName = "auto-play";
      
        if (isLeavingTab == true) //also handle undefind/unknown states
        {
            //OR: if wan't to avoid logging in some cases if not sure if playing:
            //if {vid.isPlaying != false) console.log("Paused video playback because switched away from this tab for video with source: ", vid.currentSrc); else logIt = false; 
          
            vid.wasPausedOnChangeTab = true;
          
            eventName = "on leaving tab";
            //console.log("Paused video playback because switched away from this tab for video with source: ", vid.currentSrc);
        
        }
        
        console.log("Paused video " + eventName + " from source: ", vid.currentSrc);
        
        //remember video is no longer playing -  just in case, though event handler for pause should also set this
        vid.isPlaying = false;
      
        //always pause regardless of isPlaying or isVideoPlaying() since aren't always reliable
        vid.pause();
            
    }
  
    function onPlay(e)
    {
        onPlayOrLoaded(e, true);
    }
    function onPlaying(e)
    {
        onPlayOrLoaded(e, false);
    }
    function onPlayOrLoaded(e, isPlayConfirmed) { // 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;
        }

        //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 && !isAutoPlayAllowedForSource(vid.currentSrc)) {

           pauseVideo(vid);
        } else
        {
           vid.isPlaying = isPlayConfirmed || treatPlayingLikeOnPlay;
        }
    }
  
    
    function addListenersToVideo(vid, srcChanged)
    { 
        var pauseNow = false;
        //if this is first time found this video
        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?
            if  (handlePlayingInAdditionToPlayEvent)
              safeAddHandler(vid, "playing", onPlaying);
            

            safeAddHandler(vid, "pause", onPaused);
            safeAddHandler(vid, "ended", onPaused);
            
            pauseNow = true;
        }
       //if video source URL has NOT changed and had already hooked up and paused this video before, then exit, don't pause again (in case user had clicked to play it earlier but another video injected into the page caused inspecting all videos again)
      //else if (srcChanged != true) 
      //      return; //exit, don't pause it again
      
       //pause the video since this is the first time was found OR src attribute had changed
      if (pauseNow || srcChanged == true) {
        
          pauseVideo(vid);
      
          if (allowPauseAgainAfterFirstFound) {
            vid.isPlaying = false; //allow upcoming first play event to cause pausing too this first time
          }
      }
    }
    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, true);
            }

            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();

})();