Js使用ffmpeg在视频中添加png或gif

Js使用ffmpeg在视频中添加png或gif

ffmpeg

使用场景是需要在web端对视频进行编辑 添加图片和gif。

注意:

以下所有的使用案例均基于vue3 setup。

同时由于@ffmpeg版本不同会导致使用的api不同,使用案例前需要注意@ffmpeg版本问题

如果使用的是0.12+需要使用新的api,详情请看 文档

npm

bash 复制代码
npm install @ffmpeg/ffmpeg@^0.11.0

npm install @ffmpeg/core@^0.11.0

视频添加png

html 复制代码
<template></template>

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

const ffmpeg = createFFmpeg({ log: true });
const fileType = ref("") // 视频文件类型

/**
 * 将图片合成到视频中
 * @param {string} url 视频在线地址
 * @param {object} picItem 图片素材对象
 * @param {string} picItem.startT 图片素材出现的开始时间
 * @param {number} picItem.duration 素材的出现持续时间
 * @param {number} picItem.scale 素材的放大比例
 * @param {string} picItem.url 图片素材url地址
 * @param {number} picItem.x 素材离视频顶部的距离
 * @param {number} picItem.y 素材离视频左侧的距离
 * @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));

    // 运行 FFmpeg 命令
    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"
        );

        // 读取输出文件
        let arrayBuffer = ffmpeg.FS('readFile', outputName).buffer; // 读取缓存

        // 创建下载链接并通过回调下载保存到本地
        const fileUrl = URL.createObjectURL(new Blob([arrayBuffer])); // 转为Blob URL

        // 释放内存
        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>

视频添加gif

流程与添加图片类似,但添加滤镜的命令不相同。

js 复制代码
/*
 执行FFmpeg命令的部分替换
 
 `-ignore_loop`, `0` 让gif图片循环播放 否则只播放一次
 `-itsoffset`, `${+startT}` gif图片在视频中出现时间
 fade=t=in:st=${+startT}:d=1:alpha=1[wm]; gif图片在视频中淡入的时间
 :shortest=1 视频的时长为初始视频时长 否则由于gif添加会导致视频时间增长
 :enable='between(t,${+startT},${+startT + duration})' gif的出现时间
 "-hide_banner" 隐藏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"
            );

整合

可以在添加时对图片的类型进行判断,执行不同的添加逻辑

js 复制代码
/**
 * 将图片合成到视频中
 * @param {string} url 视频在线地址
 * @param {object} picItem 图片素材对象
 * @param {string} picItem.startT 图片素材出现的开始时间
 * @param {number} picItem.duration 素材的出现持续时间
 * @param {number} picItem.scale 素材的放大比例
 * @param {string} picItem.url 图片素材url地址
 * @param {number} picItem.x 素材离视频顶部的距离
 * @param {number} picItem.y 素材离视频左侧的距离
 * @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}`;

    // 将输入文件保存到虚拟文件系统
    if (url.startsWith('blob:')) {
        // 处理 Blob URL
        const arrayBuffer = await fetchBlobAsArrayBuffer(url);
        ffmpeg.FS('writeFile', inputName, new Uint8Array(arrayBuffer));
    } else if (url.startsWith('http://') || url.startsWith('https://')) {
        // 处理网络地址
        await ffmpeg.FS('writeFile', inputName, await fetchFile(url));
    }
    await ffmpeg.FS('writeFile', imageFileName, await fetchFile(picUrl));

    // 运行 FFmpeg 命令
    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"
            );
        }

        // 读取输出文件
        let arrayBuffer = ffmpeg.FS('readFile', outputName).buffer; // 读取缓存

        // 创建下载链接并通过回调下载保存到本地
        const fileUrl = URL.createObjectURL(new Blob([arrayBuffer])); // 转为Blob URL

        // 释放内存
        ffmpeg.FS('unlink', inputName);
        ffmpeg.FS('unlink', outputName);

        return {
            fileUrl,
            outputName
        };
    } catch (e) {
        console.log(e);
    }
}
相关推荐
酒尘&1 小时前
JS数组不止Array!索引集合类全面解析
开发语言·前端·javascript·学习·js
用户47949283569153 小时前
"讲讲原型链" —— 面试官最爱问的 JavaScript 基础
前端·javascript·面试
用户47949283569153 小时前
2025 年 TC39 都在忙什么?Import Bytes、Iterator Chunking 来了
前端·javascript·面试
音视频牛哥3 小时前
Android音视频开发:基于 Camera2 API 实现RTMP推流、RTSP服务与录像一体化方案
android·音视频·安卓camera2推流·安卓camera2推送rtmp·安卓camera2 rtsp·安卓camera2录制mp4·安卓实现ipc摄像头
大怪v4 小时前
【Virtual World 04】我们的目标,无限宇宙!!
前端·javascript·代码规范
蓝瑟7 小时前
告别重复造轮子!业务组件多场景复用实战指南
前端·javascript·设计模式
summerkissyou19877 小时前
android13-audio-AudioTrack-写数据流程
android·音视频
渴望成为python大神的前端小菜鸟8 小时前
浏览器及其他 面试题
前端·javascript·ajax·面试题·浏览器
卢卡上学8 小时前
【AI工具】Coze智能体工作流:5分钟制作10个10w+治愈视频,无需拍摄剪辑
人工智能·音视频·ai视频·ai智能体
1024肥宅8 小时前
手写 new 操作符和 instanceof:深入理解 JavaScript 对象创建与原型链检测
前端·javascript·ecmascript 6