VDecoder.js 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199
  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 (
  77. !(
  78. isArray(rangeArray) &&
  79. (rangeArray.length === 1 || rangeArray.length === 2)
  80. )
  81. ) {
  82. throw new Error("range must is an array!");
  83. }
  84. if (this.tempVideos.length > this.maxChip) {
  85. this.flush();
  86. console.log("flush");
  87. }
  88. let rangeFetch = [];
  89. if (rangeArray[0] < 0 || rangeArray[1] < 0) {
  90. console.error(
  91. "[VDecoder]:range: 非法",
  92. `${[rangeArray[0], rangeArray[1]]}`
  93. );
  94. return;
  95. }
  96. if (rangeArray.length > 1) {
  97. // range入口
  98. if (rangeArray[0] < rangeArray[1]) {
  99. rangeFetch = range(rangeArray[0], rangeArray[1] + 1);
  100. console.log("[VDecoder]:顺时 +", rangeFetch);
  101. } else {
  102. rangeFetch = range(rangeArray[1], rangeArray[0] + 1).reverse();
  103. console.log("[VDecoder]:逆时 -", rangeFetch);
  104. }
  105. } else {
  106. // 单例 [i]
  107. rangeFetch = rangeArray
  108. console.log("[VDecoder]:single", rangeFetch);
  109. }
  110. const allFetch = rangeFetch.map((i) => {
  111. return fetch(`${url}/${i}`).then((response) => {
  112. return response.arrayBuffer().then(function (buffer) {
  113. return new Uint8Array(buffer);
  114. });
  115. });
  116. });
  117. return Promise.all(allFetch)
  118. .then((data) => {
  119. const clip = { id: uuidv4(), data: data };
  120. if (data.length > 0) {
  121. this.emit("fetchDone", clip);
  122. this.cacheBuffer = data.slice();
  123. this.tempVideos.push(clip);
  124. console.log("[VDecoder]:获取clip,", clip);
  125. if (decode) {
  126. this.start = Date.now();
  127. this.cacheBufferTotal = clip.data.length;
  128. this.decodeNext(clip.id);
  129. }
  130. return Promise.resolve(clip);
  131. } else {
  132. console.warn("[VDecoder]:fetch取帧为空", rangeFetch);
  133. }
  134. })
  135. .catch((error) => {
  136. console.log("error", error);
  137. });
  138. }
  139. /**
  140. * @param {Uint8Array} h264Nal
  141. */
  142. decode(h264Nal, id) {
  143. this.worker.postMessage(
  144. {
  145. type: "decode",
  146. data: h264Nal.buffer,
  147. offset: h264Nal.byteOffset,
  148. length: h264Nal.byteLength,
  149. renderStateId: id,
  150. },
  151. [h264Nal.buffer]
  152. );
  153. }
  154. decodeNext(clipId) {
  155. const nextFrame = this.cacheBuffer.shift();
  156. this.decodingId = clipId;
  157. this.decoding = true;
  158. let tempId = this.cacheBufferTotal - this.cacheBuffer.length - 1;
  159. if (nextFrame) {
  160. this.decode(nextFrame, tempId);
  161. } else {
  162. console.log("tempVideos", this.tempVideos.length);
  163. const clip = this.tempVideos.find(({ id }) => id === this.decodingId);
  164. if (clip) {
  165. const fps = (1000 / (Date.now() - this.start)) * clip.data.length;
  166. console.log(
  167. `Decoded ${clip.data.length} frames in ${
  168. Date.now() - this.start
  169. }ms @ ${fps >> 0}FPS`
  170. );
  171. } else {
  172. console.warn("不存在clip");
  173. }
  174. this.decoding = false;
  175. // this.decodingId = null;
  176. tempId = 0;
  177. clip && clip.id && this.emit("decodeDone", clip.id);
  178. }
  179. }
  180. flush() {
  181. this.tempVideos = [];
  182. }
  183. preloader(preload) {}
  184. }