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);
if (element) {
let [x, y] = middle(element);
let video = chromeWin.CastingApps.getVideo(element, x, y);
if (video) {
let matchPoster = (test.poster == video.poster);
let matchSource = (test.source == video.source);
ok(matchPoster && matchSource && test.pass, test.text);
} else {
ok(!test.pass, test.text);
}
do_test_pending();
do_print("Starting to getVideo");
chromeWin.CastingApps.getVideo(element, x, y, (video) => {
do_print("got a Video");
if (video) {
let matchPoster = (test.poster == video.poster);
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 {
ok(false, "test element not found: [" + test.id + "]");
run_next_test();
}
run_next_test();
}
let videoTest;

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

@ -246,12 +246,12 @@ var CastingApps = {
return;
}
if (!this.getVideo(video, 0, 0)) {
return;
}
// Let the binding know casting is allowed
this._sendEventToVideo(video, { allow: true });
this.getVideo(video, 0, 0, (aBundle) => {
// Let the binding know casting is allowed
if (aBundle) {
this._sendEventToVideo(aBundle.element, { allow: true });
}
});
},
handleVideoBindingCast: function handleVideoBindingCast(aTab, aEvent) {
@ -274,43 +274,77 @@ var CastingApps = {
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 types = SimpleServiceDiscovery.getSupportedMimeTypes();
// Fast path: Is the given element a video element
let video = this._getVideo(aElement, types, extensions);
if (video) {
return video;
// Fast path: Is the given element a video element?
if (aElement instanceof HTMLVideoElement) {
// If we found a video element, no need to look further, even if no
// 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
// 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) {
video = this._getVideo(element, types, extensions);
if (video) {
break;
}
// Once we find a <video> under the overlay, we check it and exit.
this._getVideo(element, types, extensions, aCallback);
return;
}
}
} catch(e) {}
// Could be null
return video;
},
_getVideo: function(aElement, aTypes, aExtensions) {
if (!(aElement instanceof HTMLVideoElement)) {
return null;
}
_getContentTypeForURI: function(aURI, aCallback) {
let channel = Services.io.newChannelFromURI(aURI);
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>
let posterURL = aElement.poster;
@ -327,8 +361,20 @@ var CastingApps = {
// Use the file extension to guess the mime type
let sourceURI = this.makeURI(sourceURL, null, this.makeURI(aElement.baseURI));
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
@ -339,23 +385,77 @@ var CastingApps = {
// 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
if (this.allowableMimeType(sourceNode.type, aTypes) || this.allowableExtension(sourceURI, aExtensions)) {
return { element: aElement, source: sourceURI.spec, poster: posterURL, sourceURI: sourceURI, type: sourceNode.type };
if (this.allowableExtension(sourceURI, aExtensions)) {
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: {
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)
return false;
let video = CastingApps.getVideo(aElement, aX, aY);
if (CastingApps.session) {
return (video && CastingApps.session.data.source != video.source);
}
return (video != null);
return CastingApps.isVideoCastable(aElement, aX, aY);
}
},
@ -495,13 +595,16 @@ var CastingApps = {
openExternal: function(aElement, aX, aY) {
// Start a second screen media service
let video = this.getVideo(aElement, aX, aY);
if (!video) {
this.getVideo(aElement, aX, aY, this._openExternal.bind(this));
},
_openExternal: function(aVideo) {
if (!aVideo) {
return;
}
function filterFunc(service) {
return this.allowableExtension(video.sourceURI, service.extensions) || this.allowableMimeType(video.type, service.types);
function filterFunc(aService) {
return this.allowableExtension(aVideo.sourceURI, aService.extensions) || this.allowableMimeType(aVideo.type, aService.types);
}
this.prompt(function(aService) {
@ -513,11 +616,12 @@ var CastingApps = {
if (!app)
return;
video.title = aElement.ownerDocument.defaultView.top.document.title;
if (video.element) {
if (aVideo.element) {
aVideo.title = aVideo.element.ownerDocument.defaultView.top.document.title;
// If the video is currently playing on the device, pause it
if (!video.element.paused) {
video.element.pause();
if (!aVideo.element.paused) {
aVideo.element.pause();
}
}
@ -539,11 +643,11 @@ var CastingApps = {
app: app,
remoteMedia: aRemoteMedia,
data: {
title: video.title,
source: video.source,
poster: video.poster
title: aVideo.title,
source: aVideo.source,
poster: aVideo.poster
},
videoRef: Cu.getWeakReference(video.element)
videoRef: Cu.getWeakReference(aVideo.element)
};
}.bind(this), this);
}.bind(this));
@ -601,21 +705,5 @@ var CastingApps = {
if (status == "completed") {
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;
}
};