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來管理這些請求比較好一些~


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