-
Notifications
You must be signed in to change notification settings - Fork 7.5k
/
player.js
2258 lines (1941 loc) · 67.3 KB
/
player.js
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
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// Subclasses Component
import Component from './component.js';
import document from 'global/document';
import window from 'global/window';
import * as Events from './utils/events.js';
import * as Dom from './utils/dom.js';
import * as Fn from './utils/fn.js';
import * as Guid from './utils/guid.js';
import * as browser from './utils/browser.js';
import log from './utils/log.js';
import toTitleCase from './utils/to-title-case.js';
import { createTimeRange } from './utils/time-ranges.js';
import { bufferedPercent } from './utils/buffer.js';
import FullscreenApi from './fullscreen-api.js';
import MediaError from './media-error.js';
import globalOptions from './global-options.js';
import safeParseTuple from 'safe-json-parse/tuple';
import assign from 'object.assign';
import mergeOptions from './utils/merge-options.js';
// Include required child components (importing also registers them)
import MediaLoader from './tech/loader.js';
import PosterImage from './poster-image.js';
import TextTrackDisplay from './tracks/text-track-display.js';
import LoadingSpinner from './loading-spinner.js';
import BigPlayButton from './big-play-button.js';
import ControlBar from './control-bar/control-bar.js';
import ErrorDisplay from './error-display.js';
import TextTrackSettings from './tracks/text-track-settings.js';
// Require html5 tech, at least for disposing the original video tag
import Html5 from './tech/html5.js';
/**
* An instance of the `Player` class is created when any of the Video.js setup methods are used to initialize a video.
*
* ```js
* var myPlayer = videojs('example_video_1');
* ```
*
* In the following example, the `data-setup` attribute tells the Video.js library to create a player instance when the library is ready.
*
* ```html
* <video id="example_video_1" data-setup='{}' controls>
* <source src="my-source.mp4" type="video/mp4">
* </video>
* ```
*
* After an instance has been created it can be accessed globally using `Video('example_video_1')`.
*
* @class
* @extends Component
*/
class Player extends Component {
/**
* player's constructor function
*
* @constructs
* @method init
* @param {Element} tag The original video tag used for configuring options
* @param {Object=} options Player options
* @param {Function=} ready Ready callback function
*/
constructor(tag, options, ready){
// Make sure tag ID exists
tag.id = tag.id || `vjs_video_${Guid.newGUID()}`;
// Set Options
// The options argument overrides options set in the video tag
// which overrides globally set options.
// This latter part coincides with the load order
// (tag must exist before Player)
options = assign(Player.getTagSettings(tag), options);
// Delay the initialization of children because we need to set up
// player properties first, and can't use `this` before `super()`
options.initChildren = false;
// Same with creating the element
options.createEl = false;
// we don't want the player to report touch activity on itself
// see enableTouchActivity in Component
options.reportTouchActivity = false;
// Run base component initializing with new options
super(null, options, ready);
// if the global option object was accidentally blown away by
// someone, bail early with an informative error
if (!this.options_ ||
!this.options_.techOrder ||
!this.options_.techOrder.length) {
throw new Error('No techOrder specified. Did you overwrite ' +
'videojs.options instead of just changing the ' +
'properties you want to override?');
}
this.tag = tag; // Store the original tag used to set options
// Store the tag attributes used to restore html5 element
this.tagAttributes = tag && Dom.getElAttributes(tag);
// Update current language
this.language(options.language || globalOptions.language);
// Update Supported Languages
if (options['languages']) {
// Normalise player option languages to lowercase
let languagesToLower = {};
Object.getOwnPropertyNames(options['languages']).forEach(function(name) {
languagesToLower[name.toLowerCase()] = options['languages'][name];
});
this.languages_ = languagesToLower;
} else {
this.languages_ = globalOptions['languages'];
}
// Cache for video property values.
this.cache_ = {};
// Set poster
this.poster_ = options['poster'] || '';
// Set controls
this.controls_ = !!options['controls'];
// Original tag settings stored in options
// now remove immediately so native controls don't flash.
// May be turned back on by HTML5 tech if nativeControlsForTouch is true
tag.controls = false;
/**
* Store the internal state of scrubbing
* @private
* @return {Boolean} True if the user is scrubbing
*/
this.scrubbing_ = false;
this.el_ = this.createEl();
// Load plugins
if (options['plugins']) {
let plugins = options['plugins'];
Object.getOwnPropertyNames(plugins).forEach(function(name){
this[name](plugins[name]);
}, this);
}
this.initChildren();
// Set isAudio based on whether or not an audio tag was used
this.isAudio(tag.nodeName.toLowerCase() === 'audio');
// Update controls className. Can't do this when the controls are initially
// set because the element doesn't exist yet.
if (this.controls()) {
this.addClass('vjs-controls-enabled');
} else {
this.addClass('vjs-controls-disabled');
}
if (this.isAudio()) {
this.addClass('vjs-audio');
}
if (this.flexNotSupported_()) {
this.addClass('vjs-no-flex');
}
// TODO: Make this smarter. Toggle user state between touching/mousing
// using events, since devices can have both touch and mouse events.
// if (browser.TOUCH_ENABLED) {
// this.addClass('vjs-touch-enabled');
// }
// Make player easily findable by ID
Player.players[this.id_] = this;
// When the player is first initialized, trigger activity so components
// like the control bar show themselves if needed
this.userActive_ = true;
this.reportUserActivity();
this.listenForUserActivity();
this.on('fullscreenchange', this.handleFullscreenChange);
this.on('stageclick', this.handleStageClick);
}
/**
* Destroys the video player and does any necessary cleanup
*
* myPlayer.dispose();
*
* This is especially helpful if you are dynamically adding and removing videos
* to/from the DOM.
*/
dispose() {
this.trigger('dispose');
// prevent dispose from being called twice
this.off('dispose');
// Kill reference to this player
Player.players[this.id_] = null;
if (this.tag && this.tag['player']) { this.tag['player'] = null; }
if (this.el_ && this.el_['player']) { this.el_['player'] = null; }
if (this.tech) { this.tech.dispose(); }
super.dispose();
}
createEl() {
let el = this.el_ = super.createEl('div');
let tag = this.tag;
// Remove width/height attrs from tag so CSS can make it 100% width/height
tag.removeAttribute('width');
tag.removeAttribute('height');
// Copy over all the attributes from the tag, including ID and class
// ID will now reference player box, not the video tag
const attrs = Dom.getElAttributes(tag);
Object.getOwnPropertyNames(attrs).forEach(function(attr){
// workaround so we don't totally break IE7
// http://stackoverflow.com/questions/3653444/css-styles-not-applied-on-dynamic-elements-in-internet-explorer-7
if (attr === 'class') {
el.className = attrs[attr];
} else {
el.setAttribute(attr, attrs[attr]);
}
});
// Update tag id/class for use as HTML5 playback tech
// Might think we should do this after embedding in container so .vjs-tech class
// doesn't flash 100% width/height, but class only applies with .video-js parent
tag.id += '_html5_api';
tag.className = 'vjs-tech';
// Make player findable on elements
tag['player'] = el['player'] = this;
// Default state of video is paused
this.addClass('vjs-paused');
// Add a style element in the player that we'll use to set the width/height
// of the player in a way that's still overrideable by CSS, just like the
// video element
this.styleEl_ = document.createElement('style');
el.appendChild(this.styleEl_);
// Pass in the width/height/aspectRatio options which will update the style el
this.width(this.options_['width']);
this.height(this.options_['height']);
this.fluid(this.options_['fluid']);
this.aspectRatio(this.options_['aspectRatio']);
// insertElFirst seems to cause the networkState to flicker from 3 to 2, so
// keep track of the original for later so we can know if the source originally failed
tag.initNetworkState_ = tag.networkState;
// Wrap video tag in div (el/box) container
if (tag.parentNode) {
tag.parentNode.insertBefore(el, tag);
}
Dom.insertElFirst(tag, el); // Breaks iPhone, fixed in HTML5 setup.
this.el_ = el;
return el;
}
width(value) {
return this.dimension('width', value);
}
height(value) {
return this.dimension('height', value);
}
dimension(dimension, value) {
let privDimension = dimension + '_';
if (value === undefined) {
return this[privDimension] || 0;
}
if (value === '') {
// If an empty string is given, reset the dimension to be automatic
this[privDimension] = undefined;
} else {
let parsedVal = parseFloat(value);
if (isNaN(parsedVal)) {
log.error(`Improper value "${value}" supplied for for ${dimension}`);
return this;
}
this[privDimension] = parsedVal;
}
this.updateStyleEl_();
return this;
}
fluid(bool) {
if (bool === undefined) {
return !!this.fluid_;
}
this.fluid_ = !!bool;
if (bool) {
this.addClass('vjs-fluid');
} else {
this.removeClass('vjs-fluid');
}
}
aspectRatio(ratio) {
if (ratio === undefined) {
return this.aspectRatio_;
}
// Check for width:height format
if (!/^\d+\:\d+$/.test(ratio)) {
throw new Error('Improper value suplied for aspect ratio. The format should be width:height, for example 16:9.');
}
this.aspectRatio_ = ratio;
// We're assuming if you set an aspect ratio you want fluid mode,
// because in fixed mode you could calculate width and height yourself.
this.fluid(true);
this.updateStyleEl_();
}
updateStyleEl_() {
let width;
let height;
let aspectRatio;
// The aspect ratio is either used directly or to calculate width and height.
if (this.aspectRatio_ !== undefined && this.aspectRatio_ !== 'auto') {
// Use any aspectRatio that's been specifically set
aspectRatio = this.aspectRatio_;
} else if (this.videoWidth()) {
// Otherwise try to get the aspect ratio from the video metadata
aspectRatio = this.videoWidth() + ':' + this.videoHeight();
} else {
// Or use a default. The video element's is 2:1, but 16:9 is more common.
aspectRatio = '16:9';
}
// Get the ratio as a decimal we can use to calculate dimensions
let ratioParts = aspectRatio.split(':');
let ratioMultiplier = ratioParts[1] / ratioParts[0];
if (this.width_ !== undefined) {
// Use any width that's been specifically set
width = this.width_;
} else if (this.height_ !== undefined) {
// Or calulate the width from the aspect ratio if a height has been set
width = this.height_ / ratioMultiplier;
} else {
// Or use the video's metadata, or use the video el's default of 300
width = this.videoWidth() || 300;
}
if (this.height_ !== undefined) {
// Use any height that's been specifically set
height = this.height_;
} else {
// Otherwise calculate the height from the ratio and the width
height = width * ratioMultiplier;
}
let idClass = this.id()+'-dimensions';
// Ensure the right class is still on the player for the style element
this.addClass(idClass);
// Create the width/height CSS
var css = `.${idClass} { width: ${width}px; height: ${height}px; }`;
// Add the aspect ratio CSS for when using a fluid layout
css += `.${idClass}.vjs-fluid { padding-top: ${ratioMultiplier * 100}%; }`;
// Update the style el
if (this.styleEl_.styleSheet){
this.styleEl_.styleSheet.cssText = css;
} else {
this.styleEl_.innerHTML = css;
}
}
/**
* Load the Media Playback Technology (tech)
* Load/Create an instance of playback technology including element and API methods
* And append playback element in player div.
*/
loadTech(techName, source) {
// Pause and remove current playback technology
if (this.tech) {
this.unloadTech();
}
// get rid of the HTML5 video tag as soon as we are using another tech
if (techName !== 'Html5' && this.tag) {
Component.getComponent('Html5').disposeMediaElement(this.tag);
this.tag.player = null;
this.tag = null;
}
this.techName = techName;
// Turn off API access because we're loading a new tech that might load asynchronously
this.isReady_ = false;
var techReady = Fn.bind(this, function() {
this.triggerReady();
});
// Grab tech-specific options from player options and add source and parent element to use.
var techOptions = assign({
'source': source,
'playerId': this.id(),
'techId': `${this.id()}_${techName}_api`,
'textTracks': this.textTracks_,
'autoplay': this.options_.autoplay,
'preload': this.options_.preload,
'loop': this.options_.loop,
'muted': this.options_.muted
}, this.options_[techName.toLowerCase()]);
if (this.tag) {
techOptions.tag = this.tag;
}
if (source) {
this.currentType_ = source.type;
if (source.src === this.cache_.src && this.cache_.currentTime > 0) {
techOptions['startTime'] = this.cache_.currentTime;
}
this.cache_.src = source.src;
}
// Initialize tech instance
let techComponent = Component.getComponent(techName);
this.tech = new techComponent(techOptions);
this.on(this.tech, 'ready', this.handleTechReady);
this.on(this.tech, 'usenativecontrols', this.handleTechUseNativeControls);
// Listen to every HTML5 events and trigger them back on the player for the plugins
this.on(this.tech, 'loadstart', this.handleTechLoadStart);
this.on(this.tech, 'waiting', this.handleTechWaiting);
this.on(this.tech, 'canplay', this.handleTechCanPlay);
this.on(this.tech, 'canplaythrough', this.handleTechCanPlayThrough);
this.on(this.tech, 'playing', this.handleTechPlaying);
this.on(this.tech, 'ended', this.handleTechEnded);
this.on(this.tech, 'seeking', this.handleTechSeeking);
this.on(this.tech, 'seeked', this.handleTechSeeked);
this.on(this.tech, 'play', this.handleTechPlay);
this.on(this.tech, 'firstplay', this.handleTechFirstPlay);
this.on(this.tech, 'pause', this.handleTechPause);
this.on(this.tech, 'progress', this.handleTechProgress);
this.on(this.tech, 'durationchange', this.handleTechDurationChange);
this.on(this.tech, 'fullscreenchange', this.handleTechFullscreenChange);
this.on(this.tech, 'error', this.handleTechError);
this.on(this.tech, 'suspend', this.handleTechSuspend);
this.on(this.tech, 'abort', this.handleTechAbort);
this.on(this.tech, 'emptied', this.handleTechEmptied);
this.on(this.tech, 'stalled', this.handleTechStalled);
this.on(this.tech, 'loadedmetadata', this.handleTechLoadedMetaData);
this.on(this.tech, 'loadeddata', this.handleTechLoadedData);
this.on(this.tech, 'timeupdate', this.handleTechTimeUpdate);
this.on(this.tech, 'ratechange', this.handleTechRateChange);
this.on(this.tech, 'volumechange', this.handleTechVolumeChange);
this.on(this.tech, 'texttrackchange', this.onTextTrackChange);
this.on(this.tech, 'loadedmetadata', this.updateStyleEl_);
if (this.controls() && !this.usingNativeControls()) {
this.addTechControlsListeners();
}
// Add the tech element in the DOM if it was not already there
// Make sure to not insert the original video element if using Html5
if (this.tech.el().parentNode !== this.el() && (techName !== 'Html5' || !this.tag)) {
Dom.insertElFirst(this.tech.el(), this.el());
}
// Get rid of the original video tag reference after the first tech is loaded
if (this.tag) {
this.tag.player = null;
this.tag = null;
}
this.tech.ready(techReady);
}
unloadTech() {
// Save the current text tracks so that we can reuse the same text tracks with the next tech
this.textTracks_ = this.textTracks();
this.isReady_ = false;
this.tech.dispose();
this.tech = false;
}
addTechControlsListeners() {
// Some browsers (Chrome & IE) don't trigger a click on a flash swf, but do
// trigger mousedown/up.
// http://stackoverflow.com/questions/1444562/javascript-onclick-event-over-flash-object
// Any touch events are set to block the mousedown event from happening
this.on(this.tech, 'mousedown', this.handleTechClick);
// If the controls were hidden we don't want that to change without a tap event
// so we'll check if the controls were already showing before reporting user
// activity
this.on(this.tech, 'touchstart', this.handleTechTouchStart);
this.on(this.tech, 'touchmove', this.handleTechTouchMove);
this.on(this.tech, 'touchend', this.handleTechTouchEnd);
// The tap listener needs to come after the touchend listener because the tap
// listener cancels out any reportedUserActivity when setting userActive(false)
this.on(this.tech, 'tap', this.handleTechTap);
}
/**
* Remove the listeners used for click and tap controls. This is needed for
* toggling to controls disabled, where a tap/touch should do nothing.
*/
removeTechControlsListeners() {
// We don't want to just use `this.off()` because there might be other needed
// listeners added by techs that extend this.
this.off(this.tech, 'tap', this.handleTechTap);
this.off(this.tech, 'touchstart', this.handleTechTouchStart);
this.off(this.tech, 'touchmove', this.handleTechTouchMove);
this.off(this.tech, 'touchend', this.handleTechTouchEnd);
this.off(this.tech, 'mousedown', this.handleTechClick);
}
/**
* Player waits for the tech to be ready
* @private
*/
handleTechReady() {
this.triggerReady();
// Chrome and Safari both have issues with autoplay.
// In Safari (5.1.1), when we move the video element into the container div, autoplay doesn't work.
// In Chrome (15), if you have autoplay + a poster + no controls, the video gets hidden (but audio plays)
// This fixes both issues. Need to wait for API, so it updates displays correctly
if (this.tag && this.options_.autoplay && this.paused()) {
delete this.tag.poster; // Chrome Fix. Fixed in Chrome v16.
this.play();
}
}
/**
* Fired when the native controls are used
* @private
*/
handleTechUseNativeControls() {
this.usingNativeControls(true);
}
/**
* Fired when the user agent begins looking for media data
* @event loadstart
*/
handleTechLoadStart() {
// TODO: Update to use `emptied` event instead. See #1277.
this.removeClass('vjs-ended');
// reset the error state
this.error(null);
// If it's already playing we want to trigger a firstplay event now.
// The firstplay event relies on both the play and loadstart events
// which can happen in any order for a new source
if (!this.paused()) {
this.trigger('loadstart');
this.trigger('firstplay');
} else {
// reset the hasStarted state
this.hasStarted(false);
this.trigger('loadstart');
}
}
hasStarted(hasStarted) {
if (hasStarted !== undefined) {
// only update if this is a new value
if (this.hasStarted_ !== hasStarted) {
this.hasStarted_ = hasStarted;
if (hasStarted) {
this.addClass('vjs-has-started');
// trigger the firstplay event if this newly has played
this.trigger('firstplay');
} else {
this.removeClass('vjs-has-started');
}
}
return this;
}
return !!this.hasStarted_;
}
/**
* Fired whenever the media begins or resumes playback
* @event play
*/
handleTechPlay() {
this.removeClass('vjs-ended');
this.removeClass('vjs-paused');
this.addClass('vjs-playing');
// hide the poster when the user hits play
// https://html.spec.whatwg.org/multipage/embedded-content.html#dom-media-play
this.hasStarted(true);
this.trigger('play');
}
/**
* Fired whenever the media begins waiting
* @event waiting
*/
handleTechWaiting() {
this.addClass('vjs-waiting');
this.trigger('waiting');
}
/**
* A handler for events that signal that waiting has ended
* which is not consistent between browsers. See #1351
* @event canplay
*/
handleTechCanPlay() {
this.removeClass('vjs-waiting');
this.trigger('canplay');
}
/**
* A handler for events that signal that waiting has ended
* which is not consistent between browsers. See #1351
* @event canplaythrough
*/
handleTechCanPlayThrough() {
this.removeClass('vjs-waiting');
this.trigger('canplaythrough');
}
/**
* A handler for events that signal that waiting has ended
* which is not consistent between browsers. See #1351
* @event playing
*/
handleTechPlaying() {
this.removeClass('vjs-waiting');
this.trigger('playing');
}
/**
* Fired whenever the player is jumping to a new time
* @event seeking
*/
handleTechSeeking() {
this.addClass('vjs-seeking');
this.trigger('seeking');
}
/**
* Fired when the player has finished jumping to a new time
* @event seeked
*/
handleTechSeeked() {
this.removeClass('vjs-seeking');
this.trigger('seeked');
}
/**
* Fired the first time a video is played
*
* Not part of the HLS spec, and we're not sure if this is the best
* implementation yet, so use sparingly. If you don't have a reason to
* prevent playback, use `myPlayer.one('play');` instead.
*
* @event firstplay
*/
handleTechFirstPlay() {
//If the first starttime attribute is specified
//then we will start at the given offset in seconds
if(this.options_['starttime']){
this.currentTime(this.options_['starttime']);
}
this.addClass('vjs-has-started');
this.trigger('firstplay');
}
/**
* Fired whenever the media has been paused
* @event pause
*/
handleTechPause() {
this.removeClass('vjs-playing');
this.addClass('vjs-paused');
this.trigger('pause');
}
/**
* Fired while the user agent is downloading media data
* @event progress
*/
handleTechProgress() {
this.trigger('progress');
// Add custom event for when source is finished downloading.
if (this.bufferedPercent() === 1) {
this.trigger('loadedalldata');
}
}
/**
* Fired when the end of the media resource is reached (currentTime == duration)
* @event ended
*/
handleTechEnded() {
this.addClass('vjs-ended');
if (this.options_['loop']) {
this.currentTime(0);
this.play();
} else if (!this.paused()) {
this.pause();
}
this.trigger('ended');
}
/**
* Fired when the duration of the media resource is first known or changed
* @event durationchange
*/
handleTechDurationChange() {
this.updateDuration();
this.trigger('durationchange');
}
/**
* Handle a click on the media element to play/pause
*/
handleTechClick(event) {
// We're using mousedown to detect clicks thanks to Flash, but mousedown
// will also be triggered with right-clicks, so we need to prevent that
if (event.button !== 0) return;
// When controls are disabled a click should not toggle playback because
// the click is considered a control
if (this.controls()) {
if (this.paused()) {
this.play();
} else {
this.pause();
}
}
}
/**
* Handle a tap on the media element. It will toggle the user
* activity state, which hides and shows the controls.
*/
handleTechTap() {
this.userActive(!this.userActive());
}
handleTechTouchStart() {
this.userWasActive = this.userActive();
}
handleTechTouchMove() {
if (this.userWasActive){
this.reportUserActivity();
}
}
handleTechTouchEnd(event) {
// Stop the mouse events from also happening
event.preventDefault();
}
/**
* Update the duration of the player using the tech
* @private
*/
updateDuration() {
// Allows for caching value instead of asking player each time.
// We need to get the techGet response and check for a value so we don't
// accidentally cause the stack to blow up.
var duration = this.techGet('duration');
if (duration) {
if (duration < 0) {
duration = Infinity;
}
this.duration(duration);
// Determine if the stream is live and propagate styles down to UI.
if (duration === Infinity) {
this.addClass('vjs-live');
} else {
this.removeClass('vjs-live');
}
}
}
/**
* Fired when the player switches in or out of fullscreen mode
* @event fullscreenchange
*/
handleFullscreenChange() {
if (this.isFullscreen()) {
this.addClass('vjs-fullscreen');
} else {
this.removeClass('vjs-fullscreen');
}
}
/**
* native click events on the SWF aren't triggered on IE11, Win8.1RT
* use stageclick events triggered from inside the SWF instead
* @private
*/
handleStageClick() {
this.reportUserActivity();
}
handleTechFullscreenChange() {
this.trigger('fullscreenchange');
}
/**
* Fires when an error occurred during the loading of an audio/video
* @event error
*/
handleTechError() {
this.error(this.tech.error().code);
}
/**
* Fires when the browser is intentionally not getting media data
* @event suspend
*/
handleTechSuspend() {
this.trigger('suspend');
}
/**
* Fires when the loading of an audio/video is aborted
* @event abort
*/
handleTechAbort() {
this.trigger('abort');
}
/**
* Fires when the current playlist is empty
* @event emptied
*/
handleTechEmptied() {
this.trigger('emptied');
}
/**
* Fires when the browser is trying to get media data, but data is not available
* @event stalled
*/
handleTechStalled() {
this.trigger('stalled');
}
/**
* Fires when the browser has loaded meta data for the audio/video
* @event loadedmetadata
*/
handleTechLoadedMetaData() {
this.trigger('loadedmetadata');
}
/**
* Fires when the browser has loaded the current frame of the audio/video
* @event loaddata
*/
handleTechLoadedData() {
this.trigger('loadeddata');
}
/**
* Fires when the current playback position has changed
* @event timeupdate
*/
handleTechTimeUpdate() {
this.trigger('timeupdate');
}
/**
* Fires when the playing speed of the audio/video is changed
* @event ratechange
*/
handleTechRateChange() {
this.trigger('ratechange');
}
/**
* Fires when the volume has been changed
* @event volumechange
*/
handleTechVolumeChange() {
this.trigger('volumechange');
}
/**
* Fires when the text track has been changed
* @event texttrackchange
*/
onTextTrackChange() {
this.trigger('texttrackchange');
}
/**
* Object for cached values.
*/
getCache() {
return this.cache_;
}
// Pass values to the playback tech
techCall(method, arg) {
// If it's not ready yet, call method when it is
if (this.tech && !this.tech.isReady_) {
this.tech.ready(function(){
this[method](arg);
});
// Otherwise call method now
} else {
try {
this.tech[method](arg);
} catch(e) {
log(e);
throw e;
}
}
}
// Get calls can't wait for the tech, and sometimes don't need to.
techGet(method) {
if (this.tech && this.tech.isReady_) {
// Flash likes to die and reload when you hide or reposition it.
// In these cases the object methods go away and we get errors.
// When that happens we'll catch the errors and inform tech that it's not ready any more.
try {
return this.tech[method]();
} catch(e) {
// When building additional tech libs, an expected method may not be defined yet
if (this.tech[method] === undefined) {
log(`Video.js: ${method} method not defined for ${this.techName} playback technology.`, e);
} else {
// When a method isn't available on the object it throws a TypeError
if (e.name === 'TypeError') {
log(`Video.js: ${method} unavailable on ${this.techName} playback technology element.`, e);
this.tech.isReady_ = false;
} else {
log(e);
}
}
throw e;
}
}
return;
}
/**
* start media playback
*
* myPlayer.play();
*
* @return {Player} self
*/
play() {