Android音视频编码(2)

这篇具有很好参考价值的文章主要介绍了Android音视频编码(2)。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

Android本身提供了音视频编解码工具,很多时候是不需要第三方工具的,比如ffmpeg, OpenCV等,在android中引入第三库比较复杂,在Android音视频编码中介绍了如何引入第三方库libpng来进行进行图片处理,同时引入这些第三方库,是程序结构变得复杂。

本文介绍的音视频编解码利用的就是android自带的MediaCodec。视频编码之后,你可以对视频做任何形式的处理,比如添加广告,剪辑等等

MediaCodec

官方文档: MediaCodec

MediaCodec class can be used to access low-level media codecs, i.e. encoder/decoder components. It is part of the Android low-level multimedia support infrastructure (normally used together with MediaExtractor, MediaSync, MediaMuxer, MediaCrypto, MediaDrm, Image, Surface, and AudioTrack.)

Android音视频编码(2),android,音视频
在其生命周期中,编解码器概念上存在三种状态:停止、执行和释放。停止状态实际上是三种状态的集合:未初始化、已配置和错误。而执行状态在概念上分为三个子状态:刷新、运行和流结束。
Android音视频编码(2),android,音视频

使用Buffers进行异步处理

Since Build.VERSION_CODES.LOLLIPOP, the preferred method is to process data asynchronously by setting a callback before calling configure.

从Android5.0之后,谷歌就建议使用异步的方式。
Android音视频编码(2),android,音视频

 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() {}
  @Override
  void onCryptoError() {}
 });
 codec.configure(format,);
 mOutputFormat = codec.getOutputFormat(); // option B
 codec.start();
 // wait for processing to complete
 codec.stop();
 codec.release();

Since Build.VERSION_CODES.LOLLIPOP, you should retrieve input and output buffers using getInput/OutputBuffer(int) and/or getInput/OutputImage(int) even when using the codec in synchronous mode

在5.0之前,使用同步的方式。通过 getInput/OutputBuffer(int) 或者getInput/OutputImage(int) 来获取输入输出流。

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();

音视频编解码

这里使用同步编码,异步解码。

使用场景是需要截取某一段视频某个时间段的内容,则需要首先对原视频进行解码,获取相应时间段的开始帧和结束帧率。然后根据从开始帧和结束帧对视频进行重新编码。

关于视频帧

Android音视频编码(2),android,音视频
I帧又称帧内编码帧,是一种自带全部信息的独立帧,无需参考其他图像便可独立进行解码,可以简单理解为一张静态画面。视频序列中的第一个帧始终都是I帧,因为它是关键帧

P帧又称帧间预测编码帧,需要参考前面的I帧才能进行编码。表示的是当前帧画面与前一帧(前一帧可能是I帧也可能是P帧)的差别

B帧又称双向预测编码帧,也就是B帧记录的是本帧与前后帧的差别。也就是说要解码B帧,不仅要取得之前的缓存画面,还要解码之后的画面,通过前后画面的与本帧数据的叠加取得最终的画面。

这里直接使用获取的帧,可能是i,p,b帧,根据场景需要可以自己修改。

同步音视频编码
public class VideoEncoder {

    private static final String TAG = "VideoEncoder";

    private final static String MIME_TYPE = MediaFormat.MIMETYPE_VIDEO_AVC;
    private static final long DEFAULT_TIMEOUT_US = 10000;

    private MediaCodec mEncoder;
    private MediaMuxer mMediaMuxer;
    private int mVideoTrackIndex;
    private boolean mStop = false;
    private AudioEncode mAudioEncode;

    public void init(String outPath, int width, int height) {
        try {
            mStop = false;
            mVideoTrackIndex = -1;
            mMediaMuxer = new MediaMuxer(outPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
            mEncoder = MediaCodec.createEncoderByType(MIME_TYPE);
            MediaFormat mediaFormat = MediaFormat.createVideoFormat(MIME_TYPE, width, height);
            mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar);
            mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, width * height * 6);
            mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 30);
            mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
            mEncoder.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
            mEncoder.start();
        } catch (Throwable e) {
            e.printStackTrace();
        }
    }

    public void release() {
        mStop = true;
        if (mEncoder != null) {
            mEncoder.stop();
            mEncoder.release();
            mEncoder = null;
        }
        if (mMediaMuxer != null) {
            mMediaMuxer.stop();
            mMediaMuxer.release();
            mMediaMuxer = null;
        }
        if (mAudioEncode != null) {
            mAudioEncode.release();
        }
    }

    public void encode(byte[] yuv, long presentationTimeUs) {
        if (mEncoder == null || mMediaMuxer == null) {
            Log.e(TAG, "mEncoder or mMediaMuxer is null");
            return;
        }
        if (yuv == null) {
            Log.e(TAG, "input yuv data is null");
            return;
        }
        int inputBufferIndex = mEncoder.dequeueInputBuffer(DEFAULT_TIMEOUT_US);
        Log.d(TAG, "inputBufferIndex: " + inputBufferIndex);
        if (inputBufferIndex == -1) {
            Log.e(TAG, "no valid buffer available");
            return;
        }
        ByteBuffer inputBuffer = mEncoder.getInputBuffer(inputBufferIndex);
        inputBuffer.put(yuv);
        mEncoder.queueInputBuffer(inputBufferIndex, 0, yuv.length, presentationTimeUs, 0);
        while (!mStop) {
            MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
            int outputBufferIndex = mEncoder.dequeueOutputBuffer(bufferInfo, DEFAULT_TIMEOUT_US);
            Log.d(TAG, "outputBufferIndex: " + outputBufferIndex);
            if (outputBufferIndex >= 0) {
                ByteBuffer outputBuffer = mEncoder.getOutputBuffer(outputBufferIndex);
                // write head info
                if (mVideoTrackIndex == -1) {
                    Log.d(TAG, "this is first frame, call writeHeadInfo first");
                    mVideoTrackIndex = writeHeadInfo(outputBuffer, bufferInfo);
                }
                if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) == 0) {
                    Log.d(TAG, "write outputBuffer");
                    mMediaMuxer.writeSampleData(mVideoTrackIndex, outputBuffer, bufferInfo);
                }
                mEncoder.releaseOutputBuffer(outputBufferIndex, false);
                break; // 跳出循环
            }
        }
    }

    public void encodeAudio() {
        //音频混合
        mAudioEncode.encodeAudio();
        release();
    }

    private int writeHeadInfo(ByteBuffer outputBuffer, MediaCodec.BufferInfo bufferInfo) {
        byte[] csd = new byte[bufferInfo.size];
        outputBuffer.limit(bufferInfo.offset + bufferInfo.size);
        outputBuffer.position(bufferInfo.offset);
        outputBuffer.get(csd);
        ByteBuffer sps = null;
        ByteBuffer pps = null;
        for (int i = bufferInfo.size - 1; i > 3; i--) {
            if (csd[i] == 1 && csd[i - 1] == 0 && csd[i - 2] == 0 && csd[i - 3] == 0) {
                sps = ByteBuffer.allocate(i - 3);
                pps = ByteBuffer.allocate(bufferInfo.size - (i - 3));
                sps.put(csd, 0, i - 3).position(0);
                pps.put(csd, i - 3, bufferInfo.size - (i - 3)).position(0);
            }
        }
        MediaFormat outputFormat = mEncoder.getOutputFormat();
        if (sps != null && pps != null) {
            outputFormat.setByteBuffer("csd-0", sps);
            outputFormat.setByteBuffer("csd-1", pps);
        }
        int videoTrackIndex = mMediaMuxer.addTrack(outputFormat);
        mAudioEncode = new AudioEncode(mMediaMuxer);
        Log.d(TAG, "videoTrackIndex: " + videoTrackIndex);
        mMediaMuxer.start();
        return videoTrackIndex;
    }
}

音频编码:

public class AudioEncode {
    private  MediaMuxer mediaMuxer;
    private  int audioTrack;
    private MyExtractor audioExtractor;
    private MediaFormat audioFormat;
    public AudioEncode(MediaMuxer mediaMuxer) {
        audioExtractor = new MyExtractor(Constants.VIDEO_PATH);

        audioFormat = audioExtractor.getAudioFormat();
        if (audioFormat != null) {
            audioTrack = mediaMuxer.addTrack(audioFormat);
        }
        this.mediaMuxer = mediaMuxer;
    }
    @SuppressLint("WrongConstant")
    public void encodeAudio() {
        MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
        ByteBuffer buffer = ByteBuffer.allocate(1024 * 1024);
        if (audioFormat != null) {
            //写完视频,再把音频混合进去
            int audioSize = 0;
            //读取音频帧的数据,直到结束
            while ((audioSize = audioExtractor.readBuffer(buffer, false)) > 0) {
                bufferInfo.offset = 0;
                bufferInfo.size = audioSize;
                bufferInfo.presentationTimeUs = audioExtractor.getSampleTime();
                bufferInfo.flags = audioExtractor.getSampleFlags();
                mediaMuxer.writeSampleData(audioTrack, buffer, bufferInfo);
            }
        }
    }

