Better HTML5 YouTube

By DaVince Last update Jul 28, 2011 — Installed 13,710 times.

There are 30 previous versions of this script.

// ==UserScript==
// @name           Better HTML5 YouTube
// @description    More keyboard controls for the HTML5 YouTube player (like seeking and changing video speed) and other fixes/additions to the player.
// @author         DaVince <VincentBeers@gmail.com>
// @namespace      http://davince.tengudev.com/
// @include        http://youtube.com/watch?*
// @include        http://*.youtube.com/watch?*
// @include        http://youtube.com/watch#*
// @include        http://*.youtube.com/watch#*
// @include        http://*.youtube.com/embed/*
// @include        http://youtube.com/embed/*
// @version        2011-07-28
// ==/UserScript==


/************************
  Customizable variables
*************************/
//Please use the settings panel to alter these.
var timestep = parseFloat(GM_getValue("timestep", 3));
var bigtimestep = parseFloat(GM_getValue("bigtimestep", 15));
var speed_throttle = parseFloat(GM_getValue("speed_throttle", 0.2));
var autopause = GM_getValue("autopause", 1);
var enable_overlay = GM_getValue("enable_overlay", 1);
var autofullscreen = GM_getValue("autofullscreen", 0);


/************************
  Important global variables
*************************/
var isfirefox = navigator.userAgent.indexOf("Firefox")!=-1;
var isfullscreen = false;
var video = undefined;
var overlay = undefined;
var configpanel = undefined;
window.addEventListener('load', Main, false);


function Main() {
  video = document.getElementsByTagName('video')[0];
  if (video) {
    if (autopause == 1) { video.pause(); video.currentTime = 0; }
    video.playbackRate = 1;  //Fix for Firefox
    
    var playerelement = document.getElementById("watch-player");
    document.getElementById("video-player").focus();
    FixTimeLinks();
    
    /** Special thanks to Snehonja for the following! **/
    if (isfirefox)
    {
      document.addEventListener("keydown", KeyInput, false);
      var inputs = document.getElementsByTagName("input");
      for (i = 0; i < inputs.length; i++) {
        inputs[i].addEventListener("keydown", function(e){
          e.stopPropagation();
        }, false);
      }

      var textareas = document.getElementsByTagName("textarea");
      for (i = 0; i < textareas.length; i++) {
        textareas[i].addEventListener("keydown", function(e){
          e.stopPropagation();
        }, false);
      }
    }
    else {
      playerelement.addEventListener("keydown", KeyInput, false);
    }
    
    overlay = new Overlay();
    if (autofullscreen == 1) { ToggleFullscreen(); overlay.hide(); }
    configpanel = new ConfigPanel();
    video.addEventListener("ended", function() { overlay.showSuggestions(); }, false);
  }
}


/** Fix the times in comments to work with the HTML5 player **/
function FixTimeLinks() {
  if (isfirefox) return;  //TODO: fix for FF somehow
  var comment_elements = document.getElementsByClassName("comment-text");
  var timevalue;
  
  //Get the time comments and bind new video navigation code to them.
  for (var i = 0; i < comment_elements.length; i++) {
    linknode = comment_elements[i].firstChild.nextSibling.firstChild;
    if (linknode.nodeName == "A" && linknode.innerHTML[0] != "@") {  //Filter out non-link comments and links that are @username.
      linknode.setAttribute("data-timevalue", linknode.innerHTML);
      
      linknode.onclick = function() {  //click event should be replaced in this case
        timevalue = this.getAttribute("data-timevalue").split(":");
        video.currentTime = Number(timevalue[0])*60+Number(timevalue[1]);
        overlay.show("Time: " + GetTime(video.currentTime) + " / " + GetTime(video.duration));
      }
    }
  }
  
  FixPageLinks();
}


