vue+flv.js+SpringBoot+websocket實現視頻監控與回放功能

需求:

vue+springboot的項目,需要在頁面展示出海康的硬盤錄像機連接的攝像頭的實時監控畫面以及回放功能.

  • 之前項目裡是純前端實現視頻監控和回放功能.但是有局限性.就是ip地址必須固定.新的需求裡設備ip不固定.所以必須換一種思路.
  • 通過設備的主動註冊,讓設備去主動連接服務器後端通過socket推流給前端實現實時監控和回放功能;

思路:

1:初始化設備.後端項目啟動時就調用初始化方法.
2:開啟socket連接.前端頁面加載時嘗試連接socket.
3:點擊播放,調用後端推流接口.並且前端使用flv.js實現播放.

準備工作:

1:vue項目引入flv.js。
npm install –save flv.js
main.js裡面引入
import flvjs from ‘flv.js’;
Vue.use(flvjs)
但是這裡我遇見一個坑.開發模式沒有問題.但是打包之後發現ie瀏覽器報語法錯誤.不支持此引用.所以修改引用地址.
在webpack.base.conf.js的module.exports下添加

resolve: {
    extensions: ['.js', '.vue', '.json'],
    alias: {
      'vue$': 'vue/dist/vue.esm.js',
      '@': resolve('src'),
      'flvjs':'flv.js/dist/flv.js'
    }
  },

plugins下添加

  plugins: [
    new webpack.ProvidePlugin({
      flvjs:'flvjs',
      $: "jquery",
      jQuery: "jquery",
      "window.jQuery": "jquery"
    })
  ],

最後頁面引入時:

import flvjs from "flv.js/dist/flv.js";

2.準備一個硬盤錄像機,並添加一個攝像頭設備以做測試使用.
硬盤錄像機設置為主動註冊模式.並配置好ip和端口以及子設備ID

在這裡插入圖片描述

在設置裡的網絡設置裡面

在這裡插入圖片描述

3.後端搭建好websocket工具類
包含通用的OnOpen,onClose,onError等方法.

實現:

1.項目啟動開啟設備服務.這個SDKLIB裡面都有就不介紹瞭.
2.頁面加載嘗試開啟socket連接.

//嘗試連接websocket
    startSocket(channelnum, device_value) {
      try {
        let videoWin = document.getElementById(this.currentSelect);
        if (flvjs.isSupported()) {
          let websocketName =
            "/device/monitor/videoConnection/" + channelnum + device_value;
          console.log("進入連接websocket", this.ipurl + websocketName);
          const flvPlayer = flvjs.createPlayer(
            {
              type: "flv",
              //是否是實時流
              isLive: true,
              //是否有音頻
              hasAudio: false,
              url: this.ipurl + websocketName,
              enableStashBuffer: true,
            },
            {
              enableStashBuffer: false,
              stashInitialSize: 128,
            }
          );
          flvPlayer.on("error", (err) => {
            console.log("err", err);
          });
          flvjs.getFeatureList();
          flvPlayer.attachMediaElement(videoWin);
          flvPlayer.load();
          flvPlayer.play();
          return true;
        }
      } catch (error) {
        console.log("連接websocket異常", error);
        return false;
      }
    },

這裡傳的參數是通道號和設備信息.無需在意.隻要是唯一key就可以.

2.socket連接成功後.調用後端推流方法實現播放.
這裡說一下後端的推流方法.
調用SDK裡的CLIENT_RealPlayByDataType方法

/**
     * 實時預覽拉流
     *
     * @param loginHandler 登錄句柄
     * @param channel      通道號
     * @param emDataType   回調拉出的碼流類型,{@link NetSDKLib.EM_REAL_DATA_TYPE}
     */
    public long preview(long loginHandler, int channel, NetSDKLib.fRealDataCallBackEx realDataCallBackEx, fRealDataCallBackEx2 realPlayDataCallback, int emDataType, int rType, boolean saveFile, int emAudioType) {
        NetSDKLib.NET_IN_REALPLAY_BY_DATA_TYPE inParam = new NetSDKLib.NET_IN_REALPLAY_BY_DATA_TYPE();
        NetSDKLib.NET_OUT_REALPLAY_BY_DATA_TYPE outParam = new NetSDKLib.NET_OUT_REALPLAY_BY_DATA_TYPE();
        inParam.nChannelID = channel;
        inParam.rType = rType;
        if(realDataCallBackEx!=null){
            inParam.cbRealData=realDataCallBackEx;
        }
        if(realPlayDataCallback!=null){
            inParam.cbRealDataEx = realPlayDataCallback;
        }
        inParam.emDataType = emDataType;
        inParam.emAudioType=emAudioType;
        if (saveFile) {
            inParam.szSaveFileName = UUID.randomUUID().toString().replace(".", "").replace("-", "") + "." + EMRealDataType.getRealDataType(emDataType).getFileType();
        }
        NetSDKLib.LLong realPlayHandler = netsdk.CLIENT_RealPlayByDataType(new NetSDKLib.LLong(loginHandler), inParam, outParam, 3000);
        if (realPlayHandler.longValue() != 0) {
            netsdk.CLIENT_MakeKeyFrame(new NetSDKLib.LLong(loginHandler),channel,0);
            RealPlayInfo info = new RealPlayInfo(loginHandler, emDataType, channel, rType);
            realPlayHandlers.put(realPlayHandler.longValue(), info);
        } else {
            log.error("realplay failed.error is " + ENUMERROR.getErrorMessage(), this);
        }
        return realPlayHandler.longValue();
    }

註意:這裡的碼流類型選擇flv.
回調函數裡面:

// 回調建議寫成單例模式, 回調裡處理數據,需要另開線程
    @Autowired
    private WebSocketServer server;
    private Log log = Log.get(WebSocketRealDataCallback.class);

    @Override
    public void invoke(NetSDKLib.LLong lRealHandle, int dwDataType, Pointer pBuffer, int dwBufSize, int param, Pointer dwUser) {
        RealPlayInfo info = DeviceApi.realPlayHandlers.get(lRealHandle.longValue());
        if (info != null && info.getLoginHandler() != 0) {
            //過濾碼流
            byte[] buffer = pBuffer.getByteArray(0, dwBufSize);
            if (info.getEmDataType() == 0 || info.getEmDataType() == 3) {
                //選擇私有碼流或mp4碼流,拉流出的碼流都是私有碼流
                if (dwDataType == 0) {
                    log.info(dwDataType + ",length:" + buffer.length + " " + Arrays.toString(buffer), WebSocketRealDataCallback.class);
                    sendBuffer(buffer, lRealHandle.longValue());
                }
            } else if ((dwDataType - 1000) == info.getEmDataType()) {
                log.info(dwDataType + ",length: " + buffer.length + Arrays.toString(buffer), WebSocketRealDataCallback.class);
                sendBuffer(pBuffer.getByteArray(0, dwBufSize), lRealHandle.longValue());
            }
        }
    }

以及調用Websocket裡面的sendMessageToOne發送給指定客戶端

/**
     * 發送數據
     * @param bytes
     * @param realPlayHandler
     */
    private static void sendBuffer(byte[] bytes, long realPlayHandler) {
        /**
         * 發送流數據
         * 使用pBuffer.getByteBuffer(0,dwBufSize)得到的是一個指向native pointer的ByteBuffer對象,其數據存儲在native,
         * 而webSocket發送的數據需要存儲在ByteBuffer的成員變量hb,使用pBuffer的getByteBuffer得到的ByteBuffer其hb為null
         * 所以,需要先得到pBuffer的字節數組,手動創建一個ByteBuffer
         */
        ByteBuffer buffer = ByteBuffer.wrap(bytes);
        server.sendMessageToOne(realPlayHandler, buffer);
    }

這裡傳的參數是設備初始化的時候得到的登錄句柄.以及流數據.

/**
     * 發送binary消息給指定客戶端
     *
     * @param realPlayHandler 預覽句柄
     * @param buffer          碼流數據
     */
    public void sendMessageToOne(long realPlayHandler, ByteBuffer buffer) {
        //登錄句柄無效
        if (realPlayHandler == 0) {
            log.error("loginHandler is invalid.please check.", this);
            return;
        }
        RealPlayInfo realPlayInfo = AutoRegisterEventModule.findRealPlayInfo(realPlayHandler);
        if(realPlayInfo == null){
            //連接已斷開
        }
        String key = realPlayInfo.getChannel()+realPlayInfo.getSbbh();
        Session session = sessions.get(key);
        if (session != null) {
            synchronized (session) {
                try {
                    session.getBasicRemote().sendBinary(buffer);
                    byte[] bytes=new byte[buffer.limit()];
                    buffer.get(bytes);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        } else {

            //log.error("session is null.please check.", this);
        }
    }

這樣就實現瞭視頻監控.

效果:

在這裡插入圖片描述

分享一下websocket代碼:

package com.dahuatech.netsdk.webpreview.websocket;
import cn.hutool.log.Log;
import cn.hutool.log.LogFactory;
import org.springframework.stereotype.Component;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
/**
 * @description websocket實現類
 */
@ServerEndpoint("/websocket/{realPlayHandler}")
@Component
public class WebSocketServer {
    private static Log log = LogFactory.get(WebSocketServer.class);
    private FileOutputStream outputStream;
    /**
     * 靜態變量,用來記錄當前在線連接數。應該把它設計成線程安全
     */
    private final AtomicInteger onlineCount = new AtomicInteger(0);
    /**
     * 存放每個客戶端對應的WebSocket對象,根據設備realPlayHandler建立session
     */
    public static ConcurrentHashMap<Long, Session> sessions = new ConcurrentHashMap<>();
    /**
     * 存放客戶端的對象
     *//*
    public static CopyOnWriteArrayList<Session> sessionList=new CopyOnWriteArrayList<>();*/
    /**
     * 有websocket client連接
     *
     * @param realPlayHandler 預覽句柄
     * @param session
     */
    @OnOpen
    public void OnOpen(@PathParam("realPlayHandler") long realPlayHandler, Session session) {
        if (sessions.containsKey(realPlayHandler)) {
            sessions.put(realPlayHandler, session);
        } else {
            sessions.put(realPlayHandler, session);
            addOnlineCount();
        }
        log.info("websocket connect.session: " + session);
    }
    /**
     * 連接關閉調用的方法
     *
     * @param realPlayHandler 預覽句柄
     * @param session         websocket連接對象
     */
    @OnClose
    public void onClose(@PathParam("realPlayHandler") Long realPlayHandler, Session session) {
        if (sessions.containsKey(realPlayHandler)) {
            sessions.remove(realPlayHandler);
            subOnlineCount();
        }
    }
    /**
     * 發生錯誤
     *
     * @param throwable e
     */
    @OnError
    public void onError(Throwable throwable) {
        throwable.printStackTrace();
    }
    /**
     * 收到客戶端發來消息
     *
     * @param message 消息對象
     */
    @OnMessage
    public void onMessage(ByteBuffer message) {
        log.info("服務端收到客戶端發來的消息: {}", message);
    }
    /**
     * 收到客戶端發來消息
     *
     * @param message 字符串類型消息
     */
    @OnMessage
    public void onMessage(String message) {
        log.info("服務端收到客戶端發來的消息: {}", message);
    }
    /**
     * 發送消息
     *
     * @param message 字符串類型的消息
     */
    public void sendAll(String message) {
        for (Map.Entry<Long, Session> session : sessions.entrySet()) {
            session.getValue().getAsyncRemote().sendText(message);
        }
    }
    /**
     * 發送binary消息
     *
     * @param buffer
     */
    public void sendMessage(ByteBuffer buffer) {
        for (Map.Entry<Long, Session> session : sessions.entrySet()) {
            session.getValue().getAsyncRemote().sendBinary(buffer);
        }
    }
    /**
     * 發送binary消息給指定客戶端
     *
     * @param realPlayHandler 預覽句柄
     * @param buffer          碼流數據
     */
    public void sendMessageToOne(long realPlayHandler, ByteBuffer buffer) {
        //登錄句柄無效
        if (realPlayHandler == 0) {
            log.error("loginHandler is invalid.please check.", this);
            return;
        }
        Session session = sessions.get(realPlayHandler);
        if (session != null) {
            synchronized (session) {
                try {
                    session.getBasicRemote().sendBinary(buffer);
                    byte[] bytes=new byte[buffer.limit()];
                    buffer.get(bytes);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        } else {
            //log.error("session is null.please check.", this);
        }
    }
    public void sendMessageToAll(ByteBuffer buffer) {
        for (Session session : sessions.values()) {
            synchronized (session) {
                try {
                    /**
                     * tomcat的原因,使用session.getAsyncRemote()會報Writing FULL WAITING error
                     * 需要使用session.getBasicRemote()
                     */
                    session.getBasicRemote().sendBinary(buffer);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }
    /**
     * 主動關閉websocket連接
     *
     * @param realPlayHandler 預覽句柄
     */
    public void closeSession(long realPlayHandler) {
        try {
            Session session = sessions.get(realPlayHandler);
            if (session != null) {
                session.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    /**
     * 獲取當前連接數
     *
     * @return
     */
    public int getOnlineCount() {
        return onlineCount.get();
    }
    /**
     * 增加當前連接數
     *
     * @return
     */
    public int addOnlineCount() {
        return onlineCount.getAndIncrement();
    }
    /**
     * 減少當前連接數
     *
     * @return
     */
    public int subOnlineCount() {
        return onlineCount.getAndDecrement();
    }
}

遇見的坑:
前端在播放的時候一開始始終不出畫面.流數據已經拉過來瞭.後來才發現是因為hasAudio參數

在這裡插入圖片描述

這裡如果設置成瞭true.則你的電腦必須插入耳機.不然會報錯;

總結:
之前使用純前端實現視頻監控和回放時.瀏覽器時隻支持IE.使用後端推流的方式實現視頻監控和回放時.瀏覽器支持谷歌火狐Edge等.但是又不支持IE瞭.很有意思.
flv的官方文檔解釋的是:

在這裡插入圖片描述

由於IO限制,flv.js可以支持HTTP上的FLV直播流Chrome 43+,FireFox 42+,Edge 15.15048+和Safari 10.1+現在。

最後:

由於是後端不停的拉流.所以流量和服務器壓力比較大.可能同時打開多個監控.會出現卡頓的情況.需要註意.

到此這篇關於vue+flv.js+SpringBoot+websocket實現視頻監控與回放的文章就介紹到這瞭,更多相關vue SpringBoot websocket視頻監控與回放內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: