feat(streaming): 添加自编码串流模式切换功能

This commit is contained in:
2026-07-23 12:01:08 +08:00
parent a1c29246d7
commit e56723a010
20 changed files with 1684 additions and 16 deletions

View File

@@ -0,0 +1,118 @@
import 'dart:async';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
/// 自编码解码器(控制端)。
///
/// 接收来自 `video` DataChannel 的二进制分片(由被控端 [SelfCodecEncoder] 产生),
/// 通过原生 Android `MediaCodec` 硬解码,并渲染到 Flutter 纹理(`Texture`)。
///
/// 为保证与 `WebRTCControlled` 的编码器在二进制协议上完全一致,协议的
/// 分片重组、H.264 参数集解析与硬解、纹理渲染全部在原生侧完成;
/// 本类仅负责与原生侧通信。对应 Android 端 `SelfCodecDecoder`。
///
/// 注意:原生解码器目前仅在 Android 平台实现。其它平台Windows / Linux /
/// Web / iOS调用 [create] 会失败并回调 [onError],调用方应回退到
/// WebRTC 默认媒体流。
class SelfCodecDecoder {
static const MethodChannel _channel =
MethodChannel('com.ttstd.fluttercontroller/self_codec');
/// 串流模式常量(与 ControlMessage.stream_mode 对齐)。
static const int streamModeWebRtc = 0;
static const int streamModeSelfCodec = 1;
int? _textureId;
bool _disposed = false;
bool _handlerSet = false;
/// 解码输出尺寸变化(宽/高,单位像素)。
final void Function(int width, int height)? onSize;
/// 错误回调(如当前平台不支持硬解)。
final void Function(String error)? onError;
SelfCodecDecoder({this.onSize, this.onError}) {
_channel.setMethodCallHandler(_handleNativeCall);
_handlerSet = true;
}
Future<dynamic> _handleNativeCall(MethodCall call) async {
switch (call.method) {
case 'onSize':
final args = call.arguments;
final w = args is Map ? (args['width'] as int? ?? 0) : 0;
final h = args is Map ? (args['height'] as int? ?? 0) : 0;
if (w > 0 && h > 0) onSize?.call(w, h);
break;
case 'onError':
final msg = call.arguments is String ? call.arguments as String : '';
if (msg.isNotEmpty) onError?.call(msg);
break;
}
}
/// 创建解码纹理。成功返回纹理 id>=0失败返回 null。
Future<int?> create() async {
if (_disposed) return null;
try {
final id = await _channel.invokeMethod<int>('create');
if (id == null || id < 0) return null;
_textureId = id;
return _textureId;
} on PlatformException catch (e) {
onError?.call('创建自编码解码纹理失败: ${e.message}');
return null;
} catch (e) {
onError?.call('创建自编码解码纹理失败: $e');
return null;
}
}
/// 已创建的纹理 id未创建或已释放时为 null。
int? get textureId => _textureId;
/// 用于渲染解码画面的纹理控件。
Widget build(BuildContext context) {
final id = _textureId;
if (id == null) return const SizedBox.shrink();
return Texture(textureId: id);
}
/// 喂入来自 video DataChannel 的二进制分片。
Future<void> feed(Uint8List chunk) async {
if (_disposed || _textureId == null) return;
try {
await _channel.invokeMethod<void>('feed', chunk);
} catch (_) {
// 解码未就绪或被释放时忽略。
}
}
/// 启用/停用解码(仅自编码模式为 true
Future<void> setEnabled(bool enabled) async {
if (_disposed || _textureId == null) return;
try {
await _channel.invokeMethod<void>('setEnabled', enabled);
} catch (_) {
// 忽略。
}
}
/// 释放原生解码资源。重复调用安全。
Future<void> dispose() async {
if (_disposed) return;
_disposed = true;
if (_handlerSet) {
_channel.setMethodCallHandler(null);
_handlerSet = false;
}
try {
await _channel.invokeMethod<void>('dispose');
} catch (_) {
// 忽略。
}
_textureId = null;
}
}

View File

