Red5的服務器搭建請看我的另外一篇文章:
http://blog.csdn.net/sunroyi666/article/details/52981639
最近正在開發基於流媒體的資源管理和直播系統,做了不少技術調查,框架的構思,也學到了很多東西。
現在空下來,把最近學到的技術知識和項目的架構分享一下。
首先
系統的開發目的是爲了將用戶和資源從現在的系統分離出來,爲了以後系統擴展,更好的實現統一用戶管理和資源管理這一概念。現存的系統如下,可能大家遇到很多的系統都是這樣。
新系統是這樣的:
用戶中心用CAS實現,這個以後再說。
資源管理系統實際上分兩部分,系統部分用的Tomcat,流媒體服務器用的是Red5,兩者分離。
實際上我已經把Red5和Tomcat整合好了,但是想想或許分開的話,資源的獨立性更強,所以也就分開放了。
整合在一起的話,可以更方便的去控制用戶連接到Red5,斷開連接時的一些操作。
相互獨立的話,很多操作就要放到系統中進行判斷控制,或者在Red5這邊單獨控制。
但總體影響不大。
網上看到很多系統用的是Red5+Nginx而不是+Tomcat,這裏感覺也沒必要特地使用Nginx。
接下來是系統功能和實現:
1. 資源管理這邊提供了兩種形式的上傳,一種是上傳界面,一種是Restful接口。其他就是將上傳的路徑指向Red5服務器的目錄而已(我這裏是將資源管理系統和Red5服務器部署在一起的),然後將Red5服務器上的資源路徑返回給用戶自己保存。
RTMP是Adobe的私有協議,保存在實現RTMP協議的Red5流媒體服務器上的資源,不會像在Tomcat下面那樣輸入文件在服務器上的地址就能下載,保證了資源的安全性。
2. 上傳的視頻資源可用ffmpeg轉碼成其他格式(如FLV),並截取固定時間點的圖片作爲封面。
現在Red5已經支持mp4了,所以一般沒必要把mp4轉FLV。
ffmpeg工具可以去網上自己搜。
/**
* 視頻轉碼 (PC端FLV)
* @param ffmpegPath 轉碼工具的存放路徑
* @param upFilePath 用於指定要轉換格式的文件,要截圖的視頻源文件
* @param codcFilePath 格式轉換後的的文件保存路徑
* @param mediaPicPath 截圖保存路徑
* @return
* @throws Exception
*/
public boolean exchangeToFlv(String ffmpegPath, String upFilePath, String codcFilePath,
String mediaPicPath) throws Exception {
// 創建一個List集合來保存轉換視頻文件爲flv格式的命令
List<String> convert = new ArrayList<String>();
convert.add(ffmpegPath); // 添加轉換工具路徑
convert.add("-i"); // 添加參數"-i",該參數指定要轉換的文件
convert.add(upFilePath); // 添加要轉換格式的視頻文件的路徑
convert.add("-qscale"); //指定轉換的質量
convert.add("6");
convert.add("-ab"); //設置音頻碼率
convert.add("64");
convert.add("-ac"); //設置聲道數
convert.add("2");
convert.add("-ar"); //設置聲音的採樣頻率
convert.add("22050");
convert.add("-r"); //設置幀頻
convert.add("24");
convert.add("-y"); // 添加參數"-y",該參數指定將覆蓋已存在的文件
convert.add(codcFilePath);
// 創建一個List集合來保存從視頻中截取圖片的命令
List<String> cutpic = new ArrayList<String>();
cutpic.add(ffmpegPath);
cutpic.add("-i");
cutpic.add(upFilePath); // 同上(指定的文件即可以是轉換爲flv格式之前的文件,也可以是轉換的flv文件)
cutpic.add("-y");
cutpic.add("-f");
cutpic.add("image2");
cutpic.add("-ss"); // 添加參數"-ss",該參數指定截取的起始時間
cutpic.add("16"); // 添加起始時間爲第16秒
cutpic.add("-t"); // 添加參數"-t",該參數指定持續時間
cutpic.add("0.001"); // 添加持續時間爲1毫秒
cutpic.add("-s"); // 添加參數"-s",該參數指定截取的圖片大小
cutpic.add("350*240"); // 添加截取的圖片大小爲350*240
cutpic.add(mediaPicPath); // 添加截取的圖片的保存路徑
boolean mark = true;
ProcessBuilder builder = new ProcessBuilder();
try {
builder.command(convert);
builder.redirectErrorStream(true);
builder.start();
builder.command(cutpic);
builder.redirectErrorStream(true);
// 如果此屬性爲 true,則任何由通過此對象的 start() 方法啓動的後續子進程生成的錯誤輸出都將與標準輸出合併,
//因此兩者均可使用 Process.getInputStream() 方法讀取。這使得關聯錯誤消息和相應的輸出變得更容易
builder.start();
} catch (Exception e) {
mark = false;
System.out.println(e);
e.printStackTrace();
}
return mark;
}
3. 因爲資源管理用的是Red5流媒體服務器,所以順便把直播功能也一起實現了,正好也要做這樣的系統。
直播界面用Adobe Flash Builder 4.7做的swf。
因爲我也是第一次用FlashBuilder,
這裏先普及一下Actionscript,AS3,MXML,Flex,FlexBuilder,FlashBuilder,Flash,AIR,FlashPlayer的關係。
Actionscript簡稱AS,是Flash平臺語言,根javascript其實很挺像的。
AS3是Actionscript的3.0版,也是現在最流行的版本。
Flex是官方的開發框架。
MXML是基於XML語法的語言,有點像HTML,可以與AS混用,最終編譯成SWF。
Flash是一個平臺,包含衆多產品,比如網頁動畫,工具等等。
FlexBuilder是一套基於Eclipse的IDE。FlexBuilder從4.0開始改名爲FlashBuilder。
FlashPlayer是播放用Flash製作的swf動畫的必需的插件。
AIR是一個Runtime,就像JVM。有了它,用AS3開發的軟件就可以跨平臺運行。
下面是MXML代碼,實現了簡單的攝像頭畫面顯示,開始直播和停止直播。
根據不同的用戶,直播流的名字肯定是不一樣的,這個是從HTML頁面傳參數給SWF。
當開始直播後,SWF會調用外部方法,去更新直播房間的狀態。
<?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"
xmlns:mx="library://ns.adobe.com/flex/mx" creationComplete="init()" >
<fx:Script>
<![CDATA[
import mx.controls.Alert;
import mx.core.FlexGlobals;
private var nc:NetConnection;
private var cam:Camera ;
private var mic:Microphone;
private var ns:NetStream;
//private var room_id:String = FlexGlobals.topLevelApplication.parameters.roomid;
/***
*** 初始化的方法,默認顯示攝像頭實時圖像,不發佈和錄像
* ***/
private function init():void
{
cam = Camera.getCamera();
var vi:Video = new Video();
vi.width = 313;
vi.height = 194;
vi.attachCamera(cam);
videoDisplay.addChild(vi);
}
/***
*** 發佈直播並錄製的方法
***/
private function playClick():void
{
playButton.enabled = false;
stopButton.enabled = true;
recordCheck.enabled = false;
clickHandler("1");
//之前有連接,先關閉
try{
if(nc.connected){
nc.close();
}
}catch(e:Error){}
nc = new NetConnection();
nc.addEventListener(NetStatusEvent.NET_STATUS,connectHandler);
nc.client = this;
// Red5的RTMP協議地址需要自己修改
nc.connect("rtmp://192.168.3.100:1936/oflaDemo");
}
private function connectHandler(evt:NetStatusEvent):void{
trace(evt.info.code);
//由於flash的異步機制,連接成功後才能做處理,否則NetStream會因爲conn沒能連接報參數錯誤#2126
var params:Object = FlexGlobals.topLevelApplication.parameters;
var player:String = params.player;
if (evt.info.code == 'NetConnection.Connect.Success') {
ns = new NetStream(nc);
cam = Camera.getCamera();
if(cam == null)
{
Alert.show("沒有發現攝像頭","提示")
}
mic = Microphone.getMicrophone();
setupCameraMic();
ns.attachAudio(mic);
ns.attachCamera(cam);
var vi:Video = new Video();
vi.width = videoDisplay.width;
vi.height = videoDisplay.height;
vi.attachCamera(cam);
videoDisplay.addChild(vi);
// live 直播不錄製, record 邊直播邊錄製
ns.publish(player, recordCheck.selected?"record":"live");
}
else
{
Alert.show("直播服務器連接已斷開","提示")
}
}
/***
***設置攝像頭和麥克風的參數
***/
private function setupCameraMic():void
{
cam = Camera.getCamera();
cam.setMode(320, 240, 30);
cam.setQuality(0,70);
mic = Microphone.getMicrophone();
mic.rate = 11;
mic.setSilenceLevel(0);
}
/***
***停止直播
***/
private function stopClick():void
{
clickHandler("0");
nc.close();
ns.close();
playButton.enabled = true;
stopButton.enabled = false;
recordCheck.enabled = true;
}
public function onBWCheck(...arg):void
{
//do nothing
}
public function onBWDone(...arg):void
{
//do nothing
}
//更新直播狀態
public function clickHandler(status:String):void
{
if(FlexGlobals.topLevelApplication.parameters.roomid==""){
Alert.show("用戶名或密碼不能爲空","提示");return;
}
statusService.request.roomId=FlexGlobals.topLevelApplication.parameters.roomid;
statusService.request.status=status;
statusService.send();
}
]]>
</fx:Script>
<fx:Declarations>
<s:HTTPService id="statusService" url="/mediamanager/play/mediaPlay/updateStatus" method="get">
<mx:request xmlns="">
</mx:request>
</s:HTTPService>
</fx:Declarations>
<s:Panel x="66" y="29" title="發佈直播" width="534" height="319">
<s:VideoDisplay x="6" y="10" width="370" height="262" id="videoDisplay" />
<s:Button label="開始" id="playButton" click="playClick();" x="384" y="24"/>
<s:Button label="停止" x="384" y="68" id="stopButton" click="stopClick();"/>
<s:CheckBox x="469" y="25" label="錄製" width="53" id="recordCheck"/>
</s:Panel>
</s:Application>
選擇錄製的話,會在Red5服務器的streams目錄下生成“直播流名.flv”文件。用FlashBuilder編譯以後,在bin-dug目錄下會生成一些js和swf文件,一起拷出來,在HTML中調用:
把參數fileName和roomId傳入flashvars,swf就可以獲取到外部參數了。
<script type="text/javascript" src="<c:url value="/scripts/play/server/swfobject.js"/>"></script>
<script type="text/javascript">
//<!-- For version detection, set to min. required Flash Player version, or 0 (or 0.0.0), for no version detection. -->
var swfVersionStr = "11.1.0";
// To use express install, set to playerProductInstall.swf, otherwise the empty string.
var xiSwfUrlStr = "<c:url value='/scripts/play/server/playerProductInstall.swf'/>";
var flashvars ={};
flashvars.player=$("#fileName").val();
flashvars.roomid=$("#roomId").val();
var params = {};
//params.quality = "high";
//params.bgcolor = "#ffffff";
//params.allowscriptaccess = "sameDomain";
//params.allowfullscreen = "true";
var attributes = {};
attributes.id = "server";
attributes.name = "server";
attributes.align = "middle";
swfobject.embedSWF(
"<c:url value='/scripts/play/server/server.swf'/>", "flashContent",
"100%", "100%",
swfVersionStr, xiSwfUrlStr,
flashvars, params, attributes);
//<!-- JavaScript enabled so display the flashContent div in case it is not replaced with a swf object. -->
swfobject.createCSS("#flashContent", "display:block;text-align:left;");
</script>
直播,更新房間狀態的功能就此完成。
想在PC上看直播的話,我用的是jwplayer,代碼如下:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ include file="../jsp/include/taglibs.jsp"%>
<html>
<head>
<title>JWPlayer</title>
<script type="text/javascript" src="./scripts/jwplayer/jwplayer.js"></script>
</head>
<body>
<div id="player">
<script type="text/javascript">
jwplayer("player").setup({
flashplayer: "./scripts/jwplayer/player.swf",
image:"preview.jpg",
file: "stream1477358653196",
streamer: "rtmp://192.168.3.100:1936/oflaDemo"
});
</script>
</div>
</body>
</html>
上面的file:"stream1477358653196"是直播流的名字,如果是播放資源的話,改成file:"video.mp4"即可。4. 如果在手機觀看直播的話,直播端推薦用Adobe自己的直播軟件Adobe Flash Media Live Encoder 3.2。
前面說過,RTMP是Adobe的私有協議,而ios和安卓手機都不支持Adobe,也就無法使用RTMP進行直播了。
現在主流做法是將RTMP用ffmpeg推流成爲HLS,在手機端觀看。HLS只需要H5的video標籤就能播放。
FFMPEG命令如下:
ffmpeg -i "rtmp://192.168.8.79:1937/red5/play live=1" -strict -2 -c:v libx264 -c:a aac -f hls "D:\apache-tomcat-8.0.9\webapps\resourcemanager\streams\room\hls\hls.m3u8"
注意:①推正在直播的流的話,rtmp這段裏面,"live=1"必須加,且必須用雙引號,與rtmp地址包含在一起。
②中間是視頻音頻的參數設置
③最後"D:\apache-tomcat-8.0.9\webapps\resourcemanager\streams\room\hls\hls.m3u8"是hls協議的視頻文件。
它會產生1個m3u8文件(ts文件列表),和N個ts文件(視頻片段)
有人會問,既然可以用ffmpeg推流成爲hls,那爲什麼還需要直播軟件,直接pc端用swf直播,手機端不就可以看了嗎?
請看下圖ffmpeg轉碼時的參數:
這個是用Adobe Flash Media Live Encoder 3.2
這個是用swf直播的時候:
注意音頻和視頻Stream格式的區別。
用直播軟件轉碼後,視頻格式是vp6f,音頻格式是mp3。而FlashBuilder並不支持這樣的格式,它生成的是flv1和nellymoser。
其結果是,不轉碼,用swf播放帶聲音的視頻時,會放1秒就卡死。不帶聲音的話OK,但是直播怎麼能沒聲音呢?
我嘗試了一下Red5的官方Demo直播,用手機觀看也是同樣的卡死。
我覺得肯定是音頻格式有問題,所以嘗試了下面的方法,但仍然不行:
① 修改音頻的輸入格式:mic.codec = SoundCodec.SPEEX
FlashBuilder只支持nellymoser和speex兩種格式。
② 將視頻和音頻分兩個流,然後用ffmpeg合併,視頻和音頻會出現延遲
ffmpeg.exe -i "rtmp://192.168.8.79:1936/vod/video live=1" -i "rtmp://192.168.8.79:1936/vod/audio live=1" -strict -2 -c:v libx264 -c:a aac -f hls D:\apache-tomcat-8.0.9\webapps\mediamanager\streams\room\sun\sun.m3u8
③ 在手機上放兩個播放器,一個播放音頻,一個播放視頻,還是卡死。這個就比較奇怪了。難道nellymoser和speex在手機上只能單獨播放嗎?
如果有人可以在FlashBuilder裏面將音頻轉成mp3格式輸入應該就沒問題了,可是我不知道怎麼轉mp3呀
估計單用FlashBuilder是做不到直接轉mp3的,需要C或者C++。
其實直播軟件也就是將視頻音頻轉碼的作用。
參考了一下其他直播網站,他們似乎也推薦使用直播軟件進行直播,那算了,我也別費工夫了,大家都用直播軟件吧。
雖然最終沒能解決音頻轉碼的問題,但是在研究過程中對於ffmpeg的使用,以及視頻音頻的知識有了更加深入的瞭解。
系統中還整合了shiro,cas,以及手機端使用的APICloud,等以後有時間再繼續分享吧。