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