/** Adds event listeners to comment page links so FixTimeLinks is run when they are clicked. **/
function FixPageLinks() {
  //Next and prev button will launch FixTimeLinks 1500ms after being clicked so everything gets linked properly.
  //TODO: ensure load instead of giving it 1500ms to load the comments?
  var nextprevlinks = document.getElementsByClassName("yt-uix-pager-link");
  for (var i = 0; i < nextprevlinks.length; i++) {
    nextprevlinks[i].addEventListener("click", function() { setTimeout(function() { FixTimeLinks(); }, 1500); }, false);
  }
  
  //Do the same for the 1...10 page buttons.
  var pagenavlinks = document.getElementsByClassName("yt-uix-pager")[0];
  if (pagenavlinks) {
    pagenavlinks = pagenavlinks.childNodes;
    for (var i = 0; i < pagenavlinks.length; i++) {
      if (pagenavlinks[i].nodeName == "BUTTON") {
        pagenavlinks[i].addEventListener("click", function() { setTimeout(function() { FixTimeLinks(); }, 1500); }, false);
      }
    }
  }
}


/** Get key input **/
function KeyInput(e) {
  //alert(e.keyCode);  //Uncomment to find out keypresses
  var shift = e.shiftKey;
  var ctrl = e.ctrlKey;

  switch (e.keyCode) {
    case 32:  //Space bar
      var button;
      var message;
      e.preventDefault();
      
      if (video.paused) {
        button = document.getElementsByClassName("html5-play-button")[0];
        message = 'Video play<br><span style="font-size: x-large;">Time: ' + GetTime(video.currentTime) + " / " + GetTime(video.duration);
      }
      else {
        button = document.getElementsByClassName("html5-pause-button")[0];
        message = "Pause";
      }
      button.click();
      overlay.show(message);
      break;
    
    //Left/right key navigation
    case 37: case 39:  //Key left and right
      var timedifference = e.keyCode-38;  //A second less or more
      video.currentTime += (shift ? bigtimestep : timestep) * timedifference;
      overlay.show("Time: " + GetTime(video.currentTime) + " / " + GetTime(video.duration));
    break;

    //Go out of fullscreen with the Esc key
    case 27: case 70:  //Key Esc and F
      if (!ctrl) ToggleFullscreen();
    break;

    //Change speed using + and - keys, reset speed with 0 and =
    case 107: case 221:  //+ on keypad and ]
      SetVideoSpeed(video.playbackRate+speed_throttle);
    break;

    case 109: case 189: case 219:  //- keys and [
      SetVideoSpeed(video.playbackRate-speed_throttle);
    break;

    case 187: case 220:  //key = and key \
      SetVideoSpeed(1);
    break;

    //Stop the video and return to beginning with . key (thanks, Orion751!)
      case 190: case 110: //Keys .
      video.pause();
      video.currentTime = 0;
      overlay.show("Video reset and paused");
    break;

    //Toggle Mute with m key (thanks, Orion751!)
    case 77:  //Key m
      video.muted^=true;
      //Update volume icon:
      document.getElementsByClassName('html5-icon')[3].src = document.getElementsByClassName('html5-icon')[3].src;
      if (video.muted) overlay.show("Audio: mute");
      else overlay.show("Audio: on");
    break;

    //Update Volume icon when using up and down keys (thanks, Orion751!)
    case 38: case 40: //Key up and down
    //Update volume icon:
      document.getElementsByClassName('html5-icon')[3].src = document.getElementsByClassName('html5-icon')[3].src;
      overlay.show("Volume: " + Math.round(video.volume*100) + "%");
    break;

    case 84:  //Key T
      overlay.show("Time: " + GetTime(video.currentTime) + " / " + GetTime(video.duration));
      break;

    /** IDEAS & TODOS
      * For F11: compare viewport and screen sizes and go fullscreen when they're close to equal?
      * Make a function for setting the video speed to lower code duplication
      * ---
      * Extra:
      * Have it work with embedded YouTube content.
      */
  }

  /* More types of key input go below the switch/case if they are a large group of keys. */

  //Set video position using the num keys
  if (e.keyCode >= 48 && e.keyCode <= 57) {
    var pos = e.keyCode - 48;
    video.currentTime = (video.duration/10)*pos;
    overlay.show("Time: " + GetTime(video.currentTime) + " / " + GetTime(video.duration));
  }
  else if (e.keyCode >= 96 && e.keyCode <= 105) {
    var pos = e.keyCode - 96;
    video.currentTime = (video.duration/10)*pos;
    overlay.show("Time: " + GetTime(video.currentTime) + " / " + GetTime(video.duration));
  }
}


