VDecoder.js 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184
  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. this.emit(
  53. "decodeData",
  54. Object.assign(message, { clipId: this.decodingId })
  55. );
  56. if (this.decoding && this.decodingId) {
  57. this.decodeNext(this.decodingId);
  58. }
  59. break;
  60. case "decoderReady":
  61. this.ready = true;
  62. this.emit("ready");
  63. break;
  64. }
  65. });
  66. }
  67. /**
  68. *
  69. * @param {*} rangeArray array [2,100]
  70. */
  71. fetch({ path, range: rangeArray, decode = true }) {
  72. if (!this.ready) {
  73. throw new Error("decoder is not ready");
  74. }
  75. const url = path;
  76. if (!(isArray(rangeArray) && rangeArray.length === 2)) {
  77. throw new Error("range must is an array!");
  78. }
  79. if (this.tempVideos.length > this.maxChip) {
  80. this.flush();
  81. console.log("flush");
  82. }
  83. let rangeFetch = [];
  84. if (rangeArray[0] < 0 || rangeArray[1] < 0) {
  85. console.error("[VDecoder]:range: 非法", `${[rangeArray[0], rangeArray[1]]}`);
  86. return
  87. }
  88. if (rangeArray[0] < rangeArray[1]) {
  89. rangeFetch = range(rangeArray[0], rangeArray[1] + 1);
  90. console.log("[VDecoder]:顺时 +", rangeFetch);
  91. } else {
  92. rangeFetch = range(rangeArray[1], rangeArray[0] + 1).reverse();
  93. console.log("[VDecoder]:逆时 -", rangeFetch);
  94. }
  95. const allFetch = rangeFetch.map((i) => {
  96. return fetch(`${url}/${i}`).then((response) => {
  97. return response.arrayBuffer().then(function (buffer) {
  98. return new Uint8Array(buffer);
  99. });
  100. });
  101. });
  102. return Promise.all(allFetch)
  103. .then((data) => {
  104. const clip = { id: uuidv4(), data: data };
  105. if (data.length > 0) {
  106. this.emit("fetchDone", clip);
  107. this.cacheBuffer = data.slice();
  108. this.tempVideos.push(clip);
  109. console.log("[VDecoder]:获取clip,", clip);
  110. if (decode) {
  111. this.start = Date.now();
  112. this.cacheBufferTotal = clip.data.length;
  113. this.decodeNext(clip.id);
  114. }
  115. return Promise.resolve(clip);
  116. } else {
  117. console.warn("[VDecoder]:fetch取帧为空", rangeFetch);
  118. }
  119. })
  120. .catch((error) => {
  121. console.log("error", error);
  122. });
  123. }
  124. /**
  125. * @param {Uint8Array} h264Nal
  126. */
  127. decode(h264Nal, id) {
  128. this.worker.postMessage(
  129. {
  130. type: "decode",
  131. data: h264Nal.buffer,
  132. offset: h264Nal.byteOffset,
  133. length: h264Nal.byteLength,
  134. renderStateId: id,
  135. },
  136. [h264Nal.buffer]
  137. );
  138. }
  139. decodeNext(clipId) {
  140. const nextFrame = this.cacheBuffer.shift();
  141. this.decodingId = clipId;
  142. this.decoding = true;
  143. let tempId = this.cacheBufferTotal - this.cacheBuffer.length - 1;
  144. if (nextFrame) {
  145. this.decode(nextFrame, tempId);
  146. } else {
  147. console.log("tempVideos", this.tempVideos.length);
  148. const clip = this.tempVideos.find(({ id }) => id === this.decodingId);
  149. if (clip) {
  150. const fps = (1000 / (Date.now() - this.start)) * clip.data.length;
  151. console.log(
  152. `Decoded ${clip.data.length} frames in ${
  153. Date.now() - this.start
  154. }ms @ ${fps >> 0}FPS`
  155. );
  156. } else {
  157. console.warn("不存在clip");
  158. }
  159. this.decoding = false;
  160. // this.decodingId = null;
  161. tempId = 0;
  162. clip && clip.id && this.emit("decodeDone", clip.id);
  163. }
  164. }
  165. flush() {
  166. this.tempVideos = [];
  167. }
  168. preloader(preload) {}
  169. }