Android 音视频任务7

先做任务7,是因为做完任务4感觉想要稍微深入了解一下MediaCodec,感觉这样更能把刚学到的知识连贯一下。

任务7. 学习 MediaCodec API,完成音频 AAC 硬编、硬解

MediaCodec的作用是转换编码或解码文件,支持已编码的特定格式转成原始raw数据,
原始raw数据转成特定编码的格式
解码时,mediaCodec设定的格式是输入文件的格式,输出原始raw数据
编码时,mediaCodec设定的格式是输出文件的格式,输入为原始raw数据

解码时文件的格式和解码器设置的格式一定要一样,否则报错

音频编解码:

解码:

  • 解码是吧audio/XXX给变成audio/raw,即pcm文件,最原始的未经压缩编码处理的文件。所以解码后的文件一般会比被压缩文件大得多
    流程:
  • 首先要使用MediaExtractor把音轨从文件中提取出来,把format也提取出来,根据要解码的mime创建codec:
    mDCodec = MediaCodec.createDecoderByType(srcMIMTType);
  • 把format设置到codec:mDCodec.configure(mFormat, null, null, 0); //视频文件如果要播放解码出的数据,则第二个参数选择要用于播放的surface
  • 数据回调监听:mDCodec.setCallback(mDecodeCallback); (异步处理,感觉异步的好一点)
  • 开始解码:mDCodec.start();
    callback中:
//输入
//onInputBufferAvailable:当有可用的缓冲区时,会转到这里,在该函数中往可用的缓冲区写入要解码的数据,然后把缓冲区加入待处理队列
public void onInputBufferAvailable(@NonNull MediaCodec codec, int index) {
//获得可用的缓冲
            ByteBuffer inputBuffer = codec.getInputBuffer(index);

            MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();

            int size = mExtractor.readSampleData(inputBuffer, 0);
            int flag = mExtractor.getSampleFlags();
            long presentation = mExtractor.getSampleTime();
            int offset = 0;
            Log.d(tag, "size= " + size + " flag=" + flag + " presentation=" + presentation);
//要对文件是否读到末尾单独处理,若读到末尾了,size,presentation都要置为0,flag要标注为 MediaCodec.BUFFER_FLAG_END_OF_STREAM
            if (size != -1) {
                inputSize += size;
                codec.queueInputBuffer(index, offset, size, presentation, flag);
                mExtractor.advance();
            } else { //当文件达到末尾时,用flag
                size = 0;
                presentation = 0;
                flag = MediaCodec.BUFFER_FLAG_END_OF_STREAM;
                codec.queueInputBuffer(index, offset, size, presentation, flag);
            }
}

//输出
//onOutputBufferAvailable:当有待处理数据完成解码时,会放入输出缓冲区,并转到这里,在该函数中获得解码后的raw数据,并且每次读出数据后要release当前缓冲,让它能够继续被使用,否则就那么一点缓冲区,不释放的话几下就没了
@Override
public void onOutputBufferAvailable(@NonNull MediaCodec codec, int index, @NonNull MediaCodec.BufferInfo info) {
        if (fos == null) try {
            throw new Exception("fos == null!");
        } catch (Exception e) {
            e.printStackTrace();
        }

        MediaFormat format = codec.getOutputFormat(index);
        Log.d(tag, "out format=" + format.getString(MediaFormat.KEY_MIME));

//获得可用的输出缓冲区(也就是解码好的数据)
        ByteBuffer outputBuffer = codec.getOutputBuffer(index);

        //如果直接定义 data =  new byte[100 * 1024]; 然后调用outputBuffer.get(data),
        // 则会使用data的长度去操作buffer,但buffer没这么大,导致BufferUnderflowException
        //所以下面两种方式选一种

        byte[] data = new byte[outputBuffer.limit()];
        outputBuffer.get(data);

//        byte[] data = new byte[100 * 1024];
//        outputBuffer.get(data,0, outputBuffer.limit());
//
        outputSize += data.length;
        try {
            fos.write(data); //写入流中
            fos.flush();
        } catch (IOException e) {
            e.printStackTrace();
        }
        Log.d(tag, "flag=" + info.flags);
        Log.d(tag, "outputSize= " + outputSize);
        //用完后释放这个buffer,使其可以接着被使用
        codec.releaseOutputBuffer(index, false);
//当到达末尾时,释放资源
        if (info.flags == MediaCodec.BUFFER_FLAG_END_OF_STREAM) {
            Log.d(tag, "BUFFER_FLAG_END_OF_STREAM");
            stopAndrealseCodec();
            exPCM2WAV();
        }
}
//释放资源
private void stopAndrealseCodec() {
    if (mDCodec != null) {
        mDCodec.release();
        mDCodec = null;
        mExtractor.release();
        mExtractor = null;
        Log.d(tag, "outputBuffer BUFFER_FLAG_END_OF_STREAM");
        try {
            fos.flush();
            fos.close();
        } catch (IOException e) {
            e.printStackTrace();
        }

        Log.d(tag, "stopAndrealseCodec");
        Log.d(tag, "inputSize=" + inputSize);
        Log.d(tag, "outputSize=" + outputSize);
    }
}

//把pcm文件加入wav头变成wav文件,wav文件会被识别成audio/raw,而原始的pcm文件会被识别错误
private void exPCM2WAV(){
    //为什么从视频里(只试了mp4和3gp)剥离出来的要 原本的sampleRate/2才是正常速度?
    int sampleRate = mFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE);
    PcmToWavUtil util = new PcmToWavUtil(sampleRate, mFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT),
            AudioFormat.ENCODING_PCM_16BIT);
    Log.d(tag, "sampleRate=" + sampleRate);
    util.pcmToWav(outputFileName, outputFileName + ".wav");
}

编码:

编码是吧audio/raw给变成audio/XXX,是对原始数据压缩编码,但这个有一个问题要注意的是硬件是否支持这种编码方式,比如nexus6p,由于mp3格式编码是不开源的,所以不能编码成mp3文件。下面以把wav的原生格式编码成aac
流程:
编码要比解码多一个步骤,就是设置要编码的格式的各项数据,比如

outFormat = MediaFormat.createAudioFormat(desMIMEType, SAMPLE_RATE, CHANNEL_COUNT); //根据编码格式,采样率,声道数创建format
outFormat.setInteger(MediaFormat.KEY_BIT_RATE, 96000); //比特率
outFormat.setInteger(MediaFormat.KEY_AAC_PROFILE,  MediaCodecInfo.CodecProfileLevel.AACObjectLC);  //aac特有的profile
outFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, 100 * 1024);//输入缓存的最大值,设置这个防止一帧的数据量太大而超过默认的buffer大小

//添加aac的csd-0,如果是视频,则csd-0和csd-1都要有(这个是干什么的还不清楚)
ByteBuffer csd = ByteBuffer.allocate(2);
csd.put((byte) ((aacObjLC << 3) | (sampleIndex >> 1)));
csd.position(1);
csd.put((byte) ((byte) ((sampleIndex << 7) & 0x80) | (channelCount << 3)));
csd.flip();
outFormat.setByteBuffer("csd-0", csd); // add csd-0
System.out.println(Arrays.toString(csd.array()) + "===++);
查看设备支持的编码和解码器:
MediaCodecList list = new MediaCodecList(MediaCodecList.ALL_CODECS);
MediaCodecInfo[] infos = list.getCodecInfos();
for (int i = 0; i < infos.length; i++) {
    String name = infos[i].getName();
    Log.d(tag, "i=" + i + " name=" + name);
}
根据format创建相应的编码器
//根据相应的格式找到是否有合适的编码器,如果有则返回其名称,然后创建相应的编码器
String encodeName = list.findEncoderForFormat(outFormat);
    Log.d(tag, "encodeName=" + encodeName);
    mECodec = MediaCodec.createByCodecName(encodeName);
} catch (IOException e) {
    e.printStackTrace();
}
mECodec.setCallback(mEncodeCallback); //设置回调
mECodec.configure(outFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);  //编码配置,编码时要设置最后一个参数
mECodec.start();  //开始编码

callback中:

onInputBufferAvailable和解码器的并无不同,都是由MediaExtractor读出数据放到缓存队列里等待处理

onOutputBufferAvailable和默认流程和解码器类似,都是从缓冲队列中取出已经处理好的数据(每次取一帧),但编码器不同的一点是,这个数据只是真实数据流的编码,仅仅有这些没办法封装成可读的格式,还必须要加上可识别的部分,比如wav文件是在所有数据头部加上wav头,aac文件是在每一帧数据前面加上adts头,
adts见 https://www.cnblogs.com/lihaiping/p/5284547.html

       public void onOutputBufferAvailable(@NonNull MediaCodec codec, int index, @NonNull MediaCodec.BufferInfo info) {
            if (fos == null) try {
                throw new Exception("fos == null!");
            } catch (Exception e) {
                e.printStackTrace();
            }
            int outIndex = index;
            MediaFormat format = codec.getOutputFormat(outIndex);

            ByteBuffer outputBuffer = codec.getOutputBuffer(outIndex);

            int size = outputBuffer.limit();
            outputSize += size;
            byte[] packedData = new byte[size + 7];
            addADTStoPacket(packedData, packedData.length);
            outputBuffer.get(packedData, 7, size);
            try {
                fos.write(packedData);
                fos.flush();
            } catch (IOException e) {
                e.printStackTrace();
            }
            //用完后释放这个buffer,使其可以接着被使用,如果一直不释放,如果文件太大则会导致缓冲区不够用
            outputBuffer.clear();
            codec.releaseOutputBuffer(outIndex, false);
            if (info.flags == MediaCodec.BUFFER_FLAG_END_OF_STREAM) {
                Log.d(tag, "BUFFER_FLAG_END_OF_STREAM");
                stopAndrealseCodec();
            }
        }




    //每一帧前面都要加上ADTS头,可以看做是每一个AAC帧的帧头
    private void addADTStoPacket(byte[] packet, int packetLen) {
        int profile = 2;  //AAC LC
        //39=MediaCodecInfo.CodecProfileLevel.AACObjectELD;
        int freqIdx = 4;  //44.1KHz
        int chanCfg = 2;  //CPE

//        byte[] packet1 = new byte[packetLen];
        // fill in ADTS data
        packet[0] = (byte) 0xFF;
        packet[1] = (byte) 0xF9;
        packet[2] = (byte) (((profile - 1) << 6) + (freqIdx << 2) + (chanCfg >> 2));
        packet[3] = (byte) (((chanCfg & 3) << 6) + (packetLen >> 11));
        packet[4] = (byte) ((packetLen & 0x7FF) >> 3);
        packet[5] = (byte) (((packetLen & 7) << 5) + 0x1F);
        packet[6] = (byte) 0xFC;


//        //把这些清晰的放出来,但最好还是用上面的方式
//        //syncword
//        packet[0] = (byte) 0xFF;
//        packet[1] |= 0xF << 4;
//        //id
//        packet[1] |= 0x1 << 3;
//        //layer
//        packet[1] |= 0x00 << 1;
//        //protection_abscent
//        packet[1] |= 0x1;
//        //profile
//        packet[2] |= (profile -1) << 6;
//        //sampling_frequency_index
//        packet[2] |= freqIdx << 2;
//        //private bit
//        packet[2] |= 0x0 << 1;
//        //channel_config
//        packet[2] |= chanCfg >> 2;  //高1位
//        packet[3] |= chanCfg << 6;   //低2位
//        //copy and home;
//        packet[3] |= 0x00 << 4;
//        //cib and cis
//        packet[3] |= 0x00 << 2;
//        //frame_length,单位是字节。不用管int是几个字节,把它当13位就行了,一帧的长度一般不可能超过13位能表达的最大值(8KB),
//        packet[3] |= packetLen >> 11;
//        packet[4] = (byte) (packetLen >> 3);
//        int x = packetLen << 5;
//        packet[5] |= packetLen << 5;
//
//        packet[5] |= 0x7FF >> 6;
//        packet[6] |= 0x7FF << 2;
//
    }

//释放资源
    private void stopAndrealseCodec() {
        if (mECodec != null) {
            mECodec.release();
            mECodec = null;
            mExtractor.release();
            mExtractor = null;
            Log.d(tag, "outputBuffer BUFFER_FLAG_END_OF_STREAM");
            try {
                fos.flush();
                fos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }

            Log.d(tag, "stopAndrealseCodec");
            Log.d(tag, "inputSize=" + inputSize);
            Log.d(tag, "outputSize=" + outputSize);
        }
    }

代码见 https://github.com/lujianyun06/VATask/tree/master/app/src/main/java/com/example/lll/va/Task7

猜你喜欢

转载自blog.csdn.net/weixin_43752854/article/details/84892830