diff --git a/OAT.xml b/OAT.xml
new file mode 100644
index 0000000000000000000000000000000000000000..035d39316699526415d69c03fdcd39ec79b1776c
--- /dev/null
+++ b/OAT.xml
@@ -0,0 +1,105 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/example/lib/main.dart b/example/lib/main.dart
index 9b24aef841ec536e4f1bca2174f163199533862e..1583087d0922a7068a2c339bcd2595193871809a 100644
--- a/example/lib/main.dart
+++ b/example/lib/main.dart
@@ -8,6 +8,7 @@ import 'package:flutter_webrtc/flutter_webrtc.dart';
import 'package:flutter_webrtc_example/src/capture_frame_sample.dart';
import 'src/device_enumeration_sample.dart';
+import 'src/device_enumeration_sample_remote.dart';
import 'src/get_display_media_sample.dart';
import 'src/get_user_media_sample.dart'
if (dart.library.html) 'src/get_user_media_sample_web.dart';
@@ -83,14 +84,6 @@ class _MyAppState extends State {
void _initItems() {
items = [
- RouteItem(
- title: 'GetUserMedia',
- push: (BuildContext context) {
- Navigator.push(
- context,
- MaterialPageRoute(
- builder: (BuildContext context) => GetUserMediaSample()));
- }),
RouteItem(
title: 'Device Enumeration',
push: (BuildContext context) {
@@ -101,40 +94,59 @@ class _MyAppState extends State {
DeviceEnumerationSample()));
}),
RouteItem(
- title: 'GetDisplayMedia',
- push: (BuildContext context) {
- Navigator.push(
- context,
- MaterialPageRoute(
- builder: (BuildContext context) =>
- GetDisplayMediaSample()));
- }),
- RouteItem(
- title: 'LoopBack Sample (Unified Tracks)',
+ title: 'Device Enumeration remote',
push: (BuildContext context) {
Navigator.push(
context,
MaterialPageRoute(
builder: (BuildContext context) =>
- LoopBackSampleUnifiedTracks()));
- }),
- RouteItem(
- title: 'DataChannelLoopBackSample',
- push: (BuildContext context) {
- Navigator.push(
- context,
- MaterialPageRoute(
- builder: (BuildContext context) =>
- DataChannelLoopBackSample()));
- }),
- RouteItem(
- title: 'Capture Frame',
- push: (BuildContext context) {
- Navigator.push(
- context,
- MaterialPageRoute(
- builder: (BuildContext context) => CaptureFrameSample()));
+ DeviceEnumerationSampleRemote()));
}),
+ if (!WebRTC.platformIsOhos) ...[
+ RouteItem(
+ title: 'GetUserMedia',
+ push: (BuildContext context) {
+ Navigator.push(
+ context,
+ MaterialPageRoute(
+ builder: (BuildContext context) => GetUserMediaSample()));
+ }),
+ RouteItem(
+ title: 'GetDisplayMedia',
+ push: (BuildContext context) {
+ Navigator.push(
+ context,
+ MaterialPageRoute(
+ builder: (BuildContext context) =>
+ GetDisplayMediaSample()));
+ }),
+ RouteItem(
+ title: 'LoopBack Sample (Unified Tracks)',
+ push: (BuildContext context) {
+ Navigator.push(
+ context,
+ MaterialPageRoute(
+ builder: (BuildContext context) =>
+ LoopBackSampleUnifiedTracks()));
+ }),
+ RouteItem(
+ title: 'DataChannelLoopBackSample',
+ push: (BuildContext context) {
+ Navigator.push(
+ context,
+ MaterialPageRoute(
+ builder: (BuildContext context) =>
+ DataChannelLoopBackSample()));
+ }),
+ RouteItem(
+ title: 'Capture Frame',
+ push: (BuildContext context) {
+ Navigator.push(
+ context,
+ MaterialPageRoute(
+ builder: (BuildContext context) => CaptureFrameSample()));
+ }),
+ ]
];
}
}
diff --git a/example/lib/src/device_enumeration_sample.dart b/example/lib/src/device_enumeration_sample.dart
index 4630001572cbc5ead80e0433cae5cd9512bfce5f..a9e3708ccf1b04d98a211844e7178fbe04a0f6c4 100644
--- a/example/lib/src/device_enumeration_sample.dart
+++ b/example/lib/src/device_enumeration_sample.dart
@@ -354,58 +354,58 @@ class _DeviceEnumerationSampleState extends State {
_speakerphoneOn ? Icons.speaker_phone : Icons.phone_android),
tooltip: 'Switch SpeakerPhone',
),
- PopupMenuButton(
- onSelected: _selectVideoInput,
- icon: Icon(Icons.switch_camera),
- itemBuilder: (BuildContext context) {
- return _devices
- .where((device) => device.kind == 'videoinput')
- .map((device) {
- return PopupMenuItem(
- value: device.deviceId,
- child: Text(device.label),
- );
- }).toList();
- },
- ),
- PopupMenuButton(
- onSelected: _selectVideoFps,
- icon: Icon(Icons.menu),
- itemBuilder: (BuildContext context) {
- return [
- PopupMenuItem(
- value: _selectedVideoFPS,
- child: Text('Select FPS ($_selectedVideoFPS)'),
- ),
- PopupMenuDivider(),
- ...['8', '15', '30', '60']
- .map((fps) => PopupMenuItem(
- value: fps,
- child: Text(fps),
- ))
- .toList()
- ];
- },
- ),
- PopupMenuButton(
- onSelected: _selectVideoSize,
- icon: Icon(Icons.screenshot_monitor),
- itemBuilder: (BuildContext context) {
- return [
- PopupMenuItem(
- value: _selectedVideoSize.toString(),
- child: Text('Select Video Size ($_selectedVideoSize)'),
- ),
- PopupMenuDivider(),
- ...['320x180', '640x360', '1280x720', '1920x1080']
- .map((fps) => PopupMenuItem(
- value: fps,
- child: Text(fps),
- ))
- .toList()
- ];
- },
- ),
+ // PopupMenuButton(
+ // onSelected: _selectVideoInput,
+ // icon: Icon(Icons.switch_camera),
+ // itemBuilder: (BuildContext context) {
+ // return _devices
+ // .where((device) => device.kind == 'videoinput')
+ // .map((device) {
+ // return PopupMenuItem(
+ // value: device.deviceId,
+ // child: Text(device.label),
+ // );
+ // }).toList();
+ // },
+ // ),
+ // PopupMenuButton(
+ // onSelected: _selectVideoFps,
+ // icon: Icon(Icons.menu),
+ // itemBuilder: (BuildContext context) {
+ // return [
+ // PopupMenuItem(
+ // value: _selectedVideoFPS,
+ // child: Text('Select FPS ($_selectedVideoFPS)'),
+ // ),
+ // PopupMenuDivider(),
+ // ...['8', '15', '30', '60']
+ // .map((fps) => PopupMenuItem(
+ // value: fps,
+ // child: Text(fps),
+ // ))
+ // .toList()
+ // ];
+ // },
+ // ),
+ // PopupMenuButton(
+ // onSelected: _selectVideoSize,
+ // icon: Icon(Icons.screenshot_monitor),
+ // itemBuilder: (BuildContext context) {
+ // return [
+ // PopupMenuItem(
+ // value: _selectedVideoSize.toString(),
+ // child: Text('Select Video Size ($_selectedVideoSize)'),
+ // ),
+ // PopupMenuDivider(),
+ // ...['320x180', '640x360', '1280x720', '1920x1080']
+ // .map((fps) => PopupMenuItem(
+ // value: fps,
+ // child: Text(fps),
+ // ))
+ // .toList()
+ // ];
+ // },
+ // ),
],
),
body: OrientationBuilder(
@@ -420,14 +420,20 @@ class _DeviceEnumerationSampleState extends State {
child: Container(
margin: const EdgeInsets.fromLTRB(0, 0, 0, 0),
decoration: BoxDecoration(color: Colors.black54),
- child: RTCVideoView(_localRenderer),
+ child: RTCVideoView(_localRenderer,
+ onRendererUpdated: (data) {
+ _localRenderer.surfaceId = data;
+ }),
),
),
Expanded(
child: Container(
margin: const EdgeInsets.fromLTRB(0, 0, 0, 0),
decoration: BoxDecoration(color: Colors.black54),
- child: RTCVideoView(_remoteRenderer),
+ child: RTCVideoView(_remoteRenderer,
+ onRendererUpdated: (data) {
+ _remoteRenderer.surfaceId = data;
+ }),
),
),
],
diff --git a/example/lib/src/device_enumeration_sample_remote.dart b/example/lib/src/device_enumeration_sample_remote.dart
new file mode 100644
index 0000000000000000000000000000000000000000..98859962d03e9441189c86d94f284c9777e8bf95
--- /dev/null
+++ b/example/lib/src/device_enumeration_sample_remote.dart
@@ -0,0 +1,589 @@
+import 'dart:convert';
+import 'dart:core';
+import 'dart:math';
+import 'package:collection/collection.dart';
+
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_webrtc/flutter_webrtc.dart';
+import 'package:permission_handler/permission_handler.dart';
+import 'package:web_socket_channel/web_socket_channel.dart';
+
+class VideoSize {
+ VideoSize(this.width, this.height);
+
+ factory VideoSize.fromString(String size) {
+ final parts = size.split('x');
+ return VideoSize(int.parse(parts[0]), int.parse(parts[1]));
+ }
+
+ final int width;
+ final int height;
+
+ @override
+ String toString() {
+ return '$width x $height';
+ }
+}
+
+/*
+ * DeviceEnumerationSampleRemote
+ */
+class DeviceEnumerationSampleRemote extends StatefulWidget {
+ static String tag = 'DeviceEnumerationSampleRemote';
+
+ @override
+ _DeviceEnumerationSampleRemoteState createState() =>
+ _DeviceEnumerationSampleRemoteState();
+}
+
+class _DeviceEnumerationSampleRemoteState
+ extends State {
+ MediaStream? _localStream;
+ final RTCVideoRenderer _localRenderer = RTCVideoRenderer();
+ final RTCVideoRenderer _remoteRenderer = RTCVideoRenderer();
+ bool _inCalling = false;
+
+ List _devices = [];
+
+ List get audioInputs =>
+ _devices.where((device) => device.kind == 'audioinput').toList();
+
+ List get audioOutputs =>
+ _devices.where((device) => device.kind == 'audiooutput').toList();
+
+ List get videoInputs =>
+ _devices.where((device) => device.kind == 'videoinput').toList();
+
+ String? _selectedVideoInputId;
+ String? _selectedAudioInputId;
+
+ MediaDeviceInfo get selectedAudioInput => audioInputs.firstWhere(
+ (device) => device.deviceId == _selectedVideoInputId,
+ orElse: () => audioInputs.first);
+
+ String? _selectedVideoFPS = '30';
+
+ String localId = '-1';
+
+ VideoSize _selectedVideoSize = VideoSize(1280, 720);
+ final TextEditingController _textController = TextEditingController();
+
+ late WebSocketChannel _channel;
+
+ @override
+ void initState() {
+ super.initState();
+ //初始化视频渲染器
+ initRenderers();
+ //扫描本地外设
+ loadDevices();
+ navigator.mediaDevices.ondevicechange = (event) {
+ loadDevices();
+ };
+
+ print('创建信令服务器链接');
+ // 初始化信令服务器连接
+ _channel = WebSocketChannel.connect(
+ Uri.parse('wss://youzhi.life:8443'),
+ );
+
+ // 监听信令消息
+ _channel.stream.listen((message) {
+ //收到服务器的消息
+ print('Connected and received message: $message');
+ if (message == 'SESSION_OK') {
+ _handleOffer();
+ } else if (message == 'OFFER_REQUEST') {
+ _handleOffer();
+ } else if (message == 'HELLO') {
+ _start();
+ } else {
+ Map msg = json.decode(message);
+ if (msg.containsKey('sdp')) {
+ receiveSdp(msg['sdp']);
+ } else if (msg.containsKey('ice')) {
+ receiveIce(msg['ice']);
+ }
+ }
+ }, onDone: () {
+ // 连接被关闭
+ print('Connection closed');
+ }, onError: (error) {
+ // 连接失败或出错
+ print('Connection error: $error');
+ });
+ }
+
+ void receiveSdp(dynamic sdp) async {
+ print('receiveSdp in');
+ print('receiveSdp in : ' + sdp['type']);
+ if (sdp['type'] == 'offer') {
+ await pc?.setRemoteDescription(
+ RTCSessionDescription(sdp['sdp'], sdp['type']));
+ await _handleAnswer();
+ } else if (sdp['type'] == 'answer') {
+ await pc?.setRemoteDescription(
+ RTCSessionDescription(sdp['sdp'], sdp['type']));
+ }
+ }
+
+ void receiveIce(dynamic ice) async {
+ print('receiveIce in');
+ await pc?.addCandidate(
+ RTCIceCandidate(ice['candidate'], ice['sdpMid'], ice['sdpMLineIndex']));
+ }
+
+ String generateNumericId() {
+ // 随机生成一个 2 到 4 位的数字
+ var min = pow(10, 1 + Random().nextInt(3)).toInt();
+ var max = min * 10 - 1;
+ var numericId = min + Random().nextInt(max - min + 1);
+ return numericId.toString();
+ }
+
+ @override
+ void deactivate() {
+ super.deactivate();
+ _stop();
+ _localRenderer.dispose();
+ _remoteRenderer.dispose();
+ navigator.mediaDevices.ondevicechange = null;
+ }
+
+ RTCPeerConnection? pc;
+ var senders = [];
+
+ Future initPCs() async {
+ // 初始化 WebRTC 配置
+ final configuration = {
+ 'iceServers': [
+ {
+ 'urls': 'stun:stun.l.google.com:19302',
+ },
+ ]
+ };
+ // 创建 PeerConnection
+ pc ??= await createPeerConnection(configuration);
+ // pc ??= await createPeerConnection({});
+
+ pc?.onConnectionState = (state) {
+ print('connectionState : $state');
+ };
+
+ pc!.onIceCandidate = (candidate) {
+ print('onIceCandidate $candidate');
+ if (candidate != null) {
+ _channel.sink.add(json.encode({'ice': candidate.toMap()}));
+ }
+ };
+
+ pc!.onTrack = (event) {
+ print('onTrack $event');
+ print('onTrack : ' + event.track.toString());
+ print('onTrack : ' + event.streams.length.toString());
+ if (event.track.kind == 'video') {
+ _remoteRenderer.srcObject = event.streams[0];
+ }
+ };
+ }
+
+ Future _handleOffer() async {
+ print('_handleOffer in');
+ final offer = await pc!.createOffer();
+ await pc!.setLocalDescription(offer);
+ print('_handleOffer in : ' + json.encode({'sdp': offer.toMap()}));
+ _channel.sink.add(json.encode({'sdp': offer.toMap()}));
+ }
+
+ Future _handleAnswer() async {
+ final answer = await pc!.createAnswer();
+ await pc!.setLocalDescription(answer);
+ var session = await pc!.getLocalDescription();
+ print('_handleAnswer in : ' + session!.type! ?? '');
+ _channel.sink.add(json.encode({'sdp': session.toMap()}));
+ }
+
+ Future _handleCandidate(Map data) async {
+ final candidate = RTCIceCandidate(
+ data['candidate'],
+ data['sdpMid'],
+ data['sdpMLineIndex'],
+ );
+ await pc!.addCandidate(candidate);
+ }
+
+ Future stopPCs() async {
+ await pc?.close();
+ pc = null;
+ }
+
+ Future loadDevices() async {
+ print('loadDevices');
+ if (WebRTC.platformIsAndroid || WebRTC.platformIsIOS) {
+ //Ask for runtime permissions if necessary.
+ var status = await Permission.bluetooth.request();
+ if (status.isPermanentlyDenied) {
+ print('BLEpermdisabled');
+ }
+
+ status = await Permission.bluetoothConnect.request();
+ if (status.isPermanentlyDenied) {
+ print('ConnectPermdisabled');
+ }
+ }
+ print('loadDevices 2');
+ final devices = await navigator.mediaDevices.enumerateDevices();
+ setState(() {
+ _devices = devices;
+ });
+ }
+
+ Future _selectVideoFps(String fps) async {
+ _selectedVideoFPS = fps;
+ if (!_inCalling) {
+ return;
+ }
+ await _selectVideoInput(_selectedVideoInputId);
+ setState(() {});
+ }
+
+ Future _selectVideoSize(String size) async {
+ _selectedVideoSize = VideoSize.fromString(size);
+ if (!_inCalling) {
+ return;
+ }
+ await _selectVideoInput(_selectedVideoInputId);
+ setState(() {});
+ }
+
+ Future _selectAudioInput(String? deviceId) async {
+ _selectedAudioInputId = deviceId;
+ if (!_inCalling) {
+ return;
+ }
+
+ var newLocalStream = await navigator.mediaDevices.getUserMedia({
+ 'audio': {
+ if (_selectedAudioInputId != null && kIsWeb)
+ 'deviceId': _selectedAudioInputId,
+ if (_selectedAudioInputId != null && !kIsWeb)
+ 'optional': [
+ {'sourceId': _selectedAudioInputId}
+ ],
+ },
+ 'video': false,
+ });
+
+ // replace track.
+ var newTrack = newLocalStream.getAudioTracks().first;
+ print('track.settings ' + newTrack.getSettings().toString());
+ var sender =
+ senders.firstWhereOrNull((sender) => sender.track?.kind == 'audio');
+ await sender?.replaceTrack(newTrack);
+ }
+
+ Future _selectAudioOutput(String? deviceId) async {
+ if (!_inCalling) {
+ return;
+ }
+ await _localRenderer.audioOutput(deviceId!);
+ }
+
+ var _speakerphoneOn = false;
+
+ Future _setSpeakerphoneOn() async {
+ _speakerphoneOn = !_speakerphoneOn;
+ await Helper.setSpeakerphoneOn(_speakerphoneOn);
+ setState(() {});
+ }
+
+ Future _selectVideoInput(String? deviceId) async {
+ print('_selectVideoInput : ' + (deviceId ?? ''));
+ print('_selectVideoInput : ' + _inCalling.toString());
+ _selectedVideoInputId = deviceId;
+ if (!_inCalling) {
+ return;
+ }
+ // 2) replace track.
+ // stop old track.
+ _localRenderer.srcObject = null;
+
+ _localStream?.getTracks().forEach((track) async {
+ print('track.id : ' +
+ track.id.toString() +
+ ' , kind : ' +
+ (track.kind ?? ''));
+ await track.stop();
+ });
+ await _localStream?.dispose();
+
+ var newLocalStream = await navigator.mediaDevices.getUserMedia({
+ 'audio': true,
+ 'video': {
+ if (_selectedVideoInputId != null && kIsWeb)
+ 'deviceId': _selectedVideoInputId,
+ if (_selectedVideoInputId != null && !kIsWeb)
+ 'optional': [
+ {'sourceId': _selectedVideoInputId}
+ ],
+ 'width': _selectedVideoSize.width,
+ 'height': _selectedVideoSize.height,
+ 'frameRate': _selectedVideoFPS,
+ },
+ });
+ print('track._selectVideoInput : ownerTag :' + newLocalStream.ownerTag ??
+ '0');
+
+ _localStream = newLocalStream;
+ _localRenderer.srcObject = _localStream;
+ // replace track.
+ var newTrack = _localStream?.getVideoTracks().first;
+ print('track.settings ' + newTrack!.id.toString());
+ var sender =
+ senders.firstWhereOrNull((sender) => sender.track?.kind == 'video');
+ var params = sender!.parameters;
+ print('params degradationPreference' +
+ params.degradationPreference.toString());
+ params.degradationPreference = RTCDegradationPreference.MAINTAIN_RESOLUTION;
+ print('track.setParameters : 111');
+ await sender.setParameters(params);
+ print('track.setParameters : 222');
+ await sender.replaceTrack(newTrack);
+ print('track.setParameters : 333');
+ }
+
+ Future initRenderers() async {
+ await _localRenderer.initialize();
+ await _remoteRenderer.initialize();
+ }
+
+ Future _sendId() async {
+ // 向信令服务器发送 HELLO 消息
+ localId = generateNumericId();
+ print('生成的id为: ' + localId);
+ _channel.sink.add('HELLO ' + localId);
+ print('Sent: HELLO ' + localId);
+ }
+
+ Future _start() async {
+ try {
+ _localStream = await navigator.mediaDevices.getUserMedia({
+ 'audio': true,
+ 'video': {
+ if (_selectedVideoInputId != null && kIsWeb)
+ 'deviceId': _selectedVideoInputId,
+ if (_selectedVideoInputId != null && !kIsWeb)
+ 'optional': [
+ {'sourceId': _selectedVideoInputId}
+ ],
+ 'width': _selectedVideoSize.width,
+ 'height': _selectedVideoSize.height,
+ 'frameRate': _selectedVideoFPS,
+ 'facingMode': 'user',
+ },
+ });
+ _localRenderer.srcObject = _localStream;
+ _inCalling = true;
+
+ await initPCs();
+
+ _localStream?.getTracks().forEach((track) async {
+ var rtpSender = await pc?.addTrack(track, _localStream!);
+ print('track.settings ' + track.getSettings().toString());
+ senders.add(rtpSender!);
+ });
+
+ setState(() {});
+ } catch (e) {
+ print(e.toString());
+ }
+ }
+
+ Future _stop() async {
+ try {
+ _localStream?.getTracks().forEach((track) async {
+ await track.stop();
+ });
+ await _localStream?.dispose();
+ _localStream = null;
+ _localRenderer.srcObject = null;
+ _remoteRenderer.srcObject = null;
+ senders.clear();
+ _inCalling = false;
+ await stopPCs();
+ _speakerphoneOn = false;
+ await Helper.setSpeakerphoneOn(_speakerphoneOn);
+ setState(() {});
+ } catch (e) {
+ print(e.toString());
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(
+ title: Text('DeviceEnumerationSample remote'),
+ actions: [
+ PopupMenuButton(
+ onSelected: _selectAudioInput,
+ icon: Icon(Icons.settings_voice),
+ itemBuilder: (BuildContext context) {
+ return _devices
+ .where((device) => device.kind == 'audioinput')
+ .map((device) {
+ return PopupMenuItem(
+ value: device.deviceId,
+ child: Text(device.label),
+ );
+ }).toList();
+ },
+ ),
+ if (!WebRTC.platformIsMobile)
+ PopupMenuButton(
+ onSelected: _selectAudioOutput,
+ icon: Icon(Icons.volume_down_alt),
+ itemBuilder: (BuildContext context) {
+ return _devices
+ .where((device) => device.kind == 'audiooutput')
+ .map((device) {
+ return PopupMenuItem(
+ value: device.deviceId,
+ child: Text(device.label),
+ );
+ }).toList();
+ },
+ ),
+ if (!kIsWeb && WebRTC.platformIsMobile)
+ IconButton(
+ disabledColor: Colors.grey,
+ onPressed: _setSpeakerphoneOn,
+ icon: Icon(_speakerphoneOn
+ ? Icons.speaker_phone
+ : Icons.phone_android),
+ tooltip: 'Switch SpeakerPhone',
+ ),
+ // PopupMenuButton(
+ // onSelected: _selectVideoInput,
+ // icon: Icon(Icons.switch_camera),
+ // itemBuilder: (BuildContext context) {
+ // return _devices
+ // .where((device) => device.kind == 'videoinput')
+ // .map((device) {
+ // return PopupMenuItem(
+ // value: device.deviceId,
+ // child: Text(device.label),
+ // );
+ // }).toList();
+ // },
+ // ),
+ // PopupMenuButton(
+ // onSelected: _selectVideoFps,
+ // icon: Icon(Icons.menu),
+ // itemBuilder: (BuildContext context) {
+ // return [
+ // PopupMenuItem(
+ // value: _selectedVideoFPS,
+ // child: Text('Select FPS ($_selectedVideoFPS)'),
+ // ),
+ // PopupMenuDivider(),
+ // ...['8', '15', '30', '60']
+ // .map((fps) => PopupMenuItem(
+ // value: fps,
+ // child: Text(fps),
+ // ))
+ // .toList()
+ // ];
+ // },
+ // ),
+ // PopupMenuButton(
+ // onSelected: _selectVideoSize,
+ // icon: Icon(Icons.screenshot_monitor),
+ // itemBuilder: (BuildContext context) {
+ // return [
+ // PopupMenuItem(
+ // value: _selectedVideoSize.toString(),
+ // child: Text('Select Video Size ($_selectedVideoSize)'),
+ // ),
+ // PopupMenuDivider(),
+ // ...['320x180', '640x360', '1280x720', '1920x1080']
+ // .map((fps) => PopupMenuItem(
+ // value: fps,
+ // child: Text(fps),
+ // ))
+ // .toList()
+ // ];
+ // },
+ // ),
+ ],
+ ),
+ body: OrientationBuilder(
+ builder: (context, orientation) {
+ return Center(
+ child: Container(
+ width: MediaQuery.of(context).size.width,
+ color: Colors.white10,
+ child: Row(
+ children: [
+ Expanded(
+ child: Container(
+ margin: const EdgeInsets.fromLTRB(0, 0, 0, 0),
+ decoration: BoxDecoration(color: Colors.black54),
+ child: RTCVideoView(_localRenderer,
+ onRendererUpdated: (data) {
+ _localRenderer.surfaceId = data;
+ }),
+ ),
+ ),
+ Expanded(
+ child: Container(
+ margin: const EdgeInsets.fromLTRB(0, 0, 0, 0),
+ decoration: BoxDecoration(color: Colors.black54),
+ child: RTCVideoView(_remoteRenderer,
+ onRendererUpdated: (data) {
+ _remoteRenderer.surfaceId = data;
+ }),
+ ),
+ ),
+ ],
+ )),
+ );
+ },
+ ),
+ floatingActionButton: Row(
+ mainAxisAlignment: MainAxisAlignment.end,
+ children: [
+ Text(
+ '本机id: ' + localId,
+ style: TextStyle(color: Colors.black, fontSize: 14),
+ ),
+ SizedBox(width: 8),
+ Container(
+ width: 105,
+ height: 30,
+ child: TextField(
+ controller: _textController,
+ decoration: InputDecoration(
+ border: OutlineInputBorder(), labelText: '输入对方id'))),
+ SizedBox(width: 8),
+ // 按钮
+ ElevatedButton(
+ onPressed: () {
+ _channel.sink.add('SESSION ' + _textController.text);
+ },
+ style: ElevatedButton.styleFrom(fixedSize: Size(88, 30)),
+ child: Text('加入房间'),
+ ),
+ SizedBox(width: 16),
+ FloatingActionButton(
+ onPressed: () {
+ _inCalling ? _stop() : _sendId();
+ },
+ tooltip: _inCalling ? 'Hangup' : 'Call',
+ child: Icon(_inCalling ? Icons.call_end : Icons.phone),
+ ),
+ ],
+ ));
+ }
+}
diff --git a/example/lib/src/get_display_media_sample.dart b/example/lib/src/get_display_media_sample.dart
index 5fe193d08bc699b909792c66bc6383c287aa431d..d8b1a7a1b317448d7a7e27de8f52ea2b922027e7 100644
--- a/example/lib/src/get_display_media_sample.dart
+++ b/example/lib/src/get_display_media_sample.dart
@@ -155,14 +155,16 @@ class _GetDisplayMediaSampleState extends State {
width: MediaQuery.of(context).size.width,
color: Colors.white10,
child: Stack(children: [
- if (_inCalling)
- Container(
- margin: EdgeInsets.fromLTRB(0.0, 0.0, 0.0, 0.0),
- width: MediaQuery.of(context).size.width,
- height: MediaQuery.of(context).size.height,
- decoration: BoxDecoration(color: Colors.black54),
- child: RTCVideoView(_localRenderer),
- )
+ // if (_inCalling)
+ Container(
+ margin: EdgeInsets.fromLTRB(0.0, 0.0, 0.0, 0.0),
+ width: MediaQuery.of(context).size.width,
+ height: MediaQuery.of(context).size.height,
+ decoration: BoxDecoration(color: Colors.black54),
+ child: RTCVideoView(_localRenderer, onRendererUpdated: (data) {
+ _localRenderer.surfaceId = data;
+ }),
+ )
]),
));
},
diff --git a/example/lib/src/get_user_media_sample.dart b/example/lib/src/get_user_media_sample.dart
index d9c427a0de329373649553867ea130eb8ce65c73..18583842f88ed85d463d7aa0404f50b6660ef581 100644
--- a/example/lib/src/get_user_media_sample.dart
+++ b/example/lib/src/get_user_media_sample.dart
@@ -243,7 +243,10 @@ class _GetUserMediaSampleState extends State {
setZoom(details.scale);
}
},
- child: RTCVideoView(_localRenderer, mirror: true),
+ child: RTCVideoView(_localRenderer, mirror: true,
+ onRendererUpdated: (data) {
+ _localRenderer.surfaceId = data;
+ }),
),
));
},
diff --git a/example/lib/src/loopback_sample_unified_tracks.dart b/example/lib/src/loopback_sample_unified_tracks.dart
index e5da4834ba8bf85d9f0c1b30cae9df4ac74fc426..8d37df7c0829a990069d073871b5197a9fd6290b 100644
--- a/example/lib/src/loopback_sample_unified_tracks.dart
+++ b/example/lib/src/loopback_sample_unified_tracks.dart
@@ -259,7 +259,7 @@ class _MyAppState extends State {
_keySharedProvider ??=
await _frameCyrptorFactory.createDefaultKeyProvider(keyProviderOptions);
- await _keySharedProvider?.setSharedKey(key: aesKey);
+ // await _keySharedProvider?.setSharedKey(key: aesKey);
acaps = await getRtpSenderCapabilities('audio');
print('sender audio capabilities: ${acaps!.toMap()}');
@@ -745,7 +745,10 @@ class _MyAppState extends State {
],
),
Expanded(
- child: RTCVideoView(_localRenderer, mirror: true),
+ child: RTCVideoView(_localRenderer, mirror: true,
+ onRendererUpdated: (data) {
+ _localRenderer.surfaceId = data;
+ }),
),
],
)),
@@ -778,7 +781,9 @@ class _MyAppState extends State {
],
),
Expanded(
- child: RTCVideoView(_remoteRenderer),
+ child: RTCVideoView(_remoteRenderer, onRendererUpdated: (data) {
+ _remoteRenderer.surfaceId = data;
+ }),
),
],
)),
diff --git a/example/ohos/.gitignore b/example/ohos/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..e285c291c1fc98ee161b135a0825e0222ac95438
--- /dev/null
+++ b/example/ohos/.gitignore
@@ -0,0 +1,21 @@
+/node_modules
+/oh_modules
+/local.properties
+/.idea
+**/build
+/.hvigor
+.cxx
+/.clangd
+/.clang-format
+/.clang-tidy
+**/.test
+*.har
+**/BuildProfile.ets
+**/oh-package-lock.json5
+
+**/src/main/resources/rawfile/flutter_assets/
+**/libs/arm64-v8a/libapp.so
+**/libs/arm64-v8a/libflutter.so
+**/libs/arm64-v8a/libvmservice_snapshot.so
+
+/har
diff --git a/example/ohos/AppScope/app.json5 b/example/ohos/AppScope/app.json5
new file mode 100644
index 0000000000000000000000000000000000000000..d4905431a1daca1af7ebc24a065044ad82964f7d
--- /dev/null
+++ b/example/ohos/AppScope/app.json5
@@ -0,0 +1,10 @@
+{
+ "app": {
+ "bundleName": "com.example.flutter_webrtc",
+ "vendor": "example",
+ "versionCode": 1000000,
+ "versionName": "1.0.0",
+ "icon": "$media:app_icon",
+ "label": "$string:app_name"
+ }
+}
\ No newline at end of file
diff --git a/example/ohos/AppScope/resources/base/element/string.json b/example/ohos/AppScope/resources/base/element/string.json
new file mode 100644
index 0000000000000000000000000000000000000000..f770e5cd4b83b1ac9ebc1e67e82376185aae293b
--- /dev/null
+++ b/example/ohos/AppScope/resources/base/element/string.json
@@ -0,0 +1,8 @@
+{
+ "string": [
+ {
+ "name": "app_name",
+ "value": "flutter_webrtc"
+ }
+ ]
+}
diff --git a/example/ohos/AppScope/resources/base/media/app_icon.png b/example/ohos/AppScope/resources/base/media/app_icon.png
new file mode 100644
index 0000000000000000000000000000000000000000..ce307a8827bd75456441ceb57d530e4c8d45d36c
Binary files /dev/null and b/example/ohos/AppScope/resources/base/media/app_icon.png differ
diff --git a/example/ohos/build-profile.json5 b/example/ohos/build-profile.json5
new file mode 100644
index 0000000000000000000000000000000000000000..1d12140d202702d7c73d64f1b291fe5c45a660ce
--- /dev/null
+++ b/example/ohos/build-profile.json5
@@ -0,0 +1,27 @@
+{
+ "app": {
+ "signingConfigs": [],
+ "products": [
+ {
+ "name": "default",
+ "signingConfig": "default",
+ "compatibleSdkVersion": "5.0.0(12)",
+ "runtimeOS": "HarmonyOS"
+ }
+ ]
+ },
+ "modules": [
+ {
+ "name": "entry",
+ "srcPath": "./entry",
+ "targets": [
+ {
+ "name": "default",
+ "applyToProducts": [
+ "default"
+ ]
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/example/ohos/entry/.gitignore b/example/ohos/entry/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..2795a1c5b1fe53659dd1b71d90ba0592eaf7e043
--- /dev/null
+++ b/example/ohos/entry/.gitignore
@@ -0,0 +1,7 @@
+
+/node_modules
+/oh_modules
+/.preview
+/build
+/.cxx
+/.test
\ No newline at end of file
diff --git a/example/ohos/entry/build-profile.json5 b/example/ohos/entry/build-profile.json5
new file mode 100644
index 0000000000000000000000000000000000000000..f3c17539d7ce1a28bc932c0754a5c9981a21cd73
--- /dev/null
+++ b/example/ohos/entry/build-profile.json5
@@ -0,0 +1,16 @@
+{
+ "apiType": 'stageMode',
+ "buildOption": {
+ "externalNativeOptions": {
+ "abiFilters": [
+ "arm64-v8a"
+ ]
+ },
+ },
+ "targets": [
+ {
+ "name": "default",
+ "runtimeOS": "HarmonyOS"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/example/ohos/entry/hvigorfile.ts b/example/ohos/entry/hvigorfile.ts
new file mode 100644
index 0000000000000000000000000000000000000000..80e4ec5b81689f238c34614b167a0b9e9c83e8d9
--- /dev/null
+++ b/example/ohos/entry/hvigorfile.ts
@@ -0,0 +1,2 @@
+// Script for compiling build behavior. It is built in the build plug-in and cannot be modified currently.
+export { hapTasks } from '@ohos/hvigor-ohos-plugin';
diff --git a/example/ohos/entry/oh-package.json5 b/example/ohos/entry/oh-package.json5
new file mode 100644
index 0000000000000000000000000000000000000000..f87e260aa04b2675fb0c3f54d784bc9e51f19325
--- /dev/null
+++ b/example/ohos/entry/oh-package.json5
@@ -0,0 +1,13 @@
+{
+ "name": "entry",
+ "version": "1.0.0",
+ "description": "Please describe the basic information.",
+ "main": "",
+ "author": "",
+ "license": "",
+ "dependencies": {
+ "flutter_webrtc": "file:../har/flutter_webrtc.har",
+ "path_provider_ohos": "file:../har/path_provider_ohos.har",
+ "permission_handler_ohos": "file:../har/permission_handler_ohos.har"
+ }
+}
\ No newline at end of file
diff --git a/example/ohos/entry/src/main/ets/entryability/EntryAbility.ets b/example/ohos/entry/src/main/ets/entryability/EntryAbility.ets
new file mode 100644
index 0000000000000000000000000000000000000000..5287fd39197fe0394732777dceec4b02966cc516
--- /dev/null
+++ b/example/ohos/entry/src/main/ets/entryability/EntryAbility.ets
@@ -0,0 +1,9 @@
+import { FlutterAbility, FlutterEngine } from '@ohos/flutter_ohos';
+import { GeneratedPluginRegistrant } from '../plugins/GeneratedPluginRegistrant';
+
+export default class EntryAbility extends FlutterAbility {
+ configureFlutterEngine(flutterEngine: FlutterEngine) {
+ super.configureFlutterEngine(flutterEngine)
+ GeneratedPluginRegistrant.registerWith(flutterEngine)
+ }
+}
diff --git a/example/ohos/entry/src/main/ets/pages/Index.ets b/example/ohos/entry/src/main/ets/pages/Index.ets
new file mode 100644
index 0000000000000000000000000000000000000000..ef6205b904b661723d220a64ba31b805b38515b5
--- /dev/null
+++ b/example/ohos/entry/src/main/ets/pages/Index.ets
@@ -0,0 +1,23 @@
+import common from '@ohos.app.ability.common';
+import { FlutterPage } from '@ohos/flutter_ohos'
+
+let storage = LocalStorage.getShared()
+const EVENT_BACK_PRESS = 'EVENT_BACK_PRESS'
+
+@Entry(storage)
+@Component
+struct Index {
+ private context = getContext(this) as common.UIAbilityContext
+ @LocalStorageLink('viewId') viewId: string = "";
+
+ build() {
+ Column() {
+ FlutterPage({ viewId: this.viewId })
+ }
+ }
+
+ onBackPress(): boolean {
+ this.context.eventHub.emit(EVENT_BACK_PRESS)
+ return true
+ }
+}
\ No newline at end of file
diff --git a/example/ohos/entry/src/main/module.json5 b/example/ohos/entry/src/main/module.json5
new file mode 100644
index 0000000000000000000000000000000000000000..1907287490006721d809d991e51e0adcd0ccabfc
--- /dev/null
+++ b/example/ohos/entry/src/main/module.json5
@@ -0,0 +1,39 @@
+{
+ "module": {
+ "name": "entry",
+ "type": "entry",
+ "description": "$string:module_desc",
+ "mainElement": "EntryAbility",
+ "deviceTypes": [
+ "phone"
+ ],
+ "deliveryWithInstall": true,
+ "installationFree": false,
+ "pages": "$profile:main_pages",
+ "abilities": [
+ {
+ "name": "EntryAbility",
+ "srcEntry": "./ets/entryability/EntryAbility.ets",
+ "description": "$string:EntryAbility_desc",
+ "icon": "$media:icon",
+ "label": "$string:EntryAbility_label",
+ "startWindowIcon": "$media:icon",
+ "startWindowBackground": "$color:start_window_background",
+ "exported": true,
+ "skills": [
+ {
+ "entities": [
+ "entity.system.home"
+ ],
+ "actions": [
+ "action.system.home"
+ ]
+ }
+ ]
+ }
+ ],
+ "requestPermissions": [
+ {"name" : "ohos.permission.INTERNET"},
+ ]
+ }
+}
\ No newline at end of file
diff --git a/example/ohos/entry/src/main/resources/base/element/color.json b/example/ohos/entry/src/main/resources/base/element/color.json
new file mode 100644
index 0000000000000000000000000000000000000000..3c712962da3c2751c2b9ddb53559afcbd2b54a02
--- /dev/null
+++ b/example/ohos/entry/src/main/resources/base/element/color.json
@@ -0,0 +1,8 @@
+{
+ "color": [
+ {
+ "name": "start_window_background",
+ "value": "#FFFFFF"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/example/ohos/entry/src/main/resources/base/element/string.json b/example/ohos/entry/src/main/resources/base/element/string.json
new file mode 100644
index 0000000000000000000000000000000000000000..77ca1a6ed2f3c96dacdd583a92adda3d127d1274
--- /dev/null
+++ b/example/ohos/entry/src/main/resources/base/element/string.json
@@ -0,0 +1,16 @@
+{
+ "string": [
+ {
+ "name": "module_desc",
+ "value": "module description"
+ },
+ {
+ "name": "EntryAbility_desc",
+ "value": "description"
+ },
+ {
+ "name": "EntryAbility_label",
+ "value": "flutter_webrtc"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/example/ohos/entry/src/main/resources/base/media/icon.png b/example/ohos/entry/src/main/resources/base/media/icon.png
new file mode 100644
index 0000000000000000000000000000000000000000..ce307a8827bd75456441ceb57d530e4c8d45d36c
Binary files /dev/null and b/example/ohos/entry/src/main/resources/base/media/icon.png differ
diff --git a/example/ohos/entry/src/main/resources/base/profile/main_pages.json b/example/ohos/entry/src/main/resources/base/profile/main_pages.json
new file mode 100644
index 0000000000000000000000000000000000000000..1898d94f58d6128ab712be2c68acc7c98e9ab9ce
--- /dev/null
+++ b/example/ohos/entry/src/main/resources/base/profile/main_pages.json
@@ -0,0 +1,5 @@
+{
+ "src": [
+ "pages/Index"
+ ]
+}
diff --git a/example/ohos/entry/src/main/resources/en_US/element/string.json b/example/ohos/entry/src/main/resources/en_US/element/string.json
new file mode 100644
index 0000000000000000000000000000000000000000..77ca1a6ed2f3c96dacdd583a92adda3d127d1274
--- /dev/null
+++ b/example/ohos/entry/src/main/resources/en_US/element/string.json
@@ -0,0 +1,16 @@
+{
+ "string": [
+ {
+ "name": "module_desc",
+ "value": "module description"
+ },
+ {
+ "name": "EntryAbility_desc",
+ "value": "description"
+ },
+ {
+ "name": "EntryAbility_label",
+ "value": "flutter_webrtc"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/example/ohos/entry/src/main/resources/zh_CN/element/string.json b/example/ohos/entry/src/main/resources/zh_CN/element/string.json
new file mode 100644
index 0000000000000000000000000000000000000000..8bed9a82aa5a2318317a066bba218b02df1ad5ee
--- /dev/null
+++ b/example/ohos/entry/src/main/resources/zh_CN/element/string.json
@@ -0,0 +1,16 @@
+{
+ "string": [
+ {
+ "name": "module_desc",
+ "value": "模块描述"
+ },
+ {
+ "name": "EntryAbility_desc",
+ "value": "description"
+ },
+ {
+ "name": "EntryAbility_label",
+ "value": "flutter_webrtc"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/example/ohos/hvigor/hvigor-config.json5 b/example/ohos/hvigor/hvigor-config.json5
new file mode 100644
index 0000000000000000000000000000000000000000..5ed0b910cce4b7e776973a892bf714f89085978e
--- /dev/null
+++ b/example/ohos/hvigor/hvigor-config.json5
@@ -0,0 +1,5 @@
+{
+ "modelVersion": "5.0.0",
+ "dependencies": {
+ }
+}
\ No newline at end of file
diff --git a/example/ohos/hvigorfile.ts b/example/ohos/hvigorfile.ts
new file mode 100644
index 0000000000000000000000000000000000000000..0a7e3d8d74177458336fa2f524032b873cbe9552
--- /dev/null
+++ b/example/ohos/hvigorfile.ts
@@ -0,0 +1,6 @@
+import { appTasks } from '@ohos/hvigor-ohos-plugin';
+
+export default {
+ system: appTasks, /* Built-in plugin of Hvigor. It cannot be modified. */
+ plugins:[] /* Custom plugin to extend the functionality of Hvigor. */
+}
\ No newline at end of file
diff --git a/example/ohos/oh-package.json5 b/example/ohos/oh-package.json5
new file mode 100644
index 0000000000000000000000000000000000000000..199326ecf2fa2c760ab0ee07d0a0092674622b67
--- /dev/null
+++ b/example/ohos/oh-package.json5
@@ -0,0 +1,22 @@
+{
+ "modelVersion": "5.0.0",
+ "name": "flutter_webrtc",
+ "version": "1.0.0",
+ "description": "Please describe the basic information.",
+ "main": "",
+ "author": "",
+ "license": "",
+ "dependencies": {
+ "@ohos/flutter_ohos": "file:./har/flutter.har"
+ },
+ "devDependencies": {
+ "@ohos/hypium": "1.0.6"
+ },
+ "overrides": {
+ "@ohos/flutter_ohos": "file:./har/flutter.har",
+ "flutter_webrtc": "file:./har/flutter_webrtc.har",
+ "path_provider_ohos": "file:./har/path_provider_ohos.har",
+ "permission_handler_ohos": "file:./har/permission_handler_ohos.har",
+ "@ohos/flutter_module": "file:./entry"
+ }
+}
\ No newline at end of file
diff --git a/example/pubspec.yaml b/example/pubspec.yaml
index b77402497861699fc1debf9ad566748fb7b0f080..f4219bbea5049e31b6f75508403d2a020e94a398 100644
--- a/example/pubspec.yaml
+++ b/example/pubspec.yaml
@@ -15,9 +15,16 @@ dependencies:
flutter_webrtc:
path: ../
# Required for MediaRecorder example
- path_provider: ^2.0.2
- permission_handler: ^10.2.0
+ path_provider:
+ git:
+ url: "https://gitee.com/openharmony-sig/flutter_packages.git"
+ path: "packages/path_provider/path_provider"
+ permission_handler:
+ git:
+ url: "https://gitee.com/openharmony-sig/flutter_permission_handler.git"
+ path: "permission_handler"
sdp_transform: ^0.3.2
+ web_socket_channel: 2.4.0
dev_dependencies:
@@ -26,6 +33,10 @@ dev_dependencies:
pedantic: ^1.11.0
+dependency_overrides:
+ meta: ^1.11.0 # 临时解决dart-flutter1.2.0版本与flutter-SDK依赖mate版本冲突
+ collection: 1.17.1
+
# For information on the generic Dart part of this file, see the
# following page: https://www.dartlang.org/tools/pub/pubspec
diff --git a/lib/flutter_webrtc.dart b/lib/flutter_webrtc.dart
index ef26b54a5d67a3f149aeaf404c1ecd589c33e3d0..d0358ea5977e7d160fbd91348ac7fe7fb96122aa 100644
--- a/lib/flutter_webrtc.dart
+++ b/lib/flutter_webrtc.dart
@@ -17,3 +17,4 @@ export 'src/native/utils.dart' if (dart.library.html) 'src/web/utils.dart';
export 'src/native/adapter_type.dart';
export 'src/native/android/audio_configuration.dart';
export 'src/native/ios/audio_configuration.dart';
+export 'src/native/ohos/audio_configuration.dart';
diff --git a/lib/src/helper.dart b/lib/src/helper.dart
index 23d5f5a2f6829e016c8ccd079f96208183abecaa..0aacb527bccbdf4be9bb3a5088e2e47af9b55e5c 100644
--- a/lib/src/helper.dart
+++ b/lib/src/helper.dart
@@ -69,7 +69,9 @@ class Helper {
static Future setZoom(
MediaStreamTrack videoTrack, double zoomLevel) async {
- if (WebRTC.platformIsAndroid || WebRTC.platformIsIOS) {
+ if (WebRTC.platformIsAndroid ||
+ WebRTC.platformIsIOS ||
+ WebRTC.platformIsOhos) {
await WebRTC.invokeMethod(
'mediaStreamTrackSetZoom',
{'trackId': videoTrack.id, 'zoomLevel': zoomLevel},
@@ -162,4 +164,10 @@ class Helper {
AppleNativeAudioManagement.setAppleAudioConfiguration(
AppleNativeAudioManagement.getAppleAudioConfigurationForMode(mode,
preferSpeakerOutput: preferSpeakerOutput));
+
+ /// Set the audio configuration to for Ohos.
+ static Future setOhosAudioConfiguration(
+ OhosAudioConfiguration ohosAudioConfiguration) =>
+ OhosNativeAudioManagement.setOhosAudioConfiguration(
+ ohosAudioConfiguration);
}
diff --git a/lib/src/native/ohos/audio_configuration.dart b/lib/src/native/ohos/audio_configuration.dart
new file mode 100644
index 0000000000000000000000000000000000000000..5caa5e3f5e734cfc38acc8cb5357fd1dd186363d
--- /dev/null
+++ b/lib/src/native/ohos/audio_configuration.dart
@@ -0,0 +1,86 @@
+import 'package:flutter/foundation.dart';
+
+import '../utils.dart';
+
+enum OhosStreamUsage {
+ unknown,
+ music,
+ voiceCommunication,
+ voiceAssistant,
+ alarm,
+ voiceMessage,
+ ringtone,
+ notification,
+ accessibility,
+ movie,
+ game,
+ audioBook,
+ navigation,
+ videoCommunication,
+}
+
+extension OhosStreamUsageExt on OhosStreamUsage {
+ String get value => describeEnum(this);
+}
+
+extension OhosStreamUsageEnumEx on String {
+ OhosStreamUsage toOhosStreamUsage() => OhosStreamUsage.values
+ .firstWhere((d) => describeEnum(d) == toLowerCase());
+}
+
+enum OhosSourceType {
+ invalid,
+ mic,
+ voiceRecognition,
+ voiceCommunication,
+ voiceMessage,
+ camcorder
+}
+
+extension OhosSourceTypeExt on OhosSourceType {
+ String get value => describeEnum(this);
+}
+
+extension OhosSourceTypeEnumEx on String {
+ OhosSourceType toOhosSourceType() =>
+ OhosSourceType.values.firstWhere((d) => describeEnum(d) == toLowerCase());
+}
+
+class OhosAudioConfiguration {
+ OhosAudioConfiguration({
+ this.ohosStreamUsage,
+ this.ohosSourceType,
+ });
+
+ final OhosStreamUsage? ohosStreamUsage;
+ final OhosSourceType? ohosSourceType;
+
+ Map toMap() => {
+ if (ohosStreamUsage != null) 'ohosStreamUsage': ohosStreamUsage!.value,
+ if (ohosSourceType != null) 'ohosSourceType': ohosSourceType!.value,
+ };
+
+ /// A pre-configured OhosAudioConfiguration for media playback.
+ static final media = OhosAudioConfiguration(
+ ohosStreamUsage: OhosStreamUsage.music,
+ ohosSourceType: OhosSourceType.camcorder,
+ );
+
+ /// A pre-configured OhosAudioConfiguration for voice communication.
+ static final communication = OhosAudioConfiguration(
+ ohosStreamUsage: OhosStreamUsage.voiceCommunication,
+ ohosSourceType: OhosSourceType.mic,
+ );
+}
+
+class OhosNativeAudioManagement {
+ static Future setOhosAudioConfiguration(
+ OhosAudioConfiguration config) async {
+ if (WebRTC.platformIsOhos) {
+ await WebRTC.invokeMethod(
+ 'setOhosAudioConfiguration',
+ {'configuration': config.toMap()},
+ );
+ }
+ }
+}
diff --git a/lib/src/native/ohos/video_render.dart b/lib/src/native/ohos/video_render.dart
new file mode 100644
index 0000000000000000000000000000000000000000..85ede2a54684f45f0e54a63acc5a701378ac351e
--- /dev/null
+++ b/lib/src/native/ohos/video_render.dart
@@ -0,0 +1,74 @@
+import 'dart:async';
+
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+
+typedef OnViewCreated = Function(OhosRTCVideoRenderController);
+
+///自定义OhosView
+class OhosRTCVideoRender extends StatefulWidget {
+
+ const OhosRTCVideoRender(this.onViewCreated, {Key? key}) : super(key: key);
+ final OnViewCreated onViewCreated;
+
+ @override
+ State createState() => _OhosRTCVideoRender();
+}
+
+class _OhosRTCVideoRender extends State {
+ late MethodChannel _channel;
+
+ @override
+ Widget build(BuildContext context) {
+ return _getPlatformFaceView();
+ }
+
+ Widget _getPlatformFaceView() {
+ return OhosView(
+ viewType: 'flutter.webrtc.ohos/RTCVideoRender',
+ onPlatformViewCreated: _onOhosRTCVideoRenderCreated,
+ creationParams: const {'initParams': 'hello world'},
+ creationParamsCodec: const StandardMessageCodec(),
+ );
+ }
+
+ void _onOhosRTCVideoRenderCreated(int id) {
+ _channel = MethodChannel('flutter.webrtc.ohos/RTCVideoRender$id');
+ final controller = OhosRTCVideoRenderController._(
+ _channel,
+ );
+ widget.onViewCreated(controller);
+ }
+}
+
+class OhosRTCVideoRenderController {
+
+ OhosRTCVideoRenderController._(
+ this._channel,
+ ) {
+ _channel.setMethodCallHandler(
+ (call) async {
+ print('OhosRTCVideoRender method : ${call.method}');
+ switch (call.method) {
+ case 'putSurfaceId':
+ // 从native端获取数据
+ final result = call.arguments;
+ _controller.sink.add(result);
+ break;
+ }
+ },
+ );
+ }
+ final MethodChannel _channel;
+ final StreamController _controller = StreamController();
+
+ Stream get customDataStream => _controller.stream;
+
+ // 发送数据给native
+ Future sendMessageToOhosView(String message) async {
+ await _channel.invokeMethod(
+ 'getMessageFromFlutterView',
+ message,
+ );
+ }
+}
diff --git a/lib/src/native/rtc_video_renderer_impl.dart b/lib/src/native/rtc_video_renderer_impl.dart
index 9607b3902bec40286fa08bd4aad4698fdc4efa57..057189e115337b0a3765e7286c606b31e30956e8 100644
--- a/lib/src/native/rtc_video_renderer_impl.dart
+++ b/lib/src/native/rtc_video_renderer_impl.dart
@@ -46,6 +46,8 @@ class RTCVideoRenderer extends ValueNotifier
@override
Function? onFirstFrameRendered;
+ String? surfaceId ;
+
@override
set srcObject(MediaStream? stream) {
if (_disposed) {
@@ -55,6 +57,7 @@ class RTCVideoRenderer extends ValueNotifier
_srcObject = stream;
WebRTC.invokeMethod('videoRendererSetSrcObject', {
'textureId': textureId,
+ 'surfaceId': surfaceId ?? '',
'streamId': stream?.id ?? '',
'ownerTag': stream?.ownerTag ?? ''
}).then((_) {
@@ -76,6 +79,7 @@ class RTCVideoRenderer extends ValueNotifier
try {
await WebRTC.invokeMethod('videoRendererSetSrcObject', {
'textureId': _textureId,
+ 'surfaceId': surfaceId ?? '',
'streamId': stream?.id ?? '',
'ownerTag': stream?.ownerTag ?? '',
'trackId': trackId ?? '0'
diff --git a/lib/src/native/rtc_video_view_impl.dart b/lib/src/native/rtc_video_view_impl.dart
index c579466c14a3b3da611e1392f0af7a17be880954..841f64f171a7d11cfb43ed6eed95ca22547a875f 100644
--- a/lib/src/native/rtc_video_view_impl.dart
+++ b/lib/src/native/rtc_video_view_impl.dart
@@ -1,15 +1,15 @@
import 'dart:math';
-
import 'package:flutter/material.dart';
-
+import 'package:flutter_webrtc/flutter_webrtc.dart';
import 'package:webrtc_interface/webrtc_interface.dart';
-
+import 'ohos/video_render.dart';
import 'rtc_video_renderer_impl.dart';
class RTCVideoView extends StatelessWidget {
RTCVideoView(
this._renderer, {
Key? key,
+ this.onRendererUpdated = null,
this.objectFit = RTCVideoViewObjectFit.RTCVideoViewObjectFitContain,
this.mirror = false,
this.filterQuality = FilterQuality.low,
@@ -21,9 +21,35 @@ class RTCVideoView extends StatelessWidget {
final bool mirror;
final FilterQuality filterQuality;
final WidgetBuilder? placeholderBuilder;
+ OhosRTCVideoRenderController? _controller;
+ final Function(String)? onRendererUpdated; // 外部传入的回调
RTCVideoRenderer get videoRenderer => _renderer;
+ void updateRenderer(String id) {
+ if (onRendererUpdated != null) {
+ onRendererUpdated!(id); // 调用外部回调
+ }
+ }
+
+ void _onOhosRTCVideoRenderCreated(OhosRTCVideoRenderController controller) {
+ _controller = controller;
+ _controller?.customDataStream.listen((data) {
+ //接收到来自OHOS端的数据
+ print('FlutterWebRTCPlugin-来自ohos的数据:$data');
+ updateRenderer(data['surfaceId']!);
+ });
+ }
+
+ Widget _buildOhosRTCVideoRender() {
+ return Center(
+ child: Container(
+ color: Colors.blueAccent.withAlpha(60),
+ child: OhosRTCVideoRender(_onOhosRTCVideoRenderCreated),
+ ),
+ );
+ }
+
@override
Widget build(BuildContext context) {
return LayoutBuilder(
@@ -55,12 +81,14 @@ class RTCVideoView extends StatelessWidget {
child: Transform(
transform: Matrix4.identity()..rotateY(mirror ? -pi : 0.0),
alignment: FractionalOffset.center,
- child: videoRenderer.renderVideo
- ? Texture(
- textureId: videoRenderer.textureId!,
- filterQuality: filterQuality,
- )
- : placeholderBuilder?.call(context) ?? Container(),
+ child: WebRTC.platformIsOhos
+ ? _buildOhosRTCVideoRender()
+ : (videoRenderer.renderVideo
+ ? Texture(
+ textureId: videoRenderer.textureId!,
+ filterQuality: filterQuality,
+ )
+ : placeholderBuilder?.call(context) ?? Container()),
),
),
),
diff --git a/lib/src/native/utils.dart b/lib/src/native/utils.dart
index caa518f164fb5a5baa9fec467632fdccf6659cc8..b626e85a178ec6a1b1c880e37d2fd547e327738c 100644
--- a/lib/src/native/utils.dart
+++ b/lib/src/native/utils.dart
@@ -20,6 +20,8 @@ class WebRTC {
static bool get platformIsAndroid => Platform.isAndroid;
+ static bool get platformIsOhos => Platform.operatingSystem == 'ohos';
+
static bool get platformIsWeb => false;
static Future invokeMethod(String methodName,
diff --git a/lib/src/web/utils.dart b/lib/src/web/utils.dart
index 966425d7249f9e41c33830084f1fd0caceaabd26..3e44aa6e7d00643847a6d01890deed68230f8261 100644
--- a/lib/src/web/utils.dart
+++ b/lib/src/web/utils.dart
@@ -13,6 +13,8 @@ class WebRTC {
static bool get platformIsAndroid => false;
+ static bool get platformIsOhos => false;
+
static bool get platformIsWeb => true;
static Future invokeMethod(String methodName,
diff --git a/ohos/.gitignore b/ohos/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..18835ddceb5617477fc548ddb809854a947e435a
--- /dev/null
+++ b/ohos/.gitignore
@@ -0,0 +1,8 @@
+/node_modules
+/oh_modules
+/.preview
+/build
+/.cxx
+/.test
+/oh-package-lock.json5
+/BuildProfile.ets
diff --git a/ohos/Index.ets b/ohos/Index.ets
new file mode 100644
index 0000000000000000000000000000000000000000..172c1fa49453ec9c5fa7ffc1a154f4f28b750586
--- /dev/null
+++ b/ohos/Index.ets
@@ -0,0 +1,28 @@
+/* MIT License
+*
+* Copyright (c) 2024 SwanLink (Jiangsu) Technology Development Co., LTD.
+* All rights reserved.
+* Permission is hereby granted, free of charge, to any person obtaining a copy
+* of this software and associated documentation files (the "Software"), to deal
+* in the Software without restriction, including without limitation the rights
+* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+* copies of the Software, and to permit persons to whom the Software is
+* furnished to do so, subject to the following conditions:
+*
+* The above copyright notice and this permission notice shall be included in all
+* copies or substantial portions of the Software.
+*
+* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+* SOFTWARE.
+*/
+
+import { FlutterWebRTCPlugin } from './src/main/ets/FlutterWebRTCPlugin'
+export default FlutterWebRTCPlugin
+
+
+
diff --git a/ohos/build-profile.json5 b/ohos/build-profile.json5
new file mode 100644
index 0000000000000000000000000000000000000000..2014fa25af9aa40a42ca16a454753f84cbcd8184
--- /dev/null
+++ b/ohos/build-profile.json5
@@ -0,0 +1,25 @@
+{
+ "apiType": "stageMode",
+ "buildOption": {
+ },
+ "buildOptionSet": [
+ {
+ "name": "release",
+ "arkOptions": {
+ "obfuscation": {
+ "ruleOptions": {
+ "enable": true,
+ "files": [
+ "./obfuscation-rules.txt"
+ ]
+ }
+ }
+ },
+ },
+ ],
+ "targets": [
+ {
+ "name": "default"
+ }
+ ]
+}
diff --git a/ohos/hvigorfile.ts b/ohos/hvigorfile.ts
new file mode 100644
index 0000000000000000000000000000000000000000..42187071482d292588ad40babeda74f7b8d97a23
--- /dev/null
+++ b/ohos/hvigorfile.ts
@@ -0,0 +1,6 @@
+import { harTasks } from '@ohos/hvigor-ohos-plugin';
+
+export default {
+ system: harTasks, /* Built-in plugin of Hvigor. It cannot be modified. */
+ plugins:[] /* Custom plugin to extend the functionality of Hvigor. */
+}
diff --git a/ohos/libs/arm64-v8a/libohos_webrtc.so b/ohos/libs/arm64-v8a/libohos_webrtc.so
new file mode 100644
index 0000000000000000000000000000000000000000..8a54ff2e6fdfbb1eb92d83ad2d9f7e6a08da862e
Binary files /dev/null and b/ohos/libs/arm64-v8a/libohos_webrtc.so differ
diff --git a/ohos/obfuscation-rules.txt b/ohos/obfuscation-rules.txt
new file mode 100644
index 0000000000000000000000000000000000000000..fdbb5b9852d7dd5f39bddaeb21ab5ee1f3346749
--- /dev/null
+++ b/ohos/obfuscation-rules.txt
@@ -0,0 +1,22 @@
+# Define project specific obfuscation rules here.
+# You can include the obfuscation configuration files in the current module's build-profile.json5.
+#
+# For more details, see
+# https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/source-obfuscation-V5
+
+# Obfuscation options:
+# -disable-obfuscation: disable all obfuscations
+# -enable-property-obfuscation: obfuscate the property names
+# -enable-toplevel-obfuscation: obfuscate the names in the global scope
+# -compact: remove unnecessary blank spaces and all line feeds
+# -remove-log: remove all console.* statements
+# -print-namecache: print the name cache that contains the mapping from the old names to new names
+# -apply-namecache: reuse the given cache file
+
+# Keep options:
+# -keep-property-name: specifies property names that you want to keep
+# -keep-global-name: specifies names that you want to keep in the global scope
+-enable-property-obfuscation
+-enable-toplevel-obfuscation
+-enable-filename-obfuscation
+-enable-export-obfuscation
\ No newline at end of file
diff --git a/ohos/oh-package.json5 b/ohos/oh-package.json5
new file mode 100644
index 0000000000000000000000000000000000000000..78cc628a49bdf505a0dbef8feb7f558a917fe84c
--- /dev/null
+++ b/ohos/oh-package.json5
@@ -0,0 +1,12 @@
+{
+ "name": "flutter_webrtc",
+ "version": "1.0.0",
+ "description": "Please describe the basic information.",
+ "main": "Index.ets",
+ "author": "",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "libohos_webrtc.so": "file:./src/main/libohos_webrtc",
+ "@ohos/flutter_ohos": "file:../libs/flutter.har"
+ }
+}
\ No newline at end of file
diff --git a/ohos/src/main/ets/DataChannelInit.ets b/ohos/src/main/ets/DataChannelInit.ets
new file mode 100644
index 0000000000000000000000000000000000000000..b06b7986ee1bcca61264b0e2f2e45e4333f8fc87
--- /dev/null
+++ b/ohos/src/main/ets/DataChannelInit.ets
@@ -0,0 +1,63 @@
+/* MIT License
+*
+* Copyright (c) 2024 SwanLink (Jiangsu) Technology Development Co., LTD.
+* All rights reserved.
+* Permission is hereby granted, free of charge, to any person obtaining a copy
+* of this software and associated documentation files (the "Software"), to deal
+* in the Software without restriction, including without limitation the rights
+* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+* copies of the Software, and to permit persons to whom the Software is
+* furnished to do so, subject to the following conditions:
+*
+* The above copyright notice and this permission notice shall be included in all
+* copies or substantial portions of the Software.
+*
+* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+* SOFTWARE.
+*/
+
+import { RTCDataChannelInit } from 'libohos_webrtc.so';
+
+const TAG = 'FlutterWebRTCPlugin-DataChannelInit'
+
+export class DataChannelInit implements RTCDataChannelInit {
+ public ordered: boolean = true;
+ public maxPacketLifeTime: number = -1;
+ public maxRetransmits: number = -1;
+ public protocol: string = '';
+ public negotiated: boolean = false;
+ public id: number = -1;
+
+ constructor() {
+ }
+
+ getOrdered(): boolean {
+ return this.ordered;
+ }
+
+ getMaxPacketLifeTime(): number {
+ return this.maxPacketLifeTime;
+ }
+
+ getMaxRetransmits(): number {
+ return this.maxRetransmits;
+ }
+
+ getProtocol(): string {
+ return this.protocol;
+ }
+
+ getNegotiated(): boolean {
+ return this.negotiated;
+ }
+
+ getId(): number {
+ return this.id;
+ }
+}
+
diff --git a/ohos/src/main/ets/DataChannelObserver.ets b/ohos/src/main/ets/DataChannelObserver.ets
new file mode 100644
index 0000000000000000000000000000000000000000..1768316cffa55bd7dd6047f327f4a2af4c713680
--- /dev/null
+++ b/ohos/src/main/ets/DataChannelObserver.ets
@@ -0,0 +1,142 @@
+/* MIT License
+*
+* Copyright (c) 2024 SwanLink (Jiangsu) Technology Development Co., LTD.
+* All rights reserved.
+* Permission is hereby granted, free of charge, to any person obtaining a copy
+* of this software and associated documentation files (the 'Software'), to deal
+* in the Software without restriction, including without limitation the rights
+* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+* copies of the Software, and to permit persons to whom the Software is
+* furnished to do so, subject to the following conditions:
+*
+* The above copyright notice and this permission notice shall be included in all
+* copies or substantial portions of the Software.
+*
+* THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+* SOFTWARE.
+*/
+
+import { Any, BinaryMessenger, Log } from '@ohos/flutter_ohos';
+import EventChannel, { EventSink, StreamHandler } from '@ohos/flutter_ohos/src/main/ets/plugin/common/EventChannel';
+import { RTCDataChannel, Event, MessageEvent, DataChannelState } from 'libohos_webrtc.so';
+import { ArrayList } from '@kit.ArkTS';
+import { ConstraintsMap } from './utils/ConstraintsMap';
+import { DataChannelStateCode } from './utils/ObjectType';
+import { util } from '@kit.ArkTS';
+import { isBinaryBuffer } from './utils/Utils';
+
+const TAG = 'FlutterWebRTCPlugin-DataChannelObserver'
+const CHANNEL_NAME = 'FlutterWebRTC/dataChannelEvent'
+
+export class DataChannelObserver implements StreamHandler {
+ private flutterId: string;
+ private dataChannel: RTCDataChannel;
+ private eventChannel: EventChannel;
+ private eventSink: EventSink | null = null
+ private eventQueue: ArrayList