VDecoder.js 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  1. import EventEmitter from "eventemitter3";
  2. import H264Worker from "web-worker:./h264.worker.js";
  3. import { range, isArray } from "lodash-es";
  4. import { v4 as uuidv4 } from "uuid";
  5. export class VDecoder extends EventEmitter {
  6. constructor({ chunkSize = 256 * 1024, maxChip = 100 }) {
  7. super();
  8. // this.cacheSegmentCount = cacheSegmentCount;
  9. // this.chunkSize = chunkSize;
  10. this.cacheBuffer = [];
  11. this.cacheBufferTotal = null;
  12. this.worker = new H264Worker();
  13. this.initWorker();
  14. this.tempVideos = [];
  15. this.ready = false;
  16. this.decoding = false;
  17. this.decodingId = null;
  18. this.start = null;
  19. this.maxChip = maxChip;
  20. }
  21. static isSupport() {
  22. return !!(
  23. // UC and Quark browser (iOS/Android) support wasm/asm limited,
  24. // its iOS version make wasm/asm performance very slow (maybe hook something)
  25. // its Android version removed support for wasm/asm, it just run pure javascript codes,
  26. // so it is very easy to cause memory leaks
  27. (
  28. !/UCBrowser|Quark/.test(window.navigator.userAgent) &&
  29. window.fetch &&
  30. window.ReadableStream &&
  31. window.Promise &&
  32. window.URL &&
  33. window.URL.createObjectURL &&
  34. window.Blob &&
  35. window.Worker &&
  36. !!new Audio().canPlayType("audio/aac;").replace(/^no$/, "") &&
  37. (window.AudioContext || window.webkitAudioContext)
  38. )
  39. );
  40. }
  41. initWorker() {
  42. this.worker.addEventListener("message", (e) => {
  43. const message =
  44. /** @type {{type:string, width:number, height:number, data:ArrayBuffer, renderStateId:number}} */ e.data;
  45. switch (message.type) {
  46. case "pictureReady":
  47. // onPictureReady(message);
  48. // console.log(
  49. // "[VDecoder]::decodeData",
  50. // Object.assign(message, { clipId: this.decodingId })
  51. // );
  52. const { renderStateId } = e.data;
  53. this.emit(
  54. "decodeData",
  55. Object.assign(message, { clipId: this.decodingId })
  56. );
  57. this.worker.postMessage({
  58. type: "release",
  59. renderStateId: renderStateId,
  60. });
  61. console.log("[VDecoder]::decodeData:release",renderStateId);
  62. if (this.decoding && this.decodingId) {
  63. this.decodeNext(this.decodingId);
  64. }
  65. break;
  66. case "decoderReady":
  67. this.ready = true;
  68. this.emit("ready");
  69. break;
  70. }
  71. });
  72. }
  73. /**
  74. *
  75. * @param {*} array array [2,100]
  76. */
  77. mutiFetch(data) {
  78. if (!(isArray(data) && data.length > 0)) {
  79. throw new Error("range must is an array and has value");
  80. return;
  81. }
  82. if (!(data[0] && "path" in data[0] && "frame" in data[0])) {
  83. throw new Error("props path,frame,vid must have");
  84. return;
  85. }
  86. if (!this.ready) {
  87. throw new Error("decoder is not ready");
  88. return;
  89. }
  90. const allFetch = data.map((item) => {
  91. return fetch(`${item.path}/${item.frame}`).then((response) => {
  92. return response.arrayBuffer().then(function (buffer) {
  93. return new Uint8Array(buffer);
  94. });
  95. });
  96. });
  97. console.log("allFetch", allFetch);
  98. return Promise.all(allFetch)
  99. .then((data) => {
  100. const clip = { id: uuidv4(), data: data };
  101. if (data.length > 0) {
  102. this.emit("fetchDone", clip);
  103. this.cacheBuffer = data.slice();
  104. this.tempVideos.push(clip);
  105. // console.log("[VDecoder]:获取clip,", clip);
  106. this.start = Date.now();
  107. this.cacheBufferTotal = clip.data.length;
  108. this.decodeNext(clip.id);
  109. return Promise.resolve(clip);
  110. } else {
  111. // console.warn("[VDecoder]:fetch取帧为空", rangeFetch);
  112. }
  113. })
  114. .catch((error) => {
  115. console.error("error", error);
  116. });
  117. }
  118. /**
  119. *
  120. * @param {*} rangeArray array [2,100]
  121. */
  122. fetch({ path, range: rangeArray, decode = true }) {
  123. if (!this.ready) {
  124. throw new Error("decoder is not ready");
  125. }
  126. const url = path;
  127. if (
  128. !(
  129. isArray(rangeArray) &&
  130. (rangeArray.length === 1 || rangeArray.length === 2)
  131. )
  132. ) {
  133. throw new Error("range must is an array!");
  134. }
  135. if (this.tempVideos.length > this.maxChip) {
  136. this.flush();
  137. console.log("flush");
  138. }
  139. let rangeFetch = [];
  140. if (rangeArray[0] < 0 || rangeArray[1] < 0) {
  141. console.error(
  142. "[VDecoder]:range: 非法",
  143. `${[rangeArray[0], rangeArray[1]]}`
  144. );
  145. return;
  146. }
  147. if (rangeArray.length > 1) {
  148. // range入口
  149. if (rangeArray[0] < rangeArray[1]) {
  150. rangeFetch = range(rangeArray[0], rangeArray[1] + 1);
  151. console.log("[VDecoder]:顺时 +", rangeFetch);
  152. } else {
  153. rangeFetch = range(rangeArray[1], rangeArray[0] + 1).reverse();
  154. console.log("[VDecoder]:逆时 -", rangeFetch);
  155. }
  156. } else {
  157. // 单例 [i]
  158. rangeFetch = rangeArray;
  159. console.log("[VDecoder]:single", rangeFetch);
  160. }
  161. const allFetch = rangeFetch.map((i) => {
  162. return fetch(`${url}/${i}`).then((response) => {
  163. return response.arrayBuffer().then(function (buffer) {
  164. return new Uint8Array(buffer);
  165. });
  166. });
  167. });
  168. return Promise.all(allFetch)
  169. .then((data) => {
  170. const clip = { id: uuidv4(), data: data };
  171. if (data.length > 0) {
  172. this.emit("fetchDone", clip);
  173. this.cacheBuffer = data.slice();
  174. this.tempVideos.push(clip);
  175. // console.log("[VDecoder]:获取clip,", clip);
  176. if (decode) {
  177. this.start = Date.now();
  178. this.cacheBufferTotal = clip.data.length;
  179. this.decodeNext(clip.id);
  180. }
  181. return Promise.resolve(clip);
  182. } else {
  183. console.warn("[VDecoder]:fetch取帧为空", rangeFetch);
  184. }
  185. })
  186. .catch((error) => {
  187. console.error("error", error);
  188. });
  189. }
  190. /**
  191. * @param {Uint8Array} h264Nal
  192. */
  193. decode(h264Nal, id) {
  194. this.worker.postMessage(
  195. {
  196. type: "decode",
  197. data: h264Nal.buffer,
  198. offset: h264Nal.byteOffset,
  199. length: h264Nal.byteLength,
  200. renderStateId: id,
  201. },
  202. [h264Nal.buffer]
  203. );
  204. }
  205. decodeNext(clipId) {
  206. const nextFrame = this.cacheBuffer.shift();
  207. this.decodingId = clipId;
  208. this.decoding = true;
  209. let tempId = this.cacheBufferTotal - this.cacheBuffer.length - 1;
  210. if (nextFrame) {
  211. this.decode(nextFrame, tempId);
  212. } else {
  213. // console.log("tempVideos", this.tempVideos.length);
  214. // const clip = this.tempVideos.find(({ id }) => id === this.decodingId);
  215. // if (clip) {
  216. // const fps = (1000 / (Date.now() - this.start)) * clip.data.length;
  217. // console.log(
  218. // `Decoded ${clip.data.length} frames in ${
  219. // Date.now() - this.start
  220. // }ms @ ${fps >> 0}FPS`
  221. // );
  222. // } else {
  223. // console.warn("不存在clip");
  224. // }
  225. this.decoding = false;
  226. // this.decodingId = null;
  227. tempId = 0;
  228. // clip && clip.id && this.emit("decodeDone", clip.id);
  229. }
  230. }
  231. flush() {
  232. this.tempVideos = [];
  233. this.cacheBufferTotal = null;
  234. console.warn("flush");
  235. }
  236. preloader(preload) {}
  237. }