/** Create and manage the video overlay. */
function Overlay() {
  this.element = document.createElement("div");
  this.element.setAttribute("class", "video-overlay");
  this.element.setAttribute("style", 'padding: 0 5px; float: left; display: none; position: \
                            relative; top: 10px; left: 20px; font-size: 30pt; color: white; background: rgba(0,0,0,0.5); z-index: 10000;');
  
  this.cur_timeout = 0;
  
  var playerelement = document.getElementById("video-player");
  playerelement.appendChild(this.element);
  
  this.show = function(what, indefinite) {
    if (enable_overlay == 0) return;
    clearTimeout(this.cur_timeout);
    this.element.innerHTML = what;
    this.element.style.display = "inline";
    if (!indefinite) this.cur_timeout = setTimeout(function() { overlay.hide() }, what.length*20+1000);
  }
  
  this.hide = function() {
    this.element.style.display = "none";
  }
  
  this.showSuggestions = function() {
    if (isfullscreen) ToggleFullscreen();
    overlay.show("Video stop");
    
    //TODO: to finish.
    /*
    var suggestionelement = document.createElement("div");
    suggestionelement.setAttribute("style", 'margin: 0; padding: 1.5%; position: relative; font-size: large; \
    color: white; width: 100%; height: 100%;');
    
    suggestionelement.innerHTML =
    '<span style="font-size: large;"> \
    <style type="text/css">.h5sp-suggestion-box { display: block; float: left; height: ' +
    '21%; width: 35%; margin: 0 1.5% 1.5% 0; background: rgba(140,0,0,0.85); clear: both; } \
    .h5sp-big-suggestion-box { }</style> \
    <div class="h5sp-suggestion-box">Testing!</div> \
    <div class="h5sp-suggestion-box">Testing 2!</div> \
    <div class="h5sp-suggestion-box">Testing 3!</div> \
    <div class="h5sp-suggestion-box">Testing 4!</div> \
    \
    </span>';
    
    playerelement.appendChild(suggestionelement);
    */
  }
}


/** Get video time as a neat string. **/
function GetTime(time) {
  var string = "";
  var hours = Math.floor(time/3600);
  var minutes = Math.floor(time/60);
  var seconds = Math.floor(time%60);
  
  string += (hours) ? (hours+":") : "";  //Is hours not 0? Then display it. Otherwise don't bother.
  string += (minutes<10) ? "0"+minutes : minutes;  //Are minutes a single digit? Prepend a 0.
  string += ":";
  string += (seconds<10) ? "0"+seconds : seconds;
  return string;
}


