Bug 821514 - fix YoutubeProtocolHandler.js to report errors, and also parse titles properly. r=dale a=blocking-basecamp+
authorDavid Flanagan <dflanagan@mozilla.com>
Sat, 29 Dec 2012 20:51:43 -0800
changeset 126336 eb2f5c66561bb24eb8f16e0396440f4e98562120
parent 126335 c6464865fe9fd6f6047f0b498f87fcee09a0fe30
child 126345 00ce7212c7ad9cc0f11386fd6c0343a6b17eeae9
child 127813 d6ca11be410942856d09ab0af1efa1ee4bbfbfef
push id2151
push userlsblakk@mozilla.com
push dateTue, 19 Feb 2013 18:06:57 +0000
treeherdermozilla-beta@4952e88741ec [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersdale, blocking-basecamp
bugs821514
milestone20.0a1
first release with
nightly linux32
eb2f5c66561b / 20.0a1 / 20121230030830 / files
nightly linux64
eb2f5c66561b / 20.0a1 / 20121230030830 / files
nightly mac
eb2f5c66561b / 20.0a1 / 20121230030830 / files
nightly win32
eb2f5c66561b / 20.0a1 / 20121230030830 / files
nightly win64
eb2f5c66561b / 20.0a1 / 20121230030830 / files
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
releases
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 821514 - fix YoutubeProtocolHandler.js to report errors, and also parse titles properly. r=dale a=blocking-basecamp+
b2g/components/YoutubeProtocolHandler.js
--- a/b2g/components/YoutubeProtocolHandler.js
+++ b/b2g/components/YoutubeProtocolHandler.js
@@ -11,27 +11,16 @@ const Cu = Components.utils;
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 
 XPCOMUtils.defineLazyGetter(this, "cpmm", function() {
   return Cc["@mozilla.org/childprocessmessagemanager;1"]
            .getService(Ci.nsIMessageSender);
 });
 
-// Splits parameters in a query string.
-function extractParameters(aQuery) {
-  let params = aQuery.split("&");
-  let res = {};
-  params.forEach(function(aParam) {
-    let obj = aParam.split("=");
-    res[obj[0]] = decodeURIComponent(obj[1]);
-  });
-  return res;
-}
-
 function YoutubeProtocolHandler() {
 }
 
 YoutubeProtocolHandler.prototype = {
   classID: Components.ID("{c3f1b945-7e71-49c8-95c7-5ae9cc9e2bad}"),
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIProtocolHandler]),
 
   scheme: "vnd.youtube",
