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 });
245 
246 /**
247  * The VideoPlayer support list of events
248  * @type {{PLAYING: string, PAUSED: string, STOPPED: string, COMPLETED: string}}
249  */
250 ccui.VideoPlayer.EventType = {
251     PLAYING: "play",
252     PAUSED: "pause",
253     STOPPED: "stop",
254     COMPLETED: "complete"
255 };
256 
257 (function(video){
258     /**
259      * Adapter various machines
260      * @devicePixelRatio Whether you need to consider devicePixelRatio calculated position
261      * @event To get the data using events
262      */
263     video._polyfill = {
264         devicePixelRatio: false,
265         event: "canplay",
266         canPlayType: []
267     };
268 
269     (function(){
270         /**
271          * Some old browser only supports Theora encode video
272          * But native does not support this encode,
273          * so it is best to provide mp4 and webm or ogv file
274          */
275         var dom = document.createElement("video");
276         if(dom.canPlayType("video/ogg")){
277             video._polyfill.canPlayType.push(".ogg");
278             video._polyfill.canPlayType.push(".ogv");
279         }
280         if(dom.canPlayType("video/mp4"))
281             video._polyfill.canPlayType.push(".mp4");
282         if(dom.canPlayType("video/webm"))
283             video._polyfill.canPlayType.push(".webm");
284     })();
285 
286     if(cc.sys.OS_IOS === cc.sys.os){
287         video._polyfill.devicePixelRatio = true;
288         video._polyfill.event = "progress";
289     }
290     if(cc.sys.browserType === cc.sys.BROWSER_TYPE_FIREFOX){
291         video._polyfill.autoplayAfterOperation = true;
292     }
293 
294     var style = document.createElement("style");
295     style.innerHTML = ".cocosVideo:-moz-full-screen{transform:matrix(1,0,0,1,0,0) !important;}" +
296     ".cocosVideo:full-screen{transform:matrix(1,0,0,1,0,0) !important;}" +
297     ".cocosVideo:-webkit-full-screen{transform:matrix(1,0,0,1,0,0) !important;}";
298     document.head.appendChild(style);
299 
300 })(ccui.VideoPlayer);
301 
302 (function(polyfill){
303     ccui.VideoPlayer.RenderCmd = function(node){
304         cc.Node.CanvasRenderCmd.call(this, node);
305         this._listener = null;
306         this._url = "";
307         this.initStyle();
308     };
309 
310     var proto = ccui.VideoPlayer.RenderCmd.prototype = Object.create(cc.Node.CanvasRenderCmd.prototype);
311     proto.constructor = ccui.VideoPlayer.RenderCmd;
312 
313     proto.visit = function(){
314         var self = this,
315             container = cc.container,
316             eventManager = cc.eventManager;
317         if(this._node._visible){
318             container.appendChild(this._video);
319             if(this._listener === null)
320                 this._listener = cc.eventManager.addCustomListener(cc.game.EVENT_RESIZE, function () {
321                     self.resize();
322                 });
323         }else{
324             var hasChild = false;
325             if('contains' in container) {
326                 hasChild = container.contains(this._video);
327             }else {
328                 hasChild = container.compareDocumentPosition(this._video) % 16;
329             }
330             if(hasChild)
331                 container.removeChild(this._video);
332             var list = eventManager._listenersMap[cc.game.EVENT_RESIZE].getFixedPriorityListeners();
333             eventManager._removeListenerInVector(list, this._listener);
334             this._listener = null;
335         }
336         this.updateStatus();
337     };
338 
339     proto.updateStatus = function(){
340         polyfill.devicePixelRatio = cc.view.isRetinaEnabled();
341         var flags = cc.Node._dirtyFlags, locFlag = this._dirtyFlag;
342         if(locFlag & flags.transformDirty){
343             //update the transform
344             this.transform(this.getParentRenderCmd(), true);
345             this.updateMatrix(this._worldTransform, cc.view._scaleX, cc.view._scaleY);
346             this._dirtyFlag = this._dirtyFlag & cc.Node._dirtyFlags.transformDirty ^ this._dirtyFlag;
347         }
348     };
349 
350     proto.resize = function(view){
351         view = view || cc.view;
352         var node = this._node,
353             eventManager = cc.eventManager;
354         if(node._parent && node._visible)
355             this.updateMatrix(this._worldTransform, view._scaleX, view._scaleY);
356         else{
357             var list = eventManager._listenersMap[cc.game.EVENT_RESIZE].getFixedPriorityListeners();
358             eventManager._removeListenerInVector(list, this._listener);
359             this._listener = null;
360         }
361     };
362 
363     proto.updateMatrix = function(t, scaleX, scaleY){
364         var node = this._node;
365         if(polyfill.devicePixelRatio){
366             var dpr = window.devicePixelRatio;
367             scaleX = scaleX / dpr;
368             scaleY = scaleY / dpr;
369         }
370         if(this._loaded === false) return;
371         var cw = node._contentSize.width,
372             ch = node._contentSize.height;
373         var a = t.a * scaleX,
374             b = t.b,
375             c = t.c,
376             d = t.d * scaleY,
377             tx = t.tx*scaleX - cw/2 + cw*node._scaleX/2*scaleX,
378             ty = t.ty*scaleY - ch/2 + ch*node._scaleY/2*scaleY;
379         var matrix = "matrix(" + a + "," + b + "," + c + "," + d + "," + tx + "," + -ty + ")";
380         this._video.style["transform"] = matrix;
381         this._video.style["-webkit-transform"] = matrix;
382     };
383 
384     proto.updateURL = function(path){
385         var source, video, hasChild, container, extname;
386         var node = this._node;
387 
388         if (this._url == path)
389             return;
390 
391         this._url = path;
392 
393         if(cc.loader.resPath && !/^http/.test(path))
394             path = cc.path.join(cc.loader.resPath, path);
395 
396         hasChild = false;
397         container = cc.container;
398         if('contains' in container) {
399             hasChild = container.contains(this._video);
400         }else {
401             hasChild = container.compareDocumentPosition(this._video) % 16;
402         }
403         if(hasChild)
404             container.removeChild(this._video);
405 
406         this._video = document.createElement("video");
407         video = this._video;
408         this.bindEvent();
409         var self = this;
410 
411         var cb = function(){
412             if(self._loaded == true)
413                 return;
414             self._loaded = true;
415             self.changeSize();
416             self.setDirtyFlag(cc.Node._dirtyFlags.transformDirty);
417             video.removeEventListener(polyfill.event, cb);
418             video.currentTime = 0;
419             video.style["visibility"] = "visible";
420             //IOS does not display video images
421             video.play();
422             if(!node._played){
423                 video.pause();
424                 video.currentTime = 0;
425             }
426         };
427         video.addEventListener(polyfill.event, cb);
428 
429         //video.controls = "controls";
430         video.preload = "metadata";
431         video.style["visibility"] = "hidden";
432         this._loaded = false;
433         node._played = false;
434         node._playing = false;
435         node._stopped = true;
436         this.initStyle();
437         this.visit();
438 
439         source = document.createElement("source");
440         source.src = path;
441         video.appendChild(source);
442 
443         extname = cc.path.extname(path);
444         for(var i=0; i<polyfill.canPlayType.length; i++){
445             if(extname !== polyfill.canPlayType[i]){
446                 source = document.createElement("source");
447                 source.src = path.replace(extname, polyfill.canPlayType[i]);
448                 video.appendChild(source);
449             }
450         }
451     };
452 
453     proto.bindEvent = function(){
454         var self = this,
455             node = this._node,
456             video = this._video;
457         //binding event
458         video.addEventListener("ended", function(){
459             node._renderCmd.updateMatrix(self._worldTransform, cc.view._scaleX, cc.view._scaleY);
460             node._playing = false;
461             node._dispatchEvent(ccui.VideoPlayer.EventType.COMPLETED);
462         });
463         video.addEventListener("play", function(){
464             node._dispatchEvent(ccui.VideoPlayer.EventType.PLAYING);
465         });
466         video.addEventListener("pause", function(){
467             node._dispatchEvent(ccui.VideoPlayer.EventType.PAUSED);
468         });
469     };
470 
471     proto.initStyle = function(){
472         if(!this._video)  return;
473         var video = this._video;
474         video.style.position = "absolute";
475         video.style.bottom = "0px";
476         video.style.left = "0px";
477         video.className = "cocosVideo";
478     };
479 
480     proto.changeSize = function(w, h){
481         var contentSize = this._node._contentSize;
482         w = w || contentSize.width;
483         h = h || contentSize.height;
484         var video = this._video;
485         if(video){
486             if(w !== 0)
487                 video.width = w;
488             if(h !== 0)
489                 video.height = h;
490         }
491     };
492 
493     proto.removeDom = function(){
494         var video = this._video;
495         if(video){
496             var hasChild = false;
497             if('contains' in cc.container) {
498                 hasChild = cc.container.contains(video);
499             }else {
500                 hasChild = cc.container.compareDocumentPosition(video) % 16;
501             }
502             if(hasChild)
503                 cc.container.removeChild(video);
504         }
505     };
506 
507 })(ccui.VideoPlayer._polyfill);