Bug 1067216 - Make MediaKeys.isTypeSupported() more accurate. r=edwin,jesup

This commit is contained in:
Chris Pearce 2014-10-14 13:52:56 +13:00
parent 5ac2fe1151
commit d0653badc3
7 changed files with 206 additions and 47 deletions

View File

@ -16,9 +16,16 @@
#include "nsContentUtils.h"
#include "nsIScriptObjectPrincipal.h"
#include "mozilla/Preferences.h"
#include "nsContentTypeParser.h"
#ifdef MOZ_FMP4
#include "MP4Decoder.h"
#endif
#ifdef XP_WIN
#include "mozilla/WindowsVersion.h"
#endif
#include "nsContentCID.h"
#include "nsServiceManagerUtils.h"
#include "mozIGeckoMediaPluginService.h"
namespace mozilla {
@ -107,15 +114,86 @@ MediaKeys::SetServerCertificate(const ArrayBufferViewOrArrayBuffer& aCert, Error
}
static bool
IsSupportedKeySystem(const nsAString& aKeySystem)
HaveGMPFor(const nsCString& aKeySystem,
const nsCString& aAPI,
const nsCString& aTag = EmptyCString())
{
return aKeySystem.EqualsASCII("org.w3.clearkey") ||
#ifdef XP_WIN
(aKeySystem.EqualsASCII("com.adobe.access") &&
IsVistaOrLater() &&
Preferences::GetBool("media.eme.adobe-access.enabled", false)) ||
nsCOMPtr<mozIGeckoMediaPluginService> mps =
do_GetService("@mozilla.org/gecko-media-plugin-service;1");
if (NS_WARN_IF(!mps)) {
return false;
}
nsTArray<nsCString> tags;
tags.AppendElement(aKeySystem);
if (!aTag.IsEmpty()) {
tags.AppendElement(aTag);
}
// Note: EME plugins need a non-null nodeId here, as they must
// not be shared across origins.
bool hasPlugin = false;
if (NS_FAILED(mps->HasPluginForAPI(aAPI,
&tags,
&hasPlugin))) {
return false;
}
return hasPlugin;
}
static bool
IsPlayableMP4Type(const nsAString& aContentType)
{
#ifdef MOZ_FMP4
nsContentTypeParser parser(aContentType);
nsAutoString mimeType;
nsresult rv = parser.GetType(mimeType);
if (NS_FAILED(rv)) {
return false;
}
nsAutoString codecs;
parser.GetParameter("codecs", codecs);
NS_ConvertUTF16toUTF8 mimeTypeUTF8(mimeType);
return MP4Decoder::CanHandleMediaType(mimeTypeUTF8, codecs);
#else
return false;
#endif
false;
}
bool
MediaKeys::IsTypeSupported(const nsAString& aKeySystem,
const Optional<nsAString>& aInitDataType,
const Optional<nsAString>& aContentType)
{
if (aKeySystem.EqualsLiteral("org.w3.clearkey") &&
(!aInitDataType.WasPassed() || aInitDataType.Value().EqualsLiteral("cenc")) &&
(!aContentType.WasPassed() || IsPlayableMP4Type(aContentType.Value())) &&
HaveGMPFor(NS_LITERAL_CSTRING("org.w3.clearkey"),
NS_LITERAL_CSTRING("eme-decrypt"))) {
return true;
}
#ifdef XP_WIN
// Note: Adobe Access's GMP uses WMF to decode, so anything our MP4Reader
// thinks it can play on Windows, the Access GMP should be able to play.
if (aKeySystem.EqualsLiteral("com.adobe.access") &&
Preferences::GetBool("media.eme.adobe-access.enabled", false) &&
IsVistaOrLater() && // Win Vista and later only.
(!aInitDataType.WasPassed() || aInitDataType.Value().EqualsLiteral("cenc")) &&
(!aContentType.WasPassed() || IsPlayableMP4Type(aContentType.Value())) &&
HaveGMPFor(NS_LITERAL_CSTRING("com.adobe.access"),
NS_LITERAL_CSTRING("eme-decrypt")) &&
HaveGMPFor(NS_LITERAL_CSTRING("com.adobe.access"),
NS_LITERAL_CSTRING("decode-video"),
NS_LITERAL_CSTRING("h264")) &&
HaveGMPFor(NS_LITERAL_CSTRING("com.adobe.access"),
NS_LITERAL_CSTRING("decode-audio"),
NS_LITERAL_CSTRING("aac"))) {
return true;
}
#endif
return false;
}
/* static */
@ -128,8 +206,16 @@ MediaKeys::IsTypeSupported(const GlobalObject& aGlobal,
{
// TODO: Should really get spec changed to this is async, so we can wait
// for user to consent to running plugin.
return IsSupportedKeySystem(aKeySystem) ? IsTypeSupportedResult::Maybe
: IsTypeSupportedResult::_empty;
bool supported = IsTypeSupported(aKeySystem, aInitDataType, aContentType);
EME_LOG("MediaKeys::IsTypeSupported keySystem='%s' initDataType='%s' contentType='%s' supported=%d",
NS_ConvertUTF16toUTF8(aKeySystem).get(),
(aInitDataType.WasPassed() ? NS_ConvertUTF16toUTF8(aInitDataType.Value()).get() : ""),
(aContentType.WasPassed() ? NS_ConvertUTF16toUTF8(aContentType.Value()).get() : ""),
supported);
return supported ? IsTypeSupportedResult::Probably
: IsTypeSupportedResult::_empty;
}
already_AddRefed<Promise>
@ -242,7 +328,7 @@ MediaKeys::Init(ErrorResult& aRv)
return nullptr;
}
if (!IsSupportedKeySystem(mKeySystem)) {
if (!IsTypeSupported(mKeySystem)) {
promise->MaybeReject(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
return promise.forget();
}
@ -271,7 +357,7 @@ MediaKeys::Init(ErrorResult& aRv)
promise->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR);
return promise.forget();
}
mTopLevelPrincipal = top->GetExtantDoc()->NodePrincipal();
if (!mPrincipal || !mTopLevelPrincipal) {

View File

@ -124,6 +124,10 @@ public:
private:
static bool IsTypeSupported(const nsAString& aKeySystem,
const Optional<nsAString>& aInitDataType = Optional<nsAString>(),
const Optional<nsAString>& aContentType = Optional<nsAString>());
bool IsInPrivateBrowsing();
already_AddRefed<Promise> Init(ErrorResult& aRv);

View File

@ -143,6 +143,7 @@ GeckoMediaPluginService::GeckoMediaPluginService()
: mMutex("GeckoMediaPluginService::mMutex")
, mShuttingDown(false)
, mShuttingDownOnGMPThread(false)
, mScannedPluginOnDisk(false)
, mWaitingForPluginsAsyncShutdown(false)
{
MOZ_ASSERT(NS_IsMainThread());
@ -590,7 +591,7 @@ GeckoMediaPluginService::UnloadPlugins()
MutexAutoLock lock(mMutex);
// Note: CloseActive is async; it will actually finish
// shutting down when all the plugins have unloaded.
for (uint32_t i = 0; i < mPlugins.Length(); i++) {
for (size_t i = 0; i < mPlugins.Length(); i++) {
mPlugins[i]->CloseActive(true);
}
mPlugins.Clear();
@ -619,7 +620,7 @@ GeckoMediaPluginService::CrashPlugins()
MOZ_ASSERT(NS_GetCurrentThread() == mGMPThread);
MutexAutoLock lock(mMutex);
for (uint32_t i = 0; i < mPlugins.Length(); i++) {
for (size_t i = 0; i < mPlugins.Length(); i++) {
mPlugins[i]->Crash();
}
}
@ -652,6 +653,8 @@ GeckoMediaPluginService::LoadFromEnvironment()
pos = next + 1;
}
}
mScannedPluginOnDisk = true;
}
NS_IMETHODIMP
@ -693,47 +696,88 @@ GeckoMediaPluginService::RemovePluginDirectory(const nsAString& aDirectory)
return NS_OK;
}
class DummyRunnable : public nsRunnable {
public:
NS_IMETHOD Run() { return NS_OK; }
};
NS_IMETHODIMP
GeckoMediaPluginService::HasPluginForAPI(const nsACString& aNodeId,
const nsACString& aAPI,
GeckoMediaPluginService::HasPluginForAPI(const nsACString& aAPI,
nsTArray<nsCString>* aTags,
bool* aResult)
{
NS_ENSURE_ARG(aTags && aTags->Length() > 0);
NS_ENSURE_ARG(aResult);
nsCString temp(aAPI);
GMPParent *parent = SelectPluginForAPI(aNodeId, temp, *aTags, false);
*aResult = !!parent;
const char* env = nullptr;
if (!mScannedPluginOnDisk && (env = PR_GetEnv("MOZ_GMP_PATH")) && *env) {
// We have a MOZ_GMP_PATH environment variable which may specify the
// location of plugins to load, and we haven't yet scanned the disk to
// see if there are plugins there. Get the GMP thread, which will
// cause an event to be dispatched to which scans for plugins. We
// dispatch a sync event to the GMP thread here in order to wait until
// after the GMP thread has scanned any paths in MOZ_GMP_PATH.
nsCOMPtr<nsIThread> thread;
nsresult rv = GetThread(getter_AddRefs(thread));
if (NS_FAILED(rv)) {
return rv;
}
thread->Dispatch(new DummyRunnable(), NS_DISPATCH_SYNC);
MOZ_ASSERT(mScannedPluginOnDisk, "Should have scanned MOZ_GMP_PATH by now");
}
{
MutexAutoLock lock(mMutex);
nsCString api(aAPI);
GMPParent* gmp = FindPluginForAPIFrom(0, api, *aTags, nullptr);
*aResult = (gmp != nullptr);
}
return NS_OK;
}
GMPParent*
GeckoMediaPluginService::FindPluginForAPIFrom(size_t aSearchStartIndex,
const nsCString& aAPI,
const nsTArray<nsCString>& aTags,
size_t* aOutPluginIndex)
{
mMutex.AssertCurrentThreadOwns();
for (size_t i = aSearchStartIndex; i < mPlugins.Length(); i++) {
GMPParent* gmp = mPlugins[i];
bool supportsAllTags = true;
for (size_t t = 0; t < aTags.Length(); t++) {
const nsCString& tag = aTags.ElementAt(t);
if (!gmp->SupportsAPI(aAPI, tag)) {
supportsAllTags = false;
break;
}
}
if (!supportsAllTags) {
continue;
}
if (aOutPluginIndex) {
*aOutPluginIndex = i;
}
return gmp;
}
return nullptr;
}
GMPParent*
GeckoMediaPluginService::SelectPluginForAPI(const nsACString& aNodeId,
const nsCString& aAPI,
const nsTArray<nsCString>& aTags,
bool aCloneCrossNodeIds)
const nsTArray<nsCString>& aTags)
{
MOZ_ASSERT(NS_GetCurrentThread() == mGMPThread || !aCloneCrossNodeIds,
MOZ_ASSERT(NS_GetCurrentThread() == mGMPThread,
"Can't clone GMP plugins on non-GMP threads.");
GMPParent* gmpToClone = nullptr;
{
MutexAutoLock lock(mMutex);
for (uint32_t i = 0; i < mPlugins.Length(); i++) {
GMPParent* gmp = mPlugins[i];
bool supportsAllTags = true;
for (uint32_t t = 0; t < aTags.Length(); t++) {
const nsCString& tag = aTags[t];
if (!gmp->SupportsAPI(aAPI, tag)) {
supportsAllTags = false;
break;
}
}
if (!supportsAllTags) {
continue;
}
size_t index = 0;
GMPParent* gmp = nullptr;
while ((gmp = FindPluginForAPIFrom(index, aAPI, aTags, &index))) {
if (aNodeId.IsEmpty()) {
if (gmp->CanBeSharedCrossNodeIds()) {
return gmp;
@ -744,15 +788,17 @@ GeckoMediaPluginService::SelectPluginForAPI(const nsACString& aNodeId,
return gmp;
}
// This GMP has the correct type but has the wrong origin; hold on to it
// This GMP has the correct type but has the wrong nodeId; hold on to it
// in case we need to clone it.
gmpToClone = gmp;
// Loop around and try the next plugin; it may be usable from aNodeId.
index++;
}
}
// Plugin exists, but we can't use it due to cross-origin separation. Create a
// new one.
if (aCloneCrossNodeIds && gmpToClone) {
if (gmpToClone) {
GMPParent* clone = ClonePlugin(gmpToClone);
if (!aNodeId.IsEmpty()) {
clone->SetNodeId(aNodeId);
@ -847,7 +893,7 @@ GeckoMediaPluginService::RemoveOnGMPThread(const nsAString& aDirectory)
}
MutexAutoLock lock(mMutex);
for (uint32_t i = 0; i < mPlugins.Length(); ++i) {
for (size_t i = 0; i < mPlugins.Length(); ++i) {
nsCOMPtr<nsIFile> pluginpath = mPlugins[i]->GetDirectory();
bool equals;
if (NS_SUCCEEDED(directory->Equals(pluginpath, &equals)) && equals) {

View File

@ -19,6 +19,7 @@
#include "nsITimer.h"
#include "nsClassHashtable.h"
#include "nsDataHashtable.h"
#include "mozilla/Atomics.h"
template <class> struct already_AddRefed;
@ -49,8 +50,11 @@ private:
GMPParent* SelectPluginForAPI(const nsACString& aNodeId,
const nsCString& aAPI,
const nsTArray<nsCString>& aTags,
bool aCloneCrossNodeIds = true);
const nsTArray<nsCString>& aTags);
GMPParent* FindPluginForAPIFrom(size_t aSearchStartIndex,
const nsCString& aAPI,
const nsTArray<nsCString>& aTags,
size_t* aOutPluginIndex);
void UnloadPlugins();
void CrashPlugins();
@ -94,6 +98,10 @@ private:
bool mShuttingDown;
bool mShuttingDownOnGMPThread;
// True if we've inspected MOZ_GMP_PATH on the GMP thread and loaded any
// plugins found there into mPlugins.
Atomic<bool> mScannedPluginOnDisk;
template<typename T>
class MainThreadOnly {
public:

View File

@ -26,7 +26,7 @@ class GMPVideoHost;
[ptr] native GMPDecryptorProxy(GMPDecryptorProxy);
[ptr] native GMPAudioDecoderProxy(GMPAudioDecoderProxy);
[scriptable, uuid(b350d3b6-00c9-4602-bdfe-84c2be8d1e7a)]
[scriptable, uuid(657443a4-6b5d-4181-98b0-56997b35bd57)]
interface mozIGeckoMediaPluginService : nsISupports
{
@ -40,9 +40,7 @@ interface mozIGeckoMediaPluginService : nsISupports
* Callable on any thread
*/
[noscript]
boolean hasPluginForAPI([optional] in ACString nodeId,
in ACString api,
in TagArray tags);
boolean hasPluginForAPI(in ACString api, in TagArray tags);
/**
* Get a video decoder that supports the specified tags.

View File

@ -174,6 +174,9 @@ function startTest(test, token)
}, bail("failed to create MediaKeys object")).then(function() {
info(token + " set MediaKeys on <video> element ok");
ok(MediaKeys.isTypeSupported(KEYSYSTEM_TYPE, ev.initDataType, test.type),
"MediaKeys should still support keysystem after CDM created...");
var session = v.mediaKeys.createSession(test.sessionType);
session.addEventListener("message", UpdateSessionFunc(test));
session.generateRequest(ev.initDataType, ev.initData).then(function() {
@ -202,7 +205,23 @@ function startTest(test, token)
PlayTest(test, v);
}
function testIsTypeSupported()
{
var t = MediaKeys.isTypeSupported;
const clearkey = "org.w3.clearkey";
ok(!t("bogus", "bogon", "video/bogus"), "Invalid type.");
ok(t(clearkey), "ClearKey supported.");
ok(!t(clearkey, "bogus"), "ClearKey bogus initDataType not supported.");
ok(t(clearkey, "cenc"), "ClearKey/cenc should be supported.");
ok(!t(clearkey, "cenc", "bogus"), "ClearKey/cenc bogus content type should be supported.");
ok(t(clearkey, "cenc", 'video/mp4'), "ClearKey/cenc video/mp4 supported.");
ok(t(clearkey, "cenc", 'video/mp4; codecs="avc1.4d4015,mp4a.40.2"'), "ClearKey/cenc H.264/AAC supported.");
ok(t(clearkey, "cenc", 'audio/mp4'), "ClearKey/cenc audio/mp4 supported.");
ok(t(clearkey, "cenc", 'audio/mp4; codecs="mp4a.40.2"'), "ClearKey/cenc AAC LC supported.");
}
function beginTest() {
testIsTypeSupported();
manager.runTests(gEMETests, startTest);
}

View File

@ -203,16 +203,14 @@ int VcmSIPCCBinding::getVideoCodecsGmp()
// H.264 only for now
bool has_gmp;
nsresult rv;
rv = gSelf->mGMPService->HasPluginForAPI(NS_LITERAL_CSTRING(""),
NS_LITERAL_CSTRING("encode-video"),
rv = gSelf->mGMPService->HasPluginForAPI(NS_LITERAL_CSTRING("encode-video"),
&tags,
&has_gmp);
if (NS_FAILED(rv) || !has_gmp) {
return 0;
}
rv = gSelf->mGMPService->HasPluginForAPI(NS_LITERAL_CSTRING(""),
NS_LITERAL_CSTRING("decode-video"),
rv = gSelf->mGMPService->HasPluginForAPI(NS_LITERAL_CSTRING("decode-video"),
&tags,
&has_gmp);
if (NS_FAILED(rv) || !has_gmp) {