How to make a cross-platform serverless video sharing app with Flutter, Firebase and Publitio
Tags: flutter, firebase, publitio, videos
Published: 2019-11-26 00:00:00

---
date: "2019-11-26"
title: "How to make a cross-platform serverless video sharing app with Flutter, Firebase and Publitio"
published: true
category: "flutter"
tags: "flutter, firebase, publitio, videos"
slug: "how-to-make-a-cross-platform-video-sharing-app-with-flutter-and-firebase"
canonical_url: "https://www.learningsomethingnew.com/how-to-make-a-cross-platform-video-sharing-app-with-flutter-and-firebase"
cover_image: "https://www.learningsomethingnew.com/flutter-video/cover.png"
---


## What we're building
We'll see how to build a cross-platform mobile app that allows users to upload and share videos.

![final](https://www.learningsomethingnew.com/flutter-video/final.gif)

### The stack
* [Flutter](https://flutter.dev/) - A new cross-platform mobile app development framework by Google, using the Dart language.
* [Firebase](https://firebase.google.com/) - The serverless real time database, for storing and syncing video metadata between clients.
* [Publitio](https://publit.io?fpr=jonathan43) - The hosting platform we'll use for storing and delivering the videos.

## Why Flutter
Flutter gives us a way of writing one code base for both iOS and Android, without having to duplicate the logic or the UI. 
The advantage over solutions like Cordova is that it's optimized for mobile performance, giving native-like response. The advantage over the likes of React Native is that you can write the UI once, as it circumvents the OS native UI components entirely. Flutter also has a first rate development experience, including hot reload and debugging in VSCode, which is awesome.

## Why Firebase
Keeping things serverless has huge benefits for the time of development and scalability of the app. We can focus on our app instead of server devops. Firebase let's us sync data between clients without server side code, including offline syncing features (which are quite hard to implement). Flutter also has a set of firebase plugins that make it play nicely with Flutter widgets, so we get realtime syncing of the client UI when another client changes the data.

## Why Publitio
We'll use Publitio as our Media Asset Management API. Publitio will be responsible for hosting our files, delivering them to clients using it's CDN, thumbnail/cover image generation, transformations/cropping, and other video processing features.

By using an API for this, we can keep the app serverless (and therefore more easily scalable and maintainable), and not reinvent video processing in-house.

## Let's start
First, make sure you have a working flutter environment set up, using Flutter's [Getting Started](https://flutter.dev/docs/get-started/install).

Now let's create a new project named `flutter_video_sharing`. In terminal run:

```sh
flutter create --org com.learningsomethingnew.fluttervideo --project-name flutter_video_sharing -a kotlin -i swift flutter_video_sharing
```

To check the project has created successfully, run `flutter doctor`, and see if there are any issues.

Now run the basic project using `flutter run` for a little sanity check (you can use an emulator or a real device, Android or iOS).

> This is a good time to init a git repository and make the first commit.

## Taking a video

In order to take a video using the device camera, we'll use the [Image Picker plugin for Flutter](https://pub.dev/packages/image_picker).

In `pubspec.yaml` dependencies section, add the line (change to latest version of the plugin):
```yaml
image_picker: ^0.6.1+10
```

> You might notice that there is also a [Camera Plugin](https://pub.dev/packages/camera). I found this plugin to be quite unstable for now, and has serious video quality limitations on many android devices, as it supports only camera2 API.


### iOS configuration
For iOS, we'll have to add a camera and mic usage description in `ios/Runner/Info.plist`:

```xml
<key>NSMicrophoneUsageDescription</key>
<string>Need access to mic</string>
<key>NSCameraUsageDescription</key>
<string>Need access to camera</string>
```

> In order to test the camera on iOS you'll have to use a real device as the simulator doesn't have a camera

### Using the plugin

In `lib/main.dart` edit `_MyHomePageState` class with the following code:

```dart
class _MyHomePageState extends State<MyHomePage> {
  List<String> _videos = <String>[];

  bool _imagePickerActive = false;

  void _takeVideo() async {
    if (_imagePickerActive) return;

    _imagePickerActive = true;
    final File videoFile =
        await ImagePicker.pickVideo(source: ImageSource.camera);
    _imagePickerActive = false;

    if (videoFile == null) return;

    setState(() {
      _videos.add(videoFile.path);
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
          child: ListView.builder(
              padding: const EdgeInsets.all(8),
              itemCount: _videos.length,
              itemBuilder: (BuildContext context, int index) {
                return Card(
                  child: Container(
                    padding: const EdgeInsets.all(8),
                    child: Center(child: Text(_videos[index])),
                  ),
                );
              })),
      floatingActionButton: FloatingActionButton(
        onPressed: _takeVideo,
        tooltip: 'Take Video',
        child: Icon(Icons.add),
      ),
    );
  }
}
```

What's going on here:
- We're replacing the `_incrementCounter` method with `_takeVideo`, and also making it `async`. This is what happens when we click the Floating Action Button.
- We're taking a video with `await ImagePicker.pickVideo(source: ImageSource.camera);`.
- We're saving the video file paths in the `_videos` list in our widget's state. We call `setState` so that the widget will rebuild and the changes reflect in the UI.
- In order to get some visual feedback, we replace the scaffold's main `Column` with a `ListView` using `ListView.builder` which will render our dynamic list efficiently.


Running at this stage will look like this:

![image-picker](https://www.learningsomethingnew.com/flutter-video/image-picker.png)


## Add Publitio

Create a free account at [Publit.io](https://publit.io?fpr=jonathan43), and get your credentials from the dashboard.
> Note: The link above has my referral code. If my writing is valuable to you, you can support it by using this link. Of course, you can just create an account without me 😭

### Add the package
Add the [flutter_publitio](https://pub.dev/packages/flutter_publitio) plugin to your `pubspec.yaml` dependencies section:
```yaml
dependencies:
    flutter_publitio: ^1.0.0
```

### Create an env file
A good way to store app configuration is by using `.env` files, and loading them with [flutter_dotenv](https://pub.dev/packages/flutter_dotenv), which in turn implements a guideline from [The Twelve-Factor App](http://www.12factor.net/). This file will contain our API key and secret, and therefore should not be committed to source-control.
So create an `.env` file in the project's root dir, and put your credentials in it:

```
PUBLITIO_KEY=12345abcd
PUBLITIO_SECRET=abc123
```

Now add `flutter_dotenv` as a dependency, and also add the `.env` file as an asset in `pubspec.yaml`:
```yaml
dependencies:
  flutter_dotenv: ^2.0.3

  ...

flutter:
  assets:
    - .env
```

### iOS configuration
For iOS, the keys are loaded from `Info.plist`. In order to keep our configuration in our environment and not commit it into our repository, we'll load the keys from an `xcconfig` file:
- In XCode, open `ios/Runner.xworkspace` (this is the XCode project Flutter has generated), right click on Runner -> New File -> Other -> Configuration Settings File -> Config.xcconfig
- In `Config.xcconfig`, add the keys like you did in the `.env` file:
```
PUBLITIO_KEY = 12345abcd
PUBLITIO_SECRET = abc123
```

Now we'll import this config file from `ios/Flutter/Debug.xcconfig` and `ios/Flutter/Release.xcconfig` by adding this line to the bottom of both of them:

```
#include "../Runner/Config.xcconfig"
```

> Add Config.xcconfig to .gitignore so you don't have your keys in git

The last step is to add the config keys to `ios/Runner/Info.plist`:

```xml
<key>PublitioAPIKey</key>
<string>$(PUBLITIO_KEY)</string>
<key>PublitioAPISecret</key>
<string>$(PUBLITIO_SECRET)</string>
```


### Android configuration
In Android, all we have to do is change `minSdkVersion` from 16 to 19 in `android/app/build.gradle`.
> Is it just me or is everything always simpler with Android?


## Upload the file
Now that we have publitio added, let's upload the file we got from ImagePicker.

In `main.dart`, in the `_MyHomePageState` class, we'll add a field keeping the status of the upload:

```dart
bool _uploading = false;
```

Now we'll override `initState` and call an async function `configurePublitio` that will load the API keys:
```dart
@override
void initState() {
  configurePublitio();
  super.initState();
}

static configurePublitio() async {
  await DotEnv().load('.env');
  await FlutterPublitio.configure(
      DotEnv().env['PUBLITIO_KEY'], DotEnv().env['PUBLITIO_SECRET']);
}
```

We'll add a function `_uploadVideo` that calls publitio API's `uploadFile`:

```dart
static _uploadVideo(videoFile) async {
  print('starting upload');
  final uploadOptions = {
    "privacy": "1",
    "option_download": "1",
    "option_transform": "1"
  };
  final response =
      await FlutterPublitio.uploadFile(videoFile.path, uploadOptions);
  return response;
}
```

We'll add the calling code to our `_takeVideo` function:

```dart
setState(() {
  _uploading = true;
});

try {
  final response = await _uploadVideo(videoFile);
  setState(() {
    _videos.add(response["url_preview"]);
  });
} on PlatformException catch (e) {
  print('${e.code}: ${e.message}');
  //result = 'Platform Exception: ${e.code} ${e.details}';
} finally {
  setState(() {
    _uploading = false;
  });
}
```

Notice that the response from publitio API will come as a key-value map, from which we're only saving the `url_preview`, which is the url for viewing the hosted video. We're saving that to our `_videos` collection, and returning `_uploading` to false after the upload is done. 

And finally we'll change the floating action button to a spinner whenever `_uploading` is true:
```dart
floatingActionButton: FloatingActionButton(
  child: _uploading
      ? CircularProgressIndicator(
          valueColor: new AlwaysStoppedAnimation<Color>(Colors.white),
        )
      : Icon(Icons.add),
  onPressed: _takeVideo),
``` 


### Add thumbnails
One of the things publitio makes easy is server-side video thumbnail extraction. You can use it's URL transformation features to get a thumbnail of any size, but for this we'll use the default thumbnail that is received in the upload response.

Now that we want every list item to have a url and a thumbnail, it makes sense to extract a simple POCO class for each video entry. Create a new file `lib/video_info.dart`:

```dart
class VideoInfo {
  String videoUrl;
  String thumbUrl;

  VideoInfo({this.videoUrl, this.thumbUrl});
}
```

We'll change the `_videos` collection from `String` to `VideoInfo`:
```dart
List<VideoInfo> _videos = <VideoInfo>[];
```

And after getting the upload response, we'll add a VideoInfo object with the url and the thumbnail url:
```dart
final response = await _uploadVideo(videoFile);
setState(() {
  _videos.add(VideoInfo(
      videoUrl: response["url_preview"],
      thumbUrl: response["url_thumbnail"]));
});
```

Finally we'll add the thumbnail display to the list builder item:
```dart
child: new Container(
  padding: new EdgeInsets.all(10.0),
  child: Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: <Widget>[
      Stack(
        alignment: Alignment.center,
        children: <Widget>[
          Center(child: CircularProgressIndicator()),
          Center(
            child: ClipRRect(
              borderRadius: new BorderRadius.circular(8.0),
              child: FadeInImage.memoryNetwork(
                placeholder: kTransparentImage,
                image: _videos[index].thumbUrl,
              ),
            ),
          ),
        ],
      ),
      Padding(padding: EdgeInsets.only(top: 20.0)),
      ListTile(
        title: Text(_videos[index].videoUrl),
      ),
    ],
  ),
```

A few things here:
- We're giving the thumbnail nice round borders using `ClipRRect`
- We're displaying a `CircularProgressIndicator` that displays while the thumbnail is loading (it will be hidden by the image after loading)
- For a nice fade effect, we're using `kTransparentImage` from the package `transparent_image` (which needs to be added to `pubspec.yaml`)


And now we have nice thumbnails in the list:

![thumbnail](https://www.learningsomethingnew.com/flutter-video/thumbnail.png)


## Play the video

Now that we have the list of videos, we want to play each video when tapping on the list card.


We'll use the [Chewie plugin](https://pub.dev/packages/chewie) as our player. Chewie wraps the [video_player plugin](https://pub.dev/packages/video_player) with native looking UI for playing, skipping, and full screening.
It also supports auto rotating the video according to device orientation.
What it can't do (odly), is figure out the aspect ratio of the video automatically. So we'll get that from the publitio result.

> Note: Flutter's video_player package doesn't yet support caching, so replaying the video will cause it to re-download. This should be solved soon: https://github.com/flutter/flutter/issues/28094

So add to `pubspec.yaml`:
```yaml
video_player: ^0.10.2+5
chewie: ^0.9.8+1
```

For iOS, we'll also need to add to the following to `Info.plist` to allow loading remote videos:

```xml
<key>NSAppTransportSecurity</key>
<dict>
  <key>NSAllowsArbitraryLoads</key>
  <true/>
</dict>
```


Now we'll add a new widget that will hold chewie. Create a new file `chewie_player.dart`:

```dart
import 'package:chewie/chewie.dart';
import 'package:flutter/material.dart';
import 'package:video_player/video_player.dart';
import 'video_info.dart';

class ChewiePlayer extends StatefulWidget {
  final VideoInfo video;

  const ChewiePlayer({Key key, @required this.video}) : super(key: key);

  @override
  State<StatefulWidget> createState() => _ChewiePlayerState();
}

class _ChewiePlayerState extends State<ChewiePlayer> {
  ChewieController chewieCtrl;
  VideoPlayerController videoPlayerCtrl;

  @override
  void initState() {
    super.initState();
    videoPlayerCtrl = VideoPlayerController.network(widget.video.videoUrl);
    chewieCtrl = ChewieController(
      videoPlayerController: videoPlayerCtrl,
      autoPlay: true,
      autoInitialize: true,
      aspectRatio: widget.video.aspectRatio,
      placeholder: Center(
        child: Image.network(widget.video.coverUrl),
      ),
    );
  }

  @override
  void dispose() {
    if (chewieCtrl != null) chewieCtrl.dispose();
    if (videoPlayerCtrl != null) videoPlayerCtrl.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.black,
      body: Stack(
        children: <Widget>[
          Chewie(
            controller: chewieCtrl,
          ),
          Container(
            padding: EdgeInsets.all(30.0),
            child: IconButton(
              icon: Icon(Icons.close, color: Colors.white),
              onPressed: () => Navigator.pop(context),
            ),
          ),
        ],
      ),
    );
  }
}
```

A few things to note:
- `ChewiePlayer` expects to get `VideoInfo` which is the video to be played.
- The aspect ratio is initialized from the input `VideoInfo`. We'll add this field soon.
- We show a placeholder image while the video is loading. We'll use publitio API to generate the cover image.
- We have an `IconButton` that will close this widget by calling `Navigator.pop(context)`
- We have to take care of disposing both the `VideoPlayerController` and the `ChewieController` by overriding the `dispose()` method

In `video_info.dart` we'll add the `aspectRatio` and `coverUrl` fields:
```dart
String coverUrl;
double aspectRatio;
```

And now in `main.dart`, we'll first import our new `chewie_player`:

```dart
import 'chewie_player.dart';
```

Add a calculation of `aspectRatio`:
```dart
  final response = await _uploadVideo(videoFile);
  final width = response["width"];
  final height = response["height"];
  final double aspectRatio = width / height;
    setState(() {
      _videos.add(VideoInfo(
          videoUrl: response["url_preview"],
        thumbUrl: response["url_thumbnail"]));
        thumbUrl: response["url_thumbnail"],
        aspectRatio: aspectRatio,
        coverUrl: getCoverUrl(response),
        ));
    });
```

Add a method to get the cover image from publitio API (this is just replacing the extension of the video to `jpg` - publitio does all the work): 
```dart
static const PUBLITIO_PREFIX = "https://media.publit.io/file";

static getCoverUrl(response) {
  final publicId = response["public_id"];
  return "$PUBLITIO_PREFIX/$publicId.jpg";
}
```

And wrap our list item `Card` with a `GestureDetector` to respond to tapping on the card, and call `Navigator.push` that will route to our new `ChewiePlayer` widget:
```dart
return GestureDetector(
    onTap: () {
      Navigator.push(
        context,
        MaterialPageRoute(
          builder: (context) {
            return ChewiePlayer(
              video: _videos[index],
            );
          },
        ),
      );
    },
    child: Card(
      child: new Container(
        padding: new EdgeInsets.all(10.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: <Widget>[
            Stack(
              alignment: Alignment.center,
              children: <Widget>[
                Center(child: CircularProgressIndicator()),
                Center(
                  child: ClipRRect(
                    borderRadius: new BorderRadius.circular(8.0),
                    child: FadeInImage.memoryNetwork(
                      placeholder: kTransparentImage,
                      image: _videos[index].thumbUrl,
                    ),
                  ),
                ),
              ],
            ),
            Padding(padding: EdgeInsets.only(top: 20.0)),
            ListTile(
              title: Text(_videos[index].videoUrl),
            ),
          ],
        ),
      ),
    ),
  );
```

## Add Firebase

Now that we can upload and playback videos, we want users of the app to view the videos other users posted. To do that (and keep the app serverless) we'll use Firebase.

### Firebase Setup
First, setup Firebase as described [here](https://firebase.google.com/docs/flutter/setup). This includes creating a firebase project, registering your mobile apps (Android and iOS) in the project, and configuring the credentials for both the Android and iOS projects. Then we'll add the Flutter packages [firebase_core](https://pub.dev/packages/firebase_core) and [cloud_firestore](https://pub.dev/packages/cloud_firestore).

> Cloud Firestore is the new version of the Firebase Realtime Database.
> You'll probably need to set `multiDexEnabled true` in your `build.gradle`.


### Save video info to Firebase

Instead of saving the video info to our own state, we'll save it to a new Firebase document in the `videos` collection:

```dart
final video = VideoInfo(
  videoUrl: response["url_preview"],
  thumbUrl: response["url_thumbnail"],
  coverUrl: getCoverUrl(response),
  aspectRatio: getAspectRatio(response),
);
await Firestore.instance.collection('videos').document().setData({
  "videoUrl": video.videoUrl,
  "thumbUrl": video.thumbUrl,
  "coverUrl": video.coverUrl,
  "aspectRatio": video.aspectRatio,
}); 
```

The `document()` method will create a new randomly named document inside the `videos` collection.

This is how the documents look in the Firebase Console:

![firestore](https://www.learningsomethingnew.com/flutter-video/firestore.png)

### Show video list from Firebase

Now in our `initState` method we'll want to start a Firebase query that will listen to the `videos` collection. Whenever the Firebase SDK triggers a change in the data, it will invoke the `updateVideos` method, which will update our `_videos` state (and Flutter will rebuild the UI):

```dart
@override
void initState() {
  configurePublitio();
  listenToVideos();
  super.initState();
}

listenToVideos() async {
  Firestore.instance.collection('videos').snapshots().listen(updateVideos);
}

void updateVideos(QuerySnapshot documentList) async {
  final newVideos = mapQueryToVideoInfo(documentList);
  setState(() {
    _videos = newVideos;
  });
}

static mapQueryToVideoInfo(QuerySnapshot documentList) {
  return documentList.documents.map((DocumentSnapshot ds) {
    return VideoInfo(
      videoUrl: ds.data["videoUrl"],
      thumbUrl: ds.data["thumbUrl"],
      coverUrl: ds.data["coverUrl"],
      aspectRatio: ds.data["aspectRatio"],
    );
  }).toList();
}
```

Now all the videos are shared!

![final](https://www.learningsomethingnew.com/flutter-video/final.gif)

## Refactoring

Now that everything's working, it's a good time for some refactoring. This is a really small app, but still some obvious things stand out.
We have our data access and business logic all sitting in the same place - never a good idea. It's better to have our API/data access in separate modules, providing the business logic layer with the required services, while it can stay agnostic to (and [Loosely Coupled](https://en.wikipedia.org/wiki/Loose_coupling) from) the implementation details.

In order to keep this post short(ish) I won't include these changes here, but you can see them in the final code on [GitHub](https://github.com/syonip/flutter_video_sharing)


## Future improvement: client side encoding
We can use FFMpeg to encode the video on the client before uploading.
This will save storage and make delivery faster, but will require a patient uploading user.
If you want to see how to do that, write a comment below.


Thanks for reading!
Full code can be found on [GitHub](https://github.com/syonip/flutter_video_sharing).
If you have any questions, please leave a comment!