When it comes to creating an agent/client live stream using FMS there are few parameters to keep in mind:
1. Live feed width
2. Live feed height
3. Camera resolution width
4. Camera resolution height
5. FPS (Frame Per Second)
6. Quality
7. Bandwidth
8. Microphone options
This article is just the tip of the iceberg, but it should give you a good idea and much information needed to build a high quality live stream application for Flash or Flex.
Let’s start with the video stream.
Adobe’s docs talks about this to some extent here:
http://livedocs.adobe.com/flashmediaserver/3.0/hpdocs/help.html?content=00000139.html
I often hear client asking for live stream feed with “porn” quality. The quality is really setting these variables and granular adjusting these variables until you find what’s work best and not just choosing the best CDN out there.
You cannot record a live stream video in high resolution, top quality, high FPS and high quality audio stream on an ok bandwidth and expact it stream great.
I recommend setting these variables at the beginning of the class so you can keep adjusting them until you found what suits you.
private static const LIVE_FEED_WIDTH:int = 220;
private static const LIVE_FEED_HEIGHT:int = 147;
private static const CAM_FEED_WIDTH:int = 80;
private static const CAM_FEED_HEIGHT:int = 53;
private static const FPS:Number = 15; // recommended between 15-25
private static const QUALITY:int = 95; // values from 0-100
private static const BANDWIDTH:int = 0; // when set as 0 will adjust to highest bw based on user's settings
Than when you setup your video and stream you can use these variables:
// setup cameras
cameraVideo = new Video(CAM_FEED_WIDTH,CAM_FEED_HEIGHT);
liveFeedVideo = new Video(LIVE_FEED_WIDTH,LIVE_FEED_HEIGHT);
// Cameras & quality
camera.setMode(LIVE_FEED_WIDTH*0.90,LIVE_FEED_WIDTH*0.90,FPS);
camera.setQuality(BANDWIDTH,QUALITY);
I also recommend setting the smoothing so the stream will display better:
cameraVideo.smoothing = liveFeedVideo.smoothing = true;
The FMS server can be configured as well and you can create your custom client and adjust properties manually when the Bandwidth changes:
netConnection = new NetConnection();
netConnection.client = new Client();
package
{
public class Client
{
public function onBWCheck(... rest):Number
{
return 0;
}
public function onBWDone(... rest):void
{
var p_bw:Number;
if (rest.length > 0) p_bw = rest[0];
// your application should do something here
// when the bandwidth check is complete
//trace("bandwidth = " + p_bw + " Kbps.");
}
public function onFCSubscribe(info:Object):void
{
// do something
}
}
}
In addition, using out of the box solution such as the build in mx:VideoDisplay, s:VideoDisplay or OSMF can help you set the best values without understanding too much or adjusting too many variables, however without adjusting the default behavior you may be disappointed with the quality.
For instance even the old school mx:VideoDisplay has a build in features such as the autoBandWidthDetection=”true”. When these sets as true the VideoDisplay control should use the built-in automatic bandwidth detection feature. When false, you do not require a main.asc file on Flash Media Server (FMS) 2 to connect to FMS. When true, you need to implement a main.asc and store it in the directory structure of FMS.
As noted in the ASDOC the main.asc file must define the following functions:
application.onConnect = function(p_client, p_autoSenseBW)
{
//Add security code here.
this.acceptConnection(p_client);
if (p_autoSenseBW)
this.calculateClientBw(p_client);
else
p_client.call("onBWDone");
}
application.calculateClientBw = function(p_client)
{
// Add code to set the clients BandWidth.
// Use p_client.getStats() which returns bytes_in
// and bytes_Out and check your bandWidth using
// p_client.call("onBWCheck", result, p_client.payload).
p_client.call("onBWDone");
}
Client.prototype.getStreamLength = function(p_streamName)
{
return Stream.length(p_streamName);
}
Now what about CDN? Do they have anything to do with live stream quality? The answer is yes. Let’s assume you are connecting 1:1 or 1 to many. Connections to feeds from users from same continents number of connections can all effect your stream quality and should be taken into account when selecting your CDN. Akamai and Limelight are the leaders in term of reputation and service, however when you start streaming and don’t publish too many feeds, setting Amazon EC2 may be all that you need to get top notch live stream and once you scale up and you can start taking advantage of CDNs that support more features.
Here’s an example code that can be used as the foundation to building a live stream:
<s:Application xmlns:fx="http://ns.adobe.com/mxml/2009"
xmlns:s="library://ns.adobe.com/flex/spark"
xmlns:mx="library://ns.adobe.com/flex/mx"
minWidth="955" minHeight="600"
creationComplete="init()">
<fx:Script>
<![CDATA[
import mx.controls.Alert;
import mx.core.FlexGlobals;
import mx.core.UIComponent;
private static const LIVE_FEED_WIDTH:int = 220;
private static const LIVE_FEED_HEIGHT:int = 147;
private static const CAM_FEED_WIDTH:int = 80;
private static const CAM_FEED_HEIGHT:int = 53;
private static const FPS:Number = 15; // recommended between 15-25
private static const QUALITY:int = 95; // values from 0-100
private static const BANDWIDTH:int = 0; // when set as 0 will adjust to highest bw based on user's settings
// settings
private var userType:int;
private var sessionId:String = "1";
private var debugMode:Boolean = true;
private var rtmpEndPoint:String = "rtmp://[server URL]/live";
// video variables
private var camera:Camera;
private var mic:Microphone;
private var netConnection:NetConnection;
private var cameraNetStream:NetStream;
private var liveFeedNetStream:NetStream;
private var cameraVideo:Video;
private var liveFeedVideo:Video;
// feeds ids
private var camFeedId:String = "";
private var liveFeedId:String = "";
private function init():void
{
setURLParams();
// setup cameras
cameraVideo = new Video(CAM_FEED_WIDTH,CAM_FEED_HEIGHT);
liveFeedVideo = new Video(LIVE_FEED_WIDTH,LIVE_FEED_HEIGHT);
cameraVideo.smoothing = liveFeedVideo.smoothing = true;
connectToFMS();
}
private function setURLParams():void
{
var params:Object = FlexGlobals.topLevelApplication.parameters;
userType = int(params.userType);
if ( params.userType == "undefined" || params.sessionId == "undefined" || params.rtmpEndPoint == "underfined")
{
// debug mode
userTypeDropDownList.visible = true;
}
else
{
// live mode
rtmpEndPoint = params.rtmpEndPoint;
sessionId = params.sessionId;
debugMode = false;
}
}
private function setSettingBasedOnUserType():void
{
if (userType == UserType.AGENT)
{
camFeedId = "agent" + sessionId;
liveFeedId = "client" + sessionId;
}
else // CLIENT
{
camFeedId = "client" + sessionId;
liveFeedId = "agent" + sessionId;
}
startCamFeedCamera();
startLiveFeedCamera();
}
private function connectToFMS():void
{
netConnection = new NetConnection();
netConnection.client = {};
netConnection.addEventListener(NetStatusEvent.NET_STATUS, onNetConnectStatus);
netConnection.connect(rtmpEndPoint);
}
private function onNetConnectStatus(e:NetStatusEvent):void
{
trace("Connected to FMS Successfully!");
if (!debugMode)
setSettingBasedOnUserType();
}
private function setCamera():void
{
camera = Camera.getCamera();
mic = Microphone.getMicrophone();
// mic settings
mic.setLoopBack(false);
mic.setUseEchoSuppression(true);
// Cameras & quality
camera.setMode(LIVE_FEED_WIDTH*0.90,LIVE_FEED_WIDTH*0.90,FPS);
camera.setQuality(BANDWIDTH,QUALITY);
cameraNetStream = new NetStream(netConnection);
}
private function addMask(circleMask:UIComponent, feed:Image, radius:int):void
{
if (circleMask == null)
{
circleMask = createCircle(radius);
this.addElement( circleMask );
}
feed.mask = circleMask;
}
private function createCircle(r:Number):UIComponent
{
var circle:Sprite = new Sprite();
circle.graphics.lineStyle(1, 0x000000);
circle.graphics.beginFill(0x000000);
circle.graphics.drawCircle(r, r, r);
circle.graphics.endFill();
var uicom:UIComponent = new UIComponent();
uicom.addChild(circle);
uicom.x = uicom.y = 0;
uicom.width = uicom.height = 100;
return uicom;
}
private function startCamFeedCamera():void
{
setCamera();
cameraVideo.attachCamera(camera);
cameraNetStream.attachCamera(camera);
cameraNetStream.attachAudio(mic);
camFeed.addChild( cameraVideo );
// publish to FMS
cameraNetStream.publish(getCameraFeedId, "live");
}
private function startLiveFeedCamera():void
{
// bring from FMS
liveFeedNetStream = new NetStream(netConnection);
liveFeedNetStream.play(getLiveFeedId);
liveFeedVideo.attachNetStream(liveFeedNetStream);
liveFeed.addChild( liveFeedVideo );
// mask
// addMask(circleMaskLiveFeed, liveFeed, 100);
}
private function get getLiveFeedId():String
{
return liveFeedId;
}
private function get getCameraFeedId():String
{
return camFeedId;
}
]]>
</fx:Script>
<mx:UIComponent id="liveFeed" width="{LIVE_FEED_WIDTH}" height="{LIVE_FEED_HEIGHT}" y="0" x="0" />
<mx:UIComponent id="camFeed" width="{CAM_FEED_WIDTH}" height="{CAM_FEED_HEIGHT}" />
<mx:Image id="recordButton" x="0" y="230" click="recordButton.source='assets/images/record_on_icon.gif'"
source="assets/images/record_icon.gif"/>
<mx:Image id="stopButton" y="230" x="41" click="recordButton.source='assets/images/record_icon.gif'"
source="assets/images/stop_icon.gif"/>
<s:DropDownList id="userTypeDropDownList" x="170" y="0"
width="140" selectedIndex="0"
change="{userType=userTypeDropDownList.selectedIndex-1; setSettingBasedOnUserType();}" visible="false">
<mx:ArrayCollection>
<fx:String>---</fx:String>
<fx:String>Agent</fx:String>
<fx:String>Client</fx:String>
</mx:ArrayCollection>
</s:DropDownList>
</s:Application>
In regards to the microphone, you can use the Microphone’s setUseEchoSuppression(true). setLoopBack(false) for removing mic’s echo and feedback but that is usually not enough to get top quality audio stream.
in FP 10.3, AIR 2.7 and up Adobe introduced enhanced audio, which includes acoustic echo cancellation and noise suppression etc.
there is a good article on Adobe’s ADC that takes about the new features and best practices:
http://www.adobe.com/devnet/flashplayer/articles/acoustic-echo-cancellation.html
In a nutshell here’s a method that will set the user’s mic with best possible settings:
private function setMicOptions():Microphone
{
var options:MicrophoneEnhancedOptions = new MicrophoneEnhancedOptions();
var microphone:Microphone = Microphone.getEnhancedMicrophone();
// settings
microphone.setLoopBack(false);
microphone.setUseEchoSuppression(true); // being ignored but setting it anyway
microphone.encodeQuality = 8;
microphone.enableVAD = true;
microphone.codec = SoundCodec.SPEEX;
microphone.framesPerPacket = 1;
microphone.setSilenceLevel(0, 2000);
microphone.gain = 50;
microphone.setSilenceLevel(0);
microphone.rate = 16;
microphone.addEventListener(ActivityEvent.ACTIVITY, activityHandler);
microphone.addEventListener(StatusEvent.STATUS, statusHandler);
// options
options.mode = MicrophoneEnhancedMode.FULL_DUPLEX;
options.echoPath = 128; // can be increased to 256 for less echo
options.nonLinearProcessing = true;
microphone.enhancedOptions = options;
return microphone;
}
You can fine tune and adjust these settings but that should give you better results than the default built in settings. Notice that I am using the Speex codec (available from FP 10 and up). Speex is an open-source, royalty-free codec. Flash supports Speex encoding at 16 kHz.
When you use Speex transmission delay is minimized. FP uses an adaptive Speex jitter buffer when playing out messages. FP also using the Speex noise suppression and voice activity detection, which lower transmission’s bandwidth during silence.