Saving videos to cloud in Flutter
Tags: flutter, video, voximplant
Published: 2021-10-08 11:42:16

## 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](https://voximplant.com/pricing) before jumping in.

## Content
- [How to prepare platform](#platform)
- [Client](#client)
  - [Preparations](#preparations)
  - [Camera permissions and debugging](#camera)
  - [Client code](#clientcode)
- [Conclusion](#conclusion)
- [Links](#links)

------

## Preparing platform <a name="platform"></a>
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.

1. Create a new account on [voximplant.com](voximplant.com) and login into the dashboard.
![Dashboard](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/50v5yw4rjkaj9acfbxiw.png)
2. Open left menu -> Applications -> Create application
![Create application](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0jpgu94jkrj6p7rcf8hf.png )
Name it as you wish (in my case - `recorder`). Click at the created application to go to its settings.
3. In the app dashboard go to Users -> Create user
![Create user](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qik4mkqvmu739epcy4qz.png)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!

4. Finally we can add some code. Go to Scenarios -> New Scenario. I called mine `video-recorder`. Copy-Paste this code to the editor:

    ```javascript
    //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 `Connected` event and record incoming video to the cloud. When recording is started (`RecordStarted` event), 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.
5. 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.
![Create rule](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4hi682hydbj752q7795b.png)

And that's it with the platform. So simple! Now we can switch to client part.

------

## Client <a name="client"></a>
### Preparations <a name="preparations"></a>
>💡 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](https://flutter.dev/docs/get-started/install)

1. Create empty project 

    ```bash
    flutter create video_recorder
    ```

2. Add `flutter_voximplant` package with 

    ```bash
    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 <a name="camera"></a>

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](https://github.com/voximplant/flutter_voximplant/blob/master/README.md))
#### 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](https://www.oreilly.com/library/view/flutter-for-beginners/9781788996082/378b452d-3b50-4de5-9e98-8571b813a924.xhtml))!
For Android, go to `android/app/src/(main|debug|profile)/AndroidManifest.xml` and add
```xml
<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.
![Access for app](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gi2eaedeyxoqn5xfddy1.png)  

>💡 Best cross-platform solution for Flutter to handle permissions is [permission_handler](https://pub.dev/packages/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](https://youtu.be/B4JVqKHsYzk)  

{% youtube B4JVqKHsYzk %}

**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!
![Emulator settings](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7c1exu546lxkku2yyqol.png)

Hopefully it can be solved if you run your emulator with [this command](https://stackoverflow.com/questions/63951011/how-to-access-webcam-connected-using-usb-in-android-avd) (on Mac)
```bash
cd ~/Library/Android/sdk/emulator
emulator @YOUR_DEVICE_NAME -camera-back webcam1
```
>💡 For some reason you can't use `-camera-back webcam1 -camera-front webcam1` [at the same time](https://stackoverflow.com/questions/11857006/android-webcam-enable-in-emulator/11857261) 🤷‍♀️


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 <a name="clientcode"></a>

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](https://pub.dev/packages/flutter_dotenv) package). For your prouction app you can implement login screen, and use auth token ([example](https://github.com/voximplant/flutter_demos/blob/master/video_call/lib/services/auth_service.dart))

>💡 Use user@application.account.voximplant.com format to log in.

```dart
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.

```dart
  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](https://pub.dev/packages/video_player) package.

```dart
void _onMessage(VICall call, String message) {
    log(message);
  }
```

>💡 There is a [bug](https://github.com/voximplant/flutter_voximplant/issues/24) in `flutter_voximplant` package 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.

```dart
@override
void dispose() {
  super.dispose();
  _logout();
}

Future<void> _logout() async {
  final state = await _client.getClientState();
  if (state != VIClientState.Disconnected) _client.disconnect();
}
```
>💡 `dispose` method in Flutter widgets has couple of issues, for example [not called when app quit](https://github.com/flutter/flutter/issues/28134) or [doesn't work as async](https://github.com/flutter/flutter/issues/64935). In a production environment you will need some workarounds to make sure that connection is closed properly.

### Conclusion <a name="conclusion"></a>
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! 😃
![Happy face](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/phqq6be21u0wj7g9fqkg.png)

## Links <a name="links"></a>
- [Voximplant flutter package](https://github.com/voximplant/flutter_voximplant)
- [Typescript implementation of video recorder](https://github.com/voximplant/videorecorder)
- [Video recording tutorial](https://voximplant.com/docs/tutorials/recording/video-recording)

<sub><sup>Illustration created by stories - <a href="https://www.freepik.com/vectors/mobile">www.freepik.com</a></sup></sub>