Video.hx 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276
  1. package h2d;
  2. #if hl
  3. private abstract VideoImpl(hl.Abstract<"hl_video">) {
  4. @:hlNative("video","video_close") public function close() : Void {
  5. }
  6. @:hlNative("video","video_decode_frame") public function decodeFrame( out : hl.Bytes, time : hl.Ref<Float> ) : Bool {
  7. return false;
  8. }
  9. @:hlNative("video","video_get_size") public function getSize( width : hl.Ref<Int>, height : hl.Ref<Int> ) : Void {
  10. }
  11. @:hlNative("video","video_open") public static function open( file : hl.Bytes ) : VideoImpl {
  12. return null;
  13. }
  14. @:hlNative("video","video_init") public static function init() {
  15. }
  16. }
  17. #end
  18. /**
  19. A video file playback Drawable. Due to platform specifics, each target have their own limitations.
  20. * <span class="label">Hashlink</span>: Playback ability depends on `video` library.
  21. At the time of HL 1.11 it's not bundled and have to be [compiled manually](https://github.com/HaxeFoundation/hashlink/tree/master/libs/video) with FFMPEG.
  22. * <span class="label">JavaScript</span>: HTML Video element will be used. Playback is restricted by content-security policy and browser decoder capabilities.
  23. **/
  24. class Video extends Drawable {
  25. #if hl
  26. static var INIT_DONE = false;
  27. var v : VideoImpl;
  28. var pixels : hxd.Pixels;
  29. #elseif js
  30. var v : js.html.VideoElement;
  31. var videoPlaying : Bool;
  32. var videoTimeupdate : Bool;
  33. var onReady : Void->Void;
  34. #end
  35. var texture : h3d.mat.Texture;
  36. var tile : h2d.Tile;
  37. var playTime : Float;
  38. var videoTime : Float;
  39. var frameReady : Bool;
  40. var loopVideo : Bool;
  41. /**
  42. Video width. Value is undefined until video is ready to play.
  43. **/
  44. public var videoWidth(default, null) : Int;
  45. /**
  46. Video height. Value is undefined until video is ready to play.
  47. **/
  48. public var videoHeight(default, null) : Int;
  49. /**
  50. Tells if video currently playing.
  51. **/
  52. public var playing(default, null) : Bool;
  53. /**
  54. Tells current timestamp of the video.
  55. **/
  56. public var time(get, null) : Float;
  57. /**
  58. When enabled, video will loop indefinitely.
  59. **/
  60. public var loop(get, set) : Bool;
  61. /**
  62. Create a new Video instance.
  63. @param parent An optional parent `h2d.Object` instance to which Video adds itself if set.
  64. **/
  65. public function new(?parent) {
  66. super(parent);
  67. blendMode = None;
  68. smooth = true;
  69. }
  70. /**
  71. Sent when there is an error with the decoding or playback of the video.
  72. **/
  73. public dynamic function onError( msg : String ) {
  74. }
  75. /**
  76. Sent when video playback is finished.
  77. **/
  78. public dynamic function onEnd() {
  79. }
  80. @:dox(hide) @:noCompletion
  81. public function get_time() {
  82. #if js
  83. return playing ? v.currentTime : 0;
  84. #else
  85. return playing ? haxe.Timer.stamp() - playTime : 0;
  86. #end
  87. }
  88. @:dox(hide) @:noCompletion
  89. public inline function get_loop() {
  90. return loopVideo;
  91. }
  92. @:dox(hide) @:noCompletion
  93. public function set_loop(value : Bool) : Bool {
  94. #if js
  95. return v.loop = loopVideo = value;
  96. #else
  97. return loopVideo = value;
  98. #end
  99. }
  100. /**
  101. Disposes of the currently playing Video and frees GPU memory.
  102. **/
  103. public function dispose() {
  104. #if hl
  105. if( v != null ) {
  106. v.close();
  107. v = null;
  108. };
  109. pixels = null;
  110. #elseif js
  111. if ( v != null ) {
  112. v.removeEventListener("ended", endHandler, true);
  113. v.removeEventListener("error", errorHandler, true);
  114. if (!v.paused) v.pause();
  115. v = null;
  116. }
  117. #end
  118. if( texture != null ) {
  119. texture.dispose();
  120. texture = null;
  121. }
  122. tile = null;
  123. videoWidth = 0;
  124. videoHeight = 0;
  125. time = 0;
  126. playing = false;
  127. frameReady = false;
  128. }
  129. /**
  130. Loads and starts the video playback by specified `path` and calls `onReady` when playback becomes possible.
  131. * <span class="label">Hashlink</span>: Playback being immediately after `load`, unless video was not being able to initialize.
  132. * <span class="label">JavaScript</span>: There won't be any video output until video is properly buffered enough data by the browser, in which case `onReady` is called.
  133. @param path The video path. Have to be valid file-system path for HL or valid URL (full or relative) for JS.
  134. @param onReady An optional callback signalling that video is initialized and began the video playback.
  135. **/
  136. public function load( path : String, ?onReady : Void -> Void ) {
  137. dispose();
  138. #if hl
  139. if( !INIT_DONE ) { INIT_DONE = true; VideoImpl.init(); }
  140. v = VideoImpl.open(@:privateAccess path.toUtf8());
  141. if( v == null ) {
  142. onError("Failed to init video " + path);
  143. return;
  144. }
  145. var w = 0, h = 0;
  146. v.getSize(w, h);
  147. videoWidth = w;
  148. videoHeight = h;
  149. playing = true;
  150. playTime = haxe.Timer.stamp();
  151. videoTime = 0.;
  152. if( onReady != null ) onReady();
  153. #elseif js
  154. v = js.Browser.document.createVideoElement();
  155. v.autoplay = true;
  156. v.muted = true;
  157. v.loop = loopVideo;
  158. videoPlaying = false;
  159. videoTimeupdate = false;
  160. this.onReady = onReady;
  161. v.addEventListener("playing", checkReady, true);
  162. v.addEventListener("timeupdate", checkReady, true);
  163. v.addEventListener("ended", endHandler, true);
  164. v.addEventListener("error", errorHandler, true);
  165. v.src = path;
  166. v.play();
  167. #else
  168. onError("Video not supported on this platform");
  169. #end
  170. }
  171. #if js
  172. function errorHandler(e : js.html.Event) {
  173. #if (haxe_ver >= 4)
  174. onError(v.error.code + ": " + v.error.message);
  175. #else
  176. onError(Std.string(v.error.code));
  177. #end
  178. }
  179. function endHandler(e : js.html.Event) {
  180. onEnd();
  181. }
  182. function checkReady(e : js.html.Event) {
  183. if (e.type == "playing") {
  184. videoPlaying = true;
  185. v.removeEventListener("playing", checkReady, true);
  186. } else {
  187. videoTimeupdate = true;
  188. v.removeEventListener("timeupdate", checkReady, true);
  189. }
  190. if (videoPlaying && videoTimeupdate) {
  191. frameReady = true;
  192. videoWidth = v.videoWidth;
  193. videoHeight = v.videoHeight;
  194. playing = true;
  195. playTime = haxe.Timer.stamp();
  196. videoTime = 0.0;
  197. if ( onReady != null )
  198. {
  199. onReady();
  200. onReady = null;
  201. }
  202. }
  203. }
  204. #end
  205. override function draw(ctx:RenderContext) {
  206. if( tile != null )
  207. ctx.drawTile(this, tile);
  208. }
  209. #if js
  210. @:access(h3d.mat.Texture)
  211. #end
  212. override function sync(ctx:RenderContext) {
  213. if( !playing )
  214. return;
  215. if( texture == null ) {
  216. var w = videoWidth, h = videoHeight;
  217. texture = new h3d.mat.Texture(w, h);
  218. tile = h2d.Tile.fromTexture(texture);
  219. #if hl
  220. pixels = new hxd.Pixels(w, h, haxe.io.Bytes.alloc(w * h * 4), h3d.mat.Texture.nativeFormat);
  221. #end
  222. }
  223. if( frameReady ) {
  224. #if hl
  225. texture.uploadPixels(pixels);
  226. frameReady = false;
  227. #elseif js
  228. texture.alloc();
  229. texture.checkSize(videoWidth, videoHeight, 0);
  230. @:privateAccess cast (@:privateAccess texture.mem.driver, h3d.impl.GlDriver).uploadTextureVideoElement(texture, v, 0, 0);
  231. texture.flags.set(WasCleared);
  232. texture.checkMipMapGen(0, 0);
  233. #end
  234. }
  235. #if hl
  236. if( time >= videoTime ) {
  237. var t = 0.;
  238. v.decodeFrame(pixels.bytes, t);
  239. videoTime = t;
  240. frameReady = true; // delay decode/upload for more reliable FPS
  241. }
  242. #end
  243. }
  244. }