gpac / mp4box.js

JavaScript version of GPAC's MP4Box tool
https://gpac.github.io/mp4box.js/
BSD 3-Clause "New" or "Revised" License
1.95k stars 330 forks source link

save MP4 file using WebCodecs & MP4box #375

Closed chipweinberger closed 8 months ago

chipweinberger commented 8 months ago

version: 0.5.2

Are there any steps I am missing? It's not working. The output file is 9 bytes!

I am doing this in the web browser. Thanks for this great library.

Basic steps:

  1. MP4Box.createFile();
  2. outputFile.addTrack
  3. draw to canvas
  4. createImageBitmap
  5. VideoEncoder.encode
  6. outputFile.addSample
  7. videoEncoder.flush()
  8. outputFile.flush()

I've looked here: https://github.com/gpac/mp4box.js/issues/243

THIS CODE DOES NOT WORK

// Function to export video using MP4Box
this.exportVideo = async () => {

    let duration = 2;
    let fps = 60;
    let encodedFrameCount = 0;
    let outputFile = MP4Box.createFile();

    let videoTrackId = outputFile.addTrack({
        codec: 'avc1.42001E',
        width: gl.canvas.width,
        height: gl.canvas.height,
        timescale: fps,
        duration: fps * duration,
        isAvc: true,
        isFragmented: true
    });

    const videoEncoder = new VideoEncoder({
        output: (chunk, metadata) => {
            const buffer = new Uint8Array(chunk.byteLength);
            chunk.copyTo(buffer);

            outputFile.addSample(videoTrackId, {
                data: buffer,
                is_sync: metadata.type === 'key',
                duration: 1 // You might need to adjust this based on your frame rate
            });

            encodedFrameCount++;
        },
        error: e => console.error(e),
    });

    videoEncoder.configure({
        codec: 'avc1.42001E',
        width: gl.canvas.width,
        height: gl.canvas.height,
        bitrate: 2_000_000,
        framerate: fps
    });

    const renderAndCaptureFrame = async (frameIndex, videoEncoder) => {
        this.drawToCanvas(frameIndex / fps);
        const bitmap = await createImageBitmap(gl.canvas);
        const videoFrame = new VideoFrame(bitmap, { timestamp: frameIndex / 60 * 1000 });
        await videoEncoder.encode(videoFrame);
        videoFrame.close();
        bitmap.close();
    };

    for (let frame = 0; frame < fps * duration; frame++) {
        await renderAndCaptureFrame(frame, videoEncoder);
    }

    await videoEncoder.flush();
    videoEncoder.close();

    function downloadBlob(blob, filename) {
        const url = URL.createObjectURL(blob);
        const a = document.createElement('a');
        a.href = url;
        a.download = filename;
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
        URL.revokeObjectURL(url);
    }

    let blob = new Blob([outputFile.flush()], { type: 'video/mp4' });
    downloadBlob(blob, 'exportedVideo.mp4');
};
chipweinberger commented 8 months ago

got it working. needed to use file.save, and copy some more options around.

this.exportVideo = async () => {
    // Initialization
    let w = 320;
    let h = 240;
    let fps = 60;
    let durationInSeconds = 2;
    let totalFrames = fps * durationInSeconds;

    // MP4Box Setup
    let file = MP4Box.createFile();
    let trackOptions = {
        timescale: 1_000_000,
        width: w,
        height: h,
        nb_samples: totalFrames,
        codec: 'avc1.42001E',
    };
    let sampleOptions = { duration: 1_000_000 / fps };
    let trackId = null;

    // Configure Video Encoder
    let videoEncoder = new VideoEncoder({
        output: (chunk, config) => {
            let buffer = new ArrayBuffer(chunk.byteLength);
            chunk.copyTo(buffer);

            if (trackId === null) {
                trackOptions.avcDecoderConfigRecord = config.decoderConfig.description;
                trackId = file.addTrack(trackOptions);
            }

            sampleOptions.dts = chunk.timestamp * 1_000;
            sampleOptions.cts = chunk.timestamp * 1_000;
            sampleOptions.is_sync = chunk.type === 'key';
            file.addSample(trackId, buffer, sampleOptions);
        },
        error: error => console.error('VideoEncoder error:', error)
    });

    await videoEncoder.configure({
        codec: 'avc1.42001E',
        width: w,
        height: h,
        bitrate: 1_000_000,
        framerate: fps
    });

    // Render and Encode Frames
    for (let frameIndex = 0; frameIndex < totalFrames; frameIndex++) {
        this.setTime(frameIndex / fps);
        this.draw();
        let bitmap = await createImageBitmap(gl.canvas);
        let videoFrame = new VideoFrame(bitmap, { timestamp: 1_000 * frameIndex / fps});
        videoEncoder.encode(videoFrame);
        videoFrame.close();
        bitmap.close();
    }

    // Wait for the encoder to finish
    await videoEncoder.flush();

    // Save the file
    file.save('exportedVideo.mp4');
};