function ConfigPanel() {
  //Create and place the button.
  var button = document.createElement("button");
  button.setAttribute("title", "Better HTML5 YouTube settings");
  button.setAttribute("type", "button");
  button.setAttribute("class", "yt-uix-tooltip-reverse yt-uix-button yt-uix-tooltip");
  button.setAttribute("role", "button");
  button.setAttribute("aria-pressed", "false");
  button.innerHTML = '<span class="yt-uix-button-content">HTML5 settings</span>';
  button.onclick = ";return false;";
  button.addEventListener("click", function() { (configpanel.element.style.display == "none") ? configpanel.show() : configpanel.hide(); }, false);
  document.getElementById("watch-actions").appendChild(button);
  
  //Create the config element.
  this.element = document.createElement("div");
  this.element.id = "html5-settings-panel";
  this.element.style.display = "none";
  this.element.innerHTML =
    '<style type="text/css">#html5-settings-panel td, #html5-settings-panel h3 { padding: 0 5px; }</style>'+
    '<h3><strong>Better HTML5 YouTube settings panel</strong></h3><form><table><tbody>'+
    '<tr><td>Action</td><td>Key</td><td>Setting</td></tr>'+
    
    '<tr style="display: none;"><td>Small time step</td><td>Left / right</td>'+
    '<td><input id="h5sp-timestep" type="text" style="width: 30px;" value="'+timestep+'"> seconds</td></tr>'+
    
    '<tr style="display: none;"><td>Big time step</td><td>Shift + left / right</td>'+
    '<td><input id="h5sp-bigtimestep" type="text" style="width: 30px;" value="'+bigtimestep+'"> seconds</td></tr>'+
    
    '<tr><td>Speed up/down throttle</td><td>[ or -; ] or +; = or \\</td>'+
    '<td><input id="h5sp-speed" type="text" style="width: 30px;" value="'+speed_throttle+'"> times</td></tr>'+
    
    '<tr><td>Pause on page load</td><td>N/A</td>'+
    '<td><select id="h5sp-autopause"><option value="true">True</option><option value="false">False</option></select></td></tr>'+
    
    '<tr><td>Enable video overlay</td><td>N/A</td>'+
    '<td><select id="h5sp-overlay"><option value="true">True</option><option value="false">False</option></select></td></tr>'+
    
    '<tr><td>Toggle autofullscreen</td><td>F or Esc = fullscreen</td>'+
    '<td><select id="h5sp-autofullscreen"><option value="true">True</option><option value="false">False</option></select></td></tr>'+
    
    '</tbody></table><button id="h5sp-save" class="yt-uix-button" value="save">Save settings</button>'+
    '<button id="h5sp-cancel" class="yt-uix-button" value="cancel">Close without saving</button>'+
    '</form>';
  
  document.getElementById("watch-actions").appendChild(this.element);
  document.getElementById("h5sp-save").addEventListener("click",
    function(e) { e.preventDefault(); configpanel.save(); configpanel.hide(); }, false);
  document.getElementById("h5sp-cancel").addEventListener("click", function(e) { e.preventDefault(); configpanel.hide(); }, false);
  
  //Values are flipped around because option 0 is true and option 1 is false.
  document.getElementById("h5sp-autopause").selectedIndex = (autopause == 1) ? 0 : 1;
  document.getElementById("h5sp-overlay").selectedIndex = (enable_overlay == 1) ? 0 : 1;
  document.getElementById("h5sp-autofullscreen").selectedIndex = (autofullscreen == 1) ? 0 : 1;
  
  this.show = function() {
    this.element.style.display = "block";
  }
  
  this.hide = function() {
    this.element.style.display = "none";
  }
  
  /** Save the settings. **/
  this.save = function() {
    //Load values from the settings panel into the regular variables.
    timestep = parseFloat(document.getElementById("h5sp-timestep").value);
    bigtimestep = parseFloat(document.getElementById("h5sp-bigtimestep").value);
    speed_throttle = parseFloat(document.getElementById("h5sp-speed").value);
    autopause = (document.getElementById("h5sp-autopause").selectedIndex == 1) ? 0 : 1;  //Saved as int for GreaseMonkey compatibility
    enable_overlay = (document.getElementById("h5sp-overlay").selectedIndex == 1) ? 0 : 1;
    autofullscreen = (document.getElementById("h5sp-autofullscreen").selectedIndex == 1) ? 0 : 1;
    
    //TODO: check for validity in values (must be a number)
    //Store values persistently.
    GM_setValue("timestep", String(timestep));
    GM_setValue("bigtimestep", String(bigtimestep));
    GM_setValue("speed_throttle", String(speed_throttle));
    GM_setValue("autopause", autopause);
    GM_setValue("enable_overlay", enable_overlay);
    GM_setValue("autofullscreen", autofullscreen);
    
    overlay.show("Settings saved.");
  }
}

function SetVideoSpeed(speed) {
  var speedlabel = document.getElementsByClassName('html5-speed-button')[0].childNodes[1];
  video.playbackRate = speed;
  speedlabel.innerHTML = Math.round(video.playbackRate*100)/100 + "x Speed";
  if (video.playbackRate == 1) { speedlabel.innerHTML = "Normal"; overlay.show("Speed: Normal"); }
  else overlay.show("Speed: " + Math.round(video.playbackRate*100)/100);
}

function ToggleFullscreen() {
  isfullscreen = !isfullscreen;
  document.getElementsByClassName('html5-fullscreen-button')[0].click();
  overlay.show('<span style="font-size: x-large;">Toggling fullscreen</span>');
}