DEV Community

Cover image for Saving videos to cloud in Flutter
Evgeny Kot
Evgeny Kot

Posted on

Saving videos to cloud in Flutter

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.

  1. Create a new account on voximplant.com and login into the dashboard. Dashboard
  2. Open left menu -> Applications -> Create application Create application 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 userChoose 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:

    //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

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
Enter fullscreen mode Exit fullscreen mode

Let's start preparing our client. I assume that you already have Flutter SDK up and running. If not - check their docs

  1. Create empty project

    flutter create video_recorder
    
  2. Add flutter_voximplant package 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>
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

Then open the emulator and give access to an app.
Access for 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!
Emulator settings

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
Enter fullscreen mode Exit fullscreen mode

πŸ’‘ For some reason you can't use -camera-back webcam1 -camera-front webcam1 at 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;
      });
    }
  }
Enter fullscreen mode Exit fullscreen mode

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;
  }
Enter fullscreen mode Exit fullscreen mode

_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);
  }
Enter fullscreen mode Exit fullscreen mode

πŸ’‘ There is a bug 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.

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

Future<void> _logout() async {
  final state = await _client.getClientState();
  if (state != VIClientState.Disconnected) _client.disconnect();
}
Enter fullscreen mode Exit fullscreen mode

πŸ’‘ dispose method 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! πŸ˜ƒ
Happy face

Links

Illustration created by stories - www.freepik.com

Top comments (0)