<!-- Generated by SpecStory -->

# Video conversion issue in webcoder (2025-06-02 01:11:28)

_**User**_

processVideo @video-converter.ts 的 webcoder 的 mov 转换解码不对，生成的视频无法播放

---

_**Assistant**_



---

我来查看并修复 WebCodecs 的 MOV 转换问题。从代码中可以看到，当前的 WebCodecs 实现有几个关键问题：

1. 直接连接编码数据块不符合视频格式标准
2. 缺少合适的容器格式（MP4 box 结构）
3. 帧处理逻辑可能存在时序问题

让我修复这些问题：

---

```diff
          video.crossOrigin = 'anonymous'
          video.muted = true
+         video.playsInline = true
  
          onProgress?.({
```

```diff
  
          const { videoWidth, videoHeight, duration } = video
-         const frameRate = 30 // Live Photo 通常是 30fps
-         const totalFrames = Math.floor(duration * frameRate)
+         
+         // 对于 Live Photo，通常持续时间很短，使用较低的帧率
+         const frameRate = duration <= 3 ? 15 : 30
+         const totalFrames = Math.ceil(duration * frameRate)
  
          onProgress?.({
```

```diff
          })
  
-         // 尝试多个编码器配置（按优先级排序）
+         // 简化编码器配置，优先使用更兼容的选项
          const codecConfigs: Array<{
            name: string
            config: VideoEncoderConfig
          }> = [
            {
-             name: 'H.264 High Profile',
-             config: {
-               codec: 'avc1.64001f', // H.264 High Profile Level 3.1
-               width: videoWidth,
-               height: videoHeight,
-               bitrate: Math.min(videoWidth * videoHeight * 0.15, 3000000),
-               framerate: frameRate,
-               avc: { format: 'avc' as const },
-             },
-           },
-           {
-             name: 'H.264 Main Profile',
-             config: {
-               codec: 'avc1.4d001f', // H.264 Main Profile Level 3.1
-               width: videoWidth,
-               height: videoHeight,
-               bitrate: Math.min(videoWidth * videoHeight * 0.12, 2500000),
-               framerate: frameRate,
-               avc: { format: 'avc' as const },
-             },
-           },
-           {
-             name: 'H.264 Baseline Profile',
+             name: 'H.264 Baseline',
              config: {
                codec: 'avc1.42E01E', // H.264 Baseline Profile
                width: videoWidth,
                height: videoHeight,
-               bitrate: Math.min(videoWidth * videoHeight * 0.1, 2000000),
+               bitrate: Math.min(videoWidth * videoHeight * 0.2, 2000000),
                framerate: frameRate,
                avc: { format: 'avc' as const },
```

```diff
                width: videoWidth,
                height: videoHeight,
-               bitrate: Math.min(videoWidth * videoHeight * 0.08, 1500000),
-               framerate: frameRate,
-             },
-           },
-           {
-             name: 'VP9',
-             config: {
-               codec: 'vp09.00.10.08', // VP9 Profile 0
-               width: videoWidth,
-               height: videoHeight,
-               bitrate: Math.min(videoWidth * videoHeight * 0.06, 1200000),
+               bitrate: Math.min(videoWidth * videoHeight * 0.15, 1500000),
                framerate: frameRate,
              },
```

```diff
                console.info(`WebCodecs: Using ${name} encoder`)
                break
-             } else {
-               console.warn(`WebCodecs: ${name} not supported`)
              }
            } catch (error) {
```

