FFmpeg环境的搭建在前面一篇博客中已经写了,详情参照:AndroidStudio3.5.1下搭建FFmpeg环境
本文仅实现将mp4的视频部分渲染到SurfaceView中, 不包含音频,不包含播放控制。文中的视频是在SD卡根目录中有一个input.flv文件,需要手动导入,AndroidManifest.xml中需要声明读取权限
public class MainActivity extends AppCompatActivity {
Player player;
private SurfaceView mSurface;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initView();
}
public void play(View view) {
File file = new File(Environment.getExternalStorageDirectory(), "input.flv");
player.play(file.getAbsolutePath());
}
private void initView() {
player = new Player();
mSurface = (SurfaceView) findViewById(R.id.surface);
player.setSurfaceView(mSurface);
}
}
player是个工具类,文章末尾会提供完整player类的代码
这样UI的部分就算完成了。 我们是要通过FFmpeg的库将视频渲染到SurfaceView,虽然native层没有SurfaceView这个类的,但是native层是有Surface的,而且可以用Surface来创建一个ANativeWindow,从而将视频渲染到界面上。 本质就是数据的copy
导入的库有
#include
#include
#include
#include
// 没有用extern "C"包括, 会提示undefined reference to 'xxx'
// extern "C"的作用是 标识库是用C写的,
extern "C" {
#include "include/libavutil/avutil.h"
#include "include/libavformat/avformat.h"
#include "include/libswscale/swscale.h"
#include "include/libavutil/imgutils.h"
#include "include/libswresample/swresample.h"
}
extern "C"
JNIEXPORT void JNICALL
Java_com_mobcb_ffmpegdemo_Player_native_1play(JNIEnv *env, jobject thiz, jstring path_,
jobject surface) {
const char *path = env->GetStringUTFChars(path_, 0);
// 初始化网络
avformat_network_init();
// 初始化总上下文
AVFormatContext *avFormatContext = avformat_alloc_context();
// 1.打开url/或者本地文件路径
// 打开时的参数设置
AVDictionary *opt = NULL;
// 设置超时参数值为3000000,这里单位是微秒,3000000微秒=3s
av_dict_set(&opt, "timeout", "3000000", 0);
// 打开url,ret=0标识成功,否则失败
int ret = avformat_open_input(&avFormatContext, path, NULL, &opt);
if (ret) {
return;
}
// 发送指令,让avFormatContext获取stream信息
// 一段视频中一般有两个流,视频流,音频流,有的还包含字幕流,这里就是要获取这些流信息
avformat_find_stream_info(avFormatContext, &opt);
// 视频流的id
int stream_index;
for (int i = 0; i < avFormatContext->nb_streams; ++i) {
// codecpar 是stream的参数信息, 包括 宽,高, 类型等
if (avFormatContext->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
// 是视频流
stream_index = i;
break;
}
}
// 获取流的参数信息
AVCodecParameters *avCodecParameters = avFormatContext->streams[stream_index]->codecpar;
// 根据流的参数信息取得解码器,相当于从map中以codec_id为key取value
AVCodec *avCodec = avcodec_find_decoder(avCodecParameters->codec_id);
// 根据解码器,创建解码器上下文
AVCodecContext *avCodecContext = avcodec_alloc_context3(avCodec);
// 将流的参数信息数据填充到解码器上下文
avcodec_parameters_to_context(avCodecContext, avCodecParameters);
// 使用解码器初始化解码器上下文
avcodec_open2(avCodecContext, avCodec, NULL);
// 声明包
AVPacket *avPacket = av_packet_alloc();
// 创建视频帧到RGBA的转换上下文
SwsContext *swsContext = sws_getContext(
avCodecContext->width,
avCodecContext->height,
avCodecContext->pix_fmt,
avCodecContext->width,
avCodecContext->height,
AV_PIX_FMT_RGBA,
SWS_BILINEAR, 0, 0, 0
);
// 创建渲染的窗体
ANativeWindow *aNativeWindow = ANativeWindow_fromSurface(env, surface);
// 窗体的缓冲区,绘制实际是上就是改变此缓冲区的内容
ANativeWindow_Buffer buffer;
// 设置缓冲区大小
ANativeWindow_setBuffersGeometry(aNativeWindow, avCodecContext->width, avCodecContext->height,
WINDOW_FORMAT_RGBA_8888);
// 以一个package为单位读取帧信息
while ((av_read_frame(avFormatContext, avPacket) >= 0)) {
// 向解码器输入pack 数据
avcodec_send_packet(avCodecContext, avPacket);
// 解码之后的帧数据
AVFrame *avFrame = av_frame_alloc();
// 读取帧数据信息
int ret1 = avcodec_receive_frame(avCodecContext, avFrame);
if (ret1 == AVERROR(EAGAIN)) {
//output is not available in this state - user must try
// * to send new input
// 需要再发送数据,队列里已没有数据可取
continue;
} else if (ret1 < 0) {
// 无数据, 退出循环
break;
}
uint8_t *dst_data[0];
int dst_linesize[0];
// 申请一块画布,并且数据和行数都用宽高和pix_fmt声明好
av_image_alloc(dst_data, dst_linesize, avCodecContext->width, avCodecContext->height,
AV_PIX_FMT_RGBA, 1);
if (avPacket->stream_index == stream_index) {
// 是视频流
if (ret1 == 0) {
// 读取帧信息成功
// 绘制
// 锁缓冲区
ANativeWindow_lock(aNativeWindow, &buffer, NULL);
// 将frame转换成统一格式,格式为av_image_alloc申请的AV_PIX_FMT_RGBA
sws_scale(swsContext,
avFrame->data,
avFrame->linesize,
0,
avFrame->height,
dst_data, dst_linesize);
// 将转换格式之后的frame 拷贝到ANativeWindow的缓冲区
// 输出到屏幕的输入源
uint8_t *srcFirstLine = dst_data[0];
// 输出到屏幕的输入源的行数
int srcStride = dst_linesize[0];
// 输出到屏幕时的跨度 是 缓冲区的跨度 * 4 , 因为一个pixel是4个字节,包含RGBA四个变量
int windowStride = buffer.stride * 4;
// 屏幕的首行地址
uint8_t *windowFirstLine = (uint8_t *) buffer.bits;
for (int i = 0; i < buffer.height; ++i) {
// 逐行拷贝数据
memcpy(windowFirstLine + i * windowStride, srcFirstLine + i * srcStride,
windowStride);
}
// 解锁缓冲区
ANativeWindow_unlockAndPost(aNativeWindow);
usleep(1000 * 16);
// 释放帧数据
av_frame_free(&avFrame);
}
}
av_packet_unref(avPacket);
// 这个地方必须要释放, 否则内存会飙升,已copy的帧内存释放掉
av_freep(&dst_data[0]);
}
ANativeWindow_release(aNativeWindow);
av_freep(avPacket);
sws_freeContext(swsContext);
avcodec_close(avCodecContext);
avformat_free_context(avFormatContext);
// 下面三个不可写,否则会闪退,上面的代码会将下面三个指针都释放掉
// av_freep(avCodec);
// av_freep(swsContext);
// av_freep(avCodecContext);
// 播放完要释放,否则会内存泄漏
av_freep(avFormatContext);
// 释放字符串
env->ReleaseStringUTFChars(path_, path);
}
public class Player implements SurfaceHolder.Callback {
static {
System.loadLibrary("native-lib");
}
private SurfaceHolder surfaceHolder;
public void setSurfaceView(SurfaceView surfaceView) {
if (null != this.surfaceHolder) {
this.surfaceHolder.removeCallback(this);
}
this.surfaceHolder = surfaceView.getHolder();
this.surfaceHolder.addCallback(this);
}
public void play(String path) {
native_play(path, surfaceHolder.getSurface());
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
this.surfaceHolder = holder;
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
}
public native void native_play(String path, Surface surface);
}