├── .github └── workflows │ └── build.yml ├── .gitignore ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── analysis_options.yaml ├── example ├── .gitignore ├── README.md ├── lib │ ├── main.dart │ └── views │ │ ├── echo_test.dart │ │ └── pub_sub.dart ├── pubspec.yaml ├── scripts │ ├── add-line.py │ └── project_tools.sh └── test │ └── widget_test.dart ├── flutter-ion.code-workspace ├── lib ├── flutter_ion.dart └── src │ ├── _library │ ├── apps │ │ └── room │ │ │ └── proto │ │ │ ├── room.pb.dart │ │ │ ├── room.pbenum.dart │ │ │ ├── room.pbgrpc.dart │ │ │ └── room.pbjson.dart │ └── proto │ │ └── rtc │ │ ├── rtc.pb.dart │ │ ├── rtc.pbenum.dart │ │ ├── rtc.pbgrpc.dart │ │ └── rtc.pbjson.dart │ ├── client.dart │ ├── connector │ ├── ion.dart │ ├── room.dart │ └── rtc.dart │ ├── logger.dart │ ├── signal │ ├── grpc-web │ │ ├── _channel.dart │ │ ├── _channel_html.dart │ │ ├── transport │ │ │ └── websocket_transport.dart │ │ └── websocket_channel.dart │ ├── json-rpc │ │ ├── common.dart │ │ ├── websocket.dart │ │ └── websocket_web.dart │ ├── signal.dart │ ├── signal_grpc_impl.dart │ └── signal_jsonrpc_impl.dart │ ├── stream.dart │ └── utils.dart ├── pubspec.yaml ├── renovate.json └── test ├── client_test.dart ├── sdp_test.dart └── webrtc.sdp /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | test: 11 | name: Test on ${{ matrix.os }} 12 | runs-on: ${{ matrix.os }} 13 | strategy: 14 | matrix: 15 | os: [ubuntu-latest] 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - uses: actions/setup-java@v1 20 | with: 21 | java-version: '12.x' 22 | - uses: subosito/flutter-action@v1.5.3 23 | with: 24 | flutter-version: '2.5.1' 25 | channel: 'stable' 26 | - run: flutter packages get 27 | - run: flutter format lib/ test/ 28 | - run: flutter analyze 29 | - run: flutter test 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .packages 2 | .dart_tool 3 | .idea 4 | pubspec.lock 5 | example/.flutter-plugins-dependencies 6 | .DS_Store 7 | .flutter-plugins 8 | .flutter-plugins-dependencies 9 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | -------------------------------------------- 4 | [1.0.0] - 2021.11.08 5 | * Support ion 2.0.1 6 | * Update flutter webrtc v0.7.0 7 | * Many fix 8 | 9 | [0.5.4] - 2020.09.05 10 | 11 | * Fix bug in `onnegotiationneeded` event. 12 | * Fixes analysis error and updates dependencies (#46) 13 | * Fixes analysis errors 14 | * Updates all dependencies to latest version 15 | 16 | [0.5.3] - 2020.08.14 17 | 18 | * Fix pubsub test; some update 19 | 20 | [0.5.2] - 2020.06.18 21 | 22 | * fix examples. 23 | 24 | [0.5.1] - 2020.06.18 25 | 26 | * flutter-webrtc 0.6.4. 27 | 28 | [0.5.0] - 2020.06.18 29 | 30 | * Adjust biz logic. 31 | 32 | [0.4.4] - 2020.04.22 33 | 34 | * Add support https scheme. 35 | 36 | [0.4.3] - 2020.04.05 37 | 38 | * Fix publish for macOS. 39 | 40 | [0.4.2] - 2020.04.03 41 | 42 | * Upgrade flutter-webrtc to 0.6.2. 43 | * Fix publish for Android/iOS. 44 | 45 | [0.4.1] - 2020.04.03 46 | 47 | * upgrade to flutter-webrtc 0.6.2. 48 | 49 | [0.4.0] - 2020.04.01 50 | 51 | * null safety. 52 | 53 | [0.3.1] - 2020.11.22 54 | 55 | * Fixed null transports issue. 56 | 57 | [0.3.0] - 2021.03.30 58 | 59 | * Add IonConnector for new ion. 60 | 61 | [0.2.2] - 2020.03.28 62 | 63 | * Get more pub scores. 64 | 65 | [0.2.1] - 2020.11.22 66 | 67 | * Bug fixes. 68 | 69 | [0.2.0] - 2020.11.21 70 | 71 | * Bug fixes. 72 | 73 | [0.1.1] - 2020.05.09 74 | 75 | * Make sdk cleaner. 76 | 77 | [0.1.0] - 2020.05.09 78 | 79 | * Initial release. 80 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:stretch 2 | 3 | ENV DART_VERSION=2.12.4 4 | 5 | RUN \ 6 | apt-get -q update && apt-get install --no-install-recommends -y -q gnupg2 curl ca-certificates apt-transport-https openssh-client && \ 7 | curl https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - && \ 8 | curl https://storage.googleapis.com/download.dartlang.org/linux/debian/dart_stable.list > /etc/apt/sources.list.d/dart_stable.list && \ 9 | apt-get update && \ 10 | apt-get install -y protobuf-compiler && \ 11 | apt-get install -y dart=$DART_VERSION-1 && \ 12 | rm -rf /var/lib/apt/lists/* 13 | 14 | ENV DART_SDK /usr/lib/dart 15 | ENV PATH $DART_SDK/bin:/root/.pub-cache/bin:$PATH 16 | RUN pub global activate protoc_plugin 17 | 18 | RUN apt-get -q update && apt-get install -y make 19 | 20 | WORKDIR /workspace 21 | 22 | ENTRYPOINT ["make"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Pion 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: download proto 2 | 3 | proto-gen-from-docker: 4 | docker build -t dart-protoc . 5 | docker run -v $(CURDIR):/workspace dart-protoc proto 6 | 7 | proto: 8 | mkdir -p lib/src/_library 9 | protoc ./ion/proto/rtc/rtc.proto -I./ion --dart_out=grpc:./lib/src/_library 10 | protoc ./ion/apps/room/proto/room.proto -I./ion --dart_out=grpc:./lib/src/_library 11 | 12 | dart format lib/src/_library 13 | 14 | download: 15 | git clone https://github.com/pion/ion -b refactor-business-logic --depth=1 16 | 17 | clean: 18 | rm -rf lib/src/_library 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flutter sdk for ion 2 | 3 | [![pub package](https://img.shields.io/pub/v/flutter_ion.svg)](https://pub.dartlang.org/packages/flutter_ion) 4 | 5 | Flutter sdk for the Ion backend. 6 | 7 | ## Installation 8 | 9 | Edit `pubspec.yaml` in your flutter projects. 10 | 11 | Add 12 | 13 | ```yml 14 | flutter_ion: 15 | ``` 16 | 17 | ## Platform Support 18 | 19 | * Android 20 | * iOS 21 | * macOS 22 | * Web 23 | 24 | ## Usage 25 | 26 | ```dart 27 | import 'package:flutter_ion/flutter_ion.dart' as ion; 28 | import 'package:uuid/uuid.dart'; 29 | 30 | // Connect to ion-sfu. 31 | final signal = ion.JsonRPCSignal("ws://ion-sfu:7000/ws"); 32 | 33 | final String _uuid = Uuid().v4(); 34 | 35 | ion.Client client = await ion.Client.create(sid: "test session", uid: _uuid, signal: signal); 36 | 37 | client.ontrack = (track, ion.RemoteStream stream) { 38 | /// mute a remote stream 39 | stream.mute(); 40 | /// unmute a remote stream 41 | stream.unmute(); 42 | 43 | if (track.kind == "video") { 44 | /// prefer a layer 45 | stream.preferLayer(ion.Layer.medium); 46 | 47 | /// render remote stream. 48 | /// remoteRenderer.srcObject = stream.stream; 49 | } 50 | }; 51 | 52 | ion.LocalStream localStream = await ion.LocalStream.getUserMedia( 53 | constraints: ion.Constraints.defaults..simulcast = true); 54 | 55 | /// render local stream. 56 | /// localRenderer.srcObject = localStream.stream; 57 | 58 | /// publish stream 59 | await client.publish(localStream); 60 | 61 | /// mute local straem 62 | localStream.mute(); 63 | 64 | /// unmute local stream 65 | localStream.unmute(); 66 | 67 | ``` 68 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:pedantic/analysis_options.yaml 2 | 3 | linter: 4 | rules: 5 | - always_declare_return_types 6 | - avoid_empty_else 7 | - await_only_futures 8 | - avoid_returning_null_for_void 9 | - camel_case_extensions 10 | - camel_case_types 11 | - cancel_subscriptions 12 | - directives_ordering 13 | - flutter_style_todos 14 | - sort_pub_dependencies 15 | - type_init_formals 16 | - unnecessary_brace_in_string_interps 17 | - unnecessary_const 18 | - unnecessary_new 19 | - unnecessary_getters_setters 20 | - unnecessary_null_aware_assignments 21 | - unnecessary_null_in_if_null_operators 22 | - unnecessary_overrides 23 | - unnecessary_parenthesis 24 | - unnecessary_statements 25 | - unnecessary_string_interpolations 26 | - unnecessary_this 27 | - unrelated_type_equality_checks 28 | - use_rethrow_when_possible 29 | - valid_regexps 30 | - void_checks 31 | 32 | analyzer: 33 | exclude: [lib/src/_library/**] 34 | errors: 35 | # treat missing required parameters as a warning (not a hint) 36 | missing_required_param: warning 37 | # treat missing returns as a warning (not a hint) 38 | missing_return: warning 39 | # allow having TODOs in the code 40 | todo: ignore 41 | # allow self-reference to deprecated members (we do this because otherwise we have 42 | # to annotate every member in every test, assert, etc, when we deprecate something) 43 | deprecated_member_use_from_same_package: ignore 44 | # Ignore analyzer hints for updating pubspecs when using Future or 45 | # Stream and not importing dart:async 46 | # Please see https://github.com/flutter/flutter/pull/24528 for details. 47 | sdk_version_async_exported_from_core: ignore 48 | invalid_dependency: ignore 49 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | .dart_tool/ 26 | .flutter-plugins 27 | .packages 28 | .pub-cache/ 29 | .pub/ 30 | /build/ 31 | 32 | # Android related 33 | **/android/**/gradle-wrapper.jar 34 | **/android/.gradle 35 | **/android/captures/ 36 | **/android/gradlew 37 | **/android/gradlew.bat 38 | **/android/local.properties 39 | **/android/**/GeneratedPluginRegistrant.java 40 | 41 | # iOS/XCode related 42 | **/ios/**/*.mode1v3 43 | **/ios/**/*.mode2v3 44 | **/ios/**/*.moved-aside 45 | **/ios/**/*.pbxuser 46 | **/ios/**/*.perspectivev3 47 | **/ios/**/*sync/ 48 | **/ios/**/.sconsign.dblite 49 | **/ios/**/.tags* 50 | **/ios/**/.vagrant/ 51 | **/ios/**/DerivedData/ 52 | **/ios/**/Icon? 53 | **/ios/**/Pods/ 54 | **/ios/**/.symlinks/ 55 | **/ios/**/profile 56 | **/ios/**/xcuserdata 57 | **/ios/.generated/ 58 | **/ios/Flutter/App.framework 59 | **/ios/Flutter/Flutter.framework 60 | **/ios/Flutter/Generated.xcconfig 61 | **/ios/Flutter/app.flx 62 | **/ios/Flutter/app.zip 63 | **/ios/Flutter/flutter_assets/ 64 | **/ios/ServiceDefinitions.json 65 | **/ios/Runner/GeneratedPluginRegistrant.* 66 | 67 | # Exceptions to above rules. 68 | !**/ios/**/default.mode1v3 69 | !**/ios/**/default.mode2v3 70 | !**/ios/**/default.pbxuser 71 | !**/ios/**/default.perspectivev3 72 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 73 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # pion_sfu_example 2 | 3 | A new Flutter project. 4 | 5 | ## Getting Started 6 | 7 | Make sure your flutter is using `flutter 2.x`. 8 | 9 | - `./scripts/project_tools.sh create` 10 | 11 | ## For Android/iOS 12 | - `flutter run` 13 | 14 | ## For Desktop or Web 15 | - `flutter run -d macos` 16 | - `flutter run -d web|chrome` 17 | -------------------------------------------------------------------------------- /example/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'dart:core'; 2 | 3 | import 'package:flutter/material.dart'; 4 | 5 | import 'views/echo_test.dart'; 6 | import 'views/pub_sub.dart'; 7 | 8 | void main() => runApp(MyApp()); 9 | 10 | class MyApp extends StatefulWidget { 11 | @override 12 | _MyAppState createState() => _MyAppState(); 13 | } 14 | 15 | class RouteItem { 16 | RouteItem({ 17 | required this.title, 18 | required this.subtitle, 19 | required this.push, 20 | }); 21 | 22 | final String title; 23 | final String subtitle; 24 | final Function(BuildContext context) push; 25 | } 26 | 27 | class _MyAppState extends State { 28 | List items = [ 29 | RouteItem( 30 | title: 'Echo Test (ion-sfu)', 31 | subtitle: 'echo test with simulcast.', 32 | push: (BuildContext context) { 33 | Navigator.push(context, 34 | MaterialPageRoute(builder: (BuildContext context) => EchoTest())); 35 | }), 36 | RouteItem( 37 | title: 'Pub Sub (ion-sfu)', 38 | subtitle: 'pub sub.', 39 | push: (BuildContext context) { 40 | Navigator.push(context, 41 | MaterialPageRoute(builder: (BuildContext context) => PubSub())); 42 | }), 43 | ]; 44 | 45 | @override 46 | void initState() { 47 | super.initState(); 48 | } 49 | 50 | Widget _buildRow(context, item) { 51 | return ListBody(children: [ 52 | ListTile( 53 | title: Text(item.title), 54 | onTap: () => item.push(context), 55 | trailing: Icon(Icons.arrow_right), 56 | ), 57 | Divider() 58 | ]); 59 | } 60 | 61 | @override 62 | Widget build(BuildContext context) { 63 | return MaterialApp( 64 | home: Scaffold( 65 | appBar: AppBar( 66 | title: Text('ION example'), 67 | ), 68 | body: ListView.builder( 69 | shrinkWrap: true, 70 | padding: const EdgeInsets.all(0.0), 71 | itemCount: items.length, 72 | itemBuilder: (context, i) { 73 | return _buildRow(context, items[i]); 74 | })), 75 | ); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /example/lib/views/echo_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_ion/flutter_ion.dart'; 5 | import 'package:flutter_webrtc/flutter_webrtc.dart'; 6 | import 'package:uuid/uuid.dart'; 7 | 8 | class EchoTest extends StatefulWidget { 9 | @override 10 | _EchoTestState createState() => _EchoTestState(); 11 | } 12 | 13 | class _EchoTestState extends State { 14 | var localRenderer = RTCVideoRenderer(); 15 | var remoteRenderer = RTCVideoRenderer(); 16 | final Connector _connector = Connector('http://127.0.0.1:5551'); 17 | final _room = 'ion-simulcast'; 18 | RemoteStream? _remoteStream; 19 | RTC? _sub; 20 | int subBitrate = 0; 21 | late int layerIndex; 22 | var bytesPrev; 23 | var timestampPrev; 24 | 25 | @override 26 | void initState() { 27 | super.initState(); 28 | layerIndex = 1; 29 | } 30 | 31 | void _handleConnect() async { 32 | await start_subscriber(_connector); 33 | await start_publisher(_connector); 34 | } 35 | 36 | Future start_publisher(Connector connector) async { 37 | var rtc = RTC(connector); 38 | 39 | await rtc.connect(); 40 | await rtc.join(_room, Uuid().v4(), JoinConfig()); 41 | 42 | // publish LocalStream 43 | var localStream = await LocalStream.getUserMedia( 44 | constraints: Constraints.defaults 45 | ..simulcast = true 46 | ..resolution = 'hd'); 47 | await rtc.publish(localStream); 48 | await localRenderer.initialize(); 49 | localRenderer.srcObject = localStream.stream; 50 | setState(() {}); 51 | } 52 | 53 | Future start_subscriber(Connector connector) async { 54 | var rtc = RTC(connector); 55 | rtc.ontrack = (track, RemoteStream remoteStream) async { 56 | print('onTrack: remote stream => ${remoteStream.id}'); 57 | if (track.kind == 'video') { 58 | await remoteRenderer.initialize(); 59 | remoteRenderer.srcObject = remoteStream.stream; 60 | remoteRenderer.onResize = () { 61 | setState(() {}); 62 | }; 63 | setState(() {}); 64 | Timer.periodic(Duration(seconds: 1), (_) { 65 | getStats(remoteStream.stream.getVideoTracks()[0]); 66 | }); 67 | _remoteStream = remoteStream; 68 | } 69 | }; 70 | 71 | await rtc.connect(); 72 | await rtc.join(_room, Uuid().v4(), JoinConfig()); 73 | _sub = rtc; 74 | } 75 | 76 | void getStats(MediaStreamTrack track) async { 77 | var results = await _sub?.getSubStats(track); 78 | results?.forEach((report) { 79 | var now = report.timestamp; 80 | var bitrate; 81 | if ((report.type == 'ssrc' || report.type == 'inbound-rtp') && 82 | report.values['mediaType'] == 'video') { 83 | var bytes = report.values['bytesReceived']; 84 | if (timestampPrev != null) { 85 | bitrate = (8 * 86 | (WebRTC.platformIsWeb 87 | ? bytes - bytesPrev 88 | : (int.tryParse(bytes)! - int.tryParse(bytesPrev)!))) / 89 | (now - timestampPrev); 90 | bitrate = bitrate.floor(); 91 | } 92 | bytesPrev = bytes; 93 | timestampPrev = now; 94 | } 95 | if (bitrate != null) { 96 | setState(() { 97 | subBitrate = bitrate; 98 | }); 99 | } 100 | }); 101 | } 102 | 103 | void _selectSimulcastLayer(int? idx) { 104 | switch (idx) { 105 | case 1: 106 | _remoteStream?.preferLayer!(Layer.high); 107 | break; 108 | case 2: 109 | _remoteStream?.preferLayer!(Layer.medium); 110 | break; 111 | case 3: 112 | _remoteStream?.preferLayer!(Layer.low); 113 | break; 114 | } 115 | setState(() { 116 | layerIndex = idx!; 117 | }); 118 | } 119 | 120 | @override 121 | Widget build(context) => MaterialApp( 122 | title: 'ion-sfu', 123 | home: Scaffold( 124 | appBar: AppBar(title: Text('Echo Test')), 125 | body: Center( 126 | child: Column( 127 | crossAxisAlignment: CrossAxisAlignment.center, 128 | children: [ 129 | Text( 130 | 'Local ${localRenderer.videoWidth}x${localRenderer.videoHeight}'), 131 | Expanded(child: RTCVideoView(localRenderer)), 132 | SizedBox( 133 | height: 10, 134 | ), 135 | Text('Select Layer:', textAlign: TextAlign.center), 136 | Row( 137 | mainAxisAlignment: MainAxisAlignment.center, 138 | children: [ 139 | Text('high:'), 140 | Radio( 141 | value: 1, 142 | groupValue: layerIndex, 143 | onChanged: _selectSimulcastLayer, 144 | ), 145 | SizedBox(width: 20), 146 | Text('medium:'), 147 | Radio( 148 | value: 2, 149 | groupValue: layerIndex, 150 | onChanged: _selectSimulcastLayer, 151 | ), 152 | Text('low:'), 153 | Radio( 154 | value: 3, 155 | groupValue: layerIndex, 156 | onChanged: _selectSimulcastLayer, 157 | ) 158 | ], 159 | ), 160 | Text( 161 | 'Remote ${remoteRenderer.videoWidth}x${remoteRenderer.videoHeight} $subBitrate kbps'), 162 | Expanded(child: RTCVideoView(remoteRenderer)), 163 | SizedBox( 164 | height: 10, 165 | ), 166 | ], 167 | )), 168 | floatingActionButton: FloatingActionButton( 169 | onPressed: _handleConnect, child: Icon(Icons.phone)))); 170 | } 171 | -------------------------------------------------------------------------------- /example/lib/views/pub_sub.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_ion/flutter_ion.dart'; 3 | import 'package:flutter_webrtc/flutter_webrtc.dart'; 4 | import 'package:uuid/uuid.dart'; 5 | 6 | class PubSub extends StatefulWidget { 7 | @override 8 | _PubSubState createState() => _PubSubState(); 9 | } 10 | 11 | class _PubSubState extends State { 12 | final _localRenderer = RTCVideoRenderer(); 13 | final List _remoteRenderers = []; 14 | final Connector _connector = Connector('http://127.0.0.1:5551'); 15 | final _room = 'ion'; 16 | final _uid = Uuid().v4(); 17 | late RTC _rtc; 18 | @override 19 | void initState() { 20 | super.initState(); 21 | connect(); 22 | } 23 | 24 | void connect() async { 25 | _rtc = RTC(_connector); 26 | _rtc.onspeaker = (Map list) { 27 | print('onspeaker: $list'); 28 | }; 29 | 30 | _rtc.ontrack = (track, RemoteStream remoteStream) async { 31 | print('onTrack: remote stream => ${remoteStream.id}'); 32 | if (track.kind == 'video') { 33 | var renderer = RTCVideoRenderer(); 34 | await renderer.initialize(); 35 | renderer.srcObject = remoteStream.stream; 36 | setState(() { 37 | _remoteRenderers.add(renderer); 38 | }); 39 | } 40 | }; 41 | 42 | _rtc.ontrackevent = (TrackEvent event) { 43 | print( 44 | 'ontrackevent state = ${event.state}, uid = ${event.uid}, tracks = ${event.tracks}'); 45 | if (event.state == TrackState.REMOVE) { 46 | setState(() { 47 | _remoteRenderers.removeWhere( 48 | (element) => element.srcObject?.id == event.tracks[0].stream_id); 49 | }); 50 | } 51 | }; 52 | 53 | await _rtc.connect(); 54 | await _rtc.join(_room, _uid, JoinConfig()); 55 | 56 | await _localRenderer.initialize(); 57 | // publish LocalStream 58 | var localStream = 59 | await LocalStream.getUserMedia(constraints: Constraints.defaults); 60 | await _rtc.publish(localStream); 61 | setState(() { 62 | _localRenderer.srcObject = localStream.stream; 63 | }); 64 | } 65 | 66 | @override 67 | Widget build(BuildContext context) { 68 | return MaterialApp( 69 | title: 'ion-sfu', 70 | home: Scaffold( 71 | appBar: AppBar( 72 | title: Text('ion-sfu'), 73 | ), 74 | body: OrientationBuilder(builder: (context, orientation) { 75 | return Column( 76 | children: [ 77 | Row( 78 | children: [Text('Local Video')], 79 | ), 80 | Row( 81 | children: [ 82 | SizedBox( 83 | width: 160, 84 | height: 120, 85 | child: RTCVideoView(_localRenderer, mirror: true)) 86 | ], 87 | ), 88 | Row( 89 | children: [Text('Remote Video')], 90 | ), 91 | Row( 92 | children: [ 93 | ..._remoteRenderers.map((remoteRenderer) { 94 | return SizedBox( 95 | width: 160, 96 | height: 120, 97 | child: RTCVideoView(remoteRenderer)); 98 | }).toList(), 99 | ], 100 | ), 101 | ], 102 | ); 103 | }))); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: ion_flutter_example 2 | description: A new Flutter project. 3 | version: 1.0.0+1 4 | 5 | environment: 6 | sdk: ">=2.12.0 <3.0.0" 7 | 8 | dependencies: 9 | cupertino_icons: ^1.0.0 10 | flutter: 11 | sdk: flutter 12 | flutter_icons: ^1.0.0 13 | flutter_ion: 14 | path: ../ 15 | 16 | dev_dependencies: 17 | flutter_test: 18 | sdk: flutter 19 | 20 | flutter: 21 | uses-material-design: true 22 | -------------------------------------------------------------------------------- /example/scripts/add-line.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: UTF-8 -*- 3 | 4 | import sys 5 | import getopt 6 | import re 7 | 8 | 9 | def findLine(pattern, fp): 10 | line = fp.readline() 11 | line_number = 1 12 | while line: 13 | #print("Line {}: {}".format(line_number, line.strip())) 14 | if pattern in line: 15 | return line_number 16 | line = fp.readline() 17 | line_number += 1 18 | return -1 19 | 20 | def insertBefore(filename, pattern, text): 21 | with open(filename, 'r+') as fp: 22 | line_number = findLine(pattern, fp) 23 | if(line_number > 0): 24 | print 'Insert', text,'to line', line_number 25 | fp.seek(0) 26 | lines = fp.readlines() 27 | fp.seek(0) 28 | lines.insert(line_number - 1, text + '\n') 29 | fp.writelines(lines) 30 | return 31 | print 'pattern',text,'not found!' 32 | 33 | def replaceText(filename, pattern, text): 34 | with open(filename, 'r') as fp: 35 | lines = fp.read() 36 | fp.close() 37 | lines = (re.sub(pattern, text, lines)) 38 | print 'Replace', pattern ,'to', text 39 | fp = open(filename, 'w') 40 | fp.write(lines) 41 | fp.close() 42 | 43 | def main(argv): 44 | inputfile = '' 45 | string = '' 46 | text = '' 47 | replace = False 48 | try: 49 | opts, args = getopt.getopt(argv, "hi:s:t:r") 50 | except getopt.GetoptError: 51 | print 'add-line.py -i -s -t ' 52 | sys.exit(2) 53 | for opt, arg in opts: 54 | if opt == '-h': 55 | print 'add-line.py -i -s -t ' 56 | sys.exit() 57 | elif opt in ("-i"): 58 | inputfile = arg 59 | elif opt in ("-s"): 60 | string = arg 61 | elif opt in ("-t"): 62 | text = arg 63 | elif opt in ("-r"): 64 | replace = True 65 | if(replace): 66 | replaceText(inputfile, string, text) 67 | else: 68 | insertBefore(inputfile, string, text) 69 | 70 | if __name__ == "__main__": 71 | main(sys.argv[1:]) 72 | -------------------------------------------------------------------------------- /example/scripts/project_tools.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | FLUTTER_APP_FOLDER=$(cd `dirname $0`/../; pwd) 4 | FLUTTER_APP_ORG=com.github.pion.ion 5 | FLUTTER_APP_PROJECT_NAME=ion_flutter_example 6 | CMD=$1 7 | 8 | function cleanup() { 9 | echo "Cleanup project [$FLUTTER_APP_PROJECT_NAME] files ..." 10 | cd $FLUTTER_APP_FOLDER 11 | rm -rf android build *.iml ios pubspec.lock test .flutter-plugins .metadata .packages .idea macos web 12 | } 13 | 14 | function create() { 15 | cd $FLUTTER_APP_FOLDER 16 | if [ ! -d "ios" ] && [ ! -d "android" ] && [ ! -d "macos" ]; then 17 | echo "Create flutter project: name=$FLUTTER_APP_PROJECT_NAME, org=$FLUTTER_APP_ORG ..." 18 | flutter config --enable-macos-desktop 19 | flutter config --enable-web 20 | flutter create --android-language java --ios-language objc --project-name $FLUTTER_APP_PROJECT_NAME --org $FLUTTER_APP_ORG . 21 | add_permission_label 22 | else 23 | echo "Project [$FLUTTER_APP_PROJECT_NAME] already exists!" 24 | fi 25 | } 26 | 27 | function add_permission_label() { 28 | cd $FLUTTER_APP_FOLDER/scripts 29 | echo "" 30 | echo "Add permission labels to iOS." 31 | echo "" 32 | python add-line.py -i ../ios/Runner/Info.plist -s 'UILaunchStoryboardName' -t ' NSCameraUsageDescription' 33 | python add-line.py -i ../ios/Runner/Info.plist -s 'UILaunchStoryboardName' -t ' $(PRODUCT_NAME) Camera Usage!' 34 | python add-line.py -i ../ios/Runner/Info.plist -s 'UILaunchStoryboardName' -t ' NSMicrophoneUsageDescription' 35 | python add-line.py -i ../ios/Runner/Info.plist -s 'UILaunchStoryboardName' -t ' $(PRODUCT_NAME) Microphone Usage!' 36 | python add-line.py -i ../ios/Podfile -s "# platform :ios" -t "platform :ios" -r 37 | echo "" 38 | echo "Add permission labels to AndroidManifest.xml." 39 | echo "" 40 | python add-line.py -i ../android/app/build.gradle -s 'minSdkVersion 16' -t 'minSdkVersion 21' -r 41 | python add-line.py -i ../android/app/src/main/AndroidManifest.xml -s "' 42 | python add-line.py -i ../android/app/src/main/AndroidManifest.xml -s "' 43 | python add-line.py -i ../android/app/src/main/AndroidManifest.xml -s "' 44 | python add-line.py -i ../android/app/src/main/AndroidManifest.xml -s "' 45 | python add-line.py -i ../android/app/src/main/AndroidManifest.xml -s "' 46 | python add-line.py -i ../android/app/src/main/AndroidManifest.xml -s "' 47 | python add-line.py -i ../android/app/src/main/AndroidManifest.xml -s "' 48 | echo "" 49 | echo "Add permission labels to macOS." 50 | echo "" 51 | python add-line.py -i ../macos/Runner/Info.plist -s 'CFBundleShortVersionString' -t ' NSCameraUsageDescription' 52 | python add-line.py -i ../macos/Runner/Info.plist -s 'CFBundleShortVersionString' -t ' $(PRODUCT_NAME) Camera Usage!' 53 | python add-line.py -i ../macos/Runner/Info.plist -s 'CFBundleShortVersionString' -t ' NSMicrophoneUsageDescription' 54 | python add-line.py -i ../macos/Runner/Info.plist -s 'CFBundleShortVersionString' -t ' $(PRODUCT_NAME) Microphone Usage!' 55 | 56 | python add-line.py -i ../macos/Runner/DebugProfile.entitlements -s '' -t ' com.apple.security.device.camera' 57 | python add-line.py -i ../macos/Runner/DebugProfile.entitlements -s '' -t ' ' 58 | python add-line.py -i ../macos/Runner/DebugProfile.entitlements -s '' -t ' com.apple.security.device.microphone' 59 | python add-line.py -i ../macos/Runner/DebugProfile.entitlements -s '' -t ' ' 60 | python add-line.py -i ../macos/Runner/DebugProfile.entitlements -s '' -t ' com.apple.security.network.client' 61 | python add-line.py -i ../macos/Runner/DebugProfile.entitlements -s '' -t ' ' 62 | 63 | python add-line.py -i ../macos/Runner/Release.entitlements -s '' -t ' com.apple.security.device.camera' 64 | python add-line.py -i ../macos/Runner/Release.entitlements -s '' -t ' ' 65 | python add-line.py -i ../macos/Runner/Release.entitlements -s '' -t ' com.apple.security.device.microphone' 66 | python add-line.py -i ../macos/Runner/Release.entitlements -s '' -t ' ' 67 | python add-line.py -i ../macos/Runner/Release.entitlements -s '' -t ' com.apple.security.network.client' 68 | python add-line.py -i ../macos/Runner/Release.entitlements -s '' -t ' ' 69 | } 70 | 71 | if [ "$CMD" == "create" ]; 72 | then 73 | create 74 | fi 75 | 76 | if [ "$CMD" == "cleanup" ]; 77 | then 78 | cleanup 79 | fi 80 | 81 | if [ "$CMD" == "add_permission" ]; 82 | then 83 | add_permission_label 84 | fi 85 | 86 | if [ ! -n "$1" ] ;then 87 | echo "Usage: ./project_tools.sh 'create' | 'cleanup'" 88 | fi 89 | -------------------------------------------------------------------------------- /example/test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter widget test. 2 | // 3 | // To perform an interaction with a widget in your test, use the WidgetTester 4 | // utility that Flutter provides. For example, you can send tap and scroll 5 | // gestures. You can also use WidgetTester to find child widgets in the widget 6 | // tree, read text, and verify that the values of widget properties are correct. 7 | 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter_test/flutter_test.dart'; 10 | 11 | import 'package:ion_flutter_example/main.dart'; 12 | 13 | void main() { 14 | testWidgets('Counter increments smoke test', (WidgetTester tester) async { 15 | // Build our app and trigger a frame. 16 | await tester.pumpWidget(MyApp()); 17 | 18 | // Verify that our counter starts at 0. 19 | expect(find.text('0'), findsOneWidget); 20 | expect(find.text('1'), findsNothing); 21 | 22 | // Tap the '+' icon and trigger a frame. 23 | await tester.tap(find.byIcon(Icons.add)); 24 | await tester.pump(); 25 | 26 | // Verify that our counter has incremented. 27 | expect(find.text('0'), findsNothing); 28 | expect(find.text('1'), findsOneWidget); 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /flutter-ion.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "." 5 | }, 6 | { 7 | "path": "example", 8 | "name": "example" 9 | } 10 | ], 11 | } -------------------------------------------------------------------------------- /lib/flutter_ion.dart: -------------------------------------------------------------------------------- 1 | export 'src/client.dart'; 2 | export 'src/connector/ion.dart'; 3 | export 'src/connector/room.dart'; 4 | export 'src/connector/rtc.dart'; 5 | export 'src/signal/signal.dart'; 6 | export 'src/signal/signal_grpc_impl.dart'; 7 | export 'src/signal/signal_jsonrpc_impl.dart'; 8 | export 'src/stream.dart'; 9 | -------------------------------------------------------------------------------- /lib/src/_library/apps/room/proto/room.pbenum.dart: -------------------------------------------------------------------------------- 1 | /// 2 | // Generated code. Do not modify. 3 | // source: apps/room/proto/room.proto 4 | // 5 | // @dart = 2.12 6 | // ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields 7 | 8 | // ignore_for_file: UNDEFINED_SHOWN_NAME 9 | import 'dart:core' as $core; 10 | import 'package:protobuf/protobuf.dart' as $pb; 11 | 12 | class ErrorType extends $pb.ProtobufEnum { 13 | static const ErrorType None = ErrorType._( 14 | 0, 15 | const $core.bool.fromEnvironment('protobuf.omit_enum_names') 16 | ? '' 17 | : 'None'); 18 | static const ErrorType UnkownError = ErrorType._( 19 | 1, 20 | const $core.bool.fromEnvironment('protobuf.omit_enum_names') 21 | ? '' 22 | : 'UnkownError'); 23 | static const ErrorType PermissionDenied = ErrorType._( 24 | 2, 25 | const $core.bool.fromEnvironment('protobuf.omit_enum_names') 26 | ? '' 27 | : 'PermissionDenied'); 28 | static const ErrorType ServiceUnavailable = ErrorType._( 29 | 3, 30 | const $core.bool.fromEnvironment('protobuf.omit_enum_names') 31 | ? '' 32 | : 'ServiceUnavailable'); 33 | static const ErrorType RoomLocked = ErrorType._( 34 | 4, 35 | const $core.bool.fromEnvironment('protobuf.omit_enum_names') 36 | ? '' 37 | : 'RoomLocked'); 38 | static const ErrorType PasswordRequired = ErrorType._( 39 | 5, 40 | const $core.bool.fromEnvironment('protobuf.omit_enum_names') 41 | ? '' 42 | : 'PasswordRequired'); 43 | static const ErrorType RoomAlreadyExist = ErrorType._( 44 | 6, 45 | const $core.bool.fromEnvironment('protobuf.omit_enum_names') 46 | ? '' 47 | : 'RoomAlreadyExist'); 48 | static const ErrorType RoomNotExist = ErrorType._( 49 | 7, 50 | const $core.bool.fromEnvironment('protobuf.omit_enum_names') 51 | ? '' 52 | : 'RoomNotExist'); 53 | static const ErrorType InvalidParams = ErrorType._( 54 | 8, 55 | const $core.bool.fromEnvironment('protobuf.omit_enum_names') 56 | ? '' 57 | : 'InvalidParams'); 58 | static const ErrorType PeerAlreadyExist = ErrorType._( 59 | 9, 60 | const $core.bool.fromEnvironment('protobuf.omit_enum_names') 61 | ? '' 62 | : 'PeerAlreadyExist'); 63 | static const ErrorType PeerNotExist = ErrorType._( 64 | 10, 65 | const $core.bool.fromEnvironment('protobuf.omit_enum_names') 66 | ? '' 67 | : 'PeerNotExist'); 68 | 69 | static const $core.List values = [ 70 | None, 71 | UnkownError, 72 | PermissionDenied, 73 | ServiceUnavailable, 74 | RoomLocked, 75 | PasswordRequired, 76 | RoomAlreadyExist, 77 | RoomNotExist, 78 | InvalidParams, 79 | PeerAlreadyExist, 80 | PeerNotExist, 81 | ]; 82 | 83 | static final $core.Map<$core.int, ErrorType> _byValue = 84 | $pb.ProtobufEnum.initByValue(values); 85 | static ErrorType? valueOf($core.int value) => _byValue[value]; 86 | 87 | const ErrorType._($core.int v, $core.String n) : super(v, n); 88 | } 89 | 90 | class Role extends $pb.ProtobufEnum { 91 | static const Role Host = Role._( 92 | 0, 93 | const $core.bool.fromEnvironment('protobuf.omit_enum_names') 94 | ? '' 95 | : 'Host'); 96 | static const Role Guest = Role._( 97 | 1, 98 | const $core.bool.fromEnvironment('protobuf.omit_enum_names') 99 | ? '' 100 | : 'Guest'); 101 | 102 | static const $core.List values = [ 103 | Host, 104 | Guest, 105 | ]; 106 | 107 | static final $core.Map<$core.int, Role> _byValue = 108 | $pb.ProtobufEnum.initByValue(values); 109 | static Role? valueOf($core.int value) => _byValue[value]; 110 | 111 | const Role._($core.int v, $core.String n) : super(v, n); 112 | } 113 | 114 | class Protocol extends $pb.ProtobufEnum { 115 | static const Protocol ProtocolUnknown = Protocol._( 116 | 0, 117 | const $core.bool.fromEnvironment('protobuf.omit_enum_names') 118 | ? '' 119 | : 'ProtocolUnknown'); 120 | static const Protocol WebRTC = Protocol._( 121 | 1, 122 | const $core.bool.fromEnvironment('protobuf.omit_enum_names') 123 | ? '' 124 | : 'WebRTC'); 125 | static const Protocol SIP = Protocol._( 126 | 2, 127 | const $core.bool.fromEnvironment('protobuf.omit_enum_names') 128 | ? '' 129 | : 'SIP'); 130 | static const Protocol RTMP = Protocol._( 131 | 3, 132 | const $core.bool.fromEnvironment('protobuf.omit_enum_names') 133 | ? '' 134 | : 'RTMP'); 135 | static const Protocol RTSP = Protocol._( 136 | 4, 137 | const $core.bool.fromEnvironment('protobuf.omit_enum_names') 138 | ? '' 139 | : 'RTSP'); 140 | 141 | static const $core.List values = [ 142 | ProtocolUnknown, 143 | WebRTC, 144 | SIP, 145 | RTMP, 146 | RTSP, 147 | ]; 148 | 149 | static final $core.Map<$core.int, Protocol> _byValue = 150 | $pb.ProtobufEnum.initByValue(values); 151 | static Protocol? valueOf($core.int value) => _byValue[value]; 152 | 153 | const Protocol._($core.int v, $core.String n) : super(v, n); 154 | } 155 | 156 | class PeerState extends $pb.ProtobufEnum { 157 | static const PeerState JOIN = PeerState._( 158 | 0, 159 | const $core.bool.fromEnvironment('protobuf.omit_enum_names') 160 | ? '' 161 | : 'JOIN'); 162 | static const PeerState UPDATE = PeerState._( 163 | 1, 164 | const $core.bool.fromEnvironment('protobuf.omit_enum_names') 165 | ? '' 166 | : 'UPDATE'); 167 | static const PeerState LEAVE = PeerState._( 168 | 2, 169 | const $core.bool.fromEnvironment('protobuf.omit_enum_names') 170 | ? '' 171 | : 'LEAVE'); 172 | 173 | static const $core.List values = [ 174 | JOIN, 175 | UPDATE, 176 | LEAVE, 177 | ]; 178 | 179 | static final $core.Map<$core.int, PeerState> _byValue = 180 | $pb.ProtobufEnum.initByValue(values); 181 | static PeerState? valueOf($core.int value) => _byValue[value]; 182 | 183 | const PeerState._($core.int v, $core.String n) : super(v, n); 184 | } 185 | 186 | class Peer_Direction extends $pb.ProtobufEnum { 187 | static const Peer_Direction INCOMING = Peer_Direction._( 188 | 0, 189 | const $core.bool.fromEnvironment('protobuf.omit_enum_names') 190 | ? '' 191 | : 'INCOMING'); 192 | static const Peer_Direction OUTGOING = Peer_Direction._( 193 | 1, 194 | const $core.bool.fromEnvironment('protobuf.omit_enum_names') 195 | ? '' 196 | : 'OUTGOING'); 197 | static const Peer_Direction BILATERAL = Peer_Direction._( 198 | 2, 199 | const $core.bool.fromEnvironment('protobuf.omit_enum_names') 200 | ? '' 201 | : 'BILATERAL'); 202 | 203 | static const $core.List values = [ 204 | INCOMING, 205 | OUTGOING, 206 | BILATERAL, 207 | ]; 208 | 209 | static final $core.Map<$core.int, Peer_Direction> _byValue = 210 | $pb.ProtobufEnum.initByValue(values); 211 | static Peer_Direction? valueOf($core.int value) => _byValue[value]; 212 | 213 | const Peer_Direction._($core.int v, $core.String n) : super(v, n); 214 | } 215 | -------------------------------------------------------------------------------- /lib/src/_library/apps/room/proto/room.pbgrpc.dart: -------------------------------------------------------------------------------- 1 | /// 2 | // Generated code. Do not modify. 3 | // source: apps/room/proto/room.proto 4 | // 5 | // @dart = 2.12 6 | // ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields 7 | 8 | import 'dart:async' as $async; 9 | 10 | import 'dart:core' as $core; 11 | 12 | import 'package:grpc/service_api.dart' as $grpc; 13 | import 'room.pb.dart' as $0; 14 | export 'room.pb.dart'; 15 | 16 | class RoomServiceClient extends $grpc.Client { 17 | static final _$createRoom = 18 | $grpc.ClientMethod<$0.CreateRoomRequest, $0.CreateRoomReply>( 19 | '/room.RoomService/CreateRoom', 20 | ($0.CreateRoomRequest value) => value.writeToBuffer(), 21 | ($core.List<$core.int> value) => 22 | $0.CreateRoomReply.fromBuffer(value)); 23 | static final _$updateRoom = 24 | $grpc.ClientMethod<$0.UpdateRoomRequest, $0.UpdateRoomReply>( 25 | '/room.RoomService/UpdateRoom', 26 | ($0.UpdateRoomRequest value) => value.writeToBuffer(), 27 | ($core.List<$core.int> value) => 28 | $0.UpdateRoomReply.fromBuffer(value)); 29 | static final _$endRoom = 30 | $grpc.ClientMethod<$0.EndRoomRequest, $0.EndRoomReply>( 31 | '/room.RoomService/EndRoom', 32 | ($0.EndRoomRequest value) => value.writeToBuffer(), 33 | ($core.List<$core.int> value) => $0.EndRoomReply.fromBuffer(value)); 34 | static final _$getRooms = 35 | $grpc.ClientMethod<$0.GetRoomsRequest, $0.GetRoomsReply>( 36 | '/room.RoomService/GetRooms', 37 | ($0.GetRoomsRequest value) => value.writeToBuffer(), 38 | ($core.List<$core.int> value) => $0.GetRoomsReply.fromBuffer(value)); 39 | static final _$addPeer = 40 | $grpc.ClientMethod<$0.AddPeerRequest, $0.AddPeerReply>( 41 | '/room.RoomService/AddPeer', 42 | ($0.AddPeerRequest value) => value.writeToBuffer(), 43 | ($core.List<$core.int> value) => $0.AddPeerReply.fromBuffer(value)); 44 | static final _$updatePeer = 45 | $grpc.ClientMethod<$0.UpdatePeerRequest, $0.UpdatePeerReply>( 46 | '/room.RoomService/UpdatePeer', 47 | ($0.UpdatePeerRequest value) => value.writeToBuffer(), 48 | ($core.List<$core.int> value) => 49 | $0.UpdatePeerReply.fromBuffer(value)); 50 | static final _$removePeer = 51 | $grpc.ClientMethod<$0.RemovePeerRequest, $0.RemovePeerReply>( 52 | '/room.RoomService/RemovePeer', 53 | ($0.RemovePeerRequest value) => value.writeToBuffer(), 54 | ($core.List<$core.int> value) => 55 | $0.RemovePeerReply.fromBuffer(value)); 56 | static final _$getPeers = 57 | $grpc.ClientMethod<$0.GetPeersRequest, $0.GetPeersReply>( 58 | '/room.RoomService/GetPeers', 59 | ($0.GetPeersRequest value) => value.writeToBuffer(), 60 | ($core.List<$core.int> value) => $0.GetPeersReply.fromBuffer(value)); 61 | 62 | RoomServiceClient($grpc.ClientChannel channel, 63 | {$grpc.CallOptions? options, 64 | $core.Iterable<$grpc.ClientInterceptor>? interceptors}) 65 | : super(channel, options: options, interceptors: interceptors); 66 | 67 | $grpc.ResponseFuture<$0.CreateRoomReply> createRoom( 68 | $0.CreateRoomRequest request, 69 | {$grpc.CallOptions? options}) { 70 | return $createUnaryCall(_$createRoom, request, options: options); 71 | } 72 | 73 | $grpc.ResponseFuture<$0.UpdateRoomReply> updateRoom( 74 | $0.UpdateRoomRequest request, 75 | {$grpc.CallOptions? options}) { 76 | return $createUnaryCall(_$updateRoom, request, options: options); 77 | } 78 | 79 | $grpc.ResponseFuture<$0.EndRoomReply> endRoom($0.EndRoomRequest request, 80 | {$grpc.CallOptions? options}) { 81 | return $createUnaryCall(_$endRoom, request, options: options); 82 | } 83 | 84 | $grpc.ResponseFuture<$0.GetRoomsReply> getRooms($0.GetRoomsRequest request, 85 | {$grpc.CallOptions? options}) { 86 | return $createUnaryCall(_$getRooms, request, options: options); 87 | } 88 | 89 | $grpc.ResponseFuture<$0.AddPeerReply> addPeer($0.AddPeerRequest request, 90 | {$grpc.CallOptions? options}) { 91 | return $createUnaryCall(_$addPeer, request, options: options); 92 | } 93 | 94 | $grpc.ResponseFuture<$0.UpdatePeerReply> updatePeer( 95 | $0.UpdatePeerRequest request, 96 | {$grpc.CallOptions? options}) { 97 | return $createUnaryCall(_$updatePeer, request, options: options); 98 | } 99 | 100 | $grpc.ResponseFuture<$0.RemovePeerReply> removePeer( 101 | $0.RemovePeerRequest request, 102 | {$grpc.CallOptions? options}) { 103 | return $createUnaryCall(_$removePeer, request, options: options); 104 | } 105 | 106 | $grpc.ResponseFuture<$0.GetPeersReply> getPeers($0.GetPeersRequest request, 107 | {$grpc.CallOptions? options}) { 108 | return $createUnaryCall(_$getPeers, request, options: options); 109 | } 110 | } 111 | 112 | abstract class RoomServiceBase extends $grpc.Service { 113 | $core.String get $name => 'room.RoomService'; 114 | 115 | RoomServiceBase() { 116 | $addMethod($grpc.ServiceMethod<$0.CreateRoomRequest, $0.CreateRoomReply>( 117 | 'CreateRoom', 118 | createRoom_Pre, 119 | false, 120 | false, 121 | ($core.List<$core.int> value) => $0.CreateRoomRequest.fromBuffer(value), 122 | ($0.CreateRoomReply value) => value.writeToBuffer())); 123 | $addMethod($grpc.ServiceMethod<$0.UpdateRoomRequest, $0.UpdateRoomReply>( 124 | 'UpdateRoom', 125 | updateRoom_Pre, 126 | false, 127 | false, 128 | ($core.List<$core.int> value) => $0.UpdateRoomRequest.fromBuffer(value), 129 | ($0.UpdateRoomReply value) => value.writeToBuffer())); 130 | $addMethod($grpc.ServiceMethod<$0.EndRoomRequest, $0.EndRoomReply>( 131 | 'EndRoom', 132 | endRoom_Pre, 133 | false, 134 | false, 135 | ($core.List<$core.int> value) => $0.EndRoomRequest.fromBuffer(value), 136 | ($0.EndRoomReply value) => value.writeToBuffer())); 137 | $addMethod($grpc.ServiceMethod<$0.GetRoomsRequest, $0.GetRoomsReply>( 138 | 'GetRooms', 139 | getRooms_Pre, 140 | false, 141 | false, 142 | ($core.List<$core.int> value) => $0.GetRoomsRequest.fromBuffer(value), 143 | ($0.GetRoomsReply value) => value.writeToBuffer())); 144 | $addMethod($grpc.ServiceMethod<$0.AddPeerRequest, $0.AddPeerReply>( 145 | 'AddPeer', 146 | addPeer_Pre, 147 | false, 148 | false, 149 | ($core.List<$core.int> value) => $0.AddPeerRequest.fromBuffer(value), 150 | ($0.AddPeerReply value) => value.writeToBuffer())); 151 | $addMethod($grpc.ServiceMethod<$0.UpdatePeerRequest, $0.UpdatePeerReply>( 152 | 'UpdatePeer', 153 | updatePeer_Pre, 154 | false, 155 | false, 156 | ($core.List<$core.int> value) => $0.UpdatePeerRequest.fromBuffer(value), 157 | ($0.UpdatePeerReply value) => value.writeToBuffer())); 158 | $addMethod($grpc.ServiceMethod<$0.RemovePeerRequest, $0.RemovePeerReply>( 159 | 'RemovePeer', 160 | removePeer_Pre, 161 | false, 162 | false, 163 | ($core.List<$core.int> value) => $0.RemovePeerRequest.fromBuffer(value), 164 | ($0.RemovePeerReply value) => value.writeToBuffer())); 165 | $addMethod($grpc.ServiceMethod<$0.GetPeersRequest, $0.GetPeersReply>( 166 | 'GetPeers', 167 | getPeers_Pre, 168 | false, 169 | false, 170 | ($core.List<$core.int> value) => $0.GetPeersRequest.fromBuffer(value), 171 | ($0.GetPeersReply value) => value.writeToBuffer())); 172 | } 173 | 174 | $async.Future<$0.CreateRoomReply> createRoom_Pre($grpc.ServiceCall call, 175 | $async.Future<$0.CreateRoomRequest> request) async { 176 | return createRoom(call, await request); 177 | } 178 | 179 | $async.Future<$0.UpdateRoomReply> updateRoom_Pre($grpc.ServiceCall call, 180 | $async.Future<$0.UpdateRoomRequest> request) async { 181 | return updateRoom(call, await request); 182 | } 183 | 184 | $async.Future<$0.EndRoomReply> endRoom_Pre( 185 | $grpc.ServiceCall call, $async.Future<$0.EndRoomRequest> request) async { 186 | return endRoom(call, await request); 187 | } 188 | 189 | $async.Future<$0.GetRoomsReply> getRooms_Pre( 190 | $grpc.ServiceCall call, $async.Future<$0.GetRoomsRequest> request) async { 191 | return getRooms(call, await request); 192 | } 193 | 194 | $async.Future<$0.AddPeerReply> addPeer_Pre( 195 | $grpc.ServiceCall call, $async.Future<$0.AddPeerRequest> request) async { 196 | return addPeer(call, await request); 197 | } 198 | 199 | $async.Future<$0.UpdatePeerReply> updatePeer_Pre($grpc.ServiceCall call, 200 | $async.Future<$0.UpdatePeerRequest> request) async { 201 | return updatePeer(call, await request); 202 | } 203 | 204 | $async.Future<$0.RemovePeerReply> removePeer_Pre($grpc.ServiceCall call, 205 | $async.Future<$0.RemovePeerRequest> request) async { 206 | return removePeer(call, await request); 207 | } 208 | 209 | $async.Future<$0.GetPeersReply> getPeers_Pre( 210 | $grpc.ServiceCall call, $async.Future<$0.GetPeersRequest> request) async { 211 | return getPeers(call, await request); 212 | } 213 | 214 | $async.Future<$0.CreateRoomReply> createRoom( 215 | $grpc.ServiceCall call, $0.CreateRoomRequest request); 216 | $async.Future<$0.UpdateRoomReply> updateRoom( 217 | $grpc.ServiceCall call, $0.UpdateRoomRequest request); 218 | $async.Future<$0.EndRoomReply> endRoom( 219 | $grpc.ServiceCall call, $0.EndRoomRequest request); 220 | $async.Future<$0.GetRoomsReply> getRooms( 221 | $grpc.ServiceCall call, $0.GetRoomsRequest request); 222 | $async.Future<$0.AddPeerReply> addPeer( 223 | $grpc.ServiceCall call, $0.AddPeerRequest request); 224 | $async.Future<$0.UpdatePeerReply> updatePeer( 225 | $grpc.ServiceCall call, $0.UpdatePeerRequest request); 226 | $async.Future<$0.RemovePeerReply> removePeer( 227 | $grpc.ServiceCall call, $0.RemovePeerRequest request); 228 | $async.Future<$0.GetPeersReply> getPeers( 229 | $grpc.ServiceCall call, $0.GetPeersRequest request); 230 | } 231 | 232 | class RoomSignalClient extends $grpc.Client { 233 | static final _$signal = $grpc.ClientMethod<$0.Request, $0.Reply>( 234 | '/room.RoomSignal/Signal', 235 | ($0.Request value) => value.writeToBuffer(), 236 | ($core.List<$core.int> value) => $0.Reply.fromBuffer(value)); 237 | 238 | RoomSignalClient($grpc.ClientChannel channel, 239 | {$grpc.CallOptions? options, 240 | $core.Iterable<$grpc.ClientInterceptor>? interceptors}) 241 | : super(channel, options: options, interceptors: interceptors); 242 | 243 | $grpc.ResponseStream<$0.Reply> signal($async.Stream<$0.Request> request, 244 | {$grpc.CallOptions? options}) { 245 | return $createStreamingCall(_$signal, request, options: options); 246 | } 247 | } 248 | 249 | abstract class RoomSignalServiceBase extends $grpc.Service { 250 | $core.String get $name => 'room.RoomSignal'; 251 | 252 | RoomSignalServiceBase() { 253 | $addMethod($grpc.ServiceMethod<$0.Request, $0.Reply>( 254 | 'Signal', 255 | signal, 256 | true, 257 | true, 258 | ($core.List<$core.int> value) => $0.Request.fromBuffer(value), 259 | ($0.Reply value) => value.writeToBuffer())); 260 | } 261 | 262 | $async.Stream<$0.Reply> signal( 263 | $grpc.ServiceCall call, $async.Stream<$0.Request> request); 264 | } 265 | -------------------------------------------------------------------------------- /lib/src/_library/apps/room/proto/room.pbjson.dart: -------------------------------------------------------------------------------- 1 | /// 2 | // Generated code. Do not modify. 3 | // source: apps/room/proto/room.proto 4 | // 5 | // @dart = 2.12 6 | // ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields,deprecated_member_use_from_same_package 7 | 8 | import 'dart:core' as $core; 9 | import 'dart:convert' as $convert; 10 | import 'dart:typed_data' as $typed_data; 11 | 12 | @$core.Deprecated('Use errorTypeDescriptor instead') 13 | const ErrorType$json = const { 14 | '1': 'ErrorType', 15 | '2': const [ 16 | const {'1': 'None', '2': 0}, 17 | const {'1': 'UnkownError', '2': 1}, 18 | const {'1': 'PermissionDenied', '2': 2}, 19 | const {'1': 'ServiceUnavailable', '2': 3}, 20 | const {'1': 'RoomLocked', '2': 4}, 21 | const {'1': 'PasswordRequired', '2': 5}, 22 | const {'1': 'RoomAlreadyExist', '2': 6}, 23 | const {'1': 'RoomNotExist', '2': 7}, 24 | const {'1': 'InvalidParams', '2': 8}, 25 | const {'1': 'PeerAlreadyExist', '2': 9}, 26 | const {'1': 'PeerNotExist', '2': 10}, 27 | ], 28 | }; 29 | 30 | /// Descriptor for `ErrorType`. Decode as a `google.protobuf.EnumDescriptorProto`. 31 | final $typed_data.Uint8List errorTypeDescriptor = $convert.base64Decode( 32 | 'CglFcnJvclR5cGUSCAoETm9uZRAAEg8KC1Vua293bkVycm9yEAESFAoQUGVybWlzc2lvbkRlbmllZBACEhYKElNlcnZpY2VVbmF2YWlsYWJsZRADEg4KClJvb21Mb2NrZWQQBBIUChBQYXNzd29yZFJlcXVpcmVkEAUSFAoQUm9vbUFscmVhZHlFeGlzdBAGEhAKDFJvb21Ob3RFeGlzdBAHEhEKDUludmFsaWRQYXJhbXMQCBIUChBQZWVyQWxyZWFkeUV4aXN0EAkSEAoMUGVlck5vdEV4aXN0EAo='); 33 | @$core.Deprecated('Use roleDescriptor instead') 34 | const Role$json = const { 35 | '1': 'Role', 36 | '2': const [ 37 | const {'1': 'Host', '2': 0}, 38 | const {'1': 'Guest', '2': 1}, 39 | ], 40 | }; 41 | 42 | /// Descriptor for `Role`. Decode as a `google.protobuf.EnumDescriptorProto`. 43 | final $typed_data.Uint8List roleDescriptor = 44 | $convert.base64Decode('CgRSb2xlEggKBEhvc3QQABIJCgVHdWVzdBAB'); 45 | @$core.Deprecated('Use protocolDescriptor instead') 46 | const Protocol$json = const { 47 | '1': 'Protocol', 48 | '2': const [ 49 | const {'1': 'ProtocolUnknown', '2': 0}, 50 | const {'1': 'WebRTC', '2': 1}, 51 | const {'1': 'SIP', '2': 2}, 52 | const {'1': 'RTMP', '2': 3}, 53 | const {'1': 'RTSP', '2': 4}, 54 | ], 55 | }; 56 | 57 | /// Descriptor for `Protocol`. Decode as a `google.protobuf.EnumDescriptorProto`. 58 | final $typed_data.Uint8List protocolDescriptor = $convert.base64Decode( 59 | 'CghQcm90b2NvbBITCg9Qcm90b2NvbFVua25vd24QABIKCgZXZWJSVEMQARIHCgNTSVAQAhIICgRSVE1QEAMSCAoEUlRTUBAE'); 60 | @$core.Deprecated('Use peerStateDescriptor instead') 61 | const PeerState$json = const { 62 | '1': 'PeerState', 63 | '2': const [ 64 | const {'1': 'JOIN', '2': 0}, 65 | const {'1': 'UPDATE', '2': 1}, 66 | const {'1': 'LEAVE', '2': 2}, 67 | ], 68 | }; 69 | 70 | /// Descriptor for `PeerState`. Decode as a `google.protobuf.EnumDescriptorProto`. 71 | final $typed_data.Uint8List peerStateDescriptor = $convert.base64Decode( 72 | 'CglQZWVyU3RhdGUSCAoESk9JThAAEgoKBlVQREFURRABEgkKBUxFQVZFEAI='); 73 | @$core.Deprecated('Use errorDescriptor instead') 74 | const Error$json = const { 75 | '1': 'Error', 76 | '2': const [ 77 | const { 78 | '1': 'code', 79 | '3': 1, 80 | '4': 1, 81 | '5': 14, 82 | '6': '.room.ErrorType', 83 | '10': 'code' 84 | }, 85 | const {'1': 'reason', '3': 2, '4': 1, '5': 9, '10': 'reason'}, 86 | ], 87 | }; 88 | 89 | /// Descriptor for `Error`. Decode as a `google.protobuf.DescriptorProto`. 90 | final $typed_data.Uint8List errorDescriptor = $convert.base64Decode( 91 | 'CgVFcnJvchIjCgRjb2RlGAEgASgOMg8ucm9vbS5FcnJvclR5cGVSBGNvZGUSFgoGcmVhc29uGAIgASgJUgZyZWFzb24='); 92 | @$core.Deprecated('Use requestDescriptor instead') 93 | const Request$json = const { 94 | '1': 'Request', 95 | '2': const [ 96 | const { 97 | '1': 'join', 98 | '3': 1, 99 | '4': 1, 100 | '5': 11, 101 | '6': '.room.JoinRequest', 102 | '9': 0, 103 | '10': 'join' 104 | }, 105 | const { 106 | '1': 'leave', 107 | '3': 2, 108 | '4': 1, 109 | '5': 11, 110 | '6': '.room.LeaveRequest', 111 | '9': 0, 112 | '10': 'leave' 113 | }, 114 | const { 115 | '1': 'sendMessage', 116 | '3': 3, 117 | '4': 1, 118 | '5': 11, 119 | '6': '.room.SendMessageRequest', 120 | '9': 0, 121 | '10': 'sendMessage' 122 | }, 123 | ], 124 | '8': const [ 125 | const {'1': 'payload'}, 126 | ], 127 | }; 128 | 129 | /// Descriptor for `Request`. Decode as a `google.protobuf.DescriptorProto`. 130 | final $typed_data.Uint8List requestDescriptor = $convert.base64Decode( 131 | 'CgdSZXF1ZXN0EicKBGpvaW4YASABKAsyES5yb29tLkpvaW5SZXF1ZXN0SABSBGpvaW4SKgoFbGVhdmUYAiABKAsyEi5yb29tLkxlYXZlUmVxdWVzdEgAUgVsZWF2ZRI8CgtzZW5kTWVzc2FnZRgDIAEoCzIYLnJvb20uU2VuZE1lc3NhZ2VSZXF1ZXN0SABSC3NlbmRNZXNzYWdlQgkKB3BheWxvYWQ='); 132 | @$core.Deprecated('Use replyDescriptor instead') 133 | const Reply$json = const { 134 | '1': 'Reply', 135 | '2': const [ 136 | const { 137 | '1': 'join', 138 | '3': 1, 139 | '4': 1, 140 | '5': 11, 141 | '6': '.room.JoinReply', 142 | '9': 0, 143 | '10': 'join' 144 | }, 145 | const { 146 | '1': 'leave', 147 | '3': 2, 148 | '4': 1, 149 | '5': 11, 150 | '6': '.room.LeaveReply', 151 | '9': 0, 152 | '10': 'leave' 153 | }, 154 | const { 155 | '1': 'sendMessage', 156 | '3': 3, 157 | '4': 1, 158 | '5': 11, 159 | '6': '.room.SendMessageReply', 160 | '9': 0, 161 | '10': 'sendMessage' 162 | }, 163 | const { 164 | '1': 'Peer', 165 | '3': 4, 166 | '4': 1, 167 | '5': 11, 168 | '6': '.room.PeerEvent', 169 | '9': 0, 170 | '10': 'peer' 171 | }, 172 | const { 173 | '1': 'message', 174 | '3': 5, 175 | '4': 1, 176 | '5': 11, 177 | '6': '.room.Message', 178 | '9': 0, 179 | '10': 'message' 180 | }, 181 | const { 182 | '1': 'disconnect', 183 | '3': 6, 184 | '4': 1, 185 | '5': 11, 186 | '6': '.room.Disconnect', 187 | '9': 0, 188 | '10': 'disconnect' 189 | }, 190 | const { 191 | '1': 'room', 192 | '3': 7, 193 | '4': 1, 194 | '5': 11, 195 | '6': '.room.Room', 196 | '9': 0, 197 | '10': 'room' 198 | }, 199 | ], 200 | '8': const [ 201 | const {'1': 'payload'}, 202 | ], 203 | }; 204 | 205 | /// Descriptor for `Reply`. Decode as a `google.protobuf.DescriptorProto`. 206 | final $typed_data.Uint8List replyDescriptor = $convert.base64Decode( 207 | 'CgVSZXBseRIlCgRqb2luGAEgASgLMg8ucm9vbS5Kb2luUmVwbHlIAFIEam9pbhIoCgVsZWF2ZRgCIAEoCzIQLnJvb20uTGVhdmVSZXBseUgAUgVsZWF2ZRI6CgtzZW5kTWVzc2FnZRgDIAEoCzIWLnJvb20uU2VuZE1lc3NhZ2VSZXBseUgAUgtzZW5kTWVzc2FnZRIlCgRQZWVyGAQgASgLMg8ucm9vbS5QZWVyRXZlbnRIAFIEcGVlchIpCgdtZXNzYWdlGAUgASgLMg0ucm9vbS5NZXNzYWdlSABSB21lc3NhZ2USMgoKZGlzY29ubmVjdBgGIAEoCzIQLnJvb20uRGlzY29ubmVjdEgAUgpkaXNjb25uZWN0EiAKBHJvb20YByABKAsyCi5yb29tLlJvb21IAFIEcm9vbUIJCgdwYXlsb2Fk'); 208 | @$core.Deprecated('Use createRoomRequestDescriptor instead') 209 | const CreateRoomRequest$json = const { 210 | '1': 'CreateRoomRequest', 211 | '2': const [ 212 | const { 213 | '1': 'room', 214 | '3': 1, 215 | '4': 1, 216 | '5': 11, 217 | '6': '.room.Room', 218 | '10': 'room' 219 | }, 220 | ], 221 | }; 222 | 223 | /// Descriptor for `CreateRoomRequest`. Decode as a `google.protobuf.DescriptorProto`. 224 | final $typed_data.Uint8List createRoomRequestDescriptor = $convert.base64Decode( 225 | 'ChFDcmVhdGVSb29tUmVxdWVzdBIeCgRyb29tGAEgASgLMgoucm9vbS5Sb29tUgRyb29t'); 226 | @$core.Deprecated('Use createRoomReplyDescriptor instead') 227 | const CreateRoomReply$json = const { 228 | '1': 'CreateRoomReply', 229 | '2': const [ 230 | const {'1': 'success', '3': 1, '4': 1, '5': 8, '10': 'success'}, 231 | const { 232 | '1': 'error', 233 | '3': 2, 234 | '4': 1, 235 | '5': 11, 236 | '6': '.room.Error', 237 | '10': 'error' 238 | }, 239 | ], 240 | }; 241 | 242 | /// Descriptor for `CreateRoomReply`. Decode as a `google.protobuf.DescriptorProto`. 243 | final $typed_data.Uint8List createRoomReplyDescriptor = $convert.base64Decode( 244 | 'Cg9DcmVhdGVSb29tUmVwbHkSGAoHc3VjY2VzcxgBIAEoCFIHc3VjY2VzcxIhCgVlcnJvchgCIAEoCzILLnJvb20uRXJyb3JSBWVycm9y'); 245 | @$core.Deprecated('Use deleteRoomRequestDescriptor instead') 246 | const DeleteRoomRequest$json = const { 247 | '1': 'DeleteRoomRequest', 248 | '2': const [ 249 | const {'1': 'sid', '3': 1, '4': 1, '5': 9, '10': 'sid'}, 250 | ], 251 | }; 252 | 253 | /// Descriptor for `DeleteRoomRequest`. Decode as a `google.protobuf.DescriptorProto`. 254 | final $typed_data.Uint8List deleteRoomRequestDescriptor = $convert 255 | .base64Decode('ChFEZWxldGVSb29tUmVxdWVzdBIQCgNzaWQYASABKAlSA3NpZA=='); 256 | @$core.Deprecated('Use deleteRoomReplyDescriptor instead') 257 | const DeleteRoomReply$json = const { 258 | '1': 'DeleteRoomReply', 259 | '2': const [ 260 | const {'1': 'success', '3': 1, '4': 1, '5': 8, '10': 'success'}, 261 | const { 262 | '1': 'error', 263 | '3': 2, 264 | '4': 1, 265 | '5': 11, 266 | '6': '.room.Error', 267 | '10': 'error' 268 | }, 269 | ], 270 | }; 271 | 272 | /// Descriptor for `DeleteRoomReply`. Decode as a `google.protobuf.DescriptorProto`. 273 | final $typed_data.Uint8List deleteRoomReplyDescriptor = $convert.base64Decode( 274 | 'Cg9EZWxldGVSb29tUmVwbHkSGAoHc3VjY2VzcxgBIAEoCFIHc3VjY2VzcxIhCgVlcnJvchgCIAEoCzILLnJvb20uRXJyb3JSBWVycm9y'); 275 | @$core.Deprecated('Use joinRequestDescriptor instead') 276 | const JoinRequest$json = const { 277 | '1': 'JoinRequest', 278 | '2': const [ 279 | const { 280 | '1': 'peer', 281 | '3': 1, 282 | '4': 1, 283 | '5': 11, 284 | '6': '.room.Peer', 285 | '10': 'peer' 286 | }, 287 | const {'1': 'password', '3': 2, '4': 1, '5': 9, '10': 'password'}, 288 | ], 289 | }; 290 | 291 | /// Descriptor for `JoinRequest`. Decode as a `google.protobuf.DescriptorProto`. 292 | final $typed_data.Uint8List joinRequestDescriptor = $convert.base64Decode( 293 | 'CgtKb2luUmVxdWVzdBIeCgRwZWVyGAEgASgLMgoucm9vbS5QZWVyUgRwZWVyEhoKCHBhc3N3b3JkGAIgASgJUghwYXNzd29yZA=='); 294 | @$core.Deprecated('Use roomDescriptor instead') 295 | const Room$json = const { 296 | '1': 'Room', 297 | '2': const [ 298 | const {'1': 'sid', '3': 1, '4': 1, '5': 9, '10': 'sid'}, 299 | const {'1': 'name', '3': 2, '4': 1, '5': 9, '10': 'name'}, 300 | const {'1': 'lock', '3': 3, '4': 1, '5': 8, '10': 'lock'}, 301 | const {'1': 'password', '3': 4, '4': 1, '5': 9, '10': 'password'}, 302 | const {'1': 'description', '3': 5, '4': 1, '5': 9, '10': 'description'}, 303 | const {'1': 'maxPeers', '3': 6, '4': 1, '5': 13, '10': 'maxPeers'}, 304 | ], 305 | }; 306 | 307 | /// Descriptor for `Room`. Decode as a `google.protobuf.DescriptorProto`. 308 | final $typed_data.Uint8List roomDescriptor = $convert.base64Decode( 309 | 'CgRSb29tEhAKA3NpZBgBIAEoCVIDc2lkEhIKBG5hbWUYAiABKAlSBG5hbWUSEgoEbG9jaxgDIAEoCFIEbG9jaxIaCghwYXNzd29yZBgEIAEoCVIIcGFzc3dvcmQSIAoLZGVzY3JpcHRpb24YBSABKAlSC2Rlc2NyaXB0aW9uEhoKCG1heFBlZXJzGAYgASgNUghtYXhQZWVycw=='); 310 | @$core.Deprecated('Use joinReplyDescriptor instead') 311 | const JoinReply$json = const { 312 | '1': 'JoinReply', 313 | '2': const [ 314 | const {'1': 'success', '3': 1, '4': 1, '5': 8, '10': 'success'}, 315 | const { 316 | '1': 'error', 317 | '3': 2, 318 | '4': 1, 319 | '5': 11, 320 | '6': '.room.Error', 321 | '10': 'error' 322 | }, 323 | const { 324 | '1': 'role', 325 | '3': 3, 326 | '4': 1, 327 | '5': 14, 328 | '6': '.room.Role', 329 | '10': 'role' 330 | }, 331 | const { 332 | '1': 'room', 333 | '3': 4, 334 | '4': 1, 335 | '5': 11, 336 | '6': '.room.Room', 337 | '10': 'room' 338 | }, 339 | ], 340 | }; 341 | 342 | /// Descriptor for `JoinReply`. Decode as a `google.protobuf.DescriptorProto`. 343 | final $typed_data.Uint8List joinReplyDescriptor = $convert.base64Decode( 344 | 'CglKb2luUmVwbHkSGAoHc3VjY2VzcxgBIAEoCFIHc3VjY2VzcxIhCgVlcnJvchgCIAEoCzILLnJvb20uRXJyb3JSBWVycm9yEh4KBHJvbGUYAyABKA4yCi5yb29tLlJvbGVSBHJvbGUSHgoEcm9vbRgEIAEoCzIKLnJvb20uUm9vbVIEcm9vbQ=='); 345 | @$core.Deprecated('Use leaveRequestDescriptor instead') 346 | const LeaveRequest$json = const { 347 | '1': 'LeaveRequest', 348 | '2': const [ 349 | const {'1': 'sid', '3': 1, '4': 1, '5': 9, '10': 'sid'}, 350 | const {'1': 'uid', '3': 2, '4': 1, '5': 9, '10': 'uid'}, 351 | ], 352 | }; 353 | 354 | /// Descriptor for `LeaveRequest`. Decode as a `google.protobuf.DescriptorProto`. 355 | final $typed_data.Uint8List leaveRequestDescriptor = $convert.base64Decode( 356 | 'CgxMZWF2ZVJlcXVlc3QSEAoDc2lkGAEgASgJUgNzaWQSEAoDdWlkGAIgASgJUgN1aWQ='); 357 | @$core.Deprecated('Use leaveReplyDescriptor instead') 358 | const LeaveReply$json = const { 359 | '1': 'LeaveReply', 360 | '2': const [ 361 | const {'1': 'success', '3': 1, '4': 1, '5': 8, '10': 'success'}, 362 | const { 363 | '1': 'error', 364 | '3': 2, 365 | '4': 1, 366 | '5': 11, 367 | '6': '.room.Error', 368 | '10': 'error' 369 | }, 370 | ], 371 | }; 372 | 373 | /// Descriptor for `LeaveReply`. Decode as a `google.protobuf.DescriptorProto`. 374 | final $typed_data.Uint8List leaveReplyDescriptor = $convert.base64Decode( 375 | 'CgpMZWF2ZVJlcGx5EhgKB3N1Y2Nlc3MYASABKAhSB3N1Y2Nlc3MSIQoFZXJyb3IYAiABKAsyCy5yb29tLkVycm9yUgVlcnJvcg=='); 376 | @$core.Deprecated('Use peerDescriptor instead') 377 | const Peer$json = const { 378 | '1': 'Peer', 379 | '2': const [ 380 | const {'1': 'sid', '3': 1, '4': 1, '5': 9, '10': 'sid'}, 381 | const {'1': 'uid', '3': 2, '4': 1, '5': 9, '10': 'uid'}, 382 | const {'1': 'displayName', '3': 3, '4': 1, '5': 9, '10': 'displayName'}, 383 | const {'1': 'extraInfo', '3': 4, '4': 1, '5': 12, '10': 'extraInfo'}, 384 | const {'1': 'destination', '3': 5, '4': 1, '5': 9, '10': 'destination'}, 385 | const { 386 | '1': 'role', 387 | '3': 6, 388 | '4': 1, 389 | '5': 14, 390 | '6': '.room.Role', 391 | '10': 'role' 392 | }, 393 | const { 394 | '1': 'protocol', 395 | '3': 7, 396 | '4': 1, 397 | '5': 14, 398 | '6': '.room.Protocol', 399 | '10': 'protocol' 400 | }, 401 | const {'1': 'avatar', '3': 8, '4': 1, '5': 9, '10': 'avatar'}, 402 | const { 403 | '1': 'direction', 404 | '3': 9, 405 | '4': 1, 406 | '5': 14, 407 | '6': '.room.Peer.Direction', 408 | '10': 'direction' 409 | }, 410 | const {'1': 'vendor', '3': 10, '4': 1, '5': 9, '10': 'vendor'}, 411 | ], 412 | '4': const [Peer_Direction$json], 413 | }; 414 | 415 | @$core.Deprecated('Use peerDescriptor instead') 416 | const Peer_Direction$json = const { 417 | '1': 'Direction', 418 | '2': const [ 419 | const {'1': 'INCOMING', '2': 0}, 420 | const {'1': 'OUTGOING', '2': 1}, 421 | const {'1': 'BILATERAL', '2': 2}, 422 | ], 423 | }; 424 | 425 | /// Descriptor for `Peer`. Decode as a `google.protobuf.DescriptorProto`. 426 | final $typed_data.Uint8List peerDescriptor = $convert.base64Decode( 427 | 'CgRQZWVyEhAKA3NpZBgBIAEoCVIDc2lkEhAKA3VpZBgCIAEoCVIDdWlkEiAKC2Rpc3BsYXlOYW1lGAMgASgJUgtkaXNwbGF5TmFtZRIcCglleHRyYUluZm8YBCABKAxSCWV4dHJhSW5mbxIgCgtkZXN0aW5hdGlvbhgFIAEoCVILZGVzdGluYXRpb24SHgoEcm9sZRgGIAEoDjIKLnJvb20uUm9sZVIEcm9sZRIqCghwcm90b2NvbBgHIAEoDjIOLnJvb20uUHJvdG9jb2xSCHByb3RvY29sEhYKBmF2YXRhchgIIAEoCVIGYXZhdGFyEjIKCWRpcmVjdGlvbhgJIAEoDjIULnJvb20uUGVlci5EaXJlY3Rpb25SCWRpcmVjdGlvbhIWCgZ2ZW5kb3IYCiABKAlSBnZlbmRvciI2CglEaXJlY3Rpb24SDAoISU5DT01JTkcQABIMCghPVVRHT0lORxABEg0KCUJJTEFURVJBTBAC'); 428 | @$core.Deprecated('Use addPeerRequestDescriptor instead') 429 | const AddPeerRequest$json = const { 430 | '1': 'AddPeerRequest', 431 | '2': const [ 432 | const { 433 | '1': 'peer', 434 | '3': 1, 435 | '4': 1, 436 | '5': 11, 437 | '6': '.room.Peer', 438 | '10': 'peer' 439 | }, 440 | ], 441 | }; 442 | 443 | /// Descriptor for `AddPeerRequest`. Decode as a `google.protobuf.DescriptorProto`. 444 | final $typed_data.Uint8List addPeerRequestDescriptor = $convert.base64Decode( 445 | 'Cg5BZGRQZWVyUmVxdWVzdBIeCgRwZWVyGAEgASgLMgoucm9vbS5QZWVyUgRwZWVy'); 446 | @$core.Deprecated('Use addPeerReplyDescriptor instead') 447 | const AddPeerReply$json = const { 448 | '1': 'AddPeerReply', 449 | '2': const [ 450 | const {'1': 'success', '3': 1, '4': 1, '5': 8, '10': 'success'}, 451 | const { 452 | '1': 'error', 453 | '3': 2, 454 | '4': 1, 455 | '5': 11, 456 | '6': '.room.Error', 457 | '10': 'error' 458 | }, 459 | ], 460 | }; 461 | 462 | /// Descriptor for `AddPeerReply`. Decode as a `google.protobuf.DescriptorProto`. 463 | final $typed_data.Uint8List addPeerReplyDescriptor = $convert.base64Decode( 464 | 'CgxBZGRQZWVyUmVwbHkSGAoHc3VjY2VzcxgBIAEoCFIHc3VjY2VzcxIhCgVlcnJvchgCIAEoCzILLnJvb20uRXJyb3JSBWVycm9y'); 465 | @$core.Deprecated('Use getPeersRequestDescriptor instead') 466 | const GetPeersRequest$json = const { 467 | '1': 'GetPeersRequest', 468 | '2': const [ 469 | const {'1': 'sid', '3': 1, '4': 1, '5': 9, '10': 'sid'}, 470 | ], 471 | }; 472 | 473 | /// Descriptor for `GetPeersRequest`. Decode as a `google.protobuf.DescriptorProto`. 474 | final $typed_data.Uint8List getPeersRequestDescriptor = 475 | $convert.base64Decode('Cg9HZXRQZWVyc1JlcXVlc3QSEAoDc2lkGAEgASgJUgNzaWQ='); 476 | @$core.Deprecated('Use getPeersReplyDescriptor instead') 477 | const GetPeersReply$json = const { 478 | '1': 'GetPeersReply', 479 | '2': const [ 480 | const {'1': 'success', '3': 1, '4': 1, '5': 8, '10': 'success'}, 481 | const { 482 | '1': 'error', 483 | '3': 2, 484 | '4': 1, 485 | '5': 11, 486 | '6': '.room.Error', 487 | '10': 'error' 488 | }, 489 | const { 490 | '1': 'Peers', 491 | '3': 3, 492 | '4': 3, 493 | '5': 11, 494 | '6': '.room.Peer', 495 | '10': 'peers' 496 | }, 497 | ], 498 | }; 499 | 500 | /// Descriptor for `GetPeersReply`. Decode as a `google.protobuf.DescriptorProto`. 501 | final $typed_data.Uint8List getPeersReplyDescriptor = $convert.base64Decode( 502 | 'Cg1HZXRQZWVyc1JlcGx5EhgKB3N1Y2Nlc3MYASABKAhSB3N1Y2Nlc3MSIQoFZXJyb3IYAiABKAsyCy5yb29tLkVycm9yUgVlcnJvchIgCgVQZWVycxgDIAMoCzIKLnJvb20uUGVlclIFcGVlcnM='); 503 | @$core.Deprecated('Use messageDescriptor instead') 504 | const Message$json = const { 505 | '1': 'Message', 506 | '2': const [ 507 | const {'1': 'from', '3': 1, '4': 1, '5': 9, '10': 'from'}, 508 | const {'1': 'to', '3': 2, '4': 1, '5': 9, '10': 'to'}, 509 | const {'1': 'type', '3': 3, '4': 1, '5': 9, '10': 'type'}, 510 | const {'1': 'payload', '3': 4, '4': 1, '5': 12, '10': 'payload'}, 511 | ], 512 | }; 513 | 514 | /// Descriptor for `Message`. Decode as a `google.protobuf.DescriptorProto`. 515 | final $typed_data.Uint8List messageDescriptor = $convert.base64Decode( 516 | 'CgdNZXNzYWdlEhIKBGZyb20YASABKAlSBGZyb20SDgoCdG8YAiABKAlSAnRvEhIKBHR5cGUYAyABKAlSBHR5cGUSGAoHcGF5bG9hZBgEIAEoDFIHcGF5bG9hZA=='); 517 | @$core.Deprecated('Use sendMessageRequestDescriptor instead') 518 | const SendMessageRequest$json = const { 519 | '1': 'SendMessageRequest', 520 | '2': const [ 521 | const {'1': 'sid', '3': 1, '4': 1, '5': 9, '10': 'sid'}, 522 | const { 523 | '1': 'message', 524 | '3': 2, 525 | '4': 1, 526 | '5': 11, 527 | '6': '.room.Message', 528 | '10': 'message' 529 | }, 530 | ], 531 | }; 532 | 533 | /// Descriptor for `SendMessageRequest`. Decode as a `google.protobuf.DescriptorProto`. 534 | final $typed_data.Uint8List sendMessageRequestDescriptor = $convert.base64Decode( 535 | 'ChJTZW5kTWVzc2FnZVJlcXVlc3QSEAoDc2lkGAEgASgJUgNzaWQSJwoHbWVzc2FnZRgCIAEoCzINLnJvb20uTWVzc2FnZVIHbWVzc2FnZQ=='); 536 | @$core.Deprecated('Use sendMessageReplyDescriptor instead') 537 | const SendMessageReply$json = const { 538 | '1': 'SendMessageReply', 539 | '2': const [ 540 | const {'1': 'success', '3': 1, '4': 1, '5': 8, '10': 'success'}, 541 | const { 542 | '1': 'error', 543 | '3': 2, 544 | '4': 1, 545 | '5': 11, 546 | '6': '.room.Error', 547 | '10': 'error' 548 | }, 549 | ], 550 | }; 551 | 552 | /// Descriptor for `SendMessageReply`. Decode as a `google.protobuf.DescriptorProto`. 553 | final $typed_data.Uint8List sendMessageReplyDescriptor = $convert.base64Decode( 554 | 'ChBTZW5kTWVzc2FnZVJlcGx5EhgKB3N1Y2Nlc3MYASABKAhSB3N1Y2Nlc3MSIQoFZXJyb3IYAiABKAsyCy5yb29tLkVycm9yUgVlcnJvcg=='); 555 | @$core.Deprecated('Use disconnectDescriptor instead') 556 | const Disconnect$json = const { 557 | '1': 'Disconnect', 558 | '2': const [ 559 | const {'1': 'sid', '3': 1, '4': 1, '5': 9, '10': 'sid'}, 560 | const {'1': 'reason', '3': 2, '4': 1, '5': 9, '10': 'reason'}, 561 | ], 562 | }; 563 | 564 | /// Descriptor for `Disconnect`. Decode as a `google.protobuf.DescriptorProto`. 565 | final $typed_data.Uint8List disconnectDescriptor = $convert.base64Decode( 566 | 'CgpEaXNjb25uZWN0EhAKA3NpZBgBIAEoCVIDc2lkEhYKBnJlYXNvbhgCIAEoCVIGcmVhc29u'); 567 | @$core.Deprecated('Use peerEventDescriptor instead') 568 | const PeerEvent$json = const { 569 | '1': 'PeerEvent', 570 | '2': const [ 571 | const { 572 | '1': 'Peer', 573 | '3': 1, 574 | '4': 1, 575 | '5': 11, 576 | '6': '.room.Peer', 577 | '10': 'peer' 578 | }, 579 | const { 580 | '1': 'state', 581 | '3': 2, 582 | '4': 1, 583 | '5': 14, 584 | '6': '.room.PeerState', 585 | '10': 'state' 586 | }, 587 | ], 588 | }; 589 | 590 | /// Descriptor for `PeerEvent`. Decode as a `google.protobuf.DescriptorProto`. 591 | final $typed_data.Uint8List peerEventDescriptor = $convert.base64Decode( 592 | 'CglQZWVyRXZlbnQSHgoEUGVlchgBIAEoCzIKLnJvb20uUGVlclIEcGVlchIlCgVzdGF0ZRgCIAEoDjIPLnJvb20uUGVlclN0YXRlUgVzdGF0ZQ=='); 593 | @$core.Deprecated('Use updateRoomRequestDescriptor instead') 594 | const UpdateRoomRequest$json = const { 595 | '1': 'UpdateRoomRequest', 596 | '2': const [ 597 | const { 598 | '1': 'room', 599 | '3': 1, 600 | '4': 1, 601 | '5': 11, 602 | '6': '.room.Room', 603 | '10': 'room' 604 | }, 605 | ], 606 | }; 607 | 608 | /// Descriptor for `UpdateRoomRequest`. Decode as a `google.protobuf.DescriptorProto`. 609 | final $typed_data.Uint8List updateRoomRequestDescriptor = $convert.base64Decode( 610 | 'ChFVcGRhdGVSb29tUmVxdWVzdBIeCgRyb29tGAEgASgLMgoucm9vbS5Sb29tUgRyb29t'); 611 | @$core.Deprecated('Use updateRoomReplyDescriptor instead') 612 | const UpdateRoomReply$json = const { 613 | '1': 'UpdateRoomReply', 614 | '2': const [ 615 | const {'1': 'success', '3': 1, '4': 1, '5': 8, '10': 'success'}, 616 | const { 617 | '1': 'error', 618 | '3': 2, 619 | '4': 1, 620 | '5': 11, 621 | '6': '.room.Error', 622 | '10': 'error' 623 | }, 624 | ], 625 | }; 626 | 627 | /// Descriptor for `UpdateRoomReply`. Decode as a `google.protobuf.DescriptorProto`. 628 | final $typed_data.Uint8List updateRoomReplyDescriptor = $convert.base64Decode( 629 | 'Cg9VcGRhdGVSb29tUmVwbHkSGAoHc3VjY2VzcxgBIAEoCFIHc3VjY2VzcxIhCgVlcnJvchgCIAEoCzILLnJvb20uRXJyb3JSBWVycm9y'); 630 | @$core.Deprecated('Use endRoomRequestDescriptor instead') 631 | const EndRoomRequest$json = const { 632 | '1': 'EndRoomRequest', 633 | '2': const [ 634 | const {'1': 'sid', '3': 1, '4': 1, '5': 9, '10': 'sid'}, 635 | const {'1': 'reason', '3': 2, '4': 1, '5': 9, '10': 'reason'}, 636 | const {'1': 'delete', '3': 3, '4': 1, '5': 8, '10': 'delete'}, 637 | ], 638 | }; 639 | 640 | /// Descriptor for `EndRoomRequest`. Decode as a `google.protobuf.DescriptorProto`. 641 | final $typed_data.Uint8List endRoomRequestDescriptor = $convert.base64Decode( 642 | 'Cg5FbmRSb29tUmVxdWVzdBIQCgNzaWQYASABKAlSA3NpZBIWCgZyZWFzb24YAiABKAlSBnJlYXNvbhIWCgZkZWxldGUYAyABKAhSBmRlbGV0ZQ=='); 643 | @$core.Deprecated('Use endRoomReplyDescriptor instead') 644 | const EndRoomReply$json = const { 645 | '1': 'EndRoomReply', 646 | '2': const [ 647 | const {'1': 'success', '3': 1, '4': 1, '5': 8, '10': 'success'}, 648 | const { 649 | '1': 'error', 650 | '3': 2, 651 | '4': 1, 652 | '5': 11, 653 | '6': '.room.Error', 654 | '10': 'error' 655 | }, 656 | ], 657 | }; 658 | 659 | /// Descriptor for `EndRoomReply`. Decode as a `google.protobuf.DescriptorProto`. 660 | final $typed_data.Uint8List endRoomReplyDescriptor = $convert.base64Decode( 661 | 'CgxFbmRSb29tUmVwbHkSGAoHc3VjY2VzcxgBIAEoCFIHc3VjY2VzcxIhCgVlcnJvchgCIAEoCzILLnJvb20uRXJyb3JSBWVycm9y'); 662 | @$core.Deprecated('Use getRoomsRequestDescriptor instead') 663 | const GetRoomsRequest$json = const { 664 | '1': 'GetRoomsRequest', 665 | }; 666 | 667 | /// Descriptor for `GetRoomsRequest`. Decode as a `google.protobuf.DescriptorProto`. 668 | final $typed_data.Uint8List getRoomsRequestDescriptor = 669 | $convert.base64Decode('Cg9HZXRSb29tc1JlcXVlc3Q='); 670 | @$core.Deprecated('Use getRoomsReplyDescriptor instead') 671 | const GetRoomsReply$json = const { 672 | '1': 'GetRoomsReply', 673 | '2': const [ 674 | const {'1': 'success', '3': 1, '4': 1, '5': 8, '10': 'success'}, 675 | const { 676 | '1': 'error', 677 | '3': 2, 678 | '4': 1, 679 | '5': 11, 680 | '6': '.room.Error', 681 | '10': 'error' 682 | }, 683 | const { 684 | '1': 'rooms', 685 | '3': 3, 686 | '4': 3, 687 | '5': 11, 688 | '6': '.room.Room', 689 | '10': 'rooms' 690 | }, 691 | ], 692 | }; 693 | 694 | /// Descriptor for `GetRoomsReply`. Decode as a `google.protobuf.DescriptorProto`. 695 | final $typed_data.Uint8List getRoomsReplyDescriptor = $convert.base64Decode( 696 | 'Cg1HZXRSb29tc1JlcGx5EhgKB3N1Y2Nlc3MYASABKAhSB3N1Y2Nlc3MSIQoFZXJyb3IYAiABKAsyCy5yb29tLkVycm9yUgVlcnJvchIgCgVyb29tcxgDIAMoCzIKLnJvb20uUm9vbVIFcm9vbXM='); 697 | @$core.Deprecated('Use updatePeerRequestDescriptor instead') 698 | const UpdatePeerRequest$json = const { 699 | '1': 'UpdatePeerRequest', 700 | '2': const [ 701 | const { 702 | '1': 'peer', 703 | '3': 1, 704 | '4': 1, 705 | '5': 11, 706 | '6': '.room.Peer', 707 | '10': 'peer' 708 | }, 709 | ], 710 | }; 711 | 712 | /// Descriptor for `UpdatePeerRequest`. Decode as a `google.protobuf.DescriptorProto`. 713 | final $typed_data.Uint8List updatePeerRequestDescriptor = $convert.base64Decode( 714 | 'ChFVcGRhdGVQZWVyUmVxdWVzdBIeCgRwZWVyGAEgASgLMgoucm9vbS5QZWVyUgRwZWVy'); 715 | @$core.Deprecated('Use updatePeerReplyDescriptor instead') 716 | const UpdatePeerReply$json = const { 717 | '1': 'UpdatePeerReply', 718 | '2': const [ 719 | const {'1': 'success', '3': 1, '4': 1, '5': 8, '10': 'success'}, 720 | const { 721 | '1': 'error', 722 | '3': 2, 723 | '4': 1, 724 | '5': 11, 725 | '6': '.room.Error', 726 | '10': 'error' 727 | }, 728 | ], 729 | }; 730 | 731 | /// Descriptor for `UpdatePeerReply`. Decode as a `google.protobuf.DescriptorProto`. 732 | final $typed_data.Uint8List updatePeerReplyDescriptor = $convert.base64Decode( 733 | 'Cg9VcGRhdGVQZWVyUmVwbHkSGAoHc3VjY2VzcxgBIAEoCFIHc3VjY2VzcxIhCgVlcnJvchgCIAEoCzILLnJvb20uRXJyb3JSBWVycm9y'); 734 | @$core.Deprecated('Use removePeerRequestDescriptor instead') 735 | const RemovePeerRequest$json = const { 736 | '1': 'RemovePeerRequest', 737 | '2': const [ 738 | const {'1': 'sid', '3': 1, '4': 1, '5': 9, '10': 'sid'}, 739 | const {'1': 'uid', '3': 2, '4': 1, '5': 9, '10': 'uid'}, 740 | ], 741 | }; 742 | 743 | /// Descriptor for `RemovePeerRequest`. Decode as a `google.protobuf.DescriptorProto`. 744 | final $typed_data.Uint8List removePeerRequestDescriptor = $convert.base64Decode( 745 | 'ChFSZW1vdmVQZWVyUmVxdWVzdBIQCgNzaWQYASABKAlSA3NpZBIQCgN1aWQYAiABKAlSA3VpZA=='); 746 | @$core.Deprecated('Use removePeerReplyDescriptor instead') 747 | const RemovePeerReply$json = const { 748 | '1': 'RemovePeerReply', 749 | '2': const [ 750 | const {'1': 'success', '3': 1, '4': 1, '5': 8, '10': 'success'}, 751 | const { 752 | '1': 'error', 753 | '3': 2, 754 | '4': 1, 755 | '5': 11, 756 | '6': '.room.Error', 757 | '10': 'error' 758 | }, 759 | ], 760 | }; 761 | 762 | /// Descriptor for `RemovePeerReply`. Decode as a `google.protobuf.DescriptorProto`. 763 | final $typed_data.Uint8List removePeerReplyDescriptor = $convert.base64Decode( 764 | 'Cg9SZW1vdmVQZWVyUmVwbHkSGAoHc3VjY2VzcxgBIAEoCFIHc3VjY2VzcxIhCgVlcnJvchgCIAEoCzILLnJvb20uRXJyb3JSBWVycm9y'); 765 | -------------------------------------------------------------------------------- /lib/src/_library/proto/rtc/rtc.pbenum.dart: -------------------------------------------------------------------------------- 1 | /// 2 | // Generated code. Do not modify. 3 | // source: proto/rtc/rtc.proto 4 | // 5 | // @dart = 2.12 6 | // ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields 7 | 8 | // ignore_for_file: UNDEFINED_SHOWN_NAME 9 | import 'dart:core' as $core; 10 | import 'package:protobuf/protobuf.dart' as $pb; 11 | 12 | class Target extends $pb.ProtobufEnum { 13 | static const Target PUBLISHER = Target._( 14 | 0, 15 | const $core.bool.fromEnvironment('protobuf.omit_enum_names') 16 | ? '' 17 | : 'PUBLISHER'); 18 | static const Target SUBSCRIBER = Target._( 19 | 1, 20 | const $core.bool.fromEnvironment('protobuf.omit_enum_names') 21 | ? '' 22 | : 'SUBSCRIBER'); 23 | 24 | static const $core.List values = [ 25 | PUBLISHER, 26 | SUBSCRIBER, 27 | ]; 28 | 29 | static final $core.Map<$core.int, Target> _byValue = 30 | $pb.ProtobufEnum.initByValue(values); 31 | static Target? valueOf($core.int value) => _byValue[value]; 32 | 33 | const Target._($core.int v, $core.String n) : super(v, n); 34 | } 35 | 36 | class MediaType extends $pb.ProtobufEnum { 37 | static const MediaType MediaUnknown = MediaType._( 38 | 0, 39 | const $core.bool.fromEnvironment('protobuf.omit_enum_names') 40 | ? '' 41 | : 'MediaUnknown'); 42 | static const MediaType UserMedia = MediaType._( 43 | 1, 44 | const $core.bool.fromEnvironment('protobuf.omit_enum_names') 45 | ? '' 46 | : 'UserMedia'); 47 | static const MediaType ScreenCapture = MediaType._( 48 | 2, 49 | const $core.bool.fromEnvironment('protobuf.omit_enum_names') 50 | ? '' 51 | : 'ScreenCapture'); 52 | static const MediaType Cavans = MediaType._( 53 | 3, 54 | const $core.bool.fromEnvironment('protobuf.omit_enum_names') 55 | ? '' 56 | : 'Cavans'); 57 | static const MediaType Streaming = MediaType._( 58 | 4, 59 | const $core.bool.fromEnvironment('protobuf.omit_enum_names') 60 | ? '' 61 | : 'Streaming'); 62 | static const MediaType VoIP = MediaType._( 63 | 5, 64 | const $core.bool.fromEnvironment('protobuf.omit_enum_names') 65 | ? '' 66 | : 'VoIP'); 67 | 68 | static const $core.List values = [ 69 | MediaUnknown, 70 | UserMedia, 71 | ScreenCapture, 72 | Cavans, 73 | Streaming, 74 | VoIP, 75 | ]; 76 | 77 | static final $core.Map<$core.int, MediaType> _byValue = 78 | $pb.ProtobufEnum.initByValue(values); 79 | static MediaType? valueOf($core.int value) => _byValue[value]; 80 | 81 | const MediaType._($core.int v, $core.String n) : super(v, n); 82 | } 83 | 84 | class TrackEvent_State extends $pb.ProtobufEnum { 85 | static const TrackEvent_State ADD = TrackEvent_State._( 86 | 0, 87 | const $core.bool.fromEnvironment('protobuf.omit_enum_names') 88 | ? '' 89 | : 'ADD'); 90 | static const TrackEvent_State UPDATE = TrackEvent_State._( 91 | 1, 92 | const $core.bool.fromEnvironment('protobuf.omit_enum_names') 93 | ? '' 94 | : 'UPDATE'); 95 | static const TrackEvent_State REMOVE = TrackEvent_State._( 96 | 2, 97 | const $core.bool.fromEnvironment('protobuf.omit_enum_names') 98 | ? '' 99 | : 'REMOVE'); 100 | 101 | static const $core.List values = [ 102 | ADD, 103 | UPDATE, 104 | REMOVE, 105 | ]; 106 | 107 | static final $core.Map<$core.int, TrackEvent_State> _byValue = 108 | $pb.ProtobufEnum.initByValue(values); 109 | static TrackEvent_State? valueOf($core.int value) => _byValue[value]; 110 | 111 | const TrackEvent_State._($core.int v, $core.String n) : super(v, n); 112 | } 113 | -------------------------------------------------------------------------------- /lib/src/_library/proto/rtc/rtc.pbgrpc.dart: -------------------------------------------------------------------------------- 1 | /// 2 | // Generated code. Do not modify. 3 | // source: proto/rtc/rtc.proto 4 | // 5 | // @dart = 2.12 6 | // ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields 7 | 8 | import 'dart:async' as $async; 9 | 10 | import 'dart:core' as $core; 11 | 12 | import 'package:grpc/service_api.dart' as $grpc; 13 | import 'rtc.pb.dart' as $0; 14 | export 'rtc.pb.dart'; 15 | 16 | class RTCClient extends $grpc.Client { 17 | static final _$signal = $grpc.ClientMethod<$0.Request, $0.Reply>( 18 | '/rtc.RTC/Signal', 19 | ($0.Request value) => value.writeToBuffer(), 20 | ($core.List<$core.int> value) => $0.Reply.fromBuffer(value)); 21 | 22 | RTCClient($grpc.ClientChannel channel, 23 | {$grpc.CallOptions? options, 24 | $core.Iterable<$grpc.ClientInterceptor>? interceptors}) 25 | : super(channel, options: options, interceptors: interceptors); 26 | 27 | $grpc.ResponseStream<$0.Reply> signal($async.Stream<$0.Request> request, 28 | {$grpc.CallOptions? options}) { 29 | return $createStreamingCall(_$signal, request, options: options); 30 | } 31 | } 32 | 33 | abstract class RTCServiceBase extends $grpc.Service { 34 | $core.String get $name => 'rtc.RTC'; 35 | 36 | RTCServiceBase() { 37 | $addMethod($grpc.ServiceMethod<$0.Request, $0.Reply>( 38 | 'Signal', 39 | signal, 40 | true, 41 | true, 42 | ($core.List<$core.int> value) => $0.Request.fromBuffer(value), 43 | ($0.Reply value) => value.writeToBuffer())); 44 | } 45 | 46 | $async.Stream<$0.Reply> signal( 47 | $grpc.ServiceCall call, $async.Stream<$0.Request> request); 48 | } 49 | -------------------------------------------------------------------------------- /lib/src/_library/proto/rtc/rtc.pbjson.dart: -------------------------------------------------------------------------------- 1 | /// 2 | // Generated code. Do not modify. 3 | // source: proto/rtc/rtc.proto 4 | // 5 | // @dart = 2.12 6 | // ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields,deprecated_member_use_from_same_package 7 | 8 | import 'dart:core' as $core; 9 | import 'dart:convert' as $convert; 10 | import 'dart:typed_data' as $typed_data; 11 | 12 | @$core.Deprecated('Use targetDescriptor instead') 13 | const Target$json = const { 14 | '1': 'Target', 15 | '2': const [ 16 | const {'1': 'PUBLISHER', '2': 0}, 17 | const {'1': 'SUBSCRIBER', '2': 1}, 18 | ], 19 | }; 20 | 21 | /// Descriptor for `Target`. Decode as a `google.protobuf.EnumDescriptorProto`. 22 | final $typed_data.Uint8List targetDescriptor = $convert 23 | .base64Decode('CgZUYXJnZXQSDQoJUFVCTElTSEVSEAASDgoKU1VCU0NSSUJFUhAB'); 24 | @$core.Deprecated('Use mediaTypeDescriptor instead') 25 | const MediaType$json = const { 26 | '1': 'MediaType', 27 | '2': const [ 28 | const {'1': 'MediaUnknown', '2': 0}, 29 | const {'1': 'UserMedia', '2': 1}, 30 | const {'1': 'ScreenCapture', '2': 2}, 31 | const {'1': 'Cavans', '2': 3}, 32 | const {'1': 'Streaming', '2': 4}, 33 | const {'1': 'VoIP', '2': 5}, 34 | ], 35 | }; 36 | 37 | /// Descriptor for `MediaType`. Decode as a `google.protobuf.EnumDescriptorProto`. 38 | final $typed_data.Uint8List mediaTypeDescriptor = $convert.base64Decode( 39 | 'CglNZWRpYVR5cGUSEAoMTWVkaWFVbmtub3duEAASDQoJVXNlck1lZGlhEAESEQoNU2NyZWVuQ2FwdHVyZRACEgoKBkNhdmFucxADEg0KCVN0cmVhbWluZxAEEggKBFZvSVAQBQ=='); 40 | @$core.Deprecated('Use joinRequestDescriptor instead') 41 | const JoinRequest$json = const { 42 | '1': 'JoinRequest', 43 | '2': const [ 44 | const {'1': 'sid', '3': 1, '4': 1, '5': 9, '10': 'sid'}, 45 | const {'1': 'uid', '3': 2, '4': 1, '5': 9, '10': 'uid'}, 46 | const { 47 | '1': 'config', 48 | '3': 3, 49 | '4': 3, 50 | '5': 11, 51 | '6': '.rtc.JoinRequest.ConfigEntry', 52 | '10': 'config' 53 | }, 54 | const { 55 | '1': 'description', 56 | '3': 4, 57 | '4': 1, 58 | '5': 11, 59 | '6': '.rtc.SessionDescription', 60 | '10': 'description' 61 | }, 62 | ], 63 | '3': const [JoinRequest_ConfigEntry$json], 64 | }; 65 | 66 | @$core.Deprecated('Use joinRequestDescriptor instead') 67 | const JoinRequest_ConfigEntry$json = const { 68 | '1': 'ConfigEntry', 69 | '2': const [ 70 | const {'1': 'key', '3': 1, '4': 1, '5': 9, '10': 'key'}, 71 | const {'1': 'value', '3': 2, '4': 1, '5': 9, '10': 'value'}, 72 | ], 73 | '7': const {'7': true}, 74 | }; 75 | 76 | /// Descriptor for `JoinRequest`. Decode as a `google.protobuf.DescriptorProto`. 77 | final $typed_data.Uint8List joinRequestDescriptor = $convert.base64Decode( 78 | 'CgtKb2luUmVxdWVzdBIQCgNzaWQYASABKAlSA3NpZBIQCgN1aWQYAiABKAlSA3VpZBI0CgZjb25maWcYAyADKAsyHC5ydGMuSm9pblJlcXVlc3QuQ29uZmlnRW50cnlSBmNvbmZpZxI5CgtkZXNjcmlwdGlvbhgEIAEoCzIXLnJ0Yy5TZXNzaW9uRGVzY3JpcHRpb25SC2Rlc2NyaXB0aW9uGjkKC0NvbmZpZ0VudHJ5EhAKA2tleRgBIAEoCVIDa2V5EhQKBXZhbHVlGAIgASgJUgV2YWx1ZToCOAE='); 79 | @$core.Deprecated('Use joinReplyDescriptor instead') 80 | const JoinReply$json = const { 81 | '1': 'JoinReply', 82 | '2': const [ 83 | const {'1': 'success', '3': 1, '4': 1, '5': 8, '10': 'success'}, 84 | const { 85 | '1': 'error', 86 | '3': 2, 87 | '4': 1, 88 | '5': 11, 89 | '6': '.rtc.Error', 90 | '10': 'error' 91 | }, 92 | const { 93 | '1': 'description', 94 | '3': 3, 95 | '4': 1, 96 | '5': 11, 97 | '6': '.rtc.SessionDescription', 98 | '10': 'description' 99 | }, 100 | ], 101 | }; 102 | 103 | /// Descriptor for `JoinReply`. Decode as a `google.protobuf.DescriptorProto`. 104 | final $typed_data.Uint8List joinReplyDescriptor = $convert.base64Decode( 105 | 'CglKb2luUmVwbHkSGAoHc3VjY2VzcxgBIAEoCFIHc3VjY2VzcxIgCgVlcnJvchgCIAEoCzIKLnJ0Yy5FcnJvclIFZXJyb3ISOQoLZGVzY3JpcHRpb24YAyABKAsyFy5ydGMuU2Vzc2lvbkRlc2NyaXB0aW9uUgtkZXNjcmlwdGlvbg=='); 106 | @$core.Deprecated('Use trackInfoDescriptor instead') 107 | const TrackInfo$json = const { 108 | '1': 'TrackInfo', 109 | '2': const [ 110 | const {'1': 'id', '3': 1, '4': 1, '5': 9, '10': 'id'}, 111 | const {'1': 'kind', '3': 2, '4': 1, '5': 9, '10': 'kind'}, 112 | const {'1': 'muted', '3': 3, '4': 1, '5': 8, '10': 'muted'}, 113 | const { 114 | '1': 'type', 115 | '3': 4, 116 | '4': 1, 117 | '5': 14, 118 | '6': '.rtc.MediaType', 119 | '10': 'type' 120 | }, 121 | const {'1': 'streamId', '3': 5, '4': 1, '5': 9, '10': 'streamId'}, 122 | const {'1': 'label', '3': 6, '4': 1, '5': 9, '10': 'label'}, 123 | const {'1': 'layer', '3': 7, '4': 1, '5': 9, '10': 'layer'}, 124 | const {'1': 'width', '3': 8, '4': 1, '5': 13, '10': 'width'}, 125 | const {'1': 'height', '3': 9, '4': 1, '5': 13, '10': 'height'}, 126 | const {'1': 'frameRate', '3': 10, '4': 1, '5': 13, '10': 'frameRate'}, 127 | ], 128 | }; 129 | 130 | /// Descriptor for `TrackInfo`. Decode as a `google.protobuf.DescriptorProto`. 131 | final $typed_data.Uint8List trackInfoDescriptor = $convert.base64Decode( 132 | 'CglUcmFja0luZm8SDgoCaWQYASABKAlSAmlkEhIKBGtpbmQYAiABKAlSBGtpbmQSFAoFbXV0ZWQYAyABKAhSBW11dGVkEiIKBHR5cGUYBCABKA4yDi5ydGMuTWVkaWFUeXBlUgR0eXBlEhoKCHN0cmVhbUlkGAUgASgJUghzdHJlYW1JZBIUCgVsYWJlbBgGIAEoCVIFbGFiZWwSFAoFbGF5ZXIYByABKAlSBWxheWVyEhQKBXdpZHRoGAggASgNUgV3aWR0aBIWCgZoZWlnaHQYCSABKA1SBmhlaWdodBIcCglmcmFtZVJhdGUYCiABKA1SCWZyYW1lUmF0ZQ=='); 133 | @$core.Deprecated('Use sessionDescriptionDescriptor instead') 134 | const SessionDescription$json = const { 135 | '1': 'SessionDescription', 136 | '2': const [ 137 | const { 138 | '1': 'target', 139 | '3': 1, 140 | '4': 1, 141 | '5': 14, 142 | '6': '.rtc.Target', 143 | '10': 'target' 144 | }, 145 | const {'1': 'type', '3': 2, '4': 1, '5': 9, '10': 'type'}, 146 | const {'1': 'sdp', '3': 3, '4': 1, '5': 9, '10': 'sdp'}, 147 | const { 148 | '1': 'trackInfos', 149 | '3': 4, 150 | '4': 3, 151 | '5': 11, 152 | '6': '.rtc.TrackInfo', 153 | '10': 'trackInfos' 154 | }, 155 | ], 156 | }; 157 | 158 | /// Descriptor for `SessionDescription`. Decode as a `google.protobuf.DescriptorProto`. 159 | final $typed_data.Uint8List sessionDescriptionDescriptor = $convert.base64Decode( 160 | 'ChJTZXNzaW9uRGVzY3JpcHRpb24SIwoGdGFyZ2V0GAEgASgOMgsucnRjLlRhcmdldFIGdGFyZ2V0EhIKBHR5cGUYAiABKAlSBHR5cGUSEAoDc2RwGAMgASgJUgNzZHASLgoKdHJhY2tJbmZvcxgEIAMoCzIOLnJ0Yy5UcmFja0luZm9SCnRyYWNrSW5mb3M='); 161 | @$core.Deprecated('Use trickleDescriptor instead') 162 | const Trickle$json = const { 163 | '1': 'Trickle', 164 | '2': const [ 165 | const { 166 | '1': 'target', 167 | '3': 1, 168 | '4': 1, 169 | '5': 14, 170 | '6': '.rtc.Target', 171 | '10': 'target' 172 | }, 173 | const {'1': 'init', '3': 2, '4': 1, '5': 9, '10': 'init'}, 174 | ], 175 | }; 176 | 177 | /// Descriptor for `Trickle`. Decode as a `google.protobuf.DescriptorProto`. 178 | final $typed_data.Uint8List trickleDescriptor = $convert.base64Decode( 179 | 'CgdUcmlja2xlEiMKBnRhcmdldBgBIAEoDjILLnJ0Yy5UYXJnZXRSBnRhcmdldBISCgRpbml0GAIgASgJUgRpbml0'); 180 | @$core.Deprecated('Use errorDescriptor instead') 181 | const Error$json = const { 182 | '1': 'Error', 183 | '2': const [ 184 | const {'1': 'code', '3': 1, '4': 1, '5': 5, '10': 'code'}, 185 | const {'1': 'reason', '3': 2, '4': 1, '5': 9, '10': 'reason'}, 186 | ], 187 | }; 188 | 189 | /// Descriptor for `Error`. Decode as a `google.protobuf.DescriptorProto`. 190 | final $typed_data.Uint8List errorDescriptor = $convert.base64Decode( 191 | 'CgVFcnJvchISCgRjb2RlGAEgASgFUgRjb2RlEhYKBnJlYXNvbhgCIAEoCVIGcmVhc29u'); 192 | @$core.Deprecated('Use trackEventDescriptor instead') 193 | const TrackEvent$json = const { 194 | '1': 'TrackEvent', 195 | '2': const [ 196 | const { 197 | '1': 'state', 198 | '3': 1, 199 | '4': 1, 200 | '5': 14, 201 | '6': '.rtc.TrackEvent.State', 202 | '10': 'state' 203 | }, 204 | const {'1': 'uid', '3': 2, '4': 1, '5': 9, '10': 'uid'}, 205 | const { 206 | '1': 'tracks', 207 | '3': 3, 208 | '4': 3, 209 | '5': 11, 210 | '6': '.rtc.TrackInfo', 211 | '10': 'tracks' 212 | }, 213 | ], 214 | '4': const [TrackEvent_State$json], 215 | }; 216 | 217 | @$core.Deprecated('Use trackEventDescriptor instead') 218 | const TrackEvent_State$json = const { 219 | '1': 'State', 220 | '2': const [ 221 | const {'1': 'ADD', '2': 0}, 222 | const {'1': 'UPDATE', '2': 1}, 223 | const {'1': 'REMOVE', '2': 2}, 224 | ], 225 | }; 226 | 227 | /// Descriptor for `TrackEvent`. Decode as a `google.protobuf.DescriptorProto`. 228 | final $typed_data.Uint8List trackEventDescriptor = $convert.base64Decode( 229 | 'CgpUcmFja0V2ZW50EisKBXN0YXRlGAEgASgOMhUucnRjLlRyYWNrRXZlbnQuU3RhdGVSBXN0YXRlEhAKA3VpZBgCIAEoCVIDdWlkEiYKBnRyYWNrcxgDIAMoCzIOLnJ0Yy5UcmFja0luZm9SBnRyYWNrcyIoCgVTdGF0ZRIHCgNBREQQABIKCgZVUERBVEUQARIKCgZSRU1PVkUQAg=='); 230 | @$core.Deprecated('Use subscriptionDescriptor instead') 231 | const Subscription$json = const { 232 | '1': 'Subscription', 233 | '2': const [ 234 | const {'1': 'trackId', '3': 2, '4': 1, '5': 9, '10': 'trackId'}, 235 | const {'1': 'mute', '3': 3, '4': 1, '5': 8, '10': 'mute'}, 236 | const {'1': 'subscribe', '3': 4, '4': 1, '5': 8, '10': 'subscribe'}, 237 | const {'1': 'layer', '3': 5, '4': 1, '5': 9, '10': 'layer'}, 238 | ], 239 | }; 240 | 241 | /// Descriptor for `Subscription`. Decode as a `google.protobuf.DescriptorProto`. 242 | final $typed_data.Uint8List subscriptionDescriptor = $convert.base64Decode( 243 | 'CgxTdWJzY3JpcHRpb24SGAoHdHJhY2tJZBgCIAEoCVIHdHJhY2tJZBISCgRtdXRlGAMgASgIUgRtdXRlEhwKCXN1YnNjcmliZRgEIAEoCFIJc3Vic2NyaWJlEhQKBWxheWVyGAUgASgJUgVsYXllcg=='); 244 | @$core.Deprecated('Use subscriptionRequestDescriptor instead') 245 | const SubscriptionRequest$json = const { 246 | '1': 'SubscriptionRequest', 247 | '2': const [ 248 | const { 249 | '1': 'subscriptions', 250 | '3': 1, 251 | '4': 3, 252 | '5': 11, 253 | '6': '.rtc.Subscription', 254 | '10': 'subscriptions' 255 | }, 256 | ], 257 | }; 258 | 259 | /// Descriptor for `SubscriptionRequest`. Decode as a `google.protobuf.DescriptorProto`. 260 | final $typed_data.Uint8List subscriptionRequestDescriptor = $convert.base64Decode( 261 | 'ChNTdWJzY3JpcHRpb25SZXF1ZXN0EjcKDXN1YnNjcmlwdGlvbnMYASADKAsyES5ydGMuU3Vic2NyaXB0aW9uUg1zdWJzY3JpcHRpb25z'); 262 | @$core.Deprecated('Use subscriptionReplyDescriptor instead') 263 | const SubscriptionReply$json = const { 264 | '1': 'SubscriptionReply', 265 | '2': const [ 266 | const {'1': 'success', '3': 1, '4': 1, '5': 8, '10': 'success'}, 267 | const { 268 | '1': 'error', 269 | '3': 2, 270 | '4': 1, 271 | '5': 11, 272 | '6': '.rtc.Error', 273 | '10': 'error' 274 | }, 275 | ], 276 | }; 277 | 278 | /// Descriptor for `SubscriptionReply`. Decode as a `google.protobuf.DescriptorProto`. 279 | final $typed_data.Uint8List subscriptionReplyDescriptor = $convert.base64Decode( 280 | 'ChFTdWJzY3JpcHRpb25SZXBseRIYCgdzdWNjZXNzGAEgASgIUgdzdWNjZXNzEiAKBWVycm9yGAIgASgLMgoucnRjLkVycm9yUgVlcnJvcg=='); 281 | @$core.Deprecated('Use updateTrackReplyDescriptor instead') 282 | const UpdateTrackReply$json = const { 283 | '1': 'UpdateTrackReply', 284 | '2': const [ 285 | const {'1': 'success', '3': 1, '4': 1, '5': 8, '10': 'success'}, 286 | const { 287 | '1': 'error', 288 | '3': 2, 289 | '4': 1, 290 | '5': 11, 291 | '6': '.rtc.Error', 292 | '10': 'error' 293 | }, 294 | ], 295 | }; 296 | 297 | /// Descriptor for `UpdateTrackReply`. Decode as a `google.protobuf.DescriptorProto`. 298 | final $typed_data.Uint8List updateTrackReplyDescriptor = $convert.base64Decode( 299 | 'ChBVcGRhdGVUcmFja1JlcGx5EhgKB3N1Y2Nlc3MYASABKAhSB3N1Y2Nlc3MSIAoFZXJyb3IYAiABKAsyCi5ydGMuRXJyb3JSBWVycm9y'); 300 | @$core.Deprecated('Use activeSpeakerDescriptor instead') 301 | const ActiveSpeaker$json = const { 302 | '1': 'ActiveSpeaker', 303 | '2': const [ 304 | const { 305 | '1': 'speakers', 306 | '3': 1, 307 | '4': 3, 308 | '5': 11, 309 | '6': '.rtc.AudioLevelSpeaker', 310 | '10': 'speakers' 311 | }, 312 | ], 313 | }; 314 | 315 | /// Descriptor for `ActiveSpeaker`. Decode as a `google.protobuf.DescriptorProto`. 316 | final $typed_data.Uint8List activeSpeakerDescriptor = $convert.base64Decode( 317 | 'Cg1BY3RpdmVTcGVha2VyEjIKCHNwZWFrZXJzGAEgAygLMhYucnRjLkF1ZGlvTGV2ZWxTcGVha2VyUghzcGVha2Vycw=='); 318 | @$core.Deprecated('Use audioLevelSpeakerDescriptor instead') 319 | const AudioLevelSpeaker$json = const { 320 | '1': 'AudioLevelSpeaker', 321 | '2': const [ 322 | const {'1': 'sid', '3': 1, '4': 1, '5': 9, '10': 'sid'}, 323 | const {'1': 'level', '3': 2, '4': 1, '5': 2, '10': 'level'}, 324 | const {'1': 'active', '3': 3, '4': 1, '5': 8, '10': 'active'}, 325 | ], 326 | }; 327 | 328 | /// Descriptor for `AudioLevelSpeaker`. Decode as a `google.protobuf.DescriptorProto`. 329 | final $typed_data.Uint8List audioLevelSpeakerDescriptor = $convert.base64Decode( 330 | 'ChFBdWRpb0xldmVsU3BlYWtlchIQCgNzaWQYASABKAlSA3NpZBIUCgVsZXZlbBgCIAEoAlIFbGV2ZWwSFgoGYWN0aXZlGAMgASgIUgZhY3RpdmU='); 331 | @$core.Deprecated('Use requestDescriptor instead') 332 | const Request$json = const { 333 | '1': 'Request', 334 | '2': const [ 335 | const { 336 | '1': 'join', 337 | '3': 1, 338 | '4': 1, 339 | '5': 11, 340 | '6': '.rtc.JoinRequest', 341 | '9': 0, 342 | '10': 'join' 343 | }, 344 | const { 345 | '1': 'description', 346 | '3': 2, 347 | '4': 1, 348 | '5': 11, 349 | '6': '.rtc.SessionDescription', 350 | '9': 0, 351 | '10': 'description' 352 | }, 353 | const { 354 | '1': 'trickle', 355 | '3': 3, 356 | '4': 1, 357 | '5': 11, 358 | '6': '.rtc.Trickle', 359 | '9': 0, 360 | '10': 'trickle' 361 | }, 362 | const { 363 | '1': 'subscription', 364 | '3': 4, 365 | '4': 1, 366 | '5': 11, 367 | '6': '.rtc.SubscriptionRequest', 368 | '9': 0, 369 | '10': 'subscription' 370 | }, 371 | ], 372 | '8': const [ 373 | const {'1': 'payload'}, 374 | ], 375 | }; 376 | 377 | /// Descriptor for `Request`. Decode as a `google.protobuf.DescriptorProto`. 378 | final $typed_data.Uint8List requestDescriptor = $convert.base64Decode( 379 | 'CgdSZXF1ZXN0EiYKBGpvaW4YASABKAsyEC5ydGMuSm9pblJlcXVlc3RIAFIEam9pbhI7CgtkZXNjcmlwdGlvbhgCIAEoCzIXLnJ0Yy5TZXNzaW9uRGVzY3JpcHRpb25IAFILZGVzY3JpcHRpb24SKAoHdHJpY2tsZRgDIAEoCzIMLnJ0Yy5Ucmlja2xlSABSB3RyaWNrbGUSPgoMc3Vic2NyaXB0aW9uGAQgASgLMhgucnRjLlN1YnNjcmlwdGlvblJlcXVlc3RIAFIMc3Vic2NyaXB0aW9uQgkKB3BheWxvYWQ='); 380 | @$core.Deprecated('Use replyDescriptor instead') 381 | const Reply$json = const { 382 | '1': 'Reply', 383 | '2': const [ 384 | const { 385 | '1': 'join', 386 | '3': 1, 387 | '4': 1, 388 | '5': 11, 389 | '6': '.rtc.JoinReply', 390 | '9': 0, 391 | '10': 'join' 392 | }, 393 | const { 394 | '1': 'description', 395 | '3': 2, 396 | '4': 1, 397 | '5': 11, 398 | '6': '.rtc.SessionDescription', 399 | '9': 0, 400 | '10': 'description' 401 | }, 402 | const { 403 | '1': 'trickle', 404 | '3': 3, 405 | '4': 1, 406 | '5': 11, 407 | '6': '.rtc.Trickle', 408 | '9': 0, 409 | '10': 'trickle' 410 | }, 411 | const { 412 | '1': 'trackEvent', 413 | '3': 4, 414 | '4': 1, 415 | '5': 11, 416 | '6': '.rtc.TrackEvent', 417 | '9': 0, 418 | '10': 'trackEvent' 419 | }, 420 | const { 421 | '1': 'subscription', 422 | '3': 5, 423 | '4': 1, 424 | '5': 11, 425 | '6': '.rtc.SubscriptionReply', 426 | '9': 0, 427 | '10': 'subscription' 428 | }, 429 | const { 430 | '1': 'error', 431 | '3': 7, 432 | '4': 1, 433 | '5': 11, 434 | '6': '.rtc.Error', 435 | '9': 0, 436 | '10': 'error' 437 | }, 438 | ], 439 | '8': const [ 440 | const {'1': 'payload'}, 441 | ], 442 | }; 443 | 444 | /// Descriptor for `Reply`. Decode as a `google.protobuf.DescriptorProto`. 445 | final $typed_data.Uint8List replyDescriptor = $convert.base64Decode( 446 | 'CgVSZXBseRIkCgRqb2luGAEgASgLMg4ucnRjLkpvaW5SZXBseUgAUgRqb2luEjsKC2Rlc2NyaXB0aW9uGAIgASgLMhcucnRjLlNlc3Npb25EZXNjcmlwdGlvbkgAUgtkZXNjcmlwdGlvbhIoCgd0cmlja2xlGAMgASgLMgwucnRjLlRyaWNrbGVIAFIHdHJpY2tsZRIxCgp0cmFja0V2ZW50GAQgASgLMg8ucnRjLlRyYWNrRXZlbnRIAFIKdHJhY2tFdmVudBI8CgxzdWJzY3JpcHRpb24YBSABKAsyFi5ydGMuU3Vic2NyaXB0aW9uUmVwbHlIAFIMc3Vic2NyaXB0aW9uEiIKBWVycm9yGAcgASgLMgoucnRjLkVycm9ySABSBWVycm9yQgkKB3BheWxvYWQ='); 447 | -------------------------------------------------------------------------------- /lib/src/client.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:convert'; 3 | 4 | import 'package:flutter_webrtc/flutter_webrtc.dart'; 5 | 6 | import 'logger.dart'; 7 | import 'signal/signal.dart'; 8 | import 'stream.dart'; 9 | import 'utils.dart'; 10 | 11 | const API_CHANNEL = 'ion-sfu'; 12 | const ERR_NO_SESSION = 'no active session, join first'; 13 | 14 | abstract class Sender { 15 | MediaStream get stream; 16 | 17 | /// [kind in 'video' | 'audio'] 18 | RTCRtpTransceiver get transceivers; 19 | } 20 | 21 | class RTCConfiguration { 22 | /// 'vp8' | 'vp9' | 'h264' 23 | late String codec; 24 | } 25 | 26 | class Transport { 27 | Transport(this.signal); 28 | 29 | static Future create( 30 | {required int role, 31 | required Signal signal, 32 | required Map config}) async { 33 | var transport = Transport(signal); 34 | var pc = await createPeerConnection(config); 35 | 36 | transport.pc = pc; 37 | 38 | if (role == RolePub) { 39 | transport.api = await pc.createDataChannel( 40 | API_CHANNEL, RTCDataChannelInit()..maxRetransmits = 30); 41 | } 42 | 43 | pc.onIceCandidate = (candidate) { 44 | signal.trickle(Trickle(target: role, candidate: candidate)); 45 | }; 46 | 47 | pc.onIceConnectionState = (state) => { 48 | if (pc.iceConnectionState == 49 | RTCIceConnectionState.RTCIceConnectionStateDisconnected) 50 | { 51 | /* TODO: implement pc.restartIce for flutter_webrtc. 52 | if (pc.restartIce) { 53 | // this will trigger onNegotiationNeeded 54 | pc.restartIce(); 55 | }*/ 56 | } 57 | }; 58 | 59 | return transport; 60 | } 61 | 62 | bool hasRemoteDescription = false; 63 | Function()? onapiopen; 64 | RTCDataChannel? api; 65 | Signal signal; 66 | RTCPeerConnection? pc; 67 | List candidates = []; 68 | } 69 | 70 | class Client { 71 | Client(this.signal, this.config) { 72 | signal.onnegotiate = negotiate; 73 | signal.ontrickle = trickle; 74 | } 75 | static Future create( 76 | {required String sid, 77 | required String uid, 78 | required Signal signal, 79 | Map? config}) async { 80 | var client = Client(signal, config ?? defaultConfig); 81 | 82 | client.transports = { 83 | RolePub: await Transport.create( 84 | role: RolePub, signal: signal, config: config ?? defaultConfig), 85 | RoleSub: await Transport.create( 86 | role: RoleSub, signal: signal, config: config ?? defaultConfig) 87 | }; 88 | 89 | client.signal.onready = () async { 90 | if (!client.initialized) { 91 | await client.join(sid, uid); 92 | client.initialized = true; 93 | } 94 | }; 95 | unawaited(client.signal.connect()); 96 | return client; 97 | } 98 | 99 | Map config; 100 | Function(MediaStreamTrack track, RemoteStream stream)? ontrack; 101 | Function(MediaStreamTrack track, RemoteStream stream)? onAddTrack; 102 | Function(MediaStreamTrack track, RemoteStream stream)? onRemoveTrack; 103 | Function(RTCDataChannel channel)? ondatachannel; 104 | Function(Map speakers)? onspeaker; 105 | 106 | static final defaultConfig = { 107 | 'iceServers': [ 108 | //{'urls': 'stun:stun.stunprotocol.org:3478'} 109 | ], 110 | 'sdpSemantics': 'unified-plan' 111 | }; 112 | 113 | bool initialized = false; 114 | Signal signal; 115 | Map transports = {}; 116 | 117 | Future> getPubStats(MediaStreamTrack? selector) { 118 | return transports[RolePub]!.pc!.getStats(selector); 119 | } 120 | 121 | Future> getSubStats(MediaStreamTrack? selector) { 122 | return transports[RoleSub]!.pc!.getStats(selector); 123 | } 124 | 125 | Future publish(LocalStream stream) async { 126 | await stream.publish(transports[RolePub]!.pc!); 127 | } 128 | 129 | void close() { 130 | transports.forEach((key, element) { 131 | element.pc!.close(); 132 | element.pc!.dispose(); 133 | }); 134 | signal.close(); 135 | } 136 | 137 | Future join(String sid, String uid) async { 138 | var completer = Completer(); 139 | try { 140 | transports[RoleSub]!.pc!.onTrack = (RTCTrackEvent ev) { 141 | var remote = makeRemote(ev.streams[0], transports[RoleSub]!); 142 | ontrack?.call(ev.track, remote); 143 | }; 144 | transports[RoleSub]!.pc!.onAddTrack = 145 | (MediaStream stream, MediaStreamTrack track) { 146 | var remote = makeRemote(stream, transports[RoleSub]!); 147 | onAddTrack?.call(track, remote); 148 | }; 149 | transports[RoleSub]!.pc!.onRemoveTrack = 150 | (MediaStream stream, MediaStreamTrack track) { 151 | var remote = makeRemote(stream, transports[RoleSub]!); 152 | onRemoveTrack?.call(track, remote); 153 | }; 154 | transports[RoleSub]!.pc!.onDataChannel = (RTCDataChannel channel) { 155 | if (channel.label == API_CHANNEL) { 156 | transports[RoleSub]!.api = channel; 157 | transports[RoleSub]!.onapiopen?.call(); 158 | final json = JsonDecoder(); 159 | channel.onMessage = (RTCDataChannelMessage msg) { 160 | onspeaker?.call(json.convert(msg.text)); 161 | }; 162 | completer.complete(); 163 | } 164 | ondatachannel?.call(channel); 165 | }; 166 | 167 | var pc = transports[RolePub]!.pc; 168 | if (pc != null) { 169 | try { 170 | unAwaited(pc.createOffer({}).then((offer) async { 171 | await pc.setLocalDescription(offer); 172 | var answer = await signal.join(sid, uid, offer); 173 | await pc.setRemoteDescription(answer); 174 | transports[RolePub]!.hasRemoteDescription = true; 175 | transports[RolePub]! 176 | .candidates 177 | .forEach((c) async => await pc.addCandidate(c)); 178 | pc.onRenegotiationNeeded = onnegotiationneeded; 179 | })); 180 | } catch (e) { 181 | completer.completeError(e); 182 | } 183 | } 184 | } catch (e) { 185 | print('join: e => ${e.toString()}'); 186 | completer.completeError(e); 187 | } 188 | return completer.future; 189 | } 190 | 191 | void trickle(Trickle trickle) async { 192 | var pc = transports[trickle.target]!.pc; 193 | if (pc != null && transports[trickle.target]!.hasRemoteDescription) { 194 | await pc.addCandidate(trickle.candidate); 195 | } else { 196 | transports[trickle.target]!.candidates.add(trickle.candidate); 197 | } 198 | } 199 | 200 | void negotiate(RTCSessionDescription description) async { 201 | try { 202 | var pc = transports[RoleSub]!.pc; 203 | if (pc != null) { 204 | await pc.setRemoteDescription(description); 205 | transports[RoleSub]!.candidates.forEach((c) => pc.addCandidate(c)); 206 | transports[RoleSub]!.candidates = []; 207 | var answer = await pc.createAnswer({}); 208 | await pc.setLocalDescription(answer); 209 | signal.answer(answer); 210 | } 211 | } catch (err) { 212 | log.error('negotiate: e => ${err.toString()}'); 213 | } 214 | } 215 | 216 | Future onnegotiationneeded() async { 217 | try { 218 | var pc = transports[RolePub]!.pc; 219 | if (pc != null) { 220 | var offer = await pc.createOffer({}); 221 | setPreferredCodec(offer); 222 | await pc.setLocalDescription(offer); 223 | var answer = await signal.offer(offer); 224 | await pc.setRemoteDescription(answer); 225 | } 226 | } catch (err, st) { 227 | log.error('onnegotiationneeded: e => ${err.toString()} $st'); 228 | } 229 | } 230 | 231 | void setPreferredCodec(RTCSessionDescription description) { 232 | var capSel = CodecCapabilitySelector(description.sdp!); 233 | var acaps = capSel.getCapabilities('audio'); 234 | if (acaps != null) { 235 | acaps.codecs = acaps.codecs 236 | .where((e) => (e['codec'] as String).toLowerCase() == 'opus') 237 | .toList(); 238 | acaps.setCodecPreferences('audio', acaps.codecs); 239 | capSel.setCapabilities(acaps); 240 | } 241 | 242 | var vcaps = capSel.getCapabilities('video'); 243 | if (vcaps != null) { 244 | vcaps.codecs = vcaps.codecs 245 | .where((e) => (e['codec'] as String).toLowerCase() == 'vp8') 246 | .toList(); 247 | vcaps.setCodecPreferences('video', vcaps.codecs); 248 | capSel.setCapabilities(vcaps); 249 | } 250 | description.sdp = capSel.sdp(); 251 | } 252 | 253 | Future createDataChannel(String label) { 254 | if (transports.isEmpty) { 255 | throw AssertionError(ERR_NO_SESSION); 256 | } 257 | return transports[RolePub]! 258 | .pc! 259 | .createDataChannel(label, RTCDataChannelInit()..maxRetransmits = 30); 260 | } 261 | } 262 | -------------------------------------------------------------------------------- /lib/src/connector/ion.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'package:grpc/grpc.dart' as grpc; 3 | import '../signal/grpc-web/_channel.dart' 4 | if (dart.library.html) '../signal/grpc-web/_channel_html.dart'; 5 | 6 | abstract class Service { 7 | late String name; 8 | Future connect(); 9 | bool connected = false; 10 | void close(); 11 | } 12 | 13 | class Connector { 14 | final String _uri; 15 | Map metadata = {}; 16 | Map trailers = {}; 17 | Map services = {}; 18 | void Function(Service service)? onopen; 19 | void Function(Service service, grpc.GrpcError? err)? onclose; 20 | 21 | Connector(this._uri, {String? token}) { 22 | if (token != null) { 23 | metadata['authorization'] = token; 24 | } 25 | } 26 | 27 | grpc.ClientChannel grpcClientChannel() { 28 | var uri = Uri.parse(_uri); 29 | return createChannel(uri.host, uri.port, uri.scheme == 'https'); 30 | } 31 | 32 | grpc.CallOptions callOptions() { 33 | return grpc.CallOptions( 34 | metadata: metadata, 35 | ); 36 | } 37 | 38 | void close() { 39 | services.forEach((String name, Service service) { 40 | if (service.connected) { 41 | service.close(); 42 | } 43 | }); 44 | } 45 | 46 | void onHeaders(Service service, Map headers) { 47 | print('Received header for ${service.name} metadata: $headers'); 48 | // Merge metadata. 49 | headers.forEach((key, value) { 50 | if (key.toLowerCase() != 'trailer' && 51 | key.toLowerCase() != 'content-type') { 52 | metadata[key] = value; 53 | } 54 | }); 55 | service.connected = true; 56 | onopen?.call(service); 57 | } 58 | 59 | void onTrailers(Service service, Map trailers) { 60 | print('Received trailer for ${service.name} metadata: $trailers'); 61 | // Merge trailers. 62 | trailers.forEach((key, value) { 63 | this.trailers[key] = value; 64 | }); 65 | } 66 | 67 | void onClosed(Service service) { 68 | service.connected = false; 69 | onclose?.call(service, null); 70 | } 71 | 72 | void onError(Service service, grpc.GrpcError err) { 73 | service.connected = false; 74 | onclose?.call(service, err); 75 | } 76 | 77 | void registerService(Service service) { 78 | services[service.name] = service; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /lib/src/connector/room.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:events2/events2.dart'; 4 | import 'package:grpc/grpc.dart' as grpc; 5 | 6 | import '../_library/apps/room/proto/room.pb.dart' as pb; 7 | import '../_library/apps/room/proto/room.pbgrpc.dart' as room; 8 | import 'ion.dart'; 9 | 10 | enum PeerState { 11 | NONE, 12 | JOIN, 13 | UPDATE, 14 | LEAVE, 15 | } 16 | 17 | enum Role { HOST, GUEST } 18 | 19 | enum Protocol { 20 | PROTOCOL_UNKNOWN, 21 | WEBRTC, 22 | SIP, 23 | RTMP, 24 | RTSP, 25 | } 26 | 27 | enum ErrorType { 28 | NONE, 29 | UNKOWNERROR, 30 | PERMISSIONDENIED, 31 | SERVICEUNAVAILABLE, 32 | ROOMLOCKED, 33 | PASSWORDREQUIRED, 34 | ROOMALREADYEXIST, 35 | ROOMNOTEXIST, 36 | INVALIDPARAMS, 37 | PEERALREADYEXIST, 38 | PEERNOTEXIST, 39 | } 40 | 41 | class RoomError { 42 | late ErrorType code; 43 | late String reason; 44 | } 45 | 46 | class JoinResult { 47 | late bool success; 48 | late RoomError? error; 49 | late Role role; 50 | late RoomInfo room; 51 | } 52 | 53 | enum Direction { 54 | INCOMING, 55 | OUTGOING, 56 | BILATERAL, 57 | } 58 | 59 | class Peer { 60 | late String sid; 61 | late String uid; 62 | late String displayname; 63 | late List extrainfo; 64 | late String destination; 65 | late Role role; 66 | late Protocol protocol; 67 | late String avatar; 68 | late Direction direction; 69 | late String vendor; 70 | } 71 | 72 | class PeerEvent { 73 | late PeerState state; 74 | late Peer peer; 75 | } 76 | 77 | class Message { 78 | late String from; 79 | late String to; 80 | late String type; 81 | late List payload; 82 | } 83 | 84 | class RoomInfo { 85 | late String sid; 86 | late String name; 87 | late bool lock; 88 | late String password; 89 | late String description; 90 | late int maxpeers; 91 | } 92 | 93 | class Disconnect { 94 | late String sid; 95 | late String reason; 96 | } 97 | 98 | class Room extends Service { 99 | @override 100 | String name = 'room'; 101 | Connector connector; 102 | _RoomGRPCClient? _sig; 103 | Function(Error err)? onError; 104 | Function(JoinResult result)? onJoin; 105 | Function(String reason)? onLeave; 106 | Function(PeerEvent event)? onPeerEvent; 107 | Function(Message msg)? onMessage; 108 | Function(RoomInfo info)? onRoomInfo; 109 | Function(Disconnect discon)? onDisconnect; 110 | 111 | Room(this.connector) { 112 | connector.registerService(this); 113 | } 114 | 115 | @override 116 | Future connect() async { 117 | if (_sig == null) { 118 | var sig = _RoomGRPCClient(connector, this); 119 | sig.on('join-reply', (JoinResult result) => onJoin?.call(result)); 120 | sig.on('leave-reply', (String reason) => onLeave?.call(reason)); 121 | sig.on('peer-event', (PeerEvent event) => onPeerEvent?.call(event)); 122 | sig.on('message', (Message msg) => onMessage?.call(msg)); 123 | sig.on('room-info', (RoomInfo info) => onRoomInfo?.call(info)); 124 | sig.on('disconnect', (Disconnect disc) => onDisconnect?.call(disc)); 125 | sig.on('error', (Error err) => onError?.call(err)); 126 | sig.connect(); 127 | _sig = sig; 128 | } 129 | } 130 | 131 | Future? join({required Peer peer, String? password}) { 132 | return _sig?.join(peer: peer, password: password); 133 | } 134 | 135 | void leave(String uid) => _sig?.leave(uid); 136 | 137 | void message(String sid, Message message) => _sig?.sendMessage(sid, message); 138 | 139 | void updatePeer(Peer peer) => _sig?.updatePeer(peer); 140 | 141 | @override 142 | void close() { 143 | _sig?.close(); 144 | } 145 | } 146 | 147 | class _RoomGRPCClient extends EventEmitter { 148 | Service service; 149 | Connector connector; 150 | _RoomGRPCClient(this.connector, this.service) { 151 | _client = room.RoomSignalClient(connector.grpcClientChannel(), 152 | options: connector.callOptions()); 153 | _requestStream = StreamController(); 154 | _serviceClient = room.RoomServiceClient(connector.grpcClientChannel(), 155 | options: connector.callOptions()); 156 | } 157 | 158 | late room.RoomSignalClient _client; 159 | late room.RoomServiceClient _serviceClient; 160 | late StreamController _requestStream; 161 | late grpc.ResponseStream _replyStream; 162 | 163 | void connect() { 164 | _replyStream = _client.signal(_requestStream.stream); 165 | _replyStream.listen(_onSignalReply, onDone: () { 166 | _replyStream.trailers 167 | .then((trailers) => connector.onTrailers(service, trailers)); 168 | connector.onClosed(service); 169 | }, onError: (e) { 170 | _replyStream.trailers 171 | .then((trailers) => connector.onTrailers(service, trailers)); 172 | connector.onError(service, e); 173 | }); 174 | _replyStream.headers 175 | .then((headers) => connector.onHeaders(service, headers)); 176 | } 177 | 178 | void close() { 179 | _requestStream.close(); 180 | _replyStream.cancel(); 181 | } 182 | 183 | Future? join({required Peer peer, String? password}) async { 184 | Completer completer = Completer(); 185 | var request = pb.Request() 186 | ..join = pb.JoinRequest( 187 | peer: pb.Peer() 188 | ..uid = peer.uid 189 | ..sid = peer.sid 190 | ..displayName = peer.displayname 191 | ..extraInfo = peer.extrainfo 192 | ..role = pb.Role.values[peer.role.index] 193 | ..protocol = pb.Protocol.values[peer.protocol.index] 194 | ..avatar = peer.avatar 195 | ..vendor = peer.vendor 196 | ..direction = pb.Peer_Direction.values[peer.direction.index], 197 | password: password, 198 | ); 199 | _requestStream.add(request); 200 | Function(JoinResult) handler; 201 | handler = (result) { 202 | completer.complete(result); 203 | }; 204 | once('join-reply', handler); 205 | return completer.future as Future; 206 | } 207 | 208 | Future leave(String uid) async { 209 | Completer completer = Completer(); 210 | var request = pb.Request()..leave = pb.LeaveRequest(uid: uid); 211 | _requestStream.add(request); 212 | Function() handler; 213 | handler = () { 214 | completer.complete(); 215 | }; 216 | once('leave-reply', handler); 217 | } 218 | 219 | void sendMessage(String sid, Message msg) async { 220 | var request = pb.Request() 221 | ..sendMessage = pb.SendMessageRequest( 222 | sid: sid, 223 | message: pb.Message( 224 | from: msg.from, 225 | to: msg.to, 226 | type: msg.type, 227 | payload: msg.payload, 228 | )); 229 | _requestStream.add(request); 230 | } 231 | 232 | void updatePeer(Peer peer) async { 233 | await _serviceClient.updatePeer(pb.UpdatePeerRequest( 234 | peer: pb.Peer() 235 | ..uid = peer.uid 236 | ..sid = peer.sid 237 | ..displayName = peer.displayname 238 | ..extraInfo = peer.extrainfo 239 | ..role = pb.Role.values[peer.role.index] 240 | ..protocol = pb.Protocol.values[peer.protocol.index] 241 | ..avatar = peer.avatar 242 | ..vendor = peer.vendor 243 | ..direction = pb.Peer_Direction.values[peer.direction.index], 244 | )); 245 | } 246 | 247 | void _onSignalReply(pb.Reply reply) { 248 | switch (reply.whichPayload()) { 249 | case pb.Reply_Payload.join: 250 | RoomError? err; 251 | err = RoomError() 252 | ..code = ErrorType.values[reply.join.error.code.value] 253 | ..reason = reply.join.error.reason; 254 | var room = RoomInfo() 255 | ..sid = reply.join.room.sid 256 | ..name = reply.join.room.name 257 | ..lock = reply.join.room.lock 258 | ..password = reply.join.room.password 259 | ..description = reply.join.room.description 260 | ..maxpeers = reply.join.room.maxPeers; 261 | emit( 262 | 'join-reply', 263 | JoinResult() 264 | ..success = reply.join.success 265 | ..role = Role.values[reply.join.role.value] 266 | ..error = err 267 | ..room = room); 268 | break; 269 | case pb.Reply_Payload.leave: 270 | emit('leave-reply', reply.leave); 271 | break; 272 | case pb.Reply_Payload.peer: 273 | var event = reply.peer; 274 | var state = PeerState.NONE; 275 | switch (event.state) { 276 | case pb.PeerState.JOIN: 277 | state = PeerState.JOIN; 278 | break; 279 | case pb.PeerState.UPDATE: 280 | state = PeerState.UPDATE; 281 | break; 282 | case pb.PeerState.LEAVE: 283 | state = PeerState.LEAVE; 284 | break; 285 | } 286 | var peer = Peer() 287 | ..sid = event.peer.sid 288 | ..uid = event.peer.uid 289 | ..displayname = event.peer.displayName 290 | ..extrainfo = event.peer.extraInfo 291 | ..role = Role.values[event.peer.role.value] 292 | ..protocol = Protocol.values[event.peer.protocol.value] 293 | ..avatar = event.peer.avatar 294 | ..vendor = event.peer.vendor 295 | ..destination = event.peer.destination 296 | ..direction = Direction.values[event.peer.direction.value]; 297 | emit( 298 | 'peer-event', 299 | PeerEvent() 300 | ..state = state 301 | ..peer = peer); 302 | break; 303 | case pb.Reply_Payload.message: 304 | emit( 305 | 'message', 306 | Message() 307 | ..from = reply.message.from 308 | ..to = reply.message.to 309 | ..type = reply.message.type 310 | ..payload = reply.message.payload); 311 | break; 312 | case pb.Reply_Payload.room: 313 | var room = RoomInfo() 314 | ..sid = reply.room.sid 315 | ..name = reply.room.name 316 | ..lock = reply.room.lock 317 | ..password = reply.room.password 318 | ..description = reply.room.description 319 | ..maxpeers = reply.room.maxPeers; 320 | emit('room-info', room); 321 | break; 322 | case pb.Reply_Payload.disconnect: 323 | emit( 324 | 'disconnect', 325 | Disconnect() 326 | ..sid = reply.disconnect.sid 327 | ..reason = reply.disconnect.reason); 328 | break; 329 | default: 330 | break; 331 | } 332 | } 333 | } 334 | -------------------------------------------------------------------------------- /lib/src/connector/rtc.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:convert'; 3 | 4 | import 'package:events2/events2.dart'; 5 | import 'package:flutter_webrtc/flutter_webrtc.dart'; 6 | import 'package:grpc/grpc.dart' as grpc; 7 | 8 | import '../_library/proto/rtc/rtc.pbgrpc.dart' as pb; 9 | import '../client.dart'; 10 | import '../signal/signal.dart'; 11 | import '../stream.dart'; 12 | import 'ion.dart'; 13 | 14 | class RtcError { 15 | final int code; 16 | final String reason; 17 | RtcError(this.code, this.reason); 18 | } 19 | 20 | class Result { 21 | final bool success; 22 | RtcError? error; 23 | Result(this.success, this.error); 24 | } 25 | 26 | enum MediaType { 27 | MEDIAUNKNOWN, 28 | USERMEDIA, 29 | SCREENCAPTURE, 30 | CAVANS, 31 | STREAMING, 32 | VOIP, 33 | } 34 | 35 | class TrackInfo { 36 | late String id; 37 | late String stream_id; 38 | late String kind; 39 | late bool muted; 40 | late String label; 41 | late MediaType type; 42 | late String layer; 43 | late int width; 44 | late int height; 45 | late int frame_rate; 46 | } 47 | 48 | enum TrackState { 49 | ADD, 50 | UPDATE, 51 | REMOVE, 52 | } 53 | 54 | class TrackEvent { 55 | late TrackState state; 56 | late String uid; 57 | late List tracks; 58 | } 59 | 60 | class JoinConfig { 61 | late bool no_publish; 62 | late bool no_subscribe; 63 | late bool no_auto_subscribe; 64 | JoinConfig( 65 | {this.no_publish = false, 66 | this.no_subscribe = false, 67 | this.no_auto_subscribe = false}); 68 | } 69 | 70 | class Subscription { 71 | String trackId; 72 | bool mute; 73 | bool subscribe; 74 | String layer; 75 | Subscription( 76 | {required this.trackId, 77 | required this.mute, 78 | required this.subscribe, 79 | required this.layer}); 80 | } 81 | 82 | class RTC extends Service { 83 | @override 84 | String name = 'rtc'; 85 | Map config = Client.defaultConfig; 86 | Connector connector; 87 | _IonSFUGRPCSignal? _sig; 88 | late Client _client; 89 | 90 | Function(TrackEvent event)? ontrackevent; 91 | Function(MediaStreamTrack track, RemoteStream stream)? ontrack; 92 | Function(MediaStreamTrack track, RemoteStream stream)? onAddTrack; 93 | Function(MediaStreamTrack track, RemoteStream stream)? onRemoveTrack; 94 | Function(RTCDataChannel channel)? ondatachannel; 95 | Function(Map list)? onspeaker; 96 | 97 | RTC(this.connector, {Map? cfg}) { 98 | if (cfg != null) { 99 | config.addAll(cfg); 100 | } 101 | connector.registerService(this); 102 | } 103 | 104 | @override 105 | Future connect() async { 106 | _sig ??= _IonSFUGRPCSignal(connector, this); 107 | _sig?.ontrackevent = (event) => ontrackevent?.call(event); 108 | 109 | await _sig?.connect(); 110 | 111 | _client = Client(_sig!, config); 112 | _client.transports = { 113 | RolePub: 114 | await Transport.create(role: RolePub, signal: _sig!, config: config), 115 | RoleSub: 116 | await Transport.create(role: RoleSub, signal: _sig!, config: config) 117 | }; 118 | 119 | _client.signal.onready = () async { 120 | if (_client.initialized == false) { 121 | _client.initialized = true; 122 | } 123 | }; 124 | _client.onAddTrack = (MediaStreamTrack track, RemoteStream stream) => 125 | onAddTrack?.call(track, stream); 126 | _client.onRemoveTrack = (MediaStreamTrack track, RemoteStream stream) => 127 | onRemoveTrack?.call(track, stream); 128 | _client.ontrack = (MediaStreamTrack track, RemoteStream stream) => 129 | ontrack?.call(track, stream); 130 | _client.ondatachannel = 131 | (RTCDataChannel channel) => ondatachannel?.call(channel); 132 | _client.onspeaker = (list) => onspeaker?.call(list); 133 | } 134 | 135 | Future join(String sid, String uid, JoinConfig config) { 136 | _sig?.config = config; 137 | return _client.join(sid, uid); 138 | } 139 | 140 | Future>? getPubStats(MediaStreamTrack? selector) { 141 | return _client.getPubStats(selector); 142 | } 143 | 144 | Future>? getSubStats(MediaStreamTrack? selector) { 145 | return _client.getSubStats(selector); 146 | } 147 | 148 | Future publish(LocalStream stream) { 149 | _sig?._buildTrackInfos(stream.stream); 150 | return _client.publish(stream); 151 | } 152 | 153 | Future? subscribe(List infos) { 154 | return _sig?.subscribe(infos); 155 | } 156 | 157 | Future createDataChannel(String label) { 158 | return _client.createDataChannel(label); 159 | } 160 | 161 | @override 162 | void close() { 163 | _client.close(); 164 | _sig = null; 165 | } 166 | } 167 | 168 | class _IonSFUGRPCSignal extends Signal { 169 | Service service; 170 | Connector connector; 171 | final JsonDecoder _jsonDecoder = JsonDecoder(); 172 | final JsonEncoder _jsonEncoder = JsonEncoder(); 173 | final EventEmitter _emitter = EventEmitter(); 174 | late pb.RTCClient _client; 175 | late StreamController _requestStream; 176 | late grpc.ResponseStream _replyStream; 177 | JoinConfig? config; 178 | List _tracksInfos = []; 179 | Function(TrackEvent event)? ontrackevent; 180 | 181 | _IonSFUGRPCSignal(this.connector, this.service) { 182 | _client = pb.RTCClient(connector.grpcClientChannel(), 183 | options: connector.callOptions()); 184 | _requestStream = StreamController(); 185 | } 186 | 187 | void _onSignalReply(pb.Reply reply) { 188 | switch (reply.whichPayload()) { 189 | case pb.Reply_Payload.join: 190 | _emitter.emit('join-reply', reply.join); 191 | break; 192 | case pb.Reply_Payload.description: 193 | var desc = RTCSessionDescription( 194 | reply.description.sdp, reply.description.type); 195 | if (desc.type == 'offer') { 196 | onnegotiate?.call(desc); 197 | } else { 198 | _emitter.emit('description', desc); 199 | } 200 | break; 201 | case pb.Reply_Payload.trickle: 202 | var map = { 203 | 'target': reply.trickle.target.value, 204 | 'candidate': _jsonDecoder.convert(reply.trickle.init) 205 | }; 206 | ontrickle?.call(Trickle.fromMap(map)); 207 | break; 208 | case pb.Reply_Payload.trackEvent: 209 | var state = TrackState.ADD; 210 | switch (reply.trackEvent.state) { 211 | case pb.TrackEvent_State.ADD: 212 | state = TrackState.ADD; 213 | break; 214 | case pb.TrackEvent_State.UPDATE: 215 | state = TrackState.UPDATE; 216 | break; 217 | case pb.TrackEvent_State.REMOVE: 218 | state = TrackState.REMOVE; 219 | break; 220 | } 221 | var event = TrackEvent() 222 | ..uid = reply.trackEvent.uid 223 | ..state = state; 224 | 225 | Function(pb.MediaType ptype) convMediaType = (pb.MediaType ptype) { 226 | var type = MediaType.MEDIAUNKNOWN; 227 | switch (ptype) { 228 | case pb.MediaType.UserMedia: 229 | type = MediaType.USERMEDIA; 230 | break; 231 | case pb.MediaType.ScreenCapture: 232 | type = MediaType.SCREENCAPTURE; 233 | break; 234 | case pb.MediaType.Cavans: 235 | type = MediaType.CAVANS; 236 | break; 237 | case pb.MediaType.Streaming: 238 | type = MediaType.STREAMING; 239 | break; 240 | case pb.MediaType.VoIP: 241 | type = MediaType.VOIP; 242 | break; 243 | } 244 | return type; 245 | }; 246 | 247 | event.tracks = reply.trackEvent.tracks 248 | .map((e) => TrackInfo() 249 | ..id = e.id 250 | ..stream_id = e.streamId 251 | ..kind = e.kind 252 | ..muted = e.muted 253 | ..label = e.label 254 | ..layer = e.layer 255 | ..frame_rate = e.frameRate 256 | ..width = e.width 257 | ..height = e.height 258 | ..type = convMediaType(e.type)) 259 | .toList(); 260 | ontrackevent?.call(event); 261 | break; 262 | case pb.Reply_Payload.subscription: 263 | RtcError? err; 264 | err = RtcError( 265 | reply.subscription.error.code, reply.subscription.error.reason); 266 | _emitter.emit('subscription', Result(reply.subscription.success, err)); 267 | break; 268 | case pb.Reply_Payload.error: 269 | _emitter.emit('error', RtcError(reply.error.code, reply.error.reason)); 270 | break; 271 | case pb.Reply_Payload.notSet: 272 | break; 273 | } 274 | } 275 | 276 | @override 277 | Future connect() async { 278 | _replyStream = _client.signal(_requestStream.stream); 279 | _replyStream.listen(_onSignalReply, onDone: () { 280 | onclose?.call(0, 'closed'); 281 | _replyStream.trailers 282 | .then((trailers) => connector.onTrailers(service, trailers)); 283 | connector.onClosed(service); 284 | }, onError: (e) { 285 | onclose?.call(500, '$e'); 286 | _replyStream.trailers 287 | .then((trailers) => connector.onTrailers(service, trailers)); 288 | connector.onError(service, e); 289 | }); 290 | // ignore: unawaited_futures 291 | _replyStream.headers 292 | .then((headers) => connector.onHeaders(service, headers)); 293 | onready?.call(); 294 | } 295 | 296 | @override 297 | void close() { 298 | _requestStream.close(); 299 | _replyStream.cancel(); 300 | } 301 | 302 | @override 303 | Future join( 304 | String sid, String uid, RTCSessionDescription offer) { 305 | Completer completer = Completer(); 306 | var description = pb.SessionDescription( 307 | sdp: offer.sdp, type: offer.type, trackInfos: _tracksInfos); 308 | 309 | var cfg = {}; 310 | 311 | if (config != null) { 312 | cfg['NoPublish'] = config!.no_publish ? 'true' : 'false'; 313 | cfg['NoSubscribe'] = config!.no_subscribe ? 'true' : 'false'; 314 | cfg['NoAutoSubscribe'] = config!.no_auto_subscribe ? 'true' : 'false'; 315 | } 316 | 317 | var request = pb.Request() 318 | ..join = (pb.JoinRequest( 319 | sid: sid, 320 | uid: uid, 321 | config: cfg, 322 | description: description, 323 | )); 324 | _requestStream.add(request); 325 | Function(pb.JoinReply) handler; 326 | handler = (pb.JoinReply reply) { 327 | if (reply.success) { 328 | var desc = RTCSessionDescription( 329 | reply.description.sdp, reply.description.type); 330 | completer.complete(desc); 331 | } else { 332 | completer.completeError(reply.error); 333 | } 334 | }; 335 | _emitter.once('join-reply', handler); 336 | return completer.future as Future; 337 | } 338 | 339 | @override 340 | Future offer(RTCSessionDescription offer) { 341 | Completer completer = Completer(); 342 | var description = pb.SessionDescription( 343 | sdp: offer.sdp, type: offer.type, trackInfos: _tracksInfos); 344 | var req = pb.Request(description: description); 345 | _requestStream.add(req); 346 | Function(RTCSessionDescription) handler; 347 | handler = (desc) { 348 | completer.complete(desc); 349 | }; 350 | _emitter.once('description', handler); 351 | return completer.future as Future; 352 | } 353 | 354 | @override 355 | void answer(RTCSessionDescription answer) { 356 | var description = pb.SessionDescription(sdp: answer.sdp, type: answer.type); 357 | var req = pb.Request(description: description); 358 | _requestStream.add(req); 359 | } 360 | 361 | @override 362 | void trickle(Trickle trickle) { 363 | var req = pb.Request() 364 | ..trickle = (pb.Trickle() 365 | ..target = pb.Target.valueOf(trickle.target)! 366 | ..init = _jsonEncoder.convert(trickle.candidate.toMap())); 367 | _requestStream.add(req); 368 | } 369 | 370 | Future subscribe(List infos) async { 371 | final subscription = pb.SubscriptionRequest() 372 | ..subscriptions.addAll(infos.map((e) => pb.Subscription() 373 | ..layer = e.layer 374 | ..mute = e.mute 375 | ..subscribe = e.subscribe 376 | ..trackId = e.trackId)); 377 | var req = pb.Request(); 378 | req.subscription = subscription; 379 | _requestStream.add(req); 380 | Completer completer = Completer(); 381 | Function(Result) handler; 382 | handler = (ret) { 383 | completer.complete(ret); 384 | }; 385 | _emitter.once('subscription', handler); 386 | return completer.future as Future; 387 | } 388 | 389 | void _buildTrackInfos(MediaStream stream) { 390 | final tracks = stream.getTracks(); 391 | var trackInfos = []; 392 | for (var track in tracks) { 393 | trackInfos.add(pb.TrackInfo() 394 | ..type = pb.MediaType.UserMedia 395 | ..id = track.id! 396 | ..kind = track.kind! 397 | ..muted = track.muted! 398 | ..muted = !track.enabled 399 | ..streamId = stream.id); 400 | } 401 | _tracksInfos = trackInfos; 402 | } 403 | } 404 | -------------------------------------------------------------------------------- /lib/src/logger.dart: -------------------------------------------------------------------------------- 1 | class Logger { 2 | Logger(this._appName); 3 | 4 | final String _appName; 5 | 6 | void error(error) { 7 | print('[' + _appName + '] ERROR: ' + error); 8 | } 9 | 10 | void debug(msg) { 11 | print('[' + _appName + '] DEBUG: ' + msg); 12 | } 13 | 14 | void warn(msg) { 15 | print('[' + _appName + '] WARN: ' + msg); 16 | } 17 | 18 | void failure(error) { 19 | var log = '[' + _appName + '] FAILURE: ' + error; 20 | print(log); 21 | throw log; 22 | } 23 | } 24 | 25 | final log = Logger('ion-sdk-flutter'); 26 | -------------------------------------------------------------------------------- /lib/src/signal/grpc-web/_channel.dart: -------------------------------------------------------------------------------- 1 | import 'package:grpc/grpc.dart'; 2 | 3 | ClientChannel createChannel(String host, int port, bool secure, 4 | {List? certificates, String? authority, String? password}) { 5 | return ClientChannel(host, // Your IP here or localhost 6 | port: port, 7 | options: ChannelOptions( 8 | credentials: secure 9 | ? ChannelCredentials.secure( 10 | certificates: certificates, 11 | authority: authority, 12 | password: password) 13 | : ChannelCredentials.insecure(), 14 | idleTimeout: Duration(seconds: 1), 15 | )); 16 | } 17 | -------------------------------------------------------------------------------- /lib/src/signal/grpc-web/_channel_html.dart: -------------------------------------------------------------------------------- 1 | import 'package:grpc/grpc.dart'; 2 | import 'websocket_channel.dart'; 3 | 4 | ClientChannel createChannel(String host, int port, bool secure) { 5 | return WebSocketClientChannel(host, 6 | port: port, 7 | options: ChannelOptions( 8 | credentials: secure 9 | ? ChannelCredentials.secure() 10 | : ChannelCredentials.insecure())); 11 | } 12 | -------------------------------------------------------------------------------- /lib/src/signal/grpc-web/transport/websocket_transport.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:convert'; 3 | import 'dart:typed_data'; 4 | 5 | import 'package:grpc/grpc.dart'; 6 | import 'package:grpc/src/client/connection.dart'; 7 | import 'package:grpc/src/client/transport/transport.dart'; 8 | import 'package:grpc/src/client/transport/web_streams.dart'; 9 | import 'package:web_socket_channel/html.dart'; 10 | 11 | class WebSocketTransportStream implements GrpcTransportStream { 12 | final ErrorHandler _onError; 13 | final Function(WebSocketTransportStream stream) _onDone; 14 | final StreamController _incomingProcessor = StreamController(); 15 | final StreamController _incomingMessages = StreamController(); 16 | final StreamController> _outgoingMessages = StreamController(); 17 | late HtmlWebSocketChannel _channel; 18 | StreamSubscription? _channelSubscription; 19 | final Map _metadata; 20 | final Completer _firstMessageReceived = Completer(); 21 | final Uri _uri; 22 | 23 | @override 24 | Stream get incomingMessages => _incomingMessages.stream; 25 | 26 | @override 27 | StreamSink> get outgoingMessages => _outgoingMessages.sink; 28 | 29 | WebSocketTransportStream(this._uri, this._metadata, {onError, onDone}) 30 | : _onError = onError, 31 | _onDone = onDone { 32 | _incomingProcessor.stream 33 | .transform(GrpcWebDecoder()) 34 | .transform(grpcDecompressor()) 35 | .listen((event) { 36 | if (event is GrpcMetadata) { 37 | final metadata = event.metadata; 38 | if (metadata.containsKey('grpc-status')) { 39 | _close(); 40 | } 41 | //print(event.toString()); 42 | } 43 | _incomingMessages.add(event); 44 | }, onError: _onError, onDone: _incomingMessages.close); 45 | 46 | _runRequest(); 47 | } 48 | 49 | Future _runRequest() async { 50 | if (_incomingMessages.isClosed) { 51 | return; 52 | } 53 | 54 | _channel = 55 | HtmlWebSocketChannel.connect(_uri, protocols: ['grpc-websockets']); 56 | _metadata.addAll({ 57 | 'content-type': 'application/grpc-web+proto', 58 | 'x-grpc-web': '1', 59 | }); 60 | // Force a metadata message with headers. 61 | _channel.sink.add(headersToBytes(_metadata)); 62 | 63 | _outgoingMessages.stream.listen((message) { 64 | final frame = ByteData(message.length + 6); 65 | frame.setUint32(2, message.length); 66 | var idx = 6; 67 | for (var i = 0; i < message.length; i++) { 68 | frame.setUint8(idx++, message[i]); 69 | } 70 | _channel.sink.add(frame); 71 | }); 72 | 73 | _channelSubscription = _channel.stream.listen((message) async { 74 | final listBuffer = []; 75 | for (var i = 0; i < message.length; i++) { 76 | listBuffer.add(message[i]); 77 | } 78 | _incomingProcessor.add(Uint8List.fromList(listBuffer).buffer); 79 | }, onDone: _onDone(this), onError: _onError); 80 | } 81 | 82 | final Uint8List finishSendFrame = Uint8List.fromList([1]); 83 | 84 | final AsciiCodec ascii = AsciiCodec(); 85 | 86 | Uint8List headersToBytes(Map headers) { 87 | var asString = ''; 88 | headers.forEach((key, value) { 89 | asString += '$key: $value\r\n'; 90 | }); 91 | return ascii.encoder.convert(asString); 92 | } 93 | 94 | void _close() { 95 | if (!_firstMessageReceived.isCompleted) { 96 | _firstMessageReceived.complete(false); 97 | } 98 | _channelSubscription?.cancel(); 99 | _incomingProcessor.close(); 100 | _outgoingMessages.close(); 101 | _onDone(this); 102 | } 103 | 104 | @override 105 | Future terminate() async { 106 | _channel.sink.add(finishSendFrame); 107 | _close(); 108 | } 109 | } 110 | 111 | class WebSocketClientConnection extends ClientConnection { 112 | final String host; 113 | final int? port; 114 | final ChannelOptions options; 115 | final Set _requests = {}; 116 | 117 | WebSocketClientConnection(this.host, this.port, {required this.options}) 118 | : assert(host.isNotEmpty == true), 119 | assert(port == null || port > 0); 120 | 121 | @override 122 | String get authority => 123 | '$host:${port ?? (options.credentials.isSecure ? 443 : 80)}'; 124 | 125 | @override 126 | String get scheme => "ws${options.credentials.isSecure ? "s" : ""}"; 127 | 128 | /* 129 | void _initializeRequest( 130 | IOWebSocketChannel channel, Map metadata) { 131 | for (final header in metadata.keys) { 132 | request.headers.add(header, metadata[header]); 133 | } 134 | request.headers.add('Content-Type', 'application/grpc-web+proto'); 135 | request.headers.add('X-User-Agent', 'grpc-web-dart/0.1'); 136 | request.headers.add('X-Grpc-Web', '1'); 137 | } 138 | */ 139 | 140 | @override 141 | GrpcTransportStream makeRequest(String path, Duration? timeout, 142 | Map metadata, ErrorHandler onRequestFailure, 143 | {required CallOptions callOptions}) { 144 | final uri = Uri.parse('$scheme://$host:$port$path'); 145 | final transportStream = WebSocketTransportStream(uri, metadata, 146 | onError: onRequestFailure, onDone: _removeStream); 147 | _requests.add(transportStream); 148 | return transportStream; 149 | } 150 | 151 | void _removeStream(WebSocketTransportStream stream) { 152 | _requests.remove(stream); 153 | } 154 | 155 | @override 156 | Future terminate() async { 157 | for (var request in _requests) { 158 | await request.terminate(); 159 | } 160 | } 161 | 162 | @override 163 | void dispatchCall(ClientCall call) { 164 | call.onConnectionReady(this); 165 | } 166 | 167 | @override 168 | Future shutdown() async {} 169 | } 170 | -------------------------------------------------------------------------------- /lib/src/signal/grpc-web/websocket_channel.dart: -------------------------------------------------------------------------------- 1 | import 'package:grpc/grpc.dart'; 2 | import 'package:grpc/src/client/connection.dart'; 3 | import 'transport/websocket_transport.dart'; 4 | 5 | class WebSocketClientChannel extends ClientChannel { 6 | @override 7 | final String host; 8 | @override 9 | final int port; 10 | @override 11 | final ChannelOptions options; 12 | 13 | WebSocketClientChannel(this.host, {required this.port, required this.options}) 14 | : assert(host.isNotEmpty == true), 15 | super(host, port: port, options: options); 16 | 17 | @override 18 | ClientConnection createConnection() { 19 | return WebSocketClientConnection(host, port, options: options); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lib/src/signal/json-rpc/common.dart: -------------------------------------------------------------------------------- 1 | typedef OnMessageCallback = void Function(dynamic msg); 2 | typedef OnCloseCallback = void Function(int code, String reason); 3 | typedef OnOpenCallback = void Function(); 4 | -------------------------------------------------------------------------------- /lib/src/signal/json-rpc/websocket.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:convert'; 3 | import 'dart:io'; 4 | import 'dart:math'; 5 | 6 | import '../../logger.dart'; 7 | import 'common.dart'; 8 | 9 | class SimpleWebSocket { 10 | SimpleWebSocket(this._url); 11 | 12 | final String _url; 13 | var _socket; 14 | 15 | OnOpenCallback? onOpen; 16 | OnMessageCallback? onMessage; 17 | OnCloseCallback? onClose; 18 | 19 | void connect() async { 20 | try { 21 | //_socket = await WebSocket.connect(_url); 22 | _socket = await _connectForSelfSignedCert(_url); 23 | onOpen?.call(); 24 | _socket.listen((data) { 25 | onMessage?.call(data); 26 | }, onDone: () { 27 | onClose?.call(_socket.closeCode, _socket.closeReason); 28 | }); 29 | } catch (e) { 30 | print('connect: error => $e'); 31 | onClose?.call(500, e.toString()); 32 | } 33 | } 34 | 35 | void send(data) { 36 | if (_socket != null) { 37 | _socket.add(data); 38 | log.debug('send: $data'); 39 | } 40 | } 41 | 42 | void close() { 43 | _socket?.close(); 44 | } 45 | 46 | Future _connectForSelfSignedCert(url) async { 47 | try { 48 | var r = Random(); 49 | var key = base64.encode(List.generate(8, (_) => r.nextInt(255))); 50 | var client = HttpClient(context: SecurityContext()); 51 | client.badCertificateCallback = 52 | (X509Certificate cert, String host, int port) { 53 | log.debug( 54 | 'SimpleWebSocket: Allow self-signed certificate => $host:$port. '); 55 | return true; 56 | }; 57 | 58 | var parsed_uri = Uri.parse(url); 59 | var uri = parsed_uri.replace( 60 | scheme: parsed_uri.scheme == 'wss' ? 'https' : 'http'); 61 | 62 | var request = await client.getUrl(uri); // form the correct url here 63 | request.headers.add('Connection', 'Upgrade'); 64 | request.headers.add('Upgrade', 'websocket'); 65 | request.headers.add( 66 | 'Sec-WebSocket-Version', '13'); // insert the correct version here 67 | request.headers.add('Sec-WebSocket-Key', key.toLowerCase()); 68 | 69 | var response = await request.close(); 70 | // ignore: close_sinks 71 | var socket = await response.detachSocket(); 72 | var webSocket = WebSocket.fromUpgradedSocket( 73 | socket, 74 | protocol: 'signaling', 75 | serverSide: false, 76 | ); 77 | 78 | return webSocket; 79 | } catch (e) { 80 | log.error(e); 81 | rethrow; 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /lib/src/signal/json-rpc/websocket_web.dart: -------------------------------------------------------------------------------- 1 | // ignore: avoid_web_libraries_in_flutter 2 | import 'dart:html'; 3 | 4 | import '../../logger.dart'; 5 | import 'common.dart'; 6 | 7 | class SimpleWebSocket { 8 | SimpleWebSocket(this._url); 9 | 10 | final String _url; 11 | var _socket; 12 | OnOpenCallback? onOpen; 13 | OnMessageCallback? onMessage; 14 | OnCloseCallback? onClose; 15 | 16 | void connect() async { 17 | try { 18 | _socket = WebSocket(_url); 19 | _socket.onOpen.listen((e) { 20 | onOpen?.call(); 21 | }); 22 | 23 | _socket.onMessage.listen((e) { 24 | onMessage?.call(e.data); 25 | }); 26 | 27 | _socket.onClose.listen((e) { 28 | onClose?.call(e.code, e.reason); 29 | }); 30 | } catch (e) { 31 | onClose?.call(500, e.toString()); 32 | } 33 | } 34 | 35 | void send(data) { 36 | if (_socket != null && _socket.readyState == WebSocket.OPEN) { 37 | _socket.send(data); 38 | log.debug('send: $data'); 39 | } else { 40 | log.error('WebSocket not connected, message $data not sent'); 41 | } 42 | } 43 | 44 | void close() { 45 | _socket?.close(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /lib/src/signal/signal.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_webrtc/flutter_webrtc.dart'; 2 | 3 | class Trickle { 4 | Trickle({required this.target, required this.candidate}); 5 | factory Trickle.fromMap(Map map) => Trickle( 6 | candidate: RTCIceCandidate( 7 | map['candidate']['candidate'], 8 | map['candidate']['sdpMid'], 9 | map['candidate']['sdpMLineIndex'], 10 | ), 11 | target: map['target']); 12 | RTCIceCandidate candidate; 13 | int target; 14 | Map toMap() => {'target': target, 'candidate': candidate.toMap()}; 15 | } 16 | 17 | final RolePub = 0; 18 | final RoleSub = 1; 19 | 20 | typedef OnNegotiateCallback = Function(RTCSessionDescription jsep); 21 | typedef OnReadyCallback = Function(); 22 | typedef OnTrickleCallback = Function(Trickle trickle); 23 | typedef OnCloseCallback = Function(int code, String reason); 24 | 25 | abstract class Signal { 26 | OnNegotiateCallback? onnegotiate; 27 | 28 | OnReadyCallback? onready; 29 | 30 | OnCloseCallback? onclose; 31 | 32 | OnTrickleCallback? ontrickle; 33 | 34 | Future join( 35 | String sid, String uid, RTCSessionDescription offer); 36 | 37 | Future offer(RTCSessionDescription offer); 38 | 39 | void answer(RTCSessionDescription answer); 40 | 41 | void trickle(Trickle trickle); 42 | 43 | Future connect(); 44 | 45 | void close(); 46 | } 47 | -------------------------------------------------------------------------------- /lib/src/signal/signal_grpc_impl.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:convert'; 3 | 4 | import 'package:events2/events2.dart'; 5 | import 'package:flutter_webrtc/flutter_webrtc.dart'; 6 | import 'package:grpc/grpc.dart'; 7 | import 'package:uuid/uuid.dart'; 8 | 9 | import '../_library/proto/rtc/rtc.pbgrpc.dart' as pb; 10 | 11 | import 'grpc-web/_channel.dart' 12 | if (dart.library.html) 'grpc-web/_channel_html.dart'; 13 | import 'signal.dart'; 14 | 15 | class GRPCWebSignal extends Signal { 16 | GRPCWebSignal(this._uri, 17 | {List? certificates, String? authority, String? password}) { 18 | var uri = Uri.parse(_uri); 19 | var channel = createChannel(uri.host, uri.port, uri.scheme == 'https'); 20 | _client = pb.RTCClient(channel); 21 | _requestStream = StreamController(); 22 | } 23 | 24 | final String _uri; 25 | final JsonDecoder _jsonDecoder = JsonDecoder(); 26 | final JsonEncoder _jsonEncoder = JsonEncoder(); 27 | final Uuid _uuid = Uuid(); 28 | final EventEmitter _emitter = EventEmitter(); 29 | late pb.RTCClient _client; 30 | late StreamController _requestStream; 31 | late ResponseStream _replyStream; 32 | 33 | void _onSignalReply(pb.Reply reply) { 34 | switch (reply.whichPayload()) { 35 | case pb.Reply_Payload.join: 36 | _emitter.emit('join-reply', reply.join.description); 37 | break; 38 | case pb.Reply_Payload.description: 39 | var desc = RTCSessionDescription( 40 | reply.description.sdp, reply.description.type); 41 | if (reply.description.type == 'offer') { 42 | onnegotiate?.call(desc); 43 | } else { 44 | _emitter.emit('description', reply, reply.description); 45 | } 46 | break; 47 | case pb.Reply_Payload.trickle: 48 | var map = { 49 | 'target': reply.trickle.target.value, 50 | 'candidate': _jsonDecoder.convert(reply.trickle.init) 51 | }; 52 | ontrickle?.call(Trickle.fromMap(map)); 53 | break; 54 | // case pb.Reply_Payload.iceConnectionState: 55 | case pb.Reply_Payload.error: 56 | case pb.Reply_Payload.notSet: 57 | case pb.Reply_Payload.subscription: 58 | case pb.Reply_Payload.trackEvent: 59 | break; 60 | } 61 | } 62 | 63 | @override 64 | Future connect() async { 65 | _replyStream = _client.signal(_requestStream.stream); 66 | _replyStream.listen(_onSignalReply, 67 | onDone: () => onclose?.call(0, 'closed'), 68 | onError: (e) => onclose?.call(500, '$e')); 69 | onready?.call(); 70 | } 71 | 72 | @override 73 | void close() { 74 | _requestStream.close(); 75 | _replyStream.cancel(); 76 | } 77 | 78 | @override 79 | Future join( 80 | String sid, String uid, RTCSessionDescription offer) { 81 | Completer completer = Completer(); 82 | var request = pb.Request() 83 | ..join = (pb.JoinRequest() 84 | ..description = offer.toMap() 85 | ..sid = sid 86 | ..uid = uid); 87 | _requestStream.add(request); 88 | 89 | Function(RTCSessionDescription) handler; 90 | handler = (desc) { 91 | completer.complete(desc); 92 | }; 93 | _emitter.once('join-reply', handler); 94 | return completer.future as Future; 95 | } 96 | 97 | @override 98 | Future offer(RTCSessionDescription offer) { 99 | Completer completer = Completer(); 100 | var id = _uuid.v4(); 101 | var request = pb.Request()..description = offer.toMap(); 102 | _requestStream.add(request); 103 | Function(String, dynamic) handler; 104 | handler = (respid, desc) { 105 | if (respid == id) { 106 | completer.complete(desc); 107 | } 108 | }; 109 | _emitter.once('description', handler); 110 | return completer.future as Future; 111 | } 112 | 113 | @override 114 | void answer(RTCSessionDescription answer) { 115 | var reply = pb.Request()..description = answer.toMap(); 116 | _requestStream.add(reply); 117 | } 118 | 119 | @override 120 | void trickle(Trickle trickle) { 121 | var reply = pb.Request() 122 | ..trickle = (pb.Trickle() 123 | ..target = pb.Target.valueOf(trickle.target)! 124 | ..init = _jsonEncoder.convert(trickle.candidate.toMap())); 125 | _requestStream.add(reply); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /lib/src/signal/signal_jsonrpc_impl.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:convert'; 3 | 4 | import 'package:events2/events2.dart'; 5 | import 'package:flutter_webrtc/flutter_webrtc.dart'; 6 | import 'package:uuid/uuid.dart'; 7 | 8 | import '../logger.dart'; 9 | import 'json-rpc/websocket.dart' 10 | if (dart.library.js) 'json-rpc/websocket_web.dart'; 11 | import 'signal.dart'; 12 | 13 | class JsonRPCSignal extends Signal { 14 | JsonRPCSignal(this._uri) { 15 | _socket = SimpleWebSocket(_uri); 16 | 17 | _socket.onOpen = () => onready?.call(); 18 | 19 | _socket.onClose = (int code, String reason) => onclose?.call(code, reason); 20 | 21 | _socket.onMessage = (msg) => _onmessage(msg); 22 | } 23 | 24 | final String _uri; 25 | final JsonDecoder _jsonDecoder = JsonDecoder(); 26 | final JsonEncoder _jsonEncoder = JsonEncoder(); 27 | final Uuid _uuid = Uuid(); 28 | final EventEmitter _emitter = EventEmitter(); 29 | late SimpleWebSocket _socket; 30 | 31 | void _onmessage(msg) { 32 | log.debug('msg: $msg'); 33 | try { 34 | var resp = _jsonDecoder.convert(msg); 35 | if (resp['method'] != null || resp['result'] != null) { 36 | if (resp['method'] == 'offer') { 37 | onnegotiate?.call(RTCSessionDescription( 38 | resp['params']['sdp'], resp['params']['type'])); 39 | } else if (resp['method'] == 'trickle') { 40 | ontrickle?.call(Trickle.fromMap(resp['params'])); 41 | } else { 42 | _emitter.emit('message', resp); 43 | } 44 | } else if (resp['error'] != null) { 45 | var code = resp['error']['code']; 46 | var message = resp['error']['message']; 47 | log.error('error: code => $code, message => $message'); 48 | } 49 | } catch (e) { 50 | log.error('onmessage: err => $e'); 51 | } 52 | } 53 | 54 | @override 55 | Future connect() async { 56 | _socket.connect(); 57 | } 58 | 59 | @override 60 | void close() { 61 | _socket.close(); 62 | } 63 | 64 | @override 65 | Future join( 66 | String sid, String uid, RTCSessionDescription offer) { 67 | Completer completer = Completer(); 68 | var id = _uuid.v4(); 69 | _socket.send(_jsonEncoder.convert({ 70 | 'method': 'join', 71 | 'params': {'sid': sid, 'uid': uid, 'offer': offer.toMap()}, 72 | 'id': id 73 | })); 74 | 75 | Function(dynamic) handler; 76 | handler = (resp) { 77 | if (resp['id'] == id) { 78 | completer.complete(RTCSessionDescription( 79 | resp['result']['sdp'], resp['result']['type'])); 80 | } 81 | }; 82 | _emitter.once('message', handler); 83 | return completer.future as Future; 84 | } 85 | 86 | @override 87 | Future offer(RTCSessionDescription offer) { 88 | Completer completer = Completer(); 89 | var id = _uuid.v4(); 90 | _socket.send(_jsonEncoder.convert({ 91 | 'method': 'offer', 92 | 'params': {'desc': offer.toMap()}, 93 | 'id': id 94 | })); 95 | 96 | Function(dynamic) handler; 97 | handler = (resp) { 98 | if (resp['id'] == id) { 99 | completer.complete(RTCSessionDescription( 100 | resp['result']['sdp'], resp['result']['type'])); 101 | } 102 | }; 103 | _emitter.once('message', handler); 104 | return completer.future as Future; 105 | } 106 | 107 | @override 108 | void answer(RTCSessionDescription answer) { 109 | _socket.send(_jsonEncoder.convert({ 110 | 'method': 'answer', 111 | 'params': {'desc': answer.toMap()}, 112 | })); 113 | } 114 | 115 | @override 116 | void trickle(Trickle trickle) { 117 | _socket.send(_jsonEncoder.convert({ 118 | 'method': 'trickle', 119 | 'params': trickle.toMap(), 120 | })); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /lib/src/stream.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:flutter_webrtc/flutter_webrtc.dart'; 4 | 5 | import 'client.dart'; 6 | import 'logger.dart'; 7 | 8 | class FrameRate { 9 | FrameRate({required this.ideal, required this.max}); 10 | int ideal; 11 | int max; 12 | Map toMap() => { 13 | 'ideal': ideal, 14 | 'max': max, 15 | }; 16 | } 17 | 18 | class MediaTrackConstraints { 19 | MediaTrackConstraints( 20 | {required this.frameRate, required this.height, required this.width}); 21 | 22 | /// Properties of video tracks 23 | FrameRate frameRate; 24 | int height; 25 | int width; 26 | 27 | Map toMap() => { 28 | 'width': {'ideal': width}, 29 | 'height': {'ideal': height}, 30 | 'frameRate': frameRate.toMap() 31 | }; 32 | } 33 | 34 | class VideoConstraints { 35 | VideoConstraints({required this.constraints, required this.encodings}); 36 | final MediaTrackConstraints constraints; 37 | final RTCRtpEncoding encodings; 38 | } 39 | 40 | var resolutions = ['qvga', 'vga', 'shd', 'hd', 'fhd', 'qhd']; 41 | 42 | var videoConstraints = { 43 | 'qvga': VideoConstraints( 44 | constraints: MediaTrackConstraints( 45 | width: 320, height: 180, frameRate: FrameRate(ideal: 15, max: 30)), 46 | encodings: RTCRtpEncoding(maxBitrate: 150000, maxFramerate: 15)), 47 | 'vga': VideoConstraints( 48 | constraints: MediaTrackConstraints( 49 | width: 640, height: 360, frameRate: FrameRate(ideal: 30, max: 60)), 50 | encodings: RTCRtpEncoding(maxBitrate: 500000, maxFramerate: 30)), 51 | 'shd': VideoConstraints( 52 | constraints: MediaTrackConstraints( 53 | width: 960, height: 540, frameRate: FrameRate(ideal: 30, max: 60)), 54 | encodings: RTCRtpEncoding(maxBitrate: 1200000, maxFramerate: 30)), 55 | 'hd': VideoConstraints( 56 | constraints: MediaTrackConstraints( 57 | width: 1280, height: 720, frameRate: FrameRate(ideal: 30, max: 60)), 58 | encodings: RTCRtpEncoding(maxBitrate: 2500000, maxFramerate: 30)), 59 | 'fhd': VideoConstraints( 60 | constraints: MediaTrackConstraints( 61 | width: 1920, height: 1080, frameRate: FrameRate(ideal: 30, max: 60)), 62 | encodings: RTCRtpEncoding(maxBitrate: 4000000, maxFramerate: 30)), 63 | 'qhd': VideoConstraints( 64 | constraints: MediaTrackConstraints( 65 | width: 2560, height: 1440, frameRate: FrameRate(ideal: 30, max: 60)), 66 | encodings: RTCRtpEncoding(maxBitrate: 8000000, maxFramerate: 30)), 67 | }; 68 | 69 | enum Layer { none, low, medium, high } 70 | 71 | Map layerStringType = { 72 | Layer.none: 'none', 73 | Layer.low: 'low', 74 | Layer.medium: 'medium', 75 | Layer.high: 'high' 76 | }; 77 | 78 | class Encoding { 79 | Layer? layer; 80 | int? maxBitrate; 81 | int? maxFramerate; 82 | } 83 | 84 | class Constraints { 85 | Constraints( 86 | {this.resolution, 87 | this.deviceId, 88 | this.codec, 89 | this.audio, 90 | this.video, 91 | this.simulcast}); 92 | String? resolution; 93 | String? codec; 94 | bool? simulcast; 95 | bool? audio; 96 | bool? video; 97 | String? deviceId; 98 | 99 | static final defaults = Constraints( 100 | resolution: 'hd', 101 | codec: 'vp8', 102 | audio: true, 103 | video: true, 104 | simulcast: false); 105 | } 106 | 107 | class LocalStream { 108 | LocalStream(this._stream, this._constraints); 109 | final Constraints _constraints; 110 | RTCPeerConnection? _pc; 111 | final MediaStream _stream; 112 | 113 | MediaStream get stream => _stream; 114 | 115 | static Future getUserMedia({Constraints? constraints}) async { 116 | var stream = await navigator.mediaDevices.getUserMedia({ 117 | 'audio': LocalStream.computeAudioConstraints( 118 | constraints ?? Constraints.defaults), 119 | 'video': LocalStream.computeVideoConstraints( 120 | constraints ?? Constraints.defaults) 121 | }); 122 | return LocalStream(stream, constraints ?? Constraints.defaults); 123 | } 124 | 125 | static Future getDisplayMedia({Constraints? constraints}) async { 126 | var stream = await navigator.mediaDevices.getDisplayMedia({ 127 | 'video': true, 128 | }); 129 | return LocalStream(stream, Constraints.defaults); 130 | } 131 | 132 | static dynamic computeAudioConstraints(Constraints constraints) { 133 | if (constraints.audio != null) { 134 | return true; 135 | } else if (constraints.video! && constraints.resolution != null) { 136 | return {'deviceId': constraints.deviceId}; 137 | } 138 | return false; 139 | } 140 | 141 | static dynamic computeVideoConstraints(Constraints constraints) { 142 | if (constraints.video! && constraints.resolution == null) { 143 | return true; 144 | } else if (constraints.video! && constraints.resolution != null) { 145 | var resolution = videoConstraints[constraints.resolution]!.constraints; 146 | var mobileConstraints = WebRTC.platformIsWeb 147 | ? {} 148 | : { 149 | 'mandatory': { 150 | 'minWidth': '1280', 151 | 'minHeight': '720', 152 | 'minFrameRate': '30', 153 | }, 154 | 'facingMode': 'user', 155 | 'optional': [] 156 | }; 157 | return {...resolution.toMap(), ...mobileConstraints}; 158 | } 159 | return false; 160 | } 161 | 162 | /// 'audio' | 'video' 163 | MediaStreamTrack? getTrack(String kind) { 164 | var tracks; 165 | if (kind == 'video') { 166 | tracks = _stream.getVideoTracks(); 167 | return tracks.length > 0 ? _stream.getVideoTracks()[0] : null; 168 | } 169 | tracks = _stream.getAudioTracks(); 170 | return tracks.length > 0 ? _stream.getAudioTracks()[0] : null; 171 | } 172 | 173 | /// 'audio' | 'video' 174 | Future getNewTrack(String kind) async { 175 | var stream = await navigator.mediaDevices.getUserMedia({ 176 | kind: kind == 'video' 177 | ? LocalStream.computeVideoConstraints(_constraints) 178 | : LocalStream.computeAudioConstraints(_constraints), 179 | }); 180 | return stream.getTracks()[0]; 181 | } 182 | 183 | void publishTrack({required MediaStreamTrack track}) async { 184 | if (_pc != null) { 185 | if (track.kind == 'video' && _constraints.simulcast!) { 186 | var idx = resolutions.indexOf(_constraints.resolution!); 187 | var encodings = [ 188 | RTCRtpEncoding( 189 | rid: 'f', 190 | active: true, 191 | maxBitrate: 192 | videoConstraints[resolutions[idx]]!.encodings.maxBitrate, 193 | minBitrate: 256000, 194 | scaleResolutionDownBy: 1.0, 195 | maxFramerate: 196 | videoConstraints[resolutions[idx]]!.encodings.maxFramerate, 197 | ) 198 | ]; 199 | 200 | if (idx - 1 >= 0) { 201 | encodings.add(RTCRtpEncoding( 202 | rid: 'h', 203 | active: true, 204 | scaleResolutionDownBy: 2.0, 205 | maxBitrate: 206 | videoConstraints[resolutions[idx - 1]]!.encodings.maxBitrate, 207 | minBitrate: 128000, 208 | maxFramerate: 209 | videoConstraints[resolutions[idx - 1]]!.encodings.maxFramerate, 210 | )); 211 | } 212 | 213 | if (idx - 2 >= 0) { 214 | encodings.add(RTCRtpEncoding( 215 | rid: 'q', 216 | active: true, 217 | minBitrate: 64000, 218 | scaleResolutionDownBy: 4.0, 219 | maxBitrate: 220 | videoConstraints[resolutions[idx - 2]]!.encodings.maxBitrate, 221 | maxFramerate: 222 | videoConstraints[resolutions[idx - 2]]!.encodings.maxFramerate, 223 | )); 224 | } 225 | 226 | var transceiver = await _pc?.addTransceiver( 227 | track: track, 228 | init: RTCRtpTransceiverInit( 229 | streams: [_stream], 230 | direction: TransceiverDirection.SendOnly, 231 | sendEncodings: encodings, 232 | )); 233 | setPreferredCodec(transceiver); 234 | } else { 235 | var transceiver = await _pc?.addTransceiver( 236 | track: track, 237 | init: RTCRtpTransceiverInit( 238 | streams: [_stream], 239 | direction: TransceiverDirection.SendOnly, 240 | sendEncodings: track.kind == 'video' 241 | ? [videoConstraints[_constraints.resolution]!.encodings] 242 | : [], 243 | )); 244 | if (track.kind == 'video') { 245 | setPreferredCodec(transceiver); 246 | } 247 | } 248 | } 249 | } 250 | 251 | void setPreferredCodec(RTCRtpTransceiver? transceiver) { 252 | // TODO(cloudwebrtc): need to add implementation in flutter-webrtc. 253 | /* 254 | if ('setCodecPreferences' in transceiver) { 255 | var cap = RTCRtpSender.getCapabilities('video'); 256 | if (!cap) return; 257 | var selCodec = cap.codecs.find( 258 | (c) => c.mimeType == `video/${Constraints.codec.toUpperCase()}` || c.mimeType == `audio/OPUS`, 259 | ); 260 | if (selCodec) { 261 | transceiver.setCodecPreferences([selCodec]); 262 | } 263 | } 264 | */ 265 | } 266 | 267 | Future updateTrack( 268 | {required MediaStreamTrack next, MediaStreamTrack? prev}) async { 269 | await _stream.addTrack(next); 270 | // If published, replace published track with track from new device 271 | if (prev != null && prev.enabled) { 272 | await _stream.removeTrack(prev); 273 | await prev.stop(); 274 | if (_pc != null) { 275 | await _pc! 276 | .getSenders() 277 | .then((senders) => senders.forEach((RTCRtpSender sender) { 278 | if (sender.track?.kind == next.kind) { 279 | sender.track?.stop(); 280 | sender.replaceTrack(next); 281 | } 282 | })); 283 | } 284 | } else { 285 | await _stream.addTrack(next); 286 | 287 | if (_pc != null) { 288 | publishTrack(track: next); 289 | } 290 | } 291 | } 292 | 293 | Future publish(RTCPeerConnection pc) async { 294 | _pc = pc; 295 | _stream.getTracks().forEach((track) async => publishTrack(track: track)); 296 | } 297 | 298 | Future unpublish() async { 299 | if (_pc != null) { 300 | var tracks = _stream.getTracks(); 301 | await _pc! 302 | .getSenders() 303 | .then((senders) => senders.forEach((RTCRtpSender s) async { 304 | if (tracks.contains((e) => s.track?.id == e.id)) { 305 | await _pc?.removeTrack(s); 306 | } 307 | })); 308 | } 309 | } 310 | 311 | /// 'audio' | 'video' 312 | Future switchDevice(String kind, {required String deviceId}) async { 313 | _constraints.deviceId = deviceId; 314 | var prev = getTrack(kind); 315 | var next = await getNewTrack(kind); 316 | await updateTrack(next: next, prev: prev); 317 | } 318 | 319 | // 'audio' | 'video' 320 | Future mute(String kind) async { 321 | var track = getTrack(kind); 322 | if (track != null) { 323 | await track.stop(); 324 | } 325 | } 326 | 327 | /// 'audio' | 'video' 328 | Future unmute(String kind) async { 329 | var prev = getTrack(kind); 330 | var track = await getNewTrack(kind); 331 | await updateTrack(next: track, prev: prev); 332 | } 333 | } 334 | 335 | class RemoteStream { 336 | RTCDataChannel? api; 337 | late MediaStream stream; 338 | late bool audio; 339 | late Layer video; 340 | late Layer _videoPreMute; 341 | String get id => stream.id; 342 | 343 | Function(Layer layer)? preferLayer; 344 | Function(String kind)? mute; 345 | Function(String kind)? unmute; 346 | } 347 | 348 | final jsonEncoder = JsonEncoder(); 349 | RemoteStream makeRemote(MediaStream stream, Transport transport) { 350 | var remote = RemoteStream(); 351 | remote.stream = stream; 352 | remote.audio = true; 353 | remote.video = Layer.none; 354 | remote._videoPreMute = Layer.high; 355 | 356 | var select = () { 357 | var call = { 358 | 'streamId': remote.id, 359 | 'video': layerStringType[remote.video], 360 | 'audio': remote.audio, 361 | }; 362 | if (transport.api == null) { 363 | log.warn('api datachannel not ready yet'); 364 | } 365 | 366 | if (transport.api == null || 367 | (transport.api != null && 368 | transport.api?.state != RTCDataChannelState.RTCDataChannelOpen)) { 369 | /// queue call if we aren't open yet 370 | transport.onapiopen = () { 371 | transport.api?.send(RTCDataChannelMessage(jsonEncoder.convert(call))); 372 | }; 373 | } 374 | 375 | transport.api?.send(RTCDataChannelMessage(jsonEncoder.convert(call))); 376 | }; 377 | 378 | remote.preferLayer = (Layer layer) { 379 | remote.video = layer; 380 | select(); 381 | }; 382 | 383 | remote.mute = (kind) { 384 | if (kind == 'audio') { 385 | remote.audio = false; 386 | } else if (kind == 'video') { 387 | remote._videoPreMute = remote.video; 388 | remote.video = Layer.none; 389 | } 390 | select(); 391 | }; 392 | 393 | remote.unmute = (kind) { 394 | if (kind == 'audio') { 395 | remote.audio = true; 396 | } else if (kind == 'video') { 397 | remote.video = remote._videoPreMute; 398 | } 399 | select(); 400 | }; 401 | 402 | return remote; 403 | } 404 | -------------------------------------------------------------------------------- /lib/src/utils.dart: -------------------------------------------------------------------------------- 1 | import 'package:sdp_transform/sdp_transform.dart' as sdp_transform; 2 | 3 | class CodecCapability { 4 | CodecCapability( 5 | this.kind, this.payloads, this.codecs, this.fmtp, this.rtcpFb) { 6 | codecs.forEach((element) { 7 | element['orign_payload'] = element['payload']; 8 | }); 9 | } 10 | String kind; 11 | List rtcpFb; 12 | List fmtp; 13 | List payloads; 14 | List codecs; 15 | bool setCodecPreferences(String kind, List? newCodecs) { 16 | if (newCodecs == null) { 17 | return false; 18 | } 19 | var newRtcpFb = []; 20 | var newFmtp = []; 21 | var newPayloads = []; 22 | newCodecs.forEach((element) { 23 | var orign_payload = element['orign_payload'] as int; 24 | var payload = element['payload'] as int; 25 | // change payload type 26 | if (payload != orign_payload) { 27 | newRtcpFb.addAll(rtcpFb.where((e) { 28 | if (e['payload'] == orign_payload) { 29 | e['payload'] = payload; 30 | return true; 31 | } 32 | return false; 33 | }).toList()); 34 | newFmtp.addAll(fmtp.where((e) { 35 | if (e['payload'] == orign_payload) { 36 | e['payload'] = payload; 37 | return true; 38 | } 39 | return false; 40 | }).toList()); 41 | if (payloads.contains('$orign_payload')) { 42 | newPayloads.add('$payload'); 43 | } 44 | } else { 45 | newRtcpFb.addAll(rtcpFb.where((e) => e['payload'] == payload).toList()); 46 | newFmtp.addAll(fmtp.where((e) => e['payload'] == payload).toList()); 47 | newPayloads.addAll(payloads.where((e) => e == '$payload').toList()); 48 | } 49 | }); 50 | rtcpFb = newRtcpFb; 51 | fmtp = newFmtp; 52 | payloads = newPayloads; 53 | codecs = newCodecs; 54 | return true; 55 | } 56 | } 57 | 58 | class CodecCapabilitySelector { 59 | CodecCapabilitySelector(String sdp) { 60 | _sdp = sdp; 61 | _session = sdp_transform.parse(_sdp); 62 | } 63 | late String _sdp; 64 | late Map _session; 65 | Map get session => _session; 66 | String sdp() => sdp_transform.write(_session, null); 67 | 68 | CodecCapability? getCapabilities(String kind) { 69 | var mline = _mline(kind); 70 | if (mline == null) { 71 | return null; 72 | } 73 | var rtcpFb = mline['rtcpFb'] ?? []; 74 | var fmtp = mline['fmtp'] ?? []; 75 | var payloads = (mline['payloads'] as String).split(' '); 76 | var codecs = mline['rtp'] ?? []; 77 | return CodecCapability(kind, payloads, codecs, fmtp, rtcpFb); 78 | } 79 | 80 | bool setCapabilities(CodecCapability? caps) { 81 | if (caps == null) { 82 | return false; 83 | } 84 | var mline = _mline(caps.kind); 85 | if (mline == null) { 86 | return false; 87 | } 88 | mline['payloads'] = caps.payloads.join(' '); 89 | mline['rtp'] = caps.codecs; 90 | mline['fmtp'] = caps.fmtp; 91 | mline['rtcpFb'] = caps.rtcpFb; 92 | return true; 93 | } 94 | 95 | Map? _mline(String kind) { 96 | var mlist = _session['media'] as List; 97 | return mlist.firstWhere((element) => element['type'] == kind, 98 | orElse: () => null); 99 | } 100 | } 101 | 102 | void unAwaited(Future? future) {} 103 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: flutter_ion 2 | version: 1.0.0 3 | description: Ion SDK for flutter, For live broadcast, video conference, etc., support mobile/deskop/web. 4 | homepage: https://github.com/pion/ion-sdk-flutter 5 | environment: 6 | sdk: ">=2.12.0 <3.0.0" 7 | 8 | dependencies: 9 | events2: ^1.0.0 10 | flutter_webrtc: ^0.9.11 11 | grpc: ^3.0.2 12 | protobuf: ^2.1.0 13 | sdp_transform: ^0.3.2 14 | uuid: ^3.0.6 15 | web_socket_channel: ^2.2.0 16 | 17 | dev_dependencies: 18 | flutter_test: 19 | sdk: flutter 20 | pedantic: ^1.11.1 21 | test: ^1.16.8 22 | 23 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /test/client_test.dart: -------------------------------------------------------------------------------- 1 | void main() async {} 2 | -------------------------------------------------------------------------------- /test/sdp_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | 4 | import 'package:flutter_ion/src/utils.dart'; 5 | import 'package:test/test.dart'; 6 | 7 | void main() async { 8 | test('NormalSdp', _testNormalSdp); 9 | } 10 | 11 | void _testNormalSdp() async { 12 | var sdp = await File('./test/webrtc.sdp').readAsString(); 13 | var capSel = CodecCapabilitySelector(sdp); 14 | var acaps = capSel.getCapabilities('audio'); 15 | acaps!.codecs = acaps.codecs 16 | .where((e) => (e['codec'] as String).toLowerCase() == 'opus') 17 | .toList(); 18 | acaps.setCodecPreferences('audio', acaps.codecs); 19 | capSel.setCapabilities(acaps); 20 | 21 | var vcaps = capSel.getCapabilities('video'); 22 | vcaps!.codecs = vcaps.codecs.where((e) { 23 | if ((e['codec'] as String).toLowerCase() == 'h264' || 24 | (e['codec'] as String).toLowerCase() == 'rtx') { 25 | //e['payload'] = 170; 26 | return true; 27 | } 28 | return false; 29 | }).toList(); 30 | vcaps.setCodecPreferences('audio', vcaps.codecs); 31 | capSel.setCapabilities(vcaps); 32 | 33 | var jstr = JsonEncoder.withIndent(' ').convert(capSel.session); 34 | print('session => $jstr'); 35 | print('sdp => ${capSel.sdp()}'); 36 | } 37 | -------------------------------------------------------------------------------- /test/webrtc.sdp: -------------------------------------------------------------------------------- 1 | v=0 2 | o=- 8723985918181983735 3 IN IP4 127.0.0.1 3 | s=- 4 | t=0 0 5 | a=group:BUNDLE 0 1 2 6 | a=msid-semantic: WMS DA10706B-F19B-471E-A4A0-614119A51117 7 | m=application 49890 DTLS/SCTP 5000 8 | c=IN IP4 192.168.43.68 9 | a=candidate:1161352755 1 udp 2122260223 192.168.43.68 49890 typ host generation 0 network-id 1 network-cost 50 10 | a=ice-ufrag:QHTj 11 | a=ice-pwd:ylAMsUR4JXltOoCXiAhEpIFr 12 | a=ice-options:trickle renomination 13 | a=fingerprint:sha-256 EE:A2:44:4C:71:8F:3D:DC:37:CC:19:AB:08:30:AF:D4:97:31:14:38:3B:FC:26:9E:BA:49:1D:B2:FA:EA:32:8A 14 | a=setup:actpass 15 | a=mid:0 16 | a=sctpmap:5000 webrtc-datachannel 1024 17 | m=audio 9 UDP/TLS/RTP/SAVPF 111 103 104 9 102 0 8 106 105 13 110 112 113 126 18 | c=IN IP4 0.0.0.0 19 | a=rtcp:9 IN IP4 0.0.0.0 20 | a=ice-ufrag:QHTj 21 | a=ice-pwd:ylAMsUR4JXltOoCXiAhEpIFr 22 | a=ice-options:trickle renomination 23 | a=fingerprint:sha-256 EE:A2:44:4C:71:8F:3D:DC:37:CC:19:AB:08:30:AF:D4:97:31:14:38:3B:FC:26:9E:BA:49:1D:B2:FA:EA:32:8A 24 | a=setup:actpass 25 | a=mid:1 26 | a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level 27 | a=extmap:2 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01 28 | a=extmap:3 urn:ietf:params:rtp-hdrext:sdes:mid 29 | a=extmap:4 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id 30 | a=extmap:5 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id 31 | a=sendonly 32 | a=msid:DA10706B-F19B-471E-A4A0-614119A51117 121C67A3-0098-4EF7-9581-928C08267B8C 33 | a=rtcp-mux 34 | a=rtpmap:111 opus/48000/2 35 | a=rtcp-fb:111 transport-cc 36 | a=fmtp:111 minptime=10;useinbandfec=1 37 | a=rtpmap:103 ISAC/16000 38 | a=rtpmap:104 ISAC/32000 39 | a=rtpmap:9 G722/8000 40 | a=rtpmap:102 ILBC/8000 41 | a=rtpmap:0 PCMU/8000 42 | a=rtpmap:8 PCMA/8000 43 | a=rtpmap:106 CN/32000 44 | a=rtpmap:105 CN/16000 45 | a=rtpmap:13 CN/8000 46 | a=rtpmap:110 telephone-event/48000 47 | a=rtpmap:112 telephone-event/32000 48 | a=rtpmap:113 telephone-event/16000 49 | a=rtpmap:126 telephone-event/8000 50 | a=ssrc:3626209241 cname:UjdAF8Kded/V+w/y 51 | a=ssrc:3626209241 msid:DA10706B-F19B-471E-A4A0-614119A51117 121C67A3-0098-4EF7-9581-928C08267B8C 52 | a=ssrc:3626209241 mslabel:DA10706B-F19B-471E-A4A0-614119A51117 53 | a=ssrc:3626209241 label:121C67A3-0098-4EF7-9581-928C08267B8C 54 | m=video 9 UDP/TLS/RTP/SAVPF 96 97 98 99 100 101 127 123 125 122 124 55 | c=IN IP4 0.0.0.0 56 | a=rtcp:9 IN IP4 0.0.0.0 57 | a=ice-ufrag:QHTj 58 | a=ice-pwd:ylAMsUR4JXltOoCXiAhEpIFr 59 | a=ice-options:trickle renomination 60 | a=fingerprint:sha-256 EE:A2:44:4C:71:8F:3D:DC:37:CC:19:AB:08:30:AF:D4:97:31:14:38:3B:FC:26:9E:BA:49:1D:B2:FA:EA:32:8A 61 | a=setup:actpass 62 | a=mid:2 63 | a=extmap:14 urn:ietf:params:rtp-hdrext:toffset 64 | a=extmap:13 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time 65 | a=extmap:12 urn:3gpp:video-orientation 66 | a=extmap:2 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01 67 | a=extmap:11 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay 68 | a=extmap:6 http://www.webrtc.org/experiments/rtp-hdrext/video-content-type 69 | a=extmap:7 http://www.webrtc.org/experiments/rtp-hdrext/video-timing 70 | a=extmap:8 http://tools.ietf.org/html/draft-ietf-avtext-framemarking-07 71 | a=extmap:9 http://www.webrtc.org/experiments/rtp-hdrext/color-space 72 | a=extmap:3 urn:ietf:params:rtp-hdrext:sdes:mid 73 | a=extmap:4 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id 74 | a=extmap:5 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id 75 | a=sendonly 76 | a=msid:DA10706B-F19B-471E-A4A0-614119A51117 F0815AF1-D256-42FD-9DAC-25AAC44A7C7E 77 | a=rtcp-mux 78 | a=rtcp-rsize 79 | a=rtpmap:96 H264/90000 80 | a=rtcp-fb:96 goog-remb 81 | a=rtcp-fb:96 transport-cc 82 | a=rtcp-fb:96 ccm fir 83 | a=rtcp-fb:96 nack 84 | a=rtcp-fb:96 nack pli 85 | a=fmtp:96 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=640c1f 86 | a=rtpmap:97 rtx/90000 87 | a=fmtp:97 apt=96 88 | a=rtpmap:98 H264/90000 89 | a=rtcp-fb:98 goog-remb 90 | a=rtcp-fb:98 transport-cc 91 | a=rtcp-fb:98 ccm fir 92 | a=rtcp-fb:98 nack 93 | a=rtcp-fb:98 nack pli 94 | a=fmtp:98 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f 95 | a=rtpmap:99 rtx/90000 96 | a=fmtp:99 apt=98 97 | a=rtpmap:100 VP8/90000 98 | a=rtcp-fb:100 goog-remb 99 | a=rtcp-fb:100 transport-cc 100 | a=rtcp-fb:100 ccm fir 101 | a=rtcp-fb:100 nack 102 | a=rtcp-fb:100 nack pli 103 | a=rtpmap:101 rtx/90000 104 | a=fmtp:101 apt=100 105 | a=rtpmap:127 VP9/90000 106 | a=rtcp-fb:127 goog-remb 107 | a=rtcp-fb:127 transport-cc 108 | a=rtcp-fb:127 ccm fir 109 | a=rtcp-fb:127 nack 110 | a=rtcp-fb:127 nack pli 111 | a=rtpmap:123 rtx/90000 112 | a=fmtp:123 apt=127 113 | a=rtpmap:125 red/90000 114 | a=rtpmap:122 rtx/90000 115 | a=fmtp:122 apt=125 116 | a=rtpmap:124 ulpfec/90000 117 | a=ssrc-group:FID 3758274479 3523537680 118 | a=ssrc:3758274479 cname:UjdAF8Kded/V+w/y 119 | a=ssrc:3758274479 msid:DA10706B-F19B-471E-A4A0-614119A51117 F0815AF1-D256-42FD-9DAC-25AAC44A7C7E 120 | a=ssrc:3758274479 mslabel:DA10706B-F19B-471E-A4A0-614119A51117 121 | a=ssrc:3758274479 label:F0815AF1-D256-42FD-9DAC-25AAC44A7C7E 122 | a=ssrc:3523537680 cname:UjdAF8Kded/V+w/y 123 | a=ssrc:3523537680 msid:DA10706B-F19B-471E-A4A0-614119A51117 F0815AF1-D256-42FD-9DAC-25AAC44A7C7E 124 | a=ssrc:3523537680 mslabel:DA10706B-F19B-471E-A4A0-614119A51117 125 | a=ssrc:3523537680 label:F0815AF1-D256-42FD-9DAC-25AAC44A7C7E --------------------------------------------------------------------------------