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

参考资料

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

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值