Android(安卓)FFmpeg系列——4 子线程播放音视频
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 代码
更多相关文章
- Android(安卓)Timer 分析
- Android(安卓)httpGet 使用 以及使用Handler异步更新textview的t
- Android图片动画播放
- 4.0 以上插耳机让音乐在耳机播放,屏蔽掉喇叭播放
- The RK3066/RK30SDK Android(安卓)4.2 audio codec has a bug!
- Android(安卓)FFmpeg系列——7 实现快进/快退功能
- tabhost中setup()和setup(LocalActivityManager activityGroup)
- OpenGL播放yuv数据流(着色器SHADER)-android(一)
- okhttp源码分析