fcad-core-dragon 2.1.1 → 2.1.2

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 (160) hide show
  1. package/.editorconfig +7 -7
  2. package/.gitlab-ci.yml +124 -0
  3. package/.prettierrc +11 -11
  4. package/.vscode/extensions.json +8 -8
  5. package/.vscode/settings.json +46 -16
  6. package/CHANGELOG +520 -520
  7. package/README.md +57 -57
  8. package/documentation/.vitepress/config.js +114 -114
  9. package/documentation/api-examples.md +49 -49
  10. package/documentation/composants/app-base-button.md +58 -58
  11. package/documentation/composants/app-base-error-display.md +59 -59
  12. package/documentation/composants/app-base-popover.md +68 -68
  13. package/documentation/composants/app-comp-audio.md +75 -75
  14. package/documentation/composants/app-comp-branch-buttons.md +111 -111
  15. package/documentation/composants/app-comp-button-progress.md +53 -53
  16. package/documentation/composants/app-comp-carousel.md +53 -53
  17. package/documentation/composants/app-comp-container.md +53 -53
  18. package/documentation/composants/app-comp-input-checkbox-next.md +42 -42
  19. package/documentation/composants/app-comp-input-dropdown-next.md +34 -34
  20. package/documentation/composants/app-comp-input-radio-next.md +39 -39
  21. package/documentation/composants/app-comp-input-text-next.md +35 -35
  22. package/documentation/composants/app-comp-input-text-table-next.md +34 -34
  23. package/documentation/composants/app-comp-input-text-to-fill-dropdown-next.md +53 -53
  24. package/documentation/composants/app-comp-input-text-to-fill-next.md +31 -31
  25. package/documentation/composants/app-comp-jauge.md +31 -31
  26. package/documentation/composants/app-comp-menu-item.md +55 -55
  27. package/documentation/composants/app-comp-menu.md +29 -29
  28. package/documentation/composants/app-comp-navigation.md +41 -41
  29. package/documentation/composants/app-comp-note-call.md +53 -53
  30. package/documentation/composants/app-comp-note-credit.md +53 -53
  31. package/documentation/composants/app-comp-play-bar-next.md +53 -53
  32. package/documentation/composants/app-comp-pop-up-next.md +93 -93
  33. package/documentation/composants/app-comp-quiz-next.md +235 -235
  34. package/documentation/composants/app-comp-quiz-recall.md +53 -53
  35. package/documentation/composants/app-comp-svg-next.md +53 -53
  36. package/documentation/composants/app-comp-table-of-content.md +50 -50
  37. package/documentation/composants/app-comp-video-player.md +82 -82
  38. package/documentation/composants.md +46 -46
  39. package/documentation/composants_critiques/ModelPageComposant.md +53 -53
  40. package/documentation/composants_critiques/app-base-module.md +43 -43
  41. package/documentation/composants_critiques/app-base-page.md +48 -48
  42. package/documentation/composants_critiques/app-base.md +311 -311
  43. package/documentation/composants_critiques/main.md +15 -15
  44. package/documentation/demarrage.md +50 -50
  45. package/documentation/deploiement.md +57 -57
  46. package/documentation/index.md +33 -33
  47. package/documentation/markdown-examples.md +85 -85
  48. package/documentation/public/vite.svg +14 -14
  49. package/documentation/public/vuejs.svg +1 -1
  50. package/documentation/public/vuetify.svg +5 -5
  51. package/eslint.config.js +60 -60
  52. package/junit-report.xml +182 -0
  53. package/package.json +66 -59
  54. package/playwright/index.html +12 -0
  55. package/playwright/index.js +21 -0
  56. package/playwright-ct.config.js +95 -0
  57. package/src/$locales/en.json +157 -157
  58. package/src/$locales/fr.json +120 -120
  59. package/src/assets/data/onboardingMessages.json +47 -47
  60. package/src/components/AppBase.vue +1171 -1169
  61. package/src/components/AppBaseButton.vue +90 -95
  62. package/src/components/AppBaseErrorDisplay.vue +438 -438
  63. package/src/components/AppBaseFlipCard.vue +84 -84
  64. package/src/components/AppBaseModule.vue +1639 -1634
  65. package/src/components/AppBasePage.vue +867 -866
  66. package/src/components/AppBasePopover.vue +41 -41
  67. package/src/components/AppBaseSkeleton.vue +66 -66
  68. package/src/components/AppCompAudio.vue +261 -256
  69. package/src/components/AppCompBranchButtons.vue +508 -508
  70. package/src/components/AppCompButtonProgress.vue +137 -132
  71. package/src/components/AppCompCarousel.vue +342 -336
  72. package/src/components/AppCompContainer.vue +29 -29
  73. package/src/components/AppCompInputCheckBoxNx.vue +325 -323
  74. package/src/components/AppCompInputDropdownNx.vue +302 -299
  75. package/src/components/AppCompInputRadioNx.vue +287 -284
  76. package/src/components/AppCompInputTextNx.vue +156 -153
  77. package/src/components/AppCompInputTextTableNx.vue +205 -202
  78. package/src/components/AppCompInputTextToFillDropdownNx.vue +343 -340
  79. package/src/components/AppCompInputTextToFillNx.vue +316 -313
  80. package/src/components/AppCompJauge.vue +81 -81
  81. package/src/components/AppCompMenu.vue +6 -1
  82. package/src/components/AppCompMenuItem.vue +246 -240
  83. package/src/components/AppCompNavigation.vue +977 -972
  84. package/src/components/AppCompNoteCall.vue +167 -161
  85. package/src/components/AppCompNoteCredit.vue +496 -491
  86. package/src/components/AppCompPlayBarNext.vue +2290 -2288
  87. package/src/components/AppCompPopUpNext.vue +508 -504
  88. package/src/components/AppCompQuizNext.vue +515 -510
  89. package/src/components/AppCompQuizRecall.vue +355 -350
  90. package/src/components/AppCompSVGNext.vue +346 -346
  91. package/src/components/AppCompSettingsMenu.vue +177 -172
  92. package/src/components/AppCompTableOfContent.vue +433 -427
  93. package/src/components/AppCompVideoPlayer.vue +377 -377
  94. package/src/components/AppCompViewDisplay.vue +6 -6
  95. package/src/components/BaseModule.vue +55 -55
  96. package/src/composables/useIdleDetector.js +56 -56
  97. package/src/composables/useQuiz.js +89 -89
  98. package/src/composables/useTimer.js +172 -172
  99. package/src/directives/nvdaFix.js +53 -53
  100. package/src/externalComps/ModuleView.vue +22 -22
  101. package/src/externalComps/SummaryView.vue +91 -91
  102. package/src/main.js +493 -476
  103. package/src/module/stores/appStore.js +960 -947
  104. package/src/module/xapi/ADL.js +520 -520
  105. package/src/module/xapi/Crypto/Hasher.js +241 -241
  106. package/src/module/xapi/Crypto/WordArray.js +278 -278
  107. package/src/module/xapi/Crypto/algorithms/BufferedBlockAlgorithm.js +103 -103
  108. package/src/module/xapi/Crypto/algorithms/C_algo.js +315 -315
  109. package/src/module/xapi/Crypto/algorithms/HMAC.js +9 -9
  110. package/src/module/xapi/Crypto/algorithms/SHA1.js +9 -9
  111. package/src/module/xapi/Crypto/encoders/Base.js +105 -105
  112. package/src/module/xapi/Crypto/encoders/Base64.js +99 -99
  113. package/src/module/xapi/Crypto/encoders/Hex.js +61 -61
  114. package/src/module/xapi/Crypto/encoders/Latin1.js +61 -61
  115. package/src/module/xapi/Crypto/encoders/Utf8.js +45 -45
  116. package/src/module/xapi/Crypto/index.js +53 -53
  117. package/src/module/xapi/Statement/activity.js +47 -47
  118. package/src/module/xapi/Statement/agent.js +55 -55
  119. package/src/module/xapi/Statement/group.js +26 -26
  120. package/src/module/xapi/Statement/index.js +259 -259
  121. package/src/module/xapi/Statement/statement.js +253 -253
  122. package/src/module/xapi/Statement/statementRef.js +23 -23
  123. package/src/module/xapi/Statement/substatement.js +22 -22
  124. package/src/module/xapi/Statement/verb.js +36 -36
  125. package/src/module/xapi/activitytypes.js +17 -17
  126. package/src/module/xapi/launch.js +157 -157
  127. package/src/module/xapi/utils.js +167 -167
  128. package/src/module/xapi/verbs.js +294 -294
  129. package/src/module/xapi/wrapper.js +1895 -1895
  130. package/src/module/xapi/xapiStatement.js +444 -444
  131. package/src/plugins/analytics.js +34 -34
  132. package/src/plugins/bus.js +12 -8
  133. package/src/plugins/gsap.js +17 -17
  134. package/src/plugins/helper.js +355 -358
  135. package/src/plugins/i18n.js +27 -26
  136. package/src/plugins/idb.js +227 -227
  137. package/src/plugins/save.js +37 -37
  138. package/src/plugins/scorm.js +287 -287
  139. package/src/plugins/xapi.js +11 -11
  140. package/src/public/index.html +33 -33
  141. package/src/router/index.js +57 -57
  142. package/src/router/routes.js +312 -312
  143. package/src/shared/generalfuncs.js +344 -344
  144. package/src/shared/validators.js +1018 -1018
  145. package/tests/component/AppBaseButton.spec.js +53 -0
  146. package/tests/component/pinia.spec.js +24 -0
  147. package/{src/components/tests__ → tests/unit}/AppBaseButton.spec.js +53 -53
  148. package/tests/unit/AppCompInputCheckBoxNx.spec.js +59 -0
  149. package/tests/unit/AppCompInputDropdownNx.spec.js +51 -0
  150. package/tests/unit/AppCompInputRadioNx.spec.js +59 -0
  151. package/tests/unit/AppCompInputTextNx.spec.js +44 -0
  152. package/tests/unit/AppCompInputTextTableNx.spec.js +77 -0
  153. package/tests/unit/AppCompInputTextToFillDropdownNx.spec.js +60 -0
  154. package/tests/unit/AppCompInputTextToFillNx.spec.js +45 -0
  155. package/tests/unit/AppCompQuizNext.spec.js +114 -0
  156. package/tests/unit/AppCompVideoPlayer.spec.js +177 -0
  157. package/{src/components/tests__ → tests/unit}/useTimer.spec.js +91 -91
  158. package/vitest.config.js +28 -19
  159. package/vitest.setup.js +28 -0
  160. package/src/components/AppBaseButton.test.js +0 -21
@@ -1,2288 +1,2290 @@
1
- <!--@ Description: This component is used to display the playbar associate with the videoPlayer
2
- @ What it does: The component is used to interacted with the videoPlayer or audioPlayer via buttons.
3
- mediaMixins is used for all the methods/data shared between audio and video. In the appCompPlayBar component, there is also all the methods/data specific to video-->
4
- <template>
5
- <div
6
- ref="$pb-container"
7
- class="pb-container"
8
- :class="{ video: mediaToPlay.mType === 'video' }"
9
- >
10
- <!--------------------------------- Btn Play Principal (video only) -------------------------------------->
11
- <app-base-button
12
- v-if="mediaToPlay.mType === 'video'"
13
- ref="$btn-play-playback"
14
- class="playback-button"
15
- :class="{
16
- initial: !playClicked,
17
- 'playback-animation': playBackAnim && playClicked
18
- }"
19
- :aria-label="playLabel + ' principal'"
20
- :data-title="`${playLabel}`"
21
- @click="
22
- () => {
23
- mediaToPlay.mType === 'video'
24
- ? handlePlayVideo('playBackAnim')
25
- : togglePlay()
26
- }
27
- "
28
- >
29
- <div
30
- v-show="(!playClicked && !canReplay) || (!canReplay && isPlaying)"
31
- class="playback-wrapper"
32
- >
33
- <svg
34
- class="app-icons-svg play-icon"
35
- aria-hidden="true"
36
- focusable="false"
37
- >
38
- <use href="#play-icon" />
39
- </svg>
40
- </div>
41
- <div
42
- v-show="playClicked && !isPlaying && !canReplay"
43
- class="playback-wrapper"
44
- >
45
- <svg
46
- class="app-icons-svg pause-icon"
47
- aria-hidden="true"
48
- focusable="false"
49
- >
50
- <use href="#pause-icon" />
51
- </svg>
52
- </div>
53
- <div v-show="!playClicked && canReplay" class="playback-wrapper">
54
- <svg
55
- class="app-icons-svg replay-icon"
56
- aria-hidden="true"
57
- focusable="false"
58
- >
59
- <use href="#replay-icon" />
60
- </svg>
61
- </div>
62
- </app-base-button>
63
-
64
- <div
65
- v-if="mediaToPlay"
66
- class="pb-wrapper"
67
- :class="{
68
- 'show-controls': showControlsValue || mediaToPlay.mType === 'audio',
69
- audio: mediaToPlay.mType === 'audio',
70
- video: mediaToPlay.mType === 'video'
71
- }"
72
- >
73
- <!--------------------------------- PLAY-BAR Progress (video only) -------------------------------------->
74
- <div
75
- v-if="mediaToPlay.mType === 'video'"
76
- ref="$playbar-timeline"
77
- class="pb-timeline"
78
- >
79
- <div ref="$progress-area" class="progress-area">
80
- <div
81
- ref="$progress-bar"
82
- draggable="false"
83
- tabindex="0"
84
- class="pb-progress-bar"
85
- role="slider"
86
- aria-valuemin="0"
87
- :aria-label="mediaA11Y.label"
88
- :aria-valuenow="mediaA11Y.valNow"
89
- :aria-valuemax="mediaA11Y.valMax"
90
- :aria-valuetext="mediaA11Y.valueText"
91
- @focus="changeFocusState('progressBar', true)"
92
- @blur="changeFocusState('progressBar', false)"
93
- >
94
- <!--Class progress-animation is apply when we are not using the thumb to change the progression-->
95
- <div
96
- ref="$progress-indicator"
97
- draggable="false"
98
- class="progress-indicator"
99
- :class="{ 'progress-animation': !progressThumbDown }"
100
- :style="{ width: progressBarPercentage + '%' }"
101
- >
102
- <!--Mousedown validate if the thumb is cliqued (if yes, the mouse is follow to modify the progress)
103
- MouseOver/MouseOut deactivate click event on progressBar if the mouse hovered the thumb-->
104
- <div
105
- ref="$progress-thumb"
106
- draggable="false"
107
- class="progress-thumb"
108
- style="z-index: 9"
109
- @mousedown="
110
- function () {
111
- savedIsPlaying = isPlaying
112
- progressThumbDown = true
113
- }
114
- "
115
- @mouseover="progressThumbHover = true"
116
- @mouseleave="progressThumbHover = false"
117
- ></div>
118
- </div>
119
- </div>
120
- <!--------------------------------- PLAY-BAR ToolTip (video only) ----------------------------------->
121
- <div
122
- v-if="mediaToPlay.mType === 'video'"
123
- ref="$seek-tooltip"
124
- aria-hidden="true"
125
- class="seek-tooltip"
126
- >
127
- {{ tooTipTimeCode }}
128
- </div>
129
- </div>
130
- </div>
131
- <div class="pb-controls">
132
- <div class="pb-areas left">
133
- <!--------------------------------- PLAY-BAR Play ------------------------------------>
134
- <app-base-button
135
- ref="$btn-play"
136
- class="playbar-btn playbar-play"
137
- :class="{ displayLabel: btnsLabelDisplay['playbar-play'] === true }"
138
- :aria-label="
139
- playLabel + (mediaToPlay.mType === 'video' ? ' secondaire' : '')
140
- "
141
- :data-title="`${playLabel} (k)`"
142
- @click="
143
- () => {
144
- mediaToPlay.mType === 'video' ? handlePlayVideo() : togglePlay()
145
- }
146
- "
147
- @mouseenter="activateLabel($event)"
148
- @mouseleave="deactivateLabel($event)"
149
- @focus="activateLabel($event)"
150
- @blur="deactivateLabel($event)"
151
- >
152
- <svg
153
- v-show="!canReplay && !isPlaying"
154
- class="app-icons-svg play-icon"
155
- aria-hidden="true"
156
- focusable="false"
157
- >
158
- <use href="#play-icon" />
159
- </svg>
160
- <svg
161
- v-show="isPlaying && !canReplay"
162
- class="app-icons-svg pause-icon"
163
- aria-hidden="true"
164
- focusable="false"
165
- >
166
- <use href="#pause-icon" />
167
- </svg>
168
- <svg
169
- v-show="canReplay"
170
- class="app-icons-svg replay-icon"
171
- aria-hidden="true"
172
- focusable="false"
173
- >
174
- <use href="#replay-icon" />
175
- </svg>
176
- </app-base-button>
177
- <!--------------------------------- PLAY-BAR Timer ------------------------------------>
178
- <div class="pb-timer">
179
- <span
180
- aria-hidden="true"
181
- style="-webkit-user-select: none"
182
- draggable="false"
183
- >
184
- {{ timecode }} / {{ mediaDurationTime }}
185
- </span>
186
- </div>
187
- <!--------------------------------- PLAY-BAR Progress (audio only) -------------------------------------->
188
- <div
189
- v-if="mediaToPlay.mType === 'audio'"
190
- ref="$playbar-timeline"
191
- class="pb-timeline"
192
- >
193
- <div ref="$progress-area" class="progress-area">
194
- <div
195
- id="progress-bar"
196
- ref="$progress-bar"
197
- draggable="false"
198
- tabindex="0"
199
- class="pb-progress-bar"
200
- role="slider"
201
- aria-valuemin="0"
202
- :aria-label="mediaA11Y.label"
203
- :aria-valuenow="mediaA11Y.valNow"
204
- :aria-valuemax="mediaA11Y.valMax"
205
- :aria-valuetext="mediaA11Y.valueText"
206
- @focus="changeFocusState('progressBar', true)"
207
- @blur="changeFocusState('progressBar', false)"
208
- >
209
- <!--Class progress-animation is apply when we are not using the thumb to change the progression-->
210
- <div
211
- id="progress-indicator"
212
- ref="$progress-indicator"
213
- draggable="false"
214
- class="progress-indicator"
215
- :class="{ 'progress-animation': !progressThumbDown }"
216
- :style="{ width: progressBarPercentage + '%' }"
217
- >
218
- <!--Mousedown validate if the thumb is cliqued (if yes, the mouse is follow to modify the progress)
219
- MouseOver/MouseOut deactivate click event on progressBar if the mouse hovered the thumb-->
220
- <div
221
- ref="$progress-thumb"
222
- draggable="false"
223
- class="progress-thumb"
224
- style="z-index: 9"
225
- @mousedown="
226
- function () {
227
- savedIsPlaying = isPlaying
228
- progressThumbDown = true
229
- }
230
- "
231
- @mouseover="progressThumbHover = true"
232
- @mouseleave="progressThumbHover = false"
233
- ></div>
234
- </div>
235
- </div>
236
- </div>
237
- </div>
238
- <!--------------------------------- PLAY-BAR Volume -------------------------------------->
239
- <app-base-button
240
- id="playbar-volume"
241
- ref="$btn-volume"
242
- class="volume-btn"
243
- :class="{
244
- displayLabel: btnsLabelDisplay['playbar-volume'] === true
245
- }"
246
- tabindex="0"
247
- :title="volumeLabel"
248
- :aria-label="volumeLabel"
249
- :data-title="`${volumeLabel} (m)`"
250
- @click="toggleMute"
251
- @mouseenter="activateLabel($event)"
252
- @mouseleave="deactivateLabel($event)"
253
- @focus="activateLabel($event)"
254
- @blur="deactivateLabel($event)"
255
- >
256
- <svg
257
- v-show="volumeState == 'muted'"
258
- class="app-icons-svg"
259
- aria-hidden="true"
260
- focusable="false"
261
- >
262
- <use href="#volume-mute-icon" />
263
- </svg>
264
- <svg
265
- v-show="volumeState == 'high'"
266
- class="app-icons-svg"
267
- aria-hidden="true"
268
- focusable="false"
269
- >
270
- <use href="#volume-high-icon" />
271
- </svg>
272
- <svg
273
- v-show="volumeState == 'low'"
274
- class="app-icons-svg volume-low-icon"
275
- aria-hidden="true"
276
- focusable="false"
277
- >
278
- <use href="#volume-low-icon" />
279
- </svg>
280
- </app-base-button>
281
- <div ref="zone-volume" class="volume-controls">
282
- <input
283
- ref="$volume-slider"
284
- :title="`${$t('button.volume')}`"
285
- class="volume-slider"
286
- :class="{
287
- displayLabel: btnsLabelDisplay['volume-slider'] === true
288
- }"
289
- :value="currentVolume"
290
- :aria-valuetext="volumeLevelA11Y"
291
- type="range"
292
- max="1"
293
- min="0"
294
- step="0.01"
295
- tabindex="0"
296
- :aria-label="`${$t('button.volume')}`"
297
- @click="updateVolumeLevel"
298
- @mouseenter="activateLabel($event.currentTarget)"
299
- @mouseleave="deactivateLabel($event.currentTarget)"
300
- @focus="
301
- (activateLabel($event.currentTarget),
302
- changeFocusState('volumeSlider', true))
303
- "
304
- @blur="
305
- (deactivateLabel($event.currentTarget),
306
- changeFocusState('volumeSlider', false))
307
- "
308
- />
309
- <span class="volume-label" aria-hidden="true">
310
- {{ $t('button.volume') }}
311
- </span>
312
- <!--using span because ::before on input is not supported by firefox-->
313
- <span
314
- ref="$volume-progress"
315
- class="volume-progress"
316
- :style="{
317
- '--background-size-vs': `calc(${volumeSliderBackground} - 2px)`
318
- }"
319
- ></span>
320
- </div>
321
- </div>
322
- <!--------------------------------- PLAY-BAR CC (video only) ------------------------------------------>
323
- <div v-if="mediaToPlay.mType === 'video'" class="pb-areas right">
324
- <app-base-button
325
- id="btn-subtitles"
326
- ref="$btn-subtitle"
327
- class="btn subtitleBtns"
328
- :title="ccLabel"
329
- :aria-label="ccLabel"
330
- :data-title="`${ccLabel} (c)`"
331
- :is-disabled="!hasSubtitle"
332
- :class="{
333
- md_disabled: !hasSubtitle,
334
- displayLabel: btnsLabelDisplay['btn-subtitles'] === true
335
- }"
336
- @click="toggleViewSubtitle()"
337
- @mouseenter="activateLabel($event)"
338
- @mouseleave="deactivateLabel($event)"
339
- @focus="activateLabel($event)"
340
- @blur="deactivateLabel($event)"
341
- >
342
- <svg
343
- v-show="(hasSubtitle && !subtitlesEnabled) || !hasSubtitle"
344
- class="app-icons-svg"
345
- aria-hidden="true"
346
- focusable="false"
347
- >
348
- <use href="#subtitle-off-icon" />
349
- </svg>
350
- <svg
351
- v-show="hasSubtitle && subtitlesEnabled"
352
- class="app-icons-svg"
353
- aria-hidden="true"
354
- focusable="false"
355
- >
356
- <use href="#subtitle-on-icon" />
357
- </svg>
358
- </app-base-button>
359
- <!--------------------------------- PLAY-BAR Transcript ----------------------------------->
360
- <app-base-button
361
- id="btn-transcript"
362
- ref="$btn-transcript"
363
- class="btn-transcript"
364
- :title="transcriptLabel"
365
- :aria-label="transcriptLabel"
366
- :data-title="`${transcriptLabel} (t)`"
367
- :class="{
368
- md_disabled: transcriptBtnDisabled,
369
- displayLabel: btnsLabelDisplay['btn-transcript'] === true
370
- }"
371
- :is-disabled="transcriptBtnDisabled"
372
- @click="toggleViewTranscript($event)"
373
- @mouseenter="activateLabel($event)"
374
- @mouseleave="deactivateLabel($event)"
375
- @focus="activateLabel($event)"
376
- @blur="deactivateLabel($event)"
377
- >
378
- <svg
379
- v-show="(hasTranscript && !transcriptEnabled) || !hasTranscript"
380
- class="app-icons-svg"
381
- aria-hidden="true"
382
- focusable="false"
383
- >
384
- <use href="#transcript-off-icon" />
385
- </svg>
386
- <svg
387
- v-show="hasTranscript && transcriptEnabled"
388
- class="app-icons-svg"
389
- aria-hidden="true"
390
- focusable="false"
391
- >
392
- <use href="#transcript-on-icon" />
393
- </svg>
394
- </app-base-button>
395
- <!--------------------------------- PLAY-BAR FullScreen ----------------------------------->
396
- <app-base-button
397
- id="btn-fullscreen"
398
- ref="$btn-fullscreen"
399
- class="fullscreenBtns"
400
- :title="fullscreenLabel"
401
- :aria-label="fullscreenLabel"
402
- :class="{
403
- md_disabled: fullscreenBtnDisabled,
404
- displayLabel: btnsLabelDisplay['btn-fullscreen'] === true
405
- }"
406
- :is-disabled="fullscreenBtnDisabled"
407
- :data-title="`${fullscreenLabel} (f)`"
408
- @click="toggleFullScreen()"
409
- @mouseenter="activateLabel($event)"
410
- @mouseleave="deactivateLabel($event)"
411
- @focus="activateLabel($event)"
412
- @blur="deactivateLabel($event)"
413
- >
414
- <svg
415
- v-show="!fullscreenOn"
416
- class="app-icons-svg"
417
- aria-hidden="true"
418
- focusable="false"
419
- >
420
- <use href="#fullscreen-icon" />
421
- </svg>
422
- <svg
423
- v-show="fullscreenOn"
424
- class="app-icons-svg"
425
- aria-hidden="true"
426
- focusable="false"
427
- >
428
- <use href="#fullscreen-exit-icon" />
429
- </svg>
430
- </app-base-button>
431
- </div>
432
- </div>
433
- <div v-if="appDebugMode" class="timer">
434
- <!-- <div class="timer"> -->
435
- <p></p>
436
- <p>Timer : {{ timer.getElapsedTime() }}</p>
437
- </div>
438
- </div>
439
- </div>
440
- </template>
441
-
442
- <script>
443
- import { mapState, mapActions } from 'pinia'
444
- import { useAppStore } from '../module/stores/appStore'
445
- import axios from 'axios'
446
- import { Timer } from '../composables/useTimer'
447
- import { reactive } from 'vue'
448
- export default {
449
- inject: ['userInteraction'],
450
- props: {
451
- mediaToPlay: { type: [Object, Boolean], default: false }
452
- },
453
- emits: ['resize-video'],
454
-
455
- setup(props) {
456
- const store = useAppStore()
457
- const id = `plyr_${props.mediaToPlay.id}`
458
- const timer = reactive(new Timer(id)) // Making Timer instance reactive to be able to track changes
459
-
460
- return { id, store, timer }
461
- },
462
- data() {
463
- return {
464
- //Playback animation
465
- playClicked: false,
466
- playBackAnim: true,
467
- //ProgressSeek
468
- progressSeek: null,
469
- //Tooltip
470
- seekTooltip: null,
471
- tooTipTimeCode: '00:00',
472
- //Hiding playbar animation
473
- focusTimeout: null,
474
- delayUntilHide: 5000,
475
- hideTimer: null,
476
- //Transcript
477
- transcriptEnabled: false,
478
- transcriptToShow: null,
479
- //Fullscreen
480
- fullscreenOn: false,
481
- //==========================================================
482
- //Container Playbar
483
- pbContainer: null,
484
- //ShowControls (show the playbar or not, default is true)
485
- showControlsValue: true,
486
- //Player
487
- isPlaying: false,
488
- canReplay: false,
489
- savedIsPlaying: false, //To get the playing status on mousemove on the progressBar
490
- //ProgressBar
491
- currentTime: 0,
492
- progressArea: null,
493
- progressBar: null,
494
- progressIndicator: null,
495
- progressThumb: null,
496
- progressThumbDown: false,
497
- progressThumbHover: false,
498
- //Volume
499
- volumeBtn: null,
500
- volumeSlider: null,
501
- volumeIndicator: null,
502
- currentVolume: null,
503
- savedVolume: 0.5, //Saved volume when muted
504
- //muted
505
- muted: false,
506
- isMediatSet: false,
507
-
508
- //Labels
509
- //Strings for screenreaders (duration et time)
510
- mediaA11Y: {
511
- label: 'progress',
512
- valMax: null,
513
- valNow: null,
514
- valueText: null,
515
- timeCode: 0,
516
- duration: 0
517
- },
518
- //buttons label (all the buttons that need labels management in the playbar)
519
- btnsLabelDisplay: {
520
- 'btn-subtitles': false,
521
- 'btn-transcript': false,
522
- 'btn-fullscreen': false,
523
- 'playbar-play': false,
524
- 'playbar-volume': false,
525
- 'volume-slider': false
526
- },
527
- //FocusState to manage arrows keydown on the 2 sliders (progress and volume)
528
- focusState: {
529
- progressBar: false,
530
- volumeSlider: false
531
- },
532
- otherVideoTranscriptShown: false,
533
- previousTime: 0,
534
- viewedThreshold: 0.85,
535
- viewedThresholdReached: false,
536
- startedTime: 0,
537
- playStmtSent: false,
538
- mediaRawData: null
539
-
540
- //==========================================================
541
- }
542
- },
543
- computed: {
544
- ...mapState(useAppStore, [
545
- 'getCurrentBrowser',
546
- 'getMediaSubtitles',
547
- 'getCurrentPage',
548
- 'getModuleChildren',
549
- 'hasMediaElOrTimeline',
550
- 'getCurrentMediaDuration',
551
- 'getAutoplayEnabled',
552
- 'getMediaVolume',
553
- 'getModuleInfo',
554
- 'getMediaPlaybarValues',
555
- 'getPageInteraction',
556
- 'getUserInteraction',
557
- 'getMediaMuted',
558
- 'getAppDebugMode'
559
- ]),
560
- appDebugMode() {
561
- return this.getAppDebugMode
562
- },
563
- //==========================================================
564
- //MediaElement
565
- mediaElement() {
566
- if (!this.mediaToPlay) return null
567
- const { mElement } = this.mediaToPlay
568
- return mElement ? mElement : null
569
- },
570
- //Media Progress
571
- //Time display : duration
572
- mediaDurationTime() {
573
- if (this.mediaDuration > 0)
574
- return this.$helper.formatTime(this.mediaDuration)
575
- else return '00:00'
576
- },
577
- //Time display : timecode
578
- timecode() {
579
- if (this.currentTime && typeof (this.currentTime === Number)) {
580
- return this.$helper.formatTime(this.currentTime)
581
- } else return '00:00'
582
- },
583
- //ProgressPercentage for progressBarIndicator width
584
- progressBarPercentage() {
585
- if (this.currentTime && this.mediaDuration)
586
- return (this.currentTime / this.mediaDuration) * 100
587
- else return 0
588
- },
589
- progressBarEnded() {
590
- return this.currentTime >= Math.floor(this.mediaDuration)
591
- },
592
- //Volume
593
- volumeState() {
594
- let state = null
595
- switch (true) {
596
- case this.muted || this.currentVolume <= 0:
597
- state = 'muted'
598
- break
599
-
600
- case !this.muted && this.currentVolume <= 0.5:
601
- state = 'low'
602
- break
603
- case !this.muted && this.currentVolume > 0.5:
604
- state = 'high'
605
- }
606
-
607
- return state
608
- },
609
- //Volume progress for slider background
610
- volumeSliderBackground() {
611
- return (this.currentVolume * 100).toFixed(0).toString() + '%'
612
- },
613
- //Volume from store
614
- mediaVolume() {
615
- // return this.getMediaVolume
616
- return this.getMediaPlaybarValues('volume')
617
- },
618
- //media from store
619
- mediaMuted() {
620
- return this.getMediaMuted
621
- },
622
- //Labels
623
- playLabel() {
624
- let label = null
625
- switch (true) {
626
- case this.isPlaying:
627
- label = this.$t('button.pause')
628
- break
629
-
630
- case this.canReplay:
631
- label = this.$t('button.replay')
632
- break
633
-
634
- default:
635
- label = this.$t('button.play')
636
- }
637
- return label
638
- },
639
- volumeLabel() {
640
- let label = null
641
- switch (true) {
642
- case this.muted:
643
- label = `${this.$t('button.unmute')}`
644
- break
645
- case !this.muted:
646
- label = `${this.$t('button.mute')}`
647
- break
648
- }
649
- return label
650
- },
651
- volumeLevelA11Y() {
652
- const w = this.$i18n.locale === 'en' ? 'at' : 'à'
653
- const level = Math.round(this.currentVolume * 100)
654
- let txtA11Y = `Volume ${w} ${level}%`
655
-
656
- return txtA11Y
657
- },
658
-
659
- //Transcript
660
- hasTranscript() {
661
- if (!this.mediaToPlay) return
662
- return this.mediaToPlay.mTranscript || false
663
- },
664
- //Subtitle
665
- hasSubtitle() {
666
- if (!this.mediaToPlay) return
667
- return this.mediaToPlay.mSubtitles || false
668
- },
669
- subtitlesEnabled() {
670
- if (!this.hasSubtitle) return false
671
- else return this.getMediaSubtitles
672
- },
673
- //mediaContainer (only used for video)
674
- mediaContainer() {
675
- if (!this.mediaToPlay) return
676
- return this.mediaToPlay.mMediaContainer
677
- },
678
-
679
- //MediaDuration :Number (used for tooltip)
680
- mediaDuration() {
681
- return this.mediaToPlay.mElement.duration
682
- ? this.mediaToPlay.mElement.duration
683
- : 0
684
- },
685
- //Labels
686
- fullscreenLabel() {
687
- let label = null
688
- if (this.fullscreenOn) label = `${this.$t('button.full_screen_off')}`
689
- else label = `${this.$t('button.full_screen_on')}`
690
- return label
691
- },
692
- transcriptLabel() {
693
- let label = null
694
- if (this.transcriptEnabled) label = `${this.$t('button.transcript_off')}`
695
- else label = `${this.$t('button.transcript_on')}`
696
- return label
697
- },
698
- ccLabel() {
699
- let label = null
700
- if (this.subtitlesEnabled) label = `${this.$t('button.subtitle_off')}`
701
- else label = `${this.$t('button.subtitle_on')}`
702
- return label
703
- },
704
- transcriptBtnDisabled() {
705
- return (
706
- !this.hasTranscript ||
707
- this.fullscreenOn ||
708
- this.otherVideoTranscriptShown
709
- )
710
- },
711
- fullscreenBtnDisabled() {
712
- return this.transcriptEnabled || this.otherVideoTranscriptShown
713
- }
714
- },
715
- watch: {
716
- viewedThresholdReached: {
717
- handler() {
718
- //send a "viewed" event for this media
719
- this.trackEvent('end')
720
- }
721
- },
722
- getUserInteraction: {
723
- handler() {
724
- const activityID = this.$route.meta.activity_ref
725
- const pageID = this.$route.meta.id
726
- const interaction = this.getPageInteraction(
727
- activityID,
728
- pageID
729
- ).userInteraction
730
-
731
- if (
732
- !interaction ||
733
- !interaction.mediasViewed ||
734
- !interaction.mediasViewed.length
735
- )
736
- return
737
-
738
- this.getViewedSatus(interaction.mediasViewed)
739
- this.setMediaVolume()
740
- },
741
- immediate: true
742
- // deep: true
743
- },
744
- getCurrentPage: {
745
- handler(newValue) {
746
- if (!newValue.audiosData && !newValue.videosData) return
747
- const { audiosData, videosData } = this.getCurrentPage
748
- this.mediaRawData =
749
- this.mediaToPlay.mType == 'video'
750
- ? videosData.find((el) => el.id == this.mediaToPlay.id)
751
- : audiosData.find((el) => el.id == this.mediaToPlay.id)
752
- },
753
- immediate: true,
754
- deep: true
755
- }
756
- },
757
-
758
- mounted() {
759
- //initialize media $refs
760
- this.initializeMediaElm()
761
- //initialize video $refs && set eventlistener for video
762
- if (this.mediaToPlay.mType === 'video') {
763
- this.initializeVideoElm()
764
- this.setVideoHandlers()
765
- }
766
- //Set eventlistener for media
767
- this.setMediaHandlers()
768
-
769
- //Set label for ProgressBar
770
- this.setProgressBarA11Y()
771
- //Set initial volume and duration
772
- this.updateVolumeLevel()
773
-
774
- //update the the information of the media element linked to this play bar. playbar instance will be added to the media info
775
- this.updateCurrentMediaElements({
776
- id: this.mediaToPlay.id,
777
- ...this.$parent.$refs
778
- })
779
- },
780
- beforeUnmount() {
781
- if (this.isPlaying) {
782
- this.togglePlay()
783
- }
784
- this.removeMediaHandlers()
785
-
786
- if (this.mediaToPlay.mType === 'video') this.removeVideoHandlers()
787
-
788
- if (this.firefoxTrack) {
789
- this.firefoxTrack.removeEventListener('cuechange', this.fireFoxMoveCC)
790
- }
791
-
792
- this.$bus.$off('transcript-hidden', this.resetTranscript)
793
- },
794
- methods: {
795
- ...mapActions(useAppStore, [
796
- 'setMediaSubtitles',
797
- 'updateCurrentMediaElements',
798
- 'setMediaMuted',
799
- 'setMediaPlaybarValues'
800
- ]),
801
- trackEvent(action) {
802
- const expectedActions = ['play', 'viewed']
803
- if (!action || !expectedActions.includes(action)) return
804
- const media = this.mediaRawData
805
- if (!media || !media.mSources || !media.mSources.length) return
806
- const { mTitle, mSources } = media
807
- const { mType } = this.mediaToPlay
808
- const URIBase = `media/${mType}/`
809
-
810
- //send statement
811
- const stmt = {
812
- id: (() => `${URIBase}${mSources[0].src.split('/').toReversed()[0]}`)(),
813
- verb: null,
814
- definition: mTitle || mSources[0].src,
815
- description: null,
816
- type: `https://w3id.org/xapi/${mType}/activity-type/${mType}`,
817
- context: {
818
- contextActivities: {
819
- category: [{ id: `https://w3id.org/xapi/${mType}` }]
820
- },
821
- extensions: {
822
- 'https://w3id.org/xapi/video/extensions/session-id':
823
- this.getModuleInfo.id
824
- }
825
- },
826
- result: null
827
- }
828
- switch (action) {
829
- case 'play': {
830
- stmt.verb = 'played'
831
- stmt.description = `${mType} ${stmt.id.replace(URIBase, '')}`
832
- stmt.result = {
833
- extensions: {
834
- 'https://w3id.org/xapi/video/extensions/time': Math.round(
835
- this.startedTime
836
- ),
837
- 'https://w3id.org/xapi/video/extensions/progress': Math.round(
838
- (this.startedTime / this.mediaDuration) * 100
839
- )
840
- }
841
- }
842
- break
843
- }
844
- case 'viewed': {
845
- const expectedViewTime = Number(
846
- this.timer.getElapsedTime() / Math.trunc(this.mediaDuration)
847
- )
848
- stmt.verb = 'completed'
849
- stmt.description = `${mType} ${stmt.id.replace(URIBase, '')}`
850
- stmt.result = {
851
- extensions: {
852
- 'https://w3id.org/xapi/video/extensions/progress':
853
- expectedViewTime * 100,
854
- 'https://w3id.org/xapi/video/extensions/time-from': Math.round(
855
- this.startedTime
856
- ),
857
- 'https://w3id.org/xapi/video/extensions/time-to': this.currentTime
858
- },
859
- completion:
860
- expectedViewTime >= this.viewedThreshold &&
861
- this.viewedThresholdReached
862
- }
863
- stmt.duration = this.timer.ISOTimeParser(
864
- expectedViewTime * this.mediaDuration
865
- )
866
- break
867
- }
868
- }
869
- this.$bus.$emit('send-xapi-statement', stmt)
870
- //send Analytics Event
871
- const analyticsEventName = `fcad_${mType}_${action}`
872
- this.trackWithGA(analyticsEventName)
873
- },
874
- trackWithGA(eventName) {
875
- if (
876
- !eventName ||
877
- !this.mediaRawData ||
878
- !this.mediaRawData.mSources ||
879
- !this.mediaRawData.mSources.length
880
- )
881
- return
882
- const { mTitle, mSources } = this.mediaRawData
883
-
884
- const eventParams = {
885
- title: mTitle || mSources[0].src,
886
- url: mSources[0].src
887
- }
888
-
889
- this.$analytics.sendEvent(eventName, eventParams)
890
- },
891
-
892
- //======================================================================
893
- /**
894
- * @description - Set all the DOM elements for medias
895
- */
896
- initializeMediaElm() {
897
- this.pbContainer = this.$refs['$pb-container']
898
- //ProgressBar
899
- this.progressArea = this.$refs['$progress-area']
900
- this.progressBar = this.$refs['$progress-bar']
901
- this.progressIndicator = this.$refs['$progress-indicator']
902
- this.progressThumb = this.$refs['$progress-thumb']
903
- //SeekTooltip
904
- this.seekTooltip = this.$refs['$seek-tooltip']
905
- //Volume//
906
- this.volumeBtn = this.$refs['$btn-volume']
907
- this.volumeSlider = this.$refs['$volume-slider']
908
- this.volumeIndicator = this.$refs['$volume-progress']
909
- },
910
- /**
911
- * @description - handle all the listeners for medias
912
- */
913
- setMediaHandlers() {
914
- if (this.mediaElement) {
915
- //Prevent default on keys used for navigation in the player (prevent scrollbar to be trigger when changing volume)
916
- this.pbContainer.addEventListener('keydown', this.keysPreventDefault)
917
- //progressBar events
918
- this.progressArea.addEventListener('click', this.seekingProgress)
919
- //window handlers for playbar progress
920
- window.addEventListener('mousemove', this.progressWindowMove)
921
- window.addEventListener('mouseup', this.progressWindowUp)
922
-
923
- //Update data when media as a timeupdate/ended
924
- this.mediaElement.addEventListener(
925
- 'timeupdate',
926
- this.updateProgressBarTime
927
- )
928
- this.mediaElement.addEventListener('ended', this.mediaEnded)
929
- //Volume
930
- this.volumeSlider.addEventListener('input', this.updateVolumeLevel)
931
- //Hotkeys
932
- this.pbContainer.addEventListener('keydown', this.handleMediaControls)
933
- }
934
- },
935
-
936
- /**
937
- * @description - Unset all the listeners (medias)
938
- */
939
- removeMediaHandlers() {
940
- if (this.mediaElement) {
941
- //progressBar events
942
- this.progressArea.removeEventListener('click', this.seekingProgress)
943
- //window handlers for playbar progress
944
- window.removeEventListener('mousemove', this.progressWindowMove)
945
- window.removeEventListener('mouseup', this.progressWindowUp)
946
-
947
- this.mediaElement.removeEventListener(
948
- 'timeupdate',
949
- this.updateProgressBarTime
950
- )
951
- this.mediaElement.removeEventListener('ended', this.mediaEnded)
952
-
953
- //Volume
954
- this.volumeSlider.removeEventListener('input', this.updateVolumeLevel)
955
- //Keys
956
- this.pbContainer.removeEventListener('keydown', this.keysPreventDefault)
957
- this.pbContainer.removeEventListener(
958
- 'keydown',
959
- this.handleMediaControls
960
- )
961
- }
962
- },
963
- /**
964
- * @description - play or pause the media
965
- * when the media starts playing, send a statement to the LRS.
966
- * Statement is sent only once per media play
967
- * @fires manage-media-players - to the Page the media in play
968
- */
969
- async togglePlay() {
970
- //If the progressBar is at the end, restart the media
971
- if (this.progressBarEnded) {
972
- this.mediaElement.currentTime = 0
973
- this.currentTime = 0
974
- }
975
- //Play or Pause the media
976
- this.isPlaying ? this.mediaElement.pause() : this.mediaElement.play()
977
- this.startedTime = this.mediaElement.currentTime
978
- // start or pause the timer depending of the state of the media
979
- !this.isPlaying ? this.timer.start() : this.timer.pause()
980
-
981
- if (!this.playStmtSent) {
982
- //this.sendMediaStatement("play", this.startedTime, null);
983
- this.trackEvent('play')
984
- this.playStmtSent = true
985
- }
986
- //Data
987
- if (!this.isPlaying) {
988
- //Signal to set this mediaElement as the last playing
989
- this.$bus.$emit('manage-media-players', this.mediaToPlay)
990
- this.previousTime = Math.round(this.mediaElement.currentTime)
991
- }
992
- this.isPlaying = !this.isPlaying
993
- this.canReplay = false
994
- },
995
- /**
996
- * @description - Set the media to a new time (if its a time between 0 and max)
997
- */
998
- setMediaTime(time) {
999
- if (time <= 0) this.mediaElement.currentTime = 0
1000
- else if (time > this.mediaDuration)
1001
- this.mediaElement.currentTime = this.mediaDuration
1002
- else this.mediaElement.currentTime = time
1003
- //Handle finish Media if its set to max duration
1004
- if (this.progressBarEnded) {
1005
- this.mediaEnded()
1006
- }
1007
- },
1008
- /**
1009
- * @description - Set the media Ended if it is set to the end of the progressBar by the user or if it plays until the end
1010
- */
1011
-
1012
- mediaEnded() {
1013
- //Don't make the media play when release progressBar thumb and the media is Ended
1014
- this.savedIsPlaying = false
1015
- //Pausing mediaElement is isPlaying
1016
- if (this.mediaElement && this.isPlaying) {
1017
- this.mediaElement.pause()
1018
- this.isPlaying = false
1019
- this.timer.stop()
1020
-
1021
- //this.sendMediaStatement("end", this.startedTime, expectedViewTime);
1022
- this.trackEvent('viewed')
1023
- this.playStmtSent = false //reset the play statement for next play
1024
- }
1025
- this.$bus.$emit('media-viewed', this.mediaToPlay.id)
1026
- this.canReplay = true
1027
- if (this.mediaToPlay.mType === 'video') this.showControls()
1028
- },
1029
-
1030
- /**
1031
- * @description get the view Status of the current media from UserInteraction
1032
- */
1033
- getViewedSatus(mediasViewed) {
1034
- // Should update the can replay state of media
1035
- this.canReplay = mediasViewed.includes(this.mediaElement.id)
1036
- },
1037
- /**
1038
- * @description controls the level Of the media volume
1039
- * @param d string, optional volUp or volDown to Add or Remove 0.05 from volume value
1040
- */
1041
- updateVolumeLevel(d = null) {
1042
- if (!this.mediaElement) return
1043
- if (this.mediaElement.muted) {
1044
- this.mediaElement.muted = this.muted = false
1045
- //Send muted state to the store
1046
- this.setMutedState(this.muted)
1047
- }
1048
- switch (true) {
1049
- case d == 'volUP':
1050
- //Set volume Up
1051
- if (this.mediaElement.volume >= 1) return
1052
- if (this.mediaElement.volume >= 0.95) {
1053
- this.mediaElement.volume = 1
1054
- } else {
1055
- this.mediaElement.volume = (
1056
- this.mediaElement.volume + 0.05
1057
- ).toFixed(2)
1058
- }
1059
- this.volumeSlider.value = this.currentVolume =
1060
- this.mediaElement.volume
1061
- break
1062
-
1063
- case d == 'volDOWN':
1064
- //Set volume Down
1065
- if (this.mediaElement.volume <= 0) return
1066
- if (this.mediaElement.volume <= 0.05) {
1067
- this.mediaElement.volume = 0
1068
- } else {
1069
- this.mediaElement.volume = (
1070
- this.mediaElement.volume - 0.05
1071
- ).toFixed(2)
1072
- }
1073
-
1074
- this.volumeSlider.value = this.currentVolume =
1075
- this.mediaElement.volume
1076
- break
1077
-
1078
- default: {
1079
- //set volume
1080
- this.currentVolume = this.mediaElement.volume =
1081
- this.volumeSlider.value
1082
- this.currentVolume = parseFloat(this.currentVolume).toFixed(2)
1083
- }
1084
- }
1085
- if (!this.muted) this.savedVolume = this.currentVolume
1086
-
1087
- this.setMediaPlaybarValues({
1088
- volume: this.savedVolume
1089
- }) // Save the volume in the Store
1090
- },
1091
- /**
1092
- * @description - Toggle the state fo the sound
1093
- */
1094
- toggleMute() {
1095
- if (!this.showControlsValue) this.showControls()
1096
- //Check first the volume of the media
1097
- if (this.mediaElement.volume == 0) this.muted = true
1098
-
1099
- this.muted = !this.muted
1100
- this.mediaElement.muted = this.muted // set the muted state of the media
1101
-
1102
- //Send mutedstate to the store
1103
- this.setMutedState(this.muted)
1104
-
1105
- if (this.muted) {
1106
- this.mediaElement.volume = 0
1107
- this.savedVolume = this.currentVolume
1108
- this.volumeSlider.value = 0
1109
- this.currentVolume = 0
1110
- }
1111
- if (!this.muted) {
1112
- this.volumeSlider.value = this.savedVolume
1113
- this.currentVolume = this.mediaElement.volume = this.volumeSlider.value
1114
- }
1115
- },
1116
-
1117
- /** @description Save the muted in the Store */
1118
- setMutedState() {
1119
- this.setMediaMuted(this.muted)
1120
- },
1121
- //Keyboard event methods
1122
- /**
1123
- * @description Method to prevent default on keys used for navigation in the player (prevent scrollbar to be trigger when changing volume)
1124
- * */
1125
- keysPreventDefault(evt) {
1126
- if (
1127
- ['Space', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].indexOf(
1128
- evt.key
1129
- ) > -1
1130
- ) {
1131
- evt.preventDefault()
1132
- }
1133
- },
1134
-
1135
- /**
1136
- * @description Method to handle the keyboard input to controle the media player
1137
- *@summary Keyboard actions:
1138
- * *** Left-Arrow/ Right-Arrow : move foward/Backward media by 5s
1139
- * *** Up-Arrow/ Down-Arrow: Increase/Decrease media volume by 5%
1140
- * *** c : show/ hide subtitle
1141
- * *** f : enter/ exit fullscreen
1142
- * *** k : play/ pause media
1143
- * *** m : Mute/ Unmute volume
1144
- * *** t : show transcript
1145
- */
1146
-
1147
- handleMediaControls(e) {
1148
- e = e.code
1149
- if (!this.showControlsValue) this.showControls()
1150
- switch (e) {
1151
- case 'ArrowLeft':
1152
- //If focus is on Volume, VolDown
1153
- if (this.focusState.volumeSlider) {
1154
- //VolDown
1155
- this.updateVolumeLevel('volDOWN')
1156
- break
1157
- } else {
1158
- //Go back 5s in media timeline
1159
- this.cursorLeft()
1160
- break
1161
- }
1162
-
1163
- case 'ArrowRight':
1164
- //If focus is on Volume, VolUp
1165
- if (this.focusState.volumeSlider) {
1166
- //VolUp
1167
- this.updateVolumeLevel('volUP')
1168
- break
1169
- } else {
1170
- //Move forward 5s in media timeline
1171
- this.cursorRight()
1172
- break
1173
- }
1174
-
1175
- case 'ArrowUp':
1176
- //If focus is on progressBar, cursorRight
1177
- if (this.focusState.progressBar) {
1178
- //Move forward 5s in media timeline
1179
- this.cursorRight()
1180
- break
1181
- } else {
1182
- //VolUp
1183
- this.updateVolumeLevel('volUP')
1184
- break
1185
- }
1186
-
1187
- case 'ArrowDown':
1188
- //If focus is on progressBar, cursorRight
1189
- if (this.focusState.progressBar) {
1190
- //Go back 5s in media timeline
1191
- this.cursorLeft()
1192
- break
1193
- } else {
1194
- //Voldown
1195
- this.updateVolumeLevel('volDOWN')
1196
- break
1197
- }
1198
-
1199
- case 'KeyM':
1200
- this.toggleMute()
1201
- break
1202
-
1203
- case 'KeyF':
1204
- if (this.mediaToPlay.mType == 'audio') break
1205
- //Toggle fullScreen
1206
- this.toggleFullScreen()
1207
- break
1208
-
1209
- case 'KeyT':
1210
- if (this.mediaToPlay.mType == 'audio') break
1211
- //Toggle transcript view
1212
- this.toggleViewTranscript(e)
1213
- break
1214
-
1215
- case 'KeyC':
1216
- if (this.mediaToPlay.mType == 'audio') break
1217
- //Toggle subtitle view
1218
- this.toggleViewSubtitle()
1219
- break
1220
-
1221
- case 'KeyK':
1222
- //Play Pause on k bar pressed
1223
- if (this.mediaToPlay.mType == 'audio') this.togglePlay()
1224
- //Play/pause video with the playBackAnim if it is triggerd with KeyK
1225
- else this.handlePlayVideo('playBackAnim')
1226
- break
1227
- }
1228
- },
1229
- /**
1230
- * @description - Activate the label of a button include in btnsLabelDisplay on Hover/Focus on the button. Deactivate the label in all the others btns
1231
- */
1232
- activateLabel(elm) {
1233
- const id = elm.id
1234
- this.btnsLabelDisplay[id] = true
1235
- for (const property in this.btnsLabelDisplay) {
1236
- if (property !== id) {
1237
- this.btnsLabelDisplay[property] = false
1238
- }
1239
- }
1240
- },
1241
- /**
1242
- * @description - Get currenttime from media
1243
- */
1244
-
1245
- getCurrentTime() {
1246
- this.currentTime = this.mediaElement.currentTime
1247
- return this.mediaElement.currentTime
1248
- },
1249
- /**
1250
- * @description - controls to move the media timeline backward
1251
- */
1252
- cursorLeft() {
1253
- this.setMediaTime(this.getCurrentTime() - 5)
1254
- },
1255
- /**
1256
- * @description - control to move the media timeline forwrad
1257
- */
1258
- cursorRight() {
1259
- this.setMediaTime(this.getCurrentTime() + 5)
1260
- },
1261
- //ProgressBar Methods
1262
-
1263
- /**
1264
- * @description - Update currentTime and timing strings when media is playing
1265
- */
1266
- updateProgressBarTime(e) {
1267
- //Get currentTime from the event
1268
- this.currentTime = e.target.currentTime
1269
-
1270
- //Set viewedThresholdReached to true when the threshold is reached
1271
- if (
1272
- this.timer.getElapsedTime() / Math.trunc(this.mediaDuration) >=
1273
- this.viewedThreshold
1274
- )
1275
- this.viewedThresholdReached = true
1276
-
1277
- //Update strings
1278
- this.setProgressBarA11Y()
1279
- //If replay is true and the progressIndicator is not at the end, set canReplay to false
1280
- if (this.canReplay) this.canReplay = false
1281
- //If replay is false and progressIndicator is at the end, set canReplay to true
1282
- if (
1283
- !this.canReplay &&
1284
- this.currentTime >= Math.floor(this.mediaDuration)
1285
- ) {
1286
- this.canReplay = true
1287
- }
1288
- },
1289
-
1290
- /**
1291
- * @description - Set mediatime depending on progressBar click
1292
- */
1293
- seekingProgress(e) {
1294
- if (!this.progressThumbDown && !this.progressThumbHover) {
1295
- const updatedTime =
1296
- (e.offsetX / this.progressBar.offsetWidth) * this.mediaDuration
1297
- this.setMediaTime(updatedTime)
1298
- }
1299
- },
1300
-
1301
- /**
1302
- * @description - Set mediatime depending on mousemove on the screen (when progress thumb is mousedown)
1303
- */
1304
- progressWindowMove(ev) {
1305
- if (this.progressThumbDown) {
1306
- if (this.isPlaying) this.togglePlay()
1307
- this.getTimeFromClickPos(ev)
1308
- let timeToSet = Math.floor(this.getTimeFromClickPos(ev))
1309
- if (timeToSet < 0) timeToSet = 0
1310
- else if (timeToSet > this.duration) timeToSet = this.duration
1311
- this.currentTime = timeToSet
1312
- }
1313
- },
1314
-
1315
- /**
1316
- * @description - Update thumb state (mouseup) when the mouse is up on the screen
1317
- */
1318
- progressWindowUp(ev) {
1319
- if (this.progressThumbDown) {
1320
- this.setMediaTime(this.currentTime)
1321
- this.progressThumbDown = false
1322
- //savedIsPlaying is the playing status saved on mousedown event on the thumb (the video is paused when moving on the progressBar)
1323
- if (this.isPlaying !== this.savedIsPlaying) this.togglePlay()
1324
- }
1325
- },
1326
- /**
1327
- * @description - Get the time corresponding of the position of the click on the progressBar
1328
- */
1329
- getTimeFromClickPos(ev) {
1330
- let timeToSet = 0
1331
- //La progress Bar
1332
- const progressBar = this.progressBar
1333
- //La position de la progressBar dans la page
1334
- const left = progressBar.getBoundingClientRect().left
1335
- //Position du click par rapport à la progressBar
1336
- let clickPos = ev.clientX - left
1337
- //Si la position est à gauche de la progressBar, retourner 0s
1338
- if (clickPos < 0) return 0
1339
- //Obtenir le width de la progressBar
1340
- const width = progressBar.offsetWidth
1341
- //Obtenir le ratio de la position du click vs width de la progressBar
1342
- let percentage = clickPos / width
1343
- //Si > que 1, veux dire que le clique est à droite de la progressBar (mettre le curseur à la fin)
1344
- percentage = percentage > 1 ? 1 : percentage
1345
- //Ratio * temps total du media donne la progressiob
1346
- timeToSet = percentage * this.mediaDuration
1347
- return timeToSet
1348
- },
1349
-
1350
- /** @description - methode to set A11Y support for screen reader on progress bar element (video/audio/) */
1351
- setProgressBarA11Y() {
1352
- const w = this.$i18n.locale === 'en' ? 'at' : 'à'
1353
- const dm =
1354
- this.$i18n.locale === 'en'
1355
- ? `${this.mediaToPlay.mType} duration`
1356
- : `durée ${this.mediaToPlay.mType}`
1357
-
1358
- this.mediaA11Y.valMax = Math.round(this.mediaElement.duration)
1359
- this.mediaA11Y.valNow = Math.round(this.currentTime)
1360
-
1361
- //format text output for screen reader
1362
- let currentTimeArray = this.timecode.split(':')
1363
- let fullTimeArray = this.mediaDurationTime.split(':')
1364
- let hrsTxt, mnTxt, ssTxt, HRSTxt, MNTxt, SSTxt, fTimeTxt, cTimeTxt
1365
- const mediaTitle = this.mediaToPlay.mTitle || this.mediaToPlay.mType
1366
- this.mediaA11Y.label = this.$t('a11y_sr.seek_slider')
1367
-
1368
- //format string to remove leading 0 digit for time
1369
- const formatNum = (n) => {
1370
- return n[0] == '0' ? n.substring(1).trim() : n.trim()
1371
- }
1372
-
1373
- const formatMsg = (x, y) => {
1374
- const reg = /%x|%y/g
1375
- let formatStr = this.$t('a11y_sr.range_expression').replace(
1376
- reg,
1377
- (e) => {
1378
- if (e == '%x') return x
1379
- if (e == '%y') return y
1380
- }
1381
- )
1382
-
1383
- return formatStr
1384
- }
1385
-
1386
- switch (fullTimeArray.length) {
1387
- case 3:
1388
- if (!currentTimeArray.length) return
1389
- //format current hours text text with plurialization apply
1390
- hrsTxt =
1391
- currentTimeArray.length == 3 && parseInt(currentTimeArray[0]) > 0
1392
- ? `${formatNum(currentTimeArray[0])} ${this.$tc(
1393
- 'a11y_sr.time.hours',
1394
- parseInt(formatNum(currentTimeArray[0]))
1395
- )}`
1396
- : ''
1397
-
1398
- //format minutes text text with plurialization apply
1399
- mnTxt =
1400
- parseInt(currentTimeArray[1]) > 0
1401
- ? `${formatNum(currentTimeArray[1])} ${this.$tc(
1402
- 'a11y_sr.time.minute',
1403
- parseInt(formatNum(currentTimeArray[1]))
1404
- )}`
1405
- : ''
1406
-
1407
- //format seconds text with plurialization apply
1408
- ssTxt =
1409
- parseInt(currentTimeArray[2]) > 0
1410
- ? `${formatNum(currentTimeArray[2])} ${this.$tc(
1411
- 'a11y_sr.time.second',
1412
- parseInt(formatNum(currentTimeArray[2]))
1413
- )}`
1414
- : `0 ${this.$tc('a11y_sr.time.second')}`
1415
-
1416
- HRSTxt =
1417
- parseInt(fullTimeArray[0]) > 0
1418
- ? `${formatNum(fullTimeArray[0])} ${this.$tc(
1419
- 'a11y_sr.time.hours',
1420
- parseInt(formatNum(fullTimeArray[0]))
1421
- )}`
1422
- : ''
1423
-
1424
- MNTxt =
1425
- parseInt(fullTimeArray[1]) > 0
1426
- ? `${formatNum(fullTimeArray[1])} ${this.$tc(
1427
- 'a11y_sr.time.minute',
1428
- parseInt(formatNum(fullTimeArray[1]))
1429
- )}`
1430
- : ''
1431
-
1432
- SSTxt =
1433
- parseInt(fullTimeArray[2]) > 0
1434
- ? `${formatNum(fullTimeArray[2])} ${this.$tc(
1435
- 'a11y_sr.time.second',
1436
- parseInt(formatNum(fullTimeArray[2]))
1437
- )}`
1438
- : ''
1439
-
1440
- cTimeTxt = `${hrsTxt} ${mnTxt} ${ssTxt}`
1441
- fTimeTxt = `${HRSTxt} ${MNTxt} ${SSTxt}`
1442
-
1443
- //Format to show 0 when no value is present
1444
- cTimeTxt = cTimeTxt.trim().length ? cTimeTxt.trim() : 0
1445
- // Set sr text for timeCode && duration
1446
- this.mediaA11Y.timeCode = `${this.mediaToPlay.mType} ${w} ${cTimeTxt}`
1447
- this.mediaA11Y.duration = `${dm} ${fTimeTxt}`
1448
- this.mediaA11Y.valueText = `${mediaTitle} ${formatMsg(cTimeTxt, fTimeTxt)}`
1449
-
1450
- break
1451
- case 2:
1452
- if (!currentTimeArray.length) return
1453
-
1454
- //format current time text
1455
- mnTxt =
1456
- parseInt(currentTimeArray[0]) > 0
1457
- ? `${formatNum(currentTimeArray[0])} ${this.$tc(
1458
- 'a11y_sr.time.minute',
1459
- parseInt(formatNum(currentTimeArray[0]))
1460
- )}`
1461
- : ''
1462
-
1463
- ssTxt =
1464
- parseInt(currentTimeArray[1]) > 0
1465
- ? `${formatNum(currentTimeArray[1])} ${this.$tc(
1466
- 'a11y_sr.time.second',
1467
- parseInt(formatNum(currentTimeArray[1]))
1468
- )}`
1469
- : `0 ${this.$tc('a11y_sr.time.second')}`
1470
-
1471
- // format full time text
1472
- MNTxt =
1473
- parseInt(fullTimeArray[0]) > 0
1474
- ? `${formatNum(fullTimeArray[0])} ${this.$tc(
1475
- 'a11y_sr.time.minute',
1476
- parseInt(formatNum(fullTimeArray[0]))
1477
- )}`
1478
- : ''
1479
- SSTxt =
1480
- parseInt(fullTimeArray[1]) > 0
1481
- ? `${formatNum(fullTimeArray[1])} ${this.$tc(
1482
- 'a11y_sr.time.second',
1483
- parseInt(formatNum(fullTimeArray[1]))
1484
- )}`
1485
- : ''
1486
-
1487
- cTimeTxt = `${mnTxt} ${ssTxt}`
1488
- fTimeTxt = `${MNTxt} ${SSTxt}`
1489
-
1490
- //Format to show 0 when no value is present
1491
- cTimeTxt = cTimeTxt.trim().length ? cTimeTxt.trim() : 0
1492
- // Set sr text for timeCode && duration
1493
- this.mediaA11Y.timeCode = `${this.mediaToPlay.mType} ${w} ${cTimeTxt}`
1494
- this.mediaA11Y.duration = `${dm} ${fTimeTxt}`
1495
-
1496
- this.mediaA11Y.valueText = `${mediaTitle} ${formatMsg(cTimeTxt, fTimeTxt)}`
1497
-
1498
- break
1499
- }
1500
- },
1501
- /**
1502
- * @description - Deactivate the label of a button include in btnsLabelDisplay on mouseout/blur of this button
1503
- */
1504
- deactivateLabel(elm) {
1505
- const id = elm.id
1506
- this.btnsLabelDisplay[id] = false
1507
- },
1508
- /**
1509
- * @description - Update the state of the elm describe by the string (focus or not) for keydown arrow management in handleMediaControls method (volume | progress bar can use arrow up-down and left-right only on focus)
1510
- */
1511
- changeFocusState(string, bool) {
1512
- this.focusState[string] = bool
1513
- },
1514
- /**
1515
- * @description - handle initial volume level
1516
- */
1517
- setMediaVolume() {
1518
- //Set media muted or with the savedVolume
1519
- this.muted = this.mediaMuted
1520
- if (!this.muted)
1521
- return (this.currentVolume = this.savedVolume = this.mediaVolume)
1522
- this.savedVolume = this.mediaVolume
1523
- this.currentVolume = 0
1524
- },
1525
- //======================================================================
1526
- /**
1527
- * @description - Set the DOM elements specific for video
1528
- */
1529
- initializeVideoElm() {
1530
- //SeekTooltip
1531
- this.seekTooltip = this.$refs['$seek-tooltip']
1532
- //Transcript
1533
- this.$bus.$on('transcript-hidden', this.resetTranscript)
1534
- this.setTranscript()
1535
- },
1536
-
1537
- /** "@description -update the information of the seek tooltip*/
1538
- updateSeekTooltip(evt) {
1539
- if (!this.showControlsValue) this.showControls()
1540
- //Elm progress Bar
1541
- const progressBar = this.progressBar
1542
- //progressBar position on the page
1543
- const left = progressBar.getBoundingClientRect().left
1544
- //Mouse position versus progressBar
1545
- const hoverPos = evt.clientX - left
1546
- //Get progressBar offsetWidth
1547
- const width = progressBar.offsetWidth
1548
-
1549
- //Define the value of the skip tooltip position
1550
- let offsetX = null
1551
-
1552
- switch (true) {
1553
- case hoverPos < 15:
1554
- offsetX = 20
1555
- break
1556
- case hoverPos > width - 55:
1557
- offsetX = width - 55
1558
- break
1559
- default:
1560
- offsetX = hoverPos
1561
- }
1562
-
1563
- this.seekTooltip.style.left = `${offsetX}px` // update visual position of tooltip
1564
-
1565
- //Get % of the cursor hover position/progressBar width
1566
- let percentage = 0
1567
- if (hoverPos > width) percentage = 100
1568
- if (hoverPos < 0) percentage = 0
1569
- else {
1570
- percentage = hoverPos / width
1571
- }
1572
- let skipTo = percentage * this.mediaDuration
1573
- //Update the Tooltip time
1574
- this.tooTipTimeCode = this.$helper.formatTime(skipTo)
1575
- },
1576
- /**
1577
- * @description - handle all the listeners for videos
1578
- */
1579
- setVideoHandlers() {
1580
- //Tooltip
1581
- this.progressArea.addEventListener('mousemove', this.updateSeekTooltip)
1582
- //Fullscreen
1583
- this.mediaContainer.addEventListener(
1584
- 'fullscreenchange',
1585
- this.updateFullScreenState
1586
- )
1587
- //Show controls
1588
- this.mediaContainer.addEventListener('mousemove', this.showControls)
1589
- },
1590
- /**
1591
- * @description - Unset all the listeners (video)
1592
- */
1593
- removeVideoHandlers() {
1594
- //Tooltip
1595
- this.progressArea.removeEventListener('mousemove', this.updateSeekTooltip)
1596
- //Fullscreen
1597
- this.mediaContainer.removeEventListener(
1598
- 'fullscreenchange',
1599
- this.updateFullScreenState
1600
- )
1601
- //Show controls
1602
- this.mediaContainer.removeEventListener('mousemove', this.showControls)
1603
- },
1604
- /**
1605
- /* @description - handle the play /pause of the media and the playback video animation
1606
- */
1607
- handlePlayVideo(type = null) {
1608
- //Play/pause
1609
- this.togglePlay()
1610
- //Playbar animation (show/hide)
1611
- this.isPlaying ? this.hideControls() : this.showControls()
1612
-
1613
- //Playback animation (only the first play click)
1614
- if (this.playClicked && !type) {
1615
- this.playBackAnim = false
1616
- return
1617
- }
1618
- //this.runPlaybackAnimation()
1619
- this.playBackAnim = true
1620
- this.playClicked = true
1621
- // Should indicate with media is playing and set it in the store
1622
- },
1623
- /**
1624
- * @description - Toggle the state of the screen
1625
- * Set or unset the media in full screen
1626
- */
1627
- toggleFullScreen() {
1628
- const fullscreenElement = this.mediaContainer
1629
- console.log(fullscreenElement)
1630
-
1631
- if (document.fullscreenElement) {
1632
- // exitFullscreen is only available on the Document object.
1633
- document.exitFullscreen()
1634
- } else {
1635
- //fullscreen is available on Element.
1636
- fullscreenElement.requestFullscreen()
1637
- }
1638
- },
1639
- /**
1640
- * @description update the value of fulls screen state
1641
- */
1642
- updateFullScreenState() {
1643
- if (!this.showControlsValue) this.showControls()
1644
- this.fullscreenOn = !this.fullscreenOn
1645
- },
1646
-
1647
- fireFoxMoveCC() {
1648
- if (this.firefoxTrack.activeCues[0]) {
1649
- this.firefoxTrack.activeCues[0].line = 12
1650
- }
1651
- },
1652
- /**
1653
- * @description show the subtitle
1654
- */
1655
- showSubtitles() {
1656
- this.mediaElement.textTracks[0].mode = 'showing'
1657
- this.setMediaSubtitles(true)
1658
- if (this.getCurrentBrowser == 'Firefox') {
1659
- let video = document.getElementsByTagName('video')[0]
1660
- let tracks = video.textTracks
1661
- this.firefoxTrack = tracks[0]
1662
-
1663
- this.firefoxTrack.addEventListener('cuechange', this.fireFoxMoveCC)
1664
- }
1665
- },
1666
-
1667
- hideSubtitles() {
1668
- this.mediaElement.textTracks[0].mode = 'hidden' // value can be 'disabled' also.
1669
- this.setMediaSubtitles(false)
1670
- },
1671
- toggleViewSubtitle() {
1672
- if (!this.showControlsValue) this.showControls()
1673
- if (this.subtitlesEnabled) return this.hideSubtitles()
1674
- if (!this.subtitlesEnabled) return this.showSubtitles()
1675
- },
1676
- /**
1677
- * @description method to show or hide the transcipt.
1678
- * @summary get toggle the value of transcriptEnabled and get content from the transcript file
1679
- * and transfer it to Module for display.
1680
- * @fires 'open-sidebar' to AppBaseModule
1681
- * @fires 'close-sidebar' to AppBaseModule
1682
- */
1683
- toggleViewTranscript(event) {
1684
- if (!this.showControlsValue) this.showControls()
1685
- this.transcriptEnabled = !this.transcriptEnabled
1686
- //When the function is called after a user interaction (mouse or keyboard), emit event
1687
- if (event) {
1688
- //emit transcript toggle event, listener in AppBasePage
1689
- this.$bus.$emit(
1690
- 'video-transcript-toggle',
1691
- this.mediaToPlay,
1692
- this.transcriptEnabled
1693
- )
1694
- }
1695
-
1696
- if (this.transcriptEnabled && this.transcriptToShow) {
1697
- //Resize video container
1698
- this.$emit('resize-video', 'sm')
1699
- //Open sidebar with the transcript
1700
- return this.$bus.$emit('open-sidebar', {
1701
- ctx: 'ctxTranscript',
1702
- e: this.transcriptToShow,
1703
- w: this.pbContainer
1704
- })
1705
- }
1706
-
1707
- // Send close signal for the side bar when transcipt state is not enabled
1708
- if (!this.transcriptEnabled) {
1709
- //Resize video container
1710
- this.$emit('resize-video', 'lg')
1711
- //Open sidebar with the transcript
1712
- return this.$bus.$emit('close-sidebar', 'ctxTranscript')
1713
- }
1714
- },
1715
- /**
1716
- * @description Fetching method for transcript
1717
- * @summary Fetch a transcript from HTML file to display. File URL is defined in the media data
1718
- * and return its content for display
1719
- * @return {String} HTML string
1720
- */
1721
- async fetchTranscript() {
1722
- try {
1723
- //const tFile = 'exemple_transcript2.html'
1724
- const { mTranscript } = this.mediaToPlay
1725
- const tFile = mTranscript
1726
- if (!tFile) throw new Error('Missing transcript File!')
1727
-
1728
- // validate file types. Only .html are allowed
1729
- if (!tFile.endsWith('.html'))
1730
- throw new Error(
1731
- 'Invalid valid transcript file. \n Expecting .html file'
1732
- )
1733
-
1734
- const fileUrl = !tFile.includes('/') ? `./${tFile}` : tFile //allow passing file in Public folder
1735
- // const fileUrl = new URL(tFile, import.meta.url))
1736
- const res = await axios.get(fileUrl, { responseType: 'blob' })
1737
- const content = await res.data.text()
1738
-
1739
- return content
1740
- } catch (err) {
1741
- console.warn("YOU'VE GOT AN ERROR!\n", err)
1742
- }
1743
- },
1744
- /** ******************* Rendering Methods****************************** */
1745
-
1746
- /**
1747
- * @Description Method to set the transcription that need to be displayed
1748
- */
1749
- async setTranscript() {
1750
- if (!this.hasTranscript) return (this.transcriptToShow = null)
1751
-
1752
- this.transcriptToShow = await this.fetchTranscript()
1753
- },
1754
-
1755
- /**
1756
- * @description Method to reset the transcript state
1757
- */
1758
- resetTranscript(content) {
1759
- if (!this.showControlsValue) this.showControls()
1760
- this.transcriptEnabled = false
1761
- this.otherVideoTranscriptShown = false
1762
- },
1763
-
1764
- /**
1765
- * @description Show the media controler
1766
- */
1767
- showControls() {
1768
- this.showControlsValue = true
1769
- if (this.hideTimer) clearTimeout(this.hideTimer) //cancel existing timer
1770
-
1771
- this.hideControls()
1772
- },
1773
- /**
1774
- * @description Hide the media after the video start playing
1775
- */
1776
-
1777
- hideControls() {
1778
- if (this.mediaElement.paused) return
1779
-
1780
- this.hideTimer = setTimeout(() => {
1781
- this.showControlsValue = false
1782
- }, this.delayUntilHide)
1783
- }
1784
-
1785
- /**
1786
- * @description - Send xAPI statement for media play and end
1787
- * @param {String} action - play or end
1788
- * @param {Number} startTime - time in seconds when the media start to be played
1789
- * @param {Number} endTime - time in seconds when the media stop to be played
1790
- */
1791
- }
1792
- }
1793
- </script>
1794
- <style lang="scss" scoped>
1795
- .pb-container.video {
1796
- position: absolute;
1797
- height: 100%;
1798
- width: 100%;
1799
- justify-content: center;
1800
- display: flex;
1801
- margin-bottom: 78px;
1802
- }
1803
-
1804
- .playback-button {
1805
- position: absolute;
1806
- bottom: 0;
1807
- left: 0;
1808
- width: 100%;
1809
- height: 94%;
1810
- display: flex;
1811
- flex-flow: column wrap;
1812
- justify-content: center;
1813
- align-items: center;
1814
- background-color: transparent;
1815
-
1816
- .playback-wrapper {
1817
- height: 64px;
1818
- width: 64px;
1819
- display: flex;
1820
- justify-content: center;
1821
- align-items: center;
1822
- border-radius: 50%;
1823
-
1824
- svg {
1825
- height: 32px;
1826
- width: 26.29px;
1827
- &.play-icon {
1828
- margin-left: 11%;
1829
- }
1830
- }
1831
- }
1832
- }
1833
- //Wrapper
1834
- .pb-wrapper {
1835
- width: 100%;
1836
- display: flex;
1837
- flex-direction: column;
1838
- position: absolute;
1839
- bottom: -78px;
1840
- opacity: 0;
1841
- z-index: 2;
1842
-
1843
- &.show-controls {
1844
- opacity: 1;
1845
- transition: opacity 0.35s ease-in-out;
1846
- }
1847
-
1848
- //Wrapper timeline
1849
- .pb-timeline {
1850
- --progress-bar-height: 6px;
1851
- position: relative;
1852
- width: 100%;
1853
- height: var(--progress-bar-height);
1854
- .progress-area {
1855
- width: 100%;
1856
- height: 400%; //Add contact surface of click on progressBar
1857
- position: relative;
1858
- top: -150%;
1859
- display: flex;
1860
- cursor: pointer;
1861
- align-items: center;
1862
- z-index: 8; //z-index on top of the video elm
1863
- &:hover {
1864
- .seek-tooltip {
1865
- opacity: 1;
1866
- }
1867
- }
1868
-
1869
- //Progress bar
1870
- .pb-progress-bar {
1871
- --progress-bar-border-height: 1px;
1872
- cursor: pointer;
1873
- width: 100%;
1874
- height: var(--progress-bar-height);
1875
- //background-color: transparent;
1876
- overflow: visible;
1877
- &:focus {
1878
- .progress-thumb {
1879
- }
1880
- }
1881
- }
1882
-
1883
- .progress-indicator {
1884
- width: 0;
1885
- height: 100%;
1886
- position: relative;
1887
- user-select: none;
1888
-
1889
- &.progress-animation {
1890
- transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
1891
- transition-duration: 0.01s;
1892
- transition-property: all;
1893
- transition: width 0.1s linear;
1894
- }
1895
- }
1896
- //Custom thumb
1897
- .progress-thumb {
1898
- //Cliquable thumb
1899
- --progress-thumb-size: 30px;
1900
- width: var(--progress-thumb-size);
1901
- height: var(--progress-thumb-size);
1902
- position: absolute;
1903
-
1904
- right: calc(var(--progress-thumb-size) / -2);
1905
- top: calc(
1906
- -1 *
1907
- (
1908
- var(--progress-thumb-size) / 2 - var(--progress-bar-height) / 2
1909
- ) - var(--progress-bar-border-height) /
1910
- 2
1911
- );
1912
- display: flex;
1913
- align-items: center;
1914
- justify-content: center;
1915
- &::after {
1916
- content: '';
1917
- //Visible thumb
1918
- height: 16px;
1919
- width: 16px;
1920
- //Colors and styling
1921
- border-radius: 50%;
1922
- //background-color: var(--primary700);
1923
- }
1924
- }
1925
-
1926
- &:hover {
1927
- .seek-tooltip {
1928
- //tooltip visible only when hovering progress-area
1929
- opacity: 1;
1930
- }
1931
- }
1932
-
1933
- .seek-tooltip {
1934
- position: absolute;
1935
- top: -30px;
1936
- opacity: 0;
1937
- user-select: none;
1938
- border-radius: 1px;
1939
- padding: 2px;
1940
- }
1941
- }
1942
- }
1943
-
1944
- //Controls
1945
- .pb-controls {
1946
- padding: 12px 32px;
1947
- @media screen and (max-width: 800px) {
1948
- padding: 12px 12px;
1949
- }
1950
- & {
1951
- width: 100%;
1952
- //height: 76px;
1953
- height: auto;
1954
- display: flex;
1955
- flex-flow: row wrap;
1956
- justify-content: space-between;
1957
- -webkit-justify-content: space-between;
1958
- align-items: center;
1959
- }
1960
- .pb-areas {
1961
- display: flex;
1962
- flex-flow: row nowrap;
1963
- justify-content: center;
1964
- align-items: center;
1965
-
1966
- &.right {
1967
- .btn {
1968
- margin-right: 24px;
1969
-
1970
- &:last-child {
1971
- margin-right: 0;
1972
- }
1973
- }
1974
- }
1975
-
1976
- .btn {
1977
- position: relative;
1978
-
1979
- &::before {
1980
- content: attr(data-title);
1981
- position: absolute;
1982
- top: -50px;
1983
- right: 0;
1984
- margin-left: -35px;
1985
- word-break: keep-all;
1986
- white-space: pre;
1987
- display: none;
1988
- opacity: 0;
1989
- width: fit-content;
1990
- border-radius: 1px;
1991
- padding: 2px;
1992
- }
1993
- //Icons btn specific
1994
- &.playbar-btn {
1995
- &::before {
1996
- left: 0 !important;
1997
- margin-left: 0 !important;
1998
- }
1999
- & {
2000
- margin-right: 16px;
2001
- border-radius: 50%;
2002
- }
2003
- &:hover {
2004
- &::before {
2005
- display: block;
2006
- opacity: 1;
2007
- }
2008
- }
2009
- @media screen and (max-width: 800px) {
2010
- height: 32px;
2011
- width: 32px;
2012
- }
2013
- svg.play-icon {
2014
- margin-left: 11.875%; // 11.8% off center on the design
2015
- }
2016
- }
2017
- &.volume-btn {
2018
- margin: 0 20px;
2019
- svg.volume-low-icon {
2020
- height: 17.75px;
2021
- }
2022
- }
2023
- }
2024
- //Manage displayLabel (dynamic class)
2025
- button.displayLabel {
2026
- &:hover,
2027
- &:focus {
2028
- &::before {
2029
- display: block;
2030
- opacity: 1;
2031
- }
2032
- }
2033
- }
2034
-
2035
- .pb-timer {
2036
- width: max-content;
2037
- }
2038
- //Volume
2039
- .volume-controls {
2040
- --volumecontrols-height: 22px;
2041
- --volume-thumb-size: 16px;
2042
- display: flex;
2043
- flex-flow: row wrap;
2044
- justify-content: center;
2045
- position: relative;
2046
- height: var(--volumecontrols-height);
2047
- &:hover {
2048
- //button title //DisplayLabel is a dynamic class
2049
- .volume-slider.displayLabel + span {
2050
- display: block;
2051
- opacity: 1;
2052
- }
2053
- }
2054
- .volume-slider {
2055
- -webkit-appearance: none;
2056
- width: 100px;
2057
- //Responsive
2058
- @media screen and (max-width: 800px) {
2059
- width: 80px;
2060
- }
2061
- & {
2062
- border-radius: 24px;
2063
- cursor: pointer;
2064
- position: relative;
2065
- z-index: 2;
2066
- }
2067
- &::-webkit-slider-runnable-track {
2068
- background: transparent;
2069
- }
2070
- //Input slider thumb
2071
- @mixin volume-thumb {
2072
- -webkit-appearance: none;
2073
- width: var(--volume-thumb-size);
2074
- height: var(--volume-thumb-size);
2075
- margin-top: 12px;
2076
- border-radius: 50%;
2077
- position: relative;
2078
- cursor: pointer;
2079
- }
2080
- &::-webkit-slider-thumb {
2081
- @include volume-thumb;
2082
- }
2083
- //button title
2084
- & + span {
2085
- position: absolute;
2086
- top: -62px;
2087
- right: 0;
2088
- margin-left: -35px;
2089
- word-break: keep-all;
2090
- white-space: pre;
2091
- display: none;
2092
- opacity: 0;
2093
- user-select: none;
2094
- //Colors
2095
- //background-color: var(--primary100);
2096
- //color: var(--primary700);
2097
- border-radius: 1px;
2098
- padding: 2px;
2099
- }
2100
- //DisplayLabel is a dynamic class
2101
- &.displayLabel:focus {
2102
- & + span {
2103
- display: block;
2104
- opacity: 1;
2105
- }
2106
- }
2107
- }
2108
-
2109
- .volume-progress {
2110
- --volumeIndicator-height: 9px;
2111
- height: var(--volumeIndicator-height);
2112
- top: calc(
2113
- var(--volumecontrols-height) / 2 - var(--volumeIndicator-height) / 2
2114
- ); //centrer en hauteur
2115
- width: calc(
2116
- 100% - var(--volume-thumb-size) / 2
2117
- ); //Width smaller than the range width to be sure the progress width don't exceed the range width
2118
- position: absolute;
2119
- border-radius: 20px;
2120
- background-repeat: no-repeat;
2121
- background-size: var(--background-size-vs, 0%) 100%; //Js dynamic variable
2122
- //colors
2123
- }
2124
- }
2125
- }
2126
- }
2127
-
2128
- svg {
2129
- //width: 19px;
2130
- //height: 19px;
2131
- //colors
2132
- //stroke: var(--primary700);
2133
- }
2134
-
2135
- button {
2136
- height: 48px;
2137
- width: 48px;
2138
- background-color: transparent;
2139
- padding: 0;
2140
- display: flex;
2141
- justify-content: center;
2142
- align-items: center;
2143
- svg {
2144
- height: 24px;
2145
- @media screen and (max-width: 800px) {
2146
- height: 18px;
2147
- }
2148
- }
2149
- }
2150
- }
2151
-
2152
- //CSS for audio only
2153
- .audio.pb-wrapper {
2154
- border-radius: 4px;
2155
- position: initial;
2156
- .pb-controls {
2157
- padding: 8px 16px;
2158
- .volume-controls {
2159
- margin-left: 8px;
2160
- width: 100%;
2161
- --volume-thumb-size: 12px;
2162
- .volume-slider {
2163
- width: 50px;
2164
- }
2165
- .volume-progress {
2166
- --volumeIndicator-height: 6px;
2167
- }
2168
- }
2169
- }
2170
- .pb-timer {
2171
- margin-right: 20px;
2172
- }
2173
- .pb-timeline {
2174
- min-width: 50px;
2175
-
2176
- @media screen and (max-width: 1000px) {
2177
- width: 80%;
2178
- min-width: 50px;
2179
- margin: 0 auto;
2180
- }
2181
-
2182
- .progress-area {
2183
- justify-content: center;
2184
- }
2185
- .pb-progress-bar {
2186
- border-radius: 4px;
2187
- //max-width: 90%;
2188
- }
2189
- }
2190
- .volume-btn {
2191
- margin-left: 16px;
2192
- }
2193
- .pb-areas {
2194
- width: 100%;
2195
- display: grid;
2196
- grid-template-columns:
2197
- min-content
2198
- min-content
2199
- 1fr
2200
- min-content
2201
- min-content;
2202
- align-items: center;
2203
- justify-content: center;
2204
- button {
2205
- height: 24px;
2206
- width: 24px;
2207
- &.playbar-btn {
2208
- margin-right: 8px;
2209
- }
2210
- svg {
2211
- height: 12.25px;
2212
- @media screen and (max-width: 800px) {
2213
- height: 18px;
2214
- }
2215
- }
2216
- &.volume-btn {
2217
- svg {
2218
- height: 16px;
2219
- }
2220
- svg.volume-low-icon {
2221
- height: 14px;
2222
- }
2223
- }
2224
- }
2225
- }
2226
- }
2227
- //Playback button video (to finish)
2228
- .playback-button {
2229
- .playback-wrapper {
2230
- opacity: 0;
2231
- }
2232
- &.initial {
2233
- .playback-wrapper {
2234
- animation-name: initial;
2235
- opacity: 1;
2236
- }
2237
- }
2238
- &.playback-animation {
2239
- .playback-wrapper {
2240
- opacity: 0;
2241
- animation-name: handlePlayBack;
2242
- animation-duration: 0.8s;
2243
- animation-timing-function: ease-in-out;
2244
- }
2245
- }
2246
- }
2247
-
2248
- .audio-media-player {
2249
- .pb-areas {
2250
- .btn {
2251
- &::before {
2252
- top: inherit !important;
2253
- bottom: -50px;
2254
- }
2255
- }
2256
- }
2257
- }
2258
-
2259
- .FS {
2260
- &:fullscreen {
2261
- .pb-wrapper {
2262
- bottom: -45px;
2263
- }
2264
- }
2265
- }
2266
-
2267
- @keyframes handlePlayBack {
2268
- 0% {
2269
- opacity: 0;
2270
- }
2271
- 50% {
2272
- opacity: 1;
2273
- transform: scale(0.95);
2274
- }
2275
- 100% {
2276
- opacity: 0;
2277
- }
2278
- }
2279
-
2280
- @keyframes disappear {
2281
- 0% {
2282
- opacity: 1;
2283
- }
2284
- 100% {
2285
- opacity: 0;
2286
- }
2287
- }
2288
- </style>
1
+ <!--@ Description: This component is used to display the playbar associate with the videoPlayer
2
+ @ What it does: The component is used to interacted with the videoPlayer or audioPlayer via buttons.
3
+ mediaMixins is used for all the methods/data shared between audio and video. In the appCompPlayBar component, there is also all the methods/data specific to video-->
4
+ <template>
5
+ <div
6
+ ref="$pb-container"
7
+ class="pb-container"
8
+ :class="{ video: mediaToPlay.mType === 'video' }"
9
+ >
10
+ <!--------------------------------- Btn Play Principal (video only) -------------------------------------->
11
+ <app-base-button
12
+ v-if="mediaToPlay.mType === 'video'"
13
+ ref="$btn-play-playback"
14
+ class="playback-button"
15
+ :class="{
16
+ initial: !playClicked,
17
+ 'playback-animation': playBackAnim && playClicked
18
+ }"
19
+ :aria-label="playLabel + ' principal'"
20
+ :data-title="`${playLabel}`"
21
+ @click="
22
+ () => {
23
+ mediaToPlay.mType === 'video'
24
+ ? handlePlayVideo('playBackAnim')
25
+ : togglePlay()
26
+ }
27
+ "
28
+ >
29
+ <div
30
+ v-show="(!playClicked && !canReplay) || (!canReplay && isPlaying)"
31
+ class="playback-wrapper"
32
+ >
33
+ <svg
34
+ class="app-icons-svg play-icon"
35
+ aria-hidden="true"
36
+ focusable="false"
37
+ >
38
+ <use href="#play-icon" />
39
+ </svg>
40
+ </div>
41
+ <div
42
+ v-show="playClicked && !isPlaying && !canReplay"
43
+ class="playback-wrapper"
44
+ >
45
+ <svg
46
+ class="app-icons-svg pause-icon"
47
+ aria-hidden="true"
48
+ focusable="false"
49
+ >
50
+ <use href="#pause-icon" />
51
+ </svg>
52
+ </div>
53
+ <div v-show="!playClicked && canReplay" class="playback-wrapper">
54
+ <svg
55
+ class="app-icons-svg replay-icon"
56
+ aria-hidden="true"
57
+ focusable="false"
58
+ >
59
+ <use href="#replay-icon" />
60
+ </svg>
61
+ </div>
62
+ </app-base-button>
63
+
64
+ <div
65
+ v-if="mediaToPlay"
66
+ class="pb-wrapper"
67
+ :class="{
68
+ 'show-controls': showControlsValue || mediaToPlay.mType === 'audio',
69
+ audio: mediaToPlay.mType === 'audio',
70
+ video: mediaToPlay.mType === 'video'
71
+ }"
72
+ >
73
+ <!--------------------------------- PLAY-BAR Progress (video only) -------------------------------------->
74
+ <div
75
+ v-if="mediaToPlay.mType === 'video'"
76
+ ref="$playbar-timeline"
77
+ class="pb-timeline"
78
+ >
79
+ <div ref="$progress-area" class="progress-area">
80
+ <div
81
+ ref="$progress-bar"
82
+ draggable="false"
83
+ tabindex="0"
84
+ class="pb-progress-bar"
85
+ role="slider"
86
+ aria-valuemin="0"
87
+ :aria-label="mediaA11Y.label"
88
+ :aria-valuenow="mediaA11Y.valNow"
89
+ :aria-valuemax="mediaA11Y.valMax"
90
+ :aria-valuetext="mediaA11Y.valueText"
91
+ @focus="changeFocusState('progressBar', true)"
92
+ @blur="changeFocusState('progressBar', false)"
93
+ >
94
+ <!--Class progress-animation is apply when we are not using the thumb to change the progression-->
95
+ <div
96
+ ref="$progress-indicator"
97
+ draggable="false"
98
+ class="progress-indicator"
99
+ :class="{ 'progress-animation': !progressThumbDown }"
100
+ :style="{ width: progressBarPercentage + '%' }"
101
+ >
102
+ <!--Mousedown validate if the thumb is cliqued (if yes, the mouse is follow to modify the progress)
103
+ MouseOver/MouseOut deactivate click event on progressBar if the mouse hovered the thumb-->
104
+ <div
105
+ ref="$progress-thumb"
106
+ draggable="false"
107
+ class="progress-thumb"
108
+ style="z-index: 9"
109
+ @mousedown="
110
+ function () {
111
+ savedIsPlaying = isPlaying
112
+ progressThumbDown = true
113
+ }
114
+ "
115
+ @mouseover="progressThumbHover = true"
116
+ @mouseleave="progressThumbHover = false"
117
+ ></div>
118
+ </div>
119
+ </div>
120
+ <!--------------------------------- PLAY-BAR ToolTip (video only) ----------------------------------->
121
+ <div
122
+ v-if="mediaToPlay.mType === 'video'"
123
+ ref="$seek-tooltip"
124
+ aria-hidden="true"
125
+ class="seek-tooltip"
126
+ >
127
+ {{ tooTipTimeCode }}
128
+ </div>
129
+ </div>
130
+ </div>
131
+ <div class="pb-controls">
132
+ <div class="pb-areas left">
133
+ <!--------------------------------- PLAY-BAR Play ------------------------------------>
134
+ <app-base-button
135
+ ref="$btn-play"
136
+ class="playbar-btn playbar-play"
137
+ :class="{ displayLabel: btnsLabelDisplay['playbar-play'] === true }"
138
+ :aria-label="
139
+ playLabel + (mediaToPlay.mType === 'video' ? ' secondaire' : '')
140
+ "
141
+ :data-title="`${playLabel} (k)`"
142
+ @click="
143
+ () => {
144
+ mediaToPlay.mType === 'video' ? handlePlayVideo() : togglePlay()
145
+ }
146
+ "
147
+ @mouseenter="activateLabel($event)"
148
+ @mouseleave="deactivateLabel($event)"
149
+ @focus="activateLabel($event)"
150
+ @blur="deactivateLabel($event)"
151
+ >
152
+ <svg
153
+ v-show="!canReplay && !isPlaying"
154
+ class="app-icons-svg play-icon"
155
+ aria-hidden="true"
156
+ focusable="false"
157
+ >
158
+ <use href="#play-icon" />
159
+ </svg>
160
+ <svg
161
+ v-show="isPlaying && !canReplay"
162
+ class="app-icons-svg pause-icon"
163
+ aria-hidden="true"
164
+ focusable="false"
165
+ >
166
+ <use href="#pause-icon" />
167
+ </svg>
168
+ <svg
169
+ v-show="canReplay"
170
+ class="app-icons-svg replay-icon"
171
+ aria-hidden="true"
172
+ focusable="false"
173
+ >
174
+ <use href="#replay-icon" />
175
+ </svg>
176
+ </app-base-button>
177
+ <!--------------------------------- PLAY-BAR Timer ------------------------------------>
178
+ <div class="pb-timer">
179
+ <span
180
+ aria-hidden="true"
181
+ style="-webkit-user-select: none"
182
+ draggable="false"
183
+ >
184
+ {{ timecode }} / {{ mediaDurationTime }}
185
+ </span>
186
+ </div>
187
+ <!--------------------------------- PLAY-BAR Progress (audio only) -------------------------------------->
188
+ <div
189
+ v-if="mediaToPlay.mType === 'audio'"
190
+ ref="$playbar-timeline"
191
+ class="pb-timeline"
192
+ >
193
+ <div ref="$progress-area" class="progress-area">
194
+ <div
195
+ id="progress-bar"
196
+ ref="$progress-bar"
197
+ draggable="false"
198
+ tabindex="0"
199
+ class="pb-progress-bar"
200
+ role="slider"
201
+ aria-valuemin="0"
202
+ :aria-label="mediaA11Y.label"
203
+ :aria-valuenow="mediaA11Y.valNow"
204
+ :aria-valuemax="mediaA11Y.valMax"
205
+ :aria-valuetext="mediaA11Y.valueText"
206
+ @focus="changeFocusState('progressBar', true)"
207
+ @blur="changeFocusState('progressBar', false)"
208
+ >
209
+ <!--Class progress-animation is apply when we are not using the thumb to change the progression-->
210
+ <div
211
+ id="progress-indicator"
212
+ ref="$progress-indicator"
213
+ draggable="false"
214
+ class="progress-indicator"
215
+ :class="{ 'progress-animation': !progressThumbDown }"
216
+ :style="{ width: progressBarPercentage + '%' }"
217
+ >
218
+ <!--Mousedown validate if the thumb is cliqued (if yes, the mouse is follow to modify the progress)
219
+ MouseOver/MouseOut deactivate click event on progressBar if the mouse hovered the thumb-->
220
+ <div
221
+ ref="$progress-thumb"
222
+ draggable="false"
223
+ class="progress-thumb"
224
+ style="z-index: 9"
225
+ @mousedown="
226
+ function () {
227
+ savedIsPlaying = isPlaying
228
+ progressThumbDown = true
229
+ }
230
+ "
231
+ @mouseover="progressThumbHover = true"
232
+ @mouseleave="progressThumbHover = false"
233
+ ></div>
234
+ </div>
235
+ </div>
236
+ </div>
237
+ </div>
238
+ <!--------------------------------- PLAY-BAR Volume -------------------------------------->
239
+ <app-base-button
240
+ id="playbar-volume"
241
+ ref="$btn-volume"
242
+ class="volume-btn"
243
+ :class="{
244
+ displayLabel: btnsLabelDisplay['playbar-volume'] === true
245
+ }"
246
+ tabindex="0"
247
+ :title="volumeLabel"
248
+ :aria-label="volumeLabel"
249
+ :data-title="`${volumeLabel} (m)`"
250
+ @click="toggleMute"
251
+ @mouseenter="activateLabel($event)"
252
+ @mouseleave="deactivateLabel($event)"
253
+ @focus="activateLabel($event)"
254
+ @blur="deactivateLabel($event)"
255
+ >
256
+ <svg
257
+ v-show="volumeState == 'muted'"
258
+ class="app-icons-svg"
259
+ aria-hidden="true"
260
+ focusable="false"
261
+ >
262
+ <use href="#volume-mute-icon" />
263
+ </svg>
264
+ <svg
265
+ v-show="volumeState == 'high'"
266
+ class="app-icons-svg"
267
+ aria-hidden="true"
268
+ focusable="false"
269
+ >
270
+ <use href="#volume-high-icon" />
271
+ </svg>
272
+ <svg
273
+ v-show="volumeState == 'low'"
274
+ class="app-icons-svg volume-low-icon"
275
+ aria-hidden="true"
276
+ focusable="false"
277
+ >
278
+ <use href="#volume-low-icon" />
279
+ </svg>
280
+ </app-base-button>
281
+ <div ref="zone-volume" class="volume-controls">
282
+ <input
283
+ ref="$volume-slider"
284
+ :title="`${$t('button.volume')}`"
285
+ class="volume-slider"
286
+ :class="{
287
+ displayLabel: btnsLabelDisplay['volume-slider'] === true
288
+ }"
289
+ :value="currentVolume"
290
+ :aria-valuetext="volumeLevelA11Y"
291
+ type="range"
292
+ max="1"
293
+ min="0"
294
+ step="0.01"
295
+ tabindex="0"
296
+ :aria-label="`${$t('button.volume')}`"
297
+ @click="updateVolumeLevel"
298
+ @mouseenter="activateLabel($event.currentTarget)"
299
+ @mouseleave="deactivateLabel($event.currentTarget)"
300
+ @focus="
301
+ (activateLabel($event.currentTarget),
302
+ changeFocusState('volumeSlider', true))
303
+ "
304
+ @blur="
305
+ (deactivateLabel($event.currentTarget),
306
+ changeFocusState('volumeSlider', false))
307
+ "
308
+ />
309
+ <span class="volume-label" aria-hidden="true">
310
+ {{ $t('button.volume') }}
311
+ </span>
312
+ <!--using span because ::before on input is not supported by firefox-->
313
+ <span
314
+ ref="$volume-progress"
315
+ class="volume-progress"
316
+ :style="{
317
+ '--background-size-vs': `calc(${volumeSliderBackground} - 2px)`
318
+ }"
319
+ ></span>
320
+ </div>
321
+ </div>
322
+ <!--------------------------------- PLAY-BAR CC (video only) ------------------------------------------>
323
+ <div v-if="mediaToPlay.mType === 'video'" class="pb-areas right">
324
+ <app-base-button
325
+ id="btn-subtitles"
326
+ ref="$btn-subtitle"
327
+ class="btn subtitleBtns"
328
+ :title="ccLabel"
329
+ :aria-label="ccLabel"
330
+ :data-title="`${ccLabel} (c)`"
331
+ :is-disabled="!hasSubtitle"
332
+ :class="{
333
+ md_disabled: !hasSubtitle,
334
+ displayLabel: btnsLabelDisplay['btn-subtitles'] === true
335
+ }"
336
+ @click="toggleViewSubtitle()"
337
+ @mouseenter="activateLabel($event)"
338
+ @mouseleave="deactivateLabel($event)"
339
+ @focus="activateLabel($event)"
340
+ @blur="deactivateLabel($event)"
341
+ >
342
+ <svg
343
+ v-show="(hasSubtitle && !subtitlesEnabled) || !hasSubtitle"
344
+ class="app-icons-svg"
345
+ aria-hidden="true"
346
+ focusable="false"
347
+ >
348
+ <use href="#subtitle-off-icon" />
349
+ </svg>
350
+ <svg
351
+ v-show="hasSubtitle && subtitlesEnabled"
352
+ class="app-icons-svg"
353
+ aria-hidden="true"
354
+ focusable="false"
355
+ >
356
+ <use href="#subtitle-on-icon" />
357
+ </svg>
358
+ </app-base-button>
359
+ <!--------------------------------- PLAY-BAR Transcript ----------------------------------->
360
+ <app-base-button
361
+ id="btn-transcript"
362
+ ref="$btn-transcript"
363
+ class="btn-transcript"
364
+ :title="transcriptLabel"
365
+ :aria-label="transcriptLabel"
366
+ :data-title="`${transcriptLabel} (t)`"
367
+ :class="{
368
+ md_disabled: transcriptBtnDisabled,
369
+ displayLabel: btnsLabelDisplay['btn-transcript'] === true
370
+ }"
371
+ :is-disabled="transcriptBtnDisabled"
372
+ @click="toggleViewTranscript($event)"
373
+ @mouseenter="activateLabel($event)"
374
+ @mouseleave="deactivateLabel($event)"
375
+ @focus="activateLabel($event)"
376
+ @blur="deactivateLabel($event)"
377
+ >
378
+ <svg
379
+ v-show="(hasTranscript && !transcriptEnabled) || !hasTranscript"
380
+ class="app-icons-svg"
381
+ aria-hidden="true"
382
+ focusable="false"
383
+ >
384
+ <use href="#transcript-off-icon" />
385
+ </svg>
386
+ <svg
387
+ v-show="hasTranscript && transcriptEnabled"
388
+ class="app-icons-svg"
389
+ aria-hidden="true"
390
+ focusable="false"
391
+ >
392
+ <use href="#transcript-on-icon" />
393
+ </svg>
394
+ </app-base-button>
395
+ <!--------------------------------- PLAY-BAR FullScreen ----------------------------------->
396
+ <app-base-button
397
+ id="btn-fullscreen"
398
+ ref="$btn-fullscreen"
399
+ class="fullscreenBtns"
400
+ :title="fullscreenLabel"
401
+ :aria-label="fullscreenLabel"
402
+ :class="{
403
+ md_disabled: fullscreenBtnDisabled,
404
+ displayLabel: btnsLabelDisplay['btn-fullscreen'] === true
405
+ }"
406
+ :is-disabled="fullscreenBtnDisabled"
407
+ :data-title="`${fullscreenLabel} (f)`"
408
+ @click="toggleFullScreen()"
409
+ @mouseenter="activateLabel($event)"
410
+ @mouseleave="deactivateLabel($event)"
411
+ @focus="activateLabel($event)"
412
+ @blur="deactivateLabel($event)"
413
+ >
414
+ <svg
415
+ v-show="!fullscreenOn"
416
+ class="app-icons-svg"
417
+ aria-hidden="true"
418
+ focusable="false"
419
+ >
420
+ <use href="#fullscreen-icon" />
421
+ </svg>
422
+ <svg
423
+ v-show="fullscreenOn"
424
+ class="app-icons-svg"
425
+ aria-hidden="true"
426
+ focusable="false"
427
+ >
428
+ <use href="#fullscreen-exit-icon" />
429
+ </svg>
430
+ </app-base-button>
431
+ </div>
432
+ </div>
433
+ <div v-if="appDebugMode" class="timer">
434
+ <!-- <div class="timer"> -->
435
+ <p></p>
436
+ <p>Timer : {{ timer.getElapsedTime() }}</p>
437
+ </div>
438
+ </div>
439
+ </div>
440
+ </template>
441
+
442
+ <script>
443
+ import { mapState, mapActions } from 'pinia'
444
+ import { useAppStore } from '../module/stores/appStore'
445
+ import axios from 'axios'
446
+ import { Timer } from '../composables/useTimer'
447
+ import { reactive } from 'vue'
448
+ import { useI18n } from 'vue-i18n'
449
+
450
+ export default {
451
+ inject: ['userInteraction'],
452
+ props: {
453
+ mediaToPlay: { type: [Object, Boolean], default: false }
454
+ },
455
+ emits: ['resize-video'],
456
+
457
+ setup(props) {
458
+ const store = useAppStore()
459
+ const id = `plyr_${props.mediaToPlay.id}`
460
+ const timer = reactive(new Timer(id)) // Making Timer instance reactive to be able to track changes
461
+ const { t } = useI18n()
462
+ return { id, store, timer, t }
463
+ },
464
+ data() {
465
+ return {
466
+ //Playback animation
467
+ playClicked: false,
468
+ playBackAnim: true,
469
+ //ProgressSeek
470
+ progressSeek: null,
471
+ //Tooltip
472
+ seekTooltip: null,
473
+ tooTipTimeCode: '00:00',
474
+ //Hiding playbar animation
475
+ focusTimeout: null,
476
+ delayUntilHide: 5000,
477
+ hideTimer: null,
478
+ //Transcript
479
+ transcriptEnabled: false,
480
+ transcriptToShow: null,
481
+ //Fullscreen
482
+ fullscreenOn: false,
483
+ //==========================================================
484
+ //Container Playbar
485
+ pbContainer: null,
486
+ //ShowControls (show the playbar or not, default is true)
487
+ showControlsValue: true,
488
+ //Player
489
+ isPlaying: false,
490
+ canReplay: false,
491
+ savedIsPlaying: false, //To get the playing status on mousemove on the progressBar
492
+ //ProgressBar
493
+ currentTime: 0,
494
+ progressArea: null,
495
+ progressBar: null,
496
+ progressIndicator: null,
497
+ progressThumb: null,
498
+ progressThumbDown: false,
499
+ progressThumbHover: false,
500
+ //Volume
501
+ volumeBtn: null,
502
+ volumeSlider: null,
503
+ volumeIndicator: null,
504
+ currentVolume: null,
505
+ savedVolume: 0.5, //Saved volume when muted
506
+ //muted
507
+ muted: false,
508
+ isMediatSet: false,
509
+
510
+ //Labels
511
+ //Strings for screenreaders (duration et time)
512
+ mediaA11Y: {
513
+ label: 'progress',
514
+ valMax: null,
515
+ valNow: null,
516
+ valueText: null,
517
+ timeCode: 0,
518
+ duration: 0
519
+ },
520
+ //buttons label (all the buttons that need labels management in the playbar)
521
+ btnsLabelDisplay: {
522
+ 'btn-subtitles': false,
523
+ 'btn-transcript': false,
524
+ 'btn-fullscreen': false,
525
+ 'playbar-play': false,
526
+ 'playbar-volume': false,
527
+ 'volume-slider': false
528
+ },
529
+ //FocusState to manage arrows keydown on the 2 sliders (progress and volume)
530
+ focusState: {
531
+ progressBar: false,
532
+ volumeSlider: false
533
+ },
534
+ otherVideoTranscriptShown: false,
535
+ previousTime: 0,
536
+ viewedThreshold: 0.85,
537
+ viewedThresholdReached: false,
538
+ startedTime: 0,
539
+ playStmtSent: false,
540
+ mediaRawData: null
541
+
542
+ //==========================================================
543
+ }
544
+ },
545
+ computed: {
546
+ ...mapState(useAppStore, [
547
+ 'getCurrentBrowser',
548
+ 'getMediaSubtitles',
549
+ 'getCurrentPage',
550
+ 'getModuleChildren',
551
+ 'hasMediaElOrTimeline',
552
+ 'getCurrentMediaDuration',
553
+ 'getAutoplayEnabled',
554
+ 'getMediaVolume',
555
+ 'getModuleInfo',
556
+ 'getMediaPlaybarValues',
557
+ 'getPageInteraction',
558
+ 'getUserInteraction',
559
+ 'getMediaMuted',
560
+ 'getAppDebugMode'
561
+ ]),
562
+ appDebugMode() {
563
+ return this.getAppDebugMode
564
+ },
565
+ //==========================================================
566
+ //MediaElement
567
+ mediaElement() {
568
+ if (!this.mediaToPlay) return null
569
+ const { mElement } = this.mediaToPlay
570
+ return mElement ? mElement : null
571
+ },
572
+ //Media Progress
573
+ //Time display : duration
574
+ mediaDurationTime() {
575
+ if (this.mediaDuration > 0)
576
+ return this.$helper.formatTime(this.mediaDuration)
577
+ else return '00:00'
578
+ },
579
+ //Time display : timecode
580
+ timecode() {
581
+ if (this.currentTime && typeof (this.currentTime === Number)) {
582
+ return this.$helper.formatTime(this.currentTime)
583
+ } else return '00:00'
584
+ },
585
+ //ProgressPercentage for progressBarIndicator width
586
+ progressBarPercentage() {
587
+ if (this.currentTime && this.mediaDuration)
588
+ return (this.currentTime / this.mediaDuration) * 100
589
+ else return 0
590
+ },
591
+ progressBarEnded() {
592
+ return this.currentTime >= Math.floor(this.mediaDuration)
593
+ },
594
+ //Volume
595
+ volumeState() {
596
+ let state = null
597
+ switch (true) {
598
+ case this.muted || this.currentVolume <= 0:
599
+ state = 'muted'
600
+ break
601
+
602
+ case !this.muted && this.currentVolume <= 0.5:
603
+ state = 'low'
604
+ break
605
+ case !this.muted && this.currentVolume > 0.5:
606
+ state = 'high'
607
+ }
608
+
609
+ return state
610
+ },
611
+ //Volume progress for slider background
612
+ volumeSliderBackground() {
613
+ return (this.currentVolume * 100).toFixed(0).toString() + '%'
614
+ },
615
+ //Volume from store
616
+ mediaVolume() {
617
+ // return this.getMediaVolume
618
+ return this.getMediaPlaybarValues('volume')
619
+ },
620
+ //media from store
621
+ mediaMuted() {
622
+ return this.getMediaMuted
623
+ },
624
+ //Labels
625
+ playLabel() {
626
+ let label = null
627
+ switch (true) {
628
+ case this.isPlaying:
629
+ label = this.$t('button.pause')
630
+ break
631
+
632
+ case this.canReplay:
633
+ label = this.$t('button.replay')
634
+ break
635
+
636
+ default:
637
+ label = this.$t('button.play')
638
+ }
639
+ return label
640
+ },
641
+ volumeLabel() {
642
+ let label = null
643
+ switch (true) {
644
+ case this.muted:
645
+ label = `${this.$t('button.unmute')}`
646
+ break
647
+ case !this.muted:
648
+ label = `${this.$t('button.mute')}`
649
+ break
650
+ }
651
+ return label
652
+ },
653
+ volumeLevelA11Y() {
654
+ const w = this.$i18n.locale === 'en' ? 'at' : 'à'
655
+ const level = Math.round(this.currentVolume * 100)
656
+ let txtA11Y = `Volume ${w} ${level}%`
657
+
658
+ return txtA11Y
659
+ },
660
+
661
+ //Transcript
662
+ hasTranscript() {
663
+ if (!this.mediaToPlay) return
664
+ return this.mediaToPlay.mTranscript || false
665
+ },
666
+ //Subtitle
667
+ hasSubtitle() {
668
+ if (!this.mediaToPlay) return
669
+ return this.mediaToPlay.mSubtitles || false
670
+ },
671
+ subtitlesEnabled() {
672
+ if (!this.hasSubtitle) return false
673
+ else return this.getMediaSubtitles
674
+ },
675
+ //mediaContainer (only used for video)
676
+ mediaContainer() {
677
+ if (!this.mediaToPlay) return
678
+ return this.mediaToPlay.mMediaContainer
679
+ },
680
+
681
+ //MediaDuration :Number (used for tooltip)
682
+ mediaDuration() {
683
+ return this.mediaToPlay.mElement.duration
684
+ ? this.mediaToPlay.mElement.duration
685
+ : 0
686
+ },
687
+ //Labels
688
+ fullscreenLabel() {
689
+ let label = null
690
+ if (this.fullscreenOn) label = `${this.$t('button.full_screen_off')}`
691
+ else label = `${this.$t('button.full_screen_on')}`
692
+ return label
693
+ },
694
+ transcriptLabel() {
695
+ let label = null
696
+ if (this.transcriptEnabled) label = `${this.$t('button.transcript_off')}`
697
+ else label = `${this.$t('button.transcript_on')}`
698
+ return label
699
+ },
700
+ ccLabel() {
701
+ let label = null
702
+ if (this.subtitlesEnabled) label = `${this.$t('button.subtitle_off')}`
703
+ else label = `${this.$t('button.subtitle_on')}`
704
+ return label
705
+ },
706
+ transcriptBtnDisabled() {
707
+ return (
708
+ !this.hasTranscript ||
709
+ this.fullscreenOn ||
710
+ this.otherVideoTranscriptShown
711
+ )
712
+ },
713
+ fullscreenBtnDisabled() {
714
+ return this.transcriptEnabled || this.otherVideoTranscriptShown
715
+ }
716
+ },
717
+ watch: {
718
+ viewedThresholdReached: {
719
+ handler() {
720
+ //send a "viewed" event for this media
721
+ this.trackEvent('end')
722
+ }
723
+ },
724
+ getUserInteraction: {
725
+ handler() {
726
+ const activityID = this.$route.meta.activity_ref
727
+ const pageID = this.$route.meta.id
728
+ const interaction = this.getPageInteraction(
729
+ activityID,
730
+ pageID
731
+ ).userInteraction
732
+
733
+ if (
734
+ !interaction ||
735
+ !interaction.mediasViewed ||
736
+ !interaction.mediasViewed.length
737
+ )
738
+ return
739
+
740
+ this.getViewedSatus(interaction.mediasViewed)
741
+ this.setMediaVolume()
742
+ },
743
+ immediate: true
744
+ // deep: true
745
+ },
746
+ getCurrentPage: {
747
+ handler(newValue) {
748
+ if (!newValue.audiosData && !newValue.videosData) return
749
+ const { audiosData, videosData } = this.getCurrentPage
750
+ this.mediaRawData =
751
+ this.mediaToPlay.mType == 'video'
752
+ ? videosData.find((el) => el.id == this.mediaToPlay.id)
753
+ : audiosData.find((el) => el.id == this.mediaToPlay.id)
754
+ },
755
+ immediate: true,
756
+ deep: true
757
+ }
758
+ },
759
+
760
+ mounted() {
761
+ //initialize media $refs
762
+ this.initializeMediaElm()
763
+ //initialize video $refs && set eventlistener for video
764
+ if (this.mediaToPlay.mType === 'video') {
765
+ this.initializeVideoElm()
766
+ this.setVideoHandlers()
767
+ }
768
+ //Set eventlistener for media
769
+ this.setMediaHandlers()
770
+
771
+ //Set label for ProgressBar
772
+ this.setProgressBarA11Y()
773
+ //Set initial volume and duration
774
+ this.updateVolumeLevel()
775
+
776
+ //update the the information of the media element linked to this play bar. playbar instance will be added to the media info
777
+ this.updateCurrentMediaElements({
778
+ id: this.mediaToPlay.id,
779
+ ...this.$parent.$refs
780
+ })
781
+ },
782
+ beforeUnmount() {
783
+ if (this.isPlaying) {
784
+ this.togglePlay()
785
+ }
786
+ this.removeMediaHandlers()
787
+
788
+ if (this.mediaToPlay.mType === 'video') this.removeVideoHandlers()
789
+
790
+ if (this.firefoxTrack) {
791
+ this.firefoxTrack.removeEventListener('cuechange', this.fireFoxMoveCC)
792
+ }
793
+
794
+ this.$bus.$off('transcript-hidden', this.resetTranscript)
795
+ },
796
+ methods: {
797
+ ...mapActions(useAppStore, [
798
+ 'setMediaSubtitles',
799
+ 'updateCurrentMediaElements',
800
+ 'setMediaMuted',
801
+ 'setMediaPlaybarValues'
802
+ ]),
803
+ trackEvent(action) {
804
+ const expectedActions = ['play', 'viewed']
805
+ if (!action || !expectedActions.includes(action)) return
806
+ const media = this.mediaRawData
807
+ if (!media || !media.mSources || !media.mSources.length) return
808
+ const { mTitle, mSources } = media
809
+ const { mType } = this.mediaToPlay
810
+ const URIBase = `media/${mType}/`
811
+
812
+ //send statement
813
+ const stmt = {
814
+ id: (() => `${URIBase}${mSources[0].src.split('/').toReversed()[0]}`)(),
815
+ verb: null,
816
+ definition: mTitle || mSources[0].src,
817
+ description: null,
818
+ type: `https://w3id.org/xapi/${mType}/activity-type/${mType}`,
819
+ context: {
820
+ contextActivities: {
821
+ category: [{ id: `https://w3id.org/xapi/${mType}` }]
822
+ },
823
+ extensions: {
824
+ 'https://w3id.org/xapi/video/extensions/session-id':
825
+ this.getModuleInfo.id
826
+ }
827
+ },
828
+ result: null
829
+ }
830
+ switch (action) {
831
+ case 'play': {
832
+ stmt.verb = 'played'
833
+ stmt.description = `${mType} ${stmt.id.replace(URIBase, '')}`
834
+ stmt.result = {
835
+ extensions: {
836
+ 'https://w3id.org/xapi/video/extensions/time': Math.round(
837
+ this.startedTime
838
+ ),
839
+ 'https://w3id.org/xapi/video/extensions/progress': Math.round(
840
+ (this.startedTime / this.mediaDuration) * 100
841
+ )
842
+ }
843
+ }
844
+ break
845
+ }
846
+ case 'viewed': {
847
+ const expectedViewTime = Number(
848
+ this.timer.getElapsedTime() / Math.trunc(this.mediaDuration)
849
+ )
850
+ stmt.verb = 'completed'
851
+ stmt.description = `${mType} ${stmt.id.replace(URIBase, '')}`
852
+ stmt.result = {
853
+ extensions: {
854
+ 'https://w3id.org/xapi/video/extensions/progress':
855
+ expectedViewTime * 100,
856
+ 'https://w3id.org/xapi/video/extensions/time-from': Math.round(
857
+ this.startedTime
858
+ ),
859
+ 'https://w3id.org/xapi/video/extensions/time-to': this.currentTime
860
+ },
861
+ completion:
862
+ expectedViewTime >= this.viewedThreshold &&
863
+ this.viewedThresholdReached
864
+ }
865
+ stmt.duration = this.timer.ISOTimeParser(
866
+ expectedViewTime * this.mediaDuration
867
+ )
868
+ break
869
+ }
870
+ }
871
+ this.$bus.$emit('send-xapi-statement', stmt)
872
+ //send Analytics Event
873
+ const analyticsEventName = `fcad_${mType}_${action}`
874
+ this.trackWithGA(analyticsEventName)
875
+ },
876
+ trackWithGA(eventName) {
877
+ if (
878
+ !eventName ||
879
+ !this.mediaRawData ||
880
+ !this.mediaRawData.mSources ||
881
+ !this.mediaRawData.mSources.length
882
+ )
883
+ return
884
+ const { mTitle, mSources } = this.mediaRawData
885
+
886
+ const eventParams = {
887
+ title: mTitle || mSources[0].src,
888
+ url: mSources[0].src
889
+ }
890
+
891
+ this.$analytics.sendEvent(eventName, eventParams)
892
+ },
893
+
894
+ //======================================================================
895
+ /**
896
+ * @description - Set all the DOM elements for medias
897
+ */
898
+ initializeMediaElm() {
899
+ this.pbContainer = this.$refs['$pb-container']
900
+ //ProgressBar
901
+ this.progressArea = this.$refs['$progress-area']
902
+ this.progressBar = this.$refs['$progress-bar']
903
+ this.progressIndicator = this.$refs['$progress-indicator']
904
+ this.progressThumb = this.$refs['$progress-thumb']
905
+ //SeekTooltip
906
+ this.seekTooltip = this.$refs['$seek-tooltip']
907
+ //Volume//
908
+ this.volumeBtn = this.$refs['$btn-volume']
909
+ this.volumeSlider = this.$refs['$volume-slider']
910
+ this.volumeIndicator = this.$refs['$volume-progress']
911
+ },
912
+ /**
913
+ * @description - handle all the listeners for medias
914
+ */
915
+ setMediaHandlers() {
916
+ if (this.mediaElement) {
917
+ //Prevent default on keys used for navigation in the player (prevent scrollbar to be trigger when changing volume)
918
+ this.pbContainer.addEventListener('keydown', this.keysPreventDefault)
919
+ //progressBar events
920
+ this.progressArea.addEventListener('click', this.seekingProgress)
921
+ //window handlers for playbar progress
922
+ window.addEventListener('mousemove', this.progressWindowMove)
923
+ window.addEventListener('mouseup', this.progressWindowUp)
924
+
925
+ //Update data when media as a timeupdate/ended
926
+ this.mediaElement.addEventListener(
927
+ 'timeupdate',
928
+ this.updateProgressBarTime
929
+ )
930
+ this.mediaElement.addEventListener('ended', this.mediaEnded)
931
+ //Volume
932
+ this.volumeSlider.addEventListener('input', this.updateVolumeLevel)
933
+ //Hotkeys
934
+ this.pbContainer.addEventListener('keydown', this.handleMediaControls)
935
+ }
936
+ },
937
+
938
+ /**
939
+ * @description - Unset all the listeners (medias)
940
+ */
941
+ removeMediaHandlers() {
942
+ if (this.mediaElement) {
943
+ //progressBar events
944
+ this.progressArea.removeEventListener('click', this.seekingProgress)
945
+ //window handlers for playbar progress
946
+ window.removeEventListener('mousemove', this.progressWindowMove)
947
+ window.removeEventListener('mouseup', this.progressWindowUp)
948
+
949
+ this.mediaElement.removeEventListener(
950
+ 'timeupdate',
951
+ this.updateProgressBarTime
952
+ )
953
+ this.mediaElement.removeEventListener('ended', this.mediaEnded)
954
+
955
+ //Volume
956
+ this.volumeSlider.removeEventListener('input', this.updateVolumeLevel)
957
+ //Keys
958
+ this.pbContainer.removeEventListener('keydown', this.keysPreventDefault)
959
+ this.pbContainer.removeEventListener(
960
+ 'keydown',
961
+ this.handleMediaControls
962
+ )
963
+ }
964
+ },
965
+ /**
966
+ * @description - play or pause the media
967
+ * when the media starts playing, send a statement to the LRS.
968
+ * Statement is sent only once per media play
969
+ * @fires manage-media-players - to the Page the media in play
970
+ */
971
+ async togglePlay() {
972
+ //If the progressBar is at the end, restart the media
973
+ if (this.progressBarEnded) {
974
+ this.mediaElement.currentTime = 0
975
+ this.currentTime = 0
976
+ }
977
+ //Play or Pause the media
978
+ this.isPlaying ? this.mediaElement.pause() : this.mediaElement.play()
979
+ this.startedTime = this.mediaElement.currentTime
980
+ // start or pause the timer depending of the state of the media
981
+ !this.isPlaying ? this.timer.start() : this.timer.pause()
982
+
983
+ if (!this.playStmtSent) {
984
+ //this.sendMediaStatement("play", this.startedTime, null);
985
+ this.trackEvent('play')
986
+ this.playStmtSent = true
987
+ }
988
+ //Data
989
+ if (!this.isPlaying) {
990
+ //Signal to set this mediaElement as the last playing
991
+ this.$bus.$emit('manage-media-players', this.mediaToPlay)
992
+ this.previousTime = Math.round(this.mediaElement.currentTime)
993
+ }
994
+ this.isPlaying = !this.isPlaying
995
+ this.canReplay = false
996
+ },
997
+ /**
998
+ * @description - Set the media to a new time (if its a time between 0 and max)
999
+ */
1000
+ setMediaTime(time) {
1001
+ if (time <= 0) this.mediaElement.currentTime = 0
1002
+ else if (time > this.mediaDuration)
1003
+ this.mediaElement.currentTime = this.mediaDuration
1004
+ else this.mediaElement.currentTime = time
1005
+ //Handle finish Media if its set to max duration
1006
+ if (this.progressBarEnded) {
1007
+ this.mediaEnded()
1008
+ }
1009
+ },
1010
+ /**
1011
+ * @description - Set the media Ended if it is set to the end of the progressBar by the user or if it plays until the end
1012
+ */
1013
+
1014
+ mediaEnded() {
1015
+ //Don't make the media play when release progressBar thumb and the media is Ended
1016
+ this.savedIsPlaying = false
1017
+ //Pausing mediaElement is isPlaying
1018
+ if (this.mediaElement && this.isPlaying) {
1019
+ this.mediaElement.pause()
1020
+ this.isPlaying = false
1021
+ this.timer.stop()
1022
+
1023
+ //this.sendMediaStatement("end", this.startedTime, expectedViewTime);
1024
+ this.trackEvent('viewed')
1025
+ this.playStmtSent = false //reset the play statement for next play
1026
+ }
1027
+ this.$bus.$emit('media-viewed', this.mediaToPlay.id)
1028
+ this.canReplay = true
1029
+ if (this.mediaToPlay.mType === 'video') this.showControls()
1030
+ },
1031
+
1032
+ /**
1033
+ * @description get the view Status of the current media from UserInteraction
1034
+ */
1035
+ getViewedSatus(mediasViewed) {
1036
+ // Should update the can replay state of media
1037
+ this.canReplay = mediasViewed.includes(this.mediaElement.id)
1038
+ },
1039
+ /**
1040
+ * @description controls the level Of the media volume
1041
+ * @param d string, optional volUp or volDown to Add or Remove 0.05 from volume value
1042
+ */
1043
+ updateVolumeLevel(d = null) {
1044
+ if (!this.mediaElement) return
1045
+ if (this.mediaElement.muted) {
1046
+ this.mediaElement.muted = this.muted = false
1047
+ //Send muted state to the store
1048
+ this.setMutedState(this.muted)
1049
+ }
1050
+ switch (true) {
1051
+ case d == 'volUP':
1052
+ //Set volume Up
1053
+ if (this.mediaElement.volume >= 1) return
1054
+ if (this.mediaElement.volume >= 0.95) {
1055
+ this.mediaElement.volume = 1
1056
+ } else {
1057
+ this.mediaElement.volume = (
1058
+ this.mediaElement.volume + 0.05
1059
+ ).toFixed(2)
1060
+ }
1061
+ this.volumeSlider.value = this.currentVolume =
1062
+ this.mediaElement.volume
1063
+ break
1064
+
1065
+ case d == 'volDOWN':
1066
+ //Set volume Down
1067
+ if (this.mediaElement.volume <= 0) return
1068
+ if (this.mediaElement.volume <= 0.05) {
1069
+ this.mediaElement.volume = 0
1070
+ } else {
1071
+ this.mediaElement.volume = (
1072
+ this.mediaElement.volume - 0.05
1073
+ ).toFixed(2)
1074
+ }
1075
+
1076
+ this.volumeSlider.value = this.currentVolume =
1077
+ this.mediaElement.volume
1078
+ break
1079
+
1080
+ default: {
1081
+ //set volume
1082
+ this.currentVolume = this.mediaElement.volume =
1083
+ this.volumeSlider.value
1084
+ this.currentVolume = parseFloat(this.currentVolume).toFixed(2)
1085
+ }
1086
+ }
1087
+ if (!this.muted) this.savedVolume = this.currentVolume
1088
+
1089
+ this.setMediaPlaybarValues({
1090
+ volume: this.savedVolume
1091
+ }) // Save the volume in the Store
1092
+ },
1093
+ /**
1094
+ * @description - Toggle the state fo the sound
1095
+ */
1096
+ toggleMute() {
1097
+ if (!this.showControlsValue) this.showControls()
1098
+ //Check first the volume of the media
1099
+ if (this.mediaElement.volume == 0) this.muted = true
1100
+
1101
+ this.muted = !this.muted
1102
+ this.mediaElement.muted = this.muted // set the muted state of the media
1103
+
1104
+ //Send mutedstate to the store
1105
+ this.setMutedState(this.muted)
1106
+
1107
+ if (this.muted) {
1108
+ this.mediaElement.volume = 0
1109
+ this.savedVolume = this.currentVolume
1110
+ this.volumeSlider.value = 0
1111
+ this.currentVolume = 0
1112
+ }
1113
+ if (!this.muted) {
1114
+ this.volumeSlider.value = this.savedVolume
1115
+ this.currentVolume = this.mediaElement.volume = this.volumeSlider.value
1116
+ }
1117
+ },
1118
+
1119
+ /** @description Save the muted in the Store */
1120
+ setMutedState() {
1121
+ this.setMediaMuted(this.muted)
1122
+ },
1123
+ //Keyboard event methods
1124
+ /**
1125
+ * @description Method to prevent default on keys used for navigation in the player (prevent scrollbar to be trigger when changing volume)
1126
+ * */
1127
+ keysPreventDefault(evt) {
1128
+ if (
1129
+ ['Space', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].indexOf(
1130
+ evt.key
1131
+ ) > -1
1132
+ ) {
1133
+ evt.preventDefault()
1134
+ }
1135
+ },
1136
+
1137
+ /**
1138
+ * @description Method to handle the keyboard input to controle the media player
1139
+ *@summary Keyboard actions:
1140
+ * *** Left-Arrow/ Right-Arrow : move foward/Backward media by 5s
1141
+ * *** Up-Arrow/ Down-Arrow: Increase/Decrease media volume by 5%
1142
+ * *** c : show/ hide subtitle
1143
+ * *** f : enter/ exit fullscreen
1144
+ * *** k : play/ pause media
1145
+ * *** m : Mute/ Unmute volume
1146
+ * *** t : show transcript
1147
+ */
1148
+
1149
+ handleMediaControls(e) {
1150
+ e = e.code
1151
+ if (!this.showControlsValue) this.showControls()
1152
+ switch (e) {
1153
+ case 'ArrowLeft':
1154
+ //If focus is on Volume, VolDown
1155
+ if (this.focusState.volumeSlider) {
1156
+ //VolDown
1157
+ this.updateVolumeLevel('volDOWN')
1158
+ break
1159
+ } else {
1160
+ //Go back 5s in media timeline
1161
+ this.cursorLeft()
1162
+ break
1163
+ }
1164
+
1165
+ case 'ArrowRight':
1166
+ //If focus is on Volume, VolUp
1167
+ if (this.focusState.volumeSlider) {
1168
+ //VolUp
1169
+ this.updateVolumeLevel('volUP')
1170
+ break
1171
+ } else {
1172
+ //Move forward 5s in media timeline
1173
+ this.cursorRight()
1174
+ break
1175
+ }
1176
+
1177
+ case 'ArrowUp':
1178
+ //If focus is on progressBar, cursorRight
1179
+ if (this.focusState.progressBar) {
1180
+ //Move forward 5s in media timeline
1181
+ this.cursorRight()
1182
+ break
1183
+ } else {
1184
+ //VolUp
1185
+ this.updateVolumeLevel('volUP')
1186
+ break
1187
+ }
1188
+
1189
+ case 'ArrowDown':
1190
+ //If focus is on progressBar, cursorRight
1191
+ if (this.focusState.progressBar) {
1192
+ //Go back 5s in media timeline
1193
+ this.cursorLeft()
1194
+ break
1195
+ } else {
1196
+ //Voldown
1197
+ this.updateVolumeLevel('volDOWN')
1198
+ break
1199
+ }
1200
+
1201
+ case 'KeyM':
1202
+ this.toggleMute()
1203
+ break
1204
+
1205
+ case 'KeyF':
1206
+ if (this.mediaToPlay.mType == 'audio') break
1207
+ //Toggle fullScreen
1208
+ this.toggleFullScreen()
1209
+ break
1210
+
1211
+ case 'KeyT':
1212
+ if (this.mediaToPlay.mType == 'audio') break
1213
+ //Toggle transcript view
1214
+ this.toggleViewTranscript(e)
1215
+ break
1216
+
1217
+ case 'KeyC':
1218
+ if (this.mediaToPlay.mType == 'audio') break
1219
+ //Toggle subtitle view
1220
+ this.toggleViewSubtitle()
1221
+ break
1222
+
1223
+ case 'KeyK':
1224
+ //Play Pause on k bar pressed
1225
+ if (this.mediaToPlay.mType == 'audio') this.togglePlay()
1226
+ //Play/pause video with the playBackAnim if it is triggerd with KeyK
1227
+ else this.handlePlayVideo('playBackAnim')
1228
+ break
1229
+ }
1230
+ },
1231
+ /**
1232
+ * @description - Activate the label of a button include in btnsLabelDisplay on Hover/Focus on the button. Deactivate the label in all the others btns
1233
+ */
1234
+ activateLabel(elm) {
1235
+ const id = elm.id
1236
+ this.btnsLabelDisplay[id] = true
1237
+ for (const property in this.btnsLabelDisplay) {
1238
+ if (property !== id) {
1239
+ this.btnsLabelDisplay[property] = false
1240
+ }
1241
+ }
1242
+ },
1243
+ /**
1244
+ * @description - Get currenttime from media
1245
+ */
1246
+
1247
+ getCurrentTime() {
1248
+ this.currentTime = this.mediaElement.currentTime
1249
+ return this.mediaElement.currentTime
1250
+ },
1251
+ /**
1252
+ * @description - controls to move the media timeline backward
1253
+ */
1254
+ cursorLeft() {
1255
+ this.setMediaTime(this.getCurrentTime() - 5)
1256
+ },
1257
+ /**
1258
+ * @description - control to move the media timeline forwrad
1259
+ */
1260
+ cursorRight() {
1261
+ this.setMediaTime(this.getCurrentTime() + 5)
1262
+ },
1263
+ //ProgressBar Methods
1264
+
1265
+ /**
1266
+ * @description - Update currentTime and timing strings when media is playing
1267
+ */
1268
+ updateProgressBarTime(e) {
1269
+ //Get currentTime from the event
1270
+ this.currentTime = e.target.currentTime
1271
+
1272
+ //Set viewedThresholdReached to true when the threshold is reached
1273
+ if (
1274
+ this.timer.getElapsedTime() / Math.trunc(this.mediaDuration) >=
1275
+ this.viewedThreshold
1276
+ )
1277
+ this.viewedThresholdReached = true
1278
+
1279
+ //Update strings
1280
+ this.setProgressBarA11Y()
1281
+ //If replay is true and the progressIndicator is not at the end, set canReplay to false
1282
+ if (this.canReplay) this.canReplay = false
1283
+ //If replay is false and progressIndicator is at the end, set canReplay to true
1284
+ if (
1285
+ !this.canReplay &&
1286
+ this.currentTime >= Math.floor(this.mediaDuration)
1287
+ ) {
1288
+ this.canReplay = true
1289
+ }
1290
+ },
1291
+
1292
+ /**
1293
+ * @description - Set mediatime depending on progressBar click
1294
+ */
1295
+ seekingProgress(e) {
1296
+ if (!this.progressThumbDown && !this.progressThumbHover) {
1297
+ const updatedTime =
1298
+ (e.offsetX / this.progressBar.offsetWidth) * this.mediaDuration
1299
+ this.setMediaTime(updatedTime)
1300
+ }
1301
+ },
1302
+
1303
+ /**
1304
+ * @description - Set mediatime depending on mousemove on the screen (when progress thumb is mousedown)
1305
+ */
1306
+ progressWindowMove(ev) {
1307
+ if (this.progressThumbDown) {
1308
+ if (this.isPlaying) this.togglePlay()
1309
+ this.getTimeFromClickPos(ev)
1310
+ let timeToSet = Math.floor(this.getTimeFromClickPos(ev))
1311
+ if (timeToSet < 0) timeToSet = 0
1312
+ else if (timeToSet > this.duration) timeToSet = this.duration
1313
+ this.currentTime = timeToSet
1314
+ }
1315
+ },
1316
+
1317
+ /**
1318
+ * @description - Update thumb state (mouseup) when the mouse is up on the screen
1319
+ */
1320
+ progressWindowUp(ev) {
1321
+ if (this.progressThumbDown) {
1322
+ this.setMediaTime(this.currentTime)
1323
+ this.progressThumbDown = false
1324
+ //savedIsPlaying is the playing status saved on mousedown event on the thumb (the video is paused when moving on the progressBar)
1325
+ if (this.isPlaying !== this.savedIsPlaying) this.togglePlay()
1326
+ }
1327
+ },
1328
+ /**
1329
+ * @description - Get the time corresponding of the position of the click on the progressBar
1330
+ */
1331
+ getTimeFromClickPos(ev) {
1332
+ let timeToSet = 0
1333
+ //La progress Bar
1334
+ const progressBar = this.progressBar
1335
+ //La position de la progressBar dans la page
1336
+ const left = progressBar.getBoundingClientRect().left
1337
+ //Position du click par rapport à la progressBar
1338
+ let clickPos = ev.clientX - left
1339
+ //Si la position est à gauche de la progressBar, retourner 0s
1340
+ if (clickPos < 0) return 0
1341
+ //Obtenir le width de la progressBar
1342
+ const width = progressBar.offsetWidth
1343
+ //Obtenir le ratio de la position du click vs width de la progressBar
1344
+ let percentage = clickPos / width
1345
+ //Si > que 1, veux dire que le clique est à droite de la progressBar (mettre le curseur à la fin)
1346
+ percentage = percentage > 1 ? 1 : percentage
1347
+ //Ratio * temps total du media donne la progressiob
1348
+ timeToSet = percentage * this.mediaDuration
1349
+ return timeToSet
1350
+ },
1351
+
1352
+ /** @description - methode to set A11Y support for screen reader on progress bar element (video/audio/) */
1353
+ setProgressBarA11Y() {
1354
+ const w = this.$i18n.locale === 'en' ? 'at' : 'à'
1355
+ const dm =
1356
+ this.$i18n.locale === 'en'
1357
+ ? `${this.mediaToPlay.mType} duration`
1358
+ : `durée ${this.mediaToPlay.mType}`
1359
+
1360
+ this.mediaA11Y.valMax = Math.round(this.mediaElement.duration)
1361
+ this.mediaA11Y.valNow = Math.round(this.currentTime)
1362
+
1363
+ //format text output for screen reader
1364
+ let currentTimeArray = this.timecode.split(':')
1365
+ let fullTimeArray = this.mediaDurationTime.split(':')
1366
+ let hrsTxt, mnTxt, ssTxt, HRSTxt, MNTxt, SSTxt, fTimeTxt, cTimeTxt
1367
+ const mediaTitle = this.mediaToPlay.mTitle || this.mediaToPlay.mType
1368
+ this.mediaA11Y.label = this.$t('a11y_sr.seek_slider')
1369
+
1370
+ //format string to remove leading 0 digit for time
1371
+ const formatNum = (n) => {
1372
+ return n[0] == '0' ? n.substring(1).trim() : n.trim()
1373
+ }
1374
+
1375
+ const formatMsg = (x, y) => {
1376
+ const reg = /%x|%y/g
1377
+ let formatStr = this.$t('a11y_sr.range_expression').replace(
1378
+ reg,
1379
+ (e) => {
1380
+ if (e == '%x') return x
1381
+ if (e == '%y') return y
1382
+ }
1383
+ )
1384
+
1385
+ return formatStr
1386
+ }
1387
+
1388
+ switch (fullTimeArray.length) {
1389
+ case 3:
1390
+ if (!currentTimeArray.length) return
1391
+ //format current hours text text with plurialization apply
1392
+ hrsTxt =
1393
+ currentTimeArray.length == 3 && parseInt(currentTimeArray[0]) > 0
1394
+ ? `${formatNum(currentTimeArray[0])} ${this.$t(
1395
+ 'a11y_sr.time.hours',
1396
+ parseInt(formatNum(currentTimeArray[0]))
1397
+ )}`
1398
+ : ''
1399
+
1400
+ //format minutes text text with plurialization apply
1401
+ mnTxt =
1402
+ parseInt(currentTimeArray[1]) > 0
1403
+ ? `${formatNum(currentTimeArray[1])} ${this.$t(
1404
+ 'a11y_sr.time.minute',
1405
+ parseInt(formatNum(currentTimeArray[1]))
1406
+ )}`
1407
+ : ''
1408
+
1409
+ //format seconds text with plurialization apply
1410
+ ssTxt =
1411
+ parseInt(currentTimeArray[2]) > 0
1412
+ ? `${formatNum(currentTimeArray[2])} ${this.$t(
1413
+ 'a11y_sr.time.second',
1414
+ parseInt(formatNum(currentTimeArray[2]))
1415
+ )}`
1416
+ : `0 ${this.$t('a11y_sr.time.second')}`
1417
+
1418
+ HRSTxt =
1419
+ parseInt(fullTimeArray[0]) > 0
1420
+ ? `${formatNum(fullTimeArray[0])} ${this.$t(
1421
+ 'a11y_sr.time.hours',
1422
+ parseInt(formatNum(fullTimeArray[0]))
1423
+ )}`
1424
+ : ''
1425
+
1426
+ MNTxt =
1427
+ parseInt(fullTimeArray[1]) > 0
1428
+ ? `${formatNum(fullTimeArray[1])} ${this.$t(
1429
+ 'a11y_sr.time.minute',
1430
+ parseInt(formatNum(fullTimeArray[1]))
1431
+ )}`
1432
+ : ''
1433
+
1434
+ SSTxt =
1435
+ parseInt(fullTimeArray[2]) > 0
1436
+ ? `${formatNum(fullTimeArray[2])} ${this.$t(
1437
+ 'a11y_sr.time.second',
1438
+ parseInt(formatNum(fullTimeArray[2]))
1439
+ )}`
1440
+ : ''
1441
+
1442
+ cTimeTxt = `${hrsTxt} ${mnTxt} ${ssTxt}`
1443
+ fTimeTxt = `${HRSTxt} ${MNTxt} ${SSTxt}`
1444
+
1445
+ //Format to show 0 when no value is present
1446
+ cTimeTxt = cTimeTxt.trim().length ? cTimeTxt.trim() : 0
1447
+ // Set sr text for timeCode && duration
1448
+ this.mediaA11Y.timeCode = `${this.mediaToPlay.mType} ${w} ${cTimeTxt}`
1449
+ this.mediaA11Y.duration = `${dm} ${fTimeTxt}`
1450
+ this.mediaA11Y.valueText = `${mediaTitle} ${formatMsg(cTimeTxt, fTimeTxt)}`
1451
+
1452
+ break
1453
+ case 2:
1454
+ if (!currentTimeArray.length) return
1455
+
1456
+ //format current time text
1457
+ mnTxt =
1458
+ parseInt(currentTimeArray[0]) > 0
1459
+ ? `${formatNum(currentTimeArray[0])} ${this.$t(
1460
+ 'a11y_sr.time.minute',
1461
+ parseInt(formatNum(currentTimeArray[0]))
1462
+ )}`
1463
+ : ''
1464
+
1465
+ ssTxt =
1466
+ parseInt(currentTimeArray[1]) > 0
1467
+ ? `${formatNum(currentTimeArray[1])} ${this.$t(
1468
+ 'a11y_sr.time.second',
1469
+ parseInt(formatNum(currentTimeArray[1]))
1470
+ )}`
1471
+ : `0 ${this.$t('a11y_sr.time.second')}`
1472
+
1473
+ // format full time text
1474
+ MNTxt =
1475
+ parseInt(fullTimeArray[0]) > 0
1476
+ ? `${formatNum(fullTimeArray[0])} ${this.$t(
1477
+ 'a11y_sr.time.minute',
1478
+ parseInt(formatNum(fullTimeArray[0]))
1479
+ )}`
1480
+ : ''
1481
+ SSTxt =
1482
+ parseInt(fullTimeArray[1]) > 0
1483
+ ? `${formatNum(fullTimeArray[1])} ${this.$t(
1484
+ 'a11y_sr.time.second',
1485
+ parseInt(formatNum(fullTimeArray[1]))
1486
+ )}`
1487
+ : ''
1488
+
1489
+ cTimeTxt = `${mnTxt} ${ssTxt}`
1490
+ fTimeTxt = `${MNTxt} ${SSTxt}`
1491
+
1492
+ //Format to show 0 when no value is present
1493
+ cTimeTxt = cTimeTxt.trim().length ? cTimeTxt.trim() : 0
1494
+ // Set sr text for timeCode && duration
1495
+ this.mediaA11Y.timeCode = `${this.mediaToPlay.mType} ${w} ${cTimeTxt}`
1496
+ this.mediaA11Y.duration = `${dm} ${fTimeTxt}`
1497
+
1498
+ this.mediaA11Y.valueText = `${mediaTitle} ${formatMsg(cTimeTxt, fTimeTxt)}`
1499
+
1500
+ break
1501
+ }
1502
+ },
1503
+ /**
1504
+ * @description - Deactivate the label of a button include in btnsLabelDisplay on mouseout/blur of this button
1505
+ */
1506
+ deactivateLabel(elm) {
1507
+ const id = elm.id
1508
+ this.btnsLabelDisplay[id] = false
1509
+ },
1510
+ /**
1511
+ * @description - Update the state of the elm describe by the string (focus or not) for keydown arrow management in handleMediaControls method (volume | progress bar can use arrow up-down and left-right only on focus)
1512
+ */
1513
+ changeFocusState(string, bool) {
1514
+ this.focusState[string] = bool
1515
+ },
1516
+ /**
1517
+ * @description - handle initial volume level
1518
+ */
1519
+ setMediaVolume() {
1520
+ //Set media muted or with the savedVolume
1521
+ this.muted = this.mediaMuted
1522
+ if (!this.muted)
1523
+ return (this.currentVolume = this.savedVolume = this.mediaVolume)
1524
+ this.savedVolume = this.mediaVolume
1525
+ this.currentVolume = 0
1526
+ },
1527
+ //======================================================================
1528
+ /**
1529
+ * @description - Set the DOM elements specific for video
1530
+ */
1531
+ initializeVideoElm() {
1532
+ //SeekTooltip
1533
+ this.seekTooltip = this.$refs['$seek-tooltip']
1534
+ //Transcript
1535
+ this.$bus.$on('transcript-hidden', this.resetTranscript)
1536
+ this.setTranscript()
1537
+ },
1538
+
1539
+ /** "@description -update the information of the seek tooltip*/
1540
+ updateSeekTooltip(evt) {
1541
+ if (!this.showControlsValue) this.showControls()
1542
+ //Elm progress Bar
1543
+ const progressBar = this.progressBar
1544
+ //progressBar position on the page
1545
+ const left = progressBar.getBoundingClientRect().left
1546
+ //Mouse position versus progressBar
1547
+ const hoverPos = evt.clientX - left
1548
+ //Get progressBar offsetWidth
1549
+ const width = progressBar.offsetWidth
1550
+
1551
+ //Define the value of the skip tooltip position
1552
+ let offsetX = null
1553
+
1554
+ switch (true) {
1555
+ case hoverPos < 15:
1556
+ offsetX = 20
1557
+ break
1558
+ case hoverPos > width - 55:
1559
+ offsetX = width - 55
1560
+ break
1561
+ default:
1562
+ offsetX = hoverPos
1563
+ }
1564
+
1565
+ this.seekTooltip.style.left = `${offsetX}px` // update visual position of tooltip
1566
+
1567
+ //Get % of the cursor hover position/progressBar width
1568
+ let percentage = 0
1569
+ if (hoverPos > width) percentage = 100
1570
+ if (hoverPos < 0) percentage = 0
1571
+ else {
1572
+ percentage = hoverPos / width
1573
+ }
1574
+ let skipTo = percentage * this.mediaDuration
1575
+ //Update the Tooltip time
1576
+ this.tooTipTimeCode = this.$helper.formatTime(skipTo)
1577
+ },
1578
+ /**
1579
+ * @description - handle all the listeners for videos
1580
+ */
1581
+ setVideoHandlers() {
1582
+ //Tooltip
1583
+ this.progressArea.addEventListener('mousemove', this.updateSeekTooltip)
1584
+ //Fullscreen
1585
+ this.mediaContainer.addEventListener(
1586
+ 'fullscreenchange',
1587
+ this.updateFullScreenState
1588
+ )
1589
+ //Show controls
1590
+ this.mediaContainer.addEventListener('mousemove', this.showControls)
1591
+ },
1592
+ /**
1593
+ * @description - Unset all the listeners (video)
1594
+ */
1595
+ removeVideoHandlers() {
1596
+ //Tooltip
1597
+ this.progressArea.removeEventListener('mousemove', this.updateSeekTooltip)
1598
+ //Fullscreen
1599
+ this.mediaContainer.removeEventListener(
1600
+ 'fullscreenchange',
1601
+ this.updateFullScreenState
1602
+ )
1603
+ //Show controls
1604
+ this.mediaContainer.removeEventListener('mousemove', this.showControls)
1605
+ },
1606
+ /**
1607
+ /* @description - handle the play /pause of the media and the playback video animation
1608
+ */
1609
+ handlePlayVideo(type = null) {
1610
+ //Play/pause
1611
+ this.togglePlay()
1612
+ //Playbar animation (show/hide)
1613
+ this.isPlaying ? this.hideControls() : this.showControls()
1614
+
1615
+ //Playback animation (only the first play click)
1616
+ if (this.playClicked && !type) {
1617
+ this.playBackAnim = false
1618
+ return
1619
+ }
1620
+ //this.runPlaybackAnimation()
1621
+ this.playBackAnim = true
1622
+ this.playClicked = true
1623
+ // Should indicate with media is playing and set it in the store
1624
+ },
1625
+ /**
1626
+ * @description - Toggle the state of the screen
1627
+ * Set or unset the media in full screen
1628
+ */
1629
+ toggleFullScreen() {
1630
+ const fullscreenElement = this.mediaContainer
1631
+ console.log(fullscreenElement)
1632
+
1633
+ if (document.fullscreenElement) {
1634
+ // exitFullscreen is only available on the Document object.
1635
+ document.exitFullscreen()
1636
+ } else {
1637
+ //fullscreen is available on Element.
1638
+ fullscreenElement.requestFullscreen()
1639
+ }
1640
+ },
1641
+ /**
1642
+ * @description update the value of fulls screen state
1643
+ */
1644
+ updateFullScreenState() {
1645
+ if (!this.showControlsValue) this.showControls()
1646
+ this.fullscreenOn = !this.fullscreenOn
1647
+ },
1648
+
1649
+ fireFoxMoveCC() {
1650
+ if (this.firefoxTrack.activeCues[0]) {
1651
+ this.firefoxTrack.activeCues[0].line = 12
1652
+ }
1653
+ },
1654
+ /**
1655
+ * @description show the subtitle
1656
+ */
1657
+ showSubtitles() {
1658
+ this.mediaElement.textTracks[0].mode = 'showing'
1659
+ this.setMediaSubtitles(true)
1660
+ if (this.getCurrentBrowser == 'Firefox') {
1661
+ let video = document.getElementsByTagName('video')[0]
1662
+ let tracks = video.textTracks
1663
+ this.firefoxTrack = tracks[0]
1664
+
1665
+ this.firefoxTrack.addEventListener('cuechange', this.fireFoxMoveCC)
1666
+ }
1667
+ },
1668
+
1669
+ hideSubtitles() {
1670
+ this.mediaElement.textTracks[0].mode = 'hidden' // value can be 'disabled' also.
1671
+ this.setMediaSubtitles(false)
1672
+ },
1673
+ toggleViewSubtitle() {
1674
+ if (!this.showControlsValue) this.showControls()
1675
+ if (this.subtitlesEnabled) return this.hideSubtitles()
1676
+ if (!this.subtitlesEnabled) return this.showSubtitles()
1677
+ },
1678
+ /**
1679
+ * @description method to show or hide the transcipt.
1680
+ * @summary get toggle the value of transcriptEnabled and get content from the transcript file
1681
+ * and transfer it to Module for display.
1682
+ * @fires 'open-sidebar' to AppBaseModule
1683
+ * @fires 'close-sidebar' to AppBaseModule
1684
+ */
1685
+ toggleViewTranscript(event) {
1686
+ if (!this.showControlsValue) this.showControls()
1687
+ this.transcriptEnabled = !this.transcriptEnabled
1688
+ //When the function is called after a user interaction (mouse or keyboard), emit event
1689
+ if (event) {
1690
+ //emit transcript toggle event, listener in AppBasePage
1691
+ this.$bus.$emit(
1692
+ 'video-transcript-toggle',
1693
+ this.mediaToPlay,
1694
+ this.transcriptEnabled
1695
+ )
1696
+ }
1697
+
1698
+ if (this.transcriptEnabled && this.transcriptToShow) {
1699
+ //Resize video container
1700
+ this.$emit('resize-video', 'sm')
1701
+ //Open sidebar with the transcript
1702
+ return this.$bus.$emit('open-sidebar', {
1703
+ ctx: 'ctxTranscript',
1704
+ e: this.transcriptToShow,
1705
+ w: this.pbContainer
1706
+ })
1707
+ }
1708
+
1709
+ // Send close signal for the side bar when transcipt state is not enabled
1710
+ if (!this.transcriptEnabled) {
1711
+ //Resize video container
1712
+ this.$emit('resize-video', 'lg')
1713
+ //Open sidebar with the transcript
1714
+ return this.$bus.$emit('close-sidebar', 'ctxTranscript')
1715
+ }
1716
+ },
1717
+ /**
1718
+ * @description Fetching method for transcript
1719
+ * @summary Fetch a transcript from HTML file to display. File URL is defined in the media data
1720
+ * and return its content for display
1721
+ * @return {String} HTML string
1722
+ */
1723
+ async fetchTranscript() {
1724
+ try {
1725
+ //const tFile = 'exemple_transcript2.html'
1726
+ const { mTranscript } = this.mediaToPlay
1727
+ const tFile = mTranscript
1728
+ if (!tFile) throw new Error('Missing transcript File!')
1729
+
1730
+ // validate file types. Only .html are allowed
1731
+ if (!tFile.endsWith('.html'))
1732
+ throw new Error(
1733
+ 'Invalid valid transcript file. \n Expecting .html file'
1734
+ )
1735
+
1736
+ const fileUrl = !tFile.includes('/') ? `./${tFile}` : tFile //allow passing file in Public folder
1737
+ // const fileUrl = new URL(tFile, import.meta.url))
1738
+ const res = await axios.get(fileUrl, { responseType: 'blob' })
1739
+ const content = await res.data.text()
1740
+
1741
+ return content
1742
+ } catch (err) {
1743
+ console.warn("YOU'VE GOT AN ERROR!\n", err)
1744
+ }
1745
+ },
1746
+ /** ******************* Rendering Methods****************************** */
1747
+
1748
+ /**
1749
+ * @Description Method to set the transcription that need to be displayed
1750
+ */
1751
+ async setTranscript() {
1752
+ if (!this.hasTranscript) return (this.transcriptToShow = null)
1753
+
1754
+ this.transcriptToShow = await this.fetchTranscript()
1755
+ },
1756
+
1757
+ /**
1758
+ * @description Method to reset the transcript state
1759
+ */
1760
+ resetTranscript(content) {
1761
+ if (!this.showControlsValue) this.showControls()
1762
+ this.transcriptEnabled = false
1763
+ this.otherVideoTranscriptShown = false
1764
+ },
1765
+
1766
+ /**
1767
+ * @description Show the media controler
1768
+ */
1769
+ showControls() {
1770
+ this.showControlsValue = true
1771
+ if (this.hideTimer) clearTimeout(this.hideTimer) //cancel existing timer
1772
+
1773
+ this.hideControls()
1774
+ },
1775
+ /**
1776
+ * @description Hide the media after the video start playing
1777
+ */
1778
+
1779
+ hideControls() {
1780
+ if (this.mediaElement.paused) return
1781
+
1782
+ this.hideTimer = setTimeout(() => {
1783
+ this.showControlsValue = false
1784
+ }, this.delayUntilHide)
1785
+ }
1786
+
1787
+ /**
1788
+ * @description - Send xAPI statement for media play and end
1789
+ * @param {String} action - play or end
1790
+ * @param {Number} startTime - time in seconds when the media start to be played
1791
+ * @param {Number} endTime - time in seconds when the media stop to be played
1792
+ */
1793
+ }
1794
+ }
1795
+ </script>
1796
+ <style lang="scss" scoped>
1797
+ .pb-container.video {
1798
+ position: absolute;
1799
+ height: 100%;
1800
+ width: 100%;
1801
+ justify-content: center;
1802
+ display: flex;
1803
+ margin-bottom: 78px;
1804
+ }
1805
+
1806
+ .playback-button {
1807
+ position: absolute;
1808
+ bottom: 0;
1809
+ left: 0;
1810
+ width: 100%;
1811
+ height: 94%;
1812
+ display: flex;
1813
+ flex-flow: column wrap;
1814
+ justify-content: center;
1815
+ align-items: center;
1816
+ background-color: transparent;
1817
+
1818
+ .playback-wrapper {
1819
+ height: 64px;
1820
+ width: 64px;
1821
+ display: flex;
1822
+ justify-content: center;
1823
+ align-items: center;
1824
+ border-radius: 50%;
1825
+
1826
+ svg {
1827
+ height: 32px;
1828
+ width: 26.29px;
1829
+ &.play-icon {
1830
+ margin-left: 11%;
1831
+ }
1832
+ }
1833
+ }
1834
+ }
1835
+ //Wrapper
1836
+ .pb-wrapper {
1837
+ width: 100%;
1838
+ display: flex;
1839
+ flex-direction: column;
1840
+ position: absolute;
1841
+ bottom: -78px;
1842
+ opacity: 0;
1843
+ z-index: 2;
1844
+
1845
+ &.show-controls {
1846
+ opacity: 1;
1847
+ transition: opacity 0.35s ease-in-out;
1848
+ }
1849
+
1850
+ //Wrapper timeline
1851
+ .pb-timeline {
1852
+ --progress-bar-height: 6px;
1853
+ position: relative;
1854
+ width: 100%;
1855
+ height: var(--progress-bar-height);
1856
+ .progress-area {
1857
+ width: 100%;
1858
+ height: 400%; //Add contact surface of click on progressBar
1859
+ position: relative;
1860
+ top: -150%;
1861
+ display: flex;
1862
+ cursor: pointer;
1863
+ align-items: center;
1864
+ z-index: 8; //z-index on top of the video elm
1865
+ &:hover {
1866
+ .seek-tooltip {
1867
+ opacity: 1;
1868
+ }
1869
+ }
1870
+
1871
+ //Progress bar
1872
+ .pb-progress-bar {
1873
+ --progress-bar-border-height: 1px;
1874
+ cursor: pointer;
1875
+ width: 100%;
1876
+ height: var(--progress-bar-height);
1877
+ //background-color: transparent;
1878
+ overflow: visible;
1879
+ &:focus {
1880
+ .progress-thumb {
1881
+ }
1882
+ }
1883
+ }
1884
+
1885
+ .progress-indicator {
1886
+ width: 0;
1887
+ height: 100%;
1888
+ position: relative;
1889
+ user-select: none;
1890
+
1891
+ &.progress-animation {
1892
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
1893
+ transition-duration: 0.01s;
1894
+ transition-property: all;
1895
+ transition: width 0.1s linear;
1896
+ }
1897
+ }
1898
+ //Custom thumb
1899
+ .progress-thumb {
1900
+ //Cliquable thumb
1901
+ --progress-thumb-size: 30px;
1902
+ width: var(--progress-thumb-size);
1903
+ height: var(--progress-thumb-size);
1904
+ position: absolute;
1905
+
1906
+ right: calc(var(--progress-thumb-size) / -2);
1907
+ top: calc(
1908
+ -1 *
1909
+ (
1910
+ var(--progress-thumb-size) / 2 - var(--progress-bar-height) / 2
1911
+ ) - var(--progress-bar-border-height) /
1912
+ 2
1913
+ );
1914
+ display: flex;
1915
+ align-items: center;
1916
+ justify-content: center;
1917
+ &::after {
1918
+ content: '';
1919
+ //Visible thumb
1920
+ height: 16px;
1921
+ width: 16px;
1922
+ //Colors and styling
1923
+ border-radius: 50%;
1924
+ //background-color: var(--primary700);
1925
+ }
1926
+ }
1927
+
1928
+ &:hover {
1929
+ .seek-tooltip {
1930
+ //tooltip visible only when hovering progress-area
1931
+ opacity: 1;
1932
+ }
1933
+ }
1934
+
1935
+ .seek-tooltip {
1936
+ position: absolute;
1937
+ top: -30px;
1938
+ opacity: 0;
1939
+ user-select: none;
1940
+ border-radius: 1px;
1941
+ padding: 2px;
1942
+ }
1943
+ }
1944
+ }
1945
+
1946
+ //Controls
1947
+ .pb-controls {
1948
+ padding: 12px 32px;
1949
+ @media screen and (max-width: 800px) {
1950
+ padding: 12px 12px;
1951
+ }
1952
+ & {
1953
+ width: 100%;
1954
+ //height: 76px;
1955
+ height: auto;
1956
+ display: flex;
1957
+ flex-flow: row wrap;
1958
+ justify-content: space-between;
1959
+ -webkit-justify-content: space-between;
1960
+ align-items: center;
1961
+ }
1962
+ .pb-areas {
1963
+ display: flex;
1964
+ flex-flow: row nowrap;
1965
+ justify-content: center;
1966
+ align-items: center;
1967
+
1968
+ &.right {
1969
+ .btn {
1970
+ margin-right: 24px;
1971
+
1972
+ &:last-child {
1973
+ margin-right: 0;
1974
+ }
1975
+ }
1976
+ }
1977
+
1978
+ .btn {
1979
+ position: relative;
1980
+
1981
+ &::before {
1982
+ content: attr(data-title);
1983
+ position: absolute;
1984
+ top: -50px;
1985
+ right: 0;
1986
+ margin-left: -35px;
1987
+ word-break: keep-all;
1988
+ white-space: pre;
1989
+ display: none;
1990
+ opacity: 0;
1991
+ width: fit-content;
1992
+ border-radius: 1px;
1993
+ padding: 2px;
1994
+ }
1995
+ //Icons btn specific
1996
+ &.playbar-btn {
1997
+ &::before {
1998
+ left: 0 !important;
1999
+ margin-left: 0 !important;
2000
+ }
2001
+ & {
2002
+ margin-right: 16px;
2003
+ border-radius: 50%;
2004
+ }
2005
+ &:hover {
2006
+ &::before {
2007
+ display: block;
2008
+ opacity: 1;
2009
+ }
2010
+ }
2011
+ @media screen and (max-width: 800px) {
2012
+ height: 32px;
2013
+ width: 32px;
2014
+ }
2015
+ svg.play-icon {
2016
+ margin-left: 11.875%; // 11.8% off center on the design
2017
+ }
2018
+ }
2019
+ &.volume-btn {
2020
+ margin: 0 20px;
2021
+ svg.volume-low-icon {
2022
+ height: 17.75px;
2023
+ }
2024
+ }
2025
+ }
2026
+ //Manage displayLabel (dynamic class)
2027
+ button.displayLabel {
2028
+ &:hover,
2029
+ &:focus {
2030
+ &::before {
2031
+ display: block;
2032
+ opacity: 1;
2033
+ }
2034
+ }
2035
+ }
2036
+
2037
+ .pb-timer {
2038
+ width: max-content;
2039
+ }
2040
+ //Volume
2041
+ .volume-controls {
2042
+ --volumecontrols-height: 22px;
2043
+ --volume-thumb-size: 16px;
2044
+ display: flex;
2045
+ flex-flow: row wrap;
2046
+ justify-content: center;
2047
+ position: relative;
2048
+ height: var(--volumecontrols-height);
2049
+ &:hover {
2050
+ //button title //DisplayLabel is a dynamic class
2051
+ .volume-slider.displayLabel + span {
2052
+ display: block;
2053
+ opacity: 1;
2054
+ }
2055
+ }
2056
+ .volume-slider {
2057
+ -webkit-appearance: none;
2058
+ width: 100px;
2059
+ //Responsive
2060
+ @media screen and (max-width: 800px) {
2061
+ width: 80px;
2062
+ }
2063
+ & {
2064
+ border-radius: 24px;
2065
+ cursor: pointer;
2066
+ position: relative;
2067
+ z-index: 2;
2068
+ }
2069
+ &::-webkit-slider-runnable-track {
2070
+ background: transparent;
2071
+ }
2072
+ //Input slider thumb
2073
+ @mixin volume-thumb {
2074
+ -webkit-appearance: none;
2075
+ width: var(--volume-thumb-size);
2076
+ height: var(--volume-thumb-size);
2077
+ margin-top: 12px;
2078
+ border-radius: 50%;
2079
+ position: relative;
2080
+ cursor: pointer;
2081
+ }
2082
+ &::-webkit-slider-thumb {
2083
+ @include volume-thumb;
2084
+ }
2085
+ //button title
2086
+ & + span {
2087
+ position: absolute;
2088
+ top: -62px;
2089
+ right: 0;
2090
+ margin-left: -35px;
2091
+ word-break: keep-all;
2092
+ white-space: pre;
2093
+ display: none;
2094
+ opacity: 0;
2095
+ user-select: none;
2096
+ //Colors
2097
+ //background-color: var(--primary100);
2098
+ //color: var(--primary700);
2099
+ border-radius: 1px;
2100
+ padding: 2px;
2101
+ }
2102
+ //DisplayLabel is a dynamic class
2103
+ &.displayLabel:focus {
2104
+ & + span {
2105
+ display: block;
2106
+ opacity: 1;
2107
+ }
2108
+ }
2109
+ }
2110
+
2111
+ .volume-progress {
2112
+ --volumeIndicator-height: 9px;
2113
+ height: var(--volumeIndicator-height);
2114
+ top: calc(
2115
+ var(--volumecontrols-height) / 2 - var(--volumeIndicator-height) / 2
2116
+ ); //centrer en hauteur
2117
+ width: calc(
2118
+ 100% - var(--volume-thumb-size) / 2
2119
+ ); //Width smaller than the range width to be sure the progress width don't exceed the range width
2120
+ position: absolute;
2121
+ border-radius: 20px;
2122
+ background-repeat: no-repeat;
2123
+ background-size: var(--background-size-vs, 0%) 100%; //Js dynamic variable
2124
+ //colors
2125
+ }
2126
+ }
2127
+ }
2128
+ }
2129
+
2130
+ svg {
2131
+ //width: 19px;
2132
+ //height: 19px;
2133
+ //colors
2134
+ //stroke: var(--primary700);
2135
+ }
2136
+
2137
+ button {
2138
+ height: 48px;
2139
+ width: 48px;
2140
+ background-color: transparent;
2141
+ padding: 0;
2142
+ display: flex;
2143
+ justify-content: center;
2144
+ align-items: center;
2145
+ svg {
2146
+ height: 24px;
2147
+ @media screen and (max-width: 800px) {
2148
+ height: 18px;
2149
+ }
2150
+ }
2151
+ }
2152
+ }
2153
+
2154
+ //CSS for audio only
2155
+ .audio.pb-wrapper {
2156
+ border-radius: 4px;
2157
+ position: initial;
2158
+ .pb-controls {
2159
+ padding: 8px 16px;
2160
+ .volume-controls {
2161
+ margin-left: 8px;
2162
+ width: 100%;
2163
+ --volume-thumb-size: 12px;
2164
+ .volume-slider {
2165
+ width: 50px;
2166
+ }
2167
+ .volume-progress {
2168
+ --volumeIndicator-height: 6px;
2169
+ }
2170
+ }
2171
+ }
2172
+ .pb-timer {
2173
+ margin-right: 20px;
2174
+ }
2175
+ .pb-timeline {
2176
+ min-width: 50px;
2177
+
2178
+ @media screen and (max-width: 1000px) {
2179
+ width: 80%;
2180
+ min-width: 50px;
2181
+ margin: 0 auto;
2182
+ }
2183
+
2184
+ .progress-area {
2185
+ justify-content: center;
2186
+ }
2187
+ .pb-progress-bar {
2188
+ border-radius: 4px;
2189
+ //max-width: 90%;
2190
+ }
2191
+ }
2192
+ .volume-btn {
2193
+ margin-left: 16px;
2194
+ }
2195
+ .pb-areas {
2196
+ width: 100%;
2197
+ display: grid;
2198
+ grid-template-columns:
2199
+ min-content
2200
+ min-content
2201
+ 1fr
2202
+ min-content
2203
+ min-content;
2204
+ align-items: center;
2205
+ justify-content: center;
2206
+ button {
2207
+ height: 24px;
2208
+ width: 24px;
2209
+ &.playbar-btn {
2210
+ margin-right: 8px;
2211
+ }
2212
+ svg {
2213
+ height: 12.25px;
2214
+ @media screen and (max-width: 800px) {
2215
+ height: 18px;
2216
+ }
2217
+ }
2218
+ &.volume-btn {
2219
+ svg {
2220
+ height: 16px;
2221
+ }
2222
+ svg.volume-low-icon {
2223
+ height: 14px;
2224
+ }
2225
+ }
2226
+ }
2227
+ }
2228
+ }
2229
+ //Playback button video (to finish)
2230
+ .playback-button {
2231
+ .playback-wrapper {
2232
+ opacity: 0;
2233
+ }
2234
+ &.initial {
2235
+ .playback-wrapper {
2236
+ animation-name: initial;
2237
+ opacity: 1;
2238
+ }
2239
+ }
2240
+ &.playback-animation {
2241
+ .playback-wrapper {
2242
+ opacity: 0;
2243
+ animation-name: handlePlayBack;
2244
+ animation-duration: 0.8s;
2245
+ animation-timing-function: ease-in-out;
2246
+ }
2247
+ }
2248
+ }
2249
+
2250
+ .audio-media-player {
2251
+ .pb-areas {
2252
+ .btn {
2253
+ &::before {
2254
+ top: inherit !important;
2255
+ bottom: -50px;
2256
+ }
2257
+ }
2258
+ }
2259
+ }
2260
+
2261
+ .FS {
2262
+ &:fullscreen {
2263
+ .pb-wrapper {
2264
+ bottom: -45px;
2265
+ }
2266
+ }
2267
+ }
2268
+
2269
+ @keyframes handlePlayBack {
2270
+ 0% {
2271
+ opacity: 0;
2272
+ }
2273
+ 50% {
2274
+ opacity: 1;
2275
+ transform: scale(0.95);
2276
+ }
2277
+ 100% {
2278
+ opacity: 0;
2279
+ }
2280
+ }
2281
+
2282
+ @keyframes disappear {
2283
+ 0% {
2284
+ opacity: 1;
2285
+ }
2286
+ 100% {
2287
+ opacity: 0;
2288
+ }
2289
+ }
2290
+ </style>