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,482 +1,699 @@
1
1
 
2
2
  (function ($) {
3
- AblePlayer.prototype.initYouTubePlayer = function () {
4
-
5
- var thisObj, deferred, promise, youTubeId, googleApiPromise, json;
6
- thisObj = this;
7
-
8
- deferred = new $.Deferred();
9
- promise = deferred.promise();
10
-
11
- // if a described version is available && user prefers desription
12
- // init player using the described version
13
- if (this.youTubeDescId && this.prefDesc) {
14
- youTubeId = this.youTubeDescId;
15
- }
16
- else {
17
- youTubeId = this.youTubeId;
18
- }
19
- this.activeYouTubeId = youTubeId;
20
- if (AblePlayer.youtubeIframeAPIReady) {
21
- // Script already loaded and ready.
22
- this.finalizeYoutubeInit().then(function() {
23
- deferred.resolve();
24
- });
25
- }
26
- else {
27
- // Has another player already started loading the script? If so, abort...
28
- if (!AblePlayer.loadingYoutubeIframeAPI) {
29
- $.getScript('https://www.youtube.com/iframe_api').fail(function () {
30
- deferred.fail();
31
- });
32
- }
33
-
34
- // Otherwise, keeping waiting for script load event...
35
- $('body').on('youtubeIframeAPIReady', function () {
36
- thisObj.finalizeYoutubeInit().then(function() {
37
- deferred.resolve();
38
- });
39
- });
40
- }
41
- return promise;
42
- };
43
-
44
- AblePlayer.prototype.finalizeYoutubeInit = function () {
45
-
46
- // This is called once we're sure the Youtube iFrame API is loaded -- see above
47
- var deferred, promise, thisObj, containerId, ccLoadPolicy, videoDimensions;
48
-
49
- deferred = new $.Deferred();
50
- promise = deferred.promise();
51
-
52
- thisObj = this;
53
-
54
- containerId = this.mediaId + '_youtube';
55
-
56
- this.$mediaContainer.prepend($('<div>').attr('id', containerId));
57
- // NOTE: Tried the following in place of the above in January 2016
58
- // because in some cases two videos were being added to the DOM
59
- // However, once v2.2.23 was fairly stable, unable to reproduce that problem
60
- // so maybe it's not an issue. This is preserved here temporarily, just in case it's needed...
61
- // thisObj.$mediaContainer.html($('<div>').attr('id', containerId));
62
-
63
- this.youTubeCaptionsReady = false;
64
-
65
- // if captions are provided locally via <track> elements, use those
66
- // and unload the captions provided by YouTube
67
- // Advantages of using <track>:
68
- // 1. Interactive transcript and searching within video is possible
69
- // 2. User has greater control over captions' display
70
- if (thisObj.captions.length) {
71
- // initialize YouTube player with cc_load_policy = 0
72
- // this doesn't disable captions;
73
- // it just doesn't show them automatically (depends on user's preference on YouTube)
74
- ccLoadPolicy = 0;
75
- this.usingYouTubeCaptions = false;
76
- }
77
- else {
78
- // set ccLoadPolicy to 1 only if captions are on;
79
- // this forces them on, regardless of user's preference on YouTube
80
- if (this.captionsOn) {
81
- ccLoadPolicy = 1;
82
- }
83
- else {
84
- ccLoadPolicy = 0;
85
- }
86
- }
87
- videoDimensions = this.getYouTubeDimensions(this.activeYouTubeId, containerId);
88
- if (videoDimensions) {
89
- this.ytWidth = videoDimensions[0];
90
- this.ytHeight = videoDimensions[1];
91
- this.aspectRatio = thisObj.ytWidth / thisObj.ytHeight;
92
- }
93
- else {
94
- // dimensions are initially unknown
95
- // sending null values to YouTube results in a video that uses the default YouTube dimensions
96
- // these can then be scraped from the iframe and applied to this.$ableWrapper
97
- this.ytWidth = null;
98
- this.ytHeight = null;
99
- }
100
-
101
- // NOTE: YouTube is changing the following parameters on or after Sep 25, 2018:
102
- // rel - No longer able to prevent YouTube from showing related videos
103
- // value of 0 now limits related videos to video's same channel
104
- // showinfo - No longer supported (previously, value of 0 hid title, share, & watch later buttons
105
- // Documentation https://developers.google.com/youtube/player_parameters
106
-
107
- this.youTubePlayer = new YT.Player(containerId, {
108
- videoId: this.activeYouTubeId,
109
- host: this.youTubeNoCookie ? 'https://www.youtube-nocookie.com' : 'https://www.youtube.com',
110
- width: this.ytWidth,
111
- height: this.ytHeight,
112
- playerVars: {
113
- autoplay: 0,
114
- enablejsapi: 1,
115
- disableKb: 1, // disable keyboard shortcuts, using our own
116
- playsinline: this.playsInline,
117
- start: this.startTime,
118
- controls: 0, // no controls, using our own
119
- cc_load_policy: ccLoadPolicy,
120
- hl: this.lang, // use the default language UI
121
- modestbranding: 1, // no YouTube logo in controller
122
- rel: 0, // do not show related videos when video ends
123
- html5: 1, // force html5 if browser supports it (undocumented parameter; 0 does NOT force Flash)
124
- iv_load_policy: 3 // do not show video annotations
125
- },
126
- events: {
127
- onReady: function () {
128
- if (thisObj.swappingSrc) {
129
- // swap is now complete
130
- thisObj.swappingSrc = false;
131
- if (thisObj.playing) {
132
- // resume playing
133
- thisObj.playMedia();
134
- }
135
- }
136
- if (typeof thisObj.aspectRatio === 'undefined') {
137
- thisObj.resizeYouTubePlayer(thisObj.activeYouTubeId, containerId);
138
- }
139
- deferred.resolve();
140
- },
141
- onError: function (x) {
142
- deferred.fail();
143
- },
144
- onStateChange: function (x) {
145
- var playerState = thisObj.getPlayerState(x.data);
146
- if (playerState === 'playing') {
147
- thisObj.playing = true;
148
- thisObj.startedPlaying = true;
149
- }
150
- else {
151
- thisObj.playing = false;
152
- }
153
- if (thisObj.stoppingYouTube && playerState === 'paused') {
154
- if (typeof thisObj.$posterImg !== 'undefined') {
155
- thisObj.$posterImg.show();
156
- }
157
- thisObj.stoppingYouTube = false;
158
- thisObj.seeking = false;
159
- thisObj.playing = false;
160
- }
161
- },
162
- onPlaybackQualityChange: function () {
163
- // do something
164
- },
165
- onApiChange: function (x) {
166
- // As of Able Player v2.2.23, we are now getting caption data via the YouTube Data API
167
- // prior to calling initYouTubePlayer()
168
- // Previously we got caption data via the YouTube iFrame API, and doing so was an awful mess.
169
- // onApiChange fires to indicate that the player has loaded (or unloaded) a module with exposed API methods
170
- // it isn't fired until the video starts playing
171
- // if captions are available for this video (automated captions don't count)
172
- // the 'captions' (or 'cc') module is loaded. If no captions are available, this event never fires
173
- // So, to trigger this event we had to play the video briefly, then pause, then reset.
174
- // During that brief moment of playback, the onApiChange event was fired and we could setup captions
175
- // The 'captions' and 'cc' modules are very different, and have different data and methods
176
- // NOW, in v2.2.23, we still need to initialize the caption modules in order to control captions
177
- // but we don't have to do that on load in order to get caption data
178
- // Instead, we can wait until the video starts playing normally, then retrieve the modules
179
- thisObj.initYouTubeCaptionModule();
180
- }
181
- }
182
- });
183
- thisObj.injectPoster(thisObj.$mediaContainer, 'youtube');
184
- thisObj.$media.remove();
185
- return promise;
186
- };
187
-
188
- AblePlayer.prototype.getYouTubeDimensions = function (youTubeId, youTubeContainerId) {
189
-
190
- // get dimensions of YouTube video, return array with width & height
191
- // Sources, in order of priority:
192
- // 1. The width and height attributes on <video>
193
- // 2. YouTube (not yet supported; can't seem to get this data via YouTube Data API without OAuth!)
194
-
195
- var d, url, $iframe, width, height;
196
-
197
- d = [];
198
-
199
- if (typeof this.playerMaxWidth !== 'undefined') {
200
- d[0] = this.playerMaxWidth;
201
- // optional: set height as well; not required though since YouTube will adjust height to match width
202
- if (typeof this.playerMaxHeight !== 'undefined') {
203
- d[1] = this.playerMaxHeight;
204
- }
205
- return d;
206
- }
207
- else {
208
- if (typeof $('#' + youTubeContainerId) !== 'undefined') {
209
- $iframe = $('#' + youTubeContainerId);
210
- width = $iframe.width();
211
- height = $iframe.height();
212
- if (width > 0 && height > 0) {
213
- d[0] = width;
214
- d[1] = height;
215
- return d;
216
- }
217
- }
218
- }
219
- return false;
220
- };
221
-
222
- AblePlayer.prototype.resizeYouTubePlayer = function(youTubeId, youTubeContainerId) {
223
-
224
- // called after player is ready, if youTube dimensions were previously unknown
225
- // Now need to get them from the iframe element that YouTube injected
226
- // and resize Able Player to match
227
- var d, width, height;
228
- if (typeof this.aspectRatio !== 'undefined') {
229
- // video dimensions have already been collected
230
- if (this.restoringAfterFullScreen) {
231
- // restore using saved values
232
- if (this.youTubePlayer) {
233
- this.youTubePlayer.setSize(this.ytWidth, this.ytHeight);
234
- }
235
- this.restoringAfterFullScreen = false;
236
- }
237
- else {
238
- // recalculate with new wrapper size
239
- width = this.$ableWrapper.parent().width();
240
- height = Math.round(width / this.aspectRatio);
241
- this.$ableWrapper.css({
242
- 'max-width': width + 'px',
243
- 'width': ''
244
- });
245
- this.youTubePlayer.setSize(width, height);
246
- if (this.isFullscreen()) {
247
- this.youTubePlayer.setSize(width, height);
248
- }
249
- else {
250
- // resizing due to a change in window size, not full screen
251
- this.youTubePlayer.setSize(this.ytWidth, this.ytHeight);
252
- }
253
- }
254
- }
255
- else {
256
- d = this.getYouTubeDimensions(youTubeId, youTubeContainerId);
257
- if (d) {
258
- width = d[0];
259
- height = d[1];
260
- if (width > 0 && height > 0) {
261
- this.aspectRatio = width / height;
262
- this.ytWidth = width;
263
- this.ytHeight = height;
264
- if (width !== this.$ableWrapper.width()) {
265
- // now that we've retrieved YouTube's default width,
266
- // need to adjust to fit the current player wrapper
267
- width = this.$ableWrapper.width();
268
- height = Math.round(width / this.aspectRatio);
269
- if (this.youTubePlayer) {
270
- this.youTubePlayer.setSize(width, height);
271
- }
272
- }
273
- }
274
- }
275
- }
276
- };
277
-
278
- AblePlayer.prototype.setupYouTubeCaptions = function () {
279
-
280
- // called from setupAltCaptions if player is YouTube and there are no <track> captions
281
-
282
- // use YouTube Data API to get caption data from YouTube
283
- // function is called only if these conditions are met:
284
- // 1. this.player === 'youtube'
285
- // 2. there are no <track> elements with kind="captions"
286
- // 3. youTubeDataApiKey is defined
287
-
288
- var deferred = new $.Deferred();
289
- var promise = deferred.promise();
290
-
291
- var thisObj, googleApiPromise, youTubeId, i;
292
-
293
- thisObj = this;
294
-
295
- // this.ytCaptions has the same structure as this.captions
296
- // but unfortunately does not contain cues
297
- // Google *does* offer a captions.download service for downloading captions in WebVTT
298
- // https://developers.google.com/youtube/v3/docs/captions/download
299
- // However, this requires OAUTH 2.0 (user must login and give consent)
300
- // So, for now the best we can do is create an array of available caption/subtitle tracks
301
- // and provide a button & popup menu to allow users to control them
302
- this.ytCaptions = [];
303
-
304
- // if a described version is available && user prefers desription
305
- // Use the described version, and get its captions
306
- if (this.youTubeDescId && this.prefDesc) {
307
- youTubeId = this.youTubeDescId;
308
- }
309
- else {
310
- youTubeId = this.youTubeId;
311
- }
312
-
313
- // Wait until Google Client API is loaded
314
- // When loaded, it sets global var googleApiReady to true
315
-
316
- // Thanks to Paul Tavares for $.doWhen()
317
- // https://gist.github.com/purtuga/8257269
318
- $.doWhen({
319
- when: function(){
320
- return googleApiReady;
321
- },
322
- interval: 100, // ms
323
- attempts: 1000
324
- })
325
- .done(function(){
326
- thisObj.getYouTubeCaptionData(youTubeId).done(function() {
327
- deferred.resolve();
328
- });
329
- })
330
- .fail(function(){
331
- console.log('Unable to initialize Google API. YouTube captions are currently unavailable.');
332
- });
333
-
334
- return promise;
335
- };
336
-
337
- AblePlayer.prototype.getYouTubeCaptionData = function (youTubeId) {
338
- // get data via YouTube Data API, and push data to this.ytCaptions
339
- var deferred = new $.Deferred();
340
- var promise = deferred.promise();
341
-
342
- var thisObj, i, trackId, trackLang, trackLabel, trackKind, isDraft, isDefaultTrack;
343
-
344
- thisObj = this;
345
- gapi.client.setApiKey(youTubeDataAPIKey);
346
- gapi.client
347
- .load('youtube', 'v3')
348
- .then(function() {
349
- var request = gapi.client.youtube.captions.list({
350
- 'part': 'id, snippet',
351
- 'videoId': youTubeId
352
- });
353
- request.then(function(json) {
354
- if (json.result.items.length) { // video has captions!
355
- thisObj.hasCaptions = true;
356
- thisObj.usingYouTubeCaptions = true;
357
- if (thisObj.prefCaptions === 1) {
358
- thisObj.captionsOn = true;
359
- }
360
- else {
361
- thisObj.captionsOn = false;
362
- }
363
- // Step through results and add them to cues array
364
- for (i=0; i < json.result.items.length; i++) {
365
-
366
- trackId = json.result.items[i].id;
367
- trackLabel = json.result.items[i].snippet.name; // always seems to be empty
368
- trackLang = json.result.items[i].snippet.language;
369
- trackKind = json.result.items[i].snippet.trackKind; // ASR, standard, forced
370
- isDraft = json.result.items[i].snippet.isDraft; // Boolean
371
- // Other variables that could potentially be collected from snippet:
372
- // isCC - Boolean, always seems to be false
373
- // isLarge - Boolean
374
- // isEasyReader - Boolean
375
- // isAutoSynced Boolean
376
- // status - string, always seems to be "serving"
377
-
378
- if (trackKind !== 'ASR' && !isDraft) {
379
-
380
- // if track name is empty (it always seems to be), assign a name based on trackLang
381
- if (trackLabel === '') {
382
- trackLabel = thisObj.getLanguageName(trackLang);
383
- }
384
-
385
- // assign the default track based on language of the player
386
- if (trackLang === thisObj.lang) {
387
- isDefaultTrack = true;
388
- }
389
- else {
390
- isDefaultTrack = false;
391
- }
392
-
393
- thisObj.ytCaptions.push({
394
- 'language': trackLang,
395
- 'label': trackLabel,
396
- 'def': isDefaultTrack
397
- });
398
- }
399
- }
400
- // setupPopups again with new ytCaptions array, replacing original
401
- thisObj.setupPopups('captions');
402
- deferred.resolve();
403
- }
404
- else {
405
- thisObj.hasCaptions = false;
406
- thisObj.usingYouTubeCaptions = false;
407
- deferred.resolve();
408
- }
409
- }, function (reason) {
410
- console.log('Error: ' + reason.result.error.message);
411
- });
412
- });
413
- return promise;
414
- };
415
-
416
- AblePlayer.prototype.initYouTubeCaptionModule = function () {
417
- // This function is called when YouTube onApiChange event fires
418
- // to indicate that the player has loaded (or unloaded) a module with exposed API methods
419
- // it isn't fired until the video starts playing
420
- // and only fires if captions are available for this video (automated captions don't count)
421
- // If no captions are available, onApichange event never fires & this function is never called
422
-
423
- // YouTube iFrame API documentation is incomplete related to captions
424
- // Found undocumented features on user forums and by playing around
425
- // Details are here: http://terrillthompson.com/blog/648
426
- // Summary:
427
- // User might get either the AS3 (Flash) or HTML5 YouTube player
428
- // The API uses a different caption module for each player (AS3 = 'cc'; HTML5 = 'captions')
429
- // There are differences in the data and methods available through these modules
430
- // This function therefore is used to determine which captions module is being used
431
- // If it's a known module, this.ytCaptionModule will be used elsewhere to control captions
432
- var options, fontSize, displaySettings;
433
-
434
- options = this.youTubePlayer.getOptions();
435
- if (options.length) {
436
- for (var i=0; i<options.length; i++) {
437
- if (options[i] == 'cc') { // this is the AS3 (Flash) player
438
- this.ytCaptionModule = 'cc';
439
- if (!this.hasCaptions) {
440
- // there are captions available via other sources (e.g., <track>)
441
- // so use these
442
- this.hasCaptions = true;
443
- this.usingYouTubeCaptions = true;
444
- }
445
- break;
446
- }
447
- else if (options[i] == 'captions') { // this is the HTML5 player
448
- this.ytCaptionModule = 'captions';
449
- if (!this.hasCaptions) {
450
- // there are captions available via other sources (e.g., <track>)
451
- // so use these
452
- this.hasCaptions = true;
453
- this.usingYouTubeCaptions = true;
454
- }
455
- break;
456
- }
457
- }
458
- if (typeof this.ytCaptionModule !== 'undefined') {
459
- if (this.usingYouTubeCaptions) {
460
- // set default languaage
461
- this.youTubePlayer.setOption(this.ytCaptionModule, 'track', {'languageCode': this.captionLang});
462
- // set font size using Able Player prefs (values are -1, 0, 1, 2, and 3, where 0 is default)
463
- this.youTubePlayer.setOption(this.ytCaptionModule,'fontSize',this.translatePrefs('size',this.prefCaptionsSize,'youtube'));
464
- // ideally could set other display options too, but no others seem to be supported by setOption()
465
- }
466
- else {
467
- // now that we know which cc module was loaded, unload it!
468
- // we don't want it if we're using local <track> elements for captions
469
- this.youTubePlayer.unloadModule(this.ytCaptionModule)
470
- }
471
- }
472
- }
473
- else {
474
- // no modules were loaded onApiChange
475
- // unfortunately, gonna have to disable captions if we can't control them
476
- this.hasCaptions = false;
477
- this.usingYouTubeCaptions = false;
478
- }
479
- this.refreshControls();
480
- };
3
+ AblePlayer.prototype.initYouTubePlayer = function () {
4
+
5
+ var thisObj, deferred, promise, youTubeId, googleApiPromise, json;
6
+ thisObj = this;
7
+
8
+ deferred = new $.Deferred();
9
+ promise = deferred.promise();
10
+
11
+ // if a described version is available && user prefers desription
12
+ // init player using the described version
13
+ if (this.youTubeDescId && this.prefDesc) {
14
+ youTubeId = this.youTubeDescId;
15
+ }
16
+ else {
17
+ youTubeId = this.youTubeId;
18
+ }
19
+ this.activeYouTubeId = youTubeId;
20
+ if (AblePlayer.youtubeIframeAPIReady) {
21
+ // Script already loaded and ready.
22
+ this.finalizeYoutubeInit().then(function() {
23
+ deferred.resolve();
24
+ });
25
+ }
26
+ else {
27
+ // Has another player already started loading the script? If so, abort...
28
+ if (!AblePlayer.loadingYoutubeIframeAPI) {
29
+ $.getScript('https://www.youtube.com/iframe_api').fail(function () {
30
+ deferred.fail();
31
+ });
32
+ }
33
+
34
+ // Otherwise, keeping waiting for script load event...
35
+ $('body').on('youtubeIframeAPIReady', function () {
36
+ thisObj.finalizeYoutubeInit().then(function() {
37
+ deferred.resolve();
38
+ });
39
+ });
40
+ }
41
+ return promise;
42
+ };
43
+
44
+ AblePlayer.prototype.finalizeYoutubeInit = function () {
45
+
46
+ // This is called once we're sure the Youtube iFrame API is loaded -- see above
47
+ var deferred, promise, thisObj, containerId, ccLoadPolicy, videoDimensions, autoplay;
48
+
49
+ deferred = new $.Deferred();
50
+ promise = deferred.promise();
51
+
52
+ thisObj = this;
53
+
54
+ containerId = this.mediaId + '_youtube';
55
+
56
+ this.$mediaContainer.prepend($('<div>').attr('id', containerId));
57
+ // NOTE: Tried the following in place of the above in January 2016
58
+ // because in some cases two videos were being added to the DOM
59
+ // However, once v2.2.23 was fairly stable, unable to reproduce that problem
60
+ // so maybe it's not an issue. This is preserved here temporarily, just in case it's needed...
61
+ // thisObj.$mediaContainer.html($('<div>').attr('id', containerId));
62
+
63
+ // cc_load_policy:
64
+ // 0 - show captions depending on user's preference on YouTube
65
+ // 1 - show captions by default, even if the user has turned them off
66
+ // For Able Player, init player with value of 0
67
+ // and will turn them on or off after player is initialized
68
+ // based on availability of local tracks and user's Able Player prefs
69
+ ccLoadPolicy = 0;
70
+
71
+ videoDimensions = this.getYouTubeDimensions(this.activeYouTubeId, containerId);
72
+ if (videoDimensions) {
73
+ this.ytWidth = videoDimensions[0];
74
+ this.ytHeight = videoDimensions[1];
75
+ this.aspectRatio = thisObj.ytWidth / thisObj.ytHeight;
76
+ }
77
+ else {
78
+ // dimensions are initially unknown
79
+ // sending null values to YouTube results in a video that uses the default YouTube dimensions
80
+ // these can then be scraped from the iframe and applied to this.$ableWrapper
81
+ this.ytWidth = null;
82
+ this.ytHeight = null;
83
+ }
84
+
85
+ if (this.okToPlay) {
86
+ autoplay = 1;
87
+ }
88
+ else {
89
+ autoplay = 0;
90
+ }
91
+
92
+ // NOTE: YouTube is changing the following parameters on or after Sep 25, 2018:
93
+ // rel - No longer able to prevent YouTube from showing related videos
94
+ // value of 0 now limits related videos to video's same channel
95
+ // showinfo - No longer supported (previously, value of 0 hid title, share, & watch later buttons
96
+ // Documentation https://developers.google.com/youtube/player_parameters
97
+
98
+ this.youTubePlayer = new YT.Player(containerId, {
99
+ videoId: this.activeYouTubeId,
100
+ host: this.youTubeNoCookie ? 'https://www.youtube-nocookie.com' : 'https://www.youtube.com',
101
+ width: this.ytWidth,
102
+ height: this.ytHeight,
103
+ playerVars: {
104
+ autoplay: autoplay,
105
+ enablejsapi: 1,
106
+ disableKb: 1, // disable keyboard shortcuts, using our own
107
+ playsinline: this.playsInline,
108
+ start: this.startTime,
109
+ controls: 0, // no controls, using our own
110
+ cc_load_policy: ccLoadPolicy,
111
+ hl: this.lang, // use the default language UI
112
+ modestbranding: 1, // no YouTube logo in controller
113
+ rel: 0, // do not show related videos when video ends
114
+ html5: 1, // force html5 if browser supports it (undocumented parameter; 0 does NOT force Flash)
115
+ iv_load_policy: 3 // do not show video annotations
116
+ },
117
+ events: {
118
+ onReady: function () {
119
+ if (thisObj.swappingSrc) {
120
+ // swap is now complete
121
+ thisObj.swappingSrc = false;
122
+ thisObj.cueingPlaylistItem = false;
123
+ if (thisObj.playing) {
124
+ // resume playing
125
+ thisObj.playMedia();
126
+ }
127
+ }
128
+ if (thisObj.userClickedPlaylist) {
129
+ thisObj.userClickedPlaylist = false; // reset
130
+ }
131
+ if (typeof thisObj.aspectRatio === 'undefined') {
132
+ thisObj.resizeYouTubePlayer(thisObj.activeYouTubeId, containerId);
133
+ }
134
+ deferred.resolve();
135
+ },
136
+ onError: function (x) {
137
+ deferred.fail();
138
+ },
139
+ onStateChange: function (x) {
140
+ thisObj.getPlayerState().then(function(playerState) {
141
+ // values of playerState: 'playing','paused','buffering','ended'
142
+ if (playerState === 'playing') {
143
+ thisObj.playing = true;
144
+ thisObj.startedPlaying = true;
145
+ thisObj.paused = false;
146
+ }
147
+ else if (playerState == 'ended') {
148
+ thisObj.onMediaComplete();
149
+ }
150
+ else {
151
+ thisObj.playing = false;
152
+ thisObj.paused = true;
153
+ }
154
+ if (thisObj.stoppingYouTube && playerState === 'paused') {
155
+ if (typeof thisObj.$posterImg !== 'undefined') {
156
+ thisObj.$posterImg.show();
157
+ }
158
+ thisObj.stoppingYouTube = false;
159
+ thisObj.seeking = false;
160
+ thisObj.playing = false;
161
+ thisObj.paused = true;
162
+ }
163
+ });
164
+ },
165
+ onPlaybackQualityChange: function () {
166
+ // do something
167
+ },
168
+ onApiChange: function (x) {
169
+ // As of Able Player v2.2.23, we are now getting caption data via the YouTube Data API
170
+ // prior to calling initYouTubePlayer()
171
+ // Previously we got caption data via the YouTube iFrame API, and doing so was an awful mess.
172
+ // onApiChange fires to indicate that the player has loaded (or unloaded) a module with exposed API methods
173
+ // it isn't fired until the video starts playing
174
+ // if captions are available for this video (automated captions don't count)
175
+ // the 'captions' (or 'cc') module is loaded. If no captions are available, this event never fires
176
+ // So, to trigger this event we had to play the video briefly, then pause, then reset.
177
+ // During that brief moment of playback, the onApiChange event was fired and we could setup captions
178
+ // The 'captions' and 'cc' modules are very different, and have different data and methods
179
+ // NOW, in v2.2.23, we still need to initialize the caption modules in order to control captions
180
+ // but we don't have to do that on load in order to get caption data
181
+ // Instead, we can wait until the video starts playing normally, then retrieve the modules
182
+ thisObj.initYouTubeCaptionModule();
183
+ }
184
+ }
185
+ });
186
+
187
+ this.injectPoster(this.$mediaContainer, 'youtube');
188
+ if (!this.hasPlaylist) {
189
+ // remove the media element, since YouTube replaces that with its own element in an iframe
190
+ // this is handled differently for playlists. See buildplayer.js > cuePlaylistItem()
191
+ this.$media.remove();
192
+ }
193
+ return promise;
194
+ };
195
+
196
+ AblePlayer.prototype.getYouTubeDimensions = function (youTubeContainerId) {
197
+
198
+ // get dimensions of YouTube video, return array with width & height
199
+ // Sources, in order of priority:
200
+ // 1. The width and height attributes on <video>
201
+ // 2. YouTube (not yet supported; can't seem to get this data via YouTube Data API without OAuth!)
202
+
203
+ var d, url, $iframe, width, height;
204
+
205
+ d = [];
206
+
207
+ if (typeof this.playerMaxWidth !== 'undefined') {
208
+ d[0] = this.playerMaxWidth;
209
+ // optional: set height as well; not required though since YouTube will adjust height to match width
210
+ if (typeof this.playerMaxHeight !== 'undefined') {
211
+ d[1] = this.playerMaxHeight;
212
+ }
213
+ return d;
214
+ }
215
+ else {
216
+ if (typeof $('#' + youTubeContainerId) !== 'undefined') {
217
+ $iframe = $('#' + youTubeContainerId);
218
+ width = $iframe.width();
219
+ height = $iframe.height();
220
+ if (width > 0 && height > 0) {
221
+ d[0] = width;
222
+ d[1] = height;
223
+ return d;
224
+ }
225
+ }
226
+ }
227
+ return false;
228
+ };
229
+
230
+ AblePlayer.prototype.resizeYouTubePlayer = function(youTubeId, youTubeContainerId) {
231
+
232
+ // called after player is ready, if youTube dimensions were previously unknown
233
+ // Now need to get them from the iframe element that YouTube injected
234
+ // and resize Able Player to match
235
+ var d, width, height;
236
+ if (typeof this.aspectRatio !== 'undefined') {
237
+ // video dimensions have already been collected
238
+ if (this.restoringAfterFullScreen) {
239
+ // restore using saved values
240
+ if (this.youTubePlayer) {
241
+ this.youTubePlayer.setSize(this.ytWidth, this.ytHeight);
242
+ }
243
+ this.restoringAfterFullScreen = false;
244
+ }
245
+ else {
246
+ // recalculate with new wrapper size
247
+ width = this.$ableWrapper.parent().width();
248
+ height = Math.round(width / this.aspectRatio);
249
+ this.$ableWrapper.css({
250
+ 'max-width': width + 'px',
251
+ 'width': ''
252
+ });
253
+ this.youTubePlayer.setSize(width, height);
254
+ if (this.fullscreen) {
255
+ this.youTubePlayer.setSize(width, height);
256
+ }
257
+ else {
258
+ // resizing due to a change in window size, not full screen
259
+ this.youTubePlayer.setSize(this.ytWidth, this.ytHeight);
260
+ }
261
+ }
262
+ }
263
+ else {
264
+ d = this.getYouTubeDimensions(youTubeContainerId);
265
+ if (d) {
266
+ width = d[0];
267
+ height = d[1];
268
+ if (width > 0 && height > 0) {
269
+ this.aspectRatio = width / height;
270
+ this.ytWidth = width;
271
+ this.ytHeight = height;
272
+ if (width !== this.$ableWrapper.width()) {
273
+ // now that we've retrieved YouTube's default width,
274
+ // need to adjust to fit the current player wrapper
275
+ width = this.$ableWrapper.width();
276
+ height = Math.round(width / this.aspectRatio);
277
+ if (this.youTubePlayer) {
278
+ this.youTubePlayer.setSize(width, height);
279
+ }
280
+ }
281
+ }
282
+ }
283
+ }
284
+ };
285
+
286
+ AblePlayer.prototype.setupYouTubeCaptions = function () {
287
+
288
+ // called from setupAltCaptions if player is YouTube and there are no <track> captions
289
+
290
+ // use YouTube Data API to get caption data from YouTube
291
+ // function is called only if these conditions are met:
292
+ // 1. this.player === 'youtube'
293
+ // 2. there are no <track> elements with kind="captions"
294
+ // 3. youTubeDataApiKey is defined
295
+
296
+ var deferred = new $.Deferred();
297
+ var promise = deferred.promise();
298
+
299
+ var thisObj, googleApiPromise, youTubeId, i;
300
+
301
+ thisObj = this;
302
+
303
+ // if a described version is available && user prefers desription
304
+ // Use the described version, and get its captions
305
+ if (this.youTubeDescId && this.prefDesc) {
306
+ youTubeId = this.youTubeDescId;
307
+ }
308
+ else {
309
+ youTubeId = this.youTubeId;
310
+ }
311
+
312
+ if (typeof youTubeDataAPIKey !== 'undefined') {
313
+ // Wait until Google Client API is loaded
314
+ // When loaded, it sets global var googleApiReady to true
315
+
316
+ // Thanks to Paul Tavares for $.doWhen()
317
+ // https://gist.github.com/purtuga/8257269
318
+ $.doWhen({
319
+ when: function(){
320
+ return googleApiReady;
321
+ },
322
+ interval: 100, // ms
323
+ attempts: 1000
324
+ })
325
+ .done(function(){
326
+ deferred.resolve();
327
+ })
328
+ .fail(function(){
329
+ console.log('Unable to initialize Google API. YouTube captions are currently unavailable.');
330
+ });
331
+ }
332
+ else {
333
+ deferred.resolve();
334
+ }
335
+ return promise;
336
+ };
337
+
338
+ AblePlayer.prototype.waitForGapi = function () {
339
+
340
+ // wait for Google API to initialize
341
+
342
+ var thisObj, deferred, promise, maxWaitTime, maxTries, tries, timer, interval;
343
+
344
+ thisObj = this;
345
+ deferred = new $.Deferred();
346
+ promise = deferred.promise();
347
+ maxWaitTime = 5000; // 5 seconds
348
+ maxTries = 100; // number of tries during maxWaitTime
349
+ tries = 0;
350
+ interval = Math.floor(maxWaitTime/maxTries);
351
+
352
+ timer = setInterval(function() {
353
+ tries++;
354
+ if (googleApiReady || tries >= maxTries) {
355
+ clearInterval(timer);
356
+ if (googleApiReady) { // success!
357
+ deferred.resolve(true);
358
+ }
359
+ else { // tired of waiting
360
+ deferred.resolve(false);
361
+ }
362
+ }
363
+ else {
364
+ thisObj.waitForGapi();
365
+ }
366
+ }, interval);
367
+ return promise;
368
+ };
369
+
370
+ AblePlayer.prototype.getYouTubeCaptionTracks = function (youTubeId) {
371
+
372
+ // get data via YouTube Data API, and push data to this.captions
373
+ var deferred = new $.Deferred();
374
+ var promise = deferred.promise();
375
+
376
+ var thisObj, useGoogleApi, i, trackId, trackLang, trackName, trackLabel, trackKind, isDraft, isDefaultTrack;
377
+
378
+ thisObj = this;
379
+
380
+ if (typeof youTubeDataAPIKey !== 'undefined') {
381
+ this.waitForGapi().then(function(waitResult) {
382
+
383
+ useGoogleApi = waitResult;
384
+
385
+ // useGoogleApi returns false if API failed to initalize after max wait time
386
+ // Proceed only if true. Otherwise can still use fallback method (see else loop below)
387
+ if (useGoogleApi === true) {
388
+ gapi.client.setApiKey(youTubeDataAPIKey);
389
+ gapi.client
390
+ .load('youtube', 'v3')
391
+ .then(function() {
392
+ var request = gapi.client.youtube.captions.list({
393
+ 'part': 'id, snippet',
394
+ 'videoId': youTubeId
395
+ });
396
+ request.then(function(json) {
397
+ if (json.result.items.length) { // video has captions!
398
+ thisObj.hasCaptions = true;
399
+ thisObj.usingYouTubeCaptions = true;
400
+ if (thisObj.prefCaptions === 1) {
401
+ thisObj.captionsOn = true;
402
+ }
403
+ else {
404
+ thisObj.captionsOn = false;
405
+ }
406
+ // Step through results and add them to cues array
407
+ for (i=0; i < json.result.items.length; i++) {
408
+ trackName = json.result.items[i].snippet.name; // usually seems to be empty
409
+ trackLang = json.result.items[i].snippet.language;
410
+ trackKind = json.result.items[i].snippet.trackKind; // ASR, standard, forced
411
+ isDraft = json.result.items[i].snippet.isDraft; // Boolean
412
+ // Other variables that could potentially be collected from snippet:
413
+ // isCC - Boolean, always seems to be false
414
+ // isLarge - Boolean
415
+ // isEasyReader - Boolean
416
+ // isAutoSynced Boolean
417
+ // status - string, always seems to be "serving"
418
+
419
+ var srcUrl = thisObj.getYouTubeTimedTextUrl(youTubeId,trackName,trackLang);
420
+ if (trackKind !== 'ASR' && !isDraft) {
421
+
422
+ if (trackName !== '') {
423
+ trackLabel = trackName;
424
+ }
425
+ else {
426
+ // if track name is empty (it always seems to be), assign a label based on trackLang
427
+ trackLabel = thisObj.getLanguageName(trackLang);
428
+ }
429
+
430
+ // assign the default track based on language of the player
431
+ if (trackLang === thisObj.lang) {
432
+ isDefaultTrack = true;
433
+ }
434
+ else {
435
+ isDefaultTrack = false;
436
+ }
437
+ thisObj.tracks.push({
438
+ 'kind': 'captions',
439
+ 'src': srcUrl,
440
+ 'language': trackLang,
441
+ 'label': trackLabel,
442
+ 'def': isDefaultTrack
443
+ });
444
+ }
445
+ }
446
+ // setupPopups again with new captions array, replacing original
447
+ thisObj.setupPopups('captions');
448
+ deferred.resolve();
449
+ }
450
+ else {
451
+ thisObj.hasCaptions = false;
452
+ thisObj.usingYouTubeCaptions = false;
453
+ deferred.resolve();
454
+ }
455
+ }, function (reason) {
456
+ // If video has no captions, YouTube returns an error.
457
+ // Should still proceed, but with captions disabled
458
+ // The specific error, if needed: reason.result.error.message
459
+ // If no captions, the error is: "The video identified by the <code>videoId</code> parameter could not be found."
460
+ console.log('Error retrieving captions.');
461
+ console.log('Check your video on YouTube to be sure captions are available and published.');
462
+ thisObj.hasCaptions = false;
463
+ thisObj.usingYouTubeCaptions = false;
464
+ deferred.resolve();
465
+ });
466
+ })
467
+ }
468
+ else {
469
+ // googleAPi never loaded.
470
+ this.getYouTubeCaptionTracks2(youTubeId).then(function() {
471
+ deferred.resolve();
472
+ });
473
+ }
474
+ });
475
+ }
476
+ else {
477
+ // web owner hasn't provided a Google API key
478
+ // attempt to get YouTube captions via the backup method
479
+ this.getYouTubeCaptionTracks2(youTubeId).then(function() {
480
+ deferred.resolve();
481
+ });
482
+ }
483
+ return promise;
484
+ };
485
+
486
+ AblePlayer.prototype.getYouTubeCaptionTracks2 = function (youTubeId) {
487
+
488
+ // Use alternative backup method of getting caption tracks from YouTube
489
+ // and pushing them to this.captions
490
+ // Called from getYouTubeCaptionTracks if no Google API key is defined
491
+ // or if Google API failed to initiatlize
492
+ // This method seems to be undocumented, but is referenced on StackOverflow
493
+ // We'll use that as a fallback but it could break at any moment
494
+
495
+ var deferred = new $.Deferred();
496
+ var promise = deferred.promise();
497
+
498
+ var thisObj, useGoogleApi, i, trackId, trackLang, trackName, trackLabel, trackKind, isDraft, isDefaultTrack;
499
+
500
+ thisObj = this;
501
+
502
+ $.ajax({
503
+ type: 'get',
504
+ url: 'https://www.youtube.com/api/timedtext?type=list&v=' + youTubeId,
505
+ dataType: 'xml',
506
+ success: function(xml) {
507
+ var $tracks = $(xml).find('track');
508
+ if ($tracks.length > 0) { // video has captions!
509
+ thisObj.hasCaptions = true;
510
+ thisObj.usingYouTubeCaptions = true;
511
+ if (thisObj.prefCaptions === 1) {
512
+ thisObj.captionsOn = true;
513
+ }
514
+ else {
515
+ thisObj.captionsOn = false;
516
+ }
517
+ // Step through results and add them to tracks array
518
+ $tracks.each(function() {
519
+ trackId = $(this).attr('id');
520
+ trackLang = $(this).attr('lang_code');
521
+ if ($(this).attr('name') !== '') {
522
+ trackName = $(this).attr('name');
523
+ trackLabel = trackName;
524
+ }
525
+ else {
526
+ // @name is typically null except for default track
527
+ // but lang_translated seems to be reliable
528
+ trackName = '';
529
+ trackLabel = $(this).attr('lang_translated');
530
+ }
531
+ if (trackLabel === '') {
532
+ trackLabel = thisObj.getLanguageName(trackLang);
533
+ }
534
+ // assign the default track based on language of the player
535
+ if (trackLang === thisObj.lang) {
536
+ isDefaultTrack = true;
537
+ }
538
+ else {
539
+ isDefaultTrack = false;
540
+ }
541
+
542
+ // Build URL for retrieving WebVTT source via YouTube's timedtext API
543
+ var srcUrl = thisObj.getYouTubeTimedTextUrl(youTubeId,trackName,trackLang);
544
+ thisObj.tracks.push({
545
+ 'kind': 'captions',
546
+ 'src': srcUrl,
547
+ 'language': trackLang,
548
+ 'label': trackLabel,
549
+ 'def': isDefaultTrack
550
+ });
551
+
552
+ });
553
+ // setupPopups again with new captions array, replacing original
554
+ thisObj.setupPopups('captions');
555
+ deferred.resolve();
556
+ }
557
+ else {
558
+ thisObj.hasCaptions = false;
559
+ thisObj.usingYouTubeCaptions = false;
560
+ deferred.resolve();
561
+ }
562
+ },
563
+ error: function(xhr, status) {
564
+ console.log('Error retrieving YouTube caption data for video ' + youTubeId);
565
+ deferred.resolve();
566
+ }
567
+ });
568
+ return promise;
569
+ };
570
+
571
+ AblePlayer.prototype.getYouTubeTimedTextUrl = function (youTubeId, trackName, trackLang) {
572
+
573
+ // return URL for retrieving WebVTT source via YouTube's timedtext API
574
+ // Note: This API seems to be undocumented, and could break anytime
575
+ var url = 'https://www.youtube.com/api/timedtext?fmt=vtt';
576
+ url += '&v=' + youTubeId;
577
+ url += '&lang=' + trackLang;
578
+ // if track has a value in the name field, it's *required* in the URL
579
+ if (trackName !== '') {
580
+ url += '&name=' + trackName;
581
+ }
582
+ return url;
583
+ };
584
+
585
+
586
+ AblePlayer.prototype.getYouTubeCaptionCues = function (youTubeId) {
587
+
588
+ var deferred, promise, thisObj;
589
+
590
+ var deferred = new $.Deferred();
591
+ var promise = deferred.promise();
592
+
593
+ thisObj = this;
594
+
595
+ this.tracks = [];
596
+ this.tracks.push({
597
+ 'kind': 'captions',
598
+ 'src': 'some_file.vtt',
599
+ 'language': 'en',
600
+ 'label': 'Fake English captions'
601
+ });
602
+
603
+ deferred.resolve();
604
+ return promise;
605
+ };
606
+
607
+ AblePlayer.prototype.initYouTubeCaptionModule = function () {
608
+
609
+ // This function is called when YouTube onApiChange event fires
610
+ // to indicate that the player has loaded (or unloaded) a module with exposed API methods
611
+ // it isn't fired until the video starts playing
612
+ // and only fires if captions are available for this video (automated captions don't count)
613
+ // If no captions are available, onApichange event never fires & this function is never called
614
+
615
+ // YouTube iFrame API documentation is incomplete related to captions
616
+ // Found undocumented features on user forums and by playing around
617
+ // Details are here: http://terrillthompson.com/blog/648
618
+ // Summary:
619
+ // User might get either the AS3 (Flash) or HTML5 YouTube player
620
+ // The API uses a different caption module for each player (AS3 = 'cc'; HTML5 = 'captions')
621
+ // There are differences in the data and methods available through these modules
622
+ // This function therefore is used to determine which captions module is being used
623
+ // If it's a known module, this.ytCaptionModule will be used elsewhere to control captions
624
+ var options, fontSize, displaySettings;
625
+
626
+ options = this.youTubePlayer.getOptions();
627
+ if (options.length) {
628
+ for (var i=0; i<options.length; i++) {
629
+ if (options[i] == 'cc') { // this is the AS3 (Flash) player
630
+ this.ytCaptionModule = 'cc';
631
+ if (!this.hasCaptions) {
632
+ // there are captions available via other sources (e.g., <track>)
633
+ // so use these
634
+ this.hasCaptions = true;
635
+ this.usingYouTubeCaptions = true;
636
+ }
637
+ break;
638
+ }
639
+ else if (options[i] == 'captions') { // this is the HTML5 player
640
+ this.ytCaptionModule = 'captions';
641
+ if (!this.hasCaptions) {
642
+ // there are captions available via other sources (e.g., <track>)
643
+ // so use these
644
+ this.hasCaptions = true;
645
+ this.usingYouTubeCaptions = true;
646
+ }
647
+ break;
648
+ }
649
+ }
650
+ if (typeof this.ytCaptionModule !== 'undefined') {
651
+ if (this.usingYouTubeCaptions) {
652
+ // set default languaage
653
+ this.youTubePlayer.setOption(this.ytCaptionModule, 'track', {'languageCode': this.captionLang});
654
+ // set font size using Able Player prefs (values are -1, 0, 1, 2, and 3, where 0 is default)
655
+ this.youTubePlayer.setOption(this.ytCaptionModule,'fontSize',this.translatePrefs('size',this.prefCaptionsSize,'youtube'));
656
+ // ideally could set other display options too, but no others seem to be supported by setOption()
657
+ }
658
+ else {
659
+ // now that we know which cc module was loaded, unload it!
660
+ // we don't want it if we're using local <track> elements for captions
661
+ this.youTubePlayer.unloadModule(this.ytCaptionModule)
662
+ }
663
+ }
664
+ }
665
+ else {
666
+ // no modules were loaded onApiChange
667
+ // unfortunately, gonna have to disable captions if we can't control them
668
+ this.hasCaptions = false;
669
+ this.usingYouTubeCaptions = false;
670
+ }
671
+ this.refreshControls('captions');
672
+ };
673
+
674
+ AblePlayer.prototype.getYouTubePosterUrl = function (youTubeId, width) {
675
+
676
+ // return a URL for retrieving a YouTube poster image
677
+ // supported values of width: 120, 320, 480, 640
678
+
679
+ var url = 'https://img.youtube.com/vi/' + youTubeId;
680
+ if (width == '120') {
681
+ // default (small) thumbnail, 120 x 90
682
+ return url + '/default.jpg';
683
+ }
684
+ else if (width == '320') {
685
+ // medium quality thumbnail, 320 x 180
686
+ return url + '/hqdefault.jpg';
687
+ }
688
+ else if (width == '480') {
689
+ // high quality thumbnail, 480 x 360
690
+ return url + '/hqdefault.jpg';
691
+ }
692
+ else if (width == '640') {
693
+ // standard definition poster image, 640 x 480
694
+ return url + '/sddefault.jpg';
695
+ }
696
+ return false;
697
+ };
481
698
 
482
699
  })(jQuery);