wai-website-theme 1.3.1 → 1.4

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 (61) hide show
  1. checksums.yaml +4 -4
  2. data/_includes/different.html +2 -1
  3. data/_includes/external.html +2 -1
  4. data/_includes/header.html +2 -1
  5. data/_includes/menuitem.html +6 -2
  6. data/_includes/peoplelist.html +21 -0
  7. data/_includes/prevnext-navigation.html +56 -0
  8. data/_includes/{prevnext.html → prevnext-order.html} +9 -0
  9. data/_includes/translation-note-msg.html +5 -3
  10. data/_includes/video-player.html +2 -2
  11. data/_layouts/default.html +8 -1
  12. data/_layouts/news.html +7 -1
  13. data/_layouts/policy.html +7 -1
  14. data/_layouts/sidenav.html +8 -1
  15. data/_layouts/sidenavsidebar.html +8 -1
  16. data/assets/ableplayer/Gruntfile.js +2 -1
  17. data/assets/ableplayer/README.md +158 -85
  18. data/assets/ableplayer/build/ableplayer.dist.js +15445 -13823
  19. data/assets/ableplayer/build/ableplayer.js +15445 -13823
  20. data/assets/ableplayer/build/ableplayer.min.css +1 -2
  21. data/assets/ableplayer/build/ableplayer.min.js +3 -10
  22. data/assets/ableplayer/package-lock.json +944 -346
  23. data/assets/ableplayer/package.json +8 -8
  24. data/assets/ableplayer/scripts/ableplayer-base.js +515 -524
  25. data/assets/ableplayer/scripts/browser.js +158 -158
  26. data/assets/ableplayer/scripts/buildplayer.js +1750 -1682
  27. data/assets/ableplayer/scripts/caption.js +424 -401
  28. data/assets/ableplayer/scripts/chapters.js +259 -259
  29. data/assets/ableplayer/scripts/control.js +1831 -1594
  30. data/assets/ableplayer/scripts/description.js +333 -256
  31. data/assets/ableplayer/scripts/dialog.js +145 -145
  32. data/assets/ableplayer/scripts/dragdrop.js +746 -749
  33. data/assets/ableplayer/scripts/event.js +875 -696
  34. data/assets/ableplayer/scripts/initialize.js +819 -912
  35. data/assets/ableplayer/scripts/langs.js +979 -743
  36. data/assets/ableplayer/scripts/metadata.js +124 -124
  37. data/assets/ableplayer/scripts/misc.js +170 -137
  38. data/assets/ableplayer/scripts/preference.js +904 -904
  39. data/assets/ableplayer/scripts/search.js +172 -172
  40. data/assets/ableplayer/scripts/sign.js +82 -78
  41. data/assets/ableplayer/scripts/slider.js +449 -448
  42. data/assets/ableplayer/scripts/track.js +409 -309
  43. data/assets/ableplayer/scripts/transcript.js +684 -595
  44. data/assets/ableplayer/scripts/translation.js +63 -67
  45. data/assets/ableplayer/scripts/ttml2webvtt.js +85 -85
  46. data/assets/ableplayer/scripts/vimeo.js +448 -0
  47. data/assets/ableplayer/scripts/volume.js +395 -380
  48. data/assets/ableplayer/scripts/vts.js +1077 -1077
  49. data/assets/ableplayer/scripts/webvtt.js +766 -763
  50. data/assets/ableplayer/scripts/youtube.js +695 -478
  51. data/assets/ableplayer/styles/ableplayer.css +54 -46
  52. data/assets/ableplayer/translations/nl.js +54 -54
  53. data/assets/ableplayer/translations/pt-br.js +311 -0
  54. data/assets/ableplayer/translations/tr.js +311 -0
  55. data/assets/ableplayer/translations/zh-tw.js +1 -1
  56. data/assets/css/style.css +1 -1
  57. data/assets/css/style.css.map +1 -1
  58. data/assets/images/icons.svg +5 -5
  59. data/assets/scripts/main.js +7 -0
  60. data/assets/search/tipuesearch.js +3 -3
  61. metadata +8 -3
@@ -1,1597 +1,1834 @@
1
1
  (function ($) {
2
- AblePlayer.prototype.seekTo = function (newTime) {
3
-
4
- // define variables to be used for analytics
5
- // e.g., to measure the extent to which users seek back and forward
6
- this.seekFromTime = this.media.currentTime;
7
- this.seekToTime = newTime;
8
-
9
- this.seeking = true;
10
- this.liveUpdatePending = true;
11
-
12
- if (this.player === 'html5') {
13
- var seekable;
14
-
15
- this.startTime = newTime;
16
- // Check HTML5 media "seekable" property to be sure media is seekable to startTime
17
- seekable = this.media.seekable;
18
- if (seekable.length > 0 && this.startTime >= seekable.start(0) && this.startTime <= seekable.end(0)) {
19
- // ok to seek to startTime
20
- // canplaythrough will be triggered when seeking is complete
21
- // this.seeking will be set to false at that point
22
- this.media.currentTime = this.startTime;
23
- if (this.hasSignLanguage && this.signVideo) {
24
- // keep sign languge video in sync
25
- this.signVideo.currentTime = this.startTime;
26
- }
27
- }
28
- }
29
- else if (this.player === 'jw' && this.jwPlayer) {
30
- // pause JW Player temporarily.
31
- // When seek has successfully reached newTime,
32
- // onSeek event will be called, and playback will be resumed
33
- this.jwSeekPause = true;
34
- this.jwPlayer.seek(newTime);
35
- }
36
- else if (this.player === 'youtube') {
37
- this.youTubePlayer.seekTo(newTime,true);
38
- if (newTime > 0) {
39
- if (typeof this.$posterImg !== 'undefined') {
40
- this.$posterImg.hide();
41
- }
42
- }
43
- }
44
- this.refreshControls();
45
- };
46
-
47
- AblePlayer.prototype.getDuration = function () {
48
-
49
- var duration;
50
- if (this.player === 'html5') {
51
- duration = this.media.duration;
52
- }
53
- else if (this.player === 'jw' && this.jwPlayer) {
54
- duration = this.jwPlayer.getDuration();
55
- }
56
- else if (this.player === 'youtube' && this.youTubePlayer) {
57
- duration = this.youTubePlayer.getDuration();
58
- }
59
- if (duration === undefined || isNaN(duration) || duration === -1) {
60
- return 0;
61
- }
62
- return duration;
63
- };
64
-
65
- AblePlayer.prototype.getElapsed = function () {
66
- var position;
67
- if (this.player === 'html5') {
68
- position = this.media.currentTime;
69
- }
70
- else if (this.player === 'jw' && this.jwPlayer) {
71
- if (this.jwPlayer.getState() === 'IDLE') {
72
- return 0;
73
- }
74
- position = this.jwPlayer.getPosition();
75
- }
76
- else if (this.player === 'youtube') {
77
- if (this.youTubePlayer) {
78
- position = this.youTubePlayer.getCurrentTime();
79
- }
80
- }
81
-
82
- if (position === undefined || isNaN(position) || position === -1) {
83
- return 0;
84
- }
85
- return position;
86
- };
87
-
88
- // Returns one of the following states:
89
- // 'stopped' - Not yet played for the first time, or otherwise reset to unplayed.
90
- // 'ended' - Finished playing.
91
- // 'paused' - Not playing, but not stopped or ended.
92
- // 'buffering' - Momentarily paused to load, but will resume once data is loaded.
93
- // 'playing' - Currently playing.
94
- AblePlayer.prototype.getPlayerState = function () {
95
- if (this.swappingSrc) {
96
- return;
97
- }
98
- if (this.player === 'html5') {
99
- if (this.media.paused) {
100
- if (this.getElapsed() === 0) {
101
- return 'stopped';
102
- }
103
- else if (this.media.ended) {
104
- return 'ended';
105
- }
106
- else {
107
- return 'paused';
108
- }
109
- }
110
- else if (this.media.readyState !== 4) {
111
- return 'buffering';
112
- }
113
- else {
114
- return 'playing';
115
- }
116
- }
117
- else if (this.player === 'jw' && this.jwPlayer) {
118
- if (this.jwPlayer.getState() === 'PAUSED' || this.jwPlayer.getState() === 'IDLE' || this.jwPlayer.getState() === undefined) {
119
-
120
- if (this.getElapsed() === 0) {
121
- return 'stopped';
122
- }
123
- else if (this.getElapsed() === this.getDuration()) {
124
- return 'ended';
125
- }
126
- else {
127
- return 'paused';
128
- }
129
- }
130
- else if (this.jwPlayer.getState() === 'BUFFERING') {
131
- return 'buffering';
132
- }
133
- else if (this.jwPlayer.getState() === 'PLAYING') {
134
- return 'playing';
135
- }
136
- }
137
- else if (this.player === 'youtube' && this.youTubePlayer) {
138
- var state = this.youTubePlayer.getPlayerState();
139
- if (state === -1 || state === 5) {
140
- return 'stopped';
141
- }
142
- else if (state === 0) {
143
- return 'ended';
144
- }
145
- else if (state === 1) {
146
- return 'playing';
147
- }
148
- else if (state === 2) {
149
- return 'paused';
150
- }
151
- else if (state === 3) {
152
- return 'buffering';
153
- }
154
- }
155
- };
156
-
157
- AblePlayer.prototype.isPlaybackRateSupported = function () {
158
- if (this.player === 'html5') {
159
- return this.media.playbackRate ? true : false;
160
- }
161
- else if (this.player === 'jw' && this.jwPlayer) {
162
- // Not directly supported by JW player; can hack for HTML5 version by finding the dynamically generated video tag, but decided not to do that.
163
- return false;
164
- }
165
- else if (this.player === 'youtube') {
166
- // Youtube always supports a finite list of playback rates. Only expose controls if more than one is available.
167
- return (this.youTubePlayer.getAvailablePlaybackRates().length > 1);
168
- }
169
- };
170
-
171
- AblePlayer.prototype.setPlaybackRate = function (rate) {
172
- rate = Math.max(0.5, rate);
173
- if (this.player === 'html5') {
174
- this.media.playbackRate = rate;
175
- }
176
- else if (this.player === 'youtube') {
177
- this.youTubePlayer.setPlaybackRate(rate);
178
- }
179
- if (this.hasSignLanguage && this.signVideo) {
180
- this.signVideo.playbackRate = rate;
181
- }
182
- this.$speed.text(this.tt.speed + ': ' + rate.toFixed(2).toString() + 'x');
183
- };
184
-
185
- AblePlayer.prototype.getPlaybackRate = function () {
186
- if (this.player === 'html5') {
187
- return this.media.playbackRate;
188
- }
189
- else if (this.player === 'jw' && this.jwPlayer) {
190
- // Unsupported, always the normal rate.
191
- return 1;
192
- }
193
- else if (this.player === 'youtube') {
194
- return this.youTubePlayer.getPlaybackRate();
195
- }
196
- };
197
-
198
- // Note there are three player states that count as paused in this sense,
199
- // and one of them is named 'paused'.
200
- // A better name would be 'isCurrentlyNotPlayingOrBuffering'
201
- AblePlayer.prototype.isPaused = function () {
202
- var state = this.getPlayerState();
203
- return state === 'paused' || state === 'stopped' || state === 'ended';
204
- };
205
-
206
- AblePlayer.prototype.pauseMedia = function () {
207
- if (this.player === 'html5') {
208
- this.media.pause(true);
209
- if (this.hasSignLanguage && this.signVideo) {
210
- this.signVideo.pause(true);
211
- }
212
- }
213
- else if (this.player === 'jw' && this.jwPlayer) {
214
- this.jwPlayer.pause(true);
215
- }
216
- else if (this.player === 'youtube') {
217
- this.youTubePlayer.pauseVideo();
218
- }
219
- };
220
-
221
- AblePlayer.prototype.playMedia = function () {
222
-
223
- var thisObj = this;
224
-
225
- if (this.player === 'html5') {
226
- this.media.play(true);
227
- if (this.hasSignLanguage && this.signVideo) {
228
- this.signVideo.play(true);
229
- }
230
- }
231
- else if (this.player === 'jw' && this.jwPlayer) {
232
- this.jwPlayer.play(true);
233
- }
234
- else if (this.player === 'youtube') {
235
- this.youTubePlayer.playVideo();
236
- if (typeof this.$posterImg !== 'undefined') {
237
- this.$posterImg.hide();
238
- }
239
- this.stoppingYouTube = false;
240
- }
241
- this.startedPlaying = true;
242
- if (this.hideControls) {
243
- // wait briefly after playback begins, then hide controls
244
- this.hidingControls = true;
245
- this.hideControlsTimeout = window.setTimeout(function() {
246
- thisObj.fadeControls('out');
247
- thisObj.controlsHidden = true;
248
- thisObj.hidingControls = false;
249
- },2000);
250
- }
251
- };
252
-
253
- AblePlayer.prototype.fadeControls = function(direction) {
254
-
255
- // NOTE: This is a work in progress, and is not yet fully functional
256
- // TODO: Use jQuery fadeIn() and fadeOut() to attain some sort of transition
257
- // Currently just adds or removes able-offscreen class to visibly hide content
258
- // without hiding it from screen reader users
259
-
260
- // direction is either 'out' or 'in'
261
-
262
- // One challenge:
263
- // When controls fade out in other players (e.g., YouTube, Vimeo), the transition works well because
264
- // their controls are an overlay on top of the video.
265
- // Therefore, disappearing controls don't affect the size of the video container.
266
- // Able Player's controls appear below the video, so if this.$playerDiv disappears,
267
- // that results in a reduction in the height of the video container, which is a bit jarring
268
- // Solution #1: Don't hide this.$playerDiv; instead hide the two containers nested inside it
269
- if (direction == 'out') {
270
- this.$controllerDiv.addClass('able-offscreen');
271
- this.$statusBarDiv.addClass('able-offscreen');
272
- // Removing content from $playerDiv leaves an empty controller bar in its place
273
- // What to do with the empty space?
274
- // For now, changing to a black background; will restore to original background on fade-in
275
- this.playerBackground = this.$playerDiv.css('background-color');
276
- this.$playerDiv.css('background-color','black');
277
- }
278
- else if (direction == 'in') {
279
- this.$controllerDiv.removeClass('able-offscreen');
280
- this.$statusBarDiv.removeClass('able-offscreen');
281
- if (typeof this.playerBackground !== 'undefined') {
282
- this.$playerDiv.css('background-color',this.playerBackground);
283
- }
284
- else {
285
- this.$playerDiv.css('background-color','');
286
- }
287
- }
288
- };
289
-
290
- AblePlayer.prototype.refreshControls = function() {
291
-
292
- var thisObj, duration, elapsed, lastChapterIndex, displayElapsed,
293
- updateLive, textByState, timestamp, widthUsed,
294
- leftControls, rightControls, seekbarWidth, seekbarSpacer, captionsCount,
295
- buffered, newTop, statusBarHeight, speedHeight, statusBarWidthBreakpoint,
296
- newSvgData;
297
-
298
- thisObj = this;
299
- if (this.swappingSrc) {
300
- // wait until new source has loaded before refreshing controls
301
- return;
302
- }
303
-
304
- duration = this.getDuration();
305
- elapsed = this.getElapsed();
306
-
307
- if (this.useChapterTimes) {
308
- this.chapterDuration = this.getChapterDuration();
309
- this.chapterElapsed = this.getChapterElapsed();
310
- }
311
-
312
- if (this.useFixedSeekInterval === false && this.seekIntervalCalculated === false && duration > 0) {
313
- // couldn't calculate seekInterval previously; try again.
314
- this.setSeekInterval();
315
- }
316
-
317
- if (this.seekBar) {
318
-
319
- if (this.useChapterTimes) {
320
- lastChapterIndex = this.selectedChapters.cues.length-1;
321
- if (this.selectedChapters.cues[lastChapterIndex] == this.currentChapter) {
322
- // this is the last chapter
323
- if (this.currentChapter.end !== duration) {
324
- // chapter ends before or after video ends
325
- // need to adjust seekbar duration to match video end
326
- this.seekBar.setDuration(duration - this.currentChapter.start);
327
- }
328
- else {
329
- this.seekBar.setDuration(this.chapterDuration);
330
- }
331
- }
332
- else {
333
- // this is not the last chapter
334
- this.seekBar.setDuration(this.chapterDuration);
335
- }
336
- }
337
- else {
338
- this.seekBar.setDuration(duration);
339
- }
340
- if (!(this.seekBar.tracking)) {
341
- // Only update the aria live region if we have an update pending (from a
342
- // seek button control) or if the seekBar has focus.
343
- // We use document.activeElement instead of $(':focus') due to a strange bug:
344
- // When the seekHead element is focused, .is(':focus') is failing and $(':focus') is returning an undefined element.
345
- updateLive = this.liveUpdatePending || this.seekBar.seekHead.is($(document.activeElement));
346
- this.liveUpdatePending = false;
347
- if (this.useChapterTimes) {
348
- this.seekBar.setPosition(this.chapterElapsed, updateLive);
349
- }
350
- else {
351
- this.seekBar.setPosition(elapsed, updateLive);
352
- }
353
- }
354
-
355
- // When seeking, display the seek bar time instead of the actual elapsed time.
356
- if (this.seekBar.tracking) {
357
- displayElapsed = this.seekBar.lastTrackPosition;
358
- }
359
- else {
360
- if (this.useChapterTimes) {
361
- displayElapsed = this.chapterElapsed;
362
- }
363
- else {
364
- displayElapsed = elapsed;
365
- }
366
- }
367
- }
368
- if (this.useChapterTimes) {
369
- this.$durationContainer.text(' / ' + this.formatSecondsAsColonTime(this.chapterDuration));
370
- }
371
- else {
372
- this.$durationContainer.text(' / ' + this.formatSecondsAsColonTime(duration));
373
- }
374
- this.$elapsedTimeContainer.text(this.formatSecondsAsColonTime(displayElapsed));
375
-
376
- textByState = {
377
- 'stopped': this.tt.statusStopped,
378
- 'paused': this.tt.statusPaused,
379
- 'playing': this.tt.statusPlaying,
380
- 'buffering': this.tt.statusBuffering,
381
- 'ended': this.tt.statusEnd
382
- };
383
-
384
- if (this.stoppingYouTube) {
385
- // stoppingYouTube is true temporarily while video is paused and seeking to 0
386
- // See notes in handleRestart()
387
- // this.stoppingYouTube will be reset when seek to 0 is finished (in event.js > onMediaUpdateTime())
388
- if (this.$status.text() !== this.tt.statusStopped) {
389
- this.$status.text(this.tt.statusStopped);
390
- }
391
- if (this.$playpauseButton.find('span').first().hasClass('icon-pause')) {
392
- if (this.iconType === 'font') {
393
- this.$playpauseButton.find('span').first().removeClass('icon-pause').addClass('icon-play');
394
- this.$playpauseButton.find('span.able-clipped').text(this.tt.play);
395
- }
396
- else if (this.iconType === 'svg') {
397
- newSvgData = this.getSvgData('play');
398
- this.$playpauseButton.find('svg').attr('viewBox',newSvgData[0]);
399
- this.$playpauseButton.find('path').attr('d',newSvgData[1]);
400
- }
401
- else {
402
- this.$playpauseButton.find('img').attr('src',this.playButtonImg);
403
- }
404
- }
405
- }
406
- else {
407
- if (typeof this.$status !== 'undefined' && typeof this.seekBar !== 'undefined') {
408
- // Update the text only if it's changed since it has role="alert";
409
- // also don't update while tracking, since this may Pause/Play the player but we don't want to send a Pause/Play update.
410
- if (this.$status.text() !== textByState[this.getPlayerState()] && !this.seekBar.tracking) {
411
- // Debounce updates; only update after status has stayed steadily different for 250ms.
412
- timestamp = (new Date()).getTime();
413
- if (!this.statusDebounceStart) {
414
- this.statusDebounceStart = timestamp;
415
- // Make sure refreshControls gets called again at the appropriate time to check.
416
- this.statusTimeout = setTimeout(function () {
417
- thisObj.refreshControls();
418
- }, 300);
419
- }
420
- else if ((timestamp - this.statusDebounceStart) > 250) {
421
- this.$status.text(textByState[this.getPlayerState()]);
422
- this.statusDebounceStart = null;
423
- clearTimeout(this.statusTimeout);
424
- this.statusTimeout = null;
425
- }
426
- }
427
- else {
428
- this.statusDebounceStart = null;
429
- clearTimeout(this.statusTimeout);
430
- this.statusTimeout = null;
431
- }
432
-
433
- // Don't change play/pause button display while using the seek bar (or if YouTube stopped)
434
- if (!this.seekBar.tracking && !this.stoppingYouTube) {
435
- if (this.isPaused()) {
436
- this.$playpauseButton.attr('aria-label',this.tt.play);
437
-
438
- if (this.iconType === 'font') {
439
- this.$playpauseButton.find('span').first().removeClass('icon-pause').addClass('icon-play');
440
- this.$playpauseButton.find('span.able-clipped').text(this.tt.play);
441
- }
442
- else if (this.iconType === 'svg') {
443
- newSvgData = this.getSvgData('play');
444
- this.$playpauseButton.find('svg').attr('viewBox',newSvgData[0]);
445
- this.$playpauseButton.find('path').attr('d',newSvgData[1]);
446
- }
447
- else {
448
- this.$playpauseButton.find('img').attr('src',this.playButtonImg);
449
- }
450
- }
451
- else {
452
- this.$playpauseButton.attr('aria-label',this.tt.pause);
453
-
454
- if (this.iconType === 'font') {
455
- this.$playpauseButton.find('span').first().removeClass('icon-play').addClass('icon-pause');
456
- this.$playpauseButton.find('span.able-clipped').text(this.tt.pause);
457
- }
458
- else if (this.iconType === 'svg') {
459
- newSvgData = this.getSvgData('pause');
460
- this.$playpauseButton.find('svg').attr('viewBox',newSvgData[0]);
461
- this.$playpauseButton.find('path').attr('d',newSvgData[1]);
462
- }
463
- else {
464
- this.$playpauseButton.find('img').attr('src',this.pauseButtonImg);
465
- }
466
- }
467
- }
468
- }
469
- }
470
-
471
- // Update seekbar width.
472
- // To do this, we need to calculate the width of all buttons surrounding it.
473
- if (this.seekBar) {
474
- widthUsed = 0;
475
- seekbarSpacer = 40; // adjust for discrepancies in browsers' calculated button widths
476
-
477
- leftControls = this.seekBar.wrapperDiv.parent().prev('div.able-left-controls');
478
- rightControls = leftControls.next('div.able-right-controls');
479
- leftControls.children().each(function () {
480
- if ($(this).prop('tagName')=='BUTTON') {
481
- widthUsed += $(this).width();
482
- }
483
- });
484
- rightControls.children().each(function () {
485
- if ($(this).prop('tagName')=='BUTTON') {
486
- widthUsed += $(this).width();
487
- }
488
- });
489
- if (this.isFullscreen()) {
490
- seekbarWidth = $(window).width() - widthUsed - seekbarSpacer;
491
- }
492
- else {
493
- seekbarWidth = this.$ableWrapper.width() - widthUsed - seekbarSpacer;
494
- }
495
- // Sometimes some minor fluctuations based on browser weirdness, so set a threshold.
496
- if (Math.abs(seekbarWidth - this.seekBar.getWidth()) > 5) {
497
- this.seekBar.setWidth(seekbarWidth);
498
- }
499
- }
500
-
501
- // Show/hide status bar content conditionally
502
- if (!this.isFullscreen()) {
503
- statusBarWidthBreakpoint = 300;
504
- statusBarHeight = this.$statusBarDiv.height();
505
- speedHeight = this.$statusBarDiv.find('span.able-speed').height();
506
- if (speedHeight > (statusBarHeight + 5)) {
507
- // speed bar is wrapping (happens often in German player)
508
- this.$statusBarDiv.find('span.able-speed').hide();
509
- this.hidingSpeed = true;
510
- }
511
- else {
512
- if (this.hidingSpeed) {
513
- this.$statusBarDiv.find('span.able-speed').show();
514
- this.hidingSpeed = false;
515
- }
516
- if (this.$statusBarDiv.width() < statusBarWidthBreakpoint) {
517
- // Player is too small for a speed span
518
- this.$statusBarDiv.find('span.able-speed').hide();
519
- this.hidingSpeed = true;
520
- }
521
- else {
522
- if (this.hidingSpeed) {
523
- this.$statusBarDiv.find('span.able-speed').show();
524
- this.hidingSpeed = false;
525
- }
526
- }
527
- }
528
- }
529
-
530
- if (this.$descButton) {
531
- if (this.descOn) {
532
- this.$descButton.removeClass('buttonOff').attr('aria-label',this.tt.turnOffDescriptions);
533
- this.$descButton.find('span.able-clipped').text(this.tt.turnOffDescriptions);
534
- }
535
- else {
536
- this.$descButton.addClass('buttonOff').attr('aria-label',this.tt.turnOnDescriptions);
537
- this.$descButton.find('span.able-clipped').text(this.tt.turnOnDescriptions);
538
- }
539
- }
540
-
541
- if (this.$ccButton) {
542
- if (this.usingYouTubeCaptions) {
543
- captionsCount = this.ytCaptions.length;
544
- }
545
- else {
546
- captionsCount = this.captions.length;
547
- }
548
- // Button has a different title depending on the number of captions.
549
- // If only one caption track, this is "Show captions" and "Hide captions"
550
- // Otherwise, it is just always "Captions"
551
- if (!this.captionsOn) {
552
- this.$ccButton.addClass('buttonOff');
553
- if (captionsCount === 1) {
554
- this.$ccButton.attr('aria-label',this.tt.showCaptions);
555
- this.$ccButton.find('span.able-clipped').text(this.tt.showCaptions);
556
- }
557
- }
558
- else {
559
- this.$ccButton.removeClass('buttonOff');
560
- if (captionsCount === 1) {
561
- this.$ccButton.attr('aria-label',this.tt.hideCaptions);
562
- this.$ccButton.find('span.able-clipped').text(this.tt.hideCaptions);
563
- }
564
- }
565
-
566
- if (captionsCount > 1) {
567
- this.$ccButton.attr({
568
- 'aria-label': this.tt.captions,
569
- 'aria-haspopup': 'true',
570
- 'aria-controls': this.mediaId + '-captions-menu',
571
- 'aria-expanded': 'false'
572
- });
573
- this.$ccButton.find('span.able-clipped').text(this.tt.captions);
574
- }
575
- }
576
-
577
- if (this.$chaptersButton) {
578
- this.$chaptersButton.attr({
579
- 'aria-label': this.tt.chapters,
580
- 'aria-haspopup': 'true',
581
- 'aria-controls': this.mediaId + '-chapters-menu',
582
- 'aria-expanded': 'false'
583
- });
584
- }
585
- if (this.$fullscreenButton) {
586
- if (!this.isFullscreen()) {
587
- this.$fullscreenButton.attr('aria-label', this.tt.enterFullScreen);
588
- if (this.iconType === 'font') {
589
- this.$fullscreenButton.find('span').first().removeClass('icon-fullscreen-collapse').addClass('icon-fullscreen-expand');
590
- this.$fullscreenButton.find('span.able-clipped').text(this.tt.enterFullScreen);
591
- }
592
- else if (this.iconType === 'svg') {
593
- newSvgData = this.getSvgData('fullscreen-expand');
594
- this.$fullscreenButton.find('svg').attr('viewBox',newSvgData[0]);
595
- this.$fullscreenButton.find('path').attr('d',newSvgData[1]);
596
- }
597
- else {
598
- this.$fullscreenButton.find('img').attr('src',this.fullscreenExpandButtonImg);
599
- }
600
- }
601
- else {
602
- this.$fullscreenButton.attr('aria-label',this.tt.exitFullScreen);
603
- if (this.iconType === 'font') {
604
- this.$fullscreenButton.find('span').first().removeClass('icon-fullscreen-expand').addClass('icon-fullscreen-collapse');
605
- this.$fullscreenButton.find('span.able-clipped').text(this.tt.exitFullScreen);
606
- }
607
- else if (this.iconType === 'svg') {
608
- newSvgData = this.getSvgData('fullscreen-collapse');
609
- this.$fullscreenButton.find('svg').attr('viewBox',newSvgData[0]);
610
- this.$fullscreenButton.find('path').attr('d',newSvgData[1]);
611
- }
612
- else {
613
- this.$fullscreenButton.find('img').attr('src',this.fullscreenCollapseButtonImg);
614
- }
615
- }
616
- }
617
-
618
- if (typeof this.$bigPlayButton !== 'undefined') {
619
- // Choose show/hide for big play button and adjust position.
620
- if (this.isPaused() && !this.seekBar.tracking) {
621
- if (!this.hideBigPlayButton) {
622
- this.$bigPlayButton.show();
623
- }
624
- if (this.isFullscreen()) {
625
- this.$bigPlayButton.width($(window).width());
626
- this.$bigPlayButton.height($(window).height());
627
- }
628
- else {
629
- this.$bigPlayButton.width(this.$mediaContainer.width());
630
- this.$bigPlayButton.height(this.$mediaContainer.height());
631
- }
632
- }
633
- else {
634
- this.$bigPlayButton.hide();
635
- }
636
- }
637
-
638
- if (this.transcriptType) {
639
- // Sync checkbox and autoScrollTranscript with user preference
640
- if (this.prefAutoScrollTranscript === 1) {
641
- this.autoScrollTranscript = true;
642
- this.$autoScrollTranscriptCheckbox.prop('checked',true);
643
- }
644
- else {
645
- this.autoScrollTranscript = false;
646
- this.$autoScrollTranscriptCheckbox.prop('checked',false);
647
- }
648
-
649
- // If transcript locked, scroll transcript to current highlight location.
650
- if (this.autoScrollTranscript && this.currentHighlight) {
651
- newTop = Math.floor($('.able-transcript').scrollTop() +
652
- $(this.currentHighlight).position().top -
653
- ($('.able-transcript').height() / 2) +
654
- ($(this.currentHighlight).height() / 2));
655
- if (newTop !== Math.floor($('.able-transcript').scrollTop())) {
656
- // Set a flag to ignore the coming scroll event.
657
- // there's no other way I know of to differentiate programmatic and user-initiated scroll events.
658
- this.scrollingTranscript = true;
659
- $('.able-transcript').scrollTop(newTop);
660
- }
661
- }
662
- }
663
-
664
- // Update buffering progress.
665
- // TODO: Currently only using the first HTML5 buffered interval, but this fails sometimes when buffering is split into two or more intervals.
666
- if (this.player === 'html5') {
667
- if (this.media.buffered.length > 0) {
668
- buffered = this.media.buffered.end(0)
669
- if (this.useChapterTimes) {
670
- if (buffered > this.chapterDuration) {
671
- buffered = this.chapterDuration;
672
- }
673
- if (this.seekBar) {
674
- this.seekBar.setBuffered(buffered / this.chapterDuration);
675
- }
676
- }
677
- else {
678
- if (this.seekBar) {
679
- this.seekBar.setBuffered(buffered / duration);
680
- }
681
- }
682
- }
683
- }
684
- else if (this.player === 'jw' && this.jwPlayer) {
685
- if (this.seekBar) {
686
- this.seekBar.setBuffered(this.jwPlayer.getBuffer() / 100);
687
- }
688
- }
689
- else if (this.player === 'youtube') {
690
- if (this.seekBar) {
691
- this.seekBar.setBuffered(this.youTubePlayer.getVideoLoadedFraction());
692
- }
693
- }
694
- };
695
-
696
- AblePlayer.prototype.getHiddenWidth = function($el) {
697
-
698
- // jQuery returns for width() if element is hidden
699
- // this function is a workaround
700
-
701
- // save a reference to a cloned element that can be measured
702
- var $hiddenElement = $el.clone().appendTo('body');
703
-
704
- // calculate the width of the clone
705
- var width = $hiddenElement.outerWidth();
706
-
707
- // remove the clone from the DOM
708
- $hiddenElement.remove();
709
-
710
- return width;
711
- };
712
-
713
- AblePlayer.prototype.handlePlay = function(e) {
714
- if (this.isPaused()) {
715
- this.playMedia();
716
- }
717
- else {
718
- this.pauseMedia();
719
- }
720
- this.refreshControls();
721
- };
722
-
723
- AblePlayer.prototype.handleRestart = function() {
724
-
725
- this.seekTo(0);
726
-
727
- /*
728
- // Prior to 2.3.68, this function was handleStop()
729
- // which was a bit more challenging to implement
730
- // Preserved here in case Stop is ever cool again...
731
-
732
- var thisObj = this;
733
- if (this.player == 'html5') {
734
- this.pauseMedia();
735
- this.seekTo(0);
736
- }
737
- else if (this.player === 'jw' && this.jwPlayer) {
738
- this.jwPlayer.stop();
739
- }
740
- else if (this.player === 'youtube') {
741
- // YouTube API function stopVideo() does not reset video to 0
742
- // Also, the stopped video is not seekable so seekTo(0) after stopping doesn't work
743
- // Workaround is to use pauseVideo(), followed by seekTo(0) to emulate stopping
744
- // However, the tradeoff is that YouTube doesn't restore the poster image when video is paused
745
- // Added 12/29/15: After seekTo(0) is finished, stopVideo() to reset video and restore poster image
746
- // This final step is handled in event.js > onMediaUpdate()
747
- this.youTubePlayer.pauseVideo();
748
- this.seekTo(0);
749
- this.stoppingYouTube = true;
750
- }
751
- */
752
- this.refreshControls();
753
- };
754
-
755
- AblePlayer.prototype.handleRewind = function() {
756
-
757
- var elapsed, targetTime;
758
-
759
- elapsed = this.getElapsed();
760
- targetTime = elapsed - this.seekInterval;
761
- if (this.useChapterTimes) {
762
- if (targetTime < this.currentChapter.start) {
763
- targetTime = this.currentChapter.start;
764
- }
765
- }
766
- else {
767
- if (targetTime < 0) {
768
- targetTime = 0;
769
- }
770
- }
771
- this.seekTo(targetTime);
772
- };
773
-
774
- AblePlayer.prototype.handleFastForward = function() {
775
-
776
- var elapsed, duration, targetTime, lastChapterIndex;
777
-
778
- elapsed = this.getElapsed();
779
- duration = this.getDuration();
780
- lastChapterIndex = this.chapters.length-1;
781
- targetTime = elapsed + this.seekInterval;
782
-
783
- if (this.useChapterTimes) {
784
- if (this.chapters[lastChapterIndex] == this.currentChapter) {
785
- // this is the last chapter
786
- if (targetTime > duration || targetTime > this.currentChapter.end) {
787
- // targetTime would exceed the end of the video (or chapter)
788
- // scrub to end of whichever is earliest
789
- targetTime = Math.min(duration, this.currentChapter.end);
790
- }
791
- else if (duration % targetTime < this.seekInterval) {
792
- // nothing left but pocket change after seeking to targetTime
793
- // go ahead and seek to end of video (or chapter), whichever is earliest
794
- targetTime = Math.min(duration, this.currentChapter.end);
795
- }
796
- }
797
- else {
798
- // this is not the last chapter
799
- if (targetTime > this.currentChapter.end) {
800
- // targetTime would exceed the end of the chapter
801
- // scrub exactly to end of chapter
802
- targetTime = this.currentChapter.end;
803
- }
804
- }
805
- }
806
- else {
807
- // not using chapter times
808
- if (targetTime > duration) {
809
- targetTime = duration;
810
- }
811
- }
812
- this.seekTo(targetTime);
813
- };
814
-
815
- AblePlayer.prototype.handleRateIncrease = function() {
816
- this.changeRate(1);
817
- };
818
-
819
- AblePlayer.prototype.handleRateDecrease = function() {
820
- this.changeRate(-1);
821
- };
822
-
823
- // Increases or decreases playback rate, where dir is 1 or -1 indication direction.
824
- AblePlayer.prototype.changeRate = function (dir) {
825
- if (this.player === 'html5') {
826
- this.setPlaybackRate(this.getPlaybackRate() + (0.25 * dir));
827
- }
828
- else if (this.player === 'youtube') {
829
- var rates = this.youTubePlayer.getAvailablePlaybackRates();
830
- var currentRate = this.getPlaybackRate();
831
- var index = rates.indexOf(currentRate);
832
- if (index === -1) {
833
- console.log('ERROR: Youtube returning unknown playback rate ' + currentRate.toString());
834
- }
835
- else {
836
- index += dir;
837
- // Can only increase or decrease rate if there's another rate available.
838
- if (index < rates.length && index >= 0) {
839
- this.setPlaybackRate(rates[index]);
840
- }
841
- }
842
- }
843
- };
844
-
845
- AblePlayer.prototype.handleCaptionToggle = function() {
846
-
847
- var captions;
848
-
849
- if (this.hidingPopup) {
850
- // stopgap to prevent spacebar in Firefox from reopening popup
851
- // immediately after closing it
852
- this.hidingPopup = false;
853
- return false;
854
- }
855
- if (this.captions.length) {
856
- captions = this.captions;
857
- }
858
- else if (this.ytCaptions.length) {
859
- captions = this.ytCaptions;
860
- }
861
- else {
862
- captions = [];
863
- }
864
- if (captions.length === 1) {
865
-
866
- // When there's only one set of captions, just do an on/off toggle.
867
- if (this.captionsOn === true) {
868
- // turn them off
869
- this.captionsOn = false;
870
- this.prefCaptions = 0;
871
- this.updateCookie('prefCaptions');
872
- if (this.usingYouTubeCaptions) {
873
- this.youTubePlayer.unloadModule(this.ytCaptionModule);
874
- }
875
- else {
876
- this.$captionsWrapper.hide();
877
- }
878
- }
879
- else {
880
- // captions are off. Turn them on.
881
- this.captionsOn = true;
882
- this.prefCaptions = 1;
883
- this.updateCookie('prefCaptions');
884
- if (this.usingYouTubeCaptions) {
885
- if (typeof this.ytCaptionModule !== 'undefined') {
886
- this.youTubePlayer.loadModule(this.ytCaptionModule);
887
- }
888
- }
889
- else {
890
- this.$captionsWrapper.show();
891
- }
892
- for (var i=0; i<captions.length; i++) {
893
- if (captions[i].def === true) { // this is the default language
894
- this.selectedCaptions = captions[i];
895
- }
896
- }
897
- this.selectedCaptions = this.captions[0];
898
- if (this.descriptions.length >= 0) {
899
- this.selectedDescriptions = this.descriptions[0];
900
- }
901
- }
902
- this.refreshControls();
903
- }
904
- else {
905
- // there is more than one caption track.
906
- // clicking on a track is handled via caption.js > getCaptionClickFunction()
907
- if (this.captionsPopup && this.captionsPopup.is(':visible')) {
908
- this.captionsPopup.hide();
909
- this.hidingPopup = false;
910
- this.$ccButton.attr('aria-expanded','false').focus();
911
- }
912
- else {
913
- this.closePopups();
914
- if (this.captionsPopup) {
915
- this.captionsPopup.show();
916
- this.$ccButton.attr('aria-expanded','true');
917
- this.captionsPopup.css('top', this.$ccButton.position().top - this.captionsPopup.outerHeight());
918
- this.captionsPopup.css('left', this.$ccButton.position().left)
919
- // Place focus on the first button (even if another button is checked)
920
- this.captionsPopup.find('li').removeClass('able-focus');
921
- this.captionsPopup.find('li').first().focus().addClass('able-focus');
922
- }
923
- }
924
- }
925
- };
926
-
927
- AblePlayer.prototype.handleChapters = function () {
928
- if (this.hidingPopup) {
929
- // stopgap to prevent spacebar in Firefox from reopening popup
930
- // immediately after closing it
931
- this.hidingPopup = false;
932
- return false;
933
- }
934
- if (this.chaptersPopup.is(':visible')) {
935
- this.chaptersPopup.hide();
936
- this.hidingPopup = false;
937
- this.$chaptersButton.attr('aria-expanded','false').focus();
938
- }
939
- else {
940
- this.closePopups();
941
- this.chaptersPopup.show();
942
- this.$chaptersButton.attr('aria-expanded','true');
943
- this.chaptersPopup.css('top', this.$chaptersButton.position().top - this.chaptersPopup.outerHeight());
944
- this.chaptersPopup.css('left', this.$chaptersButton.position().left)
945
-
946
- // Highlight the current chapter, if any chapters are checked
947
- // Otherwise, place focus on the first chapter
948
- this.chaptersPopup.find('li').removeClass('able-focus');
949
- if (this.chaptersPopup.find('li[aria-checked="true"]').length) {
950
- this.chaptersPopup.find('li[aria-checked="true"]').focus().addClass('able-focus');
951
- }
952
- else {
953
- this.chaptersPopup.find('li').first().addClass('able-focus').attr('aria-checked','true').focus();
954
- }
955
- }
956
- };
957
-
958
- AblePlayer.prototype.handleDescriptionToggle = function() {
959
- this.descOn = !this.descOn;
960
- this.prefDesc = + this.descOn; // convert boolean to integer
961
- this.updateCookie('prefDesc');
962
- this.refreshingDesc = true;
963
- this.initDescription();
964
- this.refreshControls();
965
- };
966
-
967
- AblePlayer.prototype.handlePrefsClick = function(pref) {
968
- // NOTE: the prefs menu is positioned near the right edge of the player
969
- // This assumes the Prefs button is also positioned in that vicinity
970
- // (last or second-last button the right)
971
-
972
- var prefsButtonPosition, prefsMenuRight, prefsMenuLeft;
973
-
974
- if (this.hidingPopup) {
975
- // stopgap to prevent spacebar in Firefox from reopening popup
976
- // immediately after closing it
977
- this.hidingPopup = false;
978
- return false;
979
- }
980
- if (this.prefsPopup.is(':visible')) {
981
- this.prefsPopup.hide();
982
- this.hidingPopup = false;
983
- this.$prefsButton.attr('aria-expanded','false').focus();
984
- // restore each menu item to original hidden state
985
- this.prefsPopup.find('li').removeClass('able-focus').attr('tabindex','-1');
986
- }
987
- else {
988
- this.closePopups();
989
- this.prefsPopup.show();
990
- this.$prefsButton.attr('aria-expanded','true');
991
- prefsButtonPosition = this.$prefsButton.position();
992
- prefsMenuRight = this.$ableDiv.width() - 5;
993
- prefsMenuLeft = prefsMenuRight - this.prefsPopup.width();
994
- this.prefsPopup.css('top', prefsButtonPosition.top - this.prefsPopup.outerHeight());
995
- this.prefsPopup.css('left', prefsMenuLeft);
996
- // remove prior focus and set focus on first item; also change tabindex from -1 to 0
997
- this.prefsPopup.find('li').removeClass('able-focus').attr('tabindex','0');
998
- this.prefsPopup.find('li').first().focus().addClass('able-focus');
999
- }
1000
- };
1001
-
1002
- AblePlayer.prototype.handleHelpClick = function() {
1003
- this.setFullscreen(false);
1004
- this.helpDialog.show();
1005
- };
1006
-
1007
- AblePlayer.prototype.handleTranscriptToggle = function () {
1008
- if (this.$transcriptDiv.is(':visible')) {
1009
- this.$transcriptArea.hide();
1010
- this.$transcriptButton.addClass('buttonOff').attr('aria-label',this.tt.showTranscript);
1011
- this.$transcriptButton.find('span.able-clipped').text(this.tt.showTranscript);
1012
- this.prefTranscript = 0;
1013
- this.$transcriptButton.focus().addClass('able-focus');
1014
- }
1015
- else {
1016
- this.positionDraggableWindow('transcript');
1017
- this.$transcriptArea.show();
1018
- this.$transcriptButton.removeClass('buttonOff').attr('aria-label',this.tt.hideTranscript);
1019
- this.$transcriptButton.find('span.able-clipped').text(this.tt.hideTranscript);
1020
- this.prefTranscript = 1;
1021
- }
1022
- this.updateCookie('prefTranscript');
1023
- };
1024
-
1025
- AblePlayer.prototype.handleSignToggle = function () {
1026
- if (this.$signWindow.is(':visible')) {
1027
- this.$signWindow.hide();
1028
- this.$signButton.addClass('buttonOff').attr('aria-label',this.tt.showSign);
1029
- this.$signButton.find('span.able-clipped').text(this.tt.showSign);
1030
- this.prefSign = 0;
1031
- this.$signButton.focus().addClass('able-focus');
1032
- }
1033
- else {
1034
- this.positionDraggableWindow('sign');
1035
- this.$signWindow.show();
1036
- this.$signButton.removeClass('buttonOff').attr('aria-label',this.tt.hideSign);
1037
- this.$signButton.find('span.able-clipped').text(this.tt.hideSign);
1038
- this.prefSign = 1;
1039
- }
1040
- this.updateCookie('prefSign');
1041
- };
1042
-
1043
- AblePlayer.prototype.isFullscreen = function () {
1044
- if (this.nativeFullscreenSupported()) {
1045
- return (document.fullscreenElement ||
1046
- document.webkitFullscreenElement ||
1047
- document.webkitCurrentFullScreenElement ||
1048
- document.mozFullScreenElement ||
1049
- document.msFullscreenElement) ? true : false;
1050
- }
1051
- else {
1052
- return this.modalFullscreenActive ? true : false;
1053
- }
1054
- }
1055
-
1056
- AblePlayer.prototype.setFullscreen = function (fullscreen) {
1057
- if (this.isFullscreen() == fullscreen) {
1058
- return;
1059
- }
1060
- var thisObj = this;
1061
- var $el = this.$ableWrapper;
1062
- var el = $el[0];
1063
-
1064
- if (this.nativeFullscreenSupported()) {
1065
- // Note: many varying names for options for browser compatibility.
1066
- if (fullscreen) {
1067
- // Initialize fullscreen
1068
-
1069
- // But first, capture current settings so they can be restored later
1070
- this.preFullScreenWidth = this.$ableWrapper.width();
1071
- this.preFullScreenHeight = this.$ableWrapper.height();
1072
-
1073
- if (el.requestFullscreen) {
1074
- el.requestFullscreen();
1075
- }
1076
- else if (el.webkitRequestFullscreen) {
1077
- el.webkitRequestFullscreen();
1078
- }
1079
- else if (el.mozRequestFullScreen) {
1080
- el.mozRequestFullScreen();
1081
- }
1082
- else if (el.msRequestFullscreen) {
1083
- el.msRequestFullscreen();
1084
- }
1085
- }
1086
- else {
1087
- // Exit fullscreen
1088
- if (document.exitFullscreen) {
1089
- document.exitFullscreen();
1090
- }
1091
- else if (document.webkitExitFullscreen) {
1092
- document.webkitExitFullscreen();
1093
- }
1094
- else if (document.webkitCancelFullScreen) {
1095
- document.webkitCancelFullScreen();
1096
- }
1097
- else if (document.mozCancelFullScreen) {
1098
- document.mozCancelFullScreen();
1099
- }
1100
- else if (document.msExitFullscreen) {
1101
- document.msExitFullscreen();
1102
- }
1103
- }
1104
- // add event handlers for changes in full screen mode
1105
- // currently most changes are made in response to windowResize event
1106
- // However, that alone is not resulting in a properly restored player size in Opera Mac
1107
- // More on the Opera Mac bug: https://github.com/ableplayer/ableplayer/issues/162
1108
- // this fullscreen event handler added specifically for Opera Mac,
1109
- // but includes event listeners for all browsers in case its functionality could be expanded
1110
- // Added functionality in 2.3.45 for handling YouTube return from fullscreen as well
1111
- $(document).on('webkitfullscreenchange mozfullscreenchange fullscreenchange MSFullscreenChange', function() {
1112
- if (!thisObj.isFullscreen()) {
1113
- // user has just exited full screen
1114
- thisObj.restoringAfterFullScreen = true;
1115
- thisObj.resizePlayer(thisObj.preFullScreenWidth,thisObj.preFullScreenHeight);
1116
- }
1117
- });
1118
- }
1119
- else {
1120
- // Non-native fullscreen support through modal dialog.
1121
- // Create dialog on first run through.
1122
- if (!this.fullscreenDialog) {
1123
- var $dialogDiv = $('<div>');
1124
- // create a hidden alert, communicated to screen readers via aria-describedby
1125
- var $fsDialogAlert = $('<p>',{
1126
- 'class': 'able-screenreader-alert'
1127
- }).text(this.tt.fullscreen); // In English: "Full screen"; TODO: Add alert text that is more descriptive
1128
- $dialogDiv.append($fsDialogAlert);
1129
- // now render this as a dialog
1130
- this.fullscreenDialog = new AccessibleDialog($dialogDiv, this.$fullscreenButton, 'dialog', 'Fullscreen video player', $fsDialogAlert, this.tt.exitFullScreen, '100%', true, function () { thisObj.handleFullscreenToggle() });
1131
- $('body').append($dialogDiv);
1132
- }
1133
-
1134
- // Track whether paused/playing before moving element; moving the element can stop playback.
1135
- var wasPaused = this.isPaused();
1136
-
1137
- if (fullscreen) {
1138
- this.modalFullscreenActive = true;
1139
- this.fullscreenDialog.show();
1140
-
1141
- // Move player element into fullscreen dialog, then show.
1142
- // Put a placeholder element where player was.
1143
- this.$modalFullscreenPlaceholder = $('<div class="placeholder">');
1144
- this.$modalFullscreenPlaceholder.insertAfter($el);
1145
- $el.appendTo(this.fullscreenDialog.modal);
1146
-
1147
- // Column left css is 50% by default; set to 100% for full screen.
1148
- if ($el === this.$ableColumnLeft) {
1149
- $el.width('100%');
1150
- }
1151
- var newHeight = $(window).height() - this.$playerDiv.height();
1152
- if (!this.$descDiv.is(':hidden')) {
1153
- newHeight -= this.$descDiv.height();
1154
- }
1155
- this.resizePlayer($(window).width(), newHeight);
1156
- }
1157
- else {
1158
- this.modalFullscreenActive = false;
1159
- if ($el === this.$ableColumnLeft) {
1160
- $el.width('50%');
1161
- }
1162
- $el.insertAfter(this.$modalFullscreenPlaceholder);
1163
- this.$modalFullscreenPlaceholder.remove();
1164
- this.fullscreenDialog.hide();
1165
- this.resizePlayer(this.$ableWrapper.width(), this.$ableWrapper.height());
1166
- }
1167
-
1168
- // TODO: JW Player freezes after being moved on iPads (instead of being reset as in most browsers)
1169
- // Need to call setup again after moving?
1170
-
1171
- // Resume playback if moving stopped it.
1172
- if (!wasPaused && this.isPaused()) {
1173
- this.playMedia();
1174
- }
1175
- }
1176
- this.refreshControls();
1177
- };
1178
-
1179
- AblePlayer.prototype.handleFullscreenToggle = function () {
1180
- var stillPaused = this.isPaused(); //add boolean variable reading return from isPaused function
1181
- this.setFullscreen(!this.isFullscreen());
1182
- if (stillPaused) {
1183
- this.pauseMedia(); // when toggling fullscreen and media is just paused, keep media paused.
1184
- }
1185
- else if (!stillPaused) {
1186
- this.playMedia(); // when toggling fullscreen and media is playing, continue playing.
1187
- }
1188
- };
1189
-
1190
- AblePlayer.prototype.handleTranscriptLockToggle = function (val) {
1191
-
1192
- this.autoScrollTranscript = val; // val is boolean
1193
- this.prefAutoScrollTranscript = +val; // convert boolean to numeric 1 or 0 for cookie
1194
- this.updateCookie('prefAutoScrollTranscript');
1195
- this.refreshControls();
1196
- };
1197
-
1198
-
1199
- AblePlayer.prototype.showTooltip = function($tooltip) {
1200
-
1201
- if (($tooltip).is(':animated')) {
1202
- $tooltip.stop(true,true).show().delay(4000).fadeOut(1000);
1203
- }
1204
- else {
1205
- $tooltip.stop().show().delay(4000).fadeOut(1000);
1206
- }
1207
- };
1208
-
1209
- AblePlayer.prototype.showAlert = function( msg, location ) {
1210
-
1211
- // location is either of the following:
1212
- // 'main' (default)
1213
- // 'screenreader
1214
- // 'sign' (sign language window)
1215
- // 'transcript' (trasncript window)
1216
- var thisObj, $alertBox, $parentWindow, alertLeft, alertTop;
1217
-
1218
- thisObj = this;
1219
-
1220
- if (location === 'transcript') {
1221
- $alertBox = this.$transcriptAlert;
1222
- $parentWindow = this.$transcriptArea;
1223
- }
1224
- else if (location === 'sign') {
1225
- $alertBox = this.$signAlert;
1226
- $parentWindow = this.$signWindow;
1227
- }
1228
- else if (location === 'screenreader') {
1229
- $alertBox = this.$srAlertBox;
1230
- }
1231
- else {
1232
- $alertBox = this.$alertBox;
1233
- }
1234
- $alertBox.show();
1235
- $alertBox.text(msg);
1236
- if (location == 'transcript' || location === 'sign') {
1237
- if ($parentWindow.width() > $alertBox.width()) {
1238
- alertLeft = $parentWindow.width() / 2 - $alertBox.width() / 2;
1239
- }
1240
- else {
1241
- // alert box is wider than its container. Position it far left and let it wrap
1242
- alertLeft = 10;
1243
- }
1244
- if (location === 'sign') {
1245
- // position alert in the lower third of the sign window (to avoid covering the signer)
1246
- alertTop = ($parentWindow.height() / 3) * 2;
1247
- }
1248
- else if (location === 'transcript') {
1249
- // position alert just beneath the toolbar to avoid getting lost among transcript text
1250
- alertTop = this.$transcriptToolbar.height() + 30;
1251
- }
1252
- $alertBox.css({
1253
- top: alertTop + 'px',
1254
- left: alertLeft + 'px'
1255
- });
1256
- }
1257
- else if (location !== 'screenreader') {
1258
- // The original formula incorporated offset() into the calculation
1259
- // but at some point this began resulting in an alert that's off-centered
1260
- // Changed in v2.2.17, but here's the original for reference in case needed:
1261
- // left: this.$playerDiv.offset().left + (this.$playerDiv.width() / 2) - ($alertBox.width() / 2)
1262
- $alertBox.css({
1263
- left: (this.$playerDiv.width() / 2) - ($alertBox.width() / 2)
1264
- });
1265
- }
1266
- if (location !== 'screenreader') {
1267
- setTimeout(function () {
1268
- $alertBox.fadeOut(300);
1269
- }, 3000);
1270
- }
1271
- };
1272
-
1273
- AblePlayer.prototype.showedAlert = function (which) {
1274
-
1275
- // returns true if the target alert has already been shown
1276
- // useful for throttling alerts that only need to be shown once
1277
- // e.g., move alerts with instructions for dragging a window
1278
-
1279
- if (which === 'transcript') {
1280
- if (this.showedTranscriptAlert) {
1281
- return true;
1282
- }
1283
- else {
1284
- return false;
1285
- }
1286
- }
1287
- else if (which === 'sign') {
1288
- if (this.showedSignAlert) {
1289
- return true;
1290
- }
1291
- else {
1292
- return false;
1293
- }
1294
- }
1295
- return false;
1296
- }
1297
-
1298
- // Resizes all relevant player attributes.
1299
- AblePlayer.prototype.resizePlayer = function (width, height) {
1300
-
1301
- var jwHeight, captionSizeOkMin, captionSizeOkMax, captionSize, newCaptionSize, newLineHeight;
1302
-
1303
- if (this.isFullscreen()) {
1304
- if (typeof this.$vidcapContainer !== 'undefined') {
1305
- this.$ableWrapper.css({
1306
- 'width': width + 'px',
1307
- 'max-width': ''
1308
- })
1309
- this.$vidcapContainer.css({
1310
- 'height': height + 'px',
1311
- 'width': width
1312
- });
1313
- this.$media.css({
1314
- 'height': height + 'px',
1315
- 'width': width
1316
- })
1317
- }
1318
- if (typeof this.$transcriptArea !== 'undefined') {
1319
- this.retrieveOffscreenWindow('transcript',width,height);
1320
- }
1321
- if (typeof this.$signWindow !== 'undefined') {
1322
- this.retrieveOffscreenWindow('sign',width,height);
1323
- }
1324
- }
1325
- else {
1326
- // player resized
1327
- if (this.restoringAfterFullScreen) {
1328
- // User has just exited fullscreen mode. Restore to previous settings
1329
- width = this.preFullScreenWidth;
1330
- height = this.preFullScreenHeight;
1331
- this.restoringAfterFullScreen = false;
1332
- this.$ableWrapper.css({
1333
- 'max-width': width + 'px',
1334
- 'width': ''
1335
- });
1336
- if (typeof this.$vidcapContainer !== 'undefined') {
1337
- this.$vidcapContainer.css({
1338
- 'height': '',
1339
- 'width': ''
1340
- });
1341
- }
1342
- this.$media.css({
1343
- 'width': '100%',
1344
- 'height': 'auto'
1345
- });
1346
- }
1347
- }
1348
-
1349
- // resize YouTube or JW Player
1350
- if (this.player === 'youtube' && this.youTubePlayer) {
1351
- this.youTubePlayer.setSize(width, height);
1352
- }
1353
- else if (this.player === 'jw' && this.jwPlayer) {
1354
- if (this.mediaType === 'audio') {
1355
- // keep height set to 0 to prevent JW PLayer from showing its own player
1356
- this.jwPlayer.resize(width,0);
1357
- }
1358
- else {
1359
- this.jwPlayer.resize(width, jwHeight);
1360
- }
1361
- }
1362
-
1363
- // Resize captions
1364
- if (typeof this.$captionsDiv !== 'undefined') {
1365
-
1366
- // Font-size is too small in full screen view & too large in small-width view
1367
- // The following vars define a somewhat arbitary zone outside of which
1368
- // caption size requires adjustment
1369
- captionSizeOkMin = 400;
1370
- captionSizeOkMax = 1000;
1371
- captionSize = parseInt(this.prefCaptionsSize,10);
1372
-
1373
- // TODO: Need a better formula so that it scales proportionally to viewport
1374
- if (width > captionSizeOkMax) {
1375
- newCaptionSize = captionSize * 1.5;
1376
- }
1377
- else if (width < captionSizeOkMin) {
1378
- newCaptionSize = captionSize / 1.5;
1379
- }
1380
- else {
1381
- newCaptionSize = captionSize;
1382
- }
1383
- newLineHeight = newCaptionSize + 25;
1384
- this.$captionsDiv.css('font-size',newCaptionSize + '%');
1385
- this.$captionsWrapper.css('line-height',newLineHeight + '%');
1386
- }
1387
-
1388
- this.refreshControls();
1389
- };
1390
-
1391
- AblePlayer.prototype.retrieveOffscreenWindow = function( which, width, height ) {
1392
-
1393
- // check to be sure popup windows ('transcript' or 'sign') are positioned on-screen
1394
- // (they sometimes disappear off-screen when entering fullscreen mode)
1395
- // if off-screen, recalculate so they are back on screen
1396
-
1397
- var window, windowPos, windowTop, windowLeft, windowRight, windowWidth, windowBottom, windowHeight;
1398
-
1399
- if (which == 'transcript') {
1400
- window = this.$transcriptArea;
1401
- }
1402
- else if (which == 'sign') {
1403
- window = this.$signWindow;
1404
- }
1405
- windowWidth = window.width();
1406
- windowHeight = window.height();
1407
- windowPos = window.position();
1408
- windowTop = windowPos.top;
1409
- windowLeft = windowPos.left;
1410
- windowRight = windowLeft + windowWidth;
1411
- windowBottom = windowTop + windowHeight;
1412
-
1413
- if (windowTop < 0) { // off-screen to the top
1414
- windowTop = 10;
1415
- window.css('top',windowTop);
1416
- }
1417
- if (windowLeft < 0) { // off-screen to the left
1418
- windowLeft = 10;
1419
- window.css('left',windowLeft);
1420
- }
1421
- if (windowRight > width) { // off-screen to the right
1422
- windowLeft = (width - 20) - windowWidth;
1423
- window.css('left',windowLeft);
1424
- }
1425
- if (windowBottom > height) { // off-screen to the bottom
1426
- windowTop = (height - 10) - windowHeight;
1427
- window.css('top',windowTop);
1428
- }
1429
- };
1430
-
1431
- AblePlayer.prototype.getHighestZIndex = function() {
1432
-
1433
- // returns the highest z-index on page
1434
- // used to ensure dialogs (or potentially other windows) are on top
1435
-
1436
- var max, $elements, z;
1437
- max = 0;
1438
-
1439
- // exclude the Able Player dialogs and windows
1440
- $elements = $('body *').not('.able-modal-dialog,.able-modal-dialog *,.able-modal-overlay,.able-modal-overlay *,.able-sign-window,.able-transcript-area');
1441
-
1442
- $elements.each(function(){
1443
- z = $(this).css('z-index');
1444
- if (Number.isInteger(+z)) { // work only with integer values, not 'auto'
1445
- if (parseInt(z) > max) {
1446
- max = parseInt(z);
1447
- }
1448
- }
1449
- });
1450
- return max;
1451
- };
1452
-
1453
- AblePlayer.prototype.updateZIndex = function(which) {
1454
-
1455
- // update z-index of 'transcript' or 'sign', relative to each other
1456
- // direction is always 'up' (i.e., move window to top)
1457
- // windows come to the top when the user clicks on them
1458
- var defHighZ, defLowZ, highestZ, transcriptZ, signZ, newHighZ, newLowZ;
1459
-
1460
- // set the default z-indexes, as defined in ableplayer.css
1461
- defHighZ = 8000; // by default, assigned to the sign window
1462
- defLowZ = 7000; // by default, assigned to the transcript area
1463
- highestZ = this.getHighestZIndex(); // highest z-index on the page, excluding Able Player windows & modals
1464
-
1465
- // NOTE: Although highestZ is collected here, it currently isn't used.
1466
- // If something on the page has a higher z-index than the transcript or sign window, do we care?
1467
- // Excluding it here assumes "No". Our immediate concern is with the relationship between our own components.
1468
- // If we elevate our z-indexes so our content is on top, we run the risk of starting a z-index war.
1469
-
1470
- if (typeof this.$transcriptArea === 'undefined' || typeof this.$signWindow === 'undefined' ) {
1471
- // at least one of the windows doesn't exist, so there's no conflict
1472
- // since z-index may have been stored to a cookie on another page, need to restore default
1473
- if (typeof this.$transcriptArea !== 'undefined') {
1474
- transcriptZ = parseInt(this.$transcriptArea.css('z-index'));
1475
- if (transcriptZ > defLowZ) {
1476
- // restore to the default
1477
- this.$transcriptArea.css('z-index',defLowZ);
1478
- }
1479
- }
1480
- else if (typeof this.$signWindow !== 'undefined') {
1481
- signZ = parseInt(this.$signWindow.css('z-index'));
1482
- if (signZ > defHighZ) {
1483
- // restore to the default
1484
- this.$signWindow.css('z-index',defHighZ);
1485
- }
1486
- }
1487
- return false;
1488
- }
1489
-
1490
- // both windows exist
1491
-
1492
- // get current values
1493
- transcriptZ = parseInt(this.$transcriptArea.css('z-index'));
1494
- signZ = parseInt(this.$signWindow.css('z-index'));
1495
-
1496
- if (transcriptZ === signZ) {
1497
- // the two windows are equal; restore defaults (the target window will be on top)
1498
- newHighZ = defHighZ;
1499
- newLowZ = defLowZ;
1500
- }
1501
- else if (transcriptZ > signZ) {
1502
- if (which === 'transcript') {
1503
- // transcript is already on top; nothing to do
1504
- return false;
1505
- }
1506
- else {
1507
- // swap z's
1508
- newHighZ = transcriptZ;
1509
- newLowZ = signZ;
1510
- }
1511
- }
1512
- else { // signZ is greater
1513
- if (which === 'sign') {
1514
- // sign is already on top; nothing to do
1515
- return false;
1516
- }
1517
- else {
1518
- newHighZ = signZ;
1519
- newLowZ = transcriptZ;
1520
- }
1521
- }
1522
- // now assign the new values
1523
- if (which === 'transcript') {
1524
- this.$transcriptArea.css('z-index',newHighZ);
1525
- this.$signWindow.css('z-index',newLowZ);
1526
- }
1527
- else if (which === 'sign') {
1528
- this.$signWindow.css('z-index',newHighZ);
1529
- this.$transcriptArea.css('z-index',newLowZ);
1530
- }
1531
- };
1532
-
1533
- AblePlayer.prototype.syncTrackLanguages = function (source, language) {
1534
-
1535
- // this function is called when the player is built (source == 'init')
1536
- // and again when user changes the language of either 'captions' or 'transcript'
1537
- // It syncs the languages of chapters, descriptions, and metadata tracks
1538
- // NOTE: Caption and transcript languages are somewhat independent from one another
1539
- // If a user changes the caption language, the transcript follows
1540
- // However, if a user changes the transcript language, this only affects the transcript
1541
- // This was a group decision based on the belief that users may want a transcript
1542
- // that is in a different language than the captions
1543
-
1544
- var i, captions, descriptions, chapters, meta;
1545
-
1546
- // Captions
1547
- for (i = 0; i < this.captions.length; i++) {
1548
- if (this.captions[i].language === language) {
1549
- captions = this.captions[i];
1550
- }
1551
- }
1552
- // Chapters
1553
- for (i = 0; i < this.chapters.length; i++) {
1554
- if (this.chapters[i].language === language) {
1555
- chapters = this.chapters[i];
1556
- }
1557
- }
1558
- // Descriptions
1559
- for (i = 0; i < this.descriptions.length; i++) {
1560
- if (this.descriptions[i].language === language) {
1561
- descriptions = this.descriptions[i];
1562
- }
1563
- }
1564
- // Metadata
1565
- for (i = 0; i < this.meta.length; i++) {
1566
- if (this.meta[i].language === language) {
1567
- meta = this.meta[i];
1568
- }
1569
- }
1570
-
1571
- // regardless of source...
1572
- this.transcriptLang = language;
1573
-
1574
- if (source === 'init' || source === 'captions') {
1575
- this.captionLang = language;
1576
- this.selectedCaptions = captions;
1577
- this.selectedChapters = chapters;
1578
- this.selectedDescriptions = descriptions;
1579
- this.selectedMeta = meta;
1580
- this.transcriptCaptions = captions;
1581
- this.transcriptChapters = chapters;
1582
- this.transcriptDescriptions = descriptions;
1583
- this.updateChaptersList();
1584
- // the following was commented out in Oct/Nov 2018.
1585
- // chapters popup is setup automatically when setupPopups() is called later with no param
1586
- // not sure why it was included here.
1587
- // this.setupPopups('chapters');
1588
- }
1589
- else if (source === 'transcript') {
1590
- this.transcriptCaptions = captions;
1591
- this.transcriptChapters = chapters;
1592
- this.transcriptDescriptions = descriptions;
1593
- }
1594
- this.updateTranscript();
1595
- };
2
+ AblePlayer.prototype.seekTo = function (newTime) {
3
+
4
+ // define variables to be used for analytics
5
+ // e.g., to measure the extent to which users seek back and forward
6
+ this.seekFromTime = this.media.currentTime;
7
+ this.seekToTime = newTime;
8
+
9
+ this.seeking = true;
10
+ this.liveUpdatePending = true;
11
+
12
+ if (this.player === 'html5') {
13
+ var seekable;
14
+
15
+ this.startTime = newTime;
16
+ // Check HTML5 media "seekable" property to be sure media is seekable to startTime
17
+ seekable = this.media.seekable;
18
+ if (seekable.length > 0 && this.startTime >= seekable.start(0) && this.startTime <= seekable.end(0)) {
19
+ // ok to seek to startTime
20
+ // canplaythrough will be triggered when seeking is complete
21
+ // this.seeking will be set to false at that point
22
+ this.media.currentTime = this.startTime;
23
+ if (this.hasSignLanguage && this.signVideo) {
24
+ // keep sign languge video in sync
25
+ this.signVideo.currentTime = this.startTime;
26
+ }
27
+ }
28
+ }
29
+ else if (this.player === 'youtube') {
30
+ this.youTubePlayer.seekTo(newTime,true);
31
+ if (newTime > 0) {
32
+ if (typeof this.$posterImg !== 'undefined') {
33
+ this.$posterImg.hide();
34
+ }
35
+ }
36
+ }
37
+ else if (this.player === 'vimeo') {
38
+ this.vimeoPlayer.setCurrentTime(newTime).then(function() {
39
+ // seek finished.
40
+ // could do something here
41
+ // successful completion also fires a 'seeked' event (see event.js)
42
+ })
43
+ }
44
+ this.refreshControls('timeline');
45
+ };
46
+
47
+ AblePlayer.prototype.getMediaTimes = function (duration, elapsed) {
48
+
49
+ // Returns an array with keys 'duration' and 'elapsed'
50
+ // Vars passed to this function come courtesy of select Vimeo events
51
+ // Use those if they're available.
52
+ // Otherwise, will need to call the relevant media API
53
+ // This function should only be called from onMediaUpdateTime()
54
+ // If duration and elapsed are needed other times, use this.duration and this.elapsed
55
+
56
+ // both values are expressed in seconds, and all player APIs are similar:
57
+ // they return a value that is rounded to the nearest second before playback begins,
58
+ // then to the nearest thousandth of a second after playback begins
59
+ // With HTML5 media API, some browsers are more precise (e.g., Firefox rounds to 6 decimal points)
60
+ // but inconsistent (values with 9 decimal points have been sporadically observed in Safari)
61
+ // For standardization, values are rounded to 6 decimal points before they're returned
62
+
63
+ var deferred, promise, thisObj, mediaTimes;
64
+ mediaTimes = {};
65
+
66
+ deferred = new $.Deferred();
67
+ promise = deferred.promise();
68
+ thisObj = this;
69
+
70
+ if (typeof duration !== 'undefined' && typeof elapsed !== 'undefined') {
71
+ mediaTimes['duration'] = duration;
72
+ mediaTimes['elapsed'] = elapsed;
73
+ deferred.resolve(mediaTimes);
74
+ }
75
+ else {
76
+ this.getDuration().then(function(duration) {
77
+ mediaTimes['duration'] = thisObj.roundDown(duration,6);
78
+ thisObj.getElapsed().then(function(elapsed) {
79
+ mediaTimes['elapsed'] = thisObj.roundDown(elapsed,6);
80
+ deferred.resolve(mediaTimes);
81
+ });
82
+ });
83
+ }
84
+ return promise;
85
+ };
86
+
87
+ AblePlayer.prototype.getDuration = function () {
88
+
89
+ // returns duration of the current media, expressed in seconds
90
+ // function is called by getMediaTimes, and return value is sanitized there
91
+
92
+ var deferred, promise, thisObj;
93
+
94
+ deferred = new $.Deferred();
95
+ promise = deferred.promise();
96
+ thisObj = this;
97
+
98
+ if (this.player === 'vimeo') {
99
+ if (this.vimeoPlayer) {
100
+ this.vimeoPlayer.getDuration().then(function(duration) {
101
+ if (duration === undefined || isNaN(duration) || duration === -1) {
102
+ deferred.resolve(0);
103
+ }
104
+ else {
105
+ deferred.resolve(duration);
106
+ }
107
+ });
108
+ }
109
+ else { // vimeoPlayer hasn't been initialized yet.
110
+ deferred.resolve(0);
111
+ }
112
+ }
113
+ else {
114
+ var duration;
115
+ if (this.player === 'html5') {
116
+ duration = this.media.duration;
117
+ }
118
+ else if (this.player === 'youtube') {
119
+ if (this.youTubePlayer) {
120
+ duration = this.youTubePlayer.getDuration();
121
+ }
122
+ else { // the YouTube player hasn't initialized yet
123
+ duration = 0;
124
+ }
125
+ }
126
+ if (duration === undefined || isNaN(duration) || duration === -1) {
127
+ deferred.resolve(0);
128
+ }
129
+ else {
130
+ deferred.resolve(duration);
131
+ }
132
+ }
133
+ return promise;
134
+ };
135
+
136
+ AblePlayer.prototype.getElapsed = function () {
137
+
138
+ // returns elapsed time of the current media, expressed in seconds
139
+ // function is called by getMediaTimes, and return value is sanitized there
140
+
141
+ var deferred, promise, thisObj;
142
+
143
+ deferred = new $.Deferred();
144
+ promise = deferred.promise();
145
+ thisObj = this;
146
+
147
+ if (this.player === 'vimeo') {
148
+ if (this.vimeoPlayer) {
149
+ this.vimeoPlayer.getCurrentTime().then(function(elapsed) {
150
+ if (elapsed === undefined || isNaN(elapsed) || elapsed === -1) {
151
+ deferred.resolve(0);
152
+ }
153
+ else {
154
+ deferred.resolve(elapsed);
155
+ }
156
+ });
157
+ }
158
+ else { // vimeoPlayer hasn't been initialized yet.
159
+ deferred.resolve(0);
160
+ }
161
+ }
162
+ else {
163
+ var elapsed;
164
+ if (this.player === 'html5') {
165
+ elapsed = this.media.currentTime;
166
+ }
167
+ else if (this.player === 'youtube') {
168
+ if (this.youTubePlayer) {
169
+ elapsed = this.youTubePlayer.getCurrentTime();
170
+ }
171
+ else { // the YouTube player hasn't initialized yet
172
+ elapsed = 0;
173
+ }
174
+ }
175
+ if (elapsed === undefined || isNaN(elapsed) || elapsed === -1) {
176
+ deferred.resolve(0);
177
+ }
178
+ else {
179
+ deferred.resolve(elapsed);
180
+ }
181
+ }
182
+ return promise;
183
+ };
184
+
185
+ AblePlayer.prototype.getPlayerState = function () {
186
+
187
+ // Returns one of the following states:
188
+ // 'stopped' - Not yet played for the first time, or otherwise reset to unplayed.
189
+ // 'ended' - Finished playing.
190
+ // 'paused' - Not playing, but not stopped or ended.
191
+ // 'buffering' - Momentarily paused to load, but will resume once data is loaded.
192
+ // 'playing' - Currently playing.
193
+
194
+ // Commented out the following in 3.2.1 - not sure of its intended purpose
195
+ // It can be useful to know player state even when swapping src
196
+ // and the overhead is seemingly minimal
197
+ /*
198
+ if (this.swappingSrc) {
199
+ return;
200
+ }
201
+ */
202
+
203
+ var deferred, promise, thisObj, duration, elapsed;
204
+ deferred = new $.Deferred();
205
+ promise = deferred.promise();
206
+ thisObj = this;
207
+
208
+ if (this.player === 'html5') {
209
+ if (this.media.ended) {
210
+ deferred.resolve('ended');
211
+ }
212
+ else if (this.media.paused) {
213
+ deferred.resolve('paused');
214
+ }
215
+ else if (this.media.readyState !== 4) {
216
+ deferred.resolve('buffering');
217
+ }
218
+ else {
219
+ deferred.resolve('playing');
220
+ }
221
+ }
222
+ else if (this.player === 'youtube' && this.youTubePlayer) {
223
+ var state = this.youTubePlayer.getPlayerState();
224
+ if (state === -1 || state === 5) {
225
+ deferred.resolve('stopped');
226
+ }
227
+ else if (state === 0) {
228
+ deferred.resolve('ended');
229
+ }
230
+ else if (state === 1) {
231
+ deferred.resolve('playing');
232
+ }
233
+ else if (state === 2) {
234
+ deferred.resolve('paused');
235
+ }
236
+ else if (state === 3) {
237
+ deferred.resolve('buffering');
238
+ }
239
+ }
240
+ else if (this.player === 'vimeo' && this.vimeoPlayer) {
241
+ // curiously, Vimeo's API has no getPlaying(), getBuffering(), or getState() methods
242
+ // so maybe if it's neither paused nor ended, it must be playing???
243
+ this.vimeoPlayer.getPaused().then(function(paused) {
244
+ if (paused) {
245
+ deferred.resolve('paused');
246
+ }
247
+ else {
248
+ thisObj.vimeoPlayer.getEnded().then(function(ended) {
249
+ if (ended) {
250
+ deferred.resolve('ended');
251
+ }
252
+ else {
253
+ deferred.resolve('playing');
254
+ }
255
+ });
256
+ }
257
+ });
258
+ }
259
+ return promise;
260
+ };
261
+
262
+ AblePlayer.prototype.isPlaybackRateSupported = function () {
263
+
264
+ if (this.player === 'html5') {
265
+ if (this.media.playbackRate) {
266
+ return true;
267
+ }
268
+ else {
269
+ return false;
270
+ }
271
+ }
272
+ else if (this.player === 'youtube') {
273
+ // Youtube supports varying playback rates per video. Only expose controls if more than one playback rate is available.
274
+ if (this.youTubePlayer.getAvailablePlaybackRates().length > 1) {
275
+ return true;
276
+ }
277
+ else {
278
+ return false;
279
+ }
280
+ }
281
+ else if (this.player === 'vimeo') {
282
+ // since this takes longer to determine, it was set previous in initVimeoPlayer()
283
+ return this.vimeoSupportsPlaybackRateChange;
284
+ }
285
+ };
286
+
287
+ AblePlayer.prototype.setPlaybackRate = function (rate) {
288
+
289
+ rate = Math.max(0.5, rate);
290
+ if (this.player === 'html5') {
291
+ this.media.playbackRate = rate;
292
+ }
293
+ else if (this.player === 'youtube') {
294
+ this.youTubePlayer.setPlaybackRate(rate);
295
+ }
296
+ else if (this.player === 'vimeo') {
297
+ this.vimeoPlayer.setPlaybackRate(rate);
298
+ }
299
+ if (this.hasSignLanguage && this.signVideo) {
300
+ this.signVideo.playbackRate = rate;
301
+ }
302
+ this.$speed.text(this.tt.speed + ': ' + rate.toFixed(2).toString() + 'x');
303
+ };
304
+
305
+ AblePlayer.prototype.getPlaybackRate = function () {
306
+
307
+ if (this.player === 'html5') {
308
+ return this.media.playbackRate;
309
+ }
310
+ else if (this.player === 'youtube') {
311
+ return this.youTubePlayer.getPlaybackRate();
312
+ }
313
+ };
314
+
315
+ AblePlayer.prototype.isPaused = function () {
316
+
317
+ // Note there are three player states that count as paused in this sense,
318
+ // and one of them is named 'paused'.
319
+ // A better name would be 'isCurrentlyNotPlayingOrBuffering'
320
+
321
+ var state;
322
+
323
+ if (this.player === 'vimeo') {
324
+ // just rely on value of this.playing
325
+ if (this.playing) {
326
+ return false;
327
+ }
328
+ else {
329
+ return true;
330
+ }
331
+ }
332
+ else {
333
+ this.getPlayerState().then(function(state) {
334
+ // if any of the following is true, consider the media 'paused'
335
+ return state === 'paused' || state === 'stopped' || state === 'ended';
336
+ });
337
+ }
338
+ };
339
+
340
+ AblePlayer.prototype.pauseMedia = function () {
341
+
342
+ var thisObj = this;
343
+
344
+ if (this.player === 'html5') {
345
+ this.media.pause(true);
346
+ if (this.hasSignLanguage && this.signVideo) {
347
+ this.signVideo.pause(true);
348
+ }
349
+ }
350
+ else if (this.player === 'youtube') {
351
+ this.youTubePlayer.pauseVideo();
352
+ }
353
+ else if (this.player === 'vimeo') {
354
+ this.vimeoPlayer.pause();
355
+ }
356
+ };
357
+
358
+ AblePlayer.prototype.playMedia = function () {
359
+
360
+ var thisObj = this;
361
+
362
+ if (this.player === 'html5') {
363
+ this.media.play(true);
364
+ if (this.hasSignLanguage && this.signVideo) {
365
+ this.signVideo.play(true);
366
+ }
367
+ }
368
+ else if (this.player === 'youtube') {
369
+ this.youTubePlayer.playVideo();
370
+ if (typeof this.$posterImg !== 'undefined') {
371
+ this.$posterImg.hide();
372
+ }
373
+ this.stoppingYouTube = false;
374
+ }
375
+ else if (this.player === 'vimeo') {
376
+ this.vimeoPlayer.play();
377
+ }
378
+ this.startedPlaying = true;
379
+ if (this.hideControls) {
380
+ // wait briefly after playback begins, then hide controls
381
+ this.hidingControls = true;
382
+ this.invokeHideControlsTimeout();
383
+ }
384
+ };
385
+
386
+ AblePlayer.prototype.fadeControls = function(direction) {
387
+
388
+ // Visibly fade controls without hiding them from screen reader users
389
+
390
+ // direction is either 'out' or 'in'
391
+
392
+ // TODO: This still needs work.
393
+ // After the player fades, it's replaced by an empty space
394
+ // Would be better if the video and captions expanded to fill the void
395
+ // Attempted to fade out to 0 opacity, then move the playerDiv offscreen
396
+ // and expand the mediaContainer to fill the vacated space
397
+ // However, my attempts to do this have been choppy and buggy
398
+ // Code is preserved below and commented out
399
+
400
+ var thisObj, mediaHeight, playerHeight, newMediaHeight;
401
+ var thisObj = this;
402
+
403
+ if (direction == 'out') {
404
+ // get the original height of two key components:
405
+ mediaHeight = this.$mediaContainer.height();
406
+ playerHeight = this.$playerDiv.height();
407
+ newMediaHeight = mediaHeight + playerHeight;
408
+
409
+ // fade slowly to transparency
410
+ this.$playerDiv.fadeTo(2000,0,function() {
411
+ /*
412
+ // when finished, position playerDiv offscreen
413
+ // thisObj.$playerDiv.addClass('able-offscreen');
414
+ // Expand the height of mediaContainer to fill the void (needs work)
415
+ thisObj.$mediaContainer.animate({
416
+ height: newMediaHeight
417
+ },500);
418
+ */
419
+ });
420
+ }
421
+ else if (direction == 'in') {
422
+ // restore vidcapContainer to its original height (needs work)
423
+ // this.$mediaContainer.removeAttr('style');
424
+ // fade relatively quickly back to its original position with full opacity
425
+ // this.$playerDiv.removeClass('able-offscreen').fadeTo(100,1);
426
+ this.$playerDiv.fadeTo(100,1);
427
+ }
428
+ };
429
+
430
+ AblePlayer.prototype.invokeHideControlsTimeout = function () {
431
+
432
+ // invoke timeout for waiting a few seconds after a mouse move or key down
433
+ // before hiding controls again
434
+ var thisObj = this;
435
+ this.hideControlsTimeout = window.setTimeout(function() {
436
+ if (typeof thisObj.playing !== 'undefined' && thisObj.playing === true && thisObj.hideControls) {
437
+ thisObj.fadeControls('out');
438
+ thisObj.controlsHidden = true;
439
+ }
440
+ },5000);
441
+ this.hideControlsTimeoutStatus = 'active';
442
+ };
443
+
444
+ AblePlayer.prototype.refreshControls = function(context, duration, elapsed) {
445
+
446
+ // context is one of the following:
447
+ // 'init' - initial build (or subsequent change that requires full rebuild)
448
+ // 'timeline' - a change may effect time-related controls
449
+ // 'captions' - a change may effect caption-related controls
450
+ // 'descriptions' - a change may effect description-related controls
451
+ // 'transcript' - a change may effect the transcript window or button
452
+ // 'fullscreen' - a change has been triggered by full screen toggle
453
+ // 'playpause' - a change triggered by either a 'play' or 'pause' event
454
+
455
+ // NOTE: context is not currently supported.
456
+ // The steps in this function have too many complex interdependencies
457
+ // The gains in efficiency are offset by the possibility of introducing bugs
458
+ // For now, executing everything
459
+ context = 'init';
460
+
461
+ // duration and elapsed are passed from callback functions of Vimeo API events
462
+ // duration is expressed as sss.xxx
463
+ // elapsed is expressed as sss.xxx
464
+
465
+ var thisObj, duration, elapsed, lastChapterIndex, displayElapsed,
466
+ updateLive, textByState, timestamp, widthUsed,
467
+ leftControls, rightControls, seekbarWidth, seekbarSpacer, captionsCount,
468
+ buffered, newTop, statusBarHeight, speedHeight, statusBarWidthBreakpoint,
469
+ newSvgData;
470
+
471
+ thisObj = this;
472
+ if (this.swappingSrc) {
473
+ // wait until new source has loaded before refreshing controls
474
+ return;
475
+ }
476
+
477
+ if (context === 'timeline' || context === 'init') {
478
+ // all timeline-related functionality requires both duration and elapsed
479
+ if (typeof this.duration === 'undefined') {
480
+ // wait until duration is known before proceeding with refresh
481
+ return;
482
+ }
483
+ if (this.useChapterTimes) {
484
+ this.chapterDuration = this.getChapterDuration();
485
+ this.chapterElapsed = this.getChapterElapsed();
486
+ }
487
+
488
+ if (this.useFixedSeekInterval === false && this.seekIntervalCalculated === false && this.duration > 0) {
489
+ // couldn't calculate seekInterval previously; try again.
490
+ this.setSeekInterval();
491
+ }
492
+
493
+ if (this.seekBar) {
494
+ if (this.useChapterTimes) {
495
+ lastChapterIndex = this.selectedChapters.cues.length-1;
496
+ if (this.selectedChapters.cues[lastChapterIndex] == this.currentChapter) {
497
+ // this is the last chapter
498
+ if (this.currentChapter.end !== this.duration) {
499
+ // chapter ends before or after video ends
500
+ // need to adjust seekbar duration to match video end
501
+ this.seekBar.setDuration(this.duration - this.currentChapter.start);
502
+ }
503
+ else {
504
+ this.seekBar.setDuration(this.chapterDuration);
505
+ }
506
+ }
507
+ else {
508
+ // this is not the last chapter
509
+ this.seekBar.setDuration(this.chapterDuration);
510
+ }
511
+ }
512
+ else {
513
+ if (!(this.duration === undefined || isNaN(this.duration) || this.duration === -1)) {
514
+ this.seekBar.setDuration(this.duration);
515
+ }
516
+ }
517
+ if (!(this.seekBar.tracking)) {
518
+ // Only update the aria live region if we have an update pending
519
+ // (from a seek button control) or if the seekBar has focus.
520
+ // We use document.activeElement instead of $(':focus') due to a strange bug:
521
+ // When the seekHead element is focused, .is(':focus') is failing and $(':focus') is returning an undefined element.
522
+ updateLive = this.liveUpdatePending || this.seekBar.seekHead.is($(document.activeElement));
523
+ this.liveUpdatePending = false;
524
+ if (this.useChapterTimes) {
525
+ this.seekBar.setPosition(this.chapterElapsed, updateLive);
526
+ }
527
+ else {
528
+ this.seekBar.setPosition(this.elapsed, updateLive);
529
+ }
530
+ }
531
+
532
+ // When seeking, display the seek bar time instead of the actual elapsed time.
533
+ if (this.seekBar.tracking) {
534
+ displayElapsed = this.seekBar.lastTrackPosition;
535
+ }
536
+ else {
537
+ if (this.useChapterTimes) {
538
+ displayElapsed = this.chapterElapsed;
539
+ }
540
+ else {
541
+ displayElapsed = this.elapsed;
542
+ }
543
+ }
544
+ }
545
+ // update elapsed & duration
546
+ if (typeof this.$durationContainer !== 'undefined') {
547
+ if (this.useChapterTimes) {
548
+ this.$durationContainer.text(' / ' + this.formatSecondsAsColonTime(this.chapterDuration));
549
+ }
550
+ else {
551
+ this.$durationContainer.text(' / ' + this.formatSecondsAsColonTime(this.duration));
552
+ }
553
+ }
554
+ if (typeof this.$elapsedTimeContainer !== 'undefined') {
555
+ this.$elapsedTimeContainer.text(this.formatSecondsAsColonTime(displayElapsed));
556
+ }
557
+
558
+ // Update seekbar width.
559
+ // To do this, we need to calculate the width of all buttons surrounding it.
560
+ if (this.seekBar) {
561
+ widthUsed = 0;
562
+ seekbarSpacer = 40; // adjust for discrepancies in browsers' calculated button widths
563
+
564
+ leftControls = this.seekBar.wrapperDiv.parent().prev('div.able-left-controls');
565
+ rightControls = leftControls.next('div.able-right-controls');
566
+ leftControls.children().each(function () {
567
+ if ($(this).prop('tagName')=='BUTTON') {
568
+ widthUsed += $(this).width();
569
+ }
570
+ });
571
+ rightControls.children().each(function () {
572
+ if ($(this).prop('tagName')=='BUTTON') {
573
+ widthUsed += $(this).width();
574
+ }
575
+ });
576
+ if (this.fullscreen) {
577
+ seekbarWidth = $(window).width() - widthUsed - seekbarSpacer;
578
+ }
579
+ else {
580
+ seekbarWidth = this.$ableWrapper.width() - widthUsed - seekbarSpacer;
581
+ }
582
+ // Sometimes some minor fluctuations based on browser weirdness, so set a threshold.
583
+ if (Math.abs(seekbarWidth - this.seekBar.getWidth()) > 5) {
584
+ this.seekBar.setWidth(seekbarWidth);
585
+ }
586
+ }
587
+
588
+ // Update buffering progress.
589
+ // TODO: Currently only using the first HTML5 buffered interval,
590
+ // but this fails sometimes when buffering is split into two or more intervals.
591
+ if (this.player === 'html5') {
592
+ if (this.media.buffered.length > 0) {
593
+ buffered = this.media.buffered.end(0);
594
+ if (this.useChapterTimes) {
595
+ if (buffered > this.chapterDuration) {
596
+ buffered = this.chapterDuration;
597
+ }
598
+ if (this.seekBar) {
599
+ this.seekBar.setBuffered(buffered / this.chapterDuration);
600
+ }
601
+ }
602
+ else {
603
+ if (this.seekBar) {
604
+ this.seekBar.setBuffered(buffered / duration);
605
+ }
606
+ }
607
+ }
608
+ }
609
+ else if (this.player === 'youtube') {
610
+ if (this.seekBar) {
611
+ this.seekBar.setBuffered(this.youTubePlayer.getVideoLoadedFraction());
612
+ }
613
+ }
614
+ else if (this.player === 'vimeo') {
615
+ // TODO: Add support for Vimeo buffering update
616
+ }
617
+ } // end if context == 'timeline' or 'init'
618
+
619
+ if (context === 'descriptions' || context == 'init'){
620
+
621
+ if (this.$descButton) {
622
+ if (this.descOn) {
623
+ this.$descButton.removeClass('buttonOff').attr('aria-label',this.tt.turnOffDescriptions);
624
+ this.$descButton.find('span.able-clipped').text(this.tt.turnOffDescriptions);
625
+ }
626
+ else {
627
+ this.$descButton.addClass('buttonOff').attr('aria-label',this.tt.turnOnDescriptions);
628
+ this.$descButton.find('span.able-clipped').text(this.tt.turnOnDescriptions);
629
+ }
630
+ }
631
+ }
632
+
633
+ if (context === 'captions' || context == 'init'){
634
+
635
+ if (this.$ccButton) {
636
+
637
+ captionsCount = this.captions.length;
638
+
639
+ // Button has a different title depending on the number of captions.
640
+ // If only one caption track, this is "Show captions" and "Hide captions"
641
+ // Otherwise, it is just always "Captions"
642
+ if (!this.captionsOn) {
643
+ this.$ccButton.addClass('buttonOff');
644
+ if (captionsCount === 1) {
645
+ this.$ccButton.attr('aria-label',this.tt.showCaptions);
646
+ this.$ccButton.find('span.able-clipped').text(this.tt.showCaptions);
647
+ }
648
+ }
649
+ else {
650
+ this.$ccButton.removeClass('buttonOff');
651
+ if (captionsCount === 1) {
652
+ this.$ccButton.attr('aria-label',this.tt.hideCaptions);
653
+ this.$ccButton.find('span.able-clipped').text(this.tt.hideCaptions);
654
+ }
655
+ }
656
+
657
+ if (captionsCount > 1) {
658
+ this.$ccButton.attr({
659
+ 'aria-label': this.tt.captions,
660
+ 'aria-haspopup': 'true',
661
+ 'aria-controls': this.mediaId + '-captions-menu',
662
+ 'aria-expanded': 'false'
663
+ });
664
+ this.$ccButton.find('span.able-clipped').text(this.tt.captions);
665
+ }
666
+ }
667
+ }
668
+
669
+ if (context === 'fullscreen' || context == 'init'){
670
+
671
+ if (this.$fullscreenButton) {
672
+ if (!this.fullscreen) {
673
+ this.$fullscreenButton.attr('aria-label', this.tt.enterFullScreen);
674
+ if (this.iconType === 'font') {
675
+ this.$fullscreenButton.find('span').first().removeClass('icon-fullscreen-collapse').addClass('icon-fullscreen-expand');
676
+ this.$fullscreenButton.find('span.able-clipped').text(this.tt.enterFullScreen);
677
+ }
678
+ else if (this.iconType === 'svg') {
679
+ newSvgData = this.getSvgData('fullscreen-expand');
680
+ this.$fullscreenButton.find('svg').attr('viewBox',newSvgData[0]);
681
+ this.$fullscreenButton.find('path').attr('d',newSvgData[1]);
682
+ }
683
+ else {
684
+ this.$fullscreenButton.find('img').attr('src',this.fullscreenExpandButtonImg);
685
+ }
686
+ }
687
+ else {
688
+ this.$fullscreenButton.attr('aria-label',this.tt.exitFullScreen);
689
+ if (this.iconType === 'font') {
690
+ this.$fullscreenButton.find('span').first().removeClass('icon-fullscreen-expand').addClass('icon-fullscreen-collapse');
691
+ this.$fullscreenButton.find('span.able-clipped').text(this.tt.exitFullScreen);
692
+ }
693
+ else if (this.iconType === 'svg') {
694
+ newSvgData = this.getSvgData('fullscreen-collapse');
695
+ this.$fullscreenButton.find('svg').attr('viewBox',newSvgData[0]);
696
+ this.$fullscreenButton.find('path').attr('d',newSvgData[1]);
697
+ }
698
+ else {
699
+ this.$fullscreenButton.find('img').attr('src',this.fullscreenCollapseButtonImg);
700
+ }
701
+ }
702
+ }
703
+ }
704
+
705
+ if (context === 'playpause' || context == 'init'){
706
+ if (typeof this.$bigPlayButton !== 'undefined' && typeof this.seekBar !== 'undefined') {
707
+ // Choose show/hide for big play button and adjust position.
708
+ if (this.paused && !this.seekBar.tracking) {
709
+ if (!this.hideBigPlayButton) {
710
+ this.$bigPlayButton.show();
711
+ }
712
+ if (this.fullscreen) {
713
+ this.$bigPlayButton.width($(window).width());
714
+ this.$bigPlayButton.height($(window).height());
715
+ }
716
+ else {
717
+ this.$bigPlayButton.width(this.$mediaContainer.width());
718
+ this.$bigPlayButton.height(this.$mediaContainer.height());
719
+ }
720
+ }
721
+ else {
722
+ this.$bigPlayButton.hide();
723
+ }
724
+ }
725
+ }
726
+
727
+ if (context === 'transcript' || context == 'init'){
728
+
729
+ if (this.transcriptType) {
730
+ // Sync checkbox and autoScrollTranscript with user preference
731
+ if (this.prefAutoScrollTranscript === 1) {
732
+ this.autoScrollTranscript = true;
733
+ this.$autoScrollTranscriptCheckbox.prop('checked',true);
734
+ }
735
+ else {
736
+ this.autoScrollTranscript = false;
737
+ this.$autoScrollTranscriptCheckbox.prop('checked',false);
738
+ }
739
+
740
+ // If transcript locked, scroll transcript to current highlight location.
741
+ if (this.autoScrollTranscript && this.currentHighlight) {
742
+ newTop = Math.floor($('.able-transcript').scrollTop() +
743
+ $(this.currentHighlight).position().top -
744
+ ($('.able-transcript').height() / 2) +
745
+ ($(this.currentHighlight).height() / 2));
746
+ if (newTop !== Math.floor($('.able-transcript').scrollTop())) {
747
+ // Set a flag to ignore the coming scroll event.
748
+ // there's no other way I know of to differentiate programmatic and user-initiated scroll events.
749
+ this.scrollingTranscript = true;
750
+ $('.able-transcript').scrollTop(newTop);
751
+ }
752
+ }
753
+ }
754
+ }
755
+
756
+ if (context === 'init') {
757
+
758
+ if (this.$chaptersButton) {
759
+ this.$chaptersButton.attr({
760
+ 'aria-label': this.tt.chapters,
761
+ 'aria-haspopup': 'true',
762
+ 'aria-controls': this.mediaId + '-chapters-menu',
763
+ 'aria-expanded': 'false'
764
+ });
765
+ }
766
+ }
767
+
768
+ if (context === 'timeline' || context === 'playpause' || context === 'init') {
769
+
770
+ // update status
771
+ textByState = {
772
+ 'stopped': this.tt.statusStopped,
773
+ 'paused': this.tt.statusPaused,
774
+ 'playing': this.tt.statusPlaying,
775
+ 'buffering': this.tt.statusBuffering,
776
+ 'ended': this.tt.statusEnd
777
+ };
778
+
779
+ if (this.stoppingYouTube) {
780
+ // stoppingYouTube is true temporarily while video is paused and seeking to 0
781
+ // See notes in handleRestart()
782
+ // this.stoppingYouTube will be reset when seek to 0 is finished (in event.js > onMediaUpdateTime())
783
+ if (this.$status.text() !== this.tt.statusStopped) {
784
+ this.$status.text(this.tt.statusStopped);
785
+ }
786
+ if (this.$playpauseButton.find('span').first().hasClass('icon-pause')) {
787
+ if (this.iconType === 'font') {
788
+ this.$playpauseButton.find('span').first().removeClass('icon-pause').addClass('icon-play');
789
+ this.$playpauseButton.find('span.able-clipped').text(this.tt.play);
790
+ }
791
+ else if (this.iconType === 'svg') {
792
+ newSvgData = this.getSvgData('play');
793
+ this.$playpauseButton.find('svg').attr('viewBox',newSvgData[0]);
794
+ this.$playpauseButton.find('path').attr('d',newSvgData[1]);
795
+ }
796
+ else {
797
+ this.$playpauseButton.find('img').attr('src',this.playButtonImg);
798
+ }
799
+ }
800
+ }
801
+ else {
802
+ if (typeof this.$status !== 'undefined' && typeof this.seekBar !== 'undefined') {
803
+ // Update the text only if it's changed since it has role="alert";
804
+ // also don't update while tracking, since this may Pause/Play the player but we don't want to send a Pause/Play update.
805
+ this.getPlayerState().then(function(currentState) {
806
+ if (thisObj.$status.text() !== textByState[currentState] && !thisObj.seekBar.tracking) {
807
+ // Debounce updates; only update after status has stayed steadily different for 250ms.
808
+ timestamp = (new Date()).getTime();
809
+ if (!thisObj.statusDebounceStart) {
810
+ thisObj.statusDebounceStart = timestamp;
811
+ // Make sure refreshControls gets called again at the appropriate time to check.
812
+ thisObj.statusTimeout = setTimeout(function () {
813
+ thisObj.refreshControls(context);
814
+ }, 300);
815
+ }
816
+ else if ((timestamp - thisObj.statusDebounceStart) > 250) {
817
+ thisObj.$status.text(textByState[currentState]);
818
+ thisObj.statusDebounceStart = null;
819
+ clearTimeout(thisObj.statusTimeout);
820
+ thisObj.statusTimeout = null;
821
+ }
822
+ }
823
+ else {
824
+ thisObj.statusDebounceStart = null;
825
+ clearTimeout(thisObj.statusTimeout);
826
+ thisObj.statusTimeout = null;
827
+ }
828
+
829
+ // Don't change play/pause button display while using the seek bar (or if YouTube stopped)
830
+ if (!thisObj.seekBar.tracking && !thisObj.stoppingYouTube) {
831
+ if (currentState === 'paused' || currentState === 'stopped') {
832
+ thisObj.$playpauseButton.attr('aria-label',thisObj.tt.play);
833
+
834
+ if (thisObj.iconType === 'font') {
835
+ thisObj.$playpauseButton.find('span').first().removeClass('icon-pause').addClass('icon-play');
836
+ thisObj.$playpauseButton.find('span.able-clipped').text(thisObj.tt.play);
837
+ }
838
+ else if (thisObj.iconType === 'svg') {
839
+ newSvgData = thisObj.getSvgData('play');
840
+ thisObj.$playpauseButton.find('svg').attr('viewBox',newSvgData[0]);
841
+ thisObj.$playpauseButton.find('path').attr('d',newSvgData[1]);
842
+ }
843
+ else {
844
+ thisObj.$playpauseButton.find('img').attr('src',thisObj.playButtonImg);
845
+ }
846
+ }
847
+ else {
848
+ thisObj.$playpauseButton.attr('aria-label',thisObj.tt.pause);
849
+
850
+ if (thisObj.iconType === 'font') {
851
+ thisObj.$playpauseButton.find('span').first().removeClass('icon-play').addClass('icon-pause');
852
+ thisObj.$playpauseButton.find('span.able-clipped').text(thisObj.tt.pause);
853
+ }
854
+ else if (thisObj.iconType === 'svg') {
855
+ newSvgData = thisObj.getSvgData('pause');
856
+ thisObj.$playpauseButton.find('svg').attr('viewBox',newSvgData[0]);
857
+ thisObj.$playpauseButton.find('path').attr('d',newSvgData[1]);
858
+ }
859
+ else {
860
+ thisObj.$playpauseButton.find('img').attr('src',thisObj.pauseButtonImg);
861
+ }
862
+ }
863
+ }
864
+ });
865
+ }
866
+ }
867
+ }
868
+
869
+ // Show/hide status bar content conditionally
870
+ if (!this.fullscreen) {
871
+ statusBarWidthBreakpoint = 300;
872
+ statusBarHeight = this.$statusBarDiv.height();
873
+ speedHeight = this.$statusBarDiv.find('span.able-speed').height();
874
+ if (speedHeight > (statusBarHeight + 5)) {
875
+ // speed bar is wrapping (happens often in German player)
876
+ this.$statusBarDiv.find('span.able-speed').hide();
877
+ this.hidingSpeed = true;
878
+ }
879
+ else {
880
+ if (this.hidingSpeed) {
881
+ this.$statusBarDiv.find('span.able-speed').show();
882
+ this.hidingSpeed = false;
883
+ }
884
+ if (this.$statusBarDiv.width() < statusBarWidthBreakpoint) {
885
+ // Player is too small for a speed span
886
+ this.$statusBarDiv.find('span.able-speed').hide();
887
+ this.hidingSpeed = true;
888
+ }
889
+ else {
890
+ if (this.hidingSpeed) {
891
+ this.$statusBarDiv.find('span.able-speed').show();
892
+ this.hidingSpeed = false;
893
+ }
894
+ }
895
+ }
896
+ }
897
+
898
+ };
899
+
900
+ AblePlayer.prototype.getHiddenWidth = function($el) {
901
+
902
+ // jQuery returns for width() if element is hidden
903
+ // this function is a workaround
904
+
905
+ // save a reference to a cloned element that can be measured
906
+ var $hiddenElement = $el.clone().appendTo('body');
907
+
908
+ // calculate the width of the clone
909
+ var width = $hiddenElement.outerWidth();
910
+
911
+ // remove the clone from the DOM
912
+ $hiddenElement.remove();
913
+
914
+ return width;
915
+ };
916
+
917
+ AblePlayer.prototype.handlePlay = function(e) {
918
+
919
+ if (this.paused) {
920
+ this.playMedia();
921
+ }
922
+ else {
923
+ this.pauseMedia();
924
+ }
925
+ };
926
+
927
+ AblePlayer.prototype.handleRestart = function() {
928
+
929
+ this.seekTo(0);
930
+ };
931
+
932
+ AblePlayer.prototype.handleRewind = function() {
933
+
934
+ var targetTime;
935
+
936
+ targetTime = this.elapsed - this.seekInterval;
937
+ if (this.useChapterTimes) {
938
+ if (targetTime < this.currentChapter.start) {
939
+ targetTime = this.currentChapter.start;
940
+ }
941
+ }
942
+ else {
943
+ if (targetTime < 0) {
944
+ targetTime = 0;
945
+ }
946
+ }
947
+ this.seekTo(targetTime);
948
+ };
949
+
950
+ AblePlayer.prototype.handleFastForward = function() {
951
+
952
+ var targetTime, lastChapterIndex;
953
+
954
+ lastChapterIndex = this.chapters.length-1;
955
+ targetTime = this.elapsed + this.seekInterval;
956
+
957
+ if (this.useChapterTimes) {
958
+ if (this.chapters[lastChapterIndex] == this.currentChapter) {
959
+ // this is the last chapter
960
+ if (targetTime > this.duration || targetTime > this.currentChapter.end) {
961
+ // targetTime would exceed the end of the video (or chapter)
962
+ // scrub to end of whichever is earliest
963
+ targetTime = Math.min(this.duration, this.currentChapter.end);
964
+ }
965
+ else if (this.duration % targetTime < this.seekInterval) {
966
+ // nothing left but pocket change after seeking to targetTime
967
+ // go ahead and seek to end of video (or chapter), whichever is earliest
968
+ targetTime = Math.min(this.duration, this.currentChapter.end);
969
+ }
970
+ }
971
+ else {
972
+ // this is not the last chapter
973
+ if (targetTime > this.currentChapter.end) {
974
+ // targetTime would exceed the end of the chapter
975
+ // scrub exactly to end of chapter
976
+ targetTime = this.currentChapter.end;
977
+ }
978
+ }
979
+ }
980
+ else {
981
+ // not using chapter times
982
+ if (targetTime > this.duration) {
983
+ targetTime = this.duration;
984
+ }
985
+ }
986
+ this.seekTo(targetTime);
987
+ };
988
+
989
+ AblePlayer.prototype.handleRateIncrease = function() {
990
+ this.changeRate(1);
991
+ };
992
+
993
+ AblePlayer.prototype.handleRateDecrease = function() {
994
+ this.changeRate(-1);
995
+ };
996
+
997
+ // Increases or decreases playback rate, where dir is 1 or -1 indication direction.
998
+ AblePlayer.prototype.changeRate = function (dir) {
999
+
1000
+ var rates, currentRate, index, newRate, vimeoMin, vimeoMax;
1001
+
1002
+ if (this.player === 'html5') {
1003
+ this.setPlaybackRate(this.getPlaybackRate() + (0.25 * dir));
1004
+ }
1005
+ else if (this.player === 'youtube') {
1006
+ rates = this.youTubePlayer.getAvailablePlaybackRates();
1007
+ currentRate = this.getPlaybackRate();
1008
+ index = rates.indexOf(currentRate);
1009
+ if (index === -1) {
1010
+ console.log('ERROR: Youtube returning unknown playback rate ' + currentRate.toString());
1011
+ }
1012
+ else {
1013
+ index += dir;
1014
+ // Can only increase or decrease rate if there's another rate available.
1015
+ if (index < rates.length && index >= 0) {
1016
+ this.setPlaybackRate(rates[index]);
1017
+ }
1018
+ }
1019
+ }
1020
+ else if (this.player === 'vimeo') {
1021
+ // range is 0.5 to 2
1022
+ // increase/decrease in inrements of 0.5
1023
+ vimeoMin = 0.5;
1024
+ vimeoMax = 2;
1025
+ if (dir === 1) {
1026
+ if (this.vimeoPlaybackRate + 0.5 <= vimeoMax) {
1027
+ newRate = this.vimeoPlaybackRate + 0.5;
1028
+ }
1029
+ else {
1030
+ newRate = vimeoMax;
1031
+ }
1032
+ }
1033
+ else if (dir === -1) {
1034
+ if (this.vimeoPlaybackRate - 0.5 >= vimeoMin) {
1035
+ newRate = this.vimeoPlaybackRate - 0.5;
1036
+ }
1037
+ else {
1038
+ newRate = vimeoMin;
1039
+ }
1040
+ }
1041
+ this.setPlaybackRate(newRate);
1042
+ }
1043
+ };
1044
+
1045
+ AblePlayer.prototype.handleCaptionToggle = function() {
1046
+
1047
+ var captions;
1048
+ if (this.hidingPopup) {
1049
+ // stopgap to prevent spacebar in Firefox from reopening popup
1050
+ // immediately after closing it
1051
+ this.hidingPopup = false;
1052
+ return false;
1053
+ }
1054
+ if (this.captions.length) {
1055
+ captions = this.captions;
1056
+ }
1057
+ else {
1058
+ captions = [];
1059
+ }
1060
+ if (captions.length === 1) {
1061
+ // When there's only one set of captions, just do an on/off toggle.
1062
+ if (this.captionsOn === true) {
1063
+ // turn them off
1064
+ this.captionsOn = false;
1065
+ this.prefCaptions = 0;
1066
+ this.updateCookie('prefCaptions');
1067
+ if (this.usingYouTubeCaptions) {
1068
+ this.youTubePlayer.unloadModule(this.ytCaptionModule);
1069
+ }
1070
+ else {
1071
+ this.$captionsWrapper.hide();
1072
+ }
1073
+ }
1074
+ else {
1075
+ // captions are off. Turn them on.
1076
+ this.captionsOn = true;
1077
+ this.prefCaptions = 1;
1078
+ this.updateCookie('prefCaptions');
1079
+ if (this.usingYouTubeCaptions) {
1080
+ if (typeof this.ytCaptionModule !== 'undefined') {
1081
+ this.youTubePlayer.loadModule(this.ytCaptionModule);
1082
+ }
1083
+ }
1084
+ else {
1085
+ this.$captionsWrapper.show();
1086
+ }
1087
+ for (var i=0; i<captions.length; i++) {
1088
+ if (captions[i].def === true) { // this is the default language
1089
+ this.selectedCaptions = captions[i];
1090
+ }
1091
+ }
1092
+ this.selectedCaptions = this.captions[0];
1093
+ if (this.descriptions.length >= 0) {
1094
+ this.selectedDescriptions = this.descriptions[0];
1095
+ }
1096
+ }
1097
+ this.refreshControls('captions');
1098
+ }
1099
+ else {
1100
+ // there is more than one caption track.
1101
+ // clicking on a track is handled via caption.js > getCaptionClickFunction()
1102
+ if (this.captionsPopup && this.captionsPopup.is(':visible')) {
1103
+ this.captionsPopup.hide();
1104
+ this.hidingPopup = false;
1105
+ this.$ccButton.attr('aria-expanded','false').focus();
1106
+ }
1107
+ else {
1108
+ this.closePopups();
1109
+ if (this.captionsPopup) {
1110
+ this.captionsPopup.show();
1111
+ this.$ccButton.attr('aria-expanded','true');
1112
+ this.captionsPopup.css('top', this.$ccButton.position().top - this.captionsPopup.outerHeight());
1113
+ this.captionsPopup.css('left', this.$ccButton.position().left)
1114
+ // Place focus on the first button (even if another button is checked)
1115
+ this.captionsPopup.find('li').removeClass('able-focus');
1116
+ this.captionsPopup.find('li').first().focus().addClass('able-focus');
1117
+ }
1118
+ }
1119
+ }
1120
+ };
1121
+
1122
+ AblePlayer.prototype.handleChapters = function () {
1123
+ if (this.hidingPopup) {
1124
+ // stopgap to prevent spacebar in Firefox from reopening popup
1125
+ // immediately after closing it
1126
+ this.hidingPopup = false;
1127
+ return false;
1128
+ }
1129
+ if (this.chaptersPopup.is(':visible')) {
1130
+ this.chaptersPopup.hide();
1131
+ this.hidingPopup = false;
1132
+ this.$chaptersButton.attr('aria-expanded','false').focus();
1133
+ }
1134
+ else {
1135
+ this.closePopups();
1136
+ this.chaptersPopup.show();
1137
+ this.$chaptersButton.attr('aria-expanded','true');
1138
+ this.chaptersPopup.css('top', this.$chaptersButton.position().top - this.chaptersPopup.outerHeight());
1139
+ this.chaptersPopup.css('left', this.$chaptersButton.position().left)
1140
+
1141
+ // Highlight the current chapter, if any chapters are checked
1142
+ // Otherwise, place focus on the first chapter
1143
+ this.chaptersPopup.find('li').removeClass('able-focus');
1144
+ if (this.chaptersPopup.find('li[aria-checked="true"]').length) {
1145
+ this.chaptersPopup.find('li[aria-checked="true"]').focus().addClass('able-focus');
1146
+ }
1147
+ else {
1148
+ this.chaptersPopup.find('li').first().addClass('able-focus').attr('aria-checked','true').focus();
1149
+ }
1150
+ }
1151
+ };
1152
+
1153
+ AblePlayer.prototype.handleDescriptionToggle = function() {
1154
+ this.descOn = !this.descOn;
1155
+ this.prefDesc = + this.descOn; // convert boolean to integer
1156
+ this.updateCookie('prefDesc');
1157
+ this.refreshingDesc = true;
1158
+ this.initDescription();
1159
+ this.refreshControls('descriptions');
1160
+ };
1161
+
1162
+ AblePlayer.prototype.handlePrefsClick = function(pref) {
1163
+ // NOTE: the prefs menu is positioned near the right edge of the player
1164
+ // This assumes the Prefs button is also positioned in that vicinity
1165
+ // (last or second-last button the right)
1166
+
1167
+ var prefsButtonPosition, prefsMenuRight, prefsMenuLeft;
1168
+
1169
+ if (this.hidingPopup) {
1170
+ // stopgap to prevent spacebar in Firefox from reopening popup
1171
+ // immediately after closing it
1172
+ this.hidingPopup = false;
1173
+ return false;
1174
+ }
1175
+ if (this.prefsPopup.is(':visible')) {
1176
+ this.prefsPopup.hide();
1177
+ this.hidingPopup = false;
1178
+ this.$prefsButton.attr('aria-expanded','false').focus();
1179
+ // restore each menu item to original hidden state
1180
+ this.prefsPopup.find('li').removeClass('able-focus').attr('tabindex','-1');
1181
+ }
1182
+ else {
1183
+ this.closePopups();
1184
+ this.prefsPopup.show();
1185
+ this.$prefsButton.attr('aria-expanded','true');
1186
+ prefsButtonPosition = this.$prefsButton.position();
1187
+ prefsMenuRight = this.$ableDiv.width() - 5;
1188
+ prefsMenuLeft = prefsMenuRight - this.prefsPopup.width();
1189
+ this.prefsPopup.css('top', prefsButtonPosition.top - this.prefsPopup.outerHeight());
1190
+ this.prefsPopup.css('left', prefsMenuLeft);
1191
+ // remove prior focus and set focus on first item; also change tabindex from -1 to 0
1192
+ this.prefsPopup.find('li').removeClass('able-focus').attr('tabindex','0');
1193
+ this.prefsPopup.find('li').first().focus().addClass('able-focus');
1194
+ }
1195
+ };
1196
+
1197
+ AblePlayer.prototype.handleHelpClick = function() {
1198
+ this.setFullscreen(false);
1199
+ this.helpDialog.show();
1200
+ };
1201
+
1202
+ AblePlayer.prototype.handleTranscriptToggle = function () {
1203
+ if (this.$transcriptDiv.is(':visible')) {
1204
+ this.$transcriptArea.hide();
1205
+ this.$transcriptButton.addClass('buttonOff').attr('aria-label',this.tt.showTranscript);
1206
+ this.$transcriptButton.find('span.able-clipped').text(this.tt.showTranscript);
1207
+ this.prefTranscript = 0;
1208
+ this.$transcriptButton.focus().addClass('able-focus');
1209
+ }
1210
+ else {
1211
+ this.positionDraggableWindow('transcript');
1212
+ this.$transcriptArea.show();
1213
+ this.$transcriptButton.removeClass('buttonOff').attr('aria-label',this.tt.hideTranscript);
1214
+ this.$transcriptButton.find('span.able-clipped').text(this.tt.hideTranscript);
1215
+ this.prefTranscript = 1;
1216
+ }
1217
+ this.updateCookie('prefTranscript');
1218
+ };
1219
+
1220
+ AblePlayer.prototype.handleSignToggle = function () {
1221
+ if (this.$signWindow.is(':visible')) {
1222
+ this.$signWindow.hide();
1223
+ this.$signButton.addClass('buttonOff').attr('aria-label',this.tt.showSign);
1224
+ this.$signButton.find('span.able-clipped').text(this.tt.showSign);
1225
+ this.prefSign = 0;
1226
+ this.$signButton.focus().addClass('able-focus');
1227
+ }
1228
+ else {
1229
+ this.positionDraggableWindow('sign');
1230
+ this.$signWindow.show();
1231
+ this.$signButton.removeClass('buttonOff').attr('aria-label',this.tt.hideSign);
1232
+ this.$signButton.find('span.able-clipped').text(this.tt.hideSign);
1233
+ this.prefSign = 1;
1234
+ }
1235
+ this.updateCookie('prefSign');
1236
+ };
1237
+
1238
+ AblePlayer.prototype.isFullscreen = function () {
1239
+
1240
+ // NOTE: This has been largely replaced as of 3.2.5 with a Boolean this.fullscreen,
1241
+ // which is defined in setFullscreen()
1242
+ // This function returns true if *any* element is fullscreen
1243
+ // but doesn't tell us whether a particular element is in fullscreen
1244
+ // (e.g., if there are multiple players on the page)
1245
+ // The Boolean this.fullscreen is defined separately for each player instance
1246
+
1247
+ if (this.nativeFullscreenSupported()) {
1248
+ return (document.fullscreenElement ||
1249
+ document.webkitFullscreenElement ||
1250
+ document.webkitCurrentFullScreenElement ||
1251
+ document.mozFullScreenElement ||
1252
+ document.msFullscreenElement) ? true : false;
1253
+ }
1254
+ else {
1255
+ return this.modalFullscreenActive ? true : false;
1256
+ }
1257
+ }
1258
+
1259
+ AblePlayer.prototype.setFullscreen = function (fullscreen) {
1260
+
1261
+ if (this.fullscreen == fullscreen) {
1262
+ // replace isFullscreen() with a Boolean. see function for explanation
1263
+ return;
1264
+ }
1265
+ var thisObj = this;
1266
+ var $el = this.$ableWrapper;
1267
+ var el = $el[0];
1268
+
1269
+ if (this.nativeFullscreenSupported()) {
1270
+ // Note: many varying names for options for browser compatibility.
1271
+ if (fullscreen) {
1272
+ // Initialize fullscreen
1273
+
1274
+ // But first, capture current settings so they can be restored later
1275
+ this.preFullScreenWidth = this.$ableWrapper.width();
1276
+ this.preFullScreenHeight = this.$ableWrapper.height();
1277
+
1278
+ if (el.requestFullscreen) {
1279
+ el.requestFullscreen();
1280
+ }
1281
+ else if (el.webkitRequestFullscreen) {
1282
+ el.webkitRequestFullscreen();
1283
+ }
1284
+ else if (el.mozRequestFullScreen) {
1285
+ el.mozRequestFullScreen();
1286
+ }
1287
+ else if (el.msRequestFullscreen) {
1288
+ el.msRequestFullscreen();
1289
+ }
1290
+ this.fullscreen = true;
1291
+ }
1292
+ else {
1293
+ // Exit fullscreen
1294
+ if (document.exitFullscreen) {
1295
+ document.exitFullscreen();
1296
+ }
1297
+ else if (document.webkitExitFullscreen) {
1298
+ document.webkitExitFullscreen();
1299
+ }
1300
+ else if (document.webkitCancelFullScreen) {
1301
+ document.webkitCancelFullScreen();
1302
+ }
1303
+ else if (document.mozCancelFullScreen) {
1304
+ document.mozCancelFullScreen();
1305
+ }
1306
+ else if (document.msExitFullscreen) {
1307
+ document.msExitFullscreen();
1308
+ }
1309
+ this.fullscreen = false;
1310
+ }
1311
+ // add event handlers for changes in full screen mode
1312
+ // currently most changes are made in response to windowResize event
1313
+ // However, that alone is not resulting in a properly restored player size in Opera Mac
1314
+ // More on the Opera Mac bug: https://github.com/ableplayer/ableplayer/issues/162
1315
+ // this fullscreen event handler added specifically for Opera Mac,
1316
+ // but includes event listeners for all browsers in case its functionality could be expanded
1317
+ // Added functionality in 2.3.45 for handling YouTube return from fullscreen as well
1318
+ $(document).on('webkitfullscreenchange mozfullscreenchange fullscreenchange MSFullscreenChange', function(e) {
1319
+ // NOTE: e.type = the specific event that fired (in case needing to control for browser-specific idiosyncrasies)
1320
+ if (!thisObj.fullscreen) {
1321
+ // user has just exited full screen
1322
+ thisObj.restoringAfterFullScreen = true;
1323
+ thisObj.resizePlayer(thisObj.preFullScreenWidth,thisObj.preFullScreenHeight);
1324
+ }
1325
+ else if (!thisObj.clickedFullscreenButton) {
1326
+ // user triggered fullscreenchange without clicking (or pressing) fullscreen button
1327
+ // this is only possible if they pressed Escape to exit fullscreen mode
1328
+ thisObj.fullscreen = false;
1329
+ thisObj.restoringAfterFullScreen = true;
1330
+ thisObj.resizePlayer(thisObj.preFullScreenWidth,thisObj.preFullScreenHeight);
1331
+ }
1332
+ // NOTE: The fullscreenchange (or browser-equivalent) event is triggered twice
1333
+ // when exiting fullscreen via the "Exit fullscreen" button (only once if using Escape)
1334
+ // Not sure why, but consequently we need to be sure thisObj.clickedFullScreenButton
1335
+ // continues to be true through both events
1336
+ // Could use a counter variable to control that (reset to false after the 2nd trigger)
1337
+ // However, since I don't know why it's happening, and whether it's 100% reliable
1338
+ // resetting clickedFullScreenButton after a timeout seems to be better approach
1339
+ setTimeout(function() {
1340
+ thisObj.clickedFullscreenButton = false;
1341
+ },1000);
1342
+ });
1343
+ }
1344
+ else {
1345
+ // Non-native fullscreen support through modal dialog.
1346
+ // Create dialog on first run through.
1347
+ if (!this.fullscreenDialog) {
1348
+ var $dialogDiv = $('<div>');
1349
+ // create a hidden alert, communicated to screen readers via aria-describedby
1350
+ var $fsDialogAlert = $('<p>',{
1351
+ 'class': 'able-screenreader-alert'
1352
+ }).text(this.tt.fullscreen); // In English: "Full screen"; TODO: Add alert text that is more descriptive
1353
+ $dialogDiv.append($fsDialogAlert);
1354
+ // now render this as a dialog
1355
+ this.fullscreenDialog = new AccessibleDialog($dialogDiv, this.$fullscreenButton, 'dialog', 'Fullscreen video player', $fsDialogAlert, this.tt.exitFullScreen, '100%', true, function () { thisObj.handleFullscreenToggle() });
1356
+ $('body').append($dialogDiv);
1357
+ }
1358
+
1359
+ // Track whether paused/playing before moving element; moving the element can stop playback.
1360
+ var wasPaused = this.paused;
1361
+
1362
+ if (fullscreen) {
1363
+ this.modalFullscreenActive = true;
1364
+ this.fullscreenDialog.show();
1365
+
1366
+ // Move player element into fullscreen dialog, then show.
1367
+ // Put a placeholder element where player was.
1368
+ this.$modalFullscreenPlaceholder = $('<div class="placeholder">');
1369
+ this.$modalFullscreenPlaceholder.insertAfter($el);
1370
+ $el.appendTo(this.fullscreenDialog.modal);
1371
+
1372
+ // Column left css is 50% by default; set to 100% for full screen.
1373
+ if ($el === this.$ableColumnLeft) {
1374
+ $el.width('100%');
1375
+ }
1376
+ var newHeight = $(window).height() - this.$playerDiv.height();
1377
+ if (!this.$descDiv.is(':hidden')) {
1378
+ newHeight -= this.$descDiv.height();
1379
+ }
1380
+ this.resizePlayer($(window).width(), newHeight);
1381
+ }
1382
+ else {
1383
+ this.modalFullscreenActive = false;
1384
+ if ($el === this.$ableColumnLeft) {
1385
+ $el.width('50%');
1386
+ }
1387
+ $el.insertAfter(this.$modalFullscreenPlaceholder);
1388
+ this.$modalFullscreenPlaceholder.remove();
1389
+ this.fullscreenDialog.hide();
1390
+ this.resizePlayer(this.$ableWrapper.width(), this.$ableWrapper.height());
1391
+ }
1392
+
1393
+ // Resume playback if moving stopped it.
1394
+ if (!wasPaused && this.paused) {
1395
+ this.playMedia();
1396
+ }
1397
+ }
1398
+ this.refreshControls('fullscreen');
1399
+ };
1400
+
1401
+ AblePlayer.prototype.handleFullscreenToggle = function () {
1402
+
1403
+ var stillPaused = this.paused;
1404
+ this.setFullscreen(!this.fullscreen);
1405
+ if (stillPaused) {
1406
+ this.pauseMedia(); // when toggling fullscreen and media is just paused, keep media paused.
1407
+ }
1408
+ else if (!stillPaused) {
1409
+ this.playMedia(); // when toggling fullscreen and media is playing, continue playing.
1410
+ }
1411
+ // automatically hide controller in fullscreen mode
1412
+ // then reset back to original setting after exiting fullscreen mode
1413
+ if (this.fullscreen) {
1414
+ this.hideControls = true;
1415
+ if (this.playing) {
1416
+ // go ahead and hide the controls
1417
+ this.fadeControls('out');
1418
+ this.controlsHidden = true;
1419
+ }
1420
+ }
1421
+ else {
1422
+ // exit fullscreen mode
1423
+ this.hideControls = this.hideControlsOriginal;
1424
+ if (!this.hideControls) { // do not hide controls
1425
+ if (this.controlsHidden) {
1426
+ this.fadeControls('in');
1427
+ this.controlsHidden = false;
1428
+ }
1429
+ // if there's an active timeout to fade controls out again, clear it
1430
+ if (this.hideControlsTimeoutStatus === 'active') {
1431
+ window.clearTimeout(this.hideControlsTimeout);
1432
+ this.hideControlsTimeoutStatus = 'clear';
1433
+ }
1434
+ }
1435
+ }
1436
+ };
1437
+
1438
+ AblePlayer.prototype.handleTranscriptLockToggle = function (val) {
1439
+
1440
+ this.autoScrollTranscript = val; // val is boolean
1441
+ this.prefAutoScrollTranscript = +val; // convert boolean to numeric 1 or 0 for cookie
1442
+ this.updateCookie('prefAutoScrollTranscript');
1443
+ this.refreshControls('transcript');
1444
+ };
1445
+
1446
+
1447
+ AblePlayer.prototype.showTooltip = function($tooltip) {
1448
+
1449
+ if (($tooltip).is(':animated')) {
1450
+ $tooltip.stop(true,true).show().delay(4000).fadeOut(1000);
1451
+ }
1452
+ else {
1453
+ $tooltip.stop().show().delay(4000).fadeOut(1000);
1454
+ }
1455
+ };
1456
+
1457
+ AblePlayer.prototype.showAlert = function( msg, location ) {
1458
+
1459
+ // location is either of the following:
1460
+ // 'main' (default)
1461
+ // 'screenreader (visibly hidden)
1462
+ // 'sign' (sign language window)
1463
+ // 'transcript' (trasncript window)
1464
+ var thisObj, $alertBox, $parentWindow, alertLeft, alertTop;
1465
+
1466
+ thisObj = this;
1467
+
1468
+ if (location === 'transcript') {
1469
+ $alertBox = this.$transcriptAlert;
1470
+ $parentWindow = this.$transcriptArea;
1471
+ }
1472
+ else if (location === 'sign') {
1473
+ $alertBox = this.$signAlert;
1474
+ $parentWindow = this.$signWindow;
1475
+ }
1476
+ else if (location === 'screenreader') {
1477
+ $alertBox = this.$srAlertBox;
1478
+ }
1479
+ else {
1480
+ $alertBox = this.$alertBox;
1481
+ }
1482
+ $alertBox.text(msg).show();
1483
+ if (location == 'transcript' || location === 'sign') {
1484
+ if ($parentWindow.width() > $alertBox.width()) {
1485
+ alertLeft = $parentWindow.width() / 2 - $alertBox.width() / 2;
1486
+ }
1487
+ else {
1488
+ // alert box is wider than its container. Position it far left and let it wrap
1489
+ alertLeft = 10;
1490
+ }
1491
+ if (location === 'sign') {
1492
+ // position alert in the lower third of the sign window (to avoid covering the signer)
1493
+ alertTop = ($parentWindow.height() / 3) * 2;
1494
+ }
1495
+ else if (location === 'transcript') {
1496
+ // position alert just beneath the toolbar to avoid getting lost among transcript text
1497
+ alertTop = this.$transcriptToolbar.height() + 30;
1498
+ }
1499
+ $alertBox.css({
1500
+ top: alertTop + 'px',
1501
+ left: alertLeft + 'px'
1502
+ });
1503
+ }
1504
+ else if (location !== 'screenreader') {
1505
+ // The original formula incorporated offset() into the calculation
1506
+ // but at some point this began resulting in an alert that's off-centered
1507
+ // Changed in v2.2.17, but here's the original for reference in case needed:
1508
+ // left: this.$playerDiv.offset().left + (this.$playerDiv.width() / 2) - ($alertBox.width() / 2)
1509
+ $alertBox.css({
1510
+ left: (this.$playerDiv.width() / 2) - ($alertBox.width() / 2)
1511
+ });
1512
+ }
1513
+ if (location !== 'screenreader') {
1514
+ setTimeout(function () {
1515
+ $alertBox.fadeOut(300);
1516
+ }, 3000);
1517
+ }
1518
+ };
1519
+
1520
+ AblePlayer.prototype.showedAlert = function (which) {
1521
+
1522
+ // returns true if the target alert has already been shown
1523
+ // useful for throttling alerts that only need to be shown once
1524
+ // e.g., move alerts with instructions for dragging a window
1525
+
1526
+ if (which === 'transcript') {
1527
+ if (this.showedTranscriptAlert) {
1528
+ return true;
1529
+ }
1530
+ else {
1531
+ return false;
1532
+ }
1533
+ }
1534
+ else if (which === 'sign') {
1535
+ if (this.showedSignAlert) {
1536
+ return true;
1537
+ }
1538
+ else {
1539
+ return false;
1540
+ }
1541
+ }
1542
+ return false;
1543
+ }
1544
+
1545
+ // Resizes all relevant player attributes.
1546
+ AblePlayer.prototype.resizePlayer = function (width, height) {
1547
+
1548
+ var captionSizeOkMin, captionSizeOkMax, captionSize, newCaptionSize, newLineHeight;
1549
+
1550
+ if (this.fullscreen) { // replace isFullscreen() with a Boolean. see function for explanation
1551
+ if (typeof this.$vidcapContainer !== 'undefined') {
1552
+ this.$ableWrapper.css({
1553
+ 'width': width + 'px',
1554
+ 'max-width': ''
1555
+ })
1556
+ this.$vidcapContainer.css({
1557
+ 'height': height + 'px',
1558
+ 'width': width
1559
+ });
1560
+ this.$media.css({
1561
+ 'height': height + 'px',
1562
+ 'width': width
1563
+ })
1564
+ }
1565
+ if (typeof this.$transcriptArea !== 'undefined') {
1566
+ this.retrieveOffscreenWindow('transcript',width,height);
1567
+ }
1568
+ if (typeof this.$signWindow !== 'undefined') {
1569
+ this.retrieveOffscreenWindow('sign',width,height);
1570
+ }
1571
+ }
1572
+ else {
1573
+ // player resized
1574
+ if (this.restoringAfterFullScreen) {
1575
+ // User has just exited fullscreen mode. Restore to previous settings
1576
+ width = this.preFullScreenWidth;
1577
+ height = this.preFullScreenHeight;
1578
+ this.restoringAfterFullScreen = false;
1579
+ this.$ableWrapper.css({
1580
+ 'max-width': width + 'px',
1581
+ 'width': ''
1582
+ });
1583
+ if (typeof this.$vidcapContainer !== 'undefined') {
1584
+ this.$vidcapContainer.css({
1585
+ 'height': '',
1586
+ 'width': ''
1587
+ });
1588
+ }
1589
+ this.$media.css({
1590
+ 'width': '100%',
1591
+ 'height': 'auto'
1592
+ });
1593
+ }
1594
+ }
1595
+
1596
+ // resize YouTube
1597
+ if (this.player === 'youtube' && this.youTubePlayer) {
1598
+ this.youTubePlayer.setSize(width, height);
1599
+ }
1600
+
1601
+ // Resize captions
1602
+ if (typeof this.$captionsDiv !== 'undefined') {
1603
+
1604
+ // Font-size is too small in full screen view & too large in small-width view
1605
+ // The following vars define a somewhat arbitary zone outside of which
1606
+ // caption size requires adjustment
1607
+ captionSizeOkMin = 400;
1608
+ captionSizeOkMax = 1000;
1609
+ captionSize = parseInt(this.prefCaptionsSize,10);
1610
+
1611
+ // TODO: Need a better formula so that it scales proportionally to viewport
1612
+ if (width > captionSizeOkMax) {
1613
+ newCaptionSize = captionSize * 1.5;
1614
+ }
1615
+ else if (width < captionSizeOkMin) {
1616
+ newCaptionSize = captionSize / 1.5;
1617
+ }
1618
+ else {
1619
+ newCaptionSize = captionSize;
1620
+ }
1621
+ newLineHeight = newCaptionSize + 25;
1622
+ this.$captionsDiv.css('font-size',newCaptionSize + '%');
1623
+ this.$captionsWrapper.css('line-height',newLineHeight + '%');
1624
+ }
1625
+ this.refreshControls('captions');
1626
+ };
1627
+
1628
+ AblePlayer.prototype.retrieveOffscreenWindow = function( which, width, height ) {
1629
+
1630
+ // check to be sure popup windows ('transcript' or 'sign') are positioned on-screen
1631
+ // (they sometimes disappear off-screen when entering fullscreen mode)
1632
+ // if off-screen, recalculate so they are back on screen
1633
+
1634
+ var window, windowPos, windowTop, windowLeft, windowRight, windowWidth, windowBottom, windowHeight;
1635
+
1636
+ if (which == 'transcript') {
1637
+ window = this.$transcriptArea;
1638
+ }
1639
+ else if (which == 'sign') {
1640
+ window = this.$signWindow;
1641
+ }
1642
+ windowWidth = window.width();
1643
+ windowHeight = window.height();
1644
+ windowPos = window.position();
1645
+ windowTop = windowPos.top;
1646
+ windowLeft = windowPos.left;
1647
+ windowRight = windowLeft + windowWidth;
1648
+ windowBottom = windowTop + windowHeight;
1649
+
1650
+ if (windowTop < 0) { // off-screen to the top
1651
+ windowTop = 10;
1652
+ window.css('top',windowTop);
1653
+ }
1654
+ if (windowLeft < 0) { // off-screen to the left
1655
+ windowLeft = 10;
1656
+ window.css('left',windowLeft);
1657
+ }
1658
+ if (windowRight > width) { // off-screen to the right
1659
+ windowLeft = (width - 20) - windowWidth;
1660
+ window.css('left',windowLeft);
1661
+ }
1662
+ if (windowBottom > height) { // off-screen to the bottom
1663
+ windowTop = (height - 10) - windowHeight;
1664
+ window.css('top',windowTop);
1665
+ }
1666
+ };
1667
+
1668
+ AblePlayer.prototype.getHighestZIndex = function() {
1669
+
1670
+ // returns the highest z-index on page
1671
+ // used to ensure dialogs (or potentially other windows) are on top
1672
+
1673
+ var max, $elements, z;
1674
+ max = 0;
1675
+
1676
+ // exclude the Able Player dialogs and windows
1677
+ $elements = $('body *').not('.able-modal-dialog,.able-modal-dialog *,.able-modal-overlay,.able-modal-overlay *,.able-sign-window,.able-transcript-area');
1678
+
1679
+ $elements.each(function(){
1680
+ z = $(this).css('z-index');
1681
+ if (Number.isInteger(+z)) { // work only with integer values, not 'auto'
1682
+ if (parseInt(z) > max) {
1683
+ max = parseInt(z);
1684
+ }
1685
+ }
1686
+ });
1687
+ return max;
1688
+ };
1689
+
1690
+ AblePlayer.prototype.updateZIndex = function(which) {
1691
+
1692
+ // update z-index of 'transcript' or 'sign', relative to each other
1693
+ // direction is always 'up' (i.e., move window to top)
1694
+ // windows come to the top when the user clicks on them
1695
+ var defHighZ, defLowZ, highestZ, transcriptZ, signZ, newHighZ, newLowZ;
1696
+
1697
+ // set the default z-indexes, as defined in ableplayer.css
1698
+ defHighZ = 8000; // by default, assigned to the sign window
1699
+ defLowZ = 7000; // by default, assigned to the transcript area
1700
+ highestZ = this.getHighestZIndex(); // highest z-index on the page, excluding Able Player windows & modals
1701
+
1702
+ // NOTE: Although highestZ is collected here, it currently isn't used.
1703
+ // If something on the page has a higher z-index than the transcript or sign window, do we care?
1704
+ // Excluding it here assumes "No". Our immediate concern is with the relationship between our own components.
1705
+ // If we elevate our z-indexes so our content is on top, we run the risk of starting a z-index war.
1706
+
1707
+ if (typeof this.$transcriptArea === 'undefined' || typeof this.$signWindow === 'undefined' ) {
1708
+ // at least one of the windows doesn't exist, so there's no conflict
1709
+ // since z-index may have been stored to a cookie on another page, need to restore default
1710
+ if (typeof this.$transcriptArea !== 'undefined') {
1711
+ transcriptZ = parseInt(this.$transcriptArea.css('z-index'));
1712
+ if (transcriptZ > defLowZ) {
1713
+ // restore to the default
1714
+ this.$transcriptArea.css('z-index',defLowZ);
1715
+ }
1716
+ }
1717
+ else if (typeof this.$signWindow !== 'undefined') {
1718
+ signZ = parseInt(this.$signWindow.css('z-index'));
1719
+ if (signZ > defHighZ) {
1720
+ // restore to the default
1721
+ this.$signWindow.css('z-index',defHighZ);
1722
+ }
1723
+ }
1724
+ return false;
1725
+ }
1726
+
1727
+ // both windows exist
1728
+
1729
+ // get current values
1730
+ transcriptZ = parseInt(this.$transcriptArea.css('z-index'));
1731
+ signZ = parseInt(this.$signWindow.css('z-index'));
1732
+
1733
+ if (transcriptZ === signZ) {
1734
+ // the two windows are equal; restore defaults (the target window will be on top)
1735
+ newHighZ = defHighZ;
1736
+ newLowZ = defLowZ;
1737
+ }
1738
+ else if (transcriptZ > signZ) {
1739
+ if (which === 'transcript') {
1740
+ // transcript is already on top; nothing to do
1741
+ return false;
1742
+ }
1743
+ else {
1744
+ // swap z's
1745
+ newHighZ = transcriptZ;
1746
+ newLowZ = signZ;
1747
+ }
1748
+ }
1749
+ else { // signZ is greater
1750
+ if (which === 'sign') {
1751
+ // sign is already on top; nothing to do
1752
+ return false;
1753
+ }
1754
+ else {
1755
+ newHighZ = signZ;
1756
+ newLowZ = transcriptZ;
1757
+ }
1758
+ }
1759
+ // now assign the new values
1760
+ if (which === 'transcript') {
1761
+ this.$transcriptArea.css('z-index',newHighZ);
1762
+ this.$signWindow.css('z-index',newLowZ);
1763
+ }
1764
+ else if (which === 'sign') {
1765
+ this.$signWindow.css('z-index',newHighZ);
1766
+ this.$transcriptArea.css('z-index',newLowZ);
1767
+ }
1768
+ };
1769
+
1770
+ AblePlayer.prototype.syncTrackLanguages = function (source, language) {
1771
+
1772
+ // this function is called when the player is built (source == 'init')
1773
+ // and again when user changes the language of either 'captions' or 'transcript'
1774
+ // It syncs the languages of chapters, descriptions, and metadata tracks
1775
+ // NOTE: Caption and transcript languages are somewhat independent from one another
1776
+ // If a user changes the caption language, the transcript follows
1777
+ // However, if a user changes the transcript language, this only affects the transcript
1778
+ // This was a group decision based on the belief that users may want a transcript
1779
+ // that is in a different language than the captions
1780
+
1781
+ var i, captions, descriptions, chapters, meta;
1782
+
1783
+ // Captions
1784
+ for (i = 0; i < this.captions.length; i++) {
1785
+ if (this.captions[i].language === language) {
1786
+ captions = this.captions[i];
1787
+ }
1788
+ }
1789
+ // Chapters
1790
+ for (i = 0; i < this.chapters.length; i++) {
1791
+ if (this.chapters[i].language === language) {
1792
+ chapters = this.chapters[i];
1793
+ }
1794
+ }
1795
+ // Descriptions
1796
+ for (i = 0; i < this.descriptions.length; i++) {
1797
+ if (this.descriptions[i].language === language) {
1798
+ descriptions = this.descriptions[i];
1799
+ }
1800
+ }
1801
+ // Metadata
1802
+ for (i = 0; i < this.meta.length; i++) {
1803
+ if (this.meta[i].language === language) {
1804
+ meta = this.meta[i];
1805
+ }
1806
+ }
1807
+
1808
+ // regardless of source...
1809
+ this.transcriptLang = language;
1810
+
1811
+ if (source === 'init' || source === 'captions') {
1812
+ this.captionLang = language;
1813
+ this.selectedCaptions = captions;
1814
+ this.selectedChapters = chapters;
1815
+ this.selectedDescriptions = descriptions;
1816
+ this.selectedMeta = meta;
1817
+ this.transcriptCaptions = captions;
1818
+ this.transcriptChapters = chapters;
1819
+ this.transcriptDescriptions = descriptions;
1820
+ this.updateChaptersList();
1821
+ // the following was commented out in Oct/Nov 2018.
1822
+ // chapters popup is setup automatically when setupPopups() is called later with no param
1823
+ // not sure why it was included here.
1824
+ // this.setupPopups('chapters');
1825
+ }
1826
+ else if (source === 'transcript') {
1827
+ this.transcriptCaptions = captions;
1828
+ this.transcriptChapters = chapters;
1829
+ this.transcriptDescriptions = descriptions;
1830
+ }
1831
+ this.updateTranscript();
1832
+ };
1596
1833
 
1597
1834
  })(jQuery);