shaka-player 4.3.0 → 4.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/.github/workflows/build-and-test.yaml +20 -0
  2. package/AUTHORS +2 -0
  3. package/CHANGELOG.md +44 -0
  4. package/CONTRIBUTORS +2 -0
  5. package/README.md +1 -1
  6. package/conditional/dummy_cast_proxy.js +120 -0
  7. package/dist/demo.compiled.debug.js +104 -110
  8. package/dist/demo.compiled.debug.map +3 -3
  9. package/dist/demo.compiled.js +104 -110
  10. package/dist/demo.compiled.map +3 -3
  11. package/dist/deps.js +8 -6
  12. package/dist/locales.js +32 -32
  13. package/dist/receiver.compiled.debug.js +54 -60
  14. package/dist/receiver.compiled.debug.map +3 -3
  15. package/dist/receiver.compiled.js +54 -60
  16. package/dist/receiver.compiled.map +3 -3
  17. package/dist/shaka-player.compiled.d.ts +19 -2
  18. package/dist/shaka-player.compiled.debug.d.ts +19 -2
  19. package/dist/shaka-player.compiled.debug.externs.js +102 -80
  20. package/dist/shaka-player.compiled.debug.js +657 -652
  21. package/dist/shaka-player.compiled.debug.map +4 -4
  22. package/dist/shaka-player.compiled.externs.js +102 -80
  23. package/dist/shaka-player.compiled.js +904 -901
  24. package/dist/shaka-player.compiled.map +4 -4
  25. package/dist/shaka-player.ui.d.ts +19 -2
  26. package/dist/shaka-player.ui.debug.d.ts +19 -2
  27. package/dist/shaka-player.ui.debug.externs.js +102 -80
  28. package/dist/shaka-player.ui.debug.js +703 -698
  29. package/dist/shaka-player.ui.debug.map +4 -4
  30. package/dist/shaka-player.ui.externs.js +102 -80
  31. package/dist/shaka-player.ui.js +1029 -1026
  32. package/dist/shaka-player.ui.map +4 -4
  33. package/docs/tutorials/welcome.md +2 -2
  34. package/externs/shaka/mp4_parser.js +3 -0
  35. package/externs/shaka/player.js +2 -1
  36. package/lib/ads/ad_manager.js +2 -2
  37. package/lib/cea/cea708_window.js +2 -2
  38. package/lib/cea/mp4_cea_parser.js +37 -16
  39. package/lib/dash/segment_template.js +1 -1
  40. package/lib/hls/hls_parser.js +49 -23
  41. package/lib/media/content_workarounds.js +12 -0
  42. package/lib/media/media_source_capabilities.js +36 -0
  43. package/lib/media/media_source_engine.js +28 -7
  44. package/lib/media/segment_reference.js +3 -0
  45. package/lib/media/streaming_engine.js +33 -10
  46. package/lib/media/transmuxer.js +6 -4
  47. package/lib/player.js +10 -6
  48. package/lib/polyfill/abort_controller.js +143 -0
  49. package/lib/polyfill/media_capabilities.js +65 -11
  50. package/lib/polyfill/patchedmediakeys_apple.js +33 -28
  51. package/lib/text/ttml_text_parser.js +32 -7
  52. package/lib/text/ui_text_displayer.js +17 -0
  53. package/lib/util/mp4_box_parsers.js +24 -0
  54. package/lib/util/mp4_parser.js +35 -0
  55. package/lib/util/player_configuration.js +10 -3
  56. package/lib/util/stream_utils.js +35 -13
  57. package/package.json +4 -4
  58. package/shaka-player.uncompiled.js +1 -0
  59. package/test/ads/ad_manager_unit.js +3 -1
  60. package/test/dash/dash_parser_manifest_unit.js +1 -0
  61. package/test/hls/hls_parser_unit.js +132 -0
  62. package/test/media/drm_engine_unit.js +11 -1
  63. package/test/media/media_source_engine_integration.js +78 -2
  64. package/test/player_integration.js +10 -3
  65. package/test/polyfill/media_capabilities_unit.js +259 -0
  66. package/test/polyfill/patchedmediakeys_apple_unit.js +6 -0
  67. package/test/test/boot.js +29 -11
  68. package/test/test/util/manifest_parser_util.js +4 -3
  69. package/test/test/util/stream_generator.js +212 -8
  70. package/test/test/util/test_scheme.js +6 -5
  71. package/test/test/util/util.js +3 -2
  72. package/test/text/mp4_ttml_parser_unit.js +6 -4
  73. package/test/text/ttml_text_parser_unit.js +119 -86
  74. package/test/text/ui_text_displayer_unit.js +76 -0
  75. package/test/util/stream_utils_unit.js +18 -0
  76. package/test/util/xml_utils_unit.js +6 -1
  77. package/third_party/closure-uri/uri.js +1 -1
  78. package/ui/controls.js +49 -36
@@ -0,0 +1,259 @@
1
+ /*! @license
2
+ * Shaka Player
3
+ * Copyright 2016 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ describe('MediaCapabilities', () => {
8
+ const Util = shaka.test.Util;
9
+ const originalCast = window['cast'];
10
+ const originalVendor = navigator.vendor;
11
+ const originalUserAgent = navigator.userAgent;
12
+ const originalRequestMediaKeySystemAccess =
13
+ navigator.requestMediaKeySystemAccess;
14
+ const originalMediaCapabilities = navigator.mediaCapabilities;
15
+ const supportMap = shaka.media.Capabilities.MediaSourceTypeSupportMap;
16
+
17
+ /** @type {MediaDecodingConfiguration} */
18
+ let mockDecodingConfig;
19
+ /** @type {!jasmine.Spy} */
20
+ let mockCanDisplayType;
21
+
22
+ beforeAll(() => {
23
+ Object.defineProperty(window['navigator'],
24
+ 'userAgent', {
25
+ value: 'unknown', configurable: true,
26
+ writable: true,
27
+ });
28
+ Object.defineProperty(window['navigator'],
29
+ 'vendor', {
30
+ value: 'unknown', configurable: true,
31
+ writable: true,
32
+ });
33
+ Object.defineProperty(window['navigator'],
34
+ 'requestMediaKeySystemAccess', {
35
+ value: 'unknown', configurable: true,
36
+ writable: true,
37
+ });
38
+ Object.defineProperty(window['navigator'],
39
+ 'mediaCapabilities', {
40
+ value: undefined, configurable: true,
41
+ writable: true,
42
+ });
43
+ });
44
+
45
+ beforeEach(() => {
46
+ mockDecodingConfig = {
47
+ audio: {
48
+ bitrate: 100891,
49
+ channels: 2,
50
+ contentType: 'audio/mp4; codecs="mp4a.40.2"',
51
+ samplerate: 48000,
52
+ spatialRendering: false,
53
+ },
54
+ keySystemConfiguration: {
55
+ audio: {robustness: 'SW_SECURE_CRYPTO'},
56
+ distinctiveIdentifier: 'optional',
57
+ initDataType: 'cenc',
58
+ keySystem: 'com.widevine.alpha',
59
+ persistentState: 'optional',
60
+ sessionTypes: ['temporary'],
61
+ video: {robustness: 'SW_SECURE_CRYPTO'},
62
+ },
63
+ type: 'media-source',
64
+ video: {
65
+ bitrate: 349265,
66
+ contentType: 'video/mp4; codecs="avc1.4D4015"',
67
+ framerate: 23.976023976023978,
68
+ height: 288,
69
+ width: 512,
70
+ },
71
+ };
72
+ shaka.polyfill.MediaCapabilities.memoizedMediaKeySystemAccessRequests_ = {};
73
+
74
+ mockCanDisplayType = jasmine.createSpy('canDisplayType');
75
+ mockCanDisplayType.and.returnValue(false);
76
+ });
77
+
78
+ afterEach(() => {
79
+ window['cast'] = originalCast;
80
+ });
81
+
82
+ afterAll(() => {
83
+ window['cast'] = originalCast;
84
+ Object.defineProperty(window['navigator'],
85
+ 'userAgent', {value: originalUserAgent});
86
+ Object.defineProperty(window['navigator'],
87
+ 'vendor', {value: originalVendor});
88
+ Object.defineProperty(window['navigator'],
89
+ 'requestMediaKeySystemAccess',
90
+ {value: originalRequestMediaKeySystemAccess});
91
+ Object.defineProperty(window['navigator'],
92
+ 'mediaCapabilities', {value: originalMediaCapabilities});
93
+ });
94
+
95
+ describe('install', () => {
96
+ it('should define decoding info method', () => {
97
+ shaka.polyfill.MediaCapabilities.install();
98
+
99
+ expect(navigator.mediaCapabilities.decodingInfo).toBeDefined();
100
+ });
101
+ });
102
+
103
+ describe('decodingInfo', () => {
104
+ it('should check codec support when MediaDecodingConfiguration.type ' +
105
+ 'is "media-source"', () => {
106
+ expect(window['MediaSource']['isTypeSupported']).toBeDefined();
107
+ shaka.polyfill.MediaCapabilities.install();
108
+ navigator.mediaCapabilities.decodingInfo(mockDecodingConfig);
109
+
110
+ expect(supportMap.has(mockDecodingConfig.video.contentType)).toBe(true);
111
+ expect(supportMap.has(mockDecodingConfig.audio.contentType)).toBe(true);
112
+ });
113
+
114
+ it('should check codec support when MediaDecodingConfiguration.type ' +
115
+ 'is "file"', () => {
116
+ const supportsMediaTypeSpy =
117
+ spyOn(shaka['util']['Platform'],
118
+ 'supportsMediaType').and.returnValue(true);
119
+ mockDecodingConfig.type = 'file';
120
+ shaka.polyfill.MediaCapabilities.install();
121
+ navigator.mediaCapabilities.decodingInfo(mockDecodingConfig);
122
+
123
+ expect(supportsMediaTypeSpy).toHaveBeenCalledTimes(2);
124
+ expect(supportsMediaTypeSpy).toHaveBeenCalledWith(
125
+ mockDecodingConfig.video.contentType,
126
+ );
127
+ expect(supportsMediaTypeSpy).toHaveBeenCalledWith(
128
+ mockDecodingConfig.audio.contentType,
129
+ );
130
+ });
131
+
132
+ it('should check MediaKeySystem when keySystemConfiguration is present',
133
+ async () => {
134
+ const mockResult = {mockKeySystemAccess: 'mockKeySystemAccess'};
135
+ spyOn(window['MediaSource'], 'isTypeSupported').and.returnValue(true);
136
+ const requestKeySystemAccessSpy =
137
+ spyOn(window['navigator'],
138
+ 'requestMediaKeySystemAccess').and.returnValue(mockResult);
139
+
140
+ shaka.polyfill.MediaCapabilities.install();
141
+ const result = await navigator.mediaCapabilities
142
+ .decodingInfo(mockDecodingConfig);
143
+
144
+ expect(requestKeySystemAccessSpy).toHaveBeenCalledWith(
145
+ 'com.widevine.alpha',
146
+ [{
147
+ audioCapabilities: [
148
+ {
149
+ robustness: 'SW_SECURE_CRYPTO',
150
+ contentType: 'audio/mp4; codecs="mp4a.40.2"',
151
+ },
152
+ ],
153
+ distinctiveIdentifier: 'optional',
154
+ initDataTypes: ['cenc'],
155
+ persistentState: 'optional',
156
+ sessionTypes: ['temporary'],
157
+ videoCapabilities: [{
158
+ robustness: 'SW_SECURE_CRYPTO',
159
+ contentType: 'video/mp4; codecs="avc1.4D4015"',
160
+ }],
161
+ }],
162
+ );
163
+ expect(result.keySystemAccess).toEqual(mockResult);
164
+ });
165
+
166
+ it('throws when the cast namespace is not available', async () => {
167
+ // Temporarily remove window.cast to trigger error. It's restored after
168
+ // every test.
169
+ delete window['cast'];
170
+
171
+ const isChromecastSpy =
172
+ spyOn(shaka['util']['Platform'],
173
+ 'isChromecast').and.returnValue(true);
174
+ const expected = Util.jasmineError(new shaka.util.Error(
175
+ shaka.util.Error.Severity.CRITICAL,
176
+ shaka.util.Error.Category.CAST,
177
+ shaka.util.Error.Code.CAST_API_UNAVAILABLE));
178
+ const isTypeSupportedSpy =
179
+ spyOn(window['MediaSource'], 'isTypeSupported').and.returnValue(true);
180
+
181
+ shaka.polyfill.MediaCapabilities.install();
182
+ await expectAsync(
183
+ navigator.mediaCapabilities.decodingInfo(mockDecodingConfig))
184
+ .toBeRejectedWith(expected);
185
+
186
+ expect(isTypeSupportedSpy).not.toHaveBeenCalled();
187
+ // 1 (during install()) + 1 (for video config check).
188
+ expect(isChromecastSpy).toHaveBeenCalledTimes(2);
189
+ });
190
+
191
+ it('falls back to isTypeSupported() when canDisplayType() missing',
192
+ async () => {
193
+ // We only set the cast namespace, but not the canDisplayType() API.
194
+ window['cast'] = {};
195
+ const isChromecastSpy =
196
+ spyOn(shaka['util']['Platform'],
197
+ 'isChromecast').and.returnValue(true);
198
+ expect(window['MediaSource']['isTypeSupported']).toBeDefined();
199
+
200
+ shaka.polyfill.MediaCapabilities.install();
201
+ await navigator.mediaCapabilities.decodingInfo(mockDecodingConfig);
202
+
203
+ expect(mockCanDisplayType).not.toHaveBeenCalled();
204
+ // 1 (during install()) + 1 (for video config check).
205
+ expect(isChromecastSpy).toHaveBeenCalledTimes(2);
206
+ // 1 (fallback in canCastDisplayType()) +
207
+ // 1 (mockDecodingConfig.audio).
208
+ expect(supportMap.has(mockDecodingConfig.video.contentType))
209
+ .toBe(true);
210
+ expect(supportMap.has(mockDecodingConfig.audio.contentType))
211
+ .toBe(true);
212
+ });
213
+
214
+ it('should use cast.__platform__.canDisplayType for "supported" field ' +
215
+ 'when platform is Cast', async () => {
216
+ // We're using quotes to access window.cast because the compiler
217
+ // knows about lots of Cast-specific APIs we aren't mocking. We
218
+ // don't need this mock strictly type-checked.
219
+ window['cast'] = {
220
+ __platform__: {canDisplayType: mockCanDisplayType},
221
+ };
222
+ const isChromecastSpy =
223
+ spyOn(shaka['util']['Platform'],
224
+ 'isChromecast').and.returnValue(true);
225
+ expect(window['MediaSource']['isTypeSupported']).toBeDefined();
226
+
227
+ // Tests an HDR stream's extended MIME type is correctly provided.
228
+ mockDecodingConfig.video.transferFunction = 'pq';
229
+ mockDecodingConfig.video.contentType =
230
+ 'video/mp4; codecs="hev1.2.4.L153.B0"';
231
+ // Round to a whole number since we can't rely on number => string
232
+ // conversion precision on all devices.
233
+ mockDecodingConfig.video.framerate = 24;
234
+
235
+ const chromecastType =
236
+ 'video/mp4; ' +
237
+ 'codecs="hev1.2.4.L153.B0"; ' +
238
+ 'width=512; ' +
239
+ 'height=288; ' +
240
+ 'framerate=24; ' +
241
+ 'eotf=smpte2084';
242
+ mockCanDisplayType.and.callFake((type) => {
243
+ expect(type).toBe(chromecastType);
244
+ supportMap.set(type, true);
245
+ return true;
246
+ });
247
+
248
+ shaka.polyfill.MediaCapabilities.install();
249
+ await navigator.mediaCapabilities.decodingInfo(mockDecodingConfig);
250
+
251
+ // 1 (during install()) + 1 (for video config check).
252
+ expect(isChromecastSpy).toHaveBeenCalledTimes(2);
253
+ // 1 (mockDecodingConfig.audio).
254
+ expect(supportMap.has(chromecastType)).toBe(true);
255
+ // Called once in canCastDisplayType.
256
+ expect(mockCanDisplayType).toHaveBeenCalledTimes(1);
257
+ });
258
+ });
259
+ });
@@ -1,3 +1,9 @@
1
+ /*! @license
2
+ * Shaka Player
3
+ * Copyright 2016 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
1
7
  describe('PatchedMediaKeys_Apple', () => {
2
8
  const PatchedMediaKeysApple = shaka.polyfill.PatchedMediaKeysApple;
3
9
  let originalMediaKeys;
package/test/test/boot.js CHANGED
@@ -66,22 +66,40 @@ function failTestsOnNamespacedElementOrAttributeNames() {
66
66
  }
67
67
 
68
68
  /**
69
- * Listen for unhandled Promise rejections (which may occur after a test) and
70
- * convert them into test failures.
69
+ * Fail the current test on a given error, constructed with a given header and
70
+ * the full stack trace of the error.
71
+ *
72
+ * @param {string} messageHeader
73
+ * @param {!Error} error
71
74
  */
