Build a streaming audio app

with React Native 

By Josh Habdas

http://habd.as blog

@jhabdas github

Turntable by Unspash (CCO Public Domain) 

Lumpen Radio

WLPN 105.5 FM Chicago

Insurgent radio from Chicago!

lumpenradio.com

low-power
freeform
commercial

Low-Power FM Broadcast Signal (LPFM)

  • Made available in 2000
  • FCC regulated
  • ~100 watts
  • ~3.5 mile radius

1.7M listener reach

Crowdfunded

> $50K raised

Graphic designed by Jeremiah Chiu for WLPN

Bridgeport Studio

Kickstarter video

React Native iOS App

For iPad and iPhone

Built to learn

Children Studying by sof_sof_0000 (CC0 Public Domain)

Stack

  • React Native
  • Babel
  • Webpack
  • StreamingKit
  • Nicecast

Languages

Objective-C, Swift, ES6/7

Open Source

Benefits of React Native

  • JavaScript components
  • Access platform APIs
  • Use Web technology
  • Renders views natively
  • Faster prototyping

But it looks funny...

"[T]hink of it as a prototype for a different direction of the web."
— James Long

Building it

Radio Mast by stux (CC0 Public Domain)

Getting started

  • Learn the basics
  • Init project with CLI
  • Generate with Yeoman
  • Start from seed

Learn the basics

Init project with CLI


  $ npm install -g react-native-cli
  $ react-native init my-project

Generate with Yeoman


  $ npm install -g yo \
        generator-react-native
  $ yo react-native

Start from seed

Kiwi by Security (CC0 Public Domain)

Create an App component

It should probably...

  • Serve as App canvas
  • Hold app container View
  • Manage screen layout
  • Nest components
  • Route between Views

Adding a background video

Using react-native-video by Brent Vatne

Turntable loop video by Scott Schiller, BSD

Install component


  $ npm install --save react-native-video

Terminal

Consider shrinkwrapping dependencies

Add to project

  • Add .xcodeproj file using the "Add Files to..." dialog shown after context-clicking Libraries.
  • Link binary and copy bundle resources as described in the project README.
  • Configure project to leverage ES7 Rest/Spread properties.

Import component


  import React from 'react-native';
  import Video from 'react-native-video';

  export default React.createClass({
    render() {
      return (
        <Video source={{uri: 'turntable-loop-h264-512kbps'}}
          style={styles.backgroundVideo}
          rate={this.state.rate}
          muted={this.state.muted}
          resizeMode={this.state.resizeMode}
          repeat={this.state.repeat} />
      )
    });
  }

Editor

Style component


  import React from 'react-native';

  let { StyleSheet } = React;

  export default StyleSheet.create({
    backgroundVideo: {
      position: 'absolute',
      top: 0,
      left: 0,
      bottom: 0,
      right: 0
    }
  }

Editor

Uses

  • Loop animations on iOS
  • Cast to Apple TV using AirPlay

Create a Native Module

@implementation AudioManager 

Module Composition

Adopt protocols


  #import "RCTBridgeModule.h" // or RCTBridge.h
  #import "STKAudioPlayer.h"

  @interface AudioManager : NSObject 
      <RCTBridgeModule, STKAudioPlayerDelegate>

  @property (nonatomic, strong)
      STKAudioPlayer *audioPlayer;

Xcode, Obj-C

Initialize module


  - (AudioManager *)init
  {
    self = [super init];
    audioPlayer = [[STKAudioPlayer alloc] init];
    [audioPlayer setDelegate:self];
  }

Xcode, Obj-C

STK player delegates to class

Define module API


  #import "RCTBridge.h"

  @implementation AudioManager

  RCT_EXPORT_MODULE();
  RCT_EXPORT_METHOD(play);
  RCT_EXPORT_METHOD(pause);
  RCT_EXPORT_METHOD(resume);
  RCT_EXPORT_METHOD(stop);

  @end

Xcode, Obj-C

Implement API methods


  RCT_EXPORT_METHOD(pause)
  {
    if (!audioPlayer) {
      return;
    } else {
      [audioPlayer pause];
    }
  }

Xcode, Obj-C

Observe player state


  - (void)audioPlayer:(STKAudioPlayer *)player
         stateChanged:(STKAudioPlayerState)state
  {
    switch(state) {
      // don't make me do stuff
    }
  }

Xcode, Obj-C

Use STK for state change handling

Dispatch state changes


  #import "RCTEventDispatcher.h"

  @synthesize bridge = _bridge;

Xcode, Obj-C

  // dispatch on STK state change

  switch (state) {
  case STKAudioPlayerStatePlaying:
    [self.bridge.eventDispatcher
     sendDeviceEventWithName:@"AudioBridgeEvent"
                        body:@{@"status": @"PLAYING"}];
    break;
  }

Create a Native Module

export class AudioPlayer

Create native interface


  import { AudioManager } from 'NativeModules';

  export class AudioPlayer
    static play() { AudioManager.play(); }
    static pause() { AudioManager.pause(); }
    static resume() { AudioManager.resume(); }
    static stop() { AudioManager.stop(); }
  }

Editor

Create component


  export default React.createClass({
    getInitialState() {
      return { status: 'STOPPED' };
    }
  });

Editor

