NDK FFmpeg音视频播放器六

news/2024/7/10 20:42:14 标签: ffmpeg, 音视频, NDK

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();
}

至此,音视频播放器项目已完成。


http://www.niftyadmin.cn/n/188156.html

相关文章

rabbitMQ的详细介绍

1.概述 RabbitMQ是一个消息中间件:它接受并转发消息。你可以把它当做一个快递站点&#xff0c;当你要发送一个包裹时&#xff0c;你把你的包裹放到快递站&#xff0c;快递员最终会把你的快递送到收件人那里&#xff0c;按照这种逻辑RabbitMQ是一个快递站&#xff0c;一个快递员…

【黑客技术】Hping攻击实验

一、重要声明 请勿攻击公网&#xff01;请勿攻击公网&#xff01;请勿攻击公网&#xff01; 一切责任自负&#xff01;一切责任自负&#xff01;一切责任自负&#xff01; 二、工具介绍 hping3是一款面向TCP/IP协议的免费的数据包生成和分析工具。Hping是用于对防火墙和网络…

毕业论文数据分析方法分类汇总

今天将常用的数据分析方法进行一个分类汇总说明&#xff0c;整理如下图&#xff1a; 1、基本描述统计 基本描述统计分析包括频数分析、描述分析、分类汇总&#xff1b;是对收集的数据进行基本的说明。 频数分析一般使用频数、百分比、饼图等形式进行描述。描述分析常见的指标…

ChatGPT:AI不取代程序员,只取代的不掌握AI的程序员

作者&#xff1a;成都兰亭集势信息技术有限公司技术总监张雄可能大家会有如下的问题&#xff0c;我就使用chatGPT这个AI工具的API来问一下。问&#xff1a;chatGPT会替换掉程序员吗&#xff1f;如果能&#xff0c;预计好久&#xff1f;答&#xff1a;作为一名 AI 语言模型&…

这可能是最全面的Spring面试题总结了

Spring是什么&#xff1f; Spring是一个轻量级的控制反转(IoC)和面向切面(AOP)的容器框架。 Spring的优点 通过控制反转和依赖注入实现松耦合。支持面向切面的编程&#xff0c;并且把应用业务逻辑和系统服务分开。通过切面和模板减少样板式代码。声明式事务的支持。可以从单…

Kafka系列之消息重新消费

概述 需求来源&#xff0c;在review前人留下的屎山代码时发现如下截图所示的代码片段&#xff1a; 也就是说代码是空实现的。 于是有此需求&#xff1a;消息重新消费。 调研 实现方案 修改偏移量&#xff0c;即offset&#xff0c;可通过脚本实现新增group&#xff0c;需通…

算法训练第四十一天|343. 整数拆分 、96.不同的二叉搜索树

343. 整数拆分 题目链接&#xff1a;343. 整数拆分 参考&#xff1a;https://programmercarl.com/0343.%E6%95%B4%E6%95%B0%E6%8B%86%E5%88%86.html 题目描述 给定一个正整数 n&#xff0c;将其拆分为至少两个正整数的和&#xff0c;并使这些整数的乘积最大化。 返回你可以获…

Web_php_unserialize

目录 一、知识基础讲解 1、三个魔术方法&#xff1a; &#xff08;1&#xff09; __construct()函数 &#xff08;2&#xff09;__destruct()函数 &#xff08;3&#xff09;__wakeup()函数 2、两个重要常见的PHP函数 &#xff08;1&#xff09;str_replace() 函数 &am…