diff options
Diffstat (limited to 'media')
24 files changed, 1686 insertions, 4 deletions
@@ -1,6 +1,7 @@ include_rules = [ "+gpu", "+jni", + "+net/test", "+third_party/ffmpeg", "+third_party/libvpx", "+third_party/libyuv", diff --git a/media/base/test_data_util.cc b/media/base/test_data_util.cc index a83fa84..9cd886f 100644 --- a/media/base/test_data_util.cc +++ b/media/base/test_data_util.cc @@ -12,14 +12,38 @@ namespace media { +const base::FilePath::CharType kTestDataPath[] = + FILE_PATH_LITERAL("media/test/data"); + base::FilePath GetTestDataFilePath(const std::string& name) { base::FilePath file_path; CHECK(PathService::Get(base::DIR_SOURCE_ROOT, &file_path)); + return file_path.Append(GetTestDataPath()).AppendASCII(name); +} + +base::FilePath GetTestDataPath() { + return base::FilePath(kTestDataPath); +} + +std::string GetURLQueryString(const QueryParams& query_params) { + std::string query = ""; + QueryParams::const_iterator itr = query_params.begin(); + for (; itr != query_params.end(); ++itr) { + if (itr != query_params.begin()) + query.append("&"); + query.append(itr->first + "=" + itr->second); + } + return query; +} - return file_path.AppendASCII("media") - .AppendASCII("test") - .AppendASCII("data") - .AppendASCII(name); +scoped_ptr<net::SpawnedTestServer> StartMediaHttpTestServer() { + scoped_ptr<net::SpawnedTestServer> http_test_server; + http_test_server.reset(new net::SpawnedTestServer( + net::SpawnedTestServer::TYPE_HTTP, + net::SpawnedTestServer::kLocalhost, + GetTestDataPath())); + CHECK(http_test_server->Start()); + return http_test_server.Pass(); } scoped_refptr<DecoderBuffer> ReadTestDataFile(const std::string& name) { diff --git a/media/base/test_data_util.h b/media/base/test_data_util.h index 8d51e96..955d615 100644 --- a/media/base/test_data_util.h +++ b/media/base/test_data_util.h @@ -6,19 +6,34 @@ #define MEDIA_BASE_TEST_DATA_UTIL_H_ #include <string> +#include <utility> +#include <vector> #include "base/basictypes.h" #include "base/files/file_path.h" #include "base/memory/ref_counted.h" #include "base/memory/scoped_ptr.h" +#include "net/test/spawned_test_server/spawned_test_server.h" namespace media { class DecoderBuffer; +typedef std::vector<std::pair<std::string, std::string> > QueryParams; + // Returns a file path for a file in the media/test/data directory. base::FilePath GetTestDataFilePath(const std::string& name); +// Returns relative path for test data folder: media/test/data. +base::FilePath GetTestDataPath(); + +// Starts an HTTP server serving files from media data path. +scoped_ptr<net::SpawnedTestServer> StartMediaHttpTestServer(); + +// Returns a string containing key value query params in the form of: +// "key_1=value_1&key_2=value2" +std::string GetURLQueryString(const QueryParams& query_params); + // Reads a test file from media/test/data directory and stores it in // a DecoderBuffer. Use DecoderBuffer vs DataBuffer to ensure no matter // what a test does, it's safe to use FFmpeg methods. diff --git a/media/media.gyp b/media/media.gyp index fdccc42..00ad811 100644 --- a/media/media.gyp +++ b/media/media.gyp @@ -1349,6 +1349,7 @@ 'media', 'shared_memory_support', '../base/base.gyp:base', + '../net/net.gyp:net_test_support', '../skia/skia.gyp:skia', '../testing/gmock.gyp:gmock', '../testing/gtest.gyp:gtest', diff --git a/media/test/data/blackwhite.html b/media/test/data/blackwhite.html new file mode 100644 index 0000000..6b9d049 --- /dev/null +++ b/media/test/data/blackwhite.html @@ -0,0 +1,231 @@ +<!DOCTYPE html> +<html> + <head> + <style> + body { + color: white; + background-color: black; + } + </style> + </head> + <body onload="main()"> + <div id="buttons"></div> + <table> + <tr> + <td>Image</td> + <td id="video_header"></td> + <td>Absolute Diff</td> + <td>Different Pixels</td> + </tr> + <tr> + <td><img src="blackwhite.png"></div> + <td><video autoplay></video></div> + <td><canvas id="diff"></canvas></td> + <td><canvas id="mask"></canvas></td> + </tr> + </div> + + <p id="result"></p> + + <script> + function log(str) { + document.getElementById('result').textContent = str; + console.log(str); + } + + function loadVideo(name) { + var videoElem = document.querySelector('video'); + videoElem.src = 'blackwhite_' + name; + + document.getElementById('video_header').textContent = name; + videoElem.addEventListener('ended', onVideoEnded); + } + + function onVideoEnded(e) { + document.title = verifyVideo() ? 'ENDED' : 'FAILED'; + } + + function onVideoError(e) { + document.title = 'ERROR'; + document.getElementById('diff').style.visibility = 'hidden'; + document.getElementById('mask').style.visibility = 'hidden'; + log('Error playing video: ' + e.target.error.code + '.'); + } + + function main() { + // Programatically create buttons for each clip for manual testing. + var buttonsElem = document.getElementById('buttons'); + + function createButton(name) { + var buttonElem = document.createElement('button'); + buttonElem.textContent = name; + buttonElem.addEventListener('click', function() { + loadVideo(name); + }); + buttonsElem.appendChild(buttonElem); + } + + var VIDEOS = [ + 'yuv420p.ogv', + 'yuv422p.ogv', + 'yuv444p.ogv', + 'yuv420p.webm', + 'yuv444p.webm', + 'yuv420p.mp4', + 'yuvj420p.mp4', + 'yuv422p.mp4', + 'yuv444p.mp4', + 'yuv420p.avi' + ]; + + for (var i = 0; i < VIDEOS.length; ++i) { + createButton(VIDEOS[i]); + } + + // Video event handlers. + var videoElem = document.querySelector('video'); + videoElem.addEventListener('error', onVideoError); + + // Check if a query parameter was provided for automated tests. + if (window.location.search.length > 1) { + loadVideo(window.location.search.substr(1)); + } else { + // If we're not an automated test, compute some pretty diffs. + document.querySelector('video').addEventListener('ended', + computeDiffs); + } + } + + function getCanvasPixels(canvas) { + try { + return canvas.getContext('2d') + .getImageData(0, 0, canvas.width, canvas.height) + .data; + } catch(e) { + var message = 'ERROR: ' + e; + if (e.name == 'SecurityError') { + message += ' Couldn\'t get image pixels, try running with ' + + '--allow-file-access-from-files.'; + } + log(message); + } + } + + function verifyVideo() { + var videoElem = document.querySelector('video'); + var offscreen = document.createElement('canvas'); + offscreen.width = videoElem.videoWidth; + offscreen.height = videoElem.videoHeight; + offscreen.getContext('2d') + .drawImage(videoElem, 0, 0, offscreen.width, offscreen.height); + + videoData = getCanvasPixels(offscreen); + if (!videoData) + return false; + + // Check the color of a givel pixel |x,y| in |imgData| against an + // expected value, |expected|, with up to |allowedError| difference. + function checkColor(imgData, x, y, stride, expected, allowedError) { + for (var i = 0; i < 3; ++i) { + if (Math.abs(imgData[(x + y * stride) * 4 + i] - expected) > + allowedError) { + return false; + } + } + return true; + } + + // Check one pixel in each quadrant (in the upper left, away from + // boundaries and the text, to avoid compression artifacts). + // Also allow a small error, for the same reason. + + // TODO(mtomasz): Once code.google.com/p/libyuv/issues/detail?id=324 is + // fixed, the allowedError should be decreased to 1. + var allowedError = 2; + + return checkColor(videoData, 30, 30, videoElem.videoWidth, 0xff, + allowedError) && + checkColor(videoData, 150, 30, videoElem.videoWidth, 0x00, + allowedError) && + checkColor(videoData, 30, 150, videoElem.videoWidth, 0x10, + allowedError) && + checkColor(videoData, 150, 150, videoElem.videoWidth, 0xef, + allowedError); + } + + // Compute a standard diff image, plus a high-contrast mask that shows + // each differing pixel more visibly. + function computeDiffs() { + var diffElem = document.getElementById('diff'); + var maskElem = document.getElementById('mask'); + var videoElem = document.querySelector('video'); + var imgElem = document.querySelector('img'); + + var width = imgElem.width; + var height = imgElem.height; + + if (videoElem.videoWidth != width || videoElem.videoHeight != height) { + log('ERROR: video dimensions don\'t match reference image ' + + 'dimensions'); + return; + } + + // Make an offscreen canvas to dump reference image pixels into. + var offscreen = document.createElement('canvas'); + offscreen.width = width; + offscreen.height = height; + + offscreen.getContext('2d').drawImage(imgElem, 0, 0, width, height); + imgData = getCanvasPixels(offscreen); + if (!imgData) + return; + + // Scale and clear diff canvases. + diffElem.width = maskElem.width = width; + diffElem.height = maskElem.height = height; + var diffCtx = diffElem.getContext('2d'); + var maskCtx = maskElem.getContext('2d'); + maskCtx.clearRect(0, 0, width, height); + diffCtx.clearRect(0, 0, width, height); + + // Copy video pixels into diff. + diffCtx.drawImage(videoElem, 0, 0, width, height); + + var diffIData = diffCtx.getImageData(0, 0, width, height); + var diffData = diffIData.data; + var maskIData = maskCtx.getImageData(0, 0, width, height); + var maskData = maskIData.data; + + // Make diffs and collect stats. + var meanSquaredError = 0; + for (var i = 0; i < imgData.length; i += 4) { + var difference = 0; + for (var j = 0; j < 3; ++j) { + diffData[i + j] = Math.abs(diffData[i + j] - imgData[i + j]); + meanSquaredError += diffData[i + j] * diffData[i + j]; + if (diffData[i + j] != 0) { + difference += diffData[i + j]; + } + } + if (difference > 0) { + if (difference <= 3) { + // If we're only off by a bit per channel or so, use darker red. + maskData[i] = 128; + } else { + // Bright red to indicate a different pixel. + maskData[i] = 255; + } + maskData[i+3] = 255; + } + } + + meanSquaredError /= width * height; + log('Mean squared error: ' + meanSquaredError); + diffCtx.putImageData(diffIData, 0, 0); + maskCtx.putImageData(maskIData, 0, 0); + document.getElementById('diff').style.visibility = 'visible'; + document.getElementById('mask').style.visibility = 'visible'; + } + </script> + </body> +</html> diff --git a/media/test/data/eme_player.html b/media/test/data/eme_player.html new file mode 100644 index 0000000..65c120f --- /dev/null +++ b/media/test/data/eme_player.html @@ -0,0 +1,108 @@ +<!DOCTYPE html> +<html lang='en-US'> + <head> + <title>EME playback test application</title> + </head> + <body style='font-family:"Lucida Console", Monaco, monospace; font-size:14px'> + <i>Clearkey works only with content encrypted using bear key.</i><br><br> + <table> + <tr title='URL param mediaFile=...'> + <td><label for='mediaFile'>Encrypted video URL:</label></td> + <td><input id='mediaFile' type='text' size='64'></td> + </tr> + <tr title='URL param licenseServerURL=...'> + <td><label for='licenseServer'>License sever URL:</label></td> + <td><input id='licenseServer' type='text' size='64'></td> + </tr> + <tr title='URL param keySystem=...'> + <td><label for='keySystemList'>Key system:</label></td> + <td><select id='keySystemList'></select></td> + </tr> + <tr title='URL param mediaType=...'> + <td><label for='mediaTypeList'>Media type:</label></td> + <td><select id='mediaTypeList'></select></td> + </tr> + <tr title='URL param usePrefixedEME=1|0'> + <td><label for='usePrefixedEME'>EME API version:</label></td> + <td><select id='usePrefixedEME'></select></td> + </tr> + <tr title='URL param useMSE=1|0'> + <td><label for='useMSE'>Load media by:</label></td> + <td> + <select id='useMSE'> + <option value='true' selected='selected'>MSE</option> + <option value='false'>src</option> + </select> + </td> + </tr> + </table> + <br> + <button onclick='Play();'>Play</button> + <br><br> + Decoded fps: <span id='decodedFPS'></span> + <br> + Dropped fps: <span id='droppedFPS'></span> + <br> + Total dropped frames: <span id='droppedFrames'></span> + <br><br> + <table> + <tr> + <td valign='top'><span id='video'></span></td> + <td valign='top'> + <label for='logs' onclick="toggleDisplay('logs');"><i>Click to toggle logs visibility (newest at top).</i><br></label> + <div id='logs' style='overflow: auto; height: 480px; width: 480px; white-space: nowrap; display: none'></div> + </td> + </tr> + </table> + <div></div> + </body> + <script src='eme_player_js/app_loader.js' type='text/javascript'></script> + <script type='text/javascript'> + var testConfig = new TestConfig(); + testConfig.loadQueryParams(); + // Update document with test configuration values. + var emeApp = new EMEApp(testConfig); + + function onTimeUpdate(e) { + var video = e.target; + if (video.currentTime < 1) + return; + // For loadSession() tests, addKey() will not be called after + // loadSession() (the key is loaded internally). Do not check keyadded + // and heartbeat for these tests. + if (!testConfig.sessionToLoad) { + // keyadded may be fired around the start of playback; check for it + // after a delay to avoid timing issues. + if (testConfig.usePrefixedEME && !video.receivedKeyAdded) + Utils.failTest('Key added event not received.'); + if (testConfig.keySystem == EXTERNAL_CLEARKEY && + !video.receivedHeartbeat) + Utils.failTest('Heartbeat keymessage event not received.'); + } + video.removeEventListener('ended', Utils.failTest); + Utils.installTitleEventHandler(video, 'ended'); + video.removeEventListener('timeupdate', onTimeUpdate); + } + + function Play() { + // Update test configuration with UI elements values. + var video = emeApp.createPlayer().video; + Utils.resetTitleChange(); + // Ended should not fire before onTimeUpdate. + video.addEventListener('ended', Utils.failTest); + video.addEventListener('timeupdate', onTimeUpdate); + video.play(); + } + + function toggleDisplay(id) { + var element = document.getElementById(id); + if (!element) + return; + if (element.style['display'] != 'none') + element.style['display'] = 'none'; + else + element.style['display'] = ''; + } + Play(); + </script> +</html> diff --git a/media/test/data/eme_player_js/app_loader.js b/media/test/data/eme_player_js/app_loader.js new file mode 100644 index 0000000..f4a3e74 --- /dev/null +++ b/media/test/data/eme_player_js/app_loader.js @@ -0,0 +1,24 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// Adds a Javascript source tag to the document. +function addScriptTag(src) { + document.write( + '<script type="text/javascript" src="eme_player_js/' + src + + '"></script>'); +} + +// Load all the dependencies for the app. +addScriptTag('globals.js'); +addScriptTag('utils.js'); +addScriptTag('test_config.js'); +addScriptTag('fps_observer.js'); +addScriptTag('media_source_utils.js'); +addScriptTag('player_utils.js'); +addScriptTag('prefixed_clearkey_player.js'); +addScriptTag('clearkey_player.js'); +addScriptTag('widevine_player.js'); +addScriptTag('prefixed_widevine_player.js'); +addScriptTag('file_io_test_player.js'); +addScriptTag('eme_app.js'); diff --git a/media/test/data/eme_player_js/clearkey_player.js b/media/test/data/eme_player_js/clearkey_player.js new file mode 100644 index 0000000..6de8408 --- /dev/null +++ b/media/test/data/eme_player_js/clearkey_player.js @@ -0,0 +1,33 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ClearKeyPlayer responsible for playing media using Clear Key key system and +// the unprefixed version of EME. +function ClearKeyPlayer(video, testConfig) { + this.video = video; + this.testConfig = testConfig; +} + +ClearKeyPlayer.prototype.init = function() { + PlayerUtils.initEMEPlayer(this); +}; + +ClearKeyPlayer.prototype.registerEventListeners = function() { + PlayerUtils.registerEMEEventListeners(this); +}; + +ClearKeyPlayer.prototype.onMessage = function(message) { + Utils.timeLog('MediaKeySession onMessage', message); + var initData = + Utils.getInitDataFromMessage(message, this.testConfig.mediaType); + var key = Utils.getDefaultKey(this.testConfig.forceInvalidResponse); + var jwkSet = Utils.createJWKData(initData, key); + if (PROMISES_SUPPORTED) { + message.target.update(jwkSet).catch(function(error) { + Utils.failTest(error, KEY_ERROR); + }); + } else { + message.target.update(jwkSet); + } +}; diff --git a/media/test/data/eme_player_js/eme_app.js b/media/test/data/eme_player_js/eme_app.js new file mode 100644 index 0000000..4c2ec0d --- /dev/null +++ b/media/test/data/eme_player_js/eme_app.js @@ -0,0 +1,81 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// EMEApp is responsible for starting playback on the eme_player.html page. +// It selects the suitable player based on key system and other test options. +function EMEApp(testConfig) { + this.video_ = null; + this.testConfig_ = testConfig; + this.updateDocument(testConfig); +} + +EMEApp.prototype.createPlayer = function() { + // Load document test configuration. + this.updateTestConfig(); + if (this.video_) { + Utils.timeLog('Delete old video tag.'); + this.video_.pause(); + this.video_.remove(); + delete(this.video_); + } + + this.video_ = document.createElement('video'); + this.video_.controls = true; + this.video_.preload = true; + this.video_.width = 848; + this.video_.height = 480; + var videoSpan = document.getElementById(VIDEO_ELEMENT_ID); + if (videoSpan) + videoSpan.appendChild(this.video_); + else + document.body.appendChild(this.video_); + + var videoPlayer = PlayerUtils.createPlayer(this.video_, this.testConfig_); + if (!videoPlayer) { + Utils.timeLog('Cannot create a media player.'); + return; + } + Utils.timeLog('Using ' + videoPlayer.constructor.name); + if (this.testConfig_.runFPS) + FPSObserver.observe(this.video_); + + videoPlayer.init(); + return videoPlayer; +}; + +EMEApp.prototype.updateDocument = function(testConfig) { + // Update document lists with test configuration values. + Utils.addOptions(KEYSYSTEM_ELEMENT_ID, KEY_SYSTEMS); + Utils.addOptions(MEDIA_TYPE_ELEMENT_ID, MEDIA_TYPES); + Utils.addOptions(USE_PREFIXED_EME_ID, EME_VERSIONS_OPTIONS, + EME_DISABLED_OPTIONS); + document.getElementById(MEDIA_FILE_ELEMENT_ID).value = + testConfig.mediaFile || DEFAULT_MEDIA_FILE; + document.getElementById(LICENSE_SERVER_ELEMENT_ID).value = + testConfig.licenseServerURL || DEFAULT_LICENSE_SERVER; + if (testConfig.keySystem) + Utils.ensureOptionInList(KEYSYSTEM_ELEMENT_ID, testConfig.keySystem); + if (testConfig.mediaType) + Utils.ensureOptionInList(MEDIA_TYPE_ELEMENT_ID, testConfig.mediaType); + document.getElementById(USE_MSE_ELEMENT_ID).value = testConfig.useMSE; + if (testConfig.usePrefixedEME) + document.getElementById(USE_PREFIXED_EME_ID).value = EME_PREFIXED_VERSION; +}; + +EMEApp.prototype.updateTestConfig = function() { + // Reload test configuration from document. + this.testConfig_.mediaFile = + document.getElementById(MEDIA_FILE_ELEMENT_ID).value; + this.testConfig_.keySystem = + document.getElementById(KEYSYSTEM_ELEMENT_ID).value; + this.testConfig_.mediaType = + document.getElementById(MEDIA_TYPE_ELEMENT_ID).value; + this.testConfig_.useMSE = + document.getElementById(USE_MSE_ELEMENT_ID).value == 'true'; + this.testConfig_.usePrefixedEME = ( + document.getElementById(USE_PREFIXED_EME_ID).value == + EME_PREFIXED_VERSION); + this.testConfig_.licenseServerURL = + document.getElementById(LICENSE_SERVER_ELEMENT_ID).value; +}; diff --git a/media/test/data/eme_player_js/file_io_test_player.js b/media/test/data/eme_player_js/file_io_test_player.js new file mode 100644 index 0000000..d259801 --- /dev/null +++ b/media/test/data/eme_player_js/file_io_test_player.js @@ -0,0 +1,34 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// File IO test player is used to test File IO CDM functionality. +function FileIOTestPlayer(video, testConfig) { + this.video = video; + this.testConfig = testConfig; +} + +FileIOTestPlayer.prototype.init = function() { + PlayerUtils.initPrefixedEMEPlayer(this); +}; + +FileIOTestPlayer.prototype.registerEventListeners = function() { + PlayerUtils.registerPrefixedEMEEventListeners(this); +}; + +FileIOTestPlayer.prototype.onWebkitKeyMessage = function(message) { + // The test result is either '0' or '1' appended to the header. + if (Utils.hasPrefix(message.message, FILE_IO_TEST_RESULT_HEADER)) { + if (message.message.length != FILE_IO_TEST_RESULT_HEADER.length + 1) { + Utils.failTest('Unexpected FileIOTest CDM message' + message.message); + return; + } + var result_index = FILE_IO_TEST_RESULT_HEADER.length; + var success = String.fromCharCode(message.message[result_index]) == 1; + Utils.timeLog('CDM file IO test: ' + (success ? 'Success' : 'Fail')); + if (success) + Utils.setResultInTitle(FILE_IO_TEST_SUCCESS); + else + Utils.failTest('File IO CDM message fail status.'); + } +}; diff --git a/media/test/data/eme_player_js/fps_observer.js b/media/test/data/eme_player_js/fps_observer.js new file mode 100644 index 0000000..8226df8 --- /dev/null +++ b/media/test/data/eme_player_js/fps_observer.js @@ -0,0 +1,67 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// FPSObserver observes a <video> and reports decoded FPS, dropped FPS, and +// total dropped frames during the video playback. +var FPSObserver = new function() { + this.video_ = null; + this.decodedFrames_ = 0; + this.droppedFrames_ = 0; + this.startTime_ = 0; + this.intID_ = null; +} + +FPSObserver.observe = function(video) { + this.video_ = video; + var observer = this; + this.video_.addEventListener('playing', function() { + observer.onVideoPlaying(); + }); + + this.video_.addEventListener('error', function() { + observer.endTest(); + }); + + this.video_.addEventListener('ended', function() { + observer.endTest(); + }); +}; + +FPSObserver.onVideoPlaying = function() { + this.decodedFrames_ = 0; + this.droppedFrames_ = 0; + this.startTime_ = window.performance.now(); + this.endTest(true); + var observer = this; + this.intID_ = window.setInterval(function() { + observer.calculateStats();}, 1000); +}; + +FPSObserver.calculateStats = function() { + if (this.video_.readyState <= HTMLMediaElement.HAVE_CURRENT_DATA || + this.video_.paused || this.video_.ended) + return; + var currentTime = window.performance.now(); + var deltaTime = (currentTime - this.startTime_) / 1000; + this.startTime_ = currentTime; + + // Calculate decoded frames per sec. + var fps = (this.video_.webkitDecodedFrameCount - this.decodedFrames_) / + deltaTime; + this.decodedFrames_ = this.video_.webkitDecodedFrameCount; + fps = fps.toFixed(2); + decodedFPSElement.innerHTML = fps; + + // Calculate dropped frames per sec. + fps = (this.video_.webkitDroppedFrameCount - this.droppedFrames_) / deltaTime; + this.droppedFrames_ = this.video_.webkitDroppedFrameCount; + fps = fps.toFixed(2); + droppedFPSElement.innerHTML = fps; + + droppedFramesElement.innerHTML = this.droppedFrames_; +}; + +FPSObserver.endTest = function() { + window.clearInterval(this.intID_); +}; diff --git a/media/test/data/eme_player_js/globals.js b/media/test/data/eme_player_js/globals.js new file mode 100644 index 0000000..c608dfe --- /dev/null +++ b/media/test/data/eme_player_js/globals.js @@ -0,0 +1,80 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// This file contains global constant variables used by the application. + +// Heart beat message header. +var HEART_BEAT_HEADER = 'HEARTBEAT'; + +// Default key used to encrypt many media files used in browser tests. +var KEY = new Uint8Array([0xeb, 0xdd, 0x62, 0xf1, 0x68, 0x14, 0xd2, 0x7b, + 0x68, 0xef, 0x12, 0x2a, 0xfc, 0xe4, 0xae, 0x3c]); + +var DEFAULT_LICENSE_SERVER = document.location.origin + '/license_server'; + +var DEFAULT_MEDIA_FILE = 'http://shadi.kir/alcatraz/Chrome_44-enc_av.webm'; + +// Key ID used for init data. +var KEY_ID = '0123456789012345'; + +// Unique strings to identify test result expectations. +var KEY_ERROR = 'KEY_ERROR'; +var FILE_IO_TEST_RESULT_HEADER = 'FILEIOTESTRESULT'; +var FILE_IO_TEST_SUCCESS = 'FILE_IO_TEST_SUCCESS'; +var PREFIXED_API_LOAD_SESSION_HEADER = 'LOAD_SESSION|'; +var NOTSUPPORTEDERROR = 'NOTSUPPORTEDERROR'; + +// Available EME key systems to use. +var PREFIXED_CLEARKEY = 'webkit-org.w3.clearkey'; +var CLEARKEY = 'org.w3.clearkey'; +var EXTERNAL_CLEARKEY = 'org.chromium.externalclearkey'; +var WIDEVINE_KEYSYSTEM = 'com.widevine.alpha'; +var FILE_IO_TEST_KEYSYSTEM = 'org.chromium.externalclearkey.fileiotest'; +var EME_PREFIXED_VERSION = 'Prefixed EME (v 0.1b)'; +var EME_UNPREFIXED_VERSION = 'Unprefixed EME (Working draft)'; + +// Key system name:value map to show on the document page. +var KEY_SYSTEMS = { + 'Widevine': WIDEVINE_KEYSYSTEM, + 'Clearkey': CLEARKEY, + 'External Clearkey': EXTERNAL_CLEARKEY +}; + +// General WebM and MP4 name:content_type map to show on the document page. +var MEDIA_TYPES = { + 'WebM - Audio Video': 'video/webm; codecs="vorbis, vp8"', + 'WebM - Video Only': 'video/webm; codecs="vp8"', + 'WebM - Audio Only': 'video/webm; codecs="vorbis"', + 'MP4 - Video Only': 'video/mp4; codecs="avc1.4D4041"', + 'MP4 - Audio Only': 'audio/mp4; codecs="mp4a.40.2"' +}; + +// Update the EME versions list by checking runtime support by the browser. +var EME_VERSIONS_OPTIONS = {}; +EME_VERSIONS_OPTIONS[EME_UNPREFIXED_VERSION] = EME_UNPREFIXED_VERSION; +EME_VERSIONS_OPTIONS[EME_PREFIXED_VERSION] = EME_PREFIXED_VERSION; + +var EME_DISABLED_OPTIONS = []; +var PROMISES_SUPPORTED = false; +if (!document.createElement('video').webkitAddKey) + EME_DISABLED_OPTIONS.push(EME_PREFIXED_VERSION); +if (!document.createElement('video').setMediaKeys) + EME_DISABLED_OPTIONS.push(EME_UNPREFIXED_VERSION); +else + PROMISES_SUPPORTED = MediaKeys.create != undefined; + +// Global document elements ID's. +var VIDEO_ELEMENT_ID = 'video'; +var MEDIA_FILE_ELEMENT_ID = 'mediaFile'; +var LICENSE_SERVER_ELEMENT_ID = 'licenseServer'; +var KEYSYSTEM_ELEMENT_ID = 'keySystemList'; +var MEDIA_TYPE_ELEMENT_ID = 'mediaTypeList'; +var USE_MSE_ELEMENT_ID = 'useMSE'; +var USE_PREFIXED_EME_ID = 'usePrefixedEME'; + +// These variables get updated every second, so better to have global pointers. +var decodedFPSElement = document.getElementById('decodedFPS'); +var droppedFPSElement = document.getElementById('droppedFPS'); +var droppedFramesElement = document.getElementById('droppedFrames'); +var docLogs = document.getElementById('logs'); diff --git a/media/test/data/eme_player_js/media_source_utils.js b/media/test/data/eme_player_js/media_source_utils.js new file mode 100644 index 0000000..8888582 --- /dev/null +++ b/media/test/data/eme_player_js/media_source_utils.js @@ -0,0 +1,70 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// MediaSourceUtils provides basic functionality to load content using MSE API. +var MediaSourceUtils = new function() { +} + +MediaSourceUtils.loadMediaSourceFromTestConfig = function(testConfig, + appendCallbackFn) { + return this.loadMediaSource(testConfig.mediaFile, + testConfig.mediaType, + appendCallbackFn); +}; + +MediaSourceUtils.loadMediaSource = function(mediaFiles, + mediaTypes, + appendCallbackFn) { + if (!mediaFiles || !mediaTypes) + Utils.failTest('Missing parameters in loadMediaSource().'); + + var mediaFiles = Utils.convertToArray(mediaFiles); + var mediaTypes = Utils.convertToArray(mediaTypes); + var totalAppended = 0; + function onSourceOpen(e) { + Utils.timeLog('onSourceOpen', e); + // We can load multiple media files using the same media type. However, if + // more than one media type is used, we expect to have a media type entry + // for each corresponding media file. + var srcBuffer = null; + for (var i = 0; i < mediaFiles.length; i++) { + if (i == 0 || mediaFiles.length == mediaTypes.length) { + Utils.timeLog('Creating a source buffer for type ' + mediaTypes[i]); + try { + srcBuffer = mediaSource.addSourceBuffer(mediaTypes[i]); + } catch (e) { + Utils.failTest('Exception adding source buffer: ' + e.message); + return; + } + } + doAppend(mediaFiles[i], srcBuffer); + } + } + + function doAppend(mediaFile, srcBuffer) { + var xhr = new XMLHttpRequest(); + xhr.open('GET', mediaFile); + xhr.responseType = 'arraybuffer'; + xhr.addEventListener('load', function(e) { + var onUpdateEnd = function(e) { + Utils.timeLog('End of appending buffer from ' + mediaFile); + srcBuffer.removeEventListener('updateend', onUpdateEnd); + totalAppended++; + if (totalAppended == mediaFiles.length) { + if (appendCallbackFn) + appendCallbackFn(mediaSource); + else + mediaSource.endOfStream(); + } + }; + srcBuffer.addEventListener('updateend', onUpdateEnd); + srcBuffer.appendBuffer(new Uint8Array(e.target.response)); + }); + xhr.send(); + } + + var mediaSource = new MediaSource(); + mediaSource.addEventListener('sourceopen', onSourceOpen); + return mediaSource; +}; diff --git a/media/test/data/eme_player_js/player_utils.js b/media/test/data/eme_player_js/player_utils.js new file mode 100644 index 0000000..ac7b6dd --- /dev/null +++ b/media/test/data/eme_player_js/player_utils.js @@ -0,0 +1,186 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// The PlayerUtils provides utility functions to binding common media events +// to specific player functions. It also provides functions to load media source +// base on test configurations. +var PlayerUtils = new function() { +} + +// Prepares a video element for playback by setting default event handlers +// and source attribute. +PlayerUtils.registerDefaultEventListeners = function(player) { + Utils.timeLog('Registering video event handlers.'); + // Map from event name to event listener function name. It is common for + // event listeners to be named onEventName. + var eventListenerMap = { + 'needkey': 'onNeedKey', + 'webkitneedkey': 'onWebkitNeedKey', + 'webkitkeymessage': 'onWebkitKeyMessage', + 'webkitkeyadded': 'onWebkitKeyAdded', + 'webkitkeyerror': 'onWebkitKeyError' + }; + for (eventName in eventListenerMap) { + var eventListenerFunction = player[eventListenerMap[eventName]]; + if (eventListenerFunction) { + player.video.addEventListener(eventName, function(e) { + player[eventListenerMap[e.type]](e); + }); + } + } + // List of events that fail tests. + var failingEvents = ['error', 'abort']; + for (var i = 0; i < failingEvents.length; i++) { + player.video.addEventListener(failingEvents[i], Utils.failTest); + } +}; + +PlayerUtils.registerEMEEventListeners = function(player) { + player.video.addEventListener('needkey', function(message) { + + function addMediaKeySessionListeners(mediaKeySession) { + mediaKeySession.addEventListener('message', function(message) { + player.video.receivedKeyMessage = true; + if (Utils.isHeartBeatMessage(message.message)) { + Utils.timeLog('MediaKeySession onMessage - heart beat', message); + player.video.receivedHeartbeat = true; + } + player.onMessage(message); + }); + mediaKeySession.addEventListener('error', function(error) { + Utils.failTest(error, KEY_ERROR); + }); + } + + Utils.timeLog('Creating new media key session for contentType: ' + + message.contentType + ', initData: ' + + Utils.getHexString(message.initData)); + try { + var session = message.target.mediaKeys.createSession( + message.contentType, message.initData); + if (PROMISES_SUPPORTED) { + session.then(addMediaKeySessionListeners) + .catch (function(error) { + Utils.failTest(error, KEY_ERROR); + }); + } else { + addMediaKeySessionListeners(session); + } + } catch (e) { + Utils.failTest(e); + } + }); + this.registerDefaultEventListeners(player); + try { + Utils.timeLog('Setting video media keys: ' + player.testConfig.keySystem); + if (PROMISES_SUPPORTED) { + MediaKeys.create(player.testConfig.keySystem).then(function(mediaKeys) { + player.video.setMediaKeys(mediaKeys); + }).catch(function(error) { + Utils.failTest(error, NOTSUPPORTEDERROR); + }); + } else { + player.video.setMediaKeys(new MediaKeys(player.testConfig.keySystem)); + } + } catch (e) { + Utils.failTest(e); + } +}; + +PlayerUtils.registerPrefixedEMEEventListeners = function(player) { + player.video.addEventListener('webkitneedkey', function(message) { + var initData = message.initData; + if (player.testConfig.sessionToLoad) { + Utils.timeLog('Loading session: ' + player.testConfig.sessionToLoad); + initData = Utils.convertToUint8Array( + PREFIXED_API_LOAD_SESSION_HEADER + player.testConfig.sessionToLoad); + } + Utils.timeLog(player.testConfig.keySystem + + ' Generate key request, initData: ' + + Utils.getHexString(initData)); + try { + message.target.webkitGenerateKeyRequest(player.testConfig.keySystem, + initData); + } catch (e) { + Utils.failTest(e); + } + }); + + player.video.addEventListener('webkitkeyadded', function(message) { + Utils.timeLog('onWebkitKeyAdded', message); + message.target.receivedKeyAdded = true; + }); + + player.video.addEventListener('webkitkeyerror', function(error) { + Utils.timeLog('onWebkitKeyError', error); + Utils.failTest(error, KEY_ERROR); + }); + + player.video.addEventListener('webkitkeymessage', function(message) { + Utils.timeLog('onWebkitKeyMessage', message); + message.target.receivedKeyMessage = true; + if (Utils.isHeartBeatMessage(message.message)) { + Utils.timeLog('onWebkitKeyMessage - heart beat', message); + message.target.receivedHeartbeat = true; + } + }); + this.registerDefaultEventListeners(player); +}; + +PlayerUtils.setVideoSource = function(player) { + if (player.testConfig.useMSE) { + Utils.timeLog('Loading media using MSE.'); + var mediaSource = + MediaSourceUtils.loadMediaSourceFromTestConfig(player.testConfig); + player.video.src = window.URL.createObjectURL(mediaSource); + } else { + Utils.timeLog('Loading media using src.'); + player.video.src = player.testConfig.mediaFile; + } +}; + +PlayerUtils.initEMEPlayer = function(player) { + this.registerEMEEventListeners(player); + this.setVideoSource(player); +}; + +PlayerUtils.initPrefixedEMEPlayer = function(player) { + this.registerPrefixedEMEEventListeners(player); + this.setVideoSource(player); +}; + +// Return the appropriate player based on test configuration. +PlayerUtils.createPlayer = function(video, testConfig) { + // Update keySystem if using prefixed Clear Key since it is not available as a + // separate key system to choose from; however it can be set in URL query. + var usePrefixedEME = testConfig.usePrefixedEME; + if (testConfig.keySystem == CLEARKEY && usePrefixedEME) + testConfig.keySystem = PREFIXED_CLEARKEY; + + function getPlayerType(keySystem) { + switch (keySystem) { + case WIDEVINE_KEYSYSTEM: + if (usePrefixedEME) + return PrefixedWidevinePlayer; + return WidevinePlayer; + case PREFIXED_CLEARKEY: + return PrefixedClearKeyPlayer; + case EXTERNAL_CLEARKEY: + case CLEARKEY: + if (usePrefixedEME) + return PrefixedClearKeyPlayer; + return ClearKeyPlayer; + case FILE_IO_TEST_KEYSYSTEM: + if (usePrefixedEME) + return FileIOTestPlayer; + default: + Utils.timeLog(keySystem + ' is not a known key system'); + if (usePrefixedEME) + return PrefixedClearKeyPlayer; + return ClearKeyPlayer; + } + } + var Player = getPlayerType(testConfig.keySystem); + return new Player(video, testConfig); +}; diff --git a/media/test/data/eme_player_js/prefixed_clearkey_player.js b/media/test/data/eme_player_js/prefixed_clearkey_player.js new file mode 100644 index 0000000..cbca7d3 --- /dev/null +++ b/media/test/data/eme_player_js/prefixed_clearkey_player.js @@ -0,0 +1,27 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ClearKey player responsible for playing media using Clear Key key system and +// prefixed EME API. +function PrefixedClearKeyPlayer(video, testConfig) { + this.video = video; + this.testConfig = testConfig; +} + +PrefixedClearKeyPlayer.prototype.init = function() { + PlayerUtils.initPrefixedEMEPlayer(this); +}; + +PrefixedClearKeyPlayer.prototype.registerEventListeners = function() { + PlayerUtils.registerPrefixedEMEEventListeners(this); +}; + +PrefixedClearKeyPlayer.prototype.onWebkitKeyMessage = function(message) { + var initData = + Utils.getInitDataFromMessage(message, this.testConfig.mediaType); + var key = Utils.getDefaultKey(this.testConfig.forceInvalidResponse); + Utils.timeLog('Adding key to sessionID: ' + message.sessionId); + message.target.webkitAddKey(this.testConfig.keySystem, key, initData, + message.sessionId); +}; diff --git a/media/test/data/eme_player_js/prefixed_widevine_player.js b/media/test/data/eme_player_js/prefixed_widevine_player.js new file mode 100644 index 0000000..adfd570 --- /dev/null +++ b/media/test/data/eme_player_js/prefixed_widevine_player.js @@ -0,0 +1,32 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// Widevine player responsible for playing media using Widevine key system +// and prefixed EME API. +function PrefixedWidevinePlayer(video, testConfig) { + this.video = video; + this.testConfig = testConfig; +} + +PrefixedWidevinePlayer.prototype.init = function() { + PlayerUtils.initPrefixedEMEPlayer(this); +}; + +PrefixedWidevinePlayer.prototype.registerEventListeners = function() { + PlayerUtils.registerPrefixedEMEEventListeners(this); +}; + +PrefixedWidevinePlayer.prototype.onWebkitKeyMessage = function(message) { + function onSuccess(response) { + var key = new Uint8Array(response); + Utils.timeLog('Adding key to sessionID: ' + message.sessionId, key); + message.target.webkitAddKey(this.testConfig.keySystem, + key, + new Uint8Array(1), + message.sessionId); + } + Utils.sendRequest('POST', 'arraybuffer', message.message, + this.testConfig.licenseServerURL, onSuccess, + this.testConfig.forceInvalidResponse); +}; diff --git a/media/test/data/eme_player_js/test_config.js b/media/test/data/eme_player_js/test_config.js new file mode 100644 index 0000000..8be8a9a --- /dev/null +++ b/media/test/data/eme_player_js/test_config.js @@ -0,0 +1,65 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// Test configuration used by test page to configure the player app and other +// test specific configurations. +function TestConfig() { + this.mediaFile = null; + this.keySystem = null; + this.mediaType = null; + this.licenseServerURL = null; + this.useMSE = false; + this.usePrefixedEME = false; + this.runFPS = false; +} + +TestConfig.prototype.loadQueryParams = function() { + // Load query parameters and set default values. + var r = /([^&=]+)=?([^&]*)/g; + // Lambda function for decoding extracted match values. Replaces '+' with + // space so decodeURIComponent functions properly. + var decodeURI = function decodeURI(s) { + return decodeURIComponent(s.replace(/\+/g, ' ')); + }; + var match; + while (match = r.exec(window.location.search.substring(1))) + this[decodeURI(match[1])] = decodeURI(match[2]); + this.useMSE = this.useMSE == '1' || this.useMSE == 'true'; + this.usePrefixedEME = + this.usePrefixedEME == '1' || this.usePrefixedEME == 'true'; +}; + +TestConfig.updateDocument = function() { + this.loadQueryParams(); + Utils.addOptions(KEYSYSTEM_ELEMENT_ID, KEY_SYSTEMS); + Utils.addOptions(MEDIA_TYPE_ELEMENT_ID, MEDIA_TYPES); + Utils.addOptions(USE_PREFIXED_EME_ID, EME_VERSIONS_OPTIONS, + EME_DISABLED_OPTIONS); + + document.getElementById(MEDIA_FILE_ELEMENT_ID).value = + this.mediaFile || DEFAULT_MEDIA_FILE; + + document.getElementById(LICENSE_SERVER_ELEMENT_ID).value = + this.licenseServerURL || DEFAULT_LICENSE_SERVER; + + if (this.keySystem) + Utils.ensureOptionInList(KEYSYSTEM_ELEMENT_ID, this.keySystem); + if (this.mediaType) + Utils.ensureOptionInList(MEDIA_TYPE_ELEMENT_ID, this.mediaType); + document.getElementById(USE_MSE_ELEMENT_ID).value = this.useMSE; + if (this.usePrefixedEME) + document.getElementById(USE_PREFIXED_EME_ID).value = EME_PREFIXED_VERSION; +}; + +TestConfig.init = function() { + // Reload test configuration from document. + this.mediaFile = document.getElementById(MEDIA_FILE_ELEMENT_ID).value; + this.keySystem = document.getElementById(KEYSYSTEM_ELEMENT_ID).value; + this.mediaType = document.getElementById(MEDIA_TYPE_ELEMENT_ID).value; + this.useMSE = document.getElementById(USE_MSE_ELEMENT_ID).value == 'true'; + this.usePrefixedEME = document.getElementById(USE_PREFIXED_EME_ID).value == + EME_PREFIXED_VERSION; + this.licenseServerURL = + document.getElementById(LICENSE_SERVER_ELEMENT_ID).value; +}; diff --git a/media/test/data/eme_player_js/utils.js b/media/test/data/eme_player_js/utils.js new file mode 100644 index 0000000..e664593 --- /dev/null +++ b/media/test/data/eme_player_js/utils.js @@ -0,0 +1,245 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// Utils provide logging functions and other JS functions commonly used by the +// app and media players. +var Utils = new function() { + this.titleChanged = false; +}; + +// Adds options to document element. +Utils.addOptions = function(elementID, keyValueOptions, disabledOptions) { + disabledOptions = disabledOptions || []; + var selectElement = document.getElementById(elementID); + var keys = Object.keys(keyValueOptions); + for (var i = 0; i < keys.length; i++) { + var key = keys[i]; + var option = new Option(key, keyValueOptions[key]); + option.title = keyValueOptions[key]; + if (disabledOptions.indexOf(key) >= 0) + option.disabled = true; + selectElement.options.add(option); + } +}; + +Utils.convertToArray = function(input) { + if (Array.isArray(input)) + return input; + return [input]; +}; + +Utils.convertToUint8Array = function(msg) { + var ans = new Uint8Array(msg.length); + for (var i = 0; i < msg.length; i++) { + ans[i] = msg.charCodeAt(i); + } + return ans; +}; + +Utils.createJWKData = function(keyId, key) { + // JWK routines copied from third_party/WebKit/LayoutTests/media/ + // encrypted-media/encrypted-media-utils.js + // + // Encodes data (Uint8Array) into base64 string without trailing '='. + // TODO(jrummell): Update once the EME spec is updated to say base64url + // encoding. + function base64Encode(data) { + var result = btoa(String.fromCharCode.apply(null, data)); + return result.replace(/=+$/g, ''); + } + + // Creates a JWK from raw key ID and key. + function createJWK(keyId, key) { + var jwk = '{"kty":"oct","kid":"'; + jwk += base64Encode(keyId); + jwk += '","k":"'; + jwk += base64Encode(key); + jwk += '"}'; + return jwk; + } + + // Creates a JWK Set from an array of JWK(s). + function createJWKSet() { + var jwkSet = '{"keys":['; + for (var i = 0; i < arguments.length; i++) { + if (i != 0) + jwkSet += ','; + jwkSet += arguments[i]; + } + jwkSet += ']}'; + return jwkSet; + } + + return Utils.convertToUint8Array(createJWKSet(createJWK(keyId, key))); +}; + +Utils.documentLog = function(log, success, time) { + if (!docLogs) + return; + time = time || Utils.getCurrentTimeString(); + var timeLog = '<span style="color: green">' + time + '</span>'; + var logColor = !success ? 'red' : 'black'; // default is true. + log = '<span style="color: "' + logColor + '>' + log + '</span>'; + docLogs.innerHTML = timeLog + ' - ' + log + '<br>' + docLogs.innerHTML; +}; + +Utils.ensureOptionInList = function(listID, option) { + var selectElement = document.getElementById(listID); + for (var i = 0; i < selectElement.length; i++) { + if (selectElement.options[i].value == option) { + selectElement.value = option; + return; + } + } + // The list does not have the option, let's add it and select it. + var optionElement = new Option(option, option); + optionElement.title = option; + selectElement.options.add(optionElement); + selectElement.value = option; +}; + +Utils.failTest = function(msg, newTitle) { + var failMessage = 'FAIL: '; + var title = 'FAILED'; + // Handle exception messages; + if (msg.message) { + title = msg.name || 'Error'; + failMessage += title + ' ' + msg.message; + } else if (msg instanceof Event) { + // Handle failing events. + failMessage = msg.target + '.' + msg.type; + title = msg.type; + } else { + failMessage += msg; + } + // Force newTitle if passed. + title = newTitle || title; + // Log failure. + Utils.documentLog(failMessage, false); + console.log(failMessage, msg); + Utils.setResultInTitle(title); +}; + +Utils.getCurrentTimeString = function() { + var date = new Date(); + var hours = ('0' + date.getHours()).slice(-2); + var minutes = ('0' + date.getMinutes()).slice(-2); + var secs = ('0' + date.getSeconds()).slice(-2); + var milliSecs = ('00' + date.getMilliseconds()).slice(-3); + return hours + ':' + minutes + ':' + secs + '.' + milliSecs; +}; + +Utils.getDefaultKey = function(forceInvalidResponse) { + if (forceInvalidResponse) { + Utils.timeLog('Forcing invalid key data.'); + return new Uint8Array([0xAA]); + } + return KEY; +}; + +Utils.getHexString = function(uintArray) { + var hex_str = ''; + for (var i = 0; i < uintArray.length; i++) { + var hex = uintArray[i].toString(16); + if (hex.length == 1) + hex = '0' + hex; + hex_str += hex; + } + return hex_str; +}; + +Utils.getInitDataFromMessage = function(message, mediaType) { + var initData = message.message; + if (mediaType.indexOf('mp4') != -1) { + // Temporary hack for Clear Key in v0.1. + // If content uses mp4, then message.message is PSSH data. Instead of + // parsing that data we hard code the initData. + initData = Utils.convertToUint8Array(KEY_ID); + } + return initData; +}; + +Utils.hasPrefix = function(msg, prefix) { + var message = String.fromCharCode.apply(null, msg); + return message.substring(0, prefix.length) == prefix; +}; + +Utils.installTitleEventHandler = function(element, event) { + element.addEventListener(event, function(e) { + Utils.setResultInTitle(e.type); + }, false); +}; + +Utils.isHeartBeatMessage = function(msg) { + return Utils.hasPrefix(msg, HEART_BEAT_HEADER); +}; + +Utils.resetTitleChange = function() { + this.titleChanged = false; + document.title = ''; +}; + +Utils.sendRequest = function(requestType, responseType, message, serverURL, + onSuccessCallbackFn, forceInvalidResponse) { + var requestAttemptCount = 0; + var MAXIMUM_REQUEST_ATTEMPTS = 3; + var REQUEST_RETRY_DELAY_MS = 3000; + + function sendRequestAttempt() { + requestAttemptCount++; + if (requestAttemptCount == MAXIMUM_REQUEST_ATTEMPTS) { + Utils.failTest('FAILED: Exceeded maximum license request attempts.'); + return; + } + var xmlhttp = new XMLHttpRequest(); + xmlhttp.responseType = responseType; + xmlhttp.open(requestType, serverURL, true); + + xmlhttp.onload = function(e) { + if (this.status == 200) { + if (onSuccessCallbackFn) + onSuccessCallbackFn(this.response); + } else { + Utils.timeLog('Bad response status: ' + this.status); + Utils.timeLog('Bad response: ' + this.response); + Utils.timeLog('Retrying request if possible in ' + + REQUEST_RETRY_DELAY_MS + 'ms'); + setTimeout(sendRequestAttempt, REQUEST_RETRY_DELAY_MS); + } + }; + Utils.timeLog('Attempt (' + requestAttemptCount + + '): sending request to server: ' + serverURL); + xmlhttp.send(message); + } + + if (forceInvalidResponse) { + Utils.timeLog('Not sending request - forcing an invalid response.'); + return onSuccessCallbackFn([0xAA]); + } + sendRequestAttempt(); +}; + +Utils.setResultInTitle = function(title) { + // If document title is 'ENDED', then update it with new title to possibly + // mark a test as failure. Otherwise, keep the first title change in place. + if (!this.titleChanged || document.title.toUpperCase() == 'ENDED') + document.title = title.toUpperCase(); + Utils.timeLog('Set document title to: ' + title + ', updated title: ' + + document.title); + this.titleChanged = true; +}; + +Utils.timeLog = function(/**/) { + if (arguments.length == 0) + return; + var time = Utils.getCurrentTimeString(); + // Log to document. + Utils.documentLog(arguments[0], time); + // Log to JS console. + var logString = time + ' - '; + for (var i = 0; i < arguments.length; i++) { + logString += ' ' + arguments[i]; + } + console.log(logString); +}; diff --git a/media/test/data/eme_player_js/widevine_player.js b/media/test/data/eme_player_js/widevine_player.js new file mode 100644 index 0000000..11e8ec86 --- /dev/null +++ b/media/test/data/eme_player_js/widevine_player.js @@ -0,0 +1,38 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// Widevine player responsible for playing media using Widevine key system +// and EME working draft API. +function WidevinePlayer(video, testConfig) { + this.video = video; + this.testConfig = testConfig; +} + +WidevinePlayer.prototype.init = function() { + PlayerUtils.initEMEPlayer(this); +}; + +WidevinePlayer.prototype.registerEventListeners = function() { + PlayerUtils.registerEMEEventListeners(this); +}; + +WidevinePlayer.prototype.onMessage = function(message) { + Utils.timeLog('MediaKeySession onMessage', message); + var mediaKeySession = message.target; + function onSuccess(response) { + var key = new Uint8Array(response); + Utils.timeLog('Update media key session with license response.', key); + if (PROMISES_SUPPORTED) { + mediaKeySession.update(key).catch(function(error) { + Utils.failTest(error, KEY_ERROR); + }); + } else { + mediaKeySession.update(key); + } + + } + Utils.sendRequest('POST', 'arraybuffer', message.message, + this.testConfig.licenseServerURL, onSuccess, + this.testConfig.forceInvalidResponse); +}; diff --git a/media/test/data/encrypted_frame_size_change.html b/media/test/data/encrypted_frame_size_change.html new file mode 100644 index 0000000..5a90990 --- /dev/null +++ b/media/test/data/encrypted_frame_size_change.html @@ -0,0 +1,50 @@ +<!DOCTYPE html> +<html> + <body onload="load()"> + <p>Tests decoding and rendering encrypted video element that has a changing + resolution.</p> + <video width=320 controls></video> + <video controls></video> + <script src='eme_player_js/app_loader.js' type='text/javascript'></script> + <script> + var firstVideoSeek = false; + var video_fixed_size = document.getElementsByTagName("video")[0]; + var video = document.getElementsByTagName("video")[1]; + var testConfig = new TestConfig(); + testConfig.loadQueryParams(); + + function load() { + loadVideo(video_fixed_size); + loadVideo(video); + } + + function loadVideo(video) { + var videoPlayer = PlayerUtils.createPlayer(video, testConfig); + videoPlayer.init(); + video.addEventListener('playing', function() { + // Make sure the video plays for a bit. + video.addEventListener('timeupdate', function() { + if (video.currentTime > 1.0) { + video.pause(); + } + }); + }); + + video.addEventListener('pause', function() { + video.addEventListener('seeked', function() { + if (!firstVideoSeek) { + Utils.timeLog('One video seeked.'); + firstVideoSeek = true; + return; + } + Utils.setResultInTitle('ENDED'); + }); + video.currentTime = 0.5; + }); + + video.addEventListener('canplay', oncanplay); + video.play(); + } + </script> + </body> +</html> diff --git a/media/test/data/media_source_player.html b/media/test/data/media_source_player.html new file mode 100644 index 0000000..7a2ecbe --- /dev/null +++ b/media/test/data/media_source_player.html @@ -0,0 +1,38 @@ +<!DOCTYPE html> +<html> + <head> + <title>Media Source Player</title> + </head> + <body onload="runTest();"> + <video controls></video> + <script src='eme_player_js/app_loader.js' type='text/javascript'></script> + <script type="text/javascript"> + var video = document.querySelector('video'); + + function onTimeUpdate() { + video.removeEventListener('timeupdate', onTimeUpdate); + video.currentTime = 0.9 * video.duration; + } + + function onSeeked() { + video.removeEventListener('ended', Utils.failTest); + Utils.installTitleEventHandler(video, 'ended'); + } + + // The test completes after media starts playing, seeks to 0.9 of + // duration and fires the ended event. + // The test stops when an error or ended event fire unexpectedly. + function runTest() { + var testConfig = new TestConfig(); + testConfig.loadQueryParams(); + Utils.installTitleEventHandler(video, 'error'); + video.addEventListener('ended', Utils.failTest); + video.addEventListener('seeked', onSeeked); + video.addEventListener('timeupdate', onTimeUpdate); + var source = MediaSourceUtils.loadMediaSourceFromTestConfig(testConfig); + video.src = window.URL.createObjectURL(source); + video.play(); + } + </script> + </body> +</html> diff --git a/media/test/data/mse_config_change.html b/media/test/data/mse_config_change.html new file mode 100644 index 0000000..f536ece --- /dev/null +++ b/media/test/data/mse_config_change.html @@ -0,0 +1,134 @@ +<html> + <head> + <title>Test media source config changes.</title> + </head> + <body onload="runTest();"> + <video controls></video> + <script src='eme_player_js/app_loader.js' type='text/javascript'></script> + <script type="text/javascript"> + var testConfig = new TestConfig(); + testConfig.loadQueryParams(); + var runEncrypted = testConfig.runEncrypted == 1; + + var video = document.querySelector('video'); + var mediaType = 'video/webm; codecs="vorbis, vp8"'; + + var MEDIA_1 = 'bear-320x240.webm'; + var MEDIA_2 = 'bear-640x360.webm'; + if (runEncrypted) { + MEDIA_1 = 'bear-320x240-av_enc-av.webm'; + MEDIA_2 = 'bear-640x360-av_enc-av.webm'; + } + + var MEDIA_1_WIDTH = 320; + var MEDIA_1_HEIGHT = 240; + + var MEDIA_2_WIDTH = 640; + var MEDIA_2_HEIGHT = 360; + var MEDIA_2_LENGTH = 2.75; + + // The time in secs to append the second media source. + var APPEND_TIME = 1; + // DELTA is the time after APPEND_TIME where the second video dimensions + // are guaranteed to take effect. + var DELTA = 0.1; + // Append MEDIA_2 source at APPEND_TIME, so expected total duration is: + var TOTAL_DURATION = APPEND_TIME + MEDIA_2_LENGTH; + + function appendNextSource(mediaSource) { + console.log('Appending next media source at ' + APPEND_TIME + 'sec.'); + var xhr = new XMLHttpRequest(); + xhr.open("GET", MEDIA_2); + xhr.responseType = 'arraybuffer'; + xhr.addEventListener('load', function(e) { + var onUpdateEnd = function(e) { + console.log('Second buffer append ended.'); + srcBuffer.removeEventListener('updateend', onUpdateEnd); + mediaSource.endOfStream(); + if (!mediaSource.duration || + Math.abs(mediaSource.duration - TOTAL_DURATION) > DELTA) { + Utils.failTest('Unexpected mediaSource.duration = ' + + mediaSource.duration + ', expected duration = ' + + TOTAL_DURATION); + return; + } + video.play(); + }; + console.log('Appending next media source at ' + APPEND_TIME + 'sec.'); + var srcBuffer = mediaSource.sourceBuffers[0]; + srcBuffer.addEventListener('updateend', onUpdateEnd); + srcBuffer.timestampOffset = APPEND_TIME; + srcBuffer.appendBuffer(new Uint8Array(e.target.response)); + }); + xhr.send(); + } + + function onTimeUpdate() { + // crbug.com/246308 + //checkVideoProperties(); + + // Seek to APPEND_TIME because after a seek a timeUpdate event is fired + // before video width and height properties get updated. + if (video.currentTime < APPEND_TIME - DELTA) { + // Seek to save test execution time (about 1 secs) and to test seek + // on the first buffer. + video.currentTime = APPEND_TIME - DELTA; + } else if (video.currentTime > APPEND_TIME + DELTA) { + // Check video duration here to guarantee that second segment has been + // appended and video total duration is updated. + // Video duration is a float value so we check it within a range. + if (!video.duration || + Math.abs(video.duration - TOTAL_DURATION) > DELTA) { + Utils.failTest('Unexpected video.duration = ' + video.duration + + ', expected duration = ' + TOTAL_DURATION); + return; + } + + video.removeEventListener('timeupdate', onTimeUpdate); + video.removeEventListener('ended', Utils.failTest); + Utils.installTitleEventHandler(video, 'ended'); + // Seek to save test execution time and to test seek on second buffer. + video.currentTime = APPEND_TIME + MEDIA_2_LENGTH * 0.9; + } + } + + function checkVideoProperties() { + if (video.currentTime <= APPEND_TIME) { + if (video.videoWidth != MEDIA_1_WIDTH || + video.videoHeight != MEDIA_1_HEIGHT) { + logVideoDimensions(); + Utils.failTest('Unexpected dimensions for first video segment.'); + return; + } + } else if (video.currentTime >= APPEND_TIME + DELTA) { + if (video.videoWidth != MEDIA_2_WIDTH || + video.videoHeight != MEDIA_2_HEIGHT) { + logVideoDimensions(); + Utils.failTest('Unexpected dimensions for second video segment.'); + return; + } + } + } + + function logVideoDimensions() { + console.log('video.currentTime = ' + video.currentTime + + ', video dimensions = ' + video.videoWidth + 'x' + + video.videoHeight + '.'); + } + + function runTest() { + testConfig.mediaFile = MEDIA_1; + testConfig.mediaType = mediaType; + video.addEventListener('timeupdate', onTimeUpdate); + video.addEventListener('ended', Utils.failTest); + if (runEncrypted) { + var emePlayer = PlayerUtils.createPlayer(video, testConfig); + emePlayer.registerEventListeners(); + } + var mediaSource = MediaSourceUtils.loadMediaSource( + MEDIA_1, mediaType, appendNextSource); + video.src = window.URL.createObjectURL(mediaSource); + } + </script> + </body> +</html> diff --git a/media/test/data/player.html b/media/test/data/player.html new file mode 100644 index 0000000..e954cf8 --- /dev/null +++ b/media/test/data/player.html @@ -0,0 +1,77 @@ +<html> +<body onload="RunTest();"> +<div id="player_container"></div> +</body> + +<script type="text/javascript"> +// <audio> or <video> player element. +var player; + +// Listen for |event| from |element|, set document.title = |event| upon event. +function InstallTitleEventHandler(element, event) { + element.addEventListener(event, function(e) { + document.title = event.toUpperCase(); + }, false); +} + +function Failed() { + document.title = 'FAILED'; + return false; +} + +function SeekTestStep(e) { + player.removeEventListener('ended', SeekTestStep, false); + + // Test completes on the next ended event. + InstallTitleEventHandler(player, 'ended'); + + player.currentTime = 0.9 * player.duration; + player.play(); +} + +function SeekTestTimeoutSetup() { + if (player.currentTime < 2) + return; + + player.removeEventListener('timeupdate', SeekTestTimeoutSetup, false); + SeekTestStep(); +} + +// Uses URL query parameters to create an audio or video element using a given +// source. URL must be of the form "player.html?[tag]=[media_url]". Plays the +// media and waits for X seconds of playback or the ended event, at which point +// the test seeks near the end of the file and resumes playback. Test completes +// when the second ended event occurs or an error event occurs at any time. +function RunTest() { + var url_parts = window.location.href.split('?'); + if (url_parts.length != 2) + return Failed(); + + var query_parts = url_parts[1].split('='); + if (query_parts.length != 2) + return Failed(); + + var tag = query_parts[0]; + var media_url = query_parts[1]; + if (tag != 'audio' && tag != 'video') + return Failed(); + + // Create player and insert into DOM. + player = document.createElement(tag); + player.controls = true; + document.getElementById('player_container').appendChild(player); + + // Transition to the seek test after X seconds of playback or when the ended + // event occurs, whichever happens first. + player.addEventListener('ended', SeekTestStep, false); + player.addEventListener('timeupdate', SeekTestTimeoutSetup, false); + + // Ensure we percolate up any error events. + InstallTitleEventHandler(player, 'error'); + + // Starts the player. + player.src = media_url; + player.play(); +} +</script> +</html> diff --git a/media/test/data/test_key_system_instantiation.html b/media/test/data/test_key_system_instantiation.html new file mode 100644 index 0000000..0199920 --- /dev/null +++ b/media/test/data/test_key_system_instantiation.html @@ -0,0 +1,21 @@ +<html> + <body> + <video controls="" name="video"> + <!-- This test doesn't play the video, so any file will do + as long as it can be loaded. --> + <source src="bear-320x240-av_enc-a.webm" type="video/webm"> + </video> + <script type="text/javascript"> + function testKeySystemInstantiation(keySystem) { + var video = document.getElementsByTagName('video')[0]; + var initData = new Uint8Array([0x41, 0x42, 0x43]); + try { + video.webkitGenerateKeyRequest(keySystem, initData); + return 'success'; + } catch (err) { + return err.name; + } + } + </script> + </body> +</html> |