NDK前期基础知识终于学完了,现在开始进入项目实战学习,通过FFmpeg实现一个简单的音视频播放器。
音视频一二三四五节已经实现了音视频的播放器功能,本节主要是对音视频播放器增加拖动条功能,以及项目的一些释放工作。
本节内容如下:
1.增加拖动条。
2.项目释放工作。
用到的ffmpeg、rtmp等库资源:
https://wwgl.lanzout.com/iN21C0qiiija
一、增加拖动条
1)获取视频播放总时长,并显示总时长和拖动条;
定义播放时间和拖动条
private SeekBar seekBar;
private TextView tvTime;
private int duration; // 视频总时长
private boolean isTouch = false; // 是否拖拽 拖动条
seekBar = findViewById(R.id.seekBar);
seekBar.setOnSeekBarChangeListener(this);
tvTime = findViewById(R.id.tv_time);
MainActivity在回调音视频解封装成回调中,通过NdkPlayer.java获取音视频总时长,并更新UI
// 准备成功的回调处 <---- native层 在子线程调用的
mNdkPlayer.setOnPreparedListener((int code, String msg) -> {
// TODO 1.拖动条 拖动条默认隐藏,如果播放视频有总时长,就显示所以拖动条控件
duration = mNdkPlayer.getDuration();
runOnUiThread(() -> {
// 1.1拖动条 显示总时长 如:duration == 119 转换成 01:59
if (duration != 0) {
tvTime.setText("00:00:00/" + TimeUtil.timeConversion(duration));
tvTime.setVisibility(View.VISIBLE); // 显示
seekBar.setVisibility(View.VISIBLE); // 显示
}
});
});
通过NdkPlayer.java调用Native层获取播放总时长
public int getDuration() {
return getDurationNative();
}
private native int getDurationNative();
Native层native-lib.cpp调用NdkPlayer.cpp获取播放总时长
/**
* 1.2拖动条 获取视频总时长
*/
extern "C"
JNIEXPORT jint JNICALL
Java_com_ndk_player_NdkPlayer_getDurationNative(JNIEnv *env, jobject thiz) {
if(ndk_player){
return ndk_player->getDuration();
}
return 0;
}
NdkPlayer.cpp获取播放总时长
int duration = 0;
int NdkPlayer::getDuration() {
return duration;
}
/**
* 真正开始 解封装
*/
void NdkPlayer::prepare_() {
//...
// 1.3拖动条 avformat_find_stream_info FFmpeg内部源码已经做(流探索)了,所以可以拿到 总时长
// format_context->duration 时间基 --> 转化为时间戳
this->duration = format_context->duration / AV_TIME_BASE;
}
2)获取视频播放时间戳,实时同步到UI更新当前播放时间和拖动条进度;
MainActivity设置视频播放进度监听,native层播放进度 回调java层 进度条动态显示
// TODO 2.拖动条 设置视频播放进度监听,native层播放进度 回调java层 进度条动态显示
mNdkPlayer.setOnProgressListener(progress -> {
// 用户手指不在拖动拖动条的情况下,实时显示播放进度条
Log.i("MainActivity", "setOnProgressListener isTouch = " + isTouch);
if (!isTouch) {
runOnUiThread(() -> {
if (duration != 0) {
// progress == 视频当前播放的时间戳,需要转化为进度%
tvTime.setText(TimeUtil.timeConversion(progress) + "/" + TimeUtil.timeConversion(duration));
seekBar.setProgress(progress * 100 / duration);
}
});
}
});
NdkPlayer.java
/**
* 2.1拖动条
* 设置准备的监听方法
*/
public void setOnProgressListener(OnProgressListener onProgressListener) {
this.onProgressListener = onProgressListener;
}
/**
* 播放进度的监听接口
*/
public interface OnProgressListener {
void onProgress(int progress);
}
/**
* 给native层jni反射调用的播放进度
*/
public void onProgress(int progress) {
if (onProgressListener != null) {
onProgressListener.onProgress(progress);
}
}
NdkPlayer.cpp
// 2.2拖动条 设置回调函数,将native层音频的播放时间,回调给java层
if (this->duration != 0) { // 非直播,才有意义把 JNICallbackHelper传递过去
audio_channel->setJINCallbackHelper(helper);
}
AudioChannel.cpp
/**
* 1.out_buffers 给予数据
* 2.out_buffers 给予数据的大小计算工作
* @return 大小还要计算,因为我们还要做重采样工作,重采样之后,大小不同了
*/
int AudioChannel::getPCM() {
//...
/**
* 获取音频播放的时间搓
* 在FFmpeg里面播放时间有自己的单位(时间基TimeBase),
* 时间基TimeBase理解:例如:(fps25 一秒钟25帧, 那么每一帧==25分之1,而25分之1就是时间基概念)
* 需要将TimeBase转换为时间戳audio_time,TimeBase在解封装的时候获取
*/
audio_time = frame->best_effort_timestamp * av_q2d(time_base); // 必须这样计算后,才能拿到真正的时间搓
// 2.3拖动条 将之前获取到的native层音频的播放时间,回调给java层
if(helper){
helper->onProgress(audio_time);
}
}
JINCallbackHelper.cpp
/**
* 2.4拖动条 回调给java层
* @param progress
*/
void JINCallbackHelper::onProgress(int progress) {
JNIEnv *env_progress;
vm->AttachCurrentThread(&env_progress, 0);
// 回调java层 NdkPlayer#onProgress(int progress)
// int -> jint无需转换
env_progress->CallVoidMethod(job, jmd_progress, progress);
vm->DetachCurrentThread();
}
3)手指拖动拖动条,视频播放到拖动条对应的时间的画面。
将拖动条% 转化成视频播放的时间戳,传给native层,并设置为视频的播放时间
/**
* 当前拖动条进度发送了改变 回调此函数
*
* @param seekBar 控件
* @param progress 1~100
* @param fromUser 是否用户拖拽导致的改变
*/
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
if (fromUser) {
// 拖拽拖动条,同步更新时间,如progress == 10%
tvTime.setText(TimeUtil.timeConversion(progress * duration / 100) + "/" + TimeUtil.timeConversion(duration));
}
}
// 手按下去,回调此函数
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
isTouch = true;
}
// 手松开(SeekBar当前值 ---> native层),回调此函数
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
isTouch = false;
// TODO 3.拖动条 将拖动条% 转化成视频播放的时间戳,传给native层,并设置为视频的播放时间
int progress = seekBar.getProgress(); // 获取当前seekbar当前进度
// SeekBar 1~100 -- 转换 --> native层播放的时间(61.546565)
int playProgress = progress * duration / 100;
mNdkPlayer.seek(playProgress);
}
NdkPlayer.java调用Native层
public void seek(int playProgress) {
Log.i("NdkPlayer", "seek playProgress = " + playProgress);
seekNative(playProgress);
}
private native void seekNative(int progress);
native-lib.cpp 将progress设置为视频的播放时间
/**
* 3.1拖动条 将play_value设置为视频的播放时间
*/
extern "C"
JNIEXPORT void JNICALL
Java_com_ndk_player_NdkPlayer_seekNative(JNIEnv *env, jobject thiz, jint progress) {
if (ndk_player) {
ndk_player->seek(progress);
}
}
NdkPlayer.cpp 将progress转化为时间基设置为视频的播放时间
void NdkPlayer::seek(jint progress) {
LOGI("NdkPlayer::seek() progress %d\n", progress);
// 健壮性判断
if (progress < 0 || progress > duration) {
return;
}
if (!audio_channel && !video_channel) {
LOGI("NdkPlayer::seek() !audio_channel && !video_channel");
return;
}
if (!format_context) {
LOGI("NdkPlayer::seek() !format_context");
return;
}
// av_seek_frame内部会对我们的format_context上下文的成员做处理,使用互斥锁,保证多线程情况下安全
pthread_mutex_lock(&seek_mutex);
/**
* 参数1:formatContext 上下文
* 参数2:-1 代表默认情况,FFmpeg自动选择 音频 还是 视频 做 seek, 模糊:0视频 1音频
* 参数3:播放时间 时间基AV_TIME_BASE 需要将java层传递过来的时间转成时间基
* 参数4:AVSEEK_FLAG_ANY(老实) 直接精准到 拖动的位置,问题:如果不是关键帧,B帧 可能会造成 花屏情况
* AVSEEK_FLAG_BACKWARD(则优 8的位置 B帧 , 找附件的关键帧 6,如果找不到他也会花屏)
* AVSEEK_FLAG_FRAME 找关键帧(非常不准确,可能会跳的太多),一般不会直接用,但是会配合用
*/
int result = av_seek_frame(format_context, -1, progress * AV_TIME_BASE, AVSEEK_FLAG_BACKWARD);
LOGI("NdkPlayer::seek() result %d\n", result);
if (result < 0) {
pthread_mutex_unlock(&seek_mutex);
return;
}
// 这四个队列,还在工作中,让他们停下来, seek完成后,重新播放
if (audio_channel) {
audio_channel->packets.setWork(0); // 队列不工作
audio_channel->frames.setWork(0); // 队列不工作
audio_channel->packets.clear();
audio_channel->frames.clear();
audio_channel->packets.setWork(1); // 队列继续工作
audio_channel->frames.setWork(1); // 队列继续工作
}
if (video_channel) {
video_channel->packets.setWork(0); // 队列不工作
video_channel->frames.setWork(0); // 队列不工作
video_channel->packets.clear();
video_channel->frames.clear();
video_channel->packets.setWork(1); // 队列继续工作
video_channel->frames.setWork(1); // 队列继续工作
}
pthread_mutex_unlock(&seek_mutex);
}
二、项目释放
NdkPlayer.cpp
/**
* 真正开始 解封装
*/
void NdkPlayer::prepare_() {
//...
int result = avformat_open_input(&format_context, data_source, 0, &dictionary);
LOGI("NdkPlayer::avformat_open_input = %d\n", result);
// 用完释放
av_dict_free(&dictionary);
if (result) {
//...
// TODO 1.项目释放
avformat_close_input(&format_context);
return;
}
/**
* TODO 第二步:查找媒体中的音视频流的信息
* @return >=0 if OK
*/
result = avformat_find_stream_info(format_context, 0);
LOGI("NdkPlayer::avformat_find_stream_info = %d\n", result);
if (result < 0) {
//...
// 1.1项目释放
avformat_close_input(&format_context);
return;
}
//...
AVCodecContext *codec_context = nullptr;
/**
* TODO 第三步:根据流信息,流的个数,用循环来找 音频流和视频流
*/
for (int i = 0; i < format_context->nb_streams; ++i) {
//...
if (!codec) {
//...
// 1.2项目释放
avformat_close_input(&format_context);
}
/**
* TODO 第七步:编解码器 上下文
*/
codec_context = avcodec_alloc_context3(codec);
if (!codec_context) {
//...
// 1.3项目释放
avcodec_free_context(&codec_context); // 释放此上下文 AVCodec 他会考虑到,你不用管*codec
avformat_close_input(&format_context);
return;
}
/**
* TODO 第八步:把参数复制到编解码器上下文(parameters copy codecContext)
* @return >= 0 on success
*/
result = avcodec_parameters_to_context(codec_context, parameters);
LOGI("NdkPlayer::avcodec_parameters_to_context = %d\n", result);
if (result < 0) {
//...
// 1.4项目释放
avcodec_free_context(&codec_context); // 释放此上下文 avcodec 他会考虑到,你不用管*codec
avformat_close_input(&format_context);
return;
}
/**
* TODO 第九步:打开解码器
* zero on success
*/
result = avcodec_open2(codec_context, codec, 0);
LOGI("NdkPlayer::avcodec_open2 = %d\n", result);
// 非0就是true,非0就是失败,true就是失败
if (result) {
//...
// 1.5项目释放
avcodec_free_context(&codec_context); // 释放此上下文 avcodec 他会考虑到,你不用管*codec
avformat_close_input(&format_context);
return;
}
//...
} // for end
/**
* TODO 第十一步: 如果流中没有音频 也没有视频,则失败【健壮性校验】
*/
if (!audio_channel && !video_channel) {
//...
// 1.6项目释放
avcodec_free_context(&codec_context); // 释放此上下文 avcodec 他会考虑到,你不用管*codec
avformat_close_input(&format_context);
return;
}
//...
}
AudioChannel.cpp
AudioChannel::~AudioChannel() {
// TODO 2.项目释放
if (swr_ctx) {
swr_free(&swr_ctx);
}
DELETE(out_buffers);
}
void AudioChannel::stop() {
// 2.1项目释放 等解码线程、播放线程,全部停止,再做释放工作
pthread_join(pid_audio_decode, nullptr);
pthread_join(pid_audio_play, nullptr);
// 保证两个线程执行完毕,再释放
isPlaying = false;
packets.setWork(0);
frames.setWork(0);
// 2.2项目释放 OpenSLES释放工作
// 1 设置停止状态
if (bqPlayerPlay) {
(*bqPlayerPlay)->SetPlayState(bqPlayerPlay, SL_PLAYSTATE_STOPPED);
bqPlayerPlay = nullptr;
}
// 2 销毁播放器
if (bqPlayerObject) {
(*bqPlayerObject)->Destroy(bqPlayerObject);
bqPlayerObject = nullptr;
bqPlayerBufferQueue = nullptr;
}
// 3 销毁混音器
if (outputMixObject) {
(*outputMixObject)->Destroy(outputMixObject);
outputMixObject = nullptr;
}
// 4 销毁引擎
if (engineObject) {
(*engineObject)->Destroy(engineObject);
engineObject = nullptr;
engineInterface = nullptr;
}
// 队列清空
packets.clear();
frames.clear();
}
VideoChannel.cpp
VideoChannel::~VideoChannel() {
// TODO 3.项目释放
DELETE(audio_channel);
}
void VideoChannel::stop() {
pthread_join(pid_video_decode, nullptr);
pthread_join(pid_video_play, nullptr);
isPlaying = false;
packets.setWork(0);
frames.setWork(0);
packets.clear();
frames.clear();
}
至此,音视频播放器项目已完成。