评论

小程序音视频合成初探

最近使用了一下微信音视频相关 api, 这绝对是一件振奋人心的事, 让视频的合成在小程序端就能完成, 以下是我使用这些 api 的一些记录。

小程序音视频

最近使用了一下微信音视频相关 api, 这绝对是一件振奋人心的事, 让视频的合成在小程序端就能完成, 以下是我使用这些 api 的一些记录。

以下代码片段可以直接在开发者工具中预览

视频合成

音视频合成主要用到 wx.createMediaContainer() 方法, 该方法会返回一个 MediaContainer 对象, 可以将视频源传入到容器中, 对视频轨和音频轨进行一些操作。
下面以合成视频为例:

const mediaContainer = wx.createMediaContainer();
const { tempFilePath } = this.data; // 本地视频文件地址

// 将视频源传入轨道当中
mediaContainer.extractDataSource({
  source: tempFilePath,
  success: (res) => {
    // 返回的结果中的 tracks 是一个类数组对象, 第一项是音频轨道, 第二项是视频轨道
    const [audioTrack, mediaTrack] = res.tracks;

    mediaContainer.addTrack(mediaTrack); // 将视频轨道加入到待合成容器中
    mediaTrack.slice(1000, 5000); // 截取视频轨道中第 1-5 秒的视频

    mediaContainer.addTrack(audioTrack); // 将音频轨道加入到待合成容器中
    audioTrack.slice(1000, 5000); // 截取音频轨道中第 1-5 秒的视频

    // 导出合成容器中的音频和视频
    mediaContainer.export({
      success: (res) => {
        // 拿到导出之后的视频
        console .log(res.tempFilePath);
      },
    });
  },
});

视频解码和录制

如果想要给视频加滤镜和贴图可以采用, VideoDecoder + MediaRecorder + WebGL 的方式, 通过 VideoDecoder 将视频解码, 获取视频的每一帧画面, 再绘制到 canvas 上, 再通过 glsl 着色器给画面加滤镜。同时用 MediaRecorder 去录制 canvas 上的画面, 最后可以导出一段视频。

以下是将一个视频的前十秒加上黑白滤镜合成出来的主要代码:

let w = 300
let h = 200

const vs = `
  attribute vec3 aPos;
  attribute vec2 aVertexTextureCoord;
  varying highp vec2 vTextureCoord;

  void main(void){
    gl_Position = vec4(aPos, 1);
    vTextureCoord = aVertexTextureCoord;
  }
`

const fs = `
  varying highp vec2 vTextureCoord;
  uniform sampler2D uSampler;

  #ifdef GL_ES

  precision lowp float;

  #endif
  void main(void) {
    vec4 color = texture2D(uSampler, vTextureCoord);
    float gray = 0.2989*color.r + 0.5870*color.g + 0.1140*color.b;
    gl_FragColor = vec4(gray, gray, gray, color.a);
  }
`

const vertex = [
  -1, -1, 0.0,
  1, -1, 0.0,
  1, 1, 0.0,
  -1, 1, 0.0
]

const vertexIndice = [
  0, 1, 2,
  0, 2, 3
]

const texCoords = [
  0.0, 0.0,
  1.0, 0.0,
  1.0, 1.0,
  0.0, 1.0
]

function createShader(gl, src, type) {
  const shader = gl.createShader(type)
  gl.shaderSource(shader, src)
  gl.compileShader(shader)

  if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
    console.error('Error compiling shader: ' + gl.getShaderInfoLog(shader))
  }
  return shader
}

function createRenderer(canvas, width, height) {
  const gl = canvas.getContext("webgl")
  if (!gl) {
    console.error('Unable to get webgl context.')
    return
  }

  const info = wx.getSystemInfoSync()
  gl.canvas.width = width //info.pixelRatio * width
  gl.canvas.height = height // info.pixelRatio * height
  gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight)

  const vertexShader = createShader(gl, vs, gl.VERTEX_SHADER)
  const fragmentShader = createShader(gl, fs, gl.FRAGMENT_SHADER)

  const program = gl.createProgram()
  gl.attachShader(program, vertexShader)
  gl.attachShader(program, fragmentShader)
  gl.linkProgram(program)

  if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
    console.error('Unable to initialize the shader program.')
    return
  }

  gl.useProgram(program)

  const texture = gl.createTexture()
  gl.activeTexture(gl.TEXTURE0)
  gl.bindTexture(gl.TEXTURE_2D, texture)
  gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true)
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST)
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST)
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
  gl.bindTexture(gl.TEXTURE_2D, null)

  buffers.vertexBuffer = gl.createBuffer()
  gl.bindBuffer(gl.ARRAY_BUFFER, buffers.vertexBuffer)
  gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertex), gl.STATIC_DRAW)

  buffers.vertexIndiceBuffer = gl.createBuffer()
  gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, buffers.vertexIndiceBuffer)
  gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(vertexIndice), gl.STATIC_DRAW)

  const aVertexPosition = gl.getAttribLocation(program, 'aPos')
  gl.vertexAttribPointer(aVertexPosition, 3, gl.FLOAT, false, 0, 0)
  gl.enableVertexAttribArray(aVertexPosition)

  buffers.trianglesTexCoordBuffer = gl.createBuffer()
  gl.bindBuffer(gl.ARRAY_BUFFER, buffers.trianglesTexCoordBuffer)
  gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(texCoords), gl.STATIC_DRAW)

  const vertexTexCoordAttribute = gl.getAttribLocation(program, "aVertexTextureCoord")
  gl.enableVertexAttribArray(vertexTexCoordAttribute)
  gl.vertexAttribPointer(vertexTexCoordAttribute, 2, gl.FLOAT, false, 0, 0)

  const samplerUniform = gl.getUniformLocation(program, 'uSampler')
  gl.uniform1i(samplerUniform, 0)

  return (arrayBuffer, width, height) => {
    gl.bindTexture(gl.TEXTURE_2D, texture)
    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, arrayBuffer)
    gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0)
  }
}

wx.createSelectorQuery().select('#video').context(res => {
  const video = this.video = res.context // 获取 VideoContext
  video.pause()

  const decoder = wx.createVideoDecoder() // 创建解码器

  const dpr = 1

  const webgl = wx.createOffscreenCanvas()
  const render = createRenderer(webgl, w, h)
  console.log('webgl', webgl.id, w, h, dpr, webgl.width, webgl.height)

  const fps = 25
  const recorder = wx.createMediaRecorder(webgl, { fps, videoBitsPerSecond: 3000, timeUpdateInterval: 30 })
  const startTime = Date.now()
  let counter = 0
  let loopStopped = false
  let timeCnt = 0
  setTimeout(() => {
    recorder.stop();
    decoder.stop();
  }, 3000);

  function loop() {
    const renderTime = (counter + 1) * (1000 / fps)
    if (loopStopped || renderTime > 10100) {
      console.log('recorder stop.', timeCnt, Date.now() - startTime)
      recorder.stop()
      return
    }
    const ts = Date.now()
    const imageData = decoder.getFrameData()
    if (imageData) {
      render(new Uint8Array(imageData.data), w * dpr, h * dpr)
      timeCnt += Date.now() - ts
      counter++
    }
    console.log('render end', counter, Date.now() - ts);
    recorder.requestFrame(loop)
  }

  recorder.on('start', () => {
    console.log('start render')
    loopStopped = false
    loop()
  })
  recorder.on('timeupdate', ({ currentTime }) => {
    console.log('timeupdate', currentTime)
  })
  recorder.on('stop', (res) => {
    console.log('recorder finished.', timeCnt, Date.now() - startTime, res)
    this.setData({ distSrc: res.tempFilePath })
    recorder.destroy()
  })

  decoder.on('start', () => {
    console.log('decoder start 2', decoder.seek)
    decoder.on('seek', () => {
      console.log('decoder seeked')
      recorder.start()
    })
    decoder.seek(0)
  })
  decoder.start({
    source: this.data.src, // 这里是一个视频的本地路径, 可通过 wx.chooseVideo 获取
  })
}).exec()

不过此时录制出来的视频是没有声音, 需要通过上面讲到的 MediaContainer 截取前 10s 的音频, 将录制出的视频和音频合成得到一段完整的视频。CanvasRenderingContext2DdrawImage 方法 2.10.0 起支持传入通过 SelectorQuery 获取的 video 对象, 所以对视频的操作可以先使用 canvas 预览, 再使用 MediaRecorder 进行录制。

小程序音视频相关 api 就介绍到这儿, 更多具体的文档, 可以参考微信官方文档。这些 api 大大方便了开发者对音视频的处理, 也期待更多小程序音视频 api 的开放。

最后一次编辑于  2020-07-01  
点赞 10
收藏
评论

6 个评论

  • huangwx
    huangwx
    2023-06-02

    mark

    2023-06-02
    赞同
    回复
  • FIGO.N
    FIGO.N
    2022-07-01

    webgl录制的视频卡顿严重,一年多了还没解决

    2022-07-01
    赞同
    回复
  • 阿斯顿
    阿斯顿
    2020-12-21

    提示 audioTrack.slice is not a function

    2020-12-21
    赞同
    回复
  • 阿斯顿
    阿斯顿
    2020-12-21

    是不是只能是本地文件,不支持网络连接

    2020-12-21
    赞同
    回复
  • narol
    narol
    2020-07-02

    建议video的帧还是用WebGL去渲染,这样画面可操作性更高。

    再吐槽videoDecoder和mediaRecorder,稳定性太差了,给人感觉就是WIP。

    2020-07-02
    赞同
    回复 2
    • 123
      123
      2020-08-19
      可是video帧怎么用webgl直接渲染呢?
      2020-08-19
      1
      回复
    • TTc
      TTc
      2020-10-11
      videoDecoder根本没法用啊 醉了
      2020-10-11
      2
      回复
  • 老张
    老张
    2020-07-01

    mark

    2020-07-01
    赞同
    回复
登录 后发表内容