shaka-player 4.8.16 → 4.8.18

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.
@@ -164,53 +164,62 @@ shaka.cea.Mp4CeaParser = class {
164
164
  /** @type {!Array<!shaka.extern.ICeaParser.CaptionPacket>} **/
165
165
  const captionPackets = [];
166
166
 
167
- // Fields that are found in MOOF boxes
168
- let defaultSampleDuration = this.defaultSampleDuration_;
169
- let defaultSampleSize = this.defaultSampleSize_;
170
167
  let moofOffset = 0;
171
- /** @type {!Array<shaka.util.ParsedTRUNBox>} */
172
- let parsedTRUNs = [];
173
- let baseMediaDecodeTime = null;
174
- let timescale = shaka.cea.CeaUtils.DEFAULT_TIMESCALE_VALUE;
168
+
169
+ /** @type {!Array<!shaka.cea.Mp4CeaParser.ParsedTRAF>} */
170
+ let parsedTRAFs = [];
175
171
 
176
172
  new Mp4Parser()
177
173
  .box('moof', (box) => {
178
174
  moofOffset = box.start;
179
- // trun box parsing is reset on each moof.
180
- parsedTRUNs = [];
175
+ // traf box parsing is reset on each moof.
176
+ parsedTRAFs = [];
177
+ Mp4Parser.children(box);
178
+ })
179
+ .box('traf', (box) => {
180
+ parsedTRAFs.push({
181
+ baseMediaDecodeTime: null,
182
+ defaultSampleDuration: this.defaultSampleDuration_,
183
+ defaultSampleSize: this.defaultSampleSize_,
184
+ parsedTRUNs: [],
185
+ timescale: shaka.cea.CeaUtils.DEFAULT_TIMESCALE_VALUE,
186
+ });
181
187
  Mp4Parser.children(box);
182
188
  })
183
- .box('traf', Mp4Parser.children)
184
189
  .fullBox('trun', (box) => {
185
190
  goog.asserts.assert(
186
- box.version != null && box.flags!=null,
191
+ box.version != null && box.flags != null,
187
192
  'TRUN is a full box and should have a valid version & flags.');
188
193
 
194
+ const lastTRAF = parsedTRAFs[parsedTRAFs.length - 1];
195
+
189
196
  const parsedTRUN = shaka.util.Mp4BoxParsers.parseTRUN(
190
197
  box.reader, box.version, box.flags);
191
- parsedTRUNs.push(parsedTRUN);
198
+ lastTRAF.parsedTRUNs.push(parsedTRUN);
192
199
  })
193
200
  .fullBox('tfhd', (box) => {
194
201
  goog.asserts.assert(
195
202
  box.flags != null,
196
203
  'TFHD is a full box and should have valid flags.');
197
204
 
205
+ const lastTRAF = parsedTRAFs[parsedTRAFs.length - 1];
206
+
198
207
  const parsedTFHD = shaka.util.Mp4BoxParsers.parseTFHD(
199
208
  box.reader, box.flags);
200
209
 
201
210
  // If specified, defaultSampleDuration and defaultSampleSize
202
211
  // override the ones specified in the TREX box
203
- defaultSampleDuration = parsedTFHD.defaultSampleDuration ||
212
+ lastTRAF.defaultSampleDuration = parsedTFHD.defaultSampleDuration ||
204
213
  this.defaultSampleDuration_;
205
214
 
206
- defaultSampleSize = parsedTFHD.defaultSampleSize ||
215
+ lastTRAF.defaultSampleSize = parsedTFHD.defaultSampleSize ||
207
216
  this.defaultSampleSize_;
208
217
 
209
218
  const trackId = parsedTFHD.trackId;
210
219
 
211
220
  // Get the timescale from the track Id
212
221
  if (this.trackIdToTimescale_.has(trackId)) {
213
- timescale = this.trackIdToTimescale_.get(trackId);
222
+ lastTRAF.timescale = this.trackIdToTimescale_.get(trackId);
214
223
  }
215
224
  })
216
225
  .fullBox('tfdt', (box) => {
@@ -218,27 +227,37 @@ shaka.cea.Mp4CeaParser = class {
218
227
  box.version != null,
219
228
  'TFDT is a full box and should have a valid version.');
220
229
 
230
+ const lastTRAF = parsedTRAFs[parsedTRAFs.length - 1];
231
+
221
232
  const parsedTFDT = shaka.util.Mp4BoxParsers.parseTFDTInaccurate(
222
233
  box.reader, box.version);
223
234
 
224
- baseMediaDecodeTime = parsedTFDT.baseMediaDecodeTime;
235
+ lastTRAF.baseMediaDecodeTime = parsedTFDT.baseMediaDecodeTime;
225
236
  })
226
237
  .box('mdat', (box) => {
227
- if (baseMediaDecodeTime === null) {
228
- // This field should have been populated by the Base Media Decode
229
- // Time in the tfdt box.
230
- shaka.log.alwaysWarn(
231
- 'Unable to find base media decode time for CEA captions!');
232
- throw new shaka.util.Error(
233
- shaka.util.Error.Severity.CRITICAL,
234
- shaka.util.Error.Category.TEXT,
235
- shaka.util.Error.Code.INVALID_MP4_CEA);
236
- }
237
-
238
238
  const offset = moofOffset - box.start - 8;
239
- this.parseMdat_(box.reader, baseMediaDecodeTime, timescale,
240
- defaultSampleDuration, defaultSampleSize, offset, parsedTRUNs,
241
- captionPackets);
239
+ const initialPosition = box.reader.getPosition();
240
+ for (const parsedTRAF of parsedTRAFs) {
241
+ if (parsedTRAF.baseMediaDecodeTime === null) {
242
+ // This field should have been populated by the Base Media Decode
243
+ // Time in the tfdt box.
244
+ shaka.log.alwaysWarn(
245
+ 'Unable to find base media decode time for CEA captions!');
246
+ throw new shaka.util.Error(
247
+ shaka.util.Error.Severity.CRITICAL,
248
+ shaka.util.Error.Category.TEXT,
249
+ shaka.util.Error.Code.INVALID_MP4_CEA);
250
+ }
251
+ box.reader.seek(initialPosition);
252
+ this.parseMdat_(box.reader,
253
+ parsedTRAF.baseMediaDecodeTime,
254
+ parsedTRAF.timescale,
255
+ parsedTRAF.defaultSampleDuration,
256
+ parsedTRAF.defaultSampleSize,
257
+ offset,
258
+ parsedTRAF.parsedTRUNs,
259
+ captionPackets);
260
+ }
242
261
  })
243
262
  .parse(mediaSegment, /* partialOkay= */ false);
244
263
 
@@ -315,7 +334,7 @@ shaka.cea.Mp4CeaParser = class {
315
334
  timeOffset = sampleData[sampleIndex].sampleCompositionTimeOffset || 0;
316
335
  }
317
336
 
318
- const pts = (time + timeOffset)/timescale;
337
+ const pts = (time + timeOffset) / timescale;
319
338
  for (const packet of this.seiProcessor_
320
339
  .process(reader.readBytes(naluSize - naluHeaderSize))) {
321
340
  captionPackets.push({
@@ -384,5 +403,22 @@ shaka.cea.Mp4CeaParser.CodecBitstreamMap_ = {
384
403
  'dvhe': shaka.cea.Mp4CeaParser.BitstreamFormat.H265,
385
404
  };
386
405
 
406
+ /**
407
+ * @typedef {{
408
+ * baseMediaDecodeTime: ?number,
409
+ * defaultSampleDuration: number,
410
+ * defaultSampleSize: number,
411
+ * parsedTRUNs: !Array<shaka.util.ParsedTRUNBox>,
412
+ * timescale: number
413
+ * }}
414
+ *
415
+ * @property {?number} baseMediaDecodeTime
416
+ * @property {number} defaultSampleDuration
417
+ * @property {number} defaultSampleSize
418
+ * @property {!Array<shaka.util.ParsedTRUNBox>} parsedTRUNs
419
+ * @property {?number} timescale
420
+ */
421
+ shaka.cea.Mp4CeaParser.ParsedTRAF;
422
+
387
423
  shaka.media.ClosedCaptionParser.registerParser('video/mp4',
388
424
  () => new shaka.cea.Mp4CeaParser());
@@ -3888,9 +3888,14 @@ shaka.hls.HlsParser = class {
3888
3888
  'Playlist should have segments!');
3889
3889
  const middleSegmentIdx = Math.trunc((playlist.segments.length - 1) / 2);
3890
3890
 
3891
+ const middleSegment = playlist.segments[middleSegmentIdx];
3892
+ if (shaka.hls.Utils.getFirstTagWithName(middleSegment.tags, 'EXT-X-GAP')) {
3893
+ return this.guessMimeTypeFallback_(contentType);
3894
+ }
3895
+
3891
3896
  const middleSegmentUris = shaka.hls.Utils.constructSegmentUris(
3892
3897
  getUris(),
3893
- playlist.segments[middleSegmentIdx].verbatimSegmentUri,
3898
+ middleSegment.verbatimSegmentUri,
3894
3899
  variables);
3895
3900
 
3896
3901
  const parsedUri = new goog.Uri(middleSegmentUris[0]);
@@ -312,4 +312,5 @@ shaka.hls.ManifestTextParser.SEGMENT_TAGS = [
312
312
  'EXT-X-KEY',
313
313
  'EXT-X-DATERANGE',
314
314
  'EXT-X-MAP',
315
+ 'EXT-X-GAP',
315
316
  ];
@@ -177,6 +177,7 @@ shaka.media.SegmentUtils = class {
177
177
  break;
178
178
  case 'ac-3':
179
179
  case 'ec-3':
180
+ case 'ac-4':
180
181
  case 'opus':
181
182
  case 'flac':
182
183
  audioCodecs.push(codecLC);
@@ -203,6 +204,19 @@ shaka.media.SegmentUtils = class {
203
204
  /** @type {?string} */
204
205
  let baseBox;
205
206
 
207
+ const genericAudioBox = (box) => {
208
+ const parsedAudioSampleEntryBox =
209
+ shaka.util.Mp4BoxParsers.audioSampleEntry(box.reader);
210
+ channelCount = parsedAudioSampleEntryBox.channelCount;
211
+ sampleRate = parsedAudioSampleEntryBox.sampleRate;
212
+ codecBoxParser(box);
213
+ };
214
+
215
+ const genericVideoBox = (box) => {
216
+ baseBox = box.name;
217
+ Mp4Parser.visualSampleEntry(box);
218
+ };
219
+
206
220
  new Mp4Parser()
207
221
  .box('moov', Mp4Parser.children)
208
222
  .box('trak', Mp4Parser.children)
@@ -231,9 +245,10 @@ shaka.media.SegmentUtils = class {
231
245
  // AUDIO
232
246
  // These are the various boxes that signal a codec.
233
247
  .box('mp4a', (box) => {
234
- const parsedMP4ABox = shaka.util.Mp4BoxParsers.parseMP4A(box.reader);
235
- channelCount = parsedMP4ABox.channelCount;
236
- sampleRate = parsedMP4ABox.sampleRate;
248
+ const parsedAudioSampleEntryBox =
249
+ shaka.util.Mp4BoxParsers.audioSampleEntry(box.reader);
250
+ channelCount = parsedAudioSampleEntryBox.channelCount;
251
+ sampleRate = parsedAudioSampleEntryBox.sampleRate;
237
252
  if (box.reader.hasMoreData()) {
238
253
  Mp4Parser.children(box);
239
254
  } else {
@@ -245,54 +260,24 @@ shaka.media.SegmentUtils = class {
245
260
  audioCodecs.push(parsedESDSBox.codec);
246
261
  hasAudio = true;
247
262
  })
248
- .box('ac-3', codecBoxParser)
249
- .box('ec-3', codecBoxParser)
250
- .box('opus', codecBoxParser)
251
- .box('Opus', codecBoxParser)
252
- .box('fLaC', codecBoxParser)
263
+ .box('ac-3', genericAudioBox)
264
+ .box('ec-3', genericAudioBox)
265
+ .box('ac-4', genericAudioBox)
266
+ .box('Opus', genericAudioBox)
267
+ .box('fLaC', genericAudioBox)
253
268
 
254
269
  // VIDEO
255
270
  // These are the various boxes that signal a codec.
256
- .box('avc1', (box) => {
257
- baseBox = box.name;
258
- Mp4Parser.visualSampleEntry(box);
259
- })
260
- .box('avc3', (box) => {
261
- baseBox = box.name;
262
- Mp4Parser.visualSampleEntry(box);
263
- })
264
- .box('hev1', (box) => {
265
- baseBox = box.name;
266
- Mp4Parser.visualSampleEntry(box);
267
- })
268
- .box('hvc1', (box) => {
269
- baseBox = box.name;
270
- Mp4Parser.visualSampleEntry(box);
271
- })
272
- .box('dva1', (box) => {
273
- baseBox = box.name;
274
- Mp4Parser.visualSampleEntry(box);
275
- })
276
- .box('dvav', (box) => {
277
- baseBox = box.name;
278
- Mp4Parser.visualSampleEntry(box);
279
- })
280
- .box('dvh1', (box) => {
281
- baseBox = box.name;
282
- Mp4Parser.visualSampleEntry(box);
283
- })
284
- .box('dvhe', (box) => {
285
- baseBox = box.name;
286
- Mp4Parser.visualSampleEntry(box);
287
- })
288
- .box('vp09', (box) => {
289
- baseBox = box.name;
290
- Mp4Parser.visualSampleEntry(box);
291
- })
292
- .box('av01', (box) => {
293
- baseBox = box.name;
294
- Mp4Parser.visualSampleEntry(box);
295
- })
271
+ .box('avc1', genericVideoBox)
272
+ .box('avc3', genericVideoBox)
273
+ .box('hev1', genericVideoBox)
274
+ .box('hvc1', genericVideoBox)
275
+ .box('dva1', genericVideoBox)
276
+ .box('dvav', genericVideoBox)
277
+ .box('dvh1', genericVideoBox)
278
+ .box('dvhe', genericVideoBox)
279
+ .box('vp09', genericVideoBox)
280
+ .box('av01', genericVideoBox)
296
281
  .box('avcC', (box) => {
297
282
  let codecBase = baseBox || '';
298
283
  switch (baseBox) {
@@ -134,6 +134,12 @@ shaka.media.StreamingEngine = class {
134
134
  projection: null,
135
135
  hfov: null,
136
136
  };
137
+
138
+ /** @private {number} */
139
+ this.playRangeStart_ = 0;
140
+
141
+ /** @private {number} */
142
+ this.playRangeEnd_ = Infinity;
137
143
  }
138
144
 
139
145
  /** @override */
@@ -251,6 +257,20 @@ shaka.media.StreamingEngine = class {
251
257
  }
252
258
 
253
259
 
260
+ /**
261
+ * Applies a playback range. This will only affect non-live content.
262
+ *
263
+ * @param {number} playRangeStart
264
+ * @param {number} playRangeEnd
265
+ */
266
+ applyPlayRange(playRangeStart, playRangeEnd) {
267
+ if (!this.manifest_.presentationTimeline.isLive()) {
268
+ this.playRangeStart_ = playRangeStart;
269
+ this.playRangeEnd_ = playRangeEnd;
270
+ }
271
+ }
272
+
273
+
254
274
  /**
255
275
  * Initialize and start streaming.
256
276
  *
@@ -1899,10 +1919,11 @@ shaka.media.StreamingEngine = class {
1899
1919
  // Rounding issues can cause us to remove the first frame of a Period, so
1900
1920
  // reduce the window start time slightly.
1901
1921
  const appendWindowStart = Math.max(0,
1902
- reference.appendWindowStart -
1922
+ Math.max(reference.appendWindowStart, this.playRangeStart_) -
1903
1923
  StreamingEngine.APPEND_WINDOW_START_FUDGE_);
1904
1924
  const appendWindowEnd =
1905
- reference.appendWindowEnd + StreamingEngine.APPEND_WINDOW_END_FUDGE_;
1925
+ Math.min(reference.appendWindowEnd, this.playRangeEnd_) +
1926
+ StreamingEngine.APPEND_WINDOW_END_FUDGE_;
1906
1927
 
1907
1928
  goog.asserts.assert(
1908
1929
  reference.startTime <= appendWindowEnd,
@@ -2670,7 +2691,10 @@ shaka.media.StreamingEngine = class {
2670
2691
  }
2671
2692
 
2672
2693
  // First fire an error event.
2673
- this.playerInterface_.onError(error);
2694
+ if (!error.handled ||
2695
+ error.code != shaka.util.Error.Code.SEGMENT_MISSING) {
2696
+ this.playerInterface_.onError(error);
2697
+ }
2674
2698
 
2675
2699
  // If the error was not handled by the application, call the failure
2676
2700
  // callback.
package/lib/player.js CHANGED
@@ -2255,6 +2255,8 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2255
2255
  this.config_.playRangeStart,
2256
2256
  this.config_.playRangeEnd);
2257
2257
 
2258
+ this.streamingEngine_.applyPlayRange(
2259
+ this.config_.playRangeStart, this.config_.playRangeEnd);
2258
2260
 
2259
2261
  const setupPlayhead = (startTime) => {
2260
2262
  this.playhead_ = this.createPlayhead(startTime);
@@ -7244,7 +7246,7 @@ shaka.Player.TYPICAL_BUFFERING_THRESHOLD_ = 0.5;
7244
7246
  * @export
7245
7247
  */
7246
7248
  // eslint-disable-next-line no-useless-concat
7247
- shaka.Player.version = 'v4.8.16' + '-uncompiled'; // x-release-please-version
7249
+ shaka.Player.version = 'v4.8.18' + '-uncompiled'; // x-release-please-version
7248
7250
 
7249
7251
  // Initialize the deprecation system using the version string we just set
7250
7252
  // on the player.
@@ -174,7 +174,12 @@ shaka.util.MimeUtils = class {
174
174
  return 'hevc'; // H265
175
175
  case base === 'dvh1':
176
176
  case base === 'dvhe':
177
- return 'dovi'; // Dolby Vision
177
+ return 'dovi-hevc'; // Dolby Vision based in HEVC
178
+ case base === 'dvav':
179
+ case base === 'dva1':
180
+ return 'dovi-avc'; // Dolby Vision based in AVC
181
+ case base === 'dav1':
182
+ return 'dovi-av1'; // Dolby Vision based in AV1
178
183
  }
179
184
  return base;
180
185
  }
@@ -275,11 +275,11 @@ shaka.util.Mp4BoxParsers = class {
275
275
  }
276
276
 
277
277
  /**
278
- * Parses a MP4A box.
278
+ * Parses an audio sample entry box.
279
279
  * @param {!shaka.util.DataViewReader} reader
280
- * @return {!shaka.util.ParsedMP4ABox}
280
+ * @return {!shaka.util.ParsedAudioSampleEntryBox}
281
281
  */
282
- static parseMP4A(reader) {
282
+ static audioSampleEntry(reader) {
283
283
  reader.skip(6); // Skip "reserved"
284
284
  reader.skip(2); // Skip "data_reference_index"
285
285
  reader.skip(8); // Skip "reserved"
@@ -801,7 +801,7 @@ shaka.util.ParsedTENCBox;
801
801
  *
802
802
  * @exportDoc
803
803
  */
804
- shaka.util.ParsedMP4ABox;
804
+ shaka.util.ParsedAudioSampleEntryBox;
805
805
 
806
806
  /**
807
807
  * @typedef {{
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.8.16",
4
+ "version": "4.8.18",
5
5
  "homepage": "https://github.com/shaka-project/shaka-player",
6
6
  "author": "Google",
7
7
  "maintainers": [
@@ -2,6 +2,7 @@
2
2
  "AD_DURATION": "Duración del anuncio",
3
3
  "AD_PROGRESS": "Anuncio [AD_ON] de [NUM_ADS]",
4
4
  "AD_TIME": "Anuncio: [AD_TIME]",
5
+ "AIRPLAY": "AirPlay",
5
6
  "AUTO_QUALITY": "Automático",
6
7
  "BACK": "Atrás",
7
8
  "CAPTIONS": "Subtítulos",
@@ -26,6 +27,7 @@
26
27
  "PICTURE_IN_PICTURE": "Imagen en imagen",
27
28
  "PLAY": "Reproducir",
28
29
  "PLAYBACK_RATE": "Velocidad de reproducción",
30
+ "QUALITY": "Calidad",
29
31
  "RECENTER_VR": "Recentrar",
30
32
  "RESOLUTION": "Resolución",
31
33
  "REWIND": "Rebobinar",
@@ -1,7 +1,9 @@
1
1
  {
2
2
  "AD_DURATION": "Durée de l'annonce",
3
- "AD_PROGRESS": "Annonce [AD_ON] sur [NUM_ADS]",
4
- "AD_TIME": "Annonce : [AD_TIME]",
3
+ "AD_PROGRESS": "Annonce [AD_ON] sur [NUM_ADS]",
4
+ "AD_STATISTICS": "Statistiques de la annonce",
5
+ "AD_TIME": "Annonce : [AD_TIME]",
6
+ "AIRPLAY": "AirPlay",
5
7
  "AUTO_QUALITY": "Automatique",
6
8
  "BACK": "Retour",
7
9
  "CAPTIONS": "Sous-titres",
@@ -10,7 +12,7 @@
10
12
  "ENTER_PICTURE_IN_PICTURE": "Utiliser le mode Picture-in-Picture",
11
13
  "EXIT_FULL_SCREEN": "Quitter le mode plein écran",
12
14
  "EXIT_LOOP_MODE": "Arrêter la lecture en boucle de la vidéo en cours",
13
- "EXIT_PICTURE_IN_PICTURE": "Quitter le mode PIP (Picture-in-picture)",
15
+ "EXIT_PICTURE_IN_PICTURE": "Quitter le mode PIP (Picture-in-picture)",
14
16
  "FAST_FORWARD": "Avance rapide",
15
17
  "FULL_SCREEN": "Plein écran",
16
18
  "LANGUAGE": "Langue",
@@ -26,11 +28,18 @@
26
28
  "PICTURE_IN_PICTURE": "Picture-in-Picture",
27
29
  "PLAY": "Visionner",
28
30
  "PLAYBACK_RATE": "Vitesse de lecture",
31
+ "QUALITY": "Qualité",
32
+ "RECENTER_VR": "Recentrer",
33
+ "REPLAY": "Rejouer",
29
34
  "RESOLUTION": "Résolution",
30
35
  "REWIND": "Retour arrière",
31
36
  "SEEK": "Chercher",
32
37
  "SKIP_AD": "Passer l'annonce",
33
38
  "SKIP_TO_LIVE": "Accéder à la partie la plus récente du direct",
39
+ "STATISTICS": "Statistiques",
40
+ "SUBTITLE_FORCED": "Forcé",
41
+ "SURROUND": "Surround",
42
+ "TOGGLE_STEREOSCOPIC": "Stéréoscopique ON/OFF",
34
43
  "UNDETERMINED_LANGUAGE": "Langue indéterminée",
35
44
  "UNMUTE": "Réactiver le son",
36
45
  "UNRECOGNIZED_LANGUAGE": "Non reconnu",
@@ -1,7 +1,9 @@
1
1
  {
2
2
  "AD_DURATION": "Duração do anúncio",
3
3
  "AD_PROGRESS": "Anúncio [AD_ON] de [NUM_ADS]",
4
+ "AD_STATISTICS": "Estatísticas de anúncios",
4
5
  "AD_TIME": "Anúncio: [AD_TIME]",
6
+ "AIRPLAY": "AirPlay",
5
7
  "AUTO_QUALITY": "Automática",
6
8
  "BACK": "Voltar",
7
9
  "CAPTIONS": "Legendas",
@@ -26,11 +28,18 @@
26
28
  "PICTURE_IN_PICTURE": "Picture-in-picture",
27
29
  "PLAY": "Reproduzir",
28
30
  "PLAYBACK_RATE": "Velocidade da reprodução",
31
+ "QUALITY": "Qualidade",
32
+ "RECENTER_VR": "Recentralizar",
33
+ "REPLAY": "Replay",
29
34
  "RESOLUTION": "Resolução",
30
35
  "REWIND": "Retroceder",
31
36
  "SEEK": "Procurar",
32
37
  "SKIP_AD": "Pular anúncio",
33
38
  "SKIP_TO_LIVE": "Pular para transmissão ao vivo",
39
+ "STATISTICS": "Estatísticas",
40
+ "SUBTITLE_FORCED": "Forçado",
41
+ "SURROUND": "Surround",
42
+ "TOGGLE_STEREOSCOPIC": "Alternar estereoscópico",
34
43
  "UNDETERMINED_LANGUAGE": "Indeterminado",
35
44
  "UNMUTE": "Ativar som",
36
45
  "UNRECOGNIZED_LANGUAGE": "Não reconhecido",
@@ -1,7 +1,9 @@
1
1
  {
2
2
  "AD_DURATION": "Duração do anúncio",
3
3
  "AD_PROGRESS": "Anúncio [AD_ON] de [NUM_ADS]",
4
+ "AD_STATISTICS": "Estatísticas de anúncios",
4
5
  "AD_TIME": "Anúncio: [AD_TIME]",
6
+ "AIRPLAY": "AirPlay",
5
7
  "AUTO_QUALITY": "Automático",
6
8
  "BACK": "Anterior",
7
9
  "CAPTIONS": "Legendas",
@@ -26,11 +28,18 @@
26
28
  "PICTURE_IN_PICTURE": "Ecrã no ecrã",
27
29
  "PLAY": "Reproduzir",
28
30
  "PLAYBACK_RATE": "Velocidade de reprodução",
31
+ "QUALITY": "Qualidade",
32
+ "RECENTER_VR": "Recentrar",
33
+ "REPLAY": "Repetição",
29
34
  "RESOLUTION": "Resolução",
30
35
  "REWIND": "Rebobinar",
31
36
  "SEEK": "Procurar",
32
37
  "SKIP_AD": "Ignorar anúncio",
33
38
  "SKIP_TO_LIVE": "Avançar para o direto",
39
+ "STATISTICS": "Estatísticas",
40
+ "SUBTITLE_FORCED": "Forçado",
41
+ "SURROUND": "Envolvente",
42
+ "TOGGLE_STEREOSCOPIC": "Alternar estereoscópico",
34
43
  "UNDETERMINED_LANGUAGE": "Indeterminado",
35
44
  "UNMUTE": "Reativar o som",
36
45
  "UNRECOGNIZED_LANGUAGE": "Não reconhecido",