最近有个项目有关于视频加水印、裁剪、压缩的需求,然后经过调研发现了ffmpeg这个插件,其实在写demo之前我看到了很多说它使用起来很慢的帖子,经过使用才知道确实挺慢的。

        1、首先装相应的两个ffmpeg的插件:

npm install @ffmpeg/ffmpeg @ffmpeg/core

         2、在文件中引入插件并且启用:

//正常情况下可以直接引用插件的方式,但是因为我这边使用的是qiankun的微前端,所以直接引用插件拿不到的createFFmpegCore,所以这边改了点代码,就用这样的方式引用了
import { createFFmpeg, fetchFile } from '../@ffmpeg/ffmpeg/src/index.js';

// 加载外部核心文件
// console.log(location.origin);
let ffmpeg = createFFmpeg({
    corePath: 'http://localhost:12216/ffmpeg-core.js',  //我将ffmpeg-core文件放在了public静态文件下,所以在本地使用的这种方法引入
    // log: true,  //是否显示log
});
(async () => {
    await ffmpeg.load();
})();

           3、配置代理:

             我这边是微前端,是配置在主服务的

  headers: {
      'Cross-Origin-Opener-Policy': 'same-origin',
      'Cross-Origin-Embedder-Policy': 'require-corp',
    },

  目前为止,ffmpeg使用前的配置就已经完成,接下来就是一些相关的使用,以下是我粗浅的理解写的一些测试代码:

<template>
    <div class="file-upload">
        <div class="uploadButton">
            <input id="upload"
              ref="file"
              type="file"
              style="display: none"
              @change="uploadFile()">
            <div v-loading="isLoading"
              :element-loading-text="loadingText"
              class="upload-file-box"
              @click="goFile">
                <i class="icon-tianjia1"
                  style="color:rgba(25, 140, 255, 1)"></i>
                <div class="text">上传图片/视频</div>
            </div>
        </div>

        <div class="uploadInfo">
            <div style="font-size: 8px;margin-right: 10px;width: 100%;">{{ fileInfo.fileName }}</div>
            <el-row v-if="statusShow"
              type="flex">
                <el-progress class="progress-1"
                  :percentage="schedule"
                  :stroke-width="4"
                  style="width:300px;line-height: 2;" />
                <span v-show="schedule === 100"
                  style="font-size: 8px;padding: 0 10px 0 20px;color: #31cd31;">上传成功!</span>
            </el-row>
        </div>
    </div>
</template>

