Build a streaming audio app
with React Native
Turntable by Unspash (CCO Public Domain)
Lumpen Radio
WLPN 105.5 FM Chicago
Insurgent radio from Chicago!
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
- Complete the Start Developing iOS Apps Tutorial
- Review the iOS Human Interface Guidelines
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!
- No official approach yet
- Said to be "in development"
- Multiple contenders arising
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.
-
Install react-native-localization module
-
Configure Xcode
-
Create component in app
-
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
- Invite users with iTunes Connect
- App downloads thru TestFlight
- Testers receive emails from Apple
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
Build a streaming audio app with React Native
By Josh Habdas
Build a streaming audio app with React Native
How to create a simple React Native app to stream audio over the web from start to app store submission.
- 42,568