我们上一节了解了MediaExtractor、MediaMuxer、MediaFormat、MediaCodec.BufferInfo。重复的内容我就不再赘述了,假如有上面的四个的一些补充还是会写一下。接下来我们学习MediaCodec,本节篇幅会比较长,知识点较多,请耐心品味。
MediaCodec类可用于访问低级媒体编解码器,即编码器/解码器组件。它是Android低级多媒体支持基础设施的一部分(通常与MediaExtractor, MediaSync, MediaMuxer, MediaCrypto, MediaDrm, Image, Surface,以及AudioTrack.)。
编解码器处理三种数据:压缩数据、原始音频数据和原始视频数据。 所有这三种数据都可以使用ByteBuffers,但您应该使用Surface用于原始视频数据以提高编解码器性能。Surface使用原生视频缓冲区,而不将其映射或复制到ByteBuffers因此,它的效率更高。
使用Surface时,通常无法访问原始视频数据,但可以使用ImageReader类来访问不安全的解码(原始)视频帧。这可能仍然比使用字节缓冲区更有效,因为一些本机缓冲区可以映射到直接的字节缓冲区。使用ByteBuffer模式时,可以使用Image类别和getInput/OutputImage(int)访问原始视频帧。
压缩数据可以作为解码器的输入、编码器的输出,需要指定数据的格式,这样codec才知道如何处理这些压缩数据
原始音频数据 — PCM音频数据帧,这是每个通道按通道顺序的一个样本。
在ByteBuffer模式下,视频buffer根据它们的MediaFormat.KEY_COLOR_FORMAT进行布局。可以从getCodecInfo(). MediaCodecInfo.getCapabilitiesForType.CodecCapability.colorFormats获取支持的颜色格式。视频编解码器可以支持三种颜色格式:
从Build.VERSION_CODES.LOLLIPOP_MR1.开始,所有视频编解码器都支持flexible的YUV 4:2:0 buffer。
四个参数:
MediaFormat format
:输入数据的格式(解码器)或输出数据的所需格式(编码器)。传null等同于传递MediaFormat.MediaFormat作为空的MediaFormat。Surface surface
:指定Surface,用于解码器输出的渲染。如果编解码器不生成原始视频输出(例如,不是视频解码器)和/或想配置解码器输出ByteBuffer,则传null。MediaCrypto crypto
:指定一个crypto对象,用于对媒体数据进行安全解密。对于非安全的编解码器,传null。int flags
:当组件是编码器时,flags指定为常量CONFIGURE_FLAG_ENCODE。
MediaCodec可以处理具体的视频流,主要有这几个方法:
- getInputBuffers:获取需要编码数据的输入流队列,返回的是一个ByteBuffer数组
- getInputBuffer(index) : 获取InputBuffers数组index下标的ByteBuffer
- queueInputBuffer:输入流入队列
- dequeueInputBuffer:从输入流队列中取数据进行编码操作
- getOutputBuffers:获取编解码之后的数据输出流队列,返回的是一个ByteBuffer数组
- getOutputBuffer(index) : 获取OutputBuffers数组index下标的ByteBuffer
- dequeueOutputBuffer:从输出队列中取出编码操作之后的数据
- releaseOutputBuffer:处理完成,释放ByteBuffer数据
MediaFormat的格式被指定为键/值对。keys是字符串。values可以是整数、长整型、浮点型、字符串或ByteBuffer。(要素元数据被指定为 字符串/布尔 对。)
名字 | 值类型 | 描述 |
---|---|---|
KEY_MIME |
String | 格式的类型。 |
KEY_CODECS_STRING |
String | 可选,媒体格式的RFC 6381编解码器字符串 |
KEY_MAX_INPUT_SIZE |
Integer | 可选,输入数据缓冲区的最大大小 |
KEY_PIXEL_ASPECT_RATIO_WIDTH |
Integer | 可选,像素长宽比宽度 |
KEY_PIXEL_ASPECT_RATIO_HEIGHT |
Integer | 可选,像素纵横比高度 |
KEY_BIT_RATE |
Integer | 仅编码器,以比特/秒为单位的所需比特率 |
KEY_DURATION |
Long | 内容的持续时间(以微秒计) |
名字 | 值类型 | 描述 |
---|---|---|
KEY_WIDTH |
Integer | |
KEY_HEIGHT |
Integer | |
KEY_COLOR_FORMAT |
Integer | 由用户为编码器设置,以解码器的输出格式可读 |
KEY_FRAME_RATE |
Integer or Float | 要求用于编码器,可选用于解码器 |
KEY_CAPTURE_RATE |
Integer | |
KEY_I_FRAME_INTERVAL |
Integer or Float | 仅编码器关键帧之间的时间间隔。添加了浮动支持android.os.Build.VERSION_CODES#N_MR1 |
KEY_INTRA_REFRESH_PERIOD |
Integer | 仅编码器,可选 |
KEY_LATENCY |
Integer | 仅编码器,可选 |
KEY_MAX_WIDTH |
Integer | 仅解码器,可选,最大分辨率宽度 |
KEY_MAX_HEIGHT |
Integer | 仅解码器,可选,最大分辨率高度 |
KEY_REPEAT_PREVIOUS_FRAME_AFTER |
Long | 仅表面模式下的编码器,可选 |
KEY_PUSH_BLANK_BUFFERS_ON_STOP |
Integer | 解码器仅渲染到表面,可选 |
KEY_TEMPORAL_LAYERING |
String | 仅编码器可选的时间分层模式 |
名字 | 值类型 | 描述 |
---|---|---|
KEY_CHANNEL_COUNT |
Integer | |
KEY_SAMPLE_RATE |
Integer | |
KEY_PCM_ENCODING |
Integer | 可选择的 |
KEY_IS_ADTS |
Integer | 可选,如果解码AAC音频内容,将此键设置为1表示每个音频帧都以ADTS标头为前缀。 |
KEY_AAC_PROFILE |
Integer | 仅编码器可选,如果内容是AAC音频,则指定所需的配置文件。 |
KEY_AAC_SBR_MODE |
Integer | 仅编码器,可选,如果内容是AAC音频,则指定所需的SBR模式。 |
KEY_AAC_DRC_TARGET_REFERENCE_LEVEL |
Integer | 仅解码器可选,如果内容是AAC音频,则指定目标参考电平。 |
KEY_AAC_ENCODED_TARGET_LEVEL |
Integer | 仅解码器可选,如果内容是AAC音频,则指定编码器使用的目标参考电平。 |
KEY_AAC_DRC_BOOST_FACTOR |
Integer | 仅解码器,可选,如果内容是AAC音频,则指定DRC增强因子。 |
KEY_AAC_DRC_ATTENUATION_FACTOR |
Integer | 仅解码器可选,如果内容是AAC音频,则指定DRC衰减系数。 |
KEY_AAC_DRC_HEAVY_COMPRESSION |
Integer | 仅解码器可选,如果内容是AAC音频,则指定是否使用重度压缩。 |
KEY_AAC_MAX_OUTPUT_CHANNEL_COUNT |
Integer | 仅解码器可选,如果内容是AAC音频,则指定解码器输出的最大通道数。 |
KEY_AAC_DRC_EFFECT_TYPE |
Integer | 仅解码器可选,如果内容是AAC音频,则指定要使用的MPEG-D DRC效果类型。 |
KEY_AAC_DRC_OUTPUT_LOUDNESS |
Integer | 仅解码器,可选,如果内容是AAC音频,则返回DRC输出响度。 |
KEY_AAC_DRC_ALBUM_MODE |
Integer | 仅解码器可选,如果内容是AAC音频,则指定MPEG-D DRC专辑模式是否处于活动状态。 |
KEY_CHANNEL_MASK |
Integer | 可选,音频通道分配的遮罩 |
KEY_ENCODER_DELAY |
Integer | 可选,从解码的音频流的开始处要修剪的帧数。 |
KEY_ENCODER_PADDING |
Integer | 可选,从解码的音频流的结尾开始修剪的帧数。 |
KEY_FLAC_COMPRESSION_LEVEL |
Integer | 仅编码器,可选,如果内容是FLAC音频,则指定所需的压缩级别。 |
KEY_MPEGH_PROFILE_LEVEL_INDICATION |
Integer | 仅解码器可选,如果内容是MPEG-H音频,则指定流的配置文件和级别。 |
KEY_MPEGH_COMPATIBLE_SETS |
ByteBuffer | 仅解码器可选,如果内容是MPEG-H音频,则指定流的兼容集(配置文件和级别)。 |
KEY_MPEGH_REFERENCE_CHANNEL_LAYOUT |
Integer | 仅解码器可选,如果内容是MPEG-H音频,则指定流的首选参考通道布局。 |
线格式的类型。KEY_LANGUAGE
线内容的语言。KEY_CAPTION_SERVICE_NUMBER
(同Internationalorganizations)国际组织可选,隐藏字幕服务或频道号。
名字 | 值类型 | 描述 |
---|---|---|
KEY_MIME | String | 格式的类型 |
KEY_LANGUAGE | String | 内容的语言 |
KEY_CAPTION_SERVICE_NUMBER | int | 可选,隐藏字幕服务或频道号 |
KEY_MIME |
String | 格式的类型。 |
KEY_WIDTH |
Integer | |
KEY_HEIGHT |
Integer | |
KEY_COLOR_FORMAT |
Integer | 由用户为编码器设置,以解码器的输出格式可读 |
KEY_TILE_WIDTH |
Integer | 如果图像有网格,则需要 |
KEY_TILE_HEIGHT |
Integer | 如果图像有网格,则需要 |
KEY_GRID_ROWS |
Integer | 如果图像有网格,则需要 |
KEY_GRID_COLUMNS |
Integer | 如果图像有网格,则需要 |
至于值,常用的也就那些,我就不提供了,自己去开发者文档查找。
与媒体源一起使用的单个完整图像缓冲区,例如 MediaCodec
或 CameraDevice
。
该类允许通过一个或多个ByteBuffers
高效地直接应用程序访问图像的像素数据。 每个缓冲区都封装在描述该平面中像素数据布局的Image.Plane
中。 由于这种直接访问方式,与Bitmap
类不同,图像不能直接用作UI资源。
由于图像通常由硬件组件直接生成或使用,因此它们是整个系统共享的有限资源,应在不再需要时立即关闭。
例如,当使用ImageReader
类从各种媒体源读出图像时,一旦达到the maximum outstanding image count
,不关闭旧的图像对象将阻止新图像的可用性。 发生这种情况时,获取新图像的函数通常会抛出IllegalStateException
。
获取该图像的像素平面阵列,平面的数量由图像的格式决定。这个需要了解ImageFormat的格式了,这个以后再了解。
使用此函数可检索ImageFormat的每个像素的位数。
这个在此就点一下算了,之后在单独开一节来具体讲解。
//解码操作,返回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
- 描述视频格式内容的颜色格式的关键字,需要在android.media.MediaCodecInfo.CodecCapabilities中声明。
- 通过MediaFormat,获取了视频信息,duration 和rotation 是比较重要的。duration /1000/1000可以判断我们需要取几帧,1s一帧,rotation判断视频旋转角度,后面生成bitmap需要用到。
//开始解码,获取帧序列
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;
}
}
}
}
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;
}
}
这一节就是对本系列的(一)(二)基本知识的利用了。这个有机会再开一个小节来讲。