最近做的一個基於FMS的Flash錄音示範,主要的幾個技術點:
1. FMS 安裝及使用
2. Flash 錄音及相應的安全性問題
3. 音效檔的轉換
1. FMS 安裝及使用
- Flash Media Server 可從Adobe 官方網站下載:http://www.adobe.com/products/flashmediaserver/
,目前版本為3.5,FMS本身包含一系列的軟體,售價在幾千美元,作為開發人員可直接下載 Flash Media Development Server,FMS絕大部分的功能都支援。
- 安裝,後台管理都比較簡單,可以百度一下 “FMS 入門教程”。
- 流媒體預設使用 rtmp 協議通過 1935 連接埠發布,如果選擇安裝Apache,將支援 HTTP 轉寄,HTTP預設連接埠是80,因為我的伺服器本身已經部署了IIS,所以需要修改連接埠,具體的設定檔在 [FMS安裝目錄]/conf/fms.ini,及[FMS安裝目錄]/conf/_defaultRoot_/Adaptor.xml,具體可參考:http://help.adobe.com/en_US/FlashMediaServer/3.5_AdminGuide/WSE2A5A7B9-E118-496f-92F9-E295038DB7DB.html
或 這裡
。
2. 用戶端串連及發布
private var nc: NetConnection;
private var ns: NetStream;
nc = new NetConnection();
nc.addEventListener(NetStatusEvent.NET_STATUS, onNetStatus);
private function onNetStatus(event:NetStatusEvent):void{
ShowTrace("onNetStatus, code: " + event.info.code);
switch(event.info.code){
//串連成功
case "NetConnection.Connect.Success":
break;
case "NetStream.Play.Start":
break;
case "NetConnection.Connect.Closed":
case "NetConnection.Connect.Failed":
case "NetConnection.Connect.Rejected":
if(flag)
ExternalInterface.call("flash_callback_proxy", "stopRecord");
else
ExternalInterface.call("flash_callback_proxy", "failRecord");
flag = false;
break;
//開始錄製,在此之前還會收到 NetSteam.Publish.Start 事件
case "NetStream.Record.Start":
ExternalInterface.call("flash_callback_proxy", "startRecord");
break;
}
}
//開始錄製
private function startRecord(audioid : String) : Boolean
{
ShowTrace("Get js call startRecord");
if(!nc.connected) {
if(retryFlag == 0)
{
ShowTrace("startRecord, need connect to server, count: " + retryFlag);
try
{
nc.connect("rtmp://192.168.5.2/demo"); //demo
}
catch(e) {
ShowTrace("startRecord, catch exception: " + e.toString());
}
}
retryFlag++;
if(retryFlag > 3)
ShowTrace("startRecord, connect to server timeout!");
else
intervalId = flash.utils.setTimeout(startRecord, 3000, audioid);
return false;
} else {
flag = true;
retryFlag = 0;
flash.utils.clearTimeout(intervalId);
}
//Publish audio
if(!setupAudio())
return false;
publishAudio(audioid);
micTimer.start();
ShowTrace("startRecord, started, id: " + audioid);
return true;
}
private function stopRecord()
{
ShowTrace("Get js call stopRecord");
//Stop publish
micTimer.stop();
ns.close();
ns = null;
nc.close();
}
3. 錄音及音量條的顯示
以下幾個和錄音有關的函數,Flash 中當你啟動錄音擷取Micphone資料時,預設情況下是會彈出一個安全警告請求使用者允許的,有趣的是我的Flash怎麼也不出來,就算在程式中強制調用Security.showSettings(SecurityPanel.PRIVACY);也不行,滑鼠點右鍵設定菜單也是灰的,搞了好久才發現原來是我的Flash在網頁中的位置太小了,空間不夠彈出這個提示框。
private function setupAudio():Boolean {
//Security.showSettings(SecurityPanel.MICROPHONE);
mic = Microphone.getMicrophone();
if(mic == null)
{
ShowTrace("setupVideos, no micphone found!");
return false;
}
else
{
ShowTrace("setupVideos, getMicrophone success!");
mic.rate = 22; //使用 22K 採樣
mic.setSilenceLevel(5, -1);
mic.addEventListener(StatusEvent.STATUS, micStatusHandler);
//mic.addEventListener(ActivityEvent.ACTIVITY, drawMicLevel);
}
return true;
}
private function publishAudio(audioid: String) {
//Start publish
ns = new NetStream(nc);
ns.client = new CustomClient();
ns.addEventListener(NetStatusEvent.NET_STATUS, onNetStatus);
ns.addEventListener(AsyncErrorEvent.ASYNC_ERROR, onAsyncError);
ns.attachAudio(mic);
ns.publish(audioid, "record"); //發布到 FMS,並錄製
}
//錄音音量條的顯示,我實際的做法是在後面畫了的漸層,然後在前面根據Micphone的音量動態畫一個白色的遮罩
private function initBackground() : void
{
var myMatrix:Matrix = new Matrix();
trace("initBackground: " + myMatrix.toString()); // (a=1, b=0, c=0, d=1, tx=0, ty=0)
myMatrix.createGradientBox(250, 250, 0, 50, 50);
trace("initBackground: " + myMatrix.toString()); // (a=0.1220703125, b=0, c=0, d=0.1220703125, tx=150, ty=150)
var colors:Array = [0x00FF00, 0xFFFF00, 0xFFFF00, 0xFF0000];
var alphas:Array = [80, 100, 100, 100];
var ratios:Array = [0, 175, 215, 0xFF];
this.graphics.beginGradientFill(GradientType.LINEAR, colors, alphas, ratios, myMatrix);
this.graphics.drawRect(0, 0, stage.stageWidth, stage.stageHeight);
this.graphics.endFill();
this.graphHolder.graphics.beginFill(0xFFFFFF);
this.graphHolder.graphics.drawRect(0, 0, stage.stageWidth, stage.stageHeight);
this.graphHolder.graphics.endFill();
}
private function drawMicLevel (e:TimerEvent) : void
{
if(!nc.connected) return;
//ShowTrace("drawMicLevel, activityLevel: " + mic.activityLevel);
var level = 100;
this.graphHolder.graphics.clear();
this.graphHolder.graphics.beginFill(0xFFFFFF);
if(vuDirection == "horizontal")
{
level = (mic.activityLevel / 100) * stage.stageWidth;
this.graphHolder.graphics.drawRect(level, 0, stage.stageWidth, stage.stageHeight);
}
else
{
level = (mic.activityLevel / 100) * stage.stageHeight;
this.graphHolder.graphics.drawRect(0, 0, stage.stageWidth, stage.stageHeight-level);
}
this.graphHolder.graphics.endFill();
//ShowTrace("drawMicLevel, level: " + level);
}
4. Flash 和Javascript 相互調用
//Flash 調用 js 函數
ExternalInterface.call("flash_callback_proxy", "failRecord");
//Flash 中要允許 JavaScript 調用,並匯出相應的函數,這裡我是做了個代理,所有js調用都經過此函數轉寄
{
Security.allowDomain("*");
Security.allowInsecureDomain("*");
ExternalInterface.addCallback("js_callback_proxy", js_callback_proxy);
}
//function call by Javascript
function js_callback_proxy(_p1, _p2) : Boolean
{
var ret : Boolean = true;
ShowTrace("js_callback_proxy, cmd: " + _p1);
switch(_p1){
case "startRecord":
ret = startRecord(_p2);
break;
case "stopRecord":
stopRecord();
break;
case "setDirection":
setDirection(_p2);
break;
case "sowPreference":
showParameters();
break;
default:
ret = false;
}
return ret;
}
//js 中調用基本就是這樣
obj.js_callback_proxy('startRecord', evalid);
個人認為還是盡量不要從 js 中調用flash中的函數,一方面可能存在一些安全性方面的問題。另一方面我實際中也發現網頁中動態載入flash對象時,由js中調用flash函數會出現一些莫名其妙的問題。
5. 音頻格式的轉換
這裡是直接採用 ffmpeg 以命令列的方式進行轉換,將 flv 轉換為 16K 16bit 單聲道 wav。
ffmpeg -i d:/xxx.flv -ar 16000 -ab 16 -ac 1 d:/xxx.wav
6. 後續工作
服務端採用流媒體即時解碼成 WAV,我看了一下 FMS 提供的 Server API,似乎並不支援即時擷取流媒體的資料。
另一個好訊息是 Adobe 已經公開了 RTMP 協議的格式,而且我也看到了網上有別人寫好的支援此協議的開源服務端代碼,比如說 RED5,所以可以在開原始碼的基礎上進行開發。
參考網頁:List of Available RTMP Servers