FFmpegAndroid技术知识Android FFMPEG

android使用ffmpeg录制和播放aac文件

2017-12-21  本文已影响74人  Cui三土

采集音频并对音频编码
文章对应的项目地址
https://github.com/cuiyaoDroid/AndroidFFmpegAac
ffmpeg编译静态库的部分只有编译脚本,ffmpeg的源码可以从官方获取。

一、使用ndk编译ffmpeg

获取ffmpeg源码。

git clone https://git.ffmpeg.org/ffmpeg.git ffmpeg

然后我们修改一下里面的configure文件,让我们编译出来的文件不会带有奇怪的名字,不被Android识别。只要把

SLIBNAME_WITH_MAJOR='$(SLIBNAME).$(LIBMAJOR)'
LIB_INSTALL_EXTRA_CMD='$$(RANLIB) "$(LIBDIR)/$(LIBNAME)"'
SLIB_INSTALL_NAME='$(SLIBNAME_WITH_VERSION)'
SLIB_INSTALL_LINKS='$(SLIBNAME_WITH_MAJOR) $(SLIBNAME)'

修改成

SLIBNAME_WITH_MAJOR='$(SLIBPREF)$(FULLNAME)-$(LIBMAJOR)$(SLIBSUF)'  
LIB_INSTALL_EXTRA_CMD='$$(RANLIB)"$(LIBDIR)/$(LIBNAME)"'  
SLIB_INSTALL_NAME='$(SLIBNAME_WITH_MAJOR)'  
SLIB_INSTALL_LINKS='$(SLIBNAME)' 

我编译时使用的脚本。需要根据自己的ndk的路径配置交叉编译脚本。
android_config_armeabi_v7a.sh,此脚本需要是编译armv7版本的静态库使用,编译armv64版本需要修改脚本。

#!/bin/bash 
NDK=/Users/yaocui/Documents/adt-bundle-mac-x86_64-20140702/android-ndk-r13b
#NDK=/Users/yaocui/Documents/adt-bundle-mac-x86_64-20140702/sdk/ndk-bundle
SYSROOT=$NDK/platforms/android-21/arch-arm64
TOOLCHAIN=$NDK/toolchains/aarch64-linux-android-4.9/prebuilt/darwin-x86_64
#TOOLCHAIN=$NDK/prebuilt/darwin-x86_64

function build_one {
./configure \
--prefix=$PREFIX \
--cc=$TOOLCHAIN/bin/aarch64-linux-android-gcc \
--nm=$TOOLCHAIN/bin/aarch64-linux-android-nm \
--enable-asm \
--enable-neon \
--enable-static \
--disable-shared \
--disable-doc \
--disable-asm \
--disable-ffmpeg \
--disable-ffplay \
--disable-ffprobe \
--disable-ffserver \
--disable-postproc \
--disable-avdevice \
--disable-symver \
--disable-stripping \
--disable-muxers \
--disable-encoders \
--enable-encoder=aac \
--disable-decoders \
--enable-decoder=aac \
--disable-demuxers \
--enable-demuxer=aac \
--disable-parsers \
--enable-parser=aac \
--cross-prefix=$TOOLCHAIN/bin/aarch64-linux-android- \
--target-os=linux \
--arch=aarch64 \
--cpu=armv8-a \
--enable-runtime-cpudetect \
--enable-gpl \
--enable-small \
--enable-cross-compile \
--sysroot=$SYSROOT \
--extra-cflags="-fPIC -DANDROID -I$NDK/platforms/android-21/arch-arm64/usr/include -I$SYSROOT/usr/include" \
--extra-ldflags="$ADDI_LDFLAGS"

sed -i '' 's/HAVE_LRINT 0/HAVE_LRINT 1/g' config.h
sed -i '' 's/HAVE_LRINTF 0/HAVE_LRINTF 1/g' config.h
sed -i '' 's/HAVE_ROUND 0/HAVE_ROUND 1/g' config.h
sed -i '' 's/HAVE_ROUNDF 0/HAVE_ROUNDF 1/g' config.h
sed -i '' 's/HAVE_TRUNC 0/HAVE_TRUNC 1/g' config.h
sed -i '' 's/HAVE_TRUNCF 0/HAVE_TRUNCF 1/g' config.h
sed -i '' 's/HAVE_CBRT 0/HAVE_CBRT 1/g' config.h
sed -i '' 's/HAVE_RINT 0/HAVE_RINT 1/g' config.h
sed -i '' 's/HAVE_LOG2 1/HAVE_LOG2 0/g' config.h
sed -i '' 's/HAVE_LOG2F 1/HAVE_LOG2F 0/g' config.h

make clean
make -j4
make install
}
#CPU=arm
#PREFIX=$(pwd)/android/$CPU
# arm v7vfp
CPU=armv8-a
OPTIMIZE_CFLAGS="-marm -march=$CPU "
PREFIX=./android/$CPU
ADDI_CFLAGS="-marm"
build_one

