最新要闻

广告

手机

iphone11大小尺寸是多少?苹果iPhone11和iPhone13的区别是什么?

iphone11大小尺寸是多少?苹果iPhone11和iPhone13的区别是什么?

警方通报辅警执法直播中被撞飞:犯罪嫌疑人已投案

警方通报辅警执法直播中被撞飞:犯罪嫌疑人已投案

家电

全球微动态丨记录监控摄像头的接入过程及web端播放

来源:博客园

1.rtsp视频流网页播放概述

需求:当我们通过ONVIF协议,获取到了摄像头的rtsp流地址(长这样:rtsp://admin:123456789@192.168.9.16: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:123456789@192.168.9.16: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;@Resourceprivate MonitorMapper monitorMapper;@Resourceprivate RedisTemplate redisTemplate;@Resourceprivate RestTemplate restTemplate;@Resourceprivate ThreadPoolTaskExecutor threadPoolTaskExecutor;private ConcurrentHashMap 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 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 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 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 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 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

未完待续...

关键词: