《Ndk中使用Mediacode解码》
《android mediacodec 编码demo(java)》
《NDK中使用mediacodec编码h264》
《Android native 层使用opengl渲染YUV420p和NV12》
《android 使用NativeWindow渲染RGB视频》
《opengl 叠加显示文字》
《android studio 编译freeType》
《最原始的yuv图像叠加文字的实现--手动操作像素》
接着上一篇ndk mediacodec解码 https://blog.csdn.net/u012459903/article/details/113046538#comments_14743056,本想也来一遍 nkd mediacodec编码,事情总是充满坎坷曲折,为方便调试先从java层来一份 mediacode 编码,毕竟在java层调试还是比较方便文档也相对丰富。
环境,需要一个 Android studio上的hello world demo程序,加上可以读写sd卡的权限即可,这里只用mediacode从yuv420文件编码成h264直接存储到h264裸流文件,不mux.
(这里使用的是 android 6.0 api 23, 要申请sd权限,android 6.0 需要动态申请,android 10.0 还需要在AndroidMainfest.xml 的 application 中添加 android:requestLegacyExternalStorage="true", android 11.0 申请方法又不一样,本片中的申请方法就是requestPermission 就适用,也是从各处网络文章搜集过来的,总之很蛋疼。 另外,这个编码器的color_format, 各个版本不同机器又不一样,具体看实际调试了。)
准备资源, yuv420p 文件, 可以用ffmpeg 提取:
./ffmpeg -i 1080ptest.mp4 -ss 00:00:00 -t 5 -pix_fmt yuv420p test.yuv
ffmpeg 可以直接去官网下载 window版本编译好的 ffmpeg.exe工具。
这里提几个问题:(部分暂未解答,后续再补充)
Q1: mediacodec怎么强制关键帧?
Bundle param = new Bundle();
param.putInt( MediaCodec.PARAMETER_KEY_REQUEST_SYNC_FRAME,0);// 0 或者是1 无所谓,底层不会用到这个值,只需要这个信息
mediaCodec.setParameters(param);
这些在源码的 frameworks/av/media/libstagefright/ACodec.cpp:: setParameters() 中。

