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

Writing 1gb file takes about ~4gb ram #385

Closed zenkyuv closed 7 months ago

zenkyuv commented 7 months ago

Is it normal that writing ~1gb file can take about 4-5 gb ram ? I have isolated my code to see whats wrong because i was having problems with memory, and seems like wiriting bit takes most of the memory, though if i let it demux it can take as much as 7-8gb ..., below code is webcodecs tweaked sample code

import {Status} from "../../../../../context/controllers/timeline/types.js"
import MP4Box, {MP4File, MP4Info, MP4MediaTrack, Trak} from "../../../../../tools/mp4boxjs/mp4box.adapter.js"

type SetStatus = (status: Status, message: string) => void
type OnChunk = (chunk: EncodedVideoChunk) => void
type OnConfig = (config: VideoDecoderConfig) => void

export class MP4FileSink {
    #setStatus: SetStatus
    #file: MP4File
    #offset = 0

    constructor(file: MP4File, setStatus: SetStatus) {
        this.#file = file
        this.#setStatus = setStatus
    }

    write(chunk: ArrayBuffer) {
        const buffer = new ArrayBuffer(chunk.byteLength)
        //@ts-ignore
        new Uint8Array(buffer).set(chunk)
        //@ts-ignore
        buffer.fileStart = this.#offset
        this.#offset += buffer.byteLength
        this.#setStatus("fetch", (this.#offset / (1024 ** 2)).toFixed(1) + " MiB")!
        //@ts-ignore
        this.#file!.appendBuffer(buffer)
    }

    close() {
        this.#setStatus("fetch", "Done")
        this.#file.flush()
    }
}
export class MP4Demuxer {
    #onConfig: OnConfig
    #onChunk: OnChunk
    #setStatus: SetStatus
    #file: MP4File
    sample_number = 0
    track: MP4MediaTrack | null = null
    track1: Trak | null = null

    constructor(file: File, {onConfig, onChunk, setStatus}: {onConfig: OnConfig, onChunk: OnChunk, setStatus: SetStatus}) {
        this.#onConfig = onConfig
        this.#onChunk = onChunk
        this.#setStatus = setStatus

        this.#file = MP4Box.createFile()
        // this.#file.onError = error => setStatus("demux", error)
        // this.#file.onReady = this.#onReady.bind(this)
        // this.#file.onSamples = this.#onSamples.bind(this)
        const fileSink = new MP4FileSink(this.#file, setStatus)
        file.stream().pipeTo(new WritableStream(fileSink, {highWaterMark: 1}))
    }

    #description(track: MP4MediaTrack) {
        const trak = this.#file.getTrackById(track.id)
        this.track1 = trak
        for (const entry of trak.mdia!.minf!.stbl!.stsd!.entries) {
        //@ts-ignore
            const box = entry.avcC || entry.hvcC || entry.vpcC || entry.av1C
            if (box) {
                //@ts-ignore
                const stream = new MP4Box.DataStream(undefined, 0, MP4Box.DataStream.BIG_ENDIAN)
                box.write(stream)
                return new Uint8Array(stream.buffer!, 8)  // Remove the box header.
            }
        }
        throw new Error("avcC, hvcC, vpcC, or av1C box not found")
    }

    #onReady(info: MP4Info) {
        this.#setStatus("demux", "Ready")
        const track = info.videoTracks[0]
        this.track = track
        this.#onConfig({
            codec: track.codec.startsWith('vp08') ? 'vp8' : track.codec,
            codedHeight: track.video.height,
            codedWidth: track.video.width,
            description: this.#description(track),
        });

        this.#file.setExtractionOptions(track.id)
        this.#file.start()
    }

    #onSamples(track_id: number, ref: any, samples: any) {
        for (const sample of samples) {
            this.#onChunk(new EncodedVideoChunk({
                type: sample.is_sync ? "key" : "delta",
                timestamp: 1e6 * sample.cts / sample.timescale,
                duration: 1e6 * sample.duration / sample.timescale,
                data: sample.data
            }))
            this.#file.flush()
        }
    }
}