您的位置:首页 > 理论基础 > 计算机网络

Red5+SpringMVC整合(RTMP+HTTP)搭建你的直播服务器

2017-04-10 11:22 302 查看

基本环境

Eclipse

Eclipse Java EE IDE for Web Developers. 

Version: Neon.3 Release (4.6.3)

Build id: 20170314-15
4000
00

地址:https://www.eclipse.org/downloads/download.php?file=/technology/epp/downloads/release/neon/3/eclipse-jee-neon-3-win32-x86_64.zip

RED5 Server

我这里用的是 Red5 Server 1.0.9
地址:https://github.com/Red5/red5-server/releases

解压server包,得到server目录



此时我们可以双击red5.bat,看看是否可以运行,如果失败,通常问题是提示jvm版本问题。

我这里用的是jdk1.8 64bit

java version "1.8.0_91"

Java(TM) SE Runtime Environment (build 1.8.0_91-b15)

Java HotSpot(TM) 64-Bit Server VM (build 25.91-b15, mixed mode)

RED5-Eclipse-Plugin

地址:https://github.com/Red5/red5-eclipse-plugin

插件的安装方法就不赘述了

插件有一个问题就是在安装后,创建项目新建server的时候会要求指向server目录,其中自动匹配red5.sh,这里是sh,我们是win平台

sh肯定是运行不了的。手动改成bat会无法进行下一步!我这个IDE是这样的或许你没事呢


我们改一下他的插件

1. 导入插件到eclipse
2. 选择 org.leagueplanet.server.glassfish 项目
3. 打开red5.serverdef
4. 搜.sh
5. 把red5-debug.sh red5.-shutdown.sh 改为 .bat 结尾即可

这样下来,在配置server路径的时候我们把 .sh 改为 .bat 就不会有错误提示,也不会无法点下一步了!

开始搭建

项目创建

创建一个Dynamic Web Project 项目
Project name: liveOnline
target runtime 选择 new runtime
Infrared5 下选择 red5 server, next



red5 Runtime 配置

选择jdk1.8 ,把red5目录指向,我们解压的red5 server文件夹



配置red5 server,端口我选的默认,这里看红色框中默认是.sh  我们改为 bat后也依然可以next




回到创建project页面我们继续进行配置,自定义修改项目配置



勾选red5 application generation 
    
  


点击完成项目创建

看项目列表,我们不仅得到了red5的项目结构,还得到了附赠的client测试端

.


测试RED5 server

我们先去server标签中启动red5服务,先跑一个空服务看看red5 server是否可以正确启动

启动如果报错,说明路径有问题

启动成功后,访问 http://127.0.0.1:5080
下面是red5 启动成功的欢迎界面,如果没有这个界面说明red5 启动报错,仔细查查吧,通常是路径配置问题,如果提示不是有效的win32程序,则是路径配置中没有修改.sh .bat 的主程序指向。

 


red5-web.properties

在eclipse 中我们打开 liveOnline中 red5-web.properties 
webapp.virtualHosts属性表示了访问控制,默认red5给加了一个 192的ip,如果你的内网IP和它不同你可以修改或者直接改为 * (星号)

red5-web.xml

<bean id="web.handler" class="org.red5.core.Application" /> 可以看到这里配置了application.java 来得到red5的各种事件状态,当然这个org.red5.core.application
 已经被自动创建了,我们可以自己修改。web.xml 没什么好说的,暂时不去改它。

基本通讯

下面我们先尝试一下这个 liveOnline 能否完成基本的rtmp通讯

我们将项目add到 red5 server,然后右键 publish

然后debug或start启动

打开http://127.0.0.1:5080/demos/publisher.html ,这是red5 提供的一个测试flex 可以完成推流拉流操作。



我们在location一行中,输入我们的项目名,再点击Connect,观察右侧console

提示
- Connecting to rtmp://localhost/liveOnline

- NetConnection.Connect.Success

证明已经成功通讯
下面我们可以切换到Video标签选择自己的摄像头,然后点start
之后修改name为9800(这个不过是一个rtmp的通讯key ,key key对应则建立推拉的都是一个流),之后选择发布,则已经开始直播了,
直播地址就是 rtmp://{ip}//liveOnlive  ,key 就是输入的9800,当然有的地方叫做 filename
我们可以选择view来观看自己的直播,切换到view界面在name中改为9800,然后点击play即可

关于如何关闭red5 server

从eclipse server标签中关闭要stop好多次才可以成功,取个巧的办法是从任务管理器中删除java.exe 进程

完善red5项目

我们先建立一下文件目录在liveOnline
修改red5-web.properties 中webapp.virtualHosts 为 * 
修改red5-web.xml 中 <bean id="web.handler" class="com.service.Application" /> 

package com.state;
/**
* 临时容器
* @author Allen 2017年3月31日
*
*/

import java.util.HashMap;

import com.state.room.RoomVo;

public class Ram {

public static HashMap<String, RoomVo> roomHm = new HashMap<>();

}
package com.state.user;

public class UserVo implements java.io.Serializable {
/**
*
*/
private static final long serialVersionUID = -6628674875994109212L;
private String red5Id;
private String red5Name;
private Long red5CreateTime;

public String getRed5Id() {
return red5Id;
}

public void setRed5Id(String red5Id) {
this.red5Id = red5Id;
}

public String getRed5Name() {
return red5Name;
}

public void setRed5Name(String red5Name) {
this.red5Name = red5Name;
}

public Long getRed5CreateTime() {
return red5CreateTime;
}

public void setRed5CreateTime(Long red5CreateTime) {
this.red5CreateTime = red5CreateTime;
}

}
package com.state.user;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Iterator;
import java.util.List;

import com.state.Ram;

