cloneElementVisually_helpers.js (6842B)
1 const TEST_VIDEO_1 = 2 "http://mochi.test:8888/tests/dom/media/test/bipbop_225w_175kbps.mp4"; 3 const TEST_VIDEO_2 = 4 "http://mochi.test:8888/tests/dom/media/test/pixel_aspect_ratio.mp4"; 5 const LONG_VIDEO = "http://mochi.test:8888/tests/dom/media/test/gizmo.mp4"; 6 7 /** 8 * Ensure that the original <video> is prepped and ready to play 9 * before starting any other tests. 10 */ 11 async function setup() { 12 await SpecialPowers.pushPrefEnv({ 13 set: [ 14 ["media.test.video-suspend", true], 15 ["media.suspend-background-video.enabled", true], 16 ["media.suspend-background-video.delay-ms", 500], 17 ["media.dormant-on-pause-timeout-ms", 0], 18 ["media.cloneElementVisually.testing", true], 19 ], 20 }); 21 22 let originalVideo = document.getElementById("original"); 23 await setVideoSrc(originalVideo, TEST_VIDEO_1); 24 } 25 26 /** 27 * Given a canvas, as well as a width and height of something to be 28 * blitted onto that canvas, makes any necessary adjustments to the 29 * canvas to work with the current display resolution. 30 * 31 * @param canvas (<canvas> element) 32 * The canvas to be adjusted. 33 * @param width (int) 34 * The width of the image to be blitted. 35 * @param height (int) 36 * The height of the image to be blitted. 37 * @return CanvasRenderingContext2D (SpecialPowers wrapper) 38 */ 39 function getWrappedScaledCanvasContext(canvas, width, height) { 40 let devRatio = window.devicePixelRatio || 1; 41 let ctx = SpecialPowers.wrap(canvas.getContext("2d")); 42 let backingRatio = ctx.webkitBackingStorePixelRatio || 1; 43 44 let ratio = 1; 45 ratio = devRatio / backingRatio; 46 canvas.width = ratio * width; 47 canvas.height = ratio * height; 48 canvas.style.width = width + "px"; 49 canvas.style.height = height + "px"; 50 ctx.scale(ratio, ratio); 51 52 return ctx; 53 } 54 55 /** 56 * Given a <video> element in the DOM, figures out its location, and captures 57 * a snapshot of what it's currently presenting. 58 * 59 * @param video (<video> element) 60 * @return ImageData (SpecialPowers wrapper) 61 */ 62 function captureFrameImageData(video) { 63 // Flush layout first, just in case the decoder has recently 64 // updated the dimensions of the video. 65 let rect = video.getBoundingClientRect(); 66 67 let width = video.videoWidth; 68 let height = video.videoHeight; 69 70 let canvas = document.createElement("canvas"); 71 let ctx = getWrappedScaledCanvasContext(canvas, width, height); 72 ctx.drawWindow(window, rect.left, rect.top, width, height, "rgb(0,0,0)"); 73 74 return ctx.getImageData(0, 0, width, height); 75 } 76 77 /** 78 * Given two <video> elements, captures snapshots of what they're currently 79 * displaying, and asserts that they're identical. 80 * 81 * @param video1 (<video> element) 82 * A video element to compare against. 83 * @param video2 (<video> element) 84 * A video to compare with video1. 85 * @returns {Promise<boolean>} 86 * Resolves as true if the two videos match. 87 */ 88 async function assertVideosMatch(video1, video2) { 89 let video1Frame = captureFrameImageData(video1); 90 let video2Frame = captureFrameImageData(video2); 91 92 let left = document.getElementById("left"); 93 let leftCtx = getWrappedScaledCanvasContext( 94 left, 95 video1Frame.width, 96 video1Frame.height 97 ); 98 leftCtx.putImageData(video1Frame, 0, 0); 99 100 let right = document.getElementById("right"); 101 let rightCtx = getWrappedScaledCanvasContext( 102 right, 103 video2Frame.width, 104 video2Frame.height 105 ); 106 rightCtx.putImageData(video2Frame, 0, 0); 107 108 if (video1Frame.data.length != video2Frame.data.length) { 109 return false; 110 } 111 112 let leftDataURL = left.toDataURL(); 113 let rightDataURL = right.toDataURL(); 114 115 if (leftDataURL != rightDataURL) { 116 dump("Left frame: " + leftDataURL + "\n\n"); 117 dump("Right frame: " + rightDataURL + "\n\n"); 118 119 return false; 120 } 121 122 return true; 123 } 124 125 /** 126 * Testing helper function that constructs a node clone of a video, 127 * injects it into the DOM, and then runs an async testing function. 128 * This also does the work of removing the clone before resolving. 129 * 130 * @param video (<video> element) 131 * The video to clone the node from. 132 * @param asyncFn (async function) 133 * A test function that will be passed the new clone as its 134 * only argument. 135 * @returns {Promise<void>} 136 * Resolves when the asyncFn has resolved and the clone has been removed from 137 * the DOM. 138 */ 139 async function withNewClone(video, asyncFn) { 140 let clone = video.cloneNode(); 141 clone.id = "clone"; 142 clone.src = ""; 143 let content = document.getElementById("content"); 144 content.appendChild(clone); 145 146 try { 147 await asyncFn(clone); 148 } finally { 149 clone.remove(); 150 } 151 } 152 153 /** 154 * Sets the src on a video and waits until its ready. 155 * 156 * @param video (<video> element) 157 * The video to set the src on. 158 * @param src (string) 159 * The URL to set as the source on a video. 160 * @returns {Promise<Event>} 161 * Resolves when the video fires the "canplay" event. 162 */ 163 async function setVideoSrc(video, src) { 164 let promiseReady = waitForEventOnce(video, "canplay"); 165 video.src = src; 166 await promiseReady; 167 } 168 169 /** 170 * Returns a Promise once target emits a particular event 171 * once. 172 * 173 * @param target (DOM node) 174 * The target to monitor for the event. 175 * @param event (string) 176 * The name of the event to wait for. 177 * @returns {Promise<Event>} 178 * Resolves when the event fires with the event. 179 */ 180 function waitForEventOnce(target, event) { 181 return new Promise(resolve => { 182 target.addEventListener(event, resolve, { once: true }); 183 }); 184 } 185 186 /** 187 * Polls the video debug data as a hacky way of knowing when 188 * when the decoders have shut down. 189 * 190 * @param video (<video> element) 191 * @returns {Promise<void>} 192 * Resolves when the decoder has shut down. 193 */ 194 function waitForShutdownDecoder(video) { 195 return SimpleTest.promiseWaitForCondition(async () => { 196 let readerData = await SpecialPowers.wrap(video).mozRequestDebugInfo(); 197 return readerData.decoder.reader.audioDecoderName == "shutdown"; 198 }, "Video decoder should eventually shut down."); 199 } 200 201 /** 202 * Ensures that both hiding and pausing the video causes the 203 * video to suspend and make dormant its decoders, respectively. 204 * 205 * @param video (<video element) 206 */ 207 async function ensureVideoSuspendable(video) { 208 video = SpecialPowers.wrap(video); 209 210 ok(!video.hasSuspendTaint(), "Should be suspendable"); 211 212 // First, we'll simulate putting the video in the background by 213 // making it invisible. 214 let suspendPromise = waitForEventOnce(video, "mozentervideosuspend"); 215 video.setVisible(false); 216 await suspendPromise; 217 ok(true, "Suspended after the video was made invisible."); 218 video.setVisible(true); 219 220 ok(!video.hasSuspendTaint(), "Should still be suspendable."); 221 222 // Next, we'll pause the video. 223 await video.pause(); 224 await waitForShutdownDecoder(video); 225 ok(true, "Shutdown decoder after the video was paused."); 226 await video.play(); 227 }