更新记录

1.0.6(2026-06-09)

_ 修复iOS端IPA上传报存在arm64e架构的问题

1.0.5(2026-05-27)

  • 发布IOS端

1.0.4(2026-03-08)

  • 补充文档说明
查看更多

平台兼容性

uni-app(4.66)

Vue2 Vue2插件版本 Vue3 Vue3插件版本 Chrome Safari app-vue app-nvue Android Android插件版本 iOS 鸿蒙
1.0.3 1.0.3 - - - - 7.0 1.0.3 13 -
微信小程序 支付宝小程序 抖音小程序 百度小程序 快手小程序 京东小程序 鸿蒙元服务 QQ小程序 飞书小程序 小红书小程序 快应用-华为 快应用-联盟
- - - - - - - - - - - -

uni-app x(4.66)

Chrome Safari Android Android插件版本 iOS 鸿蒙 微信小程序
- - 7.0 1.0.0 13 - -

xwq-ffmpeg

文档说明

插件集成ffmpeg实现安卓端对视频、音频、图片的编辑功能

  • 功能支持
    • 视频裁剪
    • 视频合并 (注:同种类型文件合并)
    • 视频格式转换
    • 音频合并 (注:同种类型文件合并)
    • 音频格式转换
    • 获取媒体信息
    • 图片裁剪
    • 图片格式转换
    • 图片旋转
    • ....更多功能可查阅官网

调用方法说明

  • 裁剪
    • useExecuteFFmpeg

方法参数说明

属性 类型 描述
cmd string FFmpeg命令,可参照官网例子
success function 成功回调
cancel function 取消回调
fail function 失败回调
taskStart function 任务开始回调
statistics function 统计信息回调

FFmpeg 命令行标准语法用例

以下示例均可直接作为 cmd 传给 useExecuteFFmpeg

# 1. 视频转码:转成 H.264 + AAC 的 MP4
-i input.mp4 -c:v libx264 -preset medium -crf 23 -pix_fmt yuv420p -c:a aac -b:a 128k -movflags +faststart -y output.mp4

# 2. 视频压缩:控制分辨率和码率
-i input.mp4 -vf scale=1280:720 -c:v libx264 -b:v 1500k -maxrate 1800k -bufsize 3000k -c:a aac -b:a 96k -movflags +faststart -y compressed.mp4

# 3. 视频压缩:固定 QP 写法
-i input.mp4 -c:v libx264 -qp 28 -s 540x280 -c:a aac -b:a 128k -movflags +faststart -y qp_output.mp4

# 4. 视频裁剪:裁出指定区域
-i input.mp4 -vf crop=640:360:100:50 -c:v libx264 -c:a aac -b:a 128k -y cropped.mp4

# 5. 缩放后再裁剪为固定尺寸
-i input.mp4 -vf scale=1280:720,crop=720:720:280:0 -c:v libx264 -c:a aac -b:a 128k -y square_output.mp4

# 6. 视频切割:从第 5 秒开始截取 10 秒
-ss 5 -t 10 -i input.mp4 -c:v libx264 -c:a aac -b:a 128k -y clip.mp4

# 7. 视频切割:无重编码快速截取
-ss 5 -t 10 -i input.mp4 -c copy -y clip_copy.mp4

# 8. 提取首帧作为封面图
-i input.mp4 -ss 0 -frames:v 1 -y cover.jpg

# 9. 提取音频为 AAC
-i input.mp4 -vn -c:a aac -b:a 128k -y audio.m4a

# 10. 切割为 HLS m3u8
-i input.mp4 -c:v libx264 -c:a aac -hls_time 6 -hls_list_size 0 -hls_segment_filename segment_d.ts -f hls -y index.m3u8

# 11. 生成点播型 m3u8
-i input.mp4 -c:v libx264 -c:a aac -hls_time 4 -hls_playlist_type vod -hls_list_size 0 -hls_flags independent_segments -hls_segment_filename vod_d.ts -f hls -y vod.m3u8

# 12. m3u8 合并回 MP4
-i index.m3u8 -c copy -bsf:a aac_adtstoasc -y merged.mp4

