Video.hx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452
  1. package h2d;
  2. #if (hl && hlvideo)
  3. enum FrameState {
  4. Free;
  5. Loading;
  6. Ready;
  7. Ended;
  8. }
  9. typedef Frame = {
  10. var pixels : hxd.Pixels;
  11. var state : FrameState;
  12. var time : Float;
  13. }
  14. class FrameCache {
  15. var frames : Array<Frame> = [];
  16. var readCursor = 0;
  17. var writeCursor = 0;
  18. var width : Int;
  19. var height : Int;
  20. public function new(size : Int, w : Int, h : Int) {
  21. width = w;
  22. height = h;
  23. frames = [];
  24. for(i in 0 ... size) {
  25. frames[i] = {
  26. pixels: new hxd.Pixels(w, h, haxe.io.Bytes.alloc(w * h * 4), h3d.mat.Texture.nativeFormat),
  27. state: Free,
  28. time: 0
  29. }
  30. }
  31. }
  32. public function currentFrame() : Frame {
  33. if( frames == null )
  34. return null;
  35. return frames[readCursor];
  36. }
  37. public function nextFrame() : Bool {
  38. var nextCursor = (readCursor + 1) % frames.length;
  39. frames[readCursor].state = Free;
  40. readCursor = nextCursor;
  41. return true;
  42. }
  43. function frameBufferSize() {
  44. if(writeCursor < readCursor)
  45. return frames.length - readCursor + writeCursor;
  46. else
  47. return writeCursor - readCursor;
  48. }
  49. public function isFull() {
  50. if(writeCursor < readCursor)
  51. return frames.length - readCursor + writeCursor >= frames.length - 1;
  52. else
  53. return writeCursor - readCursor >= frames.length - 1;
  54. }
  55. public function isEmpty() {
  56. return readCursor == writeCursor;
  57. }
  58. public function prepareFrame(webm : hl.video.Webm, codec : hl.video.Aom.Codec, loop : Bool) : Frame {
  59. if(frames[writeCursor].state != Free)
  60. return null;
  61. var savedCursor = writeCursor;
  62. var f = frames[writeCursor];
  63. var time = webm.readFrame(codec, f.pixels.bytes);
  64. if(time == null) {
  65. if(loop) {
  66. webm.rewind();
  67. time = webm.readFrame(codec, f.pixels.bytes);
  68. }
  69. else {
  70. f.time = 0;
  71. f.state = Ended;
  72. return f;
  73. }
  74. }
  75. f.time = time;
  76. f.state = Ready;
  77. writeCursor++;
  78. if(writeCursor >= frames.length)
  79. writeCursor %= frames.length;
  80. return f;
  81. }
  82. public function dispose() {
  83. for(f in frames)
  84. f.pixels.dispose();
  85. }
  86. }
  87. #end
  88. /**
  89. A video file playback Drawable. Due to platform specifics, each target have their own limitations.
  90. * <span class="label">Hashlink</span>: Playback ability depends on `https://github.com/HeapsIO/hlvideo` library. It support only video with the AV1 codec packed into a WEBM container.
  91. * <span class="label">JavaScript</span>: HTML Video element will be used. Playback is restricted by content-security policy and browser decoder capabilities.
  92. **/
  93. class Video extends Drawable {
  94. #if (hl && hlvideo)
  95. var webm : hl.video.Webm;
  96. var codec : hl.video.Aom.Codec;
  97. var multithread : Bool;
  98. var cache : FrameCache;
  99. var frameCacheSize : Int = 20;
  100. var stopThread = false;
  101. #elseif js
  102. var v : js.html.VideoElement;
  103. var videoPlaying : Bool;
  104. var videoTimeupdate : Bool;
  105. var onReady : Void->Void;
  106. #end
  107. var texture : h3d.mat.Texture;
  108. var tile : h2d.Tile;
  109. var playTime : Float;
  110. var videoTime : Float;
  111. var frameReady : Bool;
  112. var loopVideo : Bool;
  113. /**
  114. Video width. Value is undefined until video is ready to play.
  115. **/
  116. public var videoWidth(default, null) : Int;
  117. /**
  118. Video height. Value is undefined until video is ready to play.
  119. **/
  120. public var videoHeight(default, null) : Int;
  121. /**
  122. Tells if video currently playing.
  123. **/
  124. public var playing : Bool;
  125. /**
  126. Tells current timestamp of the video.
  127. **/
  128. public var time(get, null) : Float;
  129. /**
  130. When enabled, video will loop indefinitely.
  131. **/
  132. public var loop(get, set) : Bool;
  133. /**
  134. Create a new Video instance.
  135. @param parent An optional parent `h2d.Object` instance to which Video adds itself if set.
  136. @param cacheSize <span class="label">Hashlink</span>: async precomputing up to `cache` frame. If 0, synchronized computing
  137. **/
  138. public function new(?parent) {
  139. super(parent);
  140. smooth = true;
  141. }
  142. /**
  143. Sent when there is an error with the decoding or playback of the video.
  144. **/
  145. public dynamic function onError( msg : String ) {
  146. }
  147. /**
  148. Sent when video playback is finished.
  149. **/
  150. public dynamic function onEnd() {
  151. }
  152. @:dox(hide) @:noCompletion
  153. public function get_time() {
  154. #if js
  155. return playing ? v.currentTime : 0;
  156. #else
  157. return playing ? haxe.Timer.stamp() - playTime : 0;
  158. #end
  159. }
  160. @:dox(hide) @:noCompletion
  161. public inline function get_loop() {
  162. return loopVideo;
  163. }
  164. @:dox(hide) @:noCompletion
  165. public function set_loop(value : Bool) : Bool {
  166. #if js
  167. loopVideo = value;
  168. if(v != null)
  169. v.loop = loopVideo;
  170. return loopVideo;
  171. #else
  172. return loopVideo = value;
  173. #end
  174. }
  175. /**
  176. Disposes of the currently playing Video and frees GPU memory.
  177. **/
  178. public function dispose() {
  179. #if (hl && hlvideo)
  180. if( frameCacheSize > 1 ) {
  181. stopThread = true;
  182. while(stopThread)
  183. Sys.sleep(0.01);
  184. }
  185. if( webm != null ) {
  186. webm.close();
  187. webm = null;
  188. }
  189. if( codec != null ) {
  190. codec.close();
  191. codec = null;
  192. }
  193. if( cache != null )
  194. cache.dispose();
  195. cache = null;
  196. #elseif js
  197. if ( v != null ) {
  198. v.removeEventListener("ended", endHandler, true);
  199. v.removeEventListener("error", errorHandler, true);
  200. if (!v.paused) v.pause();
  201. v = null;
  202. }
  203. #end
  204. if( texture != null ) {
  205. texture.dispose();
  206. texture = null;
  207. }
  208. tile = null;
  209. videoWidth = 0;
  210. videoHeight = 0;
  211. time = 0;
  212. playing = false;
  213. frameReady = false;
  214. }
  215. /**
  216. Loads and starts the video playback by specified `path` and calls `onReady` when playback becomes possible.
  217. * <span class="label">Hashlink</span>: Playback being immediately after `loadFile`, unless video was not being able to initialize.
  218. * <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.
  219. @param path The video path. Have to be valid file-system path for HL or valid URL (full or relative) for JS.
  220. @param onReady An optional callback signalling that video is initialized and began the video playback.
  221. **/
  222. public function loadFile( path : String, ?onReady : Void -> Void ) {
  223. dispose();
  224. #if (hl && hlvideo)
  225. webm = hl.video.Webm.fromFile(path);
  226. #elseif js
  227. v = js.Browser.document.createVideoElement();
  228. v.autoplay = true;
  229. v.muted = true;
  230. v.loop = loopVideo;
  231. videoPlaying = false;
  232. videoTimeupdate = false;
  233. this.onReady = onReady;
  234. v.addEventListener("playing", checkReady, true);
  235. v.addEventListener("timeupdate", checkReady, true);
  236. v.addEventListener("ended", endHandler, true);
  237. v.addEventListener("error", errorHandler, true);
  238. v.src = path;
  239. v.play();
  240. #else
  241. onError("Video not supported on this platform");
  242. return;
  243. #end
  244. start();
  245. if( onReady != null ) onReady();
  246. }
  247. /**
  248. Loads and starts the video playback by specified `res` and calls `onReady` when playback becomes possible.
  249. * <span class="label">Hashlink</span>: Playback being immediately after `loadResource`, unless video was not being able to initialize.
  250. * <span class="label">JavaScript</span>: Not implemented
  251. @param res The heaps resource of a valid video file
  252. @param onReady An optional callback signalling that video is initialized and began the video playback.
  253. **/
  254. public function loadResource( res : hxd.res.Resource, ?onReady : Void -> Void ) {
  255. #if (hl && hlvideo)
  256. var e = res.entry;
  257. webm = hl.video.Webm.fromReader(function(offset : Int, len : Int) {
  258. var buf = haxe.io.Bytes.alloc(len);
  259. var n = e.readBytes(buf, 0, offset, len);
  260. return buf;
  261. }, res.entry.size);
  262. webm.availableSize = res.entry.size;
  263. start();
  264. if( onReady != null ) onReady();
  265. #else
  266. onError("Video from resource not supported on this platform");
  267. #end
  268. }
  269. function start() {
  270. #if (hl && hlvideo)
  271. try {
  272. webm.init();
  273. } catch(e:Any) {
  274. onError("Failed to init video : " + e);
  275. return;
  276. }
  277. codec = webm.createCodec();
  278. if(codec == null) {
  279. onError("Can't create codec " + webm.videoCodec);
  280. return;
  281. }
  282. var w = 0, h = 0;
  283. videoWidth = webm.width;
  284. videoHeight = webm.height;
  285. videoTime = 0.;
  286. texture = new h3d.mat.Texture(videoWidth, videoHeight);
  287. tile = h2d.Tile.fromTexture(texture);
  288. var multithread = frameCacheSize > 1;
  289. cache = new FrameCache(multithread ? frameCacheSize : 1, webm.width, webm.height);
  290. if(multithread) {
  291. threadInit();
  292. while(!cache.isFull()) Sys.sleep(0.01);
  293. }
  294. else
  295. loadNextFrame();
  296. playing = true;
  297. playTime = haxe.Timer.stamp();
  298. #end
  299. }
  300. #if js
  301. function errorHandler(e : js.html.Event) {
  302. onError(v.error.code + ": " + v.error.message);
  303. }
  304. function endHandler(e : js.html.Event) {
  305. onEnd();
  306. }
  307. function checkReady(e : js.html.Event) {
  308. if (e.type == "playing") {
  309. videoPlaying = true;
  310. v.removeEventListener("playing", checkReady, true);
  311. } else {
  312. videoTimeupdate = true;
  313. v.removeEventListener("timeupdate", checkReady, true);
  314. }
  315. if (videoPlaying && videoTimeupdate) {
  316. frameReady = true;
  317. videoWidth = v.videoWidth;
  318. videoHeight = v.videoHeight;
  319. texture = new h3d.mat.Texture(videoWidth, videoHeight);
  320. tile = h2d.Tile.fromTexture(texture);
  321. playing = true;
  322. playTime = haxe.Timer.stamp();
  323. videoTime = 0.0;
  324. if ( onReady != null )
  325. {
  326. onReady();
  327. onReady = null;
  328. }
  329. loadNextFrame();
  330. }
  331. }
  332. #end
  333. override function draw(ctx:RenderContext) {
  334. if( tile != null )
  335. ctx.drawTile(this, tile);
  336. }
  337. function loadNextFrame() {
  338. #if (hl && hlvideo)
  339. cache.prepareFrame(webm, codec, loopVideo);
  340. #end
  341. }
  342. #if js
  343. @:access(h3d.mat.Texture)
  344. #end
  345. override function sync(ctx:RenderContext) {
  346. if( !playing )
  347. return;
  348. #if js
  349. if( frameReady && time >= videoTime ) {
  350. texture.alloc();
  351. texture.checkSize(videoWidth, videoHeight, 0);
  352. @:privateAccess cast (@:privateAccess texture.mem.driver, h3d.impl.GlDriver).uploadTextureVideoElement(texture, v, 0, 0);
  353. texture.flags.set(WasCleared);
  354. texture.checkMipMapGen(0, 0);
  355. }
  356. #elseif (hl && hlvideo)
  357. var frame = cache.currentFrame();
  358. if( frame != null && frame.state == Ended )
  359. playing = false;
  360. if( frame != null && frame.state == Ready) {
  361. if(frame.time == 0) {
  362. videoTime = 0;
  363. }
  364. if(haxe.Timer.stamp() - playTime >= frame.time) {
  365. texture.uploadPixels(frame.pixels);
  366. videoTime = frame.time;
  367. cache.nextFrame();
  368. if(frameCacheSize <= 1)
  369. loadNextFrame();
  370. }
  371. }
  372. #end
  373. }
  374. #if (hl && hlvideo)
  375. function threadInit() {
  376. sys.thread.Thread.create(function() {
  377. var first = true;
  378. var finished = false;
  379. while(!stopThread) {
  380. if( cache.isFull() || finished ) {
  381. first = false;
  382. Sys.sleep(0.01);
  383. }
  384. else {
  385. var f = null;
  386. try {
  387. f = cache.prepareFrame(webm, codec, loopVideo);
  388. } catch(e : Dynamic) {
  389. trace(e);
  390. }
  391. if( !loopVideo && (f == null || f.state == Ended) )
  392. finished = true;
  393. }
  394. }
  395. stopThread = false;
  396. // trace("Stopping thread");
  397. });
  398. }
  399. #end
  400. override function getBoundsRec( relativeTo : Object, out : h2d.col.Bounds, forSize : Bool ) {
  401. super.getBoundsRec(relativeTo, out, forSize);
  402. if( tile != null ) addBounds(relativeTo, out, tile.dx, tile.dy, tile.width, tile.height);
  403. }
  404. }