1 /**
  2  * The MIT License (MIT)
  3  *
  4  * Copyright (c) 2016 DeNA Co., Ltd.
  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 THE
 22  * SOFTWARE.
 23  */
 24 
 25 /// <reference path="base.js"/>
 26 /// <reference path="config.js"/>
 27 /// <reference path="display_object.js"/>
 28 /// <reference path="rectangle.js"/>
 29 /// <reference path="ticker.js"/>
 30 
 31 /**
 32  * A class that encapsulates an HTMLVideoElement object.
 33  * @param {HTMLVideoElement} video
 34  * @extends {createjs.DisplayObject}
 35  * @implements {EventListener}
 36  * @constructor
 37  */
 38 createjs.Video = function(video) {
 39   /// <param type="HTMLVideoElement" name="value"/>
 40   createjs.DisplayObject.call(this);
 41 
 42   /**
 43    * The <video> element to be rendered.
 44    * @type {HTMLVideoElement}
 45    * @private
 46    */
 47   this.video_ = video;
 48 
 49   // Initialize the bounding box with the video size.
 50   this.setBoundingBox(0, 0, video.videoWidth, video.videoHeight);
 51 };
 52 createjs.inherits('Video', createjs.Video, createjs.DisplayObject);
 53 
 54 /**
 55  * Whether the associated <video> element starts over when it reaches its end.
 56  * @type {boolean}
 57  * @private
 58  */
 59 createjs.Video.prototype.loop_ = false;
 60 
 61 /**
 62  * Whether this object is listening 'ended' events.
 63  * @type {boolean}
 64  * @private
 65  */
 66 createjs.Video.prototype.ended_ = false;
 67 
 68 /**
 69  * The position of the associated <video> element when it is rendered, in
 70  * seconds.
 71  * @type {number}
 72  * @private
 73  */
 74 createjs.Video.prototype.lastTime_ = 0;
 75 
 76 /**
 77  * The last time when the associated <video> element is rendered.
 78  * @type {number}
 79  * @private
 80  */
 81 createjs.Video.prototype.runTime_ = -1;
 82 
 83 /**
 84  * The renderer that draws this object.
 85  * @type {createjs.Renderer}
 86  * @private
 87  */
 88 createjs.Video.prototype.output_ = null;
 89 
 90 /**
 91  * The <canvas> element to be rendered.
 92  * @type {HTMLCanvasElement}
 93  * @private
 94  */
 95 createjs.Video.prototype.canvas_ = null;
 96 
 97 /**
 98  * The <canvas> element to be rendered.
 99  * @type {CanvasRenderingContext2D}
100  * @private
101  */
102 createjs.Video.prototype.context_ = null;
103 
104 /**
105  * Starts playing this video. This method does not only start playing the
106  * <video> element attached to this object but it also resets the time-stamp to
107  * force drawing this object.
108  * @param {number} time
109  * @param {boolean} loop
110  * @const
111  */
112 createjs.Video.prototype.play = function(time, loop) {
113   var video = this.video_;
114   if (time) {
115     this.lastTime_ = time;
116     video.currentTime = time;
117   }
118   this.loop_ = !!loop;
119   // Play this <video> element only when the host browser can play <video>
120   // elements inline. (Set both the 'playsinline' attribute so Mobile Safari on
121   // iOS can play the <video> element inline and set the 'webkit-playsinline'
122   // attribute so standalone-mode web apps can, respectively.)
123   // earlier.)
124   if (createjs.Config.canPlayInline()) {
125     video.setAttribute('muted', '');
126     video.setAttribute('playsinline', '');
127     video.setAttribute('webkit-playsinline', '');
128     video.loop = this.loop_;
129     if (!this.loop_) {
130       if (!this.ended_) {
131         this.ended_ = true;
132         video.addEventListener('ended', this, false);
133       }
134     }
135     video.play();
136   } else {
137     video.load();
138     this.runTime_ = createjs.Ticker.getRunTime();
139   }
140 };
141 
142 /**
143  * Stops playing this video.
144  * @const
145  */
146 createjs.Video.prototype.stop = function() {
147   var video = this.video_;
148   if (this.ended_) {
149     this.ended_ = false;
150     video.removeEventListener('ended', this, false);
151   }
152   video.pause();
153 };
154 
155 /** @override */
156 createjs.Video.prototype.removeAllChildren = function(opt_destroy) {
157   /// <param type="boolean" optional="true" name="opt_destroy"/>
158   this.handleDetach();
159 };
160 
161 /** @override */
162 createjs.Video.prototype.handleDetach = function() {
163   if (this.canvas_) {
164     if (this.output_) {
165       this.output_.uncache(this.canvas_);
166     }
167     this.context_ = null;
168     this.canvas_ = null;
169   } else {
170     if (this.output_) {
171       this.output_.uncache(this.video_);
172     }
173   }
174   this.stop();
175   this.video_ = null;
176 };
177 
178 /** @override */
179 createjs.Video.prototype.paintObject = function(renderer) {
180   /// <param type="createjs.Renderer" name="renderer"/>
181   var video = this.video_;
182   // Draw this <video> element only when it has enough data to avoid a warning
183   // "WebGL: INVALID_VALUE: texImage2D: no video".
184   /** @enum {number} */
185   var ReadyState = {
186     HAVE_NOTHING: 0,
187     HAVE_METADATA: 1,
188     HAVE_CURRENT_DATA: 2,
189     HAVE_FUTURE_DATA: 3,
190     HAVE_ENOUGH_DATA: 4
191   };
192   if (video.readyState < ReadyState.HAVE_ENOUGH_DATA) {
193     return;
194   }
195   // Synchronize the video size with the bounding-box size of this object first
196   // time when this object is rendered. (The video size is reliable only when
197   // its readyState property is >= HAVE_METADATA (1).)
198   if (!this.getBoxWidth()) {
199     this.setBoundingBox(0, 0, video.videoWidth, video.videoHeight);
200   }
201   if (!this.output_) {
202     this.output_ = renderer;
203   }
204   var current = video.currentTime;
205   if (this.runTime_ >= 0) {
206     // Advance the position of the associated <video> element manually to get
207     // the image at that position if the host browser cannot play <video>
208     // elements inline, i.e. Mobile Safari on iOS 9 or earlier.
209     var runTime = createjs.Ticker.getRunTime();
210     current += (runTime - this.runTime_) * 0.001;
211     this.runTime_ = runTime;
212     var duration = video.duration;
213     if (current >= duration) {
214       if (this.loop_) {
215         current = current % duration;
216       } else {
217         current = duration;
218       }
219     }
220     video.currentTime = current;
221   }
222   if (!renderer.getExtensions()) {
223     // Create a placeholder <canvas> element and copy the current frame to it if
224     // this renderer cannot render <video> elements. (The WebGLRenderer object
225     // cannot renderer <video> elements on IE11, which does not allow creating
226     // WebGLTexture objects with <video> elements.)
227     var canvas = this.canvas_;
228     if (!canvas) {
229       canvas = createjs.createCanvas();
230       canvas.width = this.getBoxWidth();
231       canvas.height = this.getBoxHeight();
232       this.canvas_ = canvas;
233       this.context_ = createjs.getRenderingContext2D(canvas);
234       current = -1;
235     }
236     if (this.lastTime_ != current) {
237       this.lastTime_ = current;
238       this.context_.drawImage(video, 0, 0);
239       canvas.dirty_ = 1;
240     }
241     renderer.drawCanvas(canvas, 0, 0, this.getBoxWidth(), this.getBoxHeight());
242   } else {
243     if (this.lastTime_ != current) {
244       this.lastTime_ = current;
245       video.dirty_ = 1;
246     }
247     renderer.drawVideo(video, 0, 0, this.getBoxWidth(), this.getBoxHeight());
248   }
249 };
250 
251 /** @override */
252 createjs.Video.prototype.handleEvent = function(event) {
253   /// <param type="Event" name="event"/>
254   var type = event.type;
255   if (type == 'ended') {
256     this.stop();
257     this.dispatchNotification('complete');
258   }
259 };
260 
261 // Export the createjs.Video object to the global namespace.
262 createjs.exportObject('createjs.Video', createjs.Video, {
263   'play': createjs.Video.prototype.play,
264   'stop': createjs.Video.prototype.stop
265 });
266