Js uses ffmpeg to add png or gif to video

Js uses ffmpeg to add png or gif to the video

ffmpeg

The usage scenario is to edit the video on the web and add pictures and gifs.

Note:

All the following use cases are based on vue3 setup.

At the same time, different @ffmpeg versions will lead to different APIs used. You need to pay attention to @ffmpeg version issues before using the case.

If you are using 0.12+ you need to use the new API, please see the documentation for details

npm

npm install @ffmpeg/ffmpeg@^0.11.0

npm install @ffmpeg/core@^0.11.0

Video add png

<template></template>

<script setup>
import {<!-- --> ref, onUnmounted, onMounted } from 'vue'
import {<!-- --> createFFmpeg, fetchFile } from '@ffmpeg/ffmpeg';

const ffmpeg = createFFmpeg({<!-- --> log: true });
const fileType = ref("") //Video file type

/**
 * Combine pictures into videos
 * @param {string} url video online address
 * @param {object} picItem picture material object
 * @param {string} picItem.startT The start time when the picture material appears
 * @param {number} picItem.duration The appearance duration of the material
 * @param {number} picItem.scale The magnification ratio of the material
 * @param {string} picItem.url picture material url address
 * @param {number} picItem.x The distance between the material and the top of the video
 * @param {number} picItem.y The distance between the material and the left side of the video
 * @return {Promise<{outputName: string, fileUrl: string}> | undefined}
 */
const videoCompose = async (url, picItem) => {<!-- -->
    if (!ffmpeg.isLoaded()) {<!-- -->
        await ffmpeg.load();
    }
    if (!url) return;

    const {<!-- --> duration, scale, startT, url: picUrl, x, y } = picItem;
    fileType.value = url.split(".").pop();
    const inputName = `input.${<!-- -->fileType.value}`;
    const outputName = `output.${<!-- -->fileType.value}`;
    const imageType = picUrl.split(".").pop();
    const imageFileName = `image.${<!-- -->imageType}`;

    await ffmpeg.FS('writeFile', inputName, await fetchFile(url));
    await ffmpeg.FS('writeFile', imageFileName, await fetchFile(picUrl));

    //Run FFmpeg command
    try {<!-- -->
        await ffmpeg.run(
            `-i`, `${<!-- -->inputName}`,
            `-i`, `${<!-- -->imageFileName}`,
            `-filter_complex`, `[1:v]scale=iw*${<!-- -->(scale).toFixed(1)}:ih*${<!-- -->(scale).toFixed (1)}[scaled];[0:v][scaled]overlay=${<!-- -->x}:${<!-- -->y}:enable='between(t, ${<!-- --> + startT},${<!-- --> + startT + duration})'`,
            `${<!-- -->outputName}`,
            "-hide_banner"
        );

        //Read the output file
        let arrayBuffer = ffmpeg.FS('readFile', outputName).buffer; // Read cache

        //Create a download link and download and save it locally through callback
        const fileUrl = URL.createObjectURL(new Blob([arrayBuffer])); // Convert to Blob URL

        // release memory
        ffmpeg.FS('unlink', inputName);
        ffmpeg.FS('unlink', outputName);

        return {<!-- -->
            fileUrl,
            outputName
        };
    } catch (e) {<!-- -->
        console.log(e);
    }
}

const downloadFile = (url, fileName = `clip.mp4`) => {<!-- -->
    const link = document.createElement('a');
    link.href = url;
    link.download = fileName;
    link.click();
}

onMounted(async () => {<!-- -->
    const {<!-- -->fileUrl} = await videoCompose("http://xxx.mp4", {<!-- -->
        duration: 3,
        scale: 1,
        startT: "0.00",
        url: 'http://xxx.png',
        x: 100,
        y: 100
    })
    downloadFile(fileUrl)
})

onUnmounted(() => {<!-- -->
    ffmpeg.exit();
})
</script>

Add gif to video

The process is similar to adding a picture, but the commands for adding filters are different.