/**
* 用户操作
*
* @author Allen 2017年3月31日
*
*/
public class UserState {
/**
* 插入用户到房间中
* @param red5Id
* @param red5Name
* @param red5CreateTime
* @param roomKey
* @return
*/
public boolean insert(String red5Id, String red5Name, Long red5CreateTime, String roomKey) {
try {
if (Ram.roomHm.containsKey(roomKey)) {
UserVo uvo = new UserVo();
uvo.setRed5Id(red5Id);
uvo.setRed5Name(red5Name);
uvo.setRed5CreateTime(red5CreateTime);
Ram.roomHm.get(roomKey).getUserList().add(uvo);
return true;
}
selectAll(roomKey);
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
/**
* 获取房间中全部用户
* @param roomKey
* @return
*/
public List<UserVo> selectAll(String roomKey) {

try {
if (Ram.roomHm.containsKey(roomKey)) {
Iterator<UserVo> it = Ram.roomHm.get(roomKey).getUserList().iterator();
System.out.println("================================================");
while (it.hasNext()) {
UserVo temp = it.next();
System.out.println(temp.getRed5Id() + "," + temp.getRed5Name() + ","
+ new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(new Date(temp.getRed5CreateTime())));
}
System.out.println("================================================");
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 删除房间中某用户
* @param redId
* @param roomKey
* @return
*/
public boolean delete(String redId, String roomKey) {

try {
if (Ram.roomHm.containsKey(roomKey)) {
Iterator<UserVo> it = Ram.roomHm.get(roomKey).getUserList().iterator();
while (it.hasNext()) {
if (it.next().getRed5Id().equals(redId))
{
it.remove();
break;
}
}
}
return true;

} catch (Exception e) {
e.printStackTrace();
}
return false;
}
/**
* 统计房间中用户数
* @param roomKey
* @return
*/
public int count(String roomKey) {
return Ram.roomHm.containsKey(roomKey) ? Ram.roomHm.get(roomKey).getUserList().size() : 0;
}

}
package com.state.room;

import java.util.ArrayList;
import java.util.List;

import com.state.user.UserVo;

/**
* 房间VO
*
* @author Allen 2017年3月31日
*
*/
public class RoomVo {

private String roomKey;// 房间key
private String roomName;// 房间名
private List<UserVo> userList=new ArrayList<>();// 房间内用户列表

public List<UserVo> getUserList() {
return userList;
}

public void setUserList(List<UserVo> userList) {
this.userList = userList;
}

public String getRoomKey() {
return roomKey;
}

public void setRoomKey(String roomKey) {
this.roomKey = roomKey;
}

public String getRoomName() {
return roomName;
}

public void setRoomName(String roomName) {
this.roomName = roomName;
}

}
package com.state.room;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map.Entry;

import com.state.Ram;

/**
* 房间操作
*
* @author Allen 2017年3月31日
*
*/
public class RoomState {

/**
* 创建一个房间信息
*
* @param roomKey
* @param roomName
* @return
*/
public boolean insert(String roomKey, String roomName) {

try {
if (!Ram.roomHm.containsKey(roomKey)) {
RoomVo rVo = new RoomVo();
rVo.setRoomName(roomName);
Ram.roomHm.put(roomKey, rVo);
return true;
}
} catch (Exception e) {
e.printStackTrace();
}
return false;
}

/**
* 返回所有房间信息
*
* @return
*/
public List<RoomVo> selectAll() {

try {
List<RoomVo> resultList = new ArrayList<>();
Iterator<Entry<String, RoomVo>> it = Ram.roomHm.entrySet().iterator();
while (it.hasNext()) {
Entry<String, RoomVo> entry = it.next();
resultList.add(entry.getValue());
}
return resultList;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}

/**
* 删除一个房间信息
*
* @param red5Id
* @return
*/
public boolean delete(String roomKey) {
try {
Ram.roomHm.remove(roomKey);
return true;
} catch (Exception e) {
e.printStackTrace();
}

return false;
}

/**
* 统计房间总数
*
* @return
*/
public int count() {
return Ram.roomHm.size();
}

}
package com.service;

import java.text.SimpleDateFormat;
import java.util.Date;

import org.red5.server.adapter.MultiThreadedApplicationAdapter;
import org.red5.server.api.IClient;
import org.red5.server.api.IConnection;
import org.red5.server.api.scope.IScope;
import org.red5.server.api.stream.IBroadcastStream;
import org.red5.server.api.stream.ISubscriberStream;

import com.state.room.RoomState;
import com.state.user.UserState;
/**
*
* @author Allen 2017年4月7日
*
*/
public class Application extends MultiThreadedApplicationAdapter {

@Override
public boolean connect(IConnection conn) {
System.out.println("connect");
return super.connect(conn);
}

@Override
public void disconnect(IConnection arg0, IScope arg1) {
System.out.println("disconnect");
new UserState().delete(arg0.getSessionId(), arg0.getAttribute(arg0.getSessionId()).toString());
super.disconnect(arg0, arg1);
}
/**
* 开始发布直播
*/
@Override
public void streamPublishStart(IBroadcastStream stream) {
System.out.println("[streamPublishStart]********** ");
System.out.println("发布Key: " + stream.getPublishedName());
RoomState room = new RoomState();
room.insert(stream.getPublishedName(), "房间" + stream.getPublishedName());
System.out.println(
"发布时间:" + new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(new Date(stream.getCreationTime())));
System.out.println("****************************** ");
}

/**
* 流结束
*/
@Override
public void streamBroadcastClose(IBroadcastStream arg0) {
RoomState room = new RoomState();
room.delete(arg0.getPublishedName());
super.streamBroadcastClose(arg0);
}

/**
* 用户断开播放
*/
@Override
public void streamSubscriberClose(ISubscriberStream arg0) {
new UserState().delete(arg0.getConnection().getSessionId(), arg0.getBroadcastStreamPublishName());
super.streamSubscriberClose(arg0);
}

/**
* 链接rtmp服务器
*/
@Override
public boolean appConnect(IConnection arg0, Object[] arg1) {
// TODO Auto-generated method stub
System.out.println("[appConnect]********** ");
System.out.println("请求域:" + arg0.getScope().getContextPath());
System.out.println("id:" + arg0.getClient().getId());
System.out.println("name:" + arg0.getClient().getId());
System.out.println("**************** ");
return super.appConnect(arg0, arg1);
}

/**
* 加入了rtmp服务器
*/
@Override
public boolean join(IClient arg0, IScope arg1) {
// TODO Auto-generated method stub
return super.join(arg0, arg1);
}

/**
* 开始播放流
*/
@Override
public void streamSubscriberStart(ISubscriberStream stream) {
S
bfbd
ystem.out.println("[streamSubscriberStart]********** ");
System.out.println("播放域:" + stream.getScope().getContextPath());
System.out.println("播放Key:" + stream.getBroadcastStreamPublishName());
System.out.println("********************************* ");
String sessionId = stream.getConnection().getSessionId();
stream.getConnection().setAttribute(null, null);
new UserState().insert(sessionId, sessionId, stream.getCreationTime(), stream.getBroadcastStreamPublishName());
super.streamSubscriberStart(stream);
}

/**
* 离开了rtmp服务器
*/
@Override
public void leave(IClient arg0, IScope arg1) {
System.out.println("leave");
super.leave(arg0, arg1);
}

}


重新发布启动后,如果新的application中的console没有打印,则可能是新版本发布失败。
我们删除  red5 server/ webapp / liveOnline 和  red5 server/ webapp /webapp  / liveOnline  文件夹后再次在red5 server 中右键清理和发布

服务启动后我们再次使用 http://127.0.0.1:5080/demos/publisher.html 选择链接rtmp liveOnline服务

在这一步 eclipse console 如果提示 Scope not found 那就是web.handler 中class路径配错了

正确的话不仅会返回succss,而且eclipse console 会打印我们新建的application中的 System.out.print 信息

[appConnect]**********
请求域:/liveOnline
id:1
name:1
****************

SpringMVC

下面我们在red5项目中增加SpringMVC支持,来提供通过HTTP访问red5服务器内房间信息
注意一下:Spring jar包我们放到WEB-INF/libs 中,然后引用它,这里如果使用默认的lib目录,最后发布red5+springMVC的时候red5的rtmp会无法访问,会抛出Scope not found!!!!虽然这个问题在google百度github都没有解释,但是我最后还是找到的解决方案!如果你用lib没有问题,那么只能说是系统环境了。。我用的是win7 64bit。linux或者mac或许没问题吧

首先我们把WEB-INF 下面的lib改成libs,如果没有lib新建一个libs即可

我们将spring的jar包拷贝到libs,为什么选择4.3.6因为我的red5 server下lib中的spring就是4.3.6的保持版本与red5一致!



Add JARS到项目

web.xml

我们在web.xml中增加
<!-- ********************** Spring配置 ********************** -->
<!-- 配置DispatchcerServlet -->
<servlet>
<servlet-name>springDispatcherServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<!-- 配置Spring mvc下的配置文件的位置和名称 -->
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/springmvc.xml</param-value>
</init-param>
<load-on-startup>2</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>springDispatcherServlet</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>
<!-- 静态资源处理交给默认servlet -->
<servlet-mapping>
<servlet-name>default</servlet-name>
<url-pattern>*.css</url-pattern>
</servlet-mapping>

<servlet-mapping>
<servlet-name>default</servlet-name>
<url-pattern>*.gif</url-pattern>
</servlet-mapping>

<servlet-mapping>
<servlet-name>default</servlet-name>
<url-pattern>*.jpg</url-pattern>
</servlet-mapping>

<servlet-mapping>
<servlet-name>default</servlet-name>
<url-pattern>*.js</url-pattern>
</servlet-mapping>

<servlet-mapping>
<servlet-name>default</servlet-name>
<url-pattern>*.html</url-pattern>
</servlet-mapping>

springmvc.xml

新建一个springmvc.xml在WEB-INF目录下
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd"> <!-- 配置@ResponseBody 保证返回值为UTF-8 -->
<!-- 因为StringHttpMessageConverter默认是ISO8859-1 -->
<bean id="utf8Charset" class="java.nio.charset.Charset"
factory-method="forName">
<constructor-arg value="UTF-8" />
</bean>
<mvc:annotation-driven>
<mvc:message-converters>
<bean class="org.springframework.http.converter.StringHttpMessageConverter">
<constructor-arg ref="utf8Charset" />
</bean>
</mvc:message-converters>
</mvc:annotation-driven>
<!-- 配置自动扫描的包 -->
<context:component-scan base-package="com.action"></context:component-scan>
<!-- 配置视图解析器 如何把handler 方法返回值解析为实际的物理视图 -->
<bean
class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/views/"></property>
<property name="suffix" value=".jsp"></property>
</bean>
</beans>


测试类

package com.action.room;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import com.state.Ram;

/**
*
* @author Allen 2017年4月10日
*
*/
@Controller
public class room {

@ResponseBody
@RequestMapping("/roomsize")
public String hello() {
return "当前房间数: "+Ram.roomHm.size() ;
}

}


测试

 重新发布重新启动red5 server

访问:http://127.0.0.1:5080/liveOnline/roomsize
返回结果



再访问http://127.0.0.1:5080/demos/publisher.html
按照上面描述的方法,开启一个rtmp直播在liveOnline



我们再刷新roomsize发现房间数显示为1,然后我们关闭直播再刷新发现房间数返回为0。

结束

就到这里了red5提供rtmp服务,spring提供http服务,rtmp不仅可以传输stream还可以传输String,我们也可以用rtmp来获取项目中的房间数量,不过我认为还是通过http来管理这些请求比较好一些~

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: