Creating a live broadcasting app using Cordova and Ionic Framework

This guide is based on Ionic 3.x and Cordova 8.x.

Previously we've shown how you can broadcast and play back live video in a React Native mobile app, as well as how to use Bambuser's native SDK:s in Java, Swift or Objective C.

In this article we will look into the world of hybrid apps, a development approach where HTML, CSS and JavaScript makes up the core of the mobile app, with calls to native code where appropriate. Specifically, we will rely on Cordova to bootstrap a web view and provide a plugin ecosystem for native code and we will use Ionic's UI Framework to give our HTML components a native look and feel.

By the end of the article we will have a working live broadcasting app, by using cordova-plugin-bambuser and Bambuser (Free trial available).

Bootstrapping a new Ionic app

To bootstrap our project, let's follow Ionic's installation guide:

Ensure Node.js is installed and then install Ionic's and Cordova's CLI:s globally:

npm install -g ionic cordova

If you prefer to encapsulate the Node.js parts in a Docker container, you can alternatively copy our example Dockerfile and docker-compose.yml files. To start an interactive shell in the Node.js container, use ./docker-compose run --service-ports example. docker-compose.yml provides misc dev conveniences: it opens relevant ports for ionic serveand maps the project directory from the host to the container so that edits on the host appears in the container and vice versa

Then create a new project using ionic start. In this exemple we pick the tabbed layout, activate Cordova integration, opt out of Ionic Pro and provide a custom bundle id. See ionic start --help for all options or leave them unspecified for interactive mode:

ionic start bambuser-examplebroadcaster-ionic tabs --cordova --no-link --bundle-id com.example.bambuserbroadcaster

To preview the app skeleton in a browser, cd into the root of the project directory and start Ionic's dev server

cd bambuser-examplebroadcaster-ionic
npm run ionic:serve

Wait until you see "dev server running", then open http://localhost:8100 in a web browser.

In browser mode, you will not be able to use native-dependent parts of your app, such as the live streaming SDK:s, but it can be a quick way to build up the UI using Ionic's component library

Building a native app bundle with Cordova

Run ionic cordova platform add android to generate a Cordova based Android Studio project in ./platforms/android

and / or

Run ionic cordova platform add ios to generate a Cordova based XCode project in ./platforms/ios/

You generally only have to run those commands once. Although you can use ... platform rm ... + ... platform add ..., should you want to start over from an updated Cordova template, or regenerate the native project for other reasons. In other words, the files under ./platforms/ can be considered generated output. That said, If you ship a production app, it is a good idea to back up the state of platforms/ for each version you publish, so that you are able to reproduce them exactly.

Then, every time you make a change to the HTML5 parts of your hybrid app, i.e. the files in ./src/, run ionic cordova prepare. This genarates optimized JavaScript and CSS bundles, but instead of serving them over HTTP like ionic:serve, it copies them into the www subdirectory of the native projects you created in the previous step, i.e. ./platforms/*/www/

Finally, open the project in ./platforms/android with Android Studio to build an Android apk.

and / or

Open the project in platforms/ios with XCode to build for an iOS device (live streaming not supported in the simulator, only on actual devices)

Adding the live streaming SDK:s

For live streaming, we will use Bambuser's cross-platform SDK:s and cloud services and there is a Cordova plugin ready to go, which allows us to control the SDK:s native interfaces from JavaScript:

ionic cordova plugin add cordova-plugin-bambuser

Adding a viewingfinder and broadcasting controls

Now we have all the infrastructure in place and can start concentrating on building the desired user experience using Ionic components.

This is a good time to use Ionic's dev server again. Run npm run ionic:serve in a console window, open https://localhost:8100 in your browser of choice and open ./src in your favorite JavaScript IDE.

The tabs Ionic starter template we picked earlier provides a tab bar with icons that represent the root pages of the app, each with its own navigation stack - much like how the navigation in Instagram works for example. Let's add another root page called Broadcasting and leave the other pages as is for now.

Create the directory ./src/pages/broadcaster containing the files broadcaster.html, broadcaster.scss and broadcaster.ts, with the following contents:

broadcaster.html

<ion-content padding>

  <button ion-button
    *ngIf="!isBroadcasting && !isPending"
    (click)="start()">
    Start
  </button>

  <button ion-button
    *ngIf="isBroadcasting && !isPending"
    (click)="stop()"
    color="danger">
    Stop
  </button>

</ion-content>

Here we're adding buttons for all states of our broadcasting view. Angular's ngIf expression ensures that only the relevant markup is present in the DOM at any given time, based on the state of the isBroadcasting and isPending instance variables in broadcaster.ts

broadcaster.scss

page-broadcaster {

}

The Sass files generates the CSS stylesheet in our application bundle. By convention, each Ionic page has its own Sass file, with rules scoped to the selector of the page, defined in broadcaster.ts, in this case page-broadcaster. We currently don't have any styles to add here, but lets leave the scoping rule in place anyway for later use.

broadcaster.ts

import { Component } from '@angular/core';
import { ToastController } from 'ionic-angular';

@Component({
  selector: 'page-broadcaster',
  templateUrl: 'broadcaster.html'
})
export class BroadcasterPage {
  isBroadcasting = false;
  isPending = false;

  constructor(private toastCtrl: ToastController) {}

  async start() {
    if (this.isBroadcasting || this.isPending) return;
    this.isPending = true;
    const toast = this.toastCtrl.create({
      message: 'Starting broadcast...',
      position: 'middle',
    });
    toast.present();

    console.log('TODO start broadcasting');
    await new Promise(resolve => setTimeout(resolve, 3000)); // Mockup: simulated startup time

    toast.dismiss();
    this.isBroadcasting = true;
    this.isPending = false;
  }

  async stop() {
    if (!this.isBroadcasting || this.isPending) return;
    this.isPending = true;
    const toast = this.toastCtrl.create({
      message: 'Ending broadcast...',
      position: 'middle'
    });
    toast.present();

    console.log('TODO: stop broadcasting');
    await new Promise(resolve => setTimeout(resolve, 2000)); // Mockup: simulated end time

    toast.dismiss();
    this.isBroadcasting = false;
    this.isPending = false;
  }
}

This is the meat of our broadcaster component. At this point it is just a mockup suitable for browser debugging of the main UI states our broadcaster needs.

Also remember to import the BroadcasterPage component into app/app.module.ts and add it everywhere the AboutPage component is mentioned: in the declarations and entryComponents lists. Otherwise Angular and Ionic will not be able to use it.

app.module.ts

import { BroadcasterPage } from '../pages/broadcaster/broadcaster';
declarations: [
  MyApp,
  AboutPage,
  BroadcasterPage,
  ...
entryComponents: [
  MyApp,
  AboutPage,
  BroadcasterPage,
  ...

Do the same in pages/tabs/tabs.ts and pages/tabs/tabs.html to add an icon in the tab bar:

tabs.ts

import { BroadcasterPage } from '../broadcaster/broadcaster';
tab4Root = BroadcasterPage;

tabs.html

<ion-tab [root]="tab4Root" tabTitle="Broadcaster" tabIcon="aperture"></ion-tab>

<ion-tab> (and <ion-icon>) uses the Ionicons library bundled with Ionic.

Blending the layers

Let's assume our goal is to let the camera viewfinder cover the entire screen. After all, this is how most camera apps tend to operate. When using Cordova, we have a few options on how to deal with this:

A) Draw the native camera view on top of the web view, covering the web view fully: this means all UI elements within that section of the app would have to be provided by native code. This is how many Cordova plugins operate, including cordova-plugin-camera: call camera.getPicture() and a plugin-provided native modal with various standard camera controls is displayed. When the user has snapped a photo or cancelled, control is returned to the JavaScript context. Simple, but limits the web view's ability to provide any UI in that part of the app.

B) Draw the native camera view and the web view side-by-side. Requires resizing of the web view. Error-prone, probably not a well-supported scenario by Cordova.

C) Draw the native camera view below the web view. Ensure the web view is partially transparent. Let the web view provide all UI controls. This is the approach currently used by cordova-plugin-bambuser and it seems to work fine (caveat: you need to control all layers in your web view and be able to make them transparent at the appropriate time) and makes the most sense when using a UI library like Ionic.

In a typical live broadasting app you probably want a few buttons for start, stop and camera switching, a duration counter and perhaps a notification badge of some sort. In a social app, we might also want to display chat messages, viewer and like counters etc. By putting the camera view below the web view, we can build all of these features using HTML, CSS and JavaScript.

How to make the layers of an Ionic app transparent might depend a bit on which version of Ionic we are using and on any customizations we've made. This snippet of CSS seems to work out-of-the-box with recent versions of Ionic 3:

./src/pages/broadcaster/broadcaster.scss

/**
 * Make all layer background transparent that normally are opaque in Ionic
 * so that cordova-plugin-bambuser is able to draw a camera viewfinder
 * behind the web view.
 */
body.show-viewfinder {
  ion-app,
  ion-content,
  .nav-decor {
    background-color: transparent !important;
  }
}

Add this either to broadcast.scss (outside of the page-broadcaster block, since we want to affect all layers) or in some other .scss file with app-wide rules.

