1 /****************************************************************************
  2  Copyright (c) 2013-2014 Chukong Technologies Inc.
  3 
  4  http://www.cocos2d-x.org
  5 
  6  Permission is hereby granted, free of charge, to any person obtaining a copy
  7  of this software and associated documentation files (the "Software"), to deal
  8  in the Software without restriction, including without limitation the rights
  9  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 10  copies of the Software, and to permit persons to whom the Software is
 11  furnished to do so, subject to the following conditions:
 12 
 13  The above copyright notice and this permission notice shall be included in
 14  all copies or substantial portions of the Software.
 15 
 16  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 17  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 18  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 19  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 20  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 21  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 22  THE SOFTWARE.
 23  ****************************************************************************/
 24 
 25 /**
 26  * @class
 27  * @extends ccui.Widget
 28  * @brief Displays a video file.
 29  *
 30  * @note VideoPlayer displays a video file based on DOM element
 31  * VideoPlayer will be rendered above all other graphical elements.
 32  *
 33  * @property {String}   path - The video path
 34  */
 35 ccui.VideoPlayer = ccui.Widget.extend(/** @lends ccui.VideoPlayer# */{
 36 
 37     _played: false,
 38     _playing: false,
 39     _stopped: true,
 40 
 41     ctor: function(path){
 42         ccui.Widget.prototype.ctor.call(this);
 43         this._EventList = {};
 44         if(path)
 45             this.setURL(path);
 46     },
 47 
 48     _createRenderCmd: function(){
 49         return new ccui.VideoPlayer.RenderCmd(this);
 50     },
 51 
 52     /**
 53      * Set the video address
 54      * Automatically replace extname
 55      * All supported video formats will be added to the video
 56      * @param {String} address
 57      */
 58     setURL: function(address){
 59         this._renderCmd.updateURL(address);
 60     },
 61 
 62     /**
 63      * Get the video path
 64      * @returns {String}
 65      */
 66     getURL: function() {
 67         return this._renderCmd._url;
 68     },
 69 
 70     /**
 71      * Play the video
 72      */
 73     play: function(){
 74         var self = this,
 75             video = this._renderCmd._video;
 76         if(video){
 77             this._played = true;
 78             video.pause();
 79             if(this._stopped !== false || this._playing !== false || this._played !== true)
 80                 video.currentTime = 0;
 81             if(ccui.VideoPlayer._polyfill.autoplayAfterOperation){
 82                 setTimeout(function(){
 83                     video.play();
 84                     self._playing = true;
 85                     self._stopped = false;
 86                 }, 20);
 87             }else{
 88                 video.play();
 89                 this._playing = true;
 90                 this._stopped = false;
 91             }
 92         }
 93     },
 94 
 95     /**
 96      * Pause the video
 97      */
 98     pause: function(){
 99         var video = this._renderCmd._video;
100         if(video && this._playing === true && this._stopped === false){
101             video.pause();
102             this._playing = false;
103         }
104     },
105 
106     /**
107      * Resume the video
108      */
109     resume: function(){
110         if(this._stopped === false && this._playing === false && this._played === true){
111             this.play();
112         }
113     },
114 
115     /**
116      * Stop the video
117      */
118     stop: function(){
119         var self = this,
120             video = this._renderCmd._video;
121         if(video){
122             video.pause();
123             video.currentTime = 0;
124             this._playing = false;
125             this._stopped = true;
126         }
127 
128         setTimeout(function(){
129             self._dispatchEvent(ccui.VideoPlayer.EventType.STOPPED);
130         }, 0);
131     },
132     /**
133      * Jump to the specified point in time
134      * @param {Number} sec
135      */
136     seekTo: function(sec){
137         var video = this._renderCmd._video;
138         if(video){
139             video.currentTime = sec;
140             if(ccui.VideoPlayer._polyfill.autoplayAfterOperation && this.isPlaying()){
141                 setTimeout(function(){
142                     video.play();
143                 }, 20);
144             }
145         }
146     },
147 
148     /**
149      * Whether the video is playing
150      * @returns {boolean}
151      */
152     isPlaying: function(){
153         if(ccui.VideoPlayer._polyfill.autoplayAfterOperation && this._playing){
154             setTimeout(function(){
155                 video.play();
156             }, 20);
157         }
158         return this._playing;
159     },
160 
161     /**
162      * Whether to keep the aspect ratio
163      */
164     setKeepAspectRatioEnabled: function(enable){
165         cc.log("On the web is always keep the aspect ratio");
166     },
167     isKeepAspectRatioEnabled: function(){
168         return false;
169     },
170 
171     /**
172      * Set whether the full screen
173      * May appear inconsistent in different browsers
174      * @param {boolean} enable
175      */
176     setFullScreenEnabled: function(enable){
177         var video = this._renderCmd._video;
178         if(video){
179             if(enable)
180                 cc.screen.requestFullScreen(video);
181             else
182                 cc.screen.exitFullScreen(video);
183         }
184     },
185 
186     /**
187      * Determine whether already full screen
188      */
189     isFullScreenEnabled: function(){
190         cc.log("Can't know status");
191     },
192 
193     /**
194      * The binding event
195      * @param {ccui.VideoPlayer.EventType} event
196      * @param {Function} callback
197      */
198     setEventListener: function(event, callback){
199         this._EventList[event] = callback;
200     },
201 
202     /**
203      * Delete events
204      * @param {ccui.VideoPlayer.EventType} event
205      */
206     removeEventListener: function(event){
207         this._EventList[event] = null;
208     },
209 
210     _dispatchEvent: function(event) {
211         var callback = this._EventList[event];
212         if (callback)
213             callback.call(this, this, this._renderCmd._video.src);
214     },
215 
216     /**
217      * Trigger playing events
218      */
219     onPlayEvent: function(){
220         var list = this._EventList[ccui.VideoPlayer.EventType.PLAYING];
221         if(list)
222             for(var i=0; i<list.length; i++)
223                 list[i].call(this, this, this._renderCmd._video.src);
224     },
225 
226     //_createCloneInstance: function(){},
227     //_copySpecialProperties: function(){},
228 
229     setContentSize: function(w, h){
230         ccui.Widget.prototype.setContentSize.call(this, w, h);
231         if(h === undefined){
232             h = w.height;
233             w = w.width;
234         }
235         this._renderCmd.changeSize(w, h);
236     },
237 
238     cleanup: function(){
239         this._renderCmd.removeDom();
240         this.stopAllActions();
241         this.unscheduleAllCallbacks();
242     },
243 
244     onEnter: function(){
245         ccui.Widget.prototype.onEnter.call(this);
246         var list = ccui.VideoPlayer.elements;
247         if(list.indexOf(this) === -1)
248             list.push(this);
249     },
250 
251     onExit: function(){
252         ccui.Widget.prototype.onExit.call(this);
253         var list = ccui.VideoPlayer.elements;
254         var index = list.indexOf(this);
255         if(index !== -1)
256             list.splice(index, 1);
257     }
258 
259 });
260 
261 // VideoHTMLElement list
262 ccui.VideoPlayer.elements = [];
263 ccui.VideoPlayer.pauseElements = [];
264 
265 cc.eventManager.addCustomListener(cc.game.EVENT_HIDE, function () {
266     var list = ccui.VideoPlayer.elements;
267     for(var node, i=0; i<list.length; i++){
268         node = list[i];
269         if(list[i]._playing){
270             node.pause();
271             ccui.VideoPlayer.pauseElements.push(node);
272         }
273     }
274 });
275 cc.eventManager.addCustomListener(cc.game.EVENT_SHOW, function () {
276     var list = ccui.VideoPlayer.pauseElements;
277     var node = list.pop();
278     while(node){
279         node.play();
280         node = list.pop();
281     }
282 });
283 
284 /**
285  * The VideoPlayer support list of events
286  * @type {{PLAYING: string, PAUSED: string, STOPPED: string, COMPLETED: string}}
287  */
288 ccui.VideoPlayer.EventType = {
289     PLAYING: "play",
290     PAUSED: "pause",
291     STOPPED: "stop",
292     COMPLETED: "complete"
293 };
294 
295 (function(video){
296     /**
297      * Adapter various machines
298      * @devicePixelRatio Whether you need to consider devicePixelRatio calculated position
299      * @event To get the data using events
300      */
301     video._polyfill = {
302         devicePixelRatio: false,
303         event: "canplay",
304         canPlayType: []
305     };
306 
307     (function(){
308         /**
309          * Some old browser only supports Theora encode video
310          * But native does not support this encode,
311          * so it is best to provide mp4 and webm or ogv file
312          */
313         var dom = document.createElement("video");
314         if(dom.canPlayType("video/ogg")){
315             video._polyfill.canPlayType.push(".ogg");
316             video._polyfill.canPlayType.push(".ogv");
317         }
318         if(dom.canPlayType("video/mp4"))
319             video._polyfill.canPlayType.push(".mp4");
320         if(dom.canPlayType("video/webm"))
321             video._polyfill.canPlayType.push(".webm");
322     })();
323 
324     if(cc.sys.OS_IOS === cc.sys.os){
325         video._polyfill.devicePixelRatio = true;
326         video._polyfill.event = "progress";
327     }
328     if(cc.sys.browserType === cc.sys.BROWSER_TYPE_FIREFOX){
329         video._polyfill.autoplayAfterOperation = true;
330     }
331 
332     var style = document.createElement("style");
333     style.innerHTML = ".cocosVideo:-moz-full-screen{transform:matrix(1,0,0,1,0,0) !important;}" +
334     ".cocosVideo:full-screen{transform:matrix(1,0,0,1,0,0) !important;}" +
335     ".cocosVideo:-webkit-full-screen{transform:matrix(1,0,0,1,0,0) !important;}";
336     document.head.appendChild(style);
337 
338 })(ccui.VideoPlayer);
339 
340 (function(polyfill){
341     ccui.VideoPlayer.RenderCmd = function(node){
342         cc.Node.CanvasRenderCmd.call(this, node);
343         this._listener = null;
344         this._url = "";
345         this.initStyle();
346     };
347 
348     var proto = ccui.VideoPlayer.RenderCmd.prototype = Object.create(cc.Node.CanvasRenderCmd.prototype);
349     proto.constructor = ccui.VideoPlayer.RenderCmd;
350 
351     proto.visit = function(){
352         var self = this,
353             container = cc.container,
354             eventManager = cc.eventManager;
355         if(this._node._visible){
356             container.appendChild(this._video);
357             if(this._listener === null)
358                 this._listener = cc.eventManager.addCustomListener(cc.game.EVENT_RESIZE, function () {
359                     self.resize();
360                 });
361         }else{
362             var hasChild = false;
363             if('contains' in container) {
364                 hasChild = container.contains(this._video);
365             }else {
366                 hasChild = container.compareDocumentPosition(this._video) % 16;
367             }
368             if(hasChild)
369                 container.removeChild(this._video);
370             eventManager.removeListener(this._listener);
371             this._listener = null;
372         }
373         this.updateStatus();
374     };
375 
376     proto.updateStatus = function(){
377         polyfill.devicePixelRatio = cc.view.isRetinaEnabled();
378         var flags = cc.Node._dirtyFlags, locFlag = this._dirtyFlag;
379         if(locFlag & flags.transformDirty){
380             //update the transform
381             this.transform(this.getParentRenderCmd(), true);
382             this.updateMatrix(this._worldTransform, cc.view._scaleX, cc.view._scaleY);
383             this._dirtyFlag = this._dirtyFlag & cc.Node._dirtyFlags.transformDirty ^ this._dirtyFlag;
384         }
385 
386         if (locFlag & flags.orderDirty) {
387             this._dirtyFlag = this._dirtyFlag & flags.orderDirty ^ this._dirtyFlag;
388         }
389     };
390 
391     proto.resize = function(view){
392         view = view || cc.view;
393         var node = this._node,
394             eventManager = cc.eventManager;
395         if(node._parent && node._visible)
396             this.updateMatrix(this._worldTransform, view._scaleX, view._scaleY);
397         else{
398             eventManager.removeListener(this._listener);
399             this._listener = null;
400         }
401     };
402 
403     proto.updateMatrix = function(t, scaleX, scaleY){
404         var node = this._node;
405         if(polyfill.devicePixelRatio){
406             var dpr = window.devicePixelRatio;
407             scaleX = scaleX / dpr;
408             scaleY = scaleY / dpr;
409         }
410         if(this._loaded === false) return;
411         var cw = node._contentSize.width,
412             ch = node._contentSize.height;
413         var a = t.a * scaleX,
414             b = t.b,
415             c = t.c,
416             d = t.d * scaleY,
417             tx = t.tx*scaleX - cw/2 + cw*node._scaleX/2*scaleX,
418             ty = t.ty*scaleY - ch/2 + ch*node._scaleY/2*scaleY;
419         var matrix = "matrix(" + a + "," + b + "," + c + "," + d + "," + tx + "," + -ty + ")";
420         this._video.style["transform"] = matrix;
421         this._video.style["-webkit-transform"] = matrix;
422     };
423 
424     proto.updateURL = function(path){
425         var source, video, hasChild, container, extname;
426         var node = this._node;
427 
428         if (this._url == path)
429             return;
430 
431         this._url = path;
432 
433         if(cc.loader.resPath && !/^http/.test(path))
434             path = cc.path.join(cc.loader.resPath, path);
435 
436         hasChild = false;
437         container = cc.container;
438         if('contains' in container) {
439             hasChild = container.contains(this._video);
440         }else {
441             hasChild = container.compareDocumentPosition(this._video) % 16;
442         }
443         if(hasChild)
444             container.removeChild(this._video);
445 
446         this._video = document.createElement("video");
447         video = this._video;
448         this.bindEvent();
449         var self = this;
450 
451         var cb = function(){
452             if(self._loaded == true)
453                 return;
454             self._loaded = true;
455             self.changeSize();
456             self.setDirtyFlag(cc.Node._dirtyFlags.transformDirty);
457             video.removeEventListener(polyfill.event, cb);
458             video.currentTime = 0;
459             video.style["visibility"] = "visible";
460             //IOS does not display video images
461             video.play();
462             if(!node._played){
463                 video.pause();
464                 video.currentTime = 0;
465             }
466         };
467         video.addEventListener(polyfill.event, cb);
468 
469         //video.controls = "controls";
470         video.preload = "metadata";
471         video.style["visibility"] = "hidden";
472         this._loaded = false;
473         node._played = false;
474         node._playing = false;
475         node._stopped = true;
476         this.initStyle();
477         this.visit();
478 
479         source = document.createElement("source");
480         source.src = path;
481         video.appendChild(source);
482 
483         extname = cc.path.extname(path);
484         for(var i=0; i<polyfill.canPlayType.length; i++){
485             if(extname !== polyfill.canPlayType[i]){
486                 source = document.createElement("source");
487                 source.src = path.replace(extname, polyfill.canPlayType[i]);
488                 video.appendChild(source);
489             }
490         }
491     };
492 
493     proto.bindEvent = function(){
494         var self = this,
495             node = this._node,
496             video = this._video;
497         //binding event
498         video.addEventListener("ended", function(){
499             node._renderCmd.updateMatrix(self._worldTransform, cc.view._scaleX, cc.view._scaleY);
500             node._playing = false;
501             node._dispatchEvent(ccui.VideoPlayer.EventType.COMPLETED);
502         });
503         video.addEventListener("play", function(){
504             node._dispatchEvent(ccui.VideoPlayer.EventType.PLAYING);
505         });
506         video.addEventListener("pause", function(){
507             node._dispatchEvent(ccui.VideoPlayer.EventType.PAUSED);
508         });
509     };
510 
511     proto.initStyle = function(){
512         if(!this._video)  return;
513         var video = this._video;
514         video.style.position = "absolute";
515         video.style.bottom = "0px";
516         video.style.left = "0px";
517         video.className = "cocosVideo";
518     };
519 
520     proto.changeSize = function(w, h){
521         var contentSize = this._node._contentSize;
522         w = w || contentSize.width;
523         h = h || contentSize.height;
524         var video = this._video;
525         if(video){
526             if(w !== 0)
527                 video.width = w;
528             if(h !== 0)
529                 video.height = h;
530         }
531     };
532 
533     proto.removeDom = function(){
534         var video = this._video;
535         if(video){
536             var hasChild = false;
537             if('contains' in cc.container) {
538                 hasChild = cc.container.contains(video);
539             }else {
540                 hasChild = cc.container.compareDocumentPosition(video) % 16;
541             }
542             if(hasChild)
543                 cc.container.removeChild(video);
544         }
545     };
546 
547 })(ccui.VideoPlayer._polyfill);