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 
387     proto.resize = function(view){
388         view = view || cc.view;
389         var node = this._node,
390             eventManager = cc.eventManager;
391         if(node._parent && node._visible)
392             this.updateMatrix(this._worldTransform, view._scaleX, view._scaleY);
393         else{
394             eventManager.removeListener(this._listener);
395             this._listener = null;
396         }
397     };
398 
399     proto.updateMatrix = function(t, scaleX, scaleY){
400         var node = this._node;
401         if(polyfill.devicePixelRatio){
402             var dpr = window.devicePixelRatio;
403             scaleX = scaleX / dpr;
404             scaleY = scaleY / dpr;
405         }
406         if(this._loaded === false) return;
407         var cw = node._contentSize.width,
408             ch = node._contentSize.height;
409         var a = t.a * scaleX,
410             b = t.b,
411             c = t.c,
412             d = t.d * scaleY,
413             tx = t.tx*scaleX - cw/2 + cw*node._scaleX/2*scaleX,
414             ty = t.ty*scaleY - ch/2 + ch*node._scaleY/2*scaleY;
415         var matrix = "matrix(" + a + "," + b + "," + c + "," + d + "," + tx + "," + -ty + ")";
416         this._video.style["transform"] = matrix;
417         this._video.style["-webkit-transform"] = matrix;
418     };
419 
420     proto.updateURL = function(path){
421         var source, video, hasChild, container, extname;
422         var node = this._node;
423 
424         if (this._url == path)
425             return;
426 
427         this._url = path;
428 
429         if(cc.loader.resPath && !/^http/.test(path))
430             path = cc.path.join(cc.loader.resPath, path);
431 
432         hasChild = false;
433         container = cc.container;
434         if('contains' in container) {
435             hasChild = container.contains(this._video);
436         }else {
437             hasChild = container.compareDocumentPosition(this._video) % 16;
438         }
439         if(hasChild)
440             container.removeChild(this._video);
441 
442         this._video = document.createElement("video");
443         video = this._video;
444         this.bindEvent();
445         var self = this;
446 
447         var cb = function(){
448             if(self._loaded == true)
449                 return;
450             self._loaded = true;
451             self.changeSize();
452             self.setDirtyFlag(cc.Node._dirtyFlags.transformDirty);
453             video.removeEventListener(polyfill.event, cb);
454             video.currentTime = 0;
455             video.style["visibility"] = "visible";
456             //IOS does not display video images
457             video.play();
458             if(!node._played){
459                 video.pause();
460                 video.currentTime = 0;
461             }
462         };
463         video.addEventListener(polyfill.event, cb);
464 
465         //video.controls = "controls";
466         video.preload = "metadata";
467         video.style["visibility"] = "hidden";
468         this._loaded = false;
469         node._played = false;
470         node._playing = false;
471         node._stopped = true;
472         this.initStyle();
473         this.visit();
474 
475         source = document.createElement("source");
476         source.src = path;
477         video.appendChild(source);
478 
479         extname = cc.path.extname(path);
480         for(var i=0; i<polyfill.canPlayType.length; i++){
481             if(extname !== polyfill.canPlayType[i]){
482                 source = document.createElement("source");
483                 source.src = path.replace(extname, polyfill.canPlayType[i]);
484                 video.appendChild(source);
485             }
486         }
487     };
488 
489     proto.bindEvent = function(){
490         var self = this,
491             node = this._node,
492             video = this._video;
493         //binding event
494         video.addEventListener("ended", function(){
495             node._renderCmd.updateMatrix(self._worldTransform, cc.view._scaleX, cc.view._scaleY);
496             node._playing = false;
497             node._dispatchEvent(ccui.VideoPlayer.EventType.COMPLETED);
498         });
499         video.addEventListener("play", function(){
500             node._dispatchEvent(ccui.VideoPlayer.EventType.PLAYING);
501         });
502         video.addEventListener("pause", function(){
503             node._dispatchEvent(ccui.VideoPlayer.EventType.PAUSED);
504         });
505     };
506 
507     proto.initStyle = function(){
508         if(!this._video)  return;
509         var video = this._video;
510         video.style.position = "absolute";
511         video.style.bottom = "0px";
512         video.style.left = "0px";
513         video.className = "cocosVideo";
514     };
515 
516     proto.changeSize = function(w, h){
517         var contentSize = this._node._contentSize;
518         w = w || contentSize.width;
519         h = h || contentSize.height;
520         var video = this._video;
521         if(video){
522             if(w !== 0)
523                 video.width = w;
524             if(h !== 0)
525                 video.height = h;
526         }
527     };
528 
529     proto.removeDom = function(){
530         var video = this._video;
531         if(video){
532             var hasChild = false;
533             if('contains' in cc.container) {
534                 hasChild = cc.container.contains(video);
535             }else {
536                 hasChild = cc.container.compareDocumentPosition(video) % 16;
537             }
538             if(hasChild)
539                 cc.container.removeChild(video);
540         }
541     };
542 
543 })(ccui.VideoPlayer._polyfill);