(五) FFMpeg音频重采样和视频格式转换和显示

#include "hjcommon.hpp"
#include <android/native_window.h>
#include <android/native_window_jni.h>

static jobject obj_audiotrack; // AudioTrack 对象,全局引用
static jmethodID mid_write; // jmethodID 不用做成全局引用也能跨线程使用
static jmethodID mid_release; // AudioTrack 释放函数
static void initAudioTrack(JNIEnv *env, jobject instance)
{
    jclass clz_mp6 = env->GetObjectClass(instance);
    jmethodID mid_create = env->GetMethodID(clz_mp6, "createAudioTrack", "(II)Landroid/media/AudioTrack;"); // 获取创建AudioTrack实例的java函数
    jobject obj_aud = env->CallObjectMethod(instance, mid_create, 44100, 2); // 传参 44100, 2 是因为我事先知道其采样率与双通道
    jclass clz_aud = env->GetObjectClass(obj_aud);

    jmethodID mid_play = env->GetMethodID(clz_aud, "play", "()V"); // 获取AudioTrack的play函数
    env->CallVoidMethod(obj_aud, mid_play); // 调用play函数
    mid_write = env->GetMethodID(clz_aud, "write", "([BII)I"); // 获取AudioTrack的write函数
    mid_release = env->GetMethodID(clz_aud, "release", "()V");

    obj_audiotrack = env->NewGlobalRef(obj_aud);
}
static void audiotrack_write(JNIEnv *env, uint8_t *out_buffer, int out_buffer_size) // 往 AudioTrack 中写入数据,播放音频
{
    //out_buffer缓冲区数据,转成byte数组
    jbyteArray audio_sample_array = env->NewByteArray(out_buffer_size);
    jbyte* sample_bytep = env->GetByteArrayElements(audio_sample_array, NULL);
    //out_buffer的数据复制到sampe_bytep
    memcpy(sample_bytep, out_buffer, out_buffer_size);
    //同步
    env->ReleaseByteArrayElements(audio_sample_array, sample_bytep, 0);

    //AudioTrack.write PCM数据
    env->CallIntMethod(obj_audiotrack, mid_write, audio_sample_array, 0, out_buffer_size);
    //释放局部引用
    env->DeleteLocalRef(audio_sample_array);
}

JNIEXPORT void JNICALL Java_hankin_hjmedia_mpeg_some_Mp6_11Activity_resample(JNIEnv *env, jobject instance, jstring url_, jobject surface)
{
    const char * path = env->GetStringUTFChars(url_, NULL);
    if (path[0]=='\0')
    {
        LOGE("path is empty.");
        return;
    }

    initAudioTrack(env, instance); // 初始化 AudioTrack

//    av_register_all();
    int netInit = avformat_network_init();
    if (netInit!=0) LOGW("avformat_network_init is failed.");
//    avcodec_register_all();

    AVFormatContext * formatContext = NULL;
    int openRet = avformat_open_input(&formatContext, path, NULL, NULL);
    if (openRet!=0 || formatContext==NULL)
    {
        LOGE("avformat_open_input is failed.");
        return;
    }
    env->ReleaseStringUTFChars(url_, path);
    if (formatContext->duration<=0) avformat_find_stream_info(formatContext, NULL);
    LOGD("formatContext->duration=%lld, formatContext->bit_rate=%lld", formatContext->duration, formatContext->bit_rate);
    int videoStream = av_find_best_stream(formatContext, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0);
    int audioStream = av_find_best_stream(formatContext, AVMEDIA_TYPE_AUDIO, -1, -1, NULL, 0);
    if (videoStream==AVERROR_STREAM_NOT_FOUND || audioStream==AVERROR_STREAM_NOT_FOUND)
    {
        LOGE("video stream or audio stream not found.");
        return;
    }
    LOGD("vidoe->width=%d, video->height=%d, audio->sample_rate=%d", formatContext->streams[videoStream]->codecpar->width,
         formatContext->streams[videoStream]->codecpar->height, formatContext->streams[audioStream]->codecpar->sample_rate);

    AVCodecContext * videoContext = avcodec_alloc_context3(NULL);
    int gvRet = hjgetAVDecoder6_1(videoContext, formatContext->streams[videoStream]->codecpar, true, false);
    if (gvRet!=0) return;
    AVCodecContext * audioContext = avcodec_alloc_context3(NULL);
    int gaRet = hjgetAVDecoder6_1(audioContext, formatContext->streams[audioStream]->codecpar, false);
    if (gaRet!=0) return;

    AVPacket * pkt = av_packet_alloc();
    AVFrame * frame = av_frame_alloc();
    long long start = hjgetNowMs();
    int frameCount = 0;

    // 初始化视频像素格式转换的上下文
    SwsContext * swsContext = NULL;
    int outWidth = 1280; // 转换后的宽高
    int outHeight = 720;
    unsigned char * rgba = new unsigned char[1920*1080*4]; // 创建缓存,用做像素格式转换
    // 音频解码出来后时无法直接播放的,需要重采样
    SwrContext * swrContext = swr_alloc(); // 创建音频重采样上下文
    /*
        struct SwrContext *swr_alloc_set_opts( // 设置音频重采样上下文参数
            struct SwrContext *s, // 上下文
            int64_t out_ch_layout, // 输出的声道
            enum AVSampleFormat out_sample_fmt, // 输出的样本格式
            int out_sample_rate, // 输出的样本率,要改变播放速度可以通过改变此输出的样本率来改变,不过可能会失帧
            int64_t  in_ch_layout, // 输入的声道
            enum AVSampleFormat in_sample_fmt, // 输入的样本格式
            int  in_sample_rate, // 输入的样本率
            int log_offset, // 日志,传0
            void *log_ctx // 日志,传0
        );
     */ // av_get_default_channel_layout 根据给的声道数返回默认的channel layout  ,固定让输出2声道   AV_SAMPLE_FMT_S16 样本格式
    swr_alloc_set_opts(swrContext, av_get_default_channel_layout(2), AV_SAMPLE_FMT_S16, audioContext->sample_rate,
                       av_get_default_channel_layout(audioContext->channels), audioContext->sample_fmt, audioContext->sample_rate, 0, 0);
    int swrRet = swr_init(swrContext); // 初始化,返回 0 ok
    if (swrRet!=0)
    {
        LOGE("swr_init is failed : %s", av_err2str(swrRet));
        return;
    }
    unsigned char * pcm = new unsigned char[48000*4*2]; // 重采样时样本数量的缓存,设大点无所谓,但是不要小了
    // NDK中使用java传递进来的surface创建native窗口,同其他对象一样,记得用完后调用ANativeWindow_release()将引用计数减1 。 代码在 libandroid.so 中
    ANativeWindow * nwin = ANativeWindow_fromSurface(env, surface);
    if (nwin==NULL)
    {
        LOGE("ANativeWindow_fromSurface failed.");
        return;
    }
    // 设置窗口的大小、格式,要与视频转换后的像素格式匹配,显示时,native窗口会自动拉伸铺满SurfaceView的大小。 return 0 for success
    int wRet = ANativeWindow_setBuffersGeometry(nwin, outWidth, outHeight, WINDOW_FORMAT_RGBA_8888);
    if (wRet!=0)
    {
        LOGE("ANativeWindow_setBuffersGeometry is failed.");
        return;
    }
    ANativeWindow_Buffer wbuf; // surface的双缓冲,内存与显卡内存交换内存的地方

    bool isGo = true;
    while (isGo)
    {
        if (hjgetNowMs()-start >= 3000)
        {
            LOGW("3秒内平均每秒解码视频帧数 : %d", frameCount/3);
            start = hjgetNowMs();
            frameCount = 0;
        }

        int pRet = av_read_frame(formatContext, pkt);
        if (pRet!=0)
        {
            LOGI("av_read_frame end.");
            break;
        }
        LOGV("packet stream_index=%d, pts=%lld, dts=%lld, size=%d", pkt->stream_index, pkt->pts, pkt->dts, pkt->size);

        AVCodecContext * cc = pkt->stream_index==videoStream ? videoContext : audioContext;
        int spRet = avcodec_send_packet(cc, pkt);
        if (cc==videoContext) LOGV("video avcodec_send_packet=%d", spRet); else LOGV("audio avcodec_send_packet=%d", spRet);
        bool is = true;
        while (is)
        {
            int rpRet = avcodec_receive_frame(cc, frame);
            // Nexus手机上使用硬解码,解出的视频的格式为 AV_PIX_FMT_NV12 ,不同解码方式,不同设备上最后解出的格式可能不一样
            if (rpRet==0) if (cc==videoContext) LOGV("video frame pts=%lld, format=%d", frame->pts, frame->format); else LOGV("audio frame pts=%lld, format=%d", frame->pts, frame->format);
            else is = false;
            if (cc==videoContext && rpRet==0)
            {
                frameCount++;

                /*
                    struct SwsContext *sws_getContext( // 视频格式尺寸转换上下文,第一次调用时会有开销,后面就没了,多路视频格式尺寸转换时建议使用
                        int srcW,
                        int srcH,
                        enum AVPixelFormat srcFormat,
                        int dstW,
                        int dstH,
                        enum AVPixelFormat dstFormat,
                        int flags,
                        SwsFilter *srcFilter,
                        SwsFilter *dstFilter,
                        const double *param
                    );
                    struct SwsContext *sws_getCachedContext( // 与上面函数功能相同,就参数有一个差别,单个视频格式尺寸转换时建议使用
                        struct SwsContext *context, // 可以传NULL,返回为转换后的SwsContext,如果传非NULL时,如果后面要转换的参数与context的不匹配,那么context会被释放内存,然后重新创建一个符合参数的SwsContext返回
                                                                                                             如果后面要转换的参数与context一致,那么直接返回context,此函数是线程不安全的
                        int srcW, // 原宽
                        int srcH, // 原高
                        enum AVPixelFormat srcFormat, // 原像素格式
                        int dstW, // 目标宽
                        int dstH, // 目标高
                        enum AVPixelFormat dstFormat, // 目标像素格式
                        int flags, // specify which algorithm and options to use for rescaling
                                            SWS_FAST_BILINEAR		1
                                            SWS_BILINEAR			2
                                            SWS_BICUBIC				4
                                            SWS_X					8
                                            SWS_POINT				0x10
                                            SWS_AREA				0x20
                                            SWS_BICUBLIN			0x40
                        SwsFilter *srcFilter, // 过滤器,可以传NULL
                        SwsFilter *dstFilter, // 过滤器,可以传NULL
                        const double *param // 与参数 flags 算法相关,可以传NULL
                    );
                 */
                swsContext = sws_getCachedContext(swsContext, frame->width, frame->height, (AVPixelFormat) frame->format,
                                                  outWidth, outHeight, AV_PIX_FMT_RGBA,
                                                  SWS_FAST_BILINEAR, NULL, NULL, NULL);
                if (swsContext==NULL) LOGW("sws_getCachedContext failed.");
                else
                {
                    uint8_t * data[AV_NUM_DATA_POINTERS] = {0}; // AV_NUM_DATA_POINTERS 对应 AVFrame中data的长度
                    data[0] = rgba;
                    int lines[AV_NUM_DATA_POINTERS] = {0}; // AV_NUM_DATA_POINTERS 对应 AVFrame中linesize的长度
                    lines[0] = outWidth * 4;
                    /*
                        int sws_scale( // 每帧数据的处理(像素格式转换,尺寸转换),返回 the height of the output slice ,为0的话表示失败
                            struct SwsContext *c, // 像素格式尺寸转换上下文
                            const uint8_t *const srcSlice[], // 具体数据的数组,指针的数据,数据的长度由 enum AVPixelFormat srcFormat 参数决定,比如YUV、RGB交叉存放,没有平面存放的
                            const int srcStride[], // 对应 srcSlice 参数,对应 AVFrame 中的 linesize ,即一行数据的长度
                            int srcSliceY, // 深度 用不到,传0
                            int srcSliceH, // 原高度
                            uint8_t *const dst[], // 目标数据保存的地址
                            const int dstStride[] // 对应目标数据的linesize,即目标一行数据的长度
                        );
                     */
                    int height = sws_scale(swsContext, (const uint8_t *const *) frame->data, frame->linesize, 0, frame->height, data, lines);
                    LOGV("视频像素格式转换 sws_scale height=%d", height); // sws_scale height=720   即outHeight的值
                    if (height>0) // 显示视频
                    {
                        int lockRet = ANativeWindow_lock(nwin, &wbuf, 0); // 锁住窗口,并将绘制窗口所需的数据的内存地址设置到wbuf.bits中, return 0 for success
                        if (lockRet!=0)
                        {
                            LOGW("surface窗口关闭了,解码结束.");
                            isGo = false;
                        }
                        uint8_t * dst = (uint8_t *) wbuf.bits; // 内存与显卡内存交换内存的地方
                        memcpy(dst, rgba, outWidth*outHeight*4); // 将rgba内存拷贝到dst
                        int unRet = ANativeWindow_unlockAndPost(nwin); // 解锁窗口,并post到主线程显示,return 0 for success
                    }
                }
            }
            else if (cc==audioContext && rpRet==0)
            {
                uint8_t * out[2] = {0}; // 因为在上面设置重采样参数时设置了输出声道数固定为2,所以这里数组长度是2? 下标1的只是表示数组结尾NULL?
                out[0] = pcm;
                /*
                    int swr_convert( // 将一帧帧的音频转换为重采样,return number of samples output per channel, negative value on error
                        struct SwrContext *s, // 上下文
                        uint8_t **out, // 输出的数据
                        int out_count, // 输出的单通道的样本数量
                        const uint8_t **in, // 输入的数据
                        int in_count // 输入的单通道的样本数量,对应AVFrame中的nb_samples
                    );
                 */
                int len = swr_convert(swrContext, out, frame->nb_samples, (const uint8_t **) frame->data, frame->nb_samples);
                LOGV("音频重采样 swr_convert len=%d, frame->nb_samples=%d", len, frame->nb_samples); // swr_convert len=1024, frame->nb_samples=1024

                // 通过 AudioTrack 播放pcm音频
                int out_buffer_size = av_samples_get_buffer_size(NULL, 2, frame->nb_samples, (AVSampleFormat) frame->format, 1); // 根据传入的参数计算每帧音频大小
                audiotrack_write(env, out[0], out_buffer_size); // 在同一个线程中即使没做音视频同步处理,音视频也是同步的,但是只一个线程的话,音视频会卡顿
            }
            av_frame_unref(frame);
        }

        av_packet_unref(pkt);
    }
    ANativeWindow_release(nwin); // 释放窗口,java的surface引用计数会减1。 如果释放函数传的只是一级指针那么一般需要手动将指针置空,防止野指针
    nwin = NULL;
    delete [] rgba;
    sws_freeContext(swsContext); // 释放内存,调用此函数后最好手动将 swsContext置空,防止野指针
    swsContext = NULL;
    delete [] pcm;
    swr_free(&swrContext); // 释放内存

    av_packet_free(&pkt);
    av_frame_free(&frame);
    avcodec_close(videoContext);
    avcodec_free_context(&videoContext);
    avcodec_close(audioContext);
    avcodec_free_context(&audioContext);
    avformat_close_input(&formatContext);

    /*
        AudioTrack 主要函数:

            开始播放
            public void play()throws IllegalStateException{}

            停止播放音频数据,如果是STREAM模式,会等播放完最后写入buffer的数据才会停止。如果立即停止,要调用pause()方法,然后调用flush方法,会舍弃还没有播放的数据
            public void stop()throws IllegalStateException{}

            暂停播放,调用play()重新开始播放
            public void pause()throws IllegalStateException {}

            只在模式为STREAM下可用。将音频数据刷进等待播放的队列,任何写入的数据如果没有提交的话,都会被舍弃,但是并不能保证所有用于数据的缓冲空间都可用于后续的写入。
            public void flush() {}

            释放本地AudioTrack资源
            public void release() {}

            返回当前的播放状态
            public int getPlayState() {}
     */
    env->CallVoidMethod(obj_audiotrack, mid_release); // 释放 AudioTrack
    env->DeleteGlobalRef(obj_audiotrack); // 删除全局引用
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值