先说思路
网上基本都是node.js+websocket-relay.js作为中继器,其实现原理就是将RTSP数据推送到websocket-relay的接口,再通过websockclient推到websocketserver,根据这个原理我做了个类似的程序,但是不完美,视频会时不时出现马赛克,一直没能找到解决办法。
webcam.html
<!DOCTYPE html>
<html>
<head>
<title>视频</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width"/>
</head>
<body>
<canvas id="video-canvas" width="480px" height="320px"></canvas>
<script type="text/javascript" src="/js/jsmpeg.js"></script>
<script type="text/javascript" src="/js/jquery-1.8.2.min.js"></script>
<script type="text/javascript">
debugger;
var url = 'http://localhost:8080/webcam/stream';
var param = {};
param.requestType = 2;
param.requestData = 'cam2';
param.size = '480x320';
param.streamType = 1
$.ajax({
type : 'get',
url : url,
dataType : 'text',
data: param,
beforeSend : function() {
},
complete : function() {
},
success : function(data, status) {
result = data;
debugger;
var canvas = document.getElementById('video-canvas');
var player = new JSMpeg.Player(data, {
canvas : canvas
});
},
error : function(err, err1, err2) {
console.log(JSON.stringify(err) + '<br/>err1:'
+ JSON.stringify(err1) + '<br/>err2:'
+ JSON.stringify(err2));
}
});
</script>
</body>
</html>
主程序
package platform;
import java.io.UnsupportedEncodingException;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import platform.util.MediaUtil;
@SpringBootApplication
public class Application {
public static void main(String[] args) throws Exception {
SpringApplication.run(Application.class, args);
sendRequest();
}
public static void sendRequest() throws UnsupportedEncodingException {
MediaUtil.sendWebSocket("rtsp://admin:12345@10.11.4.111:554/Streaming/Channels", "D:/aaa.mp1");
// MediaUtil.sendWebSocket("rtsp://admin:12345@10.11.4.111:554/Streaming/Channels", "http://192.168.93.129:8081/123456");
}
}
web端访问接口
package platform.controller;
import java.net.URI;
import java.util.Map;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import com.alibaba.fastjson.JSON;
import com.google.common.collect.Maps;
import lombok.Cleanup;
import platform.bean.RequestParam;
import platform.bean.ResultVO;
import platform.hikvision.HikvisionUtil;
import platform.properties.Path;
import platform.util.MediaUtil;
import platform.util.URLUtil;
import platform.websocket.WSClient;
@Controller
public class RelayController extends BaseController {
public static Map<String, String> aliveMap = Maps.newHashMap();
@Autowired
private Path path;
@PostMapping("/relay/push")
public void pushStream(HttpServletRequest request, String code) {
String url = path.getWs().concat("/").concat(code);
try {
WSClient wc = new WSClient(new URI(url));
wc.connect();
Thread.sleep(3 * 1000);
@Cleanup
ServletInputStream inputStream = null;
byte[] rs = new byte[25];
inputStream = request.getInputStream();
while (inputStream.read(rs) != -1) {
wc.send(rs);
}
} catch (Exception e) {
printStack(e);
}
}
@GetMapping("/webcam/stream")
@ResponseBody
public String getStream(RequestParam param) throws InterruptedException {
try {
param.setStreamType(param.getStreamType() == null ? 1 : param.getStreamType());
logger.info("type={},code={},size={},streamType={}", param.getRequestType(), param.getRequestData(), param.getSize(), param.getStreamType());
// 清除之前所有ffmpeg进程,防止冲突
ProcessWS ws = new ProcessWS(param);
ws.start();
aliveMap.put(param.getRequestData(), path.getWs().concat("/").concat(param.getRequestData()));
} catch (Exception e) {
logger.error("推流错误={}", e);
}
Thread.sleep(3 * 1000);
String url = path.getWs().concat("/").concat(param.getRequestData());
logger.info("url: {}",url);
return url;
}
class ProcessWS extends Thread {
private RequestParam param;
public ProcessWS(RequestParam param) {
this.param = param;
}
@Override
public void run() {
try {
String url = "";
switch (param.getRequestType()) {
case 0:
String result = HikvisionUtil.getCameraPreviewURL(param.getRequestData(), param.getStreamType());
ResultVO<?> r = JSON.parseObject(result, ResultVO.class);
if (0 == r.getCode() && ("success").equals(r.getMsg())) {
url = r.getData().getUrl();
}
break;
case 1:
url = URLUtil.URLD(param.getRequestData(), "UTF-8");
break;
case 2:
url = path.getCams().get(param.getRequestData());
break;
default:
break;
}
logger.info("url:{}", url);
if(StringUtils.isBlank(url)) {
return;
}
String relay = path.getRelay().concat("?code=").concat(param.getRequestData());
Boolean rs = (param.getSize() == null) ? MediaUtil.sendWebSocket(url, relay) : MediaUtil.sendWebSocket(url, relay, param.getSize());
// if (rs && MediaUtil.FFMPEG != null) {
// if (count < 3) {
// count = count + 1;
// ProcessWS ws = new ProcessWS(param);
// ws.start();
// Thread.sleep(3000);
// }
//
// }
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
websocketclient
package platform.websocket;
import java.net.URI;
import org.java_websocket.client.WebSocketClient;
import org.java_websocket.handshake.ServerHandshake;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class WSClient extends WebSocketClient {
private static final Logger LOGGER = LoggerFactory.getLogger(WebSocketClient.class);
public WSClient(URI serverUri) {
super(serverUri);
}
@Override
public void onOpen(ServerHandshake arg0) {
// TODO Auto-generated method stub
LOGGER.info("------ MyWebSocket onOpen ------");
}
@Override
public void onClose(int arg0, String arg1, boolean arg2) {
// TODO Auto-generated method stub
LOGGER.info("------ MyWebSocket onClose ------{}",arg1);
}
@Override
public void onError(Exception arg0) {
// TODO Auto-generated method stub
LOGGER.info("------ MyWebSocket onError ------{}",arg0);
}
@Override
public void onMessage(String arg0) {
// TODO Auto-generated method stub
LOGGER.info("-------- 接收到服务端数据: " + arg0 + "--------");
}
}
websocketserver
package platform.websocket;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.concurrent.CopyOnWriteArraySet;
import javax.websocket.EncodeException;
import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import lombok.Data;
@ServerEndpoint("/websocket/{sid}")
@Component
public class WebSocketServer {
Logger logger = LoggerFactory.getLogger(this.getClass());
//静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。
private static int onlineCount = 0;
//concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。
private static CopyOnWriteArraySet<WebSocketServer> webSocketSet = new CopyOnWriteArraySet<WebSocketServer>();
//与某个客户端的连接会话,需要通过它来给客户端发送数据
private Session session;
//接收sid
private String sid="";
public String getSid() {
return sid;
}
public void setSid(String sid) {
this.sid = sid;
}
/**
* 连接建立成功调用的方法*/
@OnOpen
public void onOpen(Session session, @PathParam("sid") String sid) {
this.session = session;
webSocketSet.add(this); // 加入set中
addOnlineCount(); // 在线数加1
logger.info("有新窗口开始监听:{},当前在线人数为{}", sid, getOnlineCount());
this.sid = sid;
}
/**
* 连接关闭调用的方法
*/
@OnClose
public void onClose() {
webSocketSet.remove(this); //从set中删除
subOnlineCount(); //在线数减1
logger.info("有一连接关闭:{}!当前在线人数为:{}", this.getSid(), getOnlineCount());
}
/**
* 收到客户端消息后调用的方法
*
* @param message 客户端发送过来的消息
* @throws EncodeException */
@OnMessage
public void onMessage(byte[] message, Session session) throws EncodeException {
// 群发消息
for (WebSocketServer item : webSocketSet) {
try {
item.sendMessage(message);
} catch (IOException e) {
logger.error("发送消息出现错误:{}", e);
}
}
}
/**
*
* @param session
* @param error
*/
@OnError
public void onError(Session session, Throwable error) {
logger.error("发生错误:{}", error);
}
/**
* 实现服务器主动推送
* @throws EncodeException
*/
public void sendMessage(byte message[]) throws IOException, EncodeException {
session.getBasicRemote().sendBinary(ByteBuffer.wrap(message));
// session.getBasicRemote().getSendStream().write(message);
}
public static synchronized int getOnlineCount() {
return onlineCount;
}
public static synchronized void addOnlineCount() {
WebSocketServer.onlineCount++;
}
public static synchronized void subOnlineCount() {
WebSocketServer.onlineCount--;
}
}
MediaUtil
package platform.util;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.collect.Lists;
import platform.bean.ProcessKiller;
public class MediaUtil {
Logger logger = LoggerFactory.getLogger(this.getClass());
public final static String VIDEO_TYPE[] = { "MP4", "WMV" };
public final static String FFMPEG_PATH = "D:\\App\\ffmpeg-win64\\bin\\ffmpeg.exe";
public final static String DEFAULT_SIZE = "480X320";
public static String ffplay() {
List<String> commond = Lists.newArrayList();
commond.add("-rtsp_transport");
commond.add("tcp");
commond.add("-i");
commond.add("rtsp://111.160.20.247:1554/openUrl/1NmCVyg");
executeCommand(commond);
return "f";
}
public static Boolean sendWebSocket(String address, String to){
return sendWebSocket(address, to, DEFAULT_SIZE);
}
// ffmpeg -rtsp_transport tcp -i rtsp://111.160.20.247:1554/openUrl/qcAW7VC -f mpegts -codec:v mpeg1video -b:a 64k -ac 1 -s 480x320 -r 25 http://127.0.0.1:8081/123456
public synchronized static Boolean sendWebSocket(String address, String to, String size) {
List<String> commond = Lists.newArrayList();
commond.add("-rtsp_transport");
commond.add("tcp");
commond.add("-i");
commond.add(address);
commond.add("-f");
commond.add("mpegts");
commond.add("-codec:v");
commond.add("mpeg1video");
commond.add("-b:a");
commond.add("32k");
commond.add("-ac");
commond.add("1");
commond.add("-s");
commond.add(size);
commond.add("-r");
commond.add("25");
commond.add(to);
String r = executeCommand(commond);
return r == null ? false : true;
}
public static String executeCommand(List<String> commonds) {
if (CollectionUtils.isEmpty(commonds)) {
System.out.println("--- 指令执行失败,因为要执行的FFmpeg指令为空! ---");
return null;
}
LinkedList<String> ffmpegCmds = new LinkedList<>(commonds);
ffmpegCmds.addFirst(FFMPEG_PATH); // 设置ffmpeg程序所在路径
System.out.println("--- 待执行的FFmpeg指令为:---" + ffmpegCmds);
Runtime runtime = Runtime.getRuntime();
Process ffmpeg = null;
try {
// 执行ffmpeg指令
ProcessBuilder builder = new ProcessBuilder();
builder.command(ffmpegCmds);
ffmpeg = builder.start();
System.out.println("--- 开始执行FFmpeg指令:--- 执行线程名:" + builder.toString());
if (null != ffmpeg) {
ProcessKiller ffmpegKiller = new ProcessKiller(ffmpeg);
// JVM退出时,先通过钩子关闭FFmepg进程
runtime.addShutdownHook(ffmpegKiller);
}
// 取出输出流和错误流的信息
// 注意:必须要取出ffmpeg在执行命令过程中产生的输出信息,如果不取的话当输出流信息填满jvm存储输出留信息的缓冲区时,线程就回阻塞住
PrintStream errorStream = new PrintStream(ffmpeg.getErrorStream());
PrintStream inputStream = new PrintStream(ffmpeg.getInputStream());
errorStream.start();
inputStream.start();
// 等待ffmpeg命令执行完
ffmpeg.waitFor();
// 获取执行结果字符串
String result = errorStream.stringBuffer.append(inputStream.stringBuffer).toString();
// 输出执行的命令信息
String cmdStr = Arrays.toString(ffmpegCmds.toArray()).replace(",", "");
String resultStr = StringUtils.isBlank(result) ? "【异常】" : "正常";
System.out.println("--- 已执行的FFmepg命令: ---" + cmdStr + " 已执行完毕,执行结果: " + resultStr);
return result;
} catch (Exception e) {
System.out.println("--- FFmpeg命令执行出错! --- 出错信息: " + e.getMessage());
return null;
} finally {
if (null != ffmpeg) {
ProcessKiller ffmpegKiller = new ProcessKiller(ffmpeg);
// JVM退出时,先通过钩子关闭FFmepg进程
runtime.addShutdownHook(ffmpegKiller);
}
}
}
/**
* 视频转换
*
* 注意指定视频分辨率时,宽度和高度必须同时有值;
*
* @param fileInput
* 源视频路径
* @param fileOutPut
* 转换后的视频输出路径
* @param withAudio
* 是否保留音频;true-保留,false-不保留
* @param crf
* 指定视频的质量系数(值越小,视频质量越高,体积越大;该系数取值为0-51,直接影响视频码率大小),取值参考:CrfValueEnum.code
* @param preset
* 指定视频的编码速率(速率越快压缩率越低),取值参考:PresetVauleEnum.presetValue
* @param width
* 视频宽度;为空则保持源视频宽度
* @param height
* 视频高度;为空则保持源视频高度
*/
public static void convertVideo(File fileInput, File fileOutPut, boolean withAudio, Integer crf, String preset,
Integer width, Integer height) {
if (null == fileInput || !fileInput.exists()) {
throw new RuntimeException("源视频文件不存在,请检查源视频路径");
}
if (null == fileOutPut) {
throw new RuntimeException("转换后的视频路径为空,请检查转换后的视频存放路径是否正确");
}
if (!fileOutPut.exists()) {
try {
fileOutPut.createNewFile();
} catch (IOException e) {
System.out.println("视频转换时新建输出文件失败");
}
}
String format = getFormat(fileInput);
if (!isLegalFormat(format, VIDEO_TYPE)) {
throw new RuntimeException("无法解析的视频格式:" + format);
}
List<String> commond = new ArrayList<String>();
commond.add("-i");
commond.add(fileInput.getAbsolutePath());
if (!withAudio) { // 设置是否保留音频
commond.add("-an"); // 去掉音频
}
if (null != width && width > 0 && null != height && height > 0) { // 设置分辨率
commond.add("-s");
String resolution = width.toString() + "x" + height.toString();
commond.add(resolution);
}
commond.add("-vcodec"); // 指定输出视频文件时使用的编码器
commond.add("libx264"); // 指定使用x264编码器
commond.add("-preset"); // 当使用x264时需要带上该参数
commond.add(preset); // 指定preset参数
commond.add("-crf"); // 指定输出视频质量
commond.add(crf.toString()); // 视频质量参数,值越小视频质量越高
commond.add("-y"); // 当已存在输出文件时,不提示是否覆盖
commond.add(fileOutPut.getAbsolutePath());
executeCommand(commond);
}
/**
* 检查文件类型是否是给定的类型
*
* @param inputFile
* 源文件
* @param givenFormat
* 指定的文件类型;例如:{"MP4", "AVI"}
* @return
*/
public static boolean isGivenFormat(File inputFile, String[] givenFormat) {
if (null == inputFile || !inputFile.exists()) {
System.out.println("--- 无法检查文件类型是否满足要求,因为要检查的文件不存在 --- 源文件: " + inputFile);
return false;
}
if (null == givenFormat || givenFormat.length <= 0) {
System.out.println("--- 无法检查文件类型是否满足要求,因为没有指定的文件类型 ---");
return false;
}
String fomat = getFormat(inputFile);
return isLegalFormat(fomat, givenFormat);
}
/**
* 使用FFmpeg的"-i"命令来解析视频信息
*
* @param inputFile
* 源媒体文件
* @return 解析后的结果字符串,解析失败时为空
*/
public static String getMetaInfoFromFFmpeg(File inputFile) {
if (inputFile == null || !inputFile.exists()) {
throw new RuntimeException("源媒体文件不存在,源媒体文件路径: ");
}
List<String> commond = new ArrayList<String>();
commond.add("-i");
commond.add(inputFile.getAbsolutePath());
String executeResult = MediaUtil.executeCommand(commond);
return executeResult;
}
/**
* 检测视频格式是否合法
*
* @param format
* @param formats
* @return
*/
private static boolean isLegalFormat(String format, String formats[]) {
for (String item : formats) {
if (item.equals(StringUtils.upperCase(format))) {
return true;
}
}
return false;
}
/**
* 获取指定文件的后缀名
*
* @param file
* @return
*/
private static String getFormat(File file) {
String fileName = file.getName();
String format = fileName.substring(fileName.indexOf(".") + 1);
return format;
}
/**
* 用于取出ffmpeg线程执行过程中产生的各种输出和错误流的信息
*/
static class PrintStream extends Thread {
InputStream inputStream = null;
BufferedReader bufferedReader = null;
StringBuffer stringBuffer = new StringBuffer();
public PrintStream(InputStream inputStream) {
this.inputStream = inputStream;
}
@Override
public void run() {
try {
if (null == inputStream) {
System.out.println("--- 读取输出流出错!因为当前输出流为空!---");
}
bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
String line = null;
while ((line = bufferedReader.readLine()) != null) {
// System.out.println(line);
stringBuffer.append(line);
}
} catch (Exception e) {
System.out.println("--- 读取输入流出错了!--- 错误信息:" + e.getMessage());
} finally {
try {
if (null != bufferedReader) {
bufferedReader.close();
}
if (null != inputStream) {
inputStream.close();
}
} catch (IOException e) {
System.out.println("--- 调用PrintStream读取输出流后,关闭流时出错!---");
}
}
}
}
}
以上为主要代码,完整代码请移步https://gitee.com/guo_stone/platform.git
来源:CSDN
作者:guo_stone
链接:https://blog.csdn.net/u011873646/article/details/103477541