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

音视频开发之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实现。

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
// libDirPath 有两个,一个是 /apex/com.android.media/lib/extractors/ ,另一个是/system/lib/extractors/ 
void MediaExtractorFactory::RegisterExtractors(
const char *libDirPath, const android_dlextinfo* dlextinfo,
std::list<sp<ExtractorPlugin>> &pluginList) {
DIR *libDir = opendir(libDirPath);
if (libDir) {
struct dirent* libEntry;
while ((libEntry = readdir(libDir))) {
if (libEntry->d_name[0] == '.') {
continue;
}
String8 libPath = String8(libDirPath) + "/" + libEntry->d_name;
if (!libPath.contains("extractor.so")) {
continue;
}
// 加载 extractor.so
void *libHandle = android_dlopen_ext(
libPath.string(),
RTLD_NOW | RTLD_LOCAL, dlextinfo);

// 查找GETEXTRACTORDEF函数
GetExtractorDef getDef =
(GetExtractorDef) dlsym(libHandle, "GETEXTRACTORDEF");
// 执行GETEXTRACTORDEF函数,获取到解封装器配置,创建一个Plugin,加入到list中
RegisterExtractor(
new ExtractorPlugin(getDef(), libHandle, libPath), pluginList);
}
closedir(libDir);
} else {
ALOGE("couldn't opendir(%s)", libDirPath);
}
}

MPEG-4的GETEXTRACTORDEF函数,其中的 Sniff方法就是创建对象的构造器。

1
2
3
4
5
6
7
8
9
ExtractorDef GETEXTRACTORDEF() {
return {
EXTRACTORDEF_VERSION,
UUID("27575c67-4417-4c54-8d3d-8e626985a164"),
2, // version
"MP4 Extractor",
{ .v3 = {Sniff, extensions} },
};
}

创建解封装器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
sp<IMediaExtractor> MediaExtractorFactory::CreateFromService(
const sp<DataSource> &source, const char *mime) {

// 根据上面注册的Sniff函数,获取最合适的解封装器的CreateExtractor函数。
creator = sniff(source, &confidence, &meta, &freeMeta, plugin, &creatorVersion);
if (!creator) {
ALOGV("FAILED to autodetect media content.");
return NULL;
}

MediaExtractor *ex = nullptr;
if (creatorVersion == EXTRACTORDEF_VERSION_NDK_V1 ||
creatorVersion == EXTRACTORDEF_VERSION_NDK_V2) {
// 创建具体的解封装器实例
CMediaExtractor *ret = ((CreatorFunc)creator)(source->wrap(), meta);
if (meta != nullptr && freeMeta != nullptr) {
freeMeta(meta);
}
ex = ret != nullptr ? new MediaExtractorCUnwrapper(ret) : nullptr;
}
return CreateIMediaExtractorFromMediaExtractor(ex, source, plugin);
}
选择最合适的解封装器实例

选择具体格式的封装器是通过将DataSource依次给所有已经注册的解封装器的Sniff函数进行尝试,每次尝试都会有一个分数返回,保存在confidence中,最终会选择分数最大的做为最终的解封装器。

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
55
void *MediaExtractorFactory::sniff(
const sp<DataSource> &source, float *confidence, void **meta,
FreeMetaFunc *freeMeta, sp<ExtractorPlugin> &plugin, uint32_t *creatorVersion) {
// 初始分数为0
*confidence = 0.0f;
*meta = nullptr;
// 所有注册的解封装器实例
std::shared_ptr<std::list<sp<ExtractorPlugin>>> plugins;
{
Mutex::Autolock autoLock(gPluginMutex);
if (!gPluginsRegistered) {
return NULL;
}
plugins = gPlugins;
}
// 遍历
void *bestCreator = NULL;
for (auto it = plugins->begin(); it != plugins->end(); ++it) {
ALOGV("sniffing %s", (*it)->def.extractor_name);
float newConfidence;
void *newMeta = nullptr;
FreeMetaFunc newFreeMeta = nullptr;
// 执行对应实例的Sniff函数
void *curCreator = NULL;
if ((*it)->def.def_version == EXTRACTORDEF_VERSION_NDK_V1) {
curCreator = (void*) (*it)->def.u.v2.sniff(
source->wrap(), &newConfidence, &newMeta, &newFreeMeta);
} else if ((*it)->def.def_version == EXTRACTORDEF_VERSION_NDK_V2) {
curCreator = (void*) (*it)->def.u.v3.sniff(
source->wrap(), &newConfidence, &newMeta, &newFreeMeta);
}
// 如果实例创建成功
if (curCreator) {
// 则对比分数,分数大于上次的则将该实例作为最合适实例;
if (newConfidence > *confidence) {
*confidence = newConfidence;
if (*meta != nullptr && *freeMeta != nullptr) {
(*freeMeta)(*meta);
}
*meta = newMeta;
*freeMeta = newFreeMeta;
plugin = *it;
bestCreator = curCreator;
*creatorVersion = (*it)->def.def_version;
} else {
// 小于则释放
if (newMeta != nullptr && newFreeMeta != nullptr) {
newFreeMeta(newMeta);
}
}
}
}
// 返回分数最高的实例
return bestCreator;
}
WAV解封装器

下面就是WAV格式的解封装器的Sniff函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
static CreatorFunc Sniff(
CDataSource *source,
float *confidence,
void **,
FreeMetaFunc *) {
DataSourceHelper *helper = new DataSourceHelper(source);
char header[12];
if (helper->readAt(0, header, sizeof(header)) < (ssize_t)sizeof(header)) {
delete helper;
return NULL;
}
if (memcmp(header, "RIFF", 4) || memcmp(&header[8], "WAVE", 4)) {
delete helper;
return NULL;
}
WAVExtractor *extractor = new WAVExtractor(helper); // extractor owns the helper
int numTracks = extractor->countTracks();
delete extractor;
if (numTracks == 0) {
return NULL;
}
*confidence = 0.3f;
return CreateExtractor;
}

重要方法详解

setDataSource

​ 方法很简单,就是设置数据源,对于数据源的协议目前只支持本地文件和HTTP、HTTPS。

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
55
sp<DataSource> DataSourceFactory::CreateFromURI(
const sp<MediaHTTPService> &httpService,
const char *uri,
const KeyedVector<String8, String8> *headers,
String8 *contentType,
HTTPBase *httpSource) {
if (contentType != NULL) {
*contentType = "";
}

sp<DataSource> source;
if (!strncasecmp("file://", uri, 7)) {
// 本地文件
source = CreateFileSource(uri + 7);
} else if (!strncasecmp("http://", uri, 7) || !strncasecmp("https://", uri, 8)) {
// HTTP HTTPS
sp<HTTPBase> mediaHTTP = httpSource;
if (mediaHTTP == NULL) {
mediaHTTP = static_cast<HTTPBase *>(CreateMediaHTTP(httpService).get());
}
String8 cacheConfig;
bool disconnectAtHighwatermark = false;
KeyedVector<String8, String8> nonCacheSpecificHeaders;
if (headers != NULL) {
nonCacheSpecificHeaders = *headers;
NuCachedSource2::RemoveCacheSpecificHeaders(
&nonCacheSpecificHeaders,
&cacheConfig,
&disconnectAtHighwatermark);
}

if (mediaHTTP->connect(uri, &nonCacheSpecificHeaders) != OK) {
ALOGE("Failed to connect http source!");
return NULL;
}

if (contentType != NULL) {
*contentType = mediaHTTP->getMIMEType();
}

source = NuCachedSource2::Create(
mediaHTTP,
cacheConfig.isEmpty() ? NULL : cacheConfig.string(),
disconnectAtHighwatermark);
} else if (!strncasecmp("data:", uri, 5)) {
source = DataURISource::Create(uri);
} else {
// 如果scheme都不符合,则假设其是个本地文件
source = CreateFileSource(uri);
}
if (source == NULL || source->initCheck() != OK) {
return NULL;
}
return source;
}
selectTrack、unselectTrack

选中指定轨道和解除选中。一个媒体文件中会包含着多个轨道,比如音频轨道和视频轨道等,在MediaExtractor中对样本数据的操作都是基于指定的轨道来的,所以在操作样本数据之前,必须先选中一个轨道。

当一个轨道被选中后,后续的getSamplexxx方法都是针对该轨道,如果想换一个轨道进行处理,则必须先解除当前轨道的选中,调用unselectTrack函数。

对于如何选择想要的轨道,可以通过getTrackFormat方法来获取每一个轨道的详细信息,通过这些信息筛选出想要处理的轨道。

getTrackCount

获取当前媒体文件中的轨道数量,通常用来遍历所有的轨道。

getTrackFormat

获取指定索引的轨道的详细信息,索引的值从0开始递增。轨道信息在不同Android版本上有不同的支持,在使用的时候需要注意。

image-20200412151827474

这里有一点需要注意,就是在使用轨道的MediaFormat对象去创建解码器的时候,最好先将KEY_LEVEL的值设置为null(MediaFormat.setString(KEY_LEVEL, null)),因为这个值经常不准。

readSampleData

读取当前选中的轨道的一个样本数据,返回读取的size,这里要注意的是buffer的大小,需要足够大,否则无法一次读取完一个样本,通常可以使用MediaFormat.KEY_MAX_INPUT_SIZE来获取。

返回-1则表示当前轨道已经没有可读取的样本数据了。

getSampleTrackIndex

查询当前样本来源的轨道索引,也就是当前选中的轨道索引。如果当前轨道没有可读取的样本数据,会返回-1。

getSampleTime

返回当前样本的PTS,微秒。

getSmapleFlags

返回当前样本的标记,通过标记可以判断当前样本的一些特征。

  • SAMPLE_FLAG_ENCRYPTED 样本数据被加密
  • SAMPLE_FLAG_SYNC 样本为关键帧
seekTo

根据指定模式跳转到指定时间,只能跳转到关键帧,所以最终时间可能会和指定时间有误差。模式有三种:

  • SEEK_TO_CLOSEST_SYNC 在指定时间的前后查找最近关键帧
  • SEEK_TO_NEXT_SYNC 在指定时间的后面查找最近关键帧
  • SEEK_TO_PREVIOUS_SYNC 在指定时间的前面查找最近关键帧
advance

移动到下一个样本,每次读取完样本数据后,都需要调用该方法,此时调用getSmaplexxx方法读取到的才是下一个样本的数据。

如果当前轨道没有可读样本,则返回false。

release

释放资源

MediaMuxer 封装

MediaMuxer类是官方提供的音视频封装类。其官方使用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
26
// 创建封装器,指定输出文件和封装格式。
MediaMuxer muxer = new MediaMuxer("temp.mp4", OutputFormat.MUXER_OUTPUT_MPEG_4);
// 创建音频和视频轨道
MediaFormat audioFormat = new MediaFormat(...);
MediaFormat videoFormat = new MediaFormat(...);
// 将轨道添加到封装器中
int audioTrackIndex = muxer.addTrack(audioFormat);
int videoTrackIndex = muxer.addTrack(videoFormat);
// 创建一个buffer用来接收样本数据
ByteBuffer inputBuffer = ByteBuffer.allocate(bufferSize);
boolean finished = false;
BufferInfo bufferInfo = new BufferInfo();
// 启动封装器,启动后不可再更改
muxer.start();
while(!finished) {
// 循环写入样本数据到指定轨道中
finished = getInputBuffer(inputBuffer, isAudioSample, bufferInfo);
if (!finished) {
int currentTrackIndex = isAudioSample ? audioTrackIndex : videoTrackIndex;
muxer.writeSampleData(currentTrackIndex, inputBuffer, bufferInfo);
}
};
// 停止封装器
muxer.stop();
// 释放
muxer.release();

支持的格式

image-20200412160730708

创建封装器

封装器的创建比较简单,根据指定的OutputFormat来创建对应格式的封装器。

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
static bool isMp4Format(MediaMuxer::OutputFormat format) {
return format == MediaMuxer::OUTPUT_FORMAT_MPEG_4 ||
format == MediaMuxer::OUTPUT_FORMAT_THREE_GPP ||
format == MediaMuxer::OUTPUT_FORMAT_HEIF;
}

MediaMuxer::MediaMuxer(int fd, OutputFormat format)
: mFormat(format),
mState(UNINITIALIZED) {
// 根据指定format创建对应的writer
if (isMp4Format(format)) {
mWriter = new MPEG4Writer(fd);
} else if (format == OUTPUT_FORMAT_WEBM) {
mWriter = new WebmWriter(fd);
} else if (format == OUTPUT_FORMAT_OGG) {
mWriter = new OggWriter(fd);
}

if (mWriter != NULL) {
mFileMeta = new MetaData;
if (format == OUTPUT_FORMAT_HEIF) {
// Note that the key uses recorder file types.
mFileMeta->setInt32(kKeyFileType, output_format::OUTPUT_FORMAT_HEIF);
} else if (format == OUTPUT_FORMAT_OGG) {
mFileMeta->setInt32(kKeyFileType, output_format::OUTPUT_FORMAT_OGG);
}
mState = INITIALIZED;
}
}

重要方法详解

addTrack

创建了封装器后,需要添加对应的轨道,轨道的信息通过MediaFormat来描述,也就是上面解封装器中通过getTrackFormat方法获取到的信息,通过这些信息添加一个轨道,添加成功返回轨道的索引,后续write数据的时候使用该索引即可。

对于音频轨道,需要添加的是KEY_SAMPLE_RATE 采样率以及KEY_CHANNEL_COUT声道数,需要注意的是CSD的添加。

对于视频轨道,处理必要的KEY_WIDTH和KEY_HEIGHT外,需要注意的有KEY_BIT_RATE码率和CSD的添加。

关于CSD这里简单介绍下,也就是Codec-Specific Data,对于H.264来说,csd-0的数据为SPS序列参数集,csd-1的数据为PPS图像参数集,而对于AAC来说,csd-0的数据为ESDS。

那么对于MediaFormat的key在不同版本中的支持情况与接封装器中的又不太一样,使用的时候需要注意参考下表。

image-20200412162101726

writeSampleData

将编码后的样本数据写入到封装器中的指定轨道中,参数为轨道索引、数据buffer、bufferInfo。buffer和bufferInfo都是通过MediaCodec编码后产生的,需要注意的是bufferInfo的flag标记.

  • BUFFER_FLAG_CODEC_CONFIG 表明buffer中包含CSD数据,而并非样本数据。
  • BUFFER_FLAG_END_OF_STREAM 表明流结束。
  • BUFFER_FLAG_KEY_FRAME 关键帧
  • BUFFER_FLAG_SYNC_FRAME 关键帧,API21废弃
  • BUFFER_FLAG_PARTIAL_FRAME 表明当前数据并非完整的一帧,API26添加

这里需要注意的一点是,CSD数据是不能通过该方法来写入的,所以在调用该方法的时候需要判断bufferInfo的flag是否有BUFFER_FLAG_CODEC_CONFIG标记,没有BUFFER_FLAG_CODEC_CONFIG标记的数据才可以用该方法写入。CSD数据需要通过添加到轨道的MediaFormat中,跟随MediaFormt被addTrack到封装器中。

start

启动封装器,启动后无法再添加轨道。

stop

停止封装器。

release

释放封装器。

MediaMetadataRetriever 获取元信息 (扩展)

MediaMetadataRetriever是用于获取媒体文件信息和指定帧数据的API。

重要方法详解

extractMetadata

查询指定的媒体信息。常用的有以下几种KEY,需要注意返回的值很有可能为空。

  • METADATA_KEY_BITRATE 如果可用,此键检索平均比特率(以比特/秒为单位)。
  • METADATA_KEY_CAPTURE_FRAMERATE 该键可检索原始捕获帧率(如果可用)
  • METADATA_KEY_DATE 用于检索数据源创建或修改日期的元数据键。
  • METADATA_KEY_HAS_AUDIO 如果此键存在,则媒体包含音频内容。
  • METADATA_KEY_HAS_VIDEO 如果该键存在,则媒体包含视频内容。
  • METADATA_KEY_LOCATION 该键检索位置信息(如果可用)。
  • METADATA_KEY_MIMETYPE 用于检索数据源的MIME类型的元数据密钥。
  • METADATA_KEY_TITLE 用于检索数据源标题的元数据键。
  • METADATA_KEY_VIDEO_HEIGHT 如果媒体包含视频,则此键检索其高度。
  • METADATA_KEY_VIDEO_ROTATION 如果可用,此键以度数检索视频旋转角度。
  • METADATA_KEY_VIDEO_WIDTH 如果媒体包含视频,则此键检索其宽度。
getFrameAtTime

根据指定模式,返回接近指定时间的关键帧数据,以bitmap格式返回。一般使用该方法获取封面图。

  • OPTION_CLOSEST_SYNC 在指定时间的前后查找最近关键帧
  • OPTION_NEXT_SYNC 在指定时间的后面查找最近关键帧
  • OPTION_PREVIOUS_SYNC 在指定时间的前面查找最近关键帧

Your browser is out-of-date!

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

×