As mentioned in my previous post “The Ultimate Guide to Understanding Advanced Video Delivery with AIR for Mobile“, Adobe AIR can help to significantly reduce fragmentation on devices. Here is another example of an AIR use case that allows to drastically gain reach with minimal investment – live radio streaming.

RTMP is a widely deployed streaming protocol for radio stations. AIR allows to tap into the existing server infrastructure, and stream the content to devices. In this example we are focusing on AAC+ audio streaming to Android devices with RTMP. For other platforms and/or codecs, please refer to the codec/protocol look-up table.

Features relevant to radio live streaming

  • Broad device reach, independent from underlying streaming capabilities
  • Reduced latency e.g. for live events, news, etc.
  • Intelligent reconnect feature to ensure a continuous stream while switching networks
  • Advanced metadata for title, artist and album

As explained in the my previous summary, Adobe AIR addresses the first requirement – device fragmentation. Since AIR is supported on Android 2.2 and higher, RTMP with AAC+ works automatically on the majority of all Android devices without customization or device specific testing.

Radio on Galaxy SRTMP fulfills the second requirement, very low live latency. The protocol was originally designed for real-time 2-way video communication, therefore has very minimal delay. That said, for live on mobile a buffer is recommended for smooth playback, which is user defined and set to 5 seconds in this example. Of course it’s also possible to switch to an HTTP based delivery approach with HDS, which changes the latency characteristics and reconnect requirements.

Next on the list is the reconnect feature, which is specifically important for a protocol like RTMP that maintains a consistent connection. If the connection is lost, or switches between networks, the playback will less likely pause with this feature. The application will try to reconnect while the buffer plays out. If it connects quickly enough, no interruption of the live stream will occur. The reconnect feature is automatically enabled in the latest version of OSMF.

Last but not least, the advanced metadata for title, artist or album. Live metadata is embedded as part of the stream, which is a feature that has been supported for years. Advanced encoders like the audio optimized Orban encoders and others support live metadata.

For the best experience, the encoder should provide data of the current song as part of the metadata sent when subscribing to the stream, and live cuepoints to update the information during playback. Instead of the encoder, an intermediate server can provide this as well, but you can find more information in the live metadata article.

And finally, the complete code:

<?xml version="1.0" encoding="utf-8"?>
<s:Application xmlns:fx="http://ns.adobe.com/mxml/2009" 
			   xmlns:s="library://ns.adobe.com/flex/spark" backgroundColor="#000000" applicationDPI="160" creationComplete="init()">
	<fx:Script>
		<![CDATA[
 
			import mx.core.UIComponent;
			import org.osmf.containers.MediaContainer;
			import org.osmf.elements.AudioElement;
			import org.osmf.events.MediaElementEvent;
			import org.osmf.events.MediaPlayerStateChangeEvent;
			import org.osmf.events.TimelineMetadataEvent;
			import org.osmf.media.MediaElement;
			import org.osmf.media.MediaPlayer;
			import org.osmf.media.MediaPlayerState;
			import org.osmf.metadata.CuePoint;
			import org.osmf.metadata.TimelineMetadata;
			import org.osmf.net.NetClient;
			import org.osmf.net.StreamType;
			import org.osmf.net.StreamingURLResource;
			import org.osmf.traits.LoadTrait;
			import org.osmf.traits.MediaTraitType;
 
			private var container:MediaContainer;
			private var background:Sprite;
			private var mediaPlayer:MediaPlayer;
			private var audioElement:MediaElement;		
			private var bufferTimer:Timer;
 
			private function init() : void {
 
				// OSMF MediaContainer
				container = new MediaContainer();
				var containerui:UIComponent = new UIComponent();
				containerui.addChild(container);
				addElement(containerui);
 
				// Source
				var resource:StreamingURLResource = new StreamingURLResource("rtmp://myserver.com/live/mystream",StreamType.LIVE);							
				audioElement = new AudioElement(resource);
				container.addMediaElement(audioElement);
 
				// OSMF MediaPlayer
				mediaPlayer = new MediaPlayer();
				mediaPlayer.media = audioElement;
				mediaPlayer.autoPlay = false;
 
				//Define the buffer time
				mediaPlayer.bufferTime = 5;
 
				//Timer to display the buffer
				bufferTimer = new Timer(200);
				bufferTimer.addEventListener(TimerEvent.TIMER,bufferUpdate)
				bufferTimer.start();
 
				// Metadata events
				audioElement.addEventListener(MediaElementEvent.METADATA_ADD, onMetadataAdd);			
				mediaPlayer.addEventListener(MediaPlayerStateChangeEvent.MEDIA_PLAYER_STATE_CHANGE, onMediaStateChange);
 
			}
 
 
			// Add the metadata event handler for the first time metadata when subscribing to the live stream
			protected function onMediaStateChange(e:MediaPlayerStateChangeEvent): void 
			{
				switch(e.state) {
					case MediaPlayerState.READY:
						var loadTrait:LoadTrait = mediaPlayer.media.getTrait(MediaTraitType.LOAD) as LoadTrait;
						var netStream:NetStream = loadTrait["netStream"] as NetStream;
						var netConn:NetConnection = loadTrait["connection"] as NetConnection;
						NetClient(netStream.client).addHandler("onMetaData", onMetaDataHandler);
						break;
				}
			}
 
 
			protected function onMetaDataHandler(info:Object):void
			{
				//Parse the stream metadata information and assign to UI
			}			 
 
 
			// Monitor the live cuepoints during playback for updated metadata information
			private function onMetadataAdd(event:MediaElementEvent):void
			{
				if (event.namespaceURL == CuePoint.EMBEDDED_CUEPOINTS_NAMESPACE)
				{
					var timelineMetadata:TimelineMetadata = audioElement.getMetadata(CuePoint.EMBEDDED_CUEPOINTS_NAMESPACE) as TimelineMetadata;
					timelineMetadata.addEventListener(TimelineMetadataEvent.MARKER_TIME_REACHED, onCuePoint);
				}
			}
 
			private function onCuePoint(event:TimelineMetadataEvent):void
			{
				//Parse the stream metadata information and assign to UI
			}
 
 
			// Display the updated buffer
			private function bufferUpdate(e:TimerEvent) : void {
				buffer_txt.text = (Math.floor(mediaPlayer.bufferLength*100)/100).toString();
			}
 
			// Play and pause buttons
			private function togglePlayback() : void {
				if (mediaPlayer.playing) {
					mediaPlayer.stop();
					playbutton.label = "Start Playback";
				}
				else {
					mediaPlayer.play();
					playbutton.label = "Stop Playback";
				}	
			}
 
 
		]]>
	</fx:Script>
 
	<s:VGroup x="0" y="0" width="100%" height="100%">
		<s:Label id="buffer_txt" width="30%" height="5%" fontSize="20" text="Buffer"  color="#FFFFFF"/>
		<s:Scroller id="contentScroller" height="75%" width="100%">
			<s:Group>
				<s:Image id="coverimg" x="10" y="130" width="300" height="202"/>
				<s:Label id="title_txt" x="12" y="70" width="300" height="31" color="#BBBBBB"/>
			</s:Group>
		</s:Scroller>
		<s:Button id="playbutton" width="100%" height="20%" label="Start Playback"
				  click="togglePlayback()" fontSize="20" />
	</s:VGroup>
</s:Application>

Conclusion

With a couple of lines of client code it is possible to create a basic, but highly functional, radio application, that allows the use existing work flows and reach millions of Android users without having to worry about fragmentation.