diff --git a/support.html b/support.html
index 16647faa10..c11df031da 100644
--- a/support.html
+++ b/support.html
@@ -1,6 +1,6 @@
-
+ Shaka Player Browser Support Test
+
- Testing support... found:
diff --git a/support.js b/support.js
new file mode 100644
index 0000000000..e78561228f
--- /dev/null
+++ b/support.js
@@ -0,0 +1,369 @@
+/**
+ * Copyright 2015 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Status values for the report entries.
+const kGood = 0;
+const kInfo = 1;
+const kBad = 2;
+
+const vp8Type = 'video/webm; codecs="vp8"';
+const vp9Type = 'video/webm; codecs="vp9"';
+const mp4Type = 'video/mp4; codecs="avc1.42E01E"';
+const tsType = 'video/mp2t; codecs="avc1.42E01E"';
+
+const clearKeyId = 'org.w3.clearkey';
+const widevineId = 'com.widevine.alpha';
+const playReadyId = 'com.microsoft.playready';
+const adobeAccessId = 'com.adobe.access';
+const fairPlayId = 'com.apple.fairplay';
+
+const classPrefixes = [
+ 'WebKit',
+ 'MS',
+ 'Moz'
+];
+
+const propertyPrefixes = [
+ 'webkit',
+ 'ms',
+ 'moz'
+];
+
+const keySystemPrefixes = [
+ 'webkit-'
+];
+
+var video = document.createElement('video');
+
+// The entries in the report. Each item is an array of name, status, and value.
+var report = [];
+
+// A map of unprefixed names to the found things themselves. Properties are
+// represented as true if found.
+var found = {};
+
+// An array of Promises representing asynchronous operations.
+var async = [];
+
+// Find an entry in the report by name, then change its status to "bad".
+function markAsBad(name) {
+ for (var i = 0; i < report.length; ++i) {
+ if (report[i][0] == name) {
+ report[i][1] = kBad;
+ return;
+ }
+ }
+}
+
+// A helper used by the other testFor* methods. Look for a property named
+// |name| in |parent|. |required| is a true if it is a required property.
+// |boolValue| is true if the actual property value should be replaced with
+// true when found. |prefixes| is the list of prefixes to test if an unprefixed
+// version is not found. |prefixFn| is a function that combines a prefix and
+// name in the correct way for whatever is being tested.
+function testFor(parent, name, required, boolValue, prefixes, prefixFn) {
+ if (parent && (name in parent)) {
+ report.push([name, kGood, '(unprefixed)']);
+ found[name] = boolValue ? true : parent[name];
+ } else {
+ for (var i = 0; i < prefixes.length; ++i) {
+ var name2 = prefixFn(prefixes[i], name);
+ if (parent && (name2 in parent)) {
+ report.push([name, kInfo, prefixes[i]]);
+ found[name] = boolValue ? true : parent[name2];
+ break;
+ }
+ }
+ if (i == prefixes.length) {
+ report.push([name, required ? kBad : kInfo, '(not found)']);
+ }
+ }
+}
+
+function testForClass(parent, name, required) {
+ testFor(parent, name, required, false, classPrefixes,
+ function(prefix, name) {
+ return prefix + name;
+ });
+}
+
+function testForMethod(parent, name, required) {
+ testFor(parent, name, required, false, propertyPrefixes,
+ function(prefix, name) {
+ return prefix + name.charAt(0).toUpperCase() + name.slice(1);
+ });
+}
+
+function testForProperty(parent, name, required) {
+ testFor(parent, name, required, true, propertyPrefixes,
+ function(prefix, name) {
+ return prefix + name.charAt(0).toUpperCase() + name.slice(1);
+ });
+}
+
+function testForMimeType(type) {
+ var mse = found['MediaSource'];
+ if (mse && mse.isTypeSupported(type)) {
+ report.push([type, kGood, '(supported)']);
+ found[type] = true;
+ } else {
+ report.push([type, kInfo, '(not found)']);
+ }
+}
+
+function canPlayType2(ks) {
+ // Check if the video can play any of the well-known types with this key
+ // system, using the 2-argument version of canPlayType.
+ return video.canPlayType(vp8Type, ks) ||
+ video.canPlayType(vp9Type, ks) ||
+ video.canPlayType(mp4Type, ks) ||
+ video.canPlayType(tsType, ks);
+}
+
+function isKeySystemSupported(ks) {
+ console.assert(!navigator.requestMediaKeySystemAccess,
+ 'isKeySystemSupported() should only be used when async testing is not ' +
+ 'available!');
+
+ var mk = found['MediaKeys'];
+ if (mk && mk.isTypeSupported) {
+ return mk.isTypeSupported(ks);
+ } else {
+ if (canPlayType2('com.bogus.keysystem')) {
+ // The browser doesn't understand the 2-argument canPlayType.
+ // Don't give it any legitimate queries using this method.
+ return false;
+ }
+ return canPlayType2(ks);
+ }
+}
+
+function testForKeySystemAsync(ks) {
+ // Check unprefixed first.
+ var p = navigator.requestMediaKeySystemAccess(ks, [{}]).then(function() {
+ return '(unprefixed)';
+ });
+ for (var i = 0; i < keySystemPrefixes.length; ++i) {
+ // Chain a check for a prefixed version if the previous one failed.
+ var prefix = keySystemPrefixes[i];
+ p = p.catch(function(prefix) {
+ var ks2 = prefix + ks;
+ return navigator.requestMediaKeySystemAccess(ks2, [{}]).then(function() {
+ return prefix;
+ });
+ }.bind(prefix));
+ }
+ return p;
+}
+
+function testForKeySystemSync(ks, required) {
+ if (isKeySystemSupported(ks)) {
+ report.push([ks, kGood, '(supported)']);
+ found[ks] = true;
+ } else {
+ for (var i = 0; i < keySystemPrefixes.length; ++i) {
+ var prefix = keySystemPrefixes[i];
+ var ks2 = prefix + ks;
+ if (isKeySystemSupported(ks2)) {
+ report.push([ks, kGood, prefix]);
+ found[ks] = true;
+ break;
+ }
+ }
+ if (i == keySystemPrefixes.length) {
+ report.push([ks, required ? kBad : kInfo, '(not found)']);
+ }
+ }
+}
+
+function testForKeySystem(ks, required) {
+ var Promise = found['Promise'];
+ var mk = found['MediaKeys'];
+
+ if (Promise && mk && navigator.requestMediaKeySystemAccess) {
+ var p = testForKeySystemAsync(ks).then(function(prefix) {
+ report.push([ks, kGood, prefix]);
+ found[ks] = true;
+ }).catch(function() {
+ report.push([ks, required ? kBad : kInfo, '(not found)']);
+ });
+ async.push(p);
+ } else {
+ testForKeySystemSync(ks, required);
+ }
+}
+
+// Required, no polyfill provided:
+testForClass(window, 'HTMLMediaElement', true);
+testForClass(window, 'MediaSource', true);
+testForClass(window, 'Promise', true);
+testForProperty(document, 'children', true);
+
+// Optional:
+testForClass(window, 'VTTCue', false);
+testForProperty(document, 'fullscreenElement', false);
+testForProperty(document, 'fullScreenElement', false);
+
+testForMimeType(vp8Type);
+testForMimeType(vp9Type);
+testForMimeType(mp4Type);
+testForMimeType(tsType);
+
+// At least one of these should be supported:
+if (!found[vp8Type] && !found[vp9Type] && !found[mp4Type] && !found[tsType]) {
+ markAsBad(vp8Type);
+ markAsBad(vp9Type);
+ markAsBad(mp4Type);
+ markAsBad(tsType);
+}
+
+// QoE stats:
+testForMethod(video, 'getVideoPlaybackQuality', false);
+testForProperty(video, 'droppedFrameCount', false);
+testForProperty(video, 'decodedFrameCount', false);
+
+// MediaKeys:
+testForMethod(video, 'generateKeyRequest', false);
+testForClass(window, 'MediaKeys', false);
+testForMethod(found['MediaKeys'], 'create', false);
+testForMethod(found['MediaKeys'], 'isTypeSupported', false);
+testForMethod(navigator, 'requestMediaKeySystemAccess', false);
+testForMethod(window, 'MediaKeySystemAccess', false);
+testForMethod(found['MediaKeySystemAccess'] ?
+ found['MediaKeySystemAccess'].prototype : null,
+ 'getConfiguration', false);
+testForClass(window, 'MediaKeySession');
+
+// Specific CDMs:
+testForKeySystem(clearKeyId, found['MediaKeys'] || found['generateKeyRequest']);
+testForKeySystem(widevineId, false);
+testForKeySystem(playReadyId, false);
+testForKeySystem(adobeAccessId, false);
+testForKeySystem(fairPlayId, false);
+
+// If EME is available, at least one key system other than ClearKey should be
+// available.
+if ((found['MediaKeys'] || found['generateKeyRequest']) &&
+ (!found[clearKeyId] && !found[widevineId] && !found[playReadyId] &&
+ !found[adobeAccessId] && !found[fairPlayId])) {
+ markAsBad(clearKeyId);
+ markAsBad(widevineId);
+ markAsBad(playReadyId);
+ markAsBad(adobeAccessId);
+ markAsBad(fairPlayId);
+}
+
+if (async.length) {
+ // The browser supports Promises and we have async tests going.
+ // Create a Promise for DOMContentLoaded and add it to the list.
+ var loaded = new Promise(function(resolve, reject) {
+ onLoaded(resolve);
+ });
+ async.push(loaded);
+ Promise.all(async).then(onAsyncComplete);
+} else {
+ // The browser does not support Promises or there are no async tests.
+ // Listen for DOMContentLoaded.
+ onLoaded(onAsyncComplete);
+}
+
+function onLoaded(fn) {
+ if (document.readyState == "loading") {
+ document.addEventListener('DOMContentLoaded', fn);
+ } else {
+ fn();
+ }
+}
+
+function onAsyncComplete() {
+ // Synthesize a summary at the top from other properties.
+ // Must be done after all async tasks are complete.
+ var requiredFeatures = found['HTMLMediaElement'] && found['MediaSource'] &&
+ found['Promise'] && found['children'];
+ var qoe = found['getVideoPlaybackQuality'] || found['droppedFrameCount'];
+ var subtitles = found['VTTCue'];
+ var emeApi = found['MediaKeys'] || found['generateKeyRequest'];
+ var emeV01b = found['generateKeyRequest'];
+ var latestEme = found['requestMediaKeySystemAccess'] &&
+ found['getConfiguration'];
+ var anyKeySystems = found[clearKeyId] || found[widevineId] ||
+ found[playReadyId] || found[adobeAccessId] ||
+ found[fairPlayId];
+ var fullscreenApi = found['fullscreenElement'] || found['fullScreenElement'];
+ var requiresPolyfills = !latestEme || !found['getVideoPlaybackQuality'] ||
+ !document.fullscreenElement;
+
+ var emeStatus, emeValue;
+ if (emeApi && anyKeySystems) {
+ emeStatus = kGood;
+ if (latestEme) {
+ emeValue = 'latest EME';
+ } else if (emeV01b) {
+ emeValue = 'EME v0.1b';
+ } else {
+ emeValue = 'unknown';
+ }
+ } else if (emeApi) {
+ emeStatus = kBad;
+ emeValue = 'no known key systems!';
+ } else {
+ emeStatus = kInfo;
+ emeValue = 'not supported';
+ }
+
+ var summary = [];
+ summary.push(["userAgent", kInfo, navigator.userAgent]);
+ summary.push(reportEntry('Required Features', requiredFeatures, true));
+ summary.push(reportEntry('QoE Stats', qoe, false));
+ summary.push(reportEntry('Subtitles', subtitles, false));
+ summary.push(['Encrypted Content', emeStatus, emeValue]);
+ summary.push(['Requires Polyfills', requiresPolyfills ? kInfo : kGood,
+ 'yes', 'natively supported!']);
+ summary.push(reportDivider());
+
+ // Prepend the summary.
+ report.unshift.apply(report, summary);
+
+ // Render the final report.
+ renderReport();
+}
+
+function reportDivider() {
+ return ['=====', kInfo, '====='];
+}
+
+function reportEntry(name, ok, important) {
+ var status = ok ? kGood : (important ? kBad : kInfo);
+ var text = ok ? 'OK' : (important ? 'FAIL' : 'missing');
+ return [name, status, text];
+}
+
+function renderReport() {
+ var table = document.createElement('table');
+ for (var i = 0; i < report.length; ++i) {
+ var tr = document.createElement('tr');
+ var td0 = document.createElement('td');
+ var td1 = document.createElement('td');
+ td0.textContent = report[i][0];
+ if (report[i][1] == kGood) td1.style.color = '#070';
+ else if (report[i][1] == kBad) td1.style.color = '#700';
+ td1.textContent = report[i][2];
+ tr.appendChild(td0);
+ tr.appendChild(td1);
+ table.appendChild(tr);
+ }
+ document.body.appendChild(table);
+}