shaka-player 4.9.13 → 4.9.15

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.
@@ -1593,7 +1593,8 @@ shaka.extern.AdsConfiguration;
1593
1593
  * ignoreDevicePixelRatio: boolean,
1594
1594
  * clearBufferSwitch: boolean,
1595
1595
  * safeMarginSwitch: number,
1596
- * cacheLoadThreshold: number
1596
+ * cacheLoadThreshold: number,
1597
+ * minTimeToSwitch: number
1597
1598
  * }}
1598
1599
  *
1599
1600
  * @property {boolean} enabled
@@ -1655,6 +1656,11 @@ shaka.extern.AdsConfiguration;
1655
1656
  * Indicates the value in milliseconds from which a request is not
1656
1657
  * considered cached.
1657
1658
  * Defaults to <code>20</code>.
1659
+ * @property {number} minTimeToSwitch
1660
+ * Indicates the minimum time to change quality once the real bandwidth is
1661
+ * available, in seconds. This time is only used on the first load.
1662
+ * Defaults to <code>0</code> seconds except in Apple browsers whose default
1663
+ * value is <code>0.5</code> seconds.
1658
1664
  * @exportDoc
1659
1665
  */
1660
1666
  shaka.extern.AbrConfiguration;
@@ -433,14 +433,17 @@ shaka.abr.SimpleAbrManager = class {
433
433
  return;
434
434
  }
435
435
  this.startupComplete_ = true;
436
- } else {
437
- // Check if we've left the switch interval.
438
- const now = Date.now();
439
- const delta = now - this.lastTimeChosenMs_;
440
- if (delta < this.config_.switchInterval * 1000) {
441
- shaka.log.v2('Still within switch interval...');
442
- return;
443
- }
436
+
437
+ this.lastTimeChosenMs_ -=
438
+ (this.config_.switchInterval - this.config_.minTimeToSwitch) * 1000;
439
+ }
440
+
441
+ // Check if we've left the switch interval.
442
+ const now = Date.now();
443
+ const delta = now - this.lastTimeChosenMs_;
444
+ if (delta < this.config_.switchInterval * 1000) {
445
+ shaka.log.v2('Still within switch interval...');
446
+ return;
444
447
  }
445
448
 
446
449
  const chosenVariant = this.chooseVariant();
