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