android mediacodec 编码demo(java)

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的请求指导下。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值