基本环境
Eclipse
Eclipse Java EE IDE for Web Developers.
Version: Neon.3 Release (4.6.3)
Build id: 20170314-1500
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)
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
- 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。
结束
原文地址:https://blog.youkuaiyun.com/crazyzxljing0621/article/details/69568339
如果用ffpeg去解析生成的flv文件和用手机端直播的可以参考 : https://blog.youkuaiyun.com/qq_33730348/article/details/79931703
swfobject参数详解:https://blog.youkuaiyun.com/qq_33730348/article/details/79931773