# 代码变更：BUG-39acc48b（黑屏 - 真因修复）

## 修改文件

### `src/h5/index.ts`

#### 修改：`updateInputStream` 方法的"VideoTrack 未变 + output ended"兜底分支

**位置**：非移动端分支，line ~1755-1771

**修改前**（无效的重新 wrap 同一个 ended generator）：

```typescript
}else{
  logger.info("updateInputStream: VideoTrack is not changed, no need to update");
  // bugfix: 即使 videoTrack 引用未变，如果 output track 已经 ended，
  // 需要用当前 generator 重建 outputStream，否则客户无法恢复 output 流
  if (this.outputStream && this.core?.inputNode?.datatype === "videoframe") {
    const currentOutputTrack = this.outputStream.getInstance().getVideoTracks()[0];
    if (currentOutputTrack && currentOutputTrack.readyState === "ended") {
      logger.info("updateInputStream: output track is ended, rebuilding outputStream");
      const inputNode = this.core.inputNode as InputVideoFrame;
      const newGenerator = inputNode.generator;
      if (newGenerator && newGenerator.readyState === "live") {
        const newStream = new MediaStream([newGenerator]);
        this.outputStream.updateStream(newStream);  // ❌ 同一个 ended generator，无效
      }
    }
  }
}
```

**修改后**（强制走 updateInputNode 重建 generator）：

```typescript
}else{
  logger.info("updateInputStream: VideoTrack is not changed, no need to update");
  // bugfix(BUG-39acc48b): 即使 videoTrack 引用未变，如果 output track 已经 ended，
  // 必须真正重建 generator 才能恢复（仅 wrap 同一个 ended generator 无效）。
  // 强制走 updateInputNode(src, true) 路径，会 new MediaStreamTrackGenerator() 产生全新的 live track。
  if (this.outputStream && this.core?.inputNode?.datatype === "videoframe") {
    const currentOutputTrack = this.outputStream.getInstance().getVideoTracks()[0];
    if (currentOutputTrack && currentOutputTrack.readyState === "ended") {
      logger.info("updateInputStream: output track is ended, force rebuilding generator");
      isVideoTrackChanged = true;        // 触发下方的 updateInputNode 路径
      updateOutputDestination = true;    // 强制重建 generator
    }
  }
}
```

## 修改原理

### 为什么旧实现无效？

`MediaStreamTrackGenerator` 一旦 track 进入 `ended` 状态就**永久失活**，不可能通过任何方式让它从 ended 变回 live。仅靠 `outputStream.updateStream(new MediaStream([sameGenerator]))` 只是在容器层重新 wrap，track 自身仍然是 ended。

日志证据（实例 4，sess=1780309186407）：

```
268  updateInputStream: output track is ended, rebuilding outputStream  ← 旧修复触发
272  getOutput outputTrack videoframe ["ended"]                         ← 但仍然 ended
276  updateInputStream: output track is ended, rebuilding outputStream  ← 客户重试也无效
```

### 为什么新实现有效？

通过设置 `isVideoTrackChanged=true` + `updateOutputDestination=true`，让代码进入下方原本只有 videoTrack 真正变化时才会走的路径：

```typescript
if (this.core && isVideoTrackChanged) {
  await this.core.updateInputNode(src, updateOutputDestination);  // 内部 new MediaStreamTrackGenerator()
  // ...
  if (updateOutputDestination && this.outputStream && datatype === "videoframe") {
    const newGenerator = (this.core.inputNode as InputVideoFrame).generator;  // 全新 generator
    this.outputStream.updateStream(new MediaStream([newGenerator]));  // ✅ 真正活的 track
  }
}
```

`VideoInputVideoFrame.updateNode(src, true)` 会执行：

```typescript
this.generator = new MediaStreamTrackGenerator({ kind: "video" });  // 全新 track，readyState=live
this.writable = this.generator.writable;
```

## 兼容性 & 副作用

| 项 | 评估 |
|---|---|
| videoTrack 引用相同时 stopOldTracks | 不影响。下游 `oldVideoTrack !== newVideoTrack` 判断仍然为 false，不会停旧 track |
| audioTrack 行为 | 完全不变，audio 分支独立 |
| Worker / 检测器 / Canvas / WebGL | 完全不重建。只重建 `MediaStreamTrackProcessor` + `MediaStreamTrackGenerator` 这两个流操作对象 |
| 旧 OutputStream 客户引用 | 保持不变。`updateStream` 仅替换内部 track |
| 微信小程序 | 无影响（仅 h5 路径） |
| Output 没 ended 时 | 不触发新逻辑，行为完全不变 |

## 测试验证（建议客户复现路径）

1. **复现黑屏**：触发 outputVideoTrack 进入 ended（如频繁切 tab、GPU 资源不足导致 contextlost、或上游 input track stop）
2. **客户调用**：`sdk.updateInputStream(stream, true)`
3. **预期日志**：
   ```
   updateInputStream: VideoTrack is not changed, no need to update
   updateInputStream: output track is ended, force rebuilding generator
   updateInputNode [...]
   updateInputStream: updating outputStream with new generator
   ```
4. **预期结果**：客户保存的 outputStream 实例的 videoTrack 重新变为 live；黑屏恢复

## 已知遗留（建议客户配合）

1. **`this.input === src` 提前 return 的问题**：如果客户复用同一个 MediaStream 对象调用 `updateInputStream`，会在 line 1690 直接 return。**建议**：失活时传入 `new MediaStream([currentVideoTrack])` 强制创建新 stream 引用。
2. **释放旧 outputStream**：destroy SDK 后客户应该 `outputStream = null` 释放引用，避免持续派发事件。
3. **input 流被外部降级（如 160x120 placeholder）**：本次修复让 track 不再 ended，但流本身没数据时会进入 muted 状态。客户监听 muted/unmute 即可感知。