PARAMETER_KEY_REQUEST_SYNC_FRAME = "request-sync";
Q2: 在编码的过程中怎么动态调整码率?
本片中已有,直接调用
Bundle param = new Bundle();
param.putInt( MediaFormat.KEY_BIT_RATE,bitrate); // 准确点讲应该是 MediaCodec.PARAMETER_KEY_VIDEO_BITRATE
mediaCodec.setParameters(param);
Q3: mediacodec会按照该我设置的编码帧率,来控制编码速度么?
如下图,在实测中并不是如此,设置帧率10,dequeueOutputBuffer() 还是按照最快的速度吐出数据来,sps中的信息确实是10fps,(编码后的h264文件用ffprobe工具检查出来和编码器设置的一致为10fps)(这个速度应该是根据性能相关。如果编低分辨率的出数据可以更快),为何如此?是mediacode框架设计如此(尽最大的性能来尽快编码)?还是不同手机厂家实现的硬编码有差别?
q3补充: 利用libx264软编码做了个测试, 大概可以得出结论,(至少 x264编码器是如此)
设置码率和帧率,编码器会根据要求控制 帧平均大小, = 要求的码率(每一秒数据量)/ 要求的帧率(每一秒的帧数) 至于编码器吐数据的速度,那就是你计算机性能的问题。
测试使用 libx264编码yuv文件, 每次码率要求不变,要求帧率改变,编码出来的文件 视频码率是相同的,每一秒数据量相同,每一秒播放的帧数越多,自然每一帧的数据量越小,画质越差
编码命令:
直接在 videolan官网下载 x264, configure --disable-asm + make , x264这个工具默认从输入文件名后部 读取宽高,所有要命名成***672x378.yuv
./x264 --bitrate 500 --fps 10.0 tfdf_672x378.yuv -o tfdf_10fps.h264
./x264 --bitrate 500 --fps 20.0 tfdf_672x378.yuv -o tfdf_20fps.h264
./x264 --bitrate 500 --fps 40.0 tfdf_672x378.yuv -o tfdf_40fps.h264
./x264 --bitrate 500 --fps 40.0 tfdf_672x378.yuv -o tfdf_40fps.h264
./x264 --bitrate 500 --fps 60.0 tfdf_672x378.yuv -o tfdf_60fps.h264
./x264 --bitrate 500 --fps 80.0 tfdf_672x378.yuv -o tfdf_80fps.h264
./x264 --bitrate 500 --fps 100.0 tfdf_672x378.yuv -o tfdf_100fps.h264
编码出来的文件:
码率控制在500左右,帧率一个 100 一个10, 文件大小差不多将近10倍,画质可以明显看到差别
Q4:基于前一个问题,编码速度如果不受帧率影响,哪码率呢? 码率应该是一个平均值,1s的数据量统计,这个是否会被动态修改帧率所影响?如果降低码率的同时降低帧率,是否可以达到每一帧画质不降的效果?
上代码:
// Encoder.java canok
package com.example.myapplication;
import android.media.MediaCodec;
import android.media.MediaCodecInfo;
import android.media.MediaFormat;
import android.os.Bundle;
import android.util.Log;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
public class Encoder {
//同步编码
private static final String TAG = "Encoder";
//https://blog.csdn.net/u011913612/article/details/68943010
MediaCodec mediaCodec;
private boolean bRun = true;
private FileInputStream fileYUVSrcStream;
private FileOutputStream fileOutputStream;
private final int mWidth;
private final int mHeight;
private final int mFrameRate;
private boolean forhonor=true;
public Encoder(int w, int h, int fps) {
mWidth = w;
mHeight = h;
mFrameRate = fps;
}
public void start(String yuvfilein,String h264fileout) {
Log.d(TAG, "start: "+yuvfilein+" out:"+h264fileout);
bRun = true;
new Thread(new Runnable() {
@Override
public void run() {
int framelen = mWidth*mHeight*3/2;
byte[] frame = new byte[framelen];
int inCount =0;
int outCount=0;
int resettime =0;
try {
fileOutputStream = new FileOutputStream(h264fileout, false);
} catch (FileNotFoundException e) {
Log.d(TAG, "run: cannot create file!");
e.printStackTrace();
return;
}
try {
fileYUVSrcStream = new FileInputStream(yuvfilein);
try {
fileYUVSrcStream.mark(fileYUVSrcStream.available());
} catch (IOException e) {
e.printStackTrace();
return;
}
} catch (FileNotFoundException e) {
e.printStackTrace();
return;
}
try {
mediaCodec = MediaCodec.createEncoderByType("video/avc");
} catch (IOException e) {
e.printStackTrace();
}
if(mediaCodec == null){
Log.d(TAG, "run: err create!");
return;
}
MediaFormat inputMediaFormat = MediaFormat.createVideoFormat("video/avc", mWidth, mHeight);
inputMediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, 500*1000);
inputMediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, mFrameRate);
inputMediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 5);//单位为 秒
//inputMediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420PackedPlanar);
if(forhonor) {
//不支持?? 荣耀 老手机,Acodec报错
// inputMediaFormat.setInteger(MediaFormat.KEY_PREPEND_HEADER_TO_SYNC_FRAMES,1);
inputMediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible);
}
else {
inputMediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_QCOM_FormatYUV420SemiPlanar);
// inputMediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar);
inputMediaFormat.setInteger(MediaFormat.KEY_PREPEND_HEADER_TO_SYNC_FRAMES,1);
}
mediaCodec.configure(inputMediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
mediaCodec.start();
while (bRun) {
//无限超时
int inputBufferIndex = mediaCodec.dequeueInputBuffer(-1);
if (inputBufferIndex >= 0) {
ByteBuffer buffer = mediaCodec.getInputBuffer(inputBufferIndex);
try {
if(fileYUVSrcStream.available()<framelen){
Log.d(TAG, "reset[][][][][][][][]");
// fileYUVSrcStream.reset(); 报错
fileYUVSrcStream.close();
fileYUVSrcStream = new FileInputStream(yuvfilein); //干脆重新打开
if(0==resettime) {
changeBitRate(100);
changeFrameRate(mFrameRate * 3);
fileOutputStream.close();
fileOutputStream = new FileOutputStream(h264fileout + (resettime++), false);
}
}
} catch (IOException e) {
e.printStackTrace();
}
try {
fileYUVSrcStream.read(frame);
} catch (IOException e) {
e.printStackTrace();
// if(e instanceof EOFException){
// try {
// Log.d(TAG, "reset[][][][][][][][]");
// fileYUVSrcStream.reset();
// } catch (IOException ioException) {
// ioException.printStackTrace();
// }
// }
}
buffer.clear();
buffer.limit(frame.length);
buffer.put(frame, 0, frame.length);
//入队列,放入数据
Log.d(TAG, "in<<<<<<<"+(inCount++));
mediaCodec.queueInputBuffer(inputBufferIndex, 0, frame.length, 0, MediaCodec.BUFFER_FLAG_CODEC_CONFIG);
//取编码后的数据, 无限超时, 这里采取入一帧出一帧的模式。
MediaCodec.BufferInfo mBufferInfo = new MediaCodec.BufferInfo();
int outIndex = mediaCodec.dequeueOutputBuffer(mBufferInfo, -1);
if(outIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED){
// //头一次读,到这里 竟然报错空指针?
// MediaFormat outformat = mediaCodec.getOutputFormat();
// int bitrate = outformat.getInteger(MediaFormat.KEY_BIT_RATE);
// int w = outformat.getInteger(MediaFormat.KEY_WIDTH);
// int h = outformat.getInteger(MediaFormat.KEY_HEIGHT);
// int framrate = outformat.getInteger(MediaFormat.KEY_FRAME_RATE);
// int iInternal = outformat.getInteger(MediaFormat.KEY_I_FRAME_INTERVAL);
// Log.d(TAG, "INFO_OUTPUT_FORMAT_CHANGED: " + w + "X" + h + "@" + framrate + "|" + iInternal);
//继续读
outIndex = mediaCodec.dequeueOutputBuffer(mBufferInfo, -1);
}else if(outIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED){
Log.d(TAG, "INFO_OUTPUT_BUFFERS_CHANGED ");
}
if(outIndex >= 0) {
ByteBuffer outputBuffer = mediaCodec.getOutputBuffer(outIndex);
byte[] bb = new byte[mBufferInfo.size];
outputBuffer.get(bb);
try {
Log.d(TAG, "out>>>>>"+(outCount++)+"isKeyFram "+(mBufferInfo.flags== MediaCodec.BUFFER_FLAG_KEY_FRAME));
fileOutputStream.write(bb);
} catch (IOException e) {
e.printStackTrace();
}
//头一次读,到这里 竟然报错空指针?
// MediaFormat outformat = mediaCodec.getOutputFormat(outIndex);
// int bitrate = outformat.getInteger(MediaFormat.KEY_BIT_RATE);
// int w = outformat.getInteger(MediaFormat.KEY_WIDTH);
// int h = outformat.getInteger(MediaFormat.KEY_HEIGHT);
// int framrate = outformat.getInteger(MediaFormat.KEY_FRAME_RATE);
// int iInternal = outformat.getInteger(MediaFormat.KEY_I_FRAME_INTERVAL);
// Log.d(TAG, "INFO_OUTPUT_FORMAT_CHANGED: " + w + "X" + h + "@" + framrate + "|" + iInternal);
mediaCodec.releaseOutputBuffer(outIndex, false);
}else {
Log.d(TAG, "run: outindex "+outIndex);
}
} else {
Log.d(TAG, "run: inputBufferindex:"+inputBufferIndex);
}
}
mediaCodec.stop();
mediaCodec.release();
mediaCodec = null;
if( fileOutputStream!=null) {
try {
fileOutputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if( fileYUVSrcStream!=null) {
try {
fileYUVSrcStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}).start();
}
public void stop() {
bRun = false;
}
public void changeBitRate(int bitrate){
if(mediaCodec!=null){
Bundle param = new Bundle();
param.putInt( MediaFormat.KEY_BIT_RATE,bitrate);
Log.d(TAG, "changeBitRate: "+bitrate);
mediaCodec.setParameters(param);
}
}
public void changeFrameRate(int frameRate){
if(mediaCodec!=null){
Bundle param = new Bundle();
param.putInt( MediaFormat.KEY_FRAME_RATE,frameRate);
Log.d(TAG, "changeFrameRate: "+frameRate);
mediaCodec.setParameters(param);
}
}
}
把MainActivity也放上来:
package com.example.myapplication;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import android.Manifest;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.provider.Settings;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;
public class MainActivity extends AppCompatActivity {
private Button mButtonDecode;
private Button mButtonBitrate;
private Button mButtonFramerate;
private Encoder mEncoder;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//verifyStoragePermissions(this);
requestPermission(this);
mButtonDecode = findViewById(R.id.start);
mButtonDecode.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if(null ==mEncoder){
// mEncoder=new Encoder(768,432,10);
// mEncoder.start("/storage/emulated/0/canok/in.yuv","/storage/emulated/0/canok/out.h264");
mEncoder=new Encoder(1920,1080,10);
mEncoder.start("/storage/emulated/0/canok/1080p60.yuv","/storage/emulated/0/canok/out_1080.h264");
}
}
});
mButtonBitrate = findViewById(R.id.changebitrate);
mButtonBitrate.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if(null != mEncoder){
mEncoder.changeBitRate(1000*1000);
}
}
});
mButtonFramerate = findViewById(R.id.changeframerate);
mButtonFramerate.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if(null != mEncoder){
mEncoder.changeFrameRate(60);
}
}
});
}
private static final int REQUEST_EXTERNAL_STORAGE = 1;
private static String[] PERMISSIONS_STORAGE = {
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.INTERNET};
public static boolean verifyStoragePermissions(Activity activity) {
/*******below android 6.0*******/
if(Build.VERSION.SDK_INT < 23) {
return true;
}
// Check if we have write permission
int permission = ActivityCompat.checkSelfPermission(activity,
Manifest.permission.WRITE_EXTERNAL_STORAGE);
if (permission != PackageManager.PERMISSION_GRANTED) {
// We don't have permission so prompt the user
ActivityCompat.requestPermissions(activity,PERMISSIONS_STORAGE,
REQUEST_EXTERNAL_STORAGE);
return false;
}
else {
return true;
}
}
private static final int REQUEST_CODE = 1024;
private void requestPermission(Context context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
// 先判断有没有权限
if (Environment.isExternalStorageManager()) {
writeFile();
} else {
Intent intent = new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION);
intent.setData(Uri.parse("package:" + context.getPackageName()));
startActivityForResult(intent, REQUEST_CODE);
}
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// 先判断有没有权限
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED &&
ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {
writeFile();
} else {
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE}, REQUEST_CODE);
}
} else {
writeFile();
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == REQUEST_CODE) {
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED &&
ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {
writeFile();
} else {
}
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_CODE && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
if (Environment.isExternalStorageManager()) {
writeFile();
} else {
}
}
}
/**
* 模拟文件写入
*/
private void writeFile() {
}
}
布局文件,放三个按钮:
<?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:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
tools:ignore="MissingConstraints">
<Button
android:id="@+id/start"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="开始" />
<Button
android:id="@+id/changebitrate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="bitrate" />
<Button
android:id="@+id/changeframerate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="framerate" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
上述有一部分代码,想实现 读yuv文件到末尾的时候,将文件seek到开头,如此循环读取文件源源不断,用了 mark+reset, 配合 instanceof EOFException 来判断结尾,结果未能如愿, 没办法直接把 FileInputStream 重新new才达到目的, 有熟练java的请求指导下。