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

基本环境

Eclipse

Eclipse Java EE IDE for Web Developers. 
Version: Neon.3 Release (4.6.3)
Build id: 20170314-1500

地址: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) {
		System.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来管理这些请求比较好一些~


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章