test_eme_protection_query.html (8504B)
1 <!DOCTYPE html> 2 <html> 3 <head> 4 <title>Test Encrypted Media Extensions - Protection Query</title> 5 <script src="/tests/SimpleTest/SimpleTest.js"></script> 6 <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> 7 <script type="text/javascript" src="manifest.js"></script> 8 <script type="text/javascript" src="eme.js"></script> 9 </head> 10 <body> 11 <pre id="test"> 12 <script class="testbody" type="text/javascript"> 13 // Tests in this file check that output protection queries are performed and 14 // handled correctly. This is done by using a special clear key key system that 15 // emits key status to track protection status. 16 17 // Special key system used for these tests. 18 const kClearKeyWithProtectionQuery = 19 "org.mozilla.clearkey_with_protection_query"; 20 const kTestFile = "bipbop-cenc-video-10s.mp4"; 21 const kTestMimeType = 'video/mp4; codecs="avc1.4d4015"'; 22 const kTestKeyId = "7e571d037e571d037e571d037e571d11"; // Hex representation 23 const kTestKey = "7e5733337e5733337e5733337e573311"; 24 25 // This is the special key-id used by the mozilla clearkey CDM to signal 26 // protection query status. As hex it is "6f75747075742d70726f74656374696f6e", 27 // the hex translates to ascii "output-protection". 28 const kProtectionQueryKeyIdString = "output-protection"; 29 30 // Options for requestMediaKeySystemAccess 31 const kKeySystemOptions = [ 32 { 33 initDataTypes: ["cenc"], 34 videoCapabilities: [{ contentType: kTestMimeType }], 35 }, 36 ]; 37 38 // Helper to setup EME on `video`. 39 // @param video the HTMLMediaElement to configure EME on. 40 // @returns a media key session for the video. Callers can use this to 41 // configure the `onkeystatuseschange` event handler for each test. Callers 42 // *should not* configure other aspects of the session as this helper already 43 // does so. 44 async function setupEme(video) { 45 // Start setting up EME. 46 let access = await navigator.requestMediaKeySystemAccess( 47 kClearKeyWithProtectionQuery, 48 kKeySystemOptions 49 ); 50 let mediaKeys = await access.createMediaKeys(); 51 await video.setMediaKeys(mediaKeys); 52 53 let session = video.mediaKeys.createSession(); 54 55 video.onencrypted = async encryptedEvent => { 56 session.onmessage = () => { 57 // Handle license messages. Hard code the license because we always test 58 // with the same file and we know what the license should be. 59 const license = { 60 keys: [ 61 { 62 kty: "oct", 63 kid: HexToBase64(kTestKeyId), 64 k: HexToBase64(kTestKey), 65 }, 66 ], 67 type: "temporary", 68 }; 69 70 const encodedLicense = new TextEncoder().encode(JSON.stringify(license)); 71 72 session.update(encodedLicense); 73 }; 74 75 session.generateRequest( 76 encryptedEvent.initDataType, 77 encryptedEvent.initData 78 ); 79 }; 80 81 return session; 82 } 83 84 // Helper to setup MSE media on `video`. 85 // @param video the HTMLMediaElement to configure MSE on. 86 async function setupMse(video) { 87 const mediaSource = new MediaSource(); 88 video.src = URL.createObjectURL(mediaSource); 89 await once(mediaSource, "sourceopen"); 90 const sourceBuffer = mediaSource.addSourceBuffer("video/mp4"); 91 let fetchResponse = await fetch(kTestFile); 92 sourceBuffer.appendBuffer(await fetchResponse.arrayBuffer()); 93 await once(sourceBuffer, "updateend"); 94 mediaSource.endOfStream(); 95 await once(mediaSource, "sourceended"); 96 } 97 98 // Helper to create a video element and append it to the page. 99 function createAndAppendVideo() { 100 const video = document.createElement("video"); 101 video.id = "video"; 102 // Loop in case tests run slowly, we want video to keep playing until we 103 // get expected events. 104 video.loop = true; 105 document.body.appendChild(video); 106 return video; 107 } 108 109 // Helper to remove a video from the page. 110 function removeVideo() { 111 let video = document.getElementById("video"); 112 CleanUpMedia(video); 113 } 114 115 // Helper to get the status for the kProtectionQueryKeyIdString key id. A 116 // session can (and will) have other keys with their own status, but we want 117 // to check this special key to find the protection query status. 118 function getKeyStatusForProtectionKeyId(session) { 119 for (let [keyId, status] of session.keyStatuses) { 120 if (ArrayBufferToString(keyId) == kProtectionQueryKeyIdString) { 121 return status; 122 } 123 } 124 return null; 125 } 126 127 async function getDisplayMedia() { 128 SpecialPowers.wrap(document).notifyUserGestureActivation(); 129 return navigator.mediaDevices.getDisplayMedia(); 130 } 131 132 // Tests playing encrypted media, starting a screen capture during playback, 133 // then stopping the capture while playback continues. 134 async function testProtectionQueryWithCaptureDuringVideo() { 135 let video = createAndAppendVideo(); 136 137 // Configure the video and start it playing. KeyId should be usable (not restricted). 138 let session = await setupEme(video); 139 let keyStatusChangedPromise1 = new Promise( 140 resolve => 141 (session.onkeystatuseschange = () => { 142 // We may get status changes prior to kProtectionQueryKeyIdString changing, 143 // ensure we wait for the first kProtectionQueryKeyIdString change. 144 if (getKeyStatusForProtectionKeyId(session)) { 145 resolve(); 146 } 147 }) 148 ); 149 await setupMse(video); 150 await Promise.all([video.play(), keyStatusChangedPromise1]); 151 is( 152 getKeyStatusForProtectionKeyId(session), 153 "usable", 154 "Should be usable as capture hasn't started" 155 ); 156 157 let keyStatusChangedPromise2 = new Promise( 158 resolve => (session.onkeystatuseschange = resolve) 159 ); 160 let [displayMediaStream] = await Promise.all([ 161 // Start a screen capture, this should restrict output. 162 getDisplayMedia(), 163 keyStatusChangedPromise2, 164 ]); 165 is( 166 getKeyStatusForProtectionKeyId(session), 167 "output-restricted", 168 "Should be output-restricted as capture is happening" 169 ); 170 171 // Stop the screen capture, output should be usable again. 172 let keyStatusChangedPromise3 = new Promise( 173 resolve => (session.onkeystatuseschange = resolve) 174 ); 175 displayMediaStream.getTracks().forEach(track => track.stop()); 176 displayMediaStream = null; 177 await keyStatusChangedPromise3; 178 is( 179 getKeyStatusForProtectionKeyId(session), 180 "usable", 181 "Should be usable as capture has stopped" 182 ); 183 184 removeVideo(); 185 } 186 187 // Tests starting a screen capture, then starting encrypted playback, then 188 // stopping the screen capture while encrypted playback continues. 189 async function testProtectionQueryWithCaptureStartingBeforeVideo() { 190 // Start capture before setting up video. 191 let displayMediaStream = await getDisplayMedia(); 192 193 let video = createAndAppendVideo(); 194 195 // Configure the video and start it playing. KeyId should be restricted already. 196 let session = await setupEme(video); 197 let keyStatusChangedPromise1 = new Promise( 198 resolve => 199 (session.onkeystatuseschange = () => { 200 // We may get status changes prior to kProtectionQueryKeyIdString changing, 201 // ensure we wait for the first kProtectionQueryKeyIdString change. In 202 // rare cases the first protection status can be "usable" due to racing 203 // between playback and the machinery that detects WebRTC capture. To 204 // avoid this, wait for the first 'output-restricted' notification, 205 // which will either be the first event, or will quickly follow 'usable'. 206 if (getKeyStatusForProtectionKeyId(session) == "output-restricted") { 207 resolve(); 208 } 209 }) 210 ); 211 await setupMse(video); 212 await Promise.all([video.play(), keyStatusChangedPromise1]); 213 is( 214 getKeyStatusForProtectionKeyId(session), 215 "output-restricted", 216 "Should be restricted as capture is happening" 217 ); 218 219 // Stop the screen capture, output should be usable again. 220 let keyStatusChangedPromise2 = new Promise( 221 resolve => (session.onkeystatuseschange = resolve) 222 ); 223 displayMediaStream.getTracks().forEach(track => track.stop()); 224 displayMediaStream = null; 225 await keyStatusChangedPromise2; 226 is( 227 getKeyStatusForProtectionKeyId(session), 228 "usable", 229 "Should be usable as capture has stopped" 230 ); 231 232 removeVideo(); 233 } 234 235 add_task(async function setupEnvironment() { 236 await SpecialPowers.pushPrefEnv({ 237 set: [ 238 // Need test key systems for test key system. 239 ["media.clearkey.test-key-systems.enabled", true], 240 // Need relaxed navigator permissions for getDisplayMedia. 241 ["media.navigator.permission.disabled", true], 242 ], 243 }); 244 }); 245 add_task(testProtectionQueryWithCaptureDuringVideo); 246 add_task(testProtectionQueryWithCaptureStartingBeforeVideo); 247 </script> 248 </pre> 249 </body> 250 </html>