天天看點

Java微服務搭建一個簡易的區域網路直播(live)系統

作者:架構淺水灣

目錄

示範一下

原理說明

  一、JavaCV簡介

  二、RTMP協定

  三、Nginx推流伺服器

  四、Maven項目建構工具

  五、前端播放器

準備階段

  一、JDK版本以及作業系統

  二、搭建Nginx伺服器

  三、修改nginx.conf

項目代碼

  後端代碼

  前端代碼

常見問題

  1、錄制的隻有視訊沒有聲音

  2、Java啟動出現Exception in thread "main" java.lang.UnsatisfiedLinkError: no jniopenblas_nolapack in java.library.path

  3、通路播放位址出現404

示範一下

由于是區域網路直播系統,那麼最簡單的情況應該也有兩部分構成:錄制直播和播放直播。

  • 錄制直播

    錄制直播使用的是本機的攝像頭和麥克風,使用Java自帶的JFrame視窗播放,支援音視訊的錄制。效果如下圖:

  • 播放直播

    播放器這邊選擇的是由htm+js+css編寫的,支援輸入播放網址,點選播放按鈕播放。大家都知道html頁面隻要浏覽器就可以打開,是以隻要在區域網路内打開這個播放器輸入網址就可以看主機的直播了。效果如下圖:

原理說明

這裡我會給大家簡單介紹一下我在區域網路直播系統中使用到的關鍵技術,讓大家對該系統有一個初步的認識。

使用的技術或協定

Java、JavaCV、maven、Nginx、rtmp、hls、html等

一、JavaCV簡介

javacv開發包是用于支援java多媒體開發的一套開發包,可以适用于本地多媒體(音視訊)調用以及音視訊,圖檔等檔案後期操作(圖檔修改,音視訊解碼剪輯等等功能)。核心元件有四個幀抓取器(FrameGrabber)、幀錄制器/推流器(FrameRecorder)、過濾器(FrameFilter)、幀(Frame)。我這裡主要是應用,想看原理請參考:JavaCV原理

二、RTMP協定

RTMP(Real Time Messaging Protocol)實時消息傳送協定是Adobe Systems公司為Flash播放器和伺服器之間音頻、視訊和資料傳輸 開發的開放協定,也是一種流媒體協定,預設使用端口1935。簡單來說,就是可以将抓取的音頻流按照這個協定推送出去,是直播系統很常見的一個協定

三、Nginx推流伺服器

Nginx伺服器大家應該也不陌生,它有一個名為nginx-rtmp-module的開源子產品。nginx-rtmp-module不僅可以使 Nginx 可以支援 RTMP,用于音視訊的點播、直播,而且還可以将RTMP協定變為HLS協定,也就是常見的m3u8檔案流。這裡我使用Nginx 加上 nginx-rtmp-module 子產品作為 RTMP 服務端,FrameGrabber抓取的音視訊資料将會推送到Nginx推流伺服器中進行轉發。

四、Maven項目建構工具

這個不必多說,主要用于建構開發環境,因為JavaCV的包比較大,單獨下載下傳jar包很容易漏。

五、前端播放器

這個播放器是我從github上down下來的,既簡潔又好看,下載下傳位址在下文中會有。

Java微服務搭建一個簡易的區域網路直播(live)系統

準備階段

前面簡單介紹了一下核心技術,這裡我會介紹整個區域網路直播系統的環境如何搭建。

一、JDK版本以及作業系統

二、搭建Nginx伺服器

1、下載下傳Nginx包

下載下傳位址(選擇字尾為Gryphon):官網位址

2、下載下傳nginx-rtmp-module

下載下傳位址:代碼位址

3、解壓檔案

解壓nginx壓縮包,将nginx-rtmp-module放到Nginx檔案夾中。

三、修改nginx.conf

将nginx-win.conf檔案拷貝出來,改名為nginx.conf,将下面的配置覆寫

#user  nobody;
worker_processes  1;

#error_log  logs/error.log;
#error_log  logs/error.log  notice;
#error_log  logs/error.log  info;

#pid        logs/nginx.pid;


events {
    worker_connections  1024;
}



http {
    include       mime.types;
    default_type  application/octet-stream;

    #log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
    #                  '$status $body_bytes_sent "$http_referer" '
    #                  '"$http_user_agent" "$http_x_forwarded_for"';

    #access_log  logs/access.log  main;

    sendfile        on;
    #tcp_nopush     on;

    #keepalive_timeout  0;
    keepalive_timeout  65;

    #gzip  on;

    server {
        listen       8080;
        server_name  localhost;

        #charset koi8-r;

        #access_log  logs/host.access.log  main;

        location / {
            root   html;
            index  index.html index.htm;
        }

        # 由于使用hls播放,需要在http中添加支援
        location /live {
                types {
                   application/vnd.apple.mpegusr m3u8;
                   video/mp2t ts;
                }
                # 這裡的位址要和下面rtmp中配置的一緻,否則通路位址時會出現404
                alias D://javacv/flie/hls;
                add_header Cache-Control no-cache;
                # 跨域處理,否則下發播放器時會打不開
                add_header Access-Control-Allow-Origin *;
                add_header Access-Control-Allow-Headers "Origin, X-Requested-With,  Content-Type, Accept";
                add_header Access-Control-Methods "GET, POST, OPTIONS";

        }        

        #error_page  404              /404.html;

        # redirect server error pages to the static page /50x.html
        #
        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }

        # proxy the PHP scripts to Apache listening on 127.0.0.1:80
        #
        #location ~ \.php$ {
        #    proxy_pass   http://127.0.0.1;
        #}

        # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
        #
        #location ~ \.php$ {
        #    root           html;
        #    fastcgi_pass   127.0.0.1:9000;
        #    fastcgi_index  index.php;
        #    fastcgi_param  SCRIPT_FILENAME  /scripts$fastcgi_script_name;
        #    include        fastcgi_params;
        #}

        # deny access to .htaccess files, if Apache's document root
        # concurs with nginx's one
        #
        #location ~ /\.ht {
        #    deny  all;
        #}
    }


    # another virtual host using mix of IP-, name-, and port-based configuration
    #
    #server {
    #    listen       8000;
    #    listen       somename:8080;
    #    server_name  somename  alias  another.alias;

    #    location / {
    #        root   html;
    #        index  index.html index.htm;
    #    }
    #}


    # HTTPS server
    #
    #server {
    #    listen       443 ssl;
    #    server_name  localhost;

    #    ssl_certificate      cert.pem;
    #    ssl_certificate_key  cert.key;

    #    ssl_session_cache    shared:SSL:1m;
    #    ssl_session_timeout  5m;

    #    ssl_ciphers  HIGH:!aNULL:!MD5;
    #    ssl_prefer_server_ciphers  on;

    #    location / {
    #        root   html;
    #        index  index.html index.htm;
    #    }
    #}
    include servers/*;
}

#在http節點下面(也就是檔案的尾部)加上rtmp配置:
rtmp{
   server {
     listen 1935;
     application myapp{
        live on;
        record off;
        allow play all;
     }
     application live{
        live on;
        hls on;
        # 這裡的位址是存放ts檔案的,不會預設建立,需要預先建立好
        hls_path D://javacv/flie/hls;
        hls_fragment 5s;
        hls_playlist_length 15s;
        record off;
     }
   }
}
           

項目代碼

後端代碼

pom.xml

<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.wzhi.java_live_broadcast</groupId>
    <artifactId>java-live-broadcast</artifactId>
    <version>1.0-SNAPSHOT</version>
    <description>自建區域網路直播系統</description>
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>1.6</source>
                    <target>1.6</target>
                </configuration>
            </plugin>
        </plugins>
    </build>

    <dependencies>
        <dependency>
            <groupId>org.bytedeco</groupId>
            <artifactId>javacv-platform</artifactId>
            <version>1.4.4</version>
        </dependency>
    </dependencies>

</project>
           

啟動類

package com.wzhi.live;

import org.bytedeco.javacpp.avcodec;
import org.bytedeco.javacv.*;

import javax.sound.sampled.*;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.ShortBuffer;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class Application {
    public static void main(String[] args) throws FrameGrabber.Exception {
        //準備推流
        recordWebcamAndMicrophone(0,4,"rtmp://xxx.xxx.xxx.xxx:1935/live/test",1000,500,35);
    }
    /**
     * 推送/錄制本機的音/視訊(Webcam/Microphone)到流媒體伺服器(Stream media server)
     *
     * @param WEBCAM_DEVICE_INDEX
     *            - 視訊裝置,本機預設是0
     * @param AUDIO_DEVICE_INDEX
     *            - 音頻裝置,本機預設是4
     * @param outputFile
     *            - 輸出檔案/位址(可以是本地檔案,也可以是流媒體伺服器位址)
     * @param captureWidth
     *            - 攝像頭寬
     * @param captureHeight
     *            - 攝像頭高
     * @param FRAME_RATE
     *            - 視訊幀率:最低 25(即每秒25張圖檔,低于25就會出現閃屏)
     * @throws org.bytedeco.javacv.FrameGrabber.Exception
     */
    public static void recordWebcamAndMicrophone(int WEBCAM_DEVICE_INDEX, final int AUDIO_DEVICE_INDEX, String outputFile,
                                                 int captureWidth, int captureHeight, final int FRAME_RATE) throws org.bytedeco.javacv.FrameGrabber.Exception {
        long startTime = 0;
        long videoTS = 0;
        /**
         * FrameGrabber 類包含:OpenCVFrameGrabber
         * (opencv_videoio),C1394FrameGrabber, FlyCaptureFrameGrabber,
         * OpenKinectFrameGrabber,PS3EyeFrameGrabber,VideoInputFrameGrabber, 和
         * FFmpegFrameGrabber.
         */
        OpenCVFrameGrabber grabber = new OpenCVFrameGrabber(WEBCAM_DEVICE_INDEX);
        grabber.setImageWidth(captureWidth);
        grabber.setImageHeight(captureHeight);
        System.out.println("開始抓取攝像頭...");
        int isTrue = 0;// 攝像頭開啟狀态
        try {
            grabber.start();
            isTrue += 1;
        } catch (org.bytedeco.javacv.FrameGrabber.Exception e2) {
            if (grabber != null) {
                try {
                    grabber.restart();
                    isTrue += 1;
                } catch (org.bytedeco.javacv.FrameGrabber.Exception e) {
                    isTrue -= 1;
                    try {
                        grabber.stop();
                    } catch (org.bytedeco.javacv.FrameGrabber.Exception e1) {
                        isTrue -= 1;
                    }
                }
            }
        }
        if (isTrue < 0) {
            System.err.println("攝像頭首次開啟失敗,嘗試重新開機也失敗!");
            return;
        } else if (isTrue < 1) {
            System.err.println("攝像頭開啟失敗!");
            return;
        } else if (isTrue == 1) {
            System.err.println("攝像頭開啟成功!");
        } else if (isTrue == 1) {
            System.err.println("攝像頭首次開啟失敗,重新啟動成功!");
        }

        /**
         * FFmpegFrameRecorder(String filename, int imageWidth, int imageHeight,
         * int audioChannels) fileName可以是本地檔案(會自動建立),也可以是RTMP路徑(釋出到流媒體伺服器)
         * imageWidth = width (為捕獲器設定寬) imageHeight = height (為捕獲器設定高)
         * audioChannels = 2(立體聲);1(單聲道);0(無音頻)
         */
        final FFmpegFrameRecorder recorder = new FFmpegFrameRecorder(outputFile, captureWidth, captureHeight, 2);
        recorder.setInterleaved(true);

        /**
         * 該參數用于降低延遲 參考FFMPEG官方文檔:https://trac.ffmpeg.org/wiki/StreamingGuide
         * 官方原文參考:ffmpeg -f dshow -i video="Virtual-Camera" -vcodec libx264
         * -tune zerolatency -b 900k -f mpegts udp://10.1.0.102:1234
         */

        recorder.setVideoOption("tune", "zerolatency");
        /**
         * 權衡quality(視訊品質)和encode speed(編碼速度) values(值):
         * ultrafast(終極快),superfast(超級快), veryfast(非常快), faster(很快), fast(快),
         * medium(中等), slow(慢), slower(很慢), veryslow(非常慢)
         * ultrafast(終極快)提供最少的壓縮(低編碼器CPU)和最大的視訊流大小;而veryslow(非常慢)提供最佳的壓縮(高編碼器CPU)的同時降低視訊流的大小
         * 參考:https://trac.ffmpeg.org/wiki/Encode/H.264 官方原文參考:-preset ultrafast
         * as the name implies provides for the fastest possible encoding. If
         * some tradeoff between quality and encode speed, go for the speed.
         * This might be needed if you are going to be transcoding multiple
         * streams on one machine.
         */
        recorder.setVideoOption("preset", "ultrafast");
        /**
         * 參考轉流指令: ffmpeg
         * -i'udp://localhost:5000?fifo_size=1000000&overrun_nonfatal=1' -crf 30
         * -preset ultrafast -acodec aac -strict experimental -ar 44100 -ac
         * 2-b:a 96k -vcodec libx264 -r 25 -b:v 500k -f flv 'rtmp://<wowza
         * serverIP>/live/cam0' -crf 30
         * -設定内容速率因子,這是一個x264的動态比特率參數,它能夠在複雜場景下(使用不同比特率,即可變比特率)保持視訊品質;
         * 可以設定更低的品質(quality)和比特率(bit rate),參考Encode/H.264 -preset ultrafast
         * -參考上面preset參數,與視訊壓縮率(視訊大小)和速度有關,需要根據情況平衡兩大點:壓縮率(視訊大小),編/解碼速度 -acodec
         * aac -設定音頻編/解碼器 (内部AAC編碼) -strict experimental
         * -允許使用一些實驗的編解碼器(比如上面的内部AAC屬于實驗編解碼器) -ar 44100 設定音頻采樣率(audio sample
         * rate) -ac 2 指定雙通道音頻(即立體聲) -b:a 96k 設定音頻比特率(bit rate) -vcodec libx264
         * 設定視訊編解碼器(codec) -r 25 -設定幀率(frame rate) -b:v 500k -設定視訊比特率(bit
         * rate),比特率越高視訊越清晰,視訊體積也會變大,需要根據實際選擇合理範圍 -f flv
         * -提供輸出流封裝格式(rtmp協定隻支援flv封裝格式) 'rtmp://<FMS server
         * IP>/live/cam0'-流媒體伺服器位址
         */
        recorder.setVideoOption("crf", "25");
        // 2000 kb/s, 720P視訊的合理比特率範圍
        recorder.setVideoBitrate(2000000);
        // h264編/解碼器
        recorder.setVideoCodec(avcodec.AV_CODEC_ID_H264);
        // 封裝格式flv
        recorder.setFormat("flv");
        // 視訊幀率(保證視訊品質的情況下最低25,低于25會出現閃屏)
        recorder.setFrameRate(FRAME_RATE);
        // 關鍵幀間隔,一般與幀率相同或者是視訊幀率的兩倍
        recorder.setGopSize(FRAME_RATE * 2);
        // 不可變(固定)音頻比特率
        recorder.setAudioOption("crf", "0");
        // 最高品質
        recorder.setAudioQuality(0);
        // 音頻比特率
        recorder.setAudioBitrate(192000);
        // 音頻采樣率
        recorder.setSampleRate(44100);
        // 雙通道(立體聲)
        recorder.setAudioChannels(2);
        // 音頻編/解碼器
        recorder.setAudioCodec(avcodec.AV_CODEC_ID_AAC);
        System.out.println("開始錄制...");

        try {
            recorder.start();
        } catch (org.bytedeco.javacv.FrameRecorder.Exception e2) {
            if (recorder != null) {
                System.out.println("關閉失敗,嘗試重新開機");
                try {
                    recorder.stop();
                    recorder.start();
                } catch (org.bytedeco.javacv.FrameRecorder.Exception e) {
                    try {
                        System.out.println("開啟失敗,關閉錄制");
                        recorder.stop();
                        return;
                    } catch (org.bytedeco.javacv.FrameRecorder.Exception e1) {
                        return;
                    }
                }
            }

        }
        // 音頻捕獲
        new Thread(new Runnable() {
            @Override
            public void run() {
                /**
                 * 設定音頻編碼器 最好是系統支援的格式,否則getLine() 會發生錯誤
                 * 采樣率:44.1k;采樣率位數:16位;立體聲(stereo);是否簽名;true:
                 * big-endian位元組順序,false:little-endian位元組順序(詳見:ByteOrder類)
                 */
                AudioFormat audioFormat = new AudioFormat(44100.0F, 16, 2, true, false);

                // 通過AudioSystem擷取本地音頻混合器資訊
                Mixer.Info[] minfoSet = AudioSystem.getMixerInfo();
                // 通過AudioSystem擷取本地音頻混合器
                Mixer mixer = AudioSystem.getMixer(minfoSet[AUDIO_DEVICE_INDEX]);
                // 通過設定好的音頻編解碼器擷取資料線資訊
                DataLine.Info dataLineInfo = new DataLine.Info(TargetDataLine.class, audioFormat);
                try {
                    // 打開并開始捕獲音頻
                    // 通過line可以獲得更多控制權
                    // 擷取裝置:TargetDataLine line
                    // =(TargetDataLine)mixer.getLine(dataLineInfo);
                    final TargetDataLine line = (TargetDataLine) AudioSystem.getLine(dataLineInfo);
                    line.open(audioFormat);
                    line.start();
                    // 獲得目前音頻采樣率
                    final int sampleRate = (int) audioFormat.getSampleRate();
                    // 擷取目前音頻通道數量
                    final int numChannels = audioFormat.getChannels();
                    // 初始化音頻緩沖區(size是音頻采樣率*通道數)
                    int audioBufferSize = sampleRate * numChannels;
                    final byte[] audioBytes = new byte[audioBufferSize];

                    ScheduledThreadPoolExecutor exec = new ScheduledThreadPoolExecutor(1);
                    exec.scheduleAtFixedRate(new Runnable() {
                        @Override
                        public void run() {
                            try {
                                // 非阻塞方式讀取
                                int nBytesRead = line.read(audioBytes, 0, line.available());
                                // 因為我們設定的是16位音頻格式,是以需要将byte[]轉成short[]
                                int nSamplesRead = nBytesRead / 2;
                                short[] samples = new short[nSamplesRead];
                                /**
                                 * ByteBuffer.wrap(audioBytes)-将byte[]數組包裝到緩沖區
                                 * ByteBuffer.order(ByteOrder)-按little-endian修改位元組順序,解碼器定義的
                                 * ByteBuffer.asShortBuffer()-建立一個新的short[]緩沖區
                                 * ShortBuffer.get(samples)-将緩沖區裡short資料傳輸到short[]
                                 */
                                ByteBuffer.wrap(audioBytes).order(ByteOrder.LITTLE_ENDIAN).asShortBuffer().get(samples);
                                // 将short[]包裝到ShortBuffer
                                ShortBuffer sBuff = ShortBuffer.wrap(samples, 0, nSamplesRead);
                                // 按通道錄制shortBuffer
                                recorder.recordSamples(sampleRate, numChannels, sBuff);
                            } catch (org.bytedeco.javacv.FrameRecorder.Exception e) {
                                e.printStackTrace();
                            }
                        }
                    }, 0, (long) 1000 / FRAME_RATE, TimeUnit.MILLISECONDS);
                } catch (LineUnavailableException e1) {
                    e1.printStackTrace();
                }
            }
        }).start();

        // javaCV提供了優化非常好的硬體加速元件來幫助顯示我們抓取的攝像頭視訊
        CanvasFrame cFrame = new CanvasFrame("Capture Preview", CanvasFrame.getDefaultGamma() / grabber.getGamma());
        Frame capturedFrame = null;
        // 執行抓取(capture)過程
        while ((capturedFrame = grabber.grab()) != null) {
            if (cFrame.isVisible()) {
                //本機預覽要發送的幀
                cFrame.showImage(capturedFrame);
            }
            //定義我們的開始時間,當開始時需要先初始化時間戳
            if (startTime == 0)
                startTime = System.currentTimeMillis();

            // 建立一個 timestamp用來寫入幀中
            videoTS = 1000 * (System.currentTimeMillis() - startTime);
            //檢查偏移量
            if (videoTS > recorder.getTimestamp()) {
                //告訴錄制器寫入這個timestamp
                recorder.setTimestamp(videoTS);
            }
            // 發送幀
            try {
                recorder.record(capturedFrame);
            } catch (org.bytedeco.javacv.FrameRecorder.Exception e) {
                System.out.println("錄制幀發生異常,什麼都不做");
            }
        }

        cFrame.dispose();
        try {
            if (recorder != null) {
                recorder.stop();
            }
        } catch (org.bytedeco.javacv.FrameRecorder.Exception e) {
            System.out.println("關閉錄制器失敗");
            try {
                if (recorder != null) {
                    grabber.stop();
                }
            } catch (org.bytedeco.javacv.FrameGrabber.Exception e1) {
                System.out.println("關閉攝像頭失敗");
                return;
            }
        }
        try {
            if (recorder != null) {
                grabber.stop();
            }
        } catch (org.bytedeco.javacv.FrameGrabber.Exception e) {
            System.out.println("關閉攝像頭失敗");
        }
    }
}

           

前端代碼

下載下傳位址:GitHub項目位址
Java微服務搭建一個簡易的區域網路直播(live)系統

常見問題

1、錄制的隻有視訊沒有聲音

有些機器的采樣率、采樣率位數、通道都不太一樣,如果設定的不對,就可能沒有聲音,這裡我教大家如何找到系統麥克風的參數。

Win10:控制台—>聲音—>錄制—>麥克風—>屬性—>進階

Mac:關于本機—>系統報告—>音頻—>麥克風

2、Java啟動出現Exception in thread "main" java.lang.UnsatisfiedLinkError: no jniopenblas_nolapack in java.library.path

檢查一下javacv的版本,我使用的是javacv-platform:1.4.4。開始以為是系統或者jdk版本的問題,後來發現不是這樣的,大機率是因為導入的版本依賴問題。

3、通路播放位址出現404

首先看一下ts檔案有沒有産生

如果沒有ts檔案的話,一般是推流問題,說明Java代碼中推流的位址不對,或者nginx沒有正常啟動;

如果有ts檔案的話,一般是配置問題,看一下nginx.conf配置檔案,兩個alias對應的目錄位置是不是同一個。

我在代碼中都有詳細的注釋,出現問題可以先仔細看看代碼,看看是不是沒注意到。 最後,希望嘗試的同學可以一次成功!