72
- function failTestsOnUnhandledRejections() {
75
+ function failOnError(messageHeader, error) {
76
+ let message = `${messageHeader}: ${error}`;
77
+ // Shaka errors have the stack trace in their toString() already, so don't
78
+ // add it again. For native errors, we need to see where it came from.
79
+ if (error && error.stack && !(error instanceof shaka.util.Error)) {
80
+ message += '\n' + error.stack;
81
+ }
82
+ fail(message);
83
+ }
84
+
85
+ /**
86
+ * Listen for unhandled errors and Promise rejections (which may occur after a
87
+ * test) and convert them into test failures.
88
+ */
89
+ function failTestsOnUnhandledErrors() {
73
90
  // https://developer.mozilla.org/en-US/docs/Web/Events/unhandledrejection
74
91
  window.addEventListener('unhandledrejection', (event) => {
75
92
  /** @type {?} */
76
93
  const error = event.reason;
77
- let message = 'Unhandled rejection in Promise: ' + error;
94
+ failOnError('Unhandled rejection in Promise', error);
95
+ });
78
96
 
79
- // Shaka errors have the stack trace in their toString() already, so don't
80
- // add it again. For native errors, we need to see where it came from.
81
- if (error && error.stack && !(error instanceof shaka.util.Error)) {
82
- message += '\n' + error.stack;
83
- }
84
- fail(message);
97
+ // https://developer.mozilla.org/en-US/docs/Web/API/Window/error_event
98
+ // https://developer.mozilla.org/en-US/docs/Web/API/ErrorEvent
99
+ window.addEventListener('error', (event) => {
100
+ /** @type {?} */
101
+ const error = event['error'];
102
+ failOnError('Unhandled error', error);
85
103
  });
86
104
  }
87
105
 
@@ -387,7 +405,7 @@ function configureJasmineEnvironment() {
387
405
  function setupTestEnvironment() {
388
406
  failTestsOnFailedAssertions();
389
407
  failTestsOnNamespacedElementOrAttributeNames();
390
- failTestsOnUnhandledRejections();
408
+ failTestsOnUnhandledErrors();
391
409
  disableScrollbars();
392
410
  workAroundLegacyEdgePromiseIssues();
393
411
 
@@ -73,7 +73,7 @@ shaka.test.ManifestParser = class {
73
73
  const appendWindowStart = /** @type {?} */(jasmine.any(Number));
74
74
  const appendWindowEnd = /** @type {?} */(jasmine.any(Number));
75
75
 
76
- return new shaka.media.SegmentReference(
76
+ const ref = new shaka.media.SegmentReference(
77
77
  start, end, getUris, startByte, endByte,
78
78
  initSegmentReference,
79
79
  timestampOffset,
@@ -82,7 +82,8 @@ shaka.test.ManifestParser = class {
82
82
  partialReferences,
83
83
  tilesLayout,
84
84
  /* tileDuration= */ undefined,
85
- syncTime,
86
- );
85
+ syncTime);
86
+ ref.discontinuitySequence = /** @type {?} */(jasmine.any(Number));
87
+ return ref;
87
88
  }
88
89
  };
@@ -51,19 +51,36 @@ shaka.test.IStreamGenerator = class {
51
51
  * @implements {shaka.test.IStreamGenerator}
52
52
  */
53
53
  shaka.test.TSVodStreamGenerator = class {
54
- /** @param {string} segmentUri The URI of the segment. */
55
- constructor(segmentUri) {
54
+ /**
55
+ * @param {string} segmentUri The URI of the segment.
56
+ * @param {number} segmentDuration The duration of a single segment, in
57
+ * seconds.
58
+ */
59
+ constructor(segmentUri, segmentDuration) {
56
60
  /** @private {string} */
57
61
  this.segmentUri_ = segmentUri;
58
62
 
63
+ /**
64
+ * Internally, everything is in timescale units.
65
+ * @private {number}
66
+ */
67
+ this.segmentDuration_ = segmentDuration * 90000;
68
+
69
+ /** @private {!Array.<{offset: number, dts: ?number, pts: ?number}>} */
70
+ this.timestamps_ = [];
71
+
72
+ /** @private {number} */
73
+ this.timestampOffset_ = Infinity;
74
+
59
75
  /** @private {ArrayBuffer} */
60
- this.segment_ = null;
76
+ this.segmentTemplate_ = null;
61
77
  }
62
78
 
63
79
  /** @override */
64
80
  async init() {
65
81
  const segment = await shaka.test.Util.fetch(this.segmentUri_);
66
- this.segment_ = segment;
82
+ this.segmentTemplate_ = segment;
83
+ this.parseSegment_();
67
84
  }
68
85
 
69
86
  /** @override */
@@ -75,11 +92,198 @@ shaka.test.TSVodStreamGenerator = class {
75
92
  /** @override */
76
93
  getSegment(position, wallClockTime) {
77
94
  goog.asserts.assert(
78
- this.segment_,
95
+ this.segmentTemplate_,
79
96
  'init() must be called before getSegment().');
80
- // TODO: complete implementation; this should change the timestamps based on
81
- // the given wallClockTime, so as to simulate a long stream.
82
- return this.segment_;
97
+
98
+ // This will create a copy of the given buffer.
99
+ const buffer = shaka.util.Uint8ArrayUtils.concat(this.segmentTemplate_);
100
+
101
+ for (const timestampMetadata of this.timestamps_) {
102
+ this.setTimestamp_(buffer, timestampMetadata, position);
103
+ }
104
+
105
+ return shaka.util.BufferUtils.toArrayBuffer(buffer);
106
+ }
107
+
108
+ /** @private */
109
+ parseSegment_() {
110
+ goog.asserts.assert(
111
+ this.segmentTemplate_,
112
+ 'init() must be called before parseSegment_().');
113
+
114
+ // A TS segment can contain a timestamp in each 188-byte PES packet.
115
+ // Find all the timestamps and their offsets, and cache them.
116
+ const dataView = shaka.util.BufferUtils.toDataView(this.segmentTemplate_);
117
+ const reader = new shaka.util.DataViewReader(
118
+ dataView, shaka.util.DataViewReader.Endianness.BIG_ENDIAN);
119
+
120
+ // Read each TS packet (188 bytes).
121
+ for (let i = 0; i < reader.getLength(); i += 188) {
122
+ reader.seek(i);
123
+ const syncByte = reader.readUint8();
124
+ goog.asserts.assert(syncByte == 0x47, 'Sync byte not found!');
125
+
126
+ const flagsAndPacketId = reader.readUint16();
127
+ const packetId = flagsAndPacketId & 0x1fff;
128
+ if (packetId == 0x1fff) {
129
+ // A "null" TS packet. Skip it.
130
+ continue;
131
+ }
132
+
133
+ const hasPesPacket = flagsAndPacketId & 0x4000;
134
+ if (!hasPesPacket) {
135
+ // Not a PES packet. Skip it.
136
+ continue;
137
+ }
138
+
139
+ const flags = reader.readUint8();
140
+ const adaptationFieldControl = (flags & 0x30) >> 4;
141
+ if (adaptationFieldControl == 0 /* reserved */ ||
142
+ adaptationFieldControl == 2 /* adaptation field, no payload */) {
143
+ throw new Error(
144
+ `Unexpected adaptation field control: ${adaptationFieldControl}`);
145
+ }
146
+
147
+ if (adaptationFieldControl == 3) {
148
+ // Skip over adaptation field.
149
+ const length = reader.readUint8();
150
+ reader.skip(length);
151
+ }
152
+
153
+ // Now we come to the PES header (hopefully).
154
+ // Format reference: https://bit.ly/TsPES
155
+ const startCode = reader.readUint32();
156
+ const startCodePrefix = startCode >> 8;
157
+ if (startCodePrefix != 1) {
158
+ // Not a PES packet. Skip it.
159
+ continue;
160
+ }
161
+
162
+ // Skip the 16-bit PES length and the first 8 bits of the optional header.
163
+ reader.skip(3);
164
+ // The next 8 bits contain flags about DTS & PTS.
165
+ const ptsDtsIndicator = reader.readUint8() >> 6;
166
+ if (ptsDtsIndicator == 0 /* no timestamp */ ||
167
+ ptsDtsIndicator == 1 /* forbidden */) {
168
+ throw new Error(`Unexpected PTS/DTS flag: ${ptsDtsIndicator}`);
169
+ }
170
+
171
+ const pesHeaderLengthRemaining = reader.readUint8();
172
+ if (pesHeaderLengthRemaining == 0) {
173
+ throw new Error(`Malformed TS, no room for PTS/DTS!`);
174
+ }
175
+
176
+ const offset = reader.getPosition();
177
+ let pts = null;
178
+ let dts = null;
179
+
180
+ // Parse timestamps and keep track of the minimum timestamp seen, to use
181
+ // as a timestamp offset when we calculate new timestamps for a segment.
182
+ if (ptsDtsIndicator == 2 /* PTS only */) {
183
+ goog.asserts.assert(pesHeaderLengthRemaining == 5, 'Bad PES header?');
184
+ pts = this.parseTimestamp_(reader);
185
+ this.timestampOffset_ = Math.min(this.timestampOffset_, pts);
186
+ } else if (ptsDtsIndicator == 3 /* PTS and DTS */) {
187
+ goog.asserts.assert(pesHeaderLengthRemaining == 10, 'Bad PES header?');
188
+ pts = this.parseTimestamp_(reader);
189
+ dts = this.parseTimestamp_(reader);
190
+ this.timestampOffset_ = Math.min(this.timestampOffset_, pts, dts);
191
+ }
192
+
193
+ this.timestamps_.push({
194
+ offset,
195
+ pts,
196
+ dts,
197
+ });
198
+ }
199
+ }
200
+
201
+ /**
202
+ * @param {!shaka.util.DataViewReader} reader
203
+ * @return {number}
204
+ * @private
205
+ */
206
+ parseTimestamp_(reader) {
207
+ const pts0 = reader.readUint8();
208
+ const pts1 = reader.readUint16();
209
+ const pts2 = reader.readUint16();
210
+ // Reconstruct 33-bit PTS from the 5-byte, padded structure.
211
+ const ptsHigh3 = (pts0 & 0x0e) >> 1;
212
+ const ptsLow30 = ((pts1 & 0xfffe) << 14) | ((pts2 & 0xfffe) >> 1);
213
+ // Reconstruct the PTS as a float. Avoid bitwise operations to combine
214
+ // because bitwise ops treat the values as 32-bit ints.
215
+ return ptsHigh3 * (1 << 30) + ptsLow30;
216
+ }
217
+
218
+ /**
219
+ * @param {!Uint8Array} buffer
220
+ * @param {{offset: number, pts: ?number, dts: ?number}} timestampMetadata
221
+ * @param {number} position
222
+ * @private
223
+ */
224
+ setTimestamp_(buffer, timestampMetadata, position) {
225
+ // Wikipedia: "If only PTS is present, this is done by catenating 0010 ...
226
+ // If both PTS and DTS are present, first 4 bits are 0011 and first 4 bits
227
+ // for DTS are 0001."
228
+ const ptsHeader = timestampMetadata.dts == null ? 0b0010 : 0b0011;
229
+ const dtsHeader = 0b0001;
230
+
231
+ const segmentTime = this.segmentDuration_ * position;
232
+
233
+ if (timestampMetadata.pts != null) {
234
+ const pts = timestampMetadata.pts - this.timestampOffset_ + segmentTime;
235
+ this.writeTimestamp_(
236
+ buffer, timestampMetadata.offset, ptsHeader, this.overflow_(pts));
237
+ }
238
+ if (timestampMetadata.dts != null) {
239
+ const dts = timestampMetadata.dts - this.timestampOffset_ + segmentTime;
240
+ this.writeTimestamp_(
241
+ buffer, timestampMetadata.offset + 5, dtsHeader, this.overflow_(dts));
242
+ }
243
+ }
244
+
245
+ /**
246
+ * Write a timestamp (PTS or DTS) to a specific place in the buffer, with a
247
+ * specific header, in the PES timestamp layout.
248
+ *
249
+ * @param {!Uint8Array} buffer
250
+ * @param {number} offset Where to begin writing
251
+ * @param {number} header 4-bit header for the timestamp
252
+ * @param {number} timestamp the actual timestamp
253
+ * @private
254
+ */
255
+ writeTimestamp_(buffer, offset, header, timestamp) {
256
+ // The 33 bit timestamp is split into parts, then packed into 40 bits
257
+ // (5 bytes). Wikipedia phrases the layout as: "0010b, most significant 3
258
+ // bits from PTS, 1, following next 15 bits, 1, rest 15 bits and 1."
259
+ // https://en.wikipedia.org/wiki/Packetized_elementary_stream
260
+ const top3Bits = timestamp >> 30;
261
+ const next15Bits = (timestamp >> 15) & 0x7fff;
262
+ const last15Bits = timestamp & 0x7fff;
263
+
264
+ buffer[offset + 0] = (header << 4) | (top3Bits << 1) | 1;
265
+ buffer[offset + 1] = next15Bits >> 7;
266
+ buffer[offset + 2] = ((next15Bits & 0x7f) << 1) | 1;
267
+ buffer[offset + 3] = last15Bits >> 7;
268
+ buffer[offset + 4] = ((last15Bits & 0x7f) << 1) | 1;
269
+ }
270
+
271
+ /**
272
+ * Handle PES timestamp overflow (33 bits).
273
+ *
274
+ * @param {number} timestamp
275
+ * @return {number} The same timestamp, with TS overflow applied.
276
+ * @private
277
+ */
278
+ overflow_(timestamp) {
279
+ // NOTE: You can't get 2^33 with a bit-shift, because JavaScript will treat
280
+ // the number as a 32-bit int. Use Math.pow() instead. The result is
281
+ // still accurate as an integer.
282
+ const limit = Math.pow(2, 33);
283
+ while (timestamp >= limit) {
284
+ timestamp -= limit;
285
+ }
286
+ return timestamp;
83
287
  }
84
288
  };
85
289
 
@@ -173,7 +173,8 @@ shaka.test.TestScheme = class {
173
173
  */
174
174
  function createStreamGenerator(metadata) {
175
175
  if (metadata.segmentUri.includes('.ts')) {
176
- return new shaka.test.TSVodStreamGenerator(metadata.segmentUri);
176
+ return new shaka.test.TSVodStreamGenerator(
177
+ metadata.segmentUri, metadata.segmentDuration);
177
178
  }
178
179
  if (metadata.segmentUri.includes('.aac')) {
179
180
  return new shaka.test.AACVodStreamGenerator(metadata.segmentUri);
@@ -412,7 +413,7 @@ const sintelAudioSegment = {
412
413
  mdhdOffset: 0x1b6,
413
414
  segmentUri: '/base/test/test/assets/sintel-audio-segment.mp4',
414
415
  tfdtOffset: 0x3c,
415
- segmentDuration: 10.005,
416
+ segmentDuration: 10,
416
417
  mimeType: 'audio/mp4',
417
418
  codecs: 'mp4a.40.2',
418
419
  };
@@ -438,7 +439,7 @@ const sintelEncryptedAudio = {
438
439
  mdhdOffset: 0x1b6,
439
440
  segmentUri: '/base/test/test/assets/encrypted-sintel-audio-segment.mp4',
440
441
  tfdtOffset: 0x3c,
441
- segmentDuration: 10.005,
442
+ segmentDuration: 10,
442
443
  mimeType: 'audio/mp4',
443
444
  codecs: 'mp4a.40.2',
444
445
  initData:
@@ -634,7 +635,7 @@ shaka.test.TestScheme.DATA = {
634
635
  segmentUri: '/base/test/test/assets/captions-test.ts',
635
636
  mimeType: 'video/mp2t',
636
637
  codecs: 'avc1.64001e',
637
- segmentDuration: 2,
638
+ segmentDuration: 20, // yes, this is accurate
638
639
  },
639
640
  text: {
640
641
  mimeType: 'application/cea-608',
@@ -661,7 +662,7 @@ shaka.test.TestScheme.DATA = {
661
662
  segmentUri: '/base/test/test/assets/id3-metadata.ts',
662
663
  mimeType: 'video/mp2t',
663
664
  codecs: 'mp4a.40.5',
664
- segmentDuration: 4.99,
665
+ segmentDuration: 5,
665
666
  },
666
667
  duration: 4.99,
667
668
  },
@@ -89,10 +89,11 @@ shaka.test.Util = class {
89
89
  * given value.
90
90
  *
91
91
  * @param {number} val
92
+ * @param {number=} maxDelta
92
93
  * @return {number}
93
94
  */
94
- static closeTo(val) {
95
- const E = 0.000001;
95
+ static closeTo(val, maxDelta = 0.000001) {
96
+ const E = /** @type {number} */(maxDelta);
96
97
  return /** @type {number} */(/** @type {?} */({
97
98
  asymmetricMatch: (other) => other >= val - E && other <= val + E,
98
99
  jasmineToString: () => '<closeTo: ' + val + '>',
@@ -41,7 +41,8 @@ describe('Mp4TtmlParser', () => {
41
41
  it('handles media segments with multiple mdats', () => {
42
42
  const parser = new shaka.text.Mp4TtmlParser();
43
43
  parser.parseInit(ttmlInitSegment);
44
- const time = {periodStart: 0, segmentStart: 0, segmentEnd: 0, vttOffset: 0};
44
+ const time =
45
+ {periodStart: 0, segmentStart: 0, segmentEnd: 60, vttOffset: 0};
45
46
  const ret = parser.parseMedia(ttmlSegmentMultipleMDAT, time);
46
47
  // Bodies.
47
48
  expect(ret.length).toBe(2);
@@ -55,9 +56,9 @@ describe('Mp4TtmlParser', () => {
55
56
 
56
57
  it('accounts for offset', () => {
57
58
  const time1 =
58
- {periodStart: 0, segmentStart: 0, segmentEnd: 0, vttOffset: 0};
59
+ {periodStart: 0, segmentStart: 0, segmentEnd: 70, vttOffset: 0};
59
60
  const time2 =
60
- {periodStart: 7, segmentStart: 0, segmentEnd: 0, vttOffset: 7};
61
+ {periodStart: 7, segmentStart: 0, segmentEnd: 70, vttOffset: 7};
61
62
 
62
63
  const parser = new shaka.text.Mp4TtmlParser();
63
64
  parser.parseInit(ttmlInitSegment);
@@ -161,7 +162,8 @@ describe('Mp4TtmlParser', () => {
161
162
  ];
162
163
  const parser = new shaka.text.Mp4TtmlParser();
163
164
  parser.parseInit(ttmlInitSegment);
164
- const time = {periodStart: 0, segmentStart: 0, segmentEnd: 0, vttOffset: 0};
165
+ const time =
166
+ {periodStart: 0, segmentStart: 0, segmentEnd: 60, vttOffset: 0};
165
167
  const result = parser.parseMedia(ttmlSegment, time);
166
168
  shaka.test.TtmlUtils.verifyHelper(
167
169
  cues, result, {startTime: 23, endTime: 53.5});