Android FFmpeg系列——0 编译.so库
Android FFmpeg系列——1 播放视频
Android FFmpeg系列——2 播放音频
Android FFmpeg系列——3 C多线程使用
Android FFmpeg系列——4 子线程播放音视频
Android FFmpeg系列——5 音视频同步播放
Android FFmpeg系列——6 Java 获取播放进度
Android FFmpeg系列——7 实现快进/快退功能

利用工作闲余时间,终于实现在子线程播放音视频!

上一接学习了在 C 使用多线程,接着就是利用 C 多线程同时播放音视频(暂时还不同步)。

不多说,直接上码。

代码

// C 层播放器结构体typedef struct _Player {    // Env    JavaVM *java_vm;    // Java 实例    jobject instance;    jobject surface;    // 上下文    AVFormatContext *format_context;    // 视频相关    int video_stream_index;    AVCodecContext *video_codec_context;    ANativeWindow *native_window;    ANativeWindow_Buffer window_buffer;    uint8_t *video_out_buffer;    struct SwsContext *sws_context;    AVFrame *rgba_frame;    Queue *video_queue;    // 音频相关    int audio_stream_index;    AVCodecContext *audio_codec_context;    uint8_t *audio_out_buffer;    struct SwrContext *swr_context;    int out_channels;    jmethodID play_audio_track_method_id;    Queue *audio_queue;} Player;

这里主要把音频和视频解码之后播放需要的东西做一下封装,需要注意的是2个队列:音频压缩数据队列和视频压缩数据队列。

初始化播放器:

/** * 初始化播放器 * @param player */void player_init(Player **player, JNIEnv *env, jobject instance, jobject surface) {    *player = (Player*) malloc(sizeof(Player));    JavaVM* java_vm;    env->GetJavaVM(&java_vm);    (*player)->java_vm = java_vm;    (*player)->instance = env->NewGlobalRef(instance);    (*player)->surface = env->NewGlobalRef(surface);}

我们传入的 Player 的二级指针,主要是为了给 player 分配内存。不懂的话,可以参考这篇博文 真正明白C语言二级指针。

这里我们还需要注意一个知识点,因为 instance 和 surface 这两个变量是 jni 方法的参数:

JNIEXPORT void JNICALLJava_com_johan_player_Player_play(JNIEnv *env, jobject instance, jstring path_, jobject surface){...}

由于我们要在 C 中的子线程使用这个两个变量,所以得使用 env->NewGlobalRef 提升为全局变量。

初始化 AVFormat:

/** * 初始化 AVFormat * @return */int format_init(Player *player, const char* path) {    ...}

(代码不详细贴,要不然篇幅太长,在最后会给出代码地址!!!)

查看和打开解码器:

/** * 查找流 index * @param player * @param type * @return */int find_stream_index(Player *player, AVMediaType type) {    ...}/** * 初始化解码器 * @param player * @param type * @return */int codec_init(Player *player, AVMediaType type) {    ...}

准备播放音频和视频:

/** * 播放视频准备 * @param player */int video_prepare(Player *player, JNIEnv *env) {   ...}/** * 播放音频准备 * @param player * @return */int audio_prepare(Player *player, JNIEnv* env) {    ...}

播放音频和视频:

/** * 视频播放 * @param frame */void video_play(Player* player, AVFrame *frame, JNIEnv *env) {    ...}/** * 音频播放 * @param frame */void audio_play(Player* player, AVFrame *frame, JNIEnv *env) {    ...}

释放播放器:

/** * 释放播放器 * @param player */void player_release(Player* player) {    avformat_close_input(&(player->format_context));    av_free(player->video_out_buffer);    av_free(player->audio_out_buffer);    avcodec_close(player->video_codec_context);    ANativeWindow_release(player->native_window);    sws_freeContext(player->sws_context);    av_frame_free(&(player->rgba_frame));    avcodec_close(player->audio_codec_context);    swr_free(&(player->swr_context));    queue_destroy(player->video_queue);    queue_destroy(player->audio_queue);    player->instance = NULL;    JNIEnv *env;    int result = player->java_vm->AttachCurrentThread(&env, NULL);    if (result != JNI_OK) {        return;    }    env->DeleteGlobalRef(player->instance);    env->DeleteGlobalRef(player->surface);    player->java_vm->DetachCurrentThread();}

也就是释放各种资源,这里注意了,我们之前为 instance 和 surface 提升为全局变量,在释放时要 delete 掉。

前面的都是铺垫,接下来才是今天的正题,我们来看看 jni 方法的实现:

/** * 同步播放音视频 */extern "C"JNIEXPORT void JNICALLJava_com_johan_player_Player_play(JNIEnv *env, jobject instance, jstring path_, jobject surface) {    const char *path = env->GetStringUTFChars(path_, 0);    int result = 1;    Player* player;    player_init(&player, env, instance, surface);    if (result > 0) {        result = format_init(player, path);    }    if (result > 0) {        result = codec_init(player, AVMEDIA_TYPE_VIDEO);    }    if (result > 0) {        result = codec_init(player, AVMEDIA_TYPE_AUDIO);    }    if (result > 0) {        play_start(player);    }    env->ReleaseStringUTFChars(path_, path);}

最后调用了 play_start :

/** * 开始播放 * @param player */void play_start(Player *player) {    player->video_queue = (Queue*) malloc(sizeof(Queue));    player->audio_queue = (Queue*) malloc(sizeof(Queue));    queue_init(player->video_queue);    queue_init(player->audio_queue);    thread_init(player);}

初始化2个队列,然后初始化并启动子线程:

/** *  初始化线程 */void thread_init(Player* player) {    pthread_create(&produce_id, NULL, produce, player);    Consumer* video_consumer = (Consumer*) malloc(sizeof(Consumer));    video_consumer->player = player;    video_consumer->stream_index = player->video_stream_index;    pthread_create(&video_consume_id, NULL, consume, video_consumer);    Consumer* audio_consumer = (Consumer*) malloc(sizeof(Consumer));    audio_consumer->player = player;    audio_consumer->stream_index = player->audio_stream_index;    pthread_create(&audio_consume_id, NULL, consume, audio_consumer);}

生产函数:

/** * 生产函数 * 循环读取帧 解码 丢到对应的队列中 * @param arg * @return */void* produce(void* arg) {    Player *player = (Player*) arg;    AVPacket *packet = av_packet_alloc();    while (av_read_frame(player->format_context, packet) >= 0) {        if (packet->stream_index == player->video_stream_index) {            queue_in(player->video_queue, packet);        } else if (packet->stream_index == player->audio_stream_index) {            queue_in(player->audio_queue, packet);        }        packet = av_packet_alloc();    }    break_block(player->video_queue);    break_block(player->audio_queue);    for (;;) {        if (queue_is_empty(player->video_queue) && queue_is_empty(player->audio_queue)) {            break;        }        sleep(1);    }    LOGE("produce finish ------------------- ");    player_release(player);    return NULL;}

很简单,循环读取没一帧压缩数据,然后放到队列中。

消费函数:

/** * 消费函数 * 从队列获取解码数据 同步播放 * @param arg * @return */void* consume(void* arg) {    Consumer *consumer = (Consumer*) arg;    Player *player = consumer->player;    int index = consumer->stream_index;    JNIEnv *env;    int result = player->java_vm->AttachCurrentThread(&env, NULL);    if (result != JNI_OK) {        LOGE("Player Error : Can not get current thread env");        pthread_exit(NULL);        return NULL;    }    AVCodecContext *codec_context;    Queue *queue;    if (index == player->video_stream_index) {        codec_context = player->video_codec_context;        queue = player->video_queue;        video_prepare(player, env);    } else if (index == player->audio_stream_index) {        codec_context = player->audio_codec_context;        queue = player->audio_queue;        audio_prepare(player, env);    }    AVFrame *frame = av_frame_alloc();    for (;;) {        AVPacket *packet = queue_out(queue);        if (packet == NULL) {            break;        }        result = avcodec_send_packet(codec_context, packet);        if (result < 0 && result != AVERROR(EAGAIN) && result != AVERROR_EOF) {            print_error(result);            LOGE("Player Error : %d codec step 1 fail", index);            av_packet_free(&packet);            continue;        }        result = avcodec_receive_frame(codec_context, frame);        if (result < 0 && result != AVERROR_EOF) {            print_error(result);            LOGE("Player Error : %d codec step 2 fail", index);            av_packet_free(&packet);            continue;        }        if (index == player->video_stream_index) {            video_play(player, frame, env);        } else if (index == player->audio_stream_index) {            audio_play(player, frame, env);        }        av_packet_free(&packet);    }    player->java_vm->DetachCurrentThread();    LOGE("consume is finish ------------------------------ ");    return NULL;}

消费函数就是从队列中获取压缩数据,然后解码,播放!

虽然看起来很简单,但是我在写的过程中发现很多问题,比如解码出错呀,数据转换失败呀,一大堆,过程挺心累的,但是发现主要的问题是我写的队列有问题,控制不好,当然最主要的原因是对 C 不熟导致的!

最后的解决办法是,把线程锁和条件变量都封装到每一个队列里面,然后再 queue_in 和 queue_out 里面做控制,发现这样简单多了!!

就这样,音频和视频就能同时在子线程播放了,但是还不同步,下面学习怎么同步播放音视频。

代码地址:https://github.com/JohanMan/Player

参考资料

真正明白C语言二级指针
c++ 子线程里面调用 Android 代码

更多相关文章

  1. Android(安卓)Timer 分析
  2. Android(安卓)httpGet 使用 以及使用Handler异步更新textview的t
  3. Android图片动画播放
  4. 4.0 以上插耳机让音乐在耳机播放,屏蔽掉喇叭播放
  5. The RK3066/RK30SDK Android(安卓)4.2 audio codec has a bug!
  6. Android(安卓)FFmpeg系列——7 实现快进/快退功能
  7. tabhost中setup()和setup(LocalActivityManager activityGroup)
  8. OpenGL播放yuv数据流(着色器SHADER)-android(一)
  9. okhttp源码分析

随机推荐

  1. Android 中文 API (101) —— AsyncTask
  2. 布局管理器
  3. android 判断sdcard是否存在,以及写入权限
  4. Android(安卓)2.0中电话本contact的读写
  5. Android倒计时功能的实现
  6. android程序编写的小问题
  7. Android系统基础介绍
  8. 高亮的关键字
  9. [Android M] Bluedroid修改蓝牙默认名称
  10. Android访问网络