mp4box.appendBuffer(ab) returns offset that make an infinite loop

Open
#551 1 comment 0 reactions 0 assignees View on GitHub

Maintainers usually reply within 1 day

Nobody has claimed this yet.

Assessment

Difficulty
3/5
Estimated time
1-2 days
Newbie friendliness
32/100
Issue type
Bug
Clarity
Mostly clear
Activity status
Stale

Research direction

Start from MP4Box.appendBuffer and the sample-extraction path it drives (setExtractionOptions/onSamples/start/stop), since the returned fileStart stops advancing and the read loop ping-pongs between the same two offsets. The 'Invalid box type' warnings logged by BoxParser around the 2 MB mark point at malformed or misaligned box parsing near that chunk boundary, so trace how a partial box at the end of a chunk is buffered and how the next fileStart is computed. Done looks like appendBuffer returning a strictly advancing offset (or the caller reaching flush at EOF) on the reporter's 383 MB file; the payload does not include a failing test or the sample video, so one must be obtained from the reporter first.

Written by the indexing model from the issue text.

Description

bug

I try to ge the timecode from the sample of a video.

When it works, the appendBuffer(ab) returns the end of file and the readNextChunk() stop.

But on some video, it goes in infinite loop even if it correctly called onSamples.

With a 383mb m4v video, the process never exit. If you want the video to debug, tell me.

Output:

node index.js ./cam.m4v
appended 0 1049600
[0:00:00.012] [BoxParser] Invalid box type: ''
[0:00:00.012] [BoxParser] Invalid box type: '*d'
appended 1049600 2099200
{
  "fps": 25
}
appended 2099200 3148800
appended 3148800 2099200
appended 2099200 3148800
appended 3148800 2099200
appended 2099200 3148800
appended 3148800 2099200
appended 2099200 3148800
appended 3148800 2099200
appended 2099200 3148800
appended 3148800 2099200
appended 2099200 3148800
appended 3148800 2099200
appended 2099200 3148800
appended 3148800 2099200
appended 2099200 3148800
... NEVER STOP ...

Here is the code I use:

#!/usr/bin/env node

const fs = require('fs');
const path = require('path');
const MP4Box = require('mp4box');

const getMp4boxMetadata = async file => {
  const mp4box = MP4Box.createFile();

  const { promise, resolve } = Promise.withResolvers();

  mp4box.onError = function (err) {
    error('mp4boxMetadata: failed', JSON.stringify({ file, err }));
    resolve({});
  };

  mp4box.onReady = function (information) {
    const videoTrack = information.tracks.find(t => t.type === 'video');
    let fps;
    if (videoTrack && videoTrack.samples_duration) {
      fps = videoTrack.timescale * videoTrack.nb_samples / videoTrack.samples_duration;
    }

    const tcTrack = information.tracks.find(t => t.type === 'tmcd' || t.codec === 'tmcd' || t.name?.includes('timecode'));

    if (!tcTrack) {
      resolve({ fps });
      return;
    }

    const handle = setTimeout(() => {
      error('No onSamples found', JSON.stringify({ file }));
      resolve({ fps });
    }, 2000);

    mp4box.onSamples = function (trackId, user, samples) {
      clearTimeout(handle);

      if (!samples.length) {
        mp4box.stop();
        resolve({ fps });
        return;
      }

      const sample = samples[0];

      const dataView = new DataView(sample.data.buffer, sample.data.byteOffset, sample.data.byteLength);
      const totalFrames = dataView.getUint32(0, false); // Big-endian

      const timecode = totalFrames / fps;

      const hours = Math.floor(timecode / 3600);
      const minutes = Math.floor((timecode % 3600) / 60);
      const seconds = Math.floor(timecode % 60);
      const frameCount = Math.floor((timecode * fps) % fps);

      mp4box.stop();
      resolve({ fps, timecode, hours, minutes, seconds, frames: frameCount });
    };

    mp4box.setExtractionOptions(tcTrack.id, null, { nbSamples: 1 });
    mp4box.start();
  };

  // Load file chunk by chunk
  let offset = 0;
  const chunkSize = 1025 * 1024;
  const fd = fs.openSync(file, 'r');
  const stats = fs.statSync(file);

  const readNextChunk = () => {
    if (offset >= stats.size) {
      console.log('flushing');
      mp4box.flush();
      fs.closeSync(fd);
      return;
    }

    const buffer = Buffer.alloc(chunkSize);
    const bytesRead = fs.readSync(fd, buffer, 0, chunkSize, offset);

    // The data property needs an ArrayBuffer and fileStart initialized
    const ab = buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + bytesRead);
    ab.fileStart = offset;

    let newOffset = mp4box.appendBuffer(ab);
    console.log('appended', offset, newOffset);

    if (newOffset === offset) {
      console.error('mp4boxMetadata: stuck, newOffset === offset, should not happen', JSON.stringify({ offset, newOffset }));
      newOffset = offset + chunkSize;
    }

    offset = newOffset;

    // Instead of while loop, use setImmediate to not block the event loop and allow callbacks
    setImmediate(readNextChunk);
  };

  readNextChunk();

  return promise;
};

const main = async () => {
  const filePath = process.argv[2];

  if (!filePath) {
    error('Usage: node index.js <path-to-video-file>');
    process.exit(1);
  }

  const resolvedPath = path.resolve(filePath);

  if (!fs.existsSync(resolvedPath)) {
    error(`File not found: ${resolvedPath}`);
    process.exit(1);
  }

  try {
    const result = await getMp4boxMetadata(resolvedPath);
    console.log(JSON.stringify(result, null, 2));
  } catch (err) {
    console.error('An error occurred:', err);
    process.exit(1);
  }
};

main();

Dominant language
TypeScript
Stars
2.5k
Forks
390
Avg merge
1d 17h
Merged PRs (30d)
8

Getting set up

This project ships no dev container, Dockerfile or contributing guide, so setting up is up to you: start from its README, and see our first-contribution guide for the general steps.

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from gpac/mp4box.js

All issues in gpac/mp4box.js

Similar issues

More TypeScript issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.