更新记录
1.4.0(2023-09-04)
本次主要更新: 1.更新ios sdk,解决提交appstore,提示sdk版本号问题
1.3.0(2022-07-21)
本次主要更新: 1.andorid增加:libx264、libx265...等等的支持,就是完整版
1.2.0(2022-07-20)
本次主要更新: 1.新增 查询媒体文件信息 方法 2.增加相关回调信息
查看更多平台兼容性
Android | Android CPU类型 | iOS |
---|---|---|
适用版本区间:5.0 - 12.0 | armeabi-v7a:支持,arm64-v8a:支持,x86:支持 | 适用版本区间:10 - 16 |
原生插件通用使用流程:
- 购买插件,选择该插件绑定的项目。
- 在HBuilderX里找到项目,在manifest的app原生插件配置中勾选模块,如需要填写参数则参考插件作者的文档添加。
- 根据插件作者的提供的文档开发代码,在代码中引用插件,调用插件功能。
- 打包自定义基座,选择插件,得到自定义基座,然后运行时选择自定义基座,进行log输出测试。
- 开发完毕后正式云打包
付费原生插件目前不支持离线打包。
Android 离线打包原生插件另见文档 https://nativesupport.dcloud.net.cn/NativePlugin/offline_package/android
iOS 离线打包原生插件另见文档 https://nativesupport.dcloud.net.cn/NativePlugin/offline_package/ios
注意事项:使用HBuilderX2.7.14以下版本,如果同一插件且同一appid下购买并绑定了多个包名,提交云打包界面提示包名绑定不一致时,需要在HBuilderX项目中manifest.json->“App原生插件配置”->”云端插件“列表中删除该插件重新选择
KJ-FFmpeg
FFmpeg升级增强完整版、跟官方video不冲突、视频编辑 音频编辑 视频压缩 合成 裁剪 图片 水印 gif 转码...
使用
<template>
<view class="content">
<video :src="src" style="width: 100%;"></video>
<view class="title">命令例子</view>
<view class="btns">
<button type="primary" @click="audioMixing">音频混合</button>
<button type="primary" @click="audioMerge">音频合并</button>
<button type="primary" @click="audioCropping">音频裁剪</button>
<button type="primary" @click="audioTranscoding">音频转码</button>
<button type="primary" @click="movTomp4">mov转mp4</button>
<button type="primary" @click="changeResolution">改变分辨率</button>
<button type="primary" @click="startVideoRate">改变视频速率</button>
<button type="primary" @click="concatVideo">视频合并</button>
<button type="primary" @click="videoThumbnail">视频获取所有帧</button>
<button type="primary" @click="addWatermark">视频加水印</button>
<button type="primary" @click="imageMirror">图片上下左右镜像</button>
<button type="primary" @click="videoMirror">视频上下左右镜像</button>
</view>
<view class="title">取消任务相关方法</view>
<view class="btns">
<button type="primary" @click="cancelAll">取消所有任务</button>
<button type="primary" @click="cancelSessionId">根据任务ID取消任务</button>
<button type="primary" @click="getListSessions">获取所有已完成的任务</button>
</view>
<view class="title">获取媒体信息相关方法</view>
<view class="btns">
<button type="primary" @click="ffprobe_getMediaInformation">获取媒体信息</button>
<button type="primary" @click="ffprobe_startCommand">命令获取媒体信息</button>
</view>
</view>
</template>
<script>
const KJFFmpeg = uni.requireNativePlugin('KJ-FFmpeg');
export default {
data() {
return {
src: plus.io.convertLocalFileSystemURL("static/dad%23d.mp4"),
sessionId: null
}
},
onLoad() {
},
methods: {
startCommand(command, savePath) {
plus.nativeUI.showWaiting("开始...", {
back: 'none'
});
var dic = {
"command": command
}
console.log(JSON.stringify(dic))
KJFFmpeg.startCommand(dic, (res) => {
console.log("完成回调:" + JSON.stringify(res));
/**
* 返回字段说明:{"createTime":"2022-07-05 15:35:52","startTime":"2022-07-05 15:35:52","error":null,
* "endTime":"2022-07-05 15:36:02","sessionId":1,"duration":9691,"returnCode":0,"state":3,"command":""}
* sessionId:任务ID
* state:0(创建) 1(执行) 2(失败) 3(完成)
* returnCode: 0(成功) 255(取消) 其它(失败)
* command:执行的命令
* duration:任务执行时长
* createTime:任务创建时间
* startTime:任务开始时间
* endTime:任务结束时间
* */
plus.nativeUI.closeWaiting();
if (res.returnCode == 0) {
uni.showToast({
title: '成功了',
duration: 2000
});
this.src = savePath;
uni.saveImageToPhotosAlbum({
filePath: savePath
})
} else {
uni.showToast({
title: '失败了',
duration: 2000
});
}
}, (res) => {
console.log("统计信息回调:" + JSON.stringify(res));
/**
* 返回字段说明:{"videoQuality":0,"speed":4.732010117327661,"videoFps":115.13915252685547,"bitrate":723.1251261083256,"time":6041,"videoFrameNumber":147,"sessionId":2,"size":546089}
* videoQuality:视频质量
* speed:速度
* videoFps: 视频fps
* bitrate:比特率
* time:时长 进度=time/源文件时长
* videoFrameNumber:视频帧号
* sessionId:任务ID
* size:大小
**/
}, (res) => {
console.log("开始任务回调:" + JSON.stringify(res));
this.sessionId = res.sessionId;
}, (res) => {
console.log("log回调:" + JSON.stringify(res));
/**
* 返回字段说明:{"sessionId":1,"meessage":"ffmpeg version v4.5-dev-3393-g30322ebe3c","level":32}
* meessage:日志描述
* level:日志等级 -16(标准差) -8(不打印输出) 0(出了点问题,我们现在会崩溃) 8(出了点问题,无法恢复)
* 16(出了点问题,无法无损恢复) 24(有些东西看起来不正确) 32(标准信息) 40(详细资料) 48(仅对 libav* 开发人员有用的东西)
* 56(极其冗长的调试,对 libav* 开发很有用)
* */
})
},
audioMixing() {
var filePath = plus.io.convertLocalFileSystemURL("static/pre.mp3");
var filePath2 = plus.io.convertLocalFileSystemURL("static/yuan.mp3");
var savePath = plus.io.convertLocalFileSystemURL("_doc/" + new Date().getTime() +
".mp3");
var command = "-i " + filePath + " -i " + filePath2 +
" -filter_complex amix=inputs=2:duration=first -strict -2 " + savePath;
this.startCommand(command, savePath);
},
audioMerge() {
var filePath = plus.io.convertLocalFileSystemURL("static/pre.mp3");
var filePath2 = plus.io.convertLocalFileSystemURL("static/yuan.mp3");
var savePath = plus.io.convertLocalFileSystemURL("_doc/" + new Date().getTime() +
".mp3");
var command = "-i concat:" + filePath + "|" + filePath2 + " -acodec copy " + savePath;
this.startCommand(command, savePath);
},
audioCropping() {
var filePath = plus.io.convertLocalFileSystemURL("static/m4a.m4a");
var savePath = plus.io.convertLocalFileSystemURL("_doc/" + new Date().getTime() +
".mp3");
var command = "-i " + filePath + " -ss 00:00:00 -t 1 " + savePath; //-ss 裁剪时间,后跟裁剪开始时间,以及 -t 裁剪时间
this.startCommand(command, savePath);
},
audioTranscoding() {
var filePath = plus.io.convertLocalFileSystemURL("static/pre.mp3");
var savePath = plus.io.convertLocalFileSystemURL("_doc/" + new Date().getTime() +
".m4a");
var command = "-i " + filePath + " " + savePath;
this.startCommand(command, savePath);
},
changeResolution() {
uni.chooseVideo({
sourceType: ['camera', 'album'],
compressed: false,
success: (res) => {
var filePath = plus.io.convertLocalFileSystemURL(res.tempFilePath);
var savePath = plus.io.convertLocalFileSystemURL("_doc/" + new Date().getTime() +
".mp4");
var width = 1080
var command = "-i " + filePath + " -vf scale=" + width + ":-1 -y " + savePath;
this.startCommand(command, savePath);
},
});
},
movTomp4() {
var filePath = plus.io.convertLocalFileSystemURL("static/1.mp4");
var savePath = plus.io.convertLocalFileSystemURL("_doc/" + new Date().getTime() +
".mov");
var command = "-i " + filePath +
" -c:v libx264 -vf scale=1080:-1 -crf 23 -preset medium -movflags +faststart -c:a aac " + savePath;
this.startCommand(command, savePath);
},
startVideoRate() {
uni.chooseVideo({
sourceType: ['camera', 'album'],
compressed: false,
success: (res) => {
var filePath = plus.io.convertLocalFileSystemURL(res.tempFilePath);
var savePath = plus.io.convertLocalFileSystemURL("_doc/" + new Date().getTime() +
".mp4");
var command = "-i " + filePath +
" -an -filter_complex \"[0:v]setpts=0.5*PTS[v];[0:a]atempo=2.0[a]\" -map \"[v]\" -map \"[a]\" " +
savePath
this.startCommand(command, savePath);
}
});
},
concatVideo() {
var filePath = plus.io.convertLocalFileSystemURL("static/inputs.text");
var savePath = plus.io.convertLocalFileSystemURL("_doc/" + new Date().getTime() +
".mp4");
var command = "-f concat -i " + filePath + " -c copy " + savePath;
this.startCommand(command, savePath);
},
videoThumbnail() {
uni.chooseVideo({
sourceType: ['camera', 'album'],
compressed: false,
success: (res) => {
var filePath = plus.io.convertLocalFileSystemURL(res.tempFilePath);
uni.getVideoInfo({
src: filePath,
success: (image) => {
var savePath = plus.io.convertLocalFileSystemURL("_doc");
var command = "-i " + filePath + " -vsync 0 -s " + image.width +
"*" + image.height + " " + savePath +
"/%d.jpeg";
this.startCommand(command, savePath);
}
});
}
});
},
addWatermark() {
uni.chooseVideo({
sourceType: ['camera', 'album'],
compressed: true,
success: (res) => {
var filePath = plus.io.convertLocalFileSystemURL(res.tempFilePath);
var logoPath = plus.io.convertLocalFileSystemURL("static/logo.png");
var savePath = plus.io.convertLocalFileSystemURL("_doc/" + new Date().getTime() +
".mp4");
var command = "-i " + filePath + " -vf \"movie=" + logoPath +
",scale=64:48[watermask];[in][watermask] overlay=30:10 [out]\" " + savePath;
this.startCommand(command, savePath);
}
});
},
imageMirror() {
uni.chooseImage({
sourceType: ['camera', 'album'],
compressed: true,
success: (res) => {
console.log(JSON.stringify(res));
var filePath = plus.io.convertLocalFileSystemURL(res.tempFilePaths[0]);
var savePath = plus.io.convertLocalFileSystemURL("_doc/" + new Date().getTime() +
".jpg");
//hflip 左右镜像 vflip 上下镜像 //https://blog.csdn.net/u010164190/article/details/112689804
var command = "-i " + filePath + " -vf 'hflip' -y " + savePath;
this.startCommand(command, savePath);
}
});
},
videoMirror() {
uni.chooseVideo({
sourceType: ['camera', 'album'],
compressed: true,
success: (res) => {
var filePath = plus.io.convertLocalFileSystemURL(res.tempFilePath);
var savePath = plus.io.convertLocalFileSystemURL("_doc/" + new Date().getTime() +
".mp4");
//hflip 左右镜像 vflip 上下镜像 //https://blog.csdn.net/u010164190/article/details/112689804
var command = "-i " + filePath + " -vf 'hflip' " + savePath;
this.startCommand(command, savePath);
}
});
},
cancelAll() {
KJFFmpeg.cancelAll((res) => {
console.log("cancelAll: " + JSON.stringify(res));
})
},
cancelSessionId() {
var dic = {
"sessionId": this.sessionId
}
KJFFmpeg.cancelAll((res) => {
console.log("cancelSessionId: " + JSON.stringify(res));
})
},
getListSessions() {
KJFFmpeg.getListSessions((res) => {
console.log("getListSessions: " + JSON.stringify(res));
})
},
ffprobe_getMediaInformation() {
console.log("ssss")
var dic = {
"filePath": plus.io.convertLocalFileSystemURL("static/pre.mp3"),
"timeout": 60000 //超时时间
}
KJFFmpeg.ffprobe_getMediaInformation(dic, (res) => {
/**
* allProperties:媒体相关信息
* */
console.log("完成回调:" + JSON.stringify(res));
}, (res) => {
console.log("log回调:" + JSON.stringify(res));
})
},
ffprobe_startCommand() {
var dic = {
"command": "-i " + plus.io.convertLocalFileSystemURL("static/test.mp4") + " -hide_banner"
}
KJFFmpeg.ffprobe_startCommand(dic, (res) => {
/**
* 根据log来查看相关信息
* */
console.log("完成回调:" + JSON.stringify(res));
}, (res) => {
//console.log("log回调:" + JSON.stringify(res));
})
}
}
}
</script>
<style>
.title {
text-align: center;
margin-top: 8px;
margin-bottom: 8px;
}
button {
font-size: 12px;
margin-top: 8px;
}
.btns {
display: flex;
flex-wrap: wrap;
flex-direction: row;
align-items: center;
justify-content: start;
}
</style>
命令大全,注意:文件要传绝对路径
FFmpeg文档汇总:https://ffmpeg.org/documentation.html
FFmpeg filters文档:https://ffmpeg.org/ffmpeg-filters.html
分解、复用
抽取音频流:-i input.mp4 -acodec copy -vn out.aac
抽取视频流:-i input.mp4 -vcodec copy -an out.h264
转格式:-i input.mp4 -vcodec copy -acodec copy out.flv
音视频合并:-i input.h264 -i input.aac -vcodec copy -acodec copy out.mp4
处理原始数据
抽取yuv数据:-i input.mp4 -an -c:v rawvideo -pixel_format yuv420p out.yuv
视频中提取图片:-i input.mp4 -r 30 -ss 00:00:10 -t 2 image-%3d.jpg
-r:每秒提取30帧
-t:取t秒时间的帧
图片文件转成YUV文件:-i image.png -pix_fmt yuv420p out.yuv
YUV转H264:-f rawvideo -pix_fmt yuv420p -s 320x240 -r 30 -i input.yuv -c:v libx264 -f rawvideo out.h264
提取PCM数据:-i out.mp4 -vn -ar 44100 -ac 2 -f s16le out.pcm
PCM转WAV:-f s16be -ar 8000 -ac 2 -acodec pcm_s16be -i input.pcm output.wav
滤镜
添加水印:-i input.mp4 -vf "movie=logo.png,scale=64:48[watermask];[in][watermask] overlay=30:10 [out]" out.mp4
PCM转WAV:-f s16be -ar 8000 -ac 2 -acodec pcm_s16be -i input.pcm output.wav
拼接与裁剪
裁剪:-i input.mp4 -ss 00:00:00 -t 10 out.mp4
ss:指定开始时间
-t:被裁剪后的时长
视频合并:-f concat -i inputs.txt -c copy out.flv
inputs.txt内容如下:
file '1.flv'
file '2.flv'
file '3.flv'
音频合并:-i input1.mp3 -i input2.mp3 -filter_complex '[0:0] [1:0] concat=n=2:v=0:a=1 [a]' -map [a] out.mp3
其它
视频转JPEG:-i input.flv -r 1 -f image2 image-%3d.jpeg
视频转gif:-i input.mp4 -ss 00:00:00 -t 10 out.gif
图片转视频:-f image2 -i image-%3d.jpeg out.mp4