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 JNICALL
Java_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 JNICALL
Java_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