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.
- package/.github/workflows/build-and-test.yaml +20 -0
- package/AUTHORS +2 -0
- package/CHANGELOG.md +44 -0
- package/CONTRIBUTORS +2 -0
- package/README.md +1 -1
- package/conditional/dummy_cast_proxy.js +120 -0
- package/dist/demo.compiled.debug.js +104 -110
- package/dist/demo.compiled.debug.map +3 -3
- package/dist/demo.compiled.js +104 -110
- package/dist/demo.compiled.map +3 -3
- package/dist/deps.js +8 -6
- package/dist/locales.js +32 -32
- package/dist/receiver.compiled.debug.js +54 -60
- package/dist/receiver.compiled.debug.map +3 -3
- package/dist/receiver.compiled.js +54 -60
- package/dist/receiver.compiled.map +3 -3
- package/dist/shaka-player.compiled.d.ts +19 -2
- package/dist/shaka-player.compiled.debug.d.ts +19 -2
- package/dist/shaka-player.compiled.debug.externs.js +102 -80
- package/dist/shaka-player.compiled.debug.js +657 -652
- package/dist/shaka-player.compiled.debug.map +4 -4
- package/dist/shaka-player.compiled.externs.js +102 -80
- package/dist/shaka-player.compiled.js +904 -901
- package/dist/shaka-player.compiled.map +4 -4
- package/dist/shaka-player.ui.d.ts +19 -2
- package/dist/shaka-player.ui.debug.d.ts +19 -2
- package/dist/shaka-player.ui.debug.externs.js +102 -80
- package/dist/shaka-player.ui.debug.js +703 -698
- package/dist/shaka-player.ui.debug.map +4 -4
- package/dist/shaka-player.ui.externs.js +102 -80
- package/dist/shaka-player.ui.js +1029 -1026
- package/dist/shaka-player.ui.map +4 -4
- package/docs/tutorials/welcome.md +2 -2
- package/externs/shaka/mp4_parser.js +3 -0
- package/externs/shaka/player.js +2 -1
- package/lib/ads/ad_manager.js +2 -2
- package/lib/cea/cea708_window.js +2 -2
- package/lib/cea/mp4_cea_parser.js +37 -16
- package/lib/dash/segment_template.js +1 -1
- package/lib/hls/hls_parser.js +49 -23
- package/lib/media/content_workarounds.js +12 -0
- package/lib/media/media_source_capabilities.js +36 -0
- package/lib/media/media_source_engine.js +28 -7
- package/lib/media/segment_reference.js +3 -0
- package/lib/media/streaming_engine.js +33 -10
- package/lib/media/transmuxer.js +6 -4
- package/lib/player.js +10 -6
- package/lib/polyfill/abort_controller.js +143 -0
- package/lib/polyfill/media_capabilities.js +65 -11
- package/lib/polyfill/patchedmediakeys_apple.js +33 -28
- package/lib/text/ttml_text_parser.js +32 -7
- package/lib/text/ui_text_displayer.js +17 -0
- package/lib/util/mp4_box_parsers.js +24 -0
- package/lib/util/mp4_parser.js +35 -0
- package/lib/util/player_configuration.js +10 -3
- package/lib/util/stream_utils.js +35 -13
- package/package.json +4 -4
- package/shaka-player.uncompiled.js +1 -0
- package/test/ads/ad_manager_unit.js +3 -1
- package/test/dash/dash_parser_manifest_unit.js +1 -0
- package/test/hls/hls_parser_unit.js +132 -0
- package/test/media/drm_engine_unit.js +11 -1
- package/test/media/media_source_engine_integration.js +78 -2
- package/test/player_integration.js +10 -3
- package/test/polyfill/media_capabilities_unit.js +259 -0
- package/test/polyfill/patchedmediakeys_apple_unit.js +6 -0
- package/test/test/boot.js +29 -11
- package/test/test/util/manifest_parser_util.js +4 -3
- package/test/test/util/stream_generator.js +212 -8
- package/test/test/util/test_scheme.js +6 -5
- package/test/test/util/util.js +3 -2
- package/test/text/mp4_ttml_parser_unit.js +6 -4
- package/test/text/ttml_text_parser_unit.js +119 -86
- package/test/text/ui_text_displayer_unit.js +76 -0
- package/test/util/stream_utils_unit.js +18 -0
- package/test/util/xml_utils_unit.js +6 -1
- package/third_party/closure-uri/uri.js +1 -1
- 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
|
+
});
|
package/test/test/boot.js
CHANGED
|
@@ -66,22 +66,40 @@ function failTestsOnNamespacedElementOrAttributeNames() {
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
/**
|
|
69
|
-
*
|
|
70
|
-
*
|
|
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
|
|
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
|
-
|
|
94
|
+
failOnError('Unhandled rejection in Promise', error);
|
|
95
|
+
});
|
|
78
96
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
55
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
95
|
+
this.segmentTemplate_,
|
|
79
96
|
'init() must be called before getSegment().');
|
|
80
|
-
|
|
81
|
-
//
|
|
82
|
-
|
|
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(
|
|
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
|
|
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
|
|
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:
|
|
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:
|
|
665
|
+
segmentDuration: 5,
|
|
665
666
|
},
|
|
666
667
|
duration: 4.99,
|
|
667
668
|
},
|
package/test/test/util/util.js
CHANGED
|
@@ -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 =
|
|
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 =
|
|
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:
|
|
59
|
+
{periodStart: 0, segmentStart: 0, segmentEnd: 70, vttOffset: 0};
|
|
59
60
|
const time2 =
|
|
60
|
-
{periodStart: 7, segmentStart: 0, segmentEnd:
|
|
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 =
|
|
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});
|