/*
 Perform partial replacement of FFmpeg command
 
 `-ignore_loop`, `0` makes the gif image play in a loop, otherwise it will only play once
 `-itsoffset`, `${ + startT}` The time when the gif image appears in the video
 fade=t=in:st=${ + startT}:d=1:alpha=1[wm]; The time when the gif picture fades in the video
 :shortest=1 The length of the video is the initial video length. Otherwise, the video time will increase due to the addition of gif.
 :enable='between(t,${ + startT},${ + startT + duration})' The appearance time of gif
 "-hide_banner" hides some information of ffmpeg
*/
await ffmpeg.run(
                `-i`, `${<!-- -->inputName}`,
                `-ignore_loop`, `0`,
                `-itsoffset`, `${<!-- --> + startT}`,
                `-i`, `${<!-- -->imageFileName}`,
                `-filter_complex`, `[0:0]scale=iw:ih[a];[1:0]scale=iw*${<!-- -->(scale).toFixed(1)}:ih* ${<!-- -->(scale).toFixed(1)},fade=t=in:st=${<!-- --> + startT}:d=1:alpha=1[wm] ;[a][wm]overlay=x=${<!-- -->x}:y=${<!-- -->y}:shortest=1:enable='between(t,$ {<!-- --> + startT},${<!-- --> + startT + duration})'`,
                `${<!-- -->outputName}`,
                "-hide_banner"
            );

Integration

You can judge the type of image when adding and execute different adding logic.

/**
 * Combine pictures into videos
 * @param {string} url video online address
 * @param {object} picItem picture material object
 * @param {string} picItem.startT The start time when the picture material appears
 * @param {number} picItem.duration The appearance duration of the material
 * @param {number} picItem.scale The magnification ratio of the material
 * @param {string} picItem.url picture material url address
 * @param {number} picItem.x The distance between the material and the top of the video
 * @param {number} picItem.y The distance between the material and the left side of the video
 * @return {Promise<{outputName: string, fileUrl: string}> | undefined}
 */
const videoCompose = async (url, picItem) => {<!-- -->
    if (!ffmpeg.isLoaded()) {<!-- -->
        await ffmpeg.load();
    }
    if (!url) return;

    const {<!-- -->duration, scale, startT, url: picUrl, x, y} = picItem;
    const type = url.split(".").pop();
    const inputName = `input.${<!-- -->type}`;
    const outputName = `output.${<!-- -->type}`;
    const imageType = picUrl.split(".").pop();
    const imageFileName = `image.${<!-- -->imageType}`;

    // Save the input file to the virtual file system
    if (url.startsWith('blob:')) {<!-- -->
        // Handle Blob URL
        const arrayBuffer = await fetchBlobAsArrayBuffer(url);
        ffmpeg.FS('writeFile', inputName, new Uint8Array(arrayBuffer));
    } else if (url.startsWith('http://') || url.startsWith('https://')) {<!-- -->
        // handle network address
        await ffmpeg.FS('writeFile', inputName, await fetchFile(url));
    }
    await ffmpeg.FS('writeFile', imageFileName, await fetchFile(picUrl));

    //Run FFmpeg command
    try {<!-- -->
        if (imageType === 'gif') {<!-- -->
            await ffmpeg.run(
                `-i`, `${<!-- -->inputName}`,
                `-ignore_loop`, `0`,
                `-itsoffset`, `${<!-- --> + startT}`,
                `-i`, `${<!-- -->imageFileName}`,
                `-filter_complex`, `[0:0]scale=iw:ih[a];[1:0]scale=iw*${<!-- -->(scale).toFixed(1)}:ih* ${<!-- -->(scale).toFixed(1)},fade=t=in:st=${<!-- --> + startT}:d=1:alpha=1[wm] ;[a][wm]overlay=x=${<!-- -->x}:y=${<!-- -->y}:shortest=1:enable='between(t,$ {<!-- --> + startT},${<!-- --> + startT + duration})'`,
                `${<!-- -->outputName}`,
                "-hide_banner"
            );
        } else {<!-- -->
            await ffmpeg.run(
                `-i`, `${<!-- -->inputName}`,
                `-i`, `${<!-- -->imageFileName}`,
                `-filter_complex`, `[1:v]scale=iw*${<!-- -->(scale).toFixed(1)}:ih*${<!-- -->(scale).toFixed (1)}[scaled];[0:v][scaled]overlay=${<!-- -->x}:${<!-- -->y}:enable='between(t, ${<!-- --> + startT},${<!-- --> + startT + duration})'`,
                `${<!-- -->outputName}`,
                "-hide_banner"
            );
        }

        //Read the output file
        let arrayBuffer = ffmpeg.FS('readFile', outputName).buffer; // Read cache

        //Create a download link and download and save it locally through callback
        const fileUrl = URL.createObjectURL(new Blob([arrayBuffer])); // Convert to Blob URL

        // release memory
        ffmpeg.FS('unlink', inputName);
        ffmpeg.FS('unlink', outputName);

        return {<!-- -->
            fileUrl,
            outputName
        };
    } catch (e) {<!-- -->
        console.log(e);
    }
}