    public void  release() {
        this.audioExtractor.release();
    }
}

异步音视频解码

要使用sleepRender进行时间同步。文章来源地址https://www.toymoban.com/news/detail-796924.html

/**
 * describe:异步解码
 */
public class AsyncVideoDecode extends BaseAsyncDecode {
    private static final String TAG = "AsyncVideoDecode";
    private Surface mSurface;
    private long mTime = -1;
    private int mOutputFormat = Constants.COLOR_FORMAT_NV12;

    private byte[] mYuvBuffer;


    private Map<Integer, MediaCodec.BufferInfo> map =
            new ConcurrentHashMap<>();
    private boolean writeAudioFlag = false;

    public AsyncVideoDecode(SurfaceTexture surfaceTexture, long progress) {
        super(progress);
        mSurface = new Surface(surfaceTexture);
    }
     private ByteArrayOutputStream outStream = new ByteArrayOutputStream();
    static VideoEncoder mVideoEncoder = null;
    @Override
    public void start(){
        super.start();
        mediaCodec.setCallback(new MediaCodec.Callback() {
            @Override
            public void onInputBufferAvailable(@NonNull MediaCodec codec, int index) {
                ByteBuffer inputBuffer = mediaCodec.getInputBuffer(index);
                int size = extractor.readBuffer(inputBuffer, true);
                long st = extractor.getSampleTime();
                if (size >= 0) {
                    codec.queueInputBuffer(
                            index,
                            0,
                            size,
                            st,
                            extractor.getSampleFlags()
                    );
                } else {
                    //结束
                    codec.queueInputBuffer(
                            index,
                            0,
                            0,
                            0,
                            BUFFER_FLAG_END_OF_STREAM
                    );
                }
            }

            @Override
            public void onOutputBufferAvailable(@NonNull MediaCodec codec, int index, @NonNull MediaCodec.BufferInfo info) {
                Message msg = new Message();
                msg.what = MSG_VIDEO_OUTPUT;
                Bundle bundle = new Bundle();
                bundle.putInt("index",index);
                bundle.putLong("time",info.presentationTimeUs);
                if (info.flags == BUFFER_FLAG_END_OF_STREAM)  {
                    if (mVideoEncoder != null)  {
                        mVideoEncoder.encodeAudio();
                    }
                }
                //当MediaCodeC配置了输出Surface时,此值返回null
                Image image = codec.getOutputImage(index);
                Rect rect = image.getCropRect();
//                if (image != null) {
//                    Rect rect = null;
//                    try {
//                        rect = image.getCropRect();
//                    } catch (Exception e) {
//                        throw new RuntimeException(e);
//                    }
                    if (mYuvBuffer == null) {
                        mYuvBuffer = new byte[rect.width()*rect.height()*3/2];
                    }
                    YuvImage yuvImage = new YuvImage(ConverUtils.getDataFromImage(image), ImageFormat.NV21, rect.width(), rect.height(), null);
                    yuvImage.compressToJpeg(rect, 100, outStream);
                    byte[] bytes = outStream.toByteArray();
                    if (mVideoEncoder == null) {
                        mVideoEncoder = new VideoEncoder();
                        mVideoEncoder.init(Constants.OUTPUT_PATH,  rect.width(), rect.height());
                    }
                    getDataFromImage(image, mOutputFormat, rect.width(), rect.height());
//                    mVideoEncoder.encode(ConverUtils.getDataFromImage(image), info.presentationTimeUs);
                      mVideoEncoder.encode(mYuvBuffer, info.presentationTimeUs);

//                }
//                ByteBuffer outputBuffer = codec.getOutputBuffer(index);
//                ConverUtils.convertByteBufferToBitmap(outputBuffer);
                msg.setData(bundle);
                mHandler.sendMessage(msg);
            }

            @Override
            public void onError(@NonNull MediaCodec codec, @NonNull MediaCodec.CodecException e) {
                codec.stop();
            }

            @Override
            public void onOutputFormatChanged(@NonNull MediaCodec codec, @NonNull MediaFormat format) {

            }
        });
//        mediaCodec.configure(mediaFormat,mSurface,null,0);
       mediaCodec.configure(mediaFormat,null,null,0);
        mediaCodec.start();
    }


    private void getDataFromImage(Image image, int colorFormat, int width, int height) {
        Rect crop = image.getCropRect();
        int format = image.getFormat();
        Log.d(TAG, "crop width: " + crop.width() + ", height: " + crop.height());
        Image.Plane[] planes = image.getPlanes();

        byte[] rowData = new byte[planes[0].getRowStride()];

        int channelOffset = 0;
        int outputStride = 1;
        for (int i = 0; i < planes.length; i++) {
            switch (i) {
                case 0:
                    channelOffset = 0;
                    outputStride = 1;
                    break;
                case 1:
                    if (colorFormat == Constants.COLOR_FORMAT_I420) {
                        channelOffset = width * height;
                        outputStride = 1;
                    } else if (colorFormat == Constants.COLOR_FORMAT_NV21) {
                        channelOffset = width * height + 1;
                        outputStride = 2;
                    } else if (colorFormat == Constants.COLOR_FORMAT_NV12) {
                        channelOffset = width * height;
                        outputStride = 2;
                    }
                    break;
                case 2:
                    if (colorFormat == Constants.COLOR_FORMAT_I420) {
                        channelOffset = (int) (width * height * 1.25);
                        outputStride = 1;
                    } else if (colorFormat ==Constants.COLOR_FORMAT_NV21) {
                        channelOffset = width * height;
                        outputStride = 2;
                    } else if (colorFormat == Constants.COLOR_FORMAT_NV12) {
                        channelOffset = width * height + 1;
                        outputStride = 2;
                    }
                    break;
                default:
            }
            ByteBuffer buffer = planes[i].getBuffer();
            int rowStride = planes[i].getRowStride();
            int pixelStride = planes[i].getPixelStride();

            int shift = (i == 0) ? 0 : 1;
            int w = width >> shift;
            int h = height >> shift;
            buffer.position(rowStride * (crop.top >> shift) + pixelStride * (crop.left >> shift));
            for (int row = 0; row < h; row++) {
                int length;
                if (pixelStride == 1 && outputStride == 1) {
                    length = w;
                    buffer.get(mYuvBuffer, channelOffset, length);
                    channelOffset += length;
                } else {
                    length = (w - 1) * pixelStride + 1;
                    buffer.get(rowData, 0, length);
                    for (int col = 0; col < w; col++) {
                        mYuvBuffer[channelOffset] = rowData[col * pixelStride];
                        channelOffset += outputStride;
                    }
                }
                if (row < h - 1) {
                    buffer.position(buffer.position() + rowStride - length);
                }
            }
        }
    }
    @Override
    protected int decodeType() {
        return VIDEO;
    }

    @Override
    public boolean handleMessage(@NonNull Message msg) {
        switch (msg.what){
            case MSG_VIDEO_OUTPUT:
                try {
                    if (mTime == -1) {
                        mTime = System.currentTimeMillis();
                    }

                    Bundle bundle = msg.getData();
                    int index = bundle.getInt("index");
                    long ptsTime = bundle.getLong("time");
                    sleepRender(ptsTime,mTime);
//                    if (ptsTime + extractor.getSampleDiffTime() >= progress &&  progress >= ptsTime)
//                    {
//                        writeAudioFlag = true;
//                    }
//                    if (writeAudioFlag) {
                        mediaCodec.releaseOutputBuffer(index, false);
//                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
                break;
            default:break;
        }
        return super.handleMessage(msg);
    }

    /**
     * 数据的时间戳对齐
     **/
    private long sleepRender(long ptsTimes, long startMs) {
        /**
         * 注意这里是以 0 为出事目标的,info.presenttationTimes 的单位为微秒
         * 这里用系统时间来模拟两帧的时间差
         */
        ptsTimes = ptsTimes / 1000;
        long systemTimes = System.currentTimeMillis() - startMs;
        long timeDifference = ptsTimes - systemTimes;
        // 如果当前帧比系统时间差快了,则延时以下
        if (timeDifference > 0) {
            try {
                //todo 受系统影响,建议还是用视频本身去告诉解码器 pts 时间
                Thread.sleep(timeDifference);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }
        return timeDifference;
    }
}

到了这里,关于Android音视频编码(2)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Android之 集成音视频通话

    一,背景 1.1 最近接收一个即时通讯二开项目,即时通讯部分用的XMPP协议,音视频则是集成的国外的开源免费库jitsi-meet-sdk-2.4.0-4.aar,是基于WebRTC的开源框架。但客户想要微信那种页面的排版,后来经研究jitsi是不能修改UI的,UI部分是用混合框架ReactNative写的,这样难度就大了

    2024年02月12日
    浏览(65)
  • 精选58道——Android 音视频面试题_安卓音视频面试题(3)

    先自我介绍一下,小编浙江大学毕业,去过华为、字节跳动等大厂,目前阿里P7 深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前! 因此收集整理了一份《2024年最新Android移动开发全套学习资

    2024年04月28日
    浏览(57)
  • 5G时代下,Android音视频强势崛起,我们该如何快速入门音视频技术?

    作为Android开发者的我们到底应不应该上音视频这条船? 接下来一起分析下。 大趋势 从未来的大趋势来看,随着5G时代的到来,音视频慢慢变成人们日常生活中的必需品。除了在线教育、音视频会议、即时通讯这些必须使用音视频技术的产品外,其它的产品也需要加入音频、

    2024年04月15日
    浏览(79)
  • Android音视频开发 - MediaMetadataRetriever 相关

    MediaMetadataRetriever 是android中用于从媒体文件中提取元数据新的类. 可以获取音频,视频和图像文件的各种信息,如时长,标题,封面等. 需要申请 读写权限 . 这里我使用的是本地路径, 需要注意的是如果路径文件不存在,会抛出 IllegalArgumentException,具体的源码如下: 根据keyCode返回keyC

    2024年04月08日
    浏览(54)
  • Android-音视频学习系列-(九)Android-端实现-rtmp-推流

    视频画面的采集主要是使用各个平台提供的摄像头 API 来实现的,在为摄像头设置了合适的参数之后,将摄像头实时采集的视频帧渲染到屏幕上提供给用户预览,然后将该视频帧传递给编码通道,进行编码。 1. 权限配置 2. 打开摄像头 2.1 检查摄像头 public static void checkCameraSe

    2024年04月12日
    浏览(73)
  • Android音视频学习系列(九) — Android端实现rtmp推流

    Android音视频学习系列(一) — JNI从入门到精通 Android音视频学习系列(二) — 交叉编译动态库、静态库的入门 Android音视频学习系列(三) — Shell脚本入门 Android音视频学习系列(四) — 一键编译32/64位FFmpeg4.2.2 Android音视频学习系列(五) — 掌握音频基础知识并使用AudioTrack、OpenSL ES渲

    2024年02月09日
    浏览(50)
  • Android音视频开发实战01-环境搭建

    FFmpeg 是一款流行的开源多媒体处理工具,它可以用于转换、编辑、录制和流式传输音视频文件。FFmpeg 具有广泛的应用场景,包括视频编解码、格式转换、裁剪、合并、滤镜等等。官网:https://ffmpeg.org/ FFmpeg 支持各种常见的音视频格式,例如 MP4、AVI、FLV、MOV、AAC、MP3、M4A 等等

    2024年02月10日
    浏览(56)
  • Android音视频——OpenMAX (OMX)框架

    本文分为两个部分进行讲解 Codec 部分中的 AwesomePlayer 到 OMX 服务 前面介绍了NuPlayer最终解码都会到达OMX框架,也就是 OpenMAX框架,本文开始分析编解码部分中的AwesomePlayer到OMX服务过程,也就是开启OpenMAX准备相关内容。Android系统中用OpenMAX来做编解码,Android向上抽象了一 层O

    2023年04月09日
    浏览(56)
  • Android音视频开发实战02-Jni

    JNI是Java Native Interface的缩写,是Java提供的一种机制,用于在Java代码中调用本地(C/C++)代码。它允许Java代码与本地代码进行交互,通过JNI,Java应用程序可以调用一些原生库或者操作系统API,以获取更好的性能和更强的功能支持。 使用JNI需要编写一些Native方法,并将其实现在

    2024年02月11日
    浏览(55)
  • Android-音视频学习系列-(九)Android-端实现-rtmp-推流(2)

    配置好之后,检查一下 AudioRecord 当前的状态是否可以进行录制,可以通过 AudioRecord##getState 来获取当前的状态: STATE_UNINITIALIZED 还没有初始化,或者初始化失败了 STATE_INITIALIZED 已经初始化成功了。 2. 开启采集 创建好 AudioRecord 之后,就可以开启音频数据的采集了,可以通过调

    2024年04月12日
    浏览(62)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包