popcorn.player.js 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461
  1. (function( Popcorn ) {
  2. // combines calls of two function calls into one
  3. var combineFn = function( first, second ) {
  4. first = first || Popcorn.nop;
  5. second = second || Popcorn.nop;
  6. return function() {
  7. first.apply( this, arguments );
  8. second.apply( this, arguments );
  9. };
  10. };
  11. // ID string matching
  12. var rIdExp = /^(#([\w\-\_\.]+))$/;
  13. var audioExtensions = "ogg|oga|aac|mp3|wav",
  14. videoExtensions = "ogg|ogv|mp4|webm",
  15. mediaExtensions = audioExtensions + "|" + videoExtensions;
  16. var audioExtensionRegexp = new RegExp( "^.*\\.(" + audioExtensions + ")($|\\?)" ),
  17. mediaExtensionRegexp = new RegExp( "^.*\\.(" + mediaExtensions + ")($|\\?)" );
  18. Popcorn.player = function( name, player ) {
  19. // return early if a player already exists under this name
  20. if ( Popcorn[ name ] ) {
  21. return;
  22. }
  23. player = player || {};
  24. var playerFn = function( target, src, options ) {
  25. options = options || {};
  26. // List of events
  27. var date = new Date() / 1000,
  28. baselineTime = date,
  29. currentTime = 0,
  30. readyState = 0,
  31. volume = 1,
  32. muted = false,
  33. events = {},
  34. // The container div of the resource
  35. container = typeof target === "string" ? Popcorn.dom.find( target ) : target,
  36. basePlayer = {},
  37. timeout,
  38. popcorn;
  39. if ( !Object.prototype.__defineGetter__ ) {
  40. basePlayer = container || document.createElement( "div" );
  41. }
  42. // copies a div into the media object
  43. for( var val in container ) {
  44. // don't copy properties if using container as baseplayer
  45. if ( val in basePlayer ) {
  46. continue;
  47. }
  48. if ( typeof container[ val ] === "object" ) {
  49. basePlayer[ val ] = container[ val ];
  50. } else if ( typeof container[ val ] === "function" ) {
  51. basePlayer[ val ] = (function( value ) {
  52. // this is a stupid ugly kludgy hack in honour of Safari
  53. // in Safari a NodeList is a function, not an object
  54. if ( "length" in container[ value ] && !container[ value ].call ) {
  55. return container[ value ];
  56. } else {
  57. return function() {
  58. return container[ value ].apply( container, arguments );
  59. };
  60. }
  61. }( val ));
  62. } else {
  63. Popcorn.player.defineProperty( basePlayer, val, {
  64. get: (function( value ) {
  65. return function() {
  66. return container[ value ];
  67. };
  68. }( val )),
  69. set: Popcorn.nop,
  70. configurable: true
  71. });
  72. }
  73. }
  74. var timeupdate = function() {
  75. date = new Date() / 1000;
  76. if ( !basePlayer.paused ) {
  77. basePlayer.currentTime = basePlayer.currentTime + ( date - baselineTime );
  78. basePlayer.dispatchEvent( "timeupdate" );
  79. timeout = setTimeout( timeupdate, 10 );
  80. }
  81. baselineTime = date;
  82. };
  83. basePlayer.play = function() {
  84. this.paused = false;
  85. if ( basePlayer.readyState >= 4 ) {
  86. baselineTime = new Date() / 1000;
  87. basePlayer.dispatchEvent( "play" );
  88. timeupdate();
  89. }
  90. };
  91. basePlayer.pause = function() {
  92. this.paused = true;
  93. basePlayer.dispatchEvent( "pause" );
  94. };
  95. Popcorn.player.defineProperty( basePlayer, "currentTime", {
  96. get: function() {
  97. return currentTime;
  98. },
  99. set: function( val ) {
  100. // make sure val is a number
  101. currentTime = +val;
  102. basePlayer.dispatchEvent( "timeupdate" );
  103. return currentTime;
  104. },
  105. configurable: true
  106. });
  107. Popcorn.player.defineProperty( basePlayer, "volume", {
  108. get: function() {
  109. return volume;
  110. },
  111. set: function( val ) {
  112. // make sure val is a number
  113. volume = +val;
  114. basePlayer.dispatchEvent( "volumechange" );
  115. return volume;
  116. },
  117. configurable: true
  118. });
  119. Popcorn.player.defineProperty( basePlayer, "muted", {
  120. get: function() {
  121. return muted;
  122. },
  123. set: function( val ) {
  124. // make sure val is a number
  125. muted = +val;
  126. basePlayer.dispatchEvent( "volumechange" );
  127. return muted;
  128. },
  129. configurable: true
  130. });
  131. Popcorn.player.defineProperty( basePlayer, "readyState", {
  132. get: function() {
  133. return readyState;
  134. },
  135. set: function( val ) {
  136. readyState = val;
  137. return readyState;
  138. },
  139. configurable: true
  140. });
  141. // Adds an event listener to the object
  142. basePlayer.addEventListener = function( evtName, fn ) {
  143. if ( !events[ evtName ] ) {
  144. events[ evtName ] = [];
  145. }
  146. events[ evtName ].push( fn );
  147. return fn;
  148. };
  149. // Removes an event listener from the object
  150. basePlayer.removeEventListener = function( evtName, fn ) {
  151. var i,
  152. listeners = events[ evtName ];
  153. if ( !listeners ){
  154. return;
  155. }
  156. // walk backwards so we can safely splice
  157. for ( i = events[ evtName ].length - 1; i >= 0; i-- ) {
  158. if( fn === listeners[ i ] ) {
  159. listeners.splice(i, 1);
  160. }
  161. }
  162. return fn;
  163. };
  164. // Can take event object or simple string
  165. basePlayer.dispatchEvent = function( oEvent ) {
  166. var evt,
  167. self = this,
  168. eventInterface,
  169. eventName = oEvent.type;
  170. // A string was passed, create event object
  171. if ( !eventName ) {
  172. eventName = oEvent;
  173. eventInterface = Popcorn.events.getInterface( eventName );
  174. if ( eventInterface ) {
  175. evt = document.createEvent( eventInterface );
  176. evt.initEvent( eventName, true, true, window, 1 );
  177. }
  178. }
  179. if ( events[ eventName ] ) {
  180. for ( var i = events[ eventName ].length - 1; i >= 0; i-- ) {
  181. events[ eventName ][ i ].call( self, evt, self );
  182. }
  183. }
  184. };
  185. // Attempt to get src from playerFn parameter
  186. basePlayer.src = src || "";
  187. basePlayer.duration = 0;
  188. basePlayer.paused = true;
  189. basePlayer.ended = 0;
  190. options && options.events && Popcorn.forEach( options.events, function( val, key ) {
  191. basePlayer.addEventListener( key, val, false );
  192. });
  193. // true and undefined returns on canPlayType means we should attempt to use it,
  194. // false means we cannot play this type
  195. if ( player._canPlayType( container.nodeName, src ) !== false ) {
  196. if ( player._setup ) {
  197. player._setup.call( basePlayer, options );
  198. } else {
  199. // there is no setup, which means there is nothing to load
  200. basePlayer.readyState = 4;
  201. basePlayer.dispatchEvent( "loadedmetadata" );
  202. basePlayer.dispatchEvent( "loadeddata" );
  203. basePlayer.dispatchEvent( "canplaythrough" );
  204. }
  205. } else {
  206. // Asynchronous so that users can catch this event
  207. setTimeout( function() {
  208. basePlayer.dispatchEvent( "error" );
  209. }, 0 );
  210. }
  211. popcorn = new Popcorn.p.init( basePlayer, options );
  212. if ( player._teardown ) {
  213. popcorn.destroy = combineFn( popcorn.destroy, function() {
  214. player._teardown.call( basePlayer, options );
  215. });
  216. }
  217. return popcorn;
  218. };
  219. playerFn.canPlayType = player._canPlayType = player._canPlayType || Popcorn.nop;
  220. Popcorn[ name ] = Popcorn.player.registry[ name ] = playerFn;
  221. };
  222. Popcorn.player.registry = {};
  223. Popcorn.player.defineProperty = Object.defineProperty || function( object, description, options ) {
  224. object.__defineGetter__( description, options.get || Popcorn.nop );
  225. object.__defineSetter__( description, options.set || Popcorn.nop );
  226. };
  227. // player queue is to help players queue things like play and pause
  228. // HTML5 video's play and pause are asynch, but do fire in sequence
  229. // play() should really mean "requestPlay()" or "queuePlay()" and
  230. // stash a callback that will play the media resource when it's ready to be played
  231. Popcorn.player.playerQueue = function() {
  232. var _queue = [],
  233. _running = false;
  234. return {
  235. next: function() {
  236. _running = false;
  237. _queue.shift();
  238. _queue[ 0 ] && _queue[ 0 ]();
  239. },
  240. add: function( callback ) {
  241. _queue.push(function() {
  242. _running = true;
  243. callback && callback();
  244. });
  245. // if there is only one item on the queue, start it
  246. !_running && _queue[ 0 ]();
  247. }
  248. };
  249. };
  250. // smart will attempt to find you a match, if it does not find a match,
  251. // it will attempt to create a video element with the source,
  252. // if that failed, it will throw.
  253. Popcorn.smart = function( target, src, options ) {
  254. var playerType,
  255. elementTypes = [ "AUDIO", "VIDEO" ],
  256. sourceNode,
  257. firstSrc,
  258. node = Popcorn.dom.find( target ),
  259. i, srcResult,
  260. canPlayTypeTester = document.createElement( "video" ),
  261. canPlayTypes = {
  262. "ogg": "video/ogg",
  263. "ogv": "video/ogg",
  264. "oga": "audio/ogg",
  265. "webm": "video/webm",
  266. "mp4": "video/mp4",
  267. "mp3": "audio/mp3"
  268. };
  269. var canPlayType = function( type ) {
  270. return canPlayTypeTester.canPlayType( canPlayTypes[ type ] );
  271. };
  272. var canPlaySrc = function( src ) {
  273. srcResult = mediaExtensionRegexp.exec( src );
  274. if ( !srcResult || !srcResult[ 1 ] ) {
  275. return false;
  276. }
  277. return canPlayType( srcResult[ 1 ] );
  278. };
  279. if ( !node ) {
  280. Popcorn.error( "Specified target " + target + " was not found." );
  281. return;
  282. }
  283. // For when no src is defined.
  284. // Usually this is a video element with a src already on it.
  285. if ( elementTypes.indexOf( node.nodeName ) > -1 && !src ) {
  286. if ( typeof src === "object" ) {
  287. options = src;
  288. src = undefined;
  289. }
  290. return Popcorn( node, options );
  291. }
  292. // if our src is not an array, create an array of one.
  293. if ( typeof( src ) === "string" ) {
  294. src = [ src ];
  295. }
  296. // go through each src, and find the first playable.
  297. // this only covers player sources popcorn knows of,
  298. // and not things like a youtube src that is private.
  299. // it will still consider a private youtube video to be playable.
  300. for ( i = 0, srcLength = src.length; i < srcLength; i++ ) {
  301. // src is a playable HTML5 video, we don't need to check custom players.
  302. if ( canPlaySrc( src[ i ] ) ) {
  303. src = src[ i ];
  304. break;
  305. }
  306. // for now we loop through and use the first valid player we find.
  307. for ( var key in Popcorn.player.registry ) {
  308. if ( Popcorn.player.registry.hasOwnProperty( key ) ) {
  309. if ( Popcorn.player.registry[ key ].canPlayType( node.nodeName, src[ i ] ) ) {
  310. // Popcorn.smart( player, src, /* options */ )
  311. return Popcorn[ key ]( node, src[ i ], options );
  312. }
  313. }
  314. }
  315. }
  316. // Popcorn.smart( div, src, /* options */ )
  317. // attempting to create a video in a container
  318. if ( elementTypes.indexOf( node.nodeName ) === -1 ) {
  319. firstSrc = typeof( src ) === "string" ? src : src.length ? src[ 0 ] : src;
  320. target = document.createElement( !!audioExtensionRegexp.exec( firstSrc ) ? elementTypes[ 0 ] : elementTypes[ 1 ] );
  321. // Controls are defaulted to being present
  322. target.controls = true;
  323. node.appendChild( target );
  324. node = target;
  325. }
  326. options && options.events && options.events.error && node.addEventListener( "error", options.events.error, false );
  327. node.src = src;
  328. return Popcorn( node, options );
  329. };
  330. })( Popcorn );