常用参数说明

  • -i:输入文件路径
  • -c:v libx264:视频编码器使用 H.264,前提是运行库已编入 libx264
  • -c:a aac:音频编码器使用 AAC
  • -crf:画质控制参数,值越小画质越高、体积越大,常用 1828
  • -qp:固定量化参数,值越小画质越高,常用于快速控制压缩强度
  • -preset:编码速度与压缩效率平衡,常用 ultrafastfastmediumslow
  • -vf:视频滤镜,可组合 scalecropfps 等能力
  • -ss:开始截取时间,支持秒数或 00:00:05 格式
  • -t:截取时长
  • -movflags +faststart:把 MP4 头信息前置,便于在线播放
  • -hls_time:每个 ts 分片时长,单位秒
  • -hls_segment_filename:ts 分片输出命名模板

插件中 cmd 传参示例

const cmd = `-i ${inputPath} -c:v libx264 -qp 28 -s 540x280 -c:a aac -b:a 128k -movflags +faststart -y ${outputPath}`

useExecuteFFmpeg({
    cmd,
    success: () => {
        console.log('执行成功')
    },
    fail: () => {
        console.log('执行失败')
    }
})

使用注意事项

  • iOS 如果提示 Unknown encoder 'libx264',说明当前基座里的 ffmpeg 运行库没有编入 libx264

  • 如果命令中包含空格路径,建议先转换为实际本地绝对路径后再拼接

  • m3u8 切片建议单独放到独立目录,避免分片文件和其他输出文件混在一起

  • 某些参数是否可用取决于当前编译进去的 FFmpeg 模块,不同基座能力可能不同

  • 合并

    • ffmpegVideoMerge

方法参数说明

属性 类型 描述
type number 合并的文件类型 默认1 视频格式 ,可选2 音频格式
filePath string 文件路径 只支持同种类型文件
outPath string 文件输出路径
success function 成功回调
fail function 失败回调
  • 转换
    • convertTypeV

方法参数说明

属性 类型 描述
inputPath string 输入地址
outPath string 输出地址
vType string 要转换的视频类型
  • 媒体信息
    • getMediaInfo
属性 类型 描述
filePath string 文件地址
  • 取消
    • ffmpegCancel
属性 类型 描述
sessionId number null 会话ID,取消某个任务时需要传

UNI-APPx用方式

<template>
    <view>
        <video :src="path" :autoplay="autoplay" :controls="true" :style="{height:'250px',width:'100%'}" :object-fit="objectFit" class="`video_1`" >
        </video>
    </view>
    <view class="btn-wrap" :style="{marginBottom:'15px'}">
        <button class="btn" @click='executeFFmpeg'>{{title}}</button>
        <button class="btn" @click='videoMerge'>视频合并</button>
        <button class="btn" @click='changeVideoType'>视频格式转换</button>
        <button class="btn" @click='audioMerge'>音频合并</button>
        <button class="btn" @click='changeAudioType'>音频格式转换</button>
        <button class="btn" @click='getVideoInfo'>获取媒体信息</button>
        <button class="btn" @click='imageRota'>图片旋转</button>
        <button class="btn" @click='imageCrop'>图片裁剪</button>
        <button class="btn" @click='getVideoInfo'>获取媒体信息</button>
    </view>
    <view class="content-wrap">
        <text :style="{color:'red'}">媒体信息输出:</text>
        <view class="content">
            <textarea :value="content"></textarea>
        </view>
    </view>
    <image :src="imagePath" :style="{width:'100%',height:'auto'}"></image>

</template>

<script setup>
    import {useExecuteFFmpeg,
            FfmpegType,
            getMediaInfo,
            getLocalPath,
            TaskStartCallbackOpt,
            StatisticsCallBackOpt,
            ffmpegVideoMerge,
            MergeType,
            convertTypeV
            } from '@/uni_modules/xwq-ffmpeg';

    const title=ref('执行命令')
    const objectFit='cover'
    const autoplay=ref(false)
    const content=ref('')
    const imagePath=ref('')
    const path=ref('/static/test002.mp4')

    const buildOutputPath = (fileName:string):string => {
        return `_doc/ffmpeg-test/${fileName}`
    }

    const resolveInputPath = async (inputPath:string):Promise<string> => {
        if (inputPath.startsWith('/static/')) {
            return await getLocalPath(inputPath)
        }
        return plus.io.convertLocalFileSystemURL(inputPath)
    }

    const executeFFmpeg=async ()=>{
        let result=await getLocalPath('/static/test.mp4');
        let outPath=buildOutputPath(`video_${Date.now()}.mp4`)
        let cmd=`-i ${result} -c:v libx264 -qp 28 -s 540x280 -c:a aac -b:a 128k -movflags +faststart -y ${outPath}`;

        useExecuteFFmpeg({
            cmd,
            success:()=>{
                console.log('成功回调====')
                path.value=plus.io.convertAbsoluteFileSystem(outPath)
                autoplay.value=true
            },
            cancel:()=>{
                console.log('取消执行回调===')
            },
            fail:()=>{
                console.log('失败回调====')
            },
            taskStart:(val:TaskStartCallbackOpt)=>{
                console.log('任务开始回调===',val)
            },
            statistics:(val:StatisticsCallBackOpt)=>{
                // console.log('统计信息开始回调===',val)
            }
        } as FfmpegType)
    }

    /**
     *视频合并 
    */
    const videoMerge=async()=>{
        let filePath=['/static/test.mp4','/static/test002.mp4']
        let outPath=buildOutputPath(`merge_video_${Date.now()}.mp4`)
        ffmpegVideoMerge({
            filePath,
            outPath,
            success:()=>{
                path.value=plus.io.convertAbsoluteFileSystem(outPath)
                autoplay.value=true
            },
            fail:()=>{
                console.log('失败回调====')
            }
        } as MergeType)
    }

    //视频格式转换
    const changeVideoType=async ()=>{
        let inputPath=await resolveInputPath('/static/test.mp4');
        let videoType='flv';
        let outPath=buildOutputPath(`convertType_${Date.now()}.${videoType}`)
        convertTypeV(inputPath,videoType,outPath).then((filePath:string)=>{
            path.value=plus.io.convertAbsoluteFileSystem(filePath)
            autoplay.value=true
        });

    }

    //音频合并
    const audioMerge=()=>{
        let filePath=['/static/test001.mp3','/static/test002.mp3']
        // let filePath=['https://www.example.com/files/web/video/test001.mp4','https://www.example.com/files/web/video/test002.mp4']
        let outPath=buildOutputPath(`merge_audio_${Date.now()}.mp3`)
        ffmpegVideoMerge({
            type:2,
            filePath,
            outPath,
            success:()=>{
                // path.value=outPath
                // autoplay.value=true
            },
            fail:()=>{
                console.log('失败回调====')
            }
        } as MergeType)
    }

    //音频格式转换
    const changeAudioType=()=>{
        let inputPath='/static/test001.mp3';
        let videoType='FLAC';
        let outPath=buildOutputPath(`convertType_${Date.now()}.${videoType}`)
        convertTypeV(inputPath,videoType,outPath).then((filePath:string)=>{
            console.log('音频filePath====',filePath)
        });
    }

    //获取视频信息
    const getVideoInfo=async ()=>{
        let inputPath=await resolveInputPath('/static/test.mp4')
        // let inputPath='/storage/emulated/0/DCIM/Camera/video_1714100294515.mp4'
        getMediaInfo(inputPath,(val:string)=>{
            content.value=val
        })
    }

    //图片格式转换
    const changeImageType=async ()=>{
        let inputPath=await getLocalPath('/static/logo.png');
        let outPath=`/storage/emulated/0/DCIM/Camera/logo_${Date.now()}.jpg`
        let cmd=`-i ${inputPath} ${outPath}`;

        useExecuteFFmpeg({
            cmd,
            success:()=>{
                console.log('成功回调====')
                imagePath.value=outPath
            }
        } as FfmpegType)
    }

    //图片旋转
    const imageRota=async ()=>{
        let inputPath=await getLocalPath('/static/crop.png');
        let outPath=`/storage/emulated/0/DCIM/Camera/logo_${Date.now()}.png`
        let cmd=`-i ${inputPath} -vf "transpose=0" ${outPath}`;
        // transpose=0:逆时针旋转 90 度。
        // transpose=1:顺时针旋转 90 度。
        // transpose=4,vflip:垂直翻转。
        useExecuteFFmpeg({
            cmd,
            success:()=>{
                console.log('成功回调====')
                imagePath.value=outPath
            }
        } as FfmpegType)
    }
    //图片裁剪
    const imageCrop=async ()=>{
        let inputPath=await getLocalPath('/static/crop.png');
        let outPath=`/storage/emulated/0/DCIM/Camera/crop_${Date.now()}.png`
        let cmd=`-i ${inputPath} -vf "crop=300:200:0:0" ${outPath}`;

        // crop=w:h:x:y
        // w裁剪后的宽度
        // h裁剪后的高
        //x y 裁剪起点坐标
        useExecuteFFmpeg({
            cmd,
            success:()=>{
                console.log('成功回调====')
                imagePath.value=outPath
            }
        } as FfmpegType)
    }
