# 问题分析：BUG-39acc48b（黑屏，第二轮分析 - 真因定位）

## 客户环境
- Customer: 1253417915（与 BUG-e289b43c 同一客户）
- SDK: 2.0.3-10
- Platform: Electron 24.8.8 内嵌网页
- GPU: Intel Iris Xe（quality level = low）
- 现象: 黑屏；客户调用 `updateInputStream` 重置流后**仍然黑屏**

## 关键证据（第二份日志 a37e69ad）

### 实例 4 的关键事件链（line 251-276）

```
251  getOutput outputTrack videoframe ["live"]                            // 初次获取 output，live
252  setBackground null
257  outputVideoTrack muted.                                              // 黑屏症状出现
265  updateInputStream [true]                                             // 客户尝试修复
266  videotrack settings ["a5a0b989...", 160, 120, 15]
267  updateInputStream: VideoTrack is not changed                         // 走 BUG-e289b43c 修复路径
268  updateInputStream: output track is ended, rebuilding outputStream    // 触发了 ended 兜底
269  getOutput fps [15]                                                   // 客户重新调 getOutput
270  check generator track ...
271  output stream deviceId ["6a01bba9-0e3c-..."]                         // 新建 OutputStream 实例
272  getOutput outputTrack videoframe ["ended"]                           // ⚠️⚠️⚠️ 仍然 ended
273  updateInputStream [true]                                             // 客户再次重试
274  videotrack settings 同上
275  updateInputStream: VideoTrack is not changed
276  updateInputStream: output track is ended, rebuilding outputStream    // 仍然走兜底路径，但仍无效
```

## 真正的根因

**BUG-e289b43c 的修复路径在本场景下无效。**

第一次修复时的逻辑：

```ts
// 旧实现（无效）
if (currentOutputTrack.readyState === "ended") {
  const newGenerator = inputNode.generator;          // ❌ 这是同一个 ended generator
  if (newGenerator && newGenerator.readyState === "live") {
    this.outputStream.updateStream(new MediaStream([newGenerator]));  // wrap 同一个 ended track 没用
  }
}
```

### 失败原因

1. `core.inputNode.generator` 是 SDK 实例创建时 `new MediaStreamTrackGenerator()` 一次性绑定的
2. 一旦该 generator 的 track 进入 `ended` 状态（如 `MediaStreamTrackProcessor` 输入端 cancel/close 传播），**这个 generator 就永久失活**
3. 仅靠 `outputStream.updateStream()` 把它重新 wrap 到 MediaStream 里，**没换 track 本身，readyState 依然是 ended**
4. 客户重新 `getOutput()` 也只是 `new OutputStream(同一个 generator, ...)`（line 864），新 OutputStream 持有的依然是同一个 ended track（line 272 证实）

### Review 时的预警已被证实

之前对 BUG-e289b43c 修复 review 时就提过：
> "core.inputNode.generator 是老的 generator（除非中途有人把它换了），它的 track 一旦 ended 就再也不会变 live——重建出来的 stream 仍然是 ended 状态。"

当时担心的就是这个。本次日志正好命中。

## 真正能修复 ended 的唯一路径

要让 generator 的 track 从 ended 恢复，**必须 `new MediaStreamTrackGenerator()` 创建一个全新的 generator**。这个动作只发生在：

- `VideoInputVideoFrame.updateNode(src, updateOutputDestination=true)`（VideoInputVideoFrame.ts:191-208）
- 由 `WorkerCore.updateInputNode(src, true)` 触发
- 由 `ArSdk.updateInputStream(src, stopOldTracks, updateOutputDestination=true)` 在 `isVideoTrackChanged===true` 路径下触发

但客户场景中：
- `oldVideoTrack === newVideoTrack`（同一个 deviceId，同一个 track 对象）
- `isVideoTrackChanged` 一直是 false
- 永远走不到 `core.updateInputNode(src, true)` → 永远没机会重建 generator

## 修复方案（已实施）

修改 `updateInputStream` 中 "VideoTrack 未变 + output ended" 兜底分支：

**改前**（无效的重新 wrap）：

```ts
if (currentOutputTrack.readyState === "ended") {
  const newGenerator = inputNode.generator;
  if (newGenerator && newGenerator.readyState === "live") {
    this.outputStream.updateStream(new MediaStream([newGenerator]));
  }
}
```

**改后**（强制走重建路径）：

```ts
if (currentOutputTrack.readyState === "ended") {
  // 仅 wrap 同一个 generator 不能让 ended track 复活，必须重建 generator
  isVideoTrackChanged = true;        // 触发下方 updateInputNode 路径
  updateOutputDestination = true;    // 强制 new MediaStreamTrackGenerator
}
```

下游已有的逻辑（line 1785-1801）会负责：
1. `await this.core.updateInputNode(src, true)` → 内部 `new MediaStreamTrackGenerator()` 产生全新 live track
2. `this.outputStream.updateStream(new MediaStream([新 generator]))` → 客户拿到的 outputStream 真正活了

## 副作用 / 风险评估

| 项 | 评估 |
|---|---|
| 旧 videoTrack 是否会被 stop | 不会。line 1804 `oldVideoTrack !== newVideoTrack && stopOldTracks` 在引用相同时不触发，与原行为一致 |
| 旧 audioTrack 是否会被换 | 不影响。audio 分支独立判断 |
| 是否会强制走 updateInputNode 路径多此一举（output 没 ended 时） | 不会。只在 ended 检测命中时才设置 flag |
| GPU/Worker 资源是否会重新创建 | 不会。`updateInputNode(src, true)` 只创建新 `MediaStreamTrackProcessor` + `MediaStreamTrackGenerator`，worker/canvas/detector 不动 |
| 客户调用 `this.input === src` 时是否仍然提前 return | 是。line 1690 在传同一个 stream 对象时仍 return，**这是另一个潜在缺陷**（详见下方"已知遗留问题"） |

## 已知遗留问题（不在本次修复范围）

### 遗留 1：`this.input === src` 提前 return

```ts
if (this.input === src) {
  logger.info("updateInputStream: same MediaStream, no need to update");
  return;
}
```

如果客户复用同一个 `MediaStream` 对象调用 `updateInputStream`，会直接 return，不进入修复路径。日志中无法判断客户是否传了同一个 stream 对象（只能看到 deviceId），但若实际有此用法，本次修复仍无效。

**建议客户**：发现 output 失活时，传入"新构造的"`new MediaStream([原 videoTrack])` 来绕过此判断。或者后续给 `updateInputStream` 加一个 `force` 参数。

### 遗留 2：input 流自身降级（160x120）

日志显示 input track 被外部降级到 160x120，这是上游会议系统的占位流。SDK 即便重建 generator，也只能拿到一个空帧流。**本次修复让 track 不再 ended（变 muted）**，客户监听 muted/unmute 事件可以感知流的活性变化，比"永久 ended 不可恢复"好得多。

### 遗留 3：客户不释放旧 outputStream 引用

实例 2 的 line 97 出现 sess1 的 muted 事件，说明客户保留了旧 SDK 实例的 outputStream。建议客户在 destroy 后置 `outputStream = null` 释放引用。

### 遗留 4：lost context 自动恢复

contextlost handler 只 trigger error 不自动 resetCore。客户业务侧的重建策略激进。可后续在 SDK 内加节流的自动恢复，但属于增强不属于本次修复。

## 与 BUG-e289b43c 的关系

本次修复**取代**了 BUG-e289b43c 在 updateInputStream 分支的兜底逻辑：

- BUG-e289b43c 的 resetCore 路径修复（line 686-697）**仍然有效**，保留不动
- BUG-e289b43c 的 updateInputStream 兜底（重新 wrap）**被本次修复替换为强制重建**

## 结论

**本次是一次真正的根因修复**，不是优化。客户验证步骤：

1. 复现：触发 outputVideoTrack muted/ended（如频繁切 tab + GPU 弱）
2. 客户调 `sdk.updateInputStream(currentStream, true)`（注意：可能需要传新构造的 MediaStream，避免 `this.input === src` 提前 return）
3. 期望日志：`output track is ended, force rebuilding generator` → 经过 `updateInputNode` → `updating outputStream with new generator`
4. 期望结果：客户拿到的 outputStream 中 videoTrack 重新变 live，黑屏恢复（前提是 input 流本身有真实数据）

如果 input 流本身被降级到 160x120 placeholder，修复后 track 会从 ended 变成 live，但很快进入 muted 状态——这是上游问题，需客户业务侧恢复正常视频流后才能完全解决。
