wai-website-theme 1.3.1 → 1.4

Sign up to get free protection for your applications and to get access to all the features.
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);