FFmpeg解码RTSP流数据使用HTML+JSMPEG播放

↘锁芯ラ 提交于 2019-12-14 07:57:38

先说思路

网上基本都是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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!