index.ts 3.2 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798
  1. import type { H264Configuration, ScrcpyVideoStreamPacket } from '@yume-chan/scrcpy';
  2. import { WritableStream } from '@yume-chan/stream-extra';
  3. function toHex(value: number) {
  4. return value.toString(16).padStart(2, '0').toUpperCase();
  5. }
  6. export class WebCodecsDecoder {
  7. // Usually, browsers can decode most configurations,
  8. // So let device choose best profile and level for itself.
  9. public readonly maxProfile = undefined;
  10. public readonly maxLevel = undefined;
  11. private _writable: WritableStream<ScrcpyVideoStreamPacket>;
  12. public get writable() { return this._writable; }
  13. private _renderer: HTMLCanvasElement;
  14. public get renderer() { return this._renderer; }
  15. private _frameRendered = 0;
  16. public get frameRendered() { return this._frameRendered; }
  17. private context: CanvasRenderingContext2D;
  18. private decoder: VideoDecoder;
  19. // Limit FPS to system refresh rate
  20. private lastFrame: VideoFrame | undefined;
  21. private animationFrame: number = 0;
  22. public constructor() {
  23. this._renderer = document.createElement('canvas');
  24. this.context = this._renderer.getContext('2d', { willReadFrequently: true })!;
  25. this.decoder = new VideoDecoder({
  26. output: (frame) => {
  27. if (this.lastFrame) {
  28. this.lastFrame.close();
  29. }
  30. this.lastFrame = frame;
  31. if (!this.animationFrame) {
  32. // Start render loop on first frame
  33. this.render();
  34. }
  35. },
  36. error() { },
  37. });
  38. this._writable = new WritableStream<ScrcpyVideoStreamPacket>({
  39. write: async (packet) => {
  40. switch (packet.type) {
  41. case 'configuration':
  42. this.configure(packet.data);
  43. break;
  44. case 'frame':
  45. this.decoder.decode(new EncodedVideoChunk({
  46. // Treat `undefined` as `key`, otherwise won't decode.
  47. type: packet.keyframe === false ? 'delta' : 'key',
  48. timestamp: 0,
  49. data: packet.data,
  50. }));
  51. break;
  52. }
  53. }
  54. });
  55. }
  56. private render = () => {
  57. if (this.lastFrame) {
  58. this._frameRendered += 1;
  59. this.context.drawImage(this.lastFrame, 0, 0);
  60. this.lastFrame.close();
  61. this.lastFrame = undefined;
  62. }
  63. this.animationFrame = requestAnimationFrame(this.render);
  64. };
  65. private configure(config: H264Configuration) {
  66. const { profileIndex, constraintSet, levelIndex } = config;
  67. this._renderer.width = config.croppedWidth;
  68. this._renderer.height = config.croppedHeight;
  69. // https://www.rfc-editor.org/rfc/rfc6381#section-3.3
  70. // ISO Base Media File Format Name Space
  71. const codec = `avc1.${[profileIndex, constraintSet, levelIndex].map(toHex).join('')}`;
  72. this.decoder.configure({
  73. codec: codec,
  74. optimizeForLatency: true,
  75. });
  76. }
  77. public dispose() {
  78. cancelAnimationFrame(this.animationFrame);
  79. this.decoder.close();
  80. }
  81. }