小程序音视频
最近使用了一下微信音视频相关 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 的音频, 将录制出的视频和音频合成得到一段完整的视频。CanvasRenderingContext2D
的 drawImage
方法 2.10.0 起支持传入通过 SelectorQuery
获取的 video 对象, 所以对视频的操作可以先使用 canvas 预览, 再使用 MediaRecorder
进行录制。
小程序音视频相关 api 就介绍到这儿, 更多具体的文档, 可以参考微信官方文档。这些 api 大大方便了开发者对音视频的处理, 也期待更多小程序音视频 api 的开放。
mark
webgl录制的视频卡顿严重,一年多了还没解决
提示 audioTrack.slice is not a function
是不是只能是本地文件,不支持网络连接
建议video的帧还是用WebGL去渲染,这样画面可操作性更高。
再吐槽videoDecoder和mediaRecorder,稳定性太差了,给人感觉就是WIP。
mark