Bug 1048688 - videos without an extension in the source URL aren't castable r=wesj

This commit is contained in:
Mark Finkle 2014-08-08 12:47:55 -04:00
Родитель f977c02132
Коммит 23050006c4
2 изменённых файлов: 164 добавлений и 70 удалений

Просмотреть файл

@ -83,18 +83,24 @@ function execute_video_test(test) {
let element = browser.contentDocument.getElementById(test.id); let element = browser.contentDocument.getElementById(test.id);
if (element) { if (element) {
let [x, y] = middle(element); let [x, y] = middle(element);
let video = chromeWin.CastingApps.getVideo(element, x, y); do_test_pending();
if (video) { do_print("Starting to getVideo");
let matchPoster = (test.poster == video.poster); chromeWin.CastingApps.getVideo(element, x, y, (video) => {
let matchSource = (test.source == video.source); do_print("got a Video");
ok(matchPoster && matchSource && test.pass, test.text); if (video) {
} else { let matchPoster = (test.poster == video.poster);
ok(!test.pass, test.text); let matchSource = (test.source == video.source);
} ok(matchPoster && matchSource && test.pass, test.text);
} else {
ok(!test.pass, test.text);
}
do_test_finished();
run_next_test();
});
} else { } else {
ok(false, "test element not found: [" + test.id + "]"); ok(false, "test element not found: [" + test.id + "]");
run_next_test();
} }
run_next_test();
} }
let videoTest; let videoTest;

Просмотреть файл

@ -246,12 +246,12 @@ var CastingApps = {
return; return;
} }
if (!this.getVideo(video, 0, 0)) { this.getVideo(video, 0, 0, (aBundle) => {
return; // Let the binding know casting is allowed
} if (aBundle) {
this._sendEventToVideo(aBundle.element, { allow: true });
// Let the binding know casting is allowed }
this._sendEventToVideo(video, { allow: true }); });
}, },
handleVideoBindingCast: function handleVideoBindingCast(aTab, aEvent) { handleVideoBindingCast: function handleVideoBindingCast(aTab, aEvent) {
@ -274,43 +274,77 @@ var CastingApps = {
return Services.io.newURI(aURL, aOriginCharset, aBaseURI); return Services.io.newURI(aURL, aOriginCharset, aBaseURI);
}, },
getVideo: function(aElement, aX, aY) { allowableExtension: function(aURI, aExtensions) {
return (aURI instanceof Ci.nsIURL) && aExtensions.indexOf(aURI.fileExtension) != -1;
},
allowableMimeType: function(aType, aTypes) {
return aTypes.indexOf(aType) != -1;
},
// This method will look at the aElement (or try to find a video at aX, aY) that has
// a castable source. If found, aCallback will be called with a JSON meta bundle. If
// no castable source was found, aCallback is called with null.
getVideo: function(aElement, aX, aY, aCallback) {
let extensions = SimpleServiceDiscovery.getSupportedExtensions(); let extensions = SimpleServiceDiscovery.getSupportedExtensions();
let types = SimpleServiceDiscovery.getSupportedMimeTypes(); let types = SimpleServiceDiscovery.getSupportedMimeTypes();
// Fast path: Is the given element a video element // Fast path: Is the given element a video element?
let video = this._getVideo(aElement, types, extensions); if (aElement instanceof HTMLVideoElement) {
if (video) { // If we found a video element, no need to look further, even if no
return video; // castable video source is found.
this._getVideo(aElement, types, extensions, aCallback);
return;
} }
// Maybe this is an overlay, with the video element under it.
// Use the (x, y) location to guess at a <video> element.
// The context menu system will keep walking up the DOM giving us a chance // The context menu system will keep walking up the DOM giving us a chance
// to find an element we match. When it hits <html> things can go BOOM. // to find an element we match. When it hits <html> things can go BOOM.
try { try {
// Maybe this is an overlay, with the video element under it
// Use the (x, y) location to guess at a <video> element
let elements = aElement.ownerDocument.querySelectorAll("video"); let elements = aElement.ownerDocument.querySelectorAll("video");
for (let element of elements) { for (let element of elements) {
// Look for a video element contained in the overlay bounds // Look for a video element contained in the overlay bounds
let rect = element.getBoundingClientRect(); let rect = element.getBoundingClientRect();
if (aY >= rect.top && aX >= rect.left && aY <= rect.bottom && aX <= rect.right) { if (aY >= rect.top && aX >= rect.left && aY <= rect.bottom && aX <= rect.right) {
video = this._getVideo(element, types, extensions); // Once we find a <video> under the overlay, we check it and exit.
if (video) { this._getVideo(element, types, extensions, aCallback);
break; return;
}
} }
} }
} catch(e) {} } catch(e) {}
// Could be null
return video;
}, },
_getVideo: function(aElement, aTypes, aExtensions) { _getContentTypeForURI: function(aURI, aCallback) {
if (!(aElement instanceof HTMLVideoElement)) { let channel = Services.io.newChannelFromURI(aURI);
return null; let listener = {
} onStartRequest: function(request, context) {
switch (channel.responseStatus) {
case 301:
case 302:
case 303:
request.cancel(0);
let location = channel.getResponseHeader("Location");
CastingApps._getContentTypeForURI(CastingApps.makeURI(location), aCallback);
break;
default:
aCallback(channel.contentType);
request.cancel(0);
break;
}
},
onStopRequest: function(request, context, statusCode) {},
onDataAvailable: function(request, context, stream, offset, count) {}
};
channel.asyncOpen(listener, null)
},
// Because this method uses a callback, make sure we return ASAP if we know
// we have a castable video source.
_getVideo: function(aElement, aTypes, aExtensions, aCallback) {
// Keep a list of URIs we need for an async mimetype check
let asyncURIs = [];
// Grab the poster attribute from the <video> // Grab the poster attribute from the <video>
let posterURL = aElement.poster; let posterURL = aElement.poster;
@ -327,8 +361,20 @@ var CastingApps = {
// Use the file extension to guess the mime type // Use the file extension to guess the mime type
let sourceURI = this.makeURI(sourceURL, null, this.makeURI(aElement.baseURI)); let sourceURI = this.makeURI(sourceURL, null, this.makeURI(aElement.baseURI));
if (this.allowableExtension(sourceURI, aExtensions)) { if (this.allowableExtension(sourceURI, aExtensions)) {
return { element: aElement, source: sourceURI.spec, poster: posterURL, sourceURI: sourceURI}; aCallback({ element: aElement, source: sourceURI.spec, poster: posterURL, sourceURI: sourceURI});
return;
} }
if (aElement.type) {
// Fast sync check
if (this.allowableMimeType(aElement.type, aTypes)) {
aCallback({ element: aElement, source: sourceURI.spec, poster: posterURL, sourceURI: sourceURI, type: aElement.type });
return;
}
}
// Delay the async check until we sync scan all possible URIs
asyncURIs.push(sourceURI);
} }
// Next, look to see if there is a <source> child element that meets // Next, look to see if there is a <source> child element that meets
@ -339,23 +385,77 @@ var CastingApps = {
// Using the type attribute is our ideal way to guess the mime type. Otherwise, // Using the type attribute is our ideal way to guess the mime type. Otherwise,
// fallback to using the file extension to guess the mime type // fallback to using the file extension to guess the mime type
if (this.allowableMimeType(sourceNode.type, aTypes) || this.allowableExtension(sourceURI, aExtensions)) { if (this.allowableExtension(sourceURI, aExtensions)) {
return { element: aElement, source: sourceURI.spec, poster: posterURL, sourceURI: sourceURI, type: sourceNode.type }; aCallback({ element: aElement, source: sourceURI.spec, poster: posterURL, sourceURI: sourceURI, type: sourceNode.type });
return;
} }
if (sourceNode.type) {
// Fast sync check
if (this.allowableMimeType(sourceNode.type, aTypes)) {
aCallback({ element: aElement, source: sourceURI.spec, poster: posterURL, sourceURI: sourceURI, type: sourceNode.type });
return;
}
}
// Delay the async check until we sync scan all possible URIs
asyncURIs.push(sourceURI);
} }
return null; // If we didn't find a good URI directly, let's look using async methods
// As soon as we find a good sourceURL, avoid firing the callback any further
aCallback.fired = false;
for (let sourceURI of asyncURIs) {
// Do an async fetch to figure out the mimetype of the source video
this._getContentTypeForURI(sourceURI, (aType) => {
if (!aCallback.fired && this.allowableMimeType(aType, aTypes)) {
aCallback.fired = true;
aCallback({ element: aElement, source: sourceURI.spec, poster: posterURL, sourceURI: sourceURI, type: aType });
}
});
}
// If we didn't find any castable source, let's send back a signal
if (!aCallback.fired) {
aCallback(null);
}
},
// This code depends on handleVideoBindingAttached setting mozAllowCasting
// so we can quickly figure out if the video is castable
isVideoCastable: function(aElement, aX, aY) {
// Use the flag set when the <video> binding was created as the check
if (aElement instanceof HTMLVideoElement) {
return aElement.mozAllowCasting;
}
// This is called by the context menu system and the system will keep
// walking up the DOM giving us a chance to find an element we match.
// When it hits <html> things can go BOOM.
try {
// Maybe this is an overlay, with the video element under it
// Use the (x, y) location to guess at a <video> element
let elements = aElement.ownerDocument.querySelectorAll("video");
for (let element of elements) {
// Look for a video element contained in the overlay bounds
let rect = element.getBoundingClientRect();
if (aY >= rect.top && aX >= rect.left && aY <= rect.bottom && aX <= rect.right) {
// Use the flag set when the <video> binding was created as the check
return element.mozAllowCasting;
}
}
} catch(e) {}
return false;
}, },
filterCast: { filterCast: {
matches: function(aElement, aX, aY) { matches: function(aElement, aX, aY) {
// This behavior matches the pageaction: As long as a video is castable,
// we can cast it, even if it's already being cast to a device.
if (SimpleServiceDiscovery.services.length == 0) if (SimpleServiceDiscovery.services.length == 0)
return false; return false;
let video = CastingApps.getVideo(aElement, aX, aY); return CastingApps.isVideoCastable(aElement, aX, aY);
if (CastingApps.session) {
return (video && CastingApps.session.data.source != video.source);
}
return (video != null);
} }
}, },
@ -495,13 +595,16 @@ var CastingApps = {
openExternal: function(aElement, aX, aY) { openExternal: function(aElement, aX, aY) {
// Start a second screen media service // Start a second screen media service
let video = this.getVideo(aElement, aX, aY); this.getVideo(aElement, aX, aY, this._openExternal.bind(this));
if (!video) { },
_openExternal: function(aVideo) {
if (!aVideo) {
return; return;
} }
function filterFunc(service) { function filterFunc(aService) {
return this.allowableExtension(video.sourceURI, service.extensions) || this.allowableMimeType(video.type, service.types); return this.allowableExtension(aVideo.sourceURI, aService.extensions) || this.allowableMimeType(aVideo.type, aService.types);
} }
this.prompt(function(aService) { this.prompt(function(aService) {
@ -513,11 +616,12 @@ var CastingApps = {
if (!app) if (!app)
return; return;
video.title = aElement.ownerDocument.defaultView.top.document.title; if (aVideo.element) {
if (video.element) { aVideo.title = aVideo.element.ownerDocument.defaultView.top.document.title;
// If the video is currently playing on the device, pause it // If the video is currently playing on the device, pause it
if (!video.element.paused) { if (!aVideo.element.paused) {
video.element.pause(); aVideo.element.pause();
} }
} }
@ -539,11 +643,11 @@ var CastingApps = {
app: app, app: app,
remoteMedia: aRemoteMedia, remoteMedia: aRemoteMedia,
data: { data: {
title: video.title, title: aVideo.title,
source: video.source, source: aVideo.source,
poster: video.poster poster: aVideo.poster
}, },
videoRef: Cu.getWeakReference(video.element) videoRef: Cu.getWeakReference(aVideo.element)
}; };
}.bind(this), this); }.bind(this), this);
}.bind(this)); }.bind(this));
@ -601,21 +705,5 @@ var CastingApps = {
if (status == "completed") { if (status == "completed") {
this.closeExternal(); this.closeExternal();
} }
},
allowableExtension: function(aURI, aExtensions) {
if (aURI && aURI instanceof Ci.nsIURL) {
for (let x in aExtensions) {
if (aURI.fileExtension == aExtensions[x]) return true;
}
}
return false;
},
allowableMimeType: function(aType, aTypes) {
for (let x in aTypes) {
if (aType == aTypes[x]) return true;
}
return false;
} }
}; };