专栏首页包子的书架FFmpeg+OpenSLES 实现音频播放
原创

FFmpeg+OpenSLES 实现音频播放

前言

最近一直在学习FFmpeg,看了网上各位大神的,都玩得很溜,自己也来一波骚操作。于是乎利用FFmpeg结合OpenSles来进行对音频文件的播放。网上看的都是别人的写的代码,拿来运行下,发现不是很适用。别人的毕竟是别人的,还是要自己打通下筋脉掌握下。

1595.jpg

介绍下一些函数

FFmpeg的函数介绍

在之前的文章有介绍,可以参考:https://cloud.tencent.com/developer/article/1666126

OpenSLES的函数介绍

  • slCreateEngine 函数

源码:

SL_API SLresult SLAPIENTRY slCreateEngine(
	SLObjectItf             *pEngine,
	SLuint32                numOptions,
	const SLEngineOption    *pEngineOptions,
	SLuint32                numInterfaces,
	const SLInterfaceID     *pInterfaceIds,
	const SLboolean         * pInterfaceRequired
);

作用:

初始化引擎对象给使用者一个处理手柄对象,第四个参数(需要支持的interface数目)为零则会忽视第五、第六个参数。

例子:

SLresult result;
const SLEngineOption engineOptions[1] = {{(SLuint32) SL_ENGINEOPTION_THREADSAFE, (SLuint32) SL_BOOLEAN_TRUE}};
result = slCreateEngine(&engineObject, 1, engineOptions, 0, NULL, NULL);
  • Realize 函数

源码:

SLresult (*Realize) (
		SLObjectItf self,
		SLboolean async
	);

作用:

转化一个Object从未实例化到实例化过程,第二个参数表示是否异步

例子:

result = (*engineObject)->Realize(engineObject, SL_BOOLEAN_FALSE);
  • GetInterface 函数

源码:

SLresult (*GetInterface) (
		SLObjectItf self,
		const SLInterfaceID iid,
		void * pInterface
	);

作用:

得到由Object暴露的接口,这里指的是引擎接口,第二个参数是接口ID,第三个参数是输出的引擎接口对象。

例子:

result = (*engineObject)->GetInterface(engineObject, SL_IID_ENGINE, &engineEngine);
  • CreateOutputMix 函数

源码:

	SLresult (*CreateOutputMix) (
		SLEngineItf self,
		SLObjectItf * pMix,
		SLuint32 numInterfaces,
		const SLInterfaceID * pInterfaceIds,
		const SLboolean * pInterfaceRequired
	);

作用:

创建输出混音器--->由引擎接口创建,从第三个参数开始就是支持的interface数目,同样的为零忽略第四第五个参数.

例子:

const SLInterfaceID interfaceIds[1] = {SL_IID_ENVIRONMENTALREVERB};//这里给一个环境混响的接口id
const SLboolean reqs[1] = {SL_BOOLEAN_TRUE};
result = (*engineEngine)->CreateOutputMix(engineEngine, &outputMixObject, 1, interfaceIds,
                                     reqs);
  • CreateAudioPlayer 函数

源码:

SLresult (*CreateAudioPlayer) (
		SLEngineItf self,
		SLObjectItf * pPlayer,
		SLDataSource *pAudioSrc,
		SLDataSink *pAudioSnk,
		SLuint32 numInterfaces,
		const SLInterfaceID * pInterfaceIds,
		const SLboolean * pInterfaceRequired
	);

作用:

创建播放器---->由引擎接口创建,第三个参数表示设置播放的数据源(来播放缓存队列),第四个配置音频接收器,第四个参数(需要支持的interface数目)为零则会忽视第五、第六个参数。

  • RegisterCallback 函数

源码:

SLresult (*RegisterCallback) (
		SLAndroidSimpleBufferQueueItf self,
		slAndroidSimpleBufferQueueCallback callback,
		void* pContext
	);

typedef void (SLAPIENTRY *slAndroidSimpleBufferQueueCallback)(
	SLAndroidSimpleBufferQueueItf caller,
	void *pContext
);

作用:

设置缓存队列的回调函数,第二个就是缓存回调,第三个参数是回调函数slAndroidSimpleBufferQueueCallback的第二个参数

例子:

result = (*bqPlayerBufferQueue2)->RegisterCallback(bqPlayerBufferQueue2, bqPlayerCallback2,
                                                      (void *) "1");
//回调函数
void bqPlayerCallback2(SLAndroidSimpleBufferQueueItf bq, void *context);
  • SetPlayState 函数

源码:

SLresult (*SetPlayState) (
		SLPlayItf self,
		SLuint32 state
	);

作用:

设置播放状态

例子:

    result = (*bqPlayerPlay)->SetPlayState(bqPlayerPlay, SL_PLAYSTATE_PLAYING); //设置为播放中

源码:Enqueue 函数

SLresult (*Enqueue) (
		SLAndroidSimpleBufferQueueItf self,
		const void *pBuffer,
		SLuint32 size
	);

作用:

将解码数据防区播放缓冲数据栈, 第二个参数是播放数据,第三个是表示数据大小

例子:

result = (*bqPlayerBufferQueue)->Enqueue(bqPlayerBufferQueue, out_buffer, dst_buffer_size);

思路

整体思路:

  1. 由FFmpeg打开音频文件,获取相应的解码器。
  2. 拿到相应的音频编码格式,采样率,声道等。
  3. 编写解码函数getPCM,为了让opensles调用获取到解码的数据。
  4. 创建opensles的对象和接口,创建音频播发器,创建缓冲队列和缓冲回调函数,设置播放状态为播放中。
  5. 主动触发回调函数,在回调函数调用解码函数getPCM,将音频文件转码成pcm文件,然后将每一帧解码的数据和大小,传到openSles的数据缓冲队列中,进行音频播放。
FFmpeg和openSles的流程关系图.png

开始撸码

  • 创建FFmpeg,获取解码器和相关信息
int createFFmpeg(JNIEnv *env, jstring srcPath) {

    const char * originPath = env->GetStringUTFChars(srcPath, NULL);
    //创建avFormatContext对象
    avFormatContext = avformat_alloc_context();
    int ret = avformat_open_input(&avFormatContext, originPath, NULL, NULL);
    if(ret != 0) {
        LOGE("打开文件失败");
        return -1;
    }

    //输出文件信息
    av_dump_format(avFormatContext, 0, originPath, 0);

    ret = avformat_find_stream_info(avFormatContext, NULL);
    if(ret < 0) {
        LOGE("获取编码流信息失败");
        return -1;
    }

    //获取当前的类型流的索引位置
    for (int i = 0; i < avFormatContext->nb_streams; ++i) {
        //获取流的编码类型
        enum AVMediaType avMediaType = avFormatContext->streams[i]->codecpar->codec_type;
        if(avMediaType == AVMEDIA_TYPE_AUDIO) {
            streamIndex = i;
            break;
        }
    }

    //根据类型流对应的索引位置,获取对应的类型解码器
    AVCodecParameters *avCodecParameters = avFormatContext->streams[streamIndex]->codecpar;
    //获取对应类型的解码器id
    AVCodecID avCodecId = avCodecParameters->codec_id;

    //获取解码器
    AVCodec *avCodec = avcodec_find_decoder(avCodecId);

    //创建解码器上下文
    avCodecContext = avcodec_alloc_context3(NULL);
    if(avCodecContext == NULL) {
        LOGE("创建解码器上下文失败");
        return -1;
    }

    //将AVCodecParameter的相关内容-->AVCodecContext
    avcodec_parameters_to_context(avCodecContext, avCodecParameters);
    // 打开解码器
    ret = avcodec_open2(avCodecContext, avCodec,  NULL);
    if(ret < 0) {
        LOGE("打开解码器失败");
        return -1;
    }

    //创建源文件解码的压缩数据包对象
    avPacket = static_cast<AVPacket *>(av_mallocz(sizeof(AVPacket)));
    //创建一个用于存放解码之后的像素数据
    avFrame = av_frame_alloc();
    //创建SwrContext对象,分配空间
    swrContext = swr_alloc();
    // 原音频的采样编码格式
    AVSampleFormat srcFormat = avCodecContext->sample_fmt;
    // 生成目标采样编码格式
    AVSampleFormat  dstFormat = dst_sample_fmt;
    // 原音频的采样率
    int srcSampleRate = avCodecContext->sample_rate;
    // 生成目标采样率
    int disSampleRate = 48000;
    // 输入声道布局
    uint64_t src_ch_layout = avCodecContext->channel_layout;
    // 输出声道布局
    uint64_t dst_ch_layout = AV_CH_LAYOUT_STEREO;
    // 给Swrcontext 分配空间,设置公共参数
    swr_alloc_set_opts(swrContext, dst_ch_layout, dstFormat, disSampleRate,
                       src_ch_layout, srcFormat, srcSampleRate, 0, NULL
    );
    //SwrContext进行初始化
    swr_init(swrContext);

    // 获取声道数量
    outChannelCount = av_get_channel_layout_nb_channels(dst_ch_layout);
    LOGD("声道数量%d ", outChannelCount);
    // 设置音频缓冲区间 16bit   48000  PCM数据, 双声道
    out_buffer = (uint8_t *) av_malloc(2 * 48000);
    env->ReleaseStringUTFChars(srcPath, originPath);
    return 0;
}
  • 创建OpenSles的引擎
int createOpenslEngine() {
    SLresult result;
    //线程安全
    const SLEngineOption engineOptions[1] = {{(SLuint32) SL_ENGINEOPTION_THREADSAFE, (SLuint32) SL_BOOLEAN_TRUE}};
    //该函数表示:初始化引擎对象给使用者一个处理手柄对象,第四个参数(需要支持的interface数目)为零则会忽视第五、第六个参数
    result = slCreateEngine(&engineObject, 1, engineOptions, 0, NULL, NULL);
    if (result != SL_RESULT_SUCCESS) {
        LOGD("opensl es引擎创建初始化失败");
        return -1;
    }

    //该函数表示:转化一个Object从未实例化到实例化过程,第二个参数表示是否异步
    result = (*engineObject)->Realize(engineObject, SL_BOOLEAN_FALSE);
    if (result != SL_RESULT_SUCCESS) {
        LOGD("引擎Object实例化失败");
        return -1;
    }

    //该函数表示:得到由Object暴露的接口,这里指的是引擎接口,第二个参数是接口ID,第三个参数是输出的引擎接口对象
    result = (*engineObject)->GetInterface(engineObject, SL_IID_ENGINE, &engineEngine);
    if (result != SL_RESULT_SUCCESS) {
        LOGD("引擎接口获取失败");
        return -1;
    }

    //该函数表示:创建输出混音器--->由引擎接口创建,从第三个参数开始就是支持的interface数目,同样的为零忽略第四第五个参数
    const SLInterfaceID interfaceIds[1] = {SL_IID_ENVIRONMENTALREVERB};//这里给一个环境混响的接口id
    const SLboolean reqs[1] = {SL_BOOLEAN_TRUE};
    result = (*engineEngine)->CreateOutputMix(engineEngine, &outputMixObject, 1, interfaceIds,
                                              reqs);
    if (result != SL_RESULT_SUCCESS) {
        LOGD("创建输出混音器失败");
        return -1;
    }

    //同样的实例化输出混音器对象
    result = (*outputMixObject)->Realize(outputMixObject, SL_BOOLEAN_FALSE);
    if (result != SL_RESULT_SUCCESS) {
        LOGD("输出混音器outout mix实例化失败");
        return -1;
    }

    //因为环境混响接口的失败与否没关系的
    //同样的申请支持了环境混响EnvironmentalReverb接口就可以获取该接口对象
    result = (*outputMixObject)->GetInterface(outputMixObject, SL_IID_ENVIRONMENTALREVERB,
                                              &outputMixEnvironmentalReverb);
    if (result == SL_RESULT_SUCCESS) {
        result = (*outputMixEnvironmentalReverb)->SetEnvironmentalReverbProperties(
                outputMixEnvironmentalReverb, &reverbSettings);
        if (result != SL_RESULT_SUCCESS) {
            LOGD("混响属性设置失败");
        }
    } else {
        LOGD("获取环境混响接口失败");
    }

    return 0;
}
  • 创建播放器和缓冲队列,并且设置回调函数
/**
创建pcm播放格式:采样率、通道数、单个样本的比特率(s16le)
*/
int createBufferQueue(int sampleRate, int channels) {
    SLresult result;

    // configure audio source
    SLDataLocator_AndroidSimpleBufferQueue loc_bufq = {SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, 2};


    int numChannels = 2;
    SLuint32 samplesPerSec = SL_SAMPLINGRATE_48;//注意是毫秒赫兹
    SLuint32 bitsPerSample = SL_PCMSAMPLEFORMAT_FIXED_16;
    SLuint32 containerSize = SL_PCMSAMPLEFORMAT_FIXED_16;
    //引文channels=2,native-audio-jni.c中的例子是单声道的所以取SL_SPEAKER_FRONT_CENTER
    SLuint32 channelMask = SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT;
    SLuint32 endianness = SL_BYTEORDER_LITTLEENDIAN;

    numChannels = channels;

    if (channels == 1) {
        channelMask = SL_SPEAKER_FRONT_CENTER;
    } else {
        //2以及更多
        channelMask = SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT;
    }

    SLDataFormat_PCM format_pcm = {SL_DATAFORMAT_PCM, (SLuint32) numChannels, samplesPerSec,
                                   bitsPerSample, containerSize, channelMask, endianness};

    SLDataSource audioSrc = {&loc_bufq, &format_pcm};

    // configure audio sink
    SLDataLocator_OutputMix loc_outmix = {SL_DATALOCATOR_OUTPUTMIX, outputMixObject};
    SLDataSink audioSnk = {&loc_outmix, NULL};

    // create audio player
    const SLInterfaceID ids[1] = {SL_IID_BUFFERQUEUE};
    const SLboolean req[1] = {SL_BOOLEAN_TRUE};
    result = (*engineEngine)->CreateAudioPlayer(engineEngine, &bqPlayerObject, &audioSrc, &audioSnk,
                                                1, ids, req);
    if (result != SL_RESULT_SUCCESS) {
        LOGD("创建audioplayer失败");
        return -1;
    }


    result = (*bqPlayerObject)->Realize(bqPlayerObject, SL_BOOLEAN_FALSE);
    if (result != SL_RESULT_SUCCESS) {
        LOGD("实例化audioplayer失败");
        return -1;
    }

    LOGD("---createBufferQueueAudioPlayer---");

    // get the play interface
    result = (*bqPlayerObject)->GetInterface(bqPlayerObject, SL_IID_PLAY, &bqPlayerPlay);
    if (result != SL_RESULT_SUCCESS) {
        LOGD("获取play接口对象失败");
        return -1;
    }

    // get the buffer queue interface
    result = (*bqPlayerObject)->GetInterface(bqPlayerObject, SL_IID_BUFFERQUEUE,
                                             &bqPlayerBufferQueue);
    if (result != SL_RESULT_SUCCESS) {
        LOGD("获取BUFFERQUEUE接口对象失败");
        return -1;
    }

    // register callback on the buffer queue   这边的缓冲回调
    result = (*bqPlayerBufferQueue)->RegisterCallback(bqPlayerBufferQueue, bqPlayerCallback,   (void *) "1");
    if (result != SL_RESULT_SUCCESS) {
        LOGD("获取play接口对象失败");
        return -1;
    }

    // set the player's state to playing
    result = (*bqPlayerPlay)->SetPlayState(bqPlayerPlay, SL_PLAYSTATE_PLAYING);
    if (result != SL_RESULT_SUCCESS) {
        LOGD("设置为可播放状态失败");
        return -1;
    }

    return 0;
}
  • 获取解码的数据
