Skip to content

Commit 2f6489c

Browse files
committed
Implement Flutter client for sfu-ws
Designed to have same behavior as web client
1 parent 5cd5de2 commit 2f6489c

File tree

6 files changed

+293
-2
lines changed

6 files changed

+293
-2
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
sfu-ws/cert.pem
22
sfu-ws/key.pem
3+
go.sum

sfu-ws/flutter/.gitignore

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# Miscellaneous
2+
.metadata
3+
pubspec.lock
4+
*.class
5+
*.log
6+
*.pyc
7+
*.swp
8+
.DS_Store
9+
.atom/
10+
.buildlog/
11+
.history
12+
.svn/
13+
14+
# IntelliJ related
15+
*.iml
16+
*.ipr
17+
*.iws
18+
.idea/
19+
20+
# The .vscode folder contains launch configuration and tasks you configure in
21+
# VS Code which you may wish to be included in version control, so this line
22+
# is commented out by default.
23+
#.vscode/
24+
25+
# Flutter/Dart/Pub related
26+
**/doc/api/
27+
**/ios/Flutter/.last_build_id
28+
.dart_tool/
29+
.flutter-plugins
30+
.flutter-plugins-dependencies
31+
.packages
32+
.pub-cache/
33+
.pub/
34+
/build/
35+
36+
# Web related
37+
lib/generated_plugin_registrant.dart
38+
39+
# Symbolication related
40+
app.*.symbols
41+
42+
# Obfuscation related
43+
app.*.map.json
44+
45+
# Android Studio will place build artifacts here
46+
/android/app/debug
47+
/android/app/profile
48+
/android/app/release

