小程序端视频剪辑的春天来了?
发布于 4 年前 作者 chaozeng 2637 次浏览 来自 分享

前言-吐槽篇

在这个短视频遍地的时代,小程序端视频剪辑功能似乎一直是缺失状态,前段时间发布了一个视频编辑器的接口,试了下感觉也很鸡肋。也有在社区看到前辈们试水小程序音视频合成初探,研究了一番,其中涉及到几个重要的api:1-视频解码器,将视频解码,获得帧数据绘制到canvas上;2-音视频合成器,负责向原视频中添加音频;3-画面录制器,最后通过画面录制器导出成视频。这里必须要吐槽下小程序的文档,简约派代表,能省略的绝不多写一句,翻一下上述api文档便可石锤,几乎全靠猜,这么复杂的接口也不提供个demo。曾尝试按照上述思路做一些复杂的视频剪辑功能,比如图片视频合成一个,单单解决音频错位的问题就搞得头秃,可以说是步履维艰……

正文开始

最近发现小程序发布了一款视频剪辑插件 - 微剪,看了下官方提供的Demo还挺有趣的,相比于上述API功能更完善,感觉小程序一直在探索新姿势,真香啊。

小程序:是的,我啥都能做[手动狗头]

本文前半部分旨在结合上述插件,输出一个简单的视频剪辑小程序,感兴趣(不想动手只想白嫖)的同学也可以直接在小程序中搜索「微剪插件演示」,感受一下插件的功能;后半部分着重介绍下插件的一些核心思想和高级用法。

插件接入

微剪以__插件__的形式提供给开发者,不能单独使用,所以我们必须要__先有个小程序主体__(不会吧阿sir,这都想白嫖我?如何注册小程序请自行百度好伐)。插件的接入流程也比较简单,官方文档 写的比较清楚,这里就不啰嗦了。目前好像处于公测阶段,可以免费申请使用。

代码开发

上述准备工作做完后,我们就可以在代码中使用微剪插件了,插件暴露了两个组件clip和export,clip是入口组件,export为出口组件,其间的流程完全被插件接管,开发者无需关心也无法干涉(有点霸道总裁的味道了啊),通过一系列简单的配置后就可以看到插件的全貌了。【2020/09/23日 删除】

【2020/09/23日 新增】最新版插件提供两种开发模式,暂称为自动模式(仅通过少许配置即可)和手动模式(利用插件暴露的组件自定义开发,1.3.0版本新增)。先介绍第一种方式,按照下述步骤走一遍,应该就能看到插件的全貌了,问题不大。

step1:app.json 引入插件配置

参考这里:小程序插件配置

{
  "plugins": {
    "myPlugin": { // 自定义插件名,后面会用到
      "version": "1.3.0",  // 最新版本号即可
      "provider": "wx76f1d77827f78beb"  // 插件ID,固定值
    }
  }
}

step2:页面中添加插件

新建一个页面,在页面的json文件中引入插件暴露的clip组件

{
  "usingComponents": {
    "my-clip": "plugin://myPlugin/clip"
  }
}
my-clip: 自定义组件名字,在wxml中使用;
myPlugin: 引入插件的时候,在app.json中声明的插件名;
clip: 固定值,官方暴露的组件名称。

wxml文件:

<view>
    <my-clip settings="{{settings}}"></my-clip>
</view>

js文件:

Page({
  data: {
    settings: {
      common: {
        videoMaxDuration: 30, // 小程序限制最多拍摄30秒
        chooseMaxDuration: 1000, // 选择视频的默认时长限制
        clipMaxDuration: 60, // 裁切时长的默认限制
      }
    }
  },
})

settings有众多配置项,具体可参考官方文档;不设置也可以,插件提供了一套默认的配置项。

PS:这里得吐槽下插件的主题机制,感觉很繁琐。跟平时写vue组件等通过样式覆盖的方案不同,而是通过一系列配置项进行配置,这也意味着只能修改配置项涉及的UI,其他的一律无法修改。这里有个疑问,为什么不采用外部样式类的方案呢?感觉更灵活啊。

到这里,应该就可以在手机上看到插件的全貌了,截取了几张图如下:

step3:导出视频

点击上图的「下一步」按钮,就可以导出视频到相册中了。插件本身也支持自定义导出,下面简单介绍下用法:

step3-1:新增导出页

创建一个自定义的导出页,在导出页中引入插件提供的导出组件即可。

json文件:

{
    "usingComponents": {
      "my-export": "plugin://myPlugin/export"
    }
}

wxml文件:

<view>
    <view>自定义导出界面部分</view>
    <my-export bindexportsuccess="handleExportSuccess">
        <button>自定义导出按钮</button>
    </my-export>
</view>

然后在js文件中处理导出成功回调即可,导出组件提供了一系列属性,包括水印,导出进度等,具体导出数据结构及属性,可参考官方文档。

step3-2:配置导出页路径

新增上述导出页后,还需要通知插件导出页路径。还记得上文提到的 my-clip 组件中的settings属性吗,此为插件的配置项入口,在里面配置下即可。

  data: {
    settings: {
      common: {
        videoMaxDuration: 30, // 小程序限制最多拍摄30秒
        chooseMaxDuration: 1000, // 选择视频的默认时长限制
        clipMaxDuration: 60, // 裁切时长的默认限制
        exportPagePath: '/pages/export/export' // 自定义导出页面地址
      }
    }
  }

事已至此,通过开发工具的预览功能便可以在手机中看到自定义的导出页了。


【2020/09/23日 新增】

高级模式(1.3.0版本新增)

还记得上文提到的简单(自动)模式,我们只用到了插件暴露的两个组件,clip和export,难道插件只暴露了两个组件吗?就这?就这???

插件从1.3.0版本开始,开放了更多的组件出来,也提供了一种称为【高级接入】的方式,接下来让我们一探究竟:

高级模式需要先了解插件的数据机制,即下文提到的Track驱动模式,在此基础上可以结合插件暴露的组件进行自定义创作。

标准的数据驱动模式

高级模式,即文章开头提到的手动模式,遵循一套标准的Track数据结构,此处引用官方文档的一张图和介绍

什么是Track?

track顾名思义为轨道,播放器支持多种轨道,如:媒体(视频 或者 图片), 音乐, 特效, 滤镜, 文字 等。
每一种类型的轨道对应不同类型的track,可以简单的理解track就是轨道。

什么是Clip?

clip是轨道中的素材片段。举个例子: 你需要编辑三段视频,那么你需要将三个视频片段加入媒体轨道中,那么这里的每个视频片段就对应不同的clip,每个clip的属性会不相同,如: 视频时长,开始结束时间等。

上述官方定义看得有点懵?大白话简单理解,画面播放的时候,我们在某一时刻可以看到(听到)很多元素,比如原始画面的一帧,特效的一部分(几个小心心),滤镜,文字以及音乐等,Track就是这些元素的数据载体,不同的元素由不同类型的Track管理,所以Track是个数组。

随着时间轴的推移,我们又可以看到不同的视频、图片、特效切换效果等等,比如由图片A切换到图片B,再切换到C、由特效A切换到特效B等,这个时候就涉及到clip的概念,clip可以简单理解为上述Track的一个属性,里面存放着原始素材的信息(基本信息,展示时长等),比如上述🌰,负责媒体的Track的clips就得包含图片A、图片B以及图片C的信息,负责特效的Track的clips就得包含特效A和特效B的信息。

官方提供了详细的Track以及Clip的数据接口定义,具体请参考官方文档 数据结构文档 部分,这里只做核心思想解释。

积木已有,创作在你

最新版本(1.3.0)暴露的几个核心组件:

  1. wj-player:插件的核心播放器组件,对应的上述简单模式截图里的播放视频的容器;
  2. wj-camera:相机组件,可以拍摄视频,也可以从相册选择视频、图片,截图里的图片、视频就是从这来的;
  3. wj-clipper:裁切器组件,上述截图中可以看到视频一帧一帧画面的那部分,就是用这个组件实现的。

上述组件即插件实现的核心部分,还有个文字组件(wj-textEditor、向视频中添加文字)和导出组件(wj-export、与自动模式的导出组件基本相同)使用比较简单,每个组件都有各自的属性和方法,具体请参考高级组件API文档。结合标准的Track数据驱动,理论上可以搭配出与插件功能完全一致的视频剪辑体验,当然如果自动模式可以满足业务需求,那白嫖它,做条闲鱼不好吗?如果业务需求需要个性化,可以考虑使用上述手动模式,自力更生,丰衣足食。

光说不练假把式

下面通过一个简单的 相册集小demo 演示一下上述组件的用法,可以下载小程序片段到本地运行(注:由于插件的使用需要先申请资格,所以请填写一个有使用权限的小程序AppID)。主要使用了三个组件,wj-camera(收集素材)、wj-player(展示素材和特效)、wj-export(导出视频),通过camera页面从相册选择图片,然后搭配插件内置的一些特效输出到player页面中预览效果,最后通过export页面导出。

核心代码如下:

  • camera 页面

camera.wxml

<!--pages/camera/camare.wxml-->
<wj-camera bindmediachanged="onMediaChanged" >
    <view class="preview-button" catchtap="onClickDefaultBtn">
        <navigator data-source="jump" url="../player/player?type=default" hover-class="none">
            <text>随机特效</text>
        </navigator>
    </view>
    <view class="preview-button" style="right:230rpx;width:240rpx" catchtap="onClickPointBtn">
        <navigator data-source="jump" url="../player/player?type=wyyl" hover-class="none">
            <text>卡点视频(6图)</text>
        </navigator>
    </view>
</wj-camera>

camera.js

// pages/camera/camare.js
Page({
  data:{
    _media:[]
  },
  onMediaChanged(e){
    this.data._media = e.detail.track
  },
  onClickDefaultBtn(){
    global.testMedia = JSON.parse(JSON.stringify(this.data._media))
  },
  // 【万有引力】卡点设置
  onClickPointBtn(){
    let durationList = [2.6, 2.6, 2.6, 2.8, 2.8, 2.6], startAt = 0
    let copyMedia = JSON.parse(JSON.stringify(this.data._media))
    copyMedia.clips.forEach((item,index) => { // 更新卡点视频图片播放时长
      let duration = durationList[index]
      item.section.end = duration
      item.section.duration = duration
      item.startAt = startAt
      startAt += duration
    });
    copyMedia.duration = startAt
    global.testMedia = copyMedia
  }
})
  • player 页面

player.wxml

// pages/player/player.js
Page({
  data: {
    styleConfig: { height: 1000, width: 750 },
    type: 'default'
  },
  onLoad: function (option) {
    this.setData({
      type: option.type
    })
  },
  // 生命周期函数--监听页面初次渲染完成
  onReady: function () {
    this.bindPlayer()
  },
  // 绑定player
  bindPlayer() {
    this.player = this.selectComponent("#player");
  },
  // 设置player组件的数据
  async onPlayerReady() {
    let tracks = []
    if (this.data.type === 'default') {
      tracks = this.getDefaultTrack()
    } else {
      tracks = this.getWYYLTrack()
    }
    this.player.updateData(tracks) // 更新player的数据
    global.testExportTracks = tracks // 存储导出页数据
  },
  // 随机特效
  getDefaultTrack() {
    let startAt = -3, { TRACK_TYPES, CLIP_TYPES, Clip, ClipSection, Track } = global['wj-types']
    let mediaTrack = global.testMedia // 获取camera组件输出的media轨道
    let effectTrack = new Track({ // 新建一个effect轨道
      type: TRACK_TYPES.EFFECT,
      clips: []
    })
    let effectList = this.player.getEffects(); // 获取player内置的特效
    let randomEffects = new Array(mediaTrack.clips.length).fill(null).map((item, index) => {
      startAt += 3
      return new Clip({
        id: `effect-${index}`,
        type: CLIP_TYPES.EFFECT,
        key: this._getRandomEffect(effectList).key,
        section: new ClipSection({
          start: 0,
          end: 3
        }),
        startAt: startAt
      })
    })
    effectTrack.clips = randomEffects // 更新effect轨道的clips数据
    return [mediaTrack, effectTrack]
  },
  // 【万有引力】卡点特效
  getWYYLTrack() {
    let { TRACK_TYPES, CLIP_TYPES, Clip, ClipSection, Track } = global['wj-types']
    let mediaTrack = global.testMedia // 获取camera组件输出的media轨道
    let effectTrack = new Track({ // 新建一个effect轨道
      type: TRACK_TYPES.EFFECT,
      clips: []
    })
    // 定制卡点特效
    let effectList = [
      { key: 'Swing', duration: 2.6, startAt: 0 },

      { key: 'SoulOut', duration: 0.6, startAt: 2.6 },
      { key: 'Shining', duration: 2, startAt: 3.2 },

      { key: 'SoulOut', duration: 0.6, startAt: 5.2 },
      { key: 'Blink', duration: 2, startAt: 5.8 },

      { key: 'SoulOut', duration: 0.6, startAt: 7.8 },
      { key: 'LightCircle', duration: 2.2, startAt: 8.4 },

      { key: 'SoulOut', duration: 0.6, startAt: 10.6 },
      { key: 'FlowingLight', duration: 2.2, startAt: 11.2 },

      { key: 'SoulOut', duration: 0.6, startAt: 13.4 },
      { key: 'Heart', duration: 2, startAt: 14 },
    ]
    let randomEffects = new Array(mediaTrack.clips.length * 2 - 1).fill(null).map((item, index) => {
      return new Clip({
        id: `effect-${index}`,
        type: CLIP_TYPES.EFFECT,
        key: effectList[index].key,
        section: new ClipSection({
          start: 0,
          end: effectList[index].duration
        }),
        startAt: effectList[index].startAt
      })
    })
    effectTrack.clips = randomEffects // 更新effect轨道的clips数据
    let musicTrack = new Track({ // 新建一个music轨道
      type: TRACK_TYPES.MUSIC,
      clips: []
    })
    let bgMusic = new Clip({
      id: 'music',
      type: CLIP_TYPES.MUSIC,
      info: {
        tempFilePath: "https://imgcache.qq.com/operation/dianshi/other/wanyouyinli.c7f973d906d9b8a3e7db90a90a7874d01454614b.mp3",
      },
      section: new ClipSection({
        start: 0,
        end: 1000
      }),
      startAt: 0
    })
    musicTrack.clips = [bgMusic]
    return [mediaTrack, effectTrack, musicTrack]
  },
  // 跳转至导出页
  goExport() {
    wx.navigateTo({
      url: '../export2/export'
    })
  },
  // 获取随机特效
  _getRandomEffect(effectList = []) {
    let index = Math.floor(Math.random() * effectList.length)
    return effectList[index]
  }
})
  • export 页面

export.wxml

<wj-export tracks="{{exportTracks}}" 
    bindexportsuccess="onExportSuccess"
    bindready="onExportReady">
    <button>导出视频</button>
</wj-export>

export.js

// pages/export2/export.js
Page({
  data: {
    exportTracks: []
  },
  // 设置导出数据
  onExportReady() {
    this.setData({
      exportTracks: global.testExportTracks
    })
  },
  // 导出成功回调
  onExportSuccess(e){
    let res = e.detail;
    wx.saveVideoToPhotosAlbum({
      filePath: res.tempFilePath,
      success: res => {
        wx.showToast({
          title: "已保存至相册"
        });
      }
    });
  }
})

camera和export页面比较简单,这里就不啰嗦了。主要说下player页面的实现思路,player的Track数据暂时只涉及到了三种:media类型、effect类型以及music类型(卡点模式用),其他的暂未使用。

  1. 随机模式:针对每张图随机获得一个特效key(wj-player组件内置了一些特效,直接使用即可),然后设置其时长与media类型的clip时长一致即可(默认是3s)。
  2. 卡点模式:根据音乐节奏调整了下每张照片的展示时长。

这样一个简单的照片集应用就做出来啦,so easy,妈妈再也不用担心我的发际线了。

卡点模式最终的成果物 点此预览

PS:使用过程中发现 wj-player 组件和 wj-export 组件无法在同一个页面中使用,不知道是不是我的姿势不对,有待后续验证;还有就是素材切换的时候没有转场动画,显得有些僵硬;另外看了下,特效倒是挺多的,但是没办法一键输出卡点视频,缺少一些常见的视频模板,自己做卡点视频特别费劲,要微调好久……

自动挡虽然好开,但是真正的赛车手都用手动挡[手动狗头结束]

总结

作为小程序端的一款视频剪辑工具,麻雀虽小五脏俱全,基础的功能是齐全的。个人使用下来整体体验还不错,接入也比较简单,官方配套文档也很完善,也期待官方后续可以提供更丰富的功能、组件。

最后,对视频剪辑小程序感兴趣的同学不防申请插件尝试一下,在官方基础api让人如此头秃的情况下,使用插件或许是个不错的选择。

2 回复

哇 🐂🍺 大佬请接受我的膜拜

我试了下,剪辑经常卡死,无反应。。。。。

回到顶部