Android 系统软解码方案实现
本文档主要介绍 Android 自定义媒体提取器和解码器的实现方法。自定义媒体提取器的目标是支持更多的视频封装格式,比如:avi
、rm
、rmvb
等。自定义解码器的目标是支持更多视频解码格式,比如:wmv1
、wmv2
、RV10
、RV20
等。最终使系统原生播放器能播放所有主流的媒体文件,第三方播放器仅需调用标准的系统接口,即可实现所有主流媒体格式的兼容。
视频播放基本过程
在介绍具体实现代码前,先说明一下播放视频的一般过程:
- 解封装:从文件中分离出音频和视频的编码数据,并以数据包的形式输出。
- 解码
- 视频:将编码后的图片数据包还原成原始图片(YUV/RGB)。
- 音频:将编码后的音频数据包还原成 PCM 数据。
- 同步:根据时间戳将声音和画面进行一一对应。
- 显卡:负责将图片送给显示器显示。
- 声卡:负责将 PCM 数据转换成模拟信号驱动扬声器发声。
其中最核心部分是解封装和解码,在 Android 系统中,分别由媒体提取器 (MediaExtractor
) 和解码器 (MediaCodec
)负责这部分工作。
自定义媒体提取器
高通平台默认不支持 avi
格式的视频文件播放,如果需要播放 avi
视频,则需要自定义媒体提取器插件库。
加载插件库
MediaExtractorService
服务负责加载和管理所有媒体提取器插件,服务启动时会通过工厂类 (MediaExtractorFactory
) 加载媒体提取器插件库。
void MediaExtractorFactory::LoadExtractors() {...std::shared_ptr<std::list<sp<ExtractorPlugin>>> newList(new std::list<sp<ExtractorPlugin>>());android_namespace_t *mediaNs = android_get_exported_namespace("media");if (mediaNs != NULL) {const android_dlextinfo dlextinfo = {.flags = ANDROID_DLEXT_USE_NAMESPACE,.library_namespace = mediaNs,};RegisterExtractors("/apex/com.android.media/lib"
#ifdef __LP64__"64"
#endif"/extractors", &dlextinfo, *newList);} else {ALOGE("couldn't find media namespace.");}RegisterExtractors("/system/lib"
#ifdef __LP64__"64"
#endif"/extractors", NULL, *newList);newList->sort(compareFunc);gPlugins = newList;...gPluginsRegistered = true;
}
加载库的工作由 RegisterExtractors
函数完成,此函数第一个参数是文件的搜索路径,第二参数是保存插件引用的列表。从以上代码可以知道插件库搜索路径为:
/apex/com.android.media/lib[64]/
/system/lib[64]/extractors/
具体的 RegisterExtractors
代码实现如下:
void MediaExtractorFactory::RegisterExtractors(const char *libDirPath, const android_dlextinfo* dlextinfo,std::list<sp<ExtractorPlugin>> &pluginList) {ALOGV("search for plugins at %s", libDirPath);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;}void *libHandle = android_dlopen_ext(libPath.string(),RTLD_NOW | RTLD_LOCAL, dlextinfo);CHECK(libHandle != nullptr)<< "couldn't dlopen(" << libPath.string() << ") " << strerror(errno);GetExtractorDef getDef =(GetExtractorDef) dlsym(libHandle, "GETEXTRACTORDEF");CHECK(getDef != nullptr)<< libPath.string() << " does not contain sniffer";ALOGV("registering sniffer for %s", libPath.string());RegisterExtractor(new ExtractorPlugin(getDef(), libHandle, libPath), pluginList);}closedir(libDir);} else {ALOGE("couldn't opendir(%s)", libDirPath);}
}
首先遍历目录下所有的文件,判断文件名是否包含 extractor.so
字符串,如果包含则认为是媒体提取库文件。然后调用 android_dlopen_ext
打开此动态库,并找到库中的 GETEXTRACTORDEF
符号,将其转成 GetExtractorDef
类型的函数,最后调此函数。可见 GETEXTRACTORDEF
是插件库的入口函数。
实现插件入口
自定义插件库,首先需要实现入口函数 GETEXTRACTORDEF
:
__attribute__((visibility("default"))) ExtractorDef GETEXTRACTORDEF() {return {EXTRACTORDEF_VERSION,UUID("e44400a9-bf8e-45a3-bebd-53dae03e1909"),1,"FFmpeg Extractor",{.v3 = {[](CDataSource *source, float *confidence, void **,FreeMetaFunc *) -> CreatorFunc {DataSourceHelper helper(source);if (SniffFfmpeg(&helper, confidence)) {return [](CDataSource *source,void *) -> CMediaExtractor * {return wrap(new FfmpegExtractor(new DataSourceHelper(source)));};}return NULL;},extensions}}};
}
GETEXTRACTORDEF
函数会返回一个 ExtractorDef
类型的结构体,此结构体作为插件的引用,将会保存到 MediaExtractorService
服务的一个全局列表中, ExtractorDef
的 v3
域的第一个成员为嗅探函数指针,此处采用 lamda
表达式传参,内部调用 SniffFfmpeg
进行嗅探。每次打开媒体文件时,都会遍历插件列表,调用其嗅探函数,找到最合适的插件来解析此文件。以下是自定义媒体提取器的嗅探函数源码:
bool SniffFfmpeg(DataSourceHelper *source, float *confidence) {...int32_t size = source->readAt(0, buffer, pd.buf_size);.../* Guess format */if (!(fmt = av_probe_input_format(&pd, 1))) {ALOGE("SniffFfmpeg: av_probe_input_format error ... ");free(buffer);return false;}if ((strstr(fmt->name, "mp4") != NULL) ||(strstr(fmt->name, "ogg") != NULL) ||(strstr(fmt->name, "mp3") != NULL) ||(strstr(fmt->name, "flac") != NULL) ||(strstr(fmt->name, "wav") != NULL)) {*confidence = 0.0f;} else {*confidence = 0.9f;}...return true;
}
首先通过 source
从文件中读取一部分数据,然后将数据传给 FFmpeg
的 API 函数 av_probe_input_format
,识别出文件的封装格式。最后根据不同的封装格式赋予一个合适的信任值(用0-1之间的小数表示),将其保存到 confidence
变量中,此变量作为输出参数,将会被上层捕获使用。函数返回 true,则表示支持此格式,false 则表示不支持。嗅探的结果,会将所有支持的媒体提取器保存到一个列表,找到其中 confidence
数值最大的一个,作为解析此文件的媒体提取器。
实现媒体提取类
嗅探到合适的媒体提取器插件后会创建对应媒体提取类,可以借助 MediaExtractorPluginHelper
类来实现新的媒体提取类。具体的自定义媒体提取类声明如下:
class FfmpegExtractor : public MediaExtractorPluginHelper {public:// Extractor assumes ownership of "source".FfmpegExtractor(DataSourceHelper *source);virtual size_t countTracks();virtual MediaTrackHelper *getTrack(size_t index);virtual media_status_t getTrackMetaData(AMediaFormat *meta, size_t index,uint32_t flags);virtual media_status_t getMetaData(AMediaFormat *meta);virtual uint32_t flags() const;virtual const char *name() { return "FFmpegExtractor"; }protected:virtual ~FfmpegExtractor();private:DataSourceHelper *mDataSource;Vector<FfmpegExtractorTrack *> mTracks;unsigned int mTrackCount;FfmpegExtractor(const FfmpegExtractor &);FfmpegExtractor &operator=(const FfmpegExtractor &);
};
上面的代码只列举了关键成员。其中所有的虚函数作为 API 会被上层程序调用。
函数名 | 功能 |
---|---|
countTracks | 返回媒体文件包含的轨道数,一般包含视频轨道和音频轨道。 |
getTrack | 根据传入的索引号,返回对应的轨道。 |
getTrackMetaData | 获取对应轨道的元数据。 |
getMetaData | 获取媒体文件元数据,主要是 mime 类型。 |
flags | 返回一些标志,指示是否可以暂停、拖动时间轴等。 |
name | 返回名字。 |
轨道:媒体文件中的音频、视频、字幕等都被看作轨道。
其它成员则是内部使用,服务于具体的功能。
mDataSource
数据的输入源,由构造函数形参初始化,可以按字节从媒体文件中读取数据。
mTracks
轨道列表,从文件中识别出不同的轨道,封装后保存到 mTracks
中,getTrack
就是返回其中一个成员。
mTrackCount
轨道的数量。
最为重要的是构造函数,如果其它接口都是获取数据,那么构造函数就是准备数据。
FfmpegExtractor::FfmpegExtractor(DataSourceHelper *source) {...addTracks();...
}
构造函数中主要调用了 addTracks
函数来创建轨道。
void FfmpegExtractor::addTracks() {...if (avformat_find_stream_info(mAVFormatCtx, NULL) < 0) {ALOGE("could not find codec parameters ... ");return;}for (unsigned int i = 0; i < mAVFormatCtx->nb_streams; i++) {...switch (codecpar->codec_type) {case AVMEDIA_TYPE_AUDIO: {if (haveAudio || mimestr == NULL) {continue;}AMediaFormat_setString(meta, AMEDIAFORMAT_KEY_MIME, mimestr);AMediaFormat_setInt64(meta, AMEDIAFORMAT_KEY_DURATION,mAVFormatCtx->duration);AMediaFormat_setInt32(meta, AMEDIAFORMAT_KEY_CHANNEL_COUNT,codecpar->channels);AMediaFormat_setInt32(meta, AMEDIAFORMAT_KEY_SAMPLE_RATE,codecpar->sample_rate);AMediaFormat_setInt32(meta, AMEDIAFORMAT_KEY_BITS_PER_SAMPLE,codecpar->bits_per_coded_sample);haveAudio = true;SetCodecParams(mFfmpegParams, codecpar);AMediaFormat_setBuffer(meta, "private", mFfmpegParams,sizeof(FFmpegParamsWrap));AMediaFormat_setBuffer(meta, "extradata", codecpar->extradata,codecpar->extradata_size);} break;case AVMEDIA_TYPE_VIDEO: {if (haveVideo || mimestr == NULL) {continue;}...AMediaFormat_setString(meta, AMEDIAFORMAT_KEY_MIME, mimestr);AMediaFormat_setInt64(meta, AMEDIAFORMAT_KEY_DURATION,mAVFormatCtx->duration);AMediaFormat_setInt32(meta, AMEDIAFORMAT_KEY_WIDTH,codecpar->width);AMediaFormat_setInt32(meta, AMEDIAFORMAT_KEY_HEIGHT,codecpar->height);haveVideo = true;SetCodecParams(mFfmpegParams, codecpar);AMediaFormat_setBuffer(meta, "private", mFfmpegParams,sizeof(FFmpegParamsWrap));AMediaFormat_setBuffer(meta, "extradata", codecpar->extradata,codecpar->extradata_size);} break;default:continue;}FfmpegExtractorTrack *pTrack = new FfmpegExtractorTrack(meta, i, codecpar->codec_id, codecpar->extradata,codecpar->extradata_size);mTracks.push(pTrack);mTrackCount++;meta = nullptr;}...return;
}
FFmpeg
中的流与 Android 中的轨道对应,通过 FFmpeg
的 avformat_find_stream_info
函数可以从媒体文件中获取流信息。然后将流信息转换成对应的元数据,这些元数据与相应的轨道关联。其中关键的元数据含义如下:
元数据类型 | 含义 |
---|---|
AMEDIAFORMAT_KEY_MIME |
mime 类型,用于匹配对应 MediaCodec
|
AMEDIAFORMAT_KEY_DURATION | 媒体的播放时长,单位为微秒 |
AMEDIAFORMAT_KEY_CHANNEL_COUNT | 音频声道数量 |
AMEDIAFORMAT_KEY_SAMPLE_RATE | 音频采样率 |
AMEDIAFORMAT_KEY_BITS_PER_SAMPLE | 音频采样精度 |
AMEDIAFORMAT_KEY_WIDTH | 视频图像宽度 |
AMEDIAFORMAT_KEY_HEIGHT | 视频图像高度 |
函数的最后创建了 FfmpegExtractorTrack
对象保存到 mTracks
列表中。
实现媒体轨道类
媒体提取器的作用是提供音、视频数据包给后端使用,媒体轨道类则承担了提供数据的重任。实现轨道类需要借助 MediaTrackHelper
类,具体的自定义媒体轨道类 FfmpegExtractorTrack
的声明如下:
struct FfmpegExtractorTrack : public MediaTrackHelper {FfmpegExtractorTrack(AMediaFormat *meta, int32_t trackIndex,enum AVCodecID id, uint8_t *extradata, uint32_t size);virtual ~FfmpegExtractorTrack();virtual media_status_t start();virtual media_status_t stop();virtual media_status_t getFormat(AMediaFormat *);virtual media_status_t read(MediaBufferHelper **buffer,const ReadOptions *options);private:List<MediaBufferHelper *> mPackets;FfmpegExtractorTrack(const FfmpegExtractorTrack &);FfmpegExtractorTrack &operator=(const FfmpegExtractorTrack &);
};
既然是提供数据,那么最重要的自然是 read
函数。由于需要借助 FFmpeg
的 API 函数读取数据,实际的 read
操作是通过 FfmpegExtractor
类的 read
完成。
media_status_t FfmpegExtractor::read() {AVPacket pkt;FfmpegExtractorTrack *pAvTrack = NULL;Mutex::Autolock autoLock(mLock);if (av_read_frame(mAVFormatCtx, &pkt)) {ALOGE("av_read_frame error ... ");return AMEDIA_ERROR_UNKNOWN;}for (uint32_t i = 0; i < mTrackCount; i++) {if (pkt.stream_index == (mTracks.editItemAt(i))->getTrackIndex()) {pAvTrack = mTracks.editItemAt(i);break;}}if (pAvTrack == NULL) {av_packet_unref(&pkt);return AMEDIA_OK;}AVStream *s = mAVFormatCtx->streams[pAvTrack->getTrackIndex()];int64_t timeUs = 0;int64_t tmp = pkt.dts * 1000000 * s->time_base.num / s->time_base.den;if (tmp >= mStartTime) {timeUs = tmp - mStartTime;} else {mStartTime = tmp;timeUs = 0;}MediaBufferHelper *mediaBuffer;pAvTrack->acquire_buffer(&mediaBuffer, false /* nonblocking */,pkt.size /* requested size */);AMediaFormat *meta = mediaBuffer->meta_data();AMediaFormat_setInt64(meta, AMEDIAFORMAT_KEY_TIME_US, timeUs);memcpy(mediaBuffer->data(), pkt.data, pkt.size);mediaBuffer->set_range(0, pkt.size);AMediaFormat_setInt32(meta, AMEDIAFORMAT_KEY_IS_SYNC_FRAME,(pkt.flags & AV_PKT_FLAG_KEY) != 0);av_packet_unref(&pkt);pAvTrack->queueAccessPacket(mediaBuffer);return AMEDIA_OK;
}
首先调用 av_read_frame
函数从文件中读取一帧数据,然后判断数据属于哪一个轨道,从相应的轨道分配一块内存(共享内存),将数据缓存到此内存中。等到 FfmpegExtractorTrack
的 read
被调用时,再从缓存区取出数据返回给调用者。
注意:AMEDIAFORMAT_KEY_TIME_US
参数极为重要,它是帧时间戳,将作为音视频同步的时间参数。可以将 FFmpeg
中的 dts
或 pts
作为帧时间戳使用。注意时间戳的单位为微秒。
这便是媒体提取器的主体框架和实现方法。
自定义视频解码器
解封装后的视频数据是 H.264
/H.265
等格式,还不能直接显示到屏幕,需要通过解码器,将其还原成 yuv
或 rga
图片才能显示。由于授权和硬件限制,平台能支持的视频解码格式有限,可以通过自定义软解码器支持更多的解码格式。
Android 原生多媒体框架如下:
自定解码器在 OpenMAX
集成层 (IL) 实现。OpenMAX IL
提供了一种标准化的方式识别和使用基于硬件的自定义多媒体编解码器(称为组件)。当然基于软件的解码器,也可以按照 OpenMAX IL
组件标准实现。
加载解码库
在 Android 系统中 Omx
服务负责枚举和创建解码器,解码器与 OMXNodeInstance
是相互关联的,分配 OMXNodeInstance
对象的同时会加载对应的解码库。
Return<void> Omx::allocateNode(const hidl_string& name,const sp<IOmxObserver>& observer,allocateNode_cb _hidl_cb) {...instance = new OMXNodeInstance(this, new LWOmxObserver(observer), name.c_str());OMX_COMPONENTTYPE *handle;OMX_ERRORTYPE err = mMaster->makeComponentInstance(name.c_str(), &OMXNodeInstance::kCallbacks,instance.get(), &handle);instance->setHandle(handle);...return Void();
}
其中 mMaster
为 OMXMaster
类型,通过其 makeComponentInstance
函数创建组件实例:
OMX_ERRORTYPE OMXMaster::makeComponentInstance(const char *name,const OMX_CALLBACKTYPE *callbacks,OMX_PTR appData,OMX_COMPONENTTYPE **component) {...OMX_ERRORTYPE err =plugin->makeComponentInstance(name, callbacks, appData, component);...return err;
}
软解码库对应的 plugin
类型为 SoftOMXPlugin
,相应的 makeComponentInstance
会被调用:
OMX_ERRORTYPE SoftOMXPlugin::makeComponentInstance(const char *name,const OMX_CALLBACKTYPE *callbacks,OMX_PTR appData,OMX_COMPONENTTYPE **component) {for (size_t i = 0; i < kNumComponents; ++i) {if (strcmp(name, kComponents[i].mName)) {continueAString libName = "libstagefright_soft_";libName.append(kComponents[i].mLibNameSuffix);libName.append(".so");void *libHandle = dlopen(libName.c_str(), RTLD_NOW|RTLD_NODELETE);...CreateSoftOMXComponentFunc createSoftOMXComponent =(CreateSoftOMXComponentFunc)dlsym(libHandle,"_Z22createSoftOMXComponentPKcPK16OMX_CALLBACKTYPE""PvPP17OMX_COMPONENTTYPE");...sp<SoftOMXComponent> codec =(*createSoftOMXComponent)(name, callbacks, appData, component);...return OMX_ErrorNone;}return OMX_ErrorInvalidComponentName;
}
其中 kComponents
保存了 Omx
组件信息列表,自定解码库时需要在列表的最后添加相应的信息。
static const struct {const char *mName;const char *mLibNameSuffix;const char *mRole;
} kComponents[] = {...{ "OMX.google.audio_ffmpeg.decoder", "audio_ffmpegdec", "audio_decoder.ffmpeg" }, { "OMX.google.video_ffmpeg.decoder", "video_ffmpegdec", "video_decoder.ffmpeg" },
};
- mName 组件名称
- mLibNameSuffix 软件编解码库后缀名
- mRole 组件角色,区分编码器和解码器
SoftOMXPlugin::makeComponentInstance
函数首先遍历 kComponents
数组,通过组件名找到目标元素。然后获取 mLibNameSuffix
字段保存的库文件后缀,拼凑出库文件名,对于自定解码库,确定库文件名为:libstagefright_soft_ffmpegdec.so
。然后调用 dlopen
打开此库文件,找到 _Z22createSoftOMXComponentPKcPK16OMX_CALLBACKTYPE
符号,此符号对应解码库中的 createSoftOMXComponent
函数。调用该函数创建 SoftOMXComponent
对象,此对象就是解码器对象,并通过 component
变量与之关联, component
变量为函数的输出参数,对应 allocateNode
函数的 handle
。 allocateNode
函数中通过 instance->setHandle(handle)
这条语句,建立 OMXNodeInstance
与解码组件关联。这便是解码库的主要加载过程。
实现音频解码库
首先需要实现 createSoftOMXComponent
函数:
android::SoftOMXComponent *createSoftOMXComponent(const char *name, const OMX_CALLBACKTYPE *callbacks, OMX_PTR appData,OMX_COMPONENTTYPE **component) {return new android:: SoftFfmpegVideoDec (name, callbacks, appData, component);
}
createSoftOMXComponent
作用是创建解码器对象,这里创建的是自定义的解码器类 SoftFfmpegVideoDec
的对象。一般可以通过继承 SoftVideoDecoderOMXComponent
来实现自己的软解码器类。以下便是自定义解码器的类声明:
struct SoftFfmpegVideoDec: public SoftVideoDecoderOMXComponent {SoftFfmpeg(const char *name, const OMX_CALLBACKTYPE *callbacks,OMX_PTR appData, OMX_COMPONENTTYPE **component);
protected:virtual ~ SoftFfmpegVideoDec ();virtual void onQueueFilled(OMX_U32 portIndex);virtual void onPortFlushCompleted(OMX_U32 portIndex);virtual void onReset();virtual int getColorAspectPreference();
};
关键函数是 onQueueFilled
,它会管理两个队列:
- 输入队列,接收编码的图片数据。
- 输出队列,对图片进行解码后,将原始图片放到输出队列,进行显示。
从输入队列到输出队列便是解码的全过程,这也是软解码需要完成的主要工作。
void SoftFfmpegVideoDec::onQueueFilled(OMX_U32 portIndex) {...List<BufferInfo *> &inQueue = getPortQueue(kInputPortIndex);List<BufferInfo *> &outQueue = getPortQueue(kOutputPortIndex);while (!inQueue.empty() && !outQueue.empty()) {BufferInfo *inInfo = *inQueue.begin();OMX_BUFFERHEADERTYPE *inHeader = inInfo->mHeader;...mPkt->data = inHeader->pBuffer;mPkt->size = inHeader->nFilledLen;int ret = avcodec_send_packet(mCodecCtx, mPkt);ret = avcodec_receive_frame(mCodecCtx, mFrame);...BufferInfo *outInfo = *outQueue.begin();OMX_BUFFERHEADERTYPE *outHeader = outInfo->mHeader;outHeader->nFilledLen = 0;...ret = ConvertToI420(pointers[0], 0, outHeader->pBuffer, mFrame->width,outHeader->pBuffer + sizeY, SUBSAMPLE(mFrame->width, 2),outHeader->pBuffer + sizeY + sizeUV,SUBSAMPLE(mFrame->width, 2), 0, 0, mFrame->width,mFrame->height, mFrame->width, mFrame->height,libyuv::kRotate0, mPixFourCC);outHeader->nFilledLen = sizeY * 3 / 2;...outHeader->nFlags = inHeader->nFlags;outHeader->nOffset = 0;outHeader->nTimeStamp = inHeader->nTimeStamp;outQueue.erase(outQueue.begin());outInfo->mOwnedByUs = false;notifyFillBufferDone(outHeader);inQueue.erase(inQueue.begin());inInfo->mOwnedByUs = false;notifyEmptyBufferDone(inHeader);...}
}
以上是软件解码的主要代码片段,主要的工作如下:
- 获取输入队列和输出队列。
- 从输入队列中取一帧编码数据,初始化
mPkt
,mPkt
是AVPacket
类型,在FFmpeg
中用来描述编码数据包。 - 调用
FFmpeg api
函数avcodec_send_packet
,将mPkt
送给AVCodec
解码器解码。 - 调用
FFmpeg api
函数avcodec_receive_frame
,取出解码后的数据,保存到mFrame
,mFrame
是AVFrame
类型,在FFmpeg
中用来描述解码后的数据帧。 - 从
mFrame
从取出有效的图片数据保存到pointers
数组中,然后ConvertToI420
函数进行色彩空间转换,统一转换成 YUV420P 格式。 - 从输出队列获取缓存区,然后将 YUV 数据拷贝到缓存区。上面代码中
ConvertToI420
函数同时完成了内存拷贝工作。 - 通过 notifyFillBufferDone 通知输出队列有图片数据待显示。
- 通过 notifyEmptyBufferDone 通知输入队列处理完一帧数据解码。
以上过程循环往复,就是解码的基本流程。解码通过 FFmpeg
的 API 完成,FFmpeg
相关组件需要提前进行初始化,这部分就不再详述。
自定义音频解码器
与视频类似,解封装后的音频数据需要通过解码器还原成 PCM 数据才能送给声卡播放。音频解码器与视频解码器的加载过程相同,都是以库文件方式加载。这里直接来看解码库的实现。
实现音频解码库
首先需要实现 createSoftOMXComponent
函数:
android::SoftOMXComponent *createSoftOMXComponent(const char *name, const OMX_CALLBACKTYPE *callbacks, OMX_PTR appData,OMX_COMPONENTTYPE **component) {return new android::SoftFfmpegAudioDec(name, callbacks, appData, component);
}
SoftFfmpegAudioDec
为自定义音频解码类 。它需要继承 SimpleSoftOMXComponent
。下面是SoftFfmpegAudioDec
的声明:
struct SoftFfmpegAudioDec : public SimpleSoftOMXComponent {SoftFfmpegAudioDec(const char *name,const OMX_CALLBACKTYPE *callbacks,OMX_PTR appData,OMX_COMPONENTTYPE **component);
protected:virtual ~SoftFfmpegAudioDec();virtual OMX_ERRORTYPE internalGetParameter(OMX_INDEXTYPE index, OMX_PTR params);virtual OMX_ERRORTYPE internalSetParameter(OMX_INDEXTYPE index, const OMX_PTR params);virtual void onQueueFilled(OMX_U32 portIndex);virtual void onPortFlushCompleted(OMX_U32 portIndex);virtual void onPortEnableCompleted(OMX_U32 portIndex, bool enabled);virtual void onReset();
};
与视频解码器不同,音频解码器必须实现 internalGetParameter
和 internalSetParameter
函数,这两个函数用于获取和设置解码器参数,这些参数用于协调框架层与解码器同步工作。其中获取解码器输出端的 PCM 参数最为重要:
case OMX_IndexParamAudioPcm: {OMX_AUDIO_PARAM_PCMMODETYPE *pcmParams = (OMX_AUDIO_PARAM_PCMMODETYPE *)params;pcmParams->eNumData = mNumericalData;pcmParams->eEndian = OMX_EndianBig;pcmParams->bInterleaved = OMX_TRUE;pcmParams->nBitPerSample = mBitsPerSample;pcmParams->ePCMMode = OMX_AUDIO_PCMModeLinear;pcmParams->eChannelMapping[0] = OMX_AUDIO_ChannelLF;pcmParams->eChannelMapping[1] = OMX_AUDIO_ChannelRF;pcmParams->eChannelMapping[2] = OMX_AUDIO_ChannelCF;pcmParams->eChannelMapping[3] = OMX_AUDIO_ChannelLFE;pcmParams->eChannelMapping[4] = OMX_AUDIO_ChannelLS;pcmParams->eChannelMapping[5] = OMX_AUDIO_ChannelRS;if (!isConfigured()) {pcmParams->nChannels = 2;pcmParams->nSamplingRate = 44100;} else {pcmParams->nChannels = mFFmpegParams.channels;pcmParams->nSamplingRate = mFFmpegParams.sample_rate;}return OMX_ErrorNone;
}
PCM 参数中最重要的部分是:eNumData
表示数据采用数制,nBitPerSample
表示每次采样数据占用的位数。这是由解码器输出的采样格式决定。
与视频解码相同,音频解码工作是通过 onQueueFilled
完成。
void SoftFfmpegAudioDec::onQueueFilled(OMX_U32 /* portIndex */) {List<BufferInfo *> &inQueue = getPortQueue(0);List<BufferInfo *> &outQueue = getPortQueue(1);while ((!inQueue.empty() || mSawInputEOS) && !outQueue.empty() && !mFinishedDecoder) {BufferInfo *outInfo = *outQueue.begin();OMX_BUFFERHEADERTYPE *outHeader = outInfo->mHeader;void *outBuffer = reinterpret_cast<void *>(outHeader->pBuffer + outHeader->nOffset);size_t outBufferSize = outHeader->nAllocLen - outHeader->nOffset;if (!inQueue.empty()) {BufferInfo *inInfo = *inQueue.begin();OMX_BUFFERHEADERTYPE *inHeader = inInfo->mHeader;uint8_t *inBuffer = inHeader->pBuffer + inHeader->nOffset;uint32_t inBufferLength = inHeader->nFilledLen;status_t decoderErr = decodeOneFrame(inBuffer, inBufferLength, outBuffer, &outBufferSize, outputFloat);timeStamp = inHeader->nTimeStamp;inInfo->mOwnedByUs = false;inQueue.erase(inQueue.begin());notifyEmptyBufferDone(inHeader);}outHeader->nFilledLen = outBufferSize;outHeader->nTimeStamp = timeStamp;outInfo->mOwnedByUs = false;outQueue.erase(outQueue.begin());notifyFillBufferDone(outHeader);}
}
大体过程和视频解码相同,这里只是将解码动作进一步封装为 decodeOneFrame
,该函数通过调用 FFmpeg
API 进行解码。
结尾
本文所有代码都是以 Android 10 系统为基础实现,从系统使用的角度,简要阐述媒体提取器和解码器的实现原理。其中涉及 FFmpeg
API 函数使用,不是本文主要探讨的内容,相关函数使用方法可参阅其它文档。
Android 系统软解码方案实现相关推荐
- 软解码方案之-DSADC结果中断和时间戳中断MCAL配置实现
电机软解码方案系列 软解码方案之-DSADC结果中断和时间戳中断MCAL配置实现 前言 目前新能源汽车行业电机控制器中旋变软解码方案应用比较普遍,楼主最近做了电机控制器AUTOSAR架构项目,对软解码 ...
- Android系统级保活方案
一.防止应用在系统低内存的时候被回收 代码路径:./frameworks/base/services/java/com/android/server/am/ActivityManagerService ...
- 英飞凌TC37X-TC38X-系列之电机旋变软解码
电机旋变软解码 下面和大家分享一下英飞凌系列单片机旋变软解码实际项目测试的波形及简要说明,为做电机开发的同学提供一点参考学习的资料. 目录 电机旋变软解码 前言 一.DSADC旋变软解码框图 二.DS ...
- Android视频滤镜添加硬解码方案
由于工作的需求,研究过了一段时间的Android 的音视频播放渲染以及编辑方面的知识,这里就自己一些浅薄的了解对所了解做一个简单的介绍和记录,如有不对的地方请指正!同时也会记录下硬件解码的情况下完成滤 ...
- Android平台监听系统截屏方案预研及相关知识点
最近有个针对系统截屏的需求,所以预研了Android平台上捕获系统截屏的方案. 最直接的方式就是监听手机的系统截屏组合键(电源键+音量下键),但是这种方式实现难度大,且有的机型使用特殊手势进行截屏,兼 ...
- android外接键盘打汉字,Android在外接物理键盘时,如何强制调用系统软键盘
Android在外接物理键盘时,如何强制调用系统软键盘? 第一次写,写的不好请见谅 参考: 物理键盘映射过程: 手机/system/usr/keylayout/*.kl :内核将keyCode映射成有 ...
- 系统软键盘Android在外接物理键盘时,如何强制调用系统软键盘?
第一次写,写的不好请见谅 物理键盘映射过程: 手机/system/usr/keylayout/*.kl :内核将keyCode映射成有含义的字符串 KeycodeLabels.h : framewor ...
- 智能会议系统(34)---Android语音通话实现方案及相关技术介绍
Android语音通话实现方案及相关技术介绍 Android语音通话实现方案及相关技术介绍 语音通话 Step1语音采集和输出 Step2编解码方式 Step3网络传输 Step4去噪声消回音 语音通 ...
- android xposed软重启,Xposed插件安装更新免重启手机方案
但实际情况是,很多开发者只使用了Xposed的一部分功能,也就是de.robv.android.xposed.IXposedHookLoadPackage提供的功能.一般情况下,只是实现了该接口,然后 ...
最新文章
- LeetCode实战:合并两个有序数组
- R语言中的聚类的使用
- STL标准库六大组件
- ORACLE DataGuard主备切换
- VMware SDS 之四:VSAN的技术细节
- 2017.9.2 校内模拟赛
- Linux下c开发 之 线程通信(转)
- 面试官问:能否模拟实现JS的call和apply方法
- 论文阅读 - TransNet and TransNet V2
- python利用百度云接口实现车牌识别
- 用c++创建xml文件的两种方法
- 给自己的电脑做一个O盘 -隐藏自己私密的东
- H3C 路由备份与IP聚合
- 内核线程、轻量级进程、用户线程三种线程概念解惑(线程≠轻量级进程)
- 反编译软件ILSpy的使用教程
- qq邮箱服务器地址ip地址,如何查询对方QQ邮箱的ip地址?QQ邮箱ip地址的查询方法...
- Zipf齐夫分布及Java实现
- 统计学 | 峰度系数与肥尾理解
- Kinect黑客:机械人科技未来的转变者
- java 打印菱形和空心菱形