shaka-player 4.9.13 → 4.10.1

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.
Files changed (91) hide show
  1. package/README.md +4 -0
  2. package/dist/controls.css +2 -2
  3. package/dist/controls.css.map +1 -1
  4. package/dist/deps.js +24 -19
  5. package/dist/locales.js +46 -35
  6. package/dist/shaka-player.compiled.d.ts +291 -12
  7. package/dist/shaka-player.compiled.debug.d.ts +291 -12
  8. package/dist/shaka-player.compiled.debug.externs.js +443 -6
  9. package/dist/shaka-player.compiled.debug.js +651 -556
  10. package/dist/shaka-player.compiled.debug.map +4 -4
  11. package/dist/shaka-player.compiled.externs.js +443 -6
  12. package/dist/shaka-player.compiled.js +1325 -1270
  13. package/dist/shaka-player.compiled.map +4 -4
  14. package/dist/shaka-player.ui.d.ts +306 -13
  15. package/dist/shaka-player.ui.debug.d.ts +306 -13
  16. package/dist/shaka-player.ui.debug.externs.js +477 -18
  17. package/dist/shaka-player.ui.debug.js +799 -681
  18. package/dist/shaka-player.ui.debug.map +4 -4
  19. package/dist/shaka-player.ui.externs.js +477 -18
  20. package/dist/shaka-player.ui.js +1600 -1533
  21. package/dist/shaka-player.ui.map +4 -4
  22. package/externs/shaka/ads.js +21 -1
  23. package/externs/shaka/manifest.js +20 -1
  24. package/externs/shaka/manifest_parser.js +18 -1
  25. package/externs/shaka/player.js +164 -50
  26. package/lib/ads/ad_manager.js +225 -10
  27. package/lib/ads/ad_utils.js +323 -0
  28. package/lib/ads/ads_stats.js +24 -0
  29. package/lib/ads/client_side_ad_manager.js +42 -34
  30. package/lib/ads/interstitial_ad.js +292 -0
  31. package/lib/ads/interstitial_ad_manager.js +772 -0
  32. package/lib/ads/media_tailor_ad_manager.js +19 -22
  33. package/lib/ads/server_side_ad_manager.js +12 -15
  34. package/lib/cast/cast_utils.js +1 -0
  35. package/lib/cea/cea_utils.js +12 -0
  36. package/lib/cea/mp4_cea_parser.js +17 -0
  37. package/lib/dash/dash_parser.js +81 -28
  38. package/lib/dash/segment_base.js +3 -0
  39. package/lib/hls/hls_parser.js +429 -121
  40. package/lib/hls/manifest_text_parser.js +1 -0
  41. package/lib/media/adaptation_set_criteria.js +31 -1
  42. package/lib/media/drm_engine.js +16 -2
  43. package/lib/media/manifest_filterer.js +7 -5
  44. package/lib/media/preload_manager.js +8 -10
  45. package/lib/media/presentation_timeline.js +37 -5
  46. package/lib/media/quality_observer.js +68 -2
  47. package/lib/media/segment_reference.js +1 -1
  48. package/lib/media/segment_utils.js +33 -1
  49. package/lib/media/streaming_engine.js +120 -43
  50. package/lib/mss/mss_parser.js +13 -2
  51. package/lib/net/networking_engine.js +1 -0
  52. package/lib/offline/manifest_converter.js +3 -0
  53. package/lib/offline/offline_manifest_parser.js +5 -0
  54. package/lib/offline/storage.js +2 -0
  55. package/lib/player.js +498 -165
  56. package/lib/util/fake_event.js +1 -0
  57. package/lib/util/manifest_parser_utils.js +9 -2
  58. package/lib/util/mp4_box_parsers.js +32 -0
  59. package/lib/util/player_configuration.js +21 -9
  60. package/lib/util/stats.js +43 -0
  61. package/package.json +1 -1
  62. package/shaka-player.uncompiled.js +2 -0
  63. package/ui/ad_counter.js +3 -3
  64. package/ui/ad_position.js +3 -3
  65. package/ui/ad_statistics_button.js +237 -0
  66. package/ui/audio_language_selection.js +8 -4
  67. package/ui/controls.js +6 -3
  68. package/ui/element.js +3 -3
  69. package/ui/enums.js +1 -0
  70. package/ui/externs/ui.js +3 -0
  71. package/ui/language_utils.js +48 -3
  72. package/ui/less/containers.less +32 -0
  73. package/ui/less/thumbnails.less +13 -0
  74. package/ui/locales/en.json +2 -0
  75. package/ui/locales/es-419.json +2 -0
  76. package/ui/locales/es.json +3 -0
  77. package/ui/locales/fr.json +1 -0
  78. package/ui/locales/pl.json +1 -0
  79. package/ui/locales/pt-BR.json +1 -0
  80. package/ui/locales/pt-PT.json +1 -0
  81. package/ui/locales/source.json +8 -0
  82. package/ui/mute_button.js +4 -4
  83. package/ui/overflow_menu.js +3 -3
  84. package/ui/play_button.js +5 -7
  85. package/ui/presentation_time.js +3 -3
  86. package/ui/save_video_frame_button.js +196 -0
  87. package/ui/seek_bar.js +75 -20
  88. package/ui/skip_ad_button.js +4 -4
  89. package/ui/statistics_button.js +14 -2
  90. package/ui/ui.js +15 -0
  91. package/ui/volume_bar.js +4 -4
@@ -39,6 +39,20 @@ shaka.ui.LanguageUtils = class {
39
39
  return track.active == true;
40
40
  });
41
41
 
42
+ /** @type {!Map<string, !Set<string>>} */
43
+ const codecsByLanguage = new Map();
44
+ for (const track of tracks) {
45
+ if (!track.audioCodec) {
46
+ continue;
47
+ }
48
+ if (!codecsByLanguage.has(track.language)) {
49
+ codecsByLanguage.set(track.language, new Set());
50
+ }
51
+ codecsByLanguage.get(track.language).add(track.audioCodec);
52
+ }
53
+ const hasDifferentAudioCodecs = (language) =>
54
+ codecsByLanguage.has(language) && codecsByLanguage.get(language).size > 1;
55
+
42
56
  // Remove old tracks
43
57
  // 1. Save the back to menu button
44
58
  const backButton = shaka.ui.Utils.getFirstDescendantWithClassName(
@@ -59,7 +73,8 @@ shaka.ui.LanguageUtils = class {
59
73
  }
60
74
  };
61
75
 
62
- const getCombination = (language, rolesString, label, channelsCount) => {
76
+ const getCombination = (language, rolesString, label, channelsCount,
77
+ audioCodec) => {
63
78
  const keys = [
64
79
  language,
65
80
  rolesString,
@@ -67,6 +82,9 @@ shaka.ui.LanguageUtils = class {
67
82
  if (showAudioChannelCountVariants && channelsCount != null) {
68
83
  keys.push(channelsCount);
69
84
  }
85
+ if (hasDifferentAudioCodecs(language) && audioCodec) {
86
+ keys.push(audioCodec);
87
+ }
70
88
  if (label &&
71
89
  trackLabelFormat == shaka.ui.Overlay.TrackLabelFormat.LABEL) {
72
90
  keys.push(label);
@@ -82,6 +100,21 @@ shaka.ui.LanguageUtils = class {
82
100
  return name;
83
101
  };
84
102
 
103
+ const getAudioCodecName = (audioCodec) => {
104
+ let name = '';
105
+ audioCodec = audioCodec.toLowerCase();
106
+ if (audioCodec.startsWith('mp4a')) {
107
+ name = 'AAC';
108
+ } else if (audioCodec === 'ec-3' || audioCodec === 'ac-3') {
109
+ name = 'Dolby';
110
+ } else if (audioCodec === 'opus') {
111
+ name = 'Opus';
112
+ } else if (audioCodec === 'flac') {
113
+ name = 'fLaC';
114
+ }
115
+ return name ? ' ' + name : name;
116
+ };
117
+
85
118
  /** @type {!Map.<string, !Set.<string>>} */
86
119
  const rolesByLanguage = new Map();
87
120
  for (const track of tracks) {
@@ -96,7 +129,8 @@ shaka.ui.LanguageUtils = class {
96
129
  const combinationsMade = new Set();
97
130
  const selectedCombination = selectedTrack ? getCombination(
98
131
  selectedTrack.language, getRolesString(selectedTrack),
99
- selectedTrack.label, selectedTrack.channelsCount) : '';
132
+ selectedTrack.label, selectedTrack.channelsCount,
133
+ selectedTrack.audioCodec) : '';
100
134
 
101
135
  for (const track of tracks) {
102
136
  const language = track.language;
@@ -105,8 +139,10 @@ shaka.ui.LanguageUtils = class {
105
139
  const rolesString = getRolesString(track);
106
140
  const label = track.label;
107
141
  const channelsCount = track.channelsCount;
142
+ const audioCodec = track.audioCodec;
108
143
  const combinationName =
109
- getCombination(language, rolesString, label, channelsCount);
144
+ getCombination(language, rolesString, label, channelsCount,
145
+ audioCodec);
110
146
  if (combinationsMade.has(combinationName)) {
111
147
  continue;
112
148
  }
@@ -124,6 +160,9 @@ shaka.ui.LanguageUtils = class {
124
160
  shaka.ui.LanguageUtils.getLanguageName(language, localization);
125
161
  switch (trackLabelFormat) {
126
162
  case shaka.ui.Overlay.TrackLabelFormat.LANGUAGE:
163
+ if (hasDifferentAudioCodecs(language)) {
164
+ span.textContent += getAudioCodecName(audioCodec);
165
+ }
127
166
  if (showAudioChannelCountVariants) {
128
167
  span.textContent += getChannelsCountName(channelsCount);
129
168
  }
@@ -132,6 +171,9 @@ shaka.ui.LanguageUtils = class {
132
171
  }
133
172
  break;
134
173
  case shaka.ui.Overlay.TrackLabelFormat.ROLE:
174
+ if (hasDifferentAudioCodecs(language)) {
175
+ span.textContent += getAudioCodecName(audioCodec);
176
+ }
135
177
  if (showAudioChannelCountVariants) {
136
178
  span.textContent += getChannelsCountName(channelsCount);
137
179
  }
@@ -148,6 +190,9 @@ shaka.ui.LanguageUtils = class {
148
190
  }
149
191
  break;
150
192
  case shaka.ui.Overlay.TrackLabelFormat.LANGUAGE_ROLE:
193
+ if (hasDifferentAudioCodecs(language)) {
194
+ span.textContent += getAudioCodecName(audioCodec);
195
+ }
151
196
  if (showAudioChannelCountVariants) {
152
197
  span.textContent += getChannelsCountName(channelsCount);
153
198
  }
@@ -248,6 +248,38 @@
248
248
  }
249
249
  }
250
250
 
251
+ .shaka-ad-statistics-container {
252
+ overflow-x: hidden;
253
+ overflow-y: auto;
254
+
255
+ min-width: 150px;
256
+
257
+ color: white;
258
+ background-color: rgba(35 35 35 / 90%);
259
+
260
+ font-size: 14px;
261
+
262
+ padding: 5px 10px;
263
+ border-radius: 2px;
264
+
265
+ position: absolute;
266
+ z-index: 2;
267
+ right: 15px;
268
+ top: 15px;
269
+
270
+ /* Fades out with the other controls. */
271
+ .show-when-controls-shown();
272
+
273
+ div {
274
+ display: flex;
275
+ justify-content: space-between;
276
+ }
277
+
278
+ span {
279
+ color: rgb(150 150 150);
280
+ }
281
+ }
282
+
251
283
  .shaka-context-menu {
252
284
  background-color: rgba(35 35 35 / 90%);
253
285
 
@@ -24,3 +24,16 @@
24
24
  text-align: center;
25
25
  }
26
26
  }
27
+
28
+ #shaka-player-ui-time-container {
29
+ background-color: white;
30
+ border: 1px solid black;
31
+ border-radius: 10px;
32
+ box-shadow: 0 8px 8px 0 rgb(0 0 0 / 50%);
33
+ color: black;
34
+ overflow: hidden;
35
+ padding: 1px 7px;
36
+ position: absolute;
37
+ visibility: hidden;
38
+ z-index: 1;
39
+ }
@@ -1,12 +1,14 @@
1
1
  {
2
2
  "AD_DURATION": "Ad duration",
3
3
  "AD_PROGRESS": "Ad [AD_ON] of [NUM_ADS]",
4
+ "AD_STATISTICS": "Ad statistics",
4
5
  "AD_TIME": "Ad: [AD_TIME]",
5
6
  "AIRPLAY": "AirPlay",
6
7
  "AUTO_QUALITY": "Auto",
7
8
  "BACK": "Back",
8
9
  "CAPTIONS": "Captions",
9
10
  "CAST": "Cast...",
11
+ "DOWNLOAD_VIDEO_FRAME": "Save video frame",
10
12
  "ENTER_LOOP_MODE": "Loop the current video",
11
13
  "ENTER_PICTURE_IN_PICTURE": "Enter Picture-in-Picture",
12
14
  "EXIT_FULL_SCREEN": "Exit full screen",
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "AD_DURATION": "Duración del anuncio",
3
3
  "AD_PROGRESS": "Anuncio [AD_ON] de [NUM_ADS]",
4
+ "AD_STATISTICS": "Estadísticas de anuncios",
4
5
  "AD_TIME": "Anuncio: [AD_TIME]",
5
6
  "AUTO_QUALITY": "Automático",
6
7
  "BACK": "Atrás",
@@ -32,6 +33,7 @@
32
33
  "SEEK": "Buscar",
33
34
  "SKIP_AD": "Omitir anuncio",
34
35
  "SKIP_TO_LIVE": "Adelantar hasta la transmisión en vivo",
36
+ "STATISTICS": "Estadísticas",
35
37
  "SUBTITLE_FORCED": "Forzado",
36
38
  "SURROUND": "Envolvente",
37
39
  "TOGGLE_STEREOSCOPIC": "Alternar estereoscópico",
@@ -1,12 +1,14 @@
1
1
  {
2
2
  "AD_DURATION": "Duración del anuncio",
3
3
  "AD_PROGRESS": "Anuncio [AD_ON] de [NUM_ADS]",
4
+ "AD_STATISTICS": "Estadísticas de anuncios",
4
5
  "AD_TIME": "Anuncio: [AD_TIME]",
5
6
  "AIRPLAY": "AirPlay",
6
7
  "AUTO_QUALITY": "Automático",
7
8
  "BACK": "Atrás",
8
9
  "CAPTIONS": "Subtítulos",
9
10
  "CAST": "Enviar...",
11
+ "DOWNLOAD_VIDEO_FRAME": "Guardar fotograma",
10
12
  "ENTER_LOOP_MODE": "Reproducir en bucle el vídeo actual",
11
13
  "ENTER_PICTURE_IN_PICTURE": "Activar el modo imagen en imagen",
12
14
  "EXIT_FULL_SCREEN": "Salir del modo de pantalla completa",
@@ -34,6 +36,7 @@
34
36
  "SEEK": "Buscar",
35
37
  "SKIP_AD": "Saltar anuncio",
36
38
  "SKIP_TO_LIVE": "Ir al vídeo en directo",
39
+ "STATISTICS": "Estadísticas",
37
40
  "SUBTITLE_FORCED": "Forzado",
38
41
  "SURROUND": "Envolvente",
39
42
  "TOGGLE_STEREOSCOPIC": "Alternar estereoscópico",
@@ -8,6 +8,7 @@
8
8
  "BACK": "Retour",
9
9
  "CAPTIONS": "Sous-titres",
10
10
  "CAST": "Caster…",
11
+ "DOWNLOAD_VIDEO_FRAME": "Enregistrer l'image vidéo",
11
12
  "ENTER_LOOP_MODE": "Lire en boucle la vidéo en cours",
12
13
  "ENTER_PICTURE_IN_PICTURE": "Utiliser le mode Picture-in-Picture",
13
14
  "EXIT_FULL_SCREEN": "Quitter le mode plein écran",
@@ -8,6 +8,7 @@
8
8
  "BACK": "Wstecz",
9
9
  "CAPTIONS": "Napisy",
10
10
  "CAST": "Przesyłaj...",
11
+ "DOWNLOAD_VIDEO_FRAME": "Zapisz klatkę wideo",
11
12
  "ENTER_LOOP_MODE": "Odtwarzaj bieżący film w pętli",
12
13
  "ENTER_PICTURE_IN_PICTURE": "Włącz tryb obrazu w obrazie",
13
14
  "EXIT_FULL_SCREEN": "Zamknij pełny ekran",
@@ -8,6 +8,7 @@
8
8
  "BACK": "Voltar",
9
9
  "CAPTIONS": "Legendas",
10
10
  "CAST": "Transmitir...",
11
+ "DOWNLOAD_VIDEO_FRAME": "Salvar quadro de vídeo",
11
12
  "ENTER_LOOP_MODE": "Repetir o vídeo atual",
12
13
  "ENTER_PICTURE_IN_PICTURE": "Entrar no modo picture-in-picture",
13
14
  "EXIT_FULL_SCREEN": "Sair do modo tela cheia",
@@ -8,6 +8,7 @@
8
8
  "BACK": "Anterior",
9
9
  "CAPTIONS": "Legendas",
10
10
  "CAST": "Transmitir...",
11
+ "DOWNLOAD_VIDEO_FRAME": "Salvar quadro de vídeo",
11
12
  "ENTER_LOOP_MODE": "Repetir o vídeo atual",
12
13
  "ENTER_PICTURE_IN_PICTURE": "Entrar no modo ecrã no ecrã",
13
14
  "EXIT_FULL_SCREEN": "Sair do modo de ecrã inteiro",
@@ -7,6 +7,10 @@
7
7
  "description": "The contents of the ad countdown element, describing what ad you are on in a sequence of ads.",
8
8
  "message": "Ad [AD_ON:1] of [NUM_ADS:6]"
9
9
  },
10
+ "AD_STATISTICS": {
11
+ "description": "Label for a button that displays ad statistics.",
12
+ "message": "Ad statistics"
13
+ },
10
14
  "AD_TIME": {
11
15
  "description": "The text content of a label that shows how much time is left in the current ad.",
12
16
  "message": "Ad: [AD_TIME:0:05/0:10]"
@@ -33,6 +37,10 @@
33
37
  "description": "Label for a button used to open the native Cast dialog in the browser and select a destination to Cast to.",
34
38
  "message": "Cast..."
35
39
  },
40
+ "DOWNLOAD_VIDEO_FRAME": {
41
+ "description": "Label for a button used to download the current video frame.",
42
+ "message": "Save video frame"
43
+ },
36
44
  "ENTER_LOOP_MODE": {
37
45
  "description": "Label for a button that controls the loop mode of a video player. This mode will loop (play over and over) currently selected video.",
38
46
  "message": "Loop the current video"
package/ui/mute_button.js CHANGED
@@ -7,7 +7,7 @@
7
7
 
8
8
  goog.provide('shaka.ui.MuteButton');
9
9
 
10
- goog.require('shaka.ads.AdManager');
10
+ goog.require('shaka.ads.Utils');
11
11
  goog.require('shaka.ui.Controls');
12
12
  goog.require('shaka.ui.Element');
13
13
  goog.require('shaka.ui.Enums');
@@ -62,19 +62,19 @@ shaka.ui.MuteButton = class extends shaka.ui.Element {
62
62
  });
63
63
 
64
64
  this.eventManager.listen(this.adManager,
65
- shaka.ads.AdManager.AD_VOLUME_CHANGED, () => {
65
+ shaka.ads.Utils.AD_VOLUME_CHANGED, () => {
66
66
  this.updateAriaLabel_();
67
67
  this.updateIcon_();
68
68
  });
69
69
 
70
70
  this.eventManager.listen(this.adManager,
71
- shaka.ads.AdManager.AD_MUTED, () => {
71
+ shaka.ads.Utils.AD_MUTED, () => {
72
72
  this.updateAriaLabel_();
73
73
  this.updateIcon_();
74
74
  });
75
75
 
76
76
  this.eventManager.listen(this.adManager,
77
- shaka.ads.AdManager.AD_STOPPED, () => {
77
+ shaka.ads.Utils.AD_STOPPED, () => {
78
78
  // The base class also listens for this event and sets this.ad
79
79
  // to null. This is a safeguard in case of a race condition as
80
80
  // the label and icon code depends on this.ad being correctly
@@ -8,7 +8,7 @@
8
8
  goog.provide('shaka.ui.OverflowMenu');
9
9
 
10
10
  goog.require('goog.asserts');
11
- goog.require('shaka.ads.AdManager');
11
+ goog.require('shaka.ads.Utils');
12
12
  goog.require('shaka.log');
13
13
  goog.require('shaka.ui.Controls');
14
14
  goog.require('shaka.ui.Element');
@@ -59,14 +59,14 @@ shaka.ui.OverflowMenu = class extends shaka.ui.Element {
59
59
  });
60
60
 
61
61
  this.eventManager.listen(
62
- this.adManager, shaka.ads.AdManager.AD_STARTED, () => {
62
+ this.adManager, shaka.ads.Utils.AD_STARTED, () => {
63
63
  if (this.ad && this.ad.isLinear()) {
64
64
  shaka.ui.Utils.setDisplay(this.overflowMenuButton_, false);
65
65
  }
66
66
  });
67
67
 
68
68
  this.eventManager.listen(
69
- this.adManager, shaka.ads.AdManager.AD_STOPPED, () => {
69
+ this.adManager, shaka.ads.Utils.AD_STOPPED, () => {
70
70
  shaka.ui.Utils.setDisplay(this.overflowMenuButton_, true);
71
71
  });
72
72
 
package/ui/play_button.js CHANGED
@@ -7,7 +7,7 @@
7
7
 
8
8
  goog.provide('shaka.ui.PlayButton');
9
9
 
10
- goog.require('shaka.ads.AdManager');
10
+ goog.require('shaka.ads.Utils');
11
11
  goog.require('shaka.ui.Element');
12
12
  goog.require('shaka.ui.Localization');
13
13
  goog.require('shaka.util.Dom');
@@ -27,8 +27,6 @@ shaka.ui.PlayButton = class extends shaka.ui.Element {
27
27
  constructor(parent, controls) {
28
28
  super(parent, controls);
29
29
 
30
- const AdManager = shaka.ads.AdManager;
31
-
32
30
  /** @protected {!HTMLButtonElement} */
33
31
  this.button = shaka.util.Dom.createButton();
34
32
  this.parent.appendChild(this.button);
@@ -58,22 +56,22 @@ shaka.ui.PlayButton = class extends shaka.ui.Element {
58
56
  this.updateIcon();
59
57
  });
60
58
 
61
- this.eventManager.listen(this.adManager, AdManager.AD_PAUSED, () => {
59
+ this.eventManager.listen(this.adManager, shaka.ads.Utils.AD_PAUSED, () => {
62
60
  this.updateAriaLabel();
63
61
  this.updateIcon();
64
62
  });
65
63
 
66
- this.eventManager.listen(this.adManager, AdManager.AD_RESUMED, () => {
64
+ this.eventManager.listen(this.adManager, shaka.ads.Utils.AD_RESUMED, () => {
67
65
  this.updateAriaLabel();
68
66
  this.updateIcon();
69
67
  });
70
68
 
71
- this.eventManager.listen(this.adManager, AdManager.AD_STARTED, () => {
69
+ this.eventManager.listen(this.adManager, shaka.ads.Utils.AD_STARTED, () => {
72
70
  this.updateAriaLabel();
73
71
  this.updateIcon();
74
72
  });
75
73
 
76
- this.eventManager.listen(this.adManager, AdManager.AD_STOPPED, () => {
74
+ this.eventManager.listen(this.adManager, shaka.ads.Utils.AD_STOPPED, () => {
77
75
  this.updateAriaLabel();
78
76
  this.updateIcon();
79
77
  });
@@ -7,7 +7,7 @@
7
7
 
8
8
  goog.provide('shaka.ui.PresentationTimeTracker');
9
9
 
10
- goog.require('shaka.ads.AdManager');
10
+ goog.require('shaka.ads.Utils');
11
11
  goog.require('shaka.ui.Controls');
12
12
  goog.require('shaka.ui.Element');
13
13
  goog.require('shaka.ui.Locales');
@@ -54,12 +54,12 @@ shaka.ui.PresentationTimeTracker = class extends shaka.ui.Element {
54
54
  });
55
55
 
56
56
  this.eventManager.listen(
57
- this.adManager, shaka.ads.AdManager.AD_STARTED, () => {
57
+ this.adManager, shaka.ads.Utils.AD_STARTED, () => {
58
58
  shaka.ui.Utils.setDisplay(this.currentTime_, false);
59
59
  });
60
60
 
61
61
  this.eventManager.listen(
62
- this.adManager, shaka.ads.AdManager.AD_STOPPED, () => {
62
+ this.adManager, shaka.ads.Utils.AD_STOPPED, () => {
63
63
  shaka.ui.Utils.setDisplay(this.currentTime_, true);
64
64
  });
65
65
  }
@@ -0,0 +1,196 @@
1
+ /*! @license
2
+ * Shaka Player
3
+ * Copyright 2016 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+
8
+ goog.provide('shaka.ui.SaveVideoFrameButton');
9
+
10
+ goog.require('shaka.ads.Utils');
11
+ goog.require('shaka.ui.ContextMenu');
12
+ goog.require('shaka.ui.Controls');
13
+ goog.require('shaka.ui.Element');
14
+ goog.require('shaka.ui.Enums');
15
+ goog.require('shaka.ui.Locales');
16
+ goog.require('shaka.ui.Localization');
17
+ goog.require('shaka.ui.OverflowMenu');
18
+ goog.require('shaka.ui.Utils');
19
+ goog.require('shaka.util.Dom');
20
+
21
+
22
+ /**
23
+ * @extends {shaka.ui.Element}
24
+ * @final
25
+ * @export
26
+ */
27
+ shaka.ui.SaveVideoFrameButton = class extends shaka.ui.Element {
28
+ /**
29
+ * @param {!HTMLElement} parent
30
+ * @param {!shaka.ui.Controls} controls
31
+ */
32
+ constructor(parent, controls) {
33
+ super(parent, controls);
34
+
35
+ const LocIds = shaka.ui.Locales.Ids;
36
+ /** @private {!HTMLButtonElement} */
37
+ this.button_ = shaka.util.Dom.createButton();
38
+ this.button_.classList.add('shaka-save.video-frame-button');
39
+ this.button_.classList.add('shaka-tooltip');
40
+
41
+ /** @private {!HTMLElement} */
42
+ this.icon_ = shaka.util.Dom.createHTMLElement('i');
43
+ this.icon_.classList.add('material-icons-round');
44
+ this.icon_.textContent = shaka.ui.Enums.MaterialDesignIcons.DOWNLOAD;
45
+ this.button_.appendChild(this.icon_);
46
+
47
+ const label = shaka.util.Dom.createHTMLElement('label');
48
+ label.classList.add('shaka-overflow-button-label');
49
+ label.classList.add('shaka-overflow-menu-only');
50
+ this.nameSpan_ = shaka.util.Dom.createHTMLElement('span');
51
+ this.nameSpan_.textContent =
52
+ this.localization.resolve(LocIds.DOWNLOAD_VIDEO_FRAME);
53
+ label.appendChild(this.nameSpan_);
54
+
55
+ /** @private {!HTMLElement} */
56
+ this.currentState_ = shaka.util.Dom.createHTMLElement('span');
57
+ this.currentState_.classList.add('shaka-current-selection-span');
58
+ label.appendChild(this.currentState_);
59
+
60
+ this.button_.appendChild(label);
61
+
62
+ this.updateLocalizedStrings_();
63
+
64
+ this.parent.appendChild(this.button_);
65
+
66
+ this.eventManager.listen(
67
+ this.localization, shaka.ui.Localization.LOCALE_UPDATED, () => {
68
+ this.updateLocalizedStrings_();
69
+ });
70
+
71
+ this.eventManager.listen(
72
+ this.localization, shaka.ui.Localization.LOCALE_CHANGED, () => {
73
+ this.updateLocalizedStrings_();
74
+ });
75
+
76
+ this.eventManager.listen(this.button_, 'click', () => {
77
+ this.onClick_();
78
+ });
79
+
80
+ const vr = this.controls.getVR();
81
+ this.eventManager.listen(vr, 'vrstatuschanged', () => {
82
+ this.checkAvailability_();
83
+ });
84
+
85
+ this.eventManager.listen(
86
+ this.adManager, shaka.ads.Utils.AD_STARTED, () => {
87
+ this.checkAvailability_();
88
+ });
89
+
90
+ this.eventManager.listen(
91
+ this.adManager, shaka.ads.Utils.AD_STOPPED, () => {
92
+ this.checkAvailability_();
93
+ });
94
+
95
+ this.eventManager.listen(this.player, 'unloading', () => {
96
+ this.checkAvailability_();
97
+ });
98
+
99
+ this.eventManager.listen(this.player, 'loaded', () => {
100
+ this.checkAvailability_();
101
+ });
102
+
103
+ this.eventManager.listen(this.player, 'loaded', () => {
104
+ this.checkAvailability_();
105
+ });
106
+
107
+ this.eventManager.listen(this.video, 'play', () => {
108
+ this.checkAvailability_();
109
+ });
110
+
111
+ this.eventManager.listen(this.video, 'pause', () => {
112
+ this.checkAvailability_();
113
+ });
114
+
115
+ this.eventManager.listen(this.video, 'seeking', () => {
116
+ this.checkAvailability_();
117
+ });
118
+
119
+ this.checkAvailability_();
120
+ }
121
+
122
+
123
+ /** @private */
124
+ onClick_() {
125
+ const canvas = /** @type {!HTMLCanvasElement}*/ (
126
+ document.createElement('canvas'));
127
+ const context = /** @type {CanvasRenderingContext2D} */ (
128
+ canvas.getContext('2d'));
129
+
130
+ const video = /** @type {!HTMLVideoElement} */ (
131
+ this.controls.getLocalVideo());
132
+
133
+ canvas.width = video.videoWidth;
134
+ canvas.height = video.videoHeight;
135
+ context.drawImage(video, 0, 0, canvas.width, canvas.height);
136
+
137
+ const dataURL = canvas.toDataURL('image/png');
138
+
139
+ const downloadLink = /** @type {!HTMLAnchorElement}*/ (
140
+ document.createElement('a'));
141
+ downloadLink.href = dataURL;
142
+ downloadLink.download =
143
+ 'videoframe_' + video.currentTime.toFixed(3) + '.png';
144
+ downloadLink.click();
145
+ }
146
+
147
+
148
+ /**
149
+ * @private
150
+ */
151
+ checkAvailability_() {
152
+ let available = true;
153
+ if (this.controls.isPlayingVR()) {
154
+ available = false;
155
+ }
156
+ if (this.player.drmInfo() || this.player.isAudioOnly()) {
157
+ available = false;
158
+ }
159
+ if (this.ad) {
160
+ available = false;
161
+ }
162
+ shaka.ui.Utils.setDisplay(this.button_, available);
163
+ }
164
+
165
+
166
+ /**
167
+ * @private
168
+ */
169
+ updateLocalizedStrings_() {
170
+ const LocIds = shaka.ui.Locales.Ids;
171
+
172
+ this.button_.ariaLabel =
173
+ this.localization.resolve(LocIds.DOWNLOAD_VIDEO_FRAME);
174
+ this.nameSpan_.textContent =
175
+ this.localization.resolve(LocIds.DOWNLOAD_VIDEO_FRAME);
176
+ }
177
+ };
178
+
179
+
180
+ /**
181
+ * @implements {shaka.extern.IUIElement.Factory}
182
+ * @final
183
+ */
184
+ shaka.ui.SaveVideoFrameButton.Factory = class {
185
+ /** @override */
186
+ create(rootElement, controls) {
187
+ return new shaka.ui.SaveVideoFrameButton(rootElement, controls);
188
+ }
189
+ };
190
+
191
+
192
+ shaka.ui.OverflowMenu.registerElement(
193
+ 'save_video_frame', new shaka.ui.SaveVideoFrameButton.Factory());
194
+
195
+ shaka.ui.ContextMenu.registerElement(
196
+ 'save_video_frame', new shaka.ui.SaveVideoFrameButton.Factory());