@@ -1,6 +1,8 @@
import 'package:flutter/foundation.dart';
import 'package:flutter_webrtc/flutter_webrtc.dart';
import 'self_codec_decoder.dart';
import '../config/ice_servers.dart';
import '../models/signal_message.dart';
import '../proto/control_message.pb.dart';
@@ -26,6 +28,10 @@ class WebRtcController {
RTCPeerConnection? _pc;
RTCDataChannel? _dataChannel;
RTCDataChannel? _videoDataChannel;
SelfCodecDecoder? _selfCodecDecoder;
int _currentStreamMode = streamModeWebRtc;
bool _creatingDecoder = false;
/// 远端视频渲染器,由调用方持有并显示。
final RTCVideoRenderer renderer = RTCVideoRenderer();
@@ -40,6 +46,25 @@ class WebRtcController {
/// 远端视频流就绪renderer 已绑定视频轨道)。
void Function(RTCVideoRenderer renderer)? onRemoteStream;
/// 自编码解码纹理已就绪textureId >= 0
void Function(int)? onSelfCodecReady;
/// 自编码解码器已释放(连接断开时)。
void Function()? onSelfCodecLost;
/// 被控端上报当前生效的串流模式。
void Function(int mode)? onStreamModeReport;
/// 被控端上报分辨率(自编码解码器输出尺寸变化时也会触发)。
void Function(int width, int height)? onResolutionReported;
/// 当前平台Windows / Linux / Web / iOS 等)不支持原生硬解时回调。
void Function()? onSelfCodecNotSupported;
static const String kVideoChannelLabel = 'video_channel';
static const int streamModeWebRtc = 0;
static const int streamModeSelfCodec = 1;
WebRtcController({
required this.signaling,
required this.deviceId,
@@ -83,6 +108,17 @@ class WebRtcController {
_dataChannel = await _pc!.createDataChannel(kDataChannelLabel, dcInit);
_setupDataChannel(_dataChannel!);
// 创建自编码视频透传通道(无序、不可靠,类似 RTP最低延迟
// 被控端仅在收到 SET_STREAM_MODE=自编码 后才经此通道推送 H.264 裸流。
final videoInit = RTCDataChannelInit()
..ordered = false
..maxRetransmits = 0;
_videoDataChannel = await _pc!.createDataChannel(
kVideoChannelLabel,
videoInit,
);
_setupVideoDataChannel(_videoDataChannel!);
// 创建并发送 Offer。
final constraints = {
'mandatory': {
@@ -104,18 +140,22 @@ class WebRtcController {
}
void _onDataChannel(RTCDataChannel channel) {
// 被控端也可能主动创建 DataChannel统一处理。
_setupDataChannel(channel);
// 被控端也可能主动创建 DataChannel按标签统一处理。
if (channel.label == kDataChannelLabel) {
_dataChannel ??= channel;
_setupDataChannel(channel);
} else if (channel.label == kVideoChannelLabel) {
_videoDataChannel ??= channel;
_setupVideoDataChannel(channel);
}
}
void _setupDataChannel(RTCDataChannel channel) {
channel.onMessage = (RTCDataChannelMessage message) {
// 处理来自被控端的消息(本控制端主要发送,此处仅做日志记录)。
if (message.isBinary) {
try {
final command = ControlMessage.fromBuffer(message.binary);
// ignore: avoid_print
print('DataChannel message: ${command.action}');
_handleControlMessage(command);
} catch (e) {
// ignore: avoid_print
print('DataChannel message decode failed: $e');
@@ -131,6 +171,78 @@ class WebRtcController {
};
}
void _setupVideoDataChannel(RTCDataChannel channel) {
channel.onMessage = (RTCDataChannelMessage message) {
if (message.isBinary) {
_selfCodecDecoder?.feed(message.binary);
}
};
channel.onDataChannelState = (RTCDataChannelState state) {
if (state == RTCDataChannelState.RTCDataChannelOpen) {
_onVideoChannelOpen();
} else if (state == RTCDataChannelState.RTCDataChannelClosed) {
_selfCodecDecoder?.setEnabled(false);
}
};
}
void _onVideoChannelOpen() {
_ensureSelfCodecDecoder().then((_) {
_selfCodecDecoder?.setEnabled(_currentStreamMode == streamModeSelfCodec);
});
}
Future<void> _ensureSelfCodecDecoder() async {
if (_selfCodecDecoder != null || _creatingDecoder) return;
_creatingDecoder = true;
try {
final decoder = SelfCodecDecoder(
onSize: (w, h) => onResolutionReported?.call(w, h),
onError: (err) => debugPrint('自编码解码器: $err'),
);
final textureId = await decoder.create();
if (textureId != null) {
_selfCodecDecoder = decoder;
onSelfCodecReady?.call(textureId);
} else {
// 当前平台Windows / Linux / Web / iOS 等)无原生硬解能力,回退 WebRTC 媒体流。
onSelfCodecNotSupported?.call();
}
} finally {
_creatingDecoder = false;
}
}
void _handleControlMessage(ControlMessage msg) {
switch (msg.action) {
case Action.REPORT_STREAM_MODE:
_currentStreamMode = msg.streamMode;
_selfCodecDecoder?.setEnabled(msg.streamMode == streamModeSelfCodec);
onStreamModeReport?.call(msg.streamMode);
break;
case Action.REPORT_RESOLUTION:
onResolutionReported?.call(msg.width, msg.height);
break;
default:
// ignore: avoid_print
print('DataChannel message: ${msg.action}');
break;
}
}
/// 请求被控端切换屏幕串流模式SET_STREAM_MODE 指令)。
/// [mode] 为 0=WebRTC 全托管 / 1=自编码。
Future<void> sendStreamMode(int mode) async {
_currentStreamMode = mode;
if (mode == streamModeSelfCodec) {
await _ensureSelfCodecDecoder();
await _selfCodecDecoder?.setEnabled(true);
} else {
await _selfCodecDecoder?.setEnabled(false);
}
sendControlCommand(ControlCommands.streamMode(mode));
}
void _onIceCandidate(RTCIceCandidate candidate) {
final payload = {
'sdpMid': candidate.sdpMid,
@@ -413,6 +525,20 @@ class WebRtcController {
await renderer.dispose();
} catch (_) {}
_dataChannel = null;
try {
await _videoDataChannel?.close();
} catch (_) {}
_videoDataChannel = null;
try {
await _selfCodecDecoder?.dispose();
} catch (_) {}
_selfCodecDecoder = null;
_currentStreamMode = streamModeWebRtc;
onSelfCodecLost?.call();
onSelfCodecReady = null;
onStreamModeReport = null;
onResolutionReported = null;
onSelfCodecNotSupported = null;
_pc = null;
// 重置网速统计基线,避免下次连接首帧显示错误的瞬时速率。
_prevBytesReceived = 0;