wrapper.ts 2.8 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697
  1. import { PromiseResolver } from '@yume-chan/async';
  2. import { AutoDisposable, Disposable, EventEmitter } from '@yume-chan/event';
  3. let worker: Worker | undefined;
  4. let workerReady = false;
  5. const pendingResolvers: PromiseResolver<TinyH264Wrapper>[] = [];
  6. let streamId = 0;
  7. export interface PictureReadyEventArgs {
  8. renderStateId: number;
  9. width: number;
  10. height: number;
  11. data: ArrayBuffer;
  12. }
  13. const PICTURE_READY_SUBSCRIPTIONS = new Map<number, (e: PictureReadyEventArgs) => void>();
  14. function subscribePictureReady(streamId: number, handler: (e: PictureReadyEventArgs) => void): Disposable {
  15. PICTURE_READY_SUBSCRIPTIONS.set(streamId, handler);
  16. return {
  17. dispose() {
  18. PICTURE_READY_SUBSCRIPTIONS.delete(streamId);
  19. }
  20. };
  21. }
  22. export class TinyH264Wrapper extends AutoDisposable {
  23. public readonly streamId: number;
  24. private readonly pictureReadyEvent = new EventEmitter<PictureReadyEventArgs>();
  25. public get onPictureReady() { return this.pictureReadyEvent.event; }
  26. public constructor(streamId: number) {
  27. super();
  28. this.streamId = streamId;
  29. this.addDisposable(subscribePictureReady(streamId, this.handlePictureReady));
  30. }
  31. private handlePictureReady = (e: PictureReadyEventArgs) => {
  32. this.pictureReadyEvent.fire(e);
  33. };
  34. public feed(data: ArrayBuffer) {
  35. worker!.postMessage({
  36. type: 'decode',
  37. data: data,
  38. offset: 0,
  39. length: data.byteLength,
  40. renderStateId: this.streamId,
  41. }, [data]);
  42. }
  43. public override dispose() {
  44. super.dispose();
  45. worker!.postMessage({
  46. type: 'release',
  47. renderStateId: this.streamId,
  48. });
  49. }
  50. }
  51. export function createTinyH264Wrapper(): Promise<TinyH264Wrapper> {
  52. if (!worker) {
  53. worker = new Worker(new URL('./worker.js', import.meta.url));
  54. worker.addEventListener('message', (e) => {
  55. const { data } = e;
  56. switch (data.type) {
  57. case 'decoderReady':
  58. workerReady = true;
  59. for (const resolver of pendingResolvers) {
  60. resolver.resolve(new TinyH264Wrapper(streamId));
  61. streamId += 1;
  62. }
  63. pendingResolvers.length = 0;
  64. break;
  65. case 'pictureReady':
  66. PICTURE_READY_SUBSCRIPTIONS.get(data.renderStateId)?.(data);
  67. break;
  68. }
  69. });
  70. }
  71. if (!workerReady) {
  72. const resolver = new PromiseResolver<TinyH264Wrapper>();
  73. pendingResolvers.push(resolver);
  74. return resolver.promise;
  75. }
  76. const decoder = new TinyH264Wrapper(streamId);
  77. streamId += 1;
  78. return Promise.resolve(decoder);
  79. }