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); +}