├── README ├── css └── audio.css ├── images ├── background.jpg ├── radiolab-header.jpg ├── radiolab.jpg ├── sprites.gif └── waveform.png ├── index.htm ├── js ├── audio.js └── libs │ ├── Jplayer.swf │ ├── jquery.jplayer.js │ ├── jquery.min.js │ ├── jquery.scrollTo-min.js │ ├── mod.csstransitions.min.js │ ├── popcorn.js │ ├── popcorn.scComments.js │ ├── popcorn.transcript.js │ └── popcorn.wordriver.js └── transcript.html /README: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maboa/Radiolab-Soundcloud-Popcorn.js-Demo/5031cedc4b7e8e628d38702a5a98bc978777c6b0/README -------------------------------------------------------------------------------- /css/audio.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed under the MIT license. 3 | * - http://www.opensource.org/licenses/mit-license.php 4 | * 5 | * Authors: 6 | * Silvia Benvenuti @aulentina 7 | * Mark Boas @maboa 8 | * Mark Panaghiston @thepag 9 | */ 10 | 11 | .progress-container { 12 | position: relative; 13 | height: 50px; 14 | /* top:-25px;*/ 15 | } 16 | .waveform-container { 17 | position: absolute; 18 | top: 0px; 19 | } 20 | .waveform-container img { 21 | width:1024px; 22 | height: 50px; 23 | background-color: #fd7426; 24 | } 25 | .jp-progress { 26 | position: absolute; 27 | top: 0px; 28 | width:1024px; 29 | height: 50px; 30 | } 31 | .jp-seek-bar { 32 | height: 50px; 33 | background-color: #fff; 34 | opacity: 0.4; 35 | cursor: pointer; 36 | } 37 | .jp-play-bar { 38 | height: 50px; 39 | background-color: #000; 40 | opacity: 0.4; 41 | cursor: pointer; 42 | } 43 | 44 | 45 | /* @maboa hacks */ 46 | 47 | ul, ol { 48 | list-style-type: none; 49 | margin: 0; 50 | padding: 0; 51 | } 52 | 53 | body { 54 | /* 55 | background-image:url('../images/background.jpg'); 56 | background-repeat:no-repeat; 57 | background-position:50% 0; 58 | margin: 218px auto 0 auto; 59 | width: 1024px; */ 60 | font-family: Verdana, Helvetica, Arial, "Lucida Grande"; 61 | font-size: 12px; 62 | margin: 0; 63 | padding: 0; 64 | } 65 | 66 | a img{ 67 | border: none; 68 | outline:none; 69 | margin-right: 10px; 70 | } 71 | 72 | #container { 73 | position:relative; 74 | background-image:url('../images/background.jpg'); 75 | background-repeat:no-repeat; 76 | background-position:50% 0; 77 | margin:0 auto; 78 | padding-top: 140px; 79 | width: 1024px; 80 | /*z-index: -1;*/ 81 | } 82 | 83 | #transcript { 84 | position:relative; 85 | overflow:auto; 86 | height:310px; 87 | width:630px; 88 | /* background-color:#fff; */ 89 | background-color: rgba(255,255,255,0.8); 90 | color:#666; 91 | padding: 12px; 92 | /* margin:0 auto 189px auto; /* 213 - (2 * 12) = 189px */ 93 | margin:0 auto 148px auto; /* 213 - (2 * 12) = 189px */ 94 | z-index:30; 95 | } 96 | 97 | #images { 98 | position:relative; 99 | margin:0 auto 172px auto; 100 | height:310px; 101 | width:630px; 102 | z-index:30; 103 | } 104 | 105 | 106 | #transcript-content span { 107 | cursor: pointer; 108 | } 109 | 110 | #transcript-content span:hover { 111 | color: #f07d00; 112 | } 113 | 114 | 115 | #transcript-content span.transcript-grey { 116 | color: #ccc; 117 | } 118 | 119 | #images-square-left { 120 | width: 310px; 121 | height: 310px; 122 | /*background-image:url('../images/abe1.jpg'); */ 123 | float: left; 124 | } 125 | 126 | #images-square-right { 127 | width: 310px; 128 | height: 310px; 129 | /*background-image:url('../images/abe2.jpg'); */ 130 | float: right; 131 | } 132 | 133 | #images-widescreen { 134 | width: 630px; 135 | height: 310px; 136 | /*background-image:url('../images/abe2.jpg'); */ 137 | float: left; 138 | } 139 | 140 | #simple-controls { 141 | /* position: absolute; 142 | top:90px; 143 | right:0;*/ 144 | margin-top:86px; 145 | width:50px; 146 | height: 153px; 147 | text-indent:-9999px; 148 | list-style-type:none; 149 | float: right; 150 | } 151 | 152 | #social-controls { 153 | /* position: absolute; 154 | top:90px; 155 | left:0;*/ 156 | margin-top:86px; 157 | width:50px; 158 | height: 153px; 159 | list-style-type:none; 160 | float: left; 161 | } 162 | 163 | #simple-controls a, #social-controls a{ 164 | display: block; 165 | width: 50px; 166 | height: 50px; 167 | border-bottom: 1px solid #fff; 168 | } 169 | 170 | #simple-controls a:hover, #social-controls a:hover{ 171 | cursor:pointer; 172 | } 173 | 174 | .jp-play{ 175 | background:url('../images/sprites.gif') 0 0 no-repeat; 176 | } 177 | 178 | .jp-pause{ 179 | background:url('../images/sprites.gif') 0 -50px no-repeat; 180 | } 181 | 182 | .jp-pause-flash{ 183 | background:url('../images/sprites.gif') -50px -50px no-repeat; 184 | } 185 | 186 | .jp-restart{ 187 | background:url('../images/sprites.gif') 0 -200px no-repeat; 188 | } 189 | 190 | .show-trans{ 191 | background:url('../images/sprites.gif') 0 -100px no-repeat; 192 | } 193 | 194 | .hide-trans{ 195 | background:url('../images/sprites.gif') 0 -150px no-repeat; 196 | } 197 | 198 | .twitter { 199 | background:url('../images/sprites.gif') 0 -250px no-repeat; 200 | } 201 | 202 | .facebook { 203 | background:url('../images/sprites.gif') 0 -300px no-repeat; 204 | } 205 | 206 | .soundcloud { 207 | background:url('../images/sprites.gif') 0 -350px no-repeat; 208 | } 209 | 210 | 211 | 212 | #comments-public { 213 | /* position: absolute; 214 | top:480px;*/ 215 | height:18px; 216 | cursor: pointer; 217 | width: 100%; 218 | /* background-color:#fe5a03;*/ 219 | } 220 | 221 | 222 | li.timestamped-comment .marker .user-image-tiny { 223 | left: 1px; 224 | position: absolute !important; 225 | } 226 | 227 | .user-image-tiny { 228 | height: 18px; 229 | width: 18px; 230 | color: #0066CC; 231 | cursor: pointer; 232 | outline: 0 none; 233 | text-decoration: none; 234 | } 235 | 236 | .timestamped-comments { 237 | background-color: #3399FF; 238 | background-position: 0 -25px; 239 | border-top: 1px solid #CCCCCC; 240 | color: #0066CC; 241 | cursor: pointer; 242 | font-size: 12px; 243 | height: 18px; 244 | left: 0; 245 | position: absolute !important; 246 | top: 89px; 247 | width: 100%; 248 | z-index: 1180; 249 | } 250 | 251 | /*#wordsriver-container { 252 | position: absolute; 253 | top:0px; 254 | width: 100%; 255 | z-index:-1; 256 | } */ 257 | 258 | #wordsriver { 259 | position: absolute; 260 | top:0px; 261 | left:197px; 262 | width: 630px; 263 | /* height: 152px; */ 264 | height: 572px; 265 | font-family: Arial; 266 | margin-top: 50px; 267 | margin-left: auto; 268 | margin-right: auto; 269 | z-index:20; 270 | } 271 | 272 | #wordsriver .speaker-robert-krulwich { 273 | color: #6cdbd5; 274 | font-family: Arial; 275 | } 276 | 277 | #wordsriver .speaker-jad-abumrad { 278 | color: #fd7426; 279 | font-family: "Lucida Grande"; 280 | } 281 | 282 | #wordsriver .speaker-john-walter { 283 | color: #333333; 284 | font-family: Verdana; 285 | } 286 | 287 | #wordsriver .speaker-1 { 288 | color: gray; 289 | font-family: Arial; 290 | } 291 | 292 | #wordsriver .speaker-2 { 293 | color: black; 294 | font-family: Arial; 295 | } 296 | 297 | #wordsriver .speaker-3 { 298 | color: gray; 299 | font-family: Arial; 300 | } 301 | 302 | #commentOutput, #destructions{ 303 | position: absolute; 304 | background-color:#fd9d6d; 305 | color:#fff; 306 | padding: 6px; 307 | -webkit-border-radius: 8px; 308 | -moz-border-radius: 8px; 309 | border-radius: 8px; 310 | opacity: 0.8; 311 | } 312 | 313 | #commentOutput { 314 | top: 564px; 315 | left: 140px; 316 | z-index:20; 317 | } 318 | 319 | #destructions { 320 | top: 500px; 321 | left: 810px; 322 | } 323 | 324 | #commentOutput a { 325 | text-decoration: none; 326 | color:#333; 327 | cursor: pointer; 328 | } 329 | 330 | -------------------------------------------------------------------------------- /images/background.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maboa/Radiolab-Soundcloud-Popcorn.js-Demo/5031cedc4b7e8e628d38702a5a98bc978777c6b0/images/background.jpg -------------------------------------------------------------------------------- /images/radiolab-header.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maboa/Radiolab-Soundcloud-Popcorn.js-Demo/5031cedc4b7e8e628d38702a5a98bc978777c6b0/images/radiolab-header.jpg -------------------------------------------------------------------------------- /images/radiolab.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maboa/Radiolab-Soundcloud-Popcorn.js-Demo/5031cedc4b7e8e628d38702a5a98bc978777c6b0/images/radiolab.jpg -------------------------------------------------------------------------------- /images/sprites.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maboa/Radiolab-Soundcloud-Popcorn.js-Demo/5031cedc4b7e8e628d38702a5a98bc978777c6b0/images/sprites.gif -------------------------------------------------------------------------------- /images/waveform.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maboa/Radiolab-Soundcloud-Popcorn.js-Demo/5031cedc4b7e8e628d38702a5a98bc978777c6b0/images/waveform.png -------------------------------------------------------------------------------- /index.htm: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | Radiolab Player demo 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 |
36 |
37 | 38 |
39 | 40 | 47 | 48 | 54 | 55 |
moo!
56 |
Toggle between transcript and images using the transcript button above.
57 | 58 |
59 |
60 |
61 |
62 |
63 | 65 |
66 | 67 |
    68 |
