1 // ========================================================================== 2 // Project: SproutCore - JavaScript Application Framework 3 // Copyright: ©2006-2011 Strobe Inc. and contributors. 4 // Portions ©2008-2011 Apple Inc. All rights reserved. 5 // License: Licensed under MIT license (see license.js) 6 // ========================================================================== 7 8 sc_require('views/controls'); 9 sc_require('views/mini_controls'); 10 sc_require('media_capabilities'); 11 12 /** 13 @class 14 15 Renders a audioView using different technologies like HTML5 audio tag, 16 quicktime and flash. 17 18 This view wraps the different technologies so you can use one standard and 19 simple API to play audio. 20 21 You can specify and array with the order of how the technologies will degrade 22 depending on availability. For example you can set degradeList to be 23 ['html5', 'flash'] and it will load your audio in an audio tag if the 24 technology is available otherwise flash and if neither of the technologies 25 are available it will show a message saying that your machine needs to install 26 one of this technologies. 27 28 @extends SC.View 29 @since SproutCore 1.1 30 */ 31 32 SC.AudioView = SC.View.extend( 33 /** @scope SC.AudioView.prototype */{ 34 35 /** 36 Audio view className. 37 @type String 38 */ 39 classNames: ['sc-audio-view'], 40 41 /** 42 Properties that trigger a re render of the view. If the value changes, it 43 means that the audio url changed. 44 45 @type Array 46 */ 47 displayProperties: ['value'], 48 49 /** 50 Reference to the audio object once is created. 51 @type Object 52 */ 53 audioObject: null, 54 55 /** 56 Array containing the technologies and the order to load them depending 57 availability 58 59 @type Array 60 */ 61 degradeList: ['html5','quicktime', 'flash'], 62 63 /** 64 The current play time in seconds. 65 66 @type Number 67 @default 0 68 */ 69 currentTime : function(key, value) { 70 /* jshint eqnull:true */ 71 if (value != null) { 72 this.seek(value); 73 } 74 75 return value == null ? 0 : value; 76 }.property().cacheable(), 77 78 /** 79 Duration in secs 80 @type Number 81 */ 82 duration: 0, //audio duration in secs 83 84 /** 85 Volume. The value should be between 0 and 1 86 @type Number 87 */ 88 volume:0, //volume value from 0 to 1 89 90 /** 91 Tells you if the audio is paused or not. 92 @type Boolean 93 */ 94 paused: YES, //is the audio paused 95 96 /** 97 Tells you if the audio is loaded. 98 @type Boolean 99 */ 100 101 loaded: NO, //has the audio loaded 102 103 /** 104 Indicates if the audio has reached the end 105 @type Boolean 106 */ 107 108 ended: NO, //did the audio finish playing 109 110 /** 111 Indicates if the audio is ready to be played. 112 @type Boolean 113 */ 114 115 canPlay: NO, //can the audio be played 116 117 loadedTimeRanges:[], //loaded bits 118 119 /** 120 Formatted currentTime. (00:00) 121 @type String 122 */ 123 time: function(){ 124 var currentTime=this.get('currentTime'), 125 totaltimeInSecs = this.get('duration'); 126 var formattedTime = this._addZeros(Math.floor(currentTime/60))+':'+this._addZeros(Math.floor(currentTime%60))+"/"+this._addZeros(Math.floor(totaltimeInSecs/60))+':'+this._addZeros(Math.floor(totaltimeInSecs%60)); 127 return formattedTime; 128 }.property('currentTime', 'duration').cacheable(), 129 130 /** 131 Renders the appropriate HTML according for the technology to use. 132 133 @param {SC.RenderContext} context the render context 134 @param {Boolean} firstTime YES if this is creating a layer 135 @returns {void} 136 */ 137 render: function(context, firstTime) { 138 var i, j, listLen, pluginsLen, id = SC.guidFor(this); 139 140 if(firstTime){ 141 for(i=0, listLen = this.degradeList.length; i<listLen; i++){ 142 switch(this.degradeList[i]){ 143 case "html5": 144 if(!SC.mediaCapabilities.get('isHTML5AudioSupported')) 145 { 146 break; 147 } 148 context.push('<audio src="'+this.get('value')+'"'); 149 if(this.poster){ 150 context.push(' poster="'+this.poster+'"'); 151 } 152 context.push('/>'); 153 this.loaded='html5'; 154 return; 155 case "quicktime": 156 if(!SC.mediaCapabilities.get('isQuicktimeSupported')) 157 { 158 break; 159 } 160 // TODO: this doesn't seem like the best way to determine what tags to use! 161 if(SC.browser.name === SC.BROWSER.ie){ 162 context.push('<object id="qt_event_source" '+ 163 'classid="clsid:CB927D12-4FF7-4a9e-A169-56E4B8A75598" '+ 164 'codebase="http://www.apple.com/qtactivex/qtplugin.cab#version=7,2,1,0"> '+ 165 '</object> '); 166 } 167 context.push('<object width="100%" height="100%"'); 168 if(SC.browser.name === SC.BROWSER.ie){ 169 context.push('style="behavior:url(#qt_event_source);"'); 170 } 171 context.push('classid="clsid:02BF25D5-8C17-4B23-BC80-D3488ABDDC6B" '+ 172 'id="qt_'+id+'" '+ 173 'codebase="http://www.apple.com/qtactivex/qtplugin.cab">'+ 174 '<param name="src" value="'+this.get('value')+'"/>'+ 175 '<param name="autoplay" value="false"/>'+ 176 '<param name="loop" value="false"/>'+ 177 '<param name="controller" value="false"/>'+ 178 '<param name="postdomevents" value="true"/>'+ 179 '<param name="kioskmode" value="true"/>'+ 180 '<param name="bgcolor" value="000000"/>'+ 181 '<param name="scale" value="aspect"/>'+ 182 '<embed width="100%" height="100%" '+ 183 'name="qt_'+id+'" '+ 184 'src="'+this.get('value')+'" '+ 185 'autostart="false" '+ 186 'EnableJavaScript="true" '+ 187 'postdomevents="true" '+ 188 'kioskmode="true" '+ 189 'controller="false" '+ 190 'bgcolor="000000"'+ 191 'scale="aspect" '+ 192 'pluginspage="www.apple.com/quicktime/download">'+ 193 '</embed></object>'+ 194 '</object>'); 195 this.loaded='quicktime'; 196 return; 197 case "flash": 198 if(!SC.mediaCapabilities.get('isFlashSupported')) 199 { 200 break; 201 } 202 var flashURL= sc_static('videoCanvas.swf'); 203 204 var movieURL = this.get('value'); 205 if (!movieURL) return; 206 207 if(movieURL.indexOf('http:')==-1){ 208 movieURL=location.protocol+'//'+location.host+movieURL; 209 } 210 if(movieURL.indexOf('?')!=-1){ 211 movieURL=movieURL.substring(0, movieURL.indexOf('?')); 212 } 213 movieURL = encodeURI(movieURL); 214 context.push('<object classid="clsid:d27cdb6e-ae6d-11cf-96b8-444553540000" '+ 215 'codebase="http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=8,0,0,0" '+ 216 'width="100%" '+ 217 'height="100%" '+ 218 'id="flash_'+id+'" '+ 219 'align="middle">'+ 220 '<param name="allowScriptAccess" value="sameDomain" />'+ 221 '<param name="allowFullScreen" value="true" />'+ 222 '<param name="movie" value="'+flashURL+'&src='+movieURL+'&scid='+id+'" />'+ 223 '<param name="quality" value="autohigh" />'+ 224 '<param name="scale" value="default" />'+ 225 '<param name="wmode" value="transparent" />'+ 226 '<param name="menu" value="false" />'+ 227 '<param name="bgcolor" value="#000000" /> '+ 228 '<embed src="'+flashURL+'&src='+movieURL+'&scid='+id+'" '+ 229 'quality="autohigh" '+ 230 'scale="default" '+ 231 'wmode="transparent" '+ 232 'bgcolor="#000000" '+ 233 'width="100%" '+ 234 'height="100%" '+ 235 'name="flash_'+id+'" '+ 236 'align="middle" '+ 237 'allowScriptAccess="sameDomain" '+ 238 'allowFullScreen="true" '+ 239 'menu="false" '+ 240 'type="application/x-shockwave-flash" '+ 241 'pluginspage="http://www.adobe.com/go/getflashplayer" />'+ 242 '</object>'); 243 this.loaded='flash'; 244 SC.AudioView.addToAudioFlashViews(this); 245 return; 246 default: 247 context.push('audio is not supported by your browser'); 248 return; 249 } 250 } 251 } 252 }, 253 254 valueObserver:function(){ 255 this.set('currentTime', 0); 256 this.set('duration', 0); 257 this.set('volume', 0); 258 this.set('paused', YES); 259 this.set('loaded', NO); 260 this.set('ended', NO); 261 this.set('canPlay', NO); 262 this.set('loadedTimeRanges', []); 263 264 // Re-render the entire layer. 265 if (this.get('_isRendered')) { 266 this.replaceLayer(); 267 } 268 }.observes('value'), 269 270 /** 271 In didCreateLayer we add DOM events for audio tag or quicktime. 272 273 @returns {void} 274 */ 275 didCreateLayer :function(){ 276 if(this.loaded==="html5"){ 277 this.addAudioDOMEvents(); 278 } 279 if(this.loaded==="quicktime"){ 280 this.addQTDOMEvents(); 281 } 282 }, 283 284 didAppendToDocument :function(){ 285 if(this.loaded==="quicktime"){ 286 this.addQTDOMEvents(); 287 } 288 }, 289 290 /** 291 Adds all the necessary audio DOM elements. 292 293 @returns {void} 294 */ 295 addAudioDOMEvents: function() { 296 var audioElem, view=this; 297 audioElem = this.$('audio')[0]; 298 audioElem.volume = this.get('volume'); 299 this.set('audioObject', audioElem); 300 SC.Event.add(audioElem, 'durationchange', this, function () { 301 SC.run(function() { 302 view.set('duration', audioElem.duration); 303 }); 304 }) ; 305 306 SC.Event.add(audioElem, 'timeupdate', this, function() { 307 SC.run(function() { 308 view._currentTime = audioElem.currentTime; 309 view.propertyDidChange('currentTime'); 310 }); 311 }); 312 313 SC.Event.add(audioElem, 'loadstart', this, function () { 314 SC.run(function() { 315 view.set('volume', audioElem.volume); 316 }); 317 }); 318 319 SC.Event.add(audioElem, 'play', this, function () { 320 SC.run(function() { 321 view.set('paused', NO); 322 }); 323 }); 324 325 SC.Event.add(audioElem, 'pause', this, function () { 326 SC.run(function() { 327 view.set('paused', YES); 328 }); 329 }); 330 331 SC.Event.add(audioElem, 'canplay', this, function () { 332 SC.run(function() { 333 view.set('canPlay', YES); 334 }); 335 }); 336 337 SC.Event.add(audioElem, 'ended', this, function () { 338 SC.run(function() { 339 view.set('ended', YES); 340 }); 341 }); 342 343 SC.Event.add(audioElem, 'progress', this, function (e) { 344 SC.run(function() { 345 this.loadedTimeRanges=[]; 346 for (var j=0, jLen = audioElem.seekable.length; j<jLen; j++){ 347 this.loadedTimeRanges.push(audioElem.seekable.start(j)); 348 this.loadedTimeRanges.push(audioElem.seekable.end(j)); 349 } 350 351 try { 352 var trackCount=view.GetTrackCount(),i; 353 for (i=1; i<=trackCount;i++){ 354 if ("Closed Caption"===this.GetTrackType(i)){ 355 view._closedCaptionTrackIndex=i; 356 } 357 } 358 } catch (f) {} 359 }, this); 360 361 }); 362 }, 363 364 /** 365 Adds all the necessary quicktime DOM elements. 366 367 @returns {void} 368 */ 369 addQTDOMEvents: function() { 370 var media=this._getAudioObject(), 371 audioElem = this.$()[0], 372 view=this, 373 dimensions; 374 try{ 375 media.GetVolume(); 376 }catch(e){ 377 // SC.Logger.log('loaded fail trying later'); 378 this.invokeLater(this.didAppendToDocument, 100); 379 return; 380 } 381 this.set('audioObject', media); 382 view.set('duration', media.GetDuration()/media.GetTimeScale()); 383 view.set('volume', media.GetVolume()/256); 384 385 SC.Event.add(audioElem, 'qt_durationchange', this, function () { 386 SC.run(function() { 387 view.set('duration', media.GetDuration()/media.GetTimeScale()); 388 }); 389 }); 390 391 SC.Event.add(audioElem, 'qt_begin', this, function () { 392 SC.run(function() { 393 view.set('volume', media.GetVolume()/256); 394 }); 395 }); 396 397 SC.Event.add(audioElem, 'qt_loadedmetadata', this, function () { 398 SC.run(function() { 399 view.set('duration', media.GetDuration()/media.GetTimeScale()); 400 }); 401 }); 402 403 SC.Event.add(audioElem, 'qt_canplay', this, function () { 404 SC.run(function() { 405 view.set('canPlay', YES); 406 }); 407 }); 408 409 SC.Event.add(audioElem, 'qt_ended', this, function () { 410 SC.run(function() { 411 view.set('ended', YES); 412 }); 413 }); 414 415 SC.Event.add(audioElem, 'qt_pause', this, function () { 416 SC.run(function() { 417 view.set('currentTime', media.GetTime() / media.GetTimeScale()); 418 view.set('paused', YES); 419 }); 420 }); 421 422 SC.Event.add(audioElem, 'qt_play', this, function () { 423 SC.run(function() { 424 view.set('currentTime', media.GetTime() / media.GetTimeScale()); 425 view.set('paused', NO); 426 }); 427 }); 428 }, 429 430 431 /** 432 For Quicktime we need to simulated the timer as there is no data, 433 coming back from the plugin that reports back the currentTime of the 434 audio. 435 436 @returns {void} 437 */ 438 _qtTimer:function(){ 439 if (this.loaded === 'quicktime' && !this.get('paused')) { 440 this.incrementProperty('currentTime'); 441 this.invokeLater(this._qtTimer, 1000); 442 } 443 }.observes('paused'), 444 445 /** 446 Called when currentTime changes. Notifies the different technologies 447 then new currentTime. 448 449 @returns {void} 450 */ 451 seek:function(){ 452 var timeInSecs, totaltimeInSecs, formattedTime, media=this._getAudioObject(); 453 if(this.loaded==='html5'){ 454 // also check for media && media.currentTime, because media.currentTime doesn't exist on value change 455 if(media && media.currentTime) media.currentTime=this.get('currentTime'); 456 } 457 if(this.loaded==='quicktime'){ 458 media.SetTime(this.get('currentTime')*media.GetTimeScale()); 459 } 460 if(this.loaded==='flash'){ 461 media.setTime(this.get('currentTime')); 462 } 463 }, 464 465 /** 466 Set the volume of the audio. 467 468 @returns {void} 469 */ 470 setVolume:function(){ 471 var media=this._getAudioObject(); 472 if(this.loaded==="html5") media.volume=this.get('volume'); 473 if(this.loaded==="quicktime") media.SetVolume(this.get('volume')*256); 474 if(this.loaded==="flash") media.setVolume(this.get('volume')); 475 }.observes('volume'), 476 477 /** 478 Calls the right play method depending on the technology. 479 @returns {void} 480 */ 481 play: function(){ 482 var media=this._getAudioObject(); 483 if(this.loaded==="html5") media.play(); 484 if(this.loaded==="quicktime") media.Play(); 485 if(this.loaded==="flash") media.playVideo(); 486 this.set('paused', NO); 487 }, 488 489 /** 490 Calls the right stop method depending on the technology. 491 @returns {void} 492 */ 493 stop: function(){ 494 var media=this._getAudioObject(); 495 if(this.loaded==="html5") media.pause(); 496 if(this.loaded==="quicktime") media.Stop(); 497 if(this.loaded==="flash") media.pauseVideo(); 498 this.set('paused', YES); 499 }, 500 501 /** 502 Plays or stops the audio. 503 @returns {void} 504 */ 505 playPause: function(){ 506 if(this.get('paused')){ 507 this.play(); 508 }else{ 509 this.stop(); 510 } 511 }, 512 513 /** 514 Enables captions if available 515 @returns {void} 516 */ 517 closedCaption:function(){ 518 if(this.loaded==="html5"){ 519 try{ 520 if(this.get('captionsEnabled')){ 521 if(this._closedCaptionTrackIndex){ 522 this.SetTrackEnabled(this._closedCaptionTrackIndex,true); 523 this.set('captionsEnabled', YES); 524 } 525 }else{ 526 this.SetTrackEnabled(this._closedCaptionTrackIndex,false); 527 this.set('captionsEnabled', NO); 528 } 529 }catch(a){} 530 } 531 return; 532 }, 533 534 /*private*/ 535 536 537 /** 538 Gets the right audio object depending on the browser. 539 @returns {void} 540 */ 541 _getAudioObject:function(){ 542 if(this.loaded==="html5") return this.get('audioObject'); 543 if(this.loaded==="quicktime") return document['qt_'+SC.guidFor(this)]; 544 if(this.loaded==="flash") { 545 var movieName='flash_'+SC.guidFor(this); 546 if (window.document[movieName]) 547 { 548 return window.document[movieName]; 549 } 550 if (navigator.appName.indexOf("Microsoft Internet")==-1) 551 { 552 if (document.embeds && document.embeds[movieName]) { 553 return document.embeds[movieName]; 554 } 555 } 556 else 557 { 558 return document.getElementById(movieName); 559 } 560 } 561 }, 562 563 _addZeros:function(value){ 564 if(value.toString().length<2) return "0"+value; 565 return value; 566 } 567 568 }); 569 570 /** 571 Hash to store references to the different flash audios. 572 */ 573 SC.AudioView.flashViews={}; 574 575 /** 576 Adds the flash view to the flashViews hash. 577 */ 578 SC.AudioView.addToAudioFlashViews = function(view) { 579 SC.AudioView.flashViews[SC.guidFor(view)]=view; 580 } ; 581 582 /** 583 This function is called from flash to update the properties of the corresponding 584 flash view. 585 */ 586 SC.AudioView.updateProperty = function(scid, property, value) { 587 var view = SC.AudioView.flashViews[scid]; 588 if(view){ 589 SC.run(function() { 590 view.set(property, value); 591 }); 592 } 593 } ; 594 595 /** 596 Function to log events coming from flash. 597 */ 598 SC.AudioView.logFlash = function(message) { 599 SC.Logger.log("FLASHLOG: "+message); 600 } ; 601 602 603 SC.AudioPlayerView = SC.View.extend({ 604 classNames: 'sc-audio-view', 605 606 childViews: ['audioView', 'mini'], 607 608 value: null, 609 610 degradeList: null, 611 612 audioView:SC.AudioView.design({ 613 layout: { top: 0, left:0, width:100, height:100}, 614 degradeListBinding: '*parentView.degradeList', 615 valueBinding: '*parentView.value' 616 }), 617 618 mini: SC.MiniMediaControlsView.design({ 619 layout: { bottom:0, left: 0, right: 0, height: 20 }, 620 targetBinding: '*parentView.audioView' 621 }) 622 }); 623