1.rtsp視頻流網頁播放概述
需求:當我們通過ONVIF協議,獲取到了攝像頭的rtsp流地址(長這樣:rtsp://admin:[email protected]:554/cam/realmonitor?channel=1&subtype=1&unicast=true&proto=Onvif)后,通過vlc播放器,我們可以查看監控視頻內容,可是,我們應該如何在網頁上查看視頻內容呢?因為現在的瀏覽器都不支持rtsp流(詳見:https://blog.csdn.net/SY__CSDN/article/details/129255690?utm_medium=distribute.pc_relevant.none-task-blog-2defaultbaidujs_baidulandingword~default-0-129255690-blog-113454774.pc_relevant_landingrelevant&spm=1001.2101.3001.4242.1&utm_relevant_index=3),因此我所選用的解決方案便是推流 + 轉碼
(1)轉碼推流工具ffmpeg(安裝教程詳見:https://www.cnblogs.com/h2285409/p/16982120.html),安裝好之后,便可使用命令 ffmpeg -re -rtsp_transport tcp -i rtsp://admin:[email protected]:554/cam/realmonitor?channel=1&subtype=1&unicast=true&proto=Onvif -c:v copy -c:a copy -f flv rtmp://127.0.0.1/live/16 將我們的rtsp視頻流轉碼并推至流媒體服務器上,在這個命令中含有兩個URL,前面的是我們的rtsp流地址,而后面的URL便是我們流媒體服務器的地址,以及一個-f引數,指定了我們視頻流轉碼后的格式為flv
(2)流媒體服務器,主要調研了2款,一是整合了Rtmp模塊的Nginx,二是SRS視頻服務器,而我所選用的是SRS(官方檔案:http://ossrs.net/lts/zh-cn/),在使用ffmpeg推流上SRS后,便可直接從SRS獲得HTTP-FLV視頻流地址(如本例:http://127.0.0.1/live/16.flv ),然后,前端通過flv.js組件庫便可直接在頁面上播放該視頻流
SRS與ffmpeg參考:https://blog.csdn.net/diyangxia/article/details/120172920
ffmpeg進階參考:https://segmentfault.com/a/1190000039782685
flv.js參考:http://www.kaotop.com/it/446261.html
2.rtsp推流轉碼相關代碼實作
//ffmpeg安裝路徑
@Value("${ffmpegPath}")
private String ffmpegPathPrefix;
//srs視頻服務器地址
@Value("${srsAddress}")
private String srsAddress;
//srs埠,默認為8080
@Value("${srsPort}")
private String srsPort;
//srs-http-api埠,默認為1985
@Value("${srsHttpApiPort}")
private String srsHttpApiPort;
@Resource
private MonitorMapper monitorMapper;
@Resource
private RedisTemplate redisTemplate;
@Resource
private RestTemplate restTemplate;
@Resource
private ThreadPoolTaskExecutor threadPoolTaskExecutor;
private ConcurrentHashMap<String, TranscodeModel> id2transcodeModelMap = new ConcurrentHashMap<>();
/**
* 進行推流轉碼
* @param id ipc的主鍵id
* @return 轉碼推流后的http-flv地址,前端可通過flv.js直接播放
*/
public String transcodeAndPushStream(String id) {
Ipc ipc = monitorMapper.getIpcInfoById(id);
try {
//先給這個流加鎖,防止其他用戶請求該流資訊
while(!redisTemplate.opsForValue().setIfAbsent(id, 1, Duration.ofSeconds(60))) {
Thread.sleep(200);
}
//避免重復對某一個流的推流作業
if(!id2transcodeModelMap.containsKey(id)) {
String command = String.format("%sffmpeg -re -rtsp_transport tcp -i %s -c:v copy -c:a copy -f flv %s",this.ffmpegPathPrefix, ipc.getRtspUrl(), "rtmp://" + this.srsAddress + "/live/" + id);
//通過命令列執行推流轉碼
System.out.println("啟動推流轉碼, 其命令為: " + command);
Process process = Runtime.getRuntime().exec(command);
//可選,開啟異步執行緒,觀察推流行程所列印的日志
Future<Void> processOutputHandler = threadPoolTaskExecutor.submit(() -> {
BufferedReader br = new BufferedReader(new InputStreamReader(process.getErrorStream()));
String msg = null;
try {
while ((msg = br.readLine()) != null) {
if (Thread.currentThread().isInterrupted()) {
System.out.println("關閉推流行程的日志輸出執行緒: " + Thread.currentThread().getName());
break;
}
if (msg.contains("fail") || msg.contains("miss")) {
System.err.println(ipc.getId() + " 在推流程序中發生故障或丟包: " + msg);
}
}
} catch (IOException e) {
System.err.println(ipc.getId() + " 在推流轉碼程序中發生例外錯誤,原因: " + e.getMessage());
} finally {
if (Thread.currentThread().isAlive()) {
Thread.currentThread().interrupt();
}
}
return null;
});
id2transcodeModelMap.put(id, new TranscodeModel(id, command, process, processOutputHandler));
}
} catch (Exception e) {
this.closeTranscode(id);
throw new RuntimeException("啟動對 " + id + " 的推流轉碼失敗,原因: " + e.getMessage());
} finally {
redisTemplate.delete(id);
}
//回傳轉碼后的flv流地址
return "http://" + this.srsAddress + ":" + this.srsPort + "/live/" + id + ".flv";
}
/**
* 關閉推流行程
*/
private void closeTranscode(String id) {
TranscodeModel transcodeModel = null;
if((transcodeModel = id2transcodeModelMap.get(id)) != null) {
Future<Void> outputHandler = transcodeModel.getOutputHandler();
//關閉輸出執行緒
if(outputHandler != null && !outputHandler.isDone()) {
outputHandler.cancel(true);
}
//停止推流轉碼行程
if (transcodeModel.getProcess() != null) {
transcodeModel.getProcess().destroy();
}
id2transcodeModelMap.remove(id);
System.out.println("關閉對 " + id + " 的推流轉碼");
}
}
/**
* 客戶端結束播放流后,srs可配置觸發一個on_stop回呼,通過該回呼,我們就可以知道哪些流可能沒人看了,然后結束對該流進行的推流轉碼作業
* @param data srs觸發回呼時所攜帶的引數
*/
public void stopPlay(CallbackOnStopPlay data) {
String clientId = data.getClient_id();
JSONObject srsClient = this.requestSrsClientById(clientId);
if(!srsClient.isEmpty()) {
String streamId = srsClient.getString("stream");
if (!StringUtils.hasText(streamId)) {
System.err.println("獲取client " + clientId +" 的流失敗, 未關聯流");
return;
}
//在請求這個流的資訊之前,先給這個流加鎖,防止其他用戶預覽該流
try {
while(!redisTemplate.opsForValue().setIfAbsent(data.getStream(), 1, Duration.ofSeconds(60))) {
Thread.sleep(200);
}
JSONObject vidiconStream = this.requestSrsStreamById(streamId);
if(!vidiconStream.isEmpty()) {
Integer clients = vidiconStream.getInteger("clients");
//當前觀看該流的人數 <= 2時,說明沒人看了可以停止推流,至于為什么是2,可以自己觀察列印日志看看
if(clients <= 2) {
this.closeTranscode(vidiconStream.getString("name"));
}
}
} catch (Exception e) {
System.err.println("關閉視頻流 " + streamId + " 失敗, 原因: " + e.getMessage());
} finally {
redisTemplate.delete(data.getStream());
}
}
}
/**
* 根據clientId獲取某個client資訊
*/
private JSONObject requestSrsClientById(String clientId) {
if(!StringUtils.hasText(clientId)) {
return new JSONObject();
}
String url = "http://" + this.srsAddress + ":" + this.srsHttpApiPort + "/api/v1/clients/" + clientId;
ResponseEntity<JSONObject> exchange = null;
try {
exchange = restTemplate.exchange(url, HttpMethod.GET, new HttpEntity<>(null, new HttpHeaders()), JSONObject.class);
} catch (Exception e) {
System.err.println("請求srs的client " + clientId + " 失敗,原因: " + e.getMessage());
return new JSONObject();
}
if (exchange == null || exchange.getBody() == null || exchange.getBody().getInteger("code") != 0) {
System.err.println("請求srs中client " + clientId + " 失敗");
return new JSONObject();
}
System.out.println("請求到client " + clientId + " 的資訊為: " + exchange.getBody());
return exchange.getBody().getJSONObject("client");
}
/**
* 根據流的id獲取某個流
*/
private JSONObject requestSrsStreamById(String streamId) {
if(!StringUtils.hasText(streamId)) {
return new JSONObject();
}
String url = "http://" + this.srsAddress + ":" + this.srsHttpApiPort + "/api/v1/streams/" + streamId;
ResponseEntity<JSONObject> exchange = null;
try {
exchange = restTemplate.exchange(url, HttpMethod.GET, new HttpEntity<>(null, new HttpHeaders()), JSONObject.class);
} catch (Exception e) {
System.err.println("請求srs中的流 " + streamId + " 失敗,原因: " + e.getMessage());
return new JSONObject();
}
if (exchange == null || exchange.getBody() == null || exchange.getBody().getInteger("code") != 0) {
System.err.println("請求srs中的流 " + streamId + " 失敗, 由于服務器重啟或其他原因,該流已失效");
return new JSONObject();
}
System.out.println("請求到流 " + streamId + " 的資訊為: " + exchange.getBody());
return exchange.getBody().getJSONObject("stream");
}
public class TranscodeModel {
private String id;
private String command;
private Process process;
//推流程序中的輸出執行緒
private Future<Void> outputHandler;
}
//客戶端關閉流時觸發的回呼所傳遞的引數
public class CallbackOnStopPlay {
private String server_id;
private String action;
private String client_id;
private String ip;
private String vhost;
private String app;
private String stream;
private String param;
}
//ipc類
public class Ipc {
//ipc的主鍵id
private String id;
//ipc的rtsp流地址
private String rtspUrl;
}
對推流行程的關閉,可以選擇定時任務輪詢srs中流的資訊,然后對那些沒人看的流進行關閉,也可以選擇配置srs客戶端關閉流時的回呼,來進行關閉,至于回呼如何配置使用,可以詳看官方檔案中開放介面相關內容和這篇文章:https://blog.csdn.net/weixin_44341110/article/details/120829847
3.通過海康,大華NVR來接入IPC
未完待續...
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/547609.html
標籤:Java
