????????我们上一节了解了MediaExtractor、MediaMuxer、MediaFormat、MediaCodec.BufferInfo。重复的内容我就不再赘述了,假如有上面的四个的一些补充还是会写一下。接下来我们学习MediaCodec,本节篇幅会比较长,知识点较多,请耐心品味。
一、MediaCodec
????????? MediaCodec类可用于访问低级媒体编解码器,即编码器/解码器组件。它是Android低级多媒体支持基础设施的一部分(通常与MediaExtractor, MediaSync, MediaMuxer, MediaCrypto, MediaDrm, Image, Surface,以及AudioTrack.)。
1.1数据类型
????????编解码器处理三种数据:压缩数据、原始音频数据和原始视频数据。? 所有这三种数据都可以使用ByteBuffers,但您应该使用Surface用于原始视频数据以提高编解码器性能。Surface使用原生视频缓冲区,而不将其映射或复制到ByteBuffers因此,它的效率更高。
????????使用Surface时,通常无法访问原始视频数据,但可以使用ImageReader类来访问不安全的解码(原始)视频帧。这可能仍然比使用字节缓冲区更有效,因为一些本机缓冲区可以映射到直接的字节缓冲区。使用ByteBuffer模式时,可以使用Image类别和getInput/OutputImage(int)访问原始视频帧。
(1)压缩缓冲区
????????压缩数据可以作为解码器的输入、编码器的输出,需要指定数据的格式,这样codec才知道如何处理这些压缩数据
- MediaFormat.KEY_MIME格式类型。
- 对于视频类型,通常是一个单独的压缩视频帧。
- 对于音频数据,通常是一个单独的访问单元(一个编码的音频段通常包含由格式类型决定的几毫秒的音频),但是这个要求稍微宽松一些,因为一个buffer可能包含多个编码的音频访问单元。
- 在这两种情况下,buffer都不会在任意字节边界上开始或结束,而是在帧/访问单元边界上开始或结束,除非它们被BUFFER_FLAG_PARTIAL_FRAME标记。
(2)原始音频缓冲区
????????原始音频数据 — PCM音频数据帧,这是每个通道按通道顺序的一个样本。
(3)原始视频缓冲区
????????在ByteBuffer模式下,视频buffer根据它们的MediaFormat.KEY_COLOR_FORMAT进行布局。可以从getCodecInfo(). MediaCodecInfo.getCapabilitiesForType.CodecCapability.colorFormats获取支持的颜色格式。视频编解码器可以支持三种颜色格式:
- native raw video format: CodecCapabilities.COLOR_FormatSurface,可以与输入/输出的Surface一起使用。
- flexible YUV buffers 例如CodecCapabilities.COLOR_FormatYUV420Flexible, 可以使用getInput/OutputImage(int)与输入/输出Surface一起使用,也可以在ByteBuffer模式下使用。
- other, specific formats: 通常只支持ByteBuffer模式。有些颜色格式是厂商特有的,其他定义在CodecCapabilities。对于等价于flexible格式的颜色格式,可以使用getInput/OutputImage(int)。
????????从Build.VERSION_CODES.LOLLIPOP_MR1.开始,所有视频编解码器都支持flexible的YUV 4:2:0 buffer。
1.2 MediaCodec API?
(1)MediaCodec创建:
- createDecoderByType/createEncoderByType:根据特定MIME类型(如"video/avc")创建codec。然而,这不能用于注入特征,并且可能创建不能处理特定期望媒体格式的编解码器。
- createByCodecName:知道组件的确切名称(如OMX.google.mp3.decoder)的时候,根据组件名创建codec。使用MediaCodecList可以获取组件的名称。
(2)MediaCodec配置与启动:
四个参数:
MediaFormat format :输入数据的格式(解码器)或输出数据的所需格式(编码器)。传null等同于传递MediaFormat.MediaFormat作为空的MediaFormat。Surface surface :指定Surface,用于解码器输出的渲染。如果编解码器不生成原始视频输出(例如,不是视频解码器)和/或想配置解码器输出ByteBuffer,则传null。MediaCrypto crypto :指定一个crypto对象,用于对媒体数据进行安全解密。对于非安全的编解码器,传null。int flags :当组件是编码器时,flags指定为常量CONFIGURE_FLAG_ENCODE。
(3)buffer处理的接口:
MediaCodec可以处理具体的视频流,主要有这几个方法:
- getInputBuffers:获取需要编码数据的输入流队列,返回的是一个ByteBuffer数组?
- getInputBuffer(index) : 获取InputBuffers数组index下标的ByteBuffer
- queueInputBuffer:输入流入队列?
- dequeueInputBuffer:从输入流队列中取数据进行编码操作?
- getOutputBuffers:获取编解码之后的数据输出流队列,返回的是一个ByteBuffer数组?
- getOutputBuffer(index) : 获取OutputBuffers数组index下标的ByteBuffer
- dequeueOutputBuffer:从输出队列中取出编码操作之后的数据?
- releaseOutputBuffer:处理完成,释放ByteBuffer数据?
(4)处理完之后的操作:
- flush:清空的输入和输出端口。
- stop:终止decode/encode会话
- release:释放编解码器实例使用的资源。
二、MediaFormat 一些属性理解
????????MediaFormat的格式被指定为键/值对。keys是字符串。values可以是整数、长整型、浮点型、字符串或ByteBuffer。(要素元数据被指定为? 字符串/布尔? 对。)
2.1 所有音频/视频格式通用的键,所有未标记为可选的键都是强制性的:
2.2 视频格式有以下键:
2.3?音频格式有以下键:
2.4?字幕格式有以下关键字:
线格式的类型。KEY_LANGUAGE 线内容的语言。KEY_CAPTION_SERVICE_NUMBER (同Internationalorganizations)国际组织可选,隐藏字幕服务或频道号。
2.5?图像格式有以下键:
至于值,常用的也就那些,我就不提供了,自己去开发者文档查找。
三、Image?
与媒体源一起使用的单个完整图像缓冲区,例如?MediaCodec 或?CameraDevice ?。
该类允许通过一个或多个ByteBuffers 高效地直接应用程序访问图像的像素数据。?每个缓冲区都封装在描述该平面中像素数据布局的Image.Plane 中。?由于这种直接访问方式,与Bitmap 类不同,图像不能直接用作UI资源。
由于图像通常由硬件组件直接生成或使用,因此它们是整个系统共享的有限资源,应在不再需要时立即关闭。
例如,当使用ImageReader 类从各种媒体源读出图像时,一旦达到the maximum outstanding image count ?,不关闭旧的图像对象将阻止新图像的可用性。?发生这种情况时,获取新图像的函数通常会抛出IllegalStateException ?。
3.1 getPlanes() 方法
????????获取该图像的像素平面阵列,平面的数量由图像的格式决定。这个需要了解ImageFormat的格式了,这个以后再了解。
3.2?ImageFormat.getBitsPerPixel()方法
????????使用此函数可检索ImageFormat的每个像素的位数。
这个在此就点一下算了,之后在单独开一节来具体讲解。
四、demo实现
//解码操作,返回YUV加载的bitmap图片
public class ImageShowActivity extends Activity {
private TextView tv_yun;
//图片的个数
private int imageNum = 0;
private final String mVideoPath = Environment.getExternalStorageDirectory() + "/Pictures/music.mp4";
private MediaExtractor extractor;//用于解封装
private MediaFormat videoFormat;//保存视频轨道的媒体格式
private MediaCodec mediaCodec;//解码视频轨道资源
private int rotation;
private long duration;
//用于代表YUV的种类
public static final int YUV420P = 0;
public static final int YUV420SP = 1;
public static final int NV21 = 2;
//保存YUV数据的byte[]
private byte[] bytes;
private static int width;
private static int height;
//1.创建MediaExtractor和MediaCodec : MediaExtractor负责解封装,MediaCodec负责解码视频轨道资源
//2.解码获取图片,并进行转换:YUV_420_888-->NV21
//3.YuvImage 加载nv21,转化成Bitmap用于显示。
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_image_show);
tv_yun = findViewById(R.id.tv_image_yuv);
GetVideo();
}
}
//获取到视频轨道资源
private void GetVideo(){
extractor = new MediaExtractor();
try {
extractor.setDataSource(mVideoPath);
//获取轨道个数
int trackCount = extractor.getTrackCount();
for (int i = 0; i <trackCount ; i++) {
//获取某一轨道的媒体格式
MediaFormat trackFormat = extractor.getTrackFormat(i);
if(trackFormat.getString(MediaFormat.KEY_MIME).contains("video")){
videoFormat =trackFormat;
extractor.selectTrack(i);
break;
}
}
if(videoFormat == null){
throw new IllegalArgumentException("没有获取到视频的Format");
}
// 其中MediaFormat.KEY_COLOR_FORMAT 需要设置成COLOR_FormatYUV420Flexible
// MediaCodec的所有硬件解码都支持这种格式
videoFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT,COLOR_FormatYUV420Flexible);
videoFormat.setInteger(MediaFormat.KEY_WIDTH,videoFormat.getInteger(MediaFormat.KEY_WIDTH));
videoFormat.setInteger(MediaFormat.KEY_HEIGHT,videoFormat.getInteger(MediaFormat.KEY_HEIGHT));
if(videoFormat.containsKey(MediaFormat.KEY_ROTATION)){
//获取旋转多少的值
rotation = videoFormat.getInteger(MediaFormat.KEY_ROTATION);
}
//内容持续时间
duration = videoFormat.getLong(MediaFormat.KEY_DURATION);
//MediaCodec创建:createDecoderByType/createEncoderByType:根据特定MIME类型(如"video/avc")创建codec。
//createByCodecName:知道组件的确切名称(如OMX.google.mp3.decoder)的时候,根据组件名创建codec。使用MediaCodecList可以获取组件的名称。
mediaCodec = MediaCodec.createDecoderByType(videoFormat.getString(MediaFormat.KEY_MIME));
//configure:配置解码器或者编码器。
// 参数1:MediaFormat ,参数2:Surface ,参数3:MediaCrypto ,参数4:flags
mediaCodec.configure(videoFormat,null,null,0);
mediaCodec.start();
//开始解码
processByExtractor();
} catch (IOException e) {
e.printStackTrace();
}
}
//开始解码,获取帧序列
private void processByExtractor() {
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
long timeOut = 5* 1000;//5ms
boolean inputDone = false;
boolean outputDone = false;
ByteBuffer[] inputBuffers = null;
// if(Build.VERSION.SDK_INT< Build.VERSION_CODES.LOLLIPOP){
// //开始喂数据
// inputBuffers = mediaCodec.getInputBuffers();
// }
inputBuffers = mediaCodec.getInputBuffers();
//开始解码
int count = 0;
while (!outputDone){
if(!inputDone){
//喂数据
//如果是要获取所有帧序列,则不需要使用seekTo方法。
//extractor.seekTo(count * intervalMs * 1000,MediaExtractor.SEEK_TO_PREVIOUS_SYNC);
int inputBufferIndex = mediaCodec.dequeueInputBuffer(timeOut);
if(inputBufferIndex >= 0){
ByteBuffer inputBuffer;
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.LOLLIPOP){
inputBuffer = mediaCodec.getInputBuffer(inputBufferIndex);
}else {
inputBuffer = inputBuffers[inputBufferIndex];
}
int sampleDate = extractor.readSampleData(inputBuffer,0);
if(sampleDate > 0 && count * 1000 <= duration){
long sampleTime = extractor.getSampleTime();
int sampleFlags = extractor.getSampleFlags();
mediaCodec.queueInputBuffer(inputBufferIndex,0,sampleDate,sampleTime,0);
extractor.advance();
count++;
}else {
//小于0,就说明读完了
mediaCodec.queueInputBuffer(inputBufferIndex,0,0,0,MediaCodec.BUFFER_FLAG_END_OF_STREAM);
inputDone = true;
}
}
}
if(!outputDone){
//获取数据
int status = mediaCodec.dequeueOutputBuffer(bufferInfo,timeOut);
if(status == MediaCodec.INFO_TRY_AGAIN_LATER){
}else if(status == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED){
}else if(status == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED){
}else {
if((bufferInfo.flags&MediaCodec.BUFFER_FLAG_END_OF_STREAM)!=0){
outputDone = true;
}
boolean doRender = (bufferInfo.size !=0);
//获取图片并保存,getOutputImage格式是YUV_420_888
Image image = mediaCodec.getOutputImage(status);
mediaCodec.getOutputBuffer(status);
System.out.println("成功获取到图片"+"SSSSSSSSSSSSSSSSSSSSSSS");
imageNum++;
//dateFromImage(image);
//使用新方法来获取yuv数据
bytes = getBytesFromImageAsType(image,2);
//根据yuv数据获取Bitmap
Bitmap bitmap = getBitmapFromYUV(bytes,width,height,rotation);
//保存图片
if(bitmap != null){
//显示图片
String businesslogofile=Environment.getExternalStorageDirectory()+"/Pictures/logo"+imageNum+".png";
File file = new File(businesslogofile);
try {
bitmap.compress(Bitmap.CompressFormat.JPEG,100,new FileOutputStream(file));
} catch (FileNotFoundException e) {
e.printStackTrace();
}
System.out.println("图片导入成功");
}
mediaCodec.releaseOutputBuffer(status,doRender);
//这里先尝试获取第一张图片
//break;
}
}
}
}
细将一下MediaCodec的四个方法:
1. dequeueInputBuffer:
public final int dequeueInputBuffer(long timeoutUs)
- 返回用于填充有效数据的输入buffer的索引,如果当前没有可用的buffer,则返回-1。
- long timeoutUs:等待可用的输入buffer的时间。
- 如果timeoutUs == 0,则立即返回。
- 如果timeoutUs < 0,则无限期等待可用的输入buffer。
- 如果timeoutUs > 0,则等待“timeoutUs”微秒。
2.?queueInputBuffer:在指定索引处填充输入buffer后,使用queueInputBuffer将buffer提交给组件。
许多codec要求实际压缩的数据流之前必须有“特定于codec的数据”,即用于初始化codec的设置数据,如
- AVC视频中的PPS/SPS。
- vorbis音频中的code tables。
public native final void queueInputBuffer(
int index,
int offset, int size, long presentationTimeUs, int flags)
- int index:以前调用dequeueInputBuffer(long)返回的输入buffer的索引。
- int offset:数据开始时输入buffer中的字节偏移量。
- int size:有效输入数据的字节数。
- long presentationTimeUs:此buffer的PTS(以微秒为单位)。
- int flags:一个由BUFFER_FLAG_CODEC_CONFIG和BUFFER_FLAG_END_OF_STREAM标志组成的位掩码。虽然没有被禁止,但是大多数codec并不对输入buffer使用BUFFER_FLAG_KEY_FRAME标志。
- BUFFER_FLAG_END_OF_STREAM:用于指示这是输入数据的最后一部分。
- BUFFER_FLAG_CODEC_CONFIG:通过指定这个标志,可以在start()或flush()之后直接提交特定于codec的数据buffer。但是,如果您使用包含这些密钥的媒体格式配置编解码器,它们将在启动后由MediaCodec直接自动提交。因此,不建议使用BUFFER_FLAG_CODEC_CONFIG标志,只建议高级用户使用。
3. dequeueOutputBuffer:从MediaCodec获取输出buffer。
public final int dequeueOutputBuffer(
@NonNull BufferInfo info, long timeoutUs)
- 返回值:已成功解码的输出buffer的索引或INFO_*常量之一(INFO_TRY_AGAIN_LATER, INFO_OUTPUT_FORMAT_CHANGED 或 INFO_OUTPUT_BUFFERS_CHANGED)。
- 返回INFO_TRY_AGAIN_LATER而timeoutUs指定为了非负值,表示超时了。
- 返回INFO_OUTPUT_FORMAT_CHANGED表示输出格式已更改,后续数据将遵循新格式。
- BufferInfo info:输出buffer的metadata。
- long timeoutUs:含义同dequeueInputBuffer中的timeoutUs参数。
4. releaseOutputBuffer:使用此方法将输出buffer返回给codec或将其渲染在输出surface。
public void releaseOutputBuffer (int index, boolean render)
- boolean render:如果在配置codec时指定了一个有效的surface,则传递true会将此输出buffer在surface上渲染。一旦不再使用buffer,该surface将把buffer释放回codec。
//根据image获取yuv值-------------------NEW
public static byte[] getBytesFromImageAsType(Image image, int type){
try {
//获取源数据,如果是YUV格式的数据planes.length = 3
//plane[i]里面的实际数据可能存在byte[].length <= capacity (缓冲区总大小)
final Image.Plane[] planes = image.getPlanes();
//数据有效宽度,一般的,图片width <= rowStride,这也是导致byte[].length <= capacity的原因
// 所以我们只取width部分
width = image.getWidth();
height = image.getHeight();
//此处用来装填最终的YUV数据,需要1.5倍的图片大小,因为Y U V 比例为 4:1:1 (这里是YUV_420_888)
byte[] yuvBytes = new byte[width * height * ImageFormat.getBitsPerPixel(ImageFormat.YUV_420_888) / 8];
//目标数组的装填到的位置
int dstIndex = 0;
//临时存储uv数据的
byte uBytes[] = new byte[width * height / 4];
byte vBytes[] = new byte[width * height / 4];
int uIndex = 0;
int vIndex = 0;
int pixelsStride, rowStride;
for (int i = 0; i < planes.length; i++) {
pixelsStride = planes[i].getPixelStride();
rowStride = planes[i].getRowStride();
ByteBuffer buffer = planes[i].getBuffer();
//如果pixelsStride==2,一般的Y的buffer长度=640*480,UV的长度=640*480/2-1
//源数据的索引,y的数据是byte中连续的,u的数据是v向左移以为生成的,两者都是偶数位为有效数据
byte[] bytes = new byte[buffer.capacity()];
buffer.get(bytes);
int srcIndex = 0;
if (i == 0) {
//直接取出来所有Y的有效区域,也可以存储成一个临时的bytes,到下一步再copy
for (int j = 0; j < height; j++) {
System.arraycopy(bytes, srcIndex, yuvBytes, dstIndex, width);
srcIndex += rowStride;
dstIndex += width;
}
} else if (i == 1) {
//根据pixelsStride取相应的数据
for (int j = 0; j < height / 2; j++) {
for (int k = 0; k < width / 2; k++) {
uBytes[uIndex++] = bytes[srcIndex];
srcIndex += pixelsStride;
}
if (pixelsStride == 2) {
srcIndex += rowStride - width;
} else if (pixelsStride == 1) {
srcIndex += rowStride - width / 2;
}
}
} else if (i == 2) {
//根据pixelsStride取相应的数据
for (int j = 0; j < height / 2; j++) {
for (int k = 0; k < width / 2; k++) {
vBytes[vIndex++] = bytes[srcIndex];
srcIndex += pixelsStride;
}
if (pixelsStride == 2) {
srcIndex += rowStride - width;
} else if (pixelsStride == 1) {
srcIndex += rowStride - width / 2;
}
}
}
}
// image.close();
//根据要求的结果类型进行填充
switch (type) {
case YUV420P:
System.arraycopy(uBytes, 0, yuvBytes, dstIndex, uBytes.length);
System.arraycopy(vBytes, 0, yuvBytes, dstIndex + uBytes.length, vBytes.length);
break;
case YUV420SP:
for (int i = 0; i < vBytes.length; i++) {
yuvBytes[dstIndex++] = uBytes[i];
yuvBytes[dstIndex++] = vBytes[i];
}
break;
case NV21:
for (int i = 0; i < vBytes.length; i++) {
yuvBytes[dstIndex++] = vBytes[i];
yuvBytes[dstIndex++] = uBytes[i];
}
break;
}
return yuvBytes;
} catch (final Exception e) {
if (image != null) {
image.close();
}
}
return null;
}
private Bitmap getBitmapFromYUV(byte[] date, int width, int height, int rotation) {
//使用YuvImage---》NV21
YuvImage yuvImage = new YuvImage(date, ImageFormat.NV21,width,height,null);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
yuvImage.compressToJpeg(new Rect(0,0,width,height),20,baos);
byte[] jdate =baos.toByteArray();
BitmapFactory.Options bitmapFatoryOptions = new BitmapFactory.Options();
bitmapFatoryOptions.inPreferredConfig = Bitmap.Config.RGB_565;
bitmapFatoryOptions.inSampleSize = 4;
if(rotation == 0){
Bitmap bmp = BitmapFactory.decodeByteArray(jdate,0,jdate.length,bitmapFatoryOptions);
return bmp;
}else {
Matrix m = new Matrix();
m.postRotate(rotation);
Bitmap bmp = BitmapFactory.decodeByteArray(jdate,0,jdate.length,bitmapFatoryOptions);
Bitmap bml = Bitmap.createBitmap(bmp,0,0,bmp.getWidth(),bmp.getHeight(),m,true);
return bml;
}
}
这一节就是对本系列的(一)(二)基本知识的利用了。这个有机会再开一个小节来讲。
|