更新记录
1.0.6(2026-06-09)
_ 修复iOS端IPA上传报存在arm64e架构的问题
1.0.5(2026-05-27)
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实现安卓端对视频、音频、图片的编辑功能
- 功能支持
视频裁剪
视频合并 (注:同种类型文件合并)
视频格式转换
音频合并 (注:同种类型文件合并)
音频格式转换
获取媒体信息
图片裁剪
图片格式转换
图片旋转
- ....更多功能可查阅官网
调用方法说明
方法参数说明
| 属性 |
类型 |
描述 |
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:画质控制参数,值越小画质越高、体积越大,常用 18 到 28
-qp:固定量化参数,值越小画质越高,常用于快速控制压缩强度
-preset:编码速度与压缩效率平衡,常用 ultrafast、fast、medium、slow
-vf:视频滤镜,可组合 scale、crop、fps 等能力
-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 模块,不同基座能力可能不同
-
合并
方法参数说明
| 属性 |
类型 |
描述 |
type |
number |
合并的文件类型 默认1 视频格式 ,可选2 音频格式 |
filePath |
string |
文件路径 只支持同种类型文件 |
outPath |
string |
文件输出路径 |
success |
function |
成功回调 |
fail |
function |
失败回调 |
方法参数说明
| 属性 |
类型 |
描述 |
inputPath |
string |
输入地址 |
outPath |
string |
输出地址 |
vType |
string |
要转换的视频类型 |
| 属性 |
类型 |
描述 |
filePath |
string |
文件地址 |
| 属性 |
类型 |
描述 |
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>
其他插件预览