演示一下事理解释 一、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、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下来的,既简洁又好看,下载地址不才文中会有。
准备阶段前面大略先容了一下核心技能,这里我会先容全体局域网直播系统的环境如何搭建。
一、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项目地址
常见问题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对应的目录位置是不是同一个。
我在代码中都有详细的注释,涌现问题可以先仔细看看代码,看看是不是没把稳到。 末了,希望考试测验的同学可以一次成功!