Handle dispatched events


  componentDidMount() {
    this.subscription = DeviceEventEmitter.addListener(
      'AudioBridgeEvent', (evt) => this.setState(evt)
    );
    AudioPlayer.getStatus((error, status) => {
      (error) ? console.log(error) : this.setState(status)
    });
  }

Editor

Add UI button


  render() {
    return (
      <View style={styles.appContainer}>
        <TouchableOpacity
          onPress={this._onPressLogo}
          onLongPress={this._onLongPressLogo}>
          <Image
            style={styles.appLogo}
            source={require('image!RadioButton')} />
        </TouchableOpacity>
      </View>
    );
  },

Editor

Import ES6 module


  import { AudioPlayer } from '../lib/audio';

  

Editor

For use in your React components

Register button callbacks


  _onLongPressLogo() {
    AudioPlayer.play();
  },
  _onPressLogo() {
    switch (this.state.status) {
      case 'PLAYING':
        this.setState({
          status: 'PAUSED'
        });
        AudioPlayer.pause();
        break;
    }
  }

Editor

Provide connection status

Import NetInfo module


  import React from 'react-native';

  let { NetInfo } = React;

Editor, ES6

Subscribe to changes


  export default React.createClass({
    componentDidMount() {
      NetInfo.isConnected.addEventListener(
        'change',
        this._onConnectivityChange
      );
      NetInfo.isConnected.fetch().done((isConnected) => {
        this.setState({ isConnected });
      });
    },
    _onConnectivityChange(isConnected) {
      this.setState({ isConnected });
    }
  }
  

Editor, ES6

Display status


  render() {
    let message;

    if (this.state.isConnected) {
      switch(this.props.status) {
        // set status message
      }
    } else {
      message = 'Connect to the Internet.';
    }
    // ...
  }

Editor, ES6

Display status


  render() {
    // ...
    return (
      <Text style={styles.statusMessage}>
        {message}
      </Text>
    )
  }

Editor, ES6/JSX

Animating

Or inbetweening

No, this way!

React Tween State

react-motion another option...

Install component


  $ npm install --save react-tween-state

Terminal, Bash

Call mixin on mount


  export default React.createClass({
    mixins: [tweenState.Mixin],
    componentDidMount() {
      this.tweenState('opacity', {
        beginValue: 0,
        endValue: 1,
        duration: 1000
      });
    }
  }

Editor, ES6

Render with transition


  render() {
    let transitionStyle = {
      opacity: this.getTweeningValue('opacity')
    };
    
    return (
      <View style={transitionStyle}>
        // what to tween
      </View>
    )
  }

Editor, ES6

Using platform APIs

Background playback


  @import AVFoundation

  - (void)setSharedAudioSessionCategory
  {
    NSError *categoryError = nil;
    [[AVAudioSession sharedInstance]
      setCategory:AVAudioSessionCategoryPlayback
            error:&categoryError];
    if (categoryError) {
      NSLog(@"Error setting category!");
    }
  }

Xcode, Obj-C

And set background mode in Info.plist

First responder


  import UIKit

  class RootViewController: UIViewController {
  
    override func canBecomeFirstResponder() -> Bool {
      return true
    }
  
    override func viewDidAppear(animated: Bool) {
      super.viewDidAppear(animated)
      self.becomeFirstResponder()
      UIApplication.sharedApplication().beginReceivingRemoteControlEvents()
    }
  }

Xcode, Swift

Swap with vanilla controller in AppDelegate

Remote control events


  @import MediaPlayer;
  
  - (void)registerRemoteControlEvents
  {
    MPRemoteCommandCenter *commandCenter;
    commandCenter = [MPRemoteCommandCenter sharedCommandCenter];
    [commandCenter.playCommand addTarget:self
                                  action:@selector(didReceivePlayCommand:)];
    [commandCenter.pauseCommand addTarget:self
                                   action:@selector(didReceivePauseCommand:)];
    commandCenter.stopCommand.enabled = NO;
    commandCenter.nextTrackCommand.enabled = NO;
    commandCenter.previousTrackCommand.enabled = NO;
  }

Xcode, Obj-C

Buttons, control panel, lock screen

Audio interruptions


  @import AVFoundation

  

Xcode, Obj-C

See a more detailed explanation on SO

  • Stop on device disconnect
  • Resume after interruption

Creating an App Icon

Simplicity is the ultimate sophistication

Leonardo Da Vinci

Considerations

  • Using a template
  • Automating icon creation

Using a template

iOS 8 App Icon SVG Template for Inkscape

Automating icon creation

Other considerations

Voice Over

Static resources like image assets take their name from Images.xcassets.

 

Use spaces to improve aural comprehension.

Localization

Short and sweet.

 

  1. Install react-native-localization module

  2. Configure Xcode

  3. Create component in app

  4. Learn on the Wiki

Stream location

  • Define in Constants.h
  • Don't hardcode stream IPs

Debugging

  • Shrinkwrap deps, update often
  • Try different versions of Node.js
  • Cmd+Shift+K in Xcode can help
  • Review your build configuration
  • Verify Chrome debugger works

Test and submit

Beta testing

Submit to App Store

  • Get reviewed in beta
  • Establish your 1.0
  • Release when ready

Deliver sooner

Reflecting on React Native development

You are a pioneer

Screenshot of the Oregon Trail game

Thought I was too

Orin_has_died_of_dysentery by Orin Blomberg (CC BY-NC 2.0)

Graphic designed by Jeremiah Chiu for WLPN

Made with Slides.com