Discussions » Development

Embedding (flash) video players via UserScript (GM 2.2 / FF32+)

§
Posted: 14-09-2014

Embedding (flash) video players via UserScript (GM 2.2 / FF32+)

Since discovering that the folks at dailymotion have (intentionally or not) un-deprecated access to their older (lighter, and arguably more capable) flash-based player API (which was pretty much a clone of YouTube's) -- I eagerly started trying to port my Dailymotion: "Playback Quality Control" UserScript to the old API.

And there I ran into trouble straight away... I'm not sure if embedding the NEW player API via GM *ever* worked security-wise because I had been inadvertently testing on Scriptish only. In any case, the upshot is that most of the JavaScript machinery used by the OLD player API runs afoul of the FF32+ security changes.

Basically, when using the OLD Flash API, you use SWFobject to inject

http://www.dailymotion.com/swf

into the page with a URI parameter (playerId) that tells Flash which DOM element will be *replaced* when the object is instantiated (with the same "id"). It then calls a presumed window-global function

onDailymotionPlayerReady(playerId)

to inform the host page that the Flash object is ready to take API calls (via JavaScript).

Ordinarily, this global (name-based) callback is just inconvenient for encapsulation, but not too difficult to deal with.

Via GM 2.2 / FF32+ however, it seems there are a few more hurdles. If you load SWFobject via UserScript require

// @require     http://code.jquery.com/jquery-1.9.1.min.js
// @require     http://ajax.googleapis.com/ajax/libs/swfobject/2.2/swfobject.js 

and embed the Flash player like

var playerId = "ID OF YOUR DOM ELEMENT TO BE REPLACED BY FLASH OBJECT";
var params = { allowScriptAccess: "always", allowfullscreen: "true" };
var attrs = { id: playerId };
swfobject.embedSWF(
    "http://www.dailymotion.com/swf?enableApi=1&playerapiid="+playerId, 
    playerId, "640", "480", "9", null, null, params, attrs);

then you immediately run into sandbox security errors if 'window.onDailymotionPlayerReady' is defined from the UserScript and closes over UserScript-defined variables/functions. (Sometimes the errors are extremely terse NSIComponent "failure" chrome-level errors that are very hard to debug unless you recognise them from FF addon development).

The obvious (to me) solution is to have the UserScript inject a SCRIPT tag into the body of the "unsafe" window like so:

< script type="text/javascript">
function mk_DM_eventDispatcher(playerId, eventType) {
  return function() {
    var packet = { playerId: playerId, eventType: eventType };
    packet.params = Array.prototype.slice.call(arguments, 0);
    var evt = new CustomEvent("dma.playerEvent", { detail: JSON.stringify(packet) });
    window.dispatchEvent(evt);
  };
}

function onDailymotionPlayerReady(playerId) {                                                                
  var player = document.getElementById(playerId);
  window[playerId+"_onStateChange"] = mk_DM_eventDispatcher(playerId, "onStateChange");
  window[playerId+"_onPlaybackQualityChange"] = mk_DM_eventDispatcher(playerId, "onPlaybackQualityChange");
  window[playerId+"_onError"] = mk_DM_eventDispatcher(playerId, "onError");
  player.addEventListener("onStateChange", playerId+"_onStateChange");
  player.addEventListener("onPlaybackQualityChange", playerId+"_onPlaybackQualityChange");
  player.addEventListener("onError", playerId+"_onError");
  var evt = new CustomEvent("dma.ready", {detail: { playerId: playerId }});
  window.dispatchEvent();
}; 
< /script>

The CustomEvent dispatch is pretty standard stuff, except that the "detail" is JSON stringified to avoid sandbox security errors (in t his case because it closes over "playerId" defined in the "unsafe" window scope).

You then need to use the four-argument form of EventTarget.addEventListener to "listen" for those events on the UserScript side:

window.addEventListener('dma.playerEvent', function(e) {
  var detail    = JSON.parse(e.detail);
  var playerId  = detail.playerId;
  var eventType = detail.eventType;
  var params    = detail.params;

  // do something with the incoming event
  // (e.g: re-dispatch it to a UserScript-side event handler)
}, true, true);

window.addEventListener('dma.ready', function(e) {
  var playerId = e.detail.playerId;

  // do something with the player Flash object's JS API (e.g: load a video!)
}, true, true);

The fourth 'true' (boolean) argument says "yes, please listen to events from unsafe contexts", and the event handlers also need to JSON.parse the event.detail field.

Nothing earth shattering there, but what about *calling* the Flash object's JS API functions from the UserScript side? Yep, same sandbox security errors. :-(

So.... we do the same in reverse. The following is added to the SCRIPT tag injected into the "unsafe" window's DOM:

window.addEventListener("dma.command", function(e) {
  var detail = JSON.parse(e.detail);
  var obj = document.getElementById(detail.playerId);
  var method = obj[detail.command];
  var result = method.apply(obj, detail.params);
  if (detail.ticket) {
    window.dispatchEvent(new CustomEvent("dma.commandReturn", {
      detail: JSON.stringify({ ticket: detail.ticket, result: result })
    }));
  }
});

... where "callers" expecting a response also pass a "ticket" via event.detail; a single-use unique value per call that allow's the dma.commandReturn event to be matched to the caller's callback function -- which has been registered in advance on the UserScript side:

var __symbol = 0;
var genSym = function() { __symbol++; return '__symbol_'+__symbol; };

var dma_command_tickets = {};
var dma_event_handlers = {};

var dma_build_add_event_listener = function(playerId) {
  if (!dma_event_handlers.hasOwnProperty(playerId)) dma_event_handlers[playerId] = {};
  return function(eventType, cb) { dma_event_handlers[playerId][eventType] = cb;  };
};

var dma_build_proxy_call = function(playerId, method, hasResult) {
  var packetProto = { playerId: playerId, command: method };
  if (hasResult) {
    return function() {
      var packet = JSON.parse(JSON.stringify(packetProto));
      var params = Array.prototype.slice.call(arguments, 0);
      var callback = params.pop();
      packet.params = params;
      packet.ticket = genSym();
      dma_command_tickets[packet.ticket] = callback;
      var evt = new CustomEvent('dma.command', { detail: JSON.stringify(packet) });
      window.dispatchEvent(evt);
    }
  } else {
    return function() {
      var packet = JSON.parse(JSON.stringify(packetProto));
      packet.params = Array.prototype.slice.call(arguments, 0);
      var evt = new CustomEvent('dma.command', { detail: JSON.stringify(packet) });
      window.dispatchEvent(evt);
    };
  }
};

window.addEventListener('dma.commandReturn', function(e) {
  var detail = JSON.parse(e.detail);
  var ticket = detail.ticket;
  var result = detail.result;
  var cb = dma_command_tickets[ticket];
  delete dma_command_tickets[ticket];
  return cb(result);
}, true, true);

player.addEventListener   = dma_build_add_event_listener(player.playerId);
player.loadVideo          = dma_build_proxy_call(player.playerId, 'loadVideo', 0);
player.getDuration        = dma_build_proxy_call(player.playerId, 'getDuration', 1);

player.addEventListener('onStateChange', function(playerState) {
  console.log("onStateChange: "+playerState);	
});

player.addEventListener('onPlaybackQualityChange', function(quality) {
  console.log("onStateChange: "+quality);	
});

// example usage:
player.loadVideo(videoId);
player.getDuration(function(seconds){
  console.log("the video is "+seconds+" seconds long.");
});

(continuing in next post)

§
Posted: 14-09-2014

(continued...)

That *works*, but I think you'll agree, the security sandbox doesn't make it easy.

Have I missed another solution in the MUCH EASIER category?

(Yes, I'm aware that I could use window.postMesage() instead of window.dispatchEvent(), but it's not structurally *simpler* as far as I can tell -- ie: that method seems to have the same sandbox security issues).

§
Posted: 14-09-2014

Have you tried unsafeWindow? The usage changed in GM 2.0

https://blog.mozilla.org/addons/2014/04/10/changes-to-unsafewindow-for-the-add-on-sdk/

§
Posted: 14-09-2014
Edited: 14-09-2014

@Couchy said:

Have you tried unsafeWindow? The usage changed in GM 2.0

I *had* been using unsafeWindow prior to Firefox 32.0 on Scriptish, but had never gotten it to work on GM 2.0+. I was using the bi-directional form that has since been deprecated in Firefox 32.0. Scriptish is now broken as hell.

If I understand correctly, the remaining unsafeWindow functionality let addon content reach into the page arbitrarily, but bans all but primitive property resolution from the page into addon content.

The newer way for addon content to give page-content access to privileged code is to use 'cloneInto', 'exportFunction' and 'Components.utils.createObjectIn'. They talk about 'structural cloning' of data/functions... not sure how that's going to work with event binding.

That means I can probably cut down the SCRIPT tag injection stuff a bit, but I'm still unclear on how the Flash-JavaScript bridge (used by SFW players) is supposed to take advantage of this.

I don't think I can avoid the four-argument form of addEventListener and the serialisation required for that.

Can you clarify how you think it could work?

§
Posted: 14-09-2014
Edited: 14-09-2014

Ok, I've done some testing.

Neither of 'cloneInto' and 'Components.utils.createObjectIn' are helpful because the "structural cloning" is essentially just serialisation so you can't close over privileged code/variables from object defined using those APIs.

'exportFunction' is much more interesting (and could have saved me a *lot* of time in some of my previous addon work)... but it has severe limitations. UserScript functions "exported" to the page:
* don't survive async transitions (setTimeout, setImmediate, requestAnimationFrom, etc).
* don't work at all for dispatching events (still need to serialise/unserialise)
* are NOT naiively callable by the Flash-JavaScript bridge (something fails in property name resolution).

Looks like the GreaseMonkey developers will have to do a *lot* of work to restore this kind of functionality.

§
Posted: 21-09-2014

Doing a bunch more research, it seems like the way I'm doing it is actually sane. (i.e: there are well recognised limits on what can be structurally cloned).

The advice being given by very smart people is to inject your whole script into the page (running unprivilileged) and only export a small set of privileged functions (that can call GM_*) into the page to be used by your script. Until mozilla (and then GM) provide an easier way, the most general mechanism (that works for everything) is dispatchEvent and/or postMessage. For the "privileged" exports, that requires serialisation (JSON) of the messages between contexts.

I found a much more organised/generalised library for this approach using requirejs:

https://github.com/YePpHa/UserProxy

Post reply

Đăng nhập để bình luận