69 | 70 |
71 |
72 | 73 |
74 | 79 |
80 | 81 | 82 |
83 | 84 | 85 | -------------------------------------------------------------------------------- /js/audio.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed under the MIT license. 3 | * - http://www.opensource.org/licenses/mit-license.php 4 | * 5 | * Authors: 6 | * Mark Boas @maboa 7 | * Mark Panaghiston @thepag 8 | * Steven Weerdenburg 9 | */ 10 | 11 | $(document).ready(function(){ 12 | 13 | // super commenter - to be used to decide what to do with comment 14 | 15 | var admin = "Radiolab"; 16 | var mediaId = "13580897"; 17 | var apiKey = "CHAyhB5IisvLqqzGYNYbmA"; 18 | var duration = 1016; // change this later for flexibility - the issue is that we don't know the duration until the track has completely loaded 19 | 20 | // support checks 21 | 22 | var cssTransitionsSupport = Modernizr.csstransitions; 23 | var audioTagSupport = !!(document.createElement('audio').canPlayType); 24 | 25 | if (audioTagSupport == false ) { 26 | $('#commentOutput').text('Unfortunatley your browser does not support audio natively and so this demo will not run. Upgrade to the latest version of your browser for the best experience.'); 27 | $('#destructions').text('Houston we have a problem!'); 28 | } else if (cssTransitionsSupport == false) { 29 | $('#commentOutput').text('This demo features effects that rely on CSS3 transitions, which your browser does not support. Upgrade to the latest version of your browser for the best experience.'); 30 | } 31 | 32 | 33 | // Hide the URL bar for iPhone / iPad 34 | 35 | addEventListener("load", function(){ 36 | setTimeout(updateLayout, 0); 37 | }, false); 38 | 39 | 40 | 41 | /*document.ontouchmove = function (event) { 42 | if (!event.elementIsEnabled) { 43 | event.preventDefault(); 44 | } 45 | };*/ 46 | 47 | function updateLayout(){ 48 | if (navigator.userAgent.indexOf('iPhone') != -1 || navigator.userAgent.indexOf('iPod') != -1 || navigator.userAgent.indexOf('iPad') != -1) 49 | { 50 | //setTimeout("window.scrollTo(0, 14)", 0); 51 | setTimeout(function(){ 52 | window.scrollTo(0, 1); 53 | }, 100); 54 | } 55 | } 56 | 57 | 58 | // load transcript - @maboa 59 | // and wait until it is in place before doing anything 60 | 61 | $('#transcript-content').load('transcript.html',function(){ 62 | setup(); 63 | }); 64 | 65 | 66 | 67 | var client_id = '?client_id='+apiKey; 68 | var myPlayer = $("#jquery_jplayer_1"); 69 | 70 | // 71 | 72 | function setup() { 73 | 74 | $.ajax({ 75 | url: "http://api.soundcloud.com/tracks/"+mediaId+".json" + client_id, 76 | dataType: 'jsonp', 77 | success: function(data) { 78 | 79 | myPlayer.jPlayer({ 80 | ready: function (event) { 81 | $(this).jPlayer("setMedia", { 82 | mp3: "http://api.soundcloud.com/tracks/"+mediaId+"/stream?client_id="+apiKey, 83 | oga: "http://api.soundcloud.com/tracks/"+mediaId+"/download?client_id="+apiKey, 84 | 85 | //mp3: data.stream_url + client_id, 86 | //oga: data.download_url + client_id 87 | }); 88 | if(event.jPlayer.html.used && event.jPlayer.html.audio.available) { 89 | initPopcorn('#' + $(this).data("jPlayer").internal.audio.id); 90 | } 91 | }, 92 | swfPath: "js/libs", // Not important for HTML only solution. 93 | // solution: "html", 94 | supplied: "mp3,oga", 95 | cssSelectorAncestor: "", 96 | cssSelector: { 97 | play: "#simple-controls .jp-play", 98 | pause: "#simple-controls .jp-pause", 99 | seekBar: "#waveform .jp-seek-bar", 100 | playBar: "#waveform .jp-play-bar" 101 | }, 102 | preload: "none" 103 | }); 104 | 105 | $("#waveform-img").attr("src", data.waveform_url); 106 | } 107 | }); 108 | 109 | } 110 | 111 | function grabImages(text) { 112 | 113 | var url1 = ""; 114 | var url2 = ""; 115 | var space = 0; 116 | var images = [null,null]; 117 | 118 | // replacing newline or return with space 119 | text = text.replace('\n',' '); 120 | text = text.replace('\r',' '); 121 | 122 | // checking for images in the comments 123 | 124 | var urlStart = text.indexOf('http://'); 125 | 126 | if (urlStart >= 0) { 127 | 128 | text = text.substr(urlStart,text.length); 129 | space = text.indexOf(' '); 130 | 131 | if (space < 0) { 132 | url1 = text; 133 | text = ""; 134 | } else { 135 | url1 = text.substr(0,space); 136 | text = text.substr(space,text.length); 137 | } 138 | 139 | urlStart = text.indexOf('http://'); 140 | 141 | if (urlStart >= 0) { 142 | text = text.substr(urlStart,text.length); 143 | space = text.indexOf(' '); 144 | 145 | if (space < 0) { 146 | url2 = text; 147 | } else { 148 | url2 = text.substr(0,space); 149 | } 150 | } 151 | 152 | if (url1.indexOf('.jpg') >= 0) images[0] = url1; 153 | if (url2.indexOf('.jpg') >= 0) images[1] = url2; 154 | } 155 | 156 | return images; 157 | } 158 | 159 | function initPopcorn(id) { 160 | 161 | //var p = Popcorn(id); 162 | 163 | // @maboa changes for comment functionality 164 | 165 | var p = Popcorn(id) 166 | .scComments({ 167 | apikey: apiKey, 168 | mediaid: mediaId, 169 | limit: 500 170 | } ) 171 | .listen( 'scCommentIn', function( comment ) { 172 | var text = comment.text; 173 | var images = [null,null]; 174 | 175 | if (comment.user.name == admin) { 176 | // search the contents 177 | 178 | images = grabImages(text); 179 | 180 | if (images[1]) { 181 | $('#images-square-left').css("background-image", "url("+images[0]+")"); 182 | $('#images-square-right').css("background-image", "url("+images[1]+")"); 183 | $('#images-widescreen').hide(); 184 | $('#images-square').show(); 185 | } else if (images[0]){ 186 | $('#images-widescreen').css("background-image", "url("+images[0]+")"); 187 | $('#images-square').hide(); 188 | $('#images-widescreen').show(); 189 | } 190 | } 191 | 192 | if (!(images[0])) { 193 | formatComment( comment ); 194 | } 195 | }) 196 | .listen( 'scCommentOut', function( comment ) { 197 | //document.getElementById('commentOutput').innerHTML = ''; 198 | }) 199 | .listen( 'scLoadedmetadata', function( data ) { 200 | 201 | var comments = this.scComments.tracks[mediaId].comments; 202 | var lis = ""; 203 | Popcorn.forEach( comments, function ( obj ) { 204 | var pc = (100*obj.start)/duration; 205 | var scTime = obj.start*1000; 206 | 207 | if (!(obj.user.name == admin && obj.text.indexOf('.jpg') >= 0)){ 208 | lis = lis + '
  • '; 209 | } else { // let's cache 210 | images = grabImages(obj.text); 211 | 212 | if (images[0]) (new Image).src = images[0]; 213 | if (images[1]) (new Image).src = images[1]; 214 | } 215 | 216 | }); 217 | $('#comments-public').append(lis); 218 | 219 | // blink pause button 220 | 221 | $('.jp-pause').addClass('jp-pause-flash'); 222 | setTimeout(function(){ 223 | $('.jp-pause').removeClass('jp-pause-flash'); 224 | }, 400); 225 | 226 | Popcorn.sortTracks( p ); 227 | }); 228 | 229 | // can we make this delegate - more efficient 230 | //$('.timestamped-comment').delegate('a','click',function(){ 231 | $('.timestamped-comment a').live('click',function() { 232 | return false; // overkill - see http://fuelyourcoding.com/jquery-events-stop-misusing-return-false/ 233 | }); 234 | 235 | // $('.comment-holder').click(function() { 236 | 237 | $('#comments-public').click(function(e) { 238 | var offset = $(this).offset(); 239 | var x = e.pageX - offset.left; 240 | var width = $(this).width(); 241 | var time = Math.floor((x/width)*duration*1000); 242 | window.open('http://soundcloud.com/radiolab/hairpart#new-timed-comment-at-'+time); 243 | 244 | //console.log(time); 245 | //console.log($(this).position) 246 | //myPlayer.jPlayer("play",jumpTo); 247 | return false; // overkill - see http://fuelyourcoding.com/jquery-events-stop-misusing-return-false/ 248 | }); 249 | 250 | /* Colors from Squares: 251 | * Teal: #6cdbd5 252 | * Light Teal: #beecea 253 | * Orange: #fd7426 254 | * Light Orange: #fd9d6d 255 | * Grey: #88898b 256 | */ 257 | 258 | // Changes color on text containing either ':' or '[' using unknown array. 259 | // Known speakers use the object, 260 | var wrColors = { 261 | known: [ // Array of objects for known speakers 262 | { 263 | name: [ 264 | "Robert Krulwich", 265 | "R.K." 266 | ], 267 | className: "speaker-robert-krulwich" 268 | }, { 269 | name: [ 270 | "Jad Abumrad", 271 | "J.A." 272 | ], 273 | className: "speaker-jad-abumrad" 274 | }, { 275 | name: [ 276 | "John Walter", 277 | "J.W." 278 | ], 279 | className: "speaker-john-walter" 280 | } 281 | ], 282 | unknown: [ // Array of CSS Class name to use for unknown speakers. 283 | "speaker-1", 284 | "speaker-2", 285 | "speaker-3" 286 | ] 287 | }; 288 | 289 | $("#transcript-content span").each(function(i) { 290 | 291 | p.transcript({ 292 | time: $(this).attr("m") / 1000, // seconds 293 | futureClass: "transcript-grey", 294 | target: this 295 | }) 296 | .wordriver({ 297 | start: $(this).attr("m") / 1000, // seconds 298 | middle: ( $(this).attr("m") / 1000 ) + 4, // seconds 299 | end: ( $(this).attr("m") / 1000 ) + 6, // seconds 300 | // end - start is the speed in which the word moves. 301 | // make middle = start to remove its effect competely. Or middle undefined. 302 | text: $(this).text(), 303 | target: "wordsriver", 304 | opacity: { 305 | start: 0, 306 | middle: 0.5, 307 | end: 0, 308 | duration: 1 309 | }, 310 | colors: wrColors 311 | }); 312 | }); 313 | Popcorn.sortTracks( p ); 314 | myPlayer.jPlayer("play"); // Auto-play the media after all the processing has been completed. 315 | } 316 | 317 | 318 | // transcript links to audio 319 | 320 | $('#transcript').delegate('span','click',function(){ 321 | var jumpTo = $(this).attr('m')/1000; 322 | 323 | myPlayer.jPlayer("play",jumpTo); 324 | 325 | return false; 326 | }); 327 | 328 | // transcript/image toggle 329 | 330 | var oldTranscriptTop = 0; // Need to store this value for: Chrome, Safari and IE9. They reset the scroll to zero when hidden. 331 | 332 | $('.show-trans').click(function() { 333 | 334 | $('#transcript').show(); // Show it first! 335 | $('#transcript').scrollTop( oldTranscriptTop ); // Correct the position after displaying. 336 | 337 | var $target = $("#transcript-content span.transcript-grey:first").parent(); // The paragraph of the word. 338 | $target = $target.prev().length ? $target.prev() : $target; // Select the previous paragraph if there is one. 339 | 340 | // SW - Transcript has progressed beyond last paragraph, select last. Prevents crash in jquery 341 | $target = $target.length ? $target : $("#transcript-content span").last().parent(); 342 | 343 | $("#transcript").stop().scrollTo($target, 800, {axis:'y',margin:true}); 344 | 345 | $('#images').hide(); 346 | $(this).hide(); 347 | $('.hide-trans').show(); 348 | $('#destructions').fadeOut(); 349 | return false; 350 | }); 351 | 352 | 353 | $('.hide-trans').click(function() { 354 | 355 | oldTranscriptTop = $('#transcript').scrollTop(); // Store the position before hiding 356 | 357 | $('#transcript').hide(); 358 | $('#images').show(); 359 | $(this).hide(); 360 | $('.show-trans').show(); 361 | return false; 362 | }); 363 | 364 | $('.jp-restart').click( function() { 365 | myPlayer.jPlayer("play", 0); 366 | return false; 367 | }); 368 | 369 | // some utility functions useful for comments from Steven 370 | 371 | // This is simply a callback function 372 | function formatComment( comment ) { 373 | var floor = Math.floor, 374 | round = Math.round; 375 | 376 | // Calclate the difference between d and now, express as "n units ago" 377 | function ago( d ) { 378 | var diff = ( ( new Date() ).getTime() - d.getTime() )/1000; 379 | 380 | function pluralize( value, unit ) { 381 | return value + " " + unit + ( value > 1 ? "s" : "") + " ago"; 382 | } 383 | 384 | if ( diff < 60 ) { 385 | return pluralize( round( diff ), "second" ); 386 | } 387 | diff /= 60; 388 | 389 | if ( diff < 60 ) { 390 | return pluralize( round( diff ), "minute" ); 391 | } 392 | diff /= 60; 393 | 394 | if ( diff < 24 ) { 395 | return pluralize( round( diff ), "hour" ); 396 | } 397 | diff /= 24; 398 | 399 | // Rough approximation of months 400 | if ( diff < 30 ) { 401 | return pluralize( round( diff ), "day" ); 402 | } 403 | 404 | if ( diff < 365 ) { 405 | return pluralize( round( diff/30 ), "month" ); 406 | } 407 | 408 | return pluralize( round( diff/365 ), "year" ); 409 | } 410 | 411 | // Converts sec to [hr.]min.sec 412 | // Seconds are 0-prefixed, minutes are 0-prefixed if hours exist. 413 | function timeToFraction ( totalSec ) { 414 | var hr = floor( totalSec / 3600 ), 415 | min = floor( totalSec / 60 ), 416 | sec = floor( totalSec % 60 ), 417 | ret = min + "." + ( sec < 10 ? "0" : "" ) + sec; 418 | 419 | // SW - Added this to account for longer segments 420 | // Also changed calculations to floor all rather than round to be consistent with Soundcloud calculations 421 | if ( hr ) { 422 | ret = hr + "." + ( min < 10 ? "0" : "" ) + ret; 423 | } 424 | 425 | return ret; 426 | } 427 | 428 | document.getElementById('commentOutput').innerHTML = '
    ' 429 | + '' 430 | + comment.user.name + ' at ' + timeToFraction( comment.start ) + ' ' 431 | + ago( comment.date ) 432 | + '
    ' + comment.text + ''; 433 | } 434 | 435 | }); 436 | -------------------------------------------------------------------------------- /js/libs/Jplayer.swf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maboa/Radiolab-Soundcloud-Popcorn.js-Demo/5031cedc4b7e8e628d38702a5a98bc978777c6b0/js/libs/Jplayer.swf -------------------------------------------------------------------------------- /js/libs/jquery.jplayer.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jPlayer Plugin for jQuery JavaScript Library 3 | * http://www.jplayer.org 4 | * 5 | * Copyright (c) 2009 - 2011 Happyworm Ltd 6 | * Dual licensed under the MIT and GPL licenses. 7 | * - http://www.opensource.org/licenses/mit-license.php 8 | * - http://www.gnu.org/copyleft/gpl.html 9 | * 10 | * Author: Mark J Panaghiston 11 | * Version: 2.0.9 12 | * Date: 13th April 2011 13 | */ 14 | 15 | (function($, undefined) { 16 | 17 | // Adapted from jquery.ui.widget.js (1.8.7): $.widget.bridge 18 | $.fn.jPlayer = function( options ) { 19 | var name = "jPlayer"; 20 | var isMethodCall = typeof options === "string", 21 | args = Array.prototype.slice.call( arguments, 1 ), 22 | returnValue = this; 23 | 24 | // allow multiple hashes to be passed on init 25 | options = !isMethodCall && args.length ? 26 | $.extend.apply( null, [ true, options ].concat(args) ) : 27 | options; 28 | 29 | // prevent calls to internal methods 30 | if ( isMethodCall && options.charAt( 0 ) === "_" ) { 31 | return returnValue; 32 | } 33 | 34 | if ( isMethodCall ) { 35 | this.each(function() { 36 | var instance = $.data( this, name ), 37 | methodValue = instance && $.isFunction( instance[options] ) ? 38 | instance[ options ].apply( instance, args ) : 39 | instance; 40 | if ( methodValue !== instance && methodValue !== undefined ) { 41 | returnValue = methodValue; 42 | return false; 43 | } 44 | }); 45 | } else { 46 | this.each(function() { 47 | var instance = $.data( this, name ); 48 | if ( instance ) { 49 | // instance.option( options || {} )._init(); // Orig jquery.ui.widget.js code: Not recommend for jPlayer. ie., Applying new options to an existing instance (via the jPlayer constructor) and performing the _init(). The _init() is what concerns me. It would leave a lot of event handlers acting on jPlayer instance and the interface. 50 | instance.option( options || {} ); // The new constructor only changes the options. Changing options only has basic support atm. 51 | } else { 52 | $.data( this, name, new $.jPlayer( options, this ) ); 53 | } 54 | }); 55 | } 56 | 57 | return returnValue; 58 | }; 59 | 60 | $.jPlayer = function( options, element ) { 61 | // allow instantiation without initializing for simple inheritance 62 | if ( arguments.length ) { 63 | this.element = $(element); 64 | this.options = $.extend(true, {}, 65 | this.options, 66 | options 67 | ); 68 | var self = this; 69 | this.element.bind( "remove.jPlayer", function() { 70 | self.destroy(); 71 | }); 72 | this._init(); 73 | } 74 | }; 75 | // End of: (Adapted from jquery.ui.widget.js (1.8.7)) 76 | 77 | $.jPlayer.event = { 78 | ready: "jPlayer_ready", 79 | resize: "jPlayer_resize", // Not implemented. 80 | error: "jPlayer_error", // Event error code in event.jPlayer.error.type. See $.jPlayer.error 81 | warning: "jPlayer_warning", // Event warning code in event.jPlayer.warning.type. See $.jPlayer.warning 82 | 83 | // Other events match HTML5 spec. 84 | loadstart: "jPlayer_loadstart", 85 | progress: "jPlayer_progress", 86 | suspend: "jPlayer_suspend", 87 | abort: "jPlayer_abort", 88 | emptied: "jPlayer_emptied", 89 | stalled: "jPlayer_stalled", 90 | play: "jPlayer_play", 91 | pause: "jPlayer_pause", 92 | loadedmetadata: "jPlayer_loadedmetadata", 93 | loadeddata: "jPlayer_loadeddata", 94 | waiting: "jPlayer_waiting", 95 | playing: "jPlayer_playing", 96 | canplay: "jPlayer_canplay", 97 | canplaythrough: "jPlayer_canplaythrough", 98 | seeking: "jPlayer_seeking", 99 | seeked: "jPlayer_seeked", 100 | timeupdate: "jPlayer_timeupdate", 101 | ended: "jPlayer_ended", 102 | ratechange: "jPlayer_ratechange", 103 | durationchange: "jPlayer_durationchange", 104 | volumechange: "jPlayer_volumechange" 105 | }; 106 | 107 | $.jPlayer.htmlEvent = [ // These HTML events are bubbled through to the jPlayer event, without any internal action. 108 | "loadstart", 109 | // "progress", // jPlayer uses internally before bubbling. 110 | // "suspend", // jPlayer uses internally before bubbling. 111 | "abort", 112 | // "error", // jPlayer uses internally before bubbling. 113 | "emptied", 114 | "stalled", 115 | // "play", // jPlayer uses internally before bubbling. 116 | // "pause", // jPlayer uses internally before bubbling. 117 | "loadedmetadata", 118 | "loadeddata", 119 | // "waiting", // jPlayer uses internally before bubbling. 120 | // "playing", // jPlayer uses internally before bubbling. 121 | // "canplay", // jPlayer fixes the volume (for Chrome) before bubbling. 122 | "canplaythrough", 123 | // "seeking", // jPlayer uses internally before bubbling. 124 | // "seeked", // jPlayer uses internally before bubbling. 125 | // "timeupdate", // jPlayer uses internally before bubbling. 126 | // "ended", // jPlayer uses internally before bubbling. 127 | "ratechange" 128 | // "durationchange" // jPlayer uses internally before bubbling. 129 | // "volumechange" // Handled by jPlayer in volume() method, primarily due to the volume fix (for Chrome) in the canplay event. [*] Need to review whether the latest Chrome still needs the fix sometime. 130 | ]; 131 | 132 | $.jPlayer.pause = function() { 133 | // $.each($.jPlayer.instances, function(i, element) { 134 | $.each($.jPlayer.prototype.instances, function(i, element) { 135 | if(element.data("jPlayer").status.srcSet) { // Check that media is set otherwise would cause error event. 136 | element.jPlayer("pause"); 137 | } 138 | }); 139 | }; 140 | 141 | $.jPlayer.timeFormat = { 142 | showHour: false, 143 | showMin: true, 144 | showSec: true, 145 | padHour: false, 146 | padMin: true, 147 | padSec: true, 148 | sepHour: ":", 149 | sepMin: ":", 150 | sepSec: "" 151 | }; 152 | 153 | $.jPlayer.convertTime = function(sec) { 154 | var myTime = new Date(sec * 1000); 155 | var hour = myTime.getUTCHours(); 156 | var min = myTime.getUTCMinutes(); 157 | var sec = myTime.getUTCSeconds(); 158 | var strHour = ($.jPlayer.timeFormat.padHour && hour < 10) ? "0" + hour : hour; 159 | var strMin = ($.jPlayer.timeFormat.padMin && min < 10) ? "0" + min : min; 160 | var strSec = ($.jPlayer.timeFormat.padSec && sec < 10) ? "0" + sec : sec; 161 | return (($.jPlayer.timeFormat.showHour) ? strHour + $.jPlayer.timeFormat.sepHour : "") + (($.jPlayer.timeFormat.showMin) ? strMin + $.jPlayer.timeFormat.sepMin : "") + (($.jPlayer.timeFormat.showSec) ? strSec + $.jPlayer.timeFormat.sepSec : ""); 162 | }; 163 | 164 | // Adapting jQuery 1.4.4 code for jQuery.browser. Required since jQuery 1.3.2 does not detect Chrome as webkit. 165 | $.jPlayer.uaMatch = function( ua ) { 166 | var ua = ua.toLowerCase(); 167 | 168 | // Useragent RegExp 169 | var rwebkit = /(webkit)[ \/]([\w.]+)/; 170 | var ropera = /(opera)(?:.*version)?[ \/]([\w.]+)/; 171 | var rmsie = /(msie) ([\w.]+)/; 172 | var rmozilla = /(mozilla)(?:.*? rv:([\w.]+))?/; 173 | 174 | var match = rwebkit.exec( ua ) || 175 | ropera.exec( ua ) || 176 | rmsie.exec( ua ) || 177 | ua.indexOf("compatible") < 0 && rmozilla.exec( ua ) || 178 | []; 179 | 180 | return { browser: match[1] || "", version: match[2] || "0" }; 181 | }; 182 | 183 | $.jPlayer.browser = { 184 | }; 185 | 186 | var browserMatch = $.jPlayer.uaMatch(navigator.userAgent); 187 | if ( browserMatch.browser ) { 188 | $.jPlayer.browser[ browserMatch.browser ] = true; 189 | $.jPlayer.browser.version = browserMatch.version; 190 | } 191 | 192 | $.jPlayer.prototype = { 193 | count: 0, // Static Variable: Change it via prototype. 194 | version: { // Static Object 195 | script: "2.0.9", 196 | needFlash: "2.0.9", 197 | flash: "unknown" 198 | }, 199 | options: { // Instanced in $.jPlayer() constructor 200 | swfPath: "js", // Path to Jplayer.swf. Can be relative, absolute or server root relative. 201 | solution: "html, flash", // Valid solutions: html, flash. Order defines priority. 1st is highest, 202 | supplied: "mp3", // Defines which formats jPlayer will try and support and the priority by the order. 1st is highest, 203 | preload: 'metadata', // HTML5 Spec values: none, metadata, auto. 204 | volume: 0.8, // The volume. Number 0 to 1. 205 | muted: false, 206 | wmode: "window", // Default Flash wmode is: window. Valid wmode: transparent, opaque, direct, gpu 207 | backgroundColor: "#000000", // To define the jPlayer div and Flash background color. 208 | cssSelectorAncestor: "#jp_container_1", 209 | cssSelector: { // * denotes properties that should only be required when video media type required. _cssSelector() would require changes to enable splitting these into Audio and Video defaults. 210 | videoPlay: ".jp-video-play", // * 211 | play: ".jp-play", 212 | pause: ".jp-pause", 213 | stop: ".jp-stop", 214 | seekBar: ".jp-seek-bar", 215 | playBar: ".jp-play-bar", 216 | mute: ".jp-mute", 217 | unmute: ".jp-unmute", 218 | volumeBar: ".jp-volume-bar", 219 | volumeBarValue: ".jp-volume-bar-value", 220 | currentTime: ".jp-current-time", 221 | duration: ".jp-duration", 222 | fullScreen: ".jp-full-screen", // * 223 | restoreScreen: ".jp-restore-screen" // * 224 | }, 225 | fullScreen: false, 226 | // globalVolume: false, // Not implemented: Set to make volume changes affect all jPlayer instances 227 | // globalMute: false, // Not implemented: Set to make mute changes affect all jPlayer instances 228 | idPrefix: "jp", // Prefix for the ids of html elements created by jPlayer. For flash, this must not include characters: . - + * / \ 229 | noConflict: "jQuery", 230 | errorAlerts: false, 231 | warningAlerts: false 232 | }, 233 | optionsAudio: { 234 | size: { 235 | width: "0px", 236 | height: "0px", 237 | cssClass: "" 238 | }, 239 | sizeFull: { 240 | width: "0px", 241 | height: "0px", 242 | cssClass: "" 243 | } 244 | }, 245 | optionsVideo: { 246 | size: { 247 | width: "480px", 248 | height: "270px", 249 | cssClass: "jp-video-270p" 250 | }, 251 | sizeFull: { 252 | width: "100%", 253 | height: "90%", 254 | cssClass: "jp-video-full" 255 | } 256 | }, 257 | instances: {}, // Static Object 258 | status: { // Instanced in _init() 259 | src: "", 260 | media: {}, 261 | paused: true, 262 | format: {}, 263 | formatType: "", 264 | waitForPlay: true, // Same as waitForLoad except in case where preloading. 265 | waitForLoad: true, 266 | srcSet: false, 267 | video: false, // True if playing a video 268 | seekPercent: 0, 269 | currentPercentRelative: 0, 270 | currentPercentAbsolute: 0, 271 | currentTime: 0, 272 | duration: 0 273 | }, 274 | /* Persistant status properties created dynamically at _init(): 275 | width 276 | height 277 | cssClass 278 | */ 279 | internal: { // Instanced in _init() 280 | ready: false, 281 | instance: undefined, 282 | htmlDlyCmdId: undefined 283 | }, 284 | solution: { // Static Object: Defines the solutions built in jPlayer. 285 | html: true, 286 | flash: true 287 | }, 288 | // 'MPEG-4 support' : canPlayType('video/mp4; codecs="mp4v.20.8"') 289 | format: { // Static Object 290 | mp3: { 291 | codec: 'audio/mpeg; codecs="mp3"', 292 | flashCanPlay: true, 293 | media: 'audio' 294 | }, 295 | m4a: { // AAC / MP4 296 | codec: 'audio/mp4; codecs="mp4a.40.2"', 297 | flashCanPlay: true, 298 | media: 'audio' 299 | }, 300 | oga: { // OGG 301 | codec: 'audio/ogg; codecs="vorbis"', 302 | flashCanPlay: false, 303 | media: 'audio' 304 | }, 305 | wav: { // PCM 306 | codec: 'audio/wav; codecs="1"', 307 | flashCanPlay: false, 308 | media: 'audio' 309 | }, 310 | webma: { // WEBM 311 | codec: 'audio/webm; codecs="vorbis"', 312 | flashCanPlay: false, 313 | media: 'audio' 314 | }, 315 | m4v: { // H.264 / MP4 316 | codec: 'video/mp4; codecs="avc1.42E01E, mp4a.40.2"', 317 | flashCanPlay: true, 318 | media: 'video' 319 | }, 320 | ogv: { // OGG 321 | codec: 'video/ogg; codecs="theora, vorbis"', 322 | flashCanPlay: false, 323 | media: 'video' 324 | }, 325 | webmv: { // WEBM 326 | codec: 'video/webm; codecs="vorbis, vp8"', 327 | flashCanPlay: false, 328 | media: 'video' 329 | } 330 | }, 331 | _init: function() { 332 | var self = this; 333 | 334 | this.element.empty(); 335 | 336 | this.status = $.extend({}, this.status); // Copy static to unique instance. 337 | this.internal = $.extend({}, this.internal); // Copy static to unique instance. 338 | 339 | this.formats = []; // Array based on supplied string option. Order defines priority. 340 | this.solutions = []; // Array based on solution string option. Order defines priority. 341 | this.require = {}; // Which media types are required: video, audio. 342 | 343 | this.htmlElement = {}; // DOM elements created by jPlayer 344 | this.html = {}; // In _init()'s this.desired code and setmedia(): Accessed via this[solution], where solution from this.solutions array. 345 | this.html.audio = {}; 346 | this.html.video = {}; 347 | this.flash = {}; // In _init()'s this.desired code and setmedia(): Accessed via this[solution], where solution from this.solutions array. 348 | 349 | this.css = {}; 350 | this.css.cs = {}; // Holds the css selector strings 351 | this.css.jq = {}; // Holds jQuery selectors. ie., $(css.cs.method) 352 | 353 | this.ancestorJq = []; // Holds jQuery selector of cssSelectorAncestor. Init would use $() instead of [], but it is only 1.4+ 354 | 355 | this.options.volume = this._limitValue(this.options.volume, 0, 1); // Limit volume value's bounds. 356 | 357 | // Create the formats array, with prority based on the order of the supplied formats string 358 | $.each(this.options.supplied.toLowerCase().split(","), function(index1, value1) { 359 | var format = value1.replace(/^\s+|\s+$/g, ""); //trim 360 | if(self.format[format]) { // Check format is valid. 361 | var dupFound = false; 362 | $.each(self.formats, function(index2, value2) { // Check for duplicates 363 | if(format === value2) { 364 | dupFound = true; 365 | return false; 366 | } 367 | }); 368 | if(!dupFound) { 369 | self.formats.push(format); 370 | } 371 | } 372 | }); 373 | 374 | // Create the solutions array, with prority based on the order of the solution string 375 | $.each(this.options.solution.toLowerCase().split(","), function(index1, value1) { 376 | var solution = value1.replace(/^\s+|\s+$/g, ""); //trim 377 | if(self.solution[solution]) { // Check solution is valid. 378 | var dupFound = false; 379 | $.each(self.solutions, function(index2, value2) { // Check for duplicates 380 | if(solution === value2) { 381 | dupFound = true; 382 | return false; 383 | } 384 | }); 385 | if(!dupFound) { 386 | self.solutions.push(solution); 387 | } 388 | } 389 | }); 390 | 391 | this.internal.instance = "jp_" + this.count; 392 | this.instances[this.internal.instance] = this.element; 393 | 394 | // Check the jPlayer div has an id and create one if required. Important for Flash to know the unique id for comms. 395 | if(this.element.attr("id") === "") { 396 | this.element.attr("id", this.options.idPrefix + "_jplayer_" + this.count); 397 | } 398 | 399 | this.internal.self = $.extend({}, { 400 | id: this.element.attr("id"), 401 | jq: this.element 402 | }); 403 | this.internal.audio = $.extend({}, { 404 | id: this.options.idPrefix + "_audio_" + this.count, 405 | jq: undefined 406 | }); 407 | this.internal.video = $.extend({}, { 408 | id: this.options.idPrefix + "_video_" + this.count, 409 | jq: undefined 410 | }); 411 | this.internal.flash = $.extend({}, { 412 | id: this.options.idPrefix + "_flash_" + this.count, 413 | jq: undefined, 414 | swf: this.options.swfPath + ((this.options.swfPath !== "" && this.options.swfPath.slice(-1) !== "/") ? "/" : "") + "Jplayer.swf" 415 | }); 416 | this.internal.poster = $.extend({}, { 417 | id: this.options.idPrefix + "_poster_" + this.count, 418 | jq: undefined 419 | }); 420 | 421 | // Register listeners defined in the constructor 422 | $.each($.jPlayer.event, function(eventName,eventType) { 423 | if(self.options[eventName] !== undefined) { 424 | self.element.bind(eventType + ".jPlayer", self.options[eventName]); // With .jPlayer namespace. 425 | self.options[eventName] = undefined; // Destroy the handler pointer copy on the options. Reason, events can be added/removed in other ways so this could be obsolete and misleading. 426 | } 427 | }); 428 | 429 | // Determine if we require solutions for audio, video or both media types. 430 | this.require.audio = false; 431 | this.require.video = false; 432 | $.each(this.formats, function(priority, format) { 433 | self.require[self.format[format].media] = true; 434 | }); 435 | 436 | // Now required types are known, finish the options default settings. 437 | if(this.require.video) { 438 | this.options = $.extend(true, {}, 439 | this.optionsVideo, 440 | this.options 441 | ); 442 | } else { 443 | this.options = $.extend(true, {}, 444 | this.optionsAudio, 445 | this.options 446 | ); 447 | } 448 | this._setSize(); // update status and jPlayer element size 449 | 450 | // Create the poster image. 451 | this.htmlElement.poster = document.createElement('img'); 452 | this.htmlElement.poster.id = this.internal.poster.id; 453 | this.htmlElement.poster.onload = function() { // Note that this did not work on Firefox 3.6: poster.addEventListener("onload", function() {}, false); Did not investigate x-browser. 454 | if(!self.status.video || self.status.waitForPlay) { 455 | self.internal.poster.jq.show(); 456 | } 457 | }; 458 | this.element.append(this.htmlElement.poster); 459 | this.internal.poster.jq = $("#" + this.internal.poster.id); 460 | this.internal.poster.jq.css({'width': this.status.width, 'height': this.status.height}); 461 | this.internal.poster.jq.hide(); 462 | 463 | // Generate the required media elements 464 | this.html.audio.available = false; 465 | if(this.require.audio) { // If a supplied format is audio 466 | this.htmlElement.audio = document.createElement('audio'); 467 | this.htmlElement.audio.id = this.internal.audio.id; 468 | this.html.audio.available = !!this.htmlElement.audio.canPlayType; 469 | } 470 | this.html.video.available = false; 471 | if(this.require.video) { // If a supplied format is video 472 | this.htmlElement.video = document.createElement('video'); 473 | this.htmlElement.video.id = this.internal.video.id; 474 | this.html.video.available = !!this.htmlElement.video.canPlayType; 475 | } 476 | 477 | this.flash.available = this._checkForFlash(10); 478 | 479 | this.html.canPlay = {}; 480 | this.flash.canPlay = {}; 481 | $.each(this.formats, function(priority, format) { 482 | self.html.canPlay[format] = self.html[self.format[format].media].available && "" !== self.htmlElement[self.format[format].media].canPlayType(self.format[format].codec); 483 | self.flash.canPlay[format] = self.format[format].flashCanPlay && self.flash.available; 484 | }); 485 | this.html.desired = false; 486 | this.flash.desired = false; 487 | $.each(this.solutions, function(solutionPriority, solution) { 488 | if(solutionPriority === 0) { 489 | self[solution].desired = true; 490 | } else { 491 | var audioCanPlay = false; 492 | var videoCanPlay = false; 493 | $.each(self.formats, function(formatPriority, format) { 494 | if(self[self.solutions[0]].canPlay[format]) { // The other solution can play 495 | if(self.format[format].media === 'video') { 496 | videoCanPlay = true; 497 | } else { 498 | audioCanPlay = true; 499 | } 500 | } 501 | }); 502 | self[solution].desired = (self.require.audio && !audioCanPlay) || (self.require.video && !videoCanPlay); 503 | } 504 | }); 505 | // This is what jPlayer will support, based on solution and supplied. 506 | this.html.support = {}; 507 | this.flash.support = {}; 508 | $.each(this.formats, function(priority, format) { 509 | self.html.support[format] = self.html.canPlay[format] && self.html.desired; 510 | self.flash.support[format] = self.flash.canPlay[format] && self.flash.desired; 511 | }); 512 | // If jPlayer is supporting any format in a solution, then the solution is used. 513 | this.html.used = false; 514 | this.flash.used = false; 515 | $.each(this.solutions, function(solutionPriority, solution) { 516 | $.each(self.formats, function(formatPriority, format) { 517 | if(self[solution].support[format]) { 518 | self[solution].used = true; 519 | return false; 520 | } 521 | }); 522 | }); 523 | 524 | // Init solution active state and the event gates to false. 525 | this.html.active = false; 526 | this.html.audio.gate = false; 527 | this.html.video.gate = false; 528 | this.flash.active = false; 529 | this.flash.gate = false; 530 | 531 | // Set up the css selectors for the control and feedback entities. 532 | this._cssSelectorAncestor(this.options.cssSelectorAncestor); 533 | 534 | // If neither html nor flash are being used by this browser, then media playback is not possible. Trigger an error event. 535 | if(!(this.html.used || this.flash.used)) { 536 | this._error( { 537 | type: $.jPlayer.error.NO_SOLUTION, 538 | context: "{solution:'" + this.options.solution + "', supplied:'" + this.options.supplied + "'}", 539 | message: $.jPlayer.errorMsg.NO_SOLUTION, 540 | hint: $.jPlayer.errorHint.NO_SOLUTION 541 | }); 542 | } 543 | 544 | // Add the flash solution if it is being used. 545 | if(this.flash.used) { 546 | var htmlObj, 547 | flashVars = 548 | 'jQuery=' + encodeURI(this.options.noConflict) 549 | + '&id=' + encodeURI(this.internal.self.id) 550 | + '&vol=' + this.options.volume 551 | + '&muted=' + this.options.muted; 552 | 553 | // Code influenced by SWFObject 2.2: http://code.google.com/p/swfobject/ 554 | // Non IE browsers have an initial Flash size of 1 by 1 otherwise the wmode affected the Flash ready event. 555 | 556 | if($.browser.msie && Number($.browser.version) <= 8) { 557 | var objStr = '' 560 | + ''; 561 | 562 | var paramStr = [ 563 | '', 564 | '', 565 | '', 566 | '', 567 | '' 568 | ]; 569 | 570 | htmlObj = document.createElement(objStr); 571 | for(var i=0; i < paramStr.length; i++) { 572 | htmlObj.appendChild(document.createElement(paramStr[i])); 573 | } 574 | } else { 575 | var createParam = function(el, n, v) { 576 | var p = document.createElement("param"); 577 | p.setAttribute("name", n); 578 | p.setAttribute("value", v); 579 | el.appendChild(p); 580 | }; 581 | 582 | htmlObj = document.createElement("object"); 583 | htmlObj.setAttribute("id", this.internal.flash.id); 584 | htmlObj.setAttribute("data", this.internal.flash.swf); 585 | htmlObj.setAttribute("type", "application/x-shockwave-flash"); 586 | htmlObj.setAttribute("width", "1"); // Non-zero 587 | htmlObj.setAttribute("height", "1"); // Non-zero 588 | createParam(htmlObj, "flashvars", flashVars); 589 | createParam(htmlObj, "allowscriptaccess", "always"); 590 | createParam(htmlObj, "bgcolor", this.options.backgroundColor); 591 | createParam(htmlObj, "wmode", this.options.wmode); 592 | } 593 | 594 | this.element.append(htmlObj); 595 | this.internal.flash.jq = $(htmlObj); 596 | } 597 | 598 | // Add the HTML solution if being used. 599 | if(this.html.used) { 600 | 601 | // The HTML Audio handlers 602 | if(this.html.audio.available) { 603 | this._addHtmlEventListeners(this.htmlElement.audio, this.html.audio); 604 | this.element.append(this.htmlElement.audio); 605 | this.internal.audio.jq = $("#" + this.internal.audio.id); 606 | } 607 | 608 | // The HTML Video handlers 609 | if(this.html.video.available) { 610 | this._addHtmlEventListeners(this.htmlElement.video, this.html.video); 611 | this.element.append(this.htmlElement.video); 612 | this.internal.video.jq = $("#" + this.internal.video.id); 613 | this.internal.video.jq.css({'width':'0px', 'height':'0px'}); // Using size 0x0 since a .hide() causes issues in iOS 614 | } 615 | } 616 | 617 | if(this.html.used && !this.flash.used) { // If only HTML, then emulate flash ready() call after 100ms. 618 | window.setTimeout( function() { 619 | self.internal.ready = true; 620 | self.version.flash = "n/a"; 621 | self._trigger($.jPlayer.event.ready); 622 | }, 100); 623 | } 624 | 625 | this._updateInterface(); 626 | this._updateButtons(false); 627 | this._updateVolume(this.options.volume); 628 | this._updateMute(this.options.muted); 629 | if(this.css.jq.videoPlay.length) { 630 | this.css.jq.videoPlay.hide(); 631 | } 632 | $.jPlayer.prototype.count++; // Change static variable via prototype. 633 | }, 634 | destroy: function() { 635 | // MJP: The background change remains. Review later. 636 | 637 | // Reset the interface, remove seeking effect and times. 638 | this._resetStatus(); 639 | this._updateInterface(); 640 | this._seeked(); 641 | if(this.css.jq.currentTime.length) { 642 | this.css.jq.currentTime.text(""); 643 | } 644 | if(this.css.jq.duration.length) { 645 | this.css.jq.duration.text(""); 646 | } 647 | 648 | if(this.status.srcSet) { // Or you get a bogus error event 649 | this.pause(); // Pauses the media and clears any delayed commands used in the HTML solution. 650 | } 651 | $.each(this.css.jq, function(fn, jq) { // Remove any bindings from the interface controls. 652 | jq.unbind(".jPlayer"); 653 | }); 654 | this.element.removeData("jPlayer"); // Remove jPlayer data 655 | this.element.unbind(".jPlayer"); // Remove all event handlers created by the jPlayer constructor 656 | this.element.empty(); // Remove the inserted child elements 657 | 658 | this.instances[this.internal.instance] = undefined; // Clear the instance on the static instance object 659 | }, 660 | enable: function() { // Plan to implement 661 | // options.disabled = false 662 | }, 663 | disable: function () { // Plan to implement 664 | // options.disabled = true 665 | }, 666 | _addHtmlEventListeners: function(mediaElement, entity) { 667 | var self = this; 668 | mediaElement.preload = this.options.preload; 669 | mediaElement.muted = this.options.muted; 670 | mediaElement.volume = this.options.volume; 671 | 672 | // Create the event listeners 673 | // Only want the active entity to affect jPlayer and bubble events. 674 | // Using entity.gate so that object is referenced and gate property always current 675 | 676 | mediaElement.addEventListener("progress", function() { 677 | if(entity.gate && !self.status.waitForLoad) { 678 | self._getHtmlStatus(mediaElement); 679 | self._updateInterface(); 680 | self._trigger($.jPlayer.event.progress); 681 | } 682 | }, false); 683 | mediaElement.addEventListener("timeupdate", function() { 684 | if(entity.gate && !self.status.waitForLoad) { 685 | self._getHtmlStatus(mediaElement); 686 | self._updateInterface(); 687 | self._trigger($.jPlayer.event.timeupdate); 688 | } 689 | }, false); 690 | mediaElement.addEventListener("durationchange", function() { 691 | if(entity.gate && !self.status.waitForLoad) { 692 | self.status.duration = this.duration; 693 | self._getHtmlStatus(mediaElement); 694 | self._updateInterface(); 695 | self._trigger($.jPlayer.event.durationchange); 696 | } 697 | }, false); 698 | mediaElement.addEventListener("play", function() { 699 | if(entity.gate && !self.status.waitForLoad) { 700 | self._updateButtons(true); 701 | self._trigger($.jPlayer.event.play); 702 | } 703 | }, false); 704 | mediaElement.addEventListener("playing", function() { 705 | if(entity.gate && !self.status.waitForLoad) { 706 | self._updateButtons(true); 707 | self._seeked(); 708 | self._trigger($.jPlayer.event.playing); 709 | } 710 | }, false); 711 | mediaElement.addEventListener("pause", function() { 712 | if(entity.gate && !self.status.waitForLoad) { 713 | self._updateButtons(false); 714 | self._trigger($.jPlayer.event.pause); 715 | } 716 | }, false); 717 | mediaElement.addEventListener("waiting", function() { 718 | if(entity.gate && !self.status.waitForLoad) { 719 | self._seeking(); 720 | self._trigger($.jPlayer.event.waiting); 721 | } 722 | }, false); 723 | mediaElement.addEventListener("canplay", function() { 724 | if(entity.gate && !self.status.waitForLoad) { 725 | mediaElement.volume = self._volumeFix(self.options.volume); 726 | self._trigger($.jPlayer.event.canplay); 727 | } 728 | }, false); 729 | mediaElement.addEventListener("seeking", function() { 730 | if(entity.gate && !self.status.waitForLoad) { 731 | self._seeking(); 732 | self._trigger($.jPlayer.event.seeking); 733 | } 734 | }, false); 735 | mediaElement.addEventListener("seeked", function() { 736 | if(entity.gate && !self.status.waitForLoad) { 737 | self._seeked(); 738 | self._trigger($.jPlayer.event.seeked); 739 | } 740 | }, false); 741 | mediaElement.addEventListener("suspend", function() { // Seems to be the only way of capturing that the iOS4 browser did not actually play the media from the page code. ie., It needs a user gesture. 742 | if(entity.gate && !self.status.waitForLoad) { 743 | self._seeked(); 744 | self._trigger($.jPlayer.event.suspend); 745 | } 746 | }, false); 747 | mediaElement.addEventListener("ended", function() { 748 | if(entity.gate && !self.status.waitForLoad) { 749 | // Order of the next few commands are important. Change the time and then pause. 750 | // Solves a bug in Firefox, where issuing pause 1st causes the media to play from the start. ie., The pause is ignored. 751 | if(!$.jPlayer.browser.webkit) { // Chrome crashes if you do this in conjunction with a setMedia command in an ended event handler. ie., The playlist demo. 752 | self.htmlElement.media.currentTime = 0; // Safari does not care about this command. ie., It works with or without this line. (Both Safari and Chrome are Webkit.) 753 | } 754 | self.htmlElement.media.pause(); // Pause otherwise a click on the progress bar will play from that point, when it shouldn't, since it stopped playback. 755 | self._updateButtons(false); 756 | self._getHtmlStatus(mediaElement, true); // With override true. Otherwise Chrome leaves progress at full. 757 | self._updateInterface(); 758 | self._trigger($.jPlayer.event.ended); 759 | } 760 | }, false); 761 | mediaElement.addEventListener("error", function() { 762 | if(entity.gate && !self.status.waitForLoad) { 763 | self._updateButtons(false); 764 | self._seeked(); 765 | if(self.status.srcSet) { // Deals with case of clearMedia() causing an error event. 766 | self.status.waitForLoad = true; // Allows the load operation to try again. 767 | self.status.waitForPlay = true; // Reset since a play was captured. 768 | if(self.status.video) { 769 | self.internal.video.jq.css({'width':'0px', 'height':'0px'}); 770 | } 771 | if(self._validString(self.status.media.poster)) { 772 | self.internal.poster.jq.show(); 773 | } 774 | if(self.css.jq.videoPlay.length) { 775 | self.css.jq.videoPlay.show(); 776 | } 777 | self._error( { 778 | type: $.jPlayer.error.URL, 779 | context: self.status.src, // this.src shows absolute urls. Want context to show the url given. 780 | message: $.jPlayer.errorMsg.URL, 781 | hint: $.jPlayer.errorHint.URL 782 | }); 783 | } 784 | } 785 | }, false); 786 | // Create all the other event listeners that bubble up to a jPlayer event from html, without being used by jPlayer. 787 | $.each($.jPlayer.htmlEvent, function(i, eventType) { 788 | mediaElement.addEventListener(this, function() { 789 | if(entity.gate && !self.status.waitForLoad) { 790 | self._trigger($.jPlayer.event[eventType]); 791 | } 792 | }, false); 793 | }); 794 | }, 795 | _getHtmlStatus: function(media, override) { 796 | var ct = 0, d = 0, cpa = 0, sp = 0, cpr = 0; 797 | 798 | if(media.duration) { // Fixes the duration bug in iOS, where the durationchange event occurs when media.duration is not always correct. 799 | this.status.duration = media.duration; 800 | } 801 | ct = media.currentTime; 802 | cpa = (this.status.duration > 0) ? 100 * ct / this.status.duration : 0; 803 | if((typeof media.seekable === "object") && (media.seekable.length > 0)) { 804 | sp = (this.status.duration > 0) ? 100 * media.seekable.end(media.seekable.length-1) / this.status.duration : 100; 805 | cpr = 100 * media.currentTime / media.seekable.end(media.seekable.length-1); 806 | } else { 807 | sp = 100; 808 | cpr = cpa; 809 | } 810 | 811 | if(override) { 812 | ct = 0; 813 | cpr = 0; 814 | cpa = 0; 815 | } 816 | 817 | this.status.seekPercent = sp; 818 | this.status.currentPercentRelative = cpr; 819 | this.status.currentPercentAbsolute = cpa; 820 | this.status.currentTime = ct; 821 | }, 822 | _resetStatus: function() { 823 | this.status = $.extend({}, this.status, $.jPlayer.prototype.status); // Maintains the status properties that persist through a reset. 824 | }, 825 | _trigger: function(eventType, error, warning) { // eventType always valid as called using $.jPlayer.event.eventType 826 | var event = $.Event(eventType); 827 | event.jPlayer = {}; 828 | event.jPlayer.version = $.extend({}, this.version); 829 | event.jPlayer.options = $.extend(true, {}, this.options); // Deep copy 830 | event.jPlayer.status = $.extend(true, {}, this.status); // Deep copy 831 | event.jPlayer.html = $.extend(true, {}, this.html); // Deep copy 832 | event.jPlayer.flash = $.extend(true, {}, this.flash); // Deep copy 833 | if(error) event.jPlayer.error = $.extend({}, error); 834 | if(warning) event.jPlayer.warning = $.extend({}, warning); 835 | this.element.trigger(event); 836 | }, 837 | jPlayerFlashEvent: function(eventType, status) { // Called from Flash 838 | if(eventType === $.jPlayer.event.ready && !this.internal.ready) { 839 | this.internal.ready = true; 840 | this.internal.flash.jq.css({'width':'0px', 'height':'0px'}); // Once Flash generates the ready event, minimise to zero as it is not affected by wmode anymore. 841 | 842 | this.version.flash = status.version; 843 | if(this.version.needFlash !== this.version.flash) { 844 | this._error( { 845 | type: $.jPlayer.error.VERSION, 846 | context: this.version.flash, 847 | message: $.jPlayer.errorMsg.VERSION + this.version.flash, 848 | hint: $.jPlayer.errorHint.VERSION 849 | }); 850 | } 851 | this._trigger(eventType); 852 | } 853 | if(this.flash.gate) { 854 | switch(eventType) { 855 | case $.jPlayer.event.progress: 856 | this._getFlashStatus(status); 857 | this._updateInterface(); 858 | this._trigger(eventType); 859 | break; 860 | case $.jPlayer.event.timeupdate: 861 | this._getFlashStatus(status); 862 | this._updateInterface(); 863 | this._trigger(eventType); 864 | break; 865 | case $.jPlayer.event.play: 866 | this._seeked(); 867 | this._updateButtons(true); 868 | this._trigger(eventType); 869 | break; 870 | case $.jPlayer.event.pause: 871 | this._updateButtons(false); 872 | this._trigger(eventType); 873 | break; 874 | case $.jPlayer.event.ended: 875 | this._updateButtons(false); 876 | this._trigger(eventType); 877 | break; 878 | case $.jPlayer.event.error: 879 | this.status.waitForLoad = true; // Allows the load operation to try again. 880 | this.status.waitForPlay = true; // Reset since a play was captured. 881 | if(this.status.video) { 882 | this.internal.flash.jq.css({'width':'0px', 'height':'0px'}); 883 | } 884 | if(this._validString(this.status.media.poster)) { 885 | this.internal.poster.jq.show(); 886 | } 887 | if(this.css.jq.videoPlay.length) { 888 | this.css.jq.videoPlay.show(); 889 | } 890 | if(this.status.video) { // Set up for another try. Execute before error event. 891 | this._flash_setVideo(this.status.media); 892 | } else { 893 | this._flash_setAudio(this.status.media); 894 | } 895 | this._error( { 896 | type: $.jPlayer.error.URL, 897 | context:status.src, 898 | message: $.jPlayer.errorMsg.URL, 899 | hint: $.jPlayer.errorHint.URL 900 | }); 901 | break; 902 | case $.jPlayer.event.seeking: 903 | this._seeking(); 904 | this._trigger(eventType); 905 | break; 906 | case $.jPlayer.event.seeked: 907 | this._seeked(); 908 | this._trigger(eventType); 909 | break; 910 | case $.jPlayer.event.ready: 911 | // The ready event is handled outside the switch statement. 912 | // Captured here otherwise 2 ready events would be generated if the ready event handler used setMedia. 913 | break; 914 | default: 915 | this._trigger(eventType); 916 | } 917 | } 918 | return false; 919 | }, 920 | _getFlashStatus: function(status) { 921 | this.status.seekPercent = status.seekPercent; 922 | this.status.currentPercentRelative = status.currentPercentRelative; 923 | this.status.currentPercentAbsolute = status.currentPercentAbsolute; 924 | this.status.currentTime = status.currentTime; 925 | this.status.duration = status.duration; 926 | }, 927 | _updateButtons: function(playing) { 928 | this.status.paused = !playing; 929 | if(this.css.jq.play.length && this.css.jq.pause.length) { 930 | if(playing) { 931 | this.css.jq.play.hide(); 932 | this.css.jq.pause.show(); 933 | } else { 934 | this.css.jq.play.show(); 935 | this.css.jq.pause.hide(); 936 | } 937 | } 938 | }, 939 | _updateInterface: function() { 940 | if(this.css.jq.seekBar.length) { 941 | this.css.jq.seekBar.width(this.status.seekPercent+"%"); 942 | } 943 | if(this.css.jq.playBar.length) { 944 | this.css.jq.playBar.width(this.status.currentPercentRelative+"%"); 945 | } 946 | if(this.css.jq.currentTime.length) { 947 | this.css.jq.currentTime.text($.jPlayer.convertTime(this.status.currentTime)); 948 | } 949 | if(this.css.jq.duration.length) { 950 | this.css.jq.duration.text($.jPlayer.convertTime(this.status.duration)); 951 | } 952 | }, 953 | _seeking: function() { 954 | if(this.css.jq.seekBar.length) { 955 | this.css.jq.seekBar.addClass("jp-seeking-bg"); 956 | } 957 | }, 958 | _seeked: function() { 959 | if(this.css.jq.seekBar.length) { 960 | this.css.jq.seekBar.removeClass("jp-seeking-bg"); 961 | } 962 | }, 963 | setMedia: function(media) { 964 | 965 | /* media[format] = String: URL of format. Must contain all of the supplied option's video or audio formats. 966 | * media.poster = String: Video poster URL. 967 | * media.subtitles = String: * NOT IMPLEMENTED * URL of subtitles SRT file 968 | * media.chapters = String: * NOT IMPLEMENTED * URL of chapters SRT file 969 | * media.stream = Boolean: * NOT IMPLEMENTED * Designating actual media streams. ie., "false/undefined" for files. Plan to refresh the flash every so often. 970 | */ 971 | 972 | var self = this; 973 | 974 | this._seeked(); 975 | clearTimeout(this.internal.htmlDlyCmdId); // Clears any delayed commands used in the HTML solution. 976 | 977 | // Store the current html gates, since we need for clearMedia() conditions. 978 | var audioGate = this.html.audio.gate; 979 | var videoGate = this.html.video.gate; 980 | 981 | var supported = false; 982 | $.each(this.formats, function(formatPriority, format) { 983 | var isVideo = self.format[format].media === 'video'; 984 | $.each(self.solutions, function(solutionPriority, solution) { 985 | if(self[solution].support[format] && self._validString(media[format])) { // Format supported in solution and url given for format. 986 | var isHtml = solution === 'html'; 987 | 988 | if(isVideo) { 989 | if(isHtml) { 990 | self.html.audio.gate = false; 991 | self.html.video.gate = true; 992 | self.flash.gate = false; 993 | } else { 994 | self.html.audio.gate = false; 995 | self.html.video.gate = false; 996 | self.flash.gate = true; 997 | } 998 | } else { 999 | if(isHtml) { 1000 | self.html.audio.gate = true; 1001 | self.html.video.gate = false; 1002 | self.flash.gate = false; 1003 | } else { 1004 | self.html.audio.gate = false; 1005 | self.html.video.gate = false; 1006 | self.flash.gate = true; 1007 | } 1008 | } 1009 | 1010 | // Clear media of the previous solution if: 1011 | // - it was Flash 1012 | // - changing from HTML to Flash 1013 | // - the HTML solution media type (audio or video) remained the same. 1014 | // Note that, we must be careful with clearMedia() on iPhone, otherwise clearing the video when changing to audio corrupts the built in video player. 1015 | if(self.flash.active || (self.html.active && self.flash.gate) || (audioGate === self.html.audio.gate && videoGate === self.html.video.gate)) { 1016 | self.clearMedia(); 1017 | } else if(audioGate !== self.html.audio.gate && videoGate !== self.html.video.gate) { // If switching between html elements 1018 | self._html_pause(); 1019 | // Hide the video if it was being used. 1020 | if(self.status.video) { 1021 | self.internal.video.jq.css({'width':'0px', 'height':'0px'}); 1022 | } 1023 | self._resetStatus(); // Since clearMedia usually does this. Execute after status.video useage. 1024 | } 1025 | 1026 | if(isVideo) { 1027 | if(isHtml) { 1028 | self._html_setVideo(media); 1029 | self.html.active = true; 1030 | self.flash.active = false; 1031 | } else { 1032 | self._flash_setVideo(media); 1033 | self.html.active = false; 1034 | self.flash.active = true; 1035 | } 1036 | if(self.css.jq.videoPlay.length) { 1037 | self.css.jq.videoPlay.show(); 1038 | } 1039 | self.status.video = true; 1040 | } else { 1041 | if(isHtml) { 1042 | self._html_setAudio(media); 1043 | self.html.active = true; 1044 | self.flash.active = false; 1045 | } else { 1046 | self._flash_setAudio(media); 1047 | self.html.active = false; 1048 | self.flash.active = true; 1049 | } 1050 | if(self.css.jq.videoPlay.length) { 1051 | self.css.jq.videoPlay.hide(); 1052 | } 1053 | self.status.video = false; 1054 | } 1055 | 1056 | supported = true; 1057 | return false; // Exit $.each 1058 | } 1059 | }); 1060 | if(supported) { 1061 | return false; // Exit $.each 1062 | } 1063 | }); 1064 | 1065 | if(supported) { 1066 | // Set poster after the possible clearMedia() command above. IE had issues since the IMG onload event occurred immediately when cached. ie., The clearMedia() hide the poster. 1067 | if(this._validString(media.poster)) { 1068 | if(this.htmlElement.poster.src !== media.poster) { // Since some browsers do not generate img onload event. 1069 | this.htmlElement.poster.src = media.poster; 1070 | } else { 1071 | this.internal.poster.jq.show(); 1072 | } 1073 | } else { 1074 | this.internal.poster.jq.hide(); // Hide if not used, since clearMedia() does not always occur above. ie., HTML audio <-> video switching. 1075 | } 1076 | this.status.srcSet = true; 1077 | this.status.media = $.extend({}, media); 1078 | this._updateButtons(false); 1079 | this._updateInterface(); 1080 | } else { // jPlayer cannot support any formats provided in this browser 1081 | // Pause here if old media could be playing. Otherwise, playing media being changed to bad media would leave the old media playing. 1082 | if(this.status.srcSet && !this.status.waitForPlay) { 1083 | this.pause(); 1084 | } 1085 | // Reset all the control flags 1086 | this.html.audio.gate = false; 1087 | this.html.video.gate = false; 1088 | this.flash.gate = false; 1089 | this.html.active = false; 1090 | this.flash.active = false; 1091 | // Reset status and interface. 1092 | this._resetStatus(); 1093 | this._updateInterface(); 1094 | this._updateButtons(false); 1095 | // Hide the any old media 1096 | this.internal.poster.jq.hide(); 1097 | if(this.html.used && this.require.video) { 1098 | this.internal.video.jq.css({'width':'0px', 'height':'0px'}); 1099 | } 1100 | if(this.flash.used) { 1101 | this.internal.flash.jq.css({'width':'0px', 'height':'0px'}); 1102 | } 1103 | // Send an error event 1104 | this._error( { 1105 | type: $.jPlayer.error.NO_SUPPORT, 1106 | context: "{supplied:'" + this.options.supplied + "'}", 1107 | message: $.jPlayer.errorMsg.NO_SUPPORT, 1108 | hint: $.jPlayer.errorHint.NO_SUPPORT 1109 | }); 1110 | } 1111 | }, 1112 | clearMedia: function() { 1113 | this._resetStatus(); 1114 | this._updateButtons(false); 1115 | 1116 | this.internal.poster.jq.hide(); 1117 | 1118 | clearTimeout(this.internal.htmlDlyCmdId); 1119 | 1120 | if(this.html.active) { 1121 | this._html_clearMedia(); 1122 | } else if(this.flash.active) { 1123 | this._flash_clearMedia(); 1124 | } 1125 | }, 1126 | load: function() { 1127 | if(this.status.srcSet) { 1128 | if(this.html.active) { 1129 | this._html_load(); 1130 | } else if(this.flash.active) { 1131 | this._flash_load(); 1132 | } 1133 | } else { 1134 | this._urlNotSetError("load"); 1135 | } 1136 | }, 1137 | play: function(time) { 1138 | time = (typeof time === "number") ? time : NaN; // Remove jQuery event from click handler 1139 | if(this.status.srcSet) { 1140 | if(this.html.active) { 1141 | this._html_play(time); 1142 | } else if(this.flash.active) { 1143 | this._flash_play(time); 1144 | } 1145 | } else { 1146 | this._urlNotSetError("play"); 1147 | } 1148 | }, 1149 | videoPlay: function(e) { // Handles clicks on the play button over the video poster 1150 | this.play(); 1151 | }, 1152 | pause: function(time) { 1153 | time = (typeof time === "number") ? time : NaN; // Remove jQuery event from click handler 1154 | if(this.status.srcSet) { 1155 | if(this.html.active) { 1156 | this._html_pause(time); 1157 | } else if(this.flash.active) { 1158 | this._flash_pause(time); 1159 | } 1160 | } else { 1161 | this._urlNotSetError("pause"); 1162 | } 1163 | }, 1164 | pauseOthers: function() { 1165 | var self = this; 1166 | $.each(this.instances, function(i, element) { 1167 | if(self.element !== element) { // Do not this instance. 1168 | if(element.data("jPlayer").status.srcSet) { // Check that media is set otherwise would cause error event. 1169 | element.jPlayer("pause"); 1170 | } 1171 | } 1172 | }); 1173 | }, 1174 | stop: function() { 1175 | if(this.status.srcSet) { 1176 | if(this.html.active) { 1177 | this._html_pause(0); 1178 | } else if(this.flash.active) { 1179 | this._flash_pause(0); 1180 | } 1181 | } else { 1182 | this._urlNotSetError("stop"); 1183 | } 1184 | }, 1185 | playHead: function(p) { 1186 | p = this._limitValue(p, 0, 100); 1187 | if(this.status.srcSet) { 1188 | if(this.html.active) { 1189 | this._html_playHead(p); 1190 | } else if(this.flash.active) { 1191 | this._flash_playHead(p); 1192 | } 1193 | } else { 1194 | this._urlNotSetError("playHead"); 1195 | } 1196 | }, 1197 | _muted: function(muted) { 1198 | this.options.muted = muted; 1199 | if(this.html.used) { 1200 | this._html_mute(muted); 1201 | } 1202 | if(this.flash.used) { 1203 | this._flash_mute(muted); 1204 | } 1205 | this._updateMute(muted); 1206 | this._updateVolume(this.options.volume); 1207 | this._trigger($.jPlayer.event.volumechange); 1208 | }, 1209 | mute: function(mute) { // mute is either: undefined (true), an event object (true) or a boolean (muted). 1210 | mute = mute === undefined ? true : !!mute; 1211 | this._muted(mute); 1212 | }, 1213 | unmute: function(unmute) { // unmute is either: undefined (true), an event object (true) or a boolean (!muted). 1214 | unmute = unmute === undefined ? true : !!unmute; 1215 | this._muted(!unmute); 1216 | }, 1217 | _updateMute: function(mute) { 1218 | if(this.css.jq.mute.length && this.css.jq.unmute.length) { 1219 | if(mute) { 1220 | this.css.jq.mute.hide(); 1221 | this.css.jq.unmute.show(); 1222 | } else { 1223 | this.css.jq.mute.show(); 1224 | this.css.jq.unmute.hide(); 1225 | } 1226 | } 1227 | }, 1228 | volume: function(v) { 1229 | v = this._limitValue(v, 0, 1); 1230 | this.options.volume = v; 1231 | 1232 | if(this.html.used) { 1233 | this._html_volume(v); 1234 | } 1235 | if(this.flash.used) { 1236 | this._flash_volume(v); 1237 | } 1238 | this._updateVolume(v); 1239 | this._trigger($.jPlayer.event.volumechange); 1240 | }, 1241 | volumeBar: function(e) { // Handles clicks on the volumeBar 1242 | if(!this.options.muted && this.css.jq.volumeBar.length) { // Ignore clicks when muted 1243 | var offset = this.css.jq.volumeBar.offset(); 1244 | var x = e.pageX - offset.left; 1245 | var w = this.css.jq.volumeBar.width(); 1246 | var v = x/w; 1247 | this.volume(v); 1248 | } 1249 | }, 1250 | volumeBarValue: function(e) { // Handles clicks on the volumeBarValue 1251 | this.volumeBar(e); 1252 | }, 1253 | _updateVolume: function(v) { 1254 | v = this.options.muted ? 0 : v; 1255 | if(this.css.jq.volumeBarValue.length) { 1256 | this.css.jq.volumeBarValue.width((v*100)+"%"); 1257 | } 1258 | }, 1259 | _volumeFix: function(v) { // Need to review if this is still necessary on latest Chrome 1260 | var rnd = 0.001 * Math.random(); // Fix for Chrome 4: Fix volume being set multiple times before playing bug. 1261 | var fix = (v < 0.5) ? rnd : -rnd; // Fix for Chrome 4: Solves volume change before play bug. (When new vol == old vol Chrome 4 does nothing!) 1262 | return (v + fix); // Fix for Chrome 4: Event solves initial volume not being set correctly. 1263 | }, 1264 | _cssSelectorAncestor: function(ancestor) { 1265 | var self = this; 1266 | this.options.cssSelectorAncestor = ancestor; 1267 | this._removeUiClass(); 1268 | this.ancestorJq = ancestor ? $(ancestor) : []; // Would use $() instead of [], but it is only 1.4+ 1269 | if(ancestor && this.ancestorJq.length !== 1) { // So empty strings do not generate the warning. 1270 | this._warning( { 1271 | type: $.jPlayer.warning.CSS_SELECTOR_COUNT, 1272 | context: ancestor, 1273 | message: $.jPlayer.warningMsg.CSS_SELECTOR_COUNT + this.ancestorJq.length + " found for cssSelectorAncestor.", 1274 | hint: $.jPlayer.warningHint.CSS_SELECTOR_COUNT 1275 | }); 1276 | } 1277 | this._addUiClass(); 1278 | $.each(this.options.cssSelector, function(fn, cssSel) { 1279 | self._cssSelector(fn, cssSel); 1280 | }); 1281 | }, 1282 | _cssSelector: function(fn, cssSel) { 1283 | var self = this; 1284 | if(typeof cssSel === 'string') { 1285 | if($.jPlayer.prototype.options.cssSelector[fn]) { 1286 | if(this.css.jq[fn] && this.css.jq[fn].length) { 1287 | this.css.jq[fn].unbind(".jPlayer"); 1288 | } 1289 | this.options.cssSelector[fn] = cssSel; 1290 | this.css.cs[fn] = this.options.cssSelectorAncestor + " " + cssSel; 1291 | 1292 | if(cssSel) { // Checks for empty string 1293 | this.css.jq[fn] = $(this.css.cs[fn]); 1294 | } else { 1295 | this.css.jq[fn] = []; // To comply with the css.jq[fn].length check before its use. As of jQuery 1.4 could have used $() for an empty set. 1296 | } 1297 | 1298 | if(this.css.jq[fn].length) { 1299 | var handler = function(e) { 1300 | self[fn](e); 1301 | $(this).blur(); 1302 | return false; 1303 | } 1304 | this.css.jq[fn].bind("click.jPlayer", handler); // Using jPlayer namespace 1305 | } 1306 | 1307 | if(cssSel && this.css.jq[fn].length !== 1) { // So empty strings do not generate the warning. ie., they just remove the old one. 1308 | this._warning( { 1309 | type: $.jPlayer.warning.CSS_SELECTOR_COUNT, 1310 | context: this.css.cs[fn], 1311 | message: $.jPlayer.warningMsg.CSS_SELECTOR_COUNT + this.css.jq[fn].length + " found for " + fn + " method.", 1312 | hint: $.jPlayer.warningHint.CSS_SELECTOR_COUNT 1313 | }); 1314 | } 1315 | } else { 1316 | this._warning( { 1317 | type: $.jPlayer.warning.CSS_SELECTOR_METHOD, 1318 | context: fn, 1319 | message: $.jPlayer.warningMsg.CSS_SELECTOR_METHOD, 1320 | hint: $.jPlayer.warningHint.CSS_SELECTOR_METHOD 1321 | }); 1322 | } 1323 | } else { 1324 | this._warning( { 1325 | type: $.jPlayer.warning.CSS_SELECTOR_STRING, 1326 | context: cssSel, 1327 | message: $.jPlayer.warningMsg.CSS_SELECTOR_STRING, 1328 | hint: $.jPlayer.warningHint.CSS_SELECTOR_STRING 1329 | }); 1330 | } 1331 | }, 1332 | seekBar: function(e) { // Handles clicks on the seekBar 1333 | if(this.css.jq.seekBar) { 1334 | var offset = this.css.jq.seekBar.offset(); 1335 | var x = e.pageX - offset.left; 1336 | var w = this.css.jq.seekBar.width(); 1337 | var p = 100*x/w; 1338 | this.playHead(p); 1339 | } 1340 | }, 1341 | playBar: function(e) { // Handles clicks on the playBar 1342 | this.seekBar(e); 1343 | }, 1344 | currentTime: function(e) { // Handles clicks on the text 1345 | // Added to avoid errors using cssSelector system for the text 1346 | }, 1347 | duration: function(e) { // Handles clicks on the text 1348 | // Added to avoid errors using cssSelector system for the text 1349 | }, 1350 | // Options code adapted from ui.widget.js (1.8.7). Made changes so the key can use dot notation. To match previous getData solution in jPlayer 1. 1351 | option: function(key, value) { 1352 | var options = key; 1353 | 1354 | // Enables use: options(). Returns a copy of options object 1355 | if ( arguments.length === 0 ) { 1356 | return $.extend( true, {}, this.options ); 1357 | } 1358 | 1359 | if(typeof key === "string") { 1360 | var keys = key.split("."); 1361 | 1362 | // Enables use: options("someOption") Returns a copy of the option. Supports dot notation. 1363 | if(value === undefined) { 1364 | 1365 | var opt = $.extend(true, {}, this.options); 1366 | for(var i = 0; i < keys.length; i++) { 1367 | if(opt[keys[i]] !== undefined) { 1368 | opt = opt[keys[i]]; 1369 | } else { 1370 | this._warning( { 1371 | type: $.jPlayer.warning.OPTION_KEY, 1372 | context: key, 1373 | message: $.jPlayer.warningMsg.OPTION_KEY, 1374 | hint: $.jPlayer.warningHint.OPTION_KEY 1375 | }); 1376 | return undefined; 1377 | } 1378 | } 1379 | return opt; 1380 | } 1381 | 1382 | // Enables use: options("someOptionObject", someObject}). Creates: {someOptionObject:someObject} 1383 | // Enables use: options("someOption", someValue). Creates: {someOption:someValue} 1384 | // Enables use: options("someOptionObject.someOption", someValue). Creates: {someOptionObject:{someOption:someValue}} 1385 | 1386 | options = {}; 1387 | var opt = options; 1388 | 1389 | for(var i = 0; i < keys.length; i++) { 1390 | if(i < keys.length - 1) { 1391 | opt[keys[i]] = {}; 1392 | opt = opt[keys[i]]; 1393 | } else { 1394 | opt[keys[i]] = value; 1395 | } 1396 | } 1397 | } 1398 | 1399 | // Otherwise enables use: options(optionObject). Uses original object (the key) 1400 | 1401 | this._setOptions(options); 1402 | 1403 | return this; 1404 | }, 1405 | _setOptions: function(options) { 1406 | var self = this; 1407 | $.each(options, function(key, value) { // This supports the 2 level depth that the options of jPlayer has. Would review if we ever need more depth. 1408 | self._setOption(key, value); 1409 | }); 1410 | 1411 | return this; 1412 | }, 1413 | _setOption: function(key, value) { 1414 | var self = this; 1415 | 1416 | // The ability to set options is limited at this time. 1417 | 1418 | switch(key) { 1419 | case "volume" : 1420 | this.volume(value); 1421 | break; 1422 | case "muted" : 1423 | this._muted(value); 1424 | break; 1425 | case "cssSelectorAncestor" : 1426 | this._cssSelectorAncestor(value); // Set and refresh all associations for the new ancestor. 1427 | break; 1428 | case "cssSelector" : 1429 | $.each(value, function(fn, cssSel) { 1430 | self._cssSelector(fn, cssSel); // NB: The option is set inside this function, after further validity checks. 1431 | }); 1432 | break; 1433 | case "fullScreen" : 1434 | if(this.options[key] !== value) { // if changed 1435 | this._removeUiClass(); 1436 | this.options[key] = value; 1437 | this._refreshSize(); 1438 | } 1439 | break; 1440 | case "size" : 1441 | if(!this.options.fullScreen && this.options[key].cssClass !== value.cssClass) { 1442 | this._removeUiClass(); 1443 | } 1444 | this.options[key] = $.extend({}, this.options[key], value); // store a merged copy of it, incase not all properties changed. 1445 | this._refreshSize(); 1446 | break; 1447 | case "sizeFull" : 1448 | if(this.options.fullScreen && this.options[key].cssClass !== value.cssClass) { 1449 | this._removeUiClass(); 1450 | } 1451 | this.options[key] = $.extend({}, this.options[key], value); // store a merged copy of it, incase not all properties changed. 1452 | this._refreshSize(); 1453 | break; 1454 | } 1455 | 1456 | return this; 1457 | }, 1458 | // End of: (Options code adapted from ui.widget.js) 1459 | 1460 | _refreshSize: function() { 1461 | this._setSize(); // update status and jPlayer element size 1462 | this._addUiClass(); // update the ui class 1463 | this._updateSize(); // update internal sizes 1464 | }, 1465 | _setSize: function() { 1466 | // Determine the current size from the options 1467 | if(this.options.fullScreen) { 1468 | this.status.width = this.options.sizeFull.width; 1469 | this.status.height = this.options.sizeFull.height; 1470 | this.status.cssClass = this.options.sizeFull.cssClass; 1471 | } else { 1472 | this.status.width = this.options.size.width; 1473 | this.status.height = this.options.size.height; 1474 | this.status.cssClass = this.options.size.cssClass; 1475 | } 1476 | 1477 | // Set the size of the jPlayer area. 1478 | this.element.css({'width': this.status.width, 'height': this.status.height}); 1479 | }, 1480 | _addUiClass: function() { 1481 | if(this.ancestorJq.length) { 1482 | this.ancestorJq.addClass(this.status.cssClass); 1483 | } 1484 | }, 1485 | _removeUiClass: function() { 1486 | if(this.ancestorJq.length) { 1487 | this.ancestorJq.removeClass(this.status.cssClass); 1488 | } 1489 | }, 1490 | _updateSize: function() { 1491 | // The poster uses show/hide so can simply resize it. 1492 | this.internal.poster.jq.css({'width': this.status.width, 'height': this.status.height}); 1493 | 1494 | // Video html or flash resized if necessary at this time. 1495 | if(!this.status.waitForPlay) { 1496 | if(this.html.active && this.status.video) { // Only if video media 1497 | this.internal.video.jq.css({'width': this.status.width, 'height': this.status.height}); 1498 | } 1499 | else if(this.flash.active) { 1500 | this.internal.flash.jq.css({'width': this.status.width, 'height': this.status.height}); 1501 | } 1502 | } 1503 | }, 1504 | fullScreen: function() { 1505 | this._setOption("fullScreen", true); 1506 | }, 1507 | restoreScreen: function() { 1508 | this._setOption("fullScreen", false); 1509 | }, 1510 | _html_initMedia: function() { 1511 | if(this.status.srcSet && !this.status.waitForPlay) { 1512 | this.htmlElement.media.pause(); 1513 | } 1514 | if(this.options.preload !== 'none') { 1515 | this._html_load(); 1516 | } 1517 | this._trigger($.jPlayer.event.timeupdate); // The flash generates this event for its solution. 1518 | }, 1519 | _html_setAudio: function(media) { 1520 | var self = this; 1521 | // Always finds a format due to checks in setMedia() 1522 | $.each(this.formats, function(priority, format) { 1523 | if(self.html.support[format] && media[format]) { 1524 | self.status.src = media[format]; 1525 | self.status.format[format] = true; 1526 | self.status.formatType = format; 1527 | return false; 1528 | } 1529 | }); 1530 | this.htmlElement.media = this.htmlElement.audio; 1531 | this._html_initMedia(); 1532 | }, 1533 | _html_setVideo: function(media) { 1534 | var self = this; 1535 | // Always finds a format due to checks in setMedia() 1536 | $.each(this.formats, function(priority, format) { 1537 | if(self.html.support[format] && media[format]) { 1538 | self.status.src = media[format]; 1539 | self.status.format[format] = true; 1540 | self.status.formatType = format; 1541 | return false; 1542 | } 1543 | }); 1544 | this.htmlElement.media = this.htmlElement.video; 1545 | this._html_initMedia(); 1546 | }, 1547 | _html_clearMedia: function() { 1548 | if(this.htmlElement.media) { 1549 | if(this.htmlElement.media.id === this.internal.video.id) { 1550 | this.internal.video.jq.css({'width':'0px', 'height':'0px'}); 1551 | } 1552 | this.htmlElement.media.pause(); 1553 | this.htmlElement.media.src = ""; 1554 | this.htmlElement.media.load(); // Stops an old, "in progress" download from continuing the download. Triggers the loadstart, error and emptied events, due to the empty src. Also an abort event if a download was in progress. 1555 | } 1556 | }, 1557 | _html_load: function() { 1558 | if(this.status.waitForLoad) { 1559 | this.status.waitForLoad = false; 1560 | this.htmlElement.media.src = this.status.src; 1561 | this.htmlElement.media.load(); 1562 | } 1563 | clearTimeout(this.internal.htmlDlyCmdId); 1564 | }, 1565 | _html_play: function(time) { 1566 | var self = this; 1567 | this._html_load(); // Loads if required and clears any delayed commands. 1568 | 1569 | this.htmlElement.media.play(); // Before currentTime attempt otherwise Firefox 4 Beta never loads. 1570 | 1571 | if(!isNaN(time)) { 1572 | try { 1573 | this.htmlElement.media.currentTime = time; 1574 | } catch(err) { 1575 | this.internal.htmlDlyCmdId = setTimeout(function() { 1576 | self.play(time); 1577 | }, 100); 1578 | return; // Cancel execution and wait for the delayed command. 1579 | } 1580 | } 1581 | this._html_checkWaitForPlay(); 1582 | }, 1583 | _html_pause: function(time) { 1584 | var self = this; 1585 | 1586 | if(time > 0) { // We do not want the stop() command, which does pause(0), causing a load operation. 1587 | this._html_load(); // Loads if required and clears any delayed commands. 1588 | } else { 1589 | clearTimeout(this.internal.htmlDlyCmdId); 1590 | } 1591 | 1592 | // Order of these commands is important for Safari (Win) and IE9. Pause then change currentTime. 1593 | this.htmlElement.media.pause(); 1594 | 1595 | if(!isNaN(time)) { 1596 | try { 1597 | this.htmlElement.media.currentTime = time; 1598 | } catch(err) { 1599 | this.internal.htmlDlyCmdId = setTimeout(function() { 1600 | self.pause(time); 1601 | }, 100); 1602 | return; // Cancel execution and wait for the delayed command. 1603 | } 1604 | } 1605 | if(time > 0) { // Avoids a setMedia() followed by stop() or pause(0) hiding the video play button. 1606 | this._html_checkWaitForPlay(); 1607 | } 1608 | }, 1609 | _html_playHead: function(percent) { 1610 | var self = this; 1611 | this._html_load(); // Loads if required and clears any delayed commands. 1612 | try { 1613 | if((typeof this.htmlElement.media.seekable === "object") && (this.htmlElement.media.seekable.length > 0)) { 1614 | this.htmlElement.media.currentTime = percent * this.htmlElement.media.seekable.end(this.htmlElement.media.seekable.length-1) / 100; 1615 | } else if(this.htmlElement.media.duration > 0 && !isNaN(this.htmlElement.media.duration)) { 1616 | this.htmlElement.media.currentTime = percent * this.htmlElement.media.duration / 100; 1617 | } else { 1618 | throw "e"; 1619 | } 1620 | } catch(err) { 1621 | this.internal.htmlDlyCmdId = setTimeout(function() { 1622 | self.playHead(percent); 1623 | }, 100); 1624 | return; // Cancel execution and wait for the delayed command. 1625 | } 1626 | if(!this.status.waitForLoad) { 1627 | this._html_checkWaitForPlay(); 1628 | } 1629 | }, 1630 | _html_checkWaitForPlay: function() { 1631 | if(this.status.waitForPlay) { 1632 | this.status.waitForPlay = false; 1633 | if(this.css.jq.videoPlay.length) { 1634 | this.css.jq.videoPlay.hide(); 1635 | } 1636 | if(this.status.video) { 1637 | this.internal.poster.jq.hide(); 1638 | this.internal.video.jq.css({'width': this.status.width, 'height': this.status.height}); 1639 | } 1640 | } 1641 | }, 1642 | _html_volume: function(v) { 1643 | if(this.html.audio.available) { 1644 | this.htmlElement.audio.volume = v; 1645 | } 1646 | if(this.html.video.available) { 1647 | this.htmlElement.video.volume = v; 1648 | } 1649 | }, 1650 | _html_mute: function(m) { 1651 | if(this.html.audio.available) { 1652 | this.htmlElement.audio.muted = m; 1653 | } 1654 | if(this.html.video.available) { 1655 | this.htmlElement.video.muted = m; 1656 | } 1657 | }, 1658 | _flash_setAudio: function(media) { 1659 | var self = this; 1660 | try { 1661 | // Always finds a format due to checks in setMedia() 1662 | $.each(this.formats, function(priority, format) { 1663 | if(self.flash.support[format] && media[format]) { 1664 | switch (format) { 1665 | case "m4a" : 1666 | self._getMovie().fl_setAudio_m4a(media[format]); 1667 | break; 1668 | case "mp3" : 1669 | self._getMovie().fl_setAudio_mp3(media[format]); 1670 | break; 1671 | } 1672 | self.status.src = media[format]; 1673 | self.status.format[format] = true; 1674 | self.status.formatType = format; 1675 | return false; 1676 | } 1677 | }); 1678 | 1679 | if(this.options.preload === 'auto') { 1680 | this._flash_load(); 1681 | this.status.waitForLoad = false; 1682 | } 1683 | } catch(err) { this._flashError(err); } 1684 | }, 1685 | _flash_setVideo: function(media) { 1686 | var self = this; 1687 | try { 1688 | // Always finds a format due to checks in setMedia() 1689 | $.each(this.formats, function(priority, format) { 1690 | if(self.flash.support[format] && media[format]) { 1691 | switch (format) { 1692 | case "m4v" : 1693 | self._getMovie().fl_setVideo_m4v(media[format]); 1694 | break; 1695 | } 1696 | self.status.src = media[format]; 1697 | self.status.format[format] = true; 1698 | self.status.formatType = format; 1699 | return false; 1700 | } 1701 | }); 1702 | 1703 | if(this.options.preload === 'auto') { 1704 | this._flash_load(); 1705 | this.status.waitForLoad = false; 1706 | } 1707 | } catch(err) { this._flashError(err); } 1708 | }, 1709 | _flash_clearMedia: function() { 1710 | this.internal.flash.jq.css({'width':'0px', 'height':'0px'}); // Must do via CSS as setting attr() to zero causes a jQuery error in IE. 1711 | try { 1712 | this._getMovie().fl_clearMedia(); 1713 | } catch(err) { this._flashError(err); } 1714 | }, 1715 | _flash_load: function() { 1716 | try { 1717 | this._getMovie().fl_load(); 1718 | } catch(err) { this._flashError(err); } 1719 | this.status.waitForLoad = false; 1720 | }, 1721 | _flash_play: function(time) { 1722 | try { 1723 | this._getMovie().fl_play(time); 1724 | } catch(err) { this._flashError(err); } 1725 | this.status.waitForLoad = false; 1726 | this._flash_checkWaitForPlay(); 1727 | }, 1728 | _flash_pause: function(time) { 1729 | try { 1730 | this._getMovie().fl_pause(time); 1731 | } catch(err) { this._flashError(err); } 1732 | if(time > 0) { // Avoids a setMedia() followed by stop() or pause(0) hiding the video play button. 1733 | this.status.waitForLoad = false; 1734 | this._flash_checkWaitForPlay(); 1735 | } 1736 | }, 1737 | _flash_playHead: function(p) { 1738 | try { 1739 | this._getMovie().fl_play_head(p) 1740 | } catch(err) { this._flashError(err); } 1741 | if(!this.status.waitForLoad) { 1742 | this._flash_checkWaitForPlay(); 1743 | } 1744 | }, 1745 | _flash_checkWaitForPlay: function() { 1746 | if(this.status.waitForPlay) { 1747 | this.status.waitForPlay = false; 1748 | if(this.css.jq.videoPlay.length) { 1749 | this.css.jq.videoPlay.hide(); 1750 | } 1751 | if(this.status.video) { 1752 | this.internal.poster.jq.hide(); 1753 | this.internal.flash.jq.css({'width': this.status.width, 'height': this.status.height}); 1754 | } 1755 | } 1756 | }, 1757 | _flash_volume: function(v) { 1758 | try { 1759 | this._getMovie().fl_volume(v); 1760 | } catch(err) { this._flashError(err); } 1761 | }, 1762 | _flash_mute: function(m) { 1763 | try { 1764 | this._getMovie().fl_mute(m); 1765 | } catch(err) { this._flashError(err); } 1766 | }, 1767 | _getMovie: function() { 1768 | return document[this.internal.flash.id]; 1769 | }, 1770 | _checkForFlash: function (version) { 1771 | // Function checkForFlash adapted from FlashReplace by Robert Nyman 1772 | // http://code.google.com/p/flashreplace/ 1773 | var flashIsInstalled = false; 1774 | var flash; 1775 | if(window.ActiveXObject){ 1776 | try{ 1777 | flash = new ActiveXObject(("ShockwaveFlash.ShockwaveFlash." + version)); 1778 | flashIsInstalled = true; 1779 | } 1780 | catch(e){ 1781 | // Throws an error if the version isn't available 1782 | } 1783 | } 1784 | else if(navigator.plugins && navigator.mimeTypes.length > 0){ 1785 | flash = navigator.plugins["Shockwave Flash"]; 1786 | if(flash){ 1787 | var flashVersion = navigator.plugins["Shockwave Flash"].description.replace(/.*\s(\d+\.\d+).*/, "$1"); 1788 | if(flashVersion >= version){ 1789 | flashIsInstalled = true; 1790 | } 1791 | } 1792 | } 1793 | return flashIsInstalled; 1794 | }, 1795 | _validString: function(url) { 1796 | return (url && typeof url === "string"); // Empty strings return false 1797 | }, 1798 | _limitValue: function(value, min, max) { 1799 | return (value < min) ? min : ((value > max) ? max : value); 1800 | }, 1801 | _urlNotSetError: function(context) { 1802 | this._error( { 1803 | type: $.jPlayer.error.URL_NOT_SET, 1804 | context: context, 1805 | message: $.jPlayer.errorMsg.URL_NOT_SET, 1806 | hint: $.jPlayer.errorHint.URL_NOT_SET 1807 | }); 1808 | }, 1809 | _flashError: function(error) { 1810 | this._error( { 1811 | type: $.jPlayer.error.FLASH, 1812 | context: this.internal.flash.swf, 1813 | message: $.jPlayer.errorMsg.FLASH + error.message, 1814 | hint: $.jPlayer.errorHint.FLASH 1815 | }); 1816 | }, 1817 | _error: function(error) { 1818 | this._trigger($.jPlayer.event.error, error); 1819 | if(this.options.errorAlerts) { 1820 | this._alert("Error!" + (error.message ? "\n\n" + error.message : "") + (error.hint ? "\n\n" + error.hint : "") + "\n\nContext: " + error.context); 1821 | } 1822 | }, 1823 | _warning: function(warning) { 1824 | this._trigger($.jPlayer.event.warning, undefined, warning); 1825 | if(this.options.warningAlerts) { 1826 | this._alert("Warning!" + (warning.message ? "\n\n" + warning.message : "") + (warning.hint ? "\n\n" + warning.hint : "") + "\n\nContext: " + warning.context); 1827 | } 1828 | }, 1829 | _alert: function(message) { 1830 | alert("jPlayer " + this.version.script + " : id='" + this.internal.self.id +"' : " + message); 1831 | } 1832 | }; 1833 | 1834 | $.jPlayer.error = { 1835 | FLASH: "e_flash", 1836 | NO_SOLUTION: "e_no_solution", 1837 | NO_SUPPORT: "e_no_support", 1838 | URL: "e_url", 1839 | URL_NOT_SET: "e_url_not_set", 1840 | VERSION: "e_version" 1841 | }; 1842 | 1843 | $.jPlayer.errorMsg = { 1844 | FLASH: "jPlayer's Flash fallback is not configured correctly, or a command was issued before the jPlayer Ready event. Details: ", // Used in: _flashError() 1845 | NO_SOLUTION: "No solution can be found by jPlayer in this browser. Neither HTML nor Flash can be used.", // Used in: _init() 1846 | NO_SUPPORT: "It is not possible to play any media format provided in setMedia() on this browser using your current options.", // Used in: setMedia() 1847 | URL: "Media URL could not be loaded.", // Used in: jPlayerFlashEvent() and _addHtmlEventListeners() 1848 | URL_NOT_SET: "Attempt to issue media playback commands, while no media url is set.", // Used in: load(), play(), pause(), stop() and playHead() 1849 | VERSION: "jPlayer " + $.jPlayer.prototype.version.script + " needs Jplayer.swf version " + $.jPlayer.prototype.version.needFlash + " but found " // Used in: jPlayerReady() 1850 | }; 1851 | 1852 | $.jPlayer.errorHint = { 1853 | FLASH: "Check your swfPath option and that Jplayer.swf is there.", 1854 | NO_SOLUTION: "Review the jPlayer options: support and supplied.", 1855 | NO_SUPPORT: "Video or audio formats defined in the supplied option are missing.", 1856 | URL: "Check media URL is valid.", 1857 | URL_NOT_SET: "Use setMedia() to set the media URL.", 1858 | VERSION: "Update jPlayer files." 1859 | }; 1860 | 1861 | $.jPlayer.warning = { 1862 | CSS_SELECTOR_COUNT: "e_css_selector_count", 1863 | CSS_SELECTOR_METHOD: "e_css_selector_method", 1864 | CSS_SELECTOR_STRING: "e_css_selector_string", 1865 | OPTION_KEY: "e_option_key" 1866 | }; 1867 | 1868 | $.jPlayer.warningMsg = { 1869 | CSS_SELECTOR_COUNT: "The number of css selectors found did not equal one: ", 1870 | CSS_SELECTOR_METHOD: "The methodName given in jPlayer('cssSelector') is not a valid jPlayer method.", 1871 | CSS_SELECTOR_STRING: "The methodCssSelector given in jPlayer('cssSelector') is not a String or is empty.", 1872 | OPTION_KEY: "The option requested in jPlayer('option') is undefined." 1873 | }; 1874 | 1875 | $.jPlayer.warningHint = { 1876 | CSS_SELECTOR_COUNT: "Check your css selector and the ancestor.", 1877 | CSS_SELECTOR_METHOD: "Check your method name.", 1878 | CSS_SELECTOR_STRING: "Check your css selector is a string.", 1879 | OPTION_KEY: "Check your option name." 1880 | }; 1881 | })(jQuery); 1882 | -------------------------------------------------------------------------------- /js/libs/jquery.scrollTo-min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * jQuery.ScrollTo - Easy element scrolling using jQuery. 3 | * Copyright (c) 2007-2009 Ariel Flesler - aflesler(at)gmail(dot)com | http://flesler.blogspot.com 4 | * Dual licensed under MIT and GPL. 5 | * Date: 5/25/2009 6 | * @author Ariel Flesler 7 | * @version 1.4.2 8 | * 9 | * http://flesler.blogspot.com/2007/10/jqueryscrollto.html 10 | */ 11 | ;(function(d){var k=d.scrollTo=function(a,i,e){d(window).scrollTo(a,i,e)};k.defaults={axis:'xy',duration:parseFloat(d.fn.jquery)>=1.3?0:1};k.window=function(a){return d(window)._scrollable()};d.fn._scrollable=function(){return this.map(function(){var a=this,i=!a.nodeName||d.inArray(a.nodeName.toLowerCase(),['iframe','#document','html','body'])!=-1;if(!i)return a;var e=(a.contentWindow||a).document||a.ownerDocument||a;return d.browser.safari||e.compatMode=='BackCompat'?e.body:e.documentElement})};d.fn.scrollTo=function(n,j,b){if(typeof j=='object'){b=j;j=0}if(typeof b=='function')b={onAfter:b};if(n=='max')n=9e9;b=d.extend({},k.defaults,b);j=j||b.speed||b.duration;b.queue=b.queue&&b.axis.length>1;if(b.queue)j/=2;b.offset=p(b.offset);b.over=p(b.over);return this._scrollable().each(function(){var q=this,r=d(q),f=n,s,g={},u=r.is('html,body');switch(typeof f){case'number':case'string':if(/^([+-]=)?\d+(\.\d+)?(px|%)?$/.test(f)){f=p(f);break}f=d(f,this);case'object':if(f.is||f.style)s=(f=d(f)).offset()}d.each(b.axis.split(''),function(a,i){var e=i=='x'?'Left':'Top',h=e.toLowerCase(),c='scroll'+e,l=q[c],m=k.max(q,i);if(s){g[c]=s[h]+(u?0:l-r.offset()[h]);if(b.margin){g[c]-=parseInt(f.css('margin'+e))||0;g[c]-=parseInt(f.css('border'+e+'Width'))||0}g[c]+=b.offset[h]||0;if(b.over[h])g[c]+=f[i=='x'?'width':'height']()*b.over[h]}else{var o=f[h];g[c]=o.slice&&o.slice(-1)=='%'?parseFloat(o)/100*m:o}if(/^\d+$/.test(g[c]))g[c]=g[c]<=0?0:Math.min(g[c],m);if(!a&&b.queue){if(l!=g[c])t(b.onAfterFirst);delete g[c]}});t(b.onAfter);function t(a){r.animate(g,j,b.easing,a&&function(){a.call(this,n,b)})}}).end()};k.max=function(a,i){var e=i=='x'?'Width':'Height',h='scroll'+e;if(!d(a).is('html,body'))return a[h]-d(a)[e.toLowerCase()]();var c='client'+e,l=a.ownerDocument.documentElement,m=a.ownerDocument.body;return Math.max(l[h],m[h])-Math.min(l[c],m[c])};function p(a){return typeof a=='object'?a:{top:a,left:a}}})(jQuery); -------------------------------------------------------------------------------- /js/libs/mod.csstransitions.min.js: -------------------------------------------------------------------------------- 1 | /* Modernizr custom build of 1.7: csstransitions */ 2 | window.Modernizr=function(a,b,c){function G(){}function F(a,b){var c=a.charAt(0).toUpperCase()+a.substr(1),d=(a+" "+p.join(c+" ")+c).split(" ");return!!E(d,b)}function E(a,b){for(var d in a)if(k[a[d]]!==c&&(!b||b(a[d],j)))return!0}function D(a,b){return(""+a).indexOf(b)!==-1}function C(a,b){return typeof a===b}function B(a,b){return A(o.join(a+";")+(b||""))}function A(a){k.cssText=a}var d="1.7",e={},f=!0,g=b.documentElement,h=b.head||b.getElementsByTagName("head")[0],i="modernizr",j=b.createElement(i),k=j.style,l=b.createElement("input"),m=":)",n=Object.prototype.toString,o=" -webkit- -moz- -o- -ms- -khtml- ".split(" "),p="Webkit Moz O ms Khtml".split(" "),q={svg:"http://www.w3.org/2000/svg"},r={},s={},t={},u=[],v,w=function(a){var c=b.createElement("style"),d=b.createElement("div"),e;c.textContent=a+"{#modernizr{height:3px}}",h.appendChild(c),d.id="modernizr",g.appendChild(d),e=d.offsetHeight===3,c.parentNode.removeChild(c),d.parentNode.removeChild(d);return!!e},x=function(){function d(d,e){e=e||b.createElement(a[d]||"div");var f=(d="on"+d)in e;f||(e.setAttribute||(e=b.createElement("div")),e.setAttribute&&e.removeAttribute&&(e.setAttribute(d,""),f=C(e[d],"function"),C(e[d],c)||(e[d]=c),e.removeAttribute(d))),e=null;return f}var a={select:"input",change:"input",submit:"form",reset:"form",error:"img",load:"img",abort:"img"};return d}(),y=({}).hasOwnProperty,z;C(y,c)||C(y.call,c)?z=function(a,b){return b in a&&C(a.constructor.prototype[b],c)}:z=function(a,b){return y.call(a,b)},r.csstransitions=function(){return F("transitionProperty")};for(var H in r)z(r,H)&&(v=H.toLowerCase(),e[v]=r[H](),u.push((e[v]?"":"no-")+v));e.input||G(),e.crosswindowmessaging=e.postmessage,e.historymanagement=e.history,e.addTest=function(a,b){a=a.toLowerCase();if(!e[a]){b=!!b(),g.className+=" "+(b?"":"no-")+a,e[a]=b;return e}},A(""),j=l=null,e._enableHTML5=f,e._version=d,g.className=g.className.replace(/\bno-js\b/,"")+" js "+u.join(" ");return e}(this,this.document) -------------------------------------------------------------------------------- /js/libs/popcorn.js: -------------------------------------------------------------------------------- 1 | /* 2 | * popcorn.js version @VERSION 3 | * http://popcornjs.org 4 | * 5 | * Copyright 2011, Mozilla Foundation 6 | * Licensed under the MIT license 7 | */ 8 | 9 | (function(global, document) { 10 | 11 | // Cache refs to speed up calls to native utils 12 | var 13 | forEach = Array.prototype.forEach, 14 | hasOwn = Object.prototype.hasOwnProperty, 15 | slice = Array.prototype.slice, 16 | 17 | // ID string matching 18 | rIdExp = /^(#([\w\-\_\.]+))$/, 19 | 20 | // Ready fn cache 21 | readyStack = [], 22 | readyBound = false, 23 | readyFired = false, 24 | 25 | 26 | // Declare constructor 27 | // Returns an instance object. 28 | Popcorn = function( entity ) { 29 | // Return new Popcorn object 30 | return new Popcorn.p.init( entity ); 31 | }; 32 | 33 | // Instance caching 34 | Popcorn.instances = []; 35 | Popcorn.instanceIds = {}; 36 | 37 | Popcorn.removeInstance = function( instance ) { 38 | // If called prior to any instances being created 39 | // Return early to avoid splicing on nothing 40 | if ( !Popcorn.instances.length ) { 41 | return; 42 | } 43 | 44 | // Remove instance from Popcorn.instances 45 | Popcorn.instances.splice( Popcorn.instanceIds[ instance.id ], 1 ); 46 | 47 | // Delete the instance id key 48 | delete Popcorn.instanceIds[ instance.id ]; 49 | 50 | // Return current modified instances 51 | return Popcorn.instances; 52 | }; 53 | 54 | // Addes a Popcorn instance to the Popcorn instance array 55 | Popcorn.addInstance = function( instance ) { 56 | 57 | var instanceLen = Popcorn.instances.length, 58 | instanceId = instance.video.id && instance.video.id; 59 | 60 | // If the video element has its own `id` use it, otherwise provide one 61 | // Ensure that instances have unique ids and unique entries 62 | // Uses `in` operator to avoid false positives on 0 63 | instance.id = !( instanceId in Popcorn.instanceIds ) && instanceId || 64 | "__popcorn" + instanceLen; 65 | 66 | // Create a reference entry for this instance 67 | Popcorn.instanceIds[ instance.id ] = instanceLen; 68 | 69 | // Add this instance to the cache 70 | Popcorn.instances.push( instance ); 71 | 72 | // Return the current modified instances 73 | return Popcorn.instances; 74 | }; 75 | 76 | // Request Popcorn object instance by id 77 | Popcorn.getInstanceById = function( id ) { 78 | return Popcorn.instances[ Popcorn.instanceIds[ id ] ]; 79 | }; 80 | 81 | // Remove Popcorn object instance by id 82 | Popcorn.removeInstanceById = function( id ) { 83 | return Popcorn.removeInstance( Popcorn.instances[ Popcorn.instanceIds[ id ] ] ); 84 | }; 85 | 86 | // Declare a shortcut (Popcorn.p) to and a definition of 87 | // the new prototype for our Popcorn constructor 88 | Popcorn.p = Popcorn.prototype = { 89 | 90 | init: function( entity ) { 91 | 92 | var matches; 93 | 94 | // Supports Popcorn(function () { /../ }) 95 | // Originally proposed by Daniel Brooks 96 | 97 | if ( typeof entity === "function" ) { 98 | 99 | // If document ready has already fired 100 | if ( document.readyState === "interactive" || document.readyState === "complete" ) { 101 | 102 | entity(document, Popcorn); 103 | 104 | return; 105 | } 106 | // Add `entity` fn to ready stack 107 | readyStack.push( entity ); 108 | 109 | // This process should happen once per page load 110 | if ( !readyBound ) { 111 | 112 | // set readyBound flag 113 | readyBound = true; 114 | 115 | var DOMContentLoaded = function () { 116 | 117 | readyFired = true; 118 | 119 | // Remove global DOM ready listener 120 | document.removeEventListener( "DOMContentLoaded", DOMContentLoaded, false ); 121 | 122 | // Execute all ready function in the stack 123 | for ( var i = 0; i < readyStack.length; i++ ) { 124 | 125 | readyStack[i].call( document, Popcorn ); 126 | 127 | } 128 | // GC readyStack 129 | readyStack = null; 130 | }; 131 | 132 | // Register global DOM ready listener 133 | document.addEventListener( "DOMContentLoaded", DOMContentLoaded, false); 134 | } 135 | 136 | return; 137 | } 138 | 139 | // Check if entity is a valid string id 140 | matches = rIdExp.exec( entity ); 141 | 142 | // Get video element by id or object reference 143 | this.video = matches && matches.length && matches[ 2 ] ? 144 | document.getElementById( matches[ 2 ] ) : 145 | entity; 146 | 147 | // Register new instance 148 | Popcorn.addInstance( this ); 149 | 150 | this.data = { 151 | history: [], 152 | events: {}, 153 | trackEvents: { 154 | byStart: [{ 155 | start: -1, 156 | end: -1 157 | }], 158 | byEnd: [{ 159 | start: -1, 160 | end: -1 161 | }], 162 | startIndex: 0, 163 | endIndex: 0, 164 | previousUpdateTime: 0 165 | } 166 | }; 167 | 168 | // Wrap true ready check 169 | var isReady = function( that ) { 170 | 171 | if ( that.video.readyState >= 2 ) { 172 | // Adding padding to the front and end of the arrays 173 | // this is so we do not fall off either end 174 | 175 | var duration = that.video.duration; 176 | // Check for no duration info (NaN) 177 | var videoDurationPlus = duration != duration ? Number.MAX_VALUE : duration + 1; 178 | 179 | Popcorn.addTrackEvent( that, { 180 | start: videoDurationPlus, 181 | end: videoDurationPlus 182 | }); 183 | 184 | that.video.addEventListener( "timeupdate", function( event ) { 185 | 186 | var currentTime = this.currentTime, 187 | previousTime = that.data.trackEvents.previousUpdateTime, 188 | tracks = that.data.trackEvents, 189 | tracksByEnd = tracks.byEnd, 190 | tracksByStart = tracks.byStart; 191 | 192 | // Playbar advancing 193 | if ( previousTime < currentTime ) { 194 | 195 | while ( tracksByEnd[ tracks.endIndex ] && tracksByEnd[ tracks.endIndex ].end <= currentTime ) { 196 | // If plugin does not exist on this instance, remove it 197 | if ( !tracksByEnd[ tracks.endIndex ]._natives || !!that[ tracksByEnd[ tracks.endIndex ]._natives.type ] ) { 198 | if ( tracksByEnd[ tracks.endIndex ]._running === true ) { 199 | tracksByEnd[ tracks.endIndex ]._running = false; 200 | tracksByEnd[ tracks.endIndex ]._natives.end.call( that, event, tracksByEnd[ tracks.endIndex ] ); 201 | } 202 | tracks.endIndex++; 203 | } else { 204 | // remove track event 205 | Popcorn.removeTrackEvent( that, tracksByEnd[ tracks.endIndex ]._id ); 206 | return; 207 | } 208 | } 209 | 210 | while ( tracksByStart[ tracks.startIndex ] && tracksByStart[ tracks.startIndex ].start <= currentTime ) { 211 | // If plugin does not exist on this instance, remove it 212 | if ( !tracksByStart[ tracks.startIndex ]._natives || !!that[ tracksByStart[ tracks.startIndex ]._natives.type ] ) { 213 | if ( tracksByStart[ tracks.startIndex ].end > currentTime && tracksByStart[ tracks.startIndex ]._running === false ) { 214 | tracksByStart[ tracks.startIndex ]._running = true; 215 | tracksByStart[ tracks.startIndex ]._natives.start.call( that, event, tracksByStart[ tracks.startIndex ] ); 216 | } 217 | tracks.startIndex++; 218 | } else { 219 | // remove track event 220 | Popcorn.removeTrackEvent( that, tracksByStart[ tracks.startIndex ]._id ); 221 | return; 222 | } 223 | } 224 | 225 | // Playbar receding 226 | } else if ( previousTime > currentTime ) { 227 | 228 | while ( tracksByStart[ tracks.startIndex ] && tracksByStart[ tracks.startIndex ].start > currentTime ) { 229 | // if plugin does not exist on this instance, remove it 230 | if ( !tracksByStart[ tracks.startIndex ]._natives || !!that[ tracksByStart[ tracks.startIndex ]._natives.type ] ) { 231 | if ( tracksByStart[ tracks.startIndex ]._running === true ) { 232 | tracksByStart[ tracks.startIndex ]._running = false; 233 | tracksByStart[ tracks.startIndex ]._natives.end.call( that, event, tracksByStart[ tracks.startIndex ] ); 234 | } 235 | tracks.startIndex--; 236 | } else { 237 | // remove track event 238 | Popcorn.removeTrackEvent( that, tracksByStart[ tracks.startIndex ]._id ); 239 | return; 240 | } 241 | } 242 | 243 | while ( tracksByEnd[ tracks.endIndex ] && tracksByEnd[ tracks.endIndex ].end > currentTime ) { 244 | // if plugin does not exist on this instance, remove it 245 | if ( !tracksByEnd[ tracks.endIndex ]._natives || !!that[ tracksByEnd[ tracks.endIndex ]._natives.type ] ) { 246 | if ( tracksByEnd[ tracks.endIndex ].start <= currentTime && tracksByEnd[ tracks.endIndex ]._running === false ) { 247 | tracksByEnd[ tracks.endIndex ]._running = true; 248 | tracksByEnd[ tracks.endIndex ]._natives.start.call( that, event, tracksByEnd[tracks.endIndex] ); 249 | } 250 | tracks.endIndex--; 251 | } else { 252 | // remove track event 253 | Popcorn.removeTrackEvent( that, tracksByEnd[ tracks.endIndex ]._id ); 254 | return; 255 | } 256 | } 257 | } 258 | 259 | tracks.previousUpdateTime = currentTime; 260 | 261 | }, false); 262 | } else { 263 | global.setTimeout( function() { 264 | isReady( that ); 265 | }, 1); 266 | } 267 | }; 268 | 269 | isReady( this ); 270 | 271 | return this; 272 | } 273 | }; 274 | 275 | // Extend constructor prototype to instance prototype 276 | // Allows chaining methods to instances 277 | Popcorn.p.init.prototype = Popcorn.p; 278 | 279 | Popcorn.forEach = function( obj, fn, context ) { 280 | 281 | if ( !obj || !fn ) { 282 | return {}; 283 | } 284 | 285 | context = context || this; 286 | // Use native whenever possible 287 | if ( forEach && obj.forEach === forEach ) { 288 | return obj.forEach(fn, context); 289 | } 290 | 291 | for ( var key in obj ) { 292 | if ( hasOwn.call(obj, key) ) { 293 | fn.call(context, obj[key], key, obj); 294 | } 295 | } 296 | 297 | return obj; 298 | }; 299 | 300 | Popcorn.extend = function( obj ) { 301 | var dest = obj, src = slice.call(arguments, 1); 302 | 303 | Popcorn.forEach( src, function( copy ) { 304 | for ( var prop in copy ) { 305 | dest[prop] = copy[prop]; 306 | } 307 | }); 308 | return dest; 309 | }; 310 | 311 | 312 | // A Few reusable utils, memoized onto Popcorn 313 | Popcorn.extend( Popcorn, { 314 | error: function( msg ) { 315 | throw msg; 316 | }, 317 | guid: function( prefix ) { 318 | Popcorn.guid.counter++; 319 | return ( prefix ? prefix : "" ) + ( +new Date() + Popcorn.guid.counter ); 320 | }, 321 | sizeOf: function ( obj ) { 322 | var size = 0; 323 | 324 | for ( var prop in obj ) { 325 | size++; 326 | } 327 | 328 | return size; 329 | }, 330 | nop: function () {} 331 | }); 332 | 333 | // Memoized GUID Counter 334 | Popcorn.guid.counter = 1; 335 | 336 | // Factory to implement getters, setters and controllers 337 | // as Popcorn instance methods. The IIFE will create and return 338 | // an object with defined methods 339 | Popcorn.extend(Popcorn.p, (function () { 340 | 341 | var methods = "load play pause currentTime playbackRate mute volume duration", 342 | ret = {}; 343 | 344 | 345 | // Build methods, store in object that is returned and passed to extend 346 | Popcorn.forEach( methods.split(/\s+/g), function( name ) { 347 | 348 | ret[ name ] = function( arg ) { 349 | 350 | if ( typeof this.video[name] === "function" ) { 351 | this.video[ name ](); 352 | 353 | return this; 354 | } 355 | 356 | 357 | if ( arg !== false && arg !== null && typeof arg !== "undefined" ) { 358 | 359 | this.video[ name ] = arg; 360 | 361 | return this; 362 | } 363 | 364 | return this.video[ name ]; 365 | }; 366 | }); 367 | 368 | return ret; 369 | 370 | })() 371 | ); 372 | 373 | Popcorn.extend(Popcorn.p, { 374 | 375 | // Rounded currentTime 376 | roundTime: function () { 377 | return -~this.video.currentTime; 378 | }, 379 | 380 | // Attach an event to a single point in time 381 | exec: function ( time, fn ) { 382 | 383 | // Creating a one second track event with an empty end 384 | Popcorn.addTrackEvent( this, { 385 | start: time, 386 | end: time + 1, 387 | _running: false, 388 | _natives: { 389 | start: fn || Popcorn.nop, 390 | end: Popcorn.nop, 391 | type: "exec" 392 | } 393 | }); 394 | 395 | return this; 396 | } 397 | }); 398 | 399 | Popcorn.Events = { 400 | UIEvents: "blur focus focusin focusout load resize scroll unload ", 401 | MouseEvents: "mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave click dblclick", 402 | Events: "loadstart progress suspend emptied stalled play pause " + 403 | "loadedmetadata loadeddata waiting playing canplay canplaythrough " + 404 | "seeking seeked timeupdate ended ratechange durationchange volumechange" 405 | }; 406 | 407 | Popcorn.Events.Natives = Popcorn.Events.UIEvents + " " + 408 | Popcorn.Events.MouseEvents + " " + 409 | Popcorn.Events.Events; 410 | 411 | Popcorn.events = { 412 | 413 | 414 | isNative: function( type ) { 415 | 416 | var checks = Popcorn.Events.Natives.split( /\s+/g ); 417 | 418 | for ( var i = 0; i < checks.length; i++ ) { 419 | if ( checks[i] === type ) { 420 | return true; 421 | } 422 | } 423 | 424 | return false; 425 | }, 426 | getInterface: function( type ) { 427 | 428 | if ( !Popcorn.events.isNative( type ) ) { 429 | return false; 430 | } 431 | 432 | var natives = Popcorn.Events, 433 | proto; 434 | 435 | for ( var p in natives ) { 436 | if ( p !== "Natives" && natives[ p ].indexOf( type ) > -1 ) { 437 | proto = p; 438 | } 439 | } 440 | 441 | return proto; 442 | }, 443 | // Compile all native events to single array 444 | all: Popcorn.Events.Natives.split(/\s+/g), 445 | // Defines all Event handling static functions 446 | fn: { 447 | trigger: function ( type, data ) { 448 | 449 | // setup checks for custom event system 450 | if ( this.data.events[ type ] && Popcorn.sizeOf( this.data.events[ type ] ) ) { 451 | 452 | var eventInterface = Popcorn.events.getInterface( type ); 453 | 454 | if ( eventInterface ) { 455 | 456 | var evt = document.createEvent( eventInterface ); 457 | evt.initEvent(type, true, true, global, 1); 458 | 459 | this.video.dispatchEvent(evt); 460 | 461 | return this; 462 | } 463 | 464 | // Custom events 465 | Popcorn.forEach(this.data.events[ type ], function ( obj, key ) { 466 | 467 | obj.call( this, data ); 468 | 469 | }, this); 470 | 471 | } 472 | 473 | return this; 474 | }, 475 | listen: function ( type, fn ) { 476 | 477 | var self = this, hasEvents = true; 478 | 479 | if ( !this.data.events[type] ) { 480 | this.data.events[type] = {}; 481 | hasEvents = false; 482 | } 483 | 484 | // Register 485 | this.data.events[ type ][ fn.name || ( fn.toString() + Popcorn.guid() ) ] = fn; 486 | 487 | // only attach one event of any type 488 | if ( !hasEvents && Popcorn.events.all.indexOf( type ) > -1 ) { 489 | 490 | this.video.addEventListener( type, function( event ) { 491 | 492 | Popcorn.forEach( self.data.events[type], function ( obj, key ) { 493 | if ( typeof obj === "function" ) { 494 | obj.call(self, event); 495 | } 496 | 497 | }); 498 | 499 | //fn.call( self, event ); 500 | 501 | }, false); 502 | } 503 | return this; 504 | }, 505 | unlisten: function( type, fn ) { 506 | 507 | if ( this.data.events[type] && this.data.events[type][fn] ) { 508 | 509 | delete this.data.events[type][ fn ]; 510 | 511 | return this; 512 | } 513 | 514 | this.data.events[type] = null; 515 | 516 | return this; 517 | } 518 | } 519 | }; 520 | 521 | // Extend Popcorn.events.fns (listen, unlisten, trigger) to all Popcorn instances 522 | Popcorn.forEach( ["trigger", "listen", "unlisten"], function ( key ) { 523 | Popcorn.p[ key ] = Popcorn.events.fn[ key ]; 524 | }); 525 | // Protected API methods 526 | Popcorn.protect = { 527 | natives: "load play pause currentTime playbackRate mute volume duration removePlugin roundTime trigger listen unlisten".toLowerCase().split(/\s+/) 528 | }; 529 | 530 | // Internal Only 531 | Popcorn.addTrackEvent = function( obj, track ) { 532 | 533 | if ( track._natives ) { 534 | // Supports user defined track event id 535 | track._id = !track.id ? Popcorn.guid( track._natives.type ) : track.id; 536 | 537 | // Push track event ids into the history 538 | obj.data.history.push( track._id ); 539 | 540 | track._natives.start = track._natives.start || Popcorn.nop; 541 | track._natives.end = track._natives.end || Popcorn.nop; 542 | } 543 | 544 | // Store this definition in an array sorted by times 545 | obj.data.trackEvents.byStart.push( track ); 546 | obj.data.trackEvents.byEnd.push( track ); 547 | //obj.data.trackEvents.byStart.sort( function( a, b ){ 548 | // return ( a.start - b.start ); 549 | //}); 550 | //obj.data.trackEvents.byEnd.sort( function( a, b ){ 551 | // return ( a.end - b.end ); 552 | //}); 553 | 554 | }; 555 | Popcorn.sortTracks = function( obj ) { 556 | obj.data.trackEvents.byStart.sort( function( a, b ){ 557 | return ( a.start - b.start ); 558 | }); 559 | obj.data.trackEvents.byEnd.sort( function( a, b ){ 560 | return ( a.end - b.end ); 561 | }); 562 | } 563 | 564 | // removePlugin( type ) removes all tracks of that from all instances of popcorn 565 | // removePlugin( obj, type ) removes all tracks of type from obj, where obj is a single instance of popcorn 566 | Popcorn.removePlugin = function( obj, name ) { 567 | 568 | // Check if we are removing plugin from an instance or from all of Popcorn 569 | if ( !name ) { 570 | 571 | // Fix the order 572 | name = obj; 573 | obj = Popcorn.p; 574 | 575 | var registryLen = Popcorn.registry.length, 576 | registryIdx; 577 | 578 | // remove plugin reference from registry 579 | for ( registryIdx = 0; registryIdx < registryLen; registryIdx++ ) { 580 | if ( Popcorn.registry[ registryIdx ].type === name ) { 581 | Popcorn.registry.splice( registryIdx, 1 ); 582 | 583 | // delete the plugin 584 | delete obj[ name ]; 585 | 586 | // plugin found and removed, stop checking, we are done 587 | return; 588 | } 589 | } 590 | 591 | } 592 | 593 | var byStart = obj.data.trackEvents.byStart, 594 | byEnd = obj.data.trackEvents.byEnd, 595 | idx, sl; 596 | 597 | // remove all trackEvents 598 | for ( idx = 0, sl = byStart.length; idx < sl; idx++ ) { 599 | 600 | if ( ( byStart[ idx ] && byStart[ idx ]._natives && byStart[ idx ]._natives.type === name ) && 601 | ( byEnd[ idx ] && byEnd[ idx ]._natives && byEnd[ idx ]._natives.type === name ) ) { 602 | 603 | byStart.splice( idx, 1 ); 604 | byEnd.splice( idx, 1 ); 605 | 606 | // update for loop if something removed, but keep checking 607 | idx--; sl--; 608 | if ( obj.data.trackEvents.startIndex <= idx ) { 609 | obj.data.trackEvents.startIndex--; 610 | obj.data.trackEvents.endIndex--; 611 | } 612 | } 613 | } 614 | }; 615 | 616 | Popcorn.removeTrackEvent = function( obj, trackId ) { 617 | 618 | var historyLen = obj.data.history.length, 619 | indexWasAt = 0, 620 | byStart = [], 621 | byEnd = [], 622 | history = []; 623 | 624 | 625 | Popcorn.forEach( obj.data.trackEvents.byStart, function( o, i, context ) { 626 | // Preserve the original start/end trackEvents 627 | if ( !o._id ) { 628 | byStart.push( obj.data.trackEvents.byStart[i] ); 629 | byEnd.push( obj.data.trackEvents.byEnd[i] ); 630 | } 631 | 632 | // Filter for user track events (vs system track events) 633 | if ( o._id ) { 634 | 635 | // Filter for the trackevent to remove 636 | if ( o._id !== trackId ) { 637 | byStart.push( obj.data.trackEvents.byStart[i] ); 638 | byEnd.push( obj.data.trackEvents.byEnd[i] ); 639 | } 640 | 641 | // Capture the position of the track being removed. 642 | if ( o._id === trackId ) { 643 | indexWasAt = i; 644 | } 645 | } 646 | }); 647 | 648 | 649 | // Update 650 | if ( indexWasAt <= obj.data.trackEvents.startIndex ) { 651 | obj.data.trackEvents.startIndex--; 652 | } 653 | 654 | if ( indexWasAt <= obj.data.trackEvents.endIndex ) { 655 | obj.data.trackEvents.endIndex--; 656 | } 657 | 658 | 659 | obj.data.trackEvents.byStart = byStart; 660 | obj.data.trackEvents.byEnd = byEnd; 661 | 662 | 663 | for ( var i = 0; i < historyLen; i++ ) { 664 | if ( obj.data.history[i] !== trackId ) { 665 | history.push( obj.data.history[i] ); 666 | } 667 | } 668 | 669 | obj.data.history = history; 670 | 671 | }; 672 | 673 | Popcorn.getTrackEvents = function( obj ) { 674 | 675 | var trackevents = []; 676 | 677 | Popcorn.forEach( obj.data.trackEvents.byStart, function( o, i, context ) { 678 | if ( o._id ) { 679 | trackevents.push(o); 680 | } 681 | }); 682 | 683 | return trackevents; 684 | }; 685 | 686 | 687 | Popcorn.getLastTrackEventId = function( obj ) { 688 | return obj.data.history[ obj.data.history.length - 1 ]; 689 | }; 690 | 691 | // Map and Extend TrackEvent functions to all Popcorn instances 692 | Popcorn.extend( Popcorn.p, { 693 | 694 | getTrackEvents: function() { 695 | return Popcorn.getTrackEvents.call( null, this ); 696 | }, 697 | 698 | getLastTrackEventId: function() { 699 | return Popcorn.getLastTrackEventId.call( null, this ); 700 | }, 701 | 702 | removeTrackEvent: function( id ) { 703 | Popcorn.removeTrackEvent.call( null, this, id ); 704 | return this; 705 | }, 706 | 707 | removePlugin: function( name ) { 708 | Popcorn.removePlugin.call( null, this, name ); 709 | return this; 710 | } 711 | 712 | }); 713 | 714 | // Plugin manifests 715 | Popcorn.manifest = {}; 716 | // Plugins are registered 717 | Popcorn.registry = []; 718 | // An interface for extending Popcorn 719 | // with plugin functionality 720 | Popcorn.plugin = function( name, definition, manifest ) { 721 | 722 | if ( Popcorn.protect.natives.indexOf( name.toLowerCase() ) >= 0 ) { 723 | Popcorn.error("'" + name + "' is a protected function name"); 724 | return; 725 | } 726 | 727 | // Provides some sugar, but ultimately extends 728 | // the definition into Popcorn.p 729 | var reserved = [ "start", "end" ], 730 | plugin = {}, 731 | setup, 732 | isfn = typeof definition === "function"; 733 | 734 | // If `manifest` arg is undefined, check for manifest within the `definition` object 735 | // If no `definition.manifest`, an empty object is a sufficient fallback 736 | if ( !manifest ) { 737 | manifest = definition.manifest || {}; 738 | } 739 | 740 | var pluginFn = function( setup, options ) { 741 | 742 | if ( !options ) { 743 | return this; 744 | } 745 | 746 | // Storing the plugin natives 747 | options._natives = setup; 748 | options._natives.type = name; 749 | options._running = false; 750 | 751 | // Ensure a manifest object, an empty object is a sufficient fallback 752 | options._natives.manifest = manifest; 753 | 754 | // Checks for expected properties 755 | if ( !( "start" in options ) ) { 756 | options.start = 0; 757 | } 758 | 759 | if ( !( "end" in options ) ) { 760 | options.end = this.duration(); 761 | } 762 | 763 | // If a _setup was declared, then call it before 764 | // the events commence 765 | if ( "_setup" in setup && typeof setup._setup === "function" ) { 766 | 767 | // Resolves 239, 241, 242 768 | if ( !options.target ) { 769 | 770 | // Sometimes the manifest may be missing entirely 771 | // or it has an options object that doesn't have a `target` property 772 | 773 | var manifestopts = "options" in manifest && manifest.options; 774 | 775 | options.target = manifestopts && "target" in manifestopts && manifestopts.target; 776 | } 777 | 778 | setup._setup.call( this, options ); 779 | } 780 | 781 | Popcorn.addTrackEvent( this, options ); 782 | 783 | // Future support for plugin event definitions 784 | // for all of the native events 785 | Popcorn.forEach( setup, function ( callback, type ) { 786 | 787 | if ( type !== "type" ) { 788 | 789 | if ( reserved.indexOf( type ) === -1 ) { 790 | 791 | this.listen( type, callback ); 792 | } 793 | } 794 | 795 | }, this); 796 | 797 | return this; 798 | }; 799 | 800 | // Augment the manifest object 801 | if ( manifest || ( "manifest" in definition ) ) { 802 | Popcorn.manifest[ name ] = manifest || definition.manifest; 803 | } 804 | 805 | // Assign new named definition 806 | plugin[ name ] = function( options ) { 807 | return pluginFn.call( this, isfn ? definition.call( this, options ) : definition, 808 | options ); 809 | }; 810 | 811 | // Extend Popcorn.p with new named definition 812 | Popcorn.extend( Popcorn.p, plugin ); 813 | 814 | // Push into the registry 815 | Popcorn.registry.push( 816 | Popcorn.extend( plugin, { 817 | type: name 818 | }) 819 | ); 820 | 821 | return plugin; 822 | }; 823 | 824 | // stores parsers keyed on filetype 825 | Popcorn.parsers = {}; 826 | 827 | // An interface for extending Popcorn 828 | // with parser functionality 829 | Popcorn.parser = function( name, type, definition ) { 830 | 831 | if ( Popcorn.protect.natives.indexOf( name.toLowerCase() ) >= 0 ) { 832 | Popcorn.error("'" + name + "' is a protected function name"); 833 | return; 834 | } 835 | 836 | // fixes parameters for overloaded function call 837 | if ( typeof type === "function" && !definition ) { 838 | definition = type; 839 | type = ""; 840 | } 841 | 842 | if ( typeof definition !== "function" || typeof type !== "string" ) { 843 | return; 844 | } 845 | 846 | // Provides some sugar, but ultimately extends 847 | // the definition into Popcorn.p 848 | 849 | var natives = Popcorn.events.all, 850 | parseFn, 851 | parser = {}; 852 | 853 | parseFn = function ( filename, callback ) { 854 | 855 | if ( !filename ) { 856 | return this; 857 | } 858 | 859 | var that = this; 860 | 861 | Popcorn.xhr({ 862 | url: filename, 863 | dataType: type, 864 | success: function( data ) { 865 | 866 | var tracksObject = definition( data ), 867 | tracksData, 868 | tracksDataLen, 869 | tracksDef, 870 | idx = 0; 871 | 872 | tracksData = tracksObject.data || []; 873 | tracksDataLen = tracksData.length; 874 | tracksDef = null; 875 | 876 | // If no tracks to process, return immediately 877 | if ( !tracksDataLen ) { 878 | return; 879 | } 880 | 881 | // Create tracks out of parsed object 882 | for ( ; idx < tracksDataLen; idx++ ) { 883 | 884 | tracksDef = tracksData[ idx ]; 885 | 886 | for ( var key in tracksDef ) { 887 | 888 | if ( hasOwn.call( tracksDef, key ) && !!that[ key ] ) { 889 | 890 | that[ key ]( tracksDef[ key ] ); 891 | } 892 | } 893 | } 894 | if ( callback ) { 895 | callback(); 896 | } 897 | } 898 | }); 899 | 900 | return this; 901 | }; 902 | 903 | // Assign new named definition 904 | parser[ name ] = parseFn; 905 | 906 | // Extend Popcorn.p with new named definition 907 | Popcorn.extend( Popcorn.p, parser ); 908 | 909 | // keys the function name by filetype extension 910 | //Popcorn.parsers[ name ] = true; 911 | 912 | return parser; 913 | }; 914 | 915 | 916 | // Cache references to reused RegExps 917 | var rparams = /\?/, 918 | // XHR Setup object 919 | setup = { 920 | url: '', 921 | data: '', 922 | dataType: '', 923 | success: Popcorn.nop, 924 | type: 'GET', 925 | async: true, 926 | xhr: function() { 927 | return new global.XMLHttpRequest(); 928 | } 929 | }; 930 | 931 | Popcorn.xhr = function ( options ) { 932 | 933 | if ( options.dataType && 934 | ( options.dataType.toLowerCase() === "jsonp" || 935 | options.dataType.toLowerCase() === "script" ) ) { 936 | 937 | Popcorn.xhr.getJSONP( 938 | options.url, 939 | options.success, 940 | options.dataType.toLowerCase() === "script" 941 | ); 942 | return; 943 | } 944 | 945 | var settings = Popcorn.extend( {}, setup, options ); 946 | 947 | // Create new XMLHttpRequest object 948 | settings.ajax = settings.xhr(); 949 | 950 | // Normalize dataType 951 | settings.dataType = settings.dataType.toLowerCase(); 952 | 953 | 954 | if ( settings.ajax ) { 955 | 956 | if ( settings.type === "GET" && settings.data ) { 957 | 958 | // append query string 959 | settings.url += ( rparams.test( settings.url ) ? "&" : "?" ) + settings.data; 960 | 961 | // Garbage collect and reset settings.data 962 | settings.data = null; 963 | } 964 | 965 | 966 | settings.ajax.open( settings.type, settings.url, settings.async ); 967 | settings.ajax.send( settings.data || null ); 968 | 969 | return Popcorn.xhr.httpData( settings ); 970 | } 971 | }; 972 | 973 | 974 | Popcorn.xhr.httpData = function ( settings ) { 975 | 976 | var data, json = null; 977 | 978 | settings.ajax.onreadystatechange = function() { 979 | 980 | if ( settings.ajax.readyState === 4 ) { 981 | 982 | try { 983 | json = JSON.parse(settings.ajax.responseText); 984 | } catch(e) { 985 | //suppress 986 | } 987 | 988 | data = { 989 | xml: settings.ajax.responseXML, 990 | text: settings.ajax.responseText, 991 | json: json 992 | }; 993 | 994 | // If a dataType was specified, return that type of data 995 | if ( settings.dataType ) { 996 | data = data[ settings.dataType ]; 997 | } 998 | 999 | 1000 | settings.success.call( settings.ajax, data ); 1001 | 1002 | } 1003 | }; 1004 | return data; 1005 | }; 1006 | 1007 | Popcorn.xhr.getJSONP = function ( url, success, isScript ) { 1008 | 1009 | // If this is a script request, ensure that we do not call something that has already been loaded 1010 | if ( isScript ) { 1011 | 1012 | var scripts = document.querySelectorAll('script[src="' + url + '"]'); 1013 | 1014 | // If there are scripts with this url loaded, early return 1015 | if ( scripts.length ) { 1016 | 1017 | // Execute success callback and pass "exists" flag 1018 | success && success( true ); 1019 | 1020 | return; 1021 | } 1022 | } 1023 | 1024 | var head = document.head || document.getElementsByTagName("head")[0] || document.documentElement, 1025 | script = document.createElement("script"), 1026 | paramStr = url.split("?")[1], 1027 | isFired = false, 1028 | params = [], 1029 | callback, parts, callparam; 1030 | 1031 | if ( paramStr && !isScript ) { 1032 | params = paramStr.split("&"); 1033 | } 1034 | 1035 | if ( params.length ) { 1036 | parts = params[ params.length - 1 ].split("="); 1037 | } 1038 | 1039 | callback = params.length ? ( parts[1] ? parts[1] : parts[0] ) : "jsonp"; 1040 | 1041 | if ( !paramStr && !isScript ) { 1042 | url += "?callback=" + callback; 1043 | } 1044 | 1045 | if ( callback && !isScript ) { 1046 | 1047 | // If a callback name already exists 1048 | if ( !!window[ callback ] ) { 1049 | 1050 | // Create a new unique callback name 1051 | callback = Popcorn.guid( callback ); 1052 | } 1053 | 1054 | // Define the JSONP success callback globally 1055 | window[ callback ] = function ( data ) { 1056 | 1057 | success && success( data ); 1058 | isFired = true; 1059 | 1060 | }; 1061 | 1062 | // Replace callback param and callback name 1063 | url = url.replace( parts.join("="), parts[0] + "=" + callback ); 1064 | 1065 | } 1066 | 1067 | script.onload = script.onreadystatechange = function() { 1068 | 1069 | if ( !script.readyState || /loaded|complete/.test( script.readyState ) ) { 1070 | 1071 | // Handling remote script loading callbacks 1072 | if ( isScript ) { 1073 | 1074 | // getScript 1075 | success && success(); 1076 | } 1077 | 1078 | // Executing for JSONP requests 1079 | if ( isFired ) { 1080 | 1081 | // Garbage collect the callback 1082 | delete window[ callback ]; 1083 | 1084 | // Garbage collect the script resource 1085 | head.removeChild( script ); 1086 | } 1087 | } 1088 | }; 1089 | 1090 | script.src = url; 1091 | 1092 | head.insertBefore( script, head.firstChild ); 1093 | 1094 | return; 1095 | }; 1096 | 1097 | Popcorn.getJSONP = Popcorn.xhr.getJSONP; 1098 | 1099 | Popcorn.getScript = Popcorn.xhr.getScript = function( url, success ) { 1100 | 1101 | return Popcorn.xhr.getJSONP( url, success, true ); 1102 | }; 1103 | 1104 | 1105 | // Exposes Popcorn to global context 1106 | global.Popcorn = Popcorn; 1107 | 1108 | document.addEventListener( "DOMContentLoaded", function () { 1109 | 1110 | var videos = document.getElementsByTagName( "video" ); 1111 | 1112 | Popcorn.forEach( videos, function ( iter, key ) { 1113 | 1114 | var video = videos[ key ], 1115 | hasDataSources = false, 1116 | dataSources, data, popcornVideo; 1117 | 1118 | // Ensure that the DOM has an id 1119 | if ( !video.id ) { 1120 | 1121 | video.id = Popcorn.guid( "__popcorn" ); 1122 | 1123 | } 1124 | 1125 | // Ensure we're looking at a dom node 1126 | if ( video.nodeType && video.nodeType === 1 ) { 1127 | 1128 | popcornVideo = Popcorn( "#" + video.id ); 1129 | 1130 | dataSources = ( video.getAttribute( "data-timeline-sources" ) || "" ).split(","); 1131 | 1132 | if ( dataSources[ 0 ] ) { 1133 | 1134 | Popcorn.forEach( dataSources, function ( source ) { 1135 | 1136 | // split the parser and data as parser:file 1137 | data = source.split( ":" ); 1138 | 1139 | // if no parser is defined for the file, assume "parse" + file extension 1140 | if ( data.length === 1 ) { 1141 | 1142 | data = source.split( "." ); 1143 | data[ 0 ] = "parse" + data[ data.length - 1 ].toUpperCase(); 1144 | data[ 1 ] = source; 1145 | 1146 | } 1147 | 1148 | // If the video has data sources and the correct parser is registered, continue to load 1149 | if ( dataSources[ 0 ] && popcornVideo[ data[ 0 ] ] ) { 1150 | 1151 | // Set up the video and load in the datasources 1152 | popcornVideo[ data[ 0 ] ]( data[ 1 ] ); 1153 | 1154 | } 1155 | }); 1156 | 1157 | } 1158 | 1159 | // Only play the video if it was specified to do so 1160 | if ( !!popcornVideo.autoplay ) { 1161 | popcornVideo.play(); 1162 | } 1163 | 1164 | } 1165 | }); 1166 | }, false ); 1167 | 1168 | })(window, window.document); 1169 | 1170 | -------------------------------------------------------------------------------- /js/libs/popcorn.scComments.js: -------------------------------------------------------------------------------- 1 | // PLUGIN: Subtitle 2 | 3 | (function (Popcorn) { 4 | /** 5 | * ScComments popcorn plug-in 6 | * Retrieves comments from soundcloud for a media id and api key 7 | * Listen for scCommentIn and scCommentOut events to act. Acts as 8 | * a composite pattern for plugins (scComments and scComment) 9 | * 10 | * @param {Object} options 11 | * 12 | * Example: 13 | var p = Popcorn('#video') 14 | .scComments({ 15 | apikey: '1234567890abcdef', // mandatory, a soundcloud api key 16 | mediaid: 1234, // mandatory, the id of a media 17 | } ) 18 | * 19 | */ 20 | 21 | // A single comment 22 | Popcorn.plugin( "scComment" , (function() { 23 | return { 24 | _setup: function( options ) { }, 25 | /** 26 | * @member scComments 27 | * The start function will be executed when the currentTime 28 | * of the video reaches the start time provided by the 29 | * options variable 30 | */ 31 | start: function(event, options){ 32 | this.trigger( "scCommentIn", options.comment ); 33 | }, 34 | /** 35 | * @member scComments 36 | * The end function will be executed when the currentTime 37 | * of the video reaches the end time provided by the 38 | * options variable 39 | */ 40 | end: function(event, options){ 41 | this.trigger( "scCommentOut", options.comment ); 42 | } 43 | } 44 | })()); 45 | 46 | // A composite of comments 47 | Popcorn.plugin( "scComments" , (function() { 48 | var comments = []; 49 | 50 | return { 51 | manifest: { 52 | about:{ 53 | name: "Popcorn scComments Plugin", 54 | version: "0.5", 55 | author: "Steven Weerdenburg", 56 | website: "http://sweerdenburg.wordpress.com/" 57 | }, 58 | options:{ 59 | start : {elem:'input', type:'text', label:'In'}, 60 | end : {elem:'input', type:'text', label:'Out'}, 61 | target : 'Subtitle-container', 62 | apikey :{elem:'input', type:'text', label:'In'}, 63 | mediaid :{elem:'input', type:'text', label:'In'}, 64 | limit : {elem:'input', type:'text', label:'In'} 65 | } 66 | }, 67 | 68 | //tracks: {}, 69 | 70 | _setup: function( options ) { 71 | if ( !options || !options.mediaid ) { 72 | throw "Must supply a media id!"; 73 | } else if ( !options.apikey ) { 74 | throw "Must supply an api key!" 75 | } 76 | 77 | // Expose a list of tracks 78 | this.scComments.tracks = this.scComments.tracks || {}; 79 | this.scComments.tracks[options.mediaid] = {}; 80 | 81 | // Default them to 0 and undef. Undef will become media duration 82 | options.start = 0; 83 | options.end = 0; // MJP: Bug in Popcorn, the default duration() NaN is not being corrected. 84 | 85 | var pop = this, 86 | curl = 'http://api.soundcloud.com/tracks/' + options.mediaid + '.js?client_id=' + options.apikey + "&callback=jsonp"; 87 | 88 | // MJP: The xhr() success(data): 89 | // MJP: - The JSONP type xhr() has the json object in data 90 | // MJP: - The JSON type xhr() has the json object in data.json 91 | 92 | Popcorn.xhr({ 93 | url: curl, 94 | dataType: "jsonp", 95 | success: function( data ) { 96 | options.waveform = pop.scComments.tracks[options.mediaid].waveform = data.waveform_url; 97 | // pop.trigger( 'scLoadedmetadata', data ); 98 | } 99 | }); 100 | 101 | Popcorn.xhr({ 102 | url: 'https://api.soundcloud.com/tracks/' + options.mediaid + '/comments.json?client_id=' + options.apikey + "&limit="+ options.limit + "&callback=jsonp", 103 | dataType: "jsonp", 104 | success: function( data ) { 105 | var x, y, len, tmp; 106 | 107 | if ( data.error ) { 108 | throw error; 109 | } 110 | 111 | 112 | 113 | Popcorn.forEach( data, function ( obj ) { 114 | var comment = { 115 | start: obj.timestamp/1000, 116 | date: new Date( obj.created_at ), 117 | text: obj.body, 118 | user: { 119 | name: obj.user.username, 120 | profile: obj.user.permalink_url, 121 | avatar: obj.user.avatar_url 122 | } 123 | }; 124 | 125 | comments.push( comment ); 126 | }); 127 | 128 | // MB exposing the comments - am I doing it right? 129 | options.comments = pop.scComments.tracks[options.mediaid].comments = comments; 130 | 131 | // Sort comments by start time 132 | for(len = comments.length; len > 1; len--) { 133 | for(y = 0; y < len - 1; y++) { 134 | if(comments[y].start > comments[y+1].start) { 135 | tmp = comments[y+1]; 136 | comments[y+1] = comments[y]; 137 | comments[y] = tmp; 138 | } 139 | } 140 | } 141 | 142 | // for(x = 0, len = comments.length; x < len - 1; x++) { 143 | for(x = 0, len = comments.length; x < len; x++) { // MJP: Removed the minus 1 144 | tmp = comments[x]; 145 | // Infer end time 146 | // tmp.end = (comments[x+1].start-0.001) || Number.MAX_VALUE; 147 | tmp.end = comments[x+1] && (comments[x+1].start-0.001) || Number.MAX_VALUE; // MJP: Removed error on last comment 148 | 149 | pop.scComment({ 150 | start: tmp.start, 151 | end: tmp.end, 152 | target: '', 153 | comment: tmp 154 | }); 155 | } 156 | pop.trigger( 'scLoadedmetadata', data ); // MJP: Moved here so event is useful for comments, 157 | } 158 | }); 159 | }, 160 | /** 161 | * @member scComments 162 | * The start function will be executed when the currentTime 163 | * of the video reaches the start time provided by the 164 | * options variable 165 | */ 166 | start: function(event, options){ 167 | }, 168 | /** 169 | * @member scComments 170 | * The end function will be executed when the currentTime 171 | * of the video reaches the end time provided by the 172 | * options variable 173 | */ 174 | end: function(event, options){ 175 | } 176 | 177 | } 178 | })()); 179 | 180 | })( Popcorn ); -------------------------------------------------------------------------------- /js/libs/popcorn.transcript.js: -------------------------------------------------------------------------------- 1 | // PLUGIN: Transcript 2 | 3 | (function (Popcorn) { 4 | 5 | /** 6 | * Transcript popcorn plug-in 7 | * Displays a transcript in the target div or DOM node. 8 | * Options parameter will need a time and a target. 9 | * Optional parameters are futureClass. 10 | * 11 | * Time is the time that you want this plug-in to execute, 12 | * Target is the id of the document element that the content refers 13 | * to, or the DoM node itself. This target element must exist on the DOM 14 | * futureClass is the CSS class name to be used when the target has not been read yet. 15 | * 16 | * 17 | * @param {Object} options 18 | * 19 | * Example: 20 | var p = Popcorn('#video') 21 | .transcript({ 22 | time: 5, // seconds, mandatory 23 | target: 'word-42', // mandatory 24 | futureClass: 'transcript-hide' // optional 25 | } ) 26 | .transcript({ 27 | time: 32, // seconds, mandatory 28 | target: document.getElementById( 'word-84' ), // mandatory 29 | futureClass: 'transcript-grey' // optional 30 | } ) 31 | * 32 | */ 33 | 34 | Popcorn.plugin( "transcript" , { 35 | 36 | manifest: { 37 | about:{ 38 | name: "Popcorn Transcript Plugin", 39 | version: "0.1", 40 | author: "Mark Panaghiston", 41 | website: "http://www.jplayer.org/" 42 | }, 43 | options:{ 44 | time : {elem:'input', type:'text', label:'In'}, 45 | target : 'Transcript-container', 46 | futureClass : {elem:'input', type:'text', label:'Class'} 47 | } 48 | }, 49 | 50 | _setup: function( options ) { 51 | 52 | // if a target is specified and is a string, use that - Requires every word to have a unique ID. 53 | // else if target is specified and is an object, use object as DOM reference 54 | // else Throw an error. 55 | if ( options.target && typeof options.target === "string" && options.target !== 'Transcript-container' ) { 56 | options.container = document.getElementById( options.target ); 57 | } else if ( options.target && typeof options.target === "object" ) { 58 | options.container = options.target; 59 | } else { 60 | throw "Popcorn.transcript: target property must be an ID string or a pointer to the DOM of the transcript word."; 61 | } 62 | 63 | options.start = 0; 64 | options.end = options.time; 65 | 66 | if(!options.futureClass) { 67 | options.futureClass = "transcript-future" 68 | } 69 | 70 | options.transcriptRead = function() { 71 | if( options.container.classList ) { 72 | options.container.classList.remove(options.futureClass); 73 | } else { 74 | options.container.className = ""; 75 | } 76 | }; 77 | 78 | options.transcriptFuture = function() { 79 | if( options.container.classList ) { 80 | options.container.classList.add(options.futureClass); 81 | } else { 82 | options.container.className = options.futureClass; 83 | } 84 | }; 85 | 86 | // Note: end times close to zero can have issues. (Firefox 4.0 worked with 100ms. Chrome needed 200ms. iOS needed 500ms) 87 | if(options.end > options.start) { 88 | options.transcriptFuture(); 89 | } 90 | 91 | }, 92 | /** 93 | * @member transcript 94 | * The start function will be executed when the currentTime 95 | * of the video reaches the start time provided by the 96 | * options variable 97 | */ 98 | start: function(event, options){ 99 | options.transcriptFuture(); 100 | }, 101 | /** 102 | * @member transcript 103 | * The end function will be executed when the currentTime 104 | * of the video reaches the end time provided by the 105 | * options variable 106 | */ 107 | 108 | end: function(event, options){ 109 | options.transcriptRead(); 110 | } 111 | 112 | } ); 113 | 114 | })( Popcorn ); 115 | -------------------------------------------------------------------------------- /js/libs/popcorn.wordriver.js: -------------------------------------------------------------------------------- 1 | // PLUGIN: Wordriver 2 | 3 | (function (Popcorn) { 4 | 5 | var container = {}, 6 | spanLocation = 0, 7 | knownSpeaker = false, 8 | knownIndex = 0, 9 | unknownIndex = 0, 10 | setupContainer = function( target ) { 11 | 12 | container[ target ] = document.createElement( "div" ); 13 | document.getElementById( target ).appendChild( container[ target ] ); 14 | 15 | container[ target ].style.height = "100%"; 16 | container[ target ].style.position = "relative"; 17 | container[ target ].style.overflow = "hidden"; 18 | 19 | return container[ target ]; 20 | }; 21 | 22 | Popcorn.plugin( "wordriver" , { 23 | 24 | manifest: {}, 25 | 26 | _setup: function( options ) { 27 | 28 | var newSpeaker = options.text.indexOf(":"); 29 | var newBracket = options.text.indexOf("["); 30 | var text = options.text; 31 | 32 | if( newSpeaker >= 0 ) { 33 | knownSpeaker = false; 34 | for( var i in options.colors.known ) { 35 | for( var j in options.colors.known[ i ].name ) { 36 | if( options.text.toLowerCase().indexOf( options.colors.known[ i ].name[ j ].toLowerCase() ) >= 0 ) { 37 | knownSpeaker = true; 38 | knownIndex = i; 39 | break; 40 | } 41 | } 42 | if( knownSpeaker ) break; 43 | } 44 | } 45 | 46 | if( !knownSpeaker && newSpeaker >= 0 ) { 47 | unknownIndex = (unknownIndex + 1 < options.colors.unknown.length) ? unknownIndex + 1 : 0; 48 | } 49 | 50 | if( newSpeaker >= 0 ) { 51 | text = options.text.substr( newSpeaker + 1 ); 52 | } 53 | 54 | if( newBracket >= 0 ) { 55 | text = ""; 56 | } 57 | 58 | options._duration = options.end - options.start; 59 | options._container = container[ options.target ] || setupContainer( options.target ); 60 | 61 | options.opacity.duration = options.opacity.duration !== undefined ? options.opacity.duration : 1; 62 | 63 | options.word = document.createElement( "span" ); 64 | options.word.style.position = "absolute"; 65 | 66 | options.word.style.whiteSpace = "nowrap"; 67 | options.word.style.opacity = options.opacity.start; 68 | 69 | options.word.style.MozTransitionProperty = "opacity, -moz-transform"; 70 | options.word.style.webkitTransitionProperty = "opacity, -webkit-transform"; 71 | options.word.style.OTransitionProperty = "opacity, -o-transform"; 72 | options.word.style.transitionProperty = "opacity, transform"; 73 | 74 | options.word.style.MozTransitionDuration = 75 | options.word.style.webkitTransitionDuration = 76 | options.word.style.OTransitionDuration = 77 | options.word.style.transitionDuration = options.opacity.duration + "s, " + options._duration + "s"; 78 | 79 | options.word.style.MozTransitionTimingFunction = 80 | options.word.style.webkitTransitionTimingFunction = 81 | options.word.style.OTransitionTimingFunction = 82 | options.word.style.transitionTimingFunction = "linear"; 83 | 84 | options.word.innerHTML = text; 85 | 86 | if( knownSpeaker ) { 87 | if( options.word.classList ) { 88 | options.word.classList.add( options.colors.known[ knownIndex ].className ); 89 | } else { 90 | options.word.className = options.colors.known[ knownIndex ].className; 91 | } 92 | } else { 93 | if( options.word.classList ) { 94 | options.word.classList.add( options.colors.unknown[ unknownIndex ] ); 95 | } else { 96 | options.word.className = options.colors.unknown[ unknownIndex ]; 97 | } 98 | } 99 | }, 100 | start: function( event, options ){ 101 | 102 | options._container.appendChild( options.word ); 103 | 104 | // Resets the transform when changing to a new currentTime before the end event occurred. 105 | options.word.style.MozTransform = 106 | options.word.style.webkitTransform = 107 | options.word.style.OTransform = 108 | options.word.style.transform = ""; 109 | 110 | /* 111 | // Helps reduce the bunch of words appearing when changing to a new currentTime. Otherwise all words in the period between start and end time are put in the river. 112 | // Problem with this solution is that browser lag can stop words from appearing in the river. i.e., If the browser freezes up for a moment. 113 | if(this.currentTime() - options.start > 1) { 114 | options.word.style.opacity = 0; 115 | return; 116 | } 117 | */ 118 | 119 | options.word.style.fontSize = ~~( 30 + 20 * Math.random() ) + "px"; 120 | spanLocation = spanLocation % ( options._container.offsetWidth - options.word.offsetWidth ); 121 | options.word.style.left = spanLocation + "px"; 122 | spanLocation += options.word.offsetWidth + 10; 123 | 124 | var height = window.getComputedStyle(options._container.parentNode, null).getPropertyValue("height"); 125 | 126 | options.word.style.MozTransform = 127 | options.word.style.webkitTransform = 128 | options.word.style.OTransform = 129 | options.word.style.transform = "translateY(" + height + ")"; 130 | 131 | if( options.middle !== undefined && options.start !== options.middle ) { 132 | options.word.style.opacity = options.opacity.middle; 133 | 134 | setTimeout( function() { 135 | options.word.style.opacity = options.opacity.end; 136 | }, (options.middle - options.start) * 1000 ); 137 | } else { 138 | options.word.style.opacity = options.opacity.end; 139 | } 140 | 141 | }, 142 | end: function( event, options ){ 143 | 144 | options._container.removeChild( options.word ); 145 | options.word.style.opacity = options.opacity.start; 146 | 147 | options.word.style.MozTransform = 148 | options.word.style.webkitTransform = 149 | options.word.style.OTransform = 150 | options.word.style.transform = ""; 151 | } 152 | }); 153 | 154 | })( Popcorn ); 155 | --------------------------------------------------------------------------------