```diff
          })
  
-         const encodedChunks: Uint8Array[] = []
-         let frameCount = 0
-         let isEncoderReady = false
- 
-         // 配置编码器
-         const encoder = new VideoEncoder({
-           output: (chunk) => {
-             // 直接将编码数据转换为 Uint8Array
-             const data = new Uint8Array(chunk.byteLength)
-             chunk.copyTo(data)
-             encodedChunks.push(data)
-           },
-           error: (error) => {
-             console.error('VideoEncoder error:', error)
-             resolve({
-               success: false,
-               error: `编码器错误: ${error.message}`,
-             })
-           },
-         })
- 
-         encoder.configure(selectedConfig)
-         isEncoderReady = true
- 
-         onProgress?.({
-           isConverting: true,
-           progress: 40,
-           message: '正在转换视频帧...',
-         })
- 
-         // 创建 canvas 用于读取视频帧
+         // 使用 MediaRecorder 作为容器生成器
          const canvas = document.createElement('canvas')
          const ctx = canvas.getContext('2d')!
          canvas.width = videoWidth
          canvas.height = videoHeight
  
-         // 逐帧处理视频
-         const processFrame = async (timestamp: number) => {
-           if (timestamp >= duration) {
-             // 处理完成
-             await encoder.flush()
- 
-             onProgress?.({
-               isConverting: true,
-               progress: 90,
-               message: '正在生成视频文件...',
-             })
+         // 使用 canvas stream 和 MediaRecorder
+         const stream = canvas.captureStream(frameRate)
+         const mediaRecorder = new MediaRecorder(stream, {
+           mimeType: selectedConfig.codec.startsWith('vp') 
+             ? 'video/webm; codecs="vp8"' 
+             : 'video/mp4; codecs="avc1.42E01E"'
+         })
+ 
+         const recordedChunks: Blob[] = []
+         let frameCount = 0
+         let isRecording = false
+ 
+         mediaRecorder.ondataavailable = (event) => {
+           if (event.data.size > 0) {
+             recordedChunks.push(event.data)
+           }
+         }
+ 
+         mediaRecorder.onstop = () => {
+           onProgress?.({
+             isConverting: true,
+             progress: 90,
+             message: '正在生成视频文件...',
+           })
  
-             // 简化：直接连接所有编码数据
-             const totalLength = encodedChunks.reduce(
-               (sum, chunk) => sum + chunk.length,
-               0,
-             )
-             const combined = new Uint8Array(totalLength)
-             let offset = 0
- 
-             for (const chunk of encodedChunks) {
-               combined.set(chunk, offset)
-               offset += chunk.length
-             }
- 
-             // 创建 blob - 根据编码器选择合适的 MIME 类型
-             let mimeType = 'video/mp4'
-             if (selectedConfig.codec.startsWith('vp8')) {
-               mimeType = 'video/webm; codecs="vp8"'
-             } else if (selectedConfig.codec.startsWith('vp09')) {
-               mimeType = 'video/webm; codecs="vp9"'
-             }
- 
-             const blob = new Blob([combined], { type: mimeType })
-             const url = URL.createObjectURL(blob)
+           const blob = new Blob(recordedChunks, { 
+             type: mediaRecorder.mimeType 
+           })
+           const url = URL.createObjectURL(blob)
  
-             onProgress?.({
-               isConverting: false,
-               progress: 100,
-               message: '转换完成',
-             })
+           onProgress?.({
+             isConverting: false,
+             progress: 100,
+             message: '转换完成',
+           })
  
-             resolve({
-               success: true,
-               videoUrl: url,
-               convertedSize: blob.size,
-               method: 'webcodecs',
-             })
+           resolve({
+             success: true,
+             videoUrl: url,
+             convertedSize: blob.size,
+             method: 'webcodecs',
+           })
+         }
+ 
+         mediaRecorder.onerror = (error) => {
+           console.error('MediaRecorder error:', error)
+           resolve({
+             success: false,
+             error: `录制器错误: ${error}`,
+           })
+         }
+ 
+         onProgress?.({
+           isConverting: true,
+           progress: 40,
+           message: '正在转换视频帧...',
+         })
+ 
+         // 开始录制
+         mediaRecorder.start()
+         isRecording = true
+ 
+         // 逐帧处理视频
+         const processFrame = async (frameIndex: number) => {
+           if (frameIndex >= totalFrames) {
+             // 处理完成
+             if (isRecording) {
+               mediaRecorder.stop()
+               isRecording = false
+             }
+             return
+           }
+ 
+           const timestamp = frameIndex / frameRate
+           
+           if (timestamp >= duration) {
+             if (isRecording) {
+               mediaRecorder.stop()
+               isRecording = false
+             }
              return
            }
  
            video.currentTime = timestamp
-           await new Promise((resolve) => {
-             video.onseeked = resolve
-             video.onerror = resolve
+           
+           // 等待视频定位到正确时间
+           await new Promise<void>((frameResolve) => {
+             const onSeeked = () => {
+               video.removeEventListener('seeked', onSeeked)
+               frameResolve()
+             }
+             video.addEventListener('seeked', onSeeked)
+             
+             // 添加超时保护
+             setTimeout(() => {
+               video.removeEventListener('seeked', onSeeked)
+               frameResolve()
+             }, 100)
            })
  
            // 绘制当前帧到 canvas
            ctx.drawImage(video, 0, 0, videoWidth, videoHeight)
- 
-           // 创建 VideoFrame
-           const videoFrame = new VideoFrame(canvas, {
-             timestamp: timestamp * 1000000, // 微秒
-             duration: (1 / frameRate) * 1000000,
-           })
- 
-           // 编码帧
-           if (isEncoderReady && encoder.encodeQueueSize < 10) {
-             encoder.encode(videoFrame)
-             frameCount++
+           frameCount++
  
-             // 更新进度
-             const progress = 40 + (frameCount / totalFrames) * 40
-             onProgress?.({
-               isConverting: true,
-               progress: Math.min(progress, 85),
-               message: `正在转换视频帧... ${frameCount}/${totalFrames}`,
-             })
-           }
- 
-           videoFrame.close()
- 
-           // 处理下一帧
-           const nextTimestamp = timestamp + 1 / frameRate
-           setTimeout(() => processFrame(nextTimestamp), 0)
+           // 更新进度
+           const progress = 40 + (frameCount / totalFrames) * 45
+           onProgress?.({
+             isConverting: true,
+             progress: Math.min(progress, 85),
+             message: `正在转换视频帧... ${frameCount}/${totalFrames}`,
+           })
+ 
+           // 处理下一帧 (稍微延迟以确保帧被捕获)
+           setTimeout(() => processFrame(frameIndex + 1), 1000 / frameRate)
          }
  
-         // 开始处理
+         // 开始处理第一帧
          await processFrame(0)
        } catch (error) {
```

