Skytoby

Android视频基础知识

Android视频基础知识

一、图像概念

像素

像素是图像的基本单元,一个个像素就组成了图像。你可以认为像素就是图像中的一个点。在下面这张图中,你可以看到一个个方块,这些方块就是像素。

分辨率

图像(或视频)的分辨率是指图像的大小或尺寸。我们一般用像素个数来表示图像的尺寸。比如说一张1920x1080的图像,前者1920指的是该图像的宽度方向上有1920个像素点,而后者1080指的是图像的高度方向上有1080个像素点。

视频行业常见的分辨率有QCIF (176x144)、CIF (352x288)、D1 (704x576或720x576),还有我们 比较熟悉的360P (640x360)、720P (1280x720)、1080P (1920x1080)、4K (3840x2160)、8K (7680x4320)等。

1 .像素就只是一个帯有颜色的小块。

2.不能简单地认为分辨率数值越高的图像就越清晰。

原始图像的话,分辨率越高确实会越清晰,但是我们看到的 图像往往是经过后期处理的,比如放大缩小,或者磨皮美颜。经过处理过后的图像,尤其是放大之后的图 像,分辨率很高,但是它并没有很清晰。

因为放大的图像是通过”插值”处理得到的,而插值的像素是使用邻近像素经过插值算法计算得到的, 跟实际相机拍摄的像素是不一样的,相当于”脑补”出来的像素值。

位深

一般来说,我们看到的彩色图像中,都有三个通道,这三个通道就是R、G、B通道,(有的时候还会有Alpha值,代表透明度)

通常R、G、B各占8个位,我们称这种图像是8bit图像,而这个8bit就是位深,位深越大,我们能够表示的颜色值就越多,目前我们大多数情 况下看到的图像以及视频还是8bit位深的。

Stride

Stride也可以称之为跨距,指的是图像存储时内存中每行像素所占用的 空间。跨距为了能够快速读取一行像素,我们一般会对内存中的图像实现内存对齐,比如16字节对齐。

举个例子,我们现在有一张RGB图像,分辨率是1278x720。我们将它存储在内存当中,一行像素需要 1278x3 = 3834个字节,3834除以16无法整除。因此,没有16字节对齐。所以如果需要对齐的话,我们需 要在3834个字节后面填充6个字节,也就是3840个字节做16字节对齐,这样这幅图像的Stride就是3840 了。如下图所示:

也就是说,每读取一行数据时候需要跳过这多余的6个字节

帧率

FPS(frame per second 每秒钟要多少帧画面)

帧率:影响画面流畅度,与画面流畅度成正比:

帧率越大,画面越流畅;

帧率越小,画面越有跳动感。

码率

编码器每秒编出的数据大小,单位是kbps,比如800kbps代表编码器每秒产生800kb(或100KB)的数据。

RGB

RGB中的值不一定是按R-G-B顺序排列的,也可能是G-B-R顺序

YUV

YUV 颜色编码采用的是 明亮度 和 色度 来指定像素的颜色。

其中,Y 表示明亮度(Luminance、Luma),而 U 和 V 表示色度(Chrominance、Chroma)。

YUV主要分为YUV 4:4:4,YUV 4:2:2,YUV 4:2:0几种常用类型。

1.YUV 4:4:4, 每一个Y对应一组UV。

2.YUV 4:2:2,每两个Y共用一组UV。

3.YUV 4:2:0,每四个Y共用一组UV。

1.YUV 4:4:4采样

1个像素存储示意图

举个例子 :

假如图像像素为:[Y0 U0 V0]、[Y1 U1 V1]、[Y2 U2 V2]、[Y3 U3 V3]

那么采样的码流为:Y0 U0 V0 Y1 U1 V1 Y2 U2 V2 Y3 U3 V3

最后映射出的像素点依旧为 [Y0 U0 V0]、[Y1 U1 V1]、[Y2 U2 V2]、[Y3 U3 V3]

这种采样方式的图像和 RGB 颜色模型的图像大小是一样,并没有达到节省带宽的目的

2.YUV4:2:2 采样

有4种类型

1)YU16 (或者称为I422、YUV422P),Planar格式

2)YV16 (YUV422P),Planar格式

3)NV16(YUV422SP),Packed格式

4)NV61(YUV422SP),Packed格式

举个例子 :

假如图像像素为:[Y0 U0 V0]、[Y1 U1 V1]、[Y2 U2 V2]、[Y3 U3 V3]

那么采样的码流为:Y0 U0 Y1 V1 Y2 U2 Y3 V3

其中,每采样过一个像素点,都会采样其 Y 分量,而 U、V 分量就会间隔一个采集一个。

最后映射出的像素点为 [Y0 U0 V1]、[Y1 U0 V1]、[Y2 U2 V3]、[Y3 U2 V3]

一张 1280 * 720 大小的图片,在 YUV 4:2:2 采样时的大小为:

(1280 720 8 + 1280 720 0.5 8 2)/ 8 / 1024 / 1024 = 1.76 MB 。

3.YUV4:2:0

1)YU12(I420,YUV420P),Planar格式

2)YV12(YUV420P),Planar格式

3)NV12(YUV420SP),Packed格式

4)NV21(YUV420SP),Packed格式

举个例子 :

假设图像像素为:

[Y0 U0 V0]、[Y1 U1 V1]、 [Y2 U2 V2]、 [Y3 U3 V3]

[Y5 U5 V5]、[Y6 U6 V6]、 [Y7 U7 V7] 、[Y8 U8 V8]

那么采样的码流为:Y0 U0 Y1 Y2 U2 Y3 Y5 V5 Y6 Y7 V7 Y8

其中,每采样过一个像素点,都会采样其 Y 分量,而 U、V 分量就会间隔一行按照 2 : 1 进行采样。

最后映射出的像素点为:

[Y0 U0 V5]、[Y1 U0 V5]、[Y2 U2 V7]、[Y3 U2 V7]

[Y5 U0 V5]、[Y6 U0 V5]、[Y7 U2 V7]、[Y8 U2 V7]

一张 1280 * 720 大小的图片,在 YUV 4:2:0 采样时的大小为:

(1280 720 8 + 1280 720 0.25 8 2)/ 8 / 1024 / 1024 = 1.32 MB 。

YUV 存储格式

YUV 的存储格式,有两种:

planar 平面格式

指先连续存储所有像素点的 Y 分量,然后存储 U 分量,最后是 V 分量。

packed 打包模式

指每个像素点的 Y、U、V 分量是连续交替存储的。

RGB 到 YUV 的转换

对于图像显示器来说,它是通过 RGB 模型来显示图像的,而在传输图像数据时又是使用 YUV 模型,这是因为 YUV 模型可以节省带宽。因此就需要采集图像时将 RGB 模型转换到 YUV 模型,显示时再将 YUV 模型转换为 RGB 模型。

RGB 到 YUV 的转换,就是将图像所有像素点的 R、G、B 分量转换到 Y、U、V 分量。

有如下公式进行转换:

二、视频编码

视频和图像和关系

大量的图像连续起来,就是视频。

衡量视频,又是用的什么指标参数呢?最主要的一个,就是帧率(Frame Rate)。在视频中,一个帧(Frame)就是指一幅静止的画面。帧率,就是指视频每秒钟包括的画面数量(FPS,Frame per second)。

码率

编码器每秒编出的数据大小,单位是kbps,比如800kbps代表编码器每秒产生800kb(或100KB)的数据。

为什么视频数据需要编码?

有了视频之后,就涉及到两个问题:

一个是存储;

二个是传输。

未经编码的视频,它的体积是非常庞大的。

以一个分辨率1920×1280,30FPS的视频为例:

共:1920×1280=2,073,600(Pixels 像素),每个像素点是24bit(前面算过的哦);

也就是:每幅图片2073600×24=49766400 bit,8 bit(位)=1 byte(字节);

所以:49766400bit=6220800byte≈6.22MB。

这是一幅1920×1280图片的原始大小,再乘以帧率30。

也就是说:每秒视频的大小是186.6MB,每分钟大约是11GB,一部90分钟的电影,约是1000GB

显然如此大的体积需要压缩,于是编码就产生了。

什么是编码?

编码:就是按指定的方法,将信息从一种形式(格式),转换成另一种形式(格式)。视频编码:就是将一种视频格式,转换成另一种视频格式。

编码的终极目的,就是为了压缩。各种视频编码方式,都是为了让视频变得体积更小,有利于存储和传输。

要实现压缩,就要设计各种算法,将视频数据中的冗余信息去除。

当你面对一张图片,或者一段视频的时候,如果是你,你会如何进行压缩呢?

我觉得,首先你想到的,应该是找规律。是的,寻找像素之间的相关性,还有不同时间的图像帧之间,它们的相关性。

举个例子:如果一幅图(1920×1080分辨率),全是红色的,我有没有必要说2073600次[255,0,0]?我只要说一次[255,0,0],然后再说2073599次“同上”。

如果一段1分钟的视频,有十几秒画面是不动的,或者,有80%的图像面积,整个过程都是不变(不动)的。那么,是不是这块存储开销,就可以节约掉了?

图像一般都是有数据冗余的,主要包括以下4种:

空间冗余。比如说将一帧图像划分成一个个16x16的块之后,相邻的块很多时候都有比较明显的相似性, 这种就叫空间冗余。

时间冗余。一个帧率为25fps的视频中前后两帧图像相差只有40ms,两张图像的变化是比较小的,相似性很高,这种叫做时间冗余。

视觉冗余。我们的眼睛是有视觉灵敏度这个东西的。人的眼睛对于图像中高频信息的敏感度是小于低频信息的。有的时候去除图像中的一些高频信息,人眼看起来跟不去除高频信息差别不大,这种叫做视觉冗余。

信息炳冗余。我们一般会使用Zip等压缩工具去压缩文件,将文件大小减小,这个对于图像来说也是可以 做的,这种冗余叫做信息嫡冗余。

各种视频压缩算法就是为了减少上面的这几种冗余。视频编码技术优先消除的目标,就是空间冗余和时间冗余。

宏块

每一帧图像,又是划分成一个个块来进行编码的,这一个个块在H264中叫做宏块,而在VP9、AV1 中称之为超级块,其实概念是一样的。宏块大小一般是16x16 (H264、VP8) , 32x32 (H265、VP9), 64x64 (H265、VP9、AV1) , 128x128 (AV1)这几种。这里提到的H264、H265、VP8、VP9和AV1都是市面上常见的编码标准。

帧内预测 帧间预测

帧内预测——基于同一帧内已编码块预测,构造预测块,计算与当前块的残差,对残差、预测模式等信息进行编码。其主要去除的是空间冗余。

帧间预测——基于一个或多个已编码帧预测,构造预测块,计算与当前块的残差,对残差、预测模式、运动矢量残差、参考图像索引等信息进行编码。其主要去除的是时间冗余。

帧类型

帧间预测需要参考已经编码的帧,帧间编码帧可以分为只参考前面帧的前向编码帧和前后都可以参考的双向编码帧

I帧:是自带全部信息的独立帧,是最完整的画面(占用的空间最大),无需参考其它图像便可独立进行解码。视频序列中的第一个帧,始终都是I帧。

P帧:“帧间预测编码帧”,需要参考前面的I帧和/或P帧的不同部分,才能进行编码。P帧对前面的P和I参考帧有依赖性。但是,P帧压缩率比较高,占用的空间较小。

B帧:“双向预测编码帧”,以前帧后帧作为参考帧。不仅参考前面,还参考后面的帧,所以,它的压缩率最高,可以达到200:1。

如图,箭头是从参考帧指向编码帧

GOP(序列)和IDR

在H264中图像以序列为单位进行组织,一个序列是一段图像编码后的数据流。

一个序列的第一个图像叫做 IDR 图像(立即刷新图像),IDR 图像都是 I 帧图像。H.264 引入 IDR 图像是为了解码的重同步,当解码器解码到 IDR 图像时,立即将参考帧队列清空,将已解码的数据全部输出或抛弃,重新查找参数集,开始一个新的序列。这样,如果前一个序列出现重大错误,在这里可以获得重新同步的机会。IDR图像之后的图像永远不会使用IDR之前的图像的数据来解码。

一个序列就是一段内容差异不太大的图像编码后生成的一串数据流。当运动变化比较少时,一个序列可以很长,因为运动变化少就代表图像画面的内容变动很小,所以就可以编一个I帧,然后一直P帧、B帧了。当运动变化多时,可能一个序列就比较短了,比如就包含一个I帧和3、4个P帧。

在视频编码序列中,GOP即Group of picture(图像组),指两个I帧之间的距离,Reference(参考周期)指两个P帧之间的距离。两个I帧之间形成一组图片,就是GOP(Group Of Picture)。

PTS和DTS

为什么会有PTS和DTS的概念?

P帧需要参考前面的I帧或P帧才可以生成一张完整的图片,而B帧则需要参考前面I帧或P帧及其后面的一个P帧才可以生成一张完整的图片。这样就带来了一个问题:在视频流中,先到来的 B 帧无法立即解码,需要等待它依赖的后面的 I、P 帧先解码完成,这样一来播放时间与解码时间不一致了,顺序打乱了,那这些帧该如何播放呢?这时就引入了另外两个概念:DTS 和 PTS。

DTS(Decoding Time Stamp):即解码时间戳,这个时间戳的意义在于告诉播放器该在什么时候解码这一帧的数据。

PTS(Presentation Time Stamp):即显示时间戳,这个时间戳用来告诉播放器该在什么时候显示这一帧的数据。

在视频采集的时候是录制一帧就编码一帧发送一帧的,在编码的时候会生成 PTS,这里需要特别注意的是 frame(帧)的编码方式,在通常的场景中,编解码器编码一个 I 帧,然后向后跳过几个帧,用编码 I 帧作为基准帧对一个未来 P 帧进行编码,然后跳回到 I 帧之后的下一个帧。编码的 I 帧和 P 帧之间的帧被编码为 B 帧。之后,编码器会再次跳过几个帧,使用第一个 P 帧作为基准帧编码另外一个 P 帧,然后再次跳回,用 B 帧填充显示序列中的空隙。这个过程不断继续,每 12 到 15 个 P 帧和 B 帧内插入一个新的 I 帧。P 帧由前一个 I 帧或 P 帧图像来预测,而 B 帧由前后的两个 P 帧或一个 I 帧和一个 P 帧来预测,因而编解码和帧的显示顺序有所不同,如下所示:

假设编码器采集到的帧是这个样子的:

I B B P B B P

那么它的显示顺序,也就是PTS应该是这样:

1 2 3 4 5 6 7

编码器的编码顺序是:

1 4 2 3 7 5 6

推流顺序也是按照编码顺序去推的,即

I P B B P B B

那么接收断收到的视频流也就是:

I P B B P B B

这时候去解码,也是按照收到的视频流一帧一帧去解的了,接收一帧解码一帧,因为在编码的时候已经按照 I、B、P 的依赖关系编好了,接收到数据直接解码就好了。那么解码顺序是:

I P B B P B B

DTS:1 2 3 4 5 6 7

PTS:1 4 2 3 7 5 6

可以看到解码出来对应的 PTS 不是顺序的,为了正确显示视频流,这时候我们就必须按照 PTS 重新调整解码后的 frame(帧),即

I B B P B B P

DTS:1 3 4 2 6 7 5

PTS:1 2 3 4 5 6 7

三、Android视频播放流程

3.1 概述

Android上经常会使用到MediaPlayer去播放音频和视频。但是从严格意义上来说,MediaPlayer并不是播放器本身,它只是Android框架层众多媒体播放器(包括ROM厂商自定义的播放器)的“壳”。

MediaPlayer的主要操作(包括但不限于播放、暂停、快进等)都会调用到框架层的播放器,比如常见的NuPlayer播放器。

3.2 MediaPlayer播放视频

MediaPlayer的使用很简单,如果是想要在一个SurfaceView上播放assets下的video.mp4视频,以下简单的代码就能实现视频画面的显示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
SurfaceView surfaceView = (SurfaceView) findViewById(R.id.surface);
surfaceView.getHolder().addCallback(new SurfaceHolder.Callback() {
@Override
public void surfaceCreated(SurfaceHolder holder) {
MediaPlayer player = new MediaPlayer();
player.setDisplay(holder); //设置画面显示在哪
try {
player.setDataSource(getAssets().openFd("video.mp4"));//设置视频源
player.prepare(); //准备视频数据
} catch (IOException e) {
e.printStackTrace();
}
player.start(); //开始播放
}

@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {

}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {

}
});

MediaPlayer的API和用法比较简单,只要熟悉掌握下面的状态图就能很方便的使用。主要涉及的状态有setDataSource、prepare、start、pause、stop、reset、release。

3.3 从MediaPlayer到NuPlayer

3.3.1 整体架构

应用里面调了MediaPlayer的方法,其实底层都会通过IPC机制调到MediaPlayerService。其实不仅是MediaPlayer,android.media包下的媒体播放接口像AudioTrack、SoundPool、MediaCodec都是会调到MediaPlayerService去做具体的编解码操作的,安卓的媒体播放是个典型的C/S架构。

其实android.media.MediaPlayer这个java类只是native层的一个代理,具体的实现都是通过jni调用到libmedia_jni.so里面的c/c++代码。比如设置音视频数据的setDataSource()方法有如下调用关系:

1
2
3
==> android.media.MediaPlayer
==> android_media_MediaPlayer.cpp
==> mediaplayer.cpp

mediaplayer.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
status_t MediaPlayer::setDataSource(const sp<IDataSource> &source)
{
ALOGV("setDataSource(IDataSource)");
status_t err = UNKNOWN_ERROR;
const sp<IMediaPlayerService> service(getMediaPlayerService());
if (service != 0) {
sp<IMediaPlayer> player(service->create(this, mAudioSessionId));
if ((NO_ERROR != doSetRetransmitEndpoint(player)) ||
(NO_ERROR != player->setDataSource(source))) {
player.clear();
}
err = attachNewPlayer(player);
}
return err;
}

MediaPlayerService会创建一个Client返回给客户端,客户端这个Client调用到MediaPlayerService的功能了。Client是MediaPlayerService的一个内部类,它继承了BnMediaPlayerService,而BnMediaPlayer又继承了BnInterface。

3.3.2 NuPlayer创建

MediaPlayerService.cpp

1
2
3
4
5
6
7
8
9
10
11
status_t MediaPlayerService::Client::setDataSource(
const sp<IDataSource> &source) {
sp<DataSource> dataSource = DataSource::CreateFromIDataSource(source);
player_type playerType = MediaPlayerFactory::getPlayerType(this, dataSource); // 对已经注册的播放器进行打分,创建得分最高的播放器(NU_PLAYER)
sp<MediaPlayerBase> p = setDataSource_pre(playerType);
if (p == NULL) {
return NO_INIT;
}
// now set data source
return mStatus = setDataSource_post(p, p->setDataSource(dataSource));
}

MediaPlayerFactory::getPlayerType:该函数涉及Android底层媒体播放器的评分机制。通过评分,获得一个最优的播放器类型。一般情况下,函数调用返回的是NuPlayer对应的播放器类型NU_PLAYER。

setDataSource_pre:该函数的作用是根据前面获得的播放器类型创建播放器对象。

setDataSource_post:将媒体资源设置给播放器,这才是真正的setDataSource操作。

(1)setDataSource()方法中,这里先来看一下getPlayerType()获取的类型。如下,在MediaPlayerInterface.h中枚举所有的播放器类型。

MediaPlayerInterface.h

1
2
3
4
5
6
7
8
enum player_type {
STAGEFRIGHT_PLAYER = 3,
NU_PLAYER = 4,
// Test players are available only in the 'test' and 'eng' builds.
// The shared library with the test player is passed passed as an
// argument to the 'test:' url in the setDataSource call.
TEST_PLAYER = 5,
};

目前只注册了NU_PLAYER和TEST_PLAYER两种播放器。 STAGEFRIGHT_PLAYER实际上指的是AwesomePlayer,在早期的安卓系统使用AwesomePlayer去播放本地视频,用NuPlayer去播放流媒体。后来因为某些原因所以逐渐用弃用了AwesomePlayer,统一使用NuPlayer去播放。在某些过度版本的安卓系统开发者选项里面还可以选择NuPlayer代替AwesomePlayer,到后期都不用选了,只有一个NuPlayer可以用。

用于创建两种播放器的NuPlayerFactory和TestPlayerFactory,在如下代码中注册的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void MediaPlayerFactory::registerBuiltinFactories() {
Mutex::Autolock lock_(&sLock);

if (sInitComplete)
return;

IFactory* factory = new NuPlayerFactory();
if (registerFactory_l(factory, NU_PLAYER) != OK)
delete factory;
factory = new TestPlayerFactory();
if (registerFactory_l(factory, TEST_PLAYER) != OK)
delete factory;

sInitComplete = true;
}

(2)紧接着执行了setDataSource_pre()方法

MediaPlayerService.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
sp<MediaPlayerBase> MediaPlayerService::Client::setDataSource_pre(
player_type playerType)
{
...
sp<MediaPlayerBase> p = createPlayer(playerType); // 根据类型创建player
if (p == NULL) {
return p;
}
...
}
sp<MediaPlayerBase> MediaPlayerService::Client::createPlayer(player_type playerType)
{
sp<MediaPlayerBase> p = mPlayer;
...
p = MediaPlayerFactory::createPlayer(playerType, this, notify, mPid);
...
return p;
}
virtual sp<MediaPlayerBase> createPlayer(pid_t pid) {
ALOGV(" create NuPlayer");
return new NuPlayerDriver(pid);
}
mPlayer(AVNuFactory::get()->createNuPlayer(pid)),
sp<NuPlayer> AVNuFactory::createNuPlayer(pid_t pid) {
return new NuPlayer(pid);
}

NuPlayerFactory创建出来的是NuPlayerDriver,不过NuPlayerDriver内部也是封装了NuPlayer。对应上述代码中的mPlayer,后续的start(),stop(),setDataSource()等都是通过该对象进行操作。

3.3.3 播放器工厂

默认情况下,只有NuPlayerFactory和TestPlayerFactory两种播放器的工厂,ROM厂商也可以根据标准进行自定义。播放器工厂的类图如下:

参考资源

https://blog.csdn.net/u012124438/article/details/123147043

https://blog.csdn.net/u012124438/article/details/123385424

https://blog.csdn.net/yihanss/article/details/130204958