对于普通人而言,视频文件有许多格式,如mp4,rmvb,avi等,这存在的一个误区,让人们谈到视频就想到这些文件格式,而在音视频开发中,我们把这些文件格式称为封装格式,这些封装格式中,不仅仅包含着视频流,还包含着音频流,字幕流等,而视频流都按照视频编码标准进行编码,下面是视频编码标准
名称 推出机构 推出时间 目前使用领域
HEVC(H.265) MPEG/ITU-T 2013 研发中
H.264 MPEG/ITU-T 2003 各个领域
MPEG4 MPEG 2001 不温不火
MPEG2 MPEG 1994 数字电视
VP9 Google 2013 研发中
VP8 Google 2008 不普及
VC-1 Microsoft Inc. 2006 微软平台
那么为什么要编码呢?我们知道所有的颜色都可以用三原色(Red,Green,Blue)来合成,任何一个图像都可以由RGB组成,那么一个像素点的RGB该如何表示呢?对于一幅图像,一般使用整数表示方法来进行描述:
·整数表示:取值范围为0~255或者00~FF,8个比特表示一个子像素,32个比特表示一个像素,这就是类似于某些平台上表示图像格式的 RGBA_8888数据格式。比如,Android平台上RGB_565的表示方法为16比特模式表示一个像素,R用5个比特来表示,G用6个比特来表示,B用5个比特来表示。
那么,计算一张 1280×720的RGBA_8888图像的大小,可采用如下方式:
1280 * 720 * 4 = 3.516MB
很明显如果一个视频使用RGB来存储,那么它的文件大小将非常庞大,如果fps(1秒的视频帧数目)是24,按照一般电影的长度90分钟来计算,RGB数据格式来表示的话,它的文件大小为:
3.516MB * 24fps * 90min * 60s = 455.7GB
后来,生物学家利用人眼的特性,推出了YUV,YUV主要应用于优化彩色视频信号的传输,使其向后兼容老式黑白电视。与RGB视频信号传输相比,它最大的优点在于只需要占用极少的频宽(RGB要求三个独立的视频信号同时传输)。
“Y”表示明亮度 (Luminance或Luma),也称灰阶值;而“U”和“V”表示的则是色度 (Chrominance或Chroma),它们的作用是描述影像的色彩及饱和度, 用于指定像素的颜色。
YUV最常用的采样格式是4:2:0
如果某一行是4:2:0,那么其下一行就是4:0:2,再下一行是 4:2:0,以此类推。对于每个色度分量来说,水平方向和竖直方向的 抽样率都是2:1,所以可以说色度的抽样率是4:1。对非压缩的8比特量化的视频来说,8×4的一张图片需要占用48字节的内存
相较于RGB,我们可以计算一帧为1280×720的视频帧,用 YUV420P的格式来表示,其数据量的大小如下:
1280 * 720 * 1 + 1280 * 720 * 0.5 = 1.318MB
如果fps(1秒的视频帧数目)是24,按照一般电影的长度90分钟来 计算,那么这部电影用YUV420P的数据格式来表示的话,其数据量的 大小就是:
1.318MB * 24fps * 90min * 60s = 166.8GB
虽然相比于RGB而言,文件体积已经小了很多,但仅用这种方式来存储电影肯定是不可行的,更别说在网络上进行流媒体播放了,那么如何对电影进行存储以及流媒体播放呢?答案是 需要进行视频编码,即视频压缩
压缩分为有损压缩和无损压缩,无损压缩比2:1 ~ 3:1,有损压缩比10:1 ~ 20:1,一般都为有损压缩。主要针对以下几点进行压缩:
○ 空间冗余:图像相邻像素之间有较强的相关性
○ 时间冗余:视频序列的相邻图像之间内容相似
○ 编码冗余:不同像素值出现的概率不同
○ 视觉冗余:人的视觉系统对某些细节不敏感
○ 知识冗余:规律性的结构可由先验知识和背景知识得到
因此有了我们开始所说的视频编码标准,对视频进行压缩(编码)来保存文件,播放时对视频解压缩(解码)成YUV格式进行播放
接下来,利用之前编译集成好FFmpeg的as项目,初探视频解码:将视频转码成yuv格式
在手机中传入一个MP4视频:
在java中传入原始视频路径和转码视频路径:
package com.aruba.ffmpegapplication;
import android.os.Bundle;
import android.os.Environment;
import android.view.View;
import androidx.appcompat.app.AppCompatActivity;
import java.io.File;
public class MainActivity extends AppCompatActivity {
static {
System.loadLibrary("native-lib");
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
public native void decode(String inputFilePath, String outputFilePath);
public void click(View view) {
File input = new File(Environment.getExternalStorageDirectory(), "input.mp4");
File output = new File(Environment.getExternalStorageDirectory(), "output.yuv");
decode(input.getAbsolutePath(), output.getAbsolutePath());
}
}
编写相应的native方法:
#include
#include
#include
extern "C" {
//编码
#include "libavcodec/avcodec.h"
//封装格式处理
#include "libavformat/avformat.h"
//像素处理
#include "libswscale/swscale.h"
}
#define LOG_TAG "aruba"
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__)
extern "C"
JNIEXPORT void JNICALL
Java_com_aruba_ffmpegapplication_MainActivity_decode(JNIEnv *env, jobject instance,
jstring inputFilePath_,
jstring outputFilePath_) {
const char *inputFilePath = env->GetStringUTFChars(inputFilePath_, 0);
const char *outputFilePath = env->GetStringUTFChars(outputFilePath_, 0);
//注册FFmpeg中各大组件
av_register_all();
//打开文件
AVFormatContext *formatContext = avformat_alloc_context();
if (avformat_open_input(&formatContext, inputFilePath, NULL, NULL) != 0) {
LOGE("打开失败");
avformat_free_context(formatContext);
env->ReleaseStringUTFChars(inputFilePath_, inputFilePath);
env->ReleaseStringUTFChars(outputFilePath_, outputFilePath);
return;
}
//将文件信息填充进AVFormatContext
if (avformat_find_stream_info(formatContext, NULL) < 0) {
LOGE("获取文件信息失败");
avformat_free_context(formatContext);
env->ReleaseStringUTFChars(inputFilePath_, inputFilePath);
env->ReleaseStringUTFChars(outputFilePath_, outputFilePath);
return;
}
//获取视频流的编解码器上下文
AVCodecContext *codecContext = NULL;
for (int i = 0; i < formatContext->nb_streams; ++i) {
if (formatContext->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO) {//如果是视频流
codecContext = formatContext->streams[i]->codec;
break;
}
}
if (codecContext == NULL) {
avformat_free_context(formatContext);
env->ReleaseStringUTFChars(inputFilePath_, inputFilePath);
env->ReleaseStringUTFChars(outputFilePath_, outputFilePath);
return;
}
//根据编解码器上下文的id获取视频流解码器
AVCodec *codec = avcodec_find_decoder(codecContext->codec_id);
//打开解码器
if (avcodec_open2(codecContext, codec, NULL) < 0) {
LOGE("解码失败");
avformat_free_context(formatContext);
env->ReleaseStringUTFChars(inputFilePath_, inputFilePath);
env->ReleaseStringUTFChars(outputFilePath_, outputFilePath);
return;
}
//开始读每一帧
//存放压缩数据
AVPacket *pkt = (AVPacket *) (av_malloc(sizeof(AVPacket)));
av_init_packet(pkt);
//存放解压数据
AVFrame *picture = av_frame_alloc();
//存放转码数据
AVFrame *picture_yuv = av_frame_alloc();
//为转码数据分配内存
uint8_t *data_size = (uint8_t *) (av_malloc(
(size_t) avpicture_get_size(AV_PIX_FMT_YUV420P, codecContext->width,
codecContext->height)));
avpicture_fill((AVPicture *) picture_yuv, data_size, AV_PIX_FMT_YUV420P, codecContext->width,
codecContext->height);
int picture_ptr = 0;
//转码组件上下文,前三个参数为原视频的宽高和编码,后三个为转码后的视频宽高和编码,还可以传入过滤器对视频做处理,这边不做处理
SwsContext *swsContext = sws_getContext(codecContext->width, codecContext->height,
codecContext->pix_fmt,
codecContext->width, codecContext->height,
AV_PIX_FMT_YUV420P, SWS_BILINEAR, NULL, NULL, NULL
);
//文件
FILE *output_file = fopen(outputFilePath, "wb");
while (av_read_frame(formatContext, pkt) == 0) {//读到每一帧的压缩数据存放在AVPacket
//解码
avcodec_decode_video2(codecContext, picture, &picture_ptr, pkt);
if (picture_ptr > 0) {
LOGE("picture_ptr %d", picture_ptr);
//转码 data中存放着真实数据,linesize为一行的数据,0为转码起始位置,高度为整个画面高
sws_scale(swsContext, picture->data, picture->linesize, 0, picture->height,
picture_yuv->data, picture_yuv->linesize);
//一帧的数据大小
size_t size_y = codecContext->width * codecContext->height;
//写文件 y:u:v为4:1:1
//写y
fwrite(picture_yuv->data[0], sizeof(uint8_t), size_y, output_file);
//写u
fwrite(picture_yuv->data[1], sizeof(uint8_t), size_y / 4, output_file);
//写v
fwrite(picture_yuv->data[2], sizeof(uint8_t), size_y / 4, output_file);
}
av_free_packet(pkt);
}
//关闭文件
fclose(output_file);
//释放资源
sws_freeContext(swsContext);
av_frame_free(&picture_yuv);
av_frame_free(&picture);
avcodec_close(codecContext);
avformat_free_context(formatContext);
env->ReleaseStringUTFChars(inputFilePath_, inputFilePath);
env->ReleaseStringUTFChars(outputFilePath_, outputFilePath);
}