关键几个脚本
--disable-shared --enable-static 关闭动态库的生成,开启静态库的生成,本例中使用静态库,想要使用动态库也可以,但动态库最终生成的程序安装包会相对大一些。

--disable-encoders --disable-decoders... 关闭所有编码器解码器等,将无用的功能关闭掉,减小生成的库文件的大小。

--enable-encoder=aac --enable-decoder=aac 开启aac的编码器和解码器,我们只用到了aac的编解码器。

--disable-ffmpeg --disable-ffplay --disable-ffprobe --disable-ffserver --disable-postproc --disable-avdevice 关闭一些我们不需要的功能,减小库文件大小。

执行编译脚本

./android_config_armeabi_v7a.sh

生成的库文件在android目录下。

arm64的编译过程一样,可以查看我的项目。
https://github.com/cuiyaoDroid/AndroidFFmpegAac/blob/master/android_config_arm64_v8a.sh

二、创建android项目

1、创建一个android项目,添加c++支持。
2、将编译ffmpeg生成的静态库和头文件复制进项目目录下。



3、编写Application.mk,支持armeabi-v7a arm64-v8a两种架构。

APP_STL := gnustl_static
APP_LDFLAGS := -latomic
APP_ABI := armeabi-v7a arm64-v8a
APP_PLATFORM := android-21

4、编写Android.mk文件,根据不同的cpu架构使用不同的静态库。

LOCAL_PATH := $(call my-dir)

# prepare libX
include $(CLEAR_VARS)
TARGET_ARCH_ABI := armeabi-v7a arm64-v8a
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include
LOCAL_MODULE    := avcodec
ifeq ($(APP_ABI), armeabi-v7a)
  LOCAL_SRC_FILES := libarmv7a/libavcodec.a
else
  LOCAL_SRC_FILES := libarm64/libavcodec.a
endif
include $(PREBUILT_STATIC_LIBRARY)

# prepare libX
include $(CLEAR_VARS)
TARGET_ARCH_ABI := armeabi-v7a arm64-v8a
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include
LOCAL_MODULE    := avfilter
ifeq ($(APP_ABI), armeabi-v7a)
  LOCAL_SRC_FILES := libarmv7a/libavfilter.a
else
  LOCAL_SRC_FILES := libarm64/libavfilter.a
endif
include $(PREBUILT_STATIC_LIBRARY)

# prepare libX
include $(CLEAR_VARS)
TARGET_ARCH_ABI := armeabi-v7a arm64-v8a
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include
LOCAL_MODULE    := avformat
ifeq ($(APP_ABI), armeabi-v7a)
  LOCAL_SRC_FILES := libarmv7a/libavformat.a
else
  LOCAL_SRC_FILES := libarm64/libavformat.a
endif
include $(PREBUILT_STATIC_LIBRARY)

# prepare libX
include $(CLEAR_VARS)
TARGET_ARCH_ABI := armeabi-v7a arm64-v8a
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include
LOCAL_MODULE    := avutil
ifeq ($(APP_ABI), armeabi-v7a)
  LOCAL_SRC_FILES := libarmv7a/libavutil.a
else
  LOCAL_SRC_FILES := libarm64/libavutil.a
endif
include $(PREBUILT_STATIC_LIBRARY)

# prepare libX
include $(CLEAR_VARS)
TARGET_ARCH_ABI := armeabi-v7a arm64-v8a
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include
LOCAL_MODULE    := swresample
ifeq ($(APP_ABI), armeabi-v7a)
  LOCAL_SRC_FILES := libarmv7a/libswresample.a
