音视频开发之H.264码流结构与编码原理

目前主流的H.264和H.265编码格式是由ITU和MPEG两个组织合力制定的。

在早期,ITU和MPEG两家组织都是各搞各的,ITU组织推行了H.261、H.262、H.263编码格式,而MPEG组织则推行了MPEG-1、MPEG-2、MPEG-3标准族群。后来两家组织准备合力制作新一代的视频编码标准,对于ITU组织来说,将这个新一代的编码标准命名为H.264,而对于MPEG来说,这个新一代的压缩标准只是其MPEG-4标准的第10部分,其第10部分叫做高级视频编码AVC(Advanced Video Coding)。

所以可以简单的认为H.264就是MPEG-4 AVC。

码流结构

NAL单元组成

H.264原始码流是由一个一个的NALU组成的,而NALU是由一个字节的Header和RBSP组成,而RBSP是由SODB和对齐字节数据组成(因为SODB是原始数据比特流, 长度不一定是8的倍数,需补足对齐),SODB就是真实的编码数据。

对于SODB中的数据,根据NAL单元的类型不同,存储了不同的数据,比如说对于Slice类型,其内部存储的就是Slice数据
,分为Slice Header和Slice Data。Slice Data中是由一个一个的MB(宏块)数据组成,MB又可以分为一个个的子MB,在子MB中存储了mb_type(宏块类型)、mb_pred(宏块预测类型)、coded residual(残差值)。

所以简单来看一个NAL单元是由一个字节的Header加上Slice数据组成,而Slice数据是由一个一个的宏块数据组成。

对于H.264来说,编码器会将每一帧图像都拆分成一个或多个Slice,每个Slice又分割成多个宏块,每个宏块是一个nxm大小的像素区域,每个宏块又可以切分成更小的子宏块。每个Slice编码完成后,会将当前Slice的编码数据打包成NAL单元下发出去。

H264码流结构

Annex B格式

这种格式的H.264码流用于实时流的传播中,其特点是码流中每个NALU(单元块)之间通过起始码来分割,起始码分为两种,一帧开始则用四个字节的1来表示,不是一帧开始就用三个字节的1来表示。

NAL单元类型

NAL单元的Header由一个字节的数据组成,由三个数据组成

  • forbidden_zero_bit : 1个比特,在H.264规范中规定了这一位必须为0。
  • nal_ref_idc : 2个比特,取00~11,指示这个NALU的重要性,取值越大,表示当前NAL越重要,需要优先受到保护。
  • nal_unit_type : 5个比特,表示NALU单元的类型。

音视频开发之SDL2播放视频

如果熟悉OpenGL,那么SDL来渲染视频就很简单了,Window对象时用来显示的窗口,Renderer是具体渲染的渲染器,Texture可以认为就是一张图像,不断的更新Texture的内容,将其显示在窗口上,这就完成了视频的播放。

创建Window

对于渲染视频来说,首先需要创建一个Window对象,这是与Native渲染体系相关联的一个窗口。

1
2
3
4
5
6
7
8
9
if (borderless)
flags |= SDL_WINDOW_BORDERLESS; // 去掉窗口状态栏
else
flags |= SDL_WINDOW_RESIZABLE; // 窗口是否可缩放
// 创建SDL窗口
// 参数(标题,x, y, w, h, 标记)
window = SDL_CreateWindow(program_name, SDL_WINDOWPOS_UNDEFINED,
SDL_WINDOWPOS_UNDEFINED, default_width,
default_height, flags);

创建Renderer

Renderer负责具体的渲染逻辑

1
2
3
// 创建SDL渲染器,SDL_RENDERER_ACCELERATED 使用硬件加速
// 参数(渲染的Window,渲染驱动索引,标记)
renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);

创建纹理

像素格式:
YUV、RGB
纹理访问格式:
SDL_TEXTUREACCESS_STATIC : 纹理不经常变更
SDL_TEXTUREACCESS_STREAMING : 纹理经常变更
SDL_TEXTUREACCESS_TARGET : 纹理可作为渲染的target

1
2
3
// 创建纹理 
// 参数(对应的渲染器,像素格式,纹理访问格式设置,w, h)
*texture = SDL_CreateTexture(renderer, new_format,SDL_TEXTUREACCESS_STREAMING, new_width,new_height)

什么是纹理

纹理是显卡中一段连续的内存,可以用来存储图片数据,也可以用来存储计算过程中的中间结果等任意数据。
而视频图像数据则是内存中的像素数组。

狭义地讲,纹理是存在于显存的,可以存放任意数据;是视频图像数据存在于内存的,内容是像素数组。纹理存放的不是真正的像素数据,而是存放的图像的描述信息。

所以可以清楚的明白,为什么要使用纹理,将CPU中的像素数据拷贝到GPU中进行存储,然后在GPU上进行处理后交由显示屏进行显示。

更新纹理

更新YUV格式的纹理数据,就是将YUV三个分量的数据和长度一次传入。

1
2
3
ret = SDL_UpdateYUVTexture(
*tex, NULL, frame->data[0], frame->linesize[0], frame->data[1],
frame->linesize[1], frame->data[2], frame->linesize[2]);

更新RGB格式的纹理数据

1
ret = SDL_UpdateTexture(*tex, NULL, frame->data[0], frame->linesize[0]);

显示纹理

Renderer有一个默认的渲染目标,可以通过SDL_SetRenderTarget来更改。要想将纹理的内容显示在屏幕上,需要先将纹理的内容拷贝到Renderer的默认渲染目标上,然后再通过SDL_RenderPresent来刷新屏幕。

1
2
3
4
// 将纹理上的内容拷贝到渲染器的默认渲染目标上
SDL_RenderCopy(renderer, texture, nullptr, &rect);
// 将渲染目标的内容刷新到屏幕上
SDL_RenderPresent(renderer);

音视频开发之SDL2播放音频

使用SDL来播放音频非常简单,只需要根据指定的参数开启音频设备,然后设置音频回调函数,在音频回调函数中,将数据写入到指定的buffer中即可。
对于音频播放来讲,是音频设备主动向我们要数据,而并非我们主动写入数据到音频设备,音频设备维护了一个数据缓冲区,会将数据存放在缓冲区中进行逐一播放,当缓冲区中有空位时,就会通过回调函数向我们要数据,当缓冲区中数据已经填满时,则不会回调。

开启音频设备

SDL_AudioSpec是对音频参数的组合,开启音频设备的时候需要传入一组期望的参数组合,开启成功后会返回一个真正开启的参数组合。

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
27
28
29
SDL_AudioSpec wanted_spec, spec;

// 设置SDL音频播放参数

// 设置 采样率、声道数
wanted_nb_channels = av_get_channel_layout_nb_channels(wanted_channel_layout);
wanted_spec.channels = wanted_nb_channels;
wanted_spec.freq = wanted_sample_rate;


// 采样位数
wanted_spec.format = AUDIO_S16SYS;
// 静音
wanted_spec.silence = 0;
// 缓冲区样本数量(单声道样本数量)
wanted_spec.samples =
FFMAX(SDL_AUDIO_MIN_BUFFER_SIZE,
2 << av_log2(wanted_spec.freq / SDL_AUDIO_MAX_CALLBACKS_PER_SEC));
// 回调函数
wanted_spec.callback = sdl_audio_callback;
// 回调函数参数
wanted_spec.userdata = opaque;
// 开启音频播放设备,如果开启失败,则更换参数不断重试
while (
!(audio_dev = SDL_OpenAudioDevice(NULL, 0, &wanted_spec, &spec,
SDL_AUDIO_ALLOW_FREQUENCY_CHANGE |
SDL_AUDIO_ALLOW_CHANNELS_CHANGE))) {
// 更换参数组合,重新尝试
}

回调函数

回调函数是在一个单独的线程中,第一个参数就是开启设备时设置的回调函数上下文。第二个参数就是需要写入的缓冲区,第三个参数是需要的数据字节大小。

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
27
static void sdl_audio_callback(void *opaque, Uint8 *stream, int len) {
VideoState *is = opaque;
int audio_size, len1;

while (len > 0) {
// 计算还未读取的字节大小
len1 = is->audio_buf_size - is->audio_buf_index;
// 如果为读取的字节大小大于音频设备想要的大小,则使用音频设备想要的大小
if (len1 > len) len1 = len;
// 将audio_buf中的数据copy到stream中
if (!is->muted && is->audio_buf && is->audio_volume == SDL_MIX_MAXVOLUME)
memcpy(stream, (uint8_t *)is->audio_buf + is->audio_buf_index, len1);
else {
memset(stream, 0, len1);
if (!is->muted && is->audio_buf)
SDL_MixAudioFormat(stream,
(uint8_t *)is->audio_buf + is->audio_buf_index,
AUDIO_S16SYS, len1, is->audio_volume);
}
// 减去已经传递给音频设备的buffer大小,如果剩下的还有,则再次读取传递
len -= len1;
// stream加上偏移量
stream += len1;
// 更新audio_buf的偏移量
is->audio_buf_index += len1;
}
}

音视频开发之Android播放视频

图像的显示最终都是由显示器完成的,显示器通过接收到的颜色矩阵来进行对应的显示。而颜色矩阵的产生一般有两种,一种是通过GPU来进行渲染生成,另一种是通过CPU来进行渲染生成。其中GPU比较适合来处理这件事情,所以其效率高。(硬件加速也就是指使用GPU来进行渲染加速)

在Android平台上,GPU渲染的API有两套,一套就是OpenGL-ES,另一套就是7.0后推出的Vulkan。目前使用最多的还是OpenGL-ES。

整个渲染流程中主要节点如下:
SurfaceFlinger <- SurfaceView <- Surface <- EGLSurface <- EGLContext <- OpenGL-ES

接入原生渲染体系

Android原生平台上封装了一整套View体系用来进行图像的渲染和显示,所以任何的渲染都必须基于这个体系才能正确的显示出来。

