更新记录

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

原生插件通用使用流程:

  1. 购买插件,选择该插件绑定的项目。
  2. 在HBuilderX里找到项目,在manifest的app原生插件配置中勾选模块,如需要填写参数则参考插件作者的文档添加。
  3. 根据插件作者的提供的文档开发代码,在代码中引用插件,调用插件功能。
  4. 打包自定义基座,选择插件,得到自定义基座,然后运行时选择自定义基座,进行log输出测试。
  5. 开发完毕后正式云打包

付费原生插件目前不支持离线打包。
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

隐私、权限声明

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

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

插件不采集任何数据

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

使用中有什么不明白的地方,就向插件作者提问吧~ 我要提问