更新记录

1.0.0(2023-03-01)

  1. 首次发布

平台兼容性

Android Android CPU类型 iOS
适用版本区间:5.0 - 12.0 armeabi-v7a:未测试,arm64-v8a:未测试,x86:未测试 适用版本区间:9 - 15

原生插件通用使用流程:

  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原生插件配置”->”云端插件“列表中删除该插件重新选择


使用说明

创建插件对象

var videoCompressModule = uni.requireNativePlugin('VideoCompressUniPlugin-VideoCompressModule');

视频压缩

uni.showLoading({
    title: '开始压缩'
});
let appendixPath = this.filePathList[index ? index : 0];
let params = {
    //输入输出
    inPath: plus.io.convertLocalFileSystemURL(appendixPath), //绝对路径
    // outPath: '', //输出绝对路径,非必填,默认为输入路径文件夹下,文件名为时间戳.mp4
    quality: 2,
}
videoCompressModule.compress(params, (res) => {
    //done标识完成
    if (res.done) {
        uni.hideLoading();
    }
}, (e) => {
    uni.hideLoading();
    uni.showToast({
        title: e.msg ? e.msg : '压缩失败',
        icon: "none"
    })
});

传参

property description
inPath 必传,视频文件的完整路径,通过uni-file-picker等组件选择视频后会获取到文件路径,通过使用plus.io.convertLocalFileSystemURL(path)转换为完整的路径作为参数
outPath 非必传,压缩完成的视频文件路径,默认为应用沙盒内
quality 非必传,视频压缩质量:1-低质量,2-中等质量,3-高质量,4-最大长宽为640x480,5-最大长宽为960x540,6-最大长宽为1280x720,7-最大长宽为1920x1080;Android默认为中等质量,iOS默认最大长宽为960x540,如果对压缩后的视频质量有一定要求,但有不想文件太大,可以设置为5
startTime 非必传,视频剪裁的开始时间,默认为0
duration 非必传,视频剪裁的持续时间,开始时间+持续时间可以确定一个区间,默认设置为源视频的时长
deleteOrigin 非必传,是否删除源视频,默认为false
includeAudio 非必传,是否包含音频信息
frameRate 视频帧率,默认为30

回调参数

property description
outPath 压缩完成的视频文件的路径,默认为应用沙盒内

取消视频压缩

videoCompressModule.cancelCompression({}, (res) => {
    uni.showToast({
        title: res ? res : '取消成功',
    })
}, (e) => {
    uni.showToast({
        title: e ? e.msg : '取消失败',
        icon: "none"
    })
});

传参

回调参数

获取视频信息

uni.showLoading({
    title: '操作中..'
});
// path:视频压缩后的路径
videoCompressModule.getMediaInfo({
    path: converted ? path : plus.io.convertLocalFileSystemURL(path) //本地视频绝对路径
}, (res) => {
    uni.hideLoading();
    console.log(res);
    this.videoInfo = res;
}, (e) => {
    uni.hideLoading();
    console.log(e.msg ? e.msg : '获取文件信息失败');
    uni.showToast({
        title: e.msg ? e.msg : '获取文件信息失败',
        icon: "none"
    })
    this.disabled = false;
});

传参

property description
path 必传,视频文件的完整路径,通过uni-file-picker等组件选择视频后会获取到文件路径,通过使用plus.io.convertLocalFileSystemURL(path)转换为完整的路径作为参数

回调参数

property description
path 视频文件所在完整路径
title 压缩完成的视频文件的路径,默认为应用沙盒内
author 作者
duration 视频时长(毫秒)
filesize 文件大小(位),如果要格式化为MB,可以除1000*1000
width 宽度
height 高度
orientation 视频方向

获取视频帧截图base64

如果使用uni.previewImage预览base64图片,可能会导致闪退(传递的数据过大),解决方法是将base64存储到本地,得到一个图片路径,根据这个路径预览图片;

let appendixPath = this.filePathList[index ? index : 0];
let params = {
    position: position,
    path: plus.io.convertLocalFileSystemURL(appendixPath), //绝对路径
    quality: 100
}
videoCompressModule.getThumbnailBase64(params, (res) => {
    console.log("res:", res)
    uni.hideLoading();
    if (!res) {
        return;
    }
    this.imageData = res.frameBase64;
    this.imageData = this.imageData.replace(/[\r\n]/g, "");
    this.imageData = `data:image/jpg;base64,${this.imageData}`;
}, (e) => {
    uni.hideLoading();
    uni.showToast({
        title: e ? e.msg : '获取视频帧失败',
        icon: "none"
    })
});

传参