else
  LOCAL_SRC_FILES := libarm64/libswresample.a
endif
include $(PREBUILT_STATIC_LIBRARY)

# prepare libX
include $(CLEAR_VARS)
TARGET_ARCH_ABI := armeabi-v7a arm64-v8a
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include
LOCAL_MODULE    := swscale
ifeq ($(APP_ABI), armeabi-v7a)
  LOCAL_SRC_FILES := libarmv7a/libswscale.a
else
  LOCAL_SRC_FILES := libarm64/libswscale.a
endif
include $(PREBUILT_STATIC_LIBRARY)


include $(CLEAR_VARS)

TARGET_ARCH_ABI := armeabi-v7a arm64-v8a
LOCAL_MODULE     := ffmpeg_aac_jni
LOCAL_SRC_FILES  := FFmpegAacJni.cpp AacRecoder.cpp AacPlayer.cpp
LOCAL_C_INCLUDES += $(LOCAL_PATH)/include
LOCAL_CFLAGS     := -D__STDC_CONSTANT_MACROS -Wno-sign-compare -Wno-switch -Wno-pointer-sign -DHAVE_NEON=1 -mfpu=neon -mfloat-abi=softfp -fPIC -DANDROID
LOCAL_STATIC_LIBRARIES := avfilter avformat avcodec swresample swscale avutil
LOCAL_LDLIBS     := -L$(NDK_ROOT)/platforms/$(APP_PLATFORM)/arch-arm/usr/lib -L$(LOCAL_PATH) -llog -ljnigraphics -landroid -lz -ldl -lm

include $(BUILD_SHARED_LIBRARY)

脚本中使用LOCAL_STATIC_LIBRARIES配置静态库的链接,对顺序有要求,不同版本ffmpeg的顺序可能不同,可以查看ffmpeg目录下的makefile文件,查看库文件加载顺序。

...
# $(FFLIBS-yes) needs to be in linking order
FFLIBS-$(CONFIG_AVDEVICE)   += avdevice
FFLIBS-$(CONFIG_AVFILTER)   += avfilter
FFLIBS-$(CONFIG_AVFORMAT)   += avformat
FFLIBS-$(CONFIG_AVCODEC)    += avcodec
FFLIBS-$(CONFIG_AVRESAMPLE) += avresample
FFLIBS-$(CONFIG_POSTPROC)   += postproc
FFLIBS-$(CONFIG_SWRESAMPLE) += swresample
FFLIBS-$(CONFIG_SWSCALE)    += swscale

FFLIBS := avutil
...

5、创建c++文件实现编解码。


三、编解码逻辑的实现。

1、首先创建java的native方法,这里我们一共需要6个方法,分别是
编码部分:初始化编码器、编码pcm数据得到aac数据、关闭编码器。
解码部分:初始化解码器、使用解码器读取pcm数据、关闭解码器。

public class FFmpegAacNativeLib
{

    public long mNativeContextRecoder;
    public long mNativeContextPlayer;

    static {
        System.loadLibrary("ffmpeg_aac_jni");
    }
    //declare the jni functions
    public native String audioPlayerOpenFile(String path);//初始化解码器
    public native int audioPlayerGetPCM(byte[] pcmbuffer);//使用解码器读取pcm数据
    public native int audioPlayerStop();//关闭解码器

    public native int audioEncodePCMToAACInit();//初始化编码器
    public native int audioEncodePCMToAAC(byte[] pcmbuf,int len,byte[] amrbuf);//使用解码器读取pcm数据
    public native void audioEncodeStop();//关闭解码器


    //Singleton
    private static FFmpegAacNativeLib instance=null;

    public static FFmpegAacNativeLib getInstance() {
        if(instance==null)
            instance=new FFmpegAacNativeLib();
        return instance;
    }
}

2、编写jni接口。
FFmpegAacJni.cpp