@@ -56,75 +45,146 @@ YoutubeProtocolHandler.prototype = {
     // Get a list of streams for this video id.
     let infoURI = "http://www.youtube.com/get_video_info?&video_id=" +
                   aURI.path.substring(1);
 
     let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
                 .createInstance(Ci.nsIXMLHttpRequest);
     xhr.open("GET", infoURI, true);
     xhr.addEventListener("load", function() {
-      // Youtube sends the response as a double wrapped url answer:
-      // we first extract the url_encoded_fmt_stream_map parameter,
-      // and from each comma-separated entry in this value, we extract
-      // other parameters (url and type).
-      let key = "url_encoded_fmt_stream_map=";
-      let pos = xhr.responseText.indexOf(key);
-      if (pos == -1) {
-        return;
+      try {
+        let info = parseYoutubeVideoInfo(xhr.responseText);
+        cpmm.sendAsyncMessage("content-handler", info);
+      }
+      catch(e) {
+        // If parseYoutubeVideoInfo() can't find a video URL, it
+        // throws an Error. We report the error message here and do
+        // nothing. This shouldn't happen often. But if it does, the user
+        // will find that clicking on a video doesn't do anything.
+        log(e.message);
+      }
+    });
+    xhr.send(null);
+
+    function log(msg) {
+      msg = "YoutubeProtocolHandler.js: " + (msg.join ? msg.join(" ") : msg);
+      Cc["@mozilla.org/consoleservice;1"]
+        .getService(Ci.nsIConsoleService)
+        .logStringMessage(msg);
+    }
+
+    // 
+    // Parse the response from a youtube get_video_info query.
+    // 
+    // If youtube's response is a failure, this function returns an object
+    // with status, errorcode, type and reason properties. Otherwise, it returns
+    // an object with status, url, and type properties, and optional
+    // title, poster, and duration properties.
+    // 
+    function parseYoutubeVideoInfo(response) {
+      // Splits parameters in a query string.
+      function extractParameters(q) {
+        let params = q.split("&");
+        let result = {};
+        for(let i = 0, n = params.length; i < n; i++) {
+          let param = params[i];
+          let pos = param.indexOf('=');
+          if (pos === -1) 
+            continue;
+          let name = param.substring(0, pos);
+          let value = param.substring(pos+1);
+          result[name] = decodeURIComponent(value);
+        }
+        return result;
       }
-      let streams = decodeURIComponent(xhr.responseText
-                                       .substring(pos + key.length)).split(",");
-      let uri;
-      let mimeType;
 
-      // itag is an undocumented value which maps to resolution and mimetype
-      // see https://en.wikipedia.org/wiki/YouTube#Quality_and_codecs
-      // Ordered from least to most preferred
-      let recognizedItags = [
+      let params = extractParameters(response);
+      
+      // If the request failed, return an object with an error code
+      // and an error message
+      if (params.status === 'fail') {
+        // 
+        // Hopefully this error message will be properly localized.
+        // Do we need to add any parameters to the XMLHttpRequest to 
+        // specify the language we want?
+        // 
+        // Note that we include fake type and url properties in the returned
+        // object. This is because we still need to trigger the video app's
+        // view activity handler to display the error message from youtube,
+        // and those parameters are required.
+        //
+        return {
+          status: params.status,
+          errorcode: params.errorcode,
+          reason:  (params.reason || '').replace(/\+/g, ' '),
+          type: 'video/3gpp',
+          url: 'https://m.youtube.com'
+        }
+      }
+
+      // Otherwise, the query was successful
+      let result = {
+        status: params.status,
+      };
+
+      // Now parse the available streams
+      let streamsText = params.url_encoded_fmt_stream_map;
+      if (!streamsText)
+        throw Error("No url_encoded_fmt_stream_map parameter");
+      let streams = streamsText.split(',');
+      for(let i = 0, n = streams.length; i < n; i++) {
+        streams[i] = extractParameters(streams[i]);
+      }
+
+      // This is the list of youtube video formats, ordered from worst
+      // (but playable) to best.  These numbers are values used as the
+      // itag parameter of each stream description. See
+      // https://en.wikipedia.org/wiki/YouTube#Quality_and_codecs
+      let formats = [
         "17", // 144p 3GP
         "36", // 240p 3GP
         "43", // 360p WebM
 #ifdef MOZ_WIDGET_GONK
         "18", // 360p H.264
 #endif
       ];
 
-      let bestItag = -1;
-
-      let extras = { }
-
-      streams.forEach(function(aStream) {
-        let params = extractParameters(aStream);
-        let url = params["url"];
-        let type = params["type"] ? params["type"].split(";")[0] : null;
-        let itag = params["itag"];
-
-        let index;
-        if (url && type && ((index = recognizedItags.indexOf(itag)) != -1) &&
-            index > bestItag) {
-          uri = url + '&signature=' + (params["sig"] ? params['sig'] : '');
-          mimeType = type;
-          bestItag = index;
-        }
-        for (let param in params) {
-          if (["thumbnail_url", "length_seconds", "title"].indexOf(param) != -1) {
-            extras[param] = decodeURIComponent(params[param]);
-          }
-        }
+      // Sort the array of stream descriptions in order of format
+      // preference, so that the first item is the most preferred one
+      streams.sort(function(a, b) {
+        let x = a.itag ? formats.indexOf(a.itag) : -1;
+        let y = b.itag ? formats.indexOf(b.itag) : -1;
+        return y - x;
       });
 
-      if (uri && mimeType) {
-        cpmm.sendAsyncMessage("content-handler", {
-          url: uri,
-          type: mimeType,
-          extras: extras
-        });
+      let bestStream = streams[0];
+
+      // If the best stream is a format we don't support just return
+      if (formats.indexOf(bestStream.itag) === -1) 
+        throw Error("No supported video formats");
+
+      result.url = bestStream.url + '&signature=' + (bestStream.sig || '');
+      result.type = bestStream.type;
+      // Strip codec information off of the mime type
+      if (result.type && result.type.indexOf(';') !== -1) {
+        result.type = result.type.split(';',1)[0];
       }
-    });
-    xhr.send(null);
+
+      if (params.title) {
+        result.title = params.title.replace(/\+/g, ' ');
+      }
+      if (params.length_seconds) {
+        result.duration = params.length_seconds;
+      }
+      if (params.thumbnail_url) {
+        result.poster = params.thumbnail_url;
+      }
+
+      return result;
+    }
 
     throw Components.results.NS_ERROR_ILLEGAL_VALUE;
   },
 
   allowPort: function yt_phAllowPort(aPort, aScheme) {
     return false;
   }
 };