项目源码
FFmpeg开发文档
Android Studio的开发环境已经准备好,接下来开始正式的写ndk代码,首先创建一个FFmpeg工具类,添加native方法
import android.view.Surface;
public class FFmpegPlayer {
static {
System.loadLibrary("ffmpeg");
}
/**
* 播放视频
*/
public native void playVideo(String url,Surface surface);
}
传入的Surface对象是用于显示播放界面的,这里先传入,后边再说
然后生成对应的头文件,之前在eclipse上开发的时候都是通过javah命令生成头文件后将对应的方法名拷贝到我们的c文件中,这部分的生成方法可以查看之前的博客。现在我们使用的Android Studio3.x可以一键生成对应的native方法,将光标选中方法名然后alt+enter选择create function xxx即可
extern "C"
JNIEXPORT void JNICALL
Java_com_rzm_ffmpegplayer_FFmpegPlayer_playVideo(JNIEnv *env, jobject instance, jstring url_,
jobject surface) {
const char *url = env->GetStringUTFChars(url_, 0);
env->ReleaseStringUTFChars(url_, url);
}
今天来进行的是FFmpeg的第一部分,解封装
解封装
一个mp4文件可能是视频流音频流字幕流等等多个流的一个结合体,而我们要实现视频和音频的播放,就需要将这个结合体拆分开来,单独的进行视频播放,音频播放,实现同步,这是实现播放的一个必要的条件。所以播放的第一步就要进行源文件的解封装,解封装涉及到的一些接口如下
av_register_all()
初始化libavformat 并注册所有的muxers和demuxers以及各种协议,当然,如果你只需要初始化特定的支持组件,那么单独调用特定的方法即可,一般直接这样做一劳永逸
avformat_open_input
打开输入流(可以是本地流也可以是网络流,如果是网络的,那么需要avformat_network_init()方法支持)并读取视频头信息,视频头通常包含一些视频基本信息,比如说视频格式,streams的数量等等。注意此时编解码器未打开,最后必须使用avformat_close_input(&avFormatContext)进行关闭,避免造成泄漏
avformat_network_init()
初始化全局的网络组件,支持rtfp协议的时候需要打开这个开关,ffmpeg推荐打开这个全局的网络开关,因为可以减少单独对每个单独回话做设置的开销
avformat_find_stream_info()
打开流文件之后执行这个方法,读取媒体文件获取到流信息,这个方法对于没有视频头的视频尤其有效(MPEG-2),,此方法执行之后会将读取到的流信息填充到AVFormatContext中的各个流的轨道上,这样一来,AVFormatContext->streams[i]中就有信息了.这个方法可以打开部分解码器并保存在第二个参数AVDictionary中,但是他无法保证打开全部的解码器,如果存在null那么也是正常现象,这里第二个参数我们直接置为NULL
av_find_best_stream
媒体信息已经获取到了,接下来需要将视频流和音频流区分开来,可以通过av_find_best_stream来获取流轨道的index,在老版本的ffmpeg上我们是通过遍历所有的流通过对比codec_type来判断的,代码如下
for(int i = 0; i < avFormatContext->nb_streams; i++){
AVStream *avStream = avFormatContext->streams[i];
if (avStream->codecpar->codec_type == AVMEDIA_TYPE_VIDEO){
//找到视频index
videoIndex = i;
} else if (avStream->codecpar->codec_type == AVMEDIA_TYPE_AUDIO){
//找到音频index
audioIndex = i;
}
}
通过这个方法获取的方式为下,验证一下,你会发现二者结果相同
videoIndex = av_find_best_stream(avFormatContext,AVMEDIA_TYPE_VIDEO,-1,-1,NULL,0);
audioIndex = av_find_best_stream(avFormatContext,AVMEDIA_TYPE_AUDIO,-1,-1,NULL,0);
av_read_frame
返回流的下一帧,这个函数返回文件中存储的内容。每调用一次,它就将返回一帧数据,这是从文件存储的内容中切割出来的
av_packet_unref
这个方法会擦除packet空间,不再指向这个packet的缓存空间,另外也会将packet重置为默认状态。一帧数据也就是一个ACPacket使用完之后需要回收,否则会造成内存急剧增长,下边分别是调用这个方法和不调用这个方法的内存变化状态
![img_569d2f2ef61d75b696c8d03bf1806436.png](https://i-blog.csdnimg.cn/blog_migrate/29d468d2a5f99efe5dc85e98be3246cf.png)
![img_512281ce7cbb1382f29070af4516220b.png](https://i-blog.csdnimg.cn/blog_migrate/8793e2d0ede3ace13e0ec837aabb9e12.png)
完整的代码如下
/**
* 播放视频,支持本地和网络两种
*/
extern "C"
JNIEXPORT void JNICALL
Java_com_rzm_ffmpegplayer_FFmpegPlayer_playVideo(JNIEnv *env, jobject instance, jstring url_,
jobject surface) {
const char *url = env->GetStringUTFChars(url_, 0);
//初始化解封装
av_register_all();
//初始化全局网络组件,可选推荐使用,在使用网络协议的场景中这是必选的(rtfp http)
avformat_network_init();
AVFormatContext *avFormatContext = NULL;
//指定输入的格式,如果为NULL,将自动检测输入格式,所以可置为NULL
//AVInputFormat *fmt = NULL;
//打开输入文件,可以是本地视频或者网络视频
int result = avformat_open_input(&avFormatContext,url,NULL,NULL);
//打开输入内容失败
if(result != 0){
LOGE("avformat_open_input failed!:%s",av_err2str(result));
return;
}
//打开输入成功
LOGI("avformat_open_input success!:%s",av_err2str(result));
//读取媒体文件的分组以获得流信息。这个对于没有标题的文件格式(如MPEG)很有用。这个函数还计算实际的帧率在
//MPEG-2重复的情况下帧模式。
result = avformat_find_stream_info(avFormatContext,NULL);
if (result < 0){
LOGE("avformat_find_stream_info failed: %s",av_err2str(result));
}
//获取到了输入文件信息,打印一下视频时长和nb_streams
LOGI("duration = %lld nb_streams=%d",avFormatContext->duration,avFormatContext->nb_streams);
//分离音视频,获取音视频在源文件中的streams index
int videoIndex = 0;
int audioIndex = 1;
int fps = 0;
for(int i = 0; i < avFormatContext->nb_streams; i++){
AVStream *avStream = avFormatContext->streams[i];
if (avStream->codecpar->codec_type == AVMEDIA_TYPE_VIDEO){
//找到视频index
videoIndex = i;
LOGI("video index = %d",videoIndex);
//FPS是图像领域中的定义,是指画面每秒传输帧数
fps = r2d(avStream->avg_frame_rate);
LOGI("video info ---- fps = %d fps den= %d fps num= %d width=%d height=%d code id=%d format=%d",
fps,
avStream->avg_frame_rate.den,
avStream->avg_frame_rate.num,
avStream->codecpar->width,
avStream->codecpar->height,
avStream->codecpar->codec_id,
avStream->codecpar->format
);
} else if (avStream->codecpar->codec_type == AVMEDIA_TYPE_AUDIO){
//找到音频index
audioIndex = i;
LOGI("audio index = %d sampe_rate=%d channels=%d sample_format=%d",
audioIndex,
avStream->codecpar->sample_rate,
avStream->codecpar->channels,
avStream->codecpar->format
);
}
}
//上边通过遍历streams音视频的index,还可以通过提供的接口获取
videoIndex = av_find_best_stream(avFormatContext,AVMEDIA_TYPE_VIDEO,-1,-1,NULL,0);
audioIndex = av_find_best_stream(avFormatContext,AVMEDIA_TYPE_AUDIO,-1,-1,NULL,0);
LOGI("av_find_best_stream videoIndex=%d audioIndex=%d",videoIndex,audioIndex);
//读取帧数据
//Allocate an AVPacket and set its fields to default values
//存储压缩数据,对于视频,它通常应该包含一个压缩帧。对于音频它可能包含几个压缩帧
AVPacket *avPacket = av_packet_alloc();
for (;;) {
//Return the next frame of a stream.
int read_result = av_read_frame(avFormatContext,avPacket);
if(read_result != 0){
//读取到结尾处,从20秒位置继续开始播放
LOGI("读取到结尾处 %s",av_err2str(read_result));
//跳转到指定的position播放,最后一个参数表示
//int pos = 200000 * r2d(avFormatContext->streams[videoIndex]->time_base);
//av_seek_frame(avFormatContext,videoIndex,pos,AVSEEK_FLAG_BACKWARD|AVSEEK_FLAG_FRAME );
//LOGI("avFormatContext->streams[videoIndex]->time_base= %d",avFormatContext->streams[videoIndex]->time_base);
//continue;
break;
}
LOGW("stream = %d size =%d pts=%lld flag=%d pos = %d",
avPacket->stream_index,avPacket->size,avPacket->pts,avPacket->flags,avPacket->pos
);
//packet使用完成之后执行,否则内存会急剧增长
//不再引用这个packet指向的空间,并且将packet置为default状态
av_packet_unref(avPacket);
}
//关闭上下文
avformat_close_input(&avFormatContext);
env->ReleaseStringUTFChars(url_, url);
}