一,从setDataSource开始,设置播放的数据源,可以时本地视频,也可以是网络链接
EnjoyPlayer.java
private String mPath = "/sdcard/mpeg.mp4";
public void setDataSource(String path) {
setDataSource(nativeHandler, path);
}
EnjoyPlayer.cpp中的 setDataSource,只是简单记录下path,已被prepare使用:
void EnjoyPlayer::setDataSource(const char *path_) {
//C语言的实现,这里地址用自己的成员属性记录,如果直接赋值给path,当path_被释放后,
// path的指向也就无效了,所以这里做一个深拷贝的操作,自己去为指针申请一段内存。
// path = static_cast(malloc(strlen(path_) + 1));
// memset((void *)path, 0, strlen(path)+1);
// memcpy((void *)path, (void *)path_, strlen(path_));
// C++的实现方式
path = new char[strlen(path_) +1];
strcpy(path, path_);
}
//解析媒体信息,放在一个单独的线程里面执行,比如要解析的是一个来自网络的数据,
void EnjoyPlayer::prepare() {
pthread_create(&prepareTask, 0, prepare_t, this);
}
媒体信息的处理过程:
1,avFormatContext用来保存,打开的媒体文件的构成及上下文信息。
2, 查找媒体流,获取音视频流。注意这里返回值大于等于0表示成功。
3, 针对音频流,视频流,分别获取解码器,虽然ACCodec叫做解码器,但是实际我们解码时并没有直接使用,解码时实际使用的是AVCodecContext,即解码信息上下文,
4,打开解码器
5,通过 AudioChannel,VideoChannel 对音频流,视频流分别处理
6, prepare完成后,通知java层,可以播放了,
void EnjoyPlayer::_prepare() {
//avFormatContext用来保存,打开的媒体文件的构成及上下文信息。
avFormatContext = avformat_alloc_context();
//第一个参数,是一个二级指针,可以修改外部实参,
// 第三个参数表示文件的封装格式avi,flv等,如果传nullptr,会自定识别,
// 第四个参数,表示一个配置信息,比如打开网络文件时,可以指定超时时间
//AVDictionary *options;
//av_dict_set(&options, "timeout", "3000000", 0);
int ret = avformat_open_input(&avFormatContext, path, nullptr, 0);
//打开文件失败,获取失败信息,
if (0 != ret) {
char *msg = av_err2str(ret);
LOGE("打开%s 失败,返回 %d ,错误描述 %s 。", path, ret, msg);
helper->onError(FFMPEG_CAN_NOT_OPEN_URL, THREAD_CHILD);
goto ERROR;
}
//查找媒体流,获取音视频流。注意这里返回值大于等于0表示成功。
ret = avformat_find_stream_info(avFormatContext, 0);
if (ret < 0) {
LOGE("查找媒体流 %s 失败,返回:%d 错误描述:%s", path, ret, av_err2str(ret));
helper->onError(FFMPEG_CAN_NOT_FIND_STREAMS, THREAD_CHILD);
goto ERROR;
}
//获取视频时长,默认返回值单位为微妙,这里转成秒数。
duration = avFormatContext->duration/AV_TIME_BASE;
//获取媒体文件中的媒体流(音频流,视频流)
for (int i = 0; i < avFormatContext->nb_streams; ++i) {
AVStream *avStream = avFormatContext->streams[i];
//获取媒体流上的解码信息,配置信息,参数信息
AVCodecParameters *parameters = avStream->codecpar;
//查找相关的解码器,但是这个函数可能返回null,就是没有找到流对应的解码器,
// 因为在编译ffmpeg时,配置信息指定了是否开启特定格式的编解码支持,
// 可以通过命令:ffmpeg -decoders查看支持的解码格式。
AVCodec *dec = avcodec_find_decoder(parameters->codec_id);
if (!dec) {
helper->onError(FFMPEG_FIND_DECODER_FAIL, THREAD_CHILD);
goto ERROR;
}
//尽管ACCodec叫做解码器,但是实际我们解码时并没有直接使用,解码时实际使用的是AVCodecContext,即解码信息上下文,
//这里实际就是为结构体申请内存。
AVCodecContext *avCodecContext = avcodec_alloc_context3(dec);
//把解码信息,赋值给解码上下文中的成员变量
if (avcodec_parameters_to_context(avCodecContext, parameters) < 0) {
helper->onError(FFMPEG_CODEC_CONTEXT_PARAMETERS_FAIL, THREAD_CHILD);
goto ERROR;
}
//打开解码器
if (avcodec_open2(avCodecContext, dec, 0) != 0) {
helper->onError(FFMPEG_OPEN_DECODER_FAIL, THREAD_CHILD);
goto ERROR;
}
//对音频流,视频流分别处理
if (parameters->codec_type == AVMEDIA_TYPE_AUDIO) {
audioChannel = new AudioChannel(i, helper, avCodecContext, avStream->time_base);
} else if (parameters->codec_type == AVMEDIA_TYPE_VIDEO){
//从流身上,拿到视频的帧率,就是通过avg_frame_rate的分子除以分母
int fps = av_q2d(avStream->avg_frame_rate);
if (isnan(fps) || fps == 0) {
//如果获取平均帧率失败,尝试去获取基础帧率
fps = av_q2d(avStream->r_frame_rate);
}
if (isnan(fps) || fps == 0) {
//上面两种情况都获取不到帧率,将根据container,codec猜测一个值,
// 因为fps会在音视频同步时用到,所以一定要有值。
fps = av_q2d(av_guess_frame_rate(avFormatContext, avStream, 0));
}
videoChannel = new VideoChannel(i, helper, avCodecContext, avStream->time_base, fps);
videoChannel->setWindow(window);
}
}
//判断媒体文件是否包含,音频,视频,
if (!videoChannel && !audioChannel) {
helper->onError(FFMPEG_NOMEDIA,THREAD_CHILD);
goto ERROR;
}
//通知java层,可以播放了,
helper->onPrepare(THREAD_CHILD);
return;
ERROR:
LOGE("解析媒体文件失败。。。");
_release();
}
prepare之后,就可以start播放了,在这之前,还要一个重要的参数需要设置,就是Surface,为的是从这个Surface中拿到ANativeWindow,在Native层去渲染图像,通常要借助ANativeWindow,其实Java层的View组件,通过Canvas去draw内容,底层也是借助的AnativeWidnow完成的绘制.
MainActivity.java
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
Surface surface = holder.getSurface();
mEnjoyPlayer.setSurface(surface);
}
通过ANativeWindow *window = ANativeWindow_fromSurface(env, surface);从surface拿到关联的ANativeWindow.
extern "C"
JNIEXPORT void JNICALL
Java_com_test_ffmpegapplication_EnjoyPlayer_setSurface(JNIEnv *env, jobject thiz,
jlong native_handler, jobject surface) {
EnjoyPlayer *enjoyPlayer = reinterpret_cast(native_handler);
ANativeWindow *window = ANativeWindow_fromSurface(env, surface);
enjoyPlayer->setWindow(window);
}
启动播放:读取媒体源的数据,需要单独一个线程来操作读取,
//根据类型放入Audio vidoe channel队列中,
EnjoyPlayer.cpp
void EnjoyPlayer::start() {
//读取媒体源的数据,需要单独一个线程来操作读取,并且线程可以停止,
//根据类型放入Audio vidoe channel队列中,
isPlaying = 1;
if (videoChannel) {
videoChannel->audioChannel = audioChannel;
videoChannel->play();
}
if (audioChannel){
audioChannel->play();
}
pthread_create(&startTask, 0, start_t, this);
}
这里只关注视频的处理,要完成播放,就要先解码,然后把解码后的数据转成本地窗口需要格式,填充到Window中.
这里开启两个线程,一个负责解码,一个负责绘制
void VideoChannel::play() {
isPlaying = 1;
setEnable(true);
//做两个事情,解码,播放,分别在单独的线程执行,会比较流畅
pthread_create(&videoDecodeTask, 0 ,videoDecode_t, this);
pthread_create(&videoPlayTask, 0, videoPlay_t, this);
}
解码的过程,就是从packet_queue队列中取出AVPacket,交给avcodec去解码,然后拿到解码后的AVFrame数据,放入frame_queue队列,供播放线程使用.
void VideoChannel::decode() {
AVPacket *packet=0;
while (isPlaying) {
//dequeu是一个阻塞操作,取出待解码数据
int ret = pkt_queue.deQueue(packet);
//向解码器发送解码数据
ret = avcodec_send_packet(avCodecContext, packet);
//因为packet存放于堆内存,所以用完释放。
releaseAvPacket(packet);
if (ret < 0) {
break;
}
//从解码器取出解码好的数据
AVFrame *avFrame = av_frame_alloc();
ret = avcodec_receive_frame(avCodecContext, avFrame);
if (ret == AVERROR(EAGAIN)) {
//表示需要更多的待解码数据,
continue;
} else if (ret < 0) {
break;
}
frame_queue.enQueue(avFrame);
}
releaseAvPacket(packet);
}
绘制的过程:
1,YUV转RGB 通常用SwsContext来做格式转换
2,把转化后的数据,显示到window上,
3,设置window属性,获取window上数据的缓冲区,把视频数据刷到buffer中。
void VideoChannel::_play() {
AVFrame *frame = 0;
int ret;
uint8_t *data[4];
int linesize[4];
//通常用SwsContext来做格式转换,YUV转RGB,缩放,
// 或者加滤镜效果等,也可以在这个阶段做,
SwsContext *swsContext = sws_getContext(avCodecContext->width,
avCodecContext->height,
avCodecContext->pix_fmt,
avCodecContext->width,
avCodecContext->height,
AV_PIX_FMT_RGBA, SWS_FAST_BILINEAR,
0,0,0);
//根据传入的参数,确定申请的内存大小,
av_image_alloc(data, linesize, avCodecContext->width, avCodecContext->height,AV_PIX_FMT_RGBA, 1);
double frame_delay = 1.0 / fps;
while (isPlaying) {
ret = frame_queue.deQueue(frame);
if (!isPlaying) {//如果停止播放,退出处理
break;
}
if (!ret) {
continue;
}
//根据帧率,再参考额外延迟时间repeat_pict,让视频播放更流畅。delay是要让视频以正常的速度播放,
double extra_delay = frame->repeat_pict / (2*fps);
double delay = extra_delay + frame_delay;
if (audioChannel) {
//处理音视频同步,best_effort_timestamp跟pts通常是一致的,
// 区别是best_effort_timestamp经过了一些参考,得到一个最优的时间
clock = frame->best_effort_timestamp * av_q2d(time_base); //视频的时钟,
double diff = clock - audioChannel->clock;
//音频,视频的时间戳 的差,这个差有一个允许的范围(0.04 ~ 0.1)
double sync = FFMAX(AV_SYNC_THRESHOLD_MIN, FFMIN(AV_SYNC_THRESHOLD_MAX, delay));
if (diff <= -sync) {
delay = FFMAX(0, delay + diff); //视频慢了
} else if (diff > sync) {
delay = delay + diff;
}
LOGE("clock ,video:%1f ,audio:%1f, delay:%1f, V-A = %1f ",clock, audioChannel->clock, delay, diff);
}
av_usleep(delay * 1000000);
//第二个参数代表图像数据,是一个二维数组(每一维度,代表RGBA中的一个数据),所以是一个指向指针的指针,
// 每一个维度的数据就是一个指针,那么RGBA需要4个指针,所以就是4个元素的数组,数组的元素就是指针,指针数据
//第三个参数,每一行数据,的 数据个数
//第四个,从原始图像的那个位置开始转换,
//第五个,图像的高度,
//最后两个参数,得到转换后的数据结果,所以变量类型跟第二个,第三个参数一样,
sws_scale(swsContext, frame->data, frame->linesize, 0, frame->height, data, linesize);
//把转化后的数据,显示到window上,
onDraw(data, linesize, avCodecContext->width, avCodecContext->height);
releaseAvFrame(frame);
}
av_free(&data[0]);
isPlaying = 0;
releaseAvFrame(frame);
sws_freeContext(swsContext);
}
视频数据绘制, 涉及对window的操作,都要加锁,
void VideoChannel::onDraw(uint8_t **data, int *linesize, int width, int height) {
pthread_mutex_lock(&surfaceMutex);
if (!window) {
//window还不可用
pthread_mutex_unlock(&surfaceMutex);
return;
}
//把window的宽高(是指显示像素的宽高,不是物理宽高,也不是view的宽高)设置为跟要显示图像的宽高一样,
// 这样可以保证原始视频的宽高比
ANativeWindow_setBuffersGeometry(window, width, height, WINDOW_FORMAT_RGBA_8888);
//window上数据的缓冲区,
ANativeWindow_Buffer buffer;
if (ANativeWindow_lock(window, &buffer, 0) !=0) {
ANativeWindow_release(window);
window =0;
pthread_mutex_unlock(&surfaceMutex);
return;
}
//把视频数据刷到buffer中。
//得到RGBA格式的数据后,开始想ANativeWindow填充,但是在数据填充时,
// 需要根据window——buffer的步进来一行一行的拷贝,window_buffer.stride。
//因为涉及字节对齐,window的一行数据数,跟图像数据的一行数据数不同,所以要一行一行拷贝。
// 所谓字节对齐,比如说以12字节对齐为例,当一个数据大小不足12字节,比如只有10字节,,
// 会添加2个占位字节,补齐12字节,
//窗口buffer中需要的数据。
uint8_t *dstData = static_cast(buffer.bits);
int dstSize = buffer.stride * 4;//乘4,RGBA_8888占4个字节,
//data原本是一个指针的指针(4个维度),在经过sws_scale转换后,所有的RGBA数据都保存在第一个指针中了。
//视频图像的RGBA数据,
uint8_t *srcData = data[0];
int srcSize = linesize[0];
//一行一行的拷贝
for (int i = 0; i < buffer.height; ++i) {
memcpy(dstData+i*dstSize, srcData+ i*srcSize, srcSize);
}
ANativeWindow_unlockAndPost(window);
pthread_mutex_unlock(&surfaceMutex);
}
到这里,就完成了视频数据的绘制.
附件(源码):https://download.csdn.net/download/lin20044140410/12247488