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