feat: 控制指令消息改为protobuf二进制

This commit is contained in:
2026-07-15 17:03:58 +08:00
parent 92ad90cbeb
commit da1730ce19
21 changed files with 658 additions and 147 deletions

View File

@@ -4,6 +4,7 @@ import 'dart:convert';
import 'package:flutter_webrtc/flutter_webrtc.dart';
import '../models/signal_message.dart';
import '../proto/control_message.pb.dart';
import '../signaling/signaling_client.dart';
import '../webrtc/webrtc_controller.dart';
@@ -113,9 +114,9 @@ class RemoteController {
});
}
/// 发送控制指令(JSON 字符串)。
void sendControlCommand(String commandJson) {
_webRtc?.sendControlCommand(commandJson);
/// 发送控制指令(protobuf 二进制)。
void sendControlCommand(ControlMessage command) {
_webRtc?.sendControlCommand(command);
}
/// 断开连接并释放资源。

View File

@@ -0,0 +1,209 @@
// This is a generated file - do not edit.
//
// Generated from control_message.proto.
// @dart = 3.3
// ignore_for_file: annotate_overrides, camel_case_types, comment_references
// ignore_for_file: constant_identifier_names
// ignore_for_file: curly_braces_in_flow_control_structures
// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes
// ignore_for_file: non_constant_identifier_names, prefer_relative_imports
import 'dart:core' as $core;
import 'package:fixnum/fixnum.dart' as $fixnum;
import 'package:protobuf/protobuf.dart' as $pb;
import 'control_message.pbenum.dart';
export 'package:protobuf/protobuf.dart' show GeneratedMessageGenericExtensions;
export 'control_message.pbenum.dart';
/// 通过 RTCDataChannel 传输的控制指令protobuf 二进制)。
/// 坐标 x/y/x1/y1/x2/y2 均为相对屏幕的百分比,取值范围 0.0 ~ 1.0。
class ControlMessage extends $pb.GeneratedMessage {
factory ControlMessage({
Action? action,
$core.double? x,
$core.double? y,
$core.double? x1,
$core.double? y1,
$core.double? x2,
$core.double? y2,
$fixnum.Int64? duration,
$core.int? keyCode,
$core.int? keyAction,
$core.int? motionAction,
}) {
final result = create();
if (action != null) result.action = action;
if (x != null) result.x = x;
if (y != null) result.y = y;
if (x1 != null) result.x1 = x1;
if (y1 != null) result.y1 = y1;
if (x2 != null) result.x2 = x2;
if (y2 != null) result.y2 = y2;
if (duration != null) result.duration = duration;
if (keyCode != null) result.keyCode = keyCode;
if (keyAction != null) result.keyAction = keyAction;
if (motionAction != null) result.motionAction = motionAction;
return result;
}
ControlMessage._();
factory ControlMessage.fromBuffer($core.List<$core.int> data,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromBuffer(data, registry);
factory ControlMessage.fromJson($core.String json,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromJson(json, registry);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
_omitMessageNames ? '' : 'ControlMessage',
package:
const $pb.PackageName(_omitMessageNames ? '' : 'com.ttstd.control'),
createEmptyInstance: create)
..aE<Action>(1, _omitFieldNames ? '' : 'action', enumValues: Action.values)
..aD(2, _omitFieldNames ? '' : 'x')
..aD(3, _omitFieldNames ? '' : 'y')
..aD(4, _omitFieldNames ? '' : 'x1')
..aD(5, _omitFieldNames ? '' : 'y1')
..aD(6, _omitFieldNames ? '' : 'x2')
..aD(7, _omitFieldNames ? '' : 'y2')
..aInt64(8, _omitFieldNames ? '' : 'duration')
..aI(9, _omitFieldNames ? '' : 'keyCode')
..aI(10, _omitFieldNames ? '' : 'keyAction')
..aI(11, _omitFieldNames ? '' : 'motionAction')
..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
ControlMessage clone() => deepCopy();
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
ControlMessage copyWith(void Function(ControlMessage) updates) =>
super.copyWith((message) => updates(message as ControlMessage))
as ControlMessage;
@$core.override
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static ControlMessage create() => ControlMessage._();
@$core.override
ControlMessage createEmptyInstance() => create();
@$core.pragma('dart2js:noInline')
static ControlMessage getDefault() => _defaultInstance ??=
$pb.GeneratedMessage.$_defaultFor<ControlMessage>(create);
static ControlMessage? _defaultInstance;
/// 指令类型
@$pb.TagNumber(1)
Action get action => $_getN(0);
@$pb.TagNumber(1)
set action(Action value) => $_setField(1, value);
@$pb.TagNumber(1)
$core.bool hasAction() => $_has(0);
@$pb.TagNumber(1)
void clearAction() => $_clearField(1);
/// 单点坐标TOUCH / LONG_PRESS / MOTION_EVENT
@$pb.TagNumber(2)
$core.double get x => $_getN(1);
@$pb.TagNumber(2)
set x($core.double value) => $_setDouble(1, value);
@$pb.TagNumber(2)
$core.bool hasX() => $_has(1);
@$pb.TagNumber(2)
void clearX() => $_clearField(2);
@$pb.TagNumber(3)
$core.double get y => $_getN(2);
@$pb.TagNumber(3)
set y($core.double value) => $_setDouble(2, value);
@$pb.TagNumber(3)
$core.bool hasY() => $_has(2);
@$pb.TagNumber(3)
void clearY() => $_clearField(3);
/// 滑动起止坐标SWIPE
@$pb.TagNumber(4)
$core.double get x1 => $_getN(3);
@$pb.TagNumber(4)
set x1($core.double value) => $_setDouble(3, value);
@$pb.TagNumber(4)
$core.bool hasX1() => $_has(3);
@$pb.TagNumber(4)
void clearX1() => $_clearField(4);
@$pb.TagNumber(5)
$core.double get y1 => $_getN(4);
@$pb.TagNumber(5)
set y1($core.double value) => $_setDouble(4, value);
@$pb.TagNumber(5)
$core.bool hasY1() => $_has(4);
@$pb.TagNumber(5)
void clearY1() => $_clearField(5);
@$pb.TagNumber(6)
$core.double get x2 => $_getN(5);
@$pb.TagNumber(6)
set x2($core.double value) => $_setDouble(5, value);
@$pb.TagNumber(6)
$core.bool hasX2() => $_has(5);
@$pb.TagNumber(6)
void clearX2() => $_clearField(6);
@$pb.TagNumber(7)
$core.double get y2 => $_getN(6);
@$pb.TagNumber(7)
set y2($core.double value) => $_setDouble(6, value);
@$pb.TagNumber(7)
$core.bool hasY2() => $_has(6);
@$pb.TagNumber(7)
void clearY2() => $_clearField(7);
@$pb.TagNumber(8)
$fixnum.Int64 get duration => $_getI64(7);
@$pb.TagNumber(8)
set duration($fixnum.Int64 value) => $_setInt64(7, value);
@$pb.TagNumber(8)
$core.bool hasDuration() => $_has(7);
@$pb.TagNumber(8)
void clearDuration() => $_clearField(8);
/// 按键KEYkey_action 0=按下 1=抬起(对应 Android KeyEvent ACTION_DOWN/UP
@$pb.TagNumber(9)
$core.int get keyCode => $_getIZ(8);
@$pb.TagNumber(9)
set keyCode($core.int value) => $_setSignedInt32(8, value);
@$pb.TagNumber(9)
$core.bool hasKeyCode() => $_has(8);
@$pb.TagNumber(9)
void clearKeyCode() => $_clearField(9);
@$pb.TagNumber(10)
$core.int get keyAction => $_getIZ(9);
@$pb.TagNumber(10)
set keyAction($core.int value) => $_setSignedInt32(9, value);
@$pb.TagNumber(10)
$core.bool hasKeyAction() => $_has(9);
@$pb.TagNumber(10)
void clearKeyAction() => $_clearField(10);
/// 原始动作MOTION_EVENT对应 Android MotionEvent ACTION_*0=DOWN 1=UP 2=MOVE
@$pb.TagNumber(11)
$core.int get motionAction => $_getIZ(10);
@$pb.TagNumber(11)
set motionAction($core.int value) => $_setSignedInt32(10, value);
@$pb.TagNumber(11)
$core.bool hasMotionAction() => $_has(10);
@$pb.TagNumber(11)
void clearMotionAction() => $_clearField(11);
}
const $core.bool _omitFieldNames =
$core.bool.fromEnvironment('protobuf.omit_field_names');
const $core.bool _omitMessageNames =
$core.bool.fromEnvironment('protobuf.omit_message_names');

View File

@@ -0,0 +1,47 @@
// This is a generated file - do not edit.
//
// Generated from control_message.proto.
// @dart = 3.3
// ignore_for_file: annotate_overrides, camel_case_types, comment_references
// ignore_for_file: constant_identifier_names
// ignore_for_file: curly_braces_in_flow_control_structures
// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes
// ignore_for_file: non_constant_identifier_names, prefer_relative_imports
import 'dart:core' as $core;
import 'package:protobuf/protobuf.dart' as $pb;
/// 控制指令类型,对应原 JSON 字段 action。
class Action extends $pb.ProtobufEnum {
static const Action ACTION_UNKNOWN =
Action._(0, _omitEnumNames ? '' : 'ACTION_UNKNOWN');
static const Action TOUCH = Action._(1, _omitEnumNames ? '' : 'TOUCH');
static const Action SWIPE = Action._(2, _omitEnumNames ? '' : 'SWIPE');
static const Action KEY = Action._(3, _omitEnumNames ? '' : 'KEY');
static const Action LONG_PRESS =
Action._(4, _omitEnumNames ? '' : 'LONG_PRESS');
static const Action MOTION_EVENT =
Action._(5, _omitEnumNames ? '' : 'MOTION_EVENT');
static const $core.List<Action> values = <Action>[
ACTION_UNKNOWN,
TOUCH,
SWIPE,
KEY,
LONG_PRESS,
MOTION_EVENT,
];
static final $core.List<Action?> _byValue =
$pb.ProtobufEnum.$_initByValueList(values, 5);
static Action? valueOf($core.int value) =>
value < 0 || value >= _byValue.length ? null : _byValue[value];
const Action._(super.value, super.name);
}
const $core.bool _omitEnumNames =
$core.bool.fromEnvironment('protobuf.omit_enum_names');

View File

@@ -0,0 +1,68 @@
// This is a generated file - do not edit.
//
// Generated from control_message.proto.
// @dart = 3.3
// ignore_for_file: annotate_overrides, camel_case_types, comment_references
// ignore_for_file: constant_identifier_names
// ignore_for_file: curly_braces_in_flow_control_structures
// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes
// ignore_for_file: non_constant_identifier_names, prefer_relative_imports
// ignore_for_file: unused_import
import 'dart:convert' as $convert;
import 'dart:core' as $core;
import 'dart:typed_data' as $typed_data;
@$core.Deprecated('Use actionDescriptor instead')
const Action$json = {
'1': 'Action',
'2': [
{'1': 'ACTION_UNKNOWN', '2': 0},
{'1': 'TOUCH', '2': 1},
{'1': 'SWIPE', '2': 2},
{'1': 'KEY', '2': 3},
{'1': 'LONG_PRESS', '2': 4},
{'1': 'MOTION_EVENT', '2': 5},
],
};
/// Descriptor for `Action`. Decode as a `google.protobuf.EnumDescriptorProto`.
final $typed_data.Uint8List actionDescriptor = $convert.base64Decode(
'CgZBY3Rpb24SEgoOQUNUSU9OX1VOS05PV04QABIJCgVUT1VDSBABEgkKBVNXSVBFEAISBwoDS0'
'VZEAMSDgoKTE9OR19QUkVTUxAEEhAKDE1PVElPTl9FVkVOVBAF');
@$core.Deprecated('Use controlMessageDescriptor instead')
const ControlMessage$json = {
'1': 'ControlMessage',
'2': [
{
'1': 'action',
'3': 1,
'4': 1,
'5': 14,
'6': '.com.ttstd.control.Action',
'10': 'action'
},
{'1': 'x', '3': 2, '4': 1, '5': 1, '10': 'x'},
{'1': 'y', '3': 3, '4': 1, '5': 1, '10': 'y'},
{'1': 'x1', '3': 4, '4': 1, '5': 1, '10': 'x1'},
{'1': 'y1', '3': 5, '4': 1, '5': 1, '10': 'y1'},
{'1': 'x2', '3': 6, '4': 1, '5': 1, '10': 'x2'},
{'1': 'y2', '3': 7, '4': 1, '5': 1, '10': 'y2'},
{'1': 'duration', '3': 8, '4': 1, '5': 3, '10': 'duration'},
{'1': 'key_code', '3': 9, '4': 1, '5': 5, '10': 'keyCode'},
{'1': 'key_action', '3': 10, '4': 1, '5': 5, '10': 'keyAction'},
{'1': 'motion_action', '3': 11, '4': 1, '5': 5, '10': 'motionAction'},
],
};
/// Descriptor for `ControlMessage`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List controlMessageDescriptor = $convert.base64Decode(
'Cg5Db250cm9sTWVzc2FnZRIxCgZhY3Rpb24YASABKA4yGS5jb20udHRzdGQuY29udHJvbC5BY3'
'Rpb25SBmFjdGlvbhIMCgF4GAIgASgBUgF4EgwKAXkYAyABKAFSAXkSDgoCeDEYBCABKAFSAngx'
'Eg4KAnkxGAUgASgBUgJ5MRIOCgJ4MhgGIAEoAVICeDISDgoCeTIYByABKAFSAnkyEhoKCGR1cm'
'F0aW9uGAggASgDUghkdXJhdGlvbhIZCghrZXlfY29kZRgJIAEoBVIHa2V5Q29kZRIdCgprZXlf'
'YWN0aW9uGAogASgFUglrZXlBY3Rpb24SIwoNbW90aW9uX2FjdGlvbhgLIAEoBVIMbW90aW9uQW'
'N0aW9u');

View File

@@ -1,57 +1,59 @@
import 'dart:convert';
import 'package:fixnum/fixnum.dart';
import 'package:webrtc_controller_flutter/proto/control_message.pb.dart';
/// 控制指令构造工具,对应 Android 端 RemoteTouchView 的指令格式。
///
/// 指令通过 DataChannel 以 JSON 字符串发送被控端WebRTCControlled
/// 的 InputCommandHandler 负责解析执行。
/// 指令通过 DataChannel 以 protobuf 二进制([ControlMessage])发送,
/// 被控端WebRTCControlled的 InputCommandHandler 负责解析执行。
class ControlCommands {
ControlCommands._();
/// 单击指令。坐标 x/y 为相对于屏幕的百分比0.0 ~ 1.0)。
static String touch(double x, double y) => jsonEncode({
'action': 'TOUCH',
'x': x,
'y': y,
});
static ControlMessage touch(double x, double y) => ControlMessage(
action: Action.TOUCH,
x: x,
y: y,
);
/// 滑动指令。坐标为相对百分比duration 为毫秒。
static String swipe(
static ControlMessage swipe(
double x1,
double y1,
double x2,
double y2,
int durationMs,
) =>
jsonEncode({
'action': 'SWIPE',
'x1': x1,
'y1': y1,
'x2': x2,
'y2': y2,
'duration': durationMs,
});
ControlMessage(
action: Action.SWIPE,
x1: x1,
y1: y1,
x2: x2,
y2: y2,
duration: Int64(durationMs),
);
/// 按键指令。action: 0=按下, 1=抬起(对应 Android KeyEvent.ACTION_DOWN/UP
static String key(int keyCode, int action) => jsonEncode({
'action': 'KEY',
'keyCode': keyCode,
'keyAction': action,
});
static ControlMessage key(int keyCode, int action) => ControlMessage(
action: Action.KEY,
keyCode: keyCode,
keyAction: action,
);
/// 长按指令。坐标 x/y 为相对于屏幕的百分比0.0 ~ 1.0)。
/// 对应 Android 端 RemoteTouchView.createLongPressCommand。
static String longPress(double x, double y) => jsonEncode({
'action': 'LONG_PRESS',
'x': x,
'y': y,
});
static ControlMessage longPress(double x, double y) => ControlMessage(
action: Action.LONG_PRESS,
x: x,
y: y,
);
/// 原始 MotionEvent 指令,用于实现“实时跟手”。
/// action: 0=DOWN, 1=UP, 2=MOVE对应 Android MotionEvent.ACTION_XXX
static String motionEvent(int action, double x, double y) => jsonEncode({
'action': 'MOTION_EVENT',
'motionAction': action,
'x': x,
'y': y,
});
static ControlMessage motionEvent(int action, double x, double y) =>
ControlMessage(
action: Action.MOTION_EVENT,
motionAction: action,
x: x,
y: y,
);
}

View File

@@ -3,6 +3,7 @@ import 'package:flutter_webrtc/flutter_webrtc.dart';
import '../config/ice_servers.dart';
import '../models/signal_message.dart';
import '../proto/control_message.pb.dart';
import '../signaling/signaling_client.dart';
/// 封装 WebRTC 连接逻辑(对应 Android 端 WebRtcClient
@@ -105,8 +106,19 @@ class WebRtcController {
void _setupDataChannel(RTCDataChannel channel) {
channel.onMessage = (RTCDataChannelMessage message) {
// 处理来自被控端的消息(本控制端主要发送,此处仅做日志记录)。
// ignore: avoid_print
print('DataChannel message: ${message.text}');
if (message.isBinary) {
try {
final command = ControlMessage.fromBuffer(message.binary);
// ignore: avoid_print
print('DataChannel message: ${command.action}');
} catch (e) {
// ignore: avoid_print
print('DataChannel message decode failed: $e');
}
} else {
// ignore: avoid_print
print('DataChannel text message: ${message.text}');
}
};
channel.onDataChannelState = (RTCDataChannelState state) {
// ignore: avoid_print
@@ -171,11 +183,11 @@ class WebRtcController {
await _pc?.addCandidate(candidate);
}
/// 通过 DataChannel 发送控制指令(JSON 字符串)。
void sendControlCommand(String commandJson) {
/// 通过 DataChannel 发送控制指令(protobuf 二进制)。
void sendControlCommand(ControlMessage command) {
if (_dataChannel?.state == RTCDataChannelState.RTCDataChannelOpen) {
debugPrint('WebRtcController: Sending command -> $commandJson');
_dataChannel!.send(RTCDataChannelMessage(commandJson));
debugPrint('WebRtcController: Sending command -> ${command.action}');
_dataChannel!.send(RTCDataChannelMessage.fromBinary(command.writeToBuffer()));
}
}

View File

@@ -0,0 +1,41 @@
syntax = "proto3";
package com.ttstd.control;
option java_package = "com.ttstd.control";
option java_multiple_files = true;
// 控制指令类型,对应原 JSON 字段 action。
enum Action {
ACTION_UNKNOWN = 0;
TOUCH = 1;
SWIPE = 2;
KEY = 3;
LONG_PRESS = 4;
MOTION_EVENT = 5;
}
// 通过 RTCDataChannel 传输的控制指令protobuf 二进制)。
// 坐标 x/y/x1/y1/x2/y2 均为相对屏幕的百分比,取值范围 0.0 ~ 1.0。
message ControlMessage {
// 指令类型
Action action = 1;
// 单点坐标TOUCH / LONG_PRESS / MOTION_EVENT
double x = 2;
double y = 3;
// 滑动起止坐标SWIPE
double x1 = 4;
double y1 = 5;
double x2 = 6;
double y2 = 7;
int64 duration = 8;
// 按键KEYkey_action 0=按下 1=抬起(对应 Android KeyEvent ACTION_DOWN/UP
int32 key_code = 9;
int32 key_action = 10;
// 原始动作MOTION_EVENT对应 Android MotionEvent ACTION_*0=DOWN 1=UP 2=MOVE
int32 motion_action = 11;
}

View File

@@ -98,7 +98,7 @@ packages:
source: hosted
version: "2.2.0"
fixnum:
dependency: transitive
dependency: "direct main"
description:
name: fixnum
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
@@ -323,6 +323,14 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.8"
protobuf:
dependency: "direct main"
description:
name: protobuf
sha256: "75ec242d22e950bdcc79ee38dd520ce4ee0bc491d7fadc4ea47694604d22bf06"
url: "https://pub.flutter-io.cn"
source: hosted
version: "6.0.0"
pub_semver:
dependency: transitive
description:

View File

@@ -43,6 +43,8 @@ dependencies:
# 设备 ID 生成
uuid: ^4.4.0
protobuf: ^6.0.0
fixnum: ^1.1.1
dev_dependencies:
flutter_test: