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 层播放视频的,至于播放到哪里,Java 层是不知道的,所以得利用 C 调用 Java 代码来实现。
Java 代码
/**
* 同步播放音视频
* @param path
* @param surface
* @param callback
*/
public native void play(String path, Surface surface, PlayerCallback callback);
/**
* 播放器回调
*/
public interface PlayerCallback {
/**
* 播放开始
*/
void onStart();
/**
* 进度
* @param total
* @param current
*/
void onProgress(int total, int current);
/**
* 播放结束
*/
void onEnd();
}
声明一个回调接口,通过 native 方法传入到 C 层。
public void play(View view) {
player.play(videoPath, surfaceHolder.getSurface(), new Player.PlayerCallback() {
@Override
public void onStart() {
System.err.println("播放开始了 -------------------");
}
@Override
public void onProgress(final int total, final int current) {
// C 层子线程调用 子线程不能直接更新 UI
runOnUiThread(new Runnable() {
@Override
public void run() {
currentTimeView.setText(formatTime(current));
totalTimeView.setText(formatTime(total));
}
});
}
@Override
public void onEnd() {
System.err.println("播放结束了 -------------------");
}
});
}
这是 Java 层播放代码,需要注意的是,回调 onProgress 函数是在 C 层子线程调用的,大家都知道,子线程不能直接更新 UI ,所以要特别注意一下!
OK,我们再来看 C 层代码!
C 代码
/**
* 回调 Java Callback onStart方法
* @param player
*/
void call_on_start(Player *player, JNIEnv *env) {
jclass callback_class = env->GetObjectClass(player->callback);
jmethodID on_start_method_id = env->GetMethodID(callback_class, "onStart", "()V");
env->CallVoidMethod(player->callback, on_start_method_id);
env->DeleteLocalRef(callback_class);
}
/**
* 回调 Java Callback onStart方法
* @param player
*/
void call_on_end(Player *player, JNIEnv *env) {
jclass callback_class = env->GetObjectClass(player->callback);
jmethodID on_end_method_id = env->GetMethodID(callback_class, "onEnd", "()V");
env->CallVoidMethod(player->callback, on_end_method_id);
env->DeleteLocalRef(callback_class);
}
/**
* 回调 Java Callback onStart方法
* @param player
* @param env
* @param total
* @param current
*/
void call_on_progress(Player *player, JNIEnv *env, double total, double current) {
jclass callback_class = env->GetObjectClass(player->callback);
jmethodID on_progress_method_id = env->GetMethodID(callback_class, "onProgress", "(II)V");
env->CallVoidMethod(player->callback, on_progress_method_id, (int) total, (int) current);
env->DeleteLocalRef(callback_class);
}
/**
* 消费函数
* 从队列获取解码数据 同步播放
* @param arg
* @return
*/
void* consume(void* arg) {
...
if (index == player->audio_stream_index) {
// 播放开始
call_on_start(player, env);
}
double total = stream->duration * av_q2d(stream->time_base);
AVFrame *frame = av_frame_alloc();
for (;;) {
...
if (index == player->video_stream_index) {
...
} else if (index == player->audio_stream_index) {
...
// 更新进度
call_on_progress(player, env, total, player->audio_clock);
}
av_packet_free(&packet);
}
if (index == player->audio_stream_index) {
// 播放结束
call_on_end(player, env);
}
...
return NULL;
}
在测试过程中,遇到 ReferenceTable overflow (max=512) 这个Bug,是由于 JNI 局部引用表溢出导致的!然后我查了一下,发现是因为我多次调用 call_on_progress 方法导致的,以上代码是我修复之后的代码,之前的代码是这样的:
void call_on_progress(Player *player, JNIEnv *env, double total, double current) {
jclass callback_class = env->GetObjectClass(player->callback);
jmethodID on_progress_method_id = env->GetMethodID(callback_class, "onProgress", "(II)V");
env->CallVoidMethod(player->callback, on_progress_method_id, (int) total, (int) current);
// 调用 GetObjectClass 方法后,没有及时回收局部引用,会持续存在局部引用表
// 由于多次调用 最终会导致 ReferenceTable overflow
// env->DeleteLocalRef(callback_class);
}
代码注释得很清楚了!
其实还有一些局部引用需要我们 Delete 的,对于 GetObjectClass 返回的一定需要调用DeleteLocalRef,还有 jbyteArray 类型的变量需要 DeleteLocalRef,还有 NewString/ NewStringUTF/NewObject/ GetObjectField 这些都需要 DeleteLocalRef!如果再遇到 ReferenceTable overflow 局部引用表溢出,就需要检查一下局部变量问题。
代码地址:https://github.com/JohanMan/Player