# 代码变更：BUG-e289b43c-6c66-49c2-8853-eb158a243d6a

## 修改文件

### `src/common/Input.ts`

**修改内容**：在 `InputVideoFrame` 接口中补充 `getOutputTrack()` 方法声明（三个实现类 `VideoInputVideoFrame`/`CameraInputVideoFrame`/`ImageInputVideoframe` 已有该方法，仅补充类型声明）。

```typescript
export interface InputVideoFrame {
  // ...
  init(): Promise<any>;
  getVideoTrack(): Promise<MediaStreamTrack>;
  // 获取输出 track（兼容原生 MediaStreamTrackGenerator 与 polyfill VideoTrackGenerator）
  getOutputTrack(): MediaStreamTrack;
}
```

**修改原因**：原代码直接访问 `inputNode.generator` 在 polyfill 路径（`VideoTrackGenerator`）下不是 `MediaStreamTrack`，需要 `.track` 才是；通过 `getOutputTrack()` 屏蔽差异。

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

#### 修改 1：`resetCore` 方法

**位置**：`resetCore` 方法末尾，**移到 `oldCore.destroy()` 之前**

**修改内容**：新增 outputStream 同步更新逻辑

```typescript
await this.core.init(this.input);
logger.info("new core init finish");
this._initMainLoop();

// bugfix: resetCore 后同步更新 outputStream，避免 output track ended
// resetCore 创建了新的 core（新的 VideoInputVideoFrame/generator），
// outputStream 仍持有旧 generator 的 track；旧 core destroy 后旧 generator
// 的 writable 关闭会让 track 进入 ended 状态。
// 必须在 oldCore.destroy() 之前完成切换，避免客户在 destroy 瞬间观察到 ended。
if (this.outputStream && this.core.inputNode?.datatype === "videoframe") {
  const inputNode = this.core.inputNode as InputVideoFrame;
  const newTrack = inputNode.getOutputTrack?.();
  if (newTrack) {
    logger.info("resetCore: updating outputStream with new generator track");
    this.outputStream.updateStream(new MediaStream([newTrack]));
  }
}

oldCore.destroy();
```

**关键点**：`this.outputStream.updateStream` 必须在 `oldCore.destroy()` **之前**调用，否则旧 generator 的 writable 在 worker terminate 瞬间关闭，会让 outputStream 持有的 track 进入 ended 状态，存在竞态窗口。

#### 修改 2：`updateInputStream` 方法

**位置**：非移动端分支，`VideoTrack is not changed` 逻辑后面

**修改内容**：新增 output track ended 状态检测和恢复（兜底）

```typescript
} else {
  logger.info("updateInputStream: VideoTrack is not changed, no need to update");
  // bugfix: 即使 videoTrack 引用未变，如果 output track 已经 ended
  // （典型场景：期间发生过 contextlost → resetCore，旧 generator 的 track 已失活），
  // 用当前 core.inputNode 的新 generator 重建 outputStream
  if (this.outputStream && this.core?.inputNode?.datatype === "videoframe") {
    const currentOutputTrack = this.outputStream.getInstance().getVideoTracks()[0];
    if (currentOutputTrack?.readyState === "ended") {
      const inputNode = this.core.inputNode as InputVideoFrame;
      const newTrack = inputNode.getOutputTrack?.();
      if (newTrack) {
        logger.info("updateInputStream: output track is ended, rebuilding outputStream");
        this.outputStream.updateStream(new MediaStream([newTrack]));
      }
    }
  }
}
```

#### 修改 3：`_initMainLoop` 方法

**位置**：`_initMainLoop` 方法开头，注册新 visibilitychange 监听器之前

**修改内容**：先移除旧的 visibilitychange 监听器，避免 resetCore 重复注册

```typescript
this.core?.initMainLoop();
// bugfix: 移除旧的 visibilitychange 监听器，避免 resetCore 导致重复注册
if (this._visibilityChangeFunc) {
  document.removeEventListener("visibilitychange", this._visibilityChangeFunc);
}
```

**修改原因**：`resetCore` 调用 `_initMainLoop` 时会注册新的 visibilitychange 监听器，但不会移除旧的。每次 resetCore 后切 tab 就多打印一次 `visibilitychange:hidden:paused`，监听器越积越多。

## 兼容性说明

- 修改 1 / 修改 2 均仅在 `outputStream` 存在 + `videoframe` 模式下生效，正常初始化、Safari MainImpl 路径不受影响
- 通过 `getOutputTrack()` 同时兼容原生 `MediaStreamTrackGenerator` 与 polyfill `VideoTrackGenerator`
- 修改 3 在 destroy 路径已有 remove，幂等性 OK
- 对微信小程序平台无影响（仅 h5 路径）
- 三处修复都使用了 `OutputStream.updateStream` 已有方法，不引入新的 API 接口

## 后续优化建议（不阻塞本次修复）

SDK 可以暴露一个「output 流失活」事件，让客户不用轮询 `readyState` 也能感知失活并主动恢复，进一步减少类似 case 的客户感知延迟。
