Controlling Netflix (HTML5) playback with Tampermonkey/javascript

While watching a netflix video on a Netflix site, my goal is to have a userscript invoke the playback controls programmatically. Specifically, the volume, level, play/pause state, and time position of the video.

I've been able to manipulate the html5 video element itself, but controlling that directly does not provide the needed netflix control bar feedback to the user. (i.e. video is paused, but the control bar still shows it as playing).

My approach thus far has been to try and locate the elements that represent the "buttons" you click when using the normal controls, and trigger their click events through the userscript. But I can't seem to isolate the proper elements. Additionally netflix is using a javascript compressor/obfuscator which increases the difficulty of finding the proper elements that represent the buttons on the control bar.

On a site like this, how can one identify the element that is receiving the click event of an element and then create a userscript to invoke it through tampermonkey and/or greasemonkey?

In the sample code below, I've added a button on the view for testing purposes.

// ==UserScript==
// @name       Jump a minute ahead in netflix
// @version    0.1
// @description  A Test by trying to jump a minute or so ahead into a netflix movie
// @match        *://www.netflix.com/*
// @grant       GM_addStyle
// @require http://code.jquery.com/jquery-latest.js
// ==/UserScript==

var zNode       = document.createElement ('div');
zNode.innerHTML = '<button id="myButton" type="button">Try It</button>';
zNode.setAttribute ('id', 'myContainer');
document.body.appendChild (zNode);

//--- Activate the newly added button.
document.getElementById ("myButton").addEventListener (
    "click", ButtonClickAction, false
);

function ButtonClickAction (zEvent) {
    /*--- For our dummy action, we'll just add a line of text to the top of the screen.*/
    var zNode       = document.createElement ('p');
    var video = $("video:first-of-type");
    var playerSlider = document.getElementsByClassName("player-slider")[0];

    console.log(netflix);
    console.log(playerSlider);
    console.log(netflix.player);
    console.log(netflix.cadmium);
    console.log(netflix.cadmium.ui);
    console.log(netflix.cadmium.ui.playback);

    //video.get(0).pause();
    //video.get(0).currentTime = 2000.0;
    console.log(video.get(0).currentTime);
    console.log(netflix.cadmium.ui.volume);
    zNode.innerHTML = 'The button was clicked.';
    document.getElementById ("myContainer").appendChild (zNode);
}

//--- Style our newly added elements using CSS.
GM_addStyle ( multilineStr ( function () {/*!
    #myContainer {
        position:               absolute;
        top:                    0;
        left:                   0;
        font-size:              20px;
        background:             orange;
        border:                 3px outset black;
        margin:                 5px;
        opacity:                0.9;
        z-index:                222;
        padding:                5px 20px;
    }
    #myButton {
        cursor:                 pointer;
    }
    #myContainer p {
        color:                  red;
        background:             white;
    }
*/} ) );

function multilineStr (dummyFunc) {
    var str = dummyFunc.toString ();
    str     = str.replace (/^[^\/]+\/\*!?/, '') // Strip function () { /*!
    .replace (/\s*\*\/\s*\}\s*$/, '')   // Strip */ }
    .replace (/\/\/.+$/gm, '') // Double-slash comments wreck CSS. Strip them.
    ;
    return str;
}

The console.log statements are showing some of the things I've found so far. But I haven't figured out how to invoke functions off them, or which of them might have what I'm looking for (I think largely due to the javascript compressor making it difficult for me to follow the code).

Answers:

Answer

I think this was a case of my thinking too hard about the problem. Once I stepped back, I realized that I had been looking in the right direction, but wasn't triggering the click event properly.

So, for example, to get the "button" that controls play and pause, you could use: document.getElementsByClassName("player-control-button player-play-pause")[0]. Then to click it programmatically in tampermonkey, you simply invoke the click event using:

document.getElementsByClassName("player-control-button player-play-pause")[0].click();

Volume and other controls in the bar are similar. The playback position is looking to be a bit trickier, but I'll do some more digging and add a comment to this answer once I figure it out.

Answer

Well, the good news is that you can somewhat control playback by detecting the HTML5 VIDEO tag

ex's:

https://gist.github.com/rdp/93c761b3524529e591e5286073545362 find_html5_video.js

https://github.com/igrigorik/videospeed

then calling methods on that as any normal HTMLMediaElement object (mute, pause, etc.) works fine.

 video_element.paused = true // pause playback
 video_element.currentTime // get current timestamp

If you use this trick for most sites (amazon instant video, youtube), you can seek with video_element.currentTime = 3 and it just works.

However if you seek like with netflix, you get "Whoops, something went wrong... Unexpected Error There was an unexpected error. Please reload the page and try again. Error Code: M7375"

And I haven't figured out a way around that yet (though if your seek is just "fast forward" a bit, you could decrease the video size, set the playback rate to super high, then bring it back down to normal when it reaches the desired location I suppose).

So we need to find a different way to send the seek command. Apparently at one point there was a netflix.cadmium.objects.videoPlayer() or netflix.player javascript objects available with a seek method, however they appear to be absent now.

So back to your original question, it does appear possible to mimic the "click" on the slider that controls location, thus sending a seek message, as you were attempting.

Netflix Party (chrome extension, and also chrome extension `http://showgoers.tv/) does something either like it, explained here.

A poignant part seems to be

var showControls = function() {
  uiEventsHappening += 1;
  var scrubber = $('#scrubber-component');
  var eventOptions = {
    'bubbles': true,
    'button': 0,
    'currentTarget': scrubber[0]
  };
  scrubber[0].dispatchEvent(new MouseEvent('mousemove', eventOptions));
  return delay(10)().then(function() {
    uiEventsHappening -= 1;
  });
};

var seek = function(milliseconds) {
  uiEventsHappening += 1;
  var eventOptions, scrubber;
  return showControls().then(function() {
    // compute the parameters for the mouse events
    scrubber = $('#scrubber-component');
    var factor = milliseconds / getDuration();
    var mouseX = scrubber.offset().left + Math.round(scrubber.width() * factor); // relative to the document
    var mouseY = scrubber.offset().top + scrubber.height() / 2;                  // relative to the document
    eventOptions = {
      'bubbles': true,
      'button': 0,
      'screenX': mouseX - $(window).scrollLeft(),
      'screenY': mouseY - $(window).scrollTop(),
      'clientX': mouseX - $(window).scrollLeft(),
      'clientY': mouseY - $(window).scrollTop(),
      'offsetX': mouseX - scrubber.offset().left,
      'offsetY': mouseY - scrubber.offset().top,
      'pageX': mouseX,
      'pageY': mouseY,
      'currentTarget': scrubber[0]
    };

    // make the "trickplay preview" show up
    scrubber[0].dispatchEvent(new MouseEvent('mouseover', eventOptions));
  }).then(delay(10)).then(function() {
    // simulate a click on the scrubber
    scrubber[0].dispatchEvent(new MouseEvent('mousedown', eventOptions));
    scrubber[0].dispatchEvent(new MouseEvent('mouseup', eventOptions));
    scrubber[0].dispatchEvent(new MouseEvent('mouseout', eventOptions));
  }).then(delay(1)).then(hideControls).then(function() {
    uiEventsHappening -= 1;
  });
};

Tags

Recent Questions

Top Questions

Home Tags Terms of Service Privacy Policy DMCA Contact Us

©2020 All rights reserved.