Motivation
If you ever thought: "Video-calls, VoIP, codecs, all these things, it's so complicated! π€― I can't code it myself" β you're right. This area can be quite challenging for beginners (like me). Some time ago I got an idea for an app: what if in dangerous situations I can press one button on my phone and record a video directly to the cloud? But preparing servers, understanding protocols, codecs and the whole world of telephony β that's not the thing I want (and can) do in one evening.
Turns out it's much easier than I thought! In this article I will show you how to create an uber-simple Flutter app that can save video from the camera to the cloud server.
π‘ This article describes how to use VoxImplant platform. You need to bear in mind that this is a paid service, so please check their pricelist before jumping in.
Content
Preparing platform
Before we get our fingers dirty, we need to prepare the platform. VoxImplant provides a user-friendly interface and rich API for their services, so let's cook the backend part of our app.
- Create a new account on voximplant.com and login into the dashboard.
- Open left menu -> Applications -> Create application
Name it as you wish (in my case - recorder). Click at the created application to go to its settings. -
In the app dashboard go to Users -> Create user
Choose name and password.π‘ I used "Separate account balance" checkbox, so this user would have separate balance from main account. You can call me paranoid, but it's never too much security!
-
Finally we can add some code. Go to Scenarios -> New Scenario. I called mine
video-recorder. Copy-Paste this code to the editor:
//subscribe to the events VoxEngine.addEventListener(AppEvents.CallAlerting, (e) => { e.call.addEventListener(CallEvents.Connected, handleCallConnected); e.call.addEventListener(CallEvents.RecordStarted, handleRecordStarted); e.call.addEventListener(CallEvents.Failed, VoxEngine.terminate); e.call.addEventListener(CallEvents.Disconnected, VoxEngine.terminate); e.call.answer(); }); function handleCallConnected(e) { // Record call including video e.call.record({video:true}); } function handleRecordStarted(e) { // Send video URL to client e.call.sendMessage(JSON.stringify({url: e.url})); }It's self-explanatory, but in short: we subscribe to
Connectedevent and record incoming video to the cloud. When recording is started (RecordStartedevent), we send message with its link to the client. For the sake of simplicity we will not add video player to the client, so this code is here just to show how to do that. And the last thing! We should add a router that will handle our calls. Go to Routing -> Create rule. In my case the pattern accepts all incoming calls, but in a production environment you should change it to something meaningful. Choose our scenario from the box.

And that's it with the platform. So simple! Now we can switch to client part.
Client
Preparations
π‘ If you don't want to code client yourself - just clone it:
git clone git@github.com:bunopus/video_recorder.git
Don't forget to add your USERNAME and PASSWORD. Create .env file in root folder with following content and run an app.
USER=USERNAME@YOUR_APP_URL
PASSWORD=ACTUAL_PASSWORD
Let's start preparing our client. I assume that you already have Flutter SDK up and running. If not - check their docs
-
Create empty project
flutter create video_recorder -
Add
flutter_voximplantpackage with
flutter pub add flutter_voximplant
We don't need any other packages, at least for our super-simple app. You can check that it's up and running with flutter run.
Camera access and debugging
Before we continue I would like to say a couple of words about debugging apps that use camera. Even though it should be straightforward β it's not π€¦ββοΈ. At least for Android.
For VoxImplant plugin to work β add this into your project (docs)
iOS
Add the following entry to your Info.plist file, located in <project root>/ios/Runner/Info.plist:
<key>NSMicrophoneUsageDescription</key>
<string>Microphone is required to make audio calls</string>
<key>NSCameraUsageDescription</key>
<string>Camera is required to make video calls</string>
This entry allows your app to access the microphone and cameras.
Android
It is required to add Java 8 support.
Open <project root>android/app/build.gradle file and add the following lines to βandroidβ section:
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
But that's not enough (check this article)!
For Android, go to android/app/src/(main|debug|profile)/AndroidManifest.xml and add
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.handson">
...
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
</manifest>
Then open the emulator and give access to an app.
π‘ Best cross-platform solution for Flutter to handle permissions is permission_handler package. It removes neccesity to dance around your phone.
And if you want to use your actual web camera and not jumping square that comes with Android emulator - you should change setting as advised here
BUT π€¦ββοΈ in my case, i've got one problem: i have several cams, connected to my computer, and in emulator settings i had only first one!

Hopefully it can be solved if you run your emulator with this command (on Mac)
cd ~/Library/Android/sdk/emulator
emulator @YOUR_DEVICE_NAME -camera-back webcam1
π‘ For some reason you can't use
-camera-back webcam1 -camera-front webcam1at the same time π€·ββοΈ
To check if the camera is working β open the camera app in your emulator and see face of a person who spent their life trying to run this $h1t πππ
Client code
Finally we have done everything to start coding our client. Let's take a look at lib/main.dart. At the init phase we're creating a client for the platform calling Voximplant().getClient().
Then we login _client.login() using the username and password created previously. To simplify code I store them in a .env file (and use flutter_dotenv package). For your prouction app you can implement login screen, and use auth token (example)
π‘ Use user@application.account.voximplant.com format to log in.
class _MyHomePageState extends State<MyHomePage> {
final VIClient _client = Voximplant().getClient();
VICall? _call;
AppState _state = AppState.initialising;
@override
initState() {
super.initState();
_login();
}
void _login() async {
try {
await _client.connect();
await _client.login(dotenv.get('USER'), dotenv.get('PASSWORD'));
setState(() {});
_state = AppState.ready;
} on Exception catch (e) {
log(e.toString());
setState(() {
_state = AppState.error;
});
}
}
When we press "Record", our app starts a call to the platform with _client.call(). You can pass the VICallSettings structure to specify codec and other parameters. Pay attention to the empty string passed as the first argument to call: this is the route name for your call. If you remember, we put .* in our pattern field, so any name will be accepted. For production you can add a meaningful route here and then check it on the platform.
void _record() async {
var _settings = VICallSettings();
_settings.videoFlags = VIVideoFlags(sendVideo: true);
_call = await _client.call("", settings: _settings);
_call?.onCallConnected = _onCallConnected;
_call?.onCallDisconnected = _onCallDisconneced;
_call?.onMessageReceived = _onMessage;
}
_call?.onCallConnected and other callbacks are used to subscribe to events and change the UI of our app accordingly.
In the _onMessage function we just log the video url that platform sends to us. You can then show it to the user with the help of video_player package.
void _onMessage(VICall call, String message) {
log(message);
}
π‘ There is a bug in
flutter_voximplantpackage that makes messages unusable on Android. Fix is already merged into master, but not published to pub. Hopefully it will be fixed soon.
When we need to end up the call, we're just calling method _call?.hangup()
And the last thing we need to do is to close the connection when we don't need it. I'm doing that inside the dispose method.
@override
void dispose() {
super.dispose();
_logout();
}
Future<void> _logout() async {
final state = await _client.getClientState();
if (state != VIClientState.Disconnected) _client.disconnect();
}
π‘
disposemethod in Flutter widgets has couple of issues, for example not called when app quit or doesn't work as async. In a production environment you will need some workarounds to make sure that connection is closed properly.
Conclusion
Aaaand that's it π! Our client is ready, and when we press the Record button (and then Stop), we can go to App -> Call history and see the happy face of a happy developer! π

Links
Illustration created by stories - www.freepik.com
Top comments (0)