Js使用ffmpeg进行视频剪辑和画面截取

文章介绍了如何在Vue3项目中使用ffmpeg库对在线和本地视频进行裁剪、截图,并提供了代码示例,强调了在不同ffmpeg版本下API的选择和性能优化的重要性。

ffmpeg

使用场景是需要在web端进行视频的裁剪,包括使用 在线视频url本地视频文件 的裁剪,以及对视频内容的截取等功能。

前端进行视频操作可能会导致性能下降,最好通过后端使用java,c++进行处理,本文的案例是备选方案。


注意:

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

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

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


npm

npm install @ffmpeg/ffmpeg@^0.10.0

npm install @ffmpeg/core@^0.10.0

在线视频url剪辑

<script setup>
// "@ffmpeg/core": "^0.10.0",
// "@ffmpeg/ffmpeg": "^0.10.0",

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

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

/**
 * 根据在线的视频地址截取片段
 * @param {string} url 在线视频链接
 * @param {number|string} startTime 截取开始时间
 * @param {number|string} endTime 截取结束时间
 * @param {Function} callBack 回调函数
 */
const videoCut = async (url, startTime, endTime, callBack) => {
    if (!ffmpeg.isLoaded()) {
        await ffmpeg.load();
    }
    if(!url) return;

    fileType.value = url.split(".").pop()

    const inputName = `input.${fileType.value}`;
    const outputName = `output.${fileType.value}`;

    // 将输入文件保存到虚拟文件系统
    await ffmpeg.FS('writeFile', inputName, await fetchFile(url));

    // 运行 FFmpeg 命令
    try {
        await ffmpeg.run(
            '-ss', `${startTime}`,
            '-t', `${endTime - startTime}`,
            '-i', inputName,
            '-vcodec', 'copy',
            '-acodec', 'copy',
            outputName
        );

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

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

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

const downloadFile = (url, fileName = `clip.${fileType.value}`) => {
    const link = document.createElement('a');
    link.href = url;
    link.download = fileName;
    link.click();
}

onMounted(() => {
    videoCut("https://视频.mp4", 0, 3, downloadFile)
})

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

本地视频文件剪辑

<template>
    <input type="file" @change="fileChange">
</template>

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

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

const fileChange = (e) => {
    if (!e.target.files[0]) return;
    const file = e.target.files[0];
    fileType.value = file.name.split(".").pop()

    videoCut(file, 0, 3, downloadFile)
}

/**
 * 根据选择的视频文件截取片段
 * @param {file} file 选择的视频文件
 * @param {number|string} startTime 截取开始时间
 * @param {number|string} endTime 截取结束时间
 * @param {Function} callBack 回调函数
 */
const videoCut = async (file, startTime, endTime, callBack) => {
    if (!ffmpeg.isLoaded()) {
        await ffmpeg.load();
    }
    if(!file) return;

    const inputName = `input.${fileType.value}`;
    const outputName = `output.${fileType.value}`;

    const orgFileBuffer = await file.arrayBuffer()

    // 将输入文件保存到虚拟文件系统
    await ffmpeg.FS('writeFile', inputName, await fetchFile(new Blob([orgFileBuffer])));

    try {
        await ffmpeg.run(
            '-ss', `${startTime}`,
            '-t', `${endTime - startTime}`,
            '-i', inputName,
            '-vcodec', 'copy',
            '-acodec', 'copy',
            outputName
        );

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

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

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

const downloadFile = (url, fileName = `clip.${fileType.value}`) => {
    const link = document.createElement('a');
    link.href = url;
    link.download = fileName;
    link.click();
}

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

获取视频画面截图

<template>
    <input type="file" @change="fileChange">
</template>

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

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

const fileChange = (e) => {
    if (!e.target.files[0]) return;
    const file = e.target.files[0];
    fileType.value = file.name.split(".").pop()

    // 由于这里一秒截取一帧 ,截取5次, 所以如果视频不足5秒会导致截取和读取失败
    // 回调中是base64图片组成的数组,需要在前面拼接 "data:image/png;base64," ,然后在img的src中赋值即可
    videoFrame(file, 5, 1, (data) => console.log(data))
}

/**
 * 根据选择的视频文件获取视频截图
 * @param {file} file 选择的视频文件
 * @param {number|string} count 截取图片的次数
 * @param {number|string} interval 截取图片的间隔
 * @param {Function} callBack 回调
 */
const videoFrame = async (file, count, interval, callBack) => {
    if (!ffmpeg.isLoaded()) {
        await ffmpeg.load();
    }
    if(!file) return;

    const inputName = `input.${fileType.value}`;

    const orgFileBuffer = await file.arrayBuffer()

    // 将输入文件保存到虚拟文件系统
    await ffmpeg.FS('writeFile', inputName, await fetchFile(new Blob([orgFileBuffer])));

    try {
        await ffmpeg.run(
            "-i",
            inputName,
            "-r",
            `${interval}`,
            "-ss",
            "0",
            "-vframes",
            `${count}`,
            "-f",
            "image2",
            "-s",
            "88*50",
            "image-%02d.png"
        );
        const baseArr = []

        for (let i = 0; i < count; i++) {
            let temp = i + 1;
            if (temp < 10) {
                temp = "0" + temp;
            }
            baseArr.push(
                arrayBufferToBase64(ffmpeg.FS("readFile", "image-" + temp + ".png"))
            );
        }

        callBack && callBack(baseArr)

        // 释放内存
        ffmpeg.FS('unlink', inputName);
    } catch (e) {}
}

const arrayBufferToBase64 = (array) => {
    array = new Uint8Array(array);
    var length = array.byteLength;
    var table = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/'];
    var base64Str = "";
    for (var i = 0; length - i >= 3; i += 3) {
        var num1 = array[i];
        var num2 = array[i + 1];
        var num3 = array[i + 2];
        base64Str +=
            table[num1 >>> 2] +
            table[((num1 & 0b11) << 4) | (num2 >>> 4)] +
            table[((num2 & 0b1111) << 2) | (num3 >>> 6)] +
            table[num3 & 0b111111];
    }
    var lastByte = length - i;
    if (lastByte === 1) {
        var lastNum1 = array[i];
        base64Str +=
            table[lastNum1 >>> 2] + table[(lastNum1 & 0b11) << 4] + "==";
    } else if (lastByte === 2) {
        // eslint-disable-next-line no-redeclare
        var lastNum1 = array[i];
        var lastNum2 = array[i + 1];
        base64Str +=
            table[lastNum1 >>> 2] +
            table[((lastNum1 & 0b11) << 4) | (lastNum2 >>> 4)] +
            table[(lastNum2 & 0b1111) << 2] +
            "=";
    }
    return base64Str
}

onUnmounted(() => {
    ffmpeg.exit();
})
</script>
<think>首先,用户的问题是:解决在使用Vite、VueFFmpeg进行视频截取时,打包到手机端后出现'SharedArrayBuffer is not defined'错误的问题。 关键点: - 错误:'SharedArrayBuffer is not defined' - 原因:根据引用[1],SharedArrayBuffer在现代浏览器中被禁用或限制,以防止安全风险如Spectre攻击。 - 环境:Vite + Vue + FFmpeg,打包到手机端(可能指移动浏览器)。 - 解决方案:从引用[3][4]中,我看到需要在服务器配置中添加特定的HTTP头。 解决方案的核心是设置HTTP头: - 'Cross-Origin-Embedder-Policy': 'require-corp' - 'Cross-Origin-Opener-Policy': 'same-origin' 这些头需要添加到服务器配置中。用户使用Vite,所以需要修改Vite的配置文件(vite.config.js或vite.config.ts)。 步骤: 1. 在Vite配置文件中添加服务器头。 2. 确保在移动浏览器中这些头被正确设置,因为错误发生在手机端。 用户提到打包到手机端,所以可能是在本地服务器或生产环境。错误可能发生在开发服务器或生产部署中。但引用[3][4]是针对开发服务器的配置。用户说“打包后”,所以可能是生产环境。 分析: - 在开发时,Vite服务器运行本地开发服务器。 - 打包后,生成静态文件,部署到服务器。在生产服务器上,也需要设置这些头。 用户说“打包到手机端”,可能意味着将应用部署到服务器,然后在手机浏览器中访问。所以,解决方案需要在生产服务器上设置这些头。 但引用[3][4]是针对Vite开发服务器的配置。对于生产环境,需要确保生产服务器(如Nginx、Apache)也设置这些头。 用户的具体问题:使用Vite+Vue+FFmpeg进行视频截取,打包后手机端报错。 FFmpeg部分:用户可能在浏览器中使用FFmpeg.wasm,因为SharedArrayBuffer错误通常与WebAssembly相关。引用[2]提到WebAssembly错误,确认了这一点。 所以,解决方案: - 在开发环境:修改Vite配置。 - 在生产环境:修改生产服务器配置。 现在,构建回答结构: - 解释错误原因。 - 提供解决方案。 - 分步骤说明如何配置。 - 确保回答清晰、可靠。 参考引用: - [^1]:解释错误原因。 - [^2]:提到WebAssembly错误,但用户当前错误是SharedArrayBuffer。 - [^3]:在Vue项目中添加服务器头(针对开发服务器)。 - [^4]:在Vite配置中添加服务器头。 用户使用Vite,所以引用[4]更直接:在vite.config.js中添加服务器头。 对于生产环境,需要额外步骤。 回答结构: 1. **错误原因**:简要解释为什么出现'SharedArrayBuffer is not defined'错误。 2. **解决方案概述**:说明需要设置特定的HTTP头。 3. **开发环境配置**:如何修改Vite配置文件。 4. **生产环境配置**:如何修改生产服务器配置。 5. **验证**:如何测试是否解决。 6. **相关问题**:生成相关问题。 生成相关问题: - 基于主题:Vite, Vue, FFmpeg, SharedArrayBuffer, 浏览器安全等。 - 例如: 1. 如何优化Vite+Vue项目中的FFmpeg性能? 2. SharedArrayBuffer在移动浏览器中的兼容性如何? 3. 还有其他方法解决SharedArrayBuffer问题吗? 使用中文回答。 确保LaTeX格式正确:用户问题中没有数学表达式,所以可能不需要,但如果有,用$...$或$$...$$。 在回答中,引用段落末尾添加引用标识,如[^1]。 现在,草拟回答。</think>### 解决Vite+Vue+FFmpeg视频截取打包到手机端报错'SharedArrayBuffer is not defined'的方案 在使用Vite、VueFFmpeg进行视频截取时,打包到手机端出现`SharedArrayBuffer is not defined`错误,主要是因为现代浏览器(包括移动浏览器)出于安全考虑(如防止Spectre攻击),默认禁用或限制了`SharedArrayBuffer`对象。FFmpeg.wasm等WebAssembly模块依赖`SharedArrayBuffer`实现多线程操作,当浏览器未正确配置HTTP响应头时,会导致此错误[^1]。以下是完整的解决方案,分为开发环境生产环境配置。确保环境为Vite 4+、Vue 3+FFmpeg.wasm。 --- ### 错误原因分析 - **核心问题**:浏览器安全策略阻止了`SharedArrayBuffer`的使用,导致FFmpeg.wasm无法初始化。移动浏览器(如Chrome for Android或Safari)对此限制更严格[^1]。 - **触发场景**:在Vite开发服务器或生产部署的静态文件中,未设置必要的HTTP头。 - **相关引用**:SharedArrayBuffer被视为安全风险,需通过`Cross-Origin-Embedder-Policy``Cross-Origin-Opener-Policy`头启用[^1][^3][^4]。 --- ### 解决方案 #### 步骤1: 开发环境配置(Vite本地服务器) 在Vite配置文件中添加服务器头,确保本地开发时浏览器允许`SharedArrayBuffer`。 1. 打开项目根目录的`vite.config.js`(或`vite.config.ts`)。 2. 添加以下代码: ```javascript export default defineConfig({ plugins: [vue()], server: { headers: { 'Cross-Origin-Embedder-Policy': 'require-corp', 'Cross-Origin-Opener-Policy': 'same-origin' }, // 可选:开启HTTPSIP访问(确保移动设备可测试) https: true, // 如果使用HTTPS host: '0.0.0.0' // 允许局域网访问,手机可通过IP连接 } }); ``` 3. 重启开发服务器: ```bash npm run dev ``` **验证**:在手机浏览器访问开发服务器IP(如`https://<your-ip>:5173`),检查错误是否消失。如果出现HTTPS证书警告,需信任本地证书[^4]。 #### 步骤2: 生产环境配置(打包部署到服务器) 打包后(`npm run build`),静态文件需通过生产服务器(如Nginx或Apache)提供,并添加相同HTTP头。 1. **Nginx服务器示例**: - 编辑Nginx配置文件(如`/etc/nginx/nginx.conf`): ```nginx server { listen 80; server_name your-domain.com; root /path/to/your/dist; # Vite打包的dist目录 index index.html; location / { add_header 'Cross-Origin-Embedder-Policy' 'require-corp'; add_header 'Cross-Origin-Opener-Policy' 'same-origin'; try_files $uri $uri/ /index.html; } } ``` - 重启Nginx: ```bash sudo systemctl restart nginx ``` 2. **Apache服务器示例**: - 编辑`.htaccess`文件(在项目根目录): ```apache Header set Cross-Origin-Embedder-Policy "require-corp" Header set Cross-Origin-Opener-Policy "same-origin" ``` 3. **云服务(如Netlify/Vercel)**:在部署设置中添加自定义头(Key: `Cross-Origin-Embedder-Policy`, Value: `require-corp`;Key: `Cross-Origin-Opener-Policy`, Value: `same-origin`)。 **验证**:部署后,用手机浏览器访问页面,使用开发者工具检查响应头是否包含上述字段[^3][^4]。 #### 步骤3: 额外检查与优化 - **FFmpeg.wasm兼容性**:确保使用最新版FFmpeg.wasm(如`@ffmpeg/ffmpeg`),旧版本可能因WebAssembly编译问题报错(如`CompileError: WebAssembly.instantiate()`)[^2]。 - **移动端测试**:在手机浏览器中开启“桌面模式”测试,部分移动浏览器策略更严格。 - **备选方案**:如果头设置无效,考虑降级FFmpeg.wasm到单线程模式(性能较差): ```javascript import { createFFmpeg } from '@ffmpeg/ffmpeg'; const ffmpeg = createFFmpeg({ log: true, corePath: 'path/to/core', mainName: 'main' }); // 禁用多线程 ``` - **安全提示**:这些头会限制跨源资源加载,确保所有资源(如图片、API)同源或支持CORS[^1][^3]。 --- ### 总结 - **根本解决**:通过HTTP头`Cross-Origin-Embedder-Policy: require-corp``Cross-Origin-Opener-Policy: same-origin`启用`SharedArrayBuffer`。 - **适用场景**:Vite开发环境修改`vite.config.js`,生产环境配置服务器(Nginx/Apache)。 - **效果**:配置后,FFmpeg.wasm可正常初始化,视频截取功能在手机端运行。 如果问题未解决,检查浏览器控制台是否有其他错误(如WebAssembly路径问题),并确保FFmpeg.wasm文件正确加载[^2][^4]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Raccom

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值