RTMP協議實現視頻直播流實戰
相關的核心代碼我上傳會上傳到github,以下文字可以理清實現思路,
git地址:https://github.com/blackMilk123/workspace/tree/master/rtmp
准備工作
- 一個開通了RTMP協議的流地址,螢石雲之類的監控提供商都有的,格式為rtmp://rtmp01open.ys7.com/openlive/xxxxxxxxx.hd
- 向螢石雲申請開發者資質之后會得到一個appKey,appSecret,accessToken,既然能搜到這篇文章相信你肯定不會缺少這些東西。
數據庫結構
以下第一個表結構基本上是固定的,存放你開通的那個螢石雲帳號的key 密鑰 等等的信息,第二個表是根據你具體的業務來設計的,跟你的設備關聯,一般你有一個監控設備就會有對應的信息,紅框圈起來是一些比較重要的參數,這些都可以在螢石雲中文檔中看到
表一:video_appinfo 基本上都是一些固定參數,結構基本上可以不動
表二:video_deviceinfo表,圈起來的是一些比較重要的參數,都是由相應的視頻監控設備提供的
實體類等
根據以上兩張表的設計創建相應的實體類,因為字段太多,並且表二根據實際業務的不同,加上實體類代碼放上來也沒有什么意義,所以這里就不再貼代碼了,可以自行創建,以及service層,dao層的基礎映射文件也是一樣的,簡單的可以自己創建一下
創建SDK客戶端
SDK客戶端主要是用於模擬各種Http請求,帶着申請的密鑰,去向螢石雲的授權服務器獲取視頻信息等等,
public class SdkClient {
/** 獲取accessToken */
private static final String API_TOKEN_GET_URL = "https://open.ys7.com/api/lapp/token/get";
/** 添加設備 */
private static final String API_DEVICE_ADD_URL = "https://open.ys7.com/api/lapp/device/add";
/** 查詢賬號下流量消耗匯總 */
private static final String API_TRAFFIC_TOTAL = "https://open.ys7.com/api/lapp/traffic/user/total";
/** 獲取設備列表 */
private static final String API_DEVICE_LIST = "https://open.ys7.com/api/lapp/device/list";
/** 獲取攝像頭列表 */
private static final String API_CAMERA_LIST = "https://open.ys7.com/api/lapp/camera/list";
/** 添加設備 */
private static final String API_DEVICE_ADD = "https://open.ys7.com/api/lapp/device/add";
/** 獲取單個設備信息 */
private static final String API_DEVICE_INFO = "https://open.ys7.com/api/lapp/device/info";
/** 開始雲台控制 */
private static final String API_DEVICE_PTZ_START = "https://open.ys7.com/api/lapp/device/ptz/start";
/** 停止雲台控制 */
private static final String API_DEVICE_PTZ_STOP = "https://open.ys7.com/api/lapp/device/ptz/stop";
/** 關閉設備視頻加密 */
private static final String API_DEVICE_ENCRYPT_OFF = "https://open.ys7.com/api/lapp/device/encrypt/off";
/** 開啟設備視頻加密 */
private static final String API_DEVICE_ENCRYPT_ON = "https://open.ys7.com/api/lapp/device/encrypt/on";
/** 開通直播功能 */
private static final String API_LIVE_VIDEO_OPEN = "https://open.ys7.com/api/lapp/live/video/open";
/** 關閉直播功能 */
private static final String API_LIVE_VIDEO_CLOSE = "https://open.ys7.com/api/lapp/live/video/close";
/** 獲取指定有效期的直播地址 */
private static final String API_LIVE_ADDRESS_LIMITED = "https://open.ys7.com/api/lapp/live/address/limited";
/** 獲取直播地址*/
private static final String API_LIVE_ADDRESS_GET = "https://open.ys7.com/api/lapp/live/address/get";
/**
* app 標識
*/
private String appKey;
public String getAppKey() {
return appKey;
}
public void setAppKey(String appKey) {
this.appKey = appKey;
}
/**
* app 密鑰
*/
private String appSecret;
public String getAppSecret() {
return appSecret;
}
public void setAppSecret(String appSecret) {
this.appSecret = appSecret;
}
/**
* token 憑據
*/
private String accessToken;
public String getAccessToken() {
return accessToken;
}
public void setAccessToken(String accessToken) {
this.accessToken = accessToken;
}
/**
* token 有效時間
*/
private Date tokenExpire;
public Date getTokenExpire() {
return tokenExpire;
}
public void setTokenExpire(Date tokenExpire) {
this.tokenExpire = tokenExpire;
}
public SdkToken getToken() {
return getToken(false);
}
/**
* 獲取 accessToken
* @param force 是否強制獲取
* @return
*/
public SdkToken getToken(boolean force) {
if(!force && this.getAccessToken() != null) {
//准備緩存 accessToken
SdkToken existsSdkToken = new SdkToken("200", "使用 accessToken 緩存", this.getAccessToken(), this.getTokenExpire() != null ? String.valueOf(this.getTokenExpire().getTime() ): "0");
//判斷 token是否過期
if(this.getTokenExpire() != null && this.getTokenExpire().getTime() < new Date().getTime()) {
return existsSdkToken;
}
//調用接口,驗證 accessToken有效性
SdkReturn<SdkMap> sdkReturn = this.trafficTotal();
if(sdkReturn.isSuccess()) {
return existsSdkToken;
}
}
Map<String, Object> paramMap = new HashMap<>();
paramMap.put("appKey", this.appKey);
paramMap.put("appSecret", this.appSecret);
SdkToken sdkToken = getMap(API_TOKEN_GET_URL, paramMap, SdkToken.class);
if(sdkToken.isSuccess()) {
this.setAccessToken(sdkToken.getAccessToken());
this.setTokenExpire(new Date(sdkToken.getExpireTime()));
}
return sdkToken;
}
/**
* 添加設備
* @param deviceSerial 序列號
* @param validateCode 驗證碼
* @return
*/
public SdkReturnMap deviceAdd(String deviceSerial, String validateCode) {
Map<String, Object> paramMap = new HashMap<>();
paramMap.put("accessToken", this.getAccessToken());
paramMap.put("deviceSerial", deviceSerial);
paramMap.put("validateCode", validateCode);
SdkReturnMap returnMap = getMap(API_DEVICE_ADD, paramMap);
//添加容錯
if(!returnMap.isSuccess()) {
if(returnMap.getCode().equals("20017")) {
returnMap.setCode("200");
}else if(returnMap.getCode().equals("20002")) {
returnMap.setMsg(returnMap.getMsg() + ",設備未注冊至螢石雲");
}
}
return returnMap;
}
/**
* 獲取單個設備信息
* @param deviceSerial
* @return
*/
public SdkReturnMap deviceInfo(String deviceSerial) {
Map<String, Object> paramMap = new HashMap<>();
paramMap.put("accessToken", this.getAccessToken());
paramMap.put("deviceSerial", deviceSerial);
return getMap(API_DEVICE_INFO, paramMap);
}
/**
* 開啟設備視頻加密
* @param deviceSerial 設備序列號
* @return
*/
public SdkReturnMap deviceEncryptOn(String deviceSerial) {
Map<String, Object> paramMap = new HashMap<>();
paramMap.put("accessToken", this.getAccessToken());
paramMap.put("deviceSerial", deviceSerial);
return getMap(API_DEVICE_ENCRYPT_ON, paramMap);
}
/**
* 開通直播功能
* @param channelNo 通道號
* @param deviceSerials 設備序列號集合
* @return
*/
public SdkReturnMapList liveVideoOpen(int channelNo, String... deviceSerials) {
if(deviceSerials == null || deviceSerials.length == 0) {
return new SdkReturnMapList();
}
String source = "";
for(int i = 0; i < deviceSerials.length; i++) {
if(i > 0) {
source += ",";
}
source += (deviceSerials[i] + ":" + channelNo);
}
Map<String, Object> paramMap = new HashMap<>();
paramMap.put("accessToken", this.getAccessToken());
paramMap.put("source", source);
return getMapList(API_LIVE_VIDEO_OPEN, paramMap);
}
/**
* 獲取直播地址
* @param channelNo 通道號 默認 1
* @param deviceSerials 設備序列號集合
* @return
*/
public SdkReturnMapList liveVideoGetAddress(int channelNo, String... deviceSerials) {
if(deviceSerials == null || deviceSerials.length == 0) {
return new SdkReturnMapList();
}
String source = "";
for(int i = 0; i < deviceSerials.length; i++) {
if(i > 0) {
source += ",";
}
source += (deviceSerials[i] + ":" + channelNo);
}
Map<String, Object> paramMap = new HashMap<>();
paramMap.put("accessToken", this.getAccessToken());
paramMap.put("source", source);
return getMapList(API_LIVE_ADDRESS_GET, paramMap);
}
/**
* 獲取指定有效期的直播地址
* @param deviceSerial 設備序列號
* @param channelNo 通道號
* @param expireTime 地址過期時間:單位秒數,最大默認62208000(即720天),最小默認300(即5分鍾)。
* @return
*/
public SdkReturnMap liveVideoLimitedAddress(String deviceSerial, int channelNo, Integer expireTime) {
Map<String, Object> paramMap = new HashMap<>();
paramMap.put("accessToken", this.getAccessToken());
paramMap.put("deviceSerial", deviceSerial);
paramMap.put("channelNo", channelNo);
if(expireTime != null) {
paramMap.put("expireTime", expireTime);
}
return getMap(API_LIVE_ADDRESS_LIMITED, paramMap);
}
/**
* 開始雲台控制
* @param deviceSerial deviceSerial 設備序列號
* @param channelNo channelNo 通道號
* @param direction direction 操作命令:0-上,1-下,2-左,3-右,4-左上,5-左下,6-右上,7-右下,8-放大,9-縮小,10-近焦距,11-遠焦距
* @return
*/
public SdkReturnMap startDevicePtz(String deviceSerial, int channelNo, int direction) {
return this.startDevicePtz(deviceSerial, channelNo, direction, 0);
}
/**
* 開始雲台控制
* @param deviceSerial 設備序列號
* @param channelNo 通道號
* @param direction 操作命令:0-上,1-下,2-左,3-右,4-左上,5-左下,6-右上,7-右下,8-放大,9-縮小,10-近焦距,11-遠焦距
* @param speed 雲台速度:0-慢,1-適中,2-快
* @return
*/
public SdkReturnMap startDevicePtz(String deviceSerial, int channelNo, int direction, int speed) {
Map<String, Object> paramMap = new HashMap<>();
paramMap.put("accessToken", this.getAccessToken());
paramMap.put("deviceSerial", deviceSerial);
paramMap.put("channelNo", channelNo);
paramMap.put("direction", String.valueOf(direction));
paramMap.put("speed", String.valueOf(speed));
SdkReturnMap sdkReturn = getMap(API_DEVICE_PTZ_START, paramMap);
return sdkReturn;
}
/**
* 停止雲台控制
* @param deviceSerial 設備序列號
* @param channelNo 通道號
* @param direction 操作命令:0-上,1-下,2-左,3-右,4-左上,5-左下,6-右上,7-右下,8-放大,9-縮小,10-近焦距,11-遠焦距
* @return
*/
public SdkReturnMap stopDevicePtz(String deviceSerial, int channelNo, int direction) {
Map<String, Object> paramMap = new HashMap<>();
paramMap.put("accessToken", this.getAccessToken());
paramMap.put("deviceSerial", deviceSerial);
paramMap.put("channelNo", channelNo);
paramMap.put("direction", String.valueOf(direction));
SdkReturnMap sdkReturn = getMap(API_DEVICE_PTZ_STOP, paramMap);
return sdkReturn;
}
/*
public static void main(String[] args) {
SdkClient sdkClient = new SdkClient();
sdkClient.setAppKey("e82e92e47b1542d1a7ba2003199f5cf2");
sdkClient.setAppSecret("58ddf728e5e58c321bb18c3e7c8d0aa8");
sdkClient.setAccessToken("at.3h35yneydi4i9tz75alqgoe90jq8c9yx-6t55llnnkg-17ys1z5-6lttvhphk-");
sdkClient.getToken();
}
*/
/**
* 獲取 map
* @param url
* @param paramMap
* @return
*/
private SdkReturnMap getMap(String url, Map<String, Object> paramMap) {
return getMap(url, paramMap, SdkReturnMap.class);
}
/**
* 獲取 map
* @param url
* @param paramMap
* @param classz
* @return
*/
private <T extends SdkReturn<SdkMap>> T getMap(String url, Map<String, Object> paramMap, Class<T> classz) {
System.out.println(url);
try {
String result = HttpUtil.post(url, paramMap);
System.out.println(result);
T sdkReturn = JSONObject.parseObject(result, classz);
return sdkReturn;
} catch (Exception e) {
T sdkReturn = null;
try {
sdkReturn = classz.newInstance();
sdkReturn.setCode("-1");
sdkReturn.setMsg(e.getMessage());
} catch (Exception ex) {
ex.printStackTrace();
}
return sdkReturn;
}
}
/**
* 獲取 map 集合
* @param url
* @param paramMap
* @return
*/
private SdkReturnMapList getMapList(String url, Map<String, Object> paramMap) {
return getMapList(url, paramMap, SdkReturnMapList.class);
}
/**
* 獲取 map 集合
* @param url
* @param paramMap
* @param classz
* @return
*/
private <T extends SdkReturn<SdkMapList>> T getMapList(String url, Map<String, Object> paramMap, Class<T> classz) {
System.out.println(url);
try {
String result = HttpUtil.post(url, paramMap);
System.out.println(result);
T sdkReturn = JSONObject.parseObject(result, classz);
return sdkReturn;
} catch (Exception e) {
T sdkReturn = null;
try {
sdkReturn = classz.newInstance();
sdkReturn.setCode("-1");
sdkReturn.setMsg(e.getMessage());
} catch (Exception ex) {
ex.printStackTrace();
}
return sdkReturn;
}
}
}
service 層更新token
- VideoAppInfoService
- 這里的代碼主要用於更新設備的過期時間以及accessToken刷新,在直播播放之前需要先更新一下設備信息等等。
@Service("videoAppInfo")
public class VideoAppInfoServiceImpl extends com.sunrise.common.BaseServiceImpl<VideoAppInfo> implements VideoAppInfoService {
@Autowired
private VideoAppInfoMapper videoAppInfoMapper;
public VideoAppInfoServiceImpl() {
}
@Override
public com.sunrise.common.BaseMapper<VideoAppInfo> getMapper() {
return this.videoAppInfoMapper;
}
@Override
public Return<VideoAppInfo> updateVideoAppInfoToken(Long appId) {
//查詢視頻監控應用信息
VideoAppInfo videoAppInfo = this.selectByPrimaryKey(appId);
if(videoAppInfo == null) {
return new Return<>(false, "獲取視頻監控app信息失敗!");
}
//accessToken刷新
SdkClient sdkClient = new SdkClient();
sdkClient.setAppKey(videoAppInfo.getAppKey());
sdkClient.setAppSecret(videoAppInfo.getAppSecret());
sdkClient.setAccessToken(videoAppInfo.getAccessToken());
sdkClient.setTokenExpire(videoAppInfo.getTokenExpire());
SdkToken sdkToken = sdkClient.getToken();
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Calendar calendar = Calendar.getInstance();
//設置過期時間為當前時間+7天
calendar.add(Calendar.DATE,7);
String format = dateFormat.format(calendar.getTime());
//更新過期時間
videoAppInfo.setAccessToken(sdkToken.getAccessToken());
videoAppInfo.setTokenExpire(calendar.getTime());
videoAppInfo.setRemark(sdkToken.getMsg());
this.updateSelective(videoAppInfo);
if(!sdkToken.isSuccess()) {
videoAppInfo.setRemark(sdkToken.getMsg());
this.updateSelective(videoAppInfo);
return new Return<>(false, sdkToken.getMsg());
}
//如果accessToken一致,則數據不變
if(videoAppInfo.getAccessToken() != null && videoAppInfo.getAccessToken().equalsIgnoreCase(sdkToken.getAccessToken())) {
return new Return<VideoAppInfo>(true, "accessToken 無變化").setData(videoAppInfo);
}
//更新 accessToken
videoAppInfo.setAccessToken(sdkToken.getAccessToken());
videoAppInfo.setTokenExpire(new Date(sdkToken.getExpireTime()));
videoAppInfo.setRemark(sdkToken.getMsg());
this.updateSelective(videoAppInfo);
//返回結果
return new Return<VideoAppInfo>(true, "已更新 accessToken").setData(videoAppInfo);
}
}
Controller層:返回播放地址給前端
/**
* 跳轉水質視頻監控頁面
* @return
*/
@RequestMapping("/toAppVideoList")
public String toAppVideoList(HttpServletRequest request,Long id){
//更新視頻應用信息
Long appId = videoDeviceInfo != null ? videoDeviceInfo.getAppId() : 1L;
Return<VideoAppInfo> retApp = videoAppInfoService.updateVideoAppInfoToken(appId);
request.setAttribute("accessToken", retApp.isSuccess() ? retApp.getData().getAccessToken() : null);
//通過主鍵查詢視頻設備信息
VideoDeviceInfo videoDeviceInfo = this.videoDeviceInfoService.selectByPrimaryKey(id);
//傳到前端
request.setAttribute("videoDevice",deviceInfos);
return "szapp/appWaterVideoList";
}
前端播放
添加前端播放插件:
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<html lang="en">
<head>
<!doctype html>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<link rel="stylesheet" href="${staticPath }/css/screen/swiper.min.css"/>
</head>
<body>
<div>
//初始化一個視頻播放框
<div class="media-box">
<div class="media" id="player" style="width: 100%;height:40%;">
</div>
</div>
</div>
<script src="${staticPath }/js/lib/jquery/jquery-3.3.1.min.js"></script>
<script type="text/javascript" src="${staticPath}/js/sewise-player/sewise.player.min.js"></script>
<script>
//進頁面時加載
$(function () {
hospital04();
});
function hospital04() {
var title = "${videoDeviceInfo3.videoName != null ? videoDeviceInfo3.videoName : videoDeviceInfo3.deviceName}";
var videoUrl = "${videoDeviceInfo3.videoUrl}";
var poster = '${staticPath}/images/index/bg.jpg';
if (videoUrl.indexOf('rtmp://') == 0) {
SewisePlayer.setup({
server: "live",
type: "rtmp",
autostart: "true",
streamurl: videoUrl,
skin: "",
title: title,
claritybutton: "disable",//[可選]是否顯示多碼率按鈕 "enable"、"disable",缺省默認值為:"enable"
timedisplay: "disable",//[可選]是否顯示播放控制欄上的時間 "enable"、"disable",缺省默認值為:"enable"
controlbardisplay: "disable",//[可選]是否顯示播放控制欄 "enable"、"disable",缺省默認值為:"enable"
topbardisplay: "disable",//[可選]是否顯示頂部標題 "enable"、"disable",缺省默認值為:"enable"
draggable: false,
buffer: 0,
primary: "html5",
lang: "zh_CN",
poster: poster,//[可選]視頻播放前顯示的圖像
logo: " ",//[可選]播放器角落logo
}, "player");
} else if (videoUrl.indexOf('.m3u8') > -1) {
SewisePlayer.setup({
server: "vod",
type: "m3u8",
autostart: "true",
videourl: videoUrl,
skin: "vodFlowPlayer",
title: "",
claritybutton: "disable",
lang: "zh_CN",
logo: "",
}, "player");
} else if (videoUrl.indexOf('.flv') > -1) {
SewisePlayer.setup({
server: "vod",
type: "flv",
autostart: "true",
videourl: videoUrl,
skin: "vodWhite",
title: "",
claritybutton: "disable",
lang: "zh_CN",
logo: "",
}, "player");
}
}
</script>
</body>
</html>
效果
這樣一個視頻直播就做好了 效果圖