shaka-player 4.12.6 → 4.12.7

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.
@@ -60,7 +60,7 @@ shaka.cea.Cea708Service = class {
60
60
  // Control codes are in 1 of 4 logical groups:
61
61
  // CL (C0, C2), CR (C1, C3), GL (G0, G2), GR (G1, G2).
62
62
  if (controlCode >= 0x00 && controlCode <= 0x1f) {
63
- return this.handleC0_(controlCode, pts);
63
+ return this.handleC0_(dtvccPacket, controlCode, pts);
64
64
  } else if (controlCode >= 0x80 && controlCode <= 0x9f) {
65
65
  return this.handleC1_(dtvccPacket, controlCode, pts);
66
66
  } else if (controlCode >= 0x1000 && controlCode <= 0x101f) {
@@ -153,17 +153,42 @@ shaka.cea.Cea708Service = class {
153
153
 
154
154
  /**
155
155
  * Handles C0 group data.
156
+ * @param {!shaka.cea.DtvccPacket} dtvccPacket
156
157
  * @param {number} controlCode
157
158
  * @param {number} pts
158
159
  * @return {?shaka.extern.ICaptionDecoder.ClosedCaption}
159
160
  * @private
160
161
  */
161
- handleC0_(controlCode, pts) {
162
+ handleC0_(dtvccPacket, controlCode, pts) {
162
163
  // All these commands pertain to the current window, so ensure it exists.
163
164
  if (!this.currentWindow_) {
164
165
  return null;
165
166
  }
166
167
 
168
+ if (controlCode == 0x18) {
169
+ const firstByte = dtvccPacket.readByte().value;
170
+ const secondByte = dtvccPacket.readByte().value;
171
+
172
+ const isTextBlock = (b) => {
173
+ return (b >= 0x20 && b <= 0x7f) || (b >= 0xa0 && b <= 0xff);
174
+ };
175
+
176
+ if (isTextBlock(firstByte) && isTextBlock(secondByte)) {
177
+ const toHexString = (byteArray) => {
178
+ return byteArray.map((byte) => {
179
+ return ('0' + (byte & 0xFF).toString(16)).slice(-2);
180
+ }).join('');
181
+ };
182
+ const unicode = toHexString([firstByte, secondByte]);
183
+ // Takes a unicode hex string and creates a single character.
184
+ const char = String.fromCharCode(parseInt(unicode, 16));
185
+ this.currentWindow_.setCharacter(char);
186
+ return null;
187
+ } else {
188
+ dtvccPacket.rewind(2);
189
+ }
190
+ }
191
+
167
192
  const window = this.currentWindow_;
168
193
  let parsedClosedCaption = null;
169
194
 
@@ -161,6 +161,21 @@ shaka.cea.DtvccPacket = class {
161
161
  }
162
162
  this.pos_ += numBlocks;
163
163
  }
164
+
165
+ /**
166
+ * Rewinds the provided number of blocks in the buffer.
167
+ * @param {number} numBlocks
168
+ * @throws {!shaka.util.Error}
169
+ */
170
+ rewind(numBlocks) {
171
+ if (this.pos_ - numBlocks < 0) {
172
+ throw new shaka.util.Error(
173
+ shaka.util.Error.Severity.CRITICAL,
174
+ shaka.util.Error.Category.TEXT,
175
+ shaka.util.Error.Code.BUFFER_READ_OUT_OF_BOUNDS);
176
+ }
177
+ this.pos_ -= numBlocks;
178
+ }
164
179
  };
165
180
 
166
181
  /**
@@ -2295,8 +2295,12 @@ shaka.dash.DashParser = class {
2295
2295
  return element.attributes['schemeIdUri'] == expectedUri &&
2296
2296
  element.attributes['value'] == expectedValue;
2297
2297
  });
2298
+ const codecs = context.representation.codecs;
2299
+ // Detect the presence of Dolby Atmos audio content based on the codec and
2300
+ // bandwidth. Bandwidth above 384 kbps is a good indicator of Atmos content.
2301
+ const isAtmos = codecs.includes('ec-3') && context.bandwidth >= 384000;
2298
2302
  let spatialAudio = false;
2299
- if (hasJoc) {
2303
+ if (hasJoc || isAtmos) {
2300
2304
  spatialAudio = true;
2301
2305
  }
2302
2306
 
@@ -2328,7 +2332,6 @@ shaka.dash.DashParser = class {
2328
2332
 
2329
2333
  let hdr;
2330
2334
  const profiles = context.profiles;
2331
- const codecs = context.representation.codecs;
2332
2335
 
2333
2336
  const hevcHDR = 'http://dashif.org/guidelines/dash-if-uhd#hevc-hdr-pq10';
2334
2337
  if (profiles.includes(hevcHDR) && (codecs.includes('hvc1.2.4.L153.B0') ||
@@ -279,10 +279,9 @@ shaka.dash.MpdUtils = class {
279
279
 
280
280
  const durationStr =
281
281
  MpdUtils.inheritAttribute(context, callback, 'duration');
282
- let segmentDuration = TXml.parsePositiveInt(durationStr || '');
283
- if (segmentDuration) {
284
- segmentDuration /= timescale;
285
- }
282
+ const unscaledSegmentDuration = TXml.parsePositiveInt(durationStr || '');
283
+ const segmentDuration = unscaledSegmentDuration ?
284
+ unscaledSegmentDuration / timescale : null;
286
285
 
287
286
  const startNumberStr =
288
287
  MpdUtils.inheritAttribute(context, callback, 'startNumber');
@@ -309,6 +308,7 @@ shaka.dash.MpdUtils = class {
309
308
  (unscaledPresentationTimeOffset / timescale) || 0;
310
309
  return {
311
310
  timescale: timescale,
311
+ unscaledSegmentDuration: unscaledSegmentDuration,
312
312
  segmentDuration: segmentDuration,
313
313
  startNumber: startNumber,
314
314
  scaledPresentationTimeOffset: scaledPresentationTimeOffset,
@@ -564,6 +564,7 @@ shaka.dash.MpdUtils = class {
564
564
  /**
565
565
  * @typedef {{
566
566
  * timescale: number,
567
+ * unscaledSegmentDuration: ?number,
567
568
  * segmentDuration: ?number,
568
569
  * startNumber: number,
569
570
  * scaledPresentationTimeOffset: number,
@@ -576,6 +577,8 @@ shaka.dash.MpdUtils = class {
576
577
  *
577
578
  * @property {number} timescale
578
579
  * The time-scale of the representation.
580
+ * @property {?number} unscaledSegmentDuration
581
+ * The duration of the segments in timescale units, if given.
579
582
  * @property {?number} segmentDuration
580
583
  * The duration of the segments in seconds, if given.
581
584
  * @property {number} startNumber
@@ -277,6 +277,7 @@ shaka.dash.SegmentTemplate = class {
277
277
  }
278
278
 
279
279
  return {
280
+ unscaledSegmentDuration: segmentInfo.unscaledSegmentDuration,
280
281
  segmentDuration: segmentInfo.segmentDuration,
281
282
  timescale: segmentInfo.timescale,
282
283
  startNumber: segmentInfo.startNumber,
@@ -325,10 +326,12 @@ shaka.dash.SegmentTemplate = class {
325
326
  if (info.indexTemplate) {
326
327
  shaka.log.info('Using the index URL template by default.');
327
328
  info.timeline = null;
329
+ info.unscaledSegmentDuration = null;
328
330
  info.segmentDuration = null;
329
331
  } else {
330
332
  goog.asserts.assert(info.timeline, 'There should be a timeline');
331
333
  shaka.log.info('Using the SegmentTimeline by default.');
334
+ info.unscaledSegmentDuration = null;
332
335
  info.segmentDuration = null;
333
336
  }
334
337
  }
@@ -421,7 +424,6 @@ shaka.dash.SegmentTemplate = class {
421
424
  segmentDuration != null, 'Segment duration must not be null!');
422
425
 
423
426
  const startNumber = info.startNumber;
424
- const timescale = info.timescale;
425
427
 
426
428
  const template = info.mediaTemplate;
427
429
  const bandwidth = context.bandwidth || null;
@@ -505,10 +507,18 @@ shaka.dash.SegmentTemplate = class {
505
507
  const positionWithinPeriod = position - startNumber;
506
508
  const segmentPeriodTime = positionWithinPeriod * segmentDuration;
507
509
 
508
- // What will appear in the actual segment files. The media timestamp is
509
- // what is expected in the $Time$ template.
510
- const segmentMediaTime = segmentPeriodTime +
511
- info.scaledPresentationTimeOffset;
510
+ const unscaledSegmentDuration = info.unscaledSegmentDuration;
511
+ goog.asserts.assert(unscaledSegmentDuration != null,
512
+ 'Segment duration must not be null!');
513
+
514
+ // The original media timestamp from the timeline is what is expected in
515
+ // the $Time$ template. (Or based on duration, in this case.) It should
516
+ // not be adjusted with presentationTimeOffset or the Period start.
517
+ let timeReplacement = positionWithinPeriod * unscaledSegmentDuration;
518
+ if ('BigInt' in window && timeReplacement > Number.MAX_SAFE_INTEGER) {
519
+ timeReplacement =
520
+ BigInt(positionWithinPeriod) * BigInt(unscaledSegmentDuration);
521
+ }
512
522
 
513
523
  // Relative to the presentation.
514
524
  const segmentStart = segmentPeriodTime + periodStart;
@@ -533,12 +543,8 @@ shaka.dash.SegmentTemplate = class {
533
543
  const end = start + partialDuration;
534
544
  const subNumber = i + 1;
535
545
  const getPartialUris = () => {
536
- let time = segmentMediaTime * timescale;
537
- if ('BigInt' in window && time > Number.MAX_SAFE_INTEGER) {
538
- time = BigInt(segmentMediaTime) * BigInt(timescale);
539
- }
540
546
  const mediaUri = MpdUtils.fillUriTemplate(
541
- template, id, position, subNumber, bandwidth, time);
547
+ template, id, position, subNumber, bandwidth, timeReplacement);
542
548
  return ManifestParserUtils.resolveUris(
543
549
  getBaseUris(), [mediaUri], urlParams());
544
550
  };
@@ -575,12 +581,9 @@ shaka.dash.SegmentTemplate = class {
575
581
  if (numChunks) {
576
582
  return [];
577
583
  }
578
- let time = segmentMediaTime * timescale;
579
- if ('BigInt' in window && time > Number.MAX_SAFE_INTEGER) {
580
- time = BigInt(segmentMediaTime) * BigInt(timescale);
581
- }
582
584
  const mediaUri = MpdUtils.fillUriTemplate(
583
- template, id, position, /* subNumber= */ null, bandwidth, time);
585
+ template, id, position, /* subNumber= */ null, bandwidth,
586
+ timeReplacement);
584
587
  return ManifestParserUtils.resolveUris(
585
588
  getBaseUris(), [mediaUri], urlParams());
586
589
  };
@@ -1007,8 +1010,15 @@ shaka.dash.TimelineSegmentIndex = class extends shaka.media.SegmentIndex {
1007
1010
  if (!ref) {
1008
1011
  const range = this.templateInfo_.timeline[correctedPosition];
1009
1012
  const segmentReplacement = range.segmentPosition;
1010
- const timeReplacement = this.templateInfo_
1011
- .unscaledPresentationTimeOffset + range.unscaledStart;
1013
+
1014
+ // The original media timestamp from the timeline is what is expected in
1015
+ // the $Time$ template. It should not be adjusted with
1016
+ // presentationTimeOffset or the Period start, but
1017
+ // unscaledPresentationTimeOffset was already subtracted from the times
1018
+ // in timeline.
1019
+ const timeReplacement = range.unscaledStart +
1020
+ this.templateInfo_.unscaledPresentationTimeOffset;
1021
+
1012
1022
  const timestampOffset = this.periodStart_ -
1013
1023
  this.templateInfo_.scaledPresentationTimeOffset;
1014
1024
  const trueSegmentEnd = this.periodStart_ + range.end;
@@ -1155,6 +1165,7 @@ shaka.dash.TimelineSegmentIndex = class extends shaka.media.SegmentIndex {
1155
1165
  /**
1156
1166
  * @typedef {{
1157
1167
  * timescale: number,
1168
+ * unscaledSegmentDuration: ?number,
1158
1169
  * segmentDuration: ?number,
1159
1170
  * startNumber: number,
1160
1171
  * scaledPresentationTimeOffset: number,
@@ -1173,6 +1184,8 @@ shaka.dash.TimelineSegmentIndex = class extends shaka.media.SegmentIndex {
1173
1184
  *
1174
1185
  * @property {number} timescale
1175
1186
  * The time-scale of the representation.
1187
+ * @property {?number} unscaledSegmentDuration
1188
+ * The duration of the segments in seconds, in timescale units.
1176
1189
  * @property {?number} segmentDuration
1177
1190
  * The duration of the segments in seconds, if given.
1178
1191
  * @property {number} startNumber
@@ -1137,7 +1137,8 @@ shaka.hls.HlsParser = class {
1137
1137
  if (shaka.util.TsParser.probe(
1138
1138
  shaka.util.BufferUtils.toUint8(data))) {
1139
1139
  const basicInfo = shaka.media.SegmentUtils.getBasicInfoFromTs(
1140
- data, this.config_.disableAudio, this.config_.disableVideo);
1140
+ data, this.config_.disableAudio, this.config_.disableVideo,
1141
+ this.config_.disableText);
1141
1142
  if (basicInfo) {
1142
1143
  return basicInfo;
1143
1144
  }
@@ -1145,7 +1146,7 @@ shaka.hls.HlsParser = class {
1145
1146
  validMp4MimeType.includes(contentMimeType) ||
1146
1147
  (initMimeType && validMp4MimeType.includes(initMimeType))) {
1147
1148
  const basicInfo = shaka.media.SegmentUtils.getBasicInfoFromMp4(
1148
- initData, data);
1149
+ initData, data, this.config_.disableText);
1149
1150
  if (basicInfo) {
1150
1151
  return basicInfo;
1151
1152
  }
@@ -3986,7 +3987,9 @@ shaka.hls.HlsParser = class {
3986
3987
  }
3987
3988
  let dateRangeTags =
3988
3989
  shaka.hls.Utils.filterTagsByName(tags, 'EXT-X-DATERANGE');
3989
- dateRangeTags = dateRangeTags.sort((a, b) => {
3990
+ dateRangeTags = dateRangeTags.filter((tag) => {
3991
+ return tag.getAttribute('START-DATE') != null;
3992
+ }).sort((a, b) => {
3990
3993
  const aStartDateValue = a.getRequiredAttrValue('START-DATE');
3991
3994
  const bStartDateValue = b.getRequiredAttrValue('START-DATE');
3992
3995
  if (aStartDateValue < bStartDateValue) {
@@ -941,14 +941,18 @@ shaka.media.MediaSourceEngine = class {
941
941
  .box('moof', Mp4Parser.children)
942
942
  .box('traf', Mp4Parser.children)
943
943
  .fullBox('tfdt', (box) => {
944
- goog.asserts.assert(
945
- box.version == 0 || box.version == 1,
946
- 'TFDT version can only be 0 or 1');
947
- const parsed = shaka.util.Mp4BoxParsers.parseTFDTInaccurate(
948
- box.reader, box.version);
949
- startTime = parsed.baseMediaDecodeTime / timescale;
950
- parsedMedia = true;
951
- box.parser.stop();
944
+ if (!parsedMedia) {
945
+ goog.asserts.assert(
946
+ box.version == 0 || box.version == 1,
947
+ 'TFDT version can only be 0 or 1');
948
+ const parsed = shaka.util.Mp4BoxParsers.parseTFDTInaccurate(
949
+ box.reader, box.version);
950
+ startTime = parsed.baseMediaDecodeTime / timescale;
951
+ parsedMedia = true;
952
+ if (!hasEmsg) {
953
+ box.parser.stop();
954
+ }
955
+ }
952
956
  }).parse(data, /* partialOkay= */ true);
953
957
  if (parsedMedia && reference.timestampOffset == 0) {
954
958
  timestamp = startTime;
@@ -48,11 +48,12 @@ shaka.media.SegmentUtils = class {
48
48
 
49
49
  /**
50
50
  * @param {!BufferSource} data
51
- * @param {boolean=} disableAudio
52
- * @param {boolean=} disableVideo
51
+ * @param {boolean} disableAudio
52
+ * @param {boolean} disableVideo
53
+ * @param {boolean} disableText
53
54
  * @return {?shaka.media.SegmentUtils.BasicInfo}
54
55
  */
55
- static getBasicInfoFromTs(data, disableAudio = false, disableVideo = false) {
56
+ static getBasicInfoFromTs(data, disableAudio, disableVideo, disableText) {
56
57
  const uint8ArrayData = shaka.util.BufferUtils.toUint8(data);
57
58
  const tsParser = new shaka.util.TsParser().parse(uint8ArrayData);
58
59
  const tsCodecs = tsParser.getCodecs();
@@ -64,24 +65,34 @@ shaka.media.SegmentUtils = class {
64
65
  switch (tsCodecs.audio) {
65
66
  case 'aac':
66
67
  case 'aac-loas':
67
- codecs.push('mp4a.40.2');
68
- hasAudio = true;
68
+ if (tsParser.getAudioData().length) {
69
+ codecs.push('mp4a.40.2');
70
+ hasAudio = true;
71
+ }
69
72
  break;
70
73
  case 'mp3':
71
- codecs.push('mp4a.40.34');
72
- hasAudio = true;
74
+ if (tsParser.getAudioData().length) {
75
+ codecs.push('mp4a.40.34');
76
+ hasAudio = true;
77
+ }
73
78
  break;
74
79
  case 'ac3':
75
- codecs.push('ac-3');
76
- hasAudio = true;
80
+ if (tsParser.getAudioData().length) {
81
+ codecs.push('ac-3');
82
+ hasAudio = true;
83
+ }
77
84
  break;
78
85
  case 'ec3':
79
- codecs.push('ec-3');
80
- hasAudio = true;
86
+ if (tsParser.getAudioData().length) {
87
+ codecs.push('ec-3');
88
+ hasAudio = true;
89
+ }
81
90
  break;
82
91
  case 'opus':
83
- codecs.push('opus');
84
- hasAudio = true;
92
+ if (tsParser.getAudioData().length) {
93
+ codecs.push('opus');
94
+ hasAudio = true;
95
+ }
85
96
  break;
86
97
  }
87
98
  }
@@ -114,7 +125,7 @@ shaka.media.SegmentUtils = class {
114
125
  }
115
126
  const onlyAudio = hasAudio && !hasVideo;
116
127
  const closedCaptions = new Map();
117
- if (hasVideo) {
128
+ if (hasVideo && !disableText) {
118
129
  const captionParser = new shaka.media.ClosedCaptionParser('video/mp2t');
119
130
  captionParser.parseFrom(data);
120
131
  for (const stream of captionParser.getStreams()) {
@@ -141,9 +152,10 @@ shaka.media.SegmentUtils = class {
141
152
  /**
142
153
  * @param {?BufferSource} initData
143
154
  * @param {!BufferSource} data
155
+ * @param {boolean} disableText
144
156
  * @return {?shaka.media.SegmentUtils.BasicInfo}
145
157
  */
146
- static getBasicInfoFromMp4(initData, data) {
158
+ static getBasicInfoFromMp4(initData, data, disableText) {
147
159
  const Mp4Parser = shaka.util.Mp4Parser;
148
160
  const SegmentUtils = shaka.media.SegmentUtils;
149
161
 
@@ -418,7 +430,7 @@ shaka.media.SegmentUtils = class {
418
430
  }
419
431
  const onlyAudio = hasAudio && !hasVideo;
420
432
  const closedCaptions = new Map();
421
- if (hasVideo) {
433
+ if (hasVideo && !disableText) {
422
434
  const captionParser = new shaka.media.ClosedCaptionParser('video/mp4');
423
435
  if (initData) {
424
436
  captionParser.init(initData);
@@ -109,7 +109,7 @@ shaka.net.HttpFetchPlugin = class {
109
109
  const fetch = shaka.net.HttpFetchPlugin.fetch_;
110
110
  const ReadableStream = shaka.net.HttpFetchPlugin.ReadableStream_;
111
111
  let response;
112
- let arrayBuffer;
112
+ let arrayBuffer = new ArrayBuffer(0);
113
113
  let loaded = 0;
114
114
  let lastLoaded = 0;
115
115
 
@@ -6,6 +6,7 @@
6
6
 
7
7
  goog.provide('shaka.net.HttpPluginUtils');
8
8
 
9
+ goog.require('goog.asserts');
9
10
  goog.require('shaka.log');
10
11
  goog.require('shaka.util.Error');
11
12
  goog.require('shaka.util.StringUtils');
@@ -27,6 +28,8 @@ shaka.net.HttpPluginUtils = class {
27
28
  * @return {!shaka.extern.Response}
28
29
  */
29
30
  static makeResponse(headers, data, status, uri, responseURL, requestType) {
31
+ goog.asserts.assert(data, 'Data should be non-null!');
32
+
30
33
  if (status >= 200 && status <= 299 && status != 202) {
31
34
  // Most 2xx HTTP codes are success cases.
32
35
  /** @type {shaka.extern.Response} */
package/lib/player.js CHANGED
@@ -5238,10 +5238,11 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
5238
5238
  * @param {number=} channelsCount
5239
5239
  * @param {number=} safeMargin
5240
5240
  * @param {string=} codec
5241
+ * @param {boolean=} spatialAudio
5241
5242
  * @export
5242
5243
  */
5243
5244
  selectAudioLanguage(language, role, channelsCount = 0, safeMargin = 0,
5244
- codec = '') {
5245
+ codec = '', spatialAudio = false) {
5245
5246
  const selectMediaSourceMode = () => {
5246
5247
  this.currentAdaptationSetCriteria_ =
5247
5248
  new shaka.media.PreferenceBasedCriteria(
@@ -5249,7 +5250,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
5249
5250
  role || '',
5250
5251
  channelsCount || 0,
5251
5252
  /* hdrLevel= */ '',
5252
- /* spatialAudio= */ false,
5253
+ spatialAudio || false,
5253
5254
  /* videoLayout= */ '',
5254
5255
  /* audioLabel= */ '',
5255
5256
  /* videoLabel= */ '',
@@ -8298,7 +8299,7 @@ shaka.Player.TYPICAL_BUFFERING_THRESHOLD_ = 0.5;
8298
8299
  * @export
8299
8300
  */
8300
8301
  // eslint-disable-next-line no-useless-concat, max-len
8301
- shaka.Player.version = 'v4.12.6' + '-uncompiled'; // x-release-please-version
8302
+ shaka.Player.version = 'v4.12.7' + '-uncompiled'; // x-release-please-version
8302
8303
 
8303
8304
  // Initialize the deprecation system using the version string we just set
8304
8305
  // on the player.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "shaka-player",
3
3
  "description": "DASH/EME video player library",
4
- "version": "4.12.6",
4
+ "version": "4.12.7",
5
5
  "homepage": "https://github.com/shaka-project/shaka-player",
6
6
  "author": "Google",
7
7
  "maintainers": [
@@ -107,7 +107,7 @@ shaka.ui.AudioLanguageSelection = class extends shaka.ui.SettingsMenu {
107
107
  codec = track.audioCodec;
108
108
  }
109
109
  this.player.selectAudioLanguage(track.language, track.roles[0],
110
- channelsCount, /* safeMargin= */ 0, codec);
110
+ channelsCount, /* safeMargin= */ 0, codec, track.spatialAudio);
111
111
  }
112
112
 
113
113