Safari中drawImage从MediaSource视频绘至canvas无图像的解决方法问询
解决方案:Safari 11.1/MediaSource + Canvas drawImage 不显示问题
我之前踩过Safari MediaSource和Canvas drawImage兼容的坑,针对你遇到的这个问题,这里有几个经过验证的可行方案:
1. 确保视频帧完全就绪后再绘制
Safari 11.1对MediaSource视频的帧加载状态判断不够及时,直接调用drawImage可能会拿到未就绪的帧。可以通过监听timeupdate事件并检查视频的readyState来确保帧可用:
const video = document.getElementById('your-video-element'); const canvas = document.getElementById('your-canvas-element'); const ctx = canvas.getContext('2d'); video.addEventListener('timeupdate', () => { // HAVE_CURRENT_DATA(值为2)表示当前帧已加载完成 if (video.readyState >= 2) { ctx.drawImage(video, 0, 0, canvas.width, canvas.height); } });
2. 手动添加微小延迟绕过帧同步问题
有些情况下,Safari虽然触发了帧更新事件,但内部解码还没完成,此时drawImage会失效。可以给绘制操作加一个微小的延迟,确保帧数据完全可用:
function drawFrame() { setTimeout(() => { ctx.drawImage(video, 0, 0); requestAnimationFrame(drawFrame); }, 16); // 约等于一帧的时间(60fps) } // 启动绘制循环 video.addEventListener('loadeddata', drawFrame);
3. 使用WebGL替代Canvas 2D绘制
WebGL对MediaSource视频的帧读取兼容性更好,绕过Canvas 2D的drawImage限制。这里是一个简化的实现示例:
const canvas = document.getElementById('your-canvas-element'); const gl = canvas.getContext('webgl'); const video = document.getElementById('your-video-element'); // 创建基础着色器程序(顶点+片段着色器) function createShaderProgram(gl) { const vertexShader = gl.createShader(gl.VERTEX_SHADER); gl.shaderSource(vertexShader, ` attribute vec2 a_position; attribute vec2 a_texCoord; varying vec2 v_texCoord; void main() { gl_Position = vec4(a_position, 0.0, 1.0); v_texCoord = a_texCoord; } `); gl.compileShader(vertexShader); const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER); gl.shaderSource(fragmentShader, ` precision mediump float; varying vec2 v_texCoord; uniform sampler2D u_texture; void main() { gl_FragColor = texture2D(u_texture, v_texCoord); } `); gl.compileShader(fragmentShader); const program = gl.createProgram(); gl.attachShader(program, vertexShader); gl.attachShader(program, fragmentShader); gl.linkProgram(program); gl.useProgram(program); return program; } const program = createShaderProgram(gl); const texture = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, texture); // 设置纹理参数,确保视频帧正确映射 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.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); // 绘制视频帧的循环 function drawFrame() { gl.bindTexture(gl.TEXTURE_2D, texture); // 将视频帧上传到WebGL纹理 gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, video); // 绘制覆盖整个画布的四边形 const positionLocation = gl.getAttribLocation(program, 'a_position'); const texCoordLocation = gl.getAttribLocation(program, 'a_texCoord'); gl.enableVertexAttribArray(positionLocation); gl.enableVertexAttribArray(texCoordLocation); const vertices = new Float32Array([1,1, -1,1, 1,-1, -1,-1]); gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0); gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW); const texCoords = new Float32Array([1,0, 0,0, 1,1, 0,1]); gl.vertexAttribPointer(texCoordLocation, 2, gl.FLOAT, false, 0, 0); gl.bufferData(gl.ARRAY_BUFFER, texCoords, gl.STATIC_DRAW); gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); requestAnimationFrame(drawFrame); } video.addEventListener('loadeddata', drawFrame);
4. 针对受影响Safari版本降级处理
如果以上方案都不适用,可以检测浏览器版本,对受影响的Safari直接使用视频URL而非MediaSource,保证功能可用:
function isAffectedSafari() { const userAgent = navigator.userAgent; const isSafari = /^((?!chrome|android).)*safari/i.test(userAgent); if (!isSafari) return false; // 检测Safari 11.1及以上版本,或Safari Technology Preview const versionMatch = userAgent.match(/Version\/(\d+)\.(\d+)/); if (versionMatch) { const major = parseInt(versionMatch[1], 10); const minor = parseInt(versionMatch[2], 10); if (major === 11 && minor >= 1) return true; } return /Safari Technology Preview/.test(userAgent); } const video = document.getElementById('your-video-element'); if (isAffectedSafari()) { // 降级为直接URL加载 video.src = 'your-video-file-url.mp4'; } else { // 正常使用MediaSource加载逻辑 const mediaSource = new MediaSource(); video.src = URL.createObjectURL(mediaSource); // ... 你的MediaSource初始化逻辑 }
问题原因说明
虽然WebKit的Bug 125157标记为已修复,但Safari 11.1的MediaSource实现仍存在帧同步的残留问题:MediaSource提供的视频帧可能未被标记为“可读取”状态,导致Canvas 2D的drawImage无法获取到像素数据。上述方案要么等待帧就绪,要么绕过Canvas 2D的限制,要么降级处理来规避这个问题。
内容的提问来源于stack exchange,提问作者Andrey Starostin
相关产品推荐
相关产品推荐