property description
position 非必传,视频帧的位置,单位是秒
path 必传,视频文件的完整路径,通过uni-file-picker等组件选择视频后会获取到文件路径,通过使用plus.io.convertLocalFileSystemURL(path)转换为完整的路径作为参数
quality 非必传,图片质量,默认为100,范围为0-100,值越大质量越高

回调参数

property description
frameBase64 视频帧的base64字符串,拼接上data:image/jpg;base64,可以直接用于image显示

获取视频帧截图保存到文件并返回文件路径

uni.showLoading({
    title: '操作中..'
});
let appendixPath = this.filePathList[index ? index : 0];
let params = {
    position: position,
    path: plus.io.convertLocalFileSystemURL(appendixPath), //绝对路径
    quality: 100
}
console.log(params)
videoCompressModule.getThumbnailPath(params, (res) => {
    uni.hideLoading();
    console.log(res.filePath);
    this.frameImagePath = res.filePath;
}, (e) => {
    uni.hideLoading();
    uni.showToast({
        title: e ? e.msg : '获取视频帧失败',
        icon: "none"
    })
});

传参

property description
position 非必传,视频帧的位置,单位是秒
path 必传,视频文件的完整路径,通过uni-file-picker等组件选择视频后会获取到文件路径,通过使用plus.io.convertLocalFileSystemURL(path)转换为完整的路径作为参数
quality 非必传,图片质量,默认为100,范围为0-100,值越大质量越高

回调参数

property description
filePath 视频帧图片文件保存的完整路径

删除视频文件

let appendixPath = this.filePathList[index ? index : 0];
videoCompressModule.deleteVideo({
    //输入输出
    path: plus.io.convertLocalFileSystemURL(appendixPath), //绝对路径
}, (res) => {
    uni.showToast({
        title: res ? res : '删除成功'
    })
    this.reset();
}, (e) => {
    uni.showToast({
        title: e ? e.msg : '删除失败',
        icon: "none"
    })
});

传参

property description
path 必传,视频文件的完整路径,通过uni-file-picker等组件选择视频后会获取到文件路径,通过使用plus.io.convertLocalFileSystemURL(path)转换为完整的路径作为参数

回调参数

完整示例


<template>
  <view class="content">
    <view class="picker-wrapper">
      <uni-file-picker
          ref="files"
          v-model='fileList'
          :auto-upload="false"
          :disabled='disabled'
          :limit="1"
          file-mediatype='video'
          @select='handleSelect'
      />
    </view>
    <view class="operation-layout">
      <button class="operation-btn" @click="getThumbnailBase64(0,positionBase64)">获取帧截图base64字符串</button>
      <view class="layout-horizontal">
        <text>截图位置(秒):</text>
        <slider :value="positionBase64" @change="sliderChangeBase64" min="0" :max="videoDuration" step="0.1"
                show-value/>
      </view>
      <view class="layout-horizontal">
        <text>截图质量:</text>
        <slider :value="qualityBase64Snapshot" @change="sliderChangeBase64SnapshotQuality" min="0" :max="100" step="1"
                show-value/>
      </view>

      <view style="width: 100%;margin-top: 40rpx" v-if="imageData">
        <text>帧截图base64字符串:</text>
        <view style="width: 100%;background: black;display: flex;flex-direction: column"
              @click="previewImage([imageData])">
          <image style="width:100%;height: 200px;" mode="aspectFit" :src="imageData"></image>
        </view>
      </view>
    </view>
    <view class="operation-layout">
      <button class="operation-btn" @click="getThumbnailPath(0,positionFramePath)">获取帧截图文件路径</button>
      <view class="layout-horizontal">
        <text>截图位置(秒):</text>
        <slider :value="positionFramePath" @change="sliderChangeFramePath" min="0" :max="videoDuration" step="0.1"
                show-value/>
      </view>
      <view class="layout-horizontal">
        <text>截图质量:</text>
        <slider :value="qualityFramePathSnapshot" @change="sliderChangeFramePathSnapshotQuality" min="0" :max="100"
                step="1"
                show-value/>
      </view>
      <view class="text-layout" v-if="frameImagePath">
        <text>视频帧截图文件路径:</text>
        <text>{{ frameImagePath ? `\n${frameImagePath}` : '' }}</text>
        <view style="width: 100%;background: black;display: flex;flex-direction: column"
              @click="previewImage([frameImageUrl])">
          <image id="framePathImage" key="framePathImage" style="width:100%;height: 200px;" mode="aspectFit"
                 :src="frameImageUrl"></image>
        </view>
      </view>
    </view>
    <view class="operation-layout">
      <button class="operation-btn" @click="compress(0)">压缩视频</button>
      <view class="layout-horizontal">
        <text>压缩质量:</text>
        <slider :value="qualityCompress" @change="sliderChangeCompressQuality" min="1" max="7" step="1" show-value/>
      </view>
      <view class="layout-horizontal">
        <text>开始时间(秒):</text>
        <slider :value="startTime" @change="sliderChangeStartTime" min="0" :max="videoDuration" step="1" show-value/>
      </view>
      <view class="layout-horizontal">
        <text>时长(秒):</text>
        <slider :value="duration" @change="sliderChangeDuration" min="0" :max="videoDuration" step="0.1" show-value/>
      </view>
      <view class="layout-horizontal">
        <text>是否包含音频:</text>
        <radio-group @change="radioChange" style="margin-left: 15px;flex: 1;line-height:40px;margin-bottom: 10px;">
          <view class="layout-horizontal">
            <view class="layout-horizontal-pure" v-for="(item, index) in items" :key="item.value">
              <view>
                <radio :value="item.value" :checked="index === current"/>
              </view>
              <view>{{ item.name }}</view>
            </view>
          </view>

        </radio-group>
      </view>

      <view style="width: 100%;margin-top: 40rpx" v-if="compressedVideoInfo.path">
        <text>压缩后视频:</text>
        <text>{{ compressedVideoInfo.path ? `\n${JSON.stringify(compressedVideoInfo)}` : '' }}</text>
        <view style="width: 100%;height: 200px;background: black;display: flex;flex-direction: column">
          <video style="width: 100%;" :src="compressedVideoUrl" :http-cache="false"></video>
        </view>
      </view>
    </view>
    <view class="operation-layout">
      <button class="operation-btn" @click="getMediaInfo">获取视频信息</button>
      <view style="width: 100%;" v-if="videoInfo.path">
        <text>原视频:</text>
        <text>{{ videoInfo.path ? `\n${JSON.stringify(videoInfo)}` : '' }}</text>
        <view style="width: 100%;height: 200px;background: black;display: flex;flex-direction: column">
          <video style="width: 100%;" :src="sourceVideoUrl"></video>
        </view>
      </view>
    </view>
    <view class="operation-layout">
      <button class="operation-btn" @click="cancelCompression">取消压缩</button>
    </view>
    <view class="operation-layout">
      <button class="operation-btn" @click="deleteVideo(0)">删除视频文件</button>
    </view>
    <view class="operation-layout">
      <button class="operation-btn" @click="reset">重置</button>
    </view>
  </view>
</template>

<script>
// #ifdef APP-PLUS
var videoCompressModule = uni.requireNativePlugin('VideoCompressUniPlugin-VideoCompressModule');
// #endif
export default {
  data() {
    return {
      title: '视频压缩示例',
      // 视频帧base64
      imageData: undefined,
      // 是否禁用
      disabled: false,
      // 选中的文件列表
      fileList: [],
      // 文件路径列表
      filePathList: [],
      // 视频文件信息
      videoInfo: {},
      // 视频文件信息
      compressedVideoInfo: {},
      // 视频帧图片文件路径
      frameImagePath: undefined,
      // 视频帧位置
      positionBase64: 0,
      // 视频帧质量
      qualityBase64Snapshot: 100,
      // 视频帧位置
      positionFramePath: 0,
      // 视频帧质量
      qualityFramePathSnapshot: 100,
      // 视频压缩质量
      qualityCompress: 2,
      // 是否包含音频数据选项
      items: [{
        value: '1',
        name: '包含',
        checked: 'true'
      },
        {
          value: '0',
          name: '不包含'
        },
      ],
      // 当前勾选的是否包含音频
      current: 0,
      // 开始时间
      startTime: 0,
      // 持续时间
      duration: 0,
    }
  },
  computed: {
    // 将平台绝对路径转换为本地URL路径
    sourceVideoUrl() {
      if (this.videoInfo.path) {
        console.log('sourceVideoUrl', this.videoInfo.path)
        return `file://${this.videoInfo.path}`;
      }
      return undefined;
    },
    // 将平台绝对路径转换为本地URL路径
    compressedVideoUrl() {
      if (this.compressedVideoInfo.path) {
        console.log('compressedVideoUrl', this.compressedVideoInfo.path)
        return `file://${this.compressedVideoInfo.path}`;
      }
      return undefined;
    },
    // 将平台绝对路径转换为本地URL路径
    frameImageUrl() {
      if (this.frameImagePath) {
        return `file://${this.frameImagePath}`;
      }
      return undefined;
    },
    // 视频最大的位置
    videoDuration() {
      if (this.videoInfo.duration) {
        return parseFloat((this.videoInfo.duration / 1000).toFixed(1));
      }
      return 0;
    },
  },
  onLoad() {

  },
  methods: {
    radioChange: function (evt) {
      for (let i = 0; i < this.items.length; i++) {
        if (this.items[i].value === evt.detail.value) {
          this.current = i;
          break;
        }
      }
    },
    // base64位置改变
    sliderChangeBase64(e) {
      this.positionBase64 = e.detail.value;
    },
    // frame path位置改变
    sliderChangeFramePath(e) {
      this.positionFramePath = e.detail.value;
    },
    // frame path 截图质量改变
    sliderChangeBase64SnapshotQuality(e) {
      this.qualityBase64Snapshot = e.detail.value;
    },
    // frame path 截图质量改变
    sliderChangeFramePathSnapshotQuality(e) {
      this.qualityFramePathSnapshot = e.detail.value;
    },
    // 压缩质量改变
    sliderChangeCompressQuality(e) {
      this.qualityCompress = e.detail.value;
    },
    // 开始时间
    sliderChangeStartTime(e) {
      this.startTime = e.detail.value;
    },
    // 持续时间
    sliderChangeDuration(e) {
      this.duration = e.detail.value;
    },
    // // 预览图片
    // previewImageBase64(base64) {
    //   // 预览图片
    //   uni.previewImage({
    //     urls: urls,
    //     longPressActions: {
    //       itemList: ['发送给朋友', '保存图片', '收藏'],
    //       success: function (data) {
    //         console.log('选中了第' + (data.tapIndex + 1) + '个按钮,第' + (data.index + 1) + '张图片');
    //       },
    //       fail: function (err) {
    //         console.log(err.errMsg);
    //       }
    //     }
    //   });
    // },
    // 预览图片
    previewImage(urls) {
      // 预览图片
      uni.previewImage({
        urls: urls,
        longPressActions: {
          itemList: ['发送给朋友', '保存图片', '收藏'],
          success: function (data) {
            console.log('选中了第' + (data.tapIndex + 1) + '个按钮,第' + (data.index + 1) + '张图片');
          },
          fail: function (err) {
            console.log(err.errMsg);
          }
        }
      });
    },
    // 选择文件后
    handleSelect(e) {
      console.log(this.fileList)
      console.log("e11111", this.fileList)
      this.filePathList = e.tempFilePaths;
      uni.showToast({
        icon: 'none',
        title: this.filePathList,
        duration: 6000,
      });
      this.disabled = false;
      this.getMediaInfo();
    },

    // 获取视频帧base64字符串
    getThumbnailBase64(index, position) {
      uni.showLoading({
        title: '操作中..'
      });
      let appendixPath = this.filePathList[index ? index : 0];
      this.imageData = undefined;
      let params = {
        position: this.positionBase64,
        path: plus.io.convertLocalFileSystemURL(appendixPath), //绝对路径
        quality: this.qualityBase64Snapshot
      }
      console.log(params)
      videoCompressModule.getThumbnailBase64(params, (res) => {
        console.log("res:", res)
        uni.hideLoading();
        if (!res) {
          return;
        }
        this.imageData = res.frameBase64;
        this.imageData = this.imageData.replace(/[\r\n]/g, "");
        this.imageData = `data:image/jpg;base64,${this.imageData}`;
      }, (e) => {
        uni.hideLoading();
        uni.showToast({
          title: e ? e.msg : '获取视频帧失败',
          icon: "none"
        })
      });
    },
    // 获取视频帧文件路径
    getThumbnailPath(index, position) {
      uni.showLoading({
        title: '操作中..'
      });
      this.frameImagePath = undefined;
      let appendixPath = this.filePathList[index ? index : 0];
      let params = {
        position: this.positionFramePath,
        path: plus.io.convertLocalFileSystemURL(appendixPath), //绝对路径
        quality: this.qualityFramePathSnapshot
      }
      console.log(params)
      videoCompressModule.getThumbnailPath(params, (res) => {
        uni.hideLoading();
        console.log(res.filePath);
        this.frameImagePath = res.filePath;
      }, (e) => {
        uni.hideLoading();
        uni.showToast({
          title: e ? e.msg : '获取视频帧失败',
          icon: "none"
        })
      });
    },
    // 删除视频文件
    deleteVideo(index) {
      let appendixPath = this.filePathList[index ? index : 0];
      videoCompressModule.deleteVideo({
        //输入输出
        path: plus.io.convertLocalFileSystemURL(appendixPath), //绝对路径
      }, (res) => {
        uni.showToast({
          title: res ? res : '删除成功'
        })
        this.reset();
      }, (e) => {
        uni.showToast({
          title: e ? e.msg : '删除失败',
          icon: "none"
        })
      });
    },
    // 重置数据
    reset() {
      this.fileList = [];
      this.filePathList = [];
      this.videoInfo = {};
      this.compressedVideoInfo = {};
      this.imageData = undefined;
      this.frameImagePath = undefined;
      // 视频帧位置
      this.positionBase64 = 0;
      // 视频帧质量
      this.qualityBase64Snapshot = 100;
      // 视频帧位置
      this.positionFramePath = 0;
      // 视频帧质量
      this.qualityFramePathSnapshot = 100;
      // 视频压缩质量
      this.qualityCompress = 2;
      // 当前勾选的是否包含音频
      this.current = 0;
      // 开始时间
      this.startTime = 0;
      // 持续时间
      this.duration = 0;
    },
    // 取消压缩
    cancelCompression() {
      videoCompressModule.cancelCompression({}, (res) => {
        uni.showToast({
          title: res ? res : '取消成功',
        })
      }, (e) => {
        uni.showToast({
          title: e ? e.msg : '取消失败',
          icon: "none"
        })
      });
    },

    // 压缩视频
    compress(index) {
      uni.showLoading({
        title: '开始压缩'
      });
      this.compressedVideoInfo = {};
      let appendixPath = this.filePathList[index ? index : 0];
      let params = {
        //输入输出
        inPath: plus.io.convertLocalFileSystemURL(appendixPath), //绝对路径
        // 视频压缩质量
        quality: this.qualityCompress,
        // 开始时间
        startTime: this.startTime,
        // 持续时间
        duration: this.duration,
        // 是否包含音频
        includeAudio: this.items[this.current].value === '1',
      }
      console.log('params:', params)
      videoCompressModule.compress(params, (res) => {
        //done标识完成
        if (res.done) {
          uni.hideLoading();
          this.getInfo(true, res.outPath).then(info => {
            this.compressedVideoInfo = info;
          })
        }
      }, (e) => {
        uni.hideLoading();
        uni.showToast({
          title: e.msg ? e.msg : '压缩失败',
          icon: "none"
        })
      });
    },
    // 获取源视频信息
    getMediaInfo() {
      this.getInfo(false, this.filePathList[0]).then(info => {
        this.videoInfo = info;
        this.duration = parseFloat((info.duration / 1000).toFixed(1));
      })
    },
    // 获取文件信息
    getInfo(converted, path) {
      return new Promise((resolve, reject) => {
        uni.showLoading({
          title: '操作中..'
        });
        // path:视频压缩后的路径
        videoCompressModule.getMediaInfo({
          path: converted ? path : plus.io.convertLocalFileSystemURL(path) //本地视频绝对路径
        }, (res) => {
          uni.hideLoading();
          console.log(res);
          resolve(res);
        }, (e) => {
          uni.hideLoading();
          console.log(e.msg ? e.msg : '获取文件信息失败');
          uni.showToast({
            title: e.msg ? e.msg : '获取文件信息失败',
            icon: "none"
          })
          this.disabled = false;
          reject();
        });
      });
    },
  }
}
</script>

<style lang="scss">
.content {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  box-sizing: border-box;
  padding: 20px;
  overflow-y: scroll;
  overflow-x: hidden;
}

.operation-layout {
  width: 100%;
  display: flex;
  margin-top: 20px;
  box-sizing: border-box;
  padding: 10px 15px;
  flex-direction: column;
  border: 1px solid #DEDEDE;

  .layout-horizontal {
    width: 100%;
    display: flex;
    flex-direction: row;
    align-items: center;
    margin-top: 10px;

    slider {
      flex: 1;
    }
  }

  .layout-horizontal-pure {
    width: 100%;
    display: flex;
    flex-direction: row;
    align-items: center;
  }

  .operation-btn {
    width: 80%;
  }

  .operation-btn + .operation-btn {
    margin-top: 15px;
  }

}

.text-layout {
  width: 100%;
  overflow-y: scroll;
  margin-top: 20px;
}

.text-area {
  display: flex;
  justify-content: center;
}

.title {
  font-size: 18px;
  color: #8f8f94;
}

.picker-wrapper {
  width: 100%;
  margin-top: 20px;
}

text {
  font-size: 16px;
  font-family: PingFangSC-Semibold, PingFang SC;
  font-weight: 600;
  color: #333333;
  line-height: 22px;
  white-space: pre-line;
  display: block;
  text-overflow: ellipsis;
  word-wrap: break-word;
}

</style>

隐私、权限声明

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

存储

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

插件不采集任何数据

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

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