Vanilagy / mp4-muxer

MP4 multiplexer in pure TypeScript with support for WebCodecs API, video & audio.
https://vanilagy.github.io/mp4-muxer/demo
MIT License
419 stars 32 forks source link

TypeError: track.currentChunk.sampleData is not iterable #28

Closed erwanvivien closed 9 months ago

erwanvivien commented 9 months ago

Hey, I'm getting a stack trace error, kinda randomly here

    const muxer = new Mp4Muxer({
        target: new FileSystemWritableFileStreamTarget(fileStream),
        video: {
            codec: 'avc',
            width: params.width,
            height: params.height,
        },
        fastStart: false,
    });

    const encodeFrame = (chunk, meta) => {
        muxer.addVideoChunk(chunk, meta, chunk.timestamp) // I'm making adjustments to timestamp
    }

    const videoEncoder = new VideoEncoder({
        output: (chunk, meta) => {
            encodeFrame(chunk, meta);
            encodedFrameCount += 1;

            if (encodedFrameCount >= frameCount) {
                videoEncoder.flush().then(() => {
                    console.log('flushed video')
                    muxer.finalize()
                })
            }
        },
        error: (error) => {
            console.error('error', error);
        },
    });

    // The videoEncoder.configure()
    // The calls to videoEncoder.encode()
Uncaught (in promise) TypeError: track.currentChunk.sampleData is not iterable
    at Muxer.finalizeCurrentChunk_fn (mp4-muxer.js?v=ff8ce078:1250:41)
    at Muxer.finalize (mp4-muxer.js?v=ff8ce078:958:77)
    at finalize (encode.ts:38:31)
    at encode.ts:48:25

Do you know where it could come from?

Thanks for the work, it's greatly appreciated!

Vanilagy commented 9 months ago

Hi, thank you and thanks for the report!

Could it be that you're calling finalize on the muxer multiple times? It seems like your code would allow that, since finalize is called inside a callback that gets called multiple times. I've looked through my code and can't figure out another way finalizeCurrentChunk could be called with sampleData already set to null.

Vanilagy commented 9 months ago

I would advise you to use a different pattern to flush the encoder. Instead of what you're doing right now, simply flush the encoder once you've encoded the last frame (not when the encoding is complete). Flush returns a Promise that will not resolve until all pending frames have finished encoding, so you can safely wait for its resolution and then call finalize on the muxer.

erwanvivien commented 9 months ago

Thanks for your response!

Instead of what you're doing right now, simply flush the encoder once you've encoded the last frame

I feel like this is what I'm doing 🤔 frameCount is the decodedFrameCount that I retrieve from the demuxer and only when encodedFrameCount is above this count I try to trigger the videoEncoder.flush

Do you mean when I've sent all the decodes?

Something like this:

    const muxer = new Mp4Muxer({
        target: new FileSystemWritableFileStreamTarget(fileStream),
        video: {
            codec: 'avc',
            width: params.width,
            height: params.height,
        },
        fastStart: false,
    });

    const encodeFrame = (chunk, meta) => {
        muxer.addVideoChunk(chunk, meta, chunk.timestamp) // I'm making adjustments to timestamp
    }

    const videoEncoder = new VideoEncoder({
        output: encodeFrame,
        error: (error) => {
            console.error('error', error);
        },
    });

    // The videoEncoder.configure()
    // All the calls to videoEncoder.encode()
    // then await videoEncoder.flush()
    // then muxer.finalize() ?  
erwanvivien commented 9 months ago

Also it might be me having a race condition, indeed, because I'm having also the AudioEncoder (that I omitted thinking it wasn't relevant) So I have two spots where the muxer.finalize could be

This is the total code const { finalize: muxerFinalize, encodeFrame, encodeAudio } = muxers[metadata.kind](fileStream, videoConfig, { width: 640, height: 360 }); let encodedFrameCount = 0; let encodedSampleCount = 0; let resolve: () => void = () => { } let reject: (error: DOMException) => void = () => { } const close = new Promise((res, rej) => { resolve = res reject = rej }) const { audio: { sampleCount }, video: { frameCount } } = metadata const videoEncoder = new VideoEncoder({ output: (chunk, meta) => { encodeFrame(chunk, meta); encodedFrameCount += 1; if (encodedFrameCount % 100 === 0) { console.log('encoded', encodedFrameCount, 'frames out of ', frameCount); } callback(encodedFrameCount / frameCount) if (encodedFrameCount >= frameCount) { videoEncoder.flush().then(() => { console.log('flushed video') if (encodedSampleCount >= sampleCount) { muxerFinalize() resolve() } }) } }, error: (error) => { console.error('error', error); reject(error) }, }); const audioEncoder = new AudioEncoder({ output: (chunk, meta) => { encodeAudio(chunk, meta); encodedSampleCount += 1; if (encodedSampleCount % 100 === 0) { console.log('encoded', encodedSampleCount, 'samples out of ', sampleCount); } if (encodedSampleCount + 1 >= sampleCount) { audioEncoder.flush().then(() => { console.log('flushed audio') if (encodedFrameCount >= frameCount) { muxerFinalize() resolve() } }) } }, error: (error) => { console.error('error', error) reject(error) } }); const audioConfig: AudioEncoderConfig = { ...metadata.audio, bitrate: 128000, // Default to 128kbps } for (const validBitrates of [96000, 128000, 160000, 192000]) { if (validBitrates >= metadata.audio.bitrate) { audioConfig.bitrate = validBitrates; break; } } videoEncoder.configure(videoConfig); audioEncoder.configure(audioConfig); return { videoEncoder, audioEncoder, close: () => close };
Vanilagy commented 9 months ago

Do you mean when I've sent all the decodes?

Something like this:

Yes, exactly! That's all you need to do, you do not need to flush the encoder inside the callback.

When you have two encoders, it's just:

await videoEncoder.flush();
await audioEncoder.flush();
muxer.finalize();
erwanvivien commented 9 months ago

Ok, will try and get back to you, huge thanks 💜

erwanvivien commented 9 months ago

Ah I closed, but anyway I don't think the issue comes from this lib :)

Vanilagy commented 9 months ago

Alright, let me know if you encounter anything else.