###前言
上一篇文章我们对视频进行了解码,那么这次我们队解码后的数据进行播放。也就是绘制到界面上。
###视频播放
####创建自动以SurfaceView
因为视频是需要快速实时刷新界面的,因此要用到SurfaceView。
public class VideoView extends SurfaceView {
public VideoView(Context context) {
this(context, null, 0);
}
public VideoView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public VideoView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
//初始化像素绘制的格式为RGBA_8888(色彩最丰富)
SurfaceHolder holder = getHolder();
holder.setFormat(PixelFormat.RGBA_8888);
}
}
复制代码
这里我们自定义了一个SurfaceView,指定输出格式为RGBA_8888,这种格式色彩丰富度最高的。
然后添加到根布局当中:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.nan.ffmpeg.view.VideoView
android:id="@+id/sv_video"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<Button
android:id="@+id/btn_play"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="播放" />
</FrameLayout>
复制代码
####创建播放控制器类
public class VideoPlayer {
static {
System.loadLibrary("avutil-54");
System.loadLibrary("swresample-1");
System.loadLibrary("avcodec-56");
System.loadLibrary("avformat-56");
System.loadLibrary("swscale-3");
System.loadLibrary("postproc-53");
System.loadLibrary("avfilter-5");
System.loadLibrary("avdevice-56");
System.loadLibrary("ffmpeg-lib");
}
public native void render(String input, Surface surface);
}
复制代码
####native方法实现
//编码
#include "libavcodec/avcodec.h"
//封装格式处理
#include "libavformat/avformat.h"
//像素处理
#include "libswscale/swscale.h"
//使用这两个Window相关的头文件需要在CMake脚本中引入android库
#include <android/native_window_jni.h>
#include <android/native_window.h>
#include "libyuv.h"
JNIEXPORT void JNICALL
Java_com_nan_ffmpeg_utils_VideoPlayer_render(JNIEnv *env, jobject instance, jstring input_,
jobject surface) {
//需要转码的视频文件(输入的视频文件)
const char *input = (*env)->GetStringUTFChars(env, input_, 0);
//1.注册所有组件,例如初始化一些全局的变量、初始化网络等等
av_register_all();
//avcodec_register_all();
//封装格式上下文,统领全局的结构体,保存了视频文件封装格式的相关信息
AVFormatContext *pFormatCtx = avformat_alloc_context();
//2.打开输入视频文件
if (avformat_open_input(&pFormatCtx, input, NULL, NULL) != 0) {
LOGE("%s", "无法打开输入视频文件");
return;
}
//3.获取视频文件信息,例如得到视频的宽高
//第二个参数是一个字典,表示你需要获取什么信息,比如视频的元数据
if (avformat_find_stream_info(pFormatCtx, NULL) < 0) {
LOGE("%s", "无法获取视频文件信息");
return;
}
//获取视频流的索引位置
//遍历所有类型的流(音频流、视频流、字幕流),找到视频流
int v_stream_idx = -1;
int i = 0;
//number of streams
for (; i < pFormatCtx->nb_streams; i++) {
//流的类型
if (pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO) {
v_stream_idx = i;
break;
}
}
if (v_stream_idx == -1) {
LOGE("%s", "找不到视频流\n");
return;
}
//只有知道视频的编码方式,才能够根据编码方式去找到解码器
//获取视频流中的编解码上下文
AVCodecContext *pCodecCtx = pFormatCtx->streams[v_stream_idx]->codec;
//4.根据编解码上下文中的编码id查找对应的解码器
AVCodec *pCodec = avcodec_find_decoder(pCodecCtx->codec_id);
//(迅雷看看,找不到解码器,临时下载一个解码器,比如视频加密了)
if (pCodec == NULL) {
LOGE("%s", "找不到解码器,或者视频已加密\n");
return;
}
//5.打开解码器,解码器有问题(比如说我们编译FFmpeg的时候没有编译对应类型的解码器)
if (avcodec_open2(pCodecCtx, pCodec, NULL) < 0) {
LOGE("%s", "解码器无法打开\n");
return;
}
//准备读取
//AVPacket用于存储一帧一帧的压缩数据(H264)
//缓冲区,开辟空间
AVPacket *packet = (AVPacket *) av_malloc(sizeof(AVPacket));
//AVFrame用于存储解码后的像素数据(YUV)
//内存分配
AVFrame *yuv_frame = av_frame_alloc();
AVFrame *rgb_frame = av_frame_alloc();
int got_picture, ret;
int frame_count = 0;
//窗体
ANativeWindow *pWindow = ANativeWindow_fromSurface(env, surface);
//绘制时的缓冲区
ANativeWindow_Buffer out_buffer;
//6.一帧一帧的读取压缩数据
while (av_read_frame(pFormatCtx, packet) >= 0) {
//只要视频压缩数据(根据流的索引位置判断)
if (packet->stream_index == v_stream_idx) {
//7.解码一帧视频压缩数据,得到视频像素数据
ret = avcodec_decode_video2(pCodecCtx, yuv_frame, &got_picture, packet);
if (ret < 0) {
LOGE("%s", "解码错误");
return;
}
//为0说明解码完成,非0正在解码
if (got_picture) {
//1、lock window
//设置缓冲区的属性:宽高、像素格式(需要与Java层的格式一致)
ANativeWindow_setBuffersGeometry(pWindow, pCodecCtx->width, pCodecCtx->height,
WINDOW_FORMAT_RGBA_8888);
ANativeWindow_lock(pWindow, &out_buffer, NULL);
//2、fix buffer
//初始化缓冲区
//设置属性,像素格式、宽高
//rgb_frame的缓冲区就是Window的缓冲区,同一个,解锁的时候就会进行绘制
avpicture_fill((AVPicture *) rgb_frame, out_buffer.bits, AV_PIX_FMT_RGBA,
pCodecCtx->width,
pCodecCtx->height);
//YUV格式的数据转换成RGBA 8888格式的数据
//FFmpeg可以转,但是会有问题,因此我们使用libyuv这个库来做
//https://chromium.googlesource.com/external/libyuv
//参数分别是数据、对应一行的大小
//I420ToARGB(yuv_frame->data[0], yuv_frame->linesize[0],
// yuv_frame->data[1], yuv_frame->linesize[1],
// yuv_frame->data[2], yuv_frame->linesize[2],
// rgb_frame->data[0], rgb_frame->linesize[0],
// pCodecCtx->width, pCodecCtx->height);
I420ToARGB(yuv_frame->data[0], yuv_frame->linesize[0],
yuv_frame->data[2], yuv_frame->linesize[2],
yuv_frame->data[1], yuv_frame->linesize[1],
rgb_frame->data[0], rgb_frame->linesize[0],
pCodecCtx->width, pCodecCtx->height);
//3、unlock window
ANativeWindow_unlockAndPost(pWindow);
frame_count++;
LOGI("解码绘制第%d帧", frame_count);
}
}
//释放资源
av_free_packet(packet);
}
av_frame_free(&yuv_frame);
avcodec_close(pCodecCtx);
avformat_free_context(pFormatCtx);
(*env)->ReleaseStringUTFChars(env, input_, input);
}
复制代码
代码里面需要注意的是,我们使用了yuvlib这个库对YUV数据转换为RGB数据。这个库的下载地址是https://chromium.googlesource.com/external/libyuv。
修改Android.mk文件最后一行,由输出静态库改为输出动态库:
include $(BUILD_SHARED_LIBRARY)
复制代码
自己新建jni目录,把所有文件拷贝到里面,Linux执行下面的命令编译yuvlib:
ndk-build jni
复制代码
然后就会输出预编译的so库,并且在Android Studio中使用了。CMake脚本需要添加:
#YUV转RGB需要的库
add_library( yuv
SHARED
IMPORTED
)
set_target_properties(
yuv
PROPERTIES IMPORTED_LOCATION
${path_project}/app/libs/${ANDROID_ABI}/libyuv.so
)
复制代码
如果需要的话设置一些头文件的包含路径:
#配置头文件的包含路径
include_directories(${path_project}/app/src/main/cpp/include/yuv)
复制代码
最后在使用的时候包含对应的头文件即可:
#include "libyuv.h"
复制代码
还有一个注意的地方就是我们要使用到窗口的原生绘制,那么就需要引入window相关的头文件:
//使用这两个Window相关的头文件需要在CMake脚本中引入android库
#include <android/native_window_jni.h>
#include <android/native_window.h>
复制代码
这些头文件使用需要把android这个库编译进来,使用方法跟log库一样:
#找到Android的Window绘制相关的库(这个库已经在Android平台中了)
find_library(
android-lib
android
)
复制代码
记得链接到自己的库里面:
#把需要的库链接到自己的库中
target_link_libraries(
ffmpeg-lib
${log-lib}
${android-lib}
avutil
swresample
avcodec
avformat
swscale
postproc
avfilter
avdevice
yuv
)
复制代码
####窗口的原生绘制流程
绘制需要一个surface对象。
原生绘制步骤:
- lock Window。
- 初始化缓冲区,设置大小,缓冲区赋值。
- 解锁然后就绘制到窗口中了。
####测试
@Override
public void onClick(View v) {
String inputVideo = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separatorChar
+ "input.mp4";
switch (v.getId()) {
case R.id.btn_play:
sv_video = (SurfaceView) findViewById(R.id.sv_video);
//native方法中传入SurfaceView的Surface对象,在底层进行绘制
mPlayer.render(inputVideo, sv_video.getHolder().getSurface());
break;
}
}
复制代码