...
#include <jni.h>
...
static jint audioEncodePCMToAACInit(JNIEnv *env, jobject thiz) {

    return 0;

}
static jint audioEncodePCMToAAC(JNIEnv *env, jobject thiz, jbyteArray pcmbuf_,jint len,
                                                  jbyteArray amrbuf_) {
    return 0;
}
static void audioEncodeStop(JNIEnv *env, jobject thiz) {

}
static jstring audioPlayerOpenFile(JNIEnv *env, jobject thiz, jstring path) {
 
    return NULL;
}
static jint audioPlayerGetPCM(JNIEnv *env, jobject thiz, jbyteArray pcmbuffer_) {
   
    return 0;
}
static jint audioPlayerStop(JNIEnv *env, jobject thiz) {
    // TODO
    return 0;
}
static JNINativeMethod gMethods[] = {
        { "audioEncodePCMToAACInit", "()I", (void *)audioEncodePCMToAACInit },
        { "audioEncodePCMToAAC", "([BI[B)I", (void *)audioEncodePCMToAAC },
        { "audioEncodeStop", "()V", (void *)audioEncodeStop },
        { "audioPlayerOpenFile", "(Ljava/lang/String;)Ljava/lang/String;", (void *)audioPlayerOpenFile },
        { "audioPlayerGetPCM", "([B)I", (void *)audioPlayerGetPCM },
        { "audioPlayerStop", "()I", (void *)audioPlayerStop }
};


