前言
查看了相关文章然后一笔一笔打代码再调试成功出结果,
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
);
}
}
来源:CSDN
作者:码农下的天桥
链接:https://blog.csdn.net/cdnight/article/details/103887012