demuxer_mp4.js 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109
  1. import MP4Box from 'https://cdn.jsdelivr.net/npm/mp4box@0.5.3/+esm';
  2. // From: https://w3c.github.io/webcodecs/samples/video-decode-display/
  3. // Wraps an MP4Box File as a WritableStream underlying sink.
  4. class MP4FileSink {
  5. #setStatus = null;
  6. #file = null;
  7. #offset = 0;
  8. constructor(file, setStatus) {
  9. this.#file = file;
  10. this.#setStatus = setStatus;
  11. }
  12. write(chunk) {
  13. // MP4Box.js requires buffers to be ArrayBuffers, but we have a Uint8Array.
  14. const buffer = new ArrayBuffer(chunk.byteLength);
  15. new Uint8Array(buffer).set(chunk);
  16. // Inform MP4Box where in the file this chunk is from.
  17. buffer.fileStart = this.#offset;
  18. this.#offset += buffer.byteLength;
  19. // Append chunk.
  20. this.#setStatus("fetch", (this.#offset / (1024 ** 2)).toFixed(1) + " MiB");
  21. this.#file.appendBuffer(buffer);
  22. }
  23. close() {
  24. this.#setStatus("fetch", "Done");
  25. this.#file.flush();
  26. }
  27. }
  28. // Demuxes the first video track of an MP4 file using MP4Box, calling
  29. // `onConfig()` and `onChunk()` with appropriate WebCodecs objects.
  30. export class MP4Demuxer {
  31. #onConfig = null;
  32. #onChunk = null;
  33. #setStatus = null;
  34. #file = null;
  35. constructor(uri, {onConfig, onChunk, setStatus}) {
  36. this.#onConfig = onConfig;
  37. this.#onChunk = onChunk;
  38. this.#setStatus = setStatus;
  39. // Configure an MP4Box File for demuxing.
  40. this.#file = MP4Box.createFile();
  41. this.#file.onError = error => setStatus("demux", error);
  42. this.#file.onReady = this.#onReady.bind(this);
  43. this.#file.onSamples = this.#onSamples.bind(this);
  44. // Fetch the file and pipe the data through.
  45. const fileSink = new MP4FileSink(this.#file, setStatus);
  46. fetch(uri).then(response => {
  47. // highWaterMark should be large enough for smooth streaming, but lower is
  48. // better for memory usage.
  49. response.body.pipeTo(new WritableStream(fileSink, {highWaterMark: 2}));
  50. });
  51. }
  52. // Get the appropriate `description` for a specific track. Assumes that the
  53. // track is H.264, H.265, VP8, VP9, or AV1.
  54. #description(track) {
  55. const trak = this.#file.getTrackById(track.id);
  56. for (const entry of trak.mdia.minf.stbl.stsd.entries) {
  57. const box = entry.avcC || entry.hvcC || entry.vpcC || entry.av1C;
  58. if (box) {
  59. const stream = new MP4Box.DataStream(undefined, 0, MP4Box.DataStream.BIG_ENDIAN);
  60. box.write(stream);
  61. return new Uint8Array(stream.buffer, 8); // Remove the box header.
  62. }
  63. }
  64. throw new Error("avcC, hvcC, vpcC, or av1C box not found");
  65. }
  66. #onReady(info) {
  67. this.#setStatus("demux", "Ready");
  68. const track = info.videoTracks[0];
  69. // Generate and emit an appropriate VideoDecoderConfig.
  70. this.#onConfig({
  71. // Browser doesn't support parsing full vp8 codec (eg: `vp08.00.41.08`),
  72. // they only support `vp8`.
  73. codec: track.codec.startsWith('vp08') ? 'vp8' : track.codec,
  74. codedHeight: track.video.height,
  75. codedWidth: track.video.width,
  76. description: this.#description(track),
  77. });
  78. // Start demuxing.
  79. this.#file.setExtractionOptions(track.id);
  80. this.#file.start();
  81. }
  82. #onSamples(track_id, ref, samples) {
  83. // Generate and emit an EncodedVideoChunk for each demuxed sample.
  84. for (const sample of samples) {
  85. this.#onChunk(new EncodedVideoChunk({
  86. type: sample.is_sync ? "key" : "delta",
  87. timestamp: 1e6 * sample.cts / sample.timescale,
  88. duration: 1e6 * sample.duration / sample.timescale,
  89. data: sample.data
  90. }));
  91. }
  92. }
  93. }