本文档主要介绍 Android 自定义媒体提取器和解码器的实现方法。自定义媒体提取器的目标是支持更多的视频封装格式,比如:avirmrmvb 等。自定义解码器的目标是支持更多视频解码格式,比如:wmv1wmv2RV10RV20 等。最终使系统原生播放器能播放所有主流的媒体文件,第三方播放器仅需调用标准的系统接口,即可实现所有主流媒体格式的兼容。

视频播放基本过程

在介绍具体实现代码前,先说明一下播放视频的一般过程:

  • 解封装:从文件中分离出音频和视频的编码数据,并以数据包的形式输出。
  • 解码
    • 视频:将编码后的图片数据包还原成原始图片(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 服务的一个全局列表中, ExtractorDefv3 域的第一个成员为嗅探函数指针,此处采用 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 中的轨道对应,通过 FFmpegavformat_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 函数从文件中读取一帧数据,然后判断数据属于哪一个轨道,从相应的轨道分配一块内存(共享内存),将数据缓存到此内存中。等到 FfmpegExtractorTrackread 被调用时,再从缓存区取出数据返回给调用者。

注意AMEDIAFORMAT_KEY_TIME_US 参数极为重要,它是帧时间戳,将作为音视频同步的时间参数。可以将 FFmpeg 中的 dtspts 作为帧时间戳使用。注意时间戳的单位为微秒。

这便是媒体提取器的主体框架和实现方法。

自定义视频解码器

解封装后的视频数据是 H.264/H.265 等格式,还不能直接显示到屏幕,需要通过解码器,将其还原成 yuvrga 图片才能显示。由于授权和硬件限制,平台能支持的视频解码格式有限,可以通过自定义软解码器支持更多的解码格式。

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();
}

其中 mMasterOMXMaster 类型,通过其 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 函数的 handleallocateNode 函数中通过 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);...}
}

以上是软件解码的主要代码片段,主要的工作如下:

  • 获取输入队列和输出队列。
  • 从输入队列中取一帧编码数据,初始化 mPktmPktAVPacket 类型,在 FFmpeg 中用来描述编码数据包。
  • 调用 FFmpeg api 函数 avcodec_send_packet,将 mPkt 送给 AVCodec 解码器解码。
  • 调用 FFmpeg api 函数 avcodec_receive_frame,取出解码后的数据,保存到 mFramemFrameAVFrame 类型,在 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();
};

与视频解码器不同,音频解码器必须实现 internalGetParameterinternalSetParameter 函数,这两个函数用于获取和设置解码器参数,这些参数用于协调框架层与解码器同步工作。其中获取解码器输出端的 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 系统软解码方案实现相关推荐

  1. 软解码方案之-DSADC结果中断和时间戳中断MCAL配置实现

    电机软解码方案系列 软解码方案之-DSADC结果中断和时间戳中断MCAL配置实现 前言 目前新能源汽车行业电机控制器中旋变软解码方案应用比较普遍,楼主最近做了电机控制器AUTOSAR架构项目,对软解码 ...

  2. Android系统级保活方案

    一.防止应用在系统低内存的时候被回收 代码路径:./frameworks/base/services/java/com/android/server/am/ActivityManagerService ...

  3. 英飞凌TC37X-TC38X-系列之电机旋变软解码

    电机旋变软解码 下面和大家分享一下英飞凌系列单片机旋变软解码实际项目测试的波形及简要说明,为做电机开发的同学提供一点参考学习的资料. 目录 电机旋变软解码 前言 一.DSADC旋变软解码框图 二.DS ...

  4. Android视频滤镜添加硬解码方案

    由于工作的需求,研究过了一段时间的Android 的音视频播放渲染以及编辑方面的知识,这里就自己一些浅薄的了解对所了解做一个简单的介绍和记录,如有不对的地方请指正!同时也会记录下硬件解码的情况下完成滤 ...

  5. Android平台监听系统截屏方案预研及相关知识点

    最近有个针对系统截屏的需求,所以预研了Android平台上捕获系统截屏的方案. 最直接的方式就是监听手机的系统截屏组合键(电源键+音量下键),但是这种方式实现难度大,且有的机型使用特殊手势进行截屏,兼 ...

  6. android外接键盘打汉字,Android在外接物理键盘时,如何强制调用系统软键盘

    Android在外接物理键盘时,如何强制调用系统软键盘? 第一次写,写的不好请见谅 参考: 物理键盘映射过程: 手机/system/usr/keylayout/*.kl :内核将keyCode映射成有 ...

  7. 系统软键盘Android在外接物理键盘时,如何强制调用系统软键盘?

    第一次写,写的不好请见谅 物理键盘映射过程: 手机/system/usr/keylayout/*.kl :内核将keyCode映射成有含义的字符串 KeycodeLabels.h : framewor ...

  8. 智能会议系统(34)---Android语音通话实现方案及相关技术介绍

    Android语音通话实现方案及相关技术介绍 Android语音通话实现方案及相关技术介绍 语音通话 Step1语音采集和输出 Step2编解码方式 Step3网络传输 Step4去噪声消回音 语音通 ...

  9. android xposed软重启,Xposed插件安装更新免重启手机方案

    但实际情况是,很多开发者只使用了Xposed的一部分功能,也就是de.robv.android.xposed.IXposedHookLoadPackage提供的功能.一般情况下,只是实现了该接口,然后 ...

最新文章

  1. LeetCode实战:合并两个有序数组
  2. R语言中的聚类的使用
  3. STL标准库六大组件
  4. ORACLE DataGuard主备切换
  5. VMware SDS 之四:VSAN的技术细节
  6. 2017.9.2 校内模拟赛
  7. Linux下c开发 之 线程通信(转)
  8. 面试官问:能否模拟实现JS的call和apply方法
  9. 论文阅读 - TransNet and TransNet V2
  10. python利用百度云接口实现车牌识别
  11. 用c++创建xml文件的两种方法
  12. 给自己的电脑做一个O盘 -隐藏自己私密的东
  13. H3C 路由备份与IP聚合
  14. 内核线程、轻量级进程、用户线程三种线程概念解惑(线程≠轻量级进程)
  15. 反编译软件ILSpy的使用教程
  16. qq邮箱服务器地址ip地址,如何查询对方QQ邮箱的ip地址?QQ邮箱ip地址的查询方法...
  17. Zipf齐夫分布及Java实现
  18. 统计学 | 峰度系数与肥尾理解
  19. Kinect黑客:机械人科技未来的转变者
  20. java 打印菱形和空心菱形

热门文章

  1. 行业分析-全球与中国高端厨具市场现状及未来发展趋势
  2. 硬核外贸好货掀采购新热潮,来第10届上海尚品家居展抢占先机
  3. 中国云计算市场年终盘点:精彩依旧 未来可期
  4. 北京5月楼市低迷开局 会否大规模降价成关注点
  5. 程序员:未来世界的架构师,越老越吃香的一份职业?
  6. 计算机学院运动会通讯稿,学院运动会通讯稿
  7. 汉字转拼音_gb2312 C#
  8. 点面科技金融银行解决方案
  9. UseModel 简单用法
  10. 拥有成长思维,扩大舒适圈,获得不断成长