shaka-player 4.3.6 → 4.3.8
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/appspot.yaml +39 -0
- package/.github/workflows/demo-version-index.yaml +3 -3
- package/CHANGELOG.md +46 -0
- package/CONTRIBUTORS +1 -0
- package/README.md +3 -0
- package/demo/custom.js +1 -1
- package/demo/demo.less +1 -0
- package/dist/demo.compiled.debug.js +2 -2
- package/dist/demo.compiled.debug.map +2 -2
- package/dist/demo.compiled.js +2 -2
- package/dist/demo.compiled.map +2 -2
- package/dist/demo.css +1 -1
- package/dist/demo.css.map +1 -1
- package/dist/deps.js +1 -1
- package/dist/locales.js +33 -33
- package/dist/shaka-player.compiled.d.ts +30 -0
- package/dist/shaka-player.compiled.debug.d.ts +30 -0
- package/dist/shaka-player.compiled.debug.externs.js +9 -0
- package/dist/shaka-player.compiled.debug.js +263 -256
- package/dist/shaka-player.compiled.debug.map +3 -3
- package/dist/shaka-player.compiled.externs.js +9 -0
- package/dist/shaka-player.compiled.js +901 -895
- package/dist/shaka-player.compiled.map +4 -4
- package/dist/shaka-player.ui.d.ts +30 -0
- package/dist/shaka-player.ui.debug.d.ts +30 -0
- package/dist/shaka-player.ui.debug.externs.js +9 -0
- package/dist/shaka-player.ui.debug.js +268 -262
- package/dist/shaka-player.ui.debug.map +3 -3
- package/dist/shaka-player.ui.externs.js +9 -0
- package/dist/shaka-player.ui.js +926 -920
- package/dist/shaka-player.ui.map +4 -4
- package/docs/tutorials/drm-config.md +9 -6
- package/docs/tutorials/welcome.md +1 -1
- package/externs/ima.js +34 -0
- package/lib/abr/simple_abr_manager.js +1 -1
- package/lib/cea/mp4_cea_parser.js +29 -20
- package/lib/dash/dash_parser.js +18 -3
- package/lib/hls/hls_parser.js +66 -6
- package/lib/hls/manifest_text_parser.js +10 -1
- package/lib/media/drm_engine.js +23 -14
- package/lib/media/gap_jumping_controller.js +24 -2
- package/lib/media/media_source_engine.js +10 -6
- package/lib/media/playhead.js +11 -0
- package/lib/media/region_observer.js +19 -1
- package/lib/media/segment_reference.js +22 -0
- package/lib/media/streaming_engine.js +48 -23
- package/lib/media/video_wrapper.js +6 -1
- package/lib/player.js +16 -5
- package/lib/polyfill/abort_controller.js +7 -0
- package/lib/text/vtt_text_parser.js +2 -1
- package/lib/util/manifest_parser_utils.js +1 -1
- package/lib/util/stream_utils.js +1 -0
- package/package.json +9 -4
- package/test/cea/mp4_cea_parser_unit.js +45 -3
- package/test/dash/dash_parser_manifest_unit.js +120 -0
- package/test/hls/hls_live_unit.js +3 -1
- package/test/hls/hls_parser_unit.js +144 -0
- package/test/media/media_source_engine_unit.js +30 -0
- package/test/media/region_observer_unit.js +2 -1
- package/test/media/streaming_engine_unit.js +26 -2
- package/test/test/assets/h265-cea-init.mp4 +0 -0
- package/test/test/assets/h265-cea-segment.mp4 +0 -0
- package/test/test/assets/multiple-trun-init.mp4 +0 -0
- package/test/test/assets/multiple-trun-segment.mp4 +0 -0
- package/test/test/util/layout_tests.js +4 -3
- package/ui/resolution_selection.js +7 -1
|
@@ -59,15 +59,18 @@ If this is the only `<ContentProtection>` element in the manifest, Shaka will
|
|
|
59
59
|
try all key systems it knows. (Based on keySystemsByURI in
|
|
60
60
|
{@linksource shaka.extern.DashManifestConfiguration}.)
|
|
61
61
|
|
|
62
|
-
Through `player.configure()`, you can
|
|
62
|
+
Through `player.configure()`, you can update the dash key systems mapping by
|
|
63
63
|
scheme URI:
|
|
64
|
+
|
|
64
65
|
```js
|
|
65
66
|
player.configure({
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
67
|
+
manifest: {
|
|
68
|
+
dash: {
|
|
69
|
+
keySystemsByURI: {
|
|
70
|
+
'urn:uuid:9a04f079-9840-4286-ab92-e65be0885f95': 'com.microsoft.playready.recommendation',
|
|
71
|
+
'urn:uuid:79f0049a-4098-8642-ab92-e65be0885f95': 'com.microsoft.playready.recommendation',
|
|
72
|
+
}
|
|
73
|
+
}
|
|
71
74
|
}
|
|
72
75
|
});
|
|
73
76
|
```
|
|
@@ -29,7 +29,7 @@ You can build Shaka on Linux, Windows, or Mac.
|
|
|
29
29
|
To get the sources and compile the library, you will need:
|
|
30
30
|
* {@link https://git-scm.com/downloads Git v1.9+}
|
|
31
31
|
* {@link https://www.python.org/downloads/ Python v2.7 or v3.5+}
|
|
32
|
-
* {@link https://
|
|
32
|
+
* {@link https://learn.microsoft.com/en-us/java/openjdk/download Java Runtime Environment v14+}
|
|
33
33
|
* {@link https://nodejs.org/en/download/ NodeJS v14+}
|
|
34
34
|
* A local web server, such as {@link https://httpd.apache.org/ Apache}
|
|
35
35
|
* _NOTE: A local web server is required because browsers place restrictions
|
package/externs/ima.js
CHANGED
|
@@ -513,6 +513,14 @@ google.ima.dai.api.StreamRequest.prototype.streamActivityMonitorId;
|
|
|
513
513
|
google.ima.dai.api.StreamRequest.prototype.format;
|
|
514
514
|
|
|
515
515
|
|
|
516
|
+
/**
|
|
517
|
+
* @type {Object.<
|
|
518
|
+
* google.ima.dai.api.OmidVerificationVendor,
|
|
519
|
+
* (google.ima.dai.api.OmidAccessMode | undefined)>}
|
|
520
|
+
*/
|
|
521
|
+
google.ima.dai.api.StreamRequest.prototype.omidAccessModeRules;
|
|
522
|
+
|
|
523
|
+
|
|
516
524
|
/**
|
|
517
525
|
* @enum {string}
|
|
518
526
|
*/
|
|
@@ -599,3 +607,29 @@ google.ima.dai.api.StreamEvent.Type = {
|
|
|
599
607
|
SKIPPED: 'skip',
|
|
600
608
|
VIDEO_CLICKED: 'videoClicked',
|
|
601
609
|
};
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
/**
|
|
613
|
+
* @enum {number}
|
|
614
|
+
*/
|
|
615
|
+
google.ima.dai.api.OmidVerificationVendor = {
|
|
616
|
+
OTHER: 1,
|
|
617
|
+
MOAT: 2,
|
|
618
|
+
DOUBLEVERIFY: 3,
|
|
619
|
+
INTEGRAL_AD_SCIENCE: 4,
|
|
620
|
+
PIXELATE: 5,
|
|
621
|
+
NIELSEN: 6,
|
|
622
|
+
COMSCORE: 7,
|
|
623
|
+
MEETRICS: 8,
|
|
624
|
+
GOOGLE: 9,
|
|
625
|
+
};
|
|
626
|
+
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* @enum {string}
|
|
630
|
+
*/
|
|
631
|
+
google.ima.dai.api.OmidAccessMode = {
|
|
632
|
+
FULL: 'full',
|
|
633
|
+
DOMAIN: 'domain',
|
|
634
|
+
LIMITED: 'limited',
|
|
635
|
+
};
|
|
@@ -54,7 +54,7 @@ shaka.abr.SimpleAbrManager = class {
|
|
|
54
54
|
// of connectivity changes.
|
|
55
55
|
if (navigator.connection && navigator.connection.addEventListener) {
|
|
56
56
|
this.onNetworkInformationChange_ = () => {
|
|
57
|
-
if (this.
|
|
57
|
+
if (this.enabled_ && this.config_.useNetworkInformation) {
|
|
58
58
|
this.bandwidthEstimator_ = new shaka.abr.EwmaBandwidthEstimator();
|
|
59
59
|
if (this.config_) {
|
|
60
60
|
this.bandwidthEstimator_.configure(this.config_.advanced);
|
|
@@ -163,15 +163,17 @@ shaka.cea.Mp4CeaParser = class {
|
|
|
163
163
|
// Fields that are found in MOOF boxes
|
|
164
164
|
let defaultSampleDuration = this.defaultSampleDuration_;
|
|
165
165
|
let defaultSampleSize = this.defaultSampleSize_;
|
|
166
|
-
let
|
|
167
|
-
|
|
168
|
-
let
|
|
166
|
+
let moofOffset = 0;
|
|
167
|
+
/** @type {!Array<shaka.util.ParsedTRUNBox>} */
|
|
168
|
+
let parsedTRUNs = [];
|
|
169
169
|
let baseMediaDecodeTime = null;
|
|
170
170
|
let timescale = shaka.cea.ICeaParser.DEFAULT_TIMESCALE_VALUE;
|
|
171
171
|
|
|
172
172
|
new Mp4Parser()
|
|
173
173
|
.box('moof', (box) => {
|
|
174
174
|
moofOffset = box.start;
|
|
175
|
+
// trun box parsing is reset on each moof.
|
|
176
|
+
parsedTRUNs = [];
|
|
175
177
|
Mp4Parser.children(box);
|
|
176
178
|
})
|
|
177
179
|
.box('traf', Mp4Parser.children)
|
|
@@ -182,11 +184,8 @@ shaka.cea.Mp4CeaParser = class {
|
|
|
182
184
|
|
|
183
185
|
const parsedTRUN = shaka.util.Mp4BoxParsers.parseTRUN(
|
|
184
186
|
box.reader, box.version, box.flags);
|
|
185
|
-
|
|
186
|
-
sampleData = parsedTRUN.sampleData;
|
|
187
|
-
trunOffset = parsedTRUN.dataOffset;
|
|
187
|
+
parsedTRUNs.push(parsedTRUN);
|
|
188
188
|
})
|
|
189
|
-
|
|
190
189
|
.fullBox('tfhd', (box) => {
|
|
191
190
|
goog.asserts.assert(
|
|
192
191
|
box.flags != null,
|
|
@@ -210,7 +209,6 @@ shaka.cea.Mp4CeaParser = class {
|
|
|
210
209
|
timescale = this.trackIdToTimescale_.get(trackId);
|
|
211
210
|
}
|
|
212
211
|
})
|
|
213
|
-
|
|
214
212
|
.fullBox('tfdt', (box) => {
|
|
215
213
|
goog.asserts.assert(
|
|
216
214
|
box.version != null,
|
|
@@ -223,16 +221,19 @@ shaka.cea.Mp4CeaParser = class {
|
|
|
223
221
|
})
|
|
224
222
|
.box('mdat', (box) => {
|
|
225
223
|
if (baseMediaDecodeTime === null) {
|
|
226
|
-
// This field should have been populated by
|
|
227
|
-
//
|
|
224
|
+
// This field should have been populated by the Base Media Decode
|
|
225
|
+
// Time in the tfdt box.
|
|
226
|
+
shaka.log.alwaysWarn(
|
|
227
|
+
'Unable to find base media decode time for CEA captions!');
|
|
228
228
|
throw new shaka.util.Error(
|
|
229
229
|
shaka.util.Error.Severity.CRITICAL,
|
|
230
230
|
shaka.util.Error.Category.TEXT,
|
|
231
231
|
shaka.util.Error.Code.INVALID_MP4_CEA);
|
|
232
232
|
}
|
|
233
|
-
|
|
233
|
+
|
|
234
|
+
const offset = moofOffset - box.start - 8;
|
|
234
235
|
this.parseMdat_(box.reader, baseMediaDecodeTime, timescale,
|
|
235
|
-
defaultSampleDuration, defaultSampleSize, offset,
|
|
236
|
+
defaultSampleDuration, defaultSampleSize, offset, parsedTRUNs,
|
|
236
237
|
captionPackets);
|
|
237
238
|
})
|
|
238
239
|
.parse(mediaSegment, /* partialOkay= */ false);
|
|
@@ -248,12 +249,12 @@ shaka.cea.Mp4CeaParser = class {
|
|
|
248
249
|
* @param {number} defaultSampleDuration
|
|
249
250
|
* @param {number} defaultSampleSize
|
|
250
251
|
* @param {number} offset
|
|
251
|
-
* @param {!Array<shaka.util.
|
|
252
|
+
* @param {!Array<shaka.util.ParsedTRUNBox>} parsedTRUNs
|
|
252
253
|
* @param {!Array<!shaka.cea.ICeaParser.CaptionPacket>} captionPackets
|
|
253
254
|
* @private
|
|
254
255
|
*/
|
|
255
256
|
parseMdat_(reader, time, timescale, defaultSampleDuration,
|
|
256
|
-
defaultSampleSize, offset,
|
|
257
|
+
defaultSampleSize, offset, parsedTRUNs, captionPackets) {
|
|
257
258
|
const BitstreamFormat = shaka.cea.Mp4CeaParser.BitstreamFormat;
|
|
258
259
|
const ICeaParser = shaka.cea.ICeaParser;
|
|
259
260
|
let sampleIndex = 0;
|
|
@@ -264,17 +265,23 @@ shaka.cea.Mp4CeaParser = class {
|
|
|
264
265
|
// composition time offset, we default to 0.
|
|
265
266
|
let sampleSize = defaultSampleSize;
|
|
266
267
|
|
|
268
|
+
// Combine all sample data. This assumes that the samples described across
|
|
269
|
+
// multiple trun boxes are still continuous in the mdat box.
|
|
270
|
+
const sampleDatas = parsedTRUNs.map((t) => t.sampleData);
|
|
271
|
+
const sampleData = [].concat(...sampleDatas);
|
|
272
|
+
|
|
267
273
|
if (sampleData.length) {
|
|
268
274
|
sampleSize = sampleData[0].sampleSize || defaultSampleSize;
|
|
269
275
|
}
|
|
270
276
|
|
|
271
|
-
reader.skip(offset);
|
|
277
|
+
reader.skip(offset + parsedTRUNs[0].dataOffset);
|
|
272
278
|
|
|
273
279
|
while (reader.hasMoreData()) {
|
|
274
280
|
const naluSize = reader.readUint32();
|
|
275
281
|
const naluHeader = reader.readUint8();
|
|
276
282
|
let naluType = null;
|
|
277
283
|
let isSeiMessage = false;
|
|
284
|
+
let naluHeaderSize = 1;
|
|
278
285
|
|
|
279
286
|
goog.asserts.assert(this.bitstreamFormat_ != BitstreamFormat.UNKNOWN,
|
|
280
287
|
'Bitstream format should have been checked before now!');
|
|
@@ -285,6 +292,8 @@ shaka.cea.Mp4CeaParser = class {
|
|
|
285
292
|
break;
|
|
286
293
|
|
|
287
294
|
case BitstreamFormat.H265:
|
|
295
|
+
naluHeaderSize = 2;
|
|
296
|
+
reader.skip(1);
|
|
288
297
|
naluType = (naluHeader >> 1) & 0x3f;
|
|
289
298
|
isSeiMessage =
|
|
290
299
|
naluType == ICeaParser.H265_PREFIX_NALU_TYPE_SEI ||
|
|
@@ -298,13 +307,13 @@ shaka.cea.Mp4CeaParser = class {
|
|
|
298
307
|
if (isSeiMessage) {
|
|
299
308
|
let timeOffset = 0;
|
|
300
309
|
|
|
301
|
-
if (sampleData.length
|
|
310
|
+
if (sampleIndex < sampleData.length) {
|
|
302
311
|
timeOffset = sampleData[sampleIndex].sampleCompositionTimeOffset || 0;
|
|
303
312
|
}
|
|
304
313
|
|
|
305
314
|
const pts = (time + timeOffset)/timescale;
|
|
306
315
|
for (const packet of this.seiProcessor_
|
|
307
|
-
.process(reader.readBytes(naluSize -
|
|
316
|
+
.process(reader.readBytes(naluSize - naluHeaderSize))) {
|
|
308
317
|
captionPackets.push({
|
|
309
318
|
packet,
|
|
310
319
|
pts,
|
|
@@ -312,7 +321,7 @@ shaka.cea.Mp4CeaParser = class {
|
|
|
312
321
|
}
|
|
313
322
|
} else {
|
|
314
323
|
try {
|
|
315
|
-
reader.skip(naluSize -
|
|
324
|
+
reader.skip(naluSize - naluHeaderSize);
|
|
316
325
|
} catch (e) {
|
|
317
326
|
// It is necessary to ignore this error because it can break the start
|
|
318
327
|
// of playback even if the user does not want to see the subtitles.
|
|
@@ -321,7 +330,7 @@ shaka.cea.Mp4CeaParser = class {
|
|
|
321
330
|
}
|
|
322
331
|
sampleSize -= (naluSize + 4);
|
|
323
332
|
if (sampleSize == 0) {
|
|
324
|
-
if (sampleData.length
|
|
333
|
+
if (sampleIndex < sampleData.length) {
|
|
325
334
|
time += sampleData[sampleIndex].sampleDuration ||
|
|
326
335
|
defaultSampleDuration;
|
|
327
336
|
} else {
|
|
@@ -330,7 +339,7 @@ shaka.cea.Mp4CeaParser = class {
|
|
|
330
339
|
|
|
331
340
|
sampleIndex++;
|
|
332
341
|
|
|
333
|
-
if (sampleData.length
|
|
342
|
+
if (sampleIndex < sampleData.length) {
|
|
334
343
|
sampleSize = sampleData[sampleIndex].sampleSize || defaultSampleSize;
|
|
335
344
|
} else {
|
|
336
345
|
sampleSize = defaultSampleSize;
|
package/lib/dash/dash_parser.js
CHANGED
|
@@ -886,7 +886,19 @@ shaka.dash.DashParser = class {
|
|
|
886
886
|
|
|
887
887
|
// Parallel for HLS VIDEO-RANGE as defined in DASH-IF IOP v4.3 6.2.5.1.
|
|
888
888
|
let videoRange;
|
|
889
|
-
|
|
889
|
+
|
|
890
|
+
// Ref. https://dashif.org/docs/DASH-IF-IOP-v4.3.pdf
|
|
891
|
+
// If signaled, a Supplemental or Essential Property descriptor
|
|
892
|
+
// shall be used, with the schemeIdUri set to
|
|
893
|
+
// urn:mpeg:mpegB:cicp:<Parameter> as defined in
|
|
894
|
+
// ISO/IEC 23001-8 [49] and <Parameter> one of the
|
|
895
|
+
// following: ColourPrimaries, TransferCharacteristics,
|
|
896
|
+
// or MatrixCoefficients.
|
|
897
|
+
const scheme = 'urn:mpeg:mpegB:cicp';
|
|
898
|
+
const transferCharacteristicsScheme = `${scheme}:TransferCharacteristics`;
|
|
899
|
+
const colourPrimariesScheme = `${scheme}:ColourPrimaries`;
|
|
900
|
+
const matrixCoefficientsScheme = `${scheme}:MatrixCoefficients`;
|
|
901
|
+
|
|
890
902
|
const getVideoRangeFromTransferCharacteristicCICP = (cicp) => {
|
|
891
903
|
switch (cicp) {
|
|
892
904
|
case 1:
|
|
@@ -912,10 +924,13 @@ shaka.dash.DashParser = class {
|
|
|
912
924
|
const schemeId = prop.getAttribute('schemeIdUri');
|
|
913
925
|
if (schemeId == 'http://dashif.org/guidelines/trickmode') {
|
|
914
926
|
trickModeFor = prop.getAttribute('value');
|
|
915
|
-
} else if (schemeId ==
|
|
927
|
+
} else if (schemeId == transferCharacteristicsScheme) {
|
|
916
928
|
videoRange = getVideoRangeFromTransferCharacteristicCICP(
|
|
917
929
|
parseInt(prop.getAttribute('value'), 10),
|
|
918
930
|
);
|
|
931
|
+
} else if (schemeId == colourPrimariesScheme ||
|
|
932
|
+
schemeId == matrixCoefficientsScheme) {
|
|
933
|
+
continue;
|
|
919
934
|
} else {
|
|
920
935
|
unrecognizedEssentialProperty = true;
|
|
921
936
|
}
|
|
@@ -925,7 +940,7 @@ shaka.dash.DashParser = class {
|
|
|
925
940
|
XmlUtils.findChildren(elem, 'SupplementalProperty');
|
|
926
941
|
for (const prop of supplementalProperties) {
|
|
927
942
|
const schemeId = prop.getAttribute('schemeIdUri');
|
|
928
|
-
if (schemeId ==
|
|
943
|
+
if (schemeId == transferCharacteristicsScheme) {
|
|
929
944
|
videoRange = getVideoRangeFromTransferCharacteristicCICP(
|
|
930
945
|
parseInt(prop.getAttribute('value'), 10),
|
|
931
946
|
);
|
package/lib/hls/hls_parser.js
CHANGED
|
@@ -319,6 +319,9 @@ shaka.hls.HlsParser = class {
|
|
|
319
319
|
this.presentationTimeline_.setDuration(Math.min(...maxTimestamps));
|
|
320
320
|
this.playerInterface_.updateDuration();
|
|
321
321
|
}
|
|
322
|
+
if (stillLive) {
|
|
323
|
+
this.determineDuration_();
|
|
324
|
+
}
|
|
322
325
|
}
|
|
323
326
|
|
|
324
327
|
/**
|
|
@@ -400,9 +403,16 @@ shaka.hls.HlsParser = class {
|
|
|
400
403
|
mediaSequenceNumber);
|
|
401
404
|
stream.segmentIndex.evict(playlistStartTime);
|
|
402
405
|
}
|
|
406
|
+
const oldSegment = segments[0];
|
|
407
|
+
goog.asserts.assert(oldSegment, 'Should have segments!');
|
|
408
|
+
|
|
409
|
+
streamInfo.minTimestamp = oldSegment.startTime;
|
|
410
|
+
|
|
403
411
|
const newestSegment = segments[segments.length - 1];
|
|
404
412
|
goog.asserts.assert(newestSegment, 'Should have segments!');
|
|
405
413
|
|
|
414
|
+
streamInfo.maxTimestamp = newestSegment.endTime;
|
|
415
|
+
|
|
406
416
|
// Once the last segment has been added to the playlist,
|
|
407
417
|
// #EXT-X-ENDLIST tag will be appended.
|
|
408
418
|
// If that happened, treat the rest of the EVENT presentation as VOD.
|
|
@@ -413,7 +423,6 @@ shaka.hls.HlsParser = class {
|
|
|
413
423
|
// Flag this for later. We don't convert the whole presentation into VOD
|
|
414
424
|
// until we've seen the ENDLIST tag for all active playlists.
|
|
415
425
|
streamInfo.hasEndList = true;
|
|
416
|
-
streamInfo.maxTimestamp = newestSegment.endTime;
|
|
417
426
|
}
|
|
418
427
|
}
|
|
419
428
|
|
|
@@ -576,6 +585,9 @@ shaka.hls.HlsParser = class {
|
|
|
576
585
|
* @private
|
|
577
586
|
*/
|
|
578
587
|
offsetStreamInfo_(streamInfo, offset) {
|
|
588
|
+
// Adjust our accounting of the minimum timestamp.
|
|
589
|
+
streamInfo.minTimestamp += offset;
|
|
590
|
+
|
|
579
591
|
// Adjust our accounting of the maximum timestamp.
|
|
580
592
|
streamInfo.maxTimestamp += offset;
|
|
581
593
|
goog.asserts.assert(streamInfo.maxTimestamp >= 0,
|
|
@@ -804,7 +816,7 @@ shaka.hls.HlsParser = class {
|
|
|
804
816
|
const PresentationType = shaka.hls.HlsParser.PresentationType_;
|
|
805
817
|
|
|
806
818
|
if (this.presentationType_ == PresentationType.LIVE) {
|
|
807
|
-
let segmentAvailabilityDuration = this.
|
|
819
|
+
let segmentAvailabilityDuration = this.getLiveDuration_();
|
|
808
820
|
|
|
809
821
|
// This defaults to the presentation delay, which has the effect of
|
|
810
822
|
// making the live stream unseekable. This is consistent with Apple's
|
|
@@ -1655,6 +1667,7 @@ shaka.hls.HlsParser = class {
|
|
|
1655
1667
|
verbatimMediaPlaylistUri,
|
|
1656
1668
|
// These values are filled out or updated after lazy-loading:
|
|
1657
1669
|
absoluteMediaPlaylistUri: initialMediaPlaylistUri,
|
|
1670
|
+
minTimestamp: 0,
|
|
1658
1671
|
maxTimestamp: 0,
|
|
1659
1672
|
mediaSequenceToStartTime: new Map(),
|
|
1660
1673
|
canSkipSegments: false,
|
|
@@ -1700,6 +1713,7 @@ shaka.hls.HlsParser = class {
|
|
|
1700
1713
|
|
|
1701
1714
|
// Copy values from the real stream info to our initial one.
|
|
1702
1715
|
streamInfo.absoluteMediaPlaylistUri = absoluteMediaPlaylistUri;
|
|
1716
|
+
streamInfo.minTimestamp = realStreamInfo.minTimestamp;
|
|
1703
1717
|
streamInfo.maxTimestamp = realStreamInfo.maxTimestamp;
|
|
1704
1718
|
streamInfo.canSkipSegments = realStreamInfo.canSkipSegments;
|
|
1705
1719
|
streamInfo.hasEndList = realStreamInfo.hasEndList;
|
|
@@ -1833,6 +1847,22 @@ shaka.hls.HlsParser = class {
|
|
|
1833
1847
|
return minDuration;
|
|
1834
1848
|
}
|
|
1835
1849
|
|
|
1850
|
+
/**
|
|
1851
|
+
* @return {number}
|
|
1852
|
+
* @private
|
|
1853
|
+
*/
|
|
1854
|
+
getLiveDuration_() {
|
|
1855
|
+
let maxTimestamp = Infinity;
|
|
1856
|
+
let minTimestamp = Infinity;
|
|
1857
|
+
for (const streamInfo of this.uriToStreamInfosMap_.values()) {
|
|
1858
|
+
if (streamInfo.stream.segmentIndex && streamInfo.stream.type != 'text') {
|
|
1859
|
+
maxTimestamp = Math.min(maxTimestamp, streamInfo.maxTimestamp);
|
|
1860
|
+
minTimestamp = Math.min(minTimestamp, streamInfo.minTimestamp);
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
return maxTimestamp - minTimestamp;
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1836
1866
|
/**
|
|
1837
1867
|
* @param {!Array.<!shaka.extern.Stream>} streams
|
|
1838
1868
|
* @private
|
|
@@ -1978,6 +2008,7 @@ shaka.hls.HlsParser = class {
|
|
|
1978
2008
|
this.determinePresentationType_(playlist);
|
|
1979
2009
|
}
|
|
1980
2010
|
|
|
2011
|
+
const firstStartTime = segments[0].startTime;
|
|
1981
2012
|
const lastEndTime = segments[segments.length - 1].endTime;
|
|
1982
2013
|
/** @type {!shaka.media.SegmentIndex} */
|
|
1983
2014
|
const segmentIndex = new shaka.media.SegmentIndex(segments);
|
|
@@ -2000,6 +2031,7 @@ shaka.hls.HlsParser = class {
|
|
|
2000
2031
|
type,
|
|
2001
2032
|
verbatimMediaPlaylistUri,
|
|
2002
2033
|
absoluteMediaPlaylistUri,
|
|
2034
|
+
minTimestamp: firstStartTime,
|
|
2003
2035
|
maxTimestamp: lastEndTime,
|
|
2004
2036
|
canSkipSegments,
|
|
2005
2037
|
hasEndList: false,
|
|
@@ -2034,6 +2066,22 @@ shaka.hls.HlsParser = class {
|
|
|
2034
2066
|
// stream is lazy-loaded.
|
|
2035
2067
|
const mimeType = this.guessMimeTypeBeforeLoading_(type, codecs) ||
|
|
2036
2068
|
this.guessMimeTypeFallback_(type);
|
|
2069
|
+
const roles = [];
|
|
2070
|
+
if (characteristics) {
|
|
2071
|
+
for (const characteristic of characteristics.split(',')) {
|
|
2072
|
+
roles.push(characteristic);
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
|
|
2076
|
+
const kind = (type == shaka.util.ManifestParserUtils.ContentType.TEXT) ?
|
|
2077
|
+
shaka.util.ManifestParserUtils.TextStreamKind.SUBTITLE : undefined;
|
|
2078
|
+
|
|
2079
|
+
// If there are no roles, and we have defaulted to the subtitle "kind" for
|
|
2080
|
+
// this track, add the implied subtitle role.
|
|
2081
|
+
if (!roles.length &&
|
|
2082
|
+
kind === shaka.util.ManifestParserUtils.TextStreamKind.SUBTITLE) {
|
|
2083
|
+
roles.push(shaka.util.ManifestParserUtils.TextStreamKind.SUBTITLE);
|
|
2084
|
+
}
|
|
2037
2085
|
|
|
2038
2086
|
return {
|
|
2039
2087
|
id: this.globalId_++,
|
|
@@ -2042,8 +2090,7 @@ shaka.hls.HlsParser = class {
|
|
|
2042
2090
|
segmentIndex: null,
|
|
2043
2091
|
mimeType,
|
|
2044
2092
|
codecs,
|
|
2045
|
-
kind
|
|
2046
|
-
shaka.util.ManifestParserUtils.TextStreamKind.SUBTITLE : undefined,
|
|
2093
|
+
kind,
|
|
2047
2094
|
encrypted: false,
|
|
2048
2095
|
drmInfos: [],
|
|
2049
2096
|
keyIds: new Set(),
|
|
@@ -2059,7 +2106,7 @@ shaka.hls.HlsParser = class {
|
|
|
2059
2106
|
width: undefined,
|
|
2060
2107
|
height: undefined,
|
|
2061
2108
|
bandwidth: undefined,
|
|
2062
|
-
roles
|
|
2109
|
+
roles,
|
|
2063
2110
|
forced,
|
|
2064
2111
|
channelsCount,
|
|
2065
2112
|
audioSamplingRate: null,
|
|
@@ -2498,6 +2545,9 @@ shaka.hls.HlsParser = class {
|
|
|
2498
2545
|
/* tileDuration= */ null,
|
|
2499
2546
|
/* syncTime= */ null,
|
|
2500
2547
|
partialStatus);
|
|
2548
|
+
if (item.name == 'EXT-X-PRELOAD-HINT') {
|
|
2549
|
+
partial.markAsPreload();
|
|
2550
|
+
}
|
|
2501
2551
|
partialSegmentRefs.push(partial);
|
|
2502
2552
|
} // for-loop of hlsSegment.partialSegments
|
|
2503
2553
|
}
|
|
@@ -2514,6 +2564,10 @@ shaka.hls.HlsParser = class {
|
|
|
2514
2564
|
// We're interested in the duration part.
|
|
2515
2565
|
const extinfValues = extinfTag.value.split(',');
|
|
2516
2566
|
const duration = Number(extinfValues[0]);
|
|
2567
|
+
// Skip segments without duration
|
|
2568
|
+
if (duration == 0) {
|
|
2569
|
+
return null;
|
|
2570
|
+
}
|
|
2517
2571
|
endTime = startTime + duration;
|
|
2518
2572
|
} else {
|
|
2519
2573
|
endTime = partialSegmentRefs[partialSegmentRefs.length - 1].endTime;
|
|
@@ -2694,9 +2748,9 @@ shaka.hls.HlsParser = class {
|
|
|
2694
2748
|
type,
|
|
2695
2749
|
lastDiscontinuityStartTime,
|
|
2696
2750
|
hlsAes128Key);
|
|
2697
|
-
previousReference = reference;
|
|
2698
2751
|
|
|
2699
2752
|
if (reference) {
|
|
2753
|
+
previousReference = reference;
|
|
2700
2754
|
reference.discontinuitySequence = discontinuitySequence;
|
|
2701
2755
|
|
|
2702
2756
|
if (this.config_.hls.ignoreManifestProgramDateTime &&
|
|
@@ -2807,6 +2861,9 @@ shaka.hls.HlsParser = class {
|
|
|
2807
2861
|
* @private
|
|
2808
2862
|
*/
|
|
2809
2863
|
variableSubstitution_(uri, variables) {
|
|
2864
|
+
if (!variables.size) {
|
|
2865
|
+
return uri;
|
|
2866
|
+
}
|
|
2810
2867
|
let newUri = String(uri).replace(/%7B/g, '{').replace(/%7D/g, '}');
|
|
2811
2868
|
|
|
2812
2869
|
const uriVariables = newUri.match(/{\$\w*}/g);
|
|
@@ -3252,6 +3309,7 @@ shaka.hls.HlsParser = class {
|
|
|
3252
3309
|
* type: string,
|
|
3253
3310
|
* verbatimMediaPlaylistUri: string,
|
|
3254
3311
|
* absoluteMediaPlaylistUri: string,
|
|
3312
|
+
* minTimestamp: number,
|
|
3255
3313
|
* maxTimestamp: number,
|
|
3256
3314
|
* mediaSequenceToStartTime: !Map.<number, number>,
|
|
3257
3315
|
* canSkipSegments: boolean,
|
|
@@ -3275,6 +3333,8 @@ shaka.hls.HlsParser = class {
|
|
|
3275
3333
|
* @property {string} absoluteMediaPlaylistUri
|
|
3276
3334
|
* The absolute media playlist URI, resolved relative to the master playlist
|
|
3277
3335
|
* and updated to reflect any redirects.
|
|
3336
|
+
* @property {number} minTimestamp
|
|
3337
|
+
* The minimum timestamp found in the stream.
|
|
3278
3338
|
* @property {number} maxTimestamp
|
|
3279
3339
|
* The maximum timestamp found in the stream.
|
|
3280
3340
|
* @property {!Map.<number, number>} mediaSequenceToStartTime
|
|
@@ -158,7 +158,15 @@ shaka.hls.ManifestTextParser = class {
|
|
|
158
158
|
partialSegmentTags.push(tag);
|
|
159
159
|
} else if (tag.name == 'EXT-X-PRELOAD-HINT') {
|
|
160
160
|
if (tag.getAttributeValue('TYPE') == 'PART') {
|
|
161
|
-
|
|
161
|
+
// Note: BYTERANGE-START without BYTERANGE-LENGTH is being
|
|
162
|
+
// ignored.
|
|
163
|
+
if (tag.getAttributeValue('BYTERANGE-START') != null) {
|
|
164
|
+
if (tag.getAttributeValue('BYTERANGE-LENGTH') != null) {
|
|
165
|
+
partialSegmentTags.push(tag);
|
|
166
|
+
}
|
|
167
|
+
} else {
|
|
168
|
+
partialSegmentTags.push(tag);
|
|
169
|
+
}
|
|
162
170
|
} else if (tag.getAttributeValue('TYPE') == 'MAP') {
|
|
163
171
|
// Rename the Preload Hint tag to be a Map tag.
|
|
164
172
|
tag.setName('EXT-X-MAP');
|
|
@@ -300,6 +308,7 @@ shaka.hls.ManifestTextParser.MEDIA_PLAYLIST_TAGS = [
|
|
|
300
308
|
'EXT-X-ENDLIST',
|
|
301
309
|
'EXT-X-SERVER-CONTROL',
|
|
302
310
|
'EXT-X-SKIP',
|
|
311
|
+
'EXT-X-PART-INF',
|
|
303
312
|
];
|
|
304
313
|
|
|
305
314
|
|
package/lib/media/drm_engine.js
CHANGED
|
@@ -133,6 +133,9 @@ shaka.media.DrmEngine = class {
|
|
|
133
133
|
|
|
134
134
|
/** @private {Promise} */
|
|
135
135
|
this.mediaKeysAttached_ = null;
|
|
136
|
+
|
|
137
|
+
/** @private {?shaka.extern.InitDataOverride} */
|
|
138
|
+
this.manifestInitData_ = null;
|
|
136
139
|
}
|
|
137
140
|
|
|
138
141
|
/** @override */
|
|
@@ -351,18 +354,24 @@ shaka.media.DrmEngine = class {
|
|
|
351
354
|
shaka.media.DrmEngine.replaceDrmInfo_(variants, servers);
|
|
352
355
|
}
|
|
353
356
|
|
|
354
|
-
|
|
357
|
+
/** @type {!Set<shaka.extern.DrmInfo>} */
|
|
358
|
+
const drmInfos = new Set();
|
|
355
359
|
for (const variant of variants) {
|
|
356
|
-
const
|
|
357
|
-
for (const info of
|
|
358
|
-
|
|
359
|
-
info,
|
|
360
|
-
shaka.util.MapUtils.asMap(this.config_.servers),
|
|
361
|
-
shaka.util.MapUtils.asMap(this.config_.advanced || {}),
|
|
362
|
-
this.config_.keySystemsMapping);
|
|
360
|
+
const variantDrmInfos = this.getVariantDrmInfos_(variant);
|
|
361
|
+
for (const info of variantDrmInfos) {
|
|
362
|
+
drmInfos.add(info);
|
|
363
363
|
}
|
|
364
364
|
}
|
|
365
365
|
|
|
366
|
+
for (const info of drmInfos) {
|
|
367
|
+
shaka.media.DrmEngine.fillInDrmInfoDefaults_(
|
|
368
|
+
info,
|
|
369
|
+
shaka.util.MapUtils.asMap(this.config_.servers),
|
|
370
|
+
shaka.util.MapUtils.asMap(this.config_.advanced || {}),
|
|
371
|
+
this.config_.keySystemsMapping);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
|
|
366
375
|
/** @type {!Map.<string, MediaKeySystemConfiguration>} */
|
|
367
376
|
let configsByKeySystem;
|
|
368
377
|
|
|
@@ -480,10 +489,10 @@ shaka.media.DrmEngine = class {
|
|
|
480
489
|
() => this.closeOpenSessions_());
|
|
481
490
|
}
|
|
482
491
|
|
|
483
|
-
|
|
484
|
-
this.currentDrmInfo_.initData.find(
|
|
492
|
+
this.manifestInitData_ = this.currentDrmInfo_ ?
|
|
493
|
+
(this.currentDrmInfo_.initData.find(
|
|
485
494
|
(initDataOverride) => initDataOverride.initData.length > 0,
|
|
486
|
-
) : null;
|
|
495
|
+
) || null) : null;
|
|
487
496
|
|
|
488
497
|
/**
|
|
489
498
|
* We can attach media keys before the playback actually begins when:
|
|
@@ -491,7 +500,7 @@ shaka.media.DrmEngine = class {
|
|
|
491
500
|
* - Some initData already has been generated (through the manifest)
|
|
492
501
|
* - In case of an offline session
|
|
493
502
|
*/
|
|
494
|
-
if (
|
|
503
|
+
if (this.manifestInitData_ ||
|
|
495
504
|
this.currentDrmInfo_.keySystem !== 'com.apple.fps' ||
|
|
496
505
|
this.offlineSessionIds_.length) {
|
|
497
506
|
await this.attachMediaKeys_();
|
|
@@ -504,7 +513,7 @@ shaka.media.DrmEngine = class {
|
|
|
504
513
|
// Also suppress 'encrypted' events when parsing in-band ppsh
|
|
505
514
|
// from media segments because that serves the same purpose as the
|
|
506
515
|
// 'encrypted' events.
|
|
507
|
-
if (!
|
|
516
|
+
if (!this.manifestInitData_ && !this.offlineSessionIds_.length &&
|
|
508
517
|
!this.config_.parseInbandPsshEnabled) {
|
|
509
518
|
this.eventManager_.listen(
|
|
510
519
|
this.video_, 'encrypted', (e) => this.onEncryptedEvent_(e));
|
|
@@ -2361,7 +2370,7 @@ shaka.media.DrmEngine = class {
|
|
|
2361
2370
|
* @return {!Promise<void>}
|
|
2362
2371
|
*/
|
|
2363
2372
|
parseInbandPssh(contentType, mediaSegment) {
|
|
2364
|
-
if (!this.config_.parseInbandPsshEnabled) {
|
|
2373
|
+
if (!this.config_.parseInbandPsshEnabled || this.manifestInitData_) {
|
|
2365
2374
|
return Promise.resolve();
|
|
2366
2375
|
}
|
|
2367
2376
|
|
|
@@ -58,6 +58,9 @@ shaka.media.GapJumpingController = class {
|
|
|
58
58
|
/** @private {number} */
|
|
59
59
|
this.prevReadyState_ = video.readyState;
|
|
60
60
|
|
|
61
|
+
/** @private {number} */
|
|
62
|
+
this.startTime_ = 0;
|
|
63
|
+
|
|
61
64
|
/** @private {number} */
|
|
62
65
|
this.gapsJumped_ = 0;
|
|
63
66
|
|
|
@@ -123,6 +126,18 @@ shaka.media.GapJumpingController = class {
|
|
|
123
126
|
this.onPollGapJump_();
|
|
124
127
|
}
|
|
125
128
|
|
|
129
|
+
/**
|
|
130
|
+
* Called when playback has started and the video element is
|
|
131
|
+
* listening for seeks.
|
|
132
|
+
*
|
|
133
|
+
* @param {number} startTime
|
|
134
|
+
*/
|
|
135
|
+
onStarted(startTime) {
|
|
136
|
+
if (this.video_.seeking && !this.seekingEventReceived_) {
|
|
137
|
+
this.seekingEventReceived_ = true;
|
|
138
|
+
this.startTime_ = startTime;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
126
141
|
|
|
127
142
|
/** Called when a seek has started. */
|
|
128
143
|
onSeeking() {
|
|
@@ -164,8 +179,8 @@ shaka.media.GapJumpingController = class {
|
|
|
164
179
|
// while paused on a livestream. We make an exception for time 0, since we
|
|
165
180
|
// may be _required_ to seek on startup before play can begin, but only if
|
|
166
181
|
// autoplay is enabled.
|
|
167
|
-
if (this.video_.paused && (this.video_.currentTime !=
|
|
168
|
-
(!this.video_.autoplay && this.video_.currentTime ==
|
|
182
|
+
if (this.video_.paused && (this.video_.currentTime != this.startTime_ ||
|
|
183
|
+
(!this.video_.autoplay && this.video_.currentTime == this.startTime_))) {
|
|
169
184
|
return;
|
|
170
185
|
}
|
|
171
186
|
|
|
@@ -228,6 +243,13 @@ shaka.media.GapJumpingController = class {
|
|
|
228
243
|
}
|
|
229
244
|
|
|
230
245
|
this.video_.currentTime = jumpTo;
|
|
246
|
+
// This accounts for the possibility that we jump a gap at the start
|
|
247
|
+
// position but we jump _into_ another gap. By setting the start
|
|
248
|
+
// position to the new jumpTo we ensure that the check above will
|
|
249
|
+
// pass even though the video is still paused.
|
|
250
|
+
if (currentTime == this.startTime_) {
|
|
251
|
+
this.startTime_ = jumpTo;
|
|
252
|
+
}
|
|
231
253
|
this.gapsJumped_++;
|
|
232
254
|
this.onEvent_(
|
|
233
255
|
new shaka.util.FakeEvent(shaka.util.FakeEvent.EventName.GapJumped));
|