sfu-ws/flutter/README.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# flutter
2+
3+
A Flutter project for sfu-ws.
4+
5+
## Getting Started
6+
7+
- `cd sfu-ws/flutter`
8+
- `flutter create --project-name flutter_sfu_wsx_example --org com.github.pion .`
9+
10+
## iOS
11+
Add the following entry to your _Info.plist_ file, located in `./ios/Runner/Info.plist`:
12+
13+
```xml
14+
<key>NSCameraUsageDescription</key>
15+
<string>$(PRODUCT_NAME) Camera Usage!</string>
16+
<key>NSMicrophoneUsageDescription</key>
17+
<string>$(PRODUCT_NAME) Microphone Usage!</string>
18+
```
19+
20+
## Android
21+
Add the following entry to your Android Manifest file, located in `./android/app/src/main/AndroidManifest.xml:
22+
23+
```xml
24+
<uses-feature android:name="android.hardware.camera" />
25+
<uses-feature android:name="android.hardware.camera.autofocus" />
26+
<uses-permission android:name="android.permission.CAMERA" />
27+
<uses-permission android:name="android.permission.RECORD_AUDIO" />
28+
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
29+
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
30+
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
31+
```
32+
33+
Edit`android/app/build.gradle`, modify minSdkVersion to 18
34+
```gradle
35+
defaultConfig {
36+
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
37+
applicationId "com.github.pion.flutter_sfu_wsx_example"
38+
minSdkVersion 18 // <-- here
39+
targetSdkVersion 28
40+
versionCode flutterVersionCode.toInteger()
41+
versionName flutterVersionName
42+
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
43+
}
44+
```
45+
46+
## Run
47+
- `flutter pub get`
48+
- `flutter run`

sfu-ws/flutter/lib/main.dart

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
import 'dart:async';
2+
import 'dart:convert';
3+
import 'package:flutter_webrtc/flutter_webrtc.dart';
4+
import 'package:flutter/material.dart';
5+
import 'package:websocket/websocket.dart';
6+
7+
void main() => runApp(MyApp());
8+
9+
class MyApp extends StatefulWidget {
10+
@override
11+
_GetMyAppState createState() => _GetMyAppState();
12+
}
13+
14+
class _GetMyAppState extends State<MyApp> {
15+
// Local media
16+
final _localRenderer = RTCVideoRenderer();
17+
List _remoteRenderers = [];
18+
19+
WebSocket _socket;
20+
RTCPeerConnection _peerConnection;
21+
22+
@override
23+
void initState() {
24+
super.initState();
25+
connect();
26+
}
27+
28+
Future<void> connect() async {
29+
_peerConnection = await createPeerConnection({}, {});
30+
31+
await _localRenderer.initialize();
32+
var localStream = await navigator.mediaDevices
33+
.getUserMedia({'audio': true, 'video': true});
34+
_localRenderer.srcObject = localStream;
35+
36+
localStream.getTracks().forEach((track) async {
37+
await _peerConnection.addTrack(track, localStream);
38+
});
39+
40+
_peerConnection.onIceCandidate = (candidate) {
41+
if (candidate == null) {
42+
return;
43+
}
44+
45+
_socket.add(JsonEncoder().convert({
46+
"event": "candidate",
47+
"data": JsonEncoder().convert({
48+
'sdpMLineIndex': candidate.sdpMlineIndex,
49+
'sdpMid': candidate.sdpMid,
50+
'candidate': candidate.candidate,
51+
})
52+
}));
53+
};
54+
55+
_peerConnection.onTrack = (event) async {
56+
if (event.track.kind == 'video' && event.streams.isNotEmpty) {
57+
var renderer = RTCVideoRenderer();
58+
await renderer.initialize();
59+
renderer.srcObject = event.streams[0];
60+
61+
setState(() { _remoteRenderers.add(renderer); });
62+
}
63+
};
64+
65+
_peerConnection.onRemoveStream = (stream) {
66+
var rendererToRemove;
67+
var newRenderList = [];
68+
69+
// Filter existing renderers for the stream that has been stopped
70+
_remoteRenderers.forEach((r) {
71+
if (r.srcObject.id == stream.id) {
72+
rendererToRemove = r;
73+
} else {
74+
newRenderList.add(r);
75+
}
76+
});
77+
78+
// Set the new renderer list
79+
setState(() { _remoteRenderers = newRenderList; });
80+
81+
// Dispose the renderer we are done with
82+
if (rendererToRemove != null) {
83+
rendererToRemove.dispose();
84+
}
85+
};
86+
87+
_socket = await WebSocket.connect('ws://localhost:8080/websocket');
88+
_socket.stream.listen((raw) async {
89+
Map<String, dynamic> msg = jsonDecode(raw);
90+
91+
switch (msg['event']) {
92+
case 'candidate':
93+
Map<String, dynamic> parsed = jsonDecode(msg['data']);
94+
_peerConnection
95+
.addCandidate(RTCIceCandidate(parsed['candidate'], null, 0));
96+
return;
97+
case 'offer':
98+
Map<String, dynamic> offer = jsonDecode(msg['data']);
99+
100+
// SetRemoteDescription and create answer
101+
await _peerConnection.setRemoteDescription(
102+
RTCSessionDescription(offer['sdp'], offer['type']));
103+
RTCSessionDescription answer = await _peerConnection.createAnswer({});
104+
await _peerConnection.setLocalDescription(answer);
105+
106+
// Send answer over WebSocket
107+
_socket.add(JsonEncoder().convert({
108+
'event': 'answer',
109+
'data':
110+
JsonEncoder().convert({'type': answer.type, 'sdp': answer.sdp})
111+
}));
112+
return;
113+
}
114+
}, onDone: () {
115+
print('Closed by server!');
116+
});
117+
}
118+
119+
@override
120+
Widget build(BuildContext context) {
121+
return MaterialApp(
122+
title: 'sfu-ws',
123+
home: Scaffold(
124+
appBar: AppBar(
125+
title: Text('sfu-ws'),
126+
),
127+
body: OrientationBuilder(builder: (context, orientation) {
128+
return Column(
129+
children: [
130+
Row(
131+
children: [
132+
Text('Local Video', style: TextStyle(fontSize: 50.0))
133+
],
134+
),
135+
Row(
136+
children: [
137+
SizedBox(
138+
width: 160,
139+
height: 120,
140+
child: RTCVideoView(_localRenderer, mirror: true))
141+
],
142+
),
143+
Row(
144+
children: [
145+
Text('Remote Video', style: TextStyle(fontSize: 50.0))
146+
],
147+
),
148+
Row(
149+
children: [
150+
..._remoteRenderers.map((remoteRenderer) {
151+
return SizedBox(
152+
width: 160,
153+
height: 120,
154+
child: RTCVideoView(remoteRenderer));
155+
}).toList(),
156+
],
157+
),
158+
Row(
159+
children: [
160+
Text('Logs Video', style: TextStyle(fontSize: 50.0))
161+
],
162+
),
163+
],
164+
);
165+
})));
166+
}
167+
}

sfu-ws/flutter/pubspec.yaml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
name: flutter_sfu_ws
2+
description: A new Flutter project.
3+
4+
version: 1.0.0+1
5+
6+
environment:
7+
sdk: ">=2.1.0 <3.0.0"
8+
9+
dependencies:
10+
websocket: ^0.0.5
11+
flutter:
12+
sdk: flutter
13+
14+
cupertino_icons: ^0.1.2
15+
flutter_webrtc: ^0.5.6
16+
sdp_transform:
17+
shared_preferences:
18+
19+
dev_dependencies:
20+
flutter_test:
21+
sdk: flutter
22+
23+
24+
flutter:
25+
uses-material-design: true

sfu-ws/main.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,10 @@ import (
1616
)
1717

1818
var (
19-
addr = flag.String("addr", "localhost:8080", "http service address")
20-
upgrader = websocket.Upgrader{}
19+
addr = flag.String("addr", "localhost:8080", "http service address")
20+
upgrader = websocket.Upgrader{
21+
CheckOrigin: func(r *http.Request) bool { return true },
22+
}
2123
indexTemplate = &template.Template{}
2224

2325
// lock for peerConnections and trackLocals

0 commit comments

Comments
 (0)