音视频技术研究
现在主流的第三方播放器都是基于FFmpeg开源库开发的。它几乎实现了所有的封装格式、传输协议以及音视频编解码功能,功能非常强大。不过FFmpeg比较复杂,研究透需要花费大量的时间,限于时间,从音视频基本概念入手,了解码率、帧率、视频格式、编码原理等,其次通过比较Android自身提供的音视频编解码和FFmpeg多媒体库的优缺点进一步了解FFmpeg,再通过阅读源码和API大致了解FFmpeg功能,最后基于FFmpeg实现简单在Android端跑的Demo。
一、音视频基础概念简介
1) 码率
码率是传输室单位时间传送的数据位数,单位是千位每秒(kbps),分为静态码率(CBR)和动态码率(VBR),静态码率的视频文件从头到尾都是恒定码率,优点运算量小,压缩快,支持设备范围广,缺点支持范围大,画质较差。动态码率没有固定的比特率,优点画面质量高,体积小,缺点算法复杂,运算量大,压缩时间长。
2) 帧率
指每秒显示的图片数或者GPU处理时更新的次数,单位是帧每秒(FPS)。
3) 位深度
表示像素的位数,位深度越大,位数越多,可用的颜色越多,画面也越逼真。
4) 视频格式
1. 封装格式
也就是常见的视频文件格式,把视频、音频、字幕等零散信息组装在一起。ts适合网络流媒体播放,一般用于直播,mp4只包含一条视频轨和音频轨,适合大多数的播放设备,mkv包含多条音频轨、视频轨、字幕等,适合网络分享。
2. 编码格式
通过特定的压缩算法,将某种视频格式的文件转化为另一种视频格式的文件,常用的编码标准有H.26X系列、MPEG系列、Divx、xvid、WMD-HD和VC-1等。
常用的视频封装格式和编码格式对应表
移动端使用最多的MP4视频文件,主要使用H.264AVC编码格式。优点主要如下:
A. 低码率:具有很高的数据压缩率,有效减少用户带宽的占用。
B. 容错能力强,提供了在不稳定网络情况下发生丢包的解决工具。
C. 网络适应性强,能在不通的网络上传输。
D. 提供连续、流畅的高质量图像。
5) H.264编码
原理在一段变化不大的图像画面,先编码出完整的图像帧A,后面的图像帧B不编码全部图像,只编出与A帧的差别,这样B帧就小很多,若果变化依旧不大,随后的C帧也按照B帧的编码方式,就这样循环下去。H.264协议定义了三种帧,完整编码的帧(I帧),参考I帧,只包含差异的帧(P帧),参考前后帧编码的帧(B帧)。核心算法是帧内压缩(生产I帧的算法),帧间压缩(生成P帧和B帧的算法)。
6) 流媒体协议
服务器与客户端之间通信遵循的规定。主要的流媒体协议如下所示:
互联网视频服务通常RTMP,MMS,HTTP这类的协议,作为其流媒体的传输层协议,因为不会发生丢包。
7) 音频编码
音频编码的主要作用是将音频采样数据(PCM等)压缩成为音频码流,从而降低音频的数据量。
8) 网络视音频平台对比
网络视音频服务主要包括点播和直播。点播是根据用户需求播放相应的节目,大部分网站都能提供这种方式。直播在网络电视台,社交视频网站常见。
A. 直播平台参数对比
通过对比,可以了解直播服务普遍采用了RTMP作为流媒体协议,FLV作为封装格式,H.264作为视频编码格式,AAC作为音频编码格式。
B. 点播平台参数对比
点播服务普遍采用了HTTP作为流媒体协议,H.264作为视频编码格式,AAC作为音频编码格式。采用HTTP优势,传输过程不会丢包,保证视频质量,大部分Web服务器支持,节约开支。
9) RTSP协议
RTSP(Real-TimeStream Protocol )是一种基于文本的应用层协议,在语法及一些消息参数等方面,RTSP协议与HTTP协议类似。
10) Android支持的格式
支持mp4、3gp、mkv和webm四种视频封装格式。webm格式是在mkv格式的基础上开发的新格式,包含了VP8/VP9视频轨。
11) Android实现视频编码
主要使用硬编码和软编码两种,硬编码直接调用GPU进行编码处理,实现方式通过Android自身提供的MediaCodec类处理音视频,软编码是使用CPU进行运算,实现方式通过集成FFmpeg多媒体库处理音视频。硬编码优点是功耗低、编解码速度快,缺点是扩展性不强、兼容性差、适用于智能家居产品、手机摄像实时取景。FFmpeg优点是封装了很多格式,灵活,兼容性好、功能强大,缺点功耗大,占用CPU较多,效率低,适用于短时间拍摄。
二、FFmpeg简介
FFmpeg是一套用来记录、转换音频、视频,并能将其转化为流的开源程序,功能非常强大,支持视频采集、视频格式转换、视频抓图、视频加水印等功能。
1) FFmpeg目录架构
上官网http://ffmpeg.org/download.html下载最新的FFmpeg源代码,目录结构如下图所示:
libavcodec用于存放各个encode/decode模块,CODEC是编码解码器的英文缩写,用于各类声音/图像编解码。
libavformat用于存放muxer/demuxer模块,对音频视频格式的解析;用于各种音视频封装格式的生成和解析,包括获取解码所需信息以生成解码上下文结构和读取音视频帧等功能。
Libavfilter用于存放滤镜和滤镜相关的操作,给视音频添加各种滤镜效果。
libavutil文件夹包含一些公共的工具函数;用于存放内存操作等辅助性模块,是一个通用的小型函数库,该库中实现了CRC校验码的生产,内存分配,格式转换等功能 。
libavdevice:对输出输入设备的支持,读取电脑的多媒体设备的数据,或者输出数据到指定的多媒体设备上。
libpostproc:用于后期效果处理。
libswscale:用于视频场景比例缩放、色彩映射转换,比如转换像素数据的格式,同时可以拉伸图像的大小。
Libswresample:音频采样数据格式转换。
2) FFmpeg结构体分析
FFmpeg最关键的结构体分为以下几类:
C. 解协议(http,rtsp,rtmp,mms)
主要是存储视音频使用的协议(AVIOContext)和输入视音频使用的封装格式(URLProtocol)。
D. 解封装(flv,avi,rmvb,mp4)
主要有存储音视频封装格式中包含的信息(AVFormatContext)和存储输入音视频使用的封装格式(AVInputFormat)
E. 解码(h264,mpeg2,aac,mp3)
每个AVStream对应一个AVCodecContext,每个AVCodecContext中对应一个AVCodec。AVStream存储视频/音频流的相关数据,AVCodecContext存储该视频/音频流使用解码方式的相关数据,AVCodec包含该视频/音频对应的解码器。
F. 存数据
视频每个结构存一帧;音频有好几帧,解码前数据:AVPacket,解码后数据:AVframe。
关系如下所示:
AVframe结构体一般用于存储原始数据(即非压缩数据,比如视频YUV,RGB)。
AVFormatContext是一个常用的结构体,很多函数都要用它作为参数,它是FFMPEG解封装(flv,mp4,rmvb,avi)功能的结构体。
AVCodecContext中很多的参数在编码时被调用。
AVIOContext管理FFMPEG输入输出数据的结构体。其中有几个变量比较重要。buffer用于存储ffmpeg读入的数据并送给解码器解码。
AVCodec用于存储编解码器信息。
AVStream是存储每一个视频/音频流信息。
AVPacket是存储压缩编码数据相关信息。
3) 库函数源代码分析
A. 通用库函数源代码分析
av_register_all()的作用是:初始化所有组件,只有调用了该函数,才能使用复用器和编解码器。
avcodec_register_all(void)会注册所有的编解码器。
av_realloc()对申请内存的大小进行调整。
av_free()用于释放申请的内存。
avio_open2()打开FFmpeg的输入输出文件。
avcodec_find_encoder()用于查找FFmpeg的编码器。
avcodec_find_decoder()用于查找FFmpeg的解码器。
avcodec_open2()创建和初始化数据结构。
avcodec_close()用于关闭编码器。
因时间有限,目前只了解主要的一些函数,其他函数方法待后续逐渐深入了解。
B. 解码库函数源代码分析
avformat_open_input打开多媒体数据,获得一些相关信息。
avformat_find_stream_info()读取一部分视音频数据并且获得一些相关的信息。
av_read_frame()读取码流中的音频若干帧或者视频一帧。
find_decoder()查找解码器。
avcodec_open2()打开解码器。
read_frame_internal()读取完整的一帧压缩编码的数据。
C. 编码库函数源代码分析
avformat_alloc_output_context2()负责分配输出AVFormatContext。
avformat_write_header()写视频文件头,av_write_frame()用于写视频数据,av_write_trailer()用于写视频文件尾。
三、Android调用FFmpeg多媒体库
1) 调用流程图
流程可以具体分为“编译FFmpeg类库”、“编写Java端代码”、“编写C语言端代码”三个步骤。
2) 编译FFmpeg类库
A. 下载安装NDK。
B. 修改FFmpeg的configure。
下载FFmpeg源代码之后,对源代码中的configure文件进行修改
SLIBNAME_WITH_MAJOR='$(SLIBPREF)$(FULLNAME)-$(LIBMAJOR)$(SLIBSUF)'
LIB_INSTALL_EXTRA_CMD='$$(RANLIB)"$(LIBDIR)/$(LIBNAME)"'
SLIB_INSTALL_NAME='$(SLIBNAME_WITH_MAJOR)'
SLIB_INSTALL_linkS='$(SLIBNAME)'
C. 生成类库
使用NDK编译configure配置脚本。脚本如下所示:
将上述脚本拷贝至ffmpeg源代码外面,成功执行之后,会将类库和头文件生成到脚本所在目录下的“simplefflib”文件夹中。
编译成功后生成类库文件
ibavformat-56.so
libavcodec-56.so
libavfilter-5.so
libavdevice-56.so
libavutil-54.so
libpostproc-53.so
libswresample-1.so
libswscale-3.so
3) 编写Anroid端代码
//JNI
public native String urlprotocolinfo();
public native String avformatinfo();
public native String avcodecinfo();
public native String avfilterinfo();
public native String configurationinfo();
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("sffhelloworld");
}
4) 编写C语言代码。
A. 获取C语言的接口函数声明
JNIEXPORT jstring JNICALL Java_com_example_hellojni_HelloJni_stringFromJNI (JNIEnv *, jobject)
B. 编写C语言接口函数代码
jstring
Java_com_example_hellojni_HelloJni_stringFromJNI( JNIEnv* env,
jobject thiz )
{
char info[10000] = { 0 };
sprintf(info, "%s\n", avcodec_configuration());
return (*env)->NewStringUTF(env, info);
}
C. 编写Android.mk
LOCAL_PATH := $(call my-dir)
# FFmpeg library
include $(CLEAR_VARS)
LOCAL_MODULE := avcodec
LOCAL_SRC_FILES := libavcodec-56.so
include $(PREBUILT_SHARED_LIBRARY)
include $(CLEAR_VARS)
LOCAL_MODULE := avdevice
LOCAL_SRC_FILES := libavdevice-56.so
include $(PREBUILT_SHARED_LIBRARY)
include $(CLEAR_VARS)
LOCAL_MODULE := avfilter
LOCAL_SRC_FILES := libavfilter-5.so
include $(PREBUILT_SHARED_LIBRARY)
include $(CLEAR_VARS)
LOCAL_MODULE := avformat
LOCAL_SRC_FILES := libavformat-56.so
include $(PREBUILT_SHARED_LIBRARY)
include $(CLEAR_VARS)
LOCAL_MODULE := avutil
LOCAL_SRC_FILES := libavutil-54.so
include $(PREBUILT_SHARED_LIBRARY)
include $(CLEAR_VARS)
LOCAL_MODULE := postproc
LOCAL_SRC_FILES := libpostproc-53.so
include $(PREBUILT_SHARED_LIBRARY)
include $(CLEAR_VARS)
LOCAL_MODULE := swresample
LOCAL_SRC_FILES := libswresample-1.so
include $(PREBUILT_SHARED_LIBRARY)
include $(CLEAR_VARS)
LOCAL_MODULE := swscale
LOCAL_SRC_FILES := libswscale-3.so
include $(PREBUILT_SHARED_LIBRARY)
# Program
include $(CLEAR_VARS)
LOCAL_MODULE := hello-jni
LOCAL_SRC_FILES := hello-jni.c
LOCAL_C_INCLUDES += $(LOCAL_PATH)/include
LOCAL_LDLIBS := -llog -lz
LOCAL_SHARED_LIBRARIES := avcodec avdevice avfilter avformat avutil postproc swresample swscale
include $(BUILD_SHARED_LIBRARY)
5) 运行ndk-build
运行ndk-build编译生成就可以通过JNI调用的类库了
#include <stdio.h>
#include "libavcodec/avcodec.h"
#include "libavformat/avformat.h"
#include "libavfilter/avfilter.h"
//Log
#ifdef ANDROID
#include <jni.h>
#include <android/log.h>
#define LOGE(format, ...) __android_log_print(ANDROID_LOG_ERROR, "(>_<)", format, ##__VA_ARGS__)
#else
#define LOGE(format, ...) printf("(>_<) " format "\n", ##__VA_ARGS__)
#endif
//FIX
struct URLProtocol;
JNIEXPORT jstring Java_com_leixiaohua1020_sffmpegandroidhelloworld_MainActivity_urlprotocolinfo(JNIEnv *env, jobject obj){
char info[40000]={0};
av_register_all();
struct URLProtocol *pup = NULL;
//Input
struct URLProtocol **p_temp = &pup;
avio_enum_protocols((void **)p_temp, 0);
while ((*p_temp) != NULL){
sprintf(info, "%s[In ][%10s]\n", info, avio_enum_protocols((void **)p_temp, 0));
}
pup = NULL;
//Output
avio_enum_protocols((void **)p_temp, 1);
while ((*p_temp) != NULL){
sprintf(info, "%s[Out][%10s]\n", info, avio_enum_protocols((void **)p_temp, 1));
}
//LOGE("%s", info);
return (*env)->NewStringUTF(env, info);
}
JNIEXPORT jstring Java_com_leixiaohua1020_sffmpegandroidhelloworld_MainActivity_avformatinfo(JNIEnv *env, jobject obj){
char info[40000] = { 0 };
av_register_all();
AVInputFormat *if_temp = av_iformat_next(NULL);
AVOutputFormat *of_temp = av_oformat_next(NULL);
//Input
while(if_temp!=NULL){
sprintf(info, "%s[In ][%10s]\n", info, if_temp->name);
if_temp=if_temp->next;
}
//Output
while (of_temp != NULL){
sprintf(info, "%s[Out][%10s]\n", info, of_temp->name);
of_temp = of_temp->next;
}
//LOGE("%s", info);
return (*env)->NewStringUTF(env, info);
}
JNIEXPORT jstring Java_com_leixiaohua1020_sffmpegandroidhelloworld_MainActivity_avcodecinfo(JNIEnv *env, jobject obj)
{
char info[40000] = { 0 };
av_register_all();
AVCodec *c_temp = av_codec_next(NULL);
while(c_temp!=NULL){
if (c_temp->decode!=NULL){
sprintf(info, "%s[Dec]", info);
}
else{
sprintf(info, "%s[Enc]", info);
}
switch (c_temp->type){
case AVMEDIA_TYPE_VIDEO:
sprintf(info, "%s[Video]", info);
break;
case AVMEDIA_TYPE_AUDIO:
sprintf(info, "%s[Audio]", info);
break;
default:
sprintf(info, "%s[Other]", info);
break;
}
sprintf(info, "%s[%10s]\n", info, c_temp->name);
c_temp=c_temp->next;
}
//LOGE("%s", info);
return (*env)->NewStringUTF(env, info);
}
JNIEXPORT jstring Java_com_leixiaohua1020_sffmpegandroidhelloworld_MainActivity_avfilterinfo(JNIEnv *env, jobject obj)
{
char info[40000] = { 0 };
avfilter_register_all();
AVFilter *f_temp = (AVFilter *)avfilter_next(NULL);
while (f_temp != NULL){
sprintf(info, "%s[%10s]\n", info, f_temp->name);
}
//LOGE("%s", info);
return (*env)->NewStringUTF(env, info);
}
JNIEXPORT jstring Java_com_leixiaohua1020_sffmpegandroidhelloworld_MainActivity_configurationinfo(JNIEnv *env, jobject obj)
{
char info[10000] = { 0 };
av_register_all();
sprintf(info, "%s\n", avcodec_configuration());
//LOGE("%s", info);
return (*env)->NewStringUTF(env, info);
}
6) 运行结果
————————————————
版权声明:本文为CSDN博主「w_y8711」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/xu13879531489/article/details/80703465