</script>

<style>

.btn-wrap{
    flex-direction: row;
    flex-wrap: wrap;
    padding: 10px;
    justify-content: flex-start;
}
.btn{
    margin: 0 10px 15px 0;
}

.content-wrap{
    padding: 10px;
    width: 100%;
    min-height:100px;
}
.content{
    width: 100%;
    height: 100%;
    border: 1px solid #ccc;
    background-color: #f2f2f2;
}
</style>

UNIAPP 使用方式

<template>
    <scroll-view scroll-y="true" :style="{flex:1}">
        <view>
            <video :src="path" :autoplay="autoplay" :controls="true" :style="{height:'250px',width:'100%'}"
                :object-fit="objectFit" class="`video_1`">
            </video>
        </view>
        <view class="btn-wrap" :style="{marginBottom:'15px'}">
            <button class="btn" @click='executeFFmpeg'>{{title}}</button>
            <button class="btn" @click='videoMerge'>视频合并</button>
            <button class="btn" @click='changeVideoType'>视频格式转换</button>
            <button class="btn" @click='audioMerge'>音频合并</button>
            <button class="btn" @click='changeAudioType'>音频格式转换</button>
            <button class="btn" @click='changeImageType'>图片格式转换</button>
            <button class="btn" @click='imageRota'>图片旋转</button>
            <button class="btn" @click='imageCrop'>图片裁剪</button>
            <button class="btn" @click='getVideoInfo'>获取媒体信息</button>
            <button class="btn" @click='delogoV'>添加视频滤镜</button>
            <button class="btn" @click='createM3u8'>视频切割成m3u8</button>
        </view>
        <view class="content-wrap">
            <text :style="{color:'red'}">媒体信息输出:</text>
            <view class="content">
                <textarea :value="content" disabled></textarea>
            </view>
        </view>
        <image :src="imagePath" :style="{width:'100%',height:'auto'}"></image>
    </scroll-view>
</template>

