javaCV開發詳解之6:本地音頻(話筒設備)和影片(攝影機)抓取、混合併推送(錄製)到伺服器(本地)

  • 2019 年 11 月 1 日
  • 筆記

版權聲明:本文為部落客原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處鏈接和本聲明。

本文鏈接:https://blog.csdn.net/eguid_1/article/details/52804246

前言:

前面幾章已經基本把音頻或者影片如何錄製,如何抓取,如何推送的功能利用javaCV簡單的給實現了;

本章將會整合前面幾章的內容進行深入音影片混合抓取、錄製、推送

在開始本章之前可以了解一下javaCV-FFMPEG是如何幫我們做解復用和編碼:http://blog.csdn.net/eguid_1/article/details/52875793

1、實現功能

(1)抓取本地錄音設備(即,話筒)的實時音頻

(2)抓取本地攝影機實時影片

(3)音頻與影片時兩個執行緒分別進行的,互不干擾

(4)多8bit的音頻轉小位元組序問題,請參考http://blog.csdn.net/eguid_1/article/details/52790848

(5)本章程式碼包含大量注釋,用來闡述每個API的用法和作用

2、程式碼實現

/**  	 * 推送/錄製本機的音/影片(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, int AUDIO_DEVICE_INDEX, String outputFile,  			int captureWidth, int captureHeight, 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(無音頻)  		 */  		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);  					TargetDataLine line = (TargetDataLine) AudioSystem.getLine(dataLineInfo);  					line.open(audioFormat);  					line.start();  					// 獲得當前音頻取樣率  					int sampleRate = (int) audioFormat.getSampleRate();  					// 獲取當前音頻通道數量  					int numChannels = audioFormat.getChannels();  					// 初始化音頻緩衝區(size是音頻取樣率*通道數)  					int audioBufferSize = sampleRate * numChannels;  					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()) {  				System.out.println("Lip-flap correction: " + videoTS + " : " + recorder.getTimestamp() + " -> "  						+ (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("關閉攝影機失敗");  		}  	}