还是废了蛮多劲头,查了很多资料,终于能获取所有视频帧的数据了
依赖一些简单工具类,可以注释掉
还有一些不完善之处,比如如何指定解码宽高的,希望大神能指教
见代码
package a.baozouptu.editvideo.track;
import static android.media.MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible;
import android.annotation.SuppressLint;
import android.graphics.ImageFormat;
import android.media.ImageReader;
import android.media.MediaCodec;
import android.media.MediaExtractor;
import android.media.MediaFormat;
import android.os.Build;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import com.baozou.ptu.baselibrary.utils.LogUtil;
import com.baozou.ptu.baselibrary.utils.SegmentedProgressCallback;
import org.jetbrains.annotations.NotNull;
import java.lang.reflect.Field;
import java.nio.ByteBuffer;
import java.util.Locale;
/**
* 使用Android提的mediaCodec硬解码解码视频获取所有帧,回调返回解码的到的视频帧数据,硬解码特点是速度更快
* <p>
* 视频帧数据就是Android提供的ImageReader,然后再通过ImageReader获取数据做相应的处理
* <p>
* 关于解码到的ImageReader里面数据的格式,指定的是YUV格式的一类,ImageFormat.YUV_420_888 == COLOR_FormatYUV420Flexible
* 但是这个格式里面又分了很多的子格式,由于厂商实现不同等原因,处理是需要进行判断,然后处理
* <p>
* 如果想要yuv转bitmap的rgb,
* 具体可以通过libyuv库提供的接口,或者Android自带的几种方法,RenderScript,YuvImage等
* <p>
* 解码过程 包括获取视频信息 是异步的
* <p>
* 应该是可以指定解码宽高的,但是目前没成功
*/
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public class VideoFrameDecoder extends HandlerThread {
public static final String TAG = "VideoFrameDecoder";
// 控制选项
/**
* 控制取帧的间隔,为0表示所有帧
*/
private long intervalMs = 0;//ms
private boolean requestStop = false;
private boolean initSuccess = false;
@Nullable
private final SegmentedProgressCallback progressCallback;
@NotNull
private final DecoderCallBack decoderCallBack;
// 提取器->信息->解码器->读取器
private MediaExtractor extractor;
private MediaCodec mediaCodec;
private ImageReader imageReader;
private long lastPresentationTimeUs;
// 单帧解码
private FrameDecoder frameDecoder;
/**
* 解码器解码运行在这个handle所在的线程
*/
private final Handler handler;
/**
* 处理解码器解码器来的数据运行在此线程
*/
private ImageReaderHandlerThread imageReaderHandlerThread;
// 视频信息
private long durationUs;
private int rotation;
private int videoW;
private int videoH;
private int fps;
private String colorFormatName = "no data";
private int colorRange = -1;
public VideoFrameDecoder(@NotNull DecoderCallBack decoderCallBack,
@Nullable SegmentedProgressCallback progressCallback) {
super(TAG);
start();
Looper looper = getLooper();
handler = new Handler(looper);
this.progressCallback = progressCallback;
this.decoderCallBack = decoderCallBack;
extractor = new MediaExtractor();
this.frameDecoder = new FrameDecoder();
LogUtil.logTrack(TAG + " 创建完成");
}
/**
* 注意是异步的
*/
public void setVideoAndInitAsync(final String fileName) {
initSuccess = false;
handler.post(() -> {
LogUtil.logTrack("开始获取视频信息 \n 当前线程是: " + Thread.currentThread().getName());
if (progressCallback != null)
progressCallback.onProgress(progressCallback.seg + 1, "正在获取视频信息");
try {
// 设置fileName
extractor.setDataSource(fileName);
MediaFormat videoFormat = extractVideoInfo();
if (videoFormat == null) {
decoderCallBack.onError(new Exception("无法获取视频信息"));
return;
}
//todo
/* **上面提到了 MediaCodecList,这里简单说一下,使用 MediaCodecList 可以方便的列出当前设备支持的所有的编解码器
,创建 MediaCodec 的时候要选择当前格式支持的编解码器,也就是选择的编解码器需支持对应的 MediaFormat,
每个编解码器都被包装成一个 MediaCodecInfo 对象,据此可以查看该编码器的一些特性,
比如是否支持硬件加速、是软解还是硬解编解码器等,常用的简单如下**:
```java
1// 是否软解
2public boolean isSoftwareOnly ()
3// 是Android平台提供(false)还是厂商提供(true)的编解码器
4public boolean isVendor ()
5// 是否支持硬件加速
6public boolean isHardwareAccelerated ()
7// 是编码器还是解码器
8public boolean isEncoder ()
9// 获取当前编解码器支持的合适
10public String[] getSupportedTypes ()
11// ...
```*/
// 设置解码器需要的视频的参数
// 实测发现这个宽高设置无效,网上找了一些资料没找到缩放相关的,似乎不支持缩放
int dstW = videoW, dstH = videoH;
videoFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, COLOR_FormatYUV420Flexible);
videoFormat.setInteger(MediaFormat.KEY_WIDTH, dstW);
videoFormat.setInteger(MediaFormat.KEY_HEIGHT, dstH);
mediaCodec = MediaCodec.createDecoderByType(videoFormat.getString(MediaFormat.KEY_MIME));
// 设置解码输出位置 surface 的参数
int imageFormat = ImageFormat.YUV_420_888;
imageReader = ImageReader.newInstance(
dstW, dstH, // 实测发现这个宽高设置无效,网上找了一些资料没找到缩放相关的,似乎不支持缩放
imageFormat,
1 // 解码到的surface缓存的帧数
);
// 配置好解码器,准备开始
mediaCodec.configure(videoFormat, imageReader.getSurface(), null, 0);
mediaCodec.start();
LogUtil.logTrack("获取视频信息和初始化完成 当前线程是: " + Thread.currentThread().getName());
LogUtil.logTrack(String.format(Locale.CHINA,
"视频信息:width = %d height = %d, 时长 = %ds fps = %d 旋转 = %d" +
"颜色格式 = %s 颜色范围 = %d",
videoW, videoH, durationUs, fps, rotation,
colorFormatName, colorRange));
initSuccess = true;
decoderCallBack.onInitVideoSuccess();
} catch (Exception e) {
initSuccess = false;
e.printStackTrace();
LogUtil.logTrack("init 视频解码器出错 \n 当前线程是: " + Thread.currentThread().getName());
decoderCallBack.onError(new Exception("初始化视频解码器失败" + e.getMessage()));
}
});
}
private MediaFormat extractVideoInfo() {
// 遍历轨道,选中视频,并获取视频信息
MediaFormat videoFormat = null;
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) {
LogUtil.logTrack("无法获取视频信息 \n 当前线程是: " + Thread.currentThread().getName());
return null;
} else {
LogUtil.logTrack("init MediaExtractor finish \n 当前线程是: " + Thread.currentThread().getName());
}
videoW = videoFormat.getInteger(MediaFormat.KEY_WIDTH);
videoH = videoFormat.getInteger(MediaFormat.KEY_HEIGHT);
if (videoFormat.containsKey(MediaFormat.KEY_ROTATION) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
rotation = videoFormat.getInteger(MediaFormat.KEY_ROTATION);
}
durationUs = videoFormat.getLong(MediaFormat.KEY_DURATION);
if (videoFormat.containsKey(MediaFormat.KEY_FRAME_RATE))
fps = videoFormat.getInteger(MediaFormat.KEY_FRAME_RATE);
if (videoFormat.containsKey(MediaFormat.KEY_COLOR_RANGE) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
colorRange = videoFormat.getInteger(MediaFormat.KEY_COLOR_RANGE);
}
if (videoFormat.containsKey(MediaFormat.KEY_ROTATION)) {
colorFormatName = getColorFormatName(videoFormat.getInteger(MediaFormat.KEY_COLOR_FORMAT));
}
return videoFormat;
}
public void startDecode() {
if (!initSuccess) {
decoderCallBack.onError(new Exception("初始化失败,无法解码,请检查"));
return;
}
imageReaderHandlerThread = new ImageReaderHandlerThread();
handler.post(() -> {
LogUtil.logTrack("开始解码 当前线程是: \" + Thread.currentThread().getName()");
if (progressCallback != null) {
progressCallback.addSeg("正在解码...");
}
imageReader.setOnImageAvailableListener(new MyOnImageAvailableListener(),
imageReaderHandlerThread.getHandler());
extractor.seekTo(0, MediaExtractor.SEEK_TO_CLOSEST_SYNC);
frameDecoder.decodeOneFrame();
// processByExtractor(scale, decoderCallBack);
});
}
/**
* 每次解码一帧 然后等待消费位置消费完解码的那一帧之后, 回调解码器进行解码这样才不会丢帧
*/
private class FrameDecoder {
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
long timeOutUs = 5 * 1000; // 微秒,不是毫秒
/**
* 整个文件输入完成
*/
boolean inputFinish = false;
/**
* 整个文件输出完成
*/
boolean outputDone = false;
//开始进行解码。
int count = 1;
/**
*
*/
public void decodeOneFrame() {
LogUtil.logTrack("开始解码一帧,当前线程是: " + Thread.currentThread().getName());
try {
// 这个循环会不断的解码数据往输出位置送,比如imageReader的surface,
// 但是要注意的一定是,如果surface那边没有消费处理掉的帧,就会被新的解码数据覆盖,也就是说这一帧就丢失了
if (outputDone) return;
// 注意解码器一次输入的不一定是一帧的数据,且不会立即解码出来,所以要死循环等待一个完整的帧被解码出来
boolean hasDecodeFrameFinish = false;
while (!hasDecodeFrameFinish) {
if (requestStop) {
return;
}
if (!inputFinish) {
// 获取可用的输入缓冲区的索引
int inputBufferIndex = mediaCodec.dequeueInputBuffer(timeOutUs);
if (inputBufferIndex >= 0) {
ByteBuffer inputBuffer;
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
// 获取输入缓冲区
inputBuffer = mediaCodec.getInputBuffer(inputBufferIndex);
// } else {
// inputBuffer = inputBuffers[inputBufferIndex];
// }
// 填充数据
// simpleData the sample size (or -1 if no more samples are available).
// 读取数据的size,<=0表示读完了
int sampleData = extractor.readSampleData(inputBuffer, 0);
if (sampleData > 0) {
long sampleTime = extractor.getSampleTime();
LogUtil.logTrack(String.format(Locale.CHINA, "解码到的时间 = %.4fs",
sampleTime / 1000000f));
// 将填满数据的inputBuffer提交到编码队列
mediaCodec.queueInputBuffer(inputBufferIndex, 0, sampleData, sampleTime, 0);
//继续
if (intervalMs == 0) {
extractor.advance();
} else {
if (count * intervalMs * 1000 > durationUs) {
extractor.advance();
} else {
extractor.seekTo(count * intervalMs * 1000, MediaExtractor.SEEK_TO_PREVIOUS_SYNC);
count++;
}
// extractor.advance();
}
} else {
// 读完了,给解码器设置结束标志
mediaCodec.queueInputBuffer(inputBufferIndex, 0, 0, 0L,
MediaCodec.BUFFER_FLAG_END_OF_STREAM);
inputFinish = true;
LogUtil.logTrack("解码输入数据完成 \n 当前线程是: " + Thread.currentThread().getName());
if (progressCallback != null) {
progressCallback.onProgress(progressCallback.progress + 1 / 1000f);
}
}
}
}
if (!outputDone) {
//get data
// 获取已成功编解码的输出缓冲区的索引
// status就是当前解码的状态,可能正在解码中,可能解码完成了
int status = mediaCodec.dequeueOutputBuffer(bufferInfo, timeOutUs);
if (status ==
MediaCodec.INFO_TRY_AGAIN_LATER) {
//继续
} else if (status == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
//开始进行解码
} else if (status == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
//同样啥都不做
} else {
//在这里判断,当前编码器的状态
// bufferInfo.flags 标记关键帧或者结束帧
if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
LogUtil.logTrack("解码结束 output EOS");
outputDone = true;
}
boolean doRender = (bufferInfo.size != 0);
long presentationTimeUs = bufferInfo.presentationTimeUs;
if (lastPresentationTimeUs == 0) {
lastPresentationTimeUs = presentationTimeUs;
} else {
long diff = presentationTimeUs - lastPresentationTimeUs;
if (intervalMs != 0) {
if (diff / 1000 < (intervalMs - 10)) {
long lastDiff = durationUs - presentationTimeUs;
LogUtil.logTrack("duration=" + durationUs + ", lastDiff=" + lastDiff);
if (lastDiff < 50 * 1000 && diff > 0) {
//离最后50ms的
//输出最后一帧.强制输出最后一帧附近的帧的话,会比用metaRetiriever多一帧
lastPresentationTimeUs = durationUs;
} else {
doRender = false;
}
} else {
lastPresentationTimeUs = presentationTimeUs;
}
LogUtil.logTrack(
"diff time in ms =" + diff / 1000);
}
}
//有数据了.因为会直接传递给Surface,所以说明都不做好了
if (progressCallback != null) {
progressCallback.onProgress(progressCallback.progress + 1 / 1000f);
}
// 释放输出缓冲区,给到客户端调用者,直接送显就可以了
mediaCodec.releaseOutputBuffer(status, doRender);
hasDecodeFrameFinish = true;
LogUtil.logTrack("解码一帧完成,等待消费获取");
}
}
}
} catch (Exception e) {
String message = "解码过程出错,已停止解码 " + e.getMessage();
decoderCallBack.onError(new Exception(message));
LogUtil.logTrack(message);
e.printStackTrace();
}
}
public boolean isFinish() {
return outputDone;
}
/**
* 必须在最后一帧消费处理完之后调用,不然丢帧
*/
public void release() {
// 暂时不做处理时不做处理,如果还在处理数据的过程中,
// 释放或关闭会导致回调丢失
if (mediaCodec != null) {
mediaCodec.stop();
mediaCodec.release();
}
if (extractor != null) {
extractor.release();
}
}
}
// 原始代码,保留,一次性解码所有帧,如果解码出来的帧的处理的位置的速度跟不上解码器,就会丢帧
/* private void processByExtractor(int scale, DecoderCallBack callBack) {
try {
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
long timeOutUs = 5 * 1000; // 微秒,不是毫秒
boolean inputDone = false;
boolean outputDone = false;
ByteBuffer[] inputBuffers = null;
// if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
// inputBuffers = mediaCodec.getInputBuffers();
// }
//开始进行解码。
int count = 1;
LogUtil.logTrack("开始解码 \n 当前线程是: " + Thread.currentThread().getName());
if (progressCallback != null) {
progressCallback.addSeg("正在解码...");
}
// 这个循环会不断的解码数据往输出位置送,比如imageReader的surface,
// 但是要注意的一定是,如果surface那边没有消费处理掉的帧,就会被新的解码数据覆盖,也就是说这一帧就丢失了
while (!outputDone) {
if (requestStop) {
return;
}
if (!inputDone) {
//feed data
// 获取可用的输入缓冲区的索引
int inputBufferIndex = mediaCodec.dequeueInputBuffer(timeOutUs);
if (inputBufferIndex >= 0) {
ByteBuffer inputBuffer;
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
// 获取输入缓冲区
inputBuffer = mediaCodec.getInputBuffer(inputBufferIndex);
// } else {
// inputBuffer = inputBuffers[inputBufferIndex];
// }
// 填充数据
// simpleData the sample size (or -1 if no more samples are available).
// 读取数据的size,<=0表示读完了
int sampleData = extractor.readSampleData(inputBuffer, 0);
if (sampleData > 0) {
long sampleTime = extractor.getSampleTime();
LogUtil.logTrack(String.format(Locale.CHINA, "解码到的时间 = %.4fs",
sampleTime / 1000000f));
// 将填满数据的inputBuffer提交到编码队列
mediaCodec.queueInputBuffer(inputBufferIndex, 0, sampleData, sampleTime, 0);
//继续
if (intervalMs == 0) {
extractor.advance();
} else {
if (count * intervalMs * 1000 > durationUs) {
extractor.advance();
} else {
extractor.seekTo(count * intervalMs * 1000, MediaExtractor.SEEK_TO_PREVIOUS_SYNC);
count++;
}
// extractor.advance();
}
} else {
// 读完了,给解码器设置结束标志
mediaCodec.queueInputBuffer(inputBufferIndex, 0, 0, 0L,
MediaCodec.BUFFER_FLAG_END_OF_STREAM);
inputDone = true;
LogUtil.logTrack("解码输入数据完成 \n 当前线程是: " + Thread.currentThread().getName());
if (progressCallback != null) {
progressCallback.onProgress(progressCallback.progress + 1 / 1000f);
}
}
}
}
if (!outputDone) {
//get data
// 获取已成功编解码的输出缓冲区的索引
// status就是当前解码的状态,可能正在解码中,可能解码完成了
int status = mediaCodec.dequeueOutputBuffer(bufferInfo, timeOutUs);
if (status ==
MediaCodec.INFO_TRY_AGAIN_LATER) {
//继续
} else if (status == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
//开始进行解码
} else if (status == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
//同样啥都不做
} else {
//在这里判断,当前编码器的状态
// bufferInfo.flags 标记关键帧或者结束帧
if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
LogUtil.logTrack("解码结束 output EOS");
outputDone = true;
}
boolean doRender = (bufferInfo.size != 0);
long presentationTimeUs = bufferInfo.presentationTimeUs;
if (lastPresentationTimeUs == 0) {
lastPresentationTimeUs = presentationTimeUs;
} else {
long diff = presentationTimeUs - lastPresentationTimeUs;
if (intervalMs != 0) {
if (diff / 1000 < (intervalMs - 10)) {
long lastDiff = durationUs - presentationTimeUs;
LogUtil.logTrack("duration=" + durationUs + ", lastDiff=" + lastDiff);
if (lastDiff < 50 * 1000 && diff > 0) {//离最后50ms的
//输出最后一帧.强制输出最后一帧附近的帧的话,会比用metaRetiriever多一帧
lastPresentationTimeUs = durationUs;
} else {
doRender = false;
}
} else {
lastPresentationTimeUs = presentationTimeUs;
}
LogUtil.logTrack(
"diff time in ms =" + diff / 1000);
}
}
//有数据了.因为会直接传递给Surface,所以说明都不做好了
if (progressCallback != null) {
progressCallback.onProgress(progressCallback.progress + 1 / 1000f);
}
// 释放输出缓冲区,给到客户端调用者,直接送显就可以了
mediaCodec.releaseOutputBuffer(status, doRender);
}
}
}
} catch (Exception e) {
String message = "解码过程出错,已停止解码 " + e.getMessage();
if (decoderCallBack != null) {
decoderCallBack.onError(new Exception(message));
}
LogUtil.logTrack(message);
e.printStackTrace();
} finally {
// 暂时不做处理,如果还在处理数据的过程中,
// 释放或关闭会导致回调丢失
// if (mediaCodec != null) {
// mediaCodec.stop();
// mediaCodec.release();
// }
// if (extractor != null) {
// extractor.release();
// }
}
}*/
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
private class MyOnImageAvailableListener implements ImageReader.OnImageAvailableListener {
private int frameNumber = 0;
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
@Override
public void onImageAvailable(ImageReader reader) {
// 中端机器测试得,720 * 1280大小的视频,不加上其它处理,3-4ms解码出一帧yuv
// maxImages–用户希望同时访问的最大图像数。这应该尽可能小,以限制内存使用。一旦用户获得maxImages图像,必须先发布其中一个图像,然后才能通过acquireLatestImage()或acquireNextImage()访问新图像。必须大于0。
frameNumber++;
LogUtil.logTrack("");
LogUtil.logTrack("---------------------------------------------------------------------------------");
LogUtil.logTrack("收到视频帧解码第 " + frameNumber +
" 帧回调" + Thread.currentThread().getName());
LogUtil.logTrack("---------------------------------------------------------------------------------");
LogUtil.logTrack("");
/* 测试不处理是获
Image img = null;
try {
img = reader.acquireLatestImage();
} catch (Exception e) {
LogUtil.logTrack("测试获取视频帧的image出错");
} finally {
if (img != null) {
img.close();
}
}*/
decoderCallBack.syncProcessFrame(reader, frameNumber, rotation);
if (!frameDecoder.isFinish()) {
handler.post(() -> frameDecoder.decodeOneFrame());
} else {
frameDecoder.release();
decoderCallBack.onDecodeFinish();
}
}
}
@SuppressLint("PrivateApi")
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
String getColorFormatName(int formatValue) {
// TODO Auto-generated method stub
//获取所有变量的值
Class clazz = null;
try {
clazz = Class.forName("android.media.ScoreTable.CodecCapabilities");
Field[] fields = clazz.getFields();
for (Field field : fields) {
if (field.getInt(clazz) == formatValue)
return field.getName();
}
} catch (ClassNotFoundException | IllegalAccessException e) {
e.printStackTrace();
}
return "error on get colorformat name";
}
private static class ImageReaderHandlerThread extends HandlerThread {
private final Handler handler;
public ImageReaderHandlerThread() {
super("ImageReader");
start();
Looper looper = getLooper();
handler = new Handler(looper);
}
public Handler getHandler() {
return handler;
}
}
private void _release() {
LogUtil.logTrack("销毁解码器对象");
requestStop = true;
if (imageReader != null) {
imageReader.close();
imageReader = null;
}
if (mediaCodec != null) {
mediaCodec.stop();
mediaCodec.release();
mediaCodec = null;
}
if (extractor != null) {
extractor.release();
extractor = null;
}
}
public interface DecoderCallBack {
/**
* 注意!!!
* 必须在调用线程完成处理,因为共享帧数据,需要一步一步走,
* 如果切换线程顺序乱了会导致数据混乱
* <p>
* 也不能拷贝数据到其它线程,因为帧很多,数据量很大
*/
void syncProcessFrame(ImageReader frame, int frameId, int rotation);
void onFrameError(int frameId, Exception e);
void onError(Exception e);
void onInitVideoSuccess();
void onDecodeFinish();
}
}