void getPCM(void **pcm, size_t *size){
    int out_channer_nb = av_get_channel_layout_nb_channels(AV_CH_LAYOUT_STEREO);
    while (av_read_frame(avFormatContext, avPacket) >= 0) {
        if (avPacket->stream_index == streamIndex) {
            int ret = avcodec_send_packet(avCodecContext, avPacket);
            currentIndex ++;
            if (ret >= 0) {
                ret = avcodec_receive_frame(avCodecContext, avFrame);
                LOGE("解码 currentIndex = %d",currentIndex);
                // 双声道 2 * 48000
                swr_convert(swrContext, &out_buffer, 48000 * 2, (const uint8_t **) avFrame->data, avFrame->nb_samples);
                bufferSize = av_samples_get_buffer_size(NULL, out_channer_nb, avFrame->nb_samples,AV_SAMPLE_FMT_S16, 1);
                *pcm = out_buffer;
                *size = bufferSize;
            }
            break; //这边读取完一帧数据,就要break掉,不然会一直循环下去
        }
    }
}
  • 回调函数:将获取到的缓冲数据,加入队列
// 当喇叭播放完声音时回调此方法
void bqPlayerCallback(SLAndroidSimpleBufferQueueItf bq, void *context)
{

    char * args = (char *)context;
    if (strcmp(args, "1") == 0){
        LOGE("来自缓冲的回调");
    } else {
        LOGE("主动触发");
    }


    bufferLen = 0;
    //assert(NULL == context);
    getPCM(&buffer, &bufferLen);
    // for streaming playback, replace this test by logic to find and fill the next buffer
    if (NULL != buffer && 0 != bufferLen) {
        SLresult result;
        // enqueue another buffer
        result = (*bqPlayerBufferQueue)->Enqueue(bqPlayerBufferQueue, out_buffer, bufferSize);
        // the most likely other result is SL_RESULT_BUFFER_INSUFFICIENT,
        // which for this code example would indicate a programming error
        if (result != SL_RESULT_SUCCESS) {
            LOGD("入队失败");
        } else {
            LOGD("入队成功");
        }

    }
}
  • 整体的调用
JNIEXPORT void
Java_com_jason_ndk_ffmpeg_openeles_MainActivity_sound(JNIEnv *env, jobject thiz, jstring input) {

    int ret = createFFmpeg(env, input);

    if(ret < 0) {
        LOGE("创建FFmpeg失败");
        releaseResource();
        return;
    }

    //初始化opensles
    ret = createOpenslEngine();
    if (ret == JNI_FALSE) {
        LOGE("创建opensles引擎失败");
        releaseResource();
        return;
    }

    ret = createBufferQueue(avCodecContext->sample_rate, outChannelCount);
    if (ret == JNI_FALSE) {
        LOGE("创建buffer queue播放器失败");
        releaseResource();
        return;
    }

    LOGD("start av_read_frame");
    //主动调用回调函数
    bqPlayerCallback(bqPlayerBufferQueue, (void *) "0");

}

这样功能就是实现ok了

第二种方法

这边我换了一种思路,就是将解析数据加入队列的操作放在解码的循环当中去做,但是有个问题是需要,去计算每一帧播放的时间,需要手动去做休眠每一帧的播放时间,在进行下一次解码,加入队列......反复操作,来完成播放。

  • 取消掉队列的回调函数 修改上面的代码:
int createBufferQueue(int sampleRate, int channels) {
    SLresult result;

    // configure audio source
    SLDataLocator_AndroidSimpleBufferQueue loc_bufq = {SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, 2};


    int numChannels = 2;
    SLuint32 samplesPerSec = SL_SAMPLINGRATE_48;//注意是毫秒赫兹
    SLuint32 bitsPerSample = SL_PCMSAMPLEFORMAT_FIXED_16;
    SLuint32 containerSize = SL_PCMSAMPLEFORMAT_FIXED_16;
    //引文channels=2,native-audio-jni.c中的例子是单声道的所以取SL_SPEAKER_FRONT_CENTER
    SLuint32 channelMask = SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT;
    SLuint32 endianness = SL_BYTEORDER_LITTLEENDIAN;

    numChannels = channels;

    if (channels == 1) {
        channelMask = SL_SPEAKER_FRONT_CENTER;
    } else {
        //2以及更多
        channelMask = SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT;
    }

    SLDataFormat_PCM format_pcm = {SL_DATAFORMAT_PCM, (SLuint32) numChannels, samplesPerSec,
                                   bitsPerSample, containerSize, channelMask, endianness};

    SLDataSource audioSrc = {&loc_bufq, &format_pcm};

    // configure audio sink
    SLDataLocator_OutputMix loc_outmix = {SL_DATALOCATOR_OUTPUTMIX, outputMixObject};
    SLDataSink audioSnk = {&loc_outmix, NULL};

    // create audio player
    const SLInterfaceID ids[1] = {SL_IID_BUFFERQUEUE};
    const SLboolean req[1] = {SL_BOOLEAN_TRUE};
    result = (*engineEngine)->CreateAudioPlayer(engineEngine, &bqPlayerObject, &audioSrc, &audioSnk,
                                                1, ids, req);
    if (result != SL_RESULT_SUCCESS) {
        LOGD("创建audioplayer失败");
        return JNI_FALSE;
    }


    result = (*bqPlayerObject)->Realize(bqPlayerObject, SL_BOOLEAN_FALSE);
    if (result != SL_RESULT_SUCCESS) {
        LOGD("实例化audioplayer失败");
        return JNI_FALSE;
    }

    LOGD("---createBufferQueueAudioPlayer---");

    // get the play interface
    result = (*bqPlayerObject)->GetInterface(bqPlayerObject, SL_IID_PLAY, &bqPlayerPlay);
    if (result != SL_RESULT_SUCCESS) {
        LOGD("获取play接口对象失败");
        return JNI_FALSE;
    }

    // get the buffer queue interface
    result = (*bqPlayerObject)->GetInterface(bqPlayerObject, SL_IID_BUFFERQUEUE,
                                             &bqPlayerBufferQueue);
    if (result != SL_RESULT_SUCCESS) {
        LOGD("获取BUFFERQUEUE接口对象失败");
        return JNI_FALSE;
    }

    // register callback on the buffer queue   不要注册缓冲的回调  唯一的区别
    /*result = (*bqPlayerBufferQueue)->RegisterCallback(bqPlayerBufferQueue, bqPlayerCallback,
                                                      (void *) "1");
    if (result != SL_RESULT_SUCCESS) {
        LOGD("获取play接口对象失败");
        return JNI_FALSE;
    }*/

    // set the player's state to playing
    result = (*bqPlayerPlay)->SetPlayState(bqPlayerPlay, SL_PLAYSTATE_PLAYING);
    if (result != SL_RESULT_SUCCESS) {
        LOGD("设置为可播放状态失败");
        return JNI_FALSE;
    }

    return JNI_TRUE;
}
  • 队列回调 将getPCM的代码放到回调队列中