View系统中提供了两个View可以用来进行自定义渲染,一个是SurfaceView,另一个是TextureView。(关于SurfaceView和TextureView可以看我之前写的Android图形架构总览

只要将图形数据写入到SurfaceView或TextureView的Surface中,那么最终SurfaceFlinger服务就会将图形内容显示到显卡上。

所以对于播放视频来说,就是要将视频每一帧的图像数据写入到SurfaceView或TextureView的Surface中即可。

SurfaceView可以通过addCallback来接收来自原生渲染的生命周期回调,通过getSurface来获取内部的Surface对象。

1
2
this.getHolder().addCallback();
this.getHolder().getSurface()

TextureView可以通过setSurfaceTextureListener来接收生命周期回调,通过getSurfaceTexture可以获取到SurfaceTexture。

1
2
setSurfaceTextureListener();
Surface surface = new Surface(getSurfaceTexture());

Surface与EGLSurface

一个Surface对象,可以关联一个EGLSurface对象(window_surface),window_surface可以通过swap方法将其内部图像缓冲数据传入到Surface中(其它类型的EGLSurface是不可以的)。

1
2
3
4
5
6
7
// 创建一个提供给opengl-es绘制的surface(display,配置,原生window,指定属性)
if (!(eglSurface = eglCreateWindowSurface(display, config, aNativeWindow, 0))) {
return;
}

// android中创建NativeWindow
ANativeWindow *pNativeWindow = ANativeWindow_fromSurface(jenv, surface);

EGLSurface与EGLContext

EGLContext是与线程相关的,一个线程中只能激活一个EGLContext,激活的EGLContext关联一个EGLSurface,激活后,在当前线程调用OpenGL-ES的API都将作用到EGLSurface的缓冲区中。

1
2
3
4
5
// 创建context,在context中保存了opengl-es的状态信息 (display,配置,共享context的handle 一般设为null,属性)
// 一个display可以创建多个context
if (!(context = eglCreateContext(display, config, 0, context_attribs))) {
return;
}

音视频开发之Android播放音频

SDL是一个跨平台的音视频渲染库,是支持Android平台的,所以可以直接使用SDL库进行音频的播放。

但是SDL库在Android平台上的实现只有一种,就是通过JNI来调用Java层的AudioTrack来进行播放。所以如果你有其它的需求不想使用SDL库,那么可以直接使用Android平台原生API来播放音频。

Android平台音频播放API

前面录制的时候讲过,Android上音频输出的API比较繁琐,有多套实现。有Java层的实现AudioTrack,也有native层实现OpenSLES,在 Android O上又推出了新的native层实现AAudio,并且提供了Oboe库,对OpenSLES和AAudio进行了封装。

下面来详细的介绍下每套API的使用

AudioTrack

AudioTrack是Android平台提供的播放音频的Java层API,使用起来非常简单。通过设置待播放音频参数就可以创建一个AudioTrack对象,调用了play方法后,就可以开始写入数据了,通过write方法将数据写入,调用stop后就停止播放。

需要注意的是,在创建AudioTrack的时候,对于待播放的音频数据格式都已经设定好了,也就是说这个创建的AudioTrack只能播放这种格式的音频数据。所以一般在获取到音频流解码后,都需要将PCM数据进行重采样,重采样成和AudioTrack一样的数据格式,才能交给AudioTrack播放。

创建AudioTrack

AudioTrack的构造函数有两套,新的一套是在Android L推出的,老的一套已经被标记为deprecated。

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// 根据指定采样率、声道数、位数获取最小需要的缓冲区大小,后续再创建AudioTrack时设置的缓冲区大小必须大于这个值
// 如果想要将缓冲区设大一点,比如 bufferSizeFactor = 1.5
final int minBufferSizeInBytes = (int) (AudioTrack.getMinBufferSize(sampleRate, channelConfig,
AudioFormat.ENCODING_PCM_16BIT) * bufferSizeFactor);

// 一个音频帧的字节大小 = 声道数 *(位数 /8)
final int bytesPerFrame = channels * (BITS_PER_SAMPLE / 8);

// 创建buffer用来存放每一次要写入的数据,使用堆外内存,避免JNI内存拷贝
// BUFFERS_PER_SECOND = 100,预测一秒钟回调100次
// 那么44100的采样率,每次回调应该给的数据帧个数为4410个
byteBuffer = ByteBuffer.allocateDirect(bytesPerFrame * (sampleRate / BUFFERS_PER_SECOND));

AudioTrack audioTrack;
if (Build.VERSION.SDK_INT >= 21) {
// AudioFormat是对音频的格式进行封装,包括采样率、声道数、位数等。
// AudioAttributes是对播放内容的描述
audioTrack = new AudioTrack(
new AudioAttributes.Builder()
// 音频的用途
.setUsage(usageAttribute)
// 音频内容的类型
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
.build(),
new AudioFormat.Builder()
// 位数
.setEncoding(AudioFormat.ENCODING_PCM_16BIT)
// 采样率
.setSampleRate(sampleRateInHz)
// 声道数
.setChannelMask(channelConfig)
.build(),
bufferSizeInBytes,// 缓冲区大小
AudioTrack.MODE_STREAM,// 模式
// 生成新的会话ID
AudioManager.AUDIO_SESSION_ID_GENERATE );


} else {
// 构造参数:
// 音频流类型
// 采样率
// 声道数
// 位数
// 缓冲区大小
// 模式 : MODE_STATIC 预先将需要播放的音频数据读取到内存中,然后才开始播放。MODE_STREAM 边读边播,不会将数据直接加载到内存
audioTrack = new AudioTrack(AudioManager.STREAM_VOICE_CALL, sampleRateInHz, channelConfig,
AudioFormat.ENCODING_PCM_16BIT, bufferSizeInBytes, AudioTrack.MODE_STREAM);

}

if (audioTrack == null || audioTrack.getState() != AudioTrack.STATE_INITIALIZED) {
// 创建失败
}

音视频开发之Android录制视频

FFmpeg对于Android平台的视频输入设备API有一定的支持,但是比较局限,只支持Android N以上的版本,低版本无法使用。

其原因是因为FFmpeg采用的是Android N推出的native层camera API来实现的(NDK中的libcamera2ndk.so),而并非采用JNI的方式来调用Java层camera API。

Android平台视频录制API

对于视频录制API,Android平台上有两套API实现,一套是老版的Camera1,另一套是Android L之后推出的Camera2。

Camera1使用起来较为简单,但是功能相对较少,不支持多纹理输出。Camera2的API功能上虽然更加强大,但是API设计的非常底层化,不利于理解,并且YUV_420_888的数据格式,国内各大产商在实现上留下的坑太多。

Jetpack组件中,Google推出了全新的CameraX组件,对Camera1和Camera2进行了统一的封装,使得API更加简单易用。

Camera1

获取Camera设备信息
  • Camera.getNumberOfCameras() : 获取Camera设备数量
  • index : 索引就是后续会使用的CameraID
  • Camera.CameraInfo : Camera设备信息
  • Camera.getCameraInfo(index, info) : 获取指定索引的Camera设备信息,存储到info对象中
  • Camera.CameraInfo.facing : 相机面对的方向,前置还是后置 CAMERA_FACING_BACK or CAMERA_FACING_FRONT.
  • Camera.CameraInfo.orientation : 相机图像的方向,获取到的图像需要顺时针旋转该角度才能正常显示。值为0、90、180、270。(因为我们拿手机一般是竖着拿,但是摄像头可能是向左横着或向右横着被安装的)
  • Camera.open : 获取指定索引位置的Camera设备实例。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 遍历所有的camera设备
for (int index = 0; index < android.hardware.Camera.getNumberOfCameras(); ++index) {
// 获取camera设备信息
android.hardware.Camera.CameraInfo info = new android.hardware.Camera.CameraInfo();
android.hardware.Camera.getCameraInfo(index, info);
String facing =
(info.facing == android.hardware.Camera.CameraInfo.CAMERA_FACING_FRONT) ? "front" : "back";
final android.hardware.Camera camera;
try {
// 开启camera设备,获取camera实例
camera = android.hardware.Camera.open(cameraId);
} catch (RuntimeException e) {
callback.onFailure(FailureType.ERROR, e.getMessage());
return;
}
}

音视频开发之Android录制音频

由于FFmpeg一直都没有支持Android平台的音频输入设备API,所以无法使用FFmpeg在Android上录制音频。

Android平台音频录制API

Android上音频输入的API比较繁琐,有多套实现。有Java层的实现AudioRecord,也有native层实现OpenSLES,在 Android O之后又推出了新的native层实现AAudio,并且提供了Oboe库,对OpenSLES和AAudio进行了封装。

下面来详细的介绍下每套API的使用

AudioRecord

AudioRecord是Android平台提供的录制音频的Java层API,使用起来非常简单。通过设置采集参数就可以创建一个AudioRecord对象,调用了start方法后,就可以开始读取数据了,通过read方法将数据读取到指定的缓冲区中,调用stop后就停止采集。

创建AudioRecord
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
27
28
29
30
31
32
// 一个音频帧的字节大小 = 声道数 *(位数 /8)
final int bytesPerFrame = channels * (BITS_PER_SAMPLE / 8);
// BUFFERS_PER_SECOND = 100,预测一秒钟回调100次
// 那么44100的采样率,每次回调应该给的数据帧个数为4410个
final int framesPerBuffer = sampleRate / BUFFERS_PER_SECOND;
// 一次回调给的总数据字节大小 = 一次给的数据帧个数 * 一个数据帧的字节大小
// 创建一个buffer用来接收回调数据,使用堆外内存,在通过JNI传递的时候避免内存拷贝
byteBuffer = ByteBuffer.allocateDirect(bytesPerFrame * framesPerBuffer);

emptyBytes = new byte[byteBuffer.capacity()];
// 避免每次读取数据都需要传递buffer到JNI层,这里提前将buffer的地址保存在JNI层
nativeCacheDirectBufferAddress(byteBuffer, nativeAudioRecord);

final int channelConfig = channelCountToConfiguration(channels);
// 根据音频参数(采样率,声道数,位数)得到系统最小需要的缓冲区大小,创建AudioRecord时设置的缓冲区大小必须大于这个值
int minBufferSize =
AudioRecord.getMinBufferSize(sampleRate, channelConfig, AudioFormat.ENCODING_PCM_16BIT);

// BUFFER_SIZE_FACTOR = 2
// 缓冲区一般要大一点,所以下面比较了两倍的最小缓冲区和我们自己计算的缓冲区大小
// 也就是说设置的缓冲区大小最小也要是最小缓冲区的两倍。
int bufferSizeInBytes = Math.max(BUFFER_SIZE_FACTOR * minBufferSize, byteBuffer.capacity());
try {
// 根据音频参数创建AudioRecord,audioSource是指音频采集的来源
audioRecord = new AudioRecord(audioSource, sampleRate, channelConfig,
AudioFormat.ENCODING_PCM_16BIT, bufferSizeInBytes);
} catch (IllegalArgumentException e) {
return -1;
}
if (audioRecord == null || audioRecord.getState() != AudioRecord.STATE_INITIALIZED) {
return -1;
}

音视频开发之OpenGL-ES入门

名词解释

OpenGL

通俗来讲,OpenGL是一个图形库,提供了一系列操作图形图像的API。因为OpenGL只是一个规范,具体的实现一般是由显卡提供。类似的图形库还有Windows平台的DirectX。

OpenGL-ES

OpenGL-ES是专为嵌入式设备准备的一个OpenGL的裁剪版(去除了一些复杂的图元)。OpenGL-ES支持Android、iOS等平台,并且它也是WebGL的基础。

EGL

EGL是OpenGL-ES与本地窗口系统之间的一个中间层API,一般由系统实现。EGL作为OpenGL-ES与设备之间的桥梁,将OpenGL-ES绘制在设备上。

WebGL

WebGL是OpenGL-ES在浏览器上的实现,WebGL1.0基于OpenGL-ES2.0实现,WebGL2.0基于OpenGl-ES3.0实现。

图形渲染管线

图形渲染管线也叫做渲染流水线,指的是将输入的原始图形数据经过渲染管线处理,输出一帧想要的图像的过程。

在OpenGL-ES中,任何事物都在3D空间中,而屏幕和窗口却都是2D的。所以图形渲染管线主要是在将输入的3D坐标转换成2D坐标,再讲2D坐标转换成实际有颜色的像素。(将输入的3D坐标画在3D坐标系中,然后根据视锥范围截取一个2D平面,将截取的平面转换为平面坐标显示)

2D坐标精确的表示一个点在2D空间的位置;而像素是这个点的近似值,像素的显示收到屏幕分辨率的限制

与CPU不同,GPU适合处理的问题都有这样的特征,一个问题可以分解为多个相同的小问题,每个问题之间相互独立不影响,所以GPU拥有大量的处理单元,处理单元数量越多,性能也就越好。而每个单元都可以并行处理,所以GPU的并行能力非常强。

可编程的渲染管线

自OpenGL-ES 2.0开始,采用的都是可编程的渲染管线,也就是开发者可以自定义部分流水线中的着色器,使得可以更细致的控制图形渲染管线。

着色器是运行在GPU上的小程序,这些小程序在图形渲染管线的某个特定部分运行,着色器是一个非常独立的程序,是一种把输入转化为输出的程序,着色器与着色器之间无法通信。

开发者可编程的阶段有两个,一个是顶点着色器,一个是片段着色器。

OpenGL-ES渲染管线

音视频开发之Android硬件编解码

MediaCodec介绍

MediaCodec是Android平台上用来访问编码器和解码器的组件。

工作流程

mediacodec-processes

MediaCodec中维护了两个BufferQueue,一个用来存放输入数据,一个用来存放输出数据。对于编码来说,InputBufferQueue用来接收视频原始YUV数据,经过Codec编码处理后,将编码后的数据放入到OutputBufferQueue中输出。而对于解码来说,InputBufferQueue用来接收视频编码数据,经过Codec解码处理后,将解码后的视频原始YUV数据放入到OutputBufferQueue中输出。

BufferQueue的数量是有限的,每次通过dequeueInputBuffer从InputBufferQueue中获取一个空的buffer索引,将输入数据填充到该buffer,通过queueInputBuffer通知Codec该索引位置的buffer已经填充了数据,可以开始处理了。然后通过dequeueOutputBuffer从OutputBufferQueue中获取处理完毕的buffer索引,获取到该buffer中的数据进行渲染或封装,使用完后,通过releaseOutputBuffer通知Codec释放该buffer中的数据(也可以让Codec进行数据的渲染)。

MediaCodec的数据分为两种,一种是原始音视频数据,一种是压缩数据。上面讲到这两种数据都是使用bytebuffer来处理的,输入层获取到空的buffer后需要将数据copy到该buffer中,而输出层获取到buffer后也需要将数据copy出来进行消费,大量的数据copy是很影响性能的。

针对原始音视频数据,MediaCodec提供了更加高效的方式来避免了数据的copy,那就是Surface。对于解码器来说,可以通过在configure的时候,设置OutputSurface来接收输出的Buffer,这样就不需要从OutputBufferQueue中copy数据了。而对于编码器来说,可以通过createInputSurface方法创建一个用来输入的Surface,这样就不需要往InputBufferQueue里copy数据了。

mediacodec-surface-processes

数据类型

原始数据

音频原始数据类型是一个PCM音频数据帧,视频原始数据类型由color_format决定,常用的有以下两种:

  • Surface Format :这种类型的format数据,表明使用的GraphicBuffer,这是一个内存共享的缓冲区。(CodecCapabilities#COLOR_FormatSurface )。
  • YUV Format :YUV颜色格式,支持多种YUV格式(CodecCapabilities#COLOR_FormatYUV420Flexible )。

在configure的时候,对于编码器来说可以指定编码器输出的数据的format,对于解码器来说可以指定解码器输入的format。

对于编码器,如果输入的时候使用的是Surface,则format需要设置为COLOR_FormatSurface。而对于解码器来说输入的format一般从媒体文件中读取。

压缩数据

对于视频来说,buffer中是一帧的压缩数据,对于音频来说,buffer中是一个单元的压缩数据,buffer中包含的都是完整的一帧或一个单元的数据。

音视频开发之Android硬件解封装

硬件解封装

Android平台针对音视频封装提供了MediaMuxer API,支持.mp4格式的封装;针对解封装提供了MediaExtractor API,支持.mp4等格式。

API架构

frameworks/base/media 文件夹中提供了所有音视频相关的Java层API,本文中讲述的解封装API都定义在该处。而具体的服务实现都在 frameworks/av 文件夹中

android_muxer_demuxer

自7.0开始,MediaService被拆分成多个服务,每个服务都运行在各自的进程中。

MediaExtractor 解封装

MediaExtractor类是官方提供的音视频解封装类。其官方使用Demo如下所示:

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
// 创建解封装器 
MediaExtractor extractor = new MediaExtractor();
// 设置数据源
extractor.setDataSource(...);
// 遍历所有轨道
int numTracks = extractor.getTrackCount();
for (int i = 0; i < numTracks; ++i) {
MediaFormat format = extractor.getTrackFormat(i);
String mime = format.getString(MediaFormat.KEY_MIME);
// 根据轨道类型选择想要的轨道
if (weAreInterestedInThisTrack) {
extractor.selectTrack(i);
}
}
ByteBuffer inputBuffer = ByteBuffer.allocate(...)
// 读取选中轨道的样本数据
while (extractor.readSampleData(inputBuffer, ...) >= 0) {
int trackIndex = extractor.getSampleTrackIndex();
long presentationTimeUs = extractor.getSampleTime();
...
// 下一个样本
extractor.advance();
}
extractor.release();
extractor = null;

支持的格式

android_muxer_demuxer

注册解封装器

MediaExtractorService被创建的时候,会注册所有的Extractor实现。

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×