最近在玩视频相关的,也算是一步一步的深入吧。
第一版:
用海康SDK进行历史数据下载:
https://blog.csdn.net/qq_16504067/article/details/114538622?spm=1001.2014.3001.5502
https://blog.csdn.net/qq_16504067/article/details/114577693?spm=1001.2014.3001.5502
用ffmgeg转rtsp格式为rtmp格式存储到http-flv的直播流媒体服务器,然后前端直接拉取rtmp流进行播放
https://blog.csdn.net/qq_16504067/article/details/115503882?spm=1001.2014.3001.5502
https://github.com/eguid/FFCH4J
第二版:
问了海康的技术支持说他们是PS流,所以只需要解析PS流成前端可以播放的就行,现在在网上找到了一个wfs.js(https://github.com/MarkRepo/wfs.js)可以播放裸流H264,所以我就想在PS流中解析出H264的裸流,然后给wfs.js进行播放
https://blog.csdn.net/qq_16504067/article/details/117781017?spm=1001.2014.3001.5502
https://blog.yasking.org/a/hikvision-rtp-ps-stream-parser.html
https://blog.csdn.net/weixin_44517656/article/details/108412988
看以上的参考文章和c++的这个项目https://github.com/kevinfromcn/PsToH264后自己进行了一次的demo的预览回调解析,我先通过
public static String byteToHex(final byte[] bytes) {
String strHex = "";
StringBuilder sb = new StringBuilder("");
for (int n = 0; n < bytes.length; n++) {
strHex = Integer.toHexString(bytes[n] & 0xFF);
sb.append((strHex.length() == 1) ? "0" + strHex : strHex); // 每个字节由两个字符表示,位数不够,高位补0
// sb.append(" ");
}
return sb.toString().trim();
}
将海康的PS流转成16进制进行答应查看,果然是PS流的格式
然后再按照上面的参考进行解析(因为海康是一段段给的,所以我的解析也进行了简单处理)
/**
* 开启/关闭预览
*
* @return
*/
public JSONObject startOrStopPreview() {
final JSONObject result = new JSONObject();
if (lUserID.intValue() == -1) {
logger.error("请先登陆....");
result.put("status", false);
result.put("msg", "请先登陆....");
return result;
}
// 如果预览窗口没打开,不在预览
if (bRealPlay == false) {
// 要开启预览的通道号
int iChannelNum = 1;// 通道号
m_strClientInfo = new HCNetSDK.NET_DVR_CLIENTINFO();
m_strClientInfo.lChannel = new NativeLong(iChannelNum);
// 回调预览
m_strClientInfo.hPlayWnd = null;
lPreviewHandle = hCNetSDK.NET_DVR_RealPlay_V30(lUserID, m_strClientInfo, fRealDataCallBack, null, true);
long previewSucValue = lPreviewHandle.longValue();
// 预览失败时:
if (previewSucValue == -1) {
logger.error("开启预览失败......");
result.put("status", false);
result.put("msg", "开启预览失败......");
return result;
}
// 预览成功的操作
bRealPlay = true;
result.put("status", true);
result.put("msg", "开始预览成功....");
} else {// 如果在预览,停止预览,关闭窗口
hCNetSDK.NET_DVR_StopRealPlay(lPreviewHandle);
bRealPlay = false;
result.put("status", true);
result.put("msg", "停止预览成功");
}
return result;
}
/******************************************************************************
* 内部类: FRealDataCallBack 实现预览回调数据
******************************************************************************/
class FRealDataCallBack implements HCNetSDK.FRealDataCallBack_V30 {
// 预览回调
@Override
public void invoke(final NativeLong lRealHandle, final int dwDataType, final ByteByReference pBuffer,
final int dwBufSize, final Pointer pUser) {
switch (dwDataType) {
case HCNetSDK.NET_DVR_SYSHEAD: // 系统头
case HCNetSDK.NET_DVR_STREAMDATA: // 码流数据
if (dwBufSize > 0) {
byte[] outputData = pBuffer.getPointer().getByteArray(0, dwBufSize);
try {
writeESH264(outputData);
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
byte[] allEsBytes = null;
/**
* 提取H264的裸流写入文件
*
* @param outputData
* @throws IOException
*/
public void writeESH264(final byte[] outputData) throws IOException {
if (outputData.length <= 0) {
return;
}
if ((outputData[0] & 0xff) == 0x00//
&& (outputData[1] & 0xff) == 0x00//
&& (outputData[2] & 0xff) == 0x01//
&& (outputData[3] & 0xff) == 0xBA) {// RTP包开头
try {
// 一个完整的帧解析完成后将解析的数据放入BlockingQueue,websocket获取后发生给前端
if (allEsBytes != null && allEsBytes.length > 0) {
MyBlockingQueue.bq.put(allEsBytes);
}
allEsBytes = null;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 是00 00 01 eo开头的就是视频的pes包
if ((outputData[0] & 0xff) == 0x00//
&& (outputData[1] & 0xff) == 0x00//
&& (outputData[2] & 0xff) == 0x01//
&& (outputData[3] & 0xff) == 0xE0) {//
// 去掉包头后的起始位置
int from = 9 + outputData[8] & 0xff;
int len = outputData.length - 9 - (outputData[8] & 0xff);
// 获取es裸流
byte[] esBytes = new byte[len];
System.arraycopy(outputData, from, esBytes, 0, len);
if (allEsBytes == null) {
allEsBytes = esBytes;
} else {
byte[] newEsBytes = new byte[allEsBytes.length + esBytes.length];
System.arraycopy(allEsBytes, 0, newEsBytes, 0, allEsBytes.length);
System.arraycopy(esBytes, 0, newEsBytes, allEsBytes.length, esBytes.length);
allEsBytes = newEsBytes;
}
}
}
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.concurrent.atomic.AtomicInteger;
import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;
import org.springframework.stereotype.Component;
import com.example.demo.domain.MyBlockingQueue;
import lombok.extern.slf4j.Slf4j;
/**
* 前后端交互的类实现消息的接收推送(自己发送给自己)
*
* @ServerEndpoint(value = "/wstest") 前端通过此URI和后端交互,建立连接
*/
@Slf4j
@ServerEndpoint(value = "/wstest")
@Component
public class OneWebSocket {
/** 记录当前在线连接数 */
private static AtomicInteger onlineCount = new AtomicInteger(0);
/**
* 连接建立成功调用的方法
*/
@OnOpen
public void onOpen(final Session session) {
onlineCount.incrementAndGet(); // 在线数加1
log.info("有新连接加入:{},当前在线人数为:{}", session.getId(), onlineCount.get());
while (true) {
try {
byte[] esBytes = (byte[]) MyBlockingQueue.bq.take();
ByteBuffer data = ByteBuffer.wrap(esBytes);
session.getBasicRemote().sendBinary(data);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static String byteToHex(final byte[] bytes) {
String strHex = "";
StringBuilder sb = new StringBuilder("");
for (int n = 0; n < bytes.length; n++) {
strHex = Integer.toHexString(bytes[n] & 0xFF);
sb.append((strHex.length() == 1) ? "0" + strHex : strHex); // 每个字节由两个字符表示,位数不够,高位补0
// sb.append(" ");
}
return sb.toString().trim();
}
/**
* 连接关闭调用的方法
*/
@OnClose
public void onClose(final Session session) {
onlineCount.decrementAndGet(); // 在线数减1
log.info("有一连接关闭:{},当前在线人数为:{}", session.getId(), onlineCount.get());
}
/**
* 收到客户端消息后调用的方法
*
* @param message
* 客户端发送过来的消息
*/
@OnMessage
public void onMessage(final String message, final Session session) {
log.info("服务端收到客户端[{}]的消息:{}", session.getId(), message);
}
@OnError
public void onError(final Session session, final Throwable error) {
log.error("发生错误");
error.printStackTrace();
}
/**
* 服务端发送消息给客户端
*/
private void sendMessage(final String message, final Session toSession) {
try {
log.info("服务端给客户端[{}]发送消息{}", toSession.getId(), message);
toSession.getBasicRemote().sendText(message);
} catch (Exception e) {
log.error("服务端发送消息给客户端失败:{}", e);
}
}
}
前端:
<!DOCTYPE html>
<html>
<head>
<title>h.264 To fmp4</title>
<script src="js/jquery/jquery-1.12.3.js"> </script>
<script src="js/wfs.js"></script>
<link href="js/jquery/jquery-ui.css" rel="stylesheet" type="text/css" />
<style type="text/css" media="screen">
video.rotate180 {
width: 100%;
height: 100%;
transform: rotateX(180deg);
-moz-transform: rotateX(180deg);
-webkit-transform: rotateX(180deg);
-o-transform: rotateX(180deg);
-ms-transform: rotateX(180deg);
}
</style>
</head>
<body>
<h2>h.264 To fmp4</h2>
<div class="wfsjs">
<video id="video1" muted="muted" controls="controls" style="width: 100%;height: 100%;"
autoplay="autoplay" muted></video>
<div class="ratio"></div>
</div>
<script>
window.onload = function() {
if (Wfs.isSupported()) {
var video1 = document.getElementById("video1");
var wfs = new Wfs();
wfs.attachMedia(video1, 'ch1');
}
};
</script>
</body>
</html>
注意wfs.js中要修改:
{
key: 'onMediaAttached',
value: function onMediaAttached(data) {
if (data.websocketName != undefined) {
//var client = new WebSocket( 'ws://' + window.location.host + '/' + data.websocketName );
//var uri = 'ws://' + '10.122.4.17:18080';
//var protocol = 'binary';
//var client = new WebSocket(uri, protocol);
var client = new WebSocket('ws://10.122.4.17:18080/wstest');
this.wfs.attachWebsocket(client, data.channelName);
} else {
console.log('websocketName ERROE!!!');
}
}
}
{
key: 'receiveSocketMessage',
value: function receiveSocketMessage(event) {
var buffer = new Uint8Array(event.data);
this.wfs.trigger(_events2.default.H264_DATA_PARSING, { data:buffer });
}
}