详解MediaCodeC 音视频编解码原理之自定义音频格式转换

​ MediaCodeC是Android 4.1(API16 ) 版本加入的一个新的音视频处理API,旨在提高Android平台的音视频编码能力,Mediacodec类可用于访问底层的媒体编解码器,即编码器/解码器组件。这是Android底层的多媒体支持基础设施的一部分(通常与 MediaExtractor, MediaSync, MediaMuxer, MediaCrypto, MediaDrm, Image, Surface, AudioTrack

MediaCodeC工作流程:

详解MediaCodeC 音视频编解码原理之自定义音频格式转换_第1张图片

MediaCodeC 状态机


详解MediaCodeC 音视频编解码原理之自定义音频格式转换_第2张图片

codec(编解码器)通过异步的方式对输入的数据进行处理,输出处理后的数据,过程中需要一系列的输入/输出Buffers。最简单的情况下,先把数据放进一个空的输入buffer(申请或者获取得到)中,发送给codec,codec对数据进行处理转换后放进一个输出buffer中,拿到输出buffer后,自行处理输出buffer中的数据,之后释放输出buffer并返回给codec。

使用MediaCodeC需要注意几点:

1.MediaCodeC是4.1(API16)才开始引入的,后续的几个版本中API不断更改API,(大部分核心API都会发现标记为过时,需要更换新的版本) , 5.0才加入了异步模式,一般来说,5.0以下是同步获取的,需要准备一个缓冲区不断的刷新请求MediaCodeC获取解码后的数据(后面有详细代码示例);

2.MediaCodeC由于众所周知的原因,Google并没有预制很多解码器,相反,解码器都是后期手机厂商自己register进去的,MediaCodeC只提供抽象的接口,具体实现在各个手机上,当然,Android源码里面引入了一套AAC编码器(因为AAC是开放标准的 ),而大家常用的MP3是有版权的,所以原生MediaCodeC无法编码MP3,除非厂商提供MP3编码器)

3.MediaCodeC适用于音视频编/解码,其底层最后调用的是 OpenGL ES,最终调用手机 GPU 进行工作,是 Android 提供给我们的一种音视频编解码高级 API,所以编/解码效率跟手机CPU 有很大的关系,经多台测试机调查得知,高通的 CPU 明显有优势,同时,手机性能有限,编/解码需要耗费大量的内存及 cpu 时间,亲测骁龙801解码一个5分钟的音频,大概花费了30S(慢的不能忍受是吧,兄弟,你还没测试过联发科(MTK )呢,那才叫一个慢),所以,能不在手机上做编/解码工作,尽量不要做(那不废话吗,不然我讲啥!!)

详解MediaCodeC 音视频编解码原理之自定义音频格式转换_第3张图片

那如何查看自己的手机自带哪些解码器呢:

在我们手机system/etc/目录下,有一个media_codecs.xml文件,里面记录了手机厂商在出厂时配置的音视频软、硬件解码库及其支持的媒体编码格式。我们可以使用MediaCodecList相关的api来查看是否支持;

5.0(API16)以上,你可以这样获取

@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
  private void checkMediaDecoder() {
    MediaCodecList mediaCodecList = new MediaCodecList(MediaCodecList.ALL_CODECS);

    MediaCodecInfo[] codecInfos=      mediaCodecList.getCodecInfos();
    for (MediaCodecInfo codecInfo : codecInfos) {
      Log.i("TAG", "codecInfo =" + codecInfo.getName());
    }
  }

打印出(HTC E9pw):

codecInfo =OMX.MTK.AUDIO.DECODER.MP3
codecInfo =OMX.google.opus.decoder
codecInfo =OMX.MTK.AUDIO.DECODER.ADPCM.MS
codecInfo =OMX.MTK.AUDIO.DECODER.ADPCM.DVI
codecInfo =OMX.MTK.AUDIO.DECODER.GSM
codecInfo =OMX.MTK.AUDIO.DECODER.RAW
codecInfo =OMX.MTK.AUDIO.DECODER.G711.ALAW
codecInfo =OMX.MTK.AUDIO.DECODER.G711.MLAW
codecInfo =OMX.MTK.AUDIO.DECODER.FLAC
codecInfo =OMX.MTK.AUDIO.DECODER.WMA
codecInfo =OMX.MTK.AUDIO.DECODER.WMAPRO
codecInfo =OMX.MTK.AUDIO.DECODER.APE
codecInfo =OMX.MTK.AUDIO.DECODER.ALAC
codecInfo =OMX.google.amrnb.decoder
codecInfo =OMX.google.amrwb.decoder
codecInfo =OMX.google.aac.decoder
codecInfo =OMX.google.vorbis.decoder
codecInfo =OMX.MTK.VIDEO.DECODER.HEVC
codecInfo =OMX.MTK.VIDEO.DECODER.MPEG2
codecInfo =OMX.MTK.VIDEO.DECODER.MPEG4
codecInfo =OMX.MTK.VIDEO.DECODER.H263
codecInfo =OMX.MTK.VIDEO.DECODER.AVC
codecInfo =OMX.MTK.VIDEO.DECODER.VPX
codecInfo =OMX.MTK.VIDEO.DECODER.VP9
codecInfo =OMX.MTK.VIDEO.DECODER.VC1
codecInfo =OMX.MTK.VIDEO.DECODER.DIVX
codecInfo =OMX.MTK.VIDEO.DECODER.DIVX3
codecInfo =OMX.MTK.VIDEO.DECODER.XVID
codecInfo =OMX.MTK.VIDEO.DECODER.S263
codecInfo =OMX.google.vp8.decoder
codecInfo =OMX.google.h264.decoder
codecInfo =OMX.dolby.ac3.decoder
codecInfo =OMX.dolby.ec3.decoder
codecInfo =OMX.dolby.ec3_joc.decoder
codecInfo =OMX.ARICENT.VIDEO.DEC.WMV
codecInfo =OMX.MTK.VIDEO.ENCODER.MPEG4
codecInfo =OMX.MTK.VIDEO.ENCODER.H263
codecInfo =OMX.MTK.VIDEO.ENCODER.AVC
codecInfo =OMX.MTK.VIDEO.ENCODER.HEVC
codecInfo =OMX.MTK.AUDIO.ENCODER.VORBIS
codecInfo =OMX.MTK.AUDIO.ENCODER.ADPCM.MS
codecInfo =OMX.MTK.AUDIO.ENCODER.ADPCM.DVI
codecInfo =OMX.google.aac.encoder
codecInfo =OMX.google.amrnb.encoder
codecInfo =OMX.google.amrwb.encoder
codecInfo =OMX.google.flac.encoder
codecInfo =OMX.google.vp8.encoder

同时, 创建MediaCodeC的时候也会打印出来解码器类型

比如,创建一个 MediaCodeC 音频编码器,会打印出

I/OMXClient: Using client-side OMX mux.
E/OMXMaster: A component of name 'OMX.qcom.audio.decoder.aac' 

可以看到,手头的测试机(OPPO x9007 4.4)自带解码器为

OMX.qcom.audio.decoder.aac

说明可以对音频进行 AAC 编解码处理;

当然,系统版本越高,手机厂商加入的编码器越多,这里打印的内容越多

理解了 MediaCodeC 的一些基本概念,那音视频解码是怎么回事?

1.先创建一个 MediaCodeC

MediaCodeC 采用工厂方法模式,需要调用其createDecoderByType方法

MediaCodec mediaDecode = MediaCodec.createDecoderByType(mime);

参数 mine 做过音视频的童鞋肯定知道,代表解码的文件类型,以下是官方提供的类型列表

"video/x-vnd.on2.vp8" - VP8 video (i.e. video in .webm)
"video/x-vnd.on2.vp9" - VP9 video (i.e. video in .webm)
"video/avc" - H.264/AVC video
"video/hevc" - H.265/HEVC video
"video/mp4v-es" - MPEG4 video
"video/3gpp" - H.263 video
"audio/3gpp" - AMR narrowband audio
"audio/amr-wb" - AMR wideband audio
"audio/mpeg" - MPEG1/2 audio layer III
"audio/mp4a-latm" - AAC audio (note, this is raw AAC packets, not packaged in LATM!)
"audio/vorbis" - vorbis audio
"audio/g711-alaw" - G.711 alaw audio
"audio/g711-mlaw" - G.711 ulaw audio

列出了 Android 所支持的所有类型

如果是从一个音/视频文件中,如何获取该文件的mine呢

MediaExtractor闪亮登场,专门用户提取文件中的 音/视频中的一些关键信息

官方实例如下:

MediaExtractor extractor = new MediaExtractor();
 extractor.setDataSource(...);//设置数据源
 int numTracks = extractor.getTrackCount();
 for (int i = 0; i < numTracks; ++i) {
   MediaFormat format = extractor.getTrackFormat(i);
   String mime = format.getString(MediaFormat.KEY_MIME);
   if (weAreInterestedInThisTrack) {
     extractor.selectTrack(i);
   }
 }
 ByteBuffer inputBuffer = ByteBuffer.allocate(...)
 while (extractor.readSampleData(inputBuffer, ...) >= 0) {
   int trackIndex = extractor.getSampleTrackIndex();
   long presentationTimeUs = extractor.getSampleTime();
   ...
   extractor.advance();
 }

 extractor.release();
 extractor = null;

extractor.getTrackCount()获取文件的媒体轨道,我们知道,音频和视频.字幕分别在不同的轨道,遍历完轨道后用extractor.getTrackFormat(i);获取当前轨道的格式信息对象MediaFormat

拿到MediaFromat 后,你可以获取任何你想要的信息

其中,MediaFromat 是用一个 Map 存储所有的信息,而Key为属性名,

使用方法如下

//获取音频的时长
long audioLength = format.getLong(MediaFormat.KEY_DURATION);
//获取文件的 mine(类型是音频还是视频)
 String mime = format.getString(MediaFormat.KEY_MIME);

如果需要别的信息,你可以在MediaFormat定义的Static 属性里面找;

开始解码:

MediaCodec codec = MediaCodec.createByCodecName(name);
 codec.configure(format, …);//配置 MediaCodeC
 codec.start();//开始解码
 ByteBuffer[] inputBuffers = codec.getInputBuffers();//获取输入的缓存区
 ByteBuffer[] outputBuffers = codec.getOutputBuffers();//获取输出的缓冲区
 for (;;) {
   int inputBufferId = codec.dequeueInputBuffer(…);
   if (inputBufferId >= 0) {
     codec.queueInputBuffer(inputBufferId, …);
   }
   int outputBufferId = codec.dequeueOutputBuffer(…);
   if (outputBufferId >= 0) {
     codec.releaseOutputBuffer(outputBufferId, …);
   } else if (outputBufferId == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
     outputBuffers = codec.getOutputBuffers();
   } else if (outputBufferId == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
     MediaFormat format = codec.getOutputFormat();
   }
 }
 codec.stop();
 codec.release();

最终得到的解码后的数据存放到outputBuffers中,可以将outputBuffers中的数据编码成为您想要的格式,整个流程结束;

下面就 Android 平台常用的将 MP3转换成 WAV 格式功能来更深入的理解 MediaCodeC的强大功能:

初始化解码器:

/** * 初始化解码器 */
  private void initMediaDecode() {
    try {
      if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
        mediaExtractor = new MediaExtractor();// 此类可分离视频文件的音轨和视频轨道
        // 设置音频采样率,44100是目前的标准,但是某些设备仍然支持22050,16000,11025
        // final int kSampleRates[] = {8000, 11025, 22050, 44100, 48000, 24000};
        // MediaFormat.createAudioFormat(
        // MediaFormat.KEY_PCM_ENCODING, kSampleRates[3], 2);
        // 比特率 声音中的比特率是指将模拟声音信号转换成数字声音信号后,单位时间内的二进制数据量,是间接衡量音频质量的一个指标
        // final int kBitRates[] = {64000, 96000, 128000};
        // int minBufferSize = AudioRecord.getMinBufferSize(kSampleRates[3],
        // AudioFormat.CHANNEL_IN_STEREO, AudioFormat.ENCODING_PCM_16BIT);
        mediaExtractor.setDataSource(srcPath);// 媒体文件的位置
        for (int i = 0; i < mediaExtractor.getTrackCount(); i++) {// 遍历媒体轨道 此处我们传入的是音频文件,所以也就只有一条轨道
          MediaFormat format = mediaExtractor.getTrackFormat(i);
          // 比特率 声音中的比特率是指将模拟声音信号转换成数字声音信号后,单位时间内的二进制数据量,是间接衡量音频质量的一个指标
          format.setInteger(MediaFormat.KEY_BIT_RATE, AudioFormat.ENCODING_PCM_16BIT);
          try {
            audioLength = format.getLong(MediaFormat.KEY_DURATION);
          } catch (Exception e) {
            e.printStackTrace();
          }
          // format.setInteger(MediaFormat.KEY_BIT_RATE, kBitRates[1]);
          // format.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, minBufferSize);
          // format.setInteger(MediaFormat.KEY_SAMPLE_RATE, kSampleRates[3]);
          // format.setInteger(MediaFormat.KEY_CHANNEL_COUNT, AudioFormat.CHANNEL_IN_STEREO);
          String mime = format.getString(MediaFormat.KEY_MIME);
          if (mime.startsWith("audio")) {// 获取音频轨道
            mediaExtractor.selectTrack(i);// 选择此音频轨道
            mediaDecode = MediaCodec.createDecoderByType(mime);// 创建Decode解码器
            mediaDecode.configure(format, null, null, 0);
            break;
          }
        }
        if (mediaDecode == null) {
          Log.e(TAG, "create mediaDecode failed");
          return;
        }
        mediaDecode.start();// 启动MediaCodec ,等待传入数据
        decodeInputBuffers = mediaDecode.getInputBuffers();// MediaCodec在此ByteBuffer[]中获取输入数据
        decodeOutputBuffers = mediaDecode.getOutputBuffers();// MediaCodec将解码后的数据放到此ByteBuffer[]中
                                                             // 我们可以直接在这里面得到PCM数据
        decodeBufferInfo = new MediaCodec.BufferInfo();// 用于描述解码得到的byte[]数据的相关信息
        showLog("buffers:" + decodeInputBuffers.length);
      }
    } catch (Exception e) {
      e.printStackTrace();
    }
  }

5.0以下解码逻辑:

/** * 解码{@link #srcPath}音频文件 得到PCM数据块 * * @return 是否解码完所有数据 */
  private void srcAudioFormatToPCM() {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
      if (decodeInputBuffers != null) {
        try {
          for (int i = 0; i < decodeInputBuffers.length - 1; i++) {
            ;// 获取可用的inputBuffer -1代表一直等待,0表示不等待 建议-1,避免丢帧
            int inputIndex = mediaDecode.dequeueInputBuffer(-1);
            if (inputIndex < 0) {
              codeOver = true;
              return;
            }
            ByteBuffer inputBuffer = decodeInputBuffers[inputIndex];// 拿到inputBuffer
            inputBuffer.clear();// 清空之前传入inputBuffer内的数据
            int sampleSize = mediaExtractor.readSampleData(inputBuffer, 0);// MediaExtractor读取数据到inputBuffer中
            if (sampleSize < 0) {// 小于0 代表所有数据已读取完成
              codeOver = true;
            } else {
              mediaDecode.queueInputBuffer(inputIndex, 0, sampleSize, 0, 0);// 通知MediaDecode解码刚刚传入的数据
              mediaExtractor.advance();// MediaExtractor移动到下一取样处
            }
          }

          // 获取解码得到的byte[]数据 参数BufferInfo上面已介绍 10000同样为等待时间 同上-1代表一直等待,0代表不等待。此处单位为微秒
          // 此处建议不要填-1 有些时候并没有数据输出,那么他就会一直卡在这 等待
          int outputIndex = mediaDecode.dequeueOutputBuffer(decodeBufferInfo, 1000000);

          ByteBuffer outputBuffer;
          byte[] chunkPCM;
          while (outputIndex >= 0) {// 每次解码完成的数据不一定能一次吐出 所以用while循环,保证解码器吐出所有数据
            outputBuffer = decodeOutputBuffers[outputIndex];// 拿到用于存放PCM数据的Buffer
            chunkPCM = new byte[decodeBufferInfo.size];// BufferInfo内定义了此数据块的大小
            outputBuffer.get(chunkPCM);// 将Buffer内的数据取出到字节数组中
            outputBuffer.clear();// 数据取出后一定记得清空此Buffer MediaCodec是循环使用这些Buffer的,不清空下次会得到同样的数据
            putPCMData(chunkPCM);// 自己定义的方法,供编码器所在的线程获取数据,下面会贴出代码
            mediaDecode.releaseOutputBuffer(outputIndex, false);// 此操作一定要做,不然MediaCodec用完所有的Buffer后
                                                                // 将不能向外输出数据
            outputIndex = mediaDecode.dequeueOutputBuffer(decodeBufferInfo, 10000);// 再次获取数据,如果没有数据输出则outputIndex=-1
                                                                                   // 循环结束
          }

          if (codeOver) {
            PCMToMp3.pcmToMp3(srcPath, desPath, chunkPCMDataContainer, audioLength);
            if (onCompleteListener != null) {
              onCompleteListener.completed(desPath);
            }
          }
        } catch (Exception e) {
          e.printStackTrace();
          codeOver = true;
          PCMToMp3.pcmToMp3(srcPath, desPath, chunkPCMDataContainer, audioLength);
          if (onCompleteListener != null) {
            onCompleteListener.completed(desPath);
          }
        }
      }
    }
  }

5.0以上解码逻辑:

/** * android 5.0以上 */
  private void srcAudioFormatToPCMHigherApi() {
    if (Build.VERSION.SDK_INT >= 21) {
      boolean sawOutputEOS = false;
      final long kTimeOutUs = 10000;
      long presentationTimeUs;
      while (!sawOutputEOS) {
        try {
          int inputIndex = mediaDecode.dequeueInputBuffer(-1);
          if (inputIndex >= 0) {
            ByteBuffer inputBuffer = mediaDecode.getInputBuffer(inputIndex);
            if (inputBuffer != null) {
              int sampleSize = mediaExtractor.readSampleData(inputBuffer, 0);
              if (sampleSize < 0) {// 小于0 代表所有数据已读取完成
                sawOutputEOS = true;
                codeOver = true;
                break;
              } else {
                presentationTimeUs = mediaExtractor.getSampleTime();
                mediaDecode.queueInputBuffer(inputIndex, 0, sampleSize, presentationTimeUs, 0);// 通知MediaDecode解码刚刚传入的数据
                mediaExtractor.advance();
              }
            }
          } else {
            sawOutputEOS = true;
            codeOver = true;
          }
          int outputIndex = mediaDecode.dequeueOutputBuffer(decodeBufferInfo, kTimeOutUs);
          ByteBuffer outputBuffer;// = mediaDecode.getOutputBuffer(outputIndex);//
                                  // 拿到用于存放PCM数据的Buffer
          while (outputIndex >= 0) {
            outputBuffer = mediaDecode.getOutputBuffer(outputIndex);
            boolean doRender = (decodeBufferInfo.size != 0);
            if (doRender && outputBuffer != null) {
              outputBuffer.position(decodeBufferInfo.offset);
              outputBuffer.limit(decodeBufferInfo.offset + decodeBufferInfo.size);
              byte[] chunkPCM = new byte[decodeBufferInfo.size];// BufferInfo内定义了此数据块的大小
              outputBuffer.get(chunkPCM);
              outputBuffer.clear();// 数据取出后一定记得清空此Buffer MediaCodec是循环使用这些Buffer的,不清空下次会得到同样的数据
              putPCMData(chunkPCM);// 自己定义的方法,供编码器所在的线程获取数据,下面会贴出代码
              mediaDecode.releaseOutputBuffer(outputIndex, false);// 此操作一定要做,不然MediaCodec用完所有的Buffer后将不能向外输出数据
              outputIndex = mediaDecode.dequeueOutputBuffer(decodeBufferInfo, kTimeOutUs);
            }
          }
        } catch (Exception e) {
          e.printStackTrace();
          sawOutputEOS = true;
          codeOver = true;
        }
      }
      if (codeOver) {
        PCMToMp3.pcmToMp3(srcPath, desPath, chunkPCMDataContainer, audioLength);
        if (onCompleteListener != null) {
          onCompleteListener.completed(desPath);
        }
      }
    }
  }

解码成功后,将 MP3格式的文件解码成最原始的字节码即 PCM 格式,需要对 PCM 做进一步转换成WAV格式便于设备播放

PCM 转换成 wav

 /**
   * pcm文件转wav文件
   *
   * @param inFilename  源文件路径
   * @param outFilename 目标文件路径
   */
  public void pcmToWav(String inFilename, String outFilename) {
    FileInputStream in;
    FileOutputStream out;
    long totalAudioLen;
    long totalDataLen;
    long longSampleRate = mSampleRate;
    int channels = 2;
    long byteRate = 16 * mSampleRate * channels / 8;
    byte[] data = new byte[mBufferSize];
    try {
      in = new FileInputStream(inFilename);
      out = new FileOutputStream(outFilename);
      totalAudioLen = in.getChannel().size();
      totalDataLen = totalAudioLen + 36;

      writeWaveFileHeader(out, totalAudioLen, totalDataLen,
        longSampleRate, channels, byteRate);
      while (in.read(data) != -1) {
        out.write(data);
      }
      in.close();
      out.close();
    }  catch (IOException e) {
      e.printStackTrace();
    }
  }

  /**
   * 加入wav文件头
   */
  private void writeWaveFileHeader(FileOutputStream out, long totalAudioLen,
                                   long totalDataLen, long longSampleRate, int channels, long byteRate)
    throws IOException {
    byte[] header = new byte[44];
    header[0] = 'R'; // RIFF/WAVE header
    header[1] = 'I';
    header[2] = 'F';
    header[3] = 'F';
    header[4] = (byte) (totalDataLen & 0xff);
    header[5] = (byte) ((totalDataLen >> 8) & 0xff);
    header[6] = (byte) ((totalDataLen >> 16) & 0xff);
    header[7] = (byte) ((totalDataLen >> 24) & 0xff);
    header[8] = 'W';  //WAVE
    header[9] = 'A';
    header[10] = 'V';
    header[11] = 'E';
    header[12] = 'f'; // 'fmt ' chunk
    header[13] = 'm';
    header[14] = 't';
    header[15] = ' ';
    header[16] = 16;  // 4 bytes: size of 'fmt ' chunk
    header[17] = 0;
    header[18] = 0;
    header[19] = 0;
    header[20] = 1;   // format = 1
    header[21] = 0;
    header[22] = (byte) channels;
    header[23] = 0;
    header[24] = (byte) (longSampleRate & 0xff);
    header[25] = (byte) ((longSampleRate >> 8) & 0xff);
    header[26] = (byte) ((longSampleRate >> 16) & 0xff);
    header[27] = (byte) ((longSampleRate >> 24) & 0xff);
    header[28] = (byte) (byteRate & 0xff);
    header[29] = (byte) ((byteRate >> 8) & 0xff);
    header[30] = (byte) ((byteRate >> 16) & 0xff);
    header[31] = (byte) ((byteRate >> 24) & 0xff);
    header[32] = (byte) (2 * 16 / 8); // block align
    header[33] = 0;
    header[34] = 16;  // bits per sample
    header[35] = 0;
    header[36] = 'd'; //data
    header[37] = 'a';
    header[38] = 't';
    header[39] = 'a';
    header[40] = (byte) (totalAudioLen & 0xff);
    header[41] = (byte) ((totalAudioLen >> 8) & 0xff);
    header[42] = (byte) ((totalAudioLen >> 16) & 0xff);
    header[43] = (byte) ((totalAudioLen >> 24) & 0xff);
    out.write(header, 0, 44);
  }

你可能感兴趣的:(详解MediaCodeC 音视频编解码原理之自定义音频格式转换)