目录
相关文章
Android集成FFmpeg
效果展示
实现流程
实现步骤
1.布局添加SurfaceView用于显示视频
2.将Surface传给NDK层
public class MainActivity extends AppCompatActivity{
private SurfaceView surfaceView;
private Button button;
static {
System.loadLibrary("native-lib");
}
private SurfaceHolder mHolder;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
surfaceView = findViewById(R.id.sfv_player);
button = findViewById(R.id.bt_play);
button.setOnClickListener(v->{
//找到SD卡中的视频文件
File video = new File(getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),"001.mp4");
//子线程进行视频渲染
new Thread(new Runnable() {
@Override
public void run() {
native_start_play(video.getAbsolutePath(),mHolder.getSurface());
}
}).start();
});
initSurfaceHolder();
}
private void initSurfaceHolder() {
mHolder = surfaceView.getHolder();
mHolder.setFormat(PixelFormat.RGBA_8888);
}
/**
* 调用NDK的视频渲染
* @param path 播放的视频的路径
* @param surface 要渲染的surface
* @return
*/
public native void native_start_play(String path, Surface surface);
}
3.NDK层进行视频流渲染
这里的逻辑是根据实现流程来的,每一步都加入了注释
#include
#include
#include
#include
//混合C代码编译
extern "C"{
#include "libavcodec/avcodec.h"
#include "libavformat/avformat.h"
#include "libswscale/swscale.h"
#include "libavutil/imgutils.h"
}
extern "C"
JNIEXPORT void JNICALL
Java_com_itfitness_ffmpegdemo_MainActivity_native_1start_1play(JNIEnv *env, jobject thiz,
jstring path, jobject surface) {
//获取用于绘制的NativeWindow
ANativeWindow *a_native_window = ANativeWindow_fromSurface(env,surface);
//转换视频路径字符串为C中可用的
const char *video_path = env->GetStringUTFChars(path,0);
//网络模块初始化(可以播放Url)
avformat_network_init();
//获取用于获取视频文件中各种流(视频流、音频流、字幕流等)的上下文:AVFormatContext
AVFormatContext *av_format_context = avformat_alloc_context();
//配置信息
AVDictionary *options = NULL;
av_dict_set(&options,"timeout","3000000",0);
//打开视频文件
//第一个参数:AVFormatContext的二级指针
//第二个参数:视频路径
//第三个参数:非NULL的话就是设置输入格式,NULL就是自动
//第四个参数:配置项
//返回值是是否打开成功,0是成功其他为失败
int open_result = avformat_open_input(&av_format_context, video_path, NULL, &options);
//如果打开失败就返回
if(open_result){
return;
}
//让FFmpeg将流解析出来,并找到视频流对应的索引
avformat_find_stream_info(av_format_context, NULL);
int video_stream_index = 0;
for(int i = 0; i < av_format_context->nb_streams ; i++){
//如果当前流是视频流的话保存索引
if(av_format_context->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO){
video_stream_index = i;
break;
}
}
//获取视频流的解码参数(宽高等信息)
AVCodecParameters * av_codec_parameters = av_format_context->streams[video_stream_index]->codecpar;
//获取视频流的解码器
AVCodec *av_codec = avcodec_find_decoder(av_codec_parameters->codec_id);
//获取解码上下文
AVCodecContext * av_codec_context = avcodec_alloc_context3(av_codec);
//将解码器参数复制到解码上下文(因为解码上下文目前还没有解码器参数)
avcodec_parameters_to_context(av_codec_context,av_codec_parameters);
//进行解码
avcodec_open2(av_codec_context,av_codec,NULL);
//因为YUV数据被封装在了AVPacket中,因此我们需要用AVPacket去获取数据
AVPacket *av_packet = av_packet_alloc();
//获取转换上下文(把解码后的YUV数据转换为RGB数据才能在屏幕上显示)
SwsContext *sws_context = sws_getContext(av_codec_context->width,av_codec_context->height,av_codec_context->pix_fmt,
av_codec_context->width,av_codec_context->height,AV_PIX_FMT_RGBA,SWS_BILINEAR,
0,0,0);
//设置NativeWindow绘制的缓冲区
ANativeWindow_setBuffersGeometry(a_native_window,av_codec_context->width,av_codec_context->height,
WINDOW_FORMAT_RGBA_8888);
//绘制时,用于接收的缓冲区
ANativeWindow_Buffer a_native_window_buffer;
//计算出转换为RGB所需要的容器的大小
//接收的容器
uint8_t *dst_data[4];
//每一行的首地址(R、G、B、A四行)
int dst_line_size[4];
//进行计算
av_image_alloc(dst_data,dst_line_size,av_codec_context->width,av_codec_context->height,
AV_PIX_FMT_RGBA,1);
//从视频流中读数据包,返回值小于0的时候表示读取完毕
while (av_read_frame(av_format_context,av_packet) >= 0){
//将取出的数据发送出来
avcodec_send_packet(av_codec_context,av_packet);
//接收发送出来的数据
AVFrame *av_frame = av_frame_alloc();
int av_receive_result = avcodec_receive_frame(av_codec_context,av_frame);
//如果读取失败就重新读
if(av_receive_result == AVERROR(EAGAIN)){
continue;
} else if(av_receive_result < 0){
//如果到末尾了就结束循环读取
break;
}
//将取出的数据放到之前定义的RGB目标容器中
sws_scale(sws_context,av_frame->data,av_frame->linesize,0,av_frame->height,
dst_data,dst_line_size);
//加锁然后进行渲染
ANativeWindow_lock(a_native_window,&a_native_window_buffer,0);
uint8_t *first_window = static_cast(a_native_window_buffer.bits);
uint8_t *src_data = dst_data[0];
//拿到每行有多少个RGBA字节
int dst_stride = a_native_window_buffer.stride * 4;
int src_line_size = dst_line_size[0];
//循环遍历所得到的缓冲区数据
for(int i = 0; i < a_native_window_buffer.height;i++){
//内存拷贝进行渲染
memcpy(first_window+i*dst_stride,src_data+i*src_line_size,dst_stride);
}
//绘制完解锁
ANativeWindow_unlockAndPost(a_native_window);
//40000微秒之后解析下一帧(这个是根据视频的帧率来设置的,我这播放的视频帧率是25帧/秒)
usleep(1000 * 40);
//释放资源
av_frame_free(&av_frame);
av_free_packet(av_packet);
}
env->ReleaseStringUTFChars(path,video_path);
}
注意
●视频没声音
这里只是渲染了视频的画面数据,并没有进行声音的处理,因此没有声音是正常的
●每一帧画面的延迟时间
每一帧画面渲染的延迟时间是根据视频的信息来设置的,我案例中的视频是25帧/秒,换算出来是一帧40000微秒,否则会出现视频画面播放过快或过慢的问题
优化补充
●视频帧的延迟优化
之前的延迟时间是固定写死的这样来说不大好,因为每个视频的帧率可能不一样,这样就不能适配所有的视频,因此这里我们优化下,使用FFmpeg的API获取到视频的帧率然后计算出每帧的延迟时间,具体如下,我们通过编解码上下文获取到视频流里的avg_frame_rate其中frame_rate.num其实就是视频的帧率(因为frame_rate.den一般是1),不过我们还是用两个数值来计算下,然后我们再通过这个帧率来计算出每帧的延迟时间即可
//计算出视频的帧率
AVRational frame_rate = avFormatContext->streams[video_stream_id]->avg_frame_rate;
double fps = frame_rate.num / frame_rate.den;
//计算出延时时间(单位:微秒)
double delayTime = 1.0f / fps * 1000000;
最后延时哪里也需要改下
案例源码
https://gitee.com/itfitness/ffmpeg-build