忙了好几天,最近也算把Red5服务器服务端基本搞定了。
为了熟悉服务端代码,以及了解API,我仿照着FMS的模式做了一个多人聊天室。基本实现了视频、群聊、私聊几个基本功能。看到网上似乎还没有人放出这类的源代码,我索性就先当回螃蟹吧!
我们先来看代码:
[color=red]服务端:[/color]
Application.java
[align=left]package org.jerry.videochat;
import java.util.*;
import org.red5.server.adapter.ApplicationAdapter;
import org.red5.server.api.IConnection;
import org.red5.server.api.IScope;
import org.red5.server.api.service.IServiceCapableConnection;
import org.red5.server.api.so.ISharedObject;
public class Application extends ApplicationAdapter {
// 属性
private IScope appScope;
private String username;
private ISharedObject listSO;
private ISharedObject msgSO;
private Map<String, IConnection> onLineClient = new HashMap<String, IConnection>();
// 方法
// 此应用开始运行时触发的方法
public boolean appStart(IScope app) {
if (!super.appStart(app)) {
return false;
}
appScope = app;
return true;
}
// 客户端连接的时候触发的方法
public boolean appConnect(IConnection conn, Object[] params) {
username = (String) params[0];
// 登入时将连接ID和连接信息形成对应关系并存入在线列表
String link_id = conn.getClient().getId();
onLineClient.put(username, conn);
// 为用户列表共享对象添加属性
// 创建用户列表共享对象
listSO = getSharedObject(appScope, "listSO", false);
// 创建用户聊天内容共享对象
msgSO = getSharedObject(appScope, "msgSO", false);
listSO.setAttribute(link_id, username);
return true;
}
// 广播消息
public void broadcastUserMsg(String msg) {
// 公聊
// 刷新共享对象属性
msgSO.setAttribute("msg", msg);
}
// 私聊信息
public void msgFromPrivate(String msg, String from, String to) {
IServiceCapableConnection fc = (IServiceCapableConnection) onLineClient
.get(from);
IServiceCapableConnection tc = (IServiceCapableConnection) onLineClient
.get(to);
fc.invoke("showMsgByPrivate", new Object[] { msg });
tc.invoke("showMsgByPrivate", new Object[] { msg });
}
// 用户断开连接的时候触发
public void appDisconnect(IConnection conn) {
String dis_link_id = conn.getClient().getId();
String user = (String) listSO.getAttribute(dis_link_id);
// 根据ID删除对应在线纪录
onLineClient.remove(user);
// 删除用户列表共享对象的对应属性
listSO.removeAttribute(dis_link_id);
}
}
[color=red]客户端代码:[/color]
videoChat.as
package
{
import fl.controls.Button;
import fl.controls.List;
import fl.controls.TextArea;
import fl.controls.TextInput;
import fl.data.DataProvider;
import fl.managers.StyleManager;
import flash.display.Sprite;
import flash.events.AsyncErrorEvent;
import flash.events.Event;
import flash.events.KeyboardEvent;
import flash.events.MouseEvent;
import flash.events.NetStatusEvent;
import flash.events.SecurityErrorEvent;
import flash.events.SyncEvent;
import flash.media.Camera;
import flash.media.Microphone;
import flash.media.Video;
import flash.net.NetConnection;
import flash.net.NetStream;
import flash.net.SharedObject;
import flash.text.TextField;
import flash.text.TextFormat;
import flash.ui.Keyboard;
/**
* ... Red5 视频聊天 ...
* @author Jerry
*/
public class videoChat extends Sprite {
//属性
private var IP:String;
private var redPath:String;
private var nc:NetConnection;
private var ns:NetStream;
private var ns2:NetStream;
private var cam:Camera;
private var mic:Microphone;
private var listSO:SharedObject;
private var msgSO:SharedObject;
private var userArr:Array;
private var sendMsg:String;
private var now:Date;
private var userIDObj:Object;
//构造函数
public function videoChat() {
_init(); //初始化
_setComponentStyle(); //设置组件样式
_startConnect(); //开始连接服务器
}
//初始化
private function _init() {
IP = "192.168.0.10";
redPath = "rtmp://" + IP + "/videoChat";
nc = new NetConnection();
from.text = "guest" + int(Math.random() * 1000);
to.text = "所有人";
now = new Date();
}
//设置组件样式
private function _setComponentStyle() {
var myTF:TextFormat = new TextFormat();
myTF.size = 12;
myTF.font = "雅黑宋体";
StyleManager.setStyle("textFormat", myTF);
}
//开始连接
private function _startConnect() {
nc.addEventListener(NetStatusEvent.NET_STATUS, _statusHandler);
nc.addEventListener(SecurityErrorEvent.SECURITY_ERROR, _securityHandler);
nc.addEventListener(AsyncErrorEvent.ASYNC_ERROR, _asyncHandler);
nc.connect(redPath, from.text);
nc.client = this;
}
//状态监听
private function _statusHandler(evt:NetStatusEvent) {
if (evt.info.code == "NetConnection.Connect.Success") {
chatCon.text += "连接成功!\n";
_scrollToEnd();
_publishVideo(); //发布自己的视频
_setListSO(); //创建用户列表共享对象
_setMsgSO(); //创建发言信息共享对象
sendBtn.addEventListener(MouseEvent.CLICK, _sendBtnByClick); //单击发送信息
stage.addEventListener(KeyboardEvent.KEY_DOWN, _sendBtnByKey); //回车发送信息
}
if (evt.info.code == "NetConnection.Connect.Failed") {
chatCon.text += "连接失败!\n";
_scrollToEnd();
}
if (evt.info.code == "NetConnection.Connect.Closed") {
chatCon.text += "连接关闭!\n";
_scrollToEnd();
}
}
//安全性监听
private function _securityHandler(evt:SecurityError) {
chatCon.text += "安全性错误!\n";
_scrollToEnd();
}
//异步错误
private function _asyncHandler(evt:AsyncErrorEvent) {
chatCon.text += "异步错误!\n";
_scrollToEnd();
}
//发布自己的视频
private function _publishVideo() {
ns = new NetStream(nc);
cam = Camera.getCamera();
mic = Microphone.getMicrophone();
liveVideo.attachCamera(cam);
ns.client = this;
ns.addEventListener(NetStatusEvent.NET_STATUS, _statusHandler);
ns.addEventListener(AsyncErrorEvent.ASYNC_ERROR, _asyncHandler);
ns.attachCamera(cam);
ns.attachAudio(mic);
ns.publish(from.text, "live");
whoseVideo.text = from.text + "的视频";
}
//创建用户列表共享对象
private function _setListSO() {
listSO = SharedObject.getRemote("listSO", nc.uri, false);
listSO.connect(nc);
listSO.addEventListener(SyncEvent.SYNC, _listSOSyncHandler);
}
//创建发言信息共享对象
private function _setMsgSO() {
msgSO = SharedObject.getRemote("msgSO", nc.uri, false);
msgSO.addEventListener(SyncEvent.SYNC, _msgSOSyncHandler);
msgSO.connect(nc);
}
//用户列表共享对象被更新之后事件
private function _listSOSyncHandler(evt:SyncEvent) {
_showUserList(); //更新用户列表
//用户列表添加鼠标事件
userList.addEventListener(MouseEvent.CLICK, _updateChatTo);
userList.addEventListener(MouseEvent.DOUBLE_CLICK, _updateVideoShow);
}
//发言信息共享对象被更新之后事件
private function _msgSOSyncHandler(evt:SyncEvent) {
//更新聊天内容
for (var i in msgSO.data) {
chatCon.htmlText += msgSO.data[i];
}
}
//更新用户列表
private function _showUserList() {
userArr = new Array();
//用户数组更新
for (var tmp in listSO.data) {
userArr.push(listSO.data[tmp]);
}
//添加DataProvider
var tmpDP:DataProvider = new DataProvider();
for (var i = 0; i < userArr.length; i++ ) {
tmpDP.addItem( { label:userArr[i] } );
}
//名称排序
tmpDP.sortOn("label");
//在用户列表顶端加一个“所有人”
tmpDP.addItemAt( { label:"所有人" }, 0);
//将数组添加到列表中显示出来
userList.dataProvider = tmpDP;
}
//更新聊天对象
private function _updateChatTo(evt:MouseEvent) {
to.text = userList.selectedItem.label;
}
//更新视频显示和视频文本显示
private function _updateVideoShow(evt:MouseEvent) {
ns2 = new NetStream(nc);
if (from.text == to.text) {
//显示我的视频
ns2.close();
liveVideo.clear();
whoseVideo.text = "我的视频";
liveVideo.attachCamera(cam);
}
else {
//显示其他人的视频
whoseVideo.text = to.text + "的视频";
ns2.client = this;
ns2.addEventListener(NetStatusEvent.NET_STATUS, _statusHandler);
ns2.addEventListener(AsyncErrorEvent.ASYNC_ERROR, _asyncHandler);
liveVideo.attachNetStream(ns2);
ns2.play(to.text);
}
}
//单击发送信息
private function _sendBtnByClick(evt:MouseEvent) {
_sendMsg();
}
//回车发送信息
private function _sendBtnByKey(evt:KeyboardEvent) {
if (evt.keyCode == Keyboard.ENTER) {
_sendMsg();
}
}
//发送信息处理方法
private function _sendMsg() {
sendMsg="<font color='#ff0000'>" + from.text + "</font>" + " 对 " + "<font color='#ff0000'>" + to.text + "</font>" + " 说 " + "(" + "<font color='#0000ff'>" + now.getHours() + ":" + (now.getMinutes() < 10?"0" + now.getMinutes():now.getMinutes()) + ":" + (now.getSeconds() < 10?"0" + now.getSeconds():now.getSeconds()) + "</font>" + ")" + ":" +"\t" + msgInput.text + "\n";
if (from.text == to.text) {
//禁止对自己发言
chatCon.text += "对不起,您不能对自己发言!";
_scrollToEnd();
}
else if(msgInput.text==""){
//发言不能为空
chatCon.text += "请在下面的文本框中输入发言内容!";
_scrollToEnd();
}
else if (to.text == "所有人") {
//调用服务端广播方法
nc.call("broadcastUserMsg", null, sendMsg);
msgInput.text = "";
}
else {
//私聊
nc.call("msgFromPrivate", null, sendMsg, from.text, to.text);
msgInput.text = "";
}
}
//将滚动条滚动到最底端
private function _scrollToEnd() {
chatCon.verticalScrollPosition = chatCon.maxVerticalScrollPosition;
}
//私聊方法(被服务端调用)
public function showMsgByPrivate(_msg:String) {
chatCon.htmlText += _msg;
_scrollToEnd();
}
}
}
[/align]
注释写得还算详细,我想大部分人应该都能看得懂吧,实在不懂得话就多看看Red5的官方API文档。
在这里需要讲一下制作中的一个小插曲。
在FMS中我的共享对象创建代码是写在服务器开启的方法中的,本以为在Red5中照此方式也可行,但是问题出来了,Red5的共享对象机制和FMS还是有一点点区别的,Red5的共享对象的机制是这样的,创建共享对象之后,有客户端连接之后断开连接的话,该共享对象会自动销毁,再次有客户端连接的时候服务器会自动生成一个客户端的共享对象(注意:这里强调是“客户端的”),而因为之前的客户端注册的事件针对的对象是被销毁的服务端共享对象,所以就发生了一个,用其他人的话说就是“unexpected problems ”。
在我的测试中,这个问题很明显的被暴露了出来。
最开始,我将创建共享对象的代码写在了服务器开始运行的方法里面,然后就发生了一个很奇怪的现象:当最开始有一个客户端连接又断开连接之后,再次登录,用户列表就只显示“所有人”(用户列表部分使用了共享对象)。而另外一种情况即时,只要保证有一个客户端始终连接着服务器就不会出现这样的问题。当时脑袋里想到的就是,很有可能服务端的SharedObject被销毁了。查了API没有找到可以检测共享对象的,所以没办法验证自己的猜测,所以只好放弃,改用Google。Google了一下,发现了一片文章,当然,还是英文的。还好,自己的英文水平还足以理解文章的内容,最终也是验证了自己的猜测。(PS:貌似使用房间创建的方法并不会出现这样的问题,因为没有做这个测试,所以也不知道什么情况,大家可以在我的代码的基础上修改一下,最好把结果也告诉我,本人也是比较懒的程序员。)
源代码在这里放出,至于打包的源代码,因为百度不提供上传,我就没办法了,不过有想要的可以留下E-mail地址,有时间我会给你发过去的。多多共享,大家才能一起进步嘛!呵呵!
还有,大家也可以到“ActionScript天地会”论坛和“Open Red5 ”论坛去下载,我在那里都发了帖子。
原文地址:http://hi.baidu.com/cosmos53076/blog/item/84089e51985f32878d543018.html
为了熟悉服务端代码,以及了解API,我仿照着FMS的模式做了一个多人聊天室。基本实现了视频、群聊、私聊几个基本功能。看到网上似乎还没有人放出这类的源代码,我索性就先当回螃蟹吧!
我们先来看代码:
[color=red]服务端:[/color]
Application.java
[align=left]package org.jerry.videochat;
import java.util.*;
import org.red5.server.adapter.ApplicationAdapter;
import org.red5.server.api.IConnection;
import org.red5.server.api.IScope;
import org.red5.server.api.service.IServiceCapableConnection;
import org.red5.server.api.so.ISharedObject;
public class Application extends ApplicationAdapter {
// 属性
private IScope appScope;
private String username;
private ISharedObject listSO;
private ISharedObject msgSO;
private Map<String, IConnection> onLineClient = new HashMap<String, IConnection>();
// 方法
// 此应用开始运行时触发的方法
public boolean appStart(IScope app) {
if (!super.appStart(app)) {
return false;
}
appScope = app;
return true;
}
// 客户端连接的时候触发的方法
public boolean appConnect(IConnection conn, Object[] params) {
username = (String) params[0];
// 登入时将连接ID和连接信息形成对应关系并存入在线列表
String link_id = conn.getClient().getId();
onLineClient.put(username, conn);
// 为用户列表共享对象添加属性
// 创建用户列表共享对象
listSO = getSharedObject(appScope, "listSO", false);
// 创建用户聊天内容共享对象
msgSO = getSharedObject(appScope, "msgSO", false);
listSO.setAttribute(link_id, username);
return true;
}
// 广播消息
public void broadcastUserMsg(String msg) {
// 公聊
// 刷新共享对象属性
msgSO.setAttribute("msg", msg);
}
// 私聊信息
public void msgFromPrivate(String msg, String from, String to) {
IServiceCapableConnection fc = (IServiceCapableConnection) onLineClient
.get(from);
IServiceCapableConnection tc = (IServiceCapableConnection) onLineClient
.get(to);
fc.invoke("showMsgByPrivate", new Object[] { msg });
tc.invoke("showMsgByPrivate", new Object[] { msg });
}
// 用户断开连接的时候触发
public void appDisconnect(IConnection conn) {
String dis_link_id = conn.getClient().getId();
String user = (String) listSO.getAttribute(dis_link_id);
// 根据ID删除对应在线纪录
onLineClient.remove(user);
// 删除用户列表共享对象的对应属性
listSO.removeAttribute(dis_link_id);
}
}
[color=red]客户端代码:[/color]
videoChat.as
package
{
import fl.controls.Button;
import fl.controls.List;
import fl.controls.TextArea;
import fl.controls.TextInput;
import fl.data.DataProvider;
import fl.managers.StyleManager;
import flash.display.Sprite;
import flash.events.AsyncErrorEvent;
import flash.events.Event;
import flash.events.KeyboardEvent;
import flash.events.MouseEvent;
import flash.events.NetStatusEvent;
import flash.events.SecurityErrorEvent;
import flash.events.SyncEvent;
import flash.media.Camera;
import flash.media.Microphone;
import flash.media.Video;
import flash.net.NetConnection;
import flash.net.NetStream;
import flash.net.SharedObject;
import flash.text.TextField;
import flash.text.TextFormat;
import flash.ui.Keyboard;
/**
* ... Red5 视频聊天 ...
* @author Jerry
*/
public class videoChat extends Sprite {
//属性
private var IP:String;
private var redPath:String;
private var nc:NetConnection;
private var ns:NetStream;
private var ns2:NetStream;
private var cam:Camera;
private var mic:Microphone;
private var listSO:SharedObject;
private var msgSO:SharedObject;
private var userArr:Array;
private var sendMsg:String;
private var now:Date;
private var userIDObj:Object;
//构造函数
public function videoChat() {
_init(); //初始化
_setComponentStyle(); //设置组件样式
_startConnect(); //开始连接服务器
}
//初始化
private function _init() {
IP = "192.168.0.10";
redPath = "rtmp://" + IP + "/videoChat";
nc = new NetConnection();
from.text = "guest" + int(Math.random() * 1000);
to.text = "所有人";
now = new Date();
}
//设置组件样式
private function _setComponentStyle() {
var myTF:TextFormat = new TextFormat();
myTF.size = 12;
myTF.font = "雅黑宋体";
StyleManager.setStyle("textFormat", myTF);
}
//开始连接
private function _startConnect() {
nc.addEventListener(NetStatusEvent.NET_STATUS, _statusHandler);
nc.addEventListener(SecurityErrorEvent.SECURITY_ERROR, _securityHandler);
nc.addEventListener(AsyncErrorEvent.ASYNC_ERROR, _asyncHandler);
nc.connect(redPath, from.text);
nc.client = this;
}
//状态监听
private function _statusHandler(evt:NetStatusEvent) {
if (evt.info.code == "NetConnection.Connect.Success") {
chatCon.text += "连接成功!\n";
_scrollToEnd();
_publishVideo(); //发布自己的视频
_setListSO(); //创建用户列表共享对象
_setMsgSO(); //创建发言信息共享对象
sendBtn.addEventListener(MouseEvent.CLICK, _sendBtnByClick); //单击发送信息
stage.addEventListener(KeyboardEvent.KEY_DOWN, _sendBtnByKey); //回车发送信息
}
if (evt.info.code == "NetConnection.Connect.Failed") {
chatCon.text += "连接失败!\n";
_scrollToEnd();
}
if (evt.info.code == "NetConnection.Connect.Closed") {
chatCon.text += "连接关闭!\n";
_scrollToEnd();
}
}
//安全性监听
private function _securityHandler(evt:SecurityError) {
chatCon.text += "安全性错误!\n";
_scrollToEnd();
}
//异步错误
private function _asyncHandler(evt:AsyncErrorEvent) {
chatCon.text += "异步错误!\n";
_scrollToEnd();
}
//发布自己的视频
private function _publishVideo() {
ns = new NetStream(nc);
cam = Camera.getCamera();
mic = Microphone.getMicrophone();
liveVideo.attachCamera(cam);
ns.client = this;
ns.addEventListener(NetStatusEvent.NET_STATUS, _statusHandler);
ns.addEventListener(AsyncErrorEvent.ASYNC_ERROR, _asyncHandler);
ns.attachCamera(cam);
ns.attachAudio(mic);
ns.publish(from.text, "live");
whoseVideo.text = from.text + "的视频";
}
//创建用户列表共享对象
private function _setListSO() {
listSO = SharedObject.getRemote("listSO", nc.uri, false);
listSO.connect(nc);
listSO.addEventListener(SyncEvent.SYNC, _listSOSyncHandler);
}
//创建发言信息共享对象
private function _setMsgSO() {
msgSO = SharedObject.getRemote("msgSO", nc.uri, false);
msgSO.addEventListener(SyncEvent.SYNC, _msgSOSyncHandler);
msgSO.connect(nc);
}
//用户列表共享对象被更新之后事件
private function _listSOSyncHandler(evt:SyncEvent) {
_showUserList(); //更新用户列表
//用户列表添加鼠标事件
userList.addEventListener(MouseEvent.CLICK, _updateChatTo);
userList.addEventListener(MouseEvent.DOUBLE_CLICK, _updateVideoShow);
}
//发言信息共享对象被更新之后事件
private function _msgSOSyncHandler(evt:SyncEvent) {
//更新聊天内容
for (var i in msgSO.data) {
chatCon.htmlText += msgSO.data[i];
}
}
//更新用户列表
private function _showUserList() {
userArr = new Array();
//用户数组更新
for (var tmp in listSO.data) {
userArr.push(listSO.data[tmp]);
}
//添加DataProvider
var tmpDP:DataProvider = new DataProvider();
for (var i = 0; i < userArr.length; i++ ) {
tmpDP.addItem( { label:userArr[i] } );
}
//名称排序
tmpDP.sortOn("label");
//在用户列表顶端加一个“所有人”
tmpDP.addItemAt( { label:"所有人" }, 0);
//将数组添加到列表中显示出来
userList.dataProvider = tmpDP;
}
//更新聊天对象
private function _updateChatTo(evt:MouseEvent) {
to.text = userList.selectedItem.label;
}
//更新视频显示和视频文本显示
private function _updateVideoShow(evt:MouseEvent) {
ns2 = new NetStream(nc);
if (from.text == to.text) {
//显示我的视频
ns2.close();
liveVideo.clear();
whoseVideo.text = "我的视频";
liveVideo.attachCamera(cam);
}
else {
//显示其他人的视频
whoseVideo.text = to.text + "的视频";
ns2.client = this;
ns2.addEventListener(NetStatusEvent.NET_STATUS, _statusHandler);
ns2.addEventListener(AsyncErrorEvent.ASYNC_ERROR, _asyncHandler);
liveVideo.attachNetStream(ns2);
ns2.play(to.text);
}
}
//单击发送信息
private function _sendBtnByClick(evt:MouseEvent) {
_sendMsg();
}
//回车发送信息
private function _sendBtnByKey(evt:KeyboardEvent) {
if (evt.keyCode == Keyboard.ENTER) {
_sendMsg();
}
}
//发送信息处理方法
private function _sendMsg() {
sendMsg="<font color='#ff0000'>" + from.text + "</font>" + " 对 " + "<font color='#ff0000'>" + to.text + "</font>" + " 说 " + "(" + "<font color='#0000ff'>" + now.getHours() + ":" + (now.getMinutes() < 10?"0" + now.getMinutes():now.getMinutes()) + ":" + (now.getSeconds() < 10?"0" + now.getSeconds():now.getSeconds()) + "</font>" + ")" + ":" +"\t" + msgInput.text + "\n";
if (from.text == to.text) {
//禁止对自己发言
chatCon.text += "对不起,您不能对自己发言!";
_scrollToEnd();
}
else if(msgInput.text==""){
//发言不能为空
chatCon.text += "请在下面的文本框中输入发言内容!";
_scrollToEnd();
}
else if (to.text == "所有人") {
//调用服务端广播方法
nc.call("broadcastUserMsg", null, sendMsg);
msgInput.text = "";
}
else {
//私聊
nc.call("msgFromPrivate", null, sendMsg, from.text, to.text);
msgInput.text = "";
}
}
//将滚动条滚动到最底端
private function _scrollToEnd() {
chatCon.verticalScrollPosition = chatCon.maxVerticalScrollPosition;
}
//私聊方法(被服务端调用)
public function showMsgByPrivate(_msg:String) {
chatCon.htmlText += _msg;
_scrollToEnd();
}
}
}
[/align]
注释写得还算详细,我想大部分人应该都能看得懂吧,实在不懂得话就多看看Red5的官方API文档。
在这里需要讲一下制作中的一个小插曲。
在FMS中我的共享对象创建代码是写在服务器开启的方法中的,本以为在Red5中照此方式也可行,但是问题出来了,Red5的共享对象机制和FMS还是有一点点区别的,Red5的共享对象的机制是这样的,创建共享对象之后,有客户端连接之后断开连接的话,该共享对象会自动销毁,再次有客户端连接的时候服务器会自动生成一个客户端的共享对象(注意:这里强调是“客户端的”),而因为之前的客户端注册的事件针对的对象是被销毁的服务端共享对象,所以就发生了一个,用其他人的话说就是“unexpected problems ”。
在我的测试中,这个问题很明显的被暴露了出来。
最开始,我将创建共享对象的代码写在了服务器开始运行的方法里面,然后就发生了一个很奇怪的现象:当最开始有一个客户端连接又断开连接之后,再次登录,用户列表就只显示“所有人”(用户列表部分使用了共享对象)。而另外一种情况即时,只要保证有一个客户端始终连接着服务器就不会出现这样的问题。当时脑袋里想到的就是,很有可能服务端的SharedObject被销毁了。查了API没有找到可以检测共享对象的,所以没办法验证自己的猜测,所以只好放弃,改用Google。Google了一下,发现了一片文章,当然,还是英文的。还好,自己的英文水平还足以理解文章的内容,最终也是验证了自己的猜测。(PS:貌似使用房间创建的方法并不会出现这样的问题,因为没有做这个测试,所以也不知道什么情况,大家可以在我的代码的基础上修改一下,最好把结果也告诉我,本人也是比较懒的程序员。)
源代码在这里放出,至于打包的源代码,因为百度不提供上传,我就没办法了,不过有想要的可以留下E-mail地址,有时间我会给你发过去的。多多共享,大家才能一起进步嘛!呵呵!
还有,大家也可以到“ActionScript天地会”论坛和“Open Red5 ”论坛去下载,我在那里都发了帖子。
原文地址:http://hi.baidu.com/cosmos53076/blog/item/84089e51985f32878d543018.html