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