1/*
2 * jPlayer Player Plugin for Popcorn JavaScript Library
3 * http://www.jplayer.org
4 *
5 * Copyright (c) 2012 - 2014 Happyworm Ltd
6 * Licensed under the MIT license.
7 * http://opensource.org/licenses/MIT
8 *
9 * Author: Mark J Panaghiston
10 * Version: 1.1.6
11 * Date: 27th November 2014
12 *
13 * For Popcorn Version: 1.3
14 * For jPlayer Version: 2.9.0
15 * Requires: jQuery 1.7+
16 * Note: jQuery dependancy cannot be removed since jPlayer 2 is a jQuery plugin. Use of jQuery will be kept to a minimum.
17 */
18
19(function(Popcorn) {
20
21	var JQUERY_SCRIPT = '//code.jquery.com/jquery-1.11.1.min.js', // Used if jQuery not already present.
22	JPLAYER_SCRIPT = '//code.jplayer.org/2.9.0/jplayer/jquery.jplayer.min.js', // Used if jPlayer not already present.
23	JPLAYER_SWFPATH = '//code.jplayer.org/2.9.0/jplayer/jquery.jplayer.swf', // Used if not specified in jPlayer options via SRC Object.
24	SOLUTION = 'html,flash', // The default solution option.
25	DEBUG = false, // Decided to leave the debugging option and console output in for the time being. Overhead is trivial.
26	jQueryDownloading = false, // Flag to stop multiple instances from each pulling in jQuery, thus corrupting it.
27	jPlayerDownloading = false, // Flag to stop multiple instances from each pulling in jPlayer, thus corrupting it.
28	format = { // Duplicate of jPlayer 2.5.0 object, to avoid always requiring jQuery and jPlayer to be loaded before performing the _canPlayType() test.
29		mp3: {
30			codec: 'audio/mpeg',
31			flashCanPlay: true,
32			media: 'audio'
33		},
34		m4a: { // AAC / MP4
35			codec: 'audio/mp4; codecs="mp4a.40.2"',
36			flashCanPlay: true,
37			media: 'audio'
38		},
39		m3u8a: { // AAC / MP4 / Apple HLS
40			codec: 'application/vnd.apple.mpegurl; codecs="mp4a.40.2"',
41			flashCanPlay: false,
42			media: 'audio'
43		},
44		m3ua: { // M3U
45			codec: 'audio/mpegurl',
46			flashCanPlay: false,
47			media: 'audio'
48		},
49		oga: { // OGG
50			codec: 'audio/ogg; codecs="vorbis, opus"',
51			flashCanPlay: false,
52			media: 'audio'
53		},
54		flac: { // FLAC
55			codec: 'audio/x-flac',
56			flashCanPlay: false,
57			media: 'audio'
58		},
59		wav: { // PCM
60			codec: 'audio/wav; codecs="1"',
61			flashCanPlay: false,
62			media: 'audio'
63		},
64		webma: { // WEBM
65			codec: 'audio/webm; codecs="vorbis"',
66			flashCanPlay: false,
67			media: 'audio'
68		},
69		fla: { // FLV / F4A
70			codec: 'audio/x-flv',
71			flashCanPlay: true,
72			media: 'audio'
73		},
74		rtmpa: { // RTMP AUDIO
75			codec: 'audio/rtmp; codecs="rtmp"',
76			flashCanPlay: true,
77			media: 'audio'
78		},
79		m4v: { // H.264 / MP4
80			codec: 'video/mp4; codecs="avc1.42E01E, mp4a.40.2"',
81			flashCanPlay: true,
82			media: 'video'
83		},
84		m3u8v: { // H.264 / AAC / MP4 / Apple HLS
85			codec: 'application/vnd.apple.mpegurl; codecs="avc1.42E01E, mp4a.40.2"',
86			flashCanPlay: false,
87			media: 'video'
88		},
89		m3uv: { // M3U
90			codec: 'audio/mpegurl',
91			flashCanPlay: false,
92			media: 'video'
93		},
94		ogv: { // OGG
95			codec: 'video/ogg; codecs="theora, vorbis"',
96			flashCanPlay: false,
97			media: 'video'
98		},
99		webmv: { // WEBM
100			codec: 'video/webm; codecs="vorbis, vp8"',
101			flashCanPlay: false,
102			media: 'video'
103		},
104		flv: { // FLV / F4V
105			codec: 'video/x-flv',
106			flashCanPlay: true,
107			media: 'video'
108		},
109		rtmpv: { // RTMP VIDEO
110			codec: 'video/rtmp; codecs="rtmp"',
111			flashCanPlay: true,
112			media: 'video'
113		}
114	},
115	isObject = function(val) { // Basic check for Object
116		if(val && typeof val === 'object' && val.hasOwnProperty) {
117			return true;
118		} else {
119			return false;
120		}
121	},
122	getMediaType = function(url) { // Function to gleam the media type from the URL
123		var mediaType = false;
124		if(/\.mp3$/i.test(url)) {
125			mediaType = 'mp3';
126		} else if(/\.mp4$/i.test(url) || /\.m4v$/i.test(url)) {
127			mediaType = 'm4v';
128		} else if(/\.m4a$/i.test(url)) {
129			mediaType = 'm4a';
130		} else if(/\.ogg$/i.test(url) || /\.oga$/i.test(url)) {
131			mediaType = 'oga';
132		} else if(/\.ogv$/i.test(url)) {
133			mediaType = 'ogv';
134		} else if(/\.webm$/i.test(url)) {
135			mediaType = 'webmv';
136		}
137		return mediaType;
138	},
139	getSupplied = function(url) { // Function to generate a supplied option from an src object. ie., When supplied not specified.
140		var supplied = '',
141		separator = '';
142		if(isObject(url)) {
143			// Generate supplied option from object's properties. Non-format properties would be ignored by jPlayer. Order is unpredictable.
144			for(var prop in url) {
145				if(url.hasOwnProperty(prop)) {
146					supplied += separator + prop;
147					separator = ',';
148				}
149			}
150		}
151		if(DEBUG) console.log('getSupplied(): Generated: supplied = "' + supplied + '"');
152		return supplied;
153	};
154
155	Popcorn.player( 'jplayer', {
156		_canPlayType: function( containerType, url ) {
157			// url : Either a String or an Object structured similar a jPlayer media object. ie., As used by setMedia in jPlayer.
158			// The url object may also contain a solution and supplied property.
159
160			// Define the src object structure here!
161
162			var cType = containerType.toLowerCase(),
163			srcObj = {
164				media:{},
165				options:{}
166			},
167			rVal = false, // Only a boolean false means it is not supported.
168			mediaType;
169
170			if(cType !== 'video' && cType !== 'audio') {
171
172				if(typeof url === 'string') {
173					// Check it starts with http, so the URL is absolute... Well, it is not a perfect check.
174					if(/^http.*/i.test(url)) {
175						mediaType = getMediaType(url);
176						if(mediaType) {
177							srcObj.media[mediaType] = url;
178							srcObj.options.solution = SOLUTION;
179							srcObj.options.supplied = mediaType;
180						}
181					}
182				} else {
183					srcObj = url; // Assume the url is an src object.
184				}
185
186				// Check for Object and appropriate minimum data structure.
187				if(isObject(srcObj) && isObject(srcObj.media)) {
188
189					if(!isObject(srcObj.options)) {
190						srcObj.options = {};
191					}
192
193					if(!srcObj.options.solution) {
194						srcObj.options.solution = SOLUTION;
195					}
196
197					if(!srcObj.options.supplied) {
198						srcObj.options.supplied = getSupplied(srcObj.media);
199					}
200
201					// Figure out how jPlayer will play it.
202					// This may not work properly when both audio and video is supplied. ie., A media player. But it should return truethy and jPlayer can figure it out.
203
204					var solution = srcObj.options.solution.toLowerCase().split(","), // Create the solution array, with prority based on the order of the solution string.
205					supplied = srcObj.options.supplied.toLowerCase().split(","); // Create the supplied formats array, with prority based on the order of the supplied formats string.
206
207					for(var sol = 0; sol < solution.length; sol++) {
208
209						var solutionType = solution[sol].replace(/^\s+|\s+$/g, ""), //trim
210						checkingHtml = solutionType === 'html',
211						checkingFlash = solutionType === 'flash',
212						mediaElem;
213
214						for(var fmt = 0; fmt < supplied.length; fmt++) {
215							mediaType = supplied[fmt].replace(/^\s+|\s+$/g, ""); //trim
216							if(format[mediaType]) { // Check format is valid.
217
218								// Create an HTML5 media element for the type of media.
219								if(!mediaElem && checkingHtml) {
220									mediaElem = document.createElement(format[mediaType].media);
221								}
222								// See if the HTML5 media element can play the MIME / Codec type.
223								// Flash also returns the object if the format is playable, so it is truethy, but that html property is false.
224								// This assumes Flash is available, but that should be dealt with by jPlayer if that happens.
225								var htmlCanPlay = !!(mediaElem && mediaElem.canPlayType && mediaElem.canPlayType(format[mediaType].codec)),
226								htmlWillPlay = htmlCanPlay && checkingHtml,
227								flashWillPlay = format[mediaType].flashCanPlay && checkingFlash;
228								// The first one found will match what jPlayer uses.
229								if(htmlWillPlay || flashWillPlay) {
230									rVal = {
231										html: htmlWillPlay,
232										type: mediaType
233									};
234									sol = solution.length; // Exit solution loop
235									fmt = supplied.length; // Exit supplied loop
236								}
237							}
238						}
239					}
240				}
241			}
242			return rVal;
243		},
244		// _setup: function( options ) { // Warning: options is deprecated.
245		_setup: function() {
246			var media = this,
247			myPlayer, // The jQuery selector of the jPlayer element. Usually a <div>
248			jPlayerObj, // The jPlayer data instance. For performance and DRY code.
249			mediaType = 'unknown',
250			jpMedia = {},
251			jpOptions = {},
252			ready = false, // Used during init to override the annoying duration dependance in the track event padding during Popcorn's isReady(). ie., We is ready after loadeddata and duration can then be set real value at leisure.
253			duration = 0, // For the durationchange event with both HTML5 and Flash solutions. Used with 'ready' to keep control during the Popcorn isReady() via loadeddata event. (Duration=0 is bad.)
254			durationchangeId = null, // A timeout ID used with delayed durationchange event. (Because of the duration=NaN fudge to avoid Popcorn track event corruption.)
255			canplaythrough = false,
256			error = null, // The MediaError object.
257
258			dispatchDurationChange = function() {
259				if(ready) {
260					if(DEBUG) console.log('Dispatched event : durationchange : ' + duration);
261					media.dispatchEvent('durationchange');
262				} else {
263					if(DEBUG) console.log('DELAYED EVENT (!ready) : durationchange : ' + duration);
264					clearTimeout(durationchangeId); // Stop multiple triggers causing multiple timeouts running in parallel.
265					durationchangeId = setTimeout(dispatchDurationChange, 250);
266				}
267			},
268
269			jPlayerFlashEventsPatch = function() {
270
271				/* Events already supported by jPlayer Flash:
272				 * loadstart
273				 * loadedmetadata (M4A, M4V)
274				 * progress
275				 * play
276				 * pause
277				 * seeking
278				 * seeked
279				 * timeupdate
280				 * ended
281				 * volumechange
282				 * error <- See the custom handler in jPlayerInit()
283				 */
284
285				/* Events patched:
286				 * loadeddata
287				 * durationchange
288				 * canplaythrough
289				 * playing
290				 */
291
292				/* Events NOT patched:
293				 * suspend
294				 * abort
295				 * emptied
296				 * stalled
297				 * loadedmetadata (MP3)
298				 * waiting
299				 * canplay
300				 * ratechange
301				 */
302
303				// Triggering patched events through the jPlayer Object so the events are homogeneous. ie., The contain the event.jPlayer data structure.
304
305				var checkDuration = function(event) {
306					if(event.jPlayer.status.duration !== duration) {
307						duration = event.jPlayer.status.duration;
308						dispatchDurationChange();
309					}
310				},
311
312				checkCanPlayThrough = function(event) {
313					if(!canplaythrough && event.jPlayer.status.seekPercent === 100) {
314						canplaythrough = true;
315						setTimeout(function() {
316							if(DEBUG) console.log('Trigger : canplaythrough');
317							jPlayerObj._trigger($.jPlayer.event.canplaythrough);
318						}, 0);
319					}
320				};
321
322				myPlayer.bind($.jPlayer.event.loadstart, function() {
323					setTimeout(function() {
324						if(DEBUG) console.log('Trigger : loadeddata');
325						jPlayerObj._trigger($.jPlayer.event.loadeddata);
326					}, 0);
327				})
328				.bind($.jPlayer.event.progress, function(event) {
329					checkDuration(event);
330					checkCanPlayThrough(event);
331				})
332				.bind($.jPlayer.event.timeupdate, function(event) {
333					checkDuration(event);
334					checkCanPlayThrough(event);
335				})
336				.bind($.jPlayer.event.play, function() {
337					setTimeout(function() {
338						if(DEBUG) console.log('Trigger : playing');
339						jPlayerObj._trigger($.jPlayer.event.playing);
340					}, 0);
341				});
342
343				if(DEBUG) console.log('Created CUSTOM event handlers for FLASH');
344			},
345
346			jPlayerInit = function() {
347				(function($) {
348
349					myPlayer = $('#' +  media.id);
350
351					if(typeof media.src === 'string') {
352						mediaType = getMediaType(media.src);
353						jpMedia[mediaType] = media.src;
354						jpOptions.supplied = mediaType;
355						jpOptions.solution = SOLUTION;
356					} else if(isObject(media.src)) {
357						jpMedia = isObject(media.src.media) ? media.src.media : {};
358						jpOptions = isObject(media.src.options) ? media.src.options : {};
359						jpOptions.solution = jpOptions.solution || SOLUTION;
360						jpOptions.supplied = jpOptions.supplied || getSupplied(media.src.media);
361					}
362
363					// Allow the swfPath to be set to local server. ie., If the jPlayer Plugin is local and already on the page, then you can also use the local SWF.
364					jpOptions.swfPath = jpOptions.swfPath || JPLAYER_SWFPATH;
365
366					myPlayer.bind($.jPlayer.event.ready, function(event) {
367						if(event.jPlayer.flash.used) {
368							jPlayerFlashEventsPatch();
369						}
370						// Set the media andd load it, so that the Flash solution behaves similar to HTML5 solution.
371						// This also allows the loadstart event to be used to know jPlayer is ready.
372						$(this).jPlayer('setMedia', jpMedia).jPlayer('load');
373					});
374
375					// Do not auto-bubble the reserved events, nor the loadeddata and durationchange event, since the duration must be carefully handled when loadeddata event occurs.
376					// See the duration property code for more details. (Ranting.)
377
378					var reservedEvents = $.jPlayer.reservedEvent + ' loadeddata durationchange',
379					reservedEvent = reservedEvents.split(/\s+/g);
380
381					// Generate event handlers for all the standard HTML5 media events. (Except durationchange)
382
383					var bindEvent = function(name) {
384						myPlayer.bind($.jPlayer.event[name], function(event) {
385							if(DEBUG) console.log('Dispatched event: ' + name + (event && event.jPlayer ? ' (' + event.jPlayer.status.currentTime + 's)' : '')); // Must be after dispatch for some reason on Firefox/Opera
386							media.dispatchEvent(name);
387						});
388						if(DEBUG) console.log('Created event handler for: ' + name);
389					};
390
391					for(var eventName in $.jPlayer.event) {
392						if($.jPlayer.event.hasOwnProperty(eventName)) {
393							var nativeEvent = true;
394							for(var iRes in reservedEvent) {
395								if(reservedEvent.hasOwnProperty(iRes)) {
396									if(reservedEvent[iRes] === eventName) {
397										nativeEvent = false;
398										break;
399									}
400								}
401							}
402							if(nativeEvent) {
403								bindEvent(eventName);
404							} else {
405								if(DEBUG) console.log('Skipped auto event handler creation for: ' + eventName);
406							}
407						}
408					}
409
410					myPlayer.bind($.jPlayer.event.loadeddata, function(event) {
411						if(DEBUG) console.log('Dispatched event: loadeddata' + (event && event.jPlayer ? ' (' + event.jPlayer.status.currentTime + 's)' : ''));
412						media.dispatchEvent('loadeddata');
413						ready = true;
414					});
415					if(DEBUG) console.log('Created CUSTOM event handler for: loadeddata');
416
417					myPlayer.bind($.jPlayer.event.durationchange, function(event) {
418						duration = event.jPlayer.status.duration;
419						dispatchDurationChange();
420					});
421					if(DEBUG) console.log('Created CUSTOM event handler for: durationchange');
422
423					// The error event is a special case. Plus jPlayer error event assumes it is a broken URL. (It could also be a decoder error... Or aborted or a Network error.)
424					myPlayer.bind($.jPlayer.event.error, function(event) {
425						// Not sure how to handle the error situation. Popcorn does not appear to have the error or error.code property documented here: http://popcornjs.org/popcorn-docs/media-methods/
426						// If any error event happens, then something has gone pear shaped.
427
428						error = event.jPlayer.error; // Saving object pointer, not a copy of the object. Possible garbage collection issue... But the player is dead anyway, so don't care.
429
430						if(error.type === $.jPlayer.error.URL) {
431							error.code = 4; // MEDIA_ERR_SRC_NOT_SUPPORTED since jPlayer makes this assumption. It is the most common error, then the decode error. Never seen either of the other 2 error types occur.
432						} else {
433							error.code = 0; // It was a jPlayer error, not an HTML5 media error.
434						}
435
436						if(DEBUG) console.log('Dispatched event: error');
437						if(DEBUG) console.dir(error);
438						media.dispatchEvent('error');
439					});
440					if(DEBUG) console.log('Created CUSTOM event handler for: error');
441
442					Popcorn.player.defineProperty( media, 'error', {
443						set: function() {
444							// Read-only property
445							return error;
446						},
447						get: function() {
448							return error;
449						}
450					});
451
452					Popcorn.player.defineProperty( media, 'currentTime', {
453						set: function( val ) {
454							if(jPlayerObj.status.paused) {
455								myPlayer.jPlayer('pause', val);
456							} else {
457								myPlayer.jPlayer('play', val);
458							}
459							return val;
460						},
461						get: function() {
462							return jPlayerObj.status.currentTime;
463						}
464					});
465
466					/* The joy of duration and the loadeddata event isReady() handler
467					 * The duration is assumed to be a NaN or a valid duration.
468					 * jPlayer uses zero instead of a NaN and this screws up the Popcorn track event start/end arrays padding.
469					 * This line here:
470					 *  videoDurationPlus = duration != duration ? Number.MAX_VALUE : duration + 1;
471					 * Not sure why it is not simply:
472					 *  videoDurationPlus = Number.MAX_VALUE; // Who cares if the padding is close to the real duration?
473					 * So if you trigger loadeddata before the duration is correct, the track event padding is screwed up. (It pads the start, not the end... Well, duration+1 = 0+1 = 1s)
474					 * That line makes the MP3 Flash fallback difficult to setup. The whole MP3 will need to load before the duration is known.
475					 * Planning on using a NaN for duration until a >0 value is found... Except with MP3, where seekPercent must be 100% before setting the duration.
476					 * Why not just use a NaN during init... And then correct the duration later?
477					 */
478
479					Popcorn.player.defineProperty( media, 'duration', {
480						set: function() {
481							// Read-only property
482							if(ready) {
483								return duration;
484							} else {
485								return NaN;
486							}
487						},
488						get: function() {
489							if(ready) {
490								return duration; // Popcorn has initialized, we can now use duration zero or whatever without fear.
491							} else {
492								return NaN; // Keep the duration a NaN until after loadeddata event has occurred. Otherwise Popcorn track event padding is corrupted.
493							}
494						}
495					});
496
497					Popcorn.player.defineProperty( media, 'muted', {
498						set: function( val ) {
499							myPlayer.jPlayer('mute', val);
500							return jPlayerObj.options.muted;
501						},
502						get: function() {
503							return jPlayerObj.options.muted;
504						}
505					});
506
507					Popcorn.player.defineProperty( media, 'volume', {
508						set: function( val ) {
509							myPlayer.jPlayer('volume', val);
510							return jPlayerObj.options.volume;
511						},
512						get: function() {
513							return jPlayerObj.options.volume;
514						}
515					});
516
517					Popcorn.player.defineProperty( media, 'paused', {
518						set: function() {
519							// Read-only property
520							return jPlayerObj.status.paused;
521						},
522						get: function() {
523							return jPlayerObj.status.paused;
524						}
525					});
526
527					media.play = function() {
528						myPlayer.jPlayer('play');
529					};
530					media.pause = function() {
531						myPlayer.jPlayer('pause');
532					};
533
534					myPlayer.jPlayer(jpOptions); // Instance jPlayer. Note that the options should not have a ready event defined... Kill it by default?
535					jPlayerObj = myPlayer.data('jPlayer');
536
537				}(jQuery));
538			},
539
540			jPlayerCheck = function() {
541				if (!jQuery.jPlayer) {
542					if (!jPlayerDownloading) {
543						jPlayerDownloading = true;
544						Popcorn.getScript(JPLAYER_SCRIPT, function() {
545							jPlayerDownloading = false;
546							jPlayerInit();
547						});
548					} else {
549						setTimeout(jPlayerCheck, 250);
550					}
551				} else {
552					jPlayerInit();
553				}
554			},
555
556			jQueryCheck = function() {
557				if (!window.jQuery) {
558					if (!jQueryDownloading) {
559						jQueryDownloading = true;
560						Popcorn.getScript(JQUERY_SCRIPT, function() {
561							jQueryDownloading = false;
562							jPlayerCheck();
563						});
564					} else {
565						setTimeout(jQueryCheck, 250);
566					}
567				} else {
568					jPlayerCheck();
569				}
570			};
571
572			jQueryCheck();
573		},
574		_teardown: function() {
575			jQuery('#' +  this.id).jPlayer('destroy');
576		}
577	});
578
579}(Popcorn));