视频文件信息
我们先用格式工厂打开一个文件,查看文件信息
General
Complete name : C:/Users/3C001/Desktop/读书郎学生平板_穿越篇.mp4
Format : MPEG-4
Format profile : Base Media / Version 2
Codec ID : mp42 (isom/iso2/avc1/mp41)
File size : 10.2 MiB
Duration : 30 s 16 ms
Overall bit rate mode : Variable
Overall bit rate : 2 861 kb/s
Encoded date : UTC 2016-06-23 07:39:54
Tagged date : UTC 2016-06-23 07:39:54
Writing application : HandBrake 0.10.5 2016021100
Video
ID : 1
Format : AVC
Format/Info : Advanced Video Codec
Format profile : Main@L4
Format settings : CABAC / 4 Ref Frames
Format settings, CABAC : Yes
Format settings, ReFrames : 4 frames
Codec ID : avc1
Codec ID/Info : Advanced Video Coding
Duration : 30 s 0 ms
Bit rate : 2 692 kb/s
Width : 1 280 pixels
Height : 800 pixels
Display aspect ratio : 16:10
Frame rate mode : Constant
Frame rate : 25.000 FPS
Color space : YUV
Chroma subsampling : 4:2:0
Bit depth : 8 bits
Scan type : Progressive
Bits/(Pixel*Frame) : 0.105
Stream size : 9.63 MiB (94%)
Writing library : x264 core 142 r2479 dd79a61
Encoding settings : cabac=1 / ref=1 / deblock=1:0:0 / analyse=0x1:0x111 / me=hex / subme=2 / psy=1 / psy_rd=1.00:0.00 / mixed_ref=0 / me_range=16 / chroma_me=1 / trellis=0 / 8x8dct=0 / cqm=0 / deadzone=21,11 / fast_pskip=1 / chroma_qp_offset=0 / threads=12 / lookahead_threads=4 / sliced_threads=0 / nr=0 / decimate=1 / interlaced=0 / bluray_compat=0 / constrained_intra=0 / bframes=3 / b_pyramid=2 / b_adapt=1 / b_bias=0 / direct=1 / weightb=1 / open_gop=0 / weightp=1 / keyint=250 / keyint_min=25 / scenecut=40 / intra_refresh=0 / rc_lookahead=10 / rc=crf / mbtree=1 / crf=20.0 / qcomp=0.60 / qpmin=0 / qpmax=69 / qpstep=4 / vbv_maxrate=20000 / vbv_bufsize=25000 / crf_max=0.0 / nal_hrd=none / filler=0 / ip_ratio=1.40 / aq=1:1.00
Encoded date : UTC 2016-06-23 07:39:54
Tagged date : UTC 2016-06-23 07:39:54
Color range : Limited
Color primaries : BT.709
Transfer characteristics : BT.709
Matrix coefficients : BT.709
Codec configuration box : avcC
Audio
ID : 2
Format : AAC LC
Format/Info : Advanced Audio Codec Low Complexity
Codec ID : mp4a-40-2
Duration : 30 s 16 ms
Bit rate mode : Variable
Bit rate : 164 kb/s
Channel(s) : 2 channels
Channel layout : L R
Sampling rate : 48.0 kHz
Frame rate : 46.875 FPS (1024 SPF)
Compression mode : Lossy
Stream size : 599 KiB (6%)
Title : Stereo / Stereo
Language : English
Default : Yes
Alternate group : 1
Encoded date : UTC 2016-06-23 07:39:54
Tagged date : UTC 2016-06-23 07:39:54
我们看到,视频文件包括三部分General、Video、Audio
他们都有一个属性Format,
Format:MPEG-4,封装格式
Format:AVC,视频编码格式
Format:AAC LC,音频编码格式
上面的文件就是由音频源文件(pcm)通过AAC LC编码,视频源文件(yuv)通过AVC编码。音频编码后的文件通过MPEG-4进行封装,最终得到了mp4文件
我们播放器要播放mp4文件,需要解封装,得到音频流和视频流,这时候得到是压缩数据,需要分别对音视频进行解码,得到音视频原始数据。最后把原始数据发送给相关的设备播放出来
视频解码
java层代码
我们拿上一节的源码继续写
file(GLOB SOURCE *.cpp)
add_library( # Sets the name of the library.
native-lib
SHARED
${SOURCE})
file(GLOB SOURCE *.cpp)定义一个全局变量SOURCE,这个SOURCE包含了当前目录下所有.cpp文件的集合。所以上面的语句就会编译所有.cpp文件
接着,我们在布局中加一个SurfaceView,用来显示视频
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<SurfaceView
android:id="@+id/mysurface"
android:layout_width="match_parent"
android:layout_height="400dp" />
<TextView
android:id="@+id/sample_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="开始"
android:onClick="start"/>
</LinearLayout>
要播放视频的话,需要视频文件路径和SurfaceView,那么,我们就需要把这两个参数传到native层,SurfaceView不能直接传到native层,需要取到它的SurfaceHolder,然后通过SurfaceHolder取到surface传到native。我们把这些需要的内容封装成一个类
package com.example.ffmpegapplication;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
public class MyPlayer implements SurfaceHolder.Callback {
private SurfaceHolder surfaceHolder;
public void setSurfaceView(SurfaceView surfaceView) {
if (null != this.surfaceHolder) {
this.surfaceHolder.removeCallback(this);
}
this.surfaceHolder = surfaceView.getHolder();
this.surfaceHolder.addCallback(this);
}
@Override
public void surfaceCreated(SurfaceHolder surfaceHolder) {
}
@Override
public void surfaceChanged(SurfaceHolder surfaceHolder, int i, int i1, int i2) {
this.surfaceHolder = surfaceHolder;
}
@Override
public void surfaceDestroyed(SurfaceHolder surfaceHolder) {
}
public void start(String path) {
native_start(path,surfaceHolder.getSurface());
}
public native void native_start(String path,Surface surface);
}
ndk代码
接下就是调用ffmpeg方法来解码视频
首先,我们需要了解几个上下文(Context)
- AVFormatContext:用来获取视频流、音频流、字幕流等
- AVCodecContext:解压上下文,获取到宽高,编码格式等。能够通过这个上下文获取到一个解码器AVCodec,AVCodec就能把数据解码成原始数据
- SwsContext:转换上下文,把原始数据(yuv)显示到屏幕上(SurfaceView),也可以对视频进行旋转缩放等操作
附上代码先
#include <jni.h>
#include <string>
extern "C" {
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libswscale/swscale.h>
#include <libavutil/imgutils.h>
}
#include <android/native_window_jni.h>
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_ffmpegapplication_MainActivity_stringFromJNI(
JNIEnv *env,
jobject /* this */) {
std::string hello = "Hello from C++";
return env->NewStringUTF(av_version_info());
}
extern "C"
JNIEXPORT void JNICALL
Java_com_example_ffmpegapplication_MyPlayer_native_1start(JNIEnv *env, jobject instance,
jstring path_, jobject surface) {
const char *path = env->GetStringUTFChars(path_, 0);
ANativeWindow *nativeWindow = ANativeWindow_fromSurface(env, surface);
//初始化网络模块。可以播放直播,既可以传路径,也可以传地址
avformat_network_init();
//总上下文
AVFormatContext *pContext = avformat_alloc_context();
//字典
AVDictionary *pDictionary = NULL;
// 设置超时
// 参数:
// AVDictionary **pm 字典的地址,如果这个字典为空,那么会自动创建这个字典,
// const char *key 参数的key,
// const char *value 参数的值,注意timeout对应的值是微秒为单位,
// int flags
av_dict_set(&pDictionary, "timeout", "3000000", 0);
// 打开源文件输入流
// 参数:
// AVFormatContext **ps 总上下文,
// const char *url 视频地址或路径,
// AVInputFormat *fmt 输入参数,
// AVDictionary **options 字典,类似于hashmap,保存一些配置参数
//返回0代表成功,通用
int ret = avformat_open_input(&pContext, path, NULL, &pDictionary);
//失败,返回到java层
if(ret) {
return;
}
// 获取视频流信息,通知ffmpeg把流解析出来
// 参数:
// AVFormatContext *ic 总上下文,
// AVDictionary **options 字典 注意这里传NULL,否则会报错
avformat_find_stream_info(pContext, NULL);
//视频流索引
int vedio_stream_index = -1;
//nb_streams:视频里面流的数量,比如流0是视频,流1是音频,流2是字幕
for(int i = 0; i < pContext->nb_streams; i++) {
//codecpar:解码器参数,旧版本的是codec, codec_type:解码器类型。
if(pContext->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO){
//如果是视频流,保存索引
vedio_stream_index = i;
break;
}
}
//视频流的解码参数
AVCodecParameters *pParameters = pContext->streams[vedio_stream_index]->codecpar;
//通过解码器id找到解码器
AVCodec *pCodec = avcodec_find_decoder(pParameters->codec_id);
//创建解码器上下文
AVCodecContext *pCodecContext = avcodec_alloc_context3(pCodec);
//将解码器参数copy到解码器上下文
avcodec_parameters_to_context(pCodecContext, pParameters);
//打开解码器
avcodec_open2(pCodecContext, pCodec, &pDictionary);
//转换上下文,用来把yuv数据转换为rgb等
//int srcW, int srcH, enum AVPixelFormat srcFormat, 输入参数
//int dstW, int dstH, enum AVPixelFormat dstFormat, 输出参数
//int flags, 解压算法,SWS_BILINEAR:重视质量, SwsFilter *srcFilter,
//SwsFilter *dstFilter, const double *param
SwsContext *pSwsContext = sws_getContext(
pCodecContext->width, pCodecContext->height, pCodecContext->pix_fmt,
pCodecContext->width, pCodecContext->height, AV_PIX_FMT_RGBA,
SWS_BILINEAR, 0,0,0
);
//视频缓冲区
ANativeWindow_Buffer outBuffer;
//初始化视频缓冲区,修改大小和格式
ANativeWindow_setBuffersGeometry(nativeWindow, pCodecContext->width,
pCodecContext->height,
WINDOW_FORMAT_RGBA_8888);
//视频数据存储在一个AVPacket中 旧版本需要用户自己去malloc。新版本的可以理解为:待解码队列
AVPacket *pPacket = av_packet_alloc();
//解码后的yuv数据容器
AVFrame *pFrame = av_frame_alloc();
//rgb数据的容器
uint8_t *dst_data[4];
//每一行数据的首地址
int dst_linesize[4];
//按照长、宽、像素格式分配各个通道的内存大小以及步长(dst_linesize),内存地址保存到dst_data的指针数组中
av_image_alloc(dst_data, dst_linesize,
pCodecContext->width, pCodecContext->height, AV_PIX_FMT_RGBA, 1);
//从视频流中读取每一帧的数据包到packet, AVFormatContext *s, AVPacket *pkt.返回值大于等于0则是成功,出错或者读到末尾会返回负数
while (av_read_frame(pContext, pPacket) >= 0) {
//AVCodecContext *avctx, const AVPacket *avpkt 从待解码队列中取出视频流数据发送给解码器
avcodec_send_packet(pCodecContext, pPacket);
//AVCodecContext *avctx, AVFrame *frame 从解码器取出解码后到数据(yuv)到frame
int ret = avcodec_receive_frame(pCodecContext, pFrame);
if(ret == AVERROR(EAGAIN)) {
continue;
} else if(ret < 0) {
break;
}
//struct SwsContext *c, const uint8_t *const srcSlice[] 原yuv数据,
//const int srcStride[] 每一行的首地址, int srcSliceY 每一行的偏移量, int srcSliceH 有多少行,
//uint8_t *const dst[] 转化后的数据rgb, const int dstStride[]每一行的首地址
sws_scale(pSwsContext,pFrame->data, pFrame->linesize, 0, pFrame->height, dst_data, dst_linesize);
//取到视频缓冲区
ANativeWindow_lock(nativeWindow, &outBuffer, NULL);
// 拿到一行有多少个字节 RGBA
int destStride=outBuffer.stride*4;
uint8_t *src_data = dst_data[0];
int src_linesize = dst_linesize[0];
uint8_t *firstWindown = static_cast<uint8_t *>(outBuffer.bits);
//渲染
for (int i = 0; i < outBuffer.height; ++i) {
memcpy(firstWindown + i * destStride, src_data + i * src_linesize, destStride);
}
ANativeWindow_unlockAndPost(nativeWindow);
}
//释放rgb容器
av_freep(dst_data);
//释放yuv容器
av_frame_free(&pFrame);
//释放AVPacket
av_packet_free(&pPacket);
//释放解码器上下文
avcodec_free_context(&pCodecContext);
//关闭文件
avformat_close_input(&pContext);
//释放总上下文
avformat_free_context(pContext);
env->ReleaseStringUTFChars(path_, path);
}
我们来看一下上面的代码流程
初始化网路
- int avformat_open_input(AVFormatContext **ps, const char *url, AVInputFormat *fmt, AVDictionary **options);
初始化网络模块,ffmpeg支持网络直播。不需要的可以不用初始化
打开文件流
- AVFormatContext *avformat_alloc_context(void);
申请一个总上下文,用来获取视频流、音频流、字幕流等。
使用avformat_free_context来释放。 - int avformat_open_input(AVFormatContext **ps, const char *url, AVInputFormat *fmt, AVDictionary **options);
打开url的文件流
参数
url:可以是网络地址也可以是本地路径
fmt:指定输入的封装格式。一般传NULL
options:字典,保存一些配置参数,比如说打开文件超时等。
- int avformat_find_stream_info(AVFormatContext *ic, AVDictionary **options);
获取视频流信息,通知ffmpeg把流解析出来
注意这里options传NULL,否则会报错
到这里,视频文件信息就解析出来了,接下来要获取对应的解码器来解码相应的流
初始化解码器
一个视频文件是包括很多不同类型的流,我们要解码视频流,那么我们首先要获取到视频流的索引,通过索引,获取到视频流的解码参数
//视频流索引
int vedio_stream_index = -1;
//nb_streams:视频里面流的数量,比如流0是视频,流1是音频,流2是字幕
for(int i = 0; i < pContext->nb_streams; i++) {
//codecpar:解码器参数,旧版本的是codec, codec_type:解码器类型。
if(pContext->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO){
//如果是视频流,保存索引
vedio_stream_index = i;
break;
}
}
//视频流的解码参数
AVCodecParameters *pParameters = pContext->streams[vedio_stream_index]->codecpar;
常用的流类型有:
AVMEDIA_TYPE_VIDEO:视频流
AVMEDIA_TYPE_AUDIO:音频流
AVMEDIA_TYPE_SUBTITLE:字幕流
- AVCodec *avcodec_find_decoder(enum AVCodecID id);
通过AVCodecParameters 中的codec_id找到解码器AVCodec - AVCodecContext *avcodec_alloc_context3(const AVCodec *codec);
申请一个解码器上下文
使用avcodec_free_context释放 - int avcodec_parameters_to_context(AVCodecContext *codec,
const AVCodecParameters *par);
将解码器参数copy到解码器上下文 - int avcodec_open2(AVCodecContext *avctx, const AVCodec *codec, AVDictionary **options);
打开解码器
转换器的初始化和使用
- struct SwsContext *sws_getContext(int srcW, int srcH, enum AVPixelFormat srcFormat,
int dstW, int dstH, enum AVPixelFormat dstFormat,
int flags, SwsFilter *srcFilter,
SwsFilter *dstFilter, const double *param);
转换上下文
1.图像色彩空间转换,如把yuv数据转换为rgb
2.分辨率缩放
3.前后图像滤波处理
参数
srcW、srcH:源文件宽高
srcFormat:源文件输入参数,可以通过解码器上下文的pix_fmt获得
dstW、dstH:目标文件宽高
dstFormat:目标文件的输出参数,AV_PIX_FMT_RGBA等
flags:解压算法,比如,SWS_BILINEAR是重视质量, SWS_FAST_BILINEAR是重视速度
srcFilter:输入图像滤波器信息
dstFilter:输出图像滤波器信息
param:定义特定缩放算法需要的参数
- int sws_scale(struct SwsContext *c, const uint8_t *const srcSlice[],
const int srcStride[], int srcSliceY, int srcSliceH,
uint8_t *const dst[], const int dstStride[]);
转换数据
参数
srcSlice:原yuv数据,
srcStride:每一行的首地址
srcSliceY:每一行的偏移量
srcSliceH:有多少行
dst:转化后的数据rgb
dstStride:每一行的首地址
读取视频流
- AVPacket *av_packet_alloc(void);
申请一个AVPacket,视频的原始数据(也就是未解码的数据)先存储在一个AVPacket中 旧版本需要用户自己去malloc。新版本的可以理解为:待解码队列。
使用av_packet_free释放 - int av_read_frame(AVFormatContext *s, AVPacket *pkt);
从视频流中读取每一帧的数据包到packet, 一般通过while循环读取,返回值大于等于0则是成功,出错或者读到末尾会返回负数 - int avcodec_send_packet(AVCodecContext *avctx, const AVPacket *avpkt);
从待解码队列中取出视频流数据发送给解码器解码 - AVFrame *av_frame_alloc(void);
申请一个解码后的数据容器
使用av_frame_free释放 - int avcodec_receive_frame(AVCodecContext *avctx, AVFrame *frame);
从解码器取出解码后到数据(yuv)到frame - int av_image_alloc(uint8_t *pointers[4], int linesizes[4],
int w, int h, enum AVPixelFormat pix_fmt, int align);
按照长、宽、像素格式分配各个通道的内存大小以及步长(linesizes),内存地址保存到pointers的指针数组中
使用av_freep释放
播放数据到屏幕上
- ANativeWindow* ANativeWindow_fromSurface(JNIEnv* env, jobject surface);
通过java层传进来的surface得到一个视频播放窗口对象 - int32_t ANativeWindow_setBuffersGeometry(ANativeWindow* window,
int32_t width, int32_t height, int32_t format);
初始化视频缓冲区,修改大小和格式,width和height这两个参数指的是视频缓冲区宽高有多少个像素点。所以,如果这里的宽度填的是视频宽度的一半的话,那界面就只会显示一半宽度 - int32_t ANativeWindow_lock(ANativeWindow* window, ANativeWindow_Buffer* outBuffer,
ARect* inOutDirtyBounds);
取到视频缓冲区 - memcpy
渲染视频缓冲区,就是把rgb数据一行一行的拷贝到视频缓冲的内存 - int32_t ANativeWindow_unlockAndPost(ANativeWindow* window);
释放缓冲区
一些异常
编译后报错了
E:/workspace5_0/robot/FfmpegApplication/app/src/main/cpp/native-lib.cpp:27: error: undefined reference to 'ANativeWindow_fromSurface'
E:/workspace5_0/robot/FfmpegApplication/app/src/main/cpp/native-lib.cpp:90: error: undefined reference to 'ANativeWindow_setBuffersGeometry'
E:/workspace5_0/robot/FfmpegApplication/app/src/main/cpp/native-lib.cpp:123: error: undefined reference to 'ANativeWindow_lock'
E:/workspace5_0/robot/FfmpegApplication/app/src/main/cpp/native-lib.cpp:135: error: undefined reference to 'ANativeWindow_unlockAndPost'
这是因为,ANativeWindow只是引入了头文件,并没有去实现这个类。这是系统里的类
如果找不到这种系统库,那么就去ndk目录下寻找
ANativeWindow就在libandroid.so中,我们要把这个库连接到我们的项目中,由于是系统库,可以直接引入android
target_link_libraries( # Specifies the target library.
native-lib
avcodec avfilter avformat avutil swresample swscale
${log-lib}
android)
接着编译后,还会报错
libavcodec/cscd.c:96: error: undefined reference to 'uncompress'
libavcodec/dxa.c:250: error: undefined reference to 'uncompress'
libavcodec/exr.c:273: error: undefined reference to 'uncompress'
libavcodec/exr.c:837: error: undefined reference to 'uncompress'
libavcodec/flashsv.c:107: error: undefined reference to 'inflateEnd'
libavcodec/flashsv.c:126: error: undefined reference to 'inflateInit_'
libavcodec/flashsv.c:259: error: undefined reference to 'deflateInit_'
libavcodec/flashsv.c:261: error: undefined reference to 'deflateBound'
libavcodec/flashsv.c:262: error: undefined reference to 'deflateEnd'
libavcodec/flashsv.c:191: error: undefined reference to 'inflateReset'
libavcodec/flashsv.c:207: error: undefined reference to 'inflate'
libavcodec/flashsv.c:210: error: undefined reference to 'inflateSync'
libavcodec/flashsv.c:211: error: undefined reference to 'inflate'
libavcodec/flashsv.c:158: error: undefined reference to 'inflate'
libavcodec/flashsv.c:160: error: undefined reference to 'deflateInit_'
libavcodec/flashsv.c:166: error: undefined reference to 'deflate'
libavcodec/flashsv.c:167: error: undefined reference to 'deflateEnd'
libavcodec/flashsv.c:169: error: undefined reference to 'inflateReset'
libavcodec/flashsv.c:178: error: undefined reference to 'inflate'
libavcodec/lcldec.c:640: error: undefined reference to 'inflateEnd'
libavcodec/lcldec.c:134: error: undefined reference to 'inflateReset'
libavcodec/lcldec.c:614: error: undefined reference to 'inflateInit_'
libavcodec/mscc.c:249: error: undefined reference to 'inflateEnd'
libavcodec/mscc.c:232: error: undefined reference to 'inflateInit_'
libavcodec/mscc.c:168: error: undefined reference to 'inflateReset'
libavcodec/mwsc.c:175: error: undefined reference to 'inflateEnd'
libavcodec/mwsc.c:155: error: undefined reference to 'inflateInit_'
libavformat/utils.c:5611: error: undefined reference to 'av_bitstream_filter_filter'
libavformat/codec2.c:74: error: undefined reference to 'avpriv_codec2_mode_bit_rate'
libavformat/codec2.c:75: error: undefined reference to 'avpriv_codec2_mode_frame_size'
libavformat/codec2.c:76: error: undefined reference to 'avpriv_codec2_mode_block_align'
libavformat/http.c:681: error: undefined reference to 'inflateInit2_'
libavformat/http.c:686: error: undefined reference to 'zlibCompileFlags'
libavformat/spdifdec.c:63: error: undefined reference to 'av_adts_header_parse'
这种基本就是没有引入系统库导致的
我们引入libz.so,发现还是有一些错误
libavformat/utils.c:5611: error: undefined reference to 'av_bitstream_filter_filter'
libavformat/codec2.c:74: error: undefined reference to 'avpriv_codec2_mode_bit_rate'
libavformat/codec2.c:75: error: undefined reference to 'avpriv_codec2_mode_frame_size'
libavformat/codec2.c:76: error: undefined reference to 'avpriv_codec2_mode_block_align'
libavformat/spdifdec.c:63: error: undefined reference to 'av_adts_header_parse'
这是因为,连接库的顺序问题,我们把之前的
avcodec avfilter avformat avutil swresample swscale
改为
avfilter avformat avcodec avutil swresample swscale
target_link_libraries( # Specifies the target library.
native-lib
avfilter avformat avcodec avutil swresample swscale
${log-lib}
android
z
)
再次编译,通过了
也可以用
-Wl;--start-group
avcodec avfilter avformat avutil swresample swscale
-Wl;--end-group
这种写法就不要求链接顺序了