shaka-player 4.3.7 → 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/CHANGELOG.md +25 -0
- package/CONTRIBUTORS +1 -0
- package/demo/custom.js +1 -1
- 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/deps.js +1 -1
- package/dist/locales.js +31 -31
- package/dist/shaka-player.compiled.d.ts +22 -0
- package/dist/shaka-player.compiled.debug.d.ts +22 -0
- package/dist/shaka-player.compiled.debug.js +215 -213
- package/dist/shaka-player.compiled.debug.map +3 -3
- package/dist/shaka-player.compiled.js +789 -786
- package/dist/shaka-player.compiled.map +4 -4
- package/dist/shaka-player.ui.d.ts +22 -0
- package/dist/shaka-player.ui.debug.d.ts +22 -0
- package/dist/shaka-player.ui.debug.js +253 -252
- package/dist/shaka-player.ui.debug.map +3 -3
- package/dist/shaka-player.ui.js +823 -820
- package/dist/shaka-player.ui.map +4 -4
- package/docs/tutorials/drm-config.md +9 -6
- package/externs/ima.js +34 -0
- package/lib/abr/simple_abr_manager.js +1 -1
- package/lib/cea/mp4_cea_parser.js +24 -18
- package/lib/dash/dash_parser.js +18 -3
- package/lib/hls/hls_parser.js +23 -4
- package/lib/media/drm_engine.js +14 -8
- package/lib/media/gap_jumping_controller.js +24 -2
- package/lib/media/media_source_engine.js +5 -3
- package/lib/media/playhead.js +11 -0
- package/lib/media/region_observer.js +19 -1
- package/lib/media/streaming_engine.js +39 -19
- package/lib/media/video_wrapper.js +6 -1
- package/lib/player.js +12 -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 +2 -2
- package/test/cea/mp4_cea_parser_unit.js +28 -5
- package/test/dash/dash_parser_manifest_unit.js +120 -0
- package/test/hls/hls_parser_unit.js +144 -0
- package/test/media/region_observer_unit.js +2 -1
- package/test/media/streaming_engine_unit.js +26 -2
- package/test/test/assets/multiple-trun-init.mp4 +0 -0
- package/test/test/assets/multiple-trun-segment.mp4 +0 -0
- 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
|
```
|
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,11 +265,16 @@ 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();
|
|
@@ -301,7 +307,7 @@ shaka.cea.Mp4CeaParser = class {
|
|
|
301
307
|
if (isSeiMessage) {
|
|
302
308
|
let timeOffset = 0;
|
|
303
309
|
|
|
304
|
-
if (sampleData.length
|
|
310
|
+
if (sampleIndex < sampleData.length) {
|
|
305
311
|
timeOffset = sampleData[sampleIndex].sampleCompositionTimeOffset || 0;
|
|
306
312
|
}
|
|
307
313
|
|
|
@@ -324,7 +330,7 @@ shaka.cea.Mp4CeaParser = class {
|
|
|
324
330
|
}
|
|
325
331
|
sampleSize -= (naluSize + 4);
|
|
326
332
|
if (sampleSize == 0) {
|
|
327
|
-
if (sampleData.length
|
|
333
|
+
if (sampleIndex < sampleData.length) {
|
|
328
334
|
time += sampleData[sampleIndex].sampleDuration ||
|
|
329
335
|
defaultSampleDuration;
|
|
330
336
|
} else {
|
|
@@ -333,7 +339,7 @@ shaka.cea.Mp4CeaParser = class {
|
|
|
333
339
|
|
|
334
340
|
sampleIndex++;
|
|
335
341
|
|
|
336
|
-
if (sampleData.length
|
|
342
|
+
if (sampleIndex < sampleData.length) {
|
|
337
343
|
sampleSize = sampleData[sampleIndex].sampleSize || defaultSampleSize;
|
|
338
344
|
} else {
|
|
339
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
|
@@ -2066,6 +2066,22 @@ shaka.hls.HlsParser = class {
|
|
|
2066
2066
|
// stream is lazy-loaded.
|
|
2067
2067
|
const mimeType = this.guessMimeTypeBeforeLoading_(type, codecs) ||
|
|
2068
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
|
+
}
|
|
2069
2085
|
|
|
2070
2086
|
return {
|
|
2071
2087
|
id: this.globalId_++,
|
|
@@ -2074,8 +2090,7 @@ shaka.hls.HlsParser = class {
|
|
|
2074
2090
|
segmentIndex: null,
|
|
2075
2091
|
mimeType,
|
|
2076
2092
|
codecs,
|
|
2077
|
-
kind
|
|
2078
|
-
shaka.util.ManifestParserUtils.TextStreamKind.SUBTITLE : undefined,
|
|
2093
|
+
kind,
|
|
2079
2094
|
encrypted: false,
|
|
2080
2095
|
drmInfos: [],
|
|
2081
2096
|
keyIds: new Set(),
|
|
@@ -2091,7 +2106,7 @@ shaka.hls.HlsParser = class {
|
|
|
2091
2106
|
width: undefined,
|
|
2092
2107
|
height: undefined,
|
|
2093
2108
|
bandwidth: undefined,
|
|
2094
|
-
roles
|
|
2109
|
+
roles,
|
|
2095
2110
|
forced,
|
|
2096
2111
|
channelsCount,
|
|
2097
2112
|
audioSamplingRate: null,
|
|
@@ -2549,6 +2564,10 @@ shaka.hls.HlsParser = class {
|
|
|
2549
2564
|
// We're interested in the duration part.
|
|
2550
2565
|
const extinfValues = extinfTag.value.split(',');
|
|
2551
2566
|
const duration = Number(extinfValues[0]);
|
|
2567
|
+
// Skip segments without duration
|
|
2568
|
+
if (duration == 0) {
|
|
2569
|
+
return null;
|
|
2570
|
+
}
|
|
2552
2571
|
endTime = startTime + duration;
|
|
2553
2572
|
} else {
|
|
2554
2573
|
endTime = partialSegmentRefs[partialSegmentRefs.length - 1].endTime;
|
|
@@ -2729,9 +2748,9 @@ shaka.hls.HlsParser = class {
|
|
|
2729
2748
|
type,
|
|
2730
2749
|
lastDiscontinuityStartTime,
|
|
2731
2750
|
hlsAes128Key);
|
|
2732
|
-
previousReference = reference;
|
|
2733
2751
|
|
|
2734
2752
|
if (reference) {
|
|
2753
|
+
previousReference = reference;
|
|
2735
2754
|
reference.discontinuitySequence = discontinuitySequence;
|
|
2736
2755
|
|
|
2737
2756
|
if (this.config_.hls.ignoreManifestProgramDateTime &&
|
package/lib/media/drm_engine.js
CHANGED
|
@@ -354,18 +354,24 @@ shaka.media.DrmEngine = class {
|
|
|
354
354
|
shaka.media.DrmEngine.replaceDrmInfo_(variants, servers);
|
|
355
355
|
}
|
|
356
356
|
|
|
357
|
-
|
|
357
|
+
/** @type {!Set<shaka.extern.DrmInfo>} */
|
|
358
|
+
const drmInfos = new Set();
|
|
358
359
|
for (const variant of variants) {
|
|
359
|
-
const
|
|
360
|
-
for (const info of
|
|
361
|
-
|
|
362
|
-
info,
|
|
363
|
-
shaka.util.MapUtils.asMap(this.config_.servers),
|
|
364
|
-
shaka.util.MapUtils.asMap(this.config_.advanced || {}),
|
|
365
|
-
this.config_.keySystemsMapping);
|
|
360
|
+
const variantDrmInfos = this.getVariantDrmInfos_(variant);
|
|
361
|
+
for (const info of variantDrmInfos) {
|
|
362
|
+
drmInfos.add(info);
|
|
366
363
|
}
|
|
367
364
|
}
|
|
368
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
|
+
|
|
369
375
|
/** @type {!Map.<string, MediaKeySystemConfiguration>} */
|
|
370
376
|
let configsByKeySystem;
|
|
371
377
|
|
|
@@ -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));
|
|
@@ -582,11 +582,13 @@ shaka.media.MediaSourceEngine = class {
|
|
|
582
582
|
* @param {boolean=} seeked True if we just seeked
|
|
583
583
|
* @param {boolean=} adaptation True if we just automatically switched active
|
|
584
584
|
* variant(s).
|
|
585
|
+
* @param {boolean=} isChunkedData True if we add to the buffer from the
|
|
586
|
+
* partial read of the segment.
|
|
585
587
|
* @return {!Promise}
|
|
586
588
|
*/
|
|
587
589
|
async appendBuffer(
|
|
588
590
|
contentType, data, reference, hasClosedCaptions, seeked = false,
|
|
589
|
-
adaptation = false) {
|
|
591
|
+
adaptation = false, isChunkedData = false) {
|
|
590
592
|
const ContentType = shaka.util.ManifestParserUtils.ContentType;
|
|
591
593
|
|
|
592
594
|
if (contentType == ContentType.TEXT) {
|
|
@@ -787,13 +789,13 @@ shaka.media.MediaSourceEngine = class {
|
|
|
787
789
|
let bufferedBefore = null;
|
|
788
790
|
|
|
789
791
|
await this.enqueueOperation_(contentType, () => {
|
|
790
|
-
if (goog.DEBUG && reference) {
|
|
792
|
+
if (goog.DEBUG && reference && !reference.isPreload() && !isChunkedData) {
|
|
791
793
|
bufferedBefore = this.getBuffered_(contentType);
|
|
792
794
|
}
|
|
793
795
|
this.append_(contentType, data);
|
|
794
796
|
});
|
|
795
797
|
|
|
796
|
-
if (goog.DEBUG && reference && !reference.isPreload()) {
|
|
798
|
+
if (goog.DEBUG && reference && !reference.isPreload() && !isChunkedData) {
|
|
797
799
|
const bufferedAfter = this.getBuffered_(contentType);
|
|
798
800
|
const newBuffered = shaka.media.TimeRangesUtils.computeAddedRange(
|
|
799
801
|
bufferedBefore, bufferedAfter);
|
package/lib/media/playhead.js
CHANGED
|
@@ -251,6 +251,7 @@ shaka.media.MediaSourcePlayhead = class {
|
|
|
251
251
|
this.videoWrapper_ = new shaka.media.VideoWrapper(
|
|
252
252
|
mediaElement,
|
|
253
253
|
() => this.onSeeking_(),
|
|
254
|
+
(realStartTime) => this.onStarted_(realStartTime),
|
|
254
255
|
this.getStartTime_(startTime));
|
|
255
256
|
|
|
256
257
|
/** @type {shaka.util.Timer} */
|
|
@@ -390,6 +391,16 @@ shaka.media.MediaSourcePlayhead = class {
|
|
|
390
391
|
}
|
|
391
392
|
}
|
|
392
393
|
|
|
394
|
+
/**
|
|
395
|
+
* Called when the video element has started up and is listening for new seeks
|
|
396
|
+
*
|
|
397
|
+
* @param {number} startTime
|
|
398
|
+
* @private
|
|
399
|
+
*/
|
|
400
|
+
onStarted_(startTime) {
|
|
401
|
+
this.gapController_.onStarted(startTime);
|
|
402
|
+
}
|
|
403
|
+
|
|
393
404
|
/**
|
|
394
405
|
* Handles when a seek happens on the video.
|
|
395
406
|
*
|
|
@@ -27,13 +27,24 @@ shaka.media.RegionObserver = class extends shaka.util.FakeEventTarget {
|
|
|
27
27
|
* destroy the timeline.
|
|
28
28
|
*
|
|
29
29
|
* @param {!shaka.media.RegionTimeline} timeline
|
|
30
|
+
* @param {boolean} startsPastZero
|
|
30
31
|
*/
|
|
31
|
-
constructor(timeline) {
|
|
32
|
+
constructor(timeline, startsPastZero) {
|
|
32
33
|
super();
|
|
33
34
|
|
|
34
35
|
/** @private {shaka.media.RegionTimeline} */
|
|
35
36
|
this.timeline_ = timeline;
|
|
36
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Whether the asset is expected to start at a time beyond 0 seconds.
|
|
40
|
+
* For example, if the asset is a live stream.
|
|
41
|
+
* If true, we will not start polling for regions until the playhead has
|
|
42
|
+
* moved past 0 seconds, to avoid bad behaviors where the current time is
|
|
43
|
+
* briefly 0 before we have enough data to play.
|
|
44
|
+
* @private {boolean}
|
|
45
|
+
*/
|
|
46
|
+
this.startsPastZero_ = startsPastZero;
|
|
47
|
+
|
|
37
48
|
/**
|
|
38
49
|
* A mapping between a region and where we previously were relative to it.
|
|
39
50
|
* When the value here differs from what we calculate, it means we moved and
|
|
@@ -121,6 +132,13 @@ shaka.media.RegionObserver = class extends shaka.util.FakeEventTarget {
|
|
|
121
132
|
/** @override */
|
|
122
133
|
poll(positionInSeconds, wasSeeking) {
|
|
123
134
|
const RegionObserver = shaka.media.RegionObserver;
|
|
135
|
+
if (this.startsPastZero_ && positionInSeconds == 0) {
|
|
136
|
+
// Don't start checking regions until the timeline has begun moving.
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
// Now that we have seen the playhead go past 0, it's okay if it goes
|
|
140
|
+
// back there (e.g. seeking back to the start).
|
|
141
|
+
this.startsPastZero_ = false;
|
|
124
142
|
|
|
125
143
|
for (const region of this.timeline_.regions()) {
|
|
126
144
|
const previousPosition = this.oldPosition_.get(region);
|
|
@@ -1319,7 +1319,8 @@ shaka.media.StreamingEngine = class {
|
|
|
1319
1319
|
const dataToAppend = remaining.subarray(0, offset);
|
|
1320
1320
|
remaining = remaining.subarray(offset);
|
|
1321
1321
|
await this.append_(
|
|
1322
|
-
mediaState, presentationTime, stream, reference, dataToAppend
|
|
1322
|
+
mediaState, presentationTime, stream, reference, dataToAppend,
|
|
1323
|
+
/* isChunkedData= */ true);
|
|
1323
1324
|
}
|
|
1324
1325
|
};
|
|
1325
1326
|
|
|
@@ -1731,10 +1732,12 @@ shaka.media.StreamingEngine = class {
|
|
|
1731
1732
|
* @param {shaka.extern.Stream} stream
|
|
1732
1733
|
* @param {!shaka.media.SegmentReference} reference
|
|
1733
1734
|
* @param {BufferSource} segment
|
|
1735
|
+
* @param {boolean=} isChunkedData
|
|
1734
1736
|
* @return {!Promise}
|
|
1735
1737
|
* @private
|
|
1736
1738
|
*/
|
|
1737
|
-
async append_(mediaState, presentationTime, stream, reference, segment
|
|
1739
|
+
async append_(mediaState, presentationTime, stream, reference, segment,
|
|
1740
|
+
isChunkedData = false) {
|
|
1738
1741
|
const logPrefix = shaka.media.StreamingEngine.logPrefix_(mediaState);
|
|
1739
1742
|
|
|
1740
1743
|
const hasClosedCaptions = stream.closedCaptions &&
|
|
@@ -1788,7 +1791,8 @@ shaka.media.StreamingEngine = class {
|
|
|
1788
1791
|
reference,
|
|
1789
1792
|
hasClosedCaptions,
|
|
1790
1793
|
seeked,
|
|
1791
|
-
adaptation
|
|
1794
|
+
adaptation,
|
|
1795
|
+
isChunkedData);
|
|
1792
1796
|
this.destroyer_.ensureNotDestroyed();
|
|
1793
1797
|
shaka.log.v2(logPrefix, 'appended media segment');
|
|
1794
1798
|
}
|
|
@@ -1852,7 +1856,6 @@ shaka.media.StreamingEngine = class {
|
|
|
1852
1856
|
const messageData = box.reader.readBytes(
|
|
1853
1857
|
box.reader.getLength() - box.reader.getPosition());
|
|
1854
1858
|
|
|
1855
|
-
|
|
1856
1859
|
// See DASH sec. 5.10.3.3.1
|
|
1857
1860
|
// If a DASH client detects an event message box with a scheme that is not
|
|
1858
1861
|
// defined in MPD, the client is expected to ignore it.
|
|
@@ -1862,22 +1865,8 @@ shaka.media.StreamingEngine = class {
|
|
|
1862
1865
|
// A special scheme in DASH used to signal manifest updates.
|
|
1863
1866
|
if (schemeId == 'urn:mpeg:dash:event:2012') {
|
|
1864
1867
|
this.playerInterface_.onManifestUpdate();
|
|
1865
|
-
} else if (schemeId == 'https://aomedia.org/emsg/ID3') {
|
|
1866
|
-
// See https://aomediacodec.github.io/id3-emsg/
|
|
1867
|
-
const frames = shaka.util.Id3Utils.getID3Frames(messageData);
|
|
1868
|
-
if (frames.length && reference) {
|
|
1869
|
-
/** @private {shaka.extern.ID3Metadata} */
|
|
1870
|
-
const metadata = {
|
|
1871
|
-
cueTime: reference.startTime,
|
|
1872
|
-
data: messageData,
|
|
1873
|
-
frames: frames,
|
|
1874
|
-
dts: reference.startTime,
|
|
1875
|
-
pts: reference.startTime,
|
|
1876
|
-
};
|
|
1877
|
-
this.playerInterface_.onMetadata(
|
|
1878
|
-
[metadata], /* offset= */ 0, reference.endTime);
|
|
1879
|
-
}
|
|
1880
1868
|
} else {
|
|
1869
|
+
// All other schemes are dispatched as a general 'emsg' event.
|
|
1881
1870
|
/** @type {shaka.extern.EmsgInfo} */
|
|
1882
1871
|
const emsg = {
|
|
1883
1872
|
startTime: startTime,
|
|
@@ -1895,7 +1884,38 @@ shaka.media.StreamingEngine = class {
|
|
|
1895
1884
|
const eventName = shaka.util.FakeEvent.EventName.Emsg;
|
|
1896
1885
|
const data = (new Map()).set('detail', emsg);
|
|
1897
1886
|
const event = new shaka.util.FakeEvent(eventName, data);
|
|
1887
|
+
// A user can call preventDefault() on a cancelable event.
|
|
1888
|
+
event.cancelable = true;
|
|
1889
|
+
|
|
1898
1890
|
this.playerInterface_.onEvent(event);
|
|
1891
|
+
|
|
1892
|
+
if (event.defaultPrevented) {
|
|
1893
|
+
// If the caller uses preventDefault() on the 'emsg' event, don't
|
|
1894
|
+
// process any further, and don't generate an ID3 'metadata' event
|
|
1895
|
+
// for the same data.
|
|
1896
|
+
return;
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
|
+
// Additionally, ID3 events generate a 'metadata' event. This is a
|
|
1900
|
+
// pre-parsed version of the metadata blob already dispatched in the
|
|
1901
|
+
// 'emsg' event.
|
|
1902
|
+
if (schemeId == 'https://aomedia.org/emsg/ID3' ||
|
|
1903
|
+
schemeId == 'https://developer.apple.com/streaming/emsg-id3') {
|
|
1904
|
+
// See https://aomediacodec.github.io/id3-emsg/
|
|
1905
|
+
const frames = shaka.util.Id3Utils.getID3Frames(messageData);
|
|
1906
|
+
if (frames.length && reference) {
|
|
1907
|
+
/** @private {shaka.extern.ID3Metadata} */
|
|
1908
|
+
const metadata = {
|
|
1909
|
+
cueTime: reference.startTime,
|
|
1910
|
+
data: messageData,
|
|
1911
|
+
frames: frames,
|
|
1912
|
+
dts: reference.startTime,
|
|
1913
|
+
pts: reference.startTime,
|
|
1914
|
+
};
|
|
1915
|
+
this.playerInterface_.onMetadata(
|
|
1916
|
+
[metadata], /* offset= */ 0, reference.endTime);
|
|
1917
|
+
}
|
|
1918
|
+
}
|
|
1899
1919
|
}
|
|
1900
1920
|
}
|
|
1901
1921
|
}
|
|
@@ -27,15 +27,19 @@ shaka.media.VideoWrapper = class {
|
|
|
27
27
|
/**
|
|
28
28
|
* @param {!HTMLMediaElement} video
|
|
29
29
|
* @param {function()} onSeek Called when the video seeks.
|
|
30
|
+
* @param {function(number)} onStarted Called when the video has started.
|
|
30
31
|
* @param {number} startTime The time to start at.
|
|
31
32
|
*/
|
|
32
|
-
constructor(video, onSeek, startTime) {
|
|
33
|
+
constructor(video, onSeek, onStarted, startTime) {
|
|
33
34
|
/** @private {HTMLMediaElement} */
|
|
34
35
|
this.video_ = video;
|
|
35
36
|
|
|
36
37
|
/** @private {function()} */
|
|
37
38
|
this.onSeek_ = onSeek;
|
|
38
39
|
|
|
40
|
+
/** @private {function(number)} */
|
|
41
|
+
this.onStarted_ = onStarted;
|
|
42
|
+
|
|
39
43
|
/** @private {number} */
|
|
40
44
|
this.startTime_ = startTime;
|
|
41
45
|
|
|
@@ -165,6 +169,7 @@ shaka.media.VideoWrapper = class {
|
|
|
165
169
|
this.started_ = true;
|
|
166
170
|
|
|
167
171
|
this.eventManager_.listen(this.video_, 'seeking', () => this.onSeek_());
|
|
172
|
+
this.onStarted_(this.video_.currentTime);
|
|
168
173
|
}
|
|
169
174
|
};
|
|
170
175
|
|