小程序音视频
最近使用了一下微信音视频相关 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 的开放。
slice 这个方法 在文档完全没找到 我想看看还有没有其他方法
绘制出来太卡怎么解决
mark
webgl录制的视频卡顿严重,一年多了还没解决
提示 audioTrack.slice is not a function
是不是只能是本地文件,不支持网络连接
建议video的帧还是用WebGL去渲染,这样画面可操作性更高。
再吐槽videoDecoder和mediaRecorder,稳定性太差了,给人感觉就是WIP。
mark