|
1 /*! |
|
2 * Popcorn.sequence |
|
3 * |
|
4 * Copyright 2011, Rick Waldron |
|
5 * Licensed under MIT license. |
|
6 * |
|
7 */ |
|
8 |
|
9 /* jslint forin: true, maxerr: 50, indent: 4, es5: true */ |
|
10 /* global Popcorn: true */ |
|
11 |
|
12 // Requires Popcorn.js |
|
13 (function( global, Popcorn ) { |
|
14 |
|
15 // TODO: as support increases, migrate to element.dataset |
|
16 var doc = global.document, |
|
17 location = global.location, |
|
18 rprotocol = /:\/\//, |
|
19 // TODO: better solution to this sucky stop-gap |
|
20 lochref = location.href.replace( location.href.split("/").slice(-1)[0], "" ), |
|
21 // privately held |
|
22 range = function(start, stop, step) { |
|
23 |
|
24 start = start || 0; |
|
25 stop = ( stop || start || 0 ) + 1; |
|
26 step = step || 1; |
|
27 |
|
28 var len = Math.ceil((stop - start) / step) || 0, |
|
29 idx = 0, |
|
30 range = []; |
|
31 |
|
32 range.length = len; |
|
33 |
|
34 while (idx < len) { |
|
35 range[idx++] = start; |
|
36 start += step; |
|
37 } |
|
38 return range; |
|
39 }; |
|
40 |
|
41 Popcorn.sequence = function( parent, list ) { |
|
42 return new Popcorn.sequence.init( parent, list ); |
|
43 }; |
|
44 |
|
45 Popcorn.sequence.init = function( parent, list ) { |
|
46 |
|
47 // Video element |
|
48 this.parent = doc.getElementById( parent ); |
|
49 |
|
50 // Store ref to a special ID |
|
51 this.seqId = Popcorn.guid( "__sequenced" ); |
|
52 |
|
53 // List of HTMLVideoElements |
|
54 this.queue = []; |
|
55 |
|
56 // List of Popcorn objects |
|
57 this.playlist = []; |
|
58 |
|
59 // Lists of in/out points |
|
60 this.inOuts = { |
|
61 |
|
62 // Stores the video in/out times for each video in sequence |
|
63 ofVideos: [], |
|
64 |
|
65 // Stores the clip in/out times for each clip in sequences |
|
66 ofClips: [] |
|
67 |
|
68 }; |
|
69 |
|
70 // Store first video dimensions |
|
71 this.dims = { |
|
72 width: 0, //this.video.videoWidth, |
|
73 height: 0 //this.video.videoHeight |
|
74 }; |
|
75 |
|
76 this.active = 0; |
|
77 this.cycling = false; |
|
78 this.playing = false; |
|
79 |
|
80 this.times = { |
|
81 last: 0 |
|
82 }; |
|
83 |
|
84 // Store event pointers and queues |
|
85 this.events = { |
|
86 |
|
87 }; |
|
88 |
|
89 var self = this, |
|
90 clipOffset = 0; |
|
91 |
|
92 // Create `video` elements |
|
93 Popcorn.forEach( list, function( media, idx ) { |
|
94 |
|
95 var video = doc.createElement( "video" ); |
|
96 |
|
97 video.preload = "auto"; |
|
98 |
|
99 // Setup newly created video element |
|
100 video.controls = true; |
|
101 |
|
102 // If the first, show it, if the after, hide it |
|
103 video.style.display = ( idx && "none" ) || "" ; |
|
104 |
|
105 // Seta registered sequence id |
|
106 video.id = self.seqId + "-" + idx ; |
|
107 |
|
108 // Push this video into the sequence queue |
|
109 self.queue.push( video ); |
|
110 |
|
111 var //satisfy lint |
|
112 mIn = media["in"], |
|
113 mOut = media["out"]; |
|
114 |
|
115 // Push the in/out points into sequence ioVideos |
|
116 self.inOuts.ofVideos.push({ |
|
117 "in": mIn !== undefined && typeof mIn === "number" ? mIn : 1, |
|
118 "out": mOut !== undefined && typeof mOut === "number" ? mOut : 0 |
|
119 }); |
|
120 |
|
121 self.inOuts.ofVideos[ idx ]["out"] = self.inOuts.ofVideos[ idx ]["out"] || self.inOuts.ofVideos[ idx ]["in"] + 2; |
|
122 |
|
123 // Set the sources |
|
124 video.src = !rprotocol.test( media.src ) ? lochref + media.src : media.src; |
|
125 |
|
126 // Set some squence specific data vars |
|
127 video.setAttribute("data-sequence-owner", parent ); |
|
128 video.setAttribute("data-sequence-guid", self.seqId ); |
|
129 video.setAttribute("data-sequence-id", idx ); |
|
130 video.setAttribute("data-sequence-clip", [ self.inOuts.ofVideos[ idx ]["in"], self.inOuts.ofVideos[ idx ]["out"] ].join(":") ); |
|
131 |
|
132 // Append the video to the parent element |
|
133 self.parent.appendChild( video ); |
|
134 |
|
135 |
|
136 self.playlist.push( Popcorn("#" + video.id ) ); |
|
137 |
|
138 }); |
|
139 |
|
140 self.inOuts.ofVideos.forEach(function( obj ) { |
|
141 |
|
142 var clipDuration = obj["out"] - obj["in"], |
|
143 offs = { |
|
144 "in": clipOffset, |
|
145 "out": clipOffset + clipDuration |
|
146 }; |
|
147 |
|
148 self.inOuts.ofClips.push( offs ); |
|
149 |
|
150 clipOffset = offs["out"]; |
|
151 }); |
|
152 |
|
153 Popcorn.forEach( this.queue, function( media, idx ) { |
|
154 |
|
155 function canPlayThrough( event ) { |
|
156 |
|
157 // If this is idx zero, use it as dimension for all |
|
158 if ( !idx ) { |
|
159 self.dims.width = media.videoWidth; |
|
160 self.dims.height = media.videoHeight; |
|
161 } |
|
162 |
|
163 // -0.2 prevents trackEvents from firing when they trigger on a clips in value |
|
164 media.currentTime = self.inOuts.ofVideos[ idx ]["in"] - 0.2; |
|
165 |
|
166 media.removeEventListener( "canplaythrough", canPlayThrough, false ); |
|
167 |
|
168 return true; |
|
169 } |
|
170 |
|
171 // Hook up event oners for managing special playback |
|
172 media.addEventListener( "canplaythrough", canPlayThrough, false ); |
|
173 |
|
174 // TODO: consolidate & DRY |
|
175 media.addEventListener( "play", function( event ) { |
|
176 |
|
177 self.playing = true; |
|
178 |
|
179 }, false ); |
|
180 |
|
181 media.addEventListener( "pause", function( event ) { |
|
182 |
|
183 self.playing = false; |
|
184 |
|
185 }, false ); |
|
186 |
|
187 media.addEventListener( "timeupdate", function( event ) { |
|
188 |
|
189 var target = event.srcElement || event.target, |
|
190 seqIdx = +( (target.dataset && target.dataset.sequenceId) || target.getAttribute("data-sequence-id") ), |
|
191 floor = Math.floor( media.currentTime ); |
|
192 |
|
193 if ( self.times.last !== floor && |
|
194 seqIdx === self.active ) { |
|
195 |
|
196 self.times.last = floor; |
|
197 |
|
198 if ( floor === self.inOuts.ofVideos[ seqIdx ]["out"] ) { |
|
199 |
|
200 Popcorn.sequence.cycle.call( self, seqIdx ); |
|
201 } |
|
202 } |
|
203 }, false ); |
|
204 |
|
205 media.addEventListener( "ended", function( event ) { |
|
206 |
|
207 var target = event.srcElement || event.target, |
|
208 seqIdx = +( (target.dataset && target.dataset.sequenceId) || target.getAttribute("data-sequence-id") ); |
|
209 |
|
210 Popcorn.sequence.cycle.call( self, seqIdx ); |
|
211 }, false ); |
|
212 }); |
|
213 |
|
214 return this; |
|
215 }; |
|
216 |
|
217 Popcorn.sequence.init.prototype = Popcorn.sequence.prototype; |
|
218 |
|
219 Popcorn.sequence.cycle = function( fromIdx, toIdx ) { |
|
220 |
|
221 if ( !this.queue ) { |
|
222 Popcorn.error("Popcorn.sequence.cycle is not a public method"); |
|
223 } |
|
224 |
|
225 // no cycle needed, bail |
|
226 if ( fromIdx === toIdx ) { |
|
227 return this; |
|
228 } |
|
229 |
|
230 // Localize references |
|
231 var queue = this.queue, |
|
232 ioVideos = this.inOuts.ofVideos, |
|
233 current = queue[ fromIdx ], |
|
234 nextIdx, next, clip; |
|
235 |
|
236 // Popcorn instances |
|
237 var $popnext, |
|
238 $popprev; |
|
239 |
|
240 nextIdx = typeof toIdx === "number" ? toIdx : fromIdx + 1; |
|
241 |
|
242 // Reset queue |
|
243 if ( !queue[ nextIdx ] ) { |
|
244 |
|
245 nextIdx = 0; |
|
246 this.playlist[ fromIdx ].pause(); |
|
247 |
|
248 } else { |
|
249 |
|
250 next = queue[ nextIdx ]; |
|
251 clip = ioVideos[ nextIdx ]; |
|
252 |
|
253 // Constrain dimentions |
|
254 Popcorn.extend( next, { |
|
255 width: this.dims.width, |
|
256 height: this.dims.height |
|
257 }); |
|
258 |
|
259 $popnext = this.playlist[ nextIdx ]; |
|
260 $popprev = this.playlist[ fromIdx ]; |
|
261 |
|
262 current.pause(); |
|
263 |
|
264 this.active = nextIdx; |
|
265 this.times.last = clip["in"] - 1; |
|
266 |
|
267 if ($popnext !== undefined) { |
|
268 console.log("$popnext ok!") |
|
269 } |
|
270 if ($popnext.currentTime !== undefined) { |
|
271 console.log("$popnext.currentTime ok!") |
|
272 } |
|
273 if (clip !== undefined) { |
|
274 console.log("clip ok!") |
|
275 } |
|
276 if (clip["in"] !== undefined) { |
|
277 console.log("clip[in] ok!") |
|
278 } |
|
279 |
|
280 // Hide the currently ending video |
|
281 current.style.display = "none"; |
|
282 |
|
283 // Show the next video in the sequence |
|
284 next.style.display = ""; |
|
285 |
|
286 // Play the next video in the sequence |
|
287 $popnext.currentTime( clip["in"] ); |
|
288 |
|
289 $popnext.play(); |
|
290 |
|
291 // Trigger custom cycling event hook |
|
292 this.trigger( "cycle", { |
|
293 |
|
294 position: { |
|
295 previous: fromIdx, |
|
296 current: nextIdx |
|
297 } |
|
298 |
|
299 }); |
|
300 |
|
301 this.cycling = false; |
|
302 } |
|
303 |
|
304 return this; |
|
305 }; |
|
306 |
|
307 var excludes = [ "timeupdate", "play", "pause" ]; |
|
308 |
|
309 // Sequence object prototype |
|
310 Popcorn.extend( Popcorn.sequence.prototype, { |
|
311 |
|
312 // Returns Popcorn object from sequence at index |
|
313 eq: function( idx ) { |
|
314 return this.playlist[ idx ]; |
|
315 }, |
|
316 // Remove a sequence from it's playback display container |
|
317 remove: function() { |
|
318 this.parent.innerHTML = null; |
|
319 }, |
|
320 // Returns Clip object from sequence at index |
|
321 clip: function( idx ) { |
|
322 return this.inOuts.ofVideos[ idx ]; |
|
323 }, |
|
324 // Returns sum duration for all videos in sequence |
|
325 duration: function() { |
|
326 |
|
327 var ret = 0, |
|
328 seq = this.inOuts.ofClips, |
|
329 idx = 0; |
|
330 |
|
331 for ( ; idx < seq.length; idx++ ) { |
|
332 ret += seq[ idx ]["out"] - seq[ idx ]["in"]; |
|
333 } |
|
334 |
|
335 return ret; |
|
336 }, |
|
337 |
|
338 play: function() { |
|
339 |
|
340 /* |
|
341 var c = Math.round( this.queue[ this.active ].currentTime ); |
|
342 |
|
343 if ( ( ( this.queue.length - 1 ) === this.active ) && |
|
344 ( this.inOuts[ "ofVideos" ][ this.active ][ "out" ] >= Math.round( this.queue[ this.active ].currentTime ) ) ) |
|
345 { |
|
346 this.jumpTo( 0 ); |
|
347 } else */{ |
|
348 this.queue[ this.active ].play(); |
|
349 } |
|
350 |
|
351 return this; |
|
352 }, |
|
353 // Pause the sequence |
|
354 pause: function() { |
|
355 |
|
356 this.queue[ this.active ].pause(); |
|
357 |
|
358 return this; |
|
359 |
|
360 }, |
|
361 // Attach an event to a single point in time |
|
362 cue: function ( time, fn ) { |
|
363 |
|
364 var index = this.active; |
|
365 |
|
366 this.inOuts.ofClips.forEach(function( off, idx ) { |
|
367 if ( time >= off["in"] && time <= off["out"] ) { |
|
368 index = idx; |
|
369 } |
|
370 }); |
|
371 |
|
372 //offsetBy = time - self.inOuts.ofVideos[ index ].in; |
|
373 |
|
374 time += this.inOuts.ofVideos[ index ]["in"] - this.inOuts.ofClips[ index ]["in"]; |
|
375 |
|
376 // Cue up the callback on the correct popcorn instance |
|
377 this.playlist[ index ].cue( time, fn ); |
|
378 |
|
379 return this; |
|
380 }, |
|
381 // Binds event handlers that fire only when all |
|
382 // videos in sequence have heard the event |
|
383 on: function( type, callback ) { |
|
384 |
|
385 var self = this, |
|
386 seq = this.playlist, |
|
387 total = seq.length, |
|
388 count = 0, |
|
389 fnName; |
|
390 |
|
391 if ( !callback ) { |
|
392 callback = Popcorn.nop; |
|
393 } |
|
394 |
|
395 // Handling for DOM and Media events |
|
396 if ( Popcorn.Events.Natives.indexOf( type ) > -1 ) { |
|
397 Popcorn.forEach( seq, function( video ) { |
|
398 |
|
399 video.on( type, function( event ) { |
|
400 |
|
401 event.active = self; |
|
402 |
|
403 if ( excludes.indexOf( type ) > -1 ) { |
|
404 |
|
405 callback.call( video, event ); |
|
406 |
|
407 } else { |
|
408 if ( ++count === total ) { |
|
409 callback.call( video, event ); |
|
410 } |
|
411 } |
|
412 }); |
|
413 }); |
|
414 } else { |
|
415 |
|
416 // If no events registered with this name, create a cache |
|
417 if ( !this.events[ type ] ) { |
|
418 this.events[ type ] = {}; |
|
419 } |
|
420 |
|
421 // Normalize a callback name key |
|
422 fnName = callback.name || Popcorn.guid( "__" + type ); |
|
423 |
|
424 // Store in event cache |
|
425 this.events[ type ][ fnName ] = callback; |
|
426 } |
|
427 |
|
428 // Return the sequence object |
|
429 return this; |
|
430 }, |
|
431 off: function( type, name ) { |
|
432 |
|
433 var seq = this.playlist; |
|
434 |
|
435 if ( Popcorn.Events.Natives.indexOf( type ) > -1 ) { |
|
436 Popcorn.forEach( seq, function( video ) { |
|
437 video.off( type, name ); |
|
438 }); |
|
439 } else { |
|
440 |
|
441 this.events[ type ] = null; |
|
442 } |
|
443 |
|
444 return this; |
|
445 }, |
|
446 emit: function( type, data ) { |
|
447 var self = this; |
|
448 |
|
449 // Handling for DOM and Media events |
|
450 if ( Popcorn.Events.Natives.indexOf( type ) > -1 ) { |
|
451 // find the active video and trigger api events on that video. |
|
452 return; |
|
453 } else { |
|
454 // Only proceed if there are events of this type |
|
455 // currently registered on the sequence |
|
456 if ( this.events[ type ] ) { |
|
457 Popcorn.forEach( this.events[ type ], function( callback, name ) { |
|
458 callback.call( self, { type: type }, data ); |
|
459 }); |
|
460 } |
|
461 } |
|
462 return this; |
|
463 }, |
|
464 currentTime: function() { |
|
465 var index = this.active, |
|
466 currentTime = 0; |
|
467 |
|
468 this.inOuts.ofClips.forEach(function( off, idx ) { |
|
469 if ( idx < index ) { |
|
470 currentTime += this.inOuts.ofVideos[ idx ]["out"] - this.inOuts.ofVideos[ idx ]["in"]; |
|
471 } |
|
472 }, this ); |
|
473 currentTime += this.playlist[ index ].currentTime() - this.inOuts.ofVideos[ index ]["in"]; |
|
474 return currentTime; |
|
475 }, |
|
476 jumpTo: function( time ) { |
|
477 |
|
478 if ( time < 0 || time > this.duration() ) { |
|
479 return this; |
|
480 } |
|
481 |
|
482 var index, found, real, curInOuts; |
|
483 offsetTime = 0; |
|
484 |
|
485 found = false; |
|
486 |
|
487 this.inOuts.ofClips.forEach(function( off, idx ) { |
|
488 var inOuts = this.inOuts; |
|
489 if ( !found ) { |
|
490 if ( (time >= inOuts.ofClips[ idx ]["in"] && |
|
491 time <= inOuts.ofClips[ idx ]["out"]) ) { |
|
492 |
|
493 found = true; |
|
494 index = idx; |
|
495 real = ( time - offsetTime ) + inOuts.ofVideos[ idx ]["in"]; |
|
496 } else { |
|
497 offsetTime += inOuts.ofClips[ idx ]["out"] - offsetTime; |
|
498 } |
|
499 } |
|
500 }, this ); |
|
501 Popcorn.sequence.cycle.call( this, this.active, index ); |
|
502 curInOuts = this.inOuts.ofVideos[ index ] |
|
503 |
|
504 // Jump to the calculated time in the clip, making sure it's in the correct range |
|
505 this.playlist[ index ].currentTime( real >= curInOuts["in"] && real <= curInOuts["out"] ? real : curInOuts["in"] ); |
|
506 |
|
507 return this; |
|
508 } |
|
509 }); |
|
510 |
|
511 [ ["exec", "cue"], ["listen", "on"], ["unlisten", "off"], ["trigger", "emit"] ].forEach(function( remap ) { |
|
512 |
|
513 Popcorn.sequence.prototype[ remap[0] ] = Popcorn.sequence.prototype[ remap[1] ]; |
|
514 }) |
|
515 |
|
516 Popcorn.forEach( Popcorn.manifest, function( obj, plugin ) { |
|
517 |
|
518 // Implement passthrough methods to plugins |
|
519 Popcorn.sequence.prototype[ plugin ] = function( options ) { |
|
520 |
|
521 var videos = {}, assignTo = [], |
|
522 idx, off, inOuts, inIdx, outIdx, |
|
523 keys, clip, clipInOut, clipRange; |
|
524 |
|
525 for ( idx = 0; idx < this.inOuts.ofClips.length; idx++ ) { |
|
526 // store reference |
|
527 off = this.inOuts.ofClips[ idx ]; |
|
528 // array to test against |
|
529 inOuts = range( off["in"], off["out"] ); |
|
530 |
|
531 inIdx = inOuts.indexOf( options.start ); |
|
532 outIdx = inOuts.indexOf( options.end ); |
|
533 |
|
534 if ( inIdx > -1 ) { |
|
535 videos[ idx ] = Popcorn.extend( {}, off, { |
|
536 start: inOuts[ inIdx ], |
|
537 clipIdx: inIdx |
|
538 }); |
|
539 } |
|
540 |
|
541 if ( outIdx > -1 ) { |
|
542 videos[ idx ] = Popcorn.extend( {}, off, { |
|
543 end: inOuts[ outIdx ], |
|
544 clipIdx: outIdx |
|
545 }); |
|
546 } |
|
547 } |
|
548 |
|
549 keys = Object.keys( videos ).map(function( val ) { |
|
550 return +val; |
|
551 }); |
|
552 |
|
553 assignTo = range( keys[ 0 ], keys[ 1 ] ); |
|
554 |
|
555 for ( idx = 0; idx < assignTo.length; idx++ ) { |
|
556 |
|
557 var compile = {}, |
|
558 play = assignTo[ idx ], |
|
559 vClip = videos[ play ]; |
|
560 |
|
561 if ( vClip ) { |
|
562 // has instructions |
|
563 clip = this.inOuts.ofVideos[ play ]; |
|
564 clipInOut = vClip.clipIdx; |
|
565 clipRange = range( clip["in"], clip["out"] ); |
|
566 |
|
567 if ( vClip.start ) { |
|
568 compile.start = clipRange[ clipInOut ]; |
|
569 compile.end = clipRange[ clipRange.length - 1 ]; |
|
570 } |
|
571 |
|
572 if ( vClip.end ) { |
|
573 compile.start = clipRange[ 0 ]; |
|
574 compile.end = clipRange[ clipInOut ]; |
|
575 } |
|
576 } else { |
|
577 compile.start = this.inOuts.ofVideos[ play ]["in"]; |
|
578 compile.end = this.inOuts.ofVideos[ play ]["out"]; |
|
579 } |
|
580 |
|
581 // Call the plugin on the appropriate Popcorn object in the playlist |
|
582 // Merge original options object & compiled (start/end) object into |
|
583 // a new object |
|
584 this.playlist[ play ][ plugin ]( |
|
585 Popcorn.extend( {}, options, compile ) |
|
586 ); |
|
587 } |
|
588 // Return the sequence object |
|
589 return this; |
|
590 }; |
|
591 }); |
|
592 })( this, Popcorn ); |