<script>
    import {
        useExecuteFFmpeg,
        FfmpegType,
        getMediaInfo,
        getLocalPath,
        TaskStartCallbackOpt,
        StatisticsCallBackOpt,
        ffmpegVideoMerge,
        MergeType,
        convertTypeV
    } from '../../uni_modules/xwq-ffmpeg';
    export default {
        data() {
            return {
                title: "执行命令",
                objectFit: 'cover',
                autoplay: false,
                content: '',
                imagePath: '',
                path: '/static/test002.mp4'
            }
        },
        onLoad() {
            console.log('资源地址====',plus.io.convertLocalFileSystemURL(this.path))
        },
        methods: {
            buildOutputPath(fileName) {
                return `_doc/ffmpeg-test/${fileName}`
            },
            async resolveInputPath(inputPath) {
                if (inputPath.startsWith('/static/')) {
                    return await getLocalPath(inputPath)
                }
                return plus.io.convertLocalFileSystemURL(inputPath)
            },
            handleVideoOutput(filePath) {
                this.path = ''
                this.$nextTick(() => {
                    this.path = plus.io.convertAbsoluteFileSystem(filePath)
                    this.autoplay = true
                })
            },

            async executeFFmpeg() {
                let result = await this.resolveInputPath(this.path);
                let outPath = this.buildOutputPath(`video_${Date.now()}.mp4`)
                let cmd = `-i ${result} -c:v libx264 -qp 28 -s 540x280 -c:a aac -b:a 128k -movflags +faststart -y ${outPath}`;

                console.log('cmd====',cmd)
                useExecuteFFmpeg({
                    cmd,
                    success: () => {
                        console.log('成功回调====',outPath)
                        this.handleVideoOutput(outPath)
                    },
                    cancel: () => {
                        console.log('取消执行回调===')
                    },
                    fail: () => {
                        console.log('失败回调====')
                    },
                    taskStart: (val) => {
                        console.log('任务开始回调===', val)
                    }
                    // statistics:(val:StatisticsCallBackOpt)=>{
                    //  console.log('统计信息开始回调===',val)
                    // }
                })
            },

            async videoMerge() {
                // let filePath=['/static/test.mp4','/static/test002.mp4']
                let filePath = ['https://www.example.com/files/web/video/test001.mp4',
                    'https://www.example.com/files/web/video/test002.mp4'
                ]
                let outPath = this.buildOutputPath(`merge_video_${Date.now()}.mp4`)
                ffmpegVideoMerge({
                    filePath,
                    outPath,
                    success: () => {
                        this.handleVideoOutput(outPath)
                    },
                    fail: () => {
                        console.log('失败回调====')
                    }
                })
            },

            //视频格式转换
            async changeVideoType() {
                let inputPath = await this.resolveInputPath(this.path);
                let videoType = 'flv';
                let outPath = this.buildOutputPath(`convertType_${Date.now()}.${videoType}`);
                convertTypeV(inputPath, videoType, outPath).then((filePath) => {
                    this.handleVideoOutput(filePath)
                });
            },

            //音频合并
            audioMerge() {
                let filePath = [plus.io.convertLocalFileSystemURL('/static/test001.mp3'), plus.io.convertLocalFileSystemURL('/static/test002.mp3')]
                // let filePath=['https://www.example.com/files/web/video/test001.mp4','https://www.example.com/files/web/video/test002.mp4']
                let outPath = this.buildOutputPath(`merge_audio_${Date.now()}.mp3`)
                ffmpegVideoMerge({
                    type: 2,
                    filePath,
                    outPath,
                    success: () => {
                        // path.value=outPath
                        // autoplay.value=true
                    },
                    fail: () => {
                        console.log('失败回调====')
                    }
                })
            },
            //音频格式转换
            changeAudioType() {
                let inputPath = plus.io.convertLocalFileSystemURL('/static/test001.mp3');
                let videoType = 'FLAC';
                let outPath = this.buildOutputPath(`convertType_${Date.now()}.${videoType}`);
                convertTypeV(inputPath, videoType, outPath).then((filePath) => {
                    console.log('音频filePath====', filePath)
                });
            },
            //获取视频信息
            async getVideoInfo() {
                let inputPath = await this.resolveInputPath(this.path)
                // let inputPath='/storage/emulated/0/DCIM/Camera/video_1714100294515.mp4'
                getMediaInfo(inputPath, (val) => {
                    this.content = val
                })
            },

            //添加滤镜
            async delogoV() {
                // let inputPath='/static/delogo.mp4';
                let inputPath = await plus.io.convertLocalFileSystemURL(this.path);
                let outPath = `/storage/emulated/0/DCIM/Camera/delogo22${Date.now()}.mp4`;
                let watermark = '/static/logo.png';
                // let cmd=`-i ${inputPath} -vf "delogo=x=10:y=10:w=200:h=100" -c:v libx264 -crf 20 -c:a copy ${outPath}`;
                let cmd = `-i ${inputPath} -vf "delogo=x=10:y=10:w=450:h=350" ${outPath}`;
                // let cmd=`ffmpeg -i ${inputPath} -i ${watermark} -filter_complex "overlay=10:10" -y ${outPath}`
                // let cmd=`-i ${inputPath} -vf "drawbox=x=50:y=50:w=200:h=100:color=red:t=5" ${outPath}`
                useExecuteFFmpeg({
                    cmd,
                    success: () => {
                        console.log('成功回调=====')
                        // imagePath.value=outPath
                    },
                    fail: () => {
                        console.log('失败====')
                    }
                })
            },

            async createM3u8() {
                let inputPath = await this.resolveInputPath(this.path);
                let outPath = this.buildOutputPath(`index_${Date.now()}.m3u8`);
                let cmd = `-i ${inputPath} -c:v libx264 -c:a aac -hls_time 4 -hls_list_size 0 -hls_segment_filename _doc/ffmpeg-test/segment_d.ts -f hls -y ${outPath}`;

                useExecuteFFmpeg({
                    cmd,
                    success: () => {
                        console.log('成功回调=====')
                        // imagePath.value=outPath
                    },
                    fail: () => {
                        console.log('失败====')
                    }
                })
            },

            //图片格式转换
            async changeImageType() {
                let inputPath = await plus.io.convertLocalFileSystemURL('/static/logo.png');
                let outPath = `/storage/emulated/0/DCIM/Camera/logo_${Date.now()}.jpg`
                let cmd = `-i ${inputPath} ${outPath}`;

                useExecuteFFmpeg({
                    cmd,
                    success: () => {
                        console.log('成功回调====')
                        imagePath.value = outPath
                    }
                })
            },

            async imageRota() {
                let inputPath = await plus.io.convertLocalFileSystemURL('/static/crop.png');
                let outPath = `/storage/emulated/0/DCIM/Camera/logo_${Date.now()}.png`
                let cmd = `-i ${inputPath} -vf "transpose=0" ${outPath}`;
                // transpose=0:逆时针旋转 90 度。
                // transpose=1:顺时针旋转 90 度。
                // transpose=4,vflip:垂直翻转。
                useExecuteFFmpeg({
                    cmd,
                    success: () => {
                        console.log('成功回调====')
                        imagePath.value = outPath
                    }
                })
            },
            imageCrop() {
                uni.navigateTo({
                    url: '/pages/ffmpeg/cropImage'
                })
                // let inputPath=await plus.io.convertLocalFileSystemURL('/static/crop.png');
                // let outPath=`/storage/emulated/0/DCIM/Camera/crop_${Date.now()}.png`
                // let cmd=`-i ${inputPath} -vf "crop=300:200:0:0" ${outPath}`;

                // // crop=w:h:x:y
                // // w裁剪后的宽度
                // // h裁剪后的高
                // //x y 裁剪起点坐标
                // useExecuteFFmpeg({
                //  cmd,
                //  success:()=>{
                //      console.log('成功回调====')
                //      imagePath.value=outPath
                //  }
                // } as FfmpegType)
            }
        }
    }
</script>

<style>
    .video_1 {
        z-index: -99
    }

    .btn-wrap {
        flex-direction: row;
        flex-wrap: wrap;
        padding: 10px;
        justify-content: flex-start;
    }

    .btn {
        margin: 0 10px 15px 0;
    }

    .content-wrap {
        padding: 10px;
        width: 100%;
        min-height: 100px;
    }

    .content {
        width: 100%;
        height: 100%;
        border: 1px solid #ccc;
        background-color: #f2f2f2;
    }
</style>

其他插件预览

隐私、权限声明

1. 本插件需要申请的系统权限列表:

android.permission.READ_EXTERNAL_STORAGE android.permission.WRITE_EXTERNAL_STORAGE

2. 本插件采集的数据、发送的服务器地址、以及数据用途说明:

3. 本插件是否包含广告,如包含需详细说明广告表达方式、展示频率: