前言
查看了相關文章然後一筆一筆打代碼再調試成功出結果,
eguid的博客
不保證代碼能夠原封不動就能運行,
這裏做一下記錄。
ps:代碼內容有改動,原版的可以看原作者的。
代碼
package net.w2p.JCVStudio.zhiboStudy;
import org.bytedeco.ffmpeg.global.avcodec;
import org.bytedeco.javacv.CanvasFrame;
import org.bytedeco.javacv.FFmpegFrameRecorder;
import org.bytedeco.javacv.Frame;
import org.bytedeco.javacv.OpenCVFrameConverter;
import org.bytedeco.opencv.global.opencv_imgproc;
import org.bytedeco.opencv.opencv_core.Mat;
import org.bytedeco.opencv.opencv_core.Point;
import org.bytedeco.opencv.opencv_core.Scalar;
import org.bytedeco.opencv.opencv_videoio.VideoCapture;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.sound.sampled.*;
import javax.swing.*;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.ShortBuffer;
import java.text.DecimalFormat;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/***
*https://blog.csdn.net/eguid_1/article/details/52804246
* javaCV開發詳解之6:本地音頻(話筒設備)和視頻(攝像頭)抓取、混合並推送(錄製)到服務器(本地)
* **/
public class Lesson06 {
private Logger logger = LoggerFactory.getLogger("[第六課]");
private static final String key4Stop = "stop";
private static final String key4AudioTask = "audioTask";
private static final String key4VideoCapture = "videoCapture";
private static final String key4Recorder = "recorder";
private ConcurrentHashMap<String, Object> middleMap = new ConcurrentHashMap<>(5);
/**
* 推送/錄製本機的音/視頻(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 void recordWebcamAndMicrophone(
int AUDIO_DEVICE_INDEX,
String outputFile,
int FRAME_RATE) throws Exception {
final OpenCVFrameConverter.ToIplImage iplImageConverter = new OpenCVFrameConverter.ToIplImage();
//--嘗試讀取攝像頭。
VideoCapture videoCapture = null;
int videoCapIndex = -1;
for (; videoCapIndex < 5; videoCapIndex++) {
videoCapture = new VideoCapture(videoCapIndex);
if (videoCapture.grab()) {
logger.info("成功找到本機攝像頭,攝像頭當前序號:{}", videoCapIndex);
break;
}
logger.info("攝像頭{}初始化失敗,關閉釋放資源,繼續嘗試。", videoCapIndex);
videoCapture.close();//--
}
if (videoCapture != null && !videoCapture.isOpened()) {
logger.info("攝像頭初始化失敗,請檢查是否有攝像頭硬件。");
return;
}
middleMap.put(key4VideoCapture, videoCapture);
//--初始化相關顯示用的frame框。
CanvasFrame cFrame = new CanvasFrame("第六課,測試攝像頭+耳麥混合輸出");
cFrame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
cFrame.addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
logger.info("關閉了ui窗口");
middleMap.put(key4Stop, true);
try{
ScheduledFuture tasker=(ScheduledFuture)middleMap.get(key4AudioTask);
if(tasker!=null){
tasker.cancel(true);
}
}
catch (Exception ed){
ed.printStackTrace();
}
}
});
//--frame初始化結束
Mat mat = new Mat();
/**用於保存幀的開始和結束時間**/
double frameStartTime = System.currentTimeMillis();
double frameEndTime = 0;
/***用於計算寫入幀的時間戳**/
long videoTS = 0;
long videoStartTime = 0;
Frame ftmp = null;
/**打水印相關參數**/
String msg = "fps:";//水印文字
// 水印文字位置
Point point = new Point(10, 50);
// 顏色,使用黃色
Scalar scalar = new Scalar(0, 255, 255, 0);
DecimalFormat df = new DecimalFormat(".##");//數字格式化
FFmpegFrameRecorder recorder=null;
/***視頻數據採集 begin**/
for (int i = 0; ; i++) {
ftmp = null;
if (middleMap.get(key4Stop) != null) {
logger.info("接收到結束標誌位,操作結束");
videoCapture.close();
videoCapture.release();
break;
}
videoCapture.retrieve(mat);//--重新初始化mat
/***採集攝像頭數據***/
if (videoCapture.grab()) {
/***讀取一幀mat圖像**/
if (videoCapture.read(mat)) {
frameEndTime = System.currentTimeMillis();//--獲得兩幀之間的時間。
//--打水印
String ftpTips = String.format("%s:%s", msg, df.format((1000.0 / (frameEndTime - frameStartTime))));
opencv_imgproc.putText(mat, ftpTips, point, opencv_imgproc.CV_FONT_VECTOR0
, 1.2, scalar
);
ftmp=iplImageConverter.convert(mat);
cFrame.showImage(ftmp);
if(recorder==null){
recorder=initRecorder(outputFile
,ftmp.imageWidth
,ftmp.imageHeight
,25
);
try {
recorder.start();
} catch (org.bytedeco.javacv.FrameRecorder.Exception e2) {
if (recorder != null) {
logger.info("關閉失敗,嘗試重啓");
try {
recorder.stop();
recorder.start();
} catch (org.bytedeco.javacv.FrameRecorder.Exception e) {
try {
logger.info("開啓失敗,關閉錄製");
recorder.stop();
return;
} catch (org.bytedeco.javacv.FrameRecorder.Exception e1) {
return;
}
}
}
}
//--順便初始化 音頻錄製線程。
// runNewThread4Audio(0,25);
final ScheduledThreadPoolExecutor exec=new ScheduledThreadPoolExecutor(1);
Runnable crabAudio=localAudioRecordTask(0);
if(crabAudio!=null){
ScheduledFuture tasker=exec.scheduleAtFixedRate(crabAudio,
0,(long)1000/FRAME_RATE,
TimeUnit.MILLISECONDS);
middleMap.put(key4AudioTask,tasker);
}
}
//定義我們的開始時間,當開始時需要先初始化時間戳
if(videoStartTime==0){
videoStartTime=System.currentTimeMillis();
}
//--創建一個timestamp用來寫入幀中
videoTS=1000*(System.currentTimeMillis()-videoStartTime);
//--檢查偏移量
if(videoTS>recorder.getTimestamp()){
logger.info("Lip-flap correction: videoTs is:{} and recorderTimeStamp is :{}",videoTS,recorder.getTimestamp());
//--告訴錄製器寫入這個timestamp
recorder.setTimestamp(videoTS);
}
//--發送幀
try{
recorder.record(ftmp);
}
catch (Exception ed){
ed.printStackTrace();
logger.info("錄製幀發生異常:",ed.getMessage());
}
//--最後,保存上一幀的時間
frameStartTime = frameEndTime;
}
mat.release();
} else {
continue;
}
}
/***視頻數據採集 end**/
}
private FFmpegFrameRecorder initRecorder(String outputFile
, int captureWidth, int captureHeight, int FRAME_RATE
) {
/***recorder推流器初始化 begin**/
/**
* 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);
middleMap.put(key4Recorder,recorder);
/***recorder推流器初始化 end**/
return recorder;
}
/**
* 設置音頻編碼器 最好是系統支持的格式,否則getLine() 會發生錯誤
* 採樣率:44.1k;採樣率位數:16位;立體聲(stereo);是否簽名;true:
* big-endian字節順序,false:little-endian字節順序(詳見:ByteOrder類)
*/
private Runnable localAudioRecordTask(final int AUDIO_DEVICE_INDEX) throws Exception {
/**
* 設置音頻編碼器 最好是系統支持的格式,否則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];
Runnable crabAudio = new Runnable() {
ShortBuffer sBuff = null;
int nBytesRead;
int nSamplesRead;
@Override
public void run() {
if (Thread.interrupted()) {
logger.info("線程已經關閉了,無須執行其他操作。");
return;
}
if (middleMap.get(key4Stop) != null) {
logger.info("錄製聲音時候發現已經停止了。");
try {
} catch (Exception ed) {
ed.printStackTrace();
}
return;
}
if (Thread.interrupted()) {
logger.info("線程已經被停止。");
return;
}
logger.info("讀取音頻數據...");
// 非阻塞方式讀取
nBytesRead = line.read(audioBytes, 0, line.available());
// 因爲我們設置的是16位音頻格式,所以需要將byte[]轉成short[]
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);
sBuff = ShortBuffer.wrap(samples, 0, nSamplesRead);
// 按通道錄製shortBuffer
FFmpegFrameRecorder recorder = (FFmpegFrameRecorder) middleMap.get(key4Recorder);
try {
if (recorder != null) {
recorder.recordSamples(sampleRate, numChannels, sBuff);
}
} catch (Exception ed) {
ed.printStackTrace();
}
}
@Override
protected void finalize() throws Throwable {
sBuff.clear();
sBuff = null;
super.finalize();
}
};
return crabAudio;
}
catch (Exception ed2){
ed2.printStackTrace();
return null;
}
}
public static void main(String[] args) throws Exception {
final String outFile = "/home/too-white/temp/lesson06_output.flv";
final String rtmpUrl = "rtmp://localhost/live/livestream";
Lesson06 test = new Lesson06();
String strOutput = rtmpUrl;
test.recordWebcamAndMicrophone(0, strOutput
, 25
);
}
}