利用javacv解析rtsp流,通過websocket將視頻幀傳輸到web前端顯示成視頻

javacv拉取rtsp流通過websocket傳輸到web前端顯示

說明一下,我這裏只是介紹一下如何實現的一個小demo,因爲我做的這個rtsp解析主要是一個測試工具,簡單說一下需求,其他的服務器爲我提供了了一個rtsp流,我要做的就是將這個視頻流解析在web端進行展示,我的這個需求很簡單,只是作爲一個測試工具來說,並不是商業版本,所以爲了追求效率,並且也不會遇到什麼高併發的情況,只是提供這樣一種思路做出快速開發滿足於業務需求。其實這就相當於直播的感覺,因爲我獲取的rtsp流都是通過監控攝像頭獲取的實時數據,所以我這邊是要去解析rtsp流的,至於爲什麼是rtsp,就要問提供流的爲什麼是rtsp呢?爲了滿足需求,想過幾種方案,純前端直接解析rtsp,但是需要組件,而且查詢資料後發現這些插件好像並不好使,不是這個不兼容就這那個不兼容,要不是就是需要瀏覽器去安裝插件,太麻煩。後端解析,前端顯示呢,大部分的方案都說是將rtsp轉換成rtmp流,然後通過前端組件,比如video.js這種去重新解析,還要通過nginx去搭建服務武器,太麻煩,我最不想要的就是搭建各種環境,實際條件也不允許這麼麻煩還要搭建環境,最快捷的方式就是寫的這個項目,最好直接就能跑起來,所以,我就想到了視頻反正都是一幀一幀的圖片,不如前端就直接不停的刷新圖片得了,這不就是視頻了麼,再說一下,我這個是監控視頻顯示,對音頻沒要求,所以我的處理是沒處理音頻的,這裏只是做視頻顯示,說了這麼多,下面開始正題吧。項目地址:https://github.com/001ItSky/videoservice

1、首先我們需要引入相關的jar包,javacv和websocket的,pom文件如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.1.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.de</groupId>
    <artifactId>videoservice</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>videoservice</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <commons.io.version>2.5</commons.io.version>
        <commons.fileupload.version>1.3.3</commons.fileupload.version>
        <hutool.version>4.6.4</hutool.version>
        <fastjson.version>1.2.47</fastjson.version>
        <lang3.version>3.9</lang3.version>
        <jsckson.version>2.10.3</jsckson.version>
        <javacv.version>1.5.1</javacv.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
            <version>${commons.io.version}</version>
        </dependency>
        <!--文件上傳工具類 -->
        <dependency>
            <groupId>commons-fileupload</groupId>
            <artifactId>commons-fileupload</artifactId>
            <version>${commons.fileupload.version}</version>
        </dependency>
        <!-- 阿里JSON解析器 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>${fastjson.version}</version>
        </dependency>
        <!--好用的工具集-->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>${hutool.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>${lang3.version}</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>${jackson.version}</version>
        </dependency>
        <dependency>
            <groupId>org.bytedeco</groupId>
            <artifactId>javacv-platform</artifactId>
            <version>${javacv.version}</version>
            <type>pom</type>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

2、需要配置websocket,開啓websocket支持

package com.de.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

/**
 *  
 *  * @projectName videoservice
 *  * @title     WebSocketConfig   
 *  * @package    com.de.config  
 *  * @description    開啓websocket支持
 *  * @author IT_CREAT     
 *  * @date  2020 2020/4/12 0012 下午 16:39  
 *  * @version V1.0.0 
 *  
 */
@Configuration
public class WebSocketConfig {

    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}

3、編寫websocket服務

注意,因爲websocket用的是tomcat的,websoket發送的數據有三種數據類型,string類型,bytebuffer類型,object類型,但是說白了其實就只有兩種,也就是string和bytebuffer,對象是要在底層經行編碼轉換的,所以,對於對象而言我們先要寫個編碼器,不然到時候發送的時候會報解析失敗的錯誤。

 1)存放圖片數據的實體,因爲我們要想前端發送數據的是圖片數據

package com.de.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;

/**
 *  
 *  * @projectName videoservice
 *  * @title     Image   
 *  * @package    com.de.entity  
 *  * @description    存放圖片數據的實體 
 *  * @author IT_CREAT     
 *  * @date  2020 2020/4/18 0018 下午 22:54  
 *  * @version V1.0.0 
 *  
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
@Accessors(chain = true)
public class Image {
    private byte[] imageByte;
}

2)轉嗎器(轉碼器的作用就是將數據轉成json字符串返回),發送時websocket會自行調用,這裏要用到了AjaxResult,這是自己寫的同前端交互傳輸的統一對象。後面會貼出來

package com.de.entity;

import cn.hutool.core.codec.Base64;
import com.alibaba.fastjson.JSON;
import org.apache.commons.lang3.ArrayUtils;

import javax.websocket.EncodeException;
import javax.websocket.Encoder;
import javax.websocket.EndpointConfig;

/**
 *  
 *  * @projectName videoservice
 *  * @title     ImageEncoder   
 *  * @package    com.de.entity  
 *  * @description    websocket 傳輸對象轉碼 
 *  * @author IT_CREAT     
 *  * @date  2020 2020/4/18 0018 下午 22:53  
 *  * @version V1.0.0 
 *  
 */
public class ImageEncoder implements Encoder.Text<Image> {

    @Override
    public String encode(Image image) throws EncodeException {
        if(image != null && !ArrayUtils.isEmpty(image.getImageByte())){
            String base64Image= Base64.encode(image.getImageByte());
            return JSON.toJSONString(new AjaxResult(AjaxResult.Type.SUCCESS_IMG_BYTE,"獲取視頻幀成功",base64Image));
        }
        return JSON.toJSONString(AjaxResult.error("獲取視頻幀失敗"));
    }

    @Override
    public void init(EndpointConfig endpointConfig) {

    }

    @Override
    public void destroy() {

    }
}

3)websocket服務編寫,記得一定要指明添加編碼器,ServerEndpoint註解的encoders 屬性指明

package com.de.service;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.de.entity.AjaxResult;
import com.de.entity.ImageEncoder;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;

import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Collection;
import java.util.HashMap;
import java.util.concurrent.ConcurrentHashMap;

/**
 *  
 *  * @projectName videoservice
 *  * @title     WebSocketServer   
 *  * @package    com.de.service  
 *  * @description    websocket服務
 *  * @author IT_CREAT     
 *  * @date  2020 2020/4/12 0012 下午 16:41  
 *  * @version V1.0.0 
 *  
 */
@ServerEndpoint(value = "/webSocketService", encoders = {ImageEncoder.class})
@Component
@Slf4j
public class WebSocketServer {
    /**
     * 靜態變量,用來記錄當前在線連接數。應該把它設計成線程安全的。
     */
    private static int onlineCount = 0;
    /**
     * concurrent包的線程安全Set,用來存放每個客戶端對應的MyWebSocket對象。
     */
    private static ConcurrentHashMap<String, WebSocketServer> webSocketMap = new ConcurrentHashMap<>();
    /**
     * 與某個客戶端的連接會話,需要通過它來給客戶端發送數據
     */
    private Session session;
    /**
     * 接收userId
     */
    private String userId = "";

    /**
     * 連接建立成功調用的方法
     */
    @OnOpen
    public void onOpen(Session session) {
        this.session = session;
        this.userId = session.getId();
        if (webSocketMap.containsKey(userId)) {
            webSocketMap.remove(userId);
            webSocketMap.put(userId, this);
            //加入set中
        } else {
            webSocketMap.put(userId, this);
            //加入set中
            addOnlineCount();
            //在線數加1
        }
        log.info("用戶連接:" + userId + ",當前在線人數爲:" + getOnlineCount());
        HashMap<String, String> data = new HashMap<>();
        data.put("userId", userId);
        sendMessageByStr(JSON.toJSONString(AjaxResult.success("連接成功", data)));
    }

    /**
     * 連接關閉調用的方法
     */
    @OnClose
    public void onClose() {
        if (webSocketMap.containsKey(userId)) {
            webSocketMap.remove(userId);
            //從set中刪除
            subOnlineCount();
        }
        log.info("用戶退出:" + userId + ",當前在線人數爲:" + getOnlineCount());
    }

    /**
     * 收到客戶端消息後調用的方法
     *
     * @param message 客戶端發送過來的消息,必須是json串
     */
    @OnMessage
    public void onMessage(String message, Session session) {
        log.info("用戶消息:" + userId + ",報文:" + message);
        //可以羣發消息
        //消息保存到數據庫、redis
        if (StringUtils.isNotBlank(message)) {
            //解析發送的報文
            JSONObject jsonObject = JSON.parseObject(message);
            //追加發送人(防止串改)
            if (jsonObject != null) {
                jsonObject.put("fromUserId", this.userId);
                String toUserId = jsonObject.getString("toUserId");
                //傳送給對應toUserId用戶的websocket
                if (StringUtils.isNotBlank(toUserId) && webSocketMap.containsKey(toUserId)) {
                    webSocketMap.get(toUserId).sendMessageByStr(jsonObject.toJSONString());
                } else {
                    log.error("請求的userId:" + toUserId + "不在該服務器上");
                    //否則不在這個服務器上,發送到mysql或者redis
                }
            }
        }
    }

    /**
     * @param session
     * @param error
     */
    @OnError
    public void onError(Session session, Throwable error) {
        log.error("用戶錯誤:" + this.userId + ",原因:" + error.getMessage());
        log.error("websocket error: ", error);
    }

    public void sendMessageByStr(String message) {
        if (StringUtils.isNotBlank(message)) {
            try {
                if (this.session.isOpen()) {
                    this.session.getBasicRemote().sendText(message);
                }
            } catch (IOException e) {
                log.error("發送到用戶:" + this.userId + "信息失敗 ,信息是:" + message);
                log.error("websocket send str msg exception: ", e);
            }
        }
    }

    public void sendMessageByObject(Object message) {
        if (message != null) {
            try {
                this.session.getBasicRemote().sendObject(message);
            } catch (IOException | EncodeException e) {
                log.error("發送到用戶:" + this.userId + "信息失敗 ,信息是:" + message);
                log.error("websocket send object msg exception: ", e);
            }
        }
    }

    public void sendBinary(ByteBuffer message) {
        if (message != null) {
            try {
                this.session.getBasicRemote().sendBinary(message);
            } catch (IOException e) {
                log.error("發送到用戶:" + this.userId + "信息失敗 ,信息是:" + message);
                log.error("websocket send byteBuffer msg exception: ", e);
            }
        }
    }

    /**
     * 發送自定義消息
     */
    public static void sendInfo(String message, String userId) {
        log.info("發送消息到:" + userId + ",報文:" + message);
        if (StringUtils.isNotBlank(userId) && webSocketMap.containsKey(userId)) {
            webSocketMap.get(userId).sendMessageByStr(message);
        } else {
            log.error("用戶" + userId + ",不在線!");
        }
    }

    /**
     * 向所有的客戶端發送消息
     *
     * @param byteBuffer byteBuffer
     * @throws IOException IOException
     */
    public static void sendAllByBinary(ByteBuffer byteBuffer) {
        if (!webSocketMap.isEmpty()) {
            Collection<WebSocketServer> values = webSocketMap.values();
            for (WebSocketServer next : values) {
                next.sendBinary(byteBuffer);
            }
        }
    }

    public static void sendAllByObject(Object message) {
        if (!webSocketMap.isEmpty()) {
            Collection<WebSocketServer> values = webSocketMap.values();
            for (WebSocketServer next : values) {
                next.sendMessageByObject(message);
            }
        }
    }

    public static synchronized int getOnlineCount() {
        return onlineCount;
    }

    public static synchronized void addOnlineCount() {
        WebSocketServer.onlineCount++;
    }

    public static synchronized void subOnlineCount() {
        WebSocketServer.onlineCount--;
    }

}

4)AjaxResult,前面用了,說了貼出來

package com.de.entity;

import org.apache.commons.lang3.ObjectUtils;

import java.util.HashMap;

/**
 * 操作消息提醒
 *
 * @author ruoyi
 */
public class AjaxResult extends HashMap<String, Object> {
    private static final long serialVersionUID = 1L;

    /**
     * 狀態碼
     */
    public static final String CODE_TAG = "code";

    /**
     * 返回內容
     */
    public static final String MSG_TAG = "msg";

    /**
     * 數據對象
     */
    public static final String DATA_TAG = "data";

    /**
     * 狀態類型
     */
    public enum Type {
        /**
         * 成功
         */
        SUCCESS(0),
        SUCCESS_IMG_BYTE(201),
        /**
         * 警告
         */
        WARN(301),
        /**
         * 錯誤
         */
        ERROR(500);
        private final int value;

        Type(int value) {
            this.value = value;
        }

        public int value() {
            return this.value;
        }
    }

    /**
     * 初始化一個新創建的 AjaxResult 對象,使其表示一個空消息。
     */
    public AjaxResult() {
    }

    /**
     * 初始化一個新創建的 AjaxResult 對象
     *
     * @param type 狀態類型
     * @param msg  返回內容
     */
    public AjaxResult(Type type, String msg) {
        super.put(CODE_TAG, type.value);
        super.put(MSG_TAG, msg);
    }

    /**
     * 初始化一個新創建的 AjaxResult 對象
     *
     * @param type 狀態類型
     * @param msg  返回內容
     * @param data 數據對象
     */
    public AjaxResult(Type type, String msg, Object data) {
        super.put(CODE_TAG, type.value);
        super.put(MSG_TAG, msg);
        if (ObjectUtils.isNotEmpty(data)) {
            super.put(DATA_TAG, data);
        }
    }

    /**
     * 返回成功消息
     *
     * @return 成功消息
     */
    public static AjaxResult success() {
        return AjaxResult.success("操作成功");
    }

    /**
     * 返回成功數據
     *
     * @return 成功消息
     */
    public static AjaxResult success(Object data) {
        return AjaxResult.success("操作成功", data);
    }

    /**
     * 返回成功消息
     *
     * @param msg 返回內容
     * @return 成功消息
     */
    public static AjaxResult success(String msg) {
        return AjaxResult.success(msg, null);
    }

    /**
     * 返回成功消息
     *
     * @param msg  返回內容
     * @param data 數據對象
     * @return 成功消息
     */
    public static AjaxResult success(String msg, Object data) {
        return new AjaxResult(Type.SUCCESS, msg, data);
    }

    /**
     * 返回警告消息
     *
     * @param msg 返回內容
     * @return 警告消息
     */
    public static AjaxResult warn(String msg) {
        return AjaxResult.warn(msg, null);
    }

    /**
     * 返回警告消息
     *
     * @param msg  返回內容
     * @param data 數據對象
     * @return 警告消息
     */
    public static AjaxResult warn(String msg, Object data) {
        return new AjaxResult(Type.WARN, msg, data);
    }

    /**
     * 返回錯誤消息
     *
     * @return
     */
    public static AjaxResult error() {
        return AjaxResult.error("操作失敗");
    }

    /**
     * 返回錯誤消息
     *
     * @param msg 返回內容
     * @return 警告消息
     */
    public static AjaxResult error(String msg) {
        return AjaxResult.error(msg, null);
    }

    /**
     * 返回錯誤消息
     *
     * @param msg  返回內容
     * @param data 數據對象
     * @return 警告消息
     */
    public static AjaxResult error(String msg, Object data) {
        return new AjaxResult(Type.ERROR, msg, data);
    }
}

這樣整個websocket服務就大功告成了。然後就是解析rtsp流了。

4、解析rtsp,通過websokect發送到前端顯示

1) application.yml配置(主要):爲什麼我的url只是個視頻地址呢,因爲我主要是測試,你這裏寫rtsp地址也行,javacv會根據地址不同去解析,如何搭建一個快速的rtsp服務器做測試呢,我後面會說

rtsp:
  url: E:/視頻/VID_20141122_212032.mp4
  transport:
    type: udp

完整的application.yml

# 開發環境配置
server:
  # 服務器的HTTP端口,默認爲80
  port: 8080
  servlet:
    # 應用的訪問路徑
    context-path: /
  tomcat:
    # tomcat的URI編碼
    uri-encoding: UTF-8
    # tomcat最大線程數,默認爲200
    max-threads: 800
    # Tomcat啓動初始化的線程數,默認值25
    min-spare-threads: 30
 
# 日誌配置
logging:
  file:
    name: delogs.log

# Spring配置
spring:
  # 模板引擎
  thymeleaf:
    mode: HTML
    encoding: utf-8
    # 禁用緩存
    cache: false
  jackson:
    time-zone: GMT+8
    date-format: yyyy-MM-dd HH:mm:ss
    serialization:
      FAIL_ON_EMPTY_BEANS: false
#  profiles:
#    active: druid
  # 文件上傳
  servlet:
     multipart:
       # 單個文件大小
       max-file-size:  10MB
       # 設置總上傳的文件大小
       max-request-size:  20MB
  # 服務模塊
  devtools:
    restart:
      # 熱部署開關
      enabled: true

rtsp:
  url: E:/視頻/VID_20141122_212032.mp4
  transport:
    type: udp

2) 解析rtsp服務,websocket發送數據到前端

package com.de.rtsp;

import com.de.entity.Image;
import com.de.service.WebSocketServer;
import lombok.extern.slf4j.Slf4j;
import org.bytedeco.javacv.FFmpegFrameGrabber;
import org.bytedeco.javacv.Frame;
import org.bytedeco.javacv.FrameGrabber;
import org.bytedeco.javacv.Java2DFrameConverter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.stereotype.Component;

import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;

/**
 *  
 *  * @projectName videoservice
 *  * @title     MediaUtils   
 *  * @package    com.de.rtsp  
 *  * @description   獲取rtsp流,解析爲視頻幀,websocket傳遞到前臺顯示 
 *  * @author IT_CREAT     
 *  * @date  2020 2020/4/12 0012 下午 18:24  
 *  * @version V1.0.0 
 *  
 */
@Slf4j
@Component
@EnableAsync
public class MediaTransfer {

    @Value("${rtsp.url}")
    private String rtspUrl;

    @Value("${rtsp.transport.type}")
    private String rtspTransportType;

    private static FFmpegFrameGrabber grabber;

    private static boolean isStart = false;

    /**
     * 視頻幀率
     */
    public static int frameRate = 24;
    /**
     * 視頻寬度
     */
    public static int frameWidth = 480;
    /**
     * 視頻高度
     */
    public static int frameHeight = 270;

    /**
     * 開啓獲取rtsp流,通過websocket傳輸數據
     */
    @Async
    public void live() {
        log.info("連接rtsp:"+rtspUrl+",開始創建grabber");
        grabber = createGrabber(rtspUrl);
        if (grabber != null) {
            log.info("創建grabber成功");
        } else {
            log.info("創建grabber失敗");
        }
        startCameraPush();
    }

    /**
     * 構造視頻抓取器
     *
     * @param rtsp 拉流地址
     * @return
     */
    public FFmpegFrameGrabber createGrabber(String rtsp) {
        // 獲取視頻源
        try {
            FFmpegFrameGrabber grabber = FFmpegFrameGrabber.createDefault(rtsp);
            grabber.setOption("rtsp_transport", rtspTransportType);
            //設置幀率
            grabber.setFrameRate(frameRate);
            //設置獲取的視頻寬度
            grabber.setImageWidth(frameWidth);
            //設置獲取的視頻高度
            grabber.setImageHeight(frameHeight);
            //設置視頻bit率
            grabber.setVideoBitrate(2000000);
            return grabber;
        } catch (FrameGrabber.Exception e) {
            log.error("創建解析rtsp FFmpegFrameGrabber 失敗");
            log.error("create rtsp FFmpegFrameGrabber exception: ", e);
            return null;
        }
    }

    /**
     * 推送圖片(攝像機直播)
     */
    public void startCameraPush() {
        Java2DFrameConverter java2DFrameConverter = new Java2DFrameConverter();
        while (true) {
            if (grabber == null) {
                log.info("重試連接rtsp:"+rtspUrl+",開始創建grabber");
                grabber = createGrabber(rtspUrl);
                log.info("創建grabber成功");
            }
            try {
                if (grabber != null && !isStart) {
                    grabber.start();
                    isStart = true;
                    log.info("啓動grabber成功");
                }
                if (grabber != null) {
                    Frame frame = grabber.grabImage();
                    if (null == frame) {
                        continue;
                    }
                    BufferedImage bufferedImage = java2DFrameConverter.getBufferedImage(frame);
                    byte[] bytes = imageToBytes(bufferedImage, "jpg");
                    //使用websocket發送視頻幀數據
                    WebSocketServer.sendAllByObject(new Image(bytes));
                }
            } catch (FrameGrabber.Exception | RuntimeException e) {
                log.error("因爲異常,grabber關閉,rtsp連接斷開,嘗試重新連接");
                log.error("exception : " , e);
                if (grabber != null) {
                    try {
                        grabber.stop();
                    } catch (FrameGrabber.Exception ex) {
                        log.error("grabber stop exception: ", ex);
                    } finally {
                        grabber = null;
                        isStart = false;
                    }
                }
            }
        }
    }

    /**
     * 圖片轉字節數組
     *
     * @param bImage 圖片數據
     * @param format 格式
     * @return 圖片字節碼
     */
    private byte[] imageToBytes(BufferedImage bImage, String format) {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        try {
            ImageIO.write(bImage, format, out);
        } catch (IOException e) {
            log.error("bufferImage 轉 byte 數組異常");
            log.error("bufferImage transfer byte[] exception: ", e);
            return null;
        }
        return out.toByteArray();
    }

}

3)rtsp解析服務啓動實體類(可以採用多線程啓動,我這裏是用的異步的方式,上面的服務代碼用了異步@Async,其實底層也是多線程,@PostConstruct註解的作用是項目啓動即加載,只加載一次,相當與spring的ben配置的初始化加載方法,至於爲什麼異步,因爲我這裏初始化加載,不採用異步,就會一直卡在那,因爲上面使用的是死循環監聽rtsp流,進行拉流操作,所以不異步項目都起不來,會一直卡在死循環處)

package com.de.rtsp;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;

/**
 *  
 *  * @projectName videoservice
 *  * @title     MadiaStart   
 *  * @package    com.de.rtsp  
 *  * @description    程序啓動時加載一次,rtspj解析傳輸到websocket啓動類 
 *  * @author IT_CREAT     
 *  * @date  2020 2020/4/18 0018 下午 22:48  
 *  * @version V1.0.0 
 *  
 */
@Component
public class MediaStart {

    @Autowired
    MediaTransfer mediaTransfer;

    @PostConstruct
    public void init() {
        //異步加載,因爲初始化時執行,live裏面是死循環監聽rtsp,如果不異步操作,就會卡死在初始化階段,項目就會起不來
        mediaTransfer.live();
    }

}

5、前端接受數據顯示

1)首先得有websocket的插件或者是處理方法,我這裏貼出來Websocket.js

;!(function (window) {
    "use strict";
    let Event = {
        wsMesEvent: function (message) {
            console.log(message)
        }
    }, dftOpt = {
        protocol: (window.location.protocol == 'http:') ? 'ws://' : 'wss://'
        , host: window.location.host
        , port: '80'
        , path: ''
        , isReConect: false
        , wsMesEvent: Event.wsMesEvent
    }, Util = {
        arrayLike(arrayLike) {
            Array.from(arrayLike)
        },
        isArray(arr) {
            Array.isArray(arr)
        },
        forEach(array, iterate) {
            let index = -1
                , length = array.length;
            if (typeof iterate != 'function') {
                return array;
            }
            while (++index < length) {
                iterate.call(array, array[index], index);
            }
        },
        isPlainObject(obj) {
            let flag = false;
            if (!obj || typeof obj != 'object') {
                return flag;
            }
            if (obj.constructor.prototype.hasOwnProperty("isPrototypeOf")) {
                flag = true;
            }
            return flag;
        },
        extend(...args) {
            if (args.length <= 0) {
                return
            };
            let target = args[0];
            if (args.length == 1) {
                return args[0]
            };
            this.forEach(args, (arg, i) => {
                if (i != 0) {
                    var keys = Object.keys(arg);
                    this.forEach(keys, (key, i) => {
                        var val = arg[key];
                        if (this.isPlainObject(val) || this.isArray(val)) {
                            var newTarget = this.isArray(val) ? [] : {};
                            target[key] = this.extend(newTarget, val);
                        } else {
                            target[key] = val;
                        }
                    });
                }
            });
            return target;
        }
    }, Ws = function (opt) {
        //如果瀏覽器不支持websocket,直接退出
        if (!this.isSupportWs()) {
            alert("對不起,您的瀏覽器在不支持WebSocket,請先升級您的瀏覽器!!");
            return;
        }
        let config = this.config = Util.extend({}, dftOpt, opt);
        //接口地址url
        this.url = config.host === "" || config.host === "" ?
            config.protocol + config.path:
            config.protocol + config.host + ':' + config.port + config.path;
        //心跳狀態  爲false時不能執行操作 等待重連
        this.isHeartBeat = false;
        //重連狀態  避免不間斷的重連操作
        this.isReconnect = config.isReConect;
        //發送的消息
        this.curSendMes = null;
        //響應的信息
        this.message = null;
        //創建webSocket
        this.ws;
        //初始化websocket
        this.initWs = function () {
            //創建WebSocket
            let ws = this.ws = new WebSocket(this.url);
            // ws.binaryType = "arraybuffer";
            //Ws連接函數:服務器連接成功
            ws.onopen = (e) => {
                console.log(`與${this.config.host}:${this.config.port}${this.config.path}連接已建立...`)
                this.isHeartBeat = true;
                //發佈事件
                this.send();
            };
            //Ws消息接收函數:服務器向前端推送消息時觸發
            ws.onmessage = (e) => {
                //處理各種推送消
                this.message = e.data;
                config.wsMesEvent.apply(this, [e.data]);
            }
            //Ws異常事件:Ws報錯後觸發
            ws.onerror = (e) => {
                this.isHeartBeat = false;
                this.reConnect();
            }
            //Ws關閉事件:Ws連接關閉後觸發
            ws.onclose = (e) => {
                console.log('連接已關閉...');
                alert("websocket連接已關閉,按F5嘗試重新刷新頁面");
                this.isHeartBeat = false;
                ws = null;
                this.reConnect();
            };
        };
        this.initWs();
    };

    //判斷是否支持WebSocket
    Ws.prototype.isSupportWs = function () {
        return (window.WebSocket || window.MozWebSocket) ? true : false;
    }

    //重新連接
    Ws.prototype.reConnect = function () {
        //不需要重新連接,直接返回
        if (!this.isReconnect) return;
        this.isReconnect = true;
        //沒連接上 會一直重連,設置延遲避免請求過多
        setTimeout(() => {
            this.initWs()
            this.isReconnect = false;
        }, 5000);
    }

    //發送消息
    Ws.prototype.send = function (content) {
        this.curSendMes = content || this.curSendMes;
        if(this.curSendMes == null){
            return;
        }
        if (this.isHeartBeat) {
            // this.ws.send(bytes);
            this.ws.send(this.curSendMes);
        }
    }
    window.Ws = Ws;
})(window);

/***
 * 使用方式:
 * //建立連接
 * var ws1 = new Ws({
 *        host:'123.207.167.163'
 *        ,port:9010
 *        ,path:'/ajaxchattest'
 *        ,wsMesEvent:function(message){
 *            //將接收到的二進制數據轉爲字符串
		       var unit8Arr = new Uint8Array(event.data) ;
 *            console.log(message)
 *        }
 *    });
 *    //發送請求
 *    ws1.send("111");
 *
 *    //建立連接
 *    var ws2 = new Ws({
 *        host:'123.207.167.163'
 *        ,port:9010
 *        ,path:'/ajaxchattest'
 *        ,wsMesEvent:function(message){
 *            console.log(message)
 *        }
 *    });
 *    //發送請求
 *    ws2.send("222");
 * */

2)前端頁面(我用的是Thymeleaf模板,整個展示的核心就兩個js, jquery.js和上面給出的websocket.js)

<!DOCTYPE html>
<html lang="zh" xmlns:th="http://www.thymeleaf.org" xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">
<head>
    <th:block th:include="include :: header('視頻展示rtsp')"/>
</head>
<body class="gray-bg">
<div style="padding: 20px">
    <p style="font-size: 20px;color: #0a7491;font-weight: bold;font-family: 楷體">rtsp拉取視頻顯示</p>
    <img id="show_video" src="">
</div>
<th:block th:include="include :: footer"/>
<script src="../static/js/workbench/WebSocket.js" th:src="@{/js/workbench/WebSocket.js}"></script>
<script th:inline="javascript">
    var wsUrl = getWsPath() + "/webSocketService";
    //建立連接
    var ws1 = new Ws({
        host: ""
        , port: ""
        , path: wsUrl
        , wsMesEvent: function (message) {
            //將接收到的圖片數據進行刷新顯示
            var data = JSON.parse(message);
            if (data.code === 0) {
                console.log(message)
            } else if (data.code === 201) {
                $("#show_video").attr("src", "data:image/*;base64," + data.data)
            }
        }
    });

</script>
</body>
</html>

整個流程就完了,效果如下:

rtsp服務器搭建,基於vlc播放器:https://blog.csdn.net/IT_CREATE/article/details/105626071

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章