videojs_rails 4.1.0 → 4.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/lib/videojs_rails/version.rb +1 -1
- data/readme.md +1 -1
- data/vendor/assets/flash/video-js.swf +0 -0
- data/vendor/assets/javascripts/video.js.erb +637 -251
- data/vendor/assets/stylesheets/video-js.css.erb +429 -396
- metadata +9 -10
@@ -57,7 +57,7 @@ var videojs = vjs;
|
|
57
57
|
window.videojs = window.vjs = vjs;
|
58
58
|
|
59
59
|
// CDN Version. Used to target right flash swf.
|
60
|
-
vjs.CDN_VERSION = '4.
|
60
|
+
vjs.CDN_VERSION = '4.2';
|
61
61
|
vjs.ACCESS_PROTOCOL = ('https:' == document.location.protocol ? 'https://' : 'http://');
|
62
62
|
|
63
63
|
/**
|
@@ -89,11 +89,17 @@ vjs.options = {
|
|
89
89
|
'loadingSpinner': {},
|
90
90
|
'bigPlayButton': {},
|
91
91
|
'controlBar': {}
|
92
|
-
}
|
92
|
+
},
|
93
|
+
|
94
|
+
// Default message to show when a video cannot be played.
|
95
|
+
'notSupportedMessage': 'Sorry, no compatible source and playback ' +
|
96
|
+
'technology were found for this video. Try using another browser ' +
|
97
|
+
'like <a href="http://bit.ly/ccMUEC">Chrome</a> or download the ' +
|
98
|
+
'latest <a href="http://adobe.ly/mwfN1">Adobe Flash Player</a>.'
|
93
99
|
};
|
94
100
|
|
95
101
|
// Set CDN Version of swf
|
96
|
-
// The added (+) blocks the replace from changing this 4.
|
102
|
+
// The added (+) blocks the replace from changing this 4.2 string
|
97
103
|
if (vjs.CDN_VERSION !== 'GENERATED'+'_CDN_VSN') {
|
98
104
|
videojs.options['flash']['swf'] = vjs.ACCESS_PROTOCOL + 'vjs.zencdn.net/'+vjs.CDN_VERSION+'/video-js.swf';
|
99
105
|
}
|
@@ -457,8 +463,9 @@ vjs.trigger = function(elem, event) {
|
|
457
463
|
elemData.dispatcher.call(elem, event);
|
458
464
|
}
|
459
465
|
|
460
|
-
// Unless explicitly stopped
|
461
|
-
|
466
|
+
// Unless explicitly stopped or the event does not bubble (e.g. media events)
|
467
|
+
// recursively calls this function to bubble the event up the DOM.
|
468
|
+
if (parent && !event.isPropagationStopped() && event.bubbles !== false) {
|
462
469
|
vjs.trigger(parent, event);
|
463
470
|
|
464
471
|
// If at the top of the DOM, triggers the default action unless disabled.
|
@@ -509,10 +516,12 @@ vjs.trigger = function(elem, event) {
|
|
509
516
|
* @return {[type]}
|
510
517
|
*/
|
511
518
|
vjs.one = function(elem, type, fn) {
|
512
|
-
|
513
|
-
vjs.off(elem, type,
|
519
|
+
var func = function(){
|
520
|
+
vjs.off(elem, type, func);
|
514
521
|
fn.apply(this, arguments);
|
515
|
-
}
|
522
|
+
};
|
523
|
+
func.guid = fn.guid = fn.guid || vjs.guid++;
|
524
|
+
vjs.on(elem, type, func);
|
516
525
|
};
|
517
526
|
var hasOwnProp = Object.prototype.hasOwnProperty;
|
518
527
|
|
@@ -523,9 +532,11 @@ var hasOwnProp = Object.prototype.hasOwnProperty;
|
|
523
532
|
* @return {Element}
|
524
533
|
*/
|
525
534
|
vjs.createEl = function(tagName, properties){
|
526
|
-
var el
|
535
|
+
var el, propName;
|
536
|
+
|
537
|
+
el = document.createElement(tagName || 'div');
|
527
538
|
|
528
|
-
for (
|
539
|
+
for (propName in properties){
|
529
540
|
if (hasOwnProp.call(properties, propName)) {
|
530
541
|
//el[propName] = properties[propName];
|
531
542
|
// Not remembering why we were checking for dash
|
@@ -619,10 +630,9 @@ vjs.obj.merge = function(obj1, obj2){
|
|
619
630
|
* @return {Object} New object. Obj1 and Obj2 will be untouched.
|
620
631
|
*/
|
621
632
|
vjs.obj.deepMerge = function(obj1, obj2){
|
622
|
-
var key, val1, val2
|
623
|
-
objDef = '[object Object]';
|
633
|
+
var key, val1, val2;
|
624
634
|
|
625
|
-
//
|
635
|
+
// make a copy of obj1 so we're not ovewriting original values.
|
626
636
|
// like prototype.options_ and all sub options objects
|
627
637
|
obj1 = vjs.obj.copy(obj1);
|
628
638
|
|
@@ -789,15 +799,19 @@ vjs.addClass = function(element, classToAdd){
|
|
789
799
|
* @param {String} classToAdd Classname to remove
|
790
800
|
*/
|
791
801
|
vjs.removeClass = function(element, classToRemove){
|
802
|
+
var classNames, i;
|
803
|
+
|
792
804
|
if (element.className.indexOf(classToRemove) == -1) { return; }
|
793
|
-
|
794
|
-
|
795
|
-
|
805
|
+
|
806
|
+
classNames = element.className.split(' ');
|
807
|
+
|
808
|
+
// no arr.indexOf in ie8, and we don't want to add a big shim
|
809
|
+
for (i = classNames.length - 1; i >= 0; i--) {
|
796
810
|
if (classNames[i] === classToRemove) {
|
797
811
|
classNames.splice(i,1);
|
798
812
|
}
|
799
813
|
}
|
800
|
-
|
814
|
+
|
801
815
|
element.className = classNames.join(' ');
|
802
816
|
};
|
803
817
|
|
@@ -859,6 +873,7 @@ vjs.IS_OLD_ANDROID = vjs.IS_ANDROID && (/webkit/i).test(vjs.USER_AGENT) && vjs.A
|
|
859
873
|
vjs.IS_FIREFOX = (/Firefox/i).test(vjs.USER_AGENT);
|
860
874
|
vjs.IS_CHROME = (/Chrome/i).test(vjs.USER_AGENT);
|
861
875
|
|
876
|
+
vjs.TOUCH_ENABLED = ('ontouchstart' in window);
|
862
877
|
|
863
878
|
/**
|
864
879
|
* Get an element's attribute values, as defined on the HTML tag
|
@@ -869,28 +884,28 @@ vjs.IS_CHROME = (/Chrome/i).test(vjs.USER_AGENT);
|
|
869
884
|
* @return {Object}
|
870
885
|
*/
|
871
886
|
vjs.getAttributeValues = function(tag){
|
872
|
-
var obj
|
887
|
+
var obj, knownBooleans, attrs, attrName, attrVal;
|
873
888
|
|
874
|
-
|
875
|
-
|
876
|
-
//
|
877
|
-
//
|
878
|
-
|
889
|
+
obj = {};
|
890
|
+
|
891
|
+
// known boolean attributes
|
892
|
+
// we can check for matching boolean properties, but older browsers
|
893
|
+
// won't know about HTML5 boolean attributes that we still read from
|
894
|
+
knownBooleans = ','+'autoplay,controls,loop,muted,default'+',';
|
879
895
|
|
880
896
|
if (tag && tag.attributes && tag.attributes.length > 0) {
|
881
|
-
|
882
|
-
var attrName, attrVal;
|
897
|
+
attrs = tag.attributes;
|
883
898
|
|
884
899
|
for (var i = attrs.length - 1; i >= 0; i--) {
|
885
900
|
attrName = attrs[i].name;
|
886
901
|
attrVal = attrs[i].value;
|
887
902
|
|
888
|
-
//
|
889
|
-
//
|
903
|
+
// check for known booleans
|
904
|
+
// the matching element property will return a value for typeof
|
890
905
|
if (typeof tag[attrName] === 'boolean' || knownBooleans.indexOf(','+attrName+',') !== -1) {
|
891
|
-
//
|
892
|
-
// which would equal false if we just check for a false value.
|
893
|
-
//
|
906
|
+
// the value of an included boolean attribute is typically an empty
|
907
|
+
// string ('') which would equal false if we just check for a false value.
|
908
|
+
// we also don't want support bad code like autoplay='false'
|
894
909
|
attrVal = (attrVal !== null) ? true : false;
|
895
910
|
}
|
896
911
|
|
@@ -962,13 +977,21 @@ vjs.el = function(id){
|
|
962
977
|
* @return {String} Time formatted as H:MM:SS or M:SS
|
963
978
|
*/
|
964
979
|
vjs.formatTime = function(seconds, guide) {
|
965
|
-
|
980
|
+
// Default to using seconds as guide
|
981
|
+
guide = guide || seconds;
|
966
982
|
var s = Math.floor(seconds % 60),
|
967
983
|
m = Math.floor(seconds / 60 % 60),
|
968
984
|
h = Math.floor(seconds / 3600),
|
969
985
|
gm = Math.floor(guide / 60 % 60),
|
970
986
|
gh = Math.floor(guide / 3600);
|
971
987
|
|
988
|
+
// handle invalid times
|
989
|
+
if (isNaN(seconds) || seconds === Infinity) {
|
990
|
+
// '-' is false for all relational operators (e.g. <, >=) so this setting
|
991
|
+
// will add the minimum number of fields specified by the guide
|
992
|
+
h = m = s = '-';
|
993
|
+
}
|
994
|
+
|
972
995
|
// Check if we need to show hours
|
973
996
|
h = (h > 0 || gh > 0) ? h + ':' : '';
|
974
997
|
|
@@ -995,8 +1018,8 @@ vjs.unblockTextSelection = function(){ document.onselectstart = function () { re
|
|
995
1018
|
* @param {String} string String to trim
|
996
1019
|
* @return {String} Trimmed string
|
997
1020
|
*/
|
998
|
-
vjs.trim = function(
|
999
|
-
return
|
1021
|
+
vjs.trim = function(str){
|
1022
|
+
return (str+'').replace(/^\s+|\s+$/g, '');
|
1000
1023
|
};
|
1001
1024
|
|
1002
1025
|
/**
|
@@ -1034,7 +1057,7 @@ vjs.createTimeRange = function(start, end){
|
|
1034
1057
|
* @param {Function=} onError Error callback
|
1035
1058
|
*/
|
1036
1059
|
vjs.get = function(url, onSuccess, onError){
|
1037
|
-
var local
|
1060
|
+
var local, request;
|
1038
1061
|
|
1039
1062
|
if (typeof XMLHttpRequest === 'undefined') {
|
1040
1063
|
window.XMLHttpRequest = function () {
|
@@ -1045,14 +1068,15 @@ vjs.get = function(url, onSuccess, onError){
|
|
1045
1068
|
};
|
1046
1069
|
}
|
1047
1070
|
|
1048
|
-
|
1049
|
-
|
1071
|
+
request = new XMLHttpRequest();
|
1050
1072
|
try {
|
1051
1073
|
request.open('GET', url);
|
1052
1074
|
} catch(e) {
|
1053
1075
|
onError(e);
|
1054
1076
|
}
|
1055
1077
|
|
1078
|
+
local = (url.indexOf('file:') === 0 || (window.location.href.indexOf('file:') === 0 && url.indexOf('http') === -1));
|
1079
|
+
|
1056
1080
|
request.onreadystatechange = function() {
|
1057
1081
|
if (request.readyState === 4) {
|
1058
1082
|
if (request.status === 200 || local && request.status === 0) {
|
@@ -1203,6 +1227,8 @@ vjs.Component = vjs.CoreObject.extend({
|
|
1203
1227
|
* Dispose of the component and all child components.
|
1204
1228
|
*/
|
1205
1229
|
vjs.Component.prototype.dispose = function(){
|
1230
|
+
this.trigger('dispose');
|
1231
|
+
|
1206
1232
|
// Dispose all children.
|
1207
1233
|
if (this.children_) {
|
1208
1234
|
for (var i = this.children_.length - 1; i >= 0; i--) {
|
@@ -1695,26 +1721,6 @@ vjs.Component.prototype.hide = function(){
|
|
1695
1721
|
return this;
|
1696
1722
|
};
|
1697
1723
|
|
1698
|
-
/**
|
1699
|
-
* Fade a component in using CSS
|
1700
|
-
* @return {vjs.Component}
|
1701
|
-
*/
|
1702
|
-
vjs.Component.prototype.fadeIn = function(){
|
1703
|
-
this.removeClass('vjs-fade-out');
|
1704
|
-
this.addClass('vjs-fade-in');
|
1705
|
-
return this;
|
1706
|
-
};
|
1707
|
-
|
1708
|
-
/**
|
1709
|
-
* Fade a component out using CSS
|
1710
|
-
* @return {vjs.Component}
|
1711
|
-
*/
|
1712
|
-
vjs.Component.prototype.fadeOut = function(){
|
1713
|
-
this.removeClass('vjs-fade-in');
|
1714
|
-
this.addClass('vjs-fade-out');
|
1715
|
-
return this;
|
1716
|
-
};
|
1717
|
-
|
1718
1724
|
/**
|
1719
1725
|
* Lock an item in its visible state. To be used with fadeIn/fadeOut.
|
1720
1726
|
* @return {vjs.Component}
|
@@ -1739,15 +1745,8 @@ vjs.Component.prototype.unlockShowing = function(){
|
|
1739
1745
|
vjs.Component.prototype.disable = function(){
|
1740
1746
|
this.hide();
|
1741
1747
|
this.show = function(){};
|
1742
|
-
this.fadeIn = function(){};
|
1743
1748
|
};
|
1744
1749
|
|
1745
|
-
// TODO: Get enable working
|
1746
|
-
// vjs.Component.prototype.enable = function(){
|
1747
|
-
// this.fadeIn = vjs.Component.prototype.fadeIn;
|
1748
|
-
// this.show = vjs.Component.prototype.show;
|
1749
|
-
// };
|
1750
|
-
|
1751
1750
|
/**
|
1752
1751
|
* If a value is provided it will change the width of the player to that value
|
1753
1752
|
* otherwise the width is returned
|
@@ -1849,6 +1848,53 @@ vjs.Component.prototype.dimension = function(widthOrHeight, num, skipListeners){
|
|
1849
1848
|
// }
|
1850
1849
|
}
|
1851
1850
|
};
|
1851
|
+
|
1852
|
+
/**
|
1853
|
+
* Emit 'tap' events when touch events are supported. We're requireing them to
|
1854
|
+
* be enabled because otherwise every component would have this extra overhead
|
1855
|
+
* unnecessarily, on mobile devices where extra overhead is especially bad.
|
1856
|
+
*
|
1857
|
+
* This is being implemented so we can support taps on the video element
|
1858
|
+
* toggling the controls.
|
1859
|
+
*/
|
1860
|
+
vjs.Component.prototype.emitTapEvents = function(){
|
1861
|
+
var touchStart, touchTime, couldBeTap, noTap;
|
1862
|
+
|
1863
|
+
// Track the start time so we can determine how long the touch lasted
|
1864
|
+
touchStart = 0;
|
1865
|
+
|
1866
|
+
this.on('touchstart', function(event) {
|
1867
|
+
// Record start time so we can detect a tap vs. "touch and hold"
|
1868
|
+
touchStart = new Date().getTime();
|
1869
|
+
// Reset couldBeTap tracking
|
1870
|
+
couldBeTap = true;
|
1871
|
+
});
|
1872
|
+
|
1873
|
+
noTap = function(){
|
1874
|
+
couldBeTap = false;
|
1875
|
+
};
|
1876
|
+
// TODO: Listen to the original target. http://youtu.be/DujfpXOKUp8?t=13m8s
|
1877
|
+
this.on('touchmove', noTap);
|
1878
|
+
this.on('touchleave', noTap);
|
1879
|
+
this.on('touchcancel', noTap);
|
1880
|
+
|
1881
|
+
// When the touch ends, measure how long it took and trigger the appropriate
|
1882
|
+
// event
|
1883
|
+
this.on('touchend', function() {
|
1884
|
+
// Proceed only if the touchmove/leave/cancel event didn't happen
|
1885
|
+
if (couldBeTap === true) {
|
1886
|
+
// Measure how long the touch lasted
|
1887
|
+
touchTime = new Date().getTime() - touchStart;
|
1888
|
+
// The touch needs to be quick in order to consider it a tap
|
1889
|
+
if (touchTime < 250) {
|
1890
|
+
this.trigger('tap');
|
1891
|
+
// It may be good to copy the touchend event object and change the
|
1892
|
+
// type to tap, if the other event properties aren't exact after
|
1893
|
+
// vjs.fixEvent runs (e.g. event.target)
|
1894
|
+
}
|
1895
|
+
}
|
1896
|
+
});
|
1897
|
+
};
|
1852
1898
|
/* Button - Base class for all buttons
|
1853
1899
|
================================================================================ */
|
1854
1900
|
/**
|
@@ -1863,7 +1909,9 @@ vjs.Button = vjs.Component.extend({
|
|
1863
1909
|
vjs.Component.call(this, player, options);
|
1864
1910
|
|
1865
1911
|
var touchstart = false;
|
1866
|
-
this.on('touchstart', function() {
|
1912
|
+
this.on('touchstart', function(event) {
|
1913
|
+
// Stop click and other mouse events from triggering also
|
1914
|
+
event.preventDefault();
|
1867
1915
|
touchstart = true;
|
1868
1916
|
});
|
1869
1917
|
this.on('touchmove', function() {
|
@@ -1875,7 +1923,6 @@ vjs.Button = vjs.Component.extend({
|
|
1875
1923
|
self.onClick(event);
|
1876
1924
|
}
|
1877
1925
|
event.preventDefault();
|
1878
|
-
event.stopPropagation();
|
1879
1926
|
});
|
1880
1927
|
|
1881
1928
|
this.on('click', this.onClick);
|
@@ -2282,7 +2329,7 @@ vjs.MenuButton.prototype.createMenu = function(){
|
|
2282
2329
|
}));
|
2283
2330
|
}
|
2284
2331
|
|
2285
|
-
this.items = this
|
2332
|
+
this.items = this['createItems']();
|
2286
2333
|
|
2287
2334
|
if (this.items) {
|
2288
2335
|
// Add menu items to the menu
|
@@ -2386,22 +2433,30 @@ vjs.Player = vjs.Component.extend({
|
|
2386
2433
|
this.poster_ = options['poster'];
|
2387
2434
|
// Set controls
|
2388
2435
|
this.controls_ = options['controls'];
|
2389
|
-
//
|
2390
|
-
//
|
2391
|
-
|
2392
|
-
|
2393
|
-
this.controls_ = false;
|
2394
|
-
} else {
|
2395
|
-
// Original tag settings stored in options
|
2396
|
-
// now remove immediately so native controls don't flash.
|
2397
|
-
tag.controls = false;
|
2398
|
-
}
|
2436
|
+
// Original tag settings stored in options
|
2437
|
+
// now remove immediately so native controls don't flash.
|
2438
|
+
// May be turned back on by HTML5 tech if nativeControlsForTouch is true
|
2439
|
+
tag.controls = false;
|
2399
2440
|
|
2400
2441
|
// Run base component initializing with new options.
|
2401
2442
|
// Builds the element through createEl()
|
2402
2443
|
// Inits and embeds any child components in opts
|
2403
2444
|
vjs.Component.call(this, this, options, ready);
|
2404
2445
|
|
2446
|
+
// Update controls className. Can't do this when the controls are initially
|
2447
|
+
// set because the element doesn't exist yet.
|
2448
|
+
if (this.controls()) {
|
2449
|
+
this.addClass('vjs-controls-enabled');
|
2450
|
+
} else {
|
2451
|
+
this.addClass('vjs-controls-disabled');
|
2452
|
+
}
|
2453
|
+
|
2454
|
+
// TODO: Make this smarter. Toggle user state between touching/mousing
|
2455
|
+
// using events, since devices can have both touch and mouse events.
|
2456
|
+
// if (vjs.TOUCH_ENABLED) {
|
2457
|
+
// this.addClass('vjs-touch-enabled');
|
2458
|
+
// }
|
2459
|
+
|
2405
2460
|
// Firstplay event implimentation. Not sold on the event yet.
|
2406
2461
|
// Could probably just check currentTime==0?
|
2407
2462
|
this.one('play', function(e){
|
@@ -2433,6 +2488,8 @@ vjs.Player = vjs.Component.extend({
|
|
2433
2488
|
this[key](val);
|
2434
2489
|
}, this);
|
2435
2490
|
}
|
2491
|
+
|
2492
|
+
this.listenForUserActivity();
|
2436
2493
|
}
|
2437
2494
|
});
|
2438
2495
|
|
@@ -2448,7 +2505,9 @@ vjs.Player = vjs.Component.extend({
|
|
2448
2505
|
vjs.Player.prototype.options_ = vjs.options;
|
2449
2506
|
|
2450
2507
|
vjs.Player.prototype.dispose = function(){
|
2451
|
-
|
2508
|
+
this.trigger('dispose');
|
2509
|
+
// prevent dispose from being called twice
|
2510
|
+
this.off('dispose');
|
2452
2511
|
|
2453
2512
|
// Kill reference to this player
|
2454
2513
|
vjs.players[this.id_] = null;
|
@@ -2585,12 +2644,12 @@ vjs.Player.prototype.loadTech = function(techName, source){
|
|
2585
2644
|
this.player_.triggerReady();
|
2586
2645
|
|
2587
2646
|
// Manually track progress in cases where the browser/flash player doesn't report it.
|
2588
|
-
if (!this.features
|
2647
|
+
if (!this.features['progressEvents']) {
|
2589
2648
|
this.player_.manualProgressOn();
|
2590
2649
|
}
|
2591
2650
|
|
2592
2651
|
// Manually track timeudpates in cases where the browser/flash player doesn't report it.
|
2593
|
-
if (!this.features
|
2652
|
+
if (!this.features['timeupdateEvents']) {
|
2594
2653
|
this.player_.manualTimeUpdatesOn();
|
2595
2654
|
}
|
2596
2655
|
};
|
@@ -2655,7 +2714,7 @@ vjs.Player.prototype.manualProgressOn = function(){
|
|
2655
2714
|
this.tech.one('progress', function(){
|
2656
2715
|
|
2657
2716
|
// Update known progress support for this playback technology
|
2658
|
-
this.features
|
2717
|
+
this.features['progressEvents'] = true;
|
2659
2718
|
|
2660
2719
|
// Turn off manual progress tracking
|
2661
2720
|
this.player_.manualProgressOff();
|
@@ -2694,7 +2753,7 @@ vjs.Player.prototype.manualTimeUpdatesOn = function(){
|
|
2694
2753
|
// Watch for native timeupdate event
|
2695
2754
|
this.tech.one('timeupdate', function(){
|
2696
2755
|
// Update known progress support for this playback technology
|
2697
|
-
this.features
|
2756
|
+
this.features['timeupdateEvents'] = true;
|
2698
2757
|
// Turn off manual progress tracking
|
2699
2758
|
this.player_.manualTimeUpdatesOff();
|
2700
2759
|
});
|
@@ -2737,6 +2796,8 @@ vjs.Player.prototype.onFirstPlay = function(){
|
|
2737
2796
|
if(this.options_['starttime']){
|
2738
2797
|
this.currentTime(this.options_['starttime']);
|
2739
2798
|
}
|
2799
|
+
|
2800
|
+
this.addClass('vjs-has-started');
|
2740
2801
|
};
|
2741
2802
|
|
2742
2803
|
vjs.Player.prototype.onPause = function(){
|
@@ -2804,12 +2865,7 @@ vjs.Player.prototype.techCall = function(method, arg){
|
|
2804
2865
|
// Get calls can't wait for the tech, and sometimes don't need to.
|
2805
2866
|
vjs.Player.prototype.techGet = function(method){
|
2806
2867
|
|
2807
|
-
|
2808
|
-
// if (!this.tech) {
|
2809
|
-
// return;
|
2810
|
-
// }
|
2811
|
-
|
2812
|
-
if (this.tech.isReady_) {
|
2868
|
+
if (this.tech && this.tech.isReady_) {
|
2813
2869
|
|
2814
2870
|
// Flash likes to die and reload when you hide or reposition it.
|
2815
2871
|
// In these cases the object methods go away and we get errors.
|
@@ -2906,11 +2962,12 @@ vjs.Player.prototype.remainingTime = function(){
|
|
2906
2962
|
vjs.Player.prototype.buffered = function(){
|
2907
2963
|
var buffered = this.techGet('buffered'),
|
2908
2964
|
start = 0,
|
2965
|
+
buflast = buffered.length - 1,
|
2909
2966
|
// Default end to 0 and store in values
|
2910
2967
|
end = this.cache_.bufferEnd = this.cache_.bufferEnd || 0;
|
2911
2968
|
|
2912
|
-
if (buffered &&
|
2913
|
-
end = buffered.end(
|
2969
|
+
if (buffered && buflast >= 0 && buffered.end(buflast) !== end) {
|
2970
|
+
end = buffered.end(buflast);
|
2914
2971
|
// Storing values allows them be overridden by setBufferedFromProgress
|
2915
2972
|
this.cache_.bufferEnd = end;
|
2916
2973
|
}
|
@@ -3102,7 +3159,7 @@ vjs.Player.prototype.src = function(source){
|
|
3102
3159
|
}
|
3103
3160
|
} else {
|
3104
3161
|
this.el_.appendChild(vjs.createEl('p', {
|
3105
|
-
innerHTML:
|
3162
|
+
innerHTML: this.options()['notSupportedMessage']
|
3106
3163
|
}));
|
3107
3164
|
}
|
3108
3165
|
|
@@ -3207,19 +3264,188 @@ vjs.Player.prototype.controls_;
|
|
3207
3264
|
* @param {Boolean} controls Set controls to showing or not
|
3208
3265
|
* @return {Boolean} Controls are showing
|
3209
3266
|
*/
|
3210
|
-
vjs.Player.prototype.controls = function(
|
3211
|
-
if (
|
3267
|
+
vjs.Player.prototype.controls = function(bool){
|
3268
|
+
if (bool !== undefined) {
|
3269
|
+
bool = !!bool; // force boolean
|
3212
3270
|
// Don't trigger a change event unless it actually changed
|
3213
|
-
if (this.controls_ !==
|
3214
|
-
this.controls_ =
|
3215
|
-
|
3271
|
+
if (this.controls_ !== bool) {
|
3272
|
+
this.controls_ = bool;
|
3273
|
+
if (bool) {
|
3274
|
+
this.removeClass('vjs-controls-disabled');
|
3275
|
+
this.addClass('vjs-controls-enabled');
|
3276
|
+
this.trigger('controlsenabled');
|
3277
|
+
} else {
|
3278
|
+
this.removeClass('vjs-controls-enabled');
|
3279
|
+
this.addClass('vjs-controls-disabled');
|
3280
|
+
this.trigger('controlsdisabled');
|
3281
|
+
}
|
3216
3282
|
}
|
3283
|
+
return this;
|
3217
3284
|
}
|
3218
3285
|
return this.controls_;
|
3219
3286
|
};
|
3220
3287
|
|
3288
|
+
vjs.Player.prototype.usingNativeControls_;
|
3289
|
+
|
3290
|
+
/**
|
3291
|
+
* Toggle native controls on/off. Native controls are the controls built into
|
3292
|
+
* devices (e.g. default iPhone controls), Flash, or other techs
|
3293
|
+
* (e.g. Vimeo Controls)
|
3294
|
+
*
|
3295
|
+
* **This should only be set by the current tech, because only the tech knows
|
3296
|
+
* if it can support native controls**
|
3297
|
+
*
|
3298
|
+
* @param {Boolean} bool True signals that native controls are on
|
3299
|
+
* @return {vjs.Player} Returns the player
|
3300
|
+
*/
|
3301
|
+
vjs.Player.prototype.usingNativeControls = function(bool){
|
3302
|
+
if (bool !== undefined) {
|
3303
|
+
bool = !!bool; // force boolean
|
3304
|
+
// Don't trigger a change event unless it actually changed
|
3305
|
+
if (this.usingNativeControls_ !== bool) {
|
3306
|
+
this.usingNativeControls_ = bool;
|
3307
|
+
if (bool) {
|
3308
|
+
this.addClass('vjs-using-native-controls');
|
3309
|
+
this.trigger('usingnativecontrols');
|
3310
|
+
} else {
|
3311
|
+
this.removeClass('vjs-using-native-controls');
|
3312
|
+
this.trigger('usingcustomcontrols');
|
3313
|
+
}
|
3314
|
+
}
|
3315
|
+
return this;
|
3316
|
+
}
|
3317
|
+
return this.usingNativeControls_;
|
3318
|
+
};
|
3319
|
+
|
3221
3320
|
vjs.Player.prototype.error = function(){ return this.techGet('error'); };
|
3222
3321
|
vjs.Player.prototype.ended = function(){ return this.techGet('ended'); };
|
3322
|
+
vjs.Player.prototype.seeking = function(){ return this.techGet('seeking'); };
|
3323
|
+
|
3324
|
+
// When the player is first initialized, trigger activity so components
|
3325
|
+
// like the control bar show themselves if needed
|
3326
|
+
vjs.Player.prototype.userActivity_ = true;
|
3327
|
+
vjs.Player.prototype.reportUserActivity = function(event){
|
3328
|
+
this.userActivity_ = true;
|
3329
|
+
};
|
3330
|
+
|
3331
|
+
vjs.Player.prototype.userActive_ = true;
|
3332
|
+
vjs.Player.prototype.userActive = function(bool){
|
3333
|
+
if (bool !== undefined) {
|
3334
|
+
bool = !!bool;
|
3335
|
+
if (bool !== this.userActive_) {
|
3336
|
+
this.userActive_ = bool;
|
3337
|
+
if (bool) {
|
3338
|
+
// If the user was inactive and is now active we want to reset the
|
3339
|
+
// inactivity timer
|
3340
|
+
this.userActivity_ = true;
|
3341
|
+
this.removeClass('vjs-user-inactive');
|
3342
|
+
this.addClass('vjs-user-active');
|
3343
|
+
this.trigger('useractive');
|
3344
|
+
} else {
|
3345
|
+
// We're switching the state to inactive manually, so erase any other
|
3346
|
+
// activity
|
3347
|
+
this.userActivity_ = false;
|
3348
|
+
|
3349
|
+
// Chrome/Safari/IE have bugs where when you change the cursor it can
|
3350
|
+
// trigger a mousemove event. This causes an issue when you're hiding
|
3351
|
+
// the cursor when the user is inactive, and a mousemove signals user
|
3352
|
+
// activity. Making it impossible to go into inactive mode. Specifically
|
3353
|
+
// this happens in fullscreen when we really need to hide the cursor.
|
3354
|
+
//
|
3355
|
+
// When this gets resolved in ALL browsers it can be removed
|
3356
|
+
// https://code.google.com/p/chromium/issues/detail?id=103041
|
3357
|
+
this.tech.one('mousemove', function(e){
|
3358
|
+
e.stopPropagation();
|
3359
|
+
e.preventDefault();
|
3360
|
+
});
|
3361
|
+
this.removeClass('vjs-user-active');
|
3362
|
+
this.addClass('vjs-user-inactive');
|
3363
|
+
this.trigger('userinactive');
|
3364
|
+
}
|
3365
|
+
}
|
3366
|
+
return this;
|
3367
|
+
}
|
3368
|
+
return this.userActive_;
|
3369
|
+
};
|
3370
|
+
|
3371
|
+
vjs.Player.prototype.listenForUserActivity = function(){
|
3372
|
+
var onMouseActivity, onMouseDown, mouseInProgress, onMouseUp,
|
3373
|
+
activityCheck, inactivityTimeout;
|
3374
|
+
|
3375
|
+
onMouseActivity = this.reportUserActivity;
|
3376
|
+
|
3377
|
+
onMouseDown = function() {
|
3378
|
+
onMouseActivity();
|
3379
|
+
// For as long as the they are touching the device or have their mouse down,
|
3380
|
+
// we consider them active even if they're not moving their finger or mouse.
|
3381
|
+
// So we want to continue to update that they are active
|
3382
|
+
clearInterval(mouseInProgress);
|
3383
|
+
// Setting userActivity=true now and setting the interval to the same time
|
3384
|
+
// as the activityCheck interval (250) should ensure we never miss the
|
3385
|
+
// next activityCheck
|
3386
|
+
mouseInProgress = setInterval(vjs.bind(this, onMouseActivity), 250);
|
3387
|
+
};
|
3388
|
+
|
3389
|
+
onMouseUp = function(event) {
|
3390
|
+
onMouseActivity();
|
3391
|
+
// Stop the interval that maintains activity if the mouse/touch is down
|
3392
|
+
clearInterval(mouseInProgress);
|
3393
|
+
};
|
3394
|
+
|
3395
|
+
// Any mouse movement will be considered user activity
|
3396
|
+
this.on('mousedown', onMouseDown);
|
3397
|
+
this.on('mousemove', onMouseActivity);
|
3398
|
+
this.on('mouseup', onMouseUp);
|
3399
|
+
|
3400
|
+
// Listen for keyboard navigation
|
3401
|
+
// Shouldn't need to use inProgress interval because of key repeat
|
3402
|
+
this.on('keydown', onMouseActivity);
|
3403
|
+
this.on('keyup', onMouseActivity);
|
3404
|
+
|
3405
|
+
// Consider any touch events that bubble up to be activity
|
3406
|
+
// Certain touches on the tech will be blocked from bubbling because they
|
3407
|
+
// toggle controls
|
3408
|
+
this.on('touchstart', onMouseDown);
|
3409
|
+
this.on('touchmove', onMouseActivity);
|
3410
|
+
this.on('touchend', onMouseUp);
|
3411
|
+
this.on('touchcancel', onMouseUp);
|
3412
|
+
|
3413
|
+
// Run an interval every 250 milliseconds instead of stuffing everything into
|
3414
|
+
// the mousemove/touchmove function itself, to prevent performance degradation.
|
3415
|
+
// `this.reportUserActivity` simply sets this.userActivity_ to true, which
|
3416
|
+
// then gets picked up by this loop
|
3417
|
+
// http://ejohn.org/blog/learning-from-twitter/
|
3418
|
+
activityCheck = setInterval(vjs.bind(this, function() {
|
3419
|
+
// Check to see if mouse/touch activity has happened
|
3420
|
+
if (this.userActivity_) {
|
3421
|
+
// Reset the activity tracker
|
3422
|
+
this.userActivity_ = false;
|
3423
|
+
|
3424
|
+
// If the user state was inactive, set the state to active
|
3425
|
+
this.userActive(true);
|
3426
|
+
|
3427
|
+
// Clear any existing inactivity timeout to start the timer over
|
3428
|
+
clearTimeout(inactivityTimeout);
|
3429
|
+
|
3430
|
+
// In X seconds, if no more activity has occurred the user will be
|
3431
|
+
// considered inactive
|
3432
|
+
inactivityTimeout = setTimeout(vjs.bind(this, function() {
|
3433
|
+
// Protect against the case where the inactivityTimeout can trigger just
|
3434
|
+
// before the next user activity is picked up by the activityCheck loop
|
3435
|
+
// causing a flicker
|
3436
|
+
if (!this.userActivity_) {
|
3437
|
+
this.userActive(false);
|
3438
|
+
}
|
3439
|
+
}), 2000);
|
3440
|
+
}
|
3441
|
+
}), 250);
|
3442
|
+
|
3443
|
+
// Clean up the intervals when we kill the player
|
3444
|
+
this.on('dispose', function(){
|
3445
|
+
clearInterval(activityCheck);
|
3446
|
+
clearTimeout(inactivityTimeout);
|
3447
|
+
});
|
3448
|
+
};
|
3223
3449
|
|
3224
3450
|
// Methods to add support for
|
3225
3451
|
// networkState: function(){ return this.techCall('networkState'); },
|
@@ -3288,61 +3514,15 @@ vjs.Player.prototype.ended = function(){ return this.techGet('ended'); };
|
|
3288
3514
|
}
|
3289
3515
|
|
3290
3516
|
})();
|
3517
|
+
|
3518
|
+
|
3291
3519
|
/**
|
3292
3520
|
* Container of main controls
|
3293
3521
|
* @param {vjs.Player|Object} player
|
3294
3522
|
* @param {Object=} options
|
3295
3523
|
* @constructor
|
3296
3524
|
*/
|
3297
|
-
vjs.ControlBar = vjs.Component.extend(
|
3298
|
-
/** @constructor */
|
3299
|
-
init: function(player, options){
|
3300
|
-
vjs.Component.call(this, player, options);
|
3301
|
-
|
3302
|
-
if (!player.controls()) {
|
3303
|
-
this.disable();
|
3304
|
-
}
|
3305
|
-
|
3306
|
-
player.one('play', vjs.bind(this, function(){
|
3307
|
-
var touchstart,
|
3308
|
-
fadeIn = vjs.bind(this, this.fadeIn),
|
3309
|
-
fadeOut = vjs.bind(this, this.fadeOut);
|
3310
|
-
|
3311
|
-
this.fadeIn();
|
3312
|
-
|
3313
|
-
if ( !('ontouchstart' in window) ) {
|
3314
|
-
this.player_.on('mouseover', fadeIn);
|
3315
|
-
this.player_.on('mouseout', fadeOut);
|
3316
|
-
this.player_.on('pause', vjs.bind(this, this.lockShowing));
|
3317
|
-
this.player_.on('play', vjs.bind(this, this.unlockShowing));
|
3318
|
-
}
|
3319
|
-
|
3320
|
-
touchstart = false;
|
3321
|
-
this.player_.on('touchstart', function() {
|
3322
|
-
touchstart = true;
|
3323
|
-
});
|
3324
|
-
this.player_.on('touchmove', function() {
|
3325
|
-
touchstart = false;
|
3326
|
-
});
|
3327
|
-
this.player_.on('touchend', vjs.bind(this, function(event) {
|
3328
|
-
var idx;
|
3329
|
-
if (touchstart) {
|
3330
|
-
idx = this.el().className.search('fade-in');
|
3331
|
-
if (idx !== -1) {
|
3332
|
-
this.fadeOut();
|
3333
|
-
} else {
|
3334
|
-
this.fadeIn();
|
3335
|
-
}
|
3336
|
-
}
|
3337
|
-
touchstart = false;
|
3338
|
-
|
3339
|
-
if (!this.player_.paused()) {
|
3340
|
-
event.preventDefault();
|
3341
|
-
}
|
3342
|
-
}));
|
3343
|
-
}));
|
3344
|
-
}
|
3345
|
-
});
|
3525
|
+
vjs.ControlBar = vjs.Component.extend();
|
3346
3526
|
|
3347
3527
|
vjs.ControlBar.prototype.options_ = {
|
3348
3528
|
loadEvent: 'play',
|
@@ -3365,16 +3545,7 @@ vjs.ControlBar.prototype.createEl = function(){
|
|
3365
3545
|
className: 'vjs-control-bar'
|
3366
3546
|
});
|
3367
3547
|
};
|
3368
|
-
|
3369
|
-
vjs.ControlBar.prototype.fadeIn = function(){
|
3370
|
-
vjs.Component.prototype.fadeIn.call(this);
|
3371
|
-
this.player_.trigger('controlsvisible');
|
3372
|
-
};
|
3373
|
-
|
3374
|
-
vjs.ControlBar.prototype.fadeOut = function(){
|
3375
|
-
vjs.Component.prototype.fadeOut.call(this);
|
3376
|
-
this.player_.trigger('controlshidden');
|
3377
|
-
};/**
|
3548
|
+
/**
|
3378
3549
|
* Button to toggle between play and pause
|
3379
3550
|
* @param {vjs.Player|Object} player
|
3380
3551
|
* @param {Object=} options
|
@@ -3484,8 +3655,9 @@ vjs.DurationDisplay.prototype.createEl = function(){
|
|
3484
3655
|
};
|
3485
3656
|
|
3486
3657
|
vjs.DurationDisplay.prototype.updateContent = function(){
|
3487
|
-
|
3488
|
-
|
3658
|
+
var duration = this.player_.duration();
|
3659
|
+
if (duration) {
|
3660
|
+
this.content.innerHTML = '<span class="vjs-control-text">Duration Time </span>' + vjs.formatTime(duration); // label the duration time for screen reader users
|
3489
3661
|
}
|
3490
3662
|
};
|
3491
3663
|
|
@@ -3541,15 +3713,14 @@ vjs.RemainingTimeDisplay.prototype.createEl = function(){
|
|
3541
3713
|
|
3542
3714
|
vjs.RemainingTimeDisplay.prototype.updateContent = function(){
|
3543
3715
|
if (this.player_.duration()) {
|
3544
|
-
|
3545
|
-
this.content.innerHTML = '<span class="vjs-control-text">Remaining Time </span>' + '-'+ vjs.formatTime(this.player_.remainingTime());
|
3546
|
-
}
|
3716
|
+
this.content.innerHTML = '<span class="vjs-control-text">Remaining Time </span>' + '-'+ vjs.formatTime(this.player_.remainingTime());
|
3547
3717
|
}
|
3548
3718
|
|
3549
3719
|
// Allows for smooth scrubbing, when player can't keep up.
|
3550
3720
|
// var time = (this.player_.scrubbing) ? this.player_.getCache().currentTime : this.player_.currentTime();
|
3551
3721
|
// this.content.innerHTML = vjs.formatTime(time, this.player_.duration());
|
3552
|
-
}
|
3722
|
+
};
|
3723
|
+
/**
|
3553
3724
|
* Toggle fullscreen video
|
3554
3725
|
* @param {vjs.Player|Object} player
|
3555
3726
|
* @param {Object=} options
|
@@ -3643,7 +3814,25 @@ vjs.SeekBar.prototype.updateARIAAttributes = function(){
|
|
3643
3814
|
};
|
3644
3815
|
|
3645
3816
|
vjs.SeekBar.prototype.getPercent = function(){
|
3646
|
-
|
3817
|
+
var currentTime;
|
3818
|
+
// Flash RTMP provider will not report the correct time
|
3819
|
+
// immediately after a seek. This isn't noticeable if you're
|
3820
|
+
// seeking while the video is playing, but it is if you seek
|
3821
|
+
// while the video is paused.
|
3822
|
+
if (this.player_.techName === 'Flash' && this.player_.seeking()) {
|
3823
|
+
var cache = this.player_.getCache();
|
3824
|
+
if (cache.lastSetCurrentTime) {
|
3825
|
+
currentTime = cache.lastSetCurrentTime;
|
3826
|
+
}
|
3827
|
+
else {
|
3828
|
+
currentTime = this.player_.currentTime();
|
3829
|
+
}
|
3830
|
+
}
|
3831
|
+
else {
|
3832
|
+
currentTime = this.player_.currentTime();
|
3833
|
+
}
|
3834
|
+
|
3835
|
+
return currentTime / this.player_.duration();
|
3647
3836
|
};
|
3648
3837
|
|
3649
3838
|
vjs.SeekBar.prototype.onMouseDown = function(event){
|
@@ -3758,11 +3947,11 @@ vjs.VolumeControl = vjs.Component.extend({
|
|
3758
3947
|
vjs.Component.call(this, player, options);
|
3759
3948
|
|
3760
3949
|
// hide volume controls when they're not supported by the current tech
|
3761
|
-
if (player.tech && player.tech.features && player.tech.features
|
3950
|
+
if (player.tech && player.tech.features && player.tech.features['volumeControl'] === false) {
|
3762
3951
|
this.addClass('vjs-hidden');
|
3763
3952
|
}
|
3764
3953
|
player.on('loadstart', vjs.bind(this, function(){
|
3765
|
-
if (player.tech.features && player.tech.features
|
3954
|
+
if (player.tech.features && player.tech.features['volumeControl'] === false) {
|
3766
3955
|
this.addClass('vjs-hidden');
|
3767
3956
|
} else {
|
3768
3957
|
this.removeClass('vjs-hidden');
|
@@ -3879,7 +4068,8 @@ vjs.VolumeLevel.prototype.createEl = function(){
|
|
3879
4068
|
return vjs.SliderHandle.prototype.createEl.call(this, 'div', {
|
3880
4069
|
className: 'vjs-volume-handle'
|
3881
4070
|
});
|
3882
|
-
}
|
4071
|
+
};
|
4072
|
+
/**
|
3883
4073
|
* Mute the audio
|
3884
4074
|
* @param {vjs.Player|Object} player
|
3885
4075
|
* @param {Object=} options
|
@@ -3893,11 +4083,11 @@ vjs.MuteToggle = vjs.Button.extend({
|
|
3893
4083
|
player.on('volumechange', vjs.bind(this, this.update));
|
3894
4084
|
|
3895
4085
|
// hide mute toggle if the current tech doesn't support volume control
|
3896
|
-
if (player.tech && player.tech.features && player.tech.features
|
4086
|
+
if (player.tech && player.tech.features && player.tech.features['volumeControl'] === false) {
|
3897
4087
|
this.addClass('vjs-hidden');
|
3898
4088
|
}
|
3899
4089
|
player.on('loadstart', vjs.bind(this, function(){
|
3900
|
-
if (player.tech.features && player.tech.features
|
4090
|
+
if (player.tech.features && player.tech.features['volumeControl'] === false) {
|
3901
4091
|
this.addClass('vjs-hidden');
|
3902
4092
|
} else {
|
3903
4093
|
this.removeClass('vjs-hidden');
|
@@ -3947,7 +4137,8 @@ vjs.MuteToggle.prototype.update = function(){
|
|
3947
4137
|
vjs.removeClass(this.el_, 'vjs-vol-'+i);
|
3948
4138
|
}
|
3949
4139
|
vjs.addClass(this.el_, 'vjs-vol-'+level);
|
3950
|
-
}
|
4140
|
+
};
|
4141
|
+
/**
|
3951
4142
|
* Menu button with a popup for showing the volume slider.
|
3952
4143
|
* @constructor
|
3953
4144
|
*/
|
@@ -4037,7 +4228,10 @@ vjs.PosterImage.prototype.createEl = function(){
|
|
4037
4228
|
};
|
4038
4229
|
|
4039
4230
|
vjs.PosterImage.prototype.onClick = function(){
|
4040
|
-
|
4231
|
+
// Only accept clicks when controls are enabled
|
4232
|
+
if (this.player().controls()) {
|
4233
|
+
this.player_.play();
|
4234
|
+
}
|
4041
4235
|
};
|
4042
4236
|
/* Loading Spinner
|
4043
4237
|
================================================================================ */
|
@@ -4082,24 +4276,13 @@ vjs.LoadingSpinner.prototype.createEl = function(){
|
|
4082
4276
|
/* Big Play Button
|
4083
4277
|
================================================================================ */
|
4084
4278
|
/**
|
4085
|
-
* Initial play button. Shows before the video has played.
|
4279
|
+
* Initial play button. Shows before the video has played. The hiding of the
|
4280
|
+
* big play button is done via CSS and player states.
|
4086
4281
|
* @param {vjs.Player|Object} player
|
4087
4282
|
* @param {Object=} options
|
4088
4283
|
* @constructor
|
4089
4284
|
*/
|
4090
|
-
vjs.BigPlayButton = vjs.Button.extend(
|
4091
|
-
/** @constructor */
|
4092
|
-
init: function(player, options){
|
4093
|
-
vjs.Button.call(this, player, options);
|
4094
|
-
|
4095
|
-
if (!player.controls()) {
|
4096
|
-
this.hide();
|
4097
|
-
}
|
4098
|
-
|
4099
|
-
player.on('play', vjs.bind(this, this.hide));
|
4100
|
-
// player.on('ended', vjs.bind(this, this.show));
|
4101
|
-
}
|
4102
|
-
});
|
4285
|
+
vjs.BigPlayButton = vjs.Button.extend();
|
4103
4286
|
|
4104
4287
|
vjs.BigPlayButton.prototype.createEl = function(){
|
4105
4288
|
return vjs.Button.prototype.createEl.call(this, 'div', {
|
@@ -4110,15 +4293,11 @@ vjs.BigPlayButton.prototype.createEl = function(){
|
|
4110
4293
|
};
|
4111
4294
|
|
4112
4295
|
vjs.BigPlayButton.prototype.onClick = function(){
|
4113
|
-
// Go back to the beginning if big play button is showing at the end.
|
4114
|
-
// Have to check for current time otherwise it might throw a 'not ready' error.
|
4115
|
-
//if(this.player_.currentTime()) {
|
4116
|
-
//this.player_.currentTime(0);
|
4117
|
-
//}
|
4118
4296
|
this.player_.play();
|
4119
4297
|
};
|
4120
4298
|
/**
|
4121
|
-
* @fileoverview Media Technology Controller - Base class for media playback
|
4299
|
+
* @fileoverview Media Technology Controller - Base class for media playback
|
4300
|
+
* technology controllers like Flash and HTML5
|
4122
4301
|
*/
|
4123
4302
|
|
4124
4303
|
/**
|
@@ -4132,48 +4311,155 @@ vjs.MediaTechController = vjs.Component.extend({
|
|
4132
4311
|
init: function(player, options, ready){
|
4133
4312
|
vjs.Component.call(this, player, options, ready);
|
4134
4313
|
|
4135
|
-
|
4136
|
-
// this.addEvent('click', this.proxy(this.onClick));
|
4137
|
-
|
4138
|
-
// player.triggerEvent('techready');
|
4314
|
+
this.initControlsListeners();
|
4139
4315
|
}
|
4140
4316
|
});
|
4141
4317
|
|
4142
|
-
// destroy: function(){},
|
4143
|
-
// createElement: function(){},
|
4144
|
-
|
4145
4318
|
/**
|
4146
|
-
*
|
4319
|
+
* Set up click and touch listeners for the playback element
|
4320
|
+
* On desktops, a click on the video itself will toggle playback,
|
4321
|
+
* on a mobile device a click on the video toggles controls.
|
4322
|
+
* (toggling controls is done by toggling the user state between active and
|
4323
|
+
* inactive)
|
4324
|
+
*
|
4325
|
+
* A tap can signal that a user has become active, or has become inactive
|
4326
|
+
* e.g. a quick tap on an iPhone movie should reveal the controls. Another
|
4327
|
+
* quick tap should hide them again (signaling the user is in an inactive
|
4328
|
+
* viewing state)
|
4147
4329
|
*
|
4148
|
-
*
|
4149
|
-
*
|
4330
|
+
* In addition to this, we still want the user to be considered inactive after
|
4331
|
+
* a few seconds of inactivity.
|
4332
|
+
*
|
4333
|
+
* Note: the only part of iOS interaction we can't mimic with this setup
|
4334
|
+
* is a touch and hold on the video element counting as activity in order to
|
4335
|
+
* keep the controls showing, but that shouldn't be an issue. A touch and hold on
|
4336
|
+
* any controls will still keep the user active
|
4150
4337
|
*/
|
4151
|
-
vjs.MediaTechController.prototype.
|
4152
|
-
|
4153
|
-
|
4154
|
-
|
4155
|
-
|
4156
|
-
|
4157
|
-
|
4158
|
-
|
4159
|
-
|
4160
|
-
|
4161
|
-
|
4162
|
-
|
4163
|
-
|
4338
|
+
vjs.MediaTechController.prototype.initControlsListeners = function(){
|
4339
|
+
var player, tech, activateControls, deactivateControls;
|
4340
|
+
|
4341
|
+
tech = this;
|
4342
|
+
player = this.player();
|
4343
|
+
|
4344
|
+
var activateControls = function(){
|
4345
|
+
if (player.controls() && !player.usingNativeControls()) {
|
4346
|
+
tech.addControlsListeners();
|
4347
|
+
}
|
4348
|
+
};
|
4349
|
+
|
4350
|
+
deactivateControls = vjs.bind(tech, tech.removeControlsListeners);
|
4351
|
+
|
4352
|
+
// Set up event listeners once the tech is ready and has an element to apply
|
4353
|
+
// listeners to
|
4354
|
+
this.ready(activateControls);
|
4355
|
+
player.on('controlsenabled', activateControls);
|
4356
|
+
player.on('controlsdisabled', deactivateControls);
|
4357
|
+
};
|
4358
|
+
|
4359
|
+
vjs.MediaTechController.prototype.addControlsListeners = function(){
|
4360
|
+
var preventBubble, userWasActive;
|
4361
|
+
|
4362
|
+
// Some browsers (Chrome & IE) don't trigger a click on a flash swf, but do
|
4363
|
+
// trigger mousedown/up.
|
4364
|
+
// http://stackoverflow.com/questions/1444562/javascript-onclick-event-over-flash-object
|
4365
|
+
// Any touch events are set to block the mousedown event from happening
|
4366
|
+
this.on('mousedown', this.onClick);
|
4367
|
+
|
4368
|
+
// We need to block touch events on the video element from bubbling up,
|
4369
|
+
// otherwise they'll signal activity prematurely. The specific use case is
|
4370
|
+
// when the video is playing and the controls have faded out. In this case
|
4371
|
+
// only a tap (fast touch) should toggle the user active state and turn the
|
4372
|
+
// controls back on. A touch and move or touch and hold should not trigger
|
4373
|
+
// the controls (per iOS as an example at least)
|
4374
|
+
//
|
4375
|
+
// We always want to stop propagation on touchstart because touchstart
|
4376
|
+
// at the player level starts the touchInProgress interval. We can still
|
4377
|
+
// report activity on the other events, but won't let them bubble for
|
4378
|
+
// consistency. We don't want to bubble a touchend without a touchstart.
|
4379
|
+
this.on('touchstart', function(event) {
|
4380
|
+
// Stop the mouse events from also happening
|
4381
|
+
event.preventDefault();
|
4382
|
+
event.stopPropagation();
|
4383
|
+
// Record if the user was active now so we don't have to keep polling it
|
4384
|
+
userWasActive = this.player_.userActive();
|
4385
|
+
});
|
4386
|
+
|
4387
|
+
preventBubble = function(event){
|
4388
|
+
event.stopPropagation();
|
4389
|
+
if (userWasActive) {
|
4390
|
+
this.player_.reportUserActivity();
|
4391
|
+
}
|
4392
|
+
};
|
4393
|
+
|
4394
|
+
// Treat all touch events the same for consistency
|
4395
|
+
this.on('touchmove', preventBubble);
|
4396
|
+
this.on('touchleave', preventBubble);
|
4397
|
+
this.on('touchcancel', preventBubble);
|
4398
|
+
this.on('touchend', preventBubble);
|
4399
|
+
|
4400
|
+
// Turn on component tap events
|
4401
|
+
this.emitTapEvents();
|
4402
|
+
|
4403
|
+
// The tap listener needs to come after the touchend listener because the tap
|
4404
|
+
// listener cancels out any reportedUserActivity when setting userActive(false)
|
4405
|
+
this.on('tap', this.onTap);
|
4406
|
+
};
|
4407
|
+
|
4408
|
+
/**
|
4409
|
+
* Remove the listeners used for click and tap controls. This is needed for
|
4410
|
+
* toggling to controls disabled, where a tap/touch should do nothing.
|
4411
|
+
*/
|
4412
|
+
vjs.MediaTechController.prototype.removeControlsListeners = function(){
|
4413
|
+
// We don't want to just use `this.off()` because there might be other needed
|
4414
|
+
// listeners added by techs that extend this.
|
4415
|
+
this.off('tap');
|
4416
|
+
this.off('touchstart');
|
4417
|
+
this.off('touchmove');
|
4418
|
+
this.off('touchleave');
|
4419
|
+
this.off('touchcancel');
|
4420
|
+
this.off('touchend');
|
4421
|
+
this.off('click');
|
4422
|
+
this.off('mousedown');
|
4423
|
+
};
|
4424
|
+
|
4425
|
+
/**
|
4426
|
+
* Handle a click on the media element. By default will play/pause the media.
|
4427
|
+
*/
|
4428
|
+
vjs.MediaTechController.prototype.onClick = function(event){
|
4429
|
+
// We're using mousedown to detect clicks thanks to Flash, but mousedown
|
4430
|
+
// will also be triggered with right-clicks, so we need to prevent that
|
4431
|
+
if (event.button !== 0) return;
|
4432
|
+
|
4433
|
+
// When controls are disabled a click should not toggle playback because
|
4434
|
+
// the click is considered a control
|
4435
|
+
if (this.player().controls()) {
|
4436
|
+
if (this.player().paused()) {
|
4437
|
+
this.player().play();
|
4438
|
+
} else {
|
4439
|
+
this.player().pause();
|
4440
|
+
}
|
4164
4441
|
}
|
4165
|
-
}
|
4442
|
+
};
|
4443
|
+
|
4444
|
+
/**
|
4445
|
+
* Handle a tap on the media element. By default it will toggle the user
|
4446
|
+
* activity state, which hides and shows the controls.
|
4447
|
+
*/
|
4448
|
+
|
4449
|
+
vjs.MediaTechController.prototype.onTap = function(){
|
4450
|
+
this.player().userActive(!this.player().userActive());
|
4451
|
+
};
|
4166
4452
|
|
4167
4453
|
vjs.MediaTechController.prototype.features = {
|
4168
|
-
volumeControl: true,
|
4454
|
+
'volumeControl': true,
|
4169
4455
|
|
4170
4456
|
// Resizing plugins using request fullscreen reloads the plugin
|
4171
|
-
fullscreenResize: false,
|
4457
|
+
'fullscreenResize': false,
|
4172
4458
|
|
4173
4459
|
// Optional events that we can manually mimic with timers
|
4174
4460
|
// currently not triggered by video-js-swf
|
4175
|
-
progressEvents: false,
|
4176
|
-
timeupdateEvents: false
|
4461
|
+
'progressEvents': false,
|
4462
|
+
'timeupdateEvents': false
|
4177
4463
|
};
|
4178
4464
|
|
4179
4465
|
vjs.media = {};
|
@@ -4210,13 +4496,13 @@ vjs.Html5 = vjs.MediaTechController.extend({
|
|
4210
4496
|
/** @constructor */
|
4211
4497
|
init: function(player, options, ready){
|
4212
4498
|
// volume cannot be changed from 1 on iOS
|
4213
|
-
this.features
|
4499
|
+
this.features['volumeControl'] = vjs.Html5.canControlVolume();
|
4214
4500
|
|
4215
4501
|
// In iOS, if you move a video element in the DOM, it breaks video playback.
|
4216
|
-
this.features
|
4502
|
+
this.features['movingMediaElementInDOM'] = !vjs.IS_IOS;
|
4217
4503
|
|
4218
4504
|
// HTML video is able to automatically resize when going to fullscreen
|
4219
|
-
this.features
|
4505
|
+
this.features['fullscreenResize'] = true;
|
4220
4506
|
|
4221
4507
|
vjs.MediaTechController.call(this, player, options, ready);
|
4222
4508
|
|
@@ -4232,6 +4518,14 @@ vjs.Html5 = vjs.MediaTechController.extend({
|
|
4232
4518
|
this.el_.src = source.src;
|
4233
4519
|
}
|
4234
4520
|
|
4521
|
+
// Determine if native controls should be used
|
4522
|
+
// Our goal should be to get the custom controls on mobile solid everywhere
|
4523
|
+
// so we can remove this all together. Right now this will block custom
|
4524
|
+
// controls on touch enabled laptops like the Chrome Pixel
|
4525
|
+
if (vjs.TOUCH_ENABLED && player.options()['nativeControlsForTouch'] !== false) {
|
4526
|
+
this.useNativeControls();
|
4527
|
+
}
|
4528
|
+
|
4235
4529
|
// Chrome and Safari both have issues with autoplay.
|
4236
4530
|
// In Safari (5.1.1), when we move the video element into the container div, autoplay doesn't work.
|
4237
4531
|
// In Chrome (15), if you have autoplay + a poster + no controls, the video gets hidden (but audio plays)
|
@@ -4243,10 +4537,7 @@ vjs.Html5 = vjs.MediaTechController.extend({
|
|
4243
4537
|
}
|
4244
4538
|
});
|
4245
4539
|
|
4246
|
-
this.on('click', this.onClick);
|
4247
|
-
|
4248
4540
|
this.setupTriggers();
|
4249
|
-
|
4250
4541
|
this.triggerReady();
|
4251
4542
|
}
|
4252
4543
|
});
|
@@ -4264,7 +4555,7 @@ vjs.Html5.prototype.createEl = function(){
|
|
4264
4555
|
// Check if this browser supports moving the element into the box.
|
4265
4556
|
// On the iPhone video will break if you move the element,
|
4266
4557
|
// So we have to create a brand new element.
|
4267
|
-
if (!el || this.features
|
4558
|
+
if (!el || this.features['movingMediaElementInDOM'] === false) {
|
4268
4559
|
|
4269
4560
|
// If the original tag is still there, remove it.
|
4270
4561
|
if (el) {
|
@@ -4313,6 +4604,37 @@ vjs.Html5.prototype.eventHandler = function(e){
|
|
4313
4604
|
e.stopPropagation();
|
4314
4605
|
};
|
4315
4606
|
|
4607
|
+
vjs.Html5.prototype.useNativeControls = function(){
|
4608
|
+
var tech, player, controlsOn, controlsOff, cleanUp;
|
4609
|
+
|
4610
|
+
tech = this;
|
4611
|
+
player = this.player();
|
4612
|
+
|
4613
|
+
// If the player controls are enabled turn on the native controls
|
4614
|
+
tech.setControls(player.controls());
|
4615
|
+
|
4616
|
+
// Update the native controls when player controls state is updated
|
4617
|
+
controlsOn = function(){
|
4618
|
+
tech.setControls(true);
|
4619
|
+
};
|
4620
|
+
controlsOff = function(){
|
4621
|
+
tech.setControls(false);
|
4622
|
+
};
|
4623
|
+
player.on('controlsenabled', controlsOn);
|
4624
|
+
player.on('controlsdisabled', controlsOff);
|
4625
|
+
|
4626
|
+
// Clean up when not using native controls anymore
|
4627
|
+
cleanUp = function(){
|
4628
|
+
player.off('controlsenabled', controlsOn);
|
4629
|
+
player.off('controlsdisabled', controlsOff);
|
4630
|
+
};
|
4631
|
+
tech.on('dispose', cleanUp);
|
4632
|
+
player.on('usingcustomcontrols', cleanUp);
|
4633
|
+
|
4634
|
+
// Update the state of the player to using native controls
|
4635
|
+
player.usingNativeControls(true);
|
4636
|
+
};
|
4637
|
+
|
4316
4638
|
|
4317
4639
|
vjs.Html5.prototype.play = function(){ this.el_.play(); };
|
4318
4640
|
vjs.Html5.prototype.pause = function(){ this.el_.pause(); };
|
@@ -4376,29 +4698,19 @@ vjs.Html5.prototype.currentSrc = function(){ return this.el_.currentSrc; };
|
|
4376
4698
|
|
4377
4699
|
vjs.Html5.prototype.preload = function(){ return this.el_.preload; };
|
4378
4700
|
vjs.Html5.prototype.setPreload = function(val){ this.el_.preload = val; };
|
4701
|
+
|
4379
4702
|
vjs.Html5.prototype.autoplay = function(){ return this.el_.autoplay; };
|
4380
4703
|
vjs.Html5.prototype.setAutoplay = function(val){ this.el_.autoplay = val; };
|
4704
|
+
|
4705
|
+
vjs.Html5.prototype.controls = function(){ return this.el_.controls; }
|
4706
|
+
vjs.Html5.prototype.setControls = function(val){ this.el_.controls = !!val; }
|
4707
|
+
|
4381
4708
|
vjs.Html5.prototype.loop = function(){ return this.el_.loop; };
|
4382
4709
|
vjs.Html5.prototype.setLoop = function(val){ this.el_.loop = val; };
|
4383
4710
|
|
4384
4711
|
vjs.Html5.prototype.error = function(){ return this.el_.error; };
|
4385
|
-
// networkState: function(){ return this.el_.networkState; },
|
4386
|
-
// readyState: function(){ return this.el_.readyState; },
|
4387
4712
|
vjs.Html5.prototype.seeking = function(){ return this.el_.seeking; };
|
4388
|
-
// initialTime: function(){ return this.el_.initialTime; },
|
4389
|
-
// startOffsetTime: function(){ return this.el_.startOffsetTime; },
|
4390
|
-
// played: function(){ return this.el_.played; },
|
4391
|
-
// seekable: function(){ return this.el_.seekable; },
|
4392
4713
|
vjs.Html5.prototype.ended = function(){ return this.el_.ended; };
|
4393
|
-
// videoTracks: function(){ return this.el_.videoTracks; },
|
4394
|
-
// audioTracks: function(){ return this.el_.audioTracks; },
|
4395
|
-
// videoWidth: function(){ return this.el_.videoWidth; },
|
4396
|
-
// videoHeight: function(){ return this.el_.videoHeight; },
|
4397
|
-
// textTracks: function(){ return this.el_.textTracks; },
|
4398
|
-
// defaultPlaybackRate: function(){ return this.el_.defaultPlaybackRate; },
|
4399
|
-
// playbackRate: function(){ return this.el_.playbackRate; },
|
4400
|
-
// mediaGroup: function(){ return this.el_.mediaGroup; },
|
4401
|
-
// controller: function(){ return this.el_.controller; },
|
4402
4714
|
vjs.Html5.prototype.defaultMuted = function(){ return this.el_.defaultMuted; };
|
4403
4715
|
|
4404
4716
|
/* HTML5 Support Testing ---------------------------------------------------- */
|
@@ -4504,7 +4816,14 @@ vjs.Flash = vjs.MediaTechController.extend({
|
|
4504
4816
|
|
4505
4817
|
// If source was supplied pass as a flash var.
|
4506
4818
|
if (source) {
|
4507
|
-
|
4819
|
+
if (source.type && vjs.Flash.isStreamingType(source.type)) {
|
4820
|
+
var parts = vjs.Flash.streamToParts(source.src);
|
4821
|
+
flashVars['rtmpConnection'] = encodeURIComponent(parts.connection);
|
4822
|
+
flashVars['rtmpStream'] = encodeURIComponent(parts.stream);
|
4823
|
+
}
|
4824
|
+
else {
|
4825
|
+
flashVars['src'] = encodeURIComponent(vjs.getAbsoluteURL(source.src));
|
4826
|
+
}
|
4508
4827
|
}
|
4509
4828
|
|
4510
4829
|
// Add placeholder to player div
|
@@ -4622,9 +4941,6 @@ vjs.Flash = vjs.MediaTechController.extend({
|
|
4622
4941
|
// Update reference to playback technology element
|
4623
4942
|
tech.el_ = el;
|
4624
4943
|
|
4625
|
-
// Now that the element is ready, make a click on the swf play the video
|
4626
|
-
vjs.on(el, 'click', tech.bind(tech.onClick));
|
4627
|
-
|
4628
4944
|
// Make sure swf is actually ready. Sometimes the API isn't actually yet.
|
4629
4945
|
vjs.Flash.checkReady(tech);
|
4630
4946
|
});
|
@@ -4667,10 +4983,16 @@ vjs.Flash.prototype.pause = function(){
|
|
4667
4983
|
};
|
4668
4984
|
|
4669
4985
|
vjs.Flash.prototype.src = function(src){
|
4670
|
-
|
4671
|
-
|
4672
|
-
|
4673
|
-
|
4986
|
+
if (vjs.Flash.isStreamingSrc(src)) {
|
4987
|
+
src = vjs.Flash.streamToParts(src);
|
4988
|
+
this.setRtmpConnection(src.connection);
|
4989
|
+
this.setRtmpStream(src.stream);
|
4990
|
+
}
|
4991
|
+
else {
|
4992
|
+
// Make sure source URL is abosolute.
|
4993
|
+
src = vjs.getAbsoluteURL(src);
|
4994
|
+
this.el_.vjs_src(src);
|
4995
|
+
}
|
4674
4996
|
|
4675
4997
|
// Currently the SWF doesn't autoplay if you load a source later.
|
4676
4998
|
// e.g. Load player w/ no source, wait 2s, set src.
|
@@ -4680,6 +5002,20 @@ vjs.Flash.prototype.src = function(src){
|
|
4680
5002
|
}
|
4681
5003
|
};
|
4682
5004
|
|
5005
|
+
vjs.Flash.prototype.currentSrc = function(){
|
5006
|
+
var src = this.el_.vjs_getProperty('currentSrc');
|
5007
|
+
// no src, check and see if RTMP
|
5008
|
+
if (src == null) {
|
5009
|
+
var connection = this.rtmpConnection(),
|
5010
|
+
stream = this.rtmpStream();
|
5011
|
+
|
5012
|
+
if (connection && stream) {
|
5013
|
+
src = vjs.Flash.streamFromParts(connection, stream);
|
5014
|
+
}
|
5015
|
+
}
|
5016
|
+
return src;
|
5017
|
+
};
|
5018
|
+
|
4683
5019
|
vjs.Flash.prototype.load = function(){
|
4684
5020
|
this.el_.vjs_load();
|
4685
5021
|
};
|
@@ -4703,7 +5039,7 @@ vjs.Flash.prototype.enterFullScreen = function(){
|
|
4703
5039
|
|
4704
5040
|
// Create setters and getters for attributes
|
4705
5041
|
var api = vjs.Flash.prototype,
|
4706
|
-
readWrite = 'preload,currentTime,defaultPlaybackRate,playbackRate,autoplay,loop,mediaGroup,controller,controls,volume,muted,defaultMuted'.split(','),
|
5042
|
+
readWrite = 'rtmpConnection,rtmpStream,preload,currentTime,defaultPlaybackRate,playbackRate,autoplay,loop,mediaGroup,controller,controls,volume,muted,defaultMuted'.split(','),
|
4707
5043
|
readOnly = 'error,currentSrc,networkState,readyState,seeking,initialTime,duration,startOffsetTime,paused,played,seekable,ended,videoTracks,audioTracks,videoWidth,videoHeight,textTracks'.split(',');
|
4708
5044
|
// Overridden: buffered
|
4709
5045
|
|
@@ -4744,7 +5080,7 @@ vjs.Flash.isSupported = function(){
|
|
4744
5080
|
};
|
4745
5081
|
|
4746
5082
|
vjs.Flash.canPlaySource = function(srcObj){
|
4747
|
-
if (srcObj.type in vjs.Flash.formats) { return 'maybe'; }
|
5083
|
+
if (srcObj.type in vjs.Flash.formats || srcObj.type in vjs.Flash.streamingFormats) { return 'maybe'; }
|
4748
5084
|
};
|
4749
5085
|
|
4750
5086
|
vjs.Flash.formats = {
|
@@ -4754,6 +5090,11 @@ vjs.Flash.formats = {
|
|
4754
5090
|
'video/m4v': 'MP4'
|
4755
5091
|
};
|
4756
5092
|
|
5093
|
+
vjs.Flash.streamingFormats = {
|
5094
|
+
'rtmp/mp4': 'MP4',
|
5095
|
+
'rtmp/flv': 'FLV'
|
5096
|
+
};
|
5097
|
+
|
4757
5098
|
vjs.Flash['onReady'] = function(currSwf){
|
4758
5099
|
var el = vjs.el(currSwf);
|
4759
5100
|
|
@@ -4768,9 +5109,6 @@ vjs.Flash['onReady'] = function(currSwf){
|
|
4768
5109
|
// Update reference to playback technology element
|
4769
5110
|
tech.el_ = el;
|
4770
5111
|
|
4771
|
-
// Now that the element is ready, make a click on the swf play the video
|
4772
|
-
tech.on('click', tech.onClick);
|
4773
|
-
|
4774
5112
|
vjs.Flash.checkReady(tech);
|
4775
5113
|
};
|
4776
5114
|
|
@@ -4893,6 +5231,54 @@ vjs.Flash.getEmbedCode = function(swf, flashVars, params, attributes){
|
|
4893
5231
|
|
4894
5232
|
return objTag + attrsString + '>' + paramsString + '</object>';
|
4895
5233
|
};
|
5234
|
+
|
5235
|
+
vjs.Flash.streamFromParts = function(connection, stream) {
|
5236
|
+
return connection + '&' + stream;
|
5237
|
+
};
|
5238
|
+
|
5239
|
+
vjs.Flash.streamToParts = function(src) {
|
5240
|
+
var parts = {
|
5241
|
+
connection: '',
|
5242
|
+
stream: ''
|
5243
|
+
};
|
5244
|
+
|
5245
|
+
if (! src) {
|
5246
|
+
return parts;
|
5247
|
+
}
|
5248
|
+
|
5249
|
+
// Look for the normal URL separator we expect, '&'.
|
5250
|
+
// If found, we split the URL into two pieces around the
|
5251
|
+
// first '&'.
|
5252
|
+
var connEnd = src.indexOf('&');
|
5253
|
+
var streamBegin;
|
5254
|
+
if (connEnd !== -1) {
|
5255
|
+
streamBegin = connEnd + 1;
|
5256
|
+
}
|
5257
|
+
else {
|
5258
|
+
// If there's not a '&', we use the last '/' as the delimiter.
|
5259
|
+
connEnd = streamBegin = src.lastIndexOf('/') + 1;
|
5260
|
+
if (connEnd === 0) {
|
5261
|
+
// really, there's not a '/'?
|
5262
|
+
connEnd = streamBegin = src.length;
|
5263
|
+
}
|
5264
|
+
}
|
5265
|
+
parts.connection = src.substring(0, connEnd);
|
5266
|
+
parts.stream = src.substring(streamBegin, src.length);
|
5267
|
+
|
5268
|
+
return parts;
|
5269
|
+
};
|
5270
|
+
|
5271
|
+
vjs.Flash.isStreamingType = function(srcType) {
|
5272
|
+
return srcType in vjs.Flash.streamingFormats;
|
5273
|
+
};
|
5274
|
+
|
5275
|
+
// RTMP has four variations, any string starting
|
5276
|
+
// with one of these protocols should be valid
|
5277
|
+
vjs.Flash.RTMP_RE = /^rtmp[set]?:\/\//i;
|
5278
|
+
|
5279
|
+
vjs.Flash.isStreamingSrc = function(src) {
|
5280
|
+
return vjs.Flash.RTMP_RE.test(src);
|
5281
|
+
};
|
4896
5282
|
/**
|
4897
5283
|
* @constructor
|
4898
5284
|
*/
|