void bqPlayerCallback(SLAndroidSimpleBufferQueueItf bq, void *context) {
    char * args = (char *)context;
    if (strcmp(args, "1") == 0){
        LOGE("来自缓冲的回调");
        return;
    }
    LOGE("主动触发");

    while (av_read_frame(avFormatContext, avPacket) >= 0) {
        if (avPacket->stream_index == streamIndex) {
            int ret = avcodec_send_packet(avCodecContext, avPacket);
            currentIndex ++;
            while (ret >= 0) {
                ret = avcodec_receive_frame(avCodecContext, avFrame);
                swr_convert(swrContext, &out_buffer, 2 * 48000,
                            (const uint8_t **) avFrame->data, avFrame->nb_samples);

                //该函数表示:通过给定的参数得到需要的buffer size
                int dst_buffer_size = av_samples_get_buffer_size(NULL, outChannelCount, avFrame->nb_samples, AV_SAMPLE_FMT_S16, 1);
                if (dst_buffer_size <= 0) {
                    break;
                }

                //MP3每帧是1152字节,ACC每帧是1024/2048字节
                LOGD("WRITE TO AUDIOTRACK %d", dst_buffer_size);//4608

                SLresult result;
                result = (*bqPlayerBufferQueue)->Enqueue(bqPlayerBufferQueue, out_buffer, dst_buffer_size);
                if (result != SL_RESULT_SUCCESS) {
                    LOGD("入队失败");
                }
                //计算公式 acc  1024 * 1000 / 48000 = 21.34
                //计算公式 mp3  1152 * 1000 / 48000 = 24
                usleep(1000 * 24);
            }
            LOGE("正在解码%d", currentIndex++);
        }
    }

}

不过从播放的效果来看,个人比较建议用第一种方式,利用opensles的缓冲回调函数来加载每一帧数据,不需要去判断每一帧的播放时长。这样播放的音频文件就不会有问题。

结语

以上就是个人利用FFmpeg+OPensles 播放音频文件。如果有错误欢迎指正。

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Android 通过DecorView计算statusBar、navigationBar的高度

    近期在做项目的时候碰到了底部虚拟按键在各个厂商适配的问题,闷逼了一圈,后面搜索一圈,发现即使各大厂商有变动,还是离不开原生本质

    包子388321
  • Android studio项目引用三方aar出现,打包出来的app_name本替换掉了

    最近接入三方渠道商的SDK出现打包出来的apk包,在手机安装后桌面显示的名称被强制成别的名称了

    包子388321
  • unity Android 交互的常见问题总结

    1、unity2017版本,采用aar打包:出现A library uses the same package as this project:的错误 原因:...

    包子388321
  • NDK--利用OpenSL ES实现播放FFmpeg解码后的音频流

    1、创建引擎接口对象 2、创建混音器 3、创建播放器(录音器) 4、设置缓冲队列和回调函数 5、设置播放状态 6、启动回调函数

    aruba
  • Mybatis之动态sql

    爱撒谎的男孩
  • MBean与JMX源码分析

    JMX(java Management Exetensions)在Java编程语言中定义了应用程序以及网络管理和监控的体系结构、设计模式、应用程序接口以及服务。...

    歪歪梯
  • 良好的代码格式反映了程序员的编码能力,好的程序员应该这么编码

    大括号的使用约定。如果是大括号内为空,则简洁地写成{}即可,不需要换行;如果 是非空代码块则:

    一墨编程学习
  • 侃侃单片机的裸奔程序的框架

    任何对时间要求苛刻的需求都是我们的敌人,在必要的时候我们只有增加硬件成本来消灭它;比如你要8个数码管来显示,我们在没有相关的硬件支持的时候必须用MCU以动态扫描...

    morixinguan
  • JDK 的 3 个 bug 啊!

    如果两个变量中间隔了比较长的其它代码,很可能会导致开发人员将两者混淆,导致逻辑认知错误,从而写出或改出有问题的代码。

    Java技术栈
  • 说说JDK 的3个BUG

    如果两个变量中间隔了比较长的其它代码,很可能会导致开发人员将两者混淆,导致逻辑认知错误,从而写出或改出有问题的代码。

    用户1516716

扫码关注云+社区

领取腾讯云代金券