<script>
import { createFFmpeg, fetchFile } from '../@ffmpeg/ffmpeg/src/index.js';
import SparkMD5 from 'spark-md5'; 
// import { filechunk, filecompose, uploadfileList, fetchFilePicture } from '../../api/file';
// 加载外部核心文件
// console.log(location.origin);
let ffmpeg = createFFmpeg({
    corePath: 'http://localhost:12216/ffmpeg-core.js',
    // log: true,  //是否显示log
});
(async () => {
    await ffmpeg.load();
})();
export default {
    name: 'DynamicUpload',
    data() {
        return {
            fileInfo: {
                fileName: '',
                fileMd5: '',
                chunkCount: 0
            },
            chunkSize: 200 * 1024 * 1024,
            schedule: 0,
            statusShow: false,
            minDuration: 20, //最小时间
            maxDuration: 80, //最大时间
            //视频处理的loading
            isLoading: false,
            loadingText: '加载中...'
        };
    },
    mounted() {

    },
    methods: {

        // 按钮跳转
        goFile() {
            this.$refs.file.click();
        },
        async uploadFile() {
            // 获取用户选择的文件
            const file = document.getElementById('upload').files[0];
            if (file === undefined) {
                this.$message.error('没有选中文件');
                return;
            }
            // 文件类型判断是视频还是图片
            let videoTypeList = ["mp4", 'webm', 'ogg'];
            const newArr = file.name.split('.');
            if (videoTypeList.includes(newArr[newArr.length - 1])) {
                //    对上传视频做处理
                let orgFileBuffer = await file.arrayBuffer(); // 获取文件数据
                ffmpeg.FS('writeFile', file.name, await fetchFile(new Blob([orgFileBuffer]))); // 将视频数据写入内存
                //获取视频时长 ------start
                var duration;
                let getDuration = new Promise(resolve => {
                    var url = URL.createObjectURL(file);
                    var audioElement = new Audio(url);
                    audioElement.addEventListener("loadedmetadata", function (_event) {
                        duration = audioElement.duration; //时长为秒,小数,182.36
                        if (duration) {
                            resolve();
                        }
                    });
                });
                // ---------end
                getDuration.then(() => {
                    console.log(duration);
                    if (duration < this.minDuration) {
                        this.$message.error('不能上传小于' + this.minDuration + '秒的视频,请重新上传!');
                        return;
                    }
                    if (duration > this.maxDuration) {
                        //对长视频进行裁剪
                        this.splitVideo(file);
                        this.$message.warning('上传的视频超过最大时长限制,将会截前' + this.maxDuration + '秒钟的视频上传!');
                    }
                    this.formatVideo(file);//对视频做处理,结束之后上传
                });
            } else {
                //对上传图片做处理
                let orgFileBuffer = await file.arrayBuffer(); // 获取文件数据
                ffmpeg.FS('writeFile', file.name, await fetchFile(new Blob([orgFileBuffer]))); // 将视频数据写入内存
                this.formatImage(file);
            }
        },
        //视频剪切
        async splitVideo(file) {
            this.isLoading = true;
            this.loadingText = '视频截取中...';
            let start = 0; // 视频开始截取的时间
            const beginTime = start * 0.001.toFixed(3) + ''; //开始时间
            const endTime = (start + this.maxDuration) + ''; // 结束时间
            const lineTime = ['-ss', beginTime, '-to', endTime]; // 切剪的时间线的begin/end
            const newFile = ['-i', file.name]; //输入的文件名称
            // 纯视频剪切(不论大小,1s完成)
            let test = ['-c', 'copy'];  // 原视频输出,不做解码
            const end = [file.name]; //输出的文件名称
            await ffmpeg.run(...newFile, ...lineTime, ...test, ...end);
            let splitFile = this.handleTranslateType(file.name, file.name, 'video/mp4');  //经过处理之后的blob文件
            console.log(splitFile);
            // 下载到本地查看(调试时使用)
            // this.fileDownloadLocal(outputImgName);
            this.isLoading = false;
        },
        //视频处理
        async formatVideo(file) {
            this.isLoading = true;
            this.loadingText = '准备上传...';
            // 截取一帧图片作为封面
            let splitTime = '00:00:00';//截取时间
            let outputImgName = 'cover.png';
            await ffmpeg.run('-ss', splitTime, '-i', file.name, '-f', 'image2', '-r', '1', '-vframes', '14', '-s', '1920*1080', outputImgName);
            let coverPng = this.handleTranslateType(outputImgName, outputImgName, 'png');  //blob格式的封面图片
            console.log(coverPng);
            // 下载到本地查看(调试时使用)
            // this.fileDownloadLocal(outputImgName);
            let img = 'http://localhost:12216/favicon.ico';  //水印的图片
            ffmpeg.FS('writeFile', 'overlay.png', await fetchFile(img)); //将水印图片写入内存
            let fontFile = 'http://localhost:12216/UniTortred.ttf';
            ffmpeg.FS('writeFile', 'UniTortred.ttf', await fetchFile(fontFile)); //将水印图片写入内存
            let waterMark = ['-vf', "movie=overlay.png[watermark];[in][watermark] overlay=main_w-overlay_w-10:main_h-overlay_h-10[out]"];//水印相关参数
            // let waterMark = ['-vf', "drawtext=fontsize=100:fontfile=UniTortred.ttf:text='hello world':x=100:y=100:fontcolor=green:box=1:boxcolor=yellow"];//水印相关参数
            let fast = ['-preset', 'ultrafast']; //解码速率,与视频质量成反比  ultrafast, superfast, veryfast, faster, fast, medium, slow, slower, veryslow
            let threads = ['-threads', '6'];  //进程控制
            let zipFile = ["-b", "2000000",];  //压缩
            let other = ['-vcodec', 'libx264', '-r', '10']; //指定编译器和帧率调整
            const resolvingPower = ['-vf', 'scale=640:-1']; //视频输出的像素
            // 相关参数,可自行调整
            //'流畅360P', // '640:-1': '360P'
            //'标清480P', // '720:-1': '480P',
            //'高清720P', // '1280:-1'
            //'超清1080P', // '1920:-1'
            //'': '原视频输出';
            const newFile = ['-i', file.name]; //输入的文件名称
            const end = [file.name]; //输出的文件名称
            // 不建议使用,因为对大视频处理速度极慢,暂未找到优化的方法
            await ffmpeg.run(
                ...newFile,
                ...other,
                ...zipFile,
                ...threads,
                ...fast,
                ...waterMark,
                ...resolvingPower,
                ...end
            );
            let afterFile = this.handleTranslateType(file.name, file.name, 'video/mp4');  //经过处理之后的blob文件
            console.log(afterFile);
            // 下载到本地查看(调试时使用)
            this.fileDownloadLocal(file.name);
            // 得到处理的视频之后上传
            this.isLoading = false;
            this.functionUploadFile(afterFile);
        },
        //图片处理
        async formatImage(file) {
            // 给图片加水印
            this.isLoading = true;
            this.loadingText = '准备上传...';
            let img = 'http://localhost:12216/favicon.ico';  //水印的图片
            ffmpeg.FS('writeFile', 'overlay.png', await fetchFile(img)); //将水印图片写入内存
            let fontFile = 'http://localhost:12216/UniTortred.ttf';
            ffmpeg.FS('writeFile', 'UniTortred.ttf', await fetchFile(fontFile)); //将水印图片写入内存
            let waterMark = ['-vf', "movie=overlay.png[watermark];[in][watermark] overlay=main_w-overlay_w-10:main_h-overlay_h-10[out]"];//水印相关参数
            //文字水印,需要相关配置,还在调研
            // let waterMark = ['-vf', "drawtext=fontsize=100:fontfile=UniTortred.ttf:text='hello world':x=100:y=100:fontcolor=green:box=1:boxcolor=yellow"];
            let zipFile = ["-b", "2000000",];  //压缩
            const newFile = ['-i', file.name]; //输入的文件名称
            const end = [file.name]; //输出的文件名称
            // 图片处理比较快
            await ffmpeg.run(
                ...newFile,
                ...zipFile,
                ...waterMark,
                ...end
            );
            let afterFile = this.handleTranslateType(file.name, file.name, 'jpg');  //经过处理之后的blob文件
            // console.log(afterFile);
            // 下载到本地查看(调试时使用)
            // this.fileDownloadLocal(file.name);
            this.isLoading = false;
            this.functionUploadFile(afterFile);
        },
        async functionUploadFile(file) {
            this.$emit("changFileLoaing", true);
            if (file.size > 0) {
                this.statusShow = true;
                this.fileInfo.fileName = file.name;
                this.fileInfo.chunkCount = Math.ceil(file.size / this.chunkSize);
                let filemd5length = 1024 * 1024 * 10;
                if (file.size < 1024 * 1024 * 2) {
                    filemd5length = file.size;
                }
                let filemd5 = file.slice(0, filemd5length);
                this.fileInfo.fileMd5 = await this.getFileMd5(filemd5);
            }
            // 分片上传
            // filechunk(this.fileInfo).then(
            //     async res => {
            //         if (res.code == 0) {
            //             // 分片上传完成 待合并文件
            //             if (res.data.uploadCode == 3) {
            //                 this.composeFile(this.fileInfo);
            //                 return;
            //             }
            //             if (res.data.uploadCode == 1) {
            //                 this.$emit("changFileLoaing", false); //父頁面loding图标
            //                 this.$emit("setattachmentIds", res.data.id, this.fileInfo.fileName); //父页面传参
            //                 this.schedule = 100;
            //                 this.$message.success('文件上传成功');
            //                 return;
            //             }
            //             const chunkUploadUrls = res.data.chunkUploadUrls;
            //             let num = 0;//把上一个片段的进度存起来
            //             for (const item of chunkUploadUrls) {
            //                 // 分片开始位置
            //                 const start = (item.partNumber - 1) * this.chunkSize;
            //                 // 分片结束位置
            //                 const end = Math.min(file.size, start + this.chunkSize);
            //                 // 取文件指定范围内的byte,从而得到分片数据
            //                 const chunkFile = file.slice(start, end);
            //                 const config = {
            //                     onUploadProgress: (e) => {
            //                         let count = ((e.loaded / e.total) * 100) / this.fileInfo.chunkCount;
            //                         if (file.size > 1024 * 1024 * 1024 * 2) { //文件大于2G
            //                             this.schedule = num + parseInt(count);
            //                         } else {
            //                             this.schedule = num + parseInt(count / 2);
            //                         }
            //                     }
            //                 };
            //                 await uploadfileList(item.uploadUrl, chunkFile, config);
            //                 num = this.schedule;
            //             }
            //             this.composeFile(this.fileInfo);
            //         } else {
            //             this.$message.error("文件上传失败");
            //             this.$emit("changFileLoaing", false);
            //         }
            //     }
            // );
        },
        //MD5加密,为了分段上传文件
        async getFileMd5(file) {
            const fileReader = new FileReader();
            fileReader.readAsBinaryString(file);
            const spark = new SparkMD5();
            return new Promise((resolve) => {
                fileReader.onload = (e) => {
                    spark.appendBinary(e.target.result);
                    resolve(spark.end());
                };
            });
        },
        // 将文件转化为blob格式
        handleTranslateType(inputFileName, outputFileName, outputFileType) {
            const afterFile = new window.File(
                [new Blob([ffmpeg.FS('readFile', inputFileName)])],
                outputFileName,
                { type: outputFileType }
            );
            return afterFile;
        },

        // 通过文件名将MEMFS文件保存到本地
        fileDownloadLocal(filename) {
            const mime = 'application/octet-stream';
            const content = ffmpeg.FS('readFile', filename);
            // console.log(`Offering download of "${filename}", with ${content.length} bytes...`)
            var a = document.createElement('a');
            a.download = filename;
            a.href = URL.createObjectURL(new Blob([content], { type: mime }));
            a.style.display = 'none';
            document.body.appendChild(a);
            a.click();
            setTimeout(() => {
                document.body.removeChild(a);
                URL.revokeObjectURL(a.href);
            }, 2000);
            ffmpeg.FS('unlink', filename);
        },
    }

};
</script>

<style scoped lang="scss">
.file-upload {
    .upload-file-box {
        width: 200px;
        height: 150px;
        border-radius: 4px;
        background: rgba(245, 247, 250, 1);
        border: 1px dashed rgba(25, 140, 255, 0.5);
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;

        .text {
            font-size: 12px;
            font-weight: 400;
            line-height: 28px;
            color: rgba(153, 153, 153, 1);
        }
    }
}
</style>

总结:ffmpeg是一个非常强大的插件,我的理解还停留在最浅显的层面,我尝试过优化加快它的转码速度,但是都不能成功,这种写法,上传一个800M的4k视频,转码时间长达10分钟,非常漫长,所以最后还是没能使用在项目中,希望有大神看到这篇文章可以给我点意见,非常感谢!

Logo

前往低代码交流专区

更多推荐