jint JNI_OnLoad(JavaVM* vm, void* reserved){
    ALOGD("JNI_OnLoad");
    JNIEnv* env = NULL;
    jint result = -1;
    jclass clazz;

    if ((*vm).GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {
        ALOGE("GetEnv fail");
        return result;
    }
    assert(env != NULL);

    clazz = (*env).FindClass("com/cuiyao/ffmpegaac/lib/FFmpegAacNativeLib");
    if (clazz == NULL) {
        ALOGE("com/cuiyao/ffmpegaac/lib/FFmpegAacNativeLib not found");
        return result;
    }
    // 注册native方法到java中
    if ((*env).RegisterNatives(clazz, gMethods, sizeof(gMethods)/sizeof(gMethods[0])) < 0) {
        ALOGE("RegisterNatives methods fail");
        return result;
    }
    // 返回jni的版本
    return JNI_VERSION_1_4;
}

3、关键代码编码器初始化,设置目标音频各项参数,初始化编码器和缓冲区。在后面编写java层AudioRecord的参数和缓存区需要一一对应。

    ALOGW("start");
//  av_register_all();
    avcodec_register_all();
    mAVCodec = avcodec_find_encoder(AV_CODEC_ID_AAC);//查找AAC编码器
    if(!mAVCodec){
        ALOGE("encoder AV_CODEC_ID_AAC not found");
        return -1;
    }
    mAVCodecContext = avcodec_alloc_context3(mAVCodec);
    if(mAVCodecContext != NULL){
        mAVCodecContext->codec_id         = AV_CODEC_ID_AAC;
        mAVCodecContext->codec_type       = AVMEDIA_TYPE_AUDIO;
        mAVCodecContext->bit_rate         = 12200;
        mAVCodecContext->sample_fmt       = AV_SAMPLE_FMT_FLTP;
        mAVCodecContext->sample_rate      = 8000;
        mAVCodecContext->channel_layout   = AV_CH_LAYOUT_MONO;
        mAVCodecContext->channels         = av_get_channel_layout_nb_channels(mAVCodecContext->channel_layout);
    }else {
        ALOGE("avcodec_alloc_context3 fail");
        return -1;
    }
    ALOGW("start  3 channels %d",mAVCodecContext->channels);
    if(avcodec_open2(mAVCodecContext, mAVCodec, NULL) < 0){
        ALOGE("aac avcodec open fail");
        av_free(mAVCodecContext);
        mAVCodecContext = NULL;
        return -1;
    }
    mAVFrame = av_frame_alloc();
    if(!mAVFrame) {
        ALOGE("avframe alloc fail");
        avcodec_close(mAVCodecContext);
        av_free(mAVCodecContext);
        mAVCodecContext = NULL;
        return -1;
    }
    mAVFrame->nb_samples = mAVCodecContext->frame_size;
    mAVFrame->format = mAVCodecContext->sample_fmt;
    mAVFrame->channel_layout = mAVCodecContext->channel_layout;

    mBufferSize = av_samples_get_buffer_size(NULL, mAVCodecContext->channels, mAVCodecContext->frame_size, mAVCodecContext->sample_fmt, 0);
    if(mBufferSize < 0){
        ALOGE("av_samples_get_buffer_size fail");
        av_frame_free(&mAVFrame);
        mAVFrame = NULL;
        avcodec_close(mAVCodecContext);
        av_free(mAVCodecContext);
        mAVCodecContext = NULL;
        return -1;
    }
    mEncoderData = (uint8_t *)av_malloc(mBufferSize);

    if(!mEncoderData){
        ALOGE("av_malloc fail");
        av_frame_free(&mAVFrame);
        mAVFrame = NULL;
        avcodec_close(mAVCodecContext);
        av_free(mAVCodecContext);
        mAVCodecContext = NULL;
        return -1;
    }

    avcodec_fill_audio_frame(mAVFrame, mAVCodecContext->channels, mAVCodecContext->sample_fmt, (const uint8_t*)mEncoderData, mBufferSize, 0);

4、使用编码器对pcm数据进行编码得到aac数据

int AacRecoder::encode_pcm_data(void* pIn, int frameSize,jbyte * pOut){
    int encode_ret = -1;
    int got_packet_ptr = 0;
    AVPacket pkt;
    av_init_packet(&pkt);
    pkt.data = NULL;
    pkt.size = 0;
    if(mAVCodecContext && mAVFrame){
        short2float((int16_t *)pIn, mEncoderData, frameSize/2);

        mAVFrame->data[0] = mEncoderData;
        mAVFrame->pts = 0;
        //音频编码
        encode_ret = avcodec_encode_audio2(mAVCodecContext, &pkt, mAVFrame, &got_packet_ptr);
        if(encode_ret < 0){
            ALOGE("Failed to encode!\n");
            return encode_ret;
        }
        if(pkt.size > 0){

            int length = pkt.size + ADTS_HEADER_LENGTH;
            void *adts = malloc(ADTS_HEADER_LENGTH);
            //添加adts header 可以正常播放。
            addADTSheader((uint8_t *)adts, pkt.size+ADTS_HEADER_LENGTH);
//            ALOGW("header ---- =%s",adts);
            memcpy(pOut,adts,  ADTS_HEADER_LENGTH);
            free(adts);

            memcpy(pOut+ADTS_HEADER_LENGTH,pkt.data,pkt.size);
//            ALOGW("data ---- =%s",pkt.data);

            av_free_packet(&pkt);
            return length;
        }
        av_free_packet(&pkt);
        return 0;
    }
    return encode_ret;
}

对每一帧的音频加adts头,不加adts头的音频无法直接播放。

void AacRecoder::addADTSheader(uint8_t * in, int packet_size){
    int sampling_frequency_index = 11; //采样率下标
    int channel_configuration = mAVCodecContext->channels; //声道数
    in[0] = 0xFF;
    in[1] = 0xF9;
    in[2] = 0x40 | (sampling_frequency_index << 2) | (channel_configuration >> 2);//0x6c;
    in[3] = (channel_configuration & 0x3) << 6;
    in[3] |= (packet_size & 0x1800) >> 11;
    in[4] = (packet_size & 0x1FF8) >> 3;
    in[5] = ((((unsigned char)packet_size) & 0x07) << 5) | (0xff >> 3);
    in[6] = 0xFC;
}

5、解码器初始化,得到音频的音道数量和采样率。

ALOGW("start");
//    myinput = (char*)malloc(sizeof(input));
//    strcpy(myinput,input);
    ALOGI("%s", "sound");
    //注册组件
    av_register_all();
    pFormatCtx = avformat_alloc_context();
    //打开音频文件
    if (avformat_open_input(&pFormatCtx, input, NULL, NULL) != 0) {
        ALOGI("%s", "无法打开音频文件");
        return NULL;
    }
    //获取输入文件信息
    if (avformat_find_stream_info(pFormatCtx, NULL) < 0) {
        ALOGI("%s", "无法获取输入文件信息");
        return NULL;
    }
    //获取音频流索引位置
    int i = 0;
    for (; i < pFormatCtx->nb_streams; i++) {
        if (pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_AUDIO) {
            audio_stream_idx = i;
            break;
        }
    }

    //获取解码器
    codecCtx = pFormatCtx->streams[audio_stream_idx]->codec;
    AVCodec *codec = avcodec_find_decoder(codecCtx->codec_id);
    if (codec == NULL) {
        ALOGI("%s", "无法获取解码器");
        return NULL;
    }
    //打开解码器
    if (avcodec_open2(codecCtx, codec, NULL) < 0) {
        ALOGI("%s", "无法打开解码器");
        return NULL;
    }
    //压缩数据

    //解压缩数据
    frame = av_frame_alloc();
    //frame->16bit 44100 PCM 统一音频采样格式与采样率
    swrCtx = swr_alloc();

    //重采样设置参数-------------start
    //输入的采样格式
    enum AVSampleFormat in_sample_fmt = codecCtx->sample_fmt;
    //输出采样格式16bit PCM
    out_sample_fmt = AV_SAMPLE_FMT_S16;
    //输入采样率
    int in_sample_rate = codecCtx->sample_rate;
    //输出采样率
    int out_sample_rate = in_sample_rate;
    //获取输入的声道布局
    //根据声道个数获取默认的声道布局(2个声道,默认立体声stereo)
    //av_get_default_channel_layout(codecCtx->channels);
    uint64_t in_ch_layout = codecCtx->channel_layout;
    //输出的声道布局(立体声)
    uint64_t out_ch_layout = AV_CH_LAYOUT_MONO;

    swr_alloc_set_opts(swrCtx,
                       out_ch_layout, out_sample_fmt, out_sample_rate,
                       in_ch_layout, in_sample_fmt, in_sample_rate,
                       0, NULL);


    ALOGI("in_samplde_rate %d",in_sample_rate);

    swr_init(swrCtx);

    //输出的声道个数
    out_channel_nb = av_get_channel_layout_nb_channels(out_ch_layout);
    sprintf(info,"%d,%d",in_sample_rate,out_channel_nb);

6、得到解码后的pcm数据。

int AacPlayer::getpcmbuff(uint8_t* out_buffer){
    //16bit 44100 PCM 数据
    AVPacket packet;
    av_init_packet(&packet);
    int got_frame = 0, index = 0, ret;
    int out_buffer_size = 0;
    //不断读取压缩数据
    if(av_read_frame(pFormatCtx, &packet) >= 0){
        //解码音频类型的Packet
        if (packet.stream_index == audio_stream_idx) {
            //解码
            ret = avcodec_decode_audio4(codecCtx, frame, &got_frame, &packet);
            if (ret < 0) {
                ALOGI("%s", "解码完成");
            }
            //解码一帧成功
            if (got_frame > 0) {
                ALOGI("解码:%d", index++);
                swr_convert(swrCtx, &out_buffer, MAX_AUDIO_FRME_SIZE,
                            (const uint8_t **) frame->data, frame->nb_samples);
                //获取sample的size
                out_buffer_size = av_samples_get_buffer_size(NULL, out_channel_nb,
                                                                 frame->nb_samples, out_sample_fmt,
                                                                 1);
            }
        }
    }else{
        av_free_packet(&packet);
        return -1;
    }
    av_free_packet(&packet);
    return out_buffer_size;
}

这部分只贴出了关键的编解码的代码,使用c++编写aac编解码部分的代码,并通过jni接口向java层提供编解码方法,配合java中的AudioRecord和AudioTrack即可以实现录音和播放。

四、录音和播放的实现

这部分是使用AudioRecord和AudioTrack配合已经写好的native方法进行录音和播放的实现。
1、录音的流程
初始化AudioRecord和编码器—>采集pcm音频数据—>使用编码器进行编码—>将编码后的数据写入文件—>关闭AudioRecord和编码器

2、播放流程是两条线
初始化解码器加载文件—>解码文件读取解码后的pcm数据—>将解码后的数据加入播放缓冲区—>关闭解码器
初始化AudioTrack—>读取缓冲区中的pcm数据—>播放—>关闭AudioTrack

这一部分代码就不贴出来了,如果想看具体的实现可以到我的项目,里面有完整的aac音频录制和播放的demo。

项目地址
https://github.com/cuiyaoDroid/AndroidFFmpegAac

上一篇下一篇

猜你喜欢

热点阅读