@@ -572,9 +572,10 @@ shaka.hls.HlsParser = class {
572
572
  if (goog.DEBUG) {
573
573
  const firstSequenceStartTime =
574
574
  mediaSequenceToStartTime.values().next().value;
575
- shaka.log.warning(
576
- firstSequenceStartTime == segment0.startTime,
577
- 'Sequence number map is not ordered as expected!');
575
+ if (firstSequenceStartTime != segment0.startTime) {
576
+ shaka.log.warning(
577
+ 'Sequence number map is not ordered as expected!');
578
+ }
578
579
  }
579
580
  for (const [sequence, start] of mediaSequenceToStartTime) {
580
581
  if (start == segment0.startTime) {
@@ -151,6 +151,15 @@ shaka.media.MediaSourceEngine = class {
151
151
 
152
152
  /** @private {?shaka.util.TsParser} */
153
153
  this.tsParser_ = null;
154
+
155
+ /** @private {?number} */
156
+ this.firstVideoTimestamp_ = null;
157
+
158
+ /** @private {?number} */
159
+ this.firstAudioTimestamp_ = null;
160
+
161
+ /** @private {!shaka.util.PublicPromise.<number>} */
162
+ this.audioCompensation_ = new shaka.util.PublicPromise();
154
163
  }
155
164
 
156
165
  /**
@@ -924,6 +933,24 @@ shaka.media.MediaSourceEngine = class {
924
933
  const timestamp = this.getTimestampAndDispatchMetadata_(
925
934
  contentType, data, reference, mimeType, timestampOffset);
926
935
  if (timestamp != null) {
936
+ if (this.firstVideoTimestamp_ == null &&
937
+ contentType == ContentType.VIDEO) {
938
+ this.firstVideoTimestamp_ = timestamp;
939
+ if (this.firstAudioTimestamp_ != null) {
940
+ const compensation =
941
+ this.firstVideoTimestamp_ - this.firstAudioTimestamp_;
942
+ this.audioCompensation_.resolve(compensation);
943
+ }
944
+ }
945
+ if (this.firstAudioTimestamp_ == null &&
946
+ contentType == ContentType.AUDIO) {
947
+ this.firstAudioTimestamp_ = timestamp;
948
+ if (this.firstVideoTimestamp_ != null) {
949
+ const compensation =
950
+ this.firstVideoTimestamp_ - this.firstAudioTimestamp_;
951
+ this.audioCompensation_.resolve(compensation);
952
+ }
953
+ }
927
954
  const calculatedTimestampOffset = reference.startTime - timestamp;
928
955
  const timestampOffsetDifference =
929
956
  Math.abs(timestampOffset - calculatedTimestampOffset);
@@ -992,7 +1019,19 @@ shaka.media.MediaSourceEngine = class {
992
1019
  // and are either performing an unbuffered seek or handling an automatic
993
1020
  // adaptation, we need to set a new timestampOffset on the sourceBuffer.
994
1021
  if (seeked || adaptation) {
995
- const timestampOffset = reference.startTime;
1022
+ let timestampOffset = reference.startTime;
1023
+ // Audio and video may not be aligned, so we will compensate for audio
1024
+ // if necessary.
1025
+ if (this.manifestType_ == shaka.media.ManifestParser.HLS &&
1026
+ !this.needSplitMuxedContent_ &&
1027
+ contentType == ContentType.AUDIO &&
1028
+ this.sourceBuffers_[ContentType.VIDEO]) {
1029
+ const compensation = await this.audioCompensation_;
1030
+ // Only apply compensation if the difference is greater than 100ms
1031
+ if (Math.abs(compensation) > 0.1) {
1032
+ timestampOffset -= compensation;
1033
+ }
1034
+ }
996
1035
  // The logic to call abort() before setting the timestampOffset is
997
1036
  // extended during unbuffered seeks or automatic adaptations; it is
998
1037
  // possible for the append state to be PARSING_MEDIA_SEGMENT from the
@@ -218,21 +218,18 @@ shaka.media.SegmentUtils = class {
218
218
 
219
219
  const genericVideoBox = (box) => {
220
220
  baseBox = box.name;
221
- Mp4Parser.visualSampleEntry(box);
221
+ const parsedVisualSampleEntryBox =
222
+ shaka.util.Mp4BoxParsers.visualSampleEntry(box.reader);
223
+ width = String(parsedVisualSampleEntryBox.width);
224
+ height = String(parsedVisualSampleEntryBox.height);
225
+ if (box.reader.hasMoreData()) {
226
+ Mp4Parser.children(box);
227
+ }
222
228
  };
223
229
 
224
230
  new Mp4Parser()
225
231
  .box('moov', Mp4Parser.children)
226
232
  .box('trak', Mp4Parser.children)
227
- .fullBox('tkhd', (box) => {
228
- goog.asserts.assert(
229
- box.version != null,
230
- 'TKHD is a full box and should have a valid version.');
231
- const parsedTKHDBox = shaka.util.Mp4BoxParsers.parseTKHD(
232
- box.reader, box.version);
233
- height = String(parsedTKHDBox.height);
234
- width = String(parsedTKHDBox.width);
235
- })
236
233
  .box('mdia', Mp4Parser.children)
237
234
  .fullBox('mdhd', (box) => {
238
235
  goog.asserts.assert(
package/lib/player.js CHANGED
@@ -6612,7 +6612,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
6612
6612
  // Check if we should show text (based on difference between audio and text
6613
6613
  // languages).
6614
6614
  if (initialTextStream) {
6615
- if (initialVariant.audio && this.shouldInitiallyShowText_(
6615
+ if (this.shouldInitiallyShowText_(
6616
6616
  initialVariant.audio, initialTextStream)) {
6617
6617
  this.isTextVisible_ = true;
6618
6618
  }
@@ -6632,7 +6632,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
6632
6632
  /**
6633
6633
  * Check if we should show text on screen automatically.
6634
6634
  *
6635
- * @param {shaka.extern.Stream} audioStream
6635
+ * @param {?shaka.extern.Stream} audioStream
6636
6636
  * @param {shaka.extern.Stream} textStream
6637
6637
  * @return {boolean}
6638
6638
  * @private
@@ -6663,6 +6663,9 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
6663
6663
  }
6664
6664
 
6665
6665
  if (this.config_.autoShowText == AutoShowText.IF_SUBTITLES_MAY_BE_NEEDED) {
6666
+ if (!audioStream) {
6667
+ return false;
6668
+ }
6666
6669
  /* The text should automatically be shown if the text is
6667
6670
  * language-compatible with the user's text language preference, but not
6668
6671
  * compatible with the audio. These are cases where we deduce that
@@ -7473,7 +7476,7 @@ shaka.Player.TYPICAL_BUFFERING_THRESHOLD_ = 0.5;
7473
7476
  * @export
7474
7477
  */
7475
7478
  // eslint-disable-next-line no-useless-concat
7476
- shaka.Player.version = 'v4.9.13' + '-uncompiled'; // x-release-please-version
7479
+ shaka.Player.version = 'v4.9.15' + '-uncompiled'; // x-release-please-version
7477
7480
 
7478
7481
  // Initialize the deprecation system using the version string we just set
7479
7482
  // on the player.
package/lib/text/cue.js CHANGED
@@ -5,8 +5,8 @@
5
5
  */
6
6
 
7
7
  goog.provide('shaka.text.Cue');
8
- goog.provide('shaka.text.CueRegion');
9
8
 
9
+ goog.require('shaka.text.CueRegion');
10
10
  goog.require('shaka.util.ArrayUtils');
11
11
 
12
12
 
@@ -553,119 +553,3 @@ shaka.text.Cue.textDecoration = {
553
553
  'LINE_THROUGH': 'lineThrough',
554
554
  'OVERLINE': 'overline',
555
555
  };
556
-
557
-
558
- /**
559
- * @export
560
- */
561
- shaka.text.CueRegion = class {
562
- /** */
563
- constructor() {
564
- const CueRegion = shaka.text.CueRegion;
565
-
566
- /**
567
- * Region identifier.
568
- * @type {string}
569
- * @export
570
- */
571
- this.id = '';
572
-
573
- /**
574
- * The X offset to start the rendering area in viewportAnchorUnits of the
575
- * video width.
576
- * @type {number}
577
- * @export
578
- */
579
- this.viewportAnchorX = 0;
580
-
581
- /**
582
- * The X offset to start the rendering area in viewportAnchorUnits of the
583
- * video height.
584
- * @type {number}
585
- * @export
586
- */
587
- this.viewportAnchorY = 0;
588
-
589
- /**
590
- * The X offset to start the rendering area in percentage (0-100) of this
591
- * region width.
592
- * @type {number}
593
- * @export
594
- */
595
- this.regionAnchorX = 0;
596
-
597
- /**
598
- * The Y offset to start the rendering area in percentage (0-100) of the
599
- * region height.
600
- * @type {number}
601
- * @export
602
- */
603
- this.regionAnchorY = 0;
604
-
605
- /**
606
- * The width of the rendering area in widthUnits.
607
- * @type {number}
608
- * @export
609
- */
610
- this.width = 100;
611
-
612
- /**
613
- * The width of the rendering area in heightUnits.
614
- * @type {number}
615
- * @export
616
- */
617
- this.height = 100;
618
-
619
- /**
620
- * The units (percentage, pixels or lines) the region height is in.
621
- * @type {shaka.text.CueRegion.units}
622
- * @export
623
- */
624
- this.heightUnits = CueRegion.units.PERCENTAGE;
625
-
626
- /**
627
- * The units (percentage or pixels) the region width is in.
628
- * @type {shaka.text.CueRegion.units}
629
- * @export
630
- */
631
- this.widthUnits = CueRegion.units.PERCENTAGE;
632
-
633
- /**
634
- * The units (percentage or pixels) the region viewportAnchors are in.
635
- * @type {shaka.text.CueRegion.units}
636
- * @export
637
- */
638
- this.viewportAnchorUnits = CueRegion.units.PERCENTAGE;
639
-
640
- /**
641
- * If scroll=UP, it means that cues in the region will be added to the
642
- * bottom of the region and will push any already displayed cues in the
643
- * region up. Otherwise (scroll=NONE) cues will stay fixed at the location
644
- * they were first painted in.
645
- * @type {shaka.text.CueRegion.scrollMode}
646
- * @export
647
- */
648
- this.scroll = CueRegion.scrollMode.NONE;
649
- }
650
- };
651
-
652
-
653
- /**
654
- * @enum {number}
655
- * @export
656
- */
657
- shaka.text.CueRegion.units = {
658
- 'PX': 0,
659
- 'PERCENTAGE': 1,
660
- 'LINES': 2,
661
- };
662
-
663
-
664
- /**
665
- * @enum {string}
666
- * @export
667
- */
668
- shaka.text.CueRegion.scrollMode = {
669
- 'NONE': '',
670
- 'UP': 'up',
671
- };
@@ -0,0 +1,123 @@
1
+ /*! @license
2
+ * Shaka Player
3
+ * Copyright 2016 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ goog.provide('shaka.text.CueRegion');
8
+
9
+
10
+ /**
11
+ * @export
12
+ */
13
+ shaka.text.CueRegion = class {
14
+ /** */
15
+ constructor() {
16
+ const CueRegion = shaka.text.CueRegion;
17
+
18
+ /**
19
+ * Region identifier.
20
+ * @type {string}
21
+ * @export
22
+ */
23
+ this.id = '';
24
+
25
+ /**
26
+ * The X offset to start the rendering area in viewportAnchorUnits of the
27
+ * video width.
28
+ * @type {number}
29
+ * @export
30
+ */
31
+ this.viewportAnchorX = 0;
32
+
33
+ /**
34
+ * The X offset to start the rendering area in viewportAnchorUnits of the
35
+ * video height.
36
+ * @type {number}
37
+ * @export
38
+ */
39
+ this.viewportAnchorY = 0;
40
+
41
+ /**
42
+ * The X offset to start the rendering area in percentage (0-100) of this
43
+ * region width.
44
+ * @type {number}
45
+ * @export
46
+ */
47
+ this.regionAnchorX = 0;
48
+
49
+ /**
50
+ * The Y offset to start the rendering area in percentage (0-100) of the
51
+ * region height.
52
+ * @type {number}
53
+ * @export
54
+ */
55
+ this.regionAnchorY = 0;
56
+
57
+ /**
58
+ * The width of the rendering area in widthUnits.
59
+ * @type {number}
60
+ * @export
61
+ */
62
+ this.width = 100;
63
+
64
+ /**
65
+ * The width of the rendering area in heightUnits.
66
+ * @type {number}
67
+ * @export
68
+ */
69
+ this.height = 100;
70
+
71
+ /**
72
+ * The units (percentage, pixels or lines) the region height is in.
73
+ * @type {shaka.text.CueRegion.units}
74
+ * @export
75
+ */
76
+ this.heightUnits = CueRegion.units.PERCENTAGE;
77
+
78
+ /**
79
+ * The units (percentage or pixels) the region width is in.
80
+ * @type {shaka.text.CueRegion.units}
81
+ * @export
82
+ */
83
+ this.widthUnits = CueRegion.units.PERCENTAGE;
84
+
85
+ /**
86
+ * The units (percentage or pixels) the region viewportAnchors are in.
87
+ * @type {shaka.text.CueRegion.units}
88
+ * @export
89
+ */
90
+ this.viewportAnchorUnits = CueRegion.units.PERCENTAGE;
91
+
92
+ /**
93
+ * If scroll=UP, it means that cues in the region will be added to the
94
+ * bottom of the region and will push any already displayed cues in the
95
+ * region up. Otherwise (scroll=NONE) cues will stay fixed at the location
96
+ * they were first painted in.
97
+ * @type {shaka.text.CueRegion.scrollMode}
98
+ * @export
99
+ */
100
+ this.scroll = CueRegion.scrollMode.NONE;
101
+ }
102
+ };
103
+
104
+
105
+ /**
106
+ * @enum {number}
107
+ * @export
108
+ */
109
+ shaka.text.CueRegion.units = {
110
+ 'PX': 0,
111
+ 'PERCENTAGE': 1,
112
+ 'LINES': 2,
113
+ };
114
+
115
+
116
+ /**
117
+ * @enum {string}
118
+ * @export
119
+ */
120
+ shaka.text.CueRegion.scrollMode = {
121
+ 'NONE': '',
122
+ 'UP': 'up',
123
+ };
@@ -468,7 +468,9 @@ shaka.text.UITextDisplayer = class {
468
468
 
469
469
  const regionElement = shaka.util.Dom.createHTMLElement('span');
470
470
 
471
+ const linesUnit = shaka.text.CueRegion.units.LINES;
471
472
  const percentageUnit = shaka.text.CueRegion.units.PERCENTAGE;
473
+ const pixelUnit = shaka.text.CueRegion.units.PX;
472
474
  const heightUnit = region.heightUnits == percentageUnit ? '%' : 'px';
473
475
  const widthUnit = region.widthUnits == percentageUnit ? '%' : 'px';
474
476
  const viewportAnchorUnit =
@@ -479,12 +481,14 @@ shaka.text.UITextDisplayer = class {
479
481
 
480
482
  regionElement.style.position = 'absolute';
481
483
 
482
- const linesUnit = shaka.text.CueRegion.units.LINES;
483
- if (region.heightUnits === linesUnit && region.widthUnits === linesUnit) {
484
+ if (region.heightUnits === linesUnit) {
484
485
  regionElement.style.height = region.height * lineHeightMultiple + '%';
485
- regionElement.style.width = region.width * lineWidthMultiple + '%';
486
486
  } else {
487
487
  regionElement.style.height = region.height + heightUnit;
488
+ }
489
+ if (region.widthUnits === linesUnit) {
490
+ regionElement.style.width = region.width * lineWidthMultiple + '%';
491
+ } else {
488
492
  regionElement.style.width = region.width + widthUnit;
489
493
  }
490
494
 
@@ -504,6 +508,19 @@ shaka.text.UITextDisplayer = class {
504
508
  regionElement.style.left = region.viewportAnchorX -
505
509
  region.regionAnchorX * region.width / 100 + viewportAnchorUnit;
506
510
  }
511
+ if (region.heightUnits !== pixelUnit &&
512
+ region.widthUnits !== pixelUnit &&
513
+ region.viewportAnchorUnits !== pixelUnit) {
514
+ // Clip region
515
+ const top = parseInt(regionElement.style.top.slice(0, -1), 10) || 0;
516
+ const left = parseInt(regionElement.style.left.slice(0, -1), 10) || 0;
517
+ const height = parseInt(regionElement.style.height.slice(0, -1), 10) || 0;
518
+ const width = parseInt(regionElement.style.width.slice(0, -1), 10) || 0;
519
+ const realTop = Math.max(0, Math.min(100 - height, top));
520
+ const realLeft = Math.max(0, Math.min(100 - width, left));
521
+ regionElement.style.top = realTop + '%';
522
+ regionElement.style.left = realLeft + '%';
523
+ }
507
524
 
508
525
  regionElement.style.display = 'flex';
509
526
  regionElement.style.flexDirection = 'column';
@@ -274,6 +274,35 @@ shaka.util.Mp4BoxParsers = class {
274
274
  };
275
275
  }
276
276
 
277
+
278
+ /**
279
+ * Parses an visual sample entry box.
280
+ * @param {!shaka.util.DataViewReader} reader
281
+ * @return {!shaka.util.ParsedVisualSampleEntryBox}
282
+ */
283
+ static visualSampleEntry(reader) {
284
+ // Skip 6 reserved bytes.
285
+ // Skip 2-byte data reference index.
286
+ // Skip 16 more reserved bytes.
287
+ reader.skip(24);
288
+ // 4 bytes for width/height.
289
+ const width = reader.readUint16();
290
+ const height = reader.readUint16();
291
+ // Skip 8 bytes for horizontal/vertical resolution.
292
+ // Skip 4 more reserved bytes (0)
293
+ // Skip 2-byte frame count.
294
+ // Skip 32-byte compressor name (length byte, then name, then 0-padding).
295
+ // Skip 2-byte depth.
296
+ // Skip 2 more reserved bytes (0xff)
297
+ reader.skip(50);
298
+
299
+ return {
300
+ width: width,
301
+ height: height,
302
+ };
303
+ }
304
+
305
+
277
306
  /**
278
307
  * Parses an audio sample entry box.
279
308
  * @param {!shaka.util.DataViewReader} reader
@@ -845,6 +874,19 @@ shaka.util.ParsedFRMABox;
845
874
  */
846
875
  shaka.util.ParsedTENCBox;
847
876
 
877
+ /**
878
+ * @typedef {{
879
+ * width: number,
880
+ * height: number
881
+ * }}
882
+ *
883
+ * @property {number} width
884
+ * @property {number} height
885
+ *
886
+ * @exportDoc
887
+ */
888
+ shaka.util.ParsedVisualSampleEntryBox;
889
+
848
890
  /**
849
891
  * @typedef {{
850
892
  * channelCount: number,
@@ -727,15 +727,17 @@ shaka.util.Platform = class {
727
727
  'screen size 1920x1080.');
728
728
  }
729
729
  } else if (Platform.isXboxOne()) {
730
- const protectionCapabilities =
731
- new Windows.Media.Protection.ProtectionCapabilities();
732
- const protectionResult =
733
- Windows.Media.Protection.ProtectionCapabilityResult;
734
- // isTypeSupported may return "maybe", which means the operation is not
735
- // completed. This means we need to retry
736
- // https://learn.microsoft.com/en-us/uwp/api/windows.media.protection.protectioncapabilityresult?view=winrt-22621
737
- let result = null;
730
+ maxResolution.width = 1920;
731
+ maxResolution.height = 1080;
738
732
  try {
733
+ const protectionCapabilities =
734
+ new Windows.Media.Protection.ProtectionCapabilities();
735
+ const protectionResult =
736
+ Windows.Media.Protection.ProtectionCapabilityResult;
737
+ // isTypeSupported may return "maybe", which means the operation is not
738
+ // completed. This means we need to retry
739
+ // https://learn.microsoft.com/en-us/uwp/api/windows.media.protection.protectioncapabilityresult?view=winrt-22621
740
+ let result = null;
739
741
  const type =
740
742
  'video/mp4;codecs="hvc1,mp4a";features="decode-res-x=3840,' +
741
743
  'decode-res-y=2160,decode-bitrate=20000,decode-fps=30,' +
@@ -745,15 +747,13 @@ shaka.util.Platform = class {
745
747
  do {
746
748
  result = protectionCapabilities.isTypeSupported(type, keySystem);
747
749
  } while (result === protectionResult.maybe);
750
+ if (result === protectionResult.probably) {
751
+ maxResolution.width = 3840;
752
+ maxResolution.height = 2160;
753
+ }
748
754
  } catch (e) {
749
- result = protectionResult.notSupported;
750
- }
751
- if (result === protectionResult.probably) {
752
- maxResolution.width = 3840;
753
- maxResolution.height = 2160;
754
- } else {
755
- maxResolution.width = 1920;
756
- maxResolution.height = 1080;
755
+ shaka.log.alwaysWarn('Xbox: Error detecting screen size, default ' +
756
+ 'screen size 1920x1080.');
757
757
  }
758
758
  } else if (Platform.isWebOS()) {
759
759
  try {
@@ -339,6 +339,7 @@ shaka.util.PlayerConfiguration = class {
339
339
  clearBufferSwitch: false,
340
340
  safeMarginSwitch: 0,
341
341
  cacheLoadThreshold: 20,
342
+ minTimeToSwitch: shaka.util.Platform.isApple() ? 0.5 : 0,
342
343
  };
343
344
 
344
345
  const cmcd = {
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.9.13",
4
+ "version": "4.9.15",
5
5
  "homepage": "https://github.com/shaka-project/shaka-player",
6
6
  "author": "Google",
7
7
  "maintainers": [
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "AD_DURATION": "Durée de l'annonce",
3
3
  "AD_PROGRESS": "Annonce [AD_ON] sur [NUM_ADS]",
4
- "AD_STATISTICS": "Statistiques de la annonce",
4
+ "AD_STATISTICS": "Statistiques de l'annonce",
5
5
  "AD_TIME": "Annonce : [AD_TIME]",
6
6
  "AIRPLAY": "AirPlay",
7
7
  "AUTO_QUALITY": "Automatique",