summaryrefslogtreecommitdiffstats
path: root/dom/media/test/browser/browser_encrypted_play_time_telemetry.js
blob: aebc386e6b9652a83e82265ada99b9d58792aa01 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
 * You can obtain one at http://mozilla.org/MPL/2.0/. */

// This test verifies that telemetry gathered around encrypted media playtime
// is gathered as expected.

"use strict";

/* import-globals-from ../eme_standalone.js */

// Clears any existing telemetry data that has been accumulated. Returns a
// promise the will be resolved once the telemetry store is clear.
async function clearTelemetry() {
  // There's an arbitrary interval of 2 seconds in which the content
  // processes sync their event data with the parent process, we wait
  // this out to ensure that we clear everything that is left over from
  // previous tests and don't receive random events in the middle of our tests.
  // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
  await new Promise(resolve => setTimeout(resolve, 2000));

  Services.telemetry.clearEvents();
  return TestUtils.waitForCondition(() => {
    let events = Services.telemetry.snapshotEvents(
      Ci.nsITelemetry.DATASET_ALL_CHANNELS,
      true
    ).content;
    return !events || !events.length;
  });
}

// Opens a tab containing a blank page, returns a promise that will resolve
// to that tab.
async function openTab() {
  const emptyPageUri =
    "https://example.com/browser/dom/media/test/browser/file_empty_page.html";
  return BrowserTestUtils.openNewForegroundTab(window.gBrowser, emptyPageUri);
}

// Creates and configures a video element for EME playback in `tab`. Does not
// start playback for the element. Returns a promise that will resolve once
// the element is setup and ready for playback.
async function loadEmeVideo(tab) {
  const emeHelperUri =
    gTestPath.substr(0, gTestPath.lastIndexOf("/")) + "/eme_standalone.js";
  return SpecialPowers.spawn(
    tab.linkedBrowser,
    [emeHelperUri],
    async _emeHelperUri => {
      // Begin helper functions.
      async function once(target, name) {
        return new Promise(r =>
          target.addEventListener(name, r, { once: true })
        );
      }

      // Helper to clone data into content so the EME helper can use the data.
      function cloneIntoContent(data) {
        return Cu.cloneInto(data, content.wrappedJSObject);
      }
      // End helper functions.

      // Load the EME helper into content.
      Services.scriptloader.loadSubScript(_emeHelperUri, content);
      // Setup EME with the helper.
      let video = content.document.createElement("video");
      video.id = "media";
      content.document.body.appendChild(video);
      let emeHelper = new content.wrappedJSObject.EmeHelper();
      emeHelper.SetKeySystem(
        content.wrappedJSObject.EmeHelper.GetClearkeyKeySystemString()
      );
      emeHelper.SetInitDataTypes(cloneIntoContent(["webm"]));
      emeHelper.SetVideoCapabilities(
        cloneIntoContent([{ contentType: 'video/webm; codecs="vp9"' }])
      );
      emeHelper.AddKeyIdAndKey(
        "2cdb0ed6119853e7850671c3e9906c3c",
        "808b9adac384de1e4f56140f4ad76194"
      );
      emeHelper.onerror = error => {
        is(false, `Got unexpected error from EME helper: ${error}`);
      };
      await emeHelper.ConfigureEme(video);
      // Done setting up EME.

      // Setup MSE.
      const ms = new content.wrappedJSObject.MediaSource();
      video.src = content.wrappedJSObject.URL.createObjectURL(ms);
      await once(ms, "sourceopen");
      const sb = ms.addSourceBuffer("video/webm");
      const videoFile = "sintel-short-clearkey-subsample-encrypted-video.webm";
      let fetchResponse = await content.fetch(videoFile);
      sb.appendBuffer(await fetchResponse.arrayBuffer());
      await once(sb, "updateend");
      ms.endOfStream();
      await once(ms, "sourceended");
    }
  );
}

// Plays the media in `tab` until the 'ended' event is fire. Returns a promise
// that resolves once that state has been reached.
async function playMediaThrough(tab) {
  return SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
    let video = content.document.getElementById("media");
    await Promise.all([new Promise(r => (video.onended = r)), video.play()]);
  });
}

// Plays the media in `tab` until the 'timeupdate' event is fire. Returns a
// promise that resolves once that state has been reached.
async function playMediaToTimeUpdate(tab) {
  return SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
    let video = content.document.getElementById("media");
    await Promise.all([
      new Promise(r => (video.ontimeupdate = r)),
      video.play(),
    ]);
  });
}

// Aborts existing loads and replaces the media on the media element with an
// unencrypted file.
async function replaceMediaWithUnencrypted(tab) {
  return SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
    let video = content.document.getElementById("media");
    video.src = "gizmo.mp4";
    video.load();
  });
}

// Clears/nulls the media keys on the media in `tab`.
async function clearMediaKeys(tab) {
  return SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
    let video = content.document.getElementById("media");
    await video.setMediaKeys(null);
  });
}

// Wait for telemetry information to be received from the content process
// then get the relevant histograms for the tests and return the sums of
// those histograms. If a histogram does not exist this will return a 0
// sum. Returns a promise the resolves to an object with sums for
// - VIDEO_PLAY_TIME_MS
// - VIDEO_ENCRYPTED_PLAY_TIME_MS
// - VIDEO_CLEARKEY_PLAY_TIME_MS
// This function clears the histograms as it gets them.
async function getTelemetrySums() {
  // The telemetry was gathered in the content process, so we have to wait
  // until is arrived in the parent to check it. At time of writing there's
  // not a more elegant way of doing this than polling.
  return TestUtils.waitForCondition(() => {
    let histograms = Services.telemetry.getSnapshotForHistograms(
      "main",
      true
    ).content;
    // All the histogram data should come at the same time, so we just check
    // for playtime here as we always expect it in these tests, but we'll
    // grab other values if present.
    if (histograms.VIDEO_PLAY_TIME_MS) {
      // We only expect to have one value for each histogram, so returning the
      // sums is a short hand for returning that one value.
      return {
        VIDEO_PLAY_TIME_MS: histograms.VIDEO_PLAY_TIME_MS.sum,
        VIDEO_ENCRYPTED_PLAY_TIME_MS: histograms.VIDEO_ENCRYPTED_PLAY_TIME_MS
          ? histograms.VIDEO_ENCRYPTED_PLAY_TIME_MS.sum
          : 0,
        VIDEO_CLEARKEY_PLAY_TIME_MS: histograms.VIDEO_CLEARKEY_PLAY_TIME_MS
          ? histograms.VIDEO_CLEARKEY_PLAY_TIME_MS.sum
          : 0,
      };
    }
    return null;
  }, "recorded telemetry from playing media");
}

// Clear telemetry before other tests. Internally the tests clear the telemetry
// when they check it, so we shouldn't need to do this between tests.
add_task(clearTelemetry);

add_task(async function testEncryptedMediaPlayback() {
  let testTab = await openTab();

  await loadEmeVideo(testTab);
  await playMediaThrough(testTab);

  BrowserTestUtils.removeTab(testTab);

  let telemetrySums = await getTelemetrySums();

  ok(telemetrySums, "Should get play time telemetry");
  is(
    telemetrySums.VIDEO_PLAY_TIME_MS,
    telemetrySums.VIDEO_ENCRYPTED_PLAY_TIME_MS,
    "Play time should be the same as encrypted play time"
  );
  is(
    telemetrySums.VIDEO_PLAY_TIME_MS,
    telemetrySums.VIDEO_CLEARKEY_PLAY_TIME_MS,
    "Play time should be the same as clearkey play time"
  );
  ok(
    telemetrySums.VIDEO_PLAY_TIME_MS > 0,
    "Should have a play time greater than zero"
  );
});

add_task(async function testChangingFromEncryptedToUnencrypted() {
  let testTab = await openTab();

  await loadEmeVideo(testTab);
  await replaceMediaWithUnencrypted(testTab);
  await playMediaToTimeUpdate(testTab);

  BrowserTestUtils.removeTab(testTab);

  let telemetrySums = await getTelemetrySums();

  ok(telemetrySums, "Should get play time telemetry");
  is(
    telemetrySums.VIDEO_ENCRYPTED_PLAY_TIME_MS,
    0,
    "Encrypted play time should be 0"
  );
  is(
    telemetrySums.VIDEO_PLAY_TIME_MS,
    telemetrySums.VIDEO_CLEARKEY_PLAY_TIME_MS,
    "Play time should be the same as clearkey play time because the media element still has a media keys attached"
  );
  ok(
    telemetrySums.VIDEO_PLAY_TIME_MS > 0,
    "Should have a play time greater than zero"
  );
});

add_task(
  async function testChangingFromEncryptedToUnencryptedAndClearingMediaKeys() {
    let testTab = await openTab();

    await loadEmeVideo(testTab);
    await replaceMediaWithUnencrypted(testTab);
    await clearMediaKeys(testTab);
    await playMediaToTimeUpdate(testTab);

    BrowserTestUtils.removeTab(testTab);

    let telemetrySums = await getTelemetrySums();

    ok(telemetrySums, "Should get play time telemetry");
    is(
      telemetrySums.VIDEO_ENCRYPTED_PLAY_TIME_MS,
      0,
      "Encrypted play time should be 0"
    );
    is(
      telemetrySums.VIDEO_CLEARKEY_PLAY_TIME_MS,
      0,
      "Clearkey play time should be 0"
    );
    ok(
      telemetrySums.VIDEO_PLAY_TIME_MS > 0,
      "Should have a play time greater than zero"
    );
  }
);