To trigger transparent mode on our broadcast we then need to use some JavaScript to add the CSS class show-viewfinder to the <body> tag when we enter the page and to remove the class when we leave the page. We can subscribe to some of the lifecycle events of Ionic's NavController to accomplish that:

./src/pages/broadcaster/broadcaster.ts

ionViewDidLoad() {
  document.getElementsByTagName('body')[0].classList.add("show-viewfinder");
}

ionViewWillLeave() {
  document.getElementsByTagName('body')[0].classList.remove("show-viewfinder");
}

Starting a broadcast

To authorize broadcasting into the trial account we signed up for earlier, go back to the developer page and create an applicationId. Add it as a constant on BroadcasterPage:

// Application id generated at https://dashboard.bambuser.com/developer
const APPLICATION_ID:string = 'CHANGEME';

To hook up cordova-plugin-bambuser on our broadcaster page, let's wait for Ionic's platform.ready event and then check whether the Cordova runtime is available ahd has provided an instance of cordova-plugin-bambuser.

const APPLICATION_ID:string = 'CHANGEME';

@Component({
  selector: 'page-broadcaster',
  templateUrl: 'broadcaster.html'
})
export class BroadcasterPage {
  isBroadcasting = false;
  isPending = false;
  broadcaster: any;
  errListenerId = false;

  constructor(
    private toastCtrl: ToastController,
    public platform: Platform) {

    platform.ready().then(() => {
      // Using array syntax workaround, since types are not declared.
      if (window['bambuser']) {
        this.broadcaster = window['bambuser']['broadcaster'];
        this.broadcaster.setApplicationId(APPLICATION_ID);
      } else {
        // Cordova plugin not installed or running in a web browser
      }
    });

Cordova will only provide window['bambuser'] if we are running in a Cordova- enabled webview on Android or iOS. If we're running through ionic serve in a web browser, or if we forgot to install cordova-plugin-bambuser, window['bambuser'] will be undefined.

Now we can refer to the plugin interface via this.broadcaster within our page.

To display the camera viewfinder behind the web view at appropriate times, we add calls to the plugin's show and hide functions in our lifecycle events handlers:

./src/pages/broadcaster/broadcaster.ts

ionViewDidLoad() {
  console.log('Starting viewfinder');

  if (!this.broadcaster) {
    await new Promise(resolve => setTimeout(resolve, 500)); // Let page animations to finish
    alert('broadcaster is not ready yet');
    return;
  }

  this.broadcaster.showViewfinderBehindWebView();
  document.getElementsByTagName('body')[0].classList.add("show-viewfinder");
}

ionViewWillLeave() {
  console.log('Removing viewfinder');
  document.getElementsByTagName('body')[0].classList.remove("show-viewfinder");
  if (this.broadcaster) {
    this.broadcaster.hideViewfinder();
  }
}

Finally, to be able to start and stop a broadcast, let's modify the start() and stop() methods we created on BroadcasterPage earlier - remove the setTimeout dummy code and replace it with:

./src/pages/broadcaster/broadcaster.ts

async start() {
  if (this.isBroadcasting || this.isPending) return;
  this.isPending = true;
  const toast = this.toastCtrl.create({
    message: 'Starting broadcast...',
    position: 'middle',
  });
  toast.present();

  console.log('Starting broadcast');
  await this.broadcaster.startBroadcast();

  toast.dismiss();
  this.isBroadcasting = true;
  this.isPending = false;
}

and

./src/pages/broadcaster/broadcaster.ts

async stop() {
  if (!this.isBroadcasting || this.isPending) return;
  this.isPending = true;
  const toast = this.toastCtrl.create({
    message: 'Ending broadcast...',
    position: 'middle'
  });
  toast.present();

  console.log('Ending broadcast');
  await this.broadcaster.stopBroadcast();

  toast.dismiss();
  this.isBroadcasting = false;
  this.isPending = false;
}

Now it is time to deploy this to an actual device and enjoy our first broadcast!

Run ionic cordova prepare again to bundle up our JavaScript changes, then use Android Studio or XCode to build the projects found in ./platforms/android and / or ./platforms/ios for a connected phone or tablet.

Later we'll look into building an in-app viewing experience, but for now, we can use the content page on dashboard.bambuser.com to verify our results.

Putting it all together

See bambuser-examplebroadcaster-ionic on GitHub for the completed project. Be sure to follow the steps in its README: the setup described in the early part of this article needs to be done after checkout.

What's next?

In many cases you want to be able to play back your live streams in the same app you're broadcasting from. In a future article we will look in detail into how to use the Bambuser player in an Ionic app - either as a dedicated viewing app or as a page within the app we built above. In the meantime, check out bambuser-exampleplayer-ionic for an idea of how that can be accomplished.