说明: 此MediaCodec解码H264实操主要针对Android12.0系统。通过读取sd卡上的H264码流Me获取视频数据,将数据通过mediacodec解码输出到surfaceview上。
1 H264码流和MediaCodec解码简介
1.1 H264码流简介
H.264,也被称为MPEG-4 AVC(Advanced Video Coding),是一种广泛使用的数字视频压缩标准,主要用于视频编码。H.264标准由ITU-T视频编码专家组(VCEG)和ISO/IEC动态图像专家组(MPEG)共同开发,旨在提供比之前的视频编码标准更高的数据压缩效率。
H.264是一种基于块的编码技术,它将视频帧分为多个宏块(Macroblocks,MBs),每个宏块包含亮度信息和色度信息。
关于H264码流相关概念还有:
帧类型,包括I、P、B三种类型,说明如下:
- I帧(Intra-coded frames):关键帧,不依赖其他帧进行解码,包含完整的图像信息。
- P帧(Predictive-coded frames):预测帧,依赖前一个I帧或P帧进行解码,包含相对于前一帧的差分信息。
- B帧(Bidirectional predictive-coded frames):双向预测帧,依赖前后两个帧进行解码,用于提高压缩效率。
编码过程:包括帧内预测(Intra prediction)、帧间预测(Inter prediction)、变换(Transform)、量化(Quantization)和熵编码(Entropy coding)等步骤。
码流结构:H.264码流由一系列的NAL单元(Network Abstraction Layer Units)组成,每个NAL单元包含一个头部和数据负载,头部定义了负载的类型和重要性。
等等概念,想要有更多了解,可查看以下文章,持续更新中:
系统化学习 H264视频编码(01)基础概念
系统化学习 H264视频编码(02) I帧 P帧 B帧 引入及相关概念解读
系统化学习 H264视频编码(03)数据压缩流程及相关概念
。。。
1.2 MediaCodec解码说明
MediaCodec 是 Android 提供的一个音视频编解码器类,允许应用程序对音频和视频数据进行编码(压缩)和解码(解压缩)。它在 Android 4.1(API 级别 16)版本中引入,广泛应用于处理音视频数据,如播放视频、录制音频等。
以下是 MediaCodec 解码的基本步骤:
-
创建 MediaCodec 实例:通过调用
MediaCodec.createDecoderByType
方法并传入解码类型(如 "video/avc" 或 "audio/mp4a-latm")来创建解码器。 -
配置解码参数:通过调用
configure
方法配置解码器,传入解码参数如解码格式、输出格式等。 -
准备输出 Surface:为解码器准备输出 Surface。输出 Surface 用于接收解码后的数据,并显示在屏幕上。
-
开始解码:调用
start
方法启动解码器。 -
发送输入数据:将待解码的数据通过
write
方法发送到解码器的输入队列。 -
处理输出数据:监听输出队列,通过
dequeueOutputBuffer
方法获取解码后的数据,并将其显示在屏幕上。 -
停止解码:解码完成后,调用
stop
方法停止解码器。 -
释放资源:调用
release
方法释放解码器资源。
通过这些步骤,应用程序可以实现对视频和音频数据的高效编解码处理。针对本工程,主要通过从sd卡上读取h264码流,通过mediacodec解码视频并播放到surfaceview上。
2 MediaCodec解码H264码流代码完整解读(android Q)
2.1 关于权限部分的处理
关于权限,需要在AndroidManifest.xml中添加权限,具体如下所示:
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />
关于运行时权限的请求等,这里给出一个工具类参考代码,具体如下所示:
public class Permission {
public static final int REQUEST_MANAGE_EXTERNAL_STORAGE = 1;
//需要申请权限的数组
private static final String[] permissions = {
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.CAMERA
};
//保存真正需要去申请的权限
private static final List<String> permissionList = new ArrayList<>();
public static int RequestCode = 100;
public static void requestManageExternalStoragePermission(Context context, Activity activity) {
if (!Environment.isExternalStorageManager()) {
showManageExternalStorageDialog(activity);
}
}
private static void showManageExternalStorageDialog(Activity activity) {
AlertDialog dialog = new AlertDialog.Builder(activity)
.setTitle("权限请求")
.setMessage("请开启文件访问权限,否则应用将无法正常使用。")
.setNegativeButton("取消", null)
.setPositiveButton("确定", (dialogInterface, i) -> {
Intent intent = new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION);
activity.startActivityForResult(intent, REQUEST_MANAGE_EXTERNAL_STORAGE);
})
.create();
dialog.show();
}
public static void checkPermissions(Activity activity) {
for (String permission : permissions) {
if (ContextCompat.checkSelfPermission(activity, permission) != PackageManager.PERMISSION_GRANTED) {
permissionList.add(permission);
}
}
if (!permissionList.isEmpty()) {
requestPermission(activity);
}
}
public static void requestPermission(Activity activity) {
ActivityCompat.requestPermissions(activity,permissionList.toArray(new String[0]),RequestCode);
}
}
这样,如果后面又更多的权限,都可以使用该方法来处理,处理方式为:
Permission.checkPermissions(this);
Permission.requestManageExternalStoragePermission(getApplicationContext(), this);
2.2 解码的处理
关于解码部分,主要是MediaCodec的初始化、解码处理部分,代码如下所示:
public class H264Decoder implements Runnable {
private final String path;
private final String TAG = "H264Decoder";
MediaCodec mediaCodec;
boolean enablePlay = false;
public H264Decoder(String path, Surface surface, int width , int height) {
this.path = path;
try {
mediaCodec = MediaCodec.createDecoderByType("video/avc");
MediaFormat mediaformat = MediaFormat.createVideoFormat("video/avc", width, height);
mediaformat.setInteger(MediaFormat.KEY_FRAME_RATE, 15);
mediaCodec.configure(mediaformat, surface, null, 0);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public void play() {
enablePlay = true;
mediaCodec.start();
new Thread(this).start();
}
public void stop(){
enablePlay = false;
}
@Override
public void run() {
try {
byte[] bytes = null;
try {
//注意:这里是从文件中一次性读H264取码流数据,因此不适合特别大的视频
bytes = getBytes(path);
} catch (Exception e) {
throw new RuntimeException(e);
}
int startIndex = 0;
MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
while (enablePlay) {
int nextFrameStart = findByFrame(bytes, startIndex+5, bytes.length);
//MediaCodec输入缓冲区操作
int inIndex = mediaCodec.dequeueInputBuffer(10000);
if (inIndex >= 0) {
ByteBuffer byteBuffer = mediaCodec.getInputBuffer(inIndex);
int length = nextFrameStart - startIndex;
byteBuffer.put(bytes, startIndex, length);
mediaCodec.queueInputBuffer(inIndex, 0, length, 0, 0);
startIndex = nextFrameStart;
}
//MediaCodec输出缓冲区操作
int outIndex =mediaCodec.dequeueOutputBuffer(info,10000);
if (outIndex >= 0) {
try {
//这里延迟下,避免刷的过快
Thread.sleep(40);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
mediaCodec.releaseOutputBuffer(outIndex, true);
}
}
} catch (Exception e) {
Log.i(TAG, "run decoder error:"+e.toString());
}
}
private int findByFrame( byte[] bytes, int start, int totalSize) {
for (int i = start; i <= totalSize-4; i++) {
//这里是一帧的结束符 00 00 00 01 或者 00 00 01
if (((bytes[i] == 0x00) && (bytes[i + 1] == 0x00) && (bytes[i + 2] == 0x00) && (bytes[i + 3] == 0x01))
||((bytes[i] == 0x00) && (bytes[i + 1] == 0x00) && (bytes[i + 2] == 0x01))) {
return i;
}
}
return -1;
}
public byte[] getBytes(String path) throws IOException {
InputStream is = new DataInputStream(Files.newInputStream(new File(path).toPath()));
int len;
int size = 1024;
byte[] buf;
ByteArrayOutputStream bos = new ByteArrayOutputStream();
buf = new byte[size];
while ((len = is.read(buf, 0, size)) != -1)
bos.write(buf, 0, len);
buf = bos.toByteArray();
return buf;
}
}
2.3 主流程代码参考实现
这里以 H264decoderActivity 为例,给出一个MediaCodec解码功能代码的参考实现。具体实现如下:
public class H264decoderActivity extends AppCompatActivity {
H264Decoder h264Decoder;
private final String TAG = "MainActivity";
Context mContext;
Surface surface;
private boolean isPlaying = false; // 用于跟踪播放状态
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
EdgeToEdge.enable(this);
mContext = this;
setContentView(R.layout.h264_decode_activity_main);
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
return insets;
});
initSurface();
Permission.checkPermissions(this);
Permission.requestManageExternalStoragePermission(getApplicationContext(), this);
Button playButton = findViewById(R.id.button);
playButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
// 切换播放状态
isPlaying = !isPlaying;
// 根据播放状态更新按钮文本
if (isPlaying) {
playButton.setText(R.string.stopplay);
//Environment.DIRECTORY_DOWNLOADS), "ags/out.h264").getAbsolutePath(),
h264Decoder = new H264Decoder(
new File(Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_DOWNLOADS), "ags/outputtest4.h264").getAbsolutePath(),
surface,1280,720);
h264Decoder.play();
} else {
playButton.setText(R.string.startplay);
h264Decoder.stop();
}
}
});
}
private void initSurface() {
SurfaceView mSurface = findViewById(R.id.preview);
mSurface.getHolder().addCallback(new SurfaceHolder.Callback() {
@Override
public void surfaceCreated(@NonNull SurfaceHolder surfaceHolder) {
Log.d(TAG,"surfaceCreated");
surface=surfaceHolder.getSurface();
}
@Override
public void surfaceChanged(@NonNull SurfaceHolder surfaceHolder, int i, int i1, int i2) {
Log.d(TAG,"surfaceChanged");
}
@Override
public void surfaceDestroyed(@NonNull SurfaceHolder surfaceHolder) {
Log.d(TAG,"surfaceDestroyed");
}
});
}
}
这里涉及的layout布局文件内容如下:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<SurfaceView
android:id="@+id/preview"
android:layout_width="372dp"
android:layout_height="240dp"
android:visibility="visible"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/playtest"
app:layout_constraintTop_toBottomOf="@id/preview"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintHorizontal_bias="0.5"
tools:ignore="MissingConstraints" />
</androidx.constraintlayout.widget.ConstraintLayout>
2.4 解码 demo实现效果
这里是找一个mp4格式的测试视频,使用ffmpeg将mp4格式中的视频码流输出出来。使用命令为:
$ffmpeg -i inputtest.mp4 -vcodec libx264 -preset slow -b:v 2000k -crf 21 out.h264
将其push到sd卡上,完整路径为:/sdcard/Download/ags/outputtest4.h264。实际运行效果展示如下: