这篇博客应该是相当有分量的博客了。篇幅会比较长,因为内容很多。我尽力的想写的详细,而又不至于繁琐。这之间的程度是很难把握的,话不多说 进入主题。
首先,在这之前,需要对几个类,以及他们的方法的有所了解。
MediaCodec
谷歌对这个类的描述如下,MediaCodec类可用于访问底层媒体编解码器,即编码器/解码器组件。它是Android底层多媒体支持基础架构的一部分(通常与MediaExtractor、MediaSync、MediaMuxer、MediaCrypto、MediaDrm、Image、Surface和AudioTrack一起使用)。重点是 编码解码器,因为系统产生的数据 ,都是原始的数据,需要他进行处理。
原理:
下面这张图,只需要粗略看一看,你只需要知道 MediaCodec 有两个ByteBuffer,一个是输入,一个是输出。这也很好理解。毕竟编码解码器,肯定是要你给它旧数据,它编码解码完,还给你一个新数据。两个ByteBuffer 就相当于两个篮子,接受发送数据。
重要方法:
//返回要用有效数据填充的输入缓冲区的索引,如果当前没有可用的缓冲区,则返回-1。如果timeoutUs == 0,该方法将立即返回;如果timeoutUs < 0,则无限期等待输入缓冲区的可用性;如果timeoutUs > 0,则等待“timeoutUs”微秒。
public int dequeueInputBuffer (long timeoutUs)
这个方法呢,就是返回 输入缓冲区的索引(mediaCodec可以通过索引找到缓冲区)。也就是上面的ByteBuffer。
//通过上面的索引,找到输入缓冲区。
public ByteBuffer getInputBuffer (int index)
注意 上面这个是input
//返回输出缓冲区队列索引,最多阻塞“timeoutUs”微秒。返回已成功解码的输出缓冲区和INFO_*常量之一的索引。
//info 就是描述输出缓冲区数据的,例如时间,大小
public int dequeueOutputBuffer (MediaCodec.BufferInfo info, long timeoutUs)
//通过上面的索引,找到输出缓冲区。
public ByteBuffer getOutputBuffer (int index)
注意 上面这个是output
//释放输出缓冲区 ,这个也好理解,你从输出缓冲区取完数据了,得要把缓冲区清空,放回去,取下一次的数据
public void releaseOutputBuffer (int index, boolean render)
MediaMuxer
谷歌描述:MediaMuxer为muxing基本流提供便利。目前MediaMuxer支持MP4、Webm和3GP文件作为输出。它还支持muxing b帧在MP4自从Android牛轧糖。
上面编码解码完的数据,还需要写入到文件里面,这个类呢,主要就是帮助我们写文件的。
重要方法
//添加具有指定格式的跟踪。
public int addTrack (MediaFormat format)
上面这个呢,如果你了解视频的话就知道,视频里面画面 和声音 是两个不同的东西,但是都在一个文件里面。所以,他们有一个叫信道的东西。比如,声音在1信道,画面在2信道之类的。MediaMuxer只有知道信道,才知道接下来的数据要写到哪里。
//写入数据的方法
public void writeSampleData (int trackIndex, ByteBuffer byteBuf, MediaCodec.BufferInfo bufferInfo)
上面这个 byteBuf参数 就是mediaCodec的输出缓冲区,info 也是mediaCodec输出缓冲区的,我们可以info里面的值进行更改,例如时间,这样就可以暂停视频
附加:
录制视频画面的方向。
public void setOrientationHint (int degrees)
要注意,应该在start()之前调用这个方法
MediaRecorder
这个类呢,综合了上面mediaCodec和mediaMuxer,使用这个类,你可以很轻松的录制到本地。这个只能视频录制到本地,不能用于直播,因为你取不到编码后的数据。
权限申请
录屏权限
录屏权限是需要申请的,每次录制都必须要申请。
//获取MediaProjectionManager ,通过这个类 申请权限,
// 录屏是一个危险的权限,所以每次录屏的时候都得这么申请,用户同意了才行
MediaProjectionManager projectionManager = (MediaProjectionManager) getSystemService(MEDIA_PROJECTION_SERVICE);
//开启activity 然后在onActivityResult回调里面判断 用户是否同意录屏权限
startActivityForResult(projectionManager.createScreenCaptureIntent(), REQUSET_VIDEO);
申请后需要查看用户是否同意,在onActivityResult周期里面查看
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUSET_VIDEO && resultCode == RESULT_OK) {//已经成功获取到权限
//这个是生成虚拟屏幕所需要的
MediaProjection projection = projectionManager.getMediaProjection(resultCode, data);
}
}
注意上面的 MediaProjection,我下面所有的代码,都会用它,一定要记得了。
录音和读写文件权限
在androidManiFest.xml 里面
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
从android6.0以后还需要 动态申请一下。
/**
* 请求录音读写权限
*/
void requestPermission() {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) == PERMISSION_DENIED) {
String[] permission = {Manifest.permission.RECORD_AUDIO};
requestPermissions(permission, REQUSET_AUDIO);
}
}
在onRequestPermissionsResult周期里面得到结果
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == REQUSET_AUDIO) {
for (int i = 0; i < grantResults.length; i++) {
if (grantResults[i] == PERMISSION_GRANTED) {
Toast.makeText(this, "获得权限" + permissions[i], Toast.LENGTH_SHORT).show();
}
}
}
}
MediaRecorder使用范例
注意将上面申请录屏后生成的MediaProjection 传入
//录制视频 放在子线程最好,所以线程
class MediaRecordThread extends Thread {
private int mWidth;//录制视频的宽
private int mHeight;//录制视频的高
private int mBitRate;//比特率 bits per second 这个经过我测试 并不是 一定能达到这个值
private int mDpi;//视频的DPI
private String mDstPath;//录制视频文件存放地点
private MediaRecorder mMediaRecorder;//通过这个类录制
private MediaProjection mediaProjection;//通过这个类 生成虚拟屏幕
private final int FRAME_RATE = 60;//视频帧数 一秒多少张画面 并不一定能达到这个值
private VirtualDisplay virtualDisplay;
MediaRecordThread(int width, int height, int bitrate, int dpi, MediaProjection mediaProjection, String dstPath) {
mWidth = width;
mHeight = height;
mBitRate = bitrate;
mDpi = dpi;
this.mediaProjection = mediaProjection;
mDstPath = dstPath;
}
@Override
public void run() {
try {
//先实例化
initMediaRecorder();
//下面这个方法的 width height 并不是录制视频的宽高。他更明显是虚拟屏幕的宽高
//注意 mediaRecorder.getSurface() 这里我们mediaRecorder的surface 传递给虚拟屏幕,
// 虚拟屏幕显示的内容就会反映在这个surface上面,自然也就可以录制了
virtualDisplay = mediaProjection.createVirtualDisplay("luing", mWidth, mHeight, mDpi,
DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC, mediaRecorder.getSurface(), null, null
);
//开始
mMediaRecorder.start();
Zprint.log(this.getClass(), "录屏线程内部开始工作");
} catch (IllegalStateException | IOException e) {
// e.printStackTrace();
Zprint.log(this.getClass(), " 异常 ", e.toString());
}
}
//实例化MediaRecordor
void initMediaRecorder() throws IOException {
mMediaRecorder = mediaRecorder = new MediaRecorder();
//设置视频来源 录屏嘛 肯定是使用一个Surface作为视频源,如果录制视频的话 就是使用摄像头作为来源了 CAMERA
mediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE);
//设置要用于录制的音频源,必须在setOutputFormat()之前调用。参数有很多,英文注释也很简单,下面这个是录制麦克风,也就是外放的
//记住 这个必要获得录制视频权限才行,要不然 报错
mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
//设置录制期间生成的输出文件的格式。必须在prepare()之前调用
mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
//录制文件存放位置
mediaRecorder.setOutputFile(mDstPath);
//录制视频的宽高
mediaRecorder.setVideoSize(mWidth, mHeight);
//FPS
mediaRecorder.setVideoFrameRate(FRAME_RATE);
//比特率
mediaRecorder.setVideoEncodingBitRate(mBitRate);
//视频编码格式
mediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264);
//音频编码格式
mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
//准备 到这里 就可以开始录制视频了
mediaRecorder.prepare();
}
/**
* 释放资源
*/
void release() {
if (mediaRecorder != null) {
mediaRecorder.setOnErrorListener(null);
mediaRecorder.stop();
mediaRecorder.reset();
mediaRecorder.release();
mediaRecorder = null;//help GC
}
if (virtualDisplay != null) {
virtualDisplay.release();
virtualDisplay = null;//help GC
}
if (mediaProjection != null) {
mediaProjection.stop();
mediaProjection = null;//help GC
}
}
}
这个类很简单 ,没什么说的 ,注释也很多。
MediaCodec , MediaMuxer,AudioRecord
实现录屏加录音 同步方式, 可直播,可推流
//录制屏幕 加声音 同步方式
class AudioRecorderThread extends Thread {
private AudioRecord mAudiorecord;//录音类
private MediaMuxer mMediaMuxer;//通过这个将视频流写入本地文件,如果直播的话 不需要这个
// 音频源:音频输入-麦克风 我使用其他格式 就会报错
private final static int AUDIO_SOURCE = MediaRecorder.AudioSource.MIC;
// 采样率
// 44100是目前的标准,但是某些设备仍然支持22050,16000,11025
// 采样频率一般共分为22.05KHz、44.1KHz、48KHz三个等级
private final static int AUDIO_SAMPLE_RATE = 44100;
// 音频通道 默认的 可以是单声道 立体声道
private final int AUDIO_CHANNEL = AudioFormat.CHANNEL_IN_DEFAULT;
// 音频格式:PCM编码 返回音频数据的格式
private final int AUDIO_ENCODING = AudioFormat.ENCODING_PCM_16BIT;
//记录期间写入音频数据的缓冲区的总大小(以字节为单位)。
private int audioBufferSize = 0;
//缓冲数组 ,用来读取audioRecord的音频数据
private byte[] byteBuffer;
private int audioIndex;//通过MediaMuxer 向本地文件写入数据时候,这个标志是用来确定信道的
private int videoIndex;//上同
private MediaCodec mAudioMediaCodec;//音频编码器
private MediaCodec mVideoMediaCodec;//视频编码器
private MediaFormat audioFormat;//音频编码器 输出数据的格式
private MediaFormat videoFormat;//视频编码器 输出数据的格式
private MediaProjection mediaProjection;//通过这个类 生成虚拟屏幕
private Surface surface;//视频编码器 生成的surface ,用于充当 视频编码器的输入源
private VirtualDisplay virtualDisplay; //虚拟屏幕
//这个是每次在编码器 取数据的时候,这个info 携带取出数据的信息,例如 时间,大小 类型之类的 关键帧 可以通过这里的flags辨别
private MediaCodec.BufferInfo audioInfo = new MediaCodec.BufferInfo();
private MediaCodec.BufferInfo videoInfo = new MediaCodec.BufferInfo();
private volatile boolean isRun = true;//用于控制 是否录制,这个无关紧要
private int mWidth;//录制视频的宽
private int mHeight;//录制视频的高
private int mBitRate;//比特率 bits per second 这个经过我测试 并不是 一定能达到这个值
private int mDpi;//视频的DPI
private String mDstPath;//录制视频文件存放地点
private final int FRAME_RATE = 60;//视频帧数 一秒多少张画面 并不一定能达到这个值
public AudioRecorderThread(int width, int height, int bitrate, int dpi, MediaProjection mediaProjection, String dstPath) {
this.mediaProjection = mediaProjection;
this.mWidth = width;
this.mHeight = height;
mBitRate = bitrate;
mDpi = dpi;
mDstPath = dstPath;
}
@Override
public void run() {
super.run();
try {
//实例化 AudioRecord
initAudioRecord();
//实例化 写入文件的类
initMediaMuxer();
//实例化 音频编码器
initAudioMedicode();
//实例化 视频编码器
initVideoMedicodec();
//开始
mAudioMediaCodec.start();
mVideoMediaCodec.start();
int timeoutUs = -1;//这个主要是为了 第一次进入while循环 视频编码器 能阻塞到 有视频数据输出 才运行
String TAG = "audio";
while (isRun) {
//获取 输出缓冲区的索引 通过索引 可以去到缓冲区,缓冲区里面存着 编码后的视频数据 。 timeoutUs为负数的话,会一直阻塞到有缓冲区索引,0的话 立刻返回
int videoOutputID = mVideoMediaCodec.dequeueOutputBuffer(videoInfo, timeoutUs);
Log.d(TAG, "video flags " + videoInfo.flags);
timeoutUs = 0;//第二次 视频编码器 就不需要 阻塞了 0 立刻返回
//索引大于等于0 就代表有数据了
if (videoOutputID >= 0) {
Zprint.log(this.getClass(), "VIDEO 输出", videoOutputID, videoInfo.presentationTimeUs);
//flags是2的时候 代表输出的数据 是配置信息,不是媒体信息
if (videoInfo.flags != 2) {
//得到缓冲区
//这里就可以取出数据 进行网络传输
ByteBuffer outBuffer = mVideoMediaCodec.getOutputBuffer(videoOutputID);
outBuffer.flip();//准备读取
//写入文件中 注意 videoIndex
mMediaMuxer.writeSampleData(videoIndex, outBuffer, videoInfo);
}
//释放缓冲区,毕竟缓冲区一共就两个 一个输入 一个输出,用完是要还回去的
mVideoMediaCodec.releaseOutputBuffer(videoOutputID, false);
} else if (videoOutputID == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {//输出格式有变化
Zprint.log(this.getClass(), "video Format 改变");
videoFormat = mVideoMediaCodec.getOutputFormat();//得到新的输出格式
videoIndex = mMediaMuxer.addTrack(videoFormat);//重新确定信道
}
//得到 输入缓冲区的索引
int audioInputID = mAudioMediaCodec.dequeueInputBuffer(0);
//也是大于等于0 代表 可以输入数据啦
if (audioInputID >= 0) {
Zprint.log(this.getClass(), "audio 输入", audioInputID);
ByteBuffer audioInputBuffer = mAudioMediaCodec.getInputBuffer(audioInputID);
audioInputBuffer.clear();
//从 audiorecord 里面 读取原始的音频数据
int read = mAudiorecord.read(byteBuffer, 0, audioBufferSize);
if (read < audioBufferSize) {
System.out.println(" 读取的数据" + read);
}
//上面read可能小于audioBufferSize 要注意
audioInputBuffer.put(byteBuffer, 0, read);
//入列 注意下面的时间,这个是确定这段数据 时间的 ,视频音频 都是一段段的数据,每个数据都有时间 ,这样播放器才知道 先播放那个数据
// 串联起来 就是连续的了
mAudioMediaCodec.queueInputBuffer(audioInputID, 0, read, System.nanoTime() / 1000L, 0);
}
//音频输出
int audioOutputID = mAudioMediaCodec.dequeueOutputBuffer(audioInfo, 0);
Log.d(TAG, "audio flags " + audioInfo.flags);
if (audioOutputID >= 0) {
Zprint.log(this.getClass(), "audio 输出", audioOutputID, audioInfo.presentationTimeUs);
audioInfo.presentationTimeUs = videoInfo.presentationTimeUs;//保持 视频和音频的统一,防止 时间画面声音 不同步
if (audioInfo.flags != 2) {
//这里就可以取出数据 进行网络传输
ByteBuffer audioOutBuffer = mAudioMediaCodec.getOutputBuffer(audioOutputID);
audioOutBuffer.limit(audioInfo.offset + audioInfo.size);//这是另一种 和上面的 flip 没区别
audioOutBuffer.position(audioInfo.offset);
mMediaMuxer.writeSampleData(audioIndex, audioOutBuffer, audioInfo);//写入
}
//释放缓冲区
mAudioMediaCodec.releaseOutputBuffer(audioOutputID, false);
} else if (audioOutputID == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
Zprint.log(this.getClass(), "audio Format 改变");
audioFormat = mAudioMediaCodec.getOutputFormat();
audioIndex = mMediaMuxer.addTrack(audioFormat);
//注意 这里 只在start 视频哪里没有这个,这个方法只能调用一次
mMediaMuxer.start();
}
}
//释放资源
stopRecorder();
} catch (Exception e) {
e.printStackTrace();
}
}
//实例化 AUDIO 的编码器
void initAudioMedicode() throws IOException {
audioFormat = MediaFormat.createAudioFormat(MediaFormat.MIMETYPE_AUDIO_AAC, AUDIO_SAMPLE_RATE, AUDIO_CHANNEL);
audioFormat.setInteger(MediaFormat.KEY_BIT_RATE, 96000);//比特率
//描述要使用的AAC配置文件的键(仅适用于AAC音频格式)。
audioFormat.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC);
audioFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, audioBufferSize << 1);//最大输入
//这里注意 如果 你不确定 你要生成的编码器类型,就通过下面的 通过类型生成编码器
mAudioMediaCodec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_AUDIO_AAC);
//配置
mAudioMediaCodec.configure(audioFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
}
//实例化 VIDEO 的编码器
void initVideoMedicodec() throws IOException {
//这里的width height 就是录制视频的分辨率,可以更改 如果这里的分辨率小于 虚拟屏幕的分辨率 ,你会发现 视频只录制了 屏幕部分内容
videoFormat = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, mWidth, mHeight);
videoFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
videoFormat.setInteger(MediaFormat.KEY_BIT_RATE, mBitRate);//比特率 bit单位
videoFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 60);//FPS
videoFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);//关键帧 是完整的一张图片,其他的都是部分图片
//通过类型创建编码器 同理 创建解码器也是一样
mVideoMediaCodec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC);
//配置
mVideoMediaCodec.configure(videoFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
//让视频编码器 生成一个弱引用的surface, 这个surface不会保证视频编码器 不被回收,这样 编码视频的时候 就不需要 传输数据进去了
surface = mVideoMediaCodec.createInputSurface();
//创建虚拟屏幕,让虚拟屏幕内容 渲染在上面的surface上面 ,这样 才能 不用传输数据进去
virtualDisplay = mediaProjection.createVirtualDisplay("video", mWidth, mHeight, dpi, DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC, surface, null, null);
}
//录音的类,用于给音频编码器 提供原始数据
void initAudioRecord() {
//得到 音频录制时候 最小的缓冲区大小
audioBufferSize = AudioRecord.getMinBufferSize(AUDIO_SAMPLE_RATE, AUDIO_CHANNEL, AUDIO_ENCODING);
byteBuffer = new byte[audioBufferSize];
//两种方式 都可以
// mAudiorecord = new AudioRecord(AUDIO_SOURCE, AUDIO_SAMPLE_RATE, AUDIO_CHANNEL, AUDIO_ENCODING, audioBufferSize);
//通过builder方式创建
mAudiorecord = new AudioRecord.Builder()
.setAudioSource(MediaRecorder.AudioSource.MIC)
.setAudioFormat(new AudioFormat.Builder()
.setEncoding(AudioFormat.ENCODING_PCM_16BIT)
.setSampleRate(32000)
.setChannelMask(AudioFormat.CHANNEL_IN_MONO)
.build())
.setBufferSizeInBytes(audioBufferSize)
.build();
//开始录制,这里可以检查一下状态,但只要代码无误,检查是无需的 state
mAudiorecord.startRecording();
}
//如果 要录制mp4文件的话,需要调用这个方法 创建 MediaMuxer
private void initMediaMuxer() throws Exception {
//注意格式 创建录制的文件
String filePath = filePath("luyi.mp4");
//实例化 MediaMuxer 编码器取出的数据,通过它写入文件中
mMediaMuxer = new MediaMuxer(filePath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
}
//释放资源
void stopRecorder() {
if (mVideoMediaCodec != null) {
mVideoMediaCodec.stop();
mVideoMediaCodec.release();
mVideoMediaCodec = null;
}
if (mAudioMediaCodec != null) {
mAudioMediaCodec.stop();
mAudioMediaCodec.release();
mAudioMediaCodec = null;
}
if (mAudiorecord != null) {
mAudiorecord.stop();
mAudiorecord.release();
mAudiorecord = null;
}
mMediaMuxer.stop();
mMediaMuxer.release();
mMediaMuxer = null;
virtualDisplay.release();
virtualDisplay = null;
}
}
这个类呢,首先用AudioRecord来获取音频数据,用virtualDisplay来获取视频数据。因为,有音频和视频,所以 要用两个mediaCodec 来进行工作。mediaCodec有两种方式,一种是同步的,像上面这样,还有一种是异步的。两个codec配合的话,我感觉还是同步的比较好。异步也是可以实现的,只是较为繁琐。需要用到队列之类的。
细心的读者会发现,上面videoCodec 是没有取出输入缓冲区的。因为 ,它的输入工作 被surface 代替了。virtualDisplay的内容直接渲染在videoCodec的surface 里面,充当了 输入数据。
还有如果,你想进行直播的话,取出输出缓冲区的数据传输,要注意 还有头部信息,这个下面例子中。
MediaCodec 录屏 异步方式实现
//录屏 异步模式 没有声音的
class ScreenAsyn implements Runnable {
private MediaProjection mediaProjection;
private MediaFormat videoFormat;
private MediaCodec mVideoMediaCodec;
private VirtualDisplay virtualDisplay;
private MediaMuxer mediaMuxer;
private int videoIndex;
private int mWidth;//录制视频的宽
private int mHeight;//录制视频的高
private int mBitRate;//比特率 bits per second 这个经过我测试 并不是 一定能达到这个值
private int mDpi;//视频的DPI
private String mDstPath;//录制视频文件存放地点
private final int FRAME_RATE = 60;//视频帧数 一秒多少张画面 并不一定能达到这个值
public ScreenAsyn(int width, int height, int bitrate, int dpi, MediaProjection mediaProjection, String dstPath) {
this.mWidth = width;
mHeight = height;
mBitRate = bitrate;
mDpi = dpi;
this.mediaProjection = mediaProjection;
mDstPath = dstPath;
}
@Override
public void run() {
try {
mediaMuxer = new MediaMuxer(mDstPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
Surface surface;
//这里的width height 就是录制视频的分辨率,可以更改 如果这里的分辨率小于 虚拟屏幕的分辨率 ,你会发现 视频只录制了 屏幕部分内容
videoFormat = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, mWidth, mHeight);
videoFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
videoFormat.setInteger(MediaFormat.KEY_BIT_RATE, mBitRate);//比特率 bit单位
videoFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 60);//FPS
videoFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);//关键帧 是完整的一张图片,其他的都是部分图片
//通过类型创建编码器 同理 创建解码器也是一样
mVideoMediaCodec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC);
mVideoMediaCodec.setCallback(new MediaCodec.Callback() {
@Override
public void onInputBufferAvailable(@NonNull MediaCodec codec, int index) {
}
@Override
public void onOutputBufferAvailable(@NonNull MediaCodec codec, int index, @NonNull MediaCodec.BufferInfo info) {
Zprint.log(this.getClass(), "info", info.offset, info.size, info.presentationTimeUs);
ByteBuffer outBuffer = codec.getOutputBuffer(index);
outBuffer.flip();
mediaMuxer.writeSampleData(videoIndex, outBuffer, info);
codec.releaseOutputBuffer(index, false);
}
@Override
public void onError(@NonNull MediaCodec codec, @NonNull MediaCodec.CodecException e) {
}
@Override
public void onOutputFormatChanged(@NonNull MediaCodec codec, @NonNull MediaFormat format) {
//这是h.264需要的
ByteBuffer sps = format.getByteBuffer("csd-0"); // SPS
ByteBuffer pps = format.getByteBuffer("csd-1"); // PPS
//VP9 需要的
ByteBuffer CodecPrivate = format.getByteBuffer("csd-0"); //
videoIndex = mediaMuxer.addTrack(format);
mediaMuxer.start();
}
});
mVideoMediaCodec.configure(videoFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
surface = mVideoMediaCodec.createInputSurface();
virtualDisplay = mediaProjection.createVirtualDisplay("asyn", mWidth, mHeight, mDpi, mBitRate, surface, null, null);
mVideoMediaCodec.start();
} catch (IOException e) {
e.printStackTrace();
}
}
//暂停录制
void stopRecord() {
if (mVideoMediaCodec != null) {
mVideoMediaCodec.stop();
mVideoMediaCodec.release();
mVideoMediaCodec = null;
}
if (mediaMuxer != null) {
mediaMuxer.release();
mediaMuxer = null;
}
if (virtualDisplay != null) {
virtualDisplay.release();
}
}
}
这是异步实现的,个人感觉异步的实现比较好。
下面这个就是进行网络直播前,流的一些必要信息。
流的数据:
取出上面的 outBuffer 数据,就是媒体数据了,上面这个是H.264格式的数据。
总结
网上的这方面资料比较少,我研究了一段时间,写出来。在这里说一下个人心得吧。一开始可以看看网上的博客,然后,你如果想深入,那么还是看开发者文档。
附带 截图
/**
* 截图
* @param mediaProjection
* @return bitmap
*/
public Bitmap screenShot(MediaProjection mediaProjection){
Objects.requireNonNull(mediaProjection);
ImageReader imageReader = ImageReader.newInstance(1080, 1920, PixelFormat.RGBA_8888, 60);
VirtualDisplay virtualDisplay = mediaProjection.createVirtualDisplay("screen", width, height, dpi, DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC,imageReader.getSurface(), null, null);
//这个是取 现在最新的图片
Image image = imageReader.acquireLatestImage();
//也可以挨个取图片
// Image image = imageReader.acquireNextImage();
return image2Bitmap(image);
}
public static Bitmap image2Bitmap(Image image) {
if (image == null) {
System.out.println("image 为空");
return null;
}
int width = image.getWidth();
int height = image.getHeight();
System.out.println(width+" "+height);
final Image.Plane[] planes = image.getPlanes();
final ByteBuffer buffer = planes[0].getBuffer();
int pixelStride = planes[0].getPixelStride();
int rowStride = planes[0].getRowStride();
int rowPadding = rowStride - pixelStride * width;
Bitmap bitmap = Bitmap.createBitmap(width+ rowPadding / pixelStride , height, Bitmap.Config.ARGB_8888);
bitmap.copyPixelsFromBuffer(buffer);
/* //压缩图片
Matrix matrix = new Matrix();
matrix.setScale(0.5F, 0.5F);
System.out.println(bitmap.isMutable());
bitmap = Bitmap.createBitmap(bitmap, 0, 0, width, height, matrix, false);
*/
image.close();
return bitmap;
}