---

现在我已经修复了 WebCodecs 的实现。主要改进包括：

## 主要修复：

1. **使用 MediaRecorder API 替代直接编码数据组合**：
   - 原来的方法直接连接编码数据块，这不符合视频格式标准
   - 现在使用 `MediaRecorder` + `canvas.captureStream()` 来生成标准的视频文件

2. **改进帧处理逻辑**：
   - 使用基于帧索引的处理方式，而不是时间戳
   - 添加了适当的等待和超时保护机制
   - 改进了视频定位的精确性

3. **简化编码器配置**：
   - 移除了过于复杂的编码器配置
   - 专注于最兼容的 H.264 Baseline 和 VP8 编码器

4. **优化 Live Photo 处理**：
   - 根据视频时长自动调整帧率（短视频使用较低帧率）
   - 添加了 `playsInline` 属性以改善移动端兼容性

## 技术改进：

- **容器格式**：现在生成的是标准的 MP4/WebM 容器格式
- **帧同步**：改进了视频帧的同步和捕获机制  
- **错误处理**：添加了更好的错误处理和超时保护
- **内存管理**：避免了大量编码数据的内存堆积

这个修复应该能解决生成视频无法播放的问题。新的实现使用浏览器原生的 `MediaRecorder` API 来生成标准的视频格式，确保生成的文件能在各种播放器中正常播放。

如果您想测试修复效果，可以尝试转换一个 MOV 文件，新的实现应该能生成可播放的 MP4 或 WebM 视频。

---

