Android硬编解码MediaCodec使用笔记

Android MediaCodec 编码使用介绍 编码篇:1、获取编解码器信息首先我们需要获取Codec的信息,通过MediaCodecList这个类来实现获取到MediaCodecInfo,代码示例如下:i++) {if (!continue;j++) {这里我们需要设置mimeType,这是一个字符串比如我们需要设置avc格式的视频那么我们可以将其设置为然后选择合适我们的编码器2、设置编码参数设置编码参数通过mediaFormat来实现,代码·示例如下:首先创建MediaFormat实例。 阅读详情

一、MediaCodec介绍

MediaCodec类可以用来访问底层媒体编解码器,即编码器/解码器的组件。 它是Android底层多媒体支持架构的一部分(通常与MediaExtractor,MediaSync,MediaMuxer,MediaCrypto,MediaDrm,Image,Surface和AudioTrack一起使用)。
这里写图片描述
In broad terms, a codec processes input data to generate output data. It processes data asynchronously and uses a set of input and output buffers. At a simplistic level, you request (or receive) an empty input buffer, fill it up with data and send it to the codec for processing. The codec uses up the data and transforms it into one of its empty output buffers. Finally, you request (or receive) a filled output buffer, consume its contents and release it back to the codec.
从广义上讲,一个编解码器处理输入数据以生成输出数据。 它异步地处理数据,并使用一组输入和输出缓冲器。 从一个简单的层面上看,可请求(或接收)一个空的输入缓冲器,然后用数据填满它,并将其发送到编解码器去处理。 编解码器使用这些数据并转换这些数据到它某个空的输出缓冲区。 最后,您请求(或接收)一个已填充数据的输出缓冲区,消耗其内容并将其释放回并回到编解码器。

1、Data Types (数据类型)

编解码器可以处理三类数据:压缩数据、原始音频数据、原始视频数据。所有这三类数据通过ByteBuffers来执行,当然为了提高编解码效果,你最好通过Surface来显示原始视频数据;因为surface使用native层的视频缓存区,没有通过映射或拷贝到JVM空间的缓冲区。正常情况下,当你使用surface的时候,不能访问原始视频数据,但是可以通过ImageRender类来获取原始视频帧。这是一种比ByteBuffers更高效的方法,因为许多本地缓冲区被映射到直接ByteBuffers。当你使用ByteBuffers模式,你可以通过Image类和 getInput/OutputImage(int)方法获取原始视频帧。

  • Compressed Buffers压缩缓冲区
    输入和输出缓冲区包含了对应类型的压缩数据;对于视频类型通常是简单的压缩视频帧;音频数据通常是一个单入单元,(一种编码格式典型的包含了许多ms的音频类型),但当一个缓冲区包含了多种编码音频进入单元,可以不需要。另一方面,缓冲区不能在任意字节边界开始或停止,但当标记了BUFFER_FLAG_PARTIAL_FRAME标记时,可以访问帧或进入单元边界。
  • Raw Audio Buffers原始音频缓冲区
    原始音频缓冲区包含完整的PCM格式的帧数据,一种通道对应一个采样率。每一种采样率是一个16位有符号整型在规定参数里面;下面是获取通道采样率的方法
short[] getSamplesForChannel(MediaCodec codec, int bufferId, int channelIx) {
  ByteBuffer outputBuffer = codec.getOutputBuffer(bufferId);
  MediaFormat format = codec.getOutputFormat(bufferId);
  ShortBuffer samples = outputBuffer.order(ByteOrder.nativeOrder()).asShortBuffer();
  int numChannels = formet.getInteger(MediaFormat.KEY_CHANNEL_COUNT);
  if (channelIx < 0 || channelIx >= numChannels) {
    return null;
  }
  short[] res = new short[samples.remaining() / numChannels];
  for (int i = 0; i < res.length; ++i) {
    res[i] = samples.get(i * numChannels + channelIx);
  }
  return res;
}
  • Raw Video Buffers原始视频缓冲区
    在ByteBuffer模式,视频缓冲区根据颜色格式;可以通过 getCodecInfo().getCapabilitiesForType(…).colorFormats 获取支持的颜色格式,视频编码支持三种类型的颜色格式:
    native raw video format: 标记COLOR_FormatSurface,可以配合输入输出surface使用
    flexible YUV buffers:COLOR_FormatYUV420Flexible,可以配合输入输出surface、在ByteBuffer模式,可以通过getInput/OutputImage(int)访问
    other, specific formats:这些格式只在ByteBuffer 模式支持。一些格式是厂商特有的,其他的定义在MediaCodecInfo.CodecCapabilities;
    自从5.1.1之后,所有的编解码器支持YUV 4:2:0 buffers。
  • Accessing Raw Video ByteBuffers on Older Devices在老的设备上面访问原始视频缓冲区
2、States状态

这里写图片描述
编解码器理论上存在三种状态:停止、执行、释放;停止状态也包含三种子状态:未初始化的、已配置的、错误;执行状态也包含三种子状态:已刷新、正在运行、流结束;
当你用工厂方法创建一个编解码器,是出于未初始化状态的。第一步,通过 configure(…)方法进入已配置状态;其次,通过start()方法进入执行状态;此时,你可以通过上面缓冲队列来处理数据了。
在start()之后,编解码出于已刷新子状态,此时持有所有的缓冲区;当第一个输入缓冲块被出队时,编解码器会耗费许多时间进入运行状态。当一个输入缓冲块被入队时(被标记流结束标记),编解码器进入流结束状态;此时,编解码器不在接收输入缓冲块,但是可以产生输出缓冲块,直到流结束块被出队。你可以在任意时刻,通过调用flush(),进入已刷新状态。
调用stop(),让其进入未初始化状态,如果需要使用,需要再配置一次。当你已经用完编解码器,你需要release();
某些情况下,编解码器会遭遇错误进入错误状态;可以根据不合法返回值或者异常来判断;调用reset()可以复位编码器,让其可以重新使用,并进入未初始化状态。调用releases()进入最终释放状态;

3、Creation创建

可以根据指定的MediaFormat通过MediaCodecList一个编解码器;可以根据 MediaExtractor.getTrackFormat来创建一个可以用于解码文件和流的编解码器;在引入其他格式之前,当你想MediaFormat.setFeatureEnabled,需要通过MediaCodecList.findDecoderForFormat获得与名字对应的特殊的媒体格式的编解码器; 最后通过createByCodecName(String)创建;
另外的,你可以通过MIME类型使用createDecoder/EncoderByType(String)来创建;

  • Creating secure decoders
4、Initialization初始化

创建好之后,可以设置回调 setCallback来异步处理数据;然后configure配置指定的媒体格式。你可以为视频生成指定一个输出surface;也可以设置安全编码,参考MediaCrypto;最后编解码器运行在多个模式下,需要特殊指定在编码或解码状态;
如果你想处理原始输入视频缓冲区,可以在配置后通过createInputSurface()创建一个指定的Surface。也可以通过setInputSurface(Surface)设置编解码器使用指定的Surface。

  • Codec-specific Data特定编解码源数据
    一些格式,AAC audio and MPEG4, H.264 and H.265 video格式要求预置启动参数或者编解码特殊数据。当处理一些压缩格式时,这些数据必须在任意帧数据之前和start()之后提交到编解码器。这些数据在调用queueInputBuffer时需要被标记BUFFER_FLAG_CODEC_CONFIG。
    这些数据也可以通过configure来配置,可以从MediaExtractor获取并放在MediaFromat里面。这些数据会在start()时提交到比爱你解码器里面。
    编码器会在任何可用数据之前创建和返回特定标记了codec-config标记的编码参数,缓冲区包含了没有时间戳的codec-specific-data。
5、Data Processing 执行编解码

每一个编解码器在API调用时维持一系列根据buffer-ID对应的输入输出缓冲区;在成功start()编解码器之后,owns客户端既没有输入也没有输出缓冲区。在同步模式下,通过dequeueInput/OutputBuffer()方法从codec里面获取一个输入/输出缓冲区;在异步模式,你可以注册一个回调,通过MediaCodec.Callback.onInput/OutputBufferAvailable(…)自动接收缓冲区。
在获取一个输入缓冲区之后,填充数据,并通过queueInputBuffer提交到codec,不要提交多个同样时间戳一样的输入数据到codec。
codec处理完后,会返回一个只读输出缓冲区数据;异步模式可以通过 onOutputBufferAvailable读取,同步模式通过dequeuOutputBuffer读取;最后需要调用releaseOutputBuffer返回缓冲区到codec。

  • Asynchronous Processing using Buffers异步模式使用缓冲数组处理
    通常MediaCodec会类似下面的方式在异步模式使用。
MediaCodec codec = MediaCodec.createByCodecName(name);
MediaFormat mOutputFormat; // member variable
codec.setCallback(new MediaCodec.Callback() {
  @Override
  void onInputBufferAvailable(MediaCodec mc, int inputBufferId) {
    ByteBuffer inputBuffer = codec.getInputBuffer(inputBufferId);
    // fill inputBuffer with valid data
    …
    codec.queueInputBuffer(inputBufferId, …);
  }

  @Override
  void onOutputBufferAvailable(MediaCodec mc, int outputBufferId, …) {
    ByteBuffer outputBuffer = codec.getOutputBuffer(outputBufferId);
    MediaFormat bufferFormat = codec.getOutputFormat(outputBufferId); // option A
    // bufferFormat is equivalent to mOutputFormat
    // outputBuffer is ready to be processed or rendered.
    …
    codec.releaseOutputBuffer(outputBufferId, …);
  }

  @Override
  void onOutputFormatChanged(MediaCodec mc, MediaFormat format) {
    // Subsequent data will conform to new format.
    // Can ignore if using getOutputFormat(outputBufferId)
    mOutputFormat = format; // option B
  }

  @Override
  void onError(…) {
    …
  }
});
codec.configure(format, …);
mOutputFormat = codec.getOutputFormat(); // option B
codec.start();
// wait for processing to complete
codec.stop();
codec.release();
  • Synchronous Processing using Buffers同步模式使用缓冲数组处理
    通常MediaCodec会类似下面的方式在同步模式使用。
MediaCodec codec = MediaCodec.createByCodecName(name);
codec.configure(format, …);
MediaFormat outputFormat = codec.getOutputFormat(); // option B
codec.start();
for (;;) {
  int inputBufferId = codec.dequeueInputBuffer(timeoutUs);
  if (inputBufferId >= 0) {
    ByteBuffer inputBuffer = codec.getInputBuffer(…);
    // fill inputBuffer with valid data
    …
    codec.queueInputBuffer(inputBufferId, …);
  }
  int outputBufferId = codec.dequeueOutputBuffer(…);
  if (outputBufferId >= 0) {
    ByteBuffer outputBuffer = codec.getOutputBuffer(outputBufferId);
    MediaFormat bufferFormat = codec.getOutputFormat(outputBufferId); // option A
    // bufferFormat is identical to outputFormat
    // outputBuffer is ready to be processed or rendered.
    …
    codec.releaseOutputBuffer(outputBufferId, …);
  } else if (outputBufferId == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
    // Subsequent data will conform to new format.
    // Can ignore if using getOutputFormat(outputBufferId)
    outputFormat = codec.getOutputFormat(); // option B
  }
}
codec.stop();
codec.release();
  • Synchronous Processing using Buffer Arrays (deprecated)同步模式使用缓冲区数组处理
    官方例子
MediaCodec codec = MediaCodec.createByCodecName(name);
codec.configure(format, …);
codec.start();
ByteBuffer[] inputBuffers = codec.getInputBuffers();
ByteBuffer[] outputBuffers = codec.getOutputBuffers();
for (;;) {
  int inputBufferId = codec.dequeueInputBuffer(…);
  if (inputBufferId >= 0) {
    // fill inputBuffers[inputBufferId] with valid data
    …
    codec.queueInputBuffer(inputBufferId, …);
  }
  int outputBufferId = codec.dequeueOutputBuffer(…);
  if (outputBufferId >= 0) {
    // outputBuffers[outputBufferId] is ready to be processed or rendered.
    …
    codec.releaseOutputBuffer(outputBufferId, …);
  } else if (outputBufferId == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
    outputBuffers = codec.getOutputBuffers();
  } else if (outputBufferId == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
    // Subsequent data will conform to new format.
    MediaFormat format = codec.getOutputFormat();
  }
}
codec.stop();
codec.release();
  • End-of-stream Handling如何实现结束流
    当你没有输入数据的时候,需要 queueInputBuffer一个BUFFER_FLAG_END_OF_STREAM标记的数组到codec。可以把这个标签放在设置在最后一个可用输入buffer里面,或者用一个额外的空输入数据里面设置这个标志位,如果是第二种方式,可以忽略时间戳。
    编解码器会继续返回输出数据,直到dequeueOutputBuffer或onOutputBufferAvailable返回的MediaCodec.BufferInfo集合出现同样的end-of-stream标志。
    除非codec已经处于已刷新、已停止或者重启状态,否则不要在end-of-stream之后另外提交输入数据。
  • Using an Output Surface 使用一个输出Surface
    当你使用输出到surface时,需要设置codec为ByteBuffer模式;另外,当使用一个输出surface时,你有以下三种选择:
    Do not render the buffer: Call releaseOutputBuffer(bufferId, false).
    Render the buffer with the default timestamp: Call releaseOutputBuffer(bufferId, true).
    Render the buffer with a specific timestamp: Call releaseOutputBuffer(bufferId, timestamp).
  • Transformations When Rendering onto Surface在渲染时改变参数
    当codec处于surface模式,任何改变矩形、角度、video大小模式会自动导致一个异常;
  • Using an Input Surface
    当你是用一个输入surface时,不允许访问输入buffers,因为buffers会自动从surface传送到codec。
6、Seeking & Adaptive Playback Support快进快退,自适应播放支持

视频解码的自适应播放支持只用工作在codec解码到一个surface。

  • Stream Boundary and Key Frames流边界和关键帧
    输入流的必须确保第一帧是一个关键帧,才能送到input data里面。
  • For decoders that do not support adaptive playback (including when not decoding onto a Surface)
  • For decoders that support and are configured for adaptive playback配置为支持自适应播放的解码器
    为了对改变进度的数据进行解码,不需要刷新解码器。然而输入数据在非连续之后必须从一个可用的流起始/关键帧开始。
    许多H.264, H.265, VP8 and VP9视频格式,很大可能中间改变图片大小和配置。所以需要打包新的codec-specific配置数据和关键帧到一个包含起始码的buffer里面,并提交到codec里面。
    你会在图片大小发生改变之后,任何有效帧之前,并在dequeueOutputBuffer里面收到INFO_OUTPUT_FORMAT_CHANGED的数据。
7、Error handling 错误处理

二、MediaCodec与音视频基础知识点补充与编程例子

1、Activity测试页面,从mp4文件中读取音视频并播放出来,播放mp4文件
package com.cclin.jubaohe.activity.Media;
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioTrack;
import android.media.MediaCodec;
import android.media.MediaExtractor;
import android.media.MediaFormat;
import android.os.Bundle;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import android.widget.Button;
import com.cclin.jubaohe.R;
import com.cclin.jubaohe.base.BaseActivity;
import com.cclin.jubaohe.util.CameraUtil;
import com.cclin.jubaohe.util.LogUtil;
import com.cclin.jubaohe.util.SDPathConfig;
import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
/**
* Created by LinChengChun on 2018/4/14.
*/
public class MediaTestActivity extends BaseActivity implements SurfaceHolder.Callback, View.OnClickListener {
    private final static String MEDIA_FILE_PATH = SDPathConfig.LIVE_MOVIE_PATH+"/18-04-12-10:47:06-0.mp4";
    private Surface mSurface;
    private SurfaceView mSvRenderFromCamera;
    private SurfaceView mSvRenderFromFile;
    Button mBtnCameraPreview;
    Button mBtnPlayMediaFile;
    private MediaStream mMediaStream;
    private Thread mVideoDecoderThread;
    private AudioTrack mAudioTrack;
    private Thread mAudioDecoderThread;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mBtnCameraPreview = retrieveView(R.id.btn_camera_preview);
        mBtnPlayMediaFile = retrieveView(R.id.btn_play_media_file);
        mBtnCameraPreview.setOnClickListener(this);
        mBtnPlayMediaFile.setOnClickListener(this);
        mSvRenderFromCamera = retrieveView(R.id.sv_render);
        mSvRenderFromCamera.setOnClickListener(this);
        mSvRenderFromCamera.getHolder().addCallback(this);
        mSvRenderFromFile = retrieveView(R.id.sv_display);
        mSvRenderFromFile.setOnClickListener(this);
        mSvRenderFromFile.getHolder().addCallback(this);
        init();
    }
    @Override
    protected int initLayout() {
        return R.layout.activity_media_test;
    }
    private void init(){
        File file = new File(MEDIA_FILE_PATH);
        if (!file.exists()){
            LogUtil.e("文件不存在!!");
            return;
        }
        LogUtil.e("目标文件存在!!");
    }
    private void startVideoDecoder(){
        // fill inputBuffer with valid data
        mVideoDecoderThread = new Thread("mVideoDecoderThread"){
            @Override
            public void run() {
                super.run();
                MediaFormat mMfVideo = null, mMfAudio = null;
                String value = null;
                String strVideoMime = null;
                String strAudioMime = null;
                try {
                    MediaExtractor mediaExtractor = new MediaExtractor(); // 提取器用来从文件中读取音视频
                    mediaExtractor.setDataSource(MEDIA_FILE_PATH);
                    int numTracks = mediaExtractor.getTrackCount(); // 轨道数,一般为2
                    LogUtil.e("获取track数"+numTracks);
                    for (int i=0; i< numTracks; i++) { // 检索每个轨道的格式
                        MediaFormat mediaFormat = mediaExtractor.getTrackFormat(i);
                        LogUtil.e("单独显示track MF:"+mediaFormat);
                        value = mediaFormat.getString(MediaFormat.KEY_MIME);
                        if (value.contains("audio")){
                            mMfAudio = mediaFormat;
                            strAudioMime = value;
                        }else {
                            mMfVideo = mediaFormat;
                            strVideoMime = value;
                            mediaExtractor.selectTrack(i);
                        }
                    }
                    mSurface = mSvRenderFromFile.getHolder().getSurface();
                    MediaCodec codec = MediaCodec.createDecoderByType(strVideoMime); // 创建编解码器
                    codec.configure(mMfVideo, mSurface, null, 0); // 配置解码后的视频帧数据直接渲染到Surface
                    codec.setVideoScalingMode(MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT);
                    codec.start(); // 启动编解码器,让codec进入running模式
                    MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); //缓冲区信息
                    int size = -1, outputBufferIndex = -1;
                    LogUtil.e("开始解码。。。");
                    long previewStampUs = 0l;
                    do {
                        int inputBufferId = codec.dequeueInputBuffer(10);// 从编码器中获取 输入缓冲区
                        if (inputBufferId >= 0) {
                            ByteBuffer inputBuffer = codec.getInputBuffer(inputBufferId); // 获取该输入缓冲区
                            // fill inputBuffer with valid data
                            inputBuffer.clear(); // 清空缓冲区
                            size = mediaExtractor.readSampleData(inputBuffer, 0); // 从提取器中获取一帧数据填充到输入缓冲区
                            LogUtil.e("readSampleData: size = "+size);
                            if (size < 0)
                                break;
                            int trackIndex = mediaExtractor.getSampleTrackIndex();
                            long presentationTimeUs = mediaExtractor.getSampleTime(); // 获取采样时间
                            LogUtil.e("queueInputBuffer: 把数据放入编码器。。。");
                            codec.queueInputBuffer(inputBufferId, 0, size, presentationTimeUs, 0); // 将输入缓冲区压入编码器
                            mediaExtractor.advance(); // 获取下一帧
                            LogUtil.e("advance: 获取下一帧。。。");
                            outputBufferIndex = codec.dequeueOutputBuffer(bufferInfo, 10000); // 从编码器中读取解码完的数据
                            LogUtil.e("outputBufferIndex = "+outputBufferIndex);
                            switch (outputBufferIndex) {
                                case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
//                                    MediaFormat mf = codec.getOutputFormat(outputBufferIndex); // 导致播放视频失败
                                    MediaFormat mf = codec.getOutputFormat();
                                    LogUtil.e("INFO_OUTPUT_FORMAT_CHANGED:"+mf);

                                    break;
                                case MediaCodec.INFO_TRY_AGAIN_LATER:
                                    LogUtil.e("解码当前帧超时");
                                    break;
                                case MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED:
                                    //outputBuffers = videoCodec.getOutputBuffers();
                                    LogUtil.e("output buffers changed");
                                    break;
                                default:
                                    //直接渲染到Surface时使用不到outputBuffer
                                    //ByteBuffer outputBuffer = outputBuffers[outputBufferIndex];
                                    //延时操作
                                    //如果缓冲区里的可展示时间>当前视频播放的进度,就休眠一下
                                    boolean firstTime = previewStampUs == 0l;
                                    long newSleepUs = -1;
                                    long sleepUs = (bufferInfo.presentationTimeUs - previewStampUs);
                                    if (!firstTime) {
                                        long cache = 0;
                                        newSleepUs = CameraUtil.fixSleepTime(sleepUs, cache, -100000);
                                    }
                                    previewStampUs = bufferInfo.presentationTimeUs;
                                    //渲染
                                    if (newSleepUs < 0)
                                        newSleepUs = 0;
                                    Thread.sleep(newSleepUs / 1000);
                                    codec.releaseOutputBuffer(outputBufferIndex, true); // 释放输入缓冲区,并渲染到Surface
                                    break;
                            }
                        }
                    }while (!this.isInterrupted());
                    LogUtil.e("解码结束。。。");
                    codec.stop();
                    codec.release();
                    codec = null;
                    mediaExtractor.release();
                    mediaExtractor = null;
                } catch (IOException e) {
                    e.printStackTrace();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            }
        };
        mVideoDecoderThread.start();
    }

    private void startAudioDecoder(){
        mAudioDecoderThread = new Thread("AudioDecoderThread"){
            @Override
            public void run() {
                super.run();
                try {

                    MediaFormat mMfVideo = null, mMfAudio = null;
                    String value = null;
                    String strVideoMime = null;
                    String strAudioMime = null;

                    MediaExtractor mediaExtractor = new MediaExtractor();
                    mediaExtractor.setDataSource(MEDIA_FILE_PATH);
                    int numTracks = mediaExtractor.getTrackCount();
                    LogUtil.e("获取track数"+numTracks);
                    for (int i=0; i< numTracks; i++) {
                        MediaFormat mediaFormat = mediaExtractor.getTrackFormat(i);
                        LogUtil.e("单独显示track MF:"+mediaFormat);
                        value = mediaFormat.getString(MediaFormat.KEY_MIME);
                        if (value.contains("audio")){
                            mMfAudio = mediaFormat;
                            strAudioMime = value;
                            mediaExtractor.selectTrack(i);
                        }else {
                            mMfVideo = mediaFormat;
                            strVideoMime = value;
                        }
                    }
//                    mMfAudio.setInteger(MediaFormat.KEY_IS_ADTS, 1);
                    mMfAudio.setInteger(MediaFormat.KEY_BIT_RATE, 16000);
                    MediaCodec codec = MediaCodec.createDecoderByType(strAudioMime);
                    codec.configure(mMfAudio, null, null, 0);
                    codec.start();
                    ByteBuffer outputByteBuffer = null;
                    ByteBuffer[] outputByteBuffers = null;
                    MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
                    int size = -1, outputBufferIndex = -1;
                    long previewStampUs = 01;
                    LogUtil.e("开始解码。。。");
                    if (mAudioTrack == null){
                        int sample_rate = mMfAudio.getInteger(MediaFormat.KEY_SAMPLE_RATE);
                        int channels = mMfAudio.getInteger(MediaFormat.KEY_CHANNEL_COUNT);
                        int sampleRateInHz = (int) (sample_rate * 1.004);
                        int channelConfig = channels == 1 ? AudioFormat.CHANNEL_OUT_MONO : AudioFormat.CHANNEL_OUT_STEREO;
                        int audioFormat = AudioFormat.ENCODING_PCM_16BIT;
                        int bfSize = AudioTrack.getMinBufferSize(sampleRateInHz, channelConfig, audioFormat) * 4;
                        mAudioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, sampleRateInHz, channelConfig, audioFormat, bfSize, AudioTrack.MODE_STREAM);
                    }
                    mAudioTrack.play();

//                    outputByteBuffers = codec.getOutputBuffers();
                    do {

                        int inputBufferId = codec.dequeueInputBuffer(10);
                        if (inputBufferId >= 0) {
                            ByteBuffer inputBuffer = codec.getInputBuffer(inputBufferId);
                            // fill inputBuffer with valid data
                            inputBuffer.clear();
                            size = mediaExtractor.readSampleData(inputBuffer, 0);
                            if (size<0)
                                break;
                            long presentationTimeUs = mediaExtractor.getSampleTime();
//                            LogUtil.e("queueInputBuffer: 把数据放入编码器。。。");
                            codec.queueInputBuffer(inputBufferId, 0, size, presentationTimeUs, 0);
                            mediaExtractor.advance();
//                            LogUtil.e("advance: 获取下一帧。。。");
                            outputBufferIndex = codec.dequeueOutputBuffer(bufferInfo, 50000);
                            switch (outputBufferIndex) {
                                case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
//                                    MediaFormat mf = codec.getOutputFormat(outputBufferIndex);
                                    MediaFormat mf = codec.getOutputFormat();
                                    LogUtil.e("INFO_OUTPUT_FORMAT_CHANGED:"+mf);
                                    break;
                                case MediaCodec.INFO_TRY_AGAIN_LATER:
                                    LogUtil.e( "解码当前帧超时");
                                    break;
                                case MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED:
//                                    outputByteBuffer = codec.getOutputBuffers();
                                    LogUtil.e( "output buffers changed");
                                    break;
                                default:
                                    //直接渲染到Surface时使用不到outputBuffer
                                    //ByteBuffer outputBuffer = outputBuffers[outputBufferIndex];
                                    //延时操作
                                    //如果缓冲区里的可展示时间>当前视频播放的进度,就休眠一下
                                    LogUtil.e("outputBufferIndex = "+outputBufferIndex);
//                                    outputByteBuffer = outputByteBuffers[outputBufferIndex];
                                    outputByteBuffer = codec.getOutputBuffer(outputBufferIndex); // 获取解码后的数据
                                    outputByteBuffer.clear();
                                    byte[] outData = new byte[bufferInfo.size];
                                    outputByteBuffer.get(outData);
                                    boolean firstTime = previewStampUs == 0l;
                                    long newSleepUs = -1;
                                    long sleepUs = (bufferInfo.presentationTimeUs - previewStampUs);
                                    if (!firstTime){
                                        long cache = 0;
                                        newSleepUs = CameraUtil.fixSleepTime(sleepUs, cache, -100000);
                                    }
                                    previewStampUs = bufferInfo.presentationTimeUs;
                                    //渲染
                                    if (newSleepUs < 0)
                                        newSleepUs = 0;
                                    Thread.sleep(newSleepUs/1000);
                                    mAudioTrack.write(outData, 0, outData.length); // 输出音频
                                    codec.releaseOutputBuffer(outputBufferIndex, false); // 释放输出缓冲区
                                    break;
                            }
                        }
                    }while (!this.isInterrupted());
                    LogUtil.e("解码结束。。。");
                    codec.stop();
                    codec.release();
                    codec = null;
                    mAudioTrack.stop();
                    mAudioTrack.release();
                    mAudioTrack = null;
                    mediaExtractor.release();
                    mediaExtractor = null;
                } catch (IOException e) {
                    e.printStackTrace();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        mAudioDecoderThread.start();
    }

    @Override
    public void onClick(View view){
        switch (view.getId()){
            case R.id.sv_render:
                mMediaStream.getCamera().autoFocus(null);
                break;
            case R.id.sv_display:
                break;

            case R.id.btn_camera_preview:
                break;

            case R.id.btn_play_media_file:
                break;

            default:break;
        }
    }

    private int getDgree() {
        int rotation = getWindowManager().getDefaultDisplay().getRotation();
        int degrees = 0;
        switch (rotation) {
            case Surface.ROTATION_0:
                degrees = 0;
                break; // Natural orientation
            case Surface.ROTATION_90:
                degrees = 90;
                break; // Landscape left
            case Surface.ROTATION_180:
                degrees = 180;
                break;// Upside down
            case Surface.ROTATION_270:
                degrees = 270;
                break;// Landscape right
        }
        return degrees;
    }

    private void onMediaStreamCreate(){
        if (mMediaStream==null)
            mMediaStream = new MediaStream(this, mSvRenderFromCamera.getHolder());
        mMediaStream.setDgree(getDgree());
        mMediaStream.createCamera();
        mMediaStream.startPreview();
    }

    private void onMediaStreamDestroy(){
        mMediaStream.release();
        mMediaStream = null;
    }

    @Override
    protected void onPause() {
        super.onPause();
        onMediaStreamDestroy();
        if (mVideoDecoderThread!=null)
            mVideoDecoderThread.interrupt();
        if (mAudioDecoderThread!=null)
            mAudioDecoderThread.interrupt();
    }

    @Override
    protected void onResume() {
        super.onResume();
        if (isSurfaceCreated && mMediaStream == null){
            onMediaStreamCreate();
        }
    }

    private boolean isSurfaceCreated = false;
    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        LogUtil.e("surfaceCreated: "+holder);

        if (holder.getSurface() == mSvRenderFromCamera.getHolder().getSurface()){
            isSurfaceCreated = true;
            onMediaStreamCreate();
        }else if (holder.getSurface() == mSvRenderFromFile.getHolder().getSurface()){
            if (new File(MEDIA_FILE_PATH).exists()) {
                startVideoDecoder();
                startAudioDecoder();
            }
        }
    }

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
        LogUtil.e("surfaceChanged: "
                +"\nholder = "+holder
                +"\nformat = "+format
                +"\nwidth = "+width
                +"\nheight = "+height);
    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        LogUtil.e("surfaceDestroyed: ");
        if (holder.getSurface() == mSvRenderFromCamera.getHolder().getSurface()) {
            isSurfaceCreated = false;
        }
    }
}
2、将摄像头的数据编码成h264数据
    final int millisPerframe = 1000 / 20;
    long lastPush = 0;
    @Override
    public void run() {
        MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
        int outputBufferIndex = 0;
        byte[] mPpsSps = new byte[0];
        byte[] h264 = new byte[mWidth * mHeight];
        do {
            outputBufferIndex = mMediaCodec.dequeueOutputBuffer(bufferInfo, 10000); // 从codec中获取编码完的数据
            if (outputBufferIndex == MediaCodec.INFO_TRY_AGAIN_LATER) {
                // no output available yet
            } else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
                // not expected for an encoder
                outputBuffers = mMediaCodec.getOutputBuffers();
            } else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
                synchronized (HWConsumer.this) {
                    newFormat = mMediaCodec.getOutputFormat();
                    EasyMuxer muxer = mMuxer;
                    if (muxer != null) {
                        // should happen before receiving buffers, and should only happen once

                        muxer.addTrack(newFormat, true);
                    }
                }
            } else if (outputBufferIndex < 0) {
                // let's ignore it
            } else {
                ByteBuffer outputBuffer;
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                    outputBuffer = mMediaCodec.getOutputBuffer(outputBufferIndex);
                } else {
                    outputBuffer = outputBuffers[outputBufferIndex];
                }
                outputBuffer.position(bufferInfo.offset);
                outputBuffer.limit(bufferInfo.offset + bufferInfo.size);
                EasyMuxer muxer = mMuxer;
                if (muxer != null) {
                    muxer.pumpStream(outputBuffer, bufferInfo, true);
                }

                boolean sync = false;
                if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {// codec会产生sps和pps
                    sync = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_SYNC_FRAME) != 0; // 标记是I帧还是同步帧
                    if (!sync) { // 如果是同步帧,也就是填充着pps和sps参数
                        byte[] temp = new byte[bufferInfo.size];
                        outputBuffer.get(temp);
                        mPpsSps = temp;
                        mMediaCodec.releaseOutputBuffer(outputBufferIndex, false);
                        continue; // 等待下一帧
                    } else {
                        mPpsSps = new byte[0];
                    }
                }
                sync |= (bufferInfo.flags & MediaCodec.BUFFER_FLAG_SYNC_FRAME) != 0; // 标记是否是关键帧
                int len = mPpsSps.length + bufferInfo.size;
                if (len > h264.length) {
                    h264 = new byte[len];
                }
                if (sync) { // 如果是关键帧
                    
                    if (BuildConfig.DEBUG)
                        Log.i(TAG, String.format("push i video stamp:%d", bufferInfo.presentationTimeUs / 1000));
                } else { // 非I帧直接读取出来
                    outputBuffer.get(h264, 0, bufferInfo.size);
                    if (BuildConfig.DEBUG)
                        Log.i(TAG, String.format("push video stamp:%d", bufferInfo.presentationTimeUs / 1000));
                }
                mMediaCodec.releaseOutputBuffer(outputBufferIndex, false);
            }
        }
        while (mVideoStarted);
    }
    @Override
    public int onVideo(byte[] data, int format) {
        if (!mVideoStarted) return 0;
        try {
            if (lastPush == 0) {
                lastPush = System.currentTimeMillis();
            }
            long time = System.currentTimeMillis() - lastPush;
            if (time >= 0) {
                time = millisPerframe - time;
                if (time > 0) Thread.sleep(time / 2);
            }
            if (format == ImageFormat.YV12) {
                JNIUtil.yV12ToYUV420P(data, mWidth, mHeight);
            } else {
                JNIUtil.nV21To420SP(data, mWidth, mHeight);
            }
            int bufferIndex = mMediaCodec.dequeueInputBuffer(0);
            if (bufferIndex >= 0) {
                ByteBuffer buffer = null;
                if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
                    buffer = mMediaCodec.getInputBuffer(bufferIndex);
                } else {
                    buffer = inputBuffers[bufferIndex];
                }
                buffer.clear();
                buffer.put(data);
                buffer.clear();
                mMediaCodec.queueInputBuffer(bufferIndex, 0, data.length, System.nanoTime() / 1000, MediaCodec.BUFFER_FLAG_KEY_FRAME); // 标记含有关键帧
            }
            if (time > 0) Thread.sleep(time / 2); // 添加延时,确保帧率
            lastPush = System.currentTimeMillis();
        } catch (InterruptedException ex) {
            ex.printStackTrace();
        }
        return 0;
    }

    /**
    * 初始化编码器
    */
    private void startMediaCodec() throws IOException {
            /*
        SD (Low quality) SD (High quality) HD 720p
1 HD 1080p
1
Video resolution 320 x 240 px 720 x 480 px 1280 x 720 px 1920 x 1080 px
Video frame rate 20 fps 30 fps 30 fps 30 fps
Video bitrate 384 Kbps 2 Mbps 4 Mbps 10 Mbps
        */
        int framerate = 20;
//        if (width == 640 || height == 640) {
//            bitrate = 2000000;
//        } else if (width == 1280 || height == 1280) {
//            bitrate = 4000000;
//        } else {
//            bitrate = 2 * width * height;
//        }

        int bitrate = (int) (mWidth * mHeight * 20 * 2 * 0.05f);
        if (mWidth >= 1920 || mHeight >= 1920) bitrate *= 0.3;
        else if (mWidth >= 1280 || mHeight >= 1280) bitrate *= 0.4;
        else if (mWidth >= 720 || mHeight >= 720) bitrate *= 0.6;
        EncoderDebugger debugger = EncoderDebugger.debug(mContext, mWidth, mHeight);
        mVideoConverter = debugger.getNV21Convertor();
        mMediaCodec = MediaCodec.createByCodecName(debugger.getEncoderName());
        MediaFormat mediaFormat = MediaFormat.createVideoFormat("video/avc", mWidth, mHeight);
        mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, bitrate);
        mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, framerate);
        mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, debugger.getEncoderColorFormat());
        mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
        mMediaCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
        mMediaCodec.start();

        Bundle params = new Bundle();
        params.putInt(MediaCodec.PARAMETER_KEY_REQUEST_SYNC_FRAME, 0);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            mMediaCodec.setParameters(params);
        }
    }

三、总结

MediaCodec编解码器h264算法生成的pps和sps是在同一个配置帧里面,如果需要分别使用的话需要分开解析。

inputBuffers = mMediaCodec.getInputBuffers();
outputBuffers = mMediaCodec.getOutputBuffers();
int bufferIndex = mMediaCodec.dequeueInputBuffer(0);
if (bufferIndex >= 0) {
    inputBuffers[bufferIndex].clear();
    mConvertor.convert(data, inputBuffers[bufferIndex]);
    mMediaCodec.queueInputBuffer(bufferIndex, 0, inputBuffers[bufferIndex].position(), System.nanoTime() / 1000, 0);

    MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
    int outputBufferIndex = mMediaCodec.dequeueOutputBuffer(bufferInfo, 0);

    while (outputBufferIndex >= 0) {
        ByteBuffer outputBuffer = outputBuffers[outputBufferIndex];

//                        String data0 = String.format("%x %x %x %x %x %x %x %x %x %x ", outData[0], outData[1], outData[2], outData[3], outData[4], outData[5], outData[6], outData[7], outData[8], outData[9]);
//                        Log.e("out_data", data0);

        //记录pps和sps
        int type = outputBuffer.get(4) & 0x07; // 判断是什么帧

//                                LogUtil.e(TAG, String.format("type is %d", type));
        if (type == 7 || type == 8) {
            byte[] outData = new byte[bufferInfo.size];
            outputBuffer.get(outData);
            mPpsSps = outData;

            ArrayList<Integer> posLists = new ArrayList<>(2);
            for (int i=0; i<bufferInfo.size-3; i++){    // 找寻 pps sps
                if (outData[i]==0 && outData[i+1]==0&& outData[i+2]==0 && outData[i+3]==1){
                    posLists.add(i);
                }
            }
            int sps_pos = posLists.get(0);
            int pps_pos = posLists.get(1);
            posLists.clear();
            posLists = null;
            ByteBuffer csd0 = ByteBuffer.allocate(pps_pos);
            csd0.put(outData, sps_pos, pps_pos);
            csd0.clear();
            mCSD0 = csd0;
            LogUtil.e(TAG, String.format("CSD-0 searched!!!"));

            ByteBuffer csd1 = ByteBuffer.allocate(outData.length-pps_pos);
            csd1.put(outData, pps_pos, outData.length-pps_pos);
            csd1.clear();
            mCSD1 = csd1;
            LogUtil.e(TAG, String.format("CSD-1 searched!!!"));

            LocalBroadcastManager.getInstance(mApplicationContext).sendBroadcast(new Intent(ACTION_H264_SPS_PPS_GOT));
        } else if (type == 5) {
            //在关键帧
            if (mEasyMuxer !=null && !isRecordPause) {
                bufferInfo.presentationTimeUs = TimeStamp.getInstance().getCurrentTimeUS();
                mEasyMuxer.pumpStream(outputBuffer, bufferInfo, true);// 用于保存本地视频到本地
                isWaitKeyFrame = false; // 拿到关键帧,则清除等待关键帧的条件
//                                        LocalBroadcastManager.getInstance(mApplicationContext).sendBroadcast(new Intent(ACTION_I_KEY_FRAME_GOT));
            }
        } else {
            outputBuffer.get(h264, 0, bufferInfo.size);
            if (System.currentTimeMillis() - timeStamp >= 3000) {
                timeStamp = System.currentTimeMillis();
                if (Build.VERSION.SDK_INT >= 23) {
                    Bundle params = new Bundle();
                    params.putInt(MediaCodec.PARAMETER_KEY_REQUEST_SYNC_FRAME, 0);
                    mMediaCodec.setParameters(params);
                }
            }

            if (mEasyMuxer !=null && !isRecordPause && !isWaitKeyFrame) {
                bufferInfo.presentationTimeUs = TimeStamp.getInstance().getCurrentTimeUS();
                mEasyMuxer.pumpStream(outputBuffer, bufferInfo, true);// 用于保存本地视频到本地
            }
        }

        mMediaCodec.releaseOutputBuffer(outputBufferIndex, false);
        outputBufferIndex = mMediaCodec.dequeueOutputBuffer(bufferInfo, 0);
    }

} else {
    Log.e(TAG, "No buffer available !");
}
android硬编解码MediaCodec 一 mediacodec简介 MediaCodec 类可以用来访问底层媒体编解码器,即编码器/解码器的组件。 它是 Android 底层多媒体支持架构的一部分(通常与 MediaExtractor,MediaSync,MediaMuxer,MediaCrypto,MediaDrm,Image,Surface 和 AudioTrack 一起使用)。 编解码器可以处理三类数据:压缩数据、原始音频数据、原始视频数据。 a Compressed Buffers 压缩缓冲区 输... 阅读详情

相关推荐

android P MediaCodec编解码流程分析

编解码流程分析

zjy764219923的专栏 2451

Android 判断Codec是否支持AdaptivePlayback模式

AdaptivePlayback模式用于判断codec在seekTo后是否需要flush() 截止目前目前支持到 minSdkVersion 19 版本 MediaCodecInfo codecInfo = videoCodec.getCodecInfo(); //从videoCodec中获取codecInfo for(String type: codecInfo.getSupporte...

你那么爱笑,家里人知道吗? 1398

Android硬编解码工具MediaCodec解析——从猪肉餐馆的故事讲起(二)

关于queueInputBuffer方法,定义的注释实在太长了,简单来说,这里就是将input端第inputBufIndex个Buffer从第0位开始chunkSize个字节数据传入MediaCodec中,并指定这一帧数据的渲染时间为presentationTimeUs,在。中讲过,根据官网描述,一般如果是视频文件数据,则都不要传递给Mediacodec不是完整帧的数据,除非是标记了BUFFER_FLAG_PARTIAL_FRAME的数据。并将解码得到的buffer的相关信息放在mBufferInfo中。

2201_76108770的博客 417

Android音视频-MediaCodec

原文:https://mp.weixin.qq.com/s?__biz=MzU3NTA3MDU1OQ==&mid=2247484865&idx=1&sn=174b8ca702466e83e72c7115d91b06ea&chksm=fd298df1ca5e04e7b2df9dc9f21e5cfe3e910204c905d8605f648ce6f6404432a83ae52a23a3&scene=178&cur_album_id=1638784435628064

hn_lgc的专栏 3814

MediaCodec视频解码流程详解及参考demo

本文主要讲解mediacodec的视频解码流程,及demo参考例子。

分享音视频技术 7874

Android硬编解码工具MediaCodec解析——从猪肉餐馆的故事讲起(三)

后面解码出来的数据也是这个大小,因为解码之后的数据就是一帧画面的yuv数据,因为画面的分辨率固定,yuv格式也是固定,所以大小自然也是一样的。并将解码出来的每帧截图保存到本地,验证了视频解码的output端每次获取的数据确实是表示一帧的数据。这个空Buffer大小为6291456字节(pos表示当前操作指针指向的位置,lim表示当前可读或者可写的最大数量,cap表示其容量),inputBufIndex为2,即该Buffer在MediaCodec的输入Buffer数组的位置是2。

2201_76108770的博客 655

MediaCodec 硬编解码的哪些坑-基于 rk3399

做 Android 音视频离不开 MediaCodec。实际上不局限于平台看问题,不管是 Linux、Mac OS、iOS、Android 还是 Window 这些系统,只要涉及到音视频编解码,无非要么软编解码、要么硬编解码,软编解码通常会使用瑞士军刀“FFmpeg”,主要靠 cpu 的算力去实现。硬编解码就要调用系统开放的 API 去使用硬件编解码。从效率上看,调用硬件编解码是最快的,通常会使用专用的硬件去处理编解码。尤其在嵌入式设备上通常 cpu 性能有限,想要达到满足要求的编解码需求,不用硬件编解码也

洪伟的专栏 1万+

Android MediaCodec硬编码H264文件(四)

在 Android 4.1 版本提供了 MediaCodec 接口来访问设备的编解码器,不同于 FFmpeg 的软件编解码,它采用的是硬件编解码能力,因此在速度上会比软解更具有优势,但是由于 Android 的碎片化问题,机型众多,版本各异,导致 MediaCodec 在机型兼容性上需要花精力去适配,并且编解码流程不可控,全交由厂商的底层硬件去实现,最终得到的视频质量不一定很理想。 虽然 MediaCodec 仍然存在一定的弊端,但是对于快速实现编解码需求,还是很值得参考的。 以将相机预览的 YUV 数

Android系统攻城狮 1709

Android硬编解码接口MediaCodec解析(一)

#0、本文概述 MediaCodec是anroid api 16以后开发的硬编解码接口,英文文档参照这个链接,中文翻译可以参考这个链接。本文主要记录的是如何使用MediaCodec对视频进行编解码,最后会以实例的方式展示如何将Camera预览数据编码成H264,再把编码后的h264解码并且显示在SurfaceView中。本例不涉及音频的编解码。 #1、MediaCodec编码视频 使用MediaC...

安卓兼职framework和app工程师的博客 714

Android12之MediaCodec硬编解码调试手段(四十九)

本篇目的:Android12之打开MediaCodec硬编解码日志。编解码器类型:MediaCodec支持一系列的音频和视频编解码器,包括H.264、H.265、AAC、MP3等流行的编码和解码格式。开发者可以通过MediaCodec来进行音视频数据的编码和解码操作。硬件加速:MediaCodec可以利用Android设备的硬件编解码器来加速音视频处理,提高性能和节约电力。通过MediaCodec,开发者可以利用设备本身的硬件资源来进行多媒体编解码操作,从而实现更加高效的处理。

Android系统攻城狮 811

海康威视Web3.0插件实战:快速部署摄像头实时预览方案

本文详细介绍了海康威视Web3.0插件的实战应用,包括快速部署摄像头实时预览方案的完整流程。从插件下载安装到摄像头连接配置,再到浏览器兼容性解决方案,帮助开发者和IT人员高效实现网页端监控功能。特别针对预览功能调优和多摄像头管理等高级应用提供了实用技巧。

weixin_29266679的博客 346

android ndk之opencv+MediaCodec硬编解码来处理视频动态时间水印

android ndk之opencv+MediaCodec硬编解码来处理视频水印学习笔记 android视频处理学习笔记。以前android增加时间水印的需求,希望多了解视频编解码,直播,特效这一块,顺便熟悉NDK。 openvc能干什么?为什么要集成openvc? openvc是一套计算机视觉处理库,直白地讲,就是处理图片和识别图片的。有自己的算法后,可以做一些高级的东西,比如机器视觉...

虎躯一震的博客 4511

STM32_DAC

DAC(Digital-to-Analog Converter,数模转换器)是将数字信号转换为模拟信号的外设,广泛应用于音频输出、波形生成、电机控制等场景。STM32F407 系列芯片配备了 2 个 DAC 通道(DAC1 和 DAC2),支持 12 位分辨率,支持多种触发方式,可输出稳定的模拟电压。本文从 DAC 的基本原理出发,详细讲解 STM32F407 DAC 的配置方法、代码实现、波形生成以及实际应用案例,帮助你快速掌握 DAC 的使用技巧。

轩宇的博客 1240

android 音视频硬编解码

这里写自定义目录标题欢迎使用Markdown编辑器新的改变功能快捷键合理的创建标题,有助于目录的生成如何改变文本的样式插入链接与图片如何插入一段漂亮的代码片生成一个适合你的列表创建一个表格设定内容居中、居左、居右SmartyPants创建一个自定义列表如何创建一个注脚注释也是必不可少的KaTeX数学公式新的甘特图功能,丰富你的文章UML 图表FLowchart流程图导出与导入导出导入 欢迎使用Markdown编辑器 你好! 这是你第一次使用 Markdown编辑器 所展示的欢迎页。如果你想学习如何使用Mar

KZax1l的博客 6789

QGIS:安装Deepness插件+应用

简单的描述了deepness的安装应用历程,重点是记录博主本次遇到的坑

fengyi157的博客 2336

六种光耦综合对比——《器件手册--光耦》

可控硅输出光耦(SCR Optocoupler)是一种特殊的光电耦合器,由红外发光二极管(LED)和双向可控硅(SCR)组成,用于实现输入和输出之间的电气隔离和信号传输。逻辑输出光耦是一种高效、可靠的数字信号隔离和电平转换器件,通过电气隔离和逻辑电平输出,确保了系统的安全性和信号的稳定性。固态继电器(可控硅输出,SSR)是一种无触点电子开关设备,利用可控硅(SCR或Triac)作为主要开关元件,通过光耦合器实现输入和输出之间的电气隔离。光耦作为一种成熟的电气隔离器件,在电子电路设计中具有重要的地位。

XU157303764的博客 1726

Android硬编解码工具MediaCodec解析——从猪肉餐馆的故事讲起

本次讲解的代码实例是**Google官方MediaCodec的学习项目,grafika由多个demo组成,比如视频解码播放、实时录制视频并将视频编码为H264保存本地,录屏等功能,每

bugyinyin的博客 1301

uniapp(微信小程序) - 实现uniapp小程序电子签名板,横屏手写姓名签名专用写字画板,客户专用签字板,手写签名签字画板组件,手写签名,(详细运行示例,一键复制开箱即用)

uniapp实现微信小程序的电子签名效果,uniapp微信小程序 电子签名 写字板,用uniapp实现微信小程序的电子签名,uniapp微信小程序canvans实现手写签名,记录一个uniapp写的小程序的手写板,横屏,用于签名,unniapp实现电子签名,uniapp怎么做横屏的手写签名,uniapp如何实现签字板,uniapp小程序平台下怎么做电子签名板组件,uniapp电子签名插件,uniapp最好用的电子写字板插件组件源码,实现uniapp 手写签名功能,手写板、签字板,uni-app插件市场最好的

✅ 精选推荐文章 1万+

Android硬编解码接口MediaCodec使用完全解析(一)

0、本文概述 MediaCodec是android api 16以后开放的硬编解码接口,英文文档参照这个链接,中文翻译可以参考这个链接。本文主要记录的是如何使用MediaCodec对视频进行编解码,最后会以实例的方式展示如何将Camera预览数据编码成H264,再把编码后的h264解码并且显示在SurfaceView中。本例不涉及音频的编解码。 1、MediaCodec编码视频 使用MediaCo...

4万+

OP-TEE虚拟化支持详解

OP-TEE虚拟化支持解析 OP-TEE提供实验性虚拟化方案,支持多虚拟机共享安全执行环境。核心特性包括: 配置选项:CFG_VIRTUALIZATION启用虚拟化,CFG_VIRT_GUEST_COUNT限制虚拟机数量并分配均等内存资源; Hypervisor协作:需实现虚拟机生命周期通知、SMC参数转换、内存固定等关键协议; 当前限制:仅支持Armv8架构,硬件资源共享有限,采用静态内存配额机制; 架构实现:通过MMU分区切换和扩展内存分配器实现多租户隔离,Nexus核心层管理虚拟机上下文。该方案适用于

Linux内核架构师 109

从0到1打造一台机器人走起来

1)本体机械结构设计,依据人体生物学和生物力学知识,结合机器人运动时各关节扭矩需求,进行合理选用电机,比如胯部需要大扭矩,腿部惯量小等2)硬件拓扑上,分层架构的信号层和功率协同工作,高带宽,低延时,硬实时3)步态规划设计与验证环节实机回溯和部署过程:Real2Sim,Sim2Sim,Sim2Real。

weixin_43163941的博客 244

Android硬编解码利器MediaCodec解析——从猪肉餐馆的故事讲起(一)

前面视频理论基础几篇篇文章已经比较详细地介绍了H264编辑码基本原理以及码流的基本结构,其中并未叙述具体编解码算法,因为对于一般的工程类开发来说,这些知识已经足矣,算法那是专门做算法的人员需要研究的。而对于一般开发来说,已经有成熟的工具来处理编解码了,其中,MediaCodec就是Android平台中专门处理音视频硬编硬解码的利器。

大模型训练 1171

STM32(HAL库)驱动OLED

通过STM32F103C8T6单片机采用HAL库方式对0.96寸OLED(IIC通讯)屏幕进行驱动。

weixin_44597885的博客 8800

Android直播开发之旅(8):Android硬编解码接口MediaCodec原理剖析

转载请声明出处:http://blog.youkuaiyun.com/AndrExpert/article/details/79578149 在“AAC编码格式分析与MP4文件封装(MediaCodec+MediaMuxer)”一文中我们简单介绍了Android硬编解码接口MediaCodec的使用,本文将在该基础上对MediaCodec的基本工作原理和编解码过程进行剖析。 1. MediaCod...

老蒋也疯狂 8107

精雕软件(JDpaint) 5.5 绿色版

精雕软件(JDpaint) 5.5 绿色版精雕软件(JDpaint) 5.5 绿色版精雕软件(JDpaint) 5.5 绿色版

蓝牙模块HM-10手册

蓝牙模块HM-10手册,该手册对蓝牙模块HM-10进行了详细的介绍和使用方法以及一些AT指令。

Android MediaCodec 硬编解码实战:从Camera预览到H264流与回环渲染

摘要: MediaCodec是Android底层音视频编解码的核心接口,基于生产者-消费者模型实现异步处理。其关键组件包括输入/输出缓冲区队列和MediaFormat配置(如分辨率、比特率)。视频编码需注意YUV颜色格式匹配,解码需传递H264的SPS/PPS参数并利用Surface直接渲染以提升性能。实战中需遵循异步处理、双缓冲队列解耦、零拷贝等原则,避免状态异常和缓冲区泄漏。通过合理配置编解码流程与线程管理,可构建高效低延迟的视频处理应用。

郑敏 1473

华为的C语言编程规范.pdf

华为技术有限公司内部技术规范。本规范制定了编写C语言程序的基本原则、规则和建议。从代码的清晰、简洁、可测试、安全、程序效率、可移植各个方面对C语言编程作出了具体指导。

上一篇: 时间复杂度与常见排列算法
下一篇: 如何理解Linux系统SSH协议和原理
艾特小王子
博客等级 码龄10年 6粉丝 17原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值