tencentcloud-webar 2.0.3-10 → 2.0.3-12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,157 @@
1
+ # 问题分析:BUG-39acc48b(黑屏,第二轮分析 - 真因定位)
2
+
3
+ ## 客户环境
4
+ - Customer: 1253417915(与 BUG-e289b43c 同一客户)
5
+ - SDK: 2.0.3-10
6
+ - Platform: Electron 24.8.8 内嵌网页
7
+ - GPU: Intel Iris Xe(quality level = low)
8
+ - 现象: 黑屏;客户调用 `updateInputStream` 重置流后**仍然黑屏**
9
+
10
+ ## 关键证据(第二份日志 a37e69ad)
11
+
12
+ ### 实例 4 的关键事件链(line 251-276)
13
+
14
+ ```
15
+ 251 getOutput outputTrack videoframe ["live"] // 初次获取 output,live
16
+ 252 setBackground null
17
+ 257 outputVideoTrack muted. // 黑屏症状出现
18
+ 265 updateInputStream [true] // 客户尝试修复
19
+ 266 videotrack settings ["a5a0b989...", 160, 120, 15]
20
+ 267 updateInputStream: VideoTrack is not changed // 走 BUG-e289b43c 修复路径
21
+ 268 updateInputStream: output track is ended, rebuilding outputStream // 触发了 ended 兜底
22
+ 269 getOutput fps [15] // 客户重新调 getOutput
23
+ 270 check generator track ...
24
+ 271 output stream deviceId ["6a01bba9-0e3c-..."] // 新建 OutputStream 实例
25
+ 272 getOutput outputTrack videoframe ["ended"] // ⚠️⚠️⚠️ 仍然 ended
26
+ 273 updateInputStream [true] // 客户再次重试
27
+ 274 videotrack settings 同上
28
+ 275 updateInputStream: VideoTrack is not changed
29
+ 276 updateInputStream: output track is ended, rebuilding outputStream // 仍然走兜底路径,但仍无效
30
+ ```
31
+
32
+ ## 真正的根因
33
+
34
+ **BUG-e289b43c 的修复路径在本场景下无效。**
35
+
36
+ 第一次修复时的逻辑:
37
+
38
+ ```ts
39
+ // 旧实现(无效)
40
+ if (currentOutputTrack.readyState === "ended") {
41
+ const newGenerator = inputNode.generator; // ❌ 这是同一个 ended generator
42
+ if (newGenerator && newGenerator.readyState === "live") {
43
+ this.outputStream.updateStream(new MediaStream([newGenerator])); // wrap 同一个 ended track 没用
44
+ }
45
+ }
46
+ ```
47
+
48
+ ### 失败原因
49
+
50
+ 1. `core.inputNode.generator` 是 SDK 实例创建时 `new MediaStreamTrackGenerator()` 一次性绑定的
51
+ 2. 一旦该 generator 的 track 进入 `ended` 状态(如 `MediaStreamTrackProcessor` 输入端 cancel/close 传播),**这个 generator 就永久失活**
52
+ 3. 仅靠 `outputStream.updateStream()` 把它重新 wrap 到 MediaStream 里,**没换 track 本身,readyState 依然是 ended**
53
+ 4. 客户重新 `getOutput()` 也只是 `new OutputStream(同一个 generator, ...)`(line 864),新 OutputStream 持有的依然是同一个 ended track(line 272 证实)
54
+
55
+ ### Review 时的预警已被证实
56
+
57
+ 之前对 BUG-e289b43c 修复 review 时就提过:
58
+ > "core.inputNode.generator 是老的 generator(除非中途有人把它换了),它的 track 一旦 ended 就再也不会变 live——重建出来的 stream 仍然是 ended 状态。"
59
+
60
+ 当时担心的就是这个。本次日志正好命中。
61
+
62
+ ## 真正能修复 ended 的唯一路径
63
+
64
+ 要让 generator 的 track 从 ended 恢复,**必须 `new MediaStreamTrackGenerator()` 创建一个全新的 generator**。这个动作只发生在:
65
+
66
+ - `VideoInputVideoFrame.updateNode(src, updateOutputDestination=true)`(VideoInputVideoFrame.ts:191-208)
67
+ - 由 `WorkerCore.updateInputNode(src, true)` 触发
68
+ - 由 `ArSdk.updateInputStream(src, stopOldTracks, updateOutputDestination=true)` 在 `isVideoTrackChanged===true` 路径下触发
69
+
70
+ 但客户场景中:
71
+ - `oldVideoTrack === newVideoTrack`(同一个 deviceId,同一个 track 对象)
72
+ - `isVideoTrackChanged` 一直是 false
73
+ - 永远走不到 `core.updateInputNode(src, true)` → 永远没机会重建 generator
74
+
75
+ ## 修复方案(已实施)
76
+
77
+ 修改 `updateInputStream` 中 "VideoTrack 未变 + output ended" 兜底分支:
78
+
79
+ **改前**(无效的重新 wrap):
80
+
81
+ ```ts
82
+ if (currentOutputTrack.readyState === "ended") {
83
+ const newGenerator = inputNode.generator;
84
+ if (newGenerator && newGenerator.readyState === "live") {
85
+ this.outputStream.updateStream(new MediaStream([newGenerator]));
86
+ }
87
+ }
88
+ ```
89
+
90
+ **改后**(强制走重建路径):
91
+
92
+ ```ts
93
+ if (currentOutputTrack.readyState === "ended") {
94
+ // 仅 wrap 同一个 generator 不能让 ended track 复活,必须重建 generator
95
+ isVideoTrackChanged = true; // 触发下方 updateInputNode 路径
96
+ updateOutputDestination = true; // 强制 new MediaStreamTrackGenerator
97
+ }
98
+ ```
99
+
100
+ 下游已有的逻辑(line 1785-1801)会负责:
101
+ 1. `await this.core.updateInputNode(src, true)` → 内部 `new MediaStreamTrackGenerator()` 产生全新 live track
102
+ 2. `this.outputStream.updateStream(new MediaStream([新 generator]))` → 客户拿到的 outputStream 真正活了
103
+
104
+ ## 副作用 / 风险评估
105
+
106
+ | 项 | 评估 |
107
+ |---|---|
108
+ | 旧 videoTrack 是否会被 stop | 不会。line 1804 `oldVideoTrack !== newVideoTrack && stopOldTracks` 在引用相同时不触发,与原行为一致 |
109
+ | 旧 audioTrack 是否会被换 | 不影响。audio 分支独立判断 |
110
+ | 是否会强制走 updateInputNode 路径多此一举(output 没 ended 时) | 不会。只在 ended 检测命中时才设置 flag |
111
+ | GPU/Worker 资源是否会重新创建 | 不会。`updateInputNode(src, true)` 只创建新 `MediaStreamTrackProcessor` + `MediaStreamTrackGenerator`,worker/canvas/detector 不动 |
112
+ | 客户调用 `this.input === src` 时是否仍然提前 return | 是。line 1690 在传同一个 stream 对象时仍 return,**这是另一个潜在缺陷**(详见下方"已知遗留问题") |
113
+
114
+ ## 已知遗留问题(不在本次修复范围)
115
+
116
+ ### 遗留 1:`this.input === src` 提前 return
117
+
118
+ ```ts
119
+ if (this.input === src) {
120
+ logger.info("updateInputStream: same MediaStream, no need to update");
121
+ return;
122
+ }
123
+ ```
124
+
125
+ 如果客户复用同一个 `MediaStream` 对象调用 `updateInputStream`,会直接 return,不进入修复路径。日志中无法判断客户是否传了同一个 stream 对象(只能看到 deviceId),但若实际有此用法,本次修复仍无效。
126
+
127
+ **建议客户**:发现 output 失活时,传入"新构造的"`new MediaStream([原 videoTrack])` 来绕过此判断。或者后续给 `updateInputStream` 加一个 `force` 参数。
128
+
129
+ ### 遗留 2:input 流自身降级(160x120)
130
+
131
+ 日志显示 input track 被外部降级到 160x120,这是上游会议系统的占位流。SDK 即便重建 generator,也只能拿到一个空帧流。**本次修复让 track 不再 ended(变 muted)**,客户监听 muted/unmute 事件可以感知流的活性变化,比"永久 ended 不可恢复"好得多。
132
+
133
+ ### 遗留 3:客户不释放旧 outputStream 引用
134
+
135
+ 实例 2 的 line 97 出现 sess1 的 muted 事件,说明客户保留了旧 SDK 实例的 outputStream。建议客户在 destroy 后置 `outputStream = null` 释放引用。
136
+
137
+ ### 遗留 4:lost context 自动恢复
138
+
139
+ contextlost handler 只 trigger error 不自动 resetCore。客户业务侧的重建策略激进。可后续在 SDK 内加节流的自动恢复,但属于增强不属于本次修复。
140
+
141
+ ## 与 BUG-e289b43c 的关系
142
+
143
+ 本次修复**取代**了 BUG-e289b43c 在 updateInputStream 分支的兜底逻辑:
144
+
145
+ - BUG-e289b43c 的 resetCore 路径修复(line 686-697)**仍然有效**,保留不动
146
+ - BUG-e289b43c 的 updateInputStream 兜底(重新 wrap)**被本次修复替换为强制重建**
147
+
148
+ ## 结论
149
+
150
+ **本次是一次真正的根因修复**,不是优化。客户验证步骤:
151
+
152
+ 1. 复现:触发 outputVideoTrack muted/ended(如频繁切 tab + GPU 弱)
153
+ 2. 客户调 `sdk.updateInputStream(currentStream, true)`(注意:可能需要传新构造的 MediaStream,避免 `this.input === src` 提前 return)
154
+ 3. 期望日志:`output track is ended, force rebuilding generator` → 经过 `updateInputNode` → `updating outputStream with new generator`
155
+ 4. 期望结果:客户拿到的 outputStream 中 videoTrack 重新变 live,黑屏恢复(前提是 input 流本身有真实数据)
156
+
157
+ 如果 input 流本身被降级到 160x120 placeholder,修复后 track 会从 ended 变成 live,但很快进入 muted 状态——这是上游问题,需客户业务侧恢复正常视频流后才能完全解决。
@@ -0,0 +1,116 @@
1
+ # 代码变更:BUG-39acc48b(黑屏 - 真因修复)
2
+
3
+ ## 修改文件
4
+
5
+ ### `src/h5/index.ts`
6
+
7
+ #### 修改:`updateInputStream` 方法的"VideoTrack 未变 + output ended"兜底分支
8
+
9
+ **位置**:非移动端分支,line ~1755-1771
10
+
11
+ **修改前**(无效的重新 wrap 同一个 ended generator):
12
+
13
+ ```typescript
14
+ }else{
15
+ logger.info("updateInputStream: VideoTrack is not changed, no need to update");
16
+ // bugfix: 即使 videoTrack 引用未变,如果 output track 已经 ended,
17
+ // 需要用当前 generator 重建 outputStream,否则客户无法恢复 output 流
18
+ if (this.outputStream && this.core?.inputNode?.datatype === "videoframe") {
19
+ const currentOutputTrack = this.outputStream.getInstance().getVideoTracks()[0];
20
+ if (currentOutputTrack && currentOutputTrack.readyState === "ended") {
21
+ logger.info("updateInputStream: output track is ended, rebuilding outputStream");
22
+ const inputNode = this.core.inputNode as InputVideoFrame;
23
+ const newGenerator = inputNode.generator;
24
+ if (newGenerator && newGenerator.readyState === "live") {
25
+ const newStream = new MediaStream([newGenerator]);
26
+ this.outputStream.updateStream(newStream); // ❌ 同一个 ended generator,无效
27
+ }
28
+ }
29
+ }
30
+ }
31
+ ```
32
+
33
+ **修改后**(强制走 updateInputNode 重建 generator):
34
+
35
+ ```typescript
36
+ }else{
37
+ logger.info("updateInputStream: VideoTrack is not changed, no need to update");
38
+ // bugfix(BUG-39acc48b): 即使 videoTrack 引用未变,如果 output track 已经 ended,
39
+ // 必须真正重建 generator 才能恢复(仅 wrap 同一个 ended generator 无效)。
40
+ // 强制走 updateInputNode(src, true) 路径,会 new MediaStreamTrackGenerator() 产生全新的 live track。
41
+ if (this.outputStream && this.core?.inputNode?.datatype === "videoframe") {
42
+ const currentOutputTrack = this.outputStream.getInstance().getVideoTracks()[0];
43
+ if (currentOutputTrack && currentOutputTrack.readyState === "ended") {
44
+ logger.info("updateInputStream: output track is ended, force rebuilding generator");
45
+ isVideoTrackChanged = true; // 触发下方的 updateInputNode 路径
46
+ updateOutputDestination = true; // 强制重建 generator
47
+ }
48
+ }
49
+ }
50
+ ```
51
+
52
+ ## 修改原理
53
+
54
+ ### 为什么旧实现无效?
55
+
56
+ `MediaStreamTrackGenerator` 一旦 track 进入 `ended` 状态就**永久失活**,不可能通过任何方式让它从 ended 变回 live。仅靠 `outputStream.updateStream(new MediaStream([sameGenerator]))` 只是在容器层重新 wrap,track 自身仍然是 ended。
57
+
58
+ 日志证据(实例 4,sess=1780309186407):
59
+
60
+ ```
61
+ 268 updateInputStream: output track is ended, rebuilding outputStream ← 旧修复触发
62
+ 272 getOutput outputTrack videoframe ["ended"] ← 但仍然 ended
63
+ 276 updateInputStream: output track is ended, rebuilding outputStream ← 客户重试也无效
64
+ ```
65
+
66
+ ### 为什么新实现有效?
67
+
68
+ 通过设置 `isVideoTrackChanged=true` + `updateOutputDestination=true`,让代码进入下方原本只有 videoTrack 真正变化时才会走的路径:
69
+
70
+ ```typescript
71
+ if (this.core && isVideoTrackChanged) {
72
+ await this.core.updateInputNode(src, updateOutputDestination); // 内部 new MediaStreamTrackGenerator()
73
+ // ...
74
+ if (updateOutputDestination && this.outputStream && datatype === "videoframe") {
75
+ const newGenerator = (this.core.inputNode as InputVideoFrame).generator; // 全新 generator
76
+ this.outputStream.updateStream(new MediaStream([newGenerator])); // ✅ 真正活的 track
77
+ }
78
+ }
79
+ ```
80
+
81
+ `VideoInputVideoFrame.updateNode(src, true)` 会执行:
82
+
83
+ ```typescript
84
+ this.generator = new MediaStreamTrackGenerator({ kind: "video" }); // 全新 track,readyState=live
85
+ this.writable = this.generator.writable;
86
+ ```
87
+
88
+ ## 兼容性 & 副作用
89
+
90
+ | 项 | 评估 |
91
+ |---|---|
92
+ | videoTrack 引用相同时 stopOldTracks | 不影响。下游 `oldVideoTrack !== newVideoTrack` 判断仍然为 false,不会停旧 track |
93
+ | audioTrack 行为 | 完全不变,audio 分支独立 |
94
+ | Worker / 检测器 / Canvas / WebGL | 完全不重建。只重建 `MediaStreamTrackProcessor` + `MediaStreamTrackGenerator` 这两个流操作对象 |
95
+ | 旧 OutputStream 客户引用 | 保持不变。`updateStream` 仅替换内部 track |
96
+ | 微信小程序 | 无影响(仅 h5 路径) |
97
+ | Output 没 ended 时 | 不触发新逻辑,行为完全不变 |
98
+
99
+ ## 测试验证(建议客户复现路径)
100
+
101
+ 1. **复现黑屏**:触发 outputVideoTrack 进入 ended(如频繁切 tab、GPU 资源不足导致 contextlost、或上游 input track stop)
102
+ 2. **客户调用**:`sdk.updateInputStream(stream, true)`
103
+ 3. **预期日志**:
104
+ ```
105
+ updateInputStream: VideoTrack is not changed, no need to update
106
+ updateInputStream: output track is ended, force rebuilding generator
107
+ updateInputNode [...]
108
+ updateInputStream: updating outputStream with new generator
109
+ ```
110
+ 4. **预期结果**:客户保存的 outputStream 实例的 videoTrack 重新变为 live;黑屏恢复
111
+
112
+ ## 已知遗留(建议客户配合)
113
+
114
+ 1. **`this.input === src` 提前 return 的问题**:如果客户复用同一个 MediaStream 对象调用 `updateInputStream`,会在 line 1690 直接 return。**建议**:失活时传入 `new MediaStream([currentVideoTrack])` 强制创建新 stream 引用。
115
+ 2. **释放旧 outputStream**:destroy SDK 后客户应该 `outputStream = null` 释放引用,避免持续派发事件。
116
+ 3. **input 流被外部降级(如 160x120 placeholder)**:本次修复让 track 不再 ended,但流本身没数据时会进入 muted 状态。客户监听 muted/unmute 即可感知。