fix: 修复自编码模式黑屏及并发问题

- 修复 MediaCodec 回调需在 configure 前设置导致的异常
- 修复解码器线程安全问题及包乱序/重复处理
- 增加关键帧前重发 SPS/PPS 以防首帧丢失导致黑屏
- 支持分辨率切换时重置解码器
- 修复 Flutter 控制端连接类型选项 UI 显示拥挤问题
This commit is contained in:
2026-07-24 09:31:58 +08:00
parent f8048e1331
commit 778c37b8db
9 changed files with 374 additions and 107 deletions

View File

@@ -301,6 +301,11 @@ public class ScreenCaptureService extends Service {
currentCaptureHeight = targetH;
currentCaptureFps = targetFps;
// 关键:分辨率切换后,如果是自编码模式,同样需要触发扰动确保产生首帧,避免静态画面黑屏。
if (currentStreamMode == SelfCodecEncoder.STREAM_MODE_SELF_CODEC) {
updateNotification("正在切换分辨率...");
}
Log.i(TAG, "Capture resolution changed -> " + targetW + "x" + targetH + " @ " + targetFps + "fps (remote=" + fromRemote + ")");
showToast(getString(R.string.resolution_changed_message, targetW, targetH));
@@ -354,7 +359,19 @@ public class ScreenCaptureService extends Service {
if (webRtcClient != null) {
webRtcClient.stopCapture();
}
startSelfCodecEncoder();
if (!startSelfCodecEncoder()) {
// 启动失败(如 Android 14+ 不允许复用投屏授权令牌):
// 回退 WebRTC 模式并如实上报,避免两端都黑屏。
Log.e(TAG, "switch to self codec failed, fallback to webrtc mode");
if (webRtcClient != null) {
webRtcClient.startCapture();
}
currentStreamMode = SelfCodecEncoder.STREAM_MODE_WEBRTC;
reportStreamMode(SelfCodecEncoder.STREAM_MODE_WEBRTC);
return;
}
// 关键:切换到自编码模式后,立即触发一次通知刷新(扰动),确保 MediaProjection 产生首帧并喂入编码器。
updateNotification("正在使用自编码模式串流...");
} else {
// 切回 WebRTC停止自编码恢复默认屏幕采集。
stopSelfCodecEncoder();
@@ -371,10 +388,11 @@ public class ScreenCaptureService extends Service {
return currentStreamMode;
}
private void startSelfCodecEncoder() {
/** 启动自编码通道。返回 false 表示启动失败(调用方应回退 WebRTC 模式)。 */
private boolean startSelfCodecEncoder() {
if (selfEncoder != null) {
Log.w(TAG, "self codec encoder already running");
return;
return true;
}
if (webRtcClient == null || !webRtcClient.isVideoDataChannelOpen()) {
Log.w(TAG, "startSelfCodecEncoder: video channel not ready yet");
@@ -385,7 +403,7 @@ public class ScreenCaptureService extends Service {
selfMediaProjection = mpm.getMediaProjection(resultCode, resultDataIntent);
} catch (Exception e) {
Log.e(TAG, "getMediaProjection for self codec failed", e);
return;
return false;
}
int dpi = getResources().getDisplayMetrics().densityDpi;
int[] res = getCurrentCaptureResolution();
@@ -406,6 +424,7 @@ public class ScreenCaptureService extends Service {
}
});
selfEncoder.start();
return true;
}
private void stopSelfCodecEncoder() {
@@ -537,6 +556,16 @@ public class ScreenCaptureService extends Service {
webRtcClient = new WebRtcClient(this, wsClient, deviceId);
webRtcClient.initialize(eglBase);
webRtcClient.setInputCallback(commandBytes -> inputHandler.handleCommand(commandBytes));
webRtcClient.setIceEventListener(() -> {
mainHandler.post(() -> {
if (currentStreamMode == SelfCodecEncoder.STREAM_MODE_SELF_CODEC && selfEncoder != null) {
selfEncoder.requestKeyFrame();
updateNotification("远程控制已连接 (自编码)");
} else {
updateNotification("远程控制已连接");
}
});
});
// 注册远程控制断开回调:控制端断线时提示其用户名。
webRtcClient.setRemoteDisconnectCallback(controllerId ->
mainHandler.post(() -> onControllerDisconnected(controllerId)));

View File

@@ -48,6 +48,9 @@ public class SelfCodecEncoder {
private boolean running = false;
private int frameSeq = 0;
private final Object sendLock = new Object();
// 缓存 SPS/PPS 等参数集video 通道不可靠(可能丢包),在每个关键帧前重发,
// 保证控制端即使错过首次 CONFIG 也能在下一个 IDR 前完成解码器配置。
private final java.util.ArrayList<byte[]> cachedConfigs = new java.util.ArrayList<>();
public interface Callback {
void onEncoderStarted(int w, int h, int fps);
@@ -79,9 +82,10 @@ public class SelfCodecEncoder {
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
encoder = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC);
// 异步回调需在 configure() 之前设置(部分系统版本在 configure 后设置会抛异常)
encoder.setCallback(encoderCallback);
encoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
inputSurface = encoder.createInputSurface();
encoder.setCallback(encoderCallback);
encoder.start();
virtualDisplay = mediaProjection.createVirtualDisplay(
@@ -132,8 +136,14 @@ public class SelfCodecEncoder {
mc.releaseOutputBuffer(index, false);
if (isConfig) {
cacheConfig(data);
sendUnit(UNIT_TYPE_CONFIG, 0, false, data);
} else {
if (isKey) {
// 关键帧前重发参数集:解码端已配置时会忽略重复 CONFIG
// 未配置(首次 CONFIG 丢失/通道未就绪)时借此完成配置。
resendCachedConfigs();
}
sendUnit(UNIT_TYPE_FRAME, pts, isKey, data);
}
} catch (Exception e) {
@@ -152,8 +162,39 @@ public class SelfCodecEncoder {
if (format == null) return;
ByteBuffer sps = format.getByteBuffer("csd-0");
ByteBuffer pps = format.getByteBuffer("csd-1");
if (sps != null) sendUnit(UNIT_TYPE_CONFIG, 0, false, toBytes(sps));
if (pps != null) sendUnit(UNIT_TYPE_CONFIG, 0, false, toBytes(pps));
if (sps != null) {
byte[] d = toBytes(sps);
cacheConfig(d);
sendUnit(UNIT_TYPE_CONFIG, 0, false, d);
}
if (pps != null) {
byte[] d = toBytes(pps);
cacheConfig(d);
sendUnit(UNIT_TYPE_CONFIG, 0, false, d);
}
}
/** 缓存参数集(去重),供关键帧前重发。 */
private void cacheConfig(byte[] data) {
if (data == null || data.length == 0) return;
synchronized (cachedConfigs) {
for (byte[] c : cachedConfigs) {
if (java.util.Arrays.equals(c, data)) return;
}
cachedConfigs.add(data);
}
}
/** 在每个关键帧前重发缓存的参数集。 */
private void resendCachedConfigs() {
byte[][] snapshot;
synchronized (cachedConfigs) {
if (cachedConfigs.isEmpty()) return;
snapshot = cachedConfigs.toArray(new byte[0][]);
}
for (byte[] c : snapshot) {
sendUnit(UNIT_TYPE_CONFIG, 0, false, c);
}
}
private static byte[] toBytes(ByteBuffer b) {

View File

@@ -72,6 +72,12 @@ public class WebRtcClient {
private RemoteDisconnectCallback remoteDisconnectCallback;
private boolean remoteDisconnectNotified = false;
public interface IceEventListener {
void onIceConnected();
}
private IceEventListener iceEventListener;
// 关键修复:控制端(网页)会在被控端弹出"接受"对话框期间就把 ICE 候选发过来,
// 此时 peerConnection 尚未创建 / 远端描述(offer)尚未设置,直接 addIceCandidate 会被丢弃。
// 因此先把候选缓存起来,等 setRemoteDescription(offer) 完成后再统一 flush。
@@ -101,6 +107,10 @@ public class WebRtcClient {
this.remoteDisconnectCallback = callback;
}
public void setIceEventListener(IceEventListener listener) {
this.iceEventListener = listener;
}
private void notifyRemoteDisconnected() {
if (remoteDisconnectNotified) return;
remoteDisconnectNotified = true;
@@ -266,8 +276,10 @@ public class WebRtcClient {
public void onIceConnectionChange(PeerConnection.IceConnectionState newState) {
Log.d(TAG, "ICE connection state: " + newState);
if (newState == PeerConnection.IceConnectionState.CONNECTED) {
// 连接成功时,诱发编码器产生一个关键帧,而不是重启采集器
// 连接成功时,诱发编码器产生一个关键帧
// 外部 ScreenCaptureService 可通过 registerObserver 等方式监听,或者我们直接在此处回调。
triggerKeyFrame();
if (iceEventListener != null) iceEventListener.onIceConnected();
} else if (newState == PeerConnection.IceConnectionState.DISCONNECTED
|| newState == PeerConnection.IceConnectionState.FAILED
|| newState == PeerConnection.IceConnectionState.CLOSED) {
@@ -631,4 +643,12 @@ public class WebRtcClient {
}
}
}
/** 即时请求自编码器产生一个关键帧。 */
public void requestSelfCodecKeyFrame(SelfCodecEncoder encoder) {
if (encoder != null) {
Log.d(TAG, "Requesting key frame for self codec encoder");
encoder.requestKeyFrame();
}
}
}

View File

@@ -2,6 +2,7 @@ package com.ttstd.controller.webrtc;
import android.media.MediaCodec;
import android.media.MediaFormat;
import android.os.Build;
import android.util.Log;
import android.view.Surface;
@@ -33,6 +34,10 @@ public class SelfCodecDecoder {
private boolean configured = false;
private boolean enabled = false;
// 实际宽高(由被控端 REPORT_RESOLUTION 上报),若未上报则使用 PLACEHOLDER
private int videoWidth = PLACEHOLDER_W;
private int videoHeight = PLACEHOLDER_H;
// 待喂入的参数集SPS/PPS 等),以 CODEC_CONFIG 形式输入解码器
private final ArrayDeque<byte[]> pendingConfigs = new ArrayDeque<>();
// 待解码帧
@@ -67,6 +72,23 @@ public class SelfCodecDecoder {
}
}
/**
* 更新当前视频流的实际宽高(由外部 WebRtcClient 收到被控端上报后调用)。
* 如果宽高发生变化且解码器已配置,则重置解码器以应用新分辨率,避免黑屏或花屏。
*/
public void updateResolution(int w, int h) {
if (w <= 0 || h <= 0) return;
if (this.videoWidth != w || this.videoHeight != h) {
Log.i(TAG, "Resolution update: " + videoWidth + "x" + videoHeight + " -> " + w + "x" + h);
this.videoWidth = w;
this.videoHeight = h;
if (configured) {
// 分辨率变化时需重置解码器以重新 configure
releaseDecoder();
}
}
}
/** 收到 video DataChannel 的二进制消息(一条分片)。 */
public void onBinaryMessage(ByteBuffer raw) {
if (!enabled) return;
@@ -76,6 +98,11 @@ public class SelfCodecDecoder {
byte magic = b.get();
if (magic != MAGIC) return;
int seq = b.getInt();
// 关键修复:增加包序号过期检查(处理包乱序或重复)。
// 实时视频流中,如果收到了序号更旧的包,直接丢弃以保证延迟和状态一致性。
if (lastSeq != -1 && seq <= lastSeq && (lastSeq - seq) < 1000) {
return;
}
short total = b.getShort();
short idx = b.getShort();
int len = b.getInt();
@@ -83,6 +110,7 @@ public class SelfCodecDecoder {
byte[] chunk = new byte[len];
b.get(chunk);
synchronized (assembling) {
ChunkBuffer cb = assembling.get(seq);
if (cb == null) {
cb = new ChunkBuffer();
@@ -109,6 +137,7 @@ public class SelfCodecDecoder {
if (unit != null) handleUnit(unit);
}
}
}
private byte[] join(ChunkBuffer cb) {
int totalLen = 0;
@@ -132,14 +161,25 @@ public class SelfCodecDecoder {
b.get(data);
if (type == UNIT_TYPE_CONFIG) {
if (configured) {
// 已配置:忽略重复/初始参数集,避免重复以 CODEC_CONFIG 喂入报错
return;
}
synchronized (pendingConfigs) {
// 仅在尚未包含该配置时添加,避免关键帧前的重复重发堆积。
// 关键:不因为 configured 而 return确保 SPS 和 PPS 都能被加入队列。
boolean exists = false;
for (byte[] c : pendingConfigs) {
if (java.util.Arrays.equals(c, data)) {
exists = true;
break;
}
}
if (!exists) {
pendingConfigs.add(data);
}
}
if (!configured) {
tryConfigure();
} else {
feed(); // 已配置时也尝试喂入,以应对中途可能的参数集更新
}
} else if (type == UNIT_TYPE_FRAME) {
Frame f = new Frame();
f.data = data;
@@ -160,29 +200,50 @@ public class SelfCodecDecoder {
}
try {
MediaFormat format = MediaFormat.createVideoFormat(
MediaFormat.MIMETYPE_VIDEO_AVC, PLACEHOLDER_W, PLACEHOLDER_H);
MediaFormat.MIMETYPE_VIDEO_AVC, videoWidth, videoHeight);
// 关键:对部分芯片组(如 MTK/Exynos低延迟标志有助于减少首帧黑屏时间API 30+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
format.setInteger(MediaFormat.KEY_LOW_LATENCY, 1);
}
decoder = MediaCodec.createDecoderByType(MediaFormat.MIMETYPE_VIDEO_AVC);
decoder.configure(format, surface, null, 0);
// 异步回调必须在 configure() 之前设置,否则抛 IllegalStateException
// 解码器将永远无法进入工作状态(黑屏)。
decoder.setCallback(decoderCallback);
decoder.configure(format, surface, null, 0);
decoder.start();
configured = true;
Log.i(TAG, "self codec decoder configured");
feed();
} catch (Exception e) {
Log.e(TAG, "decoder configure failed", e);
// 清理半初始化的解码器,等待下一个 CONFIG 到达后重试
if (decoder != null) {
try {
decoder.release();
} catch (Exception ignored) {
}
decoder = null;
}
configured = false;
}
}
private final MediaCodec.Callback decoderCallback = new MediaCodec.Callback() {
@Override
public void onInputBufferAvailable(MediaCodec mc, int index) {
synchronized (inputQueue) {
inputQueue.add(index);
}
feed();
}
@Override
public void onOutputBufferAvailable(MediaCodec mc, int index, MediaCodec.BufferInfo info) {
try {
mc.releaseOutputBuffer(index, true);
} catch (IllegalStateException e) {
Log.w(TAG, "onOutputBufferAvailable: codec might be stopped/released", e);
}
}
@Override
@@ -199,6 +260,7 @@ public class SelfCodecDecoder {
private void feed() {
if (decoder == null || !configured) return;
synchronized (pendingConfigs) {
synchronized (inputQueue) {
while (!pendingConfigs.isEmpty() && !inputQueue.isEmpty()) {
byte[] cfg = pendingConfigs.poll();
int idx = inputQueue.poll();
@@ -215,7 +277,9 @@ public class SelfCodecDecoder {
}
}
}
}
synchronized (frameQueue) {
synchronized (inputQueue) {
while (!frameQueue.isEmpty() && !inputQueue.isEmpty()) {
Frame f = frameQueue.poll();
int idx = inputQueue.poll();
@@ -233,8 +297,14 @@ public class SelfCodecDecoder {
}
}
}
}
private void releaseDecoder() {
configured = false;
synchronized (assembling) {
lastSeq = -1; // 释放时重置序号,避免重连后首帧被误判为过期
assembling.clear();
}
if (decoder != null) {
try {
decoder.stop();
@@ -246,11 +316,15 @@ public class SelfCodecDecoder {
}
decoder = null;
}
configured = false;
synchronized (pendingConfigs) {
pendingConfigs.clear();
}
synchronized (frameQueue) {
frameQueue.clear();
}
synchronized (inputQueue) {
inputQueue.clear();
assembling.clear();
}
}
public void release() {

View File

@@ -318,6 +318,15 @@ public class WebRtcClient {
}
private void handleDataChannel(DataChannel dc) {
if (dc == null) return;
// 关键修复:区分控制端创建的两条通道。
// 虽然本端是创建方,但 PeerObserver.onDataChannel 仍可能在某些 WebRTC 协商逻辑中被触发。
if (VIDEO_CHANNEL_LABEL.equals(dc.label())) {
this.videoDataChannel = dc;
setupVideoDataChannel(dc);
return;
}
this.dataChannel = dc;
dc.registerObserver(new DataChannel.Observer() {
@Override
public void onBufferedAmountChange(long previousAmount) {}
@@ -337,14 +346,18 @@ public class WebRtcClient {
if (buffer == null || buffer.data == null) return;
byte[] data = new byte[buffer.data.remaining()];
buffer.data.get(data);
// 收到被控端回报的串流模式,单独处理。
// 收到被控端回报的消息,按 Action 分发处理。
try {
ControlMessage msg = ControlMessage.parseFrom(data);
if (msg.getAction() == Action.REPORT_STREAM_MODE) {
if (streamModeReportListener != null) {
streamModeReportListener.onStreamModeReported(msg.getStreamMode());
}
return;
} else if (msg.getAction() == Action.REPORT_RESOLUTION) {
Log.i(TAG, "Received resolution report: " + msg.getWidth() + "x" + msg.getHeight());
if (selfCodecDecoder != null) {
selfCodecDecoder.updateResolution(msg.getWidth(), msg.getHeight());
}
}
} catch (Exception ignored) {
}

View File

@@ -141,8 +141,9 @@ class SelfCodecDecoderPlugin :
val st = entry.surfaceTexture()
surfaceTexture = st
st.setDefaultBufferSize(PLACEHOLDER_W, PLACEHOLDER_H)
// MediaCodec 把解码帧渲染到该 SurfaceTexture。必须消费updateTexImage
// 新帧,否则 SurfaceTexture 一直停留在初始(黑)帧,导致切换自编码后无画面。
st.setOnFrameAvailableListener { texture: SurfaceTexture ->
// 解码器将帧渲染到 Surface 后触发,这里更新纹理内容供 Flutter 显示。
try {
texture.updateTexImage()
} catch (e: Exception) {
@@ -251,8 +252,11 @@ class SelfCodecDecoderPlugin :
}
override fun onOutputFormatChanged(codec: MediaCodec, format: MediaFormat) {
val w = format.getInteger(MediaFormat.KEY_WIDTH, 0)
val h = format.getInteger(MediaFormat.KEY_HEIGHT, 0)
// getInteger(key, default) 需要 API 29+,用 containsKey 兼容低版本
val w = if (format.containsKey(MediaFormat.KEY_WIDTH))
format.getInteger(MediaFormat.KEY_WIDTH) else 0
val h = if (format.containsKey(MediaFormat.KEY_HEIGHT))
format.getInteger(MediaFormat.KEY_HEIGHT) else 0
if (w > 0 && h > 0) {
surfaceTexture?.setDefaultBufferSize(w, h)
mainHandler.post {

View File

@@ -27,6 +27,9 @@ class RemoteController {
/// WebRTC 连接建立(可开始远程控制)。
void Function()? onConnectionEstablished;
/// 连接失败。
void Function(String error)? onConnectionFailed;
/// 连接断开。
void Function()? onDisconnected;
@@ -48,6 +51,9 @@ class RemoteController {
/// 自编码解码纹理已就绪textureId >= 0
void Function(int textureId)? onSelfCodecReady;
/// 自编码解码器已释放(连接断开时)。
void Function()? onSelfCodecLost;
/// 被控端上报当前生效的串流模式。
void Function(int mode)? onStreamModeReport;
@@ -99,17 +105,20 @@ class RemoteController {
onConnectionEstablished?.call();
_startStats();
};
_webRtc!.onConnectionFailed = (error) => onConnectionFailed?.call(error);
_webRtc!.onDisconnected = () {
onDisconnected?.call();
};
_webRtc!.onIceDisconnected = (message) => onIceDisconnected?.call(message);
_webRtc!.onRemoteStream = (renderer) => onRemoteStream?.call(renderer);
_webRtc!.onSelfCodecReady = (id) => onSelfCodecReady?.call(id);
_webRtc!.onSelfCodecLost = () => onSelfCodecLost?.call();
_webRtc!.onStreamModeReport = (mode) => onStreamModeReport?.call(mode);
_webRtc!.onResolutionReported = (w, h) => onResolutionReported?.call(w, h);
_webRtc!.onSelfCodecNotSupported = () => onSelfCodecNotSupported?.call();
_webRtc!.initialize().catchError((e) {
onStatusChanged?.call('状态: 连接失败 - $e');
onConnectionFailed?.call(e.toString());
});
}

View File

@@ -126,6 +126,69 @@ class _ControllerHomeState extends State<ControllerHome> {
String selectedType = 'NONE';
final valueController = TextEditingController();
// 纵向选项列表,避免分段控件在窄弹窗中把中文选项挤压成两行。
Widget buildOption(
String value,
String title,
String desc,
void Function(void Function()) setDialogState,
) {
final selected = selectedType == value;
return GestureDetector(
onTap: () => setDialogState(() => selectedType = value),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
margin: const EdgeInsets.only(bottom: 8),
decoration: BoxDecoration(
border: Border.all(
color: selected
? CupertinoColors.activeBlue
: CupertinoColors.systemGrey4,
),
borderRadius: BorderRadius.circular(10),
color: selected
? CupertinoColors.activeBlue.withValues(alpha: 0.06)
: null,
),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 2),
Text(
desc,
style: const TextStyle(
fontSize: 12,
color: CupertinoColors.systemGrey,
),
),
],
),
),
const SizedBox(width: 8),
Icon(
selected
? CupertinoIcons.check_mark_circled_solid
: CupertinoIcons.circle,
color: selected
? CupertinoColors.activeBlue
: CupertinoColors.systemGrey,
),
],
),
),
);
}
await showCupertinoDialog<void>(
context: context,
builder: (ctx) => StatefulBuilder(
@@ -134,36 +197,36 @@ class _ControllerHomeState extends State<ControllerHome> {
content: Column(
children: [
const SizedBox(height: 12),
CupertinoSegmentedControl<String>(
children: const {
'NONE': Padding(
padding: EdgeInsets.symmetric(horizontal: 8),
child: Text('免密连接'),
buildOption(
'NONE',
'免密连接',
'被控端弹出手动确认框,无需验证码或密码',
setDialogState,
),
'CODE': Padding(
padding: EdgeInsets.symmetric(horizontal: 8),
child: Text('动态验证码'),
buildOption(
'CODE',
'动态验证码',
'使用一次性动态验证码鉴权',
setDialogState,
),
'PASSWORD': Padding(
padding: EdgeInsets.symmetric(horizontal: 8),
child: Text('固定密码'),
buildOption(
'PASSWORD',
'固定密码',
'使用固定连接密码鉴权',
setDialogState,
),
},
groupValue: selectedType,
onValueChanged: (v) => setDialogState(() => selectedType = v),
),
const SizedBox(height: 12),
if (selectedType == 'NONE')
const Text(
'免密连接:被控端将弹出手动确认对话框,无需输入验证码或密码。',
style: TextStyle(fontSize: 13, color: CupertinoColors.systemGrey),
)
else
const SizedBox(height: 4),
CupertinoTextField(
controller: valueController,
placeholder: selectedType == 'PASSWORD' ? '请输入固定密码' : '请输入动态验证码',
enabled: selectedType != 'NONE',
placeholder: selectedType == 'NONE'
? '免密连接,无需填写'
: selectedType == 'PASSWORD'
? '请输入固定密码'
: '请输入动态验证码',
obscureText: true,
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
padding:
const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
),
],
),
@@ -245,6 +308,14 @@ class _ControllerHomeState extends State<ControllerHome> {
});
}
};
_controller!.onSelfCodecLost = () {
if (mounted) {
setState(() {
_selfCodecReady = false;
_selfCodecTextureId = null;
});
}
};
_controller!.onStreamModeReport = (mode) {
if (mounted) setState(() => _streamMode = mode);
};

View File

@@ -217,7 +217,13 @@ class WebRtcController {
switch (msg.action) {
case Action.REPORT_STREAM_MODE:
_currentStreamMode = msg.streamMode;
_selfCodecDecoder?.setEnabled(msg.streamMode == streamModeSelfCodec);
if (msg.streamMode == streamModeSelfCodec) {
_ensureSelfCodecDecoder().then((_) {
_selfCodecDecoder?.setEnabled(true);
});
} else {
_selfCodecDecoder?.setEnabled(false);
}
onStreamModeReport?.call(msg.streamMode);
break;
case Action.REPORT_RESOLUTION: