背景
美团外卖至今已迅猛发展了六年,随着外卖业务量级与日俱增,单一的文字和图片已无法满足商家的需求,商家迫切需要更丰富的商品描述手段吸引用户,增加流量,进而提高下单转化率和下单量。商品视频的引入,在一定程度上可以提升商品信息描述丰富度,以更加直观的方式为商家引流,增加收益。为此,商家端引入了视频功能,进行了一系列视频功能开发,核心功能包含视频处理(混音,滤镜,加水印,动画等)、视频拍摄、合成等,最终效果图如下所示:
自视频功能上线后,每周视频样本量及使用视频的商家量大幅增加,视频录制成功率达99.533%,视频处理成功率98.818%,音频处理成功率99.959%,Crash率稳定在0.1‰,稳定性高且可用性强。目前,视频功能已在蜜蜂App、闪购业务和商家业务上使用。
对于视频链路的开发,我们经历了方案选型、架构设计及优化、业务实践、功能测试、监控运维、更新维护等各个环节,核心环节如下图所示。在开发过程中,我们遇到了各种技术问题和挑战,下文会针对遇到的问题、挑战,及其解决方案进行重点阐述。
方案选型
在方案选型时,重点对核心流程和视频格式进行选型。我们以功能覆盖度、稳定性及效率、可定制性、成本及开源性做为核心指标,从而衡量方案的高可用性和可行性。
1.核心流程选型
视频开发涉及的核心流程包括播放、录制、合成、裁剪、后期处理(编解码、滤镜、混音、动画、水印)等。结合商家端业务场景,我们有针对性的进行方案调研。重点调研了业界现有方案,如阿里的云视频点播方案、腾讯云视频点播方案、大众点评App的UGC方案,及其它的一些第三方开源方案等,并进行了整体匹配度的对比,如下图所示:
阿里和腾讯的云视频点播方案比较成熟,集成度高,且能力丰富,稳定性及效率也很高。但两者成本较高,需要收费,且SDK大小均在15M以上,对于我们的业务场景来说有些过于臃肿,定制性较弱,无法迅速的支持我们做定制性扩展。
当时的点评App UGC方案,基础能力是满足的,但因业务场景差异:
- 比如外卖的视频拍摄功能要求在竖屏下保证16:9的视频宽高比,这就需要对原有的采集区域进行截取,视频段落的裁剪支持不够等,业务场景的差异导致了实现方案存在巨大的差异,故放弃了点评App UGC方案。其他的一些开源方案(比如Grafika等),也无法满足要求,这里不再一一赘述。
通过技术调研和分析,吸取各开源项目的优点,并参考点评App UGC、Google CTS方案,对核心流程做了最终的方案选型,打造一个适合我们业务场景的方案,如下表所示:
2.视频格式选型
- 采用H.264的视频协议:H.264的标准成熟稳定,普及率高。其最大的优势是具有很高的数据压缩比率,在同等图像质量的条件下,H.264的压缩比是MPEG-2的2倍以上,是MPEG-4的1.5~2倍。
- 采用AAC的音频协议:AAC是一种专为声音数据设计的文件压缩格式。它采用了全新的算法进行编码,是新一代的音频有损压缩技术,具有更加高效,更具有”性价比“的特点。
整体架构
我们整体的架构设计,用以满足业务扩展和平台化需要,可复用、可扩展,且可快速接入。架构采用分层设计,基础能力和组件进行下沉,业务和视频能力做分离,最大化降低业务方的接入成本,三方业务只需要接入视频基础SDK,直接使用相关能力组件或者工具即可。
整体架构分为四层,分别为平台层、核心能力层、基础组件层、业务层。
- 平台层:依赖系统提供的平台能力,比如Camera、OpenGL、MediaCodec和MediaMuxer等,也包括引入的平台能力,比如ijkplayer播放器、mp4parser 。
- 核心能力层:该层提供了视频服务的核心能力,包括音视频编解码、音视频的转码引擎、滤镜渲染能力等 。
- 基础能力层:暴露了基础组件和能力,提供了播放、裁剪、录屏等基础组件和对应的基础工具类,并提供了可定制的播放面板,可定制的缓存接口等。
- 业务层:包括段落拍摄、自由拍摄、视频空间、拍摄模版预览及加载等。
我们的视频能力层对业务层是透明的,业务层与能力层隔离,并对业务层提供了部分定制化的接口支持,这样的设计降低了业务方的接入成本,并方便业务方的扩展,比如支持蜜蜂App的播放面板定制,还支持缓存策略、编解码策略的可定制。整体设计如下图所示:
实践经验
在视频开发实践中,因业务场景的复杂性,我们遇到了多种问题和挑战。下面以核心功能为基点,围绕各功能遇到的问题做详细介绍。
视频播放
播放器是视频播放基础。针对播放器,我们进行了一系列的方案调研和选择。在此环节,遇到的挑战如下:
1.兼容性问题
2.缓存问题
针对兼容性问题,Android有原生的MediaPlayer,但其版本兼容问题偏多且支持格式有限,而我们需要支持播放本地视频,本地视频格式又无法控制,故该方案被舍弃。ijkplayer基于FFmpeg,与MediaPlayer相比,优点比较突出:具备跨平台能力,支持Android与iOS;提供了类似MediaPlayer的API,可兼容不同版本;可实现软硬解码自由切换,拥有FFmpeg的能力,支持多种流媒体协议。基于上述原因,我们最终决定选用ijkplayer。
但紧接着我们又发现ijkplayer本身不支持边缓存边播放,频繁的加载视频导致耗费大量的流量,且在弱网或者3G网络下很容易导致播放卡顿,所以这里就衍生出了缓存的问题。
针对缓存问题,我们引入AndroidVideoCache的技术方案,利用本地的代理去请求数据,先本地保存文件缓存,客户端通过Socket读取本地的文件缓存进行视频播放,这样就做到了边播放边缓存的策略,流程如下图:
此外,我们还对AndroidVideoCache做了一些技术改造:
- 优化缓存策略。针对缓存策略的单一性,支持有限的最大文件数和文件大小问题,我们调整为由业务方可以动态定制缓存策略;
- 解决内存泄露隐患。对其页面退出时请求不关闭会导致的内存泄露,我们为其添加了完整的生命周期监控,解决了内存泄露问题。
视频录制
在视频拍摄的时候,最为常用的方式是采用MediaRecorder+Camera技术,采集摄像头可见区域。但因我们的业务场景要求视频采集的时候,只录制采集区域的部分区域且比例保持宽高比16:9,在保证预览图像不拉伸的情况下,只能对完整的采集区域做裁剪,这无形增加了开发难度和挑战。通过大量的资料分析,我们重点调研了有两种方案:
- Camera+AudioRecord+MediaCodec+Surface
- MediaRecorder+MediaCodec
方案1需要Camera采集YUV帧,进行截取采集,最后再将YUV帧和PCM帧进行编码生成mp4文件,虽然其效率高,但存在不可把控的风险。
方案2综合评估后是改造风险最小的。 综合成本和风险考量,我们保守的采用了方案2,该方案是对裁剪区域进行坐标换算(如果用前置摄像头拍摄录制视频,会出现预览画面和录制的视频是镜像的问题,需要处理)。当录制完视频后,生成了mp4文件,用MediaCodec对其编码,在编码阶段再利用OpenGL做内容区域的裁剪来实现。但该方案又引发了如下挑战:
(1)对焦问题
因我们对采集区域做了裁剪,引发了点触对焦问题。比如用户点击了相机预览画面,正常情况下会触发相机的对焦动作,但是用户的点击区域只是预览画面的部分区域,这就导致了相机的对焦区域错乱,不能正常进行对焦。后期经过问题排查,对点触区域再次进行相应的坐标变换,最终得到正确的对焦区域。
(2)兼容适配
我们的视频录制利用MediaRecorder,在获取配置信息时,由于Android碎片化问题,不同的设备支持的配置信息不同,所以就会出现设备适配问题。
// VIVO Y66 模版拍摄时候,播放某些有问题的视频文件的同时去录制视频,会导致MediaServer挂掉的问题
// 发现将1080P尺寸的配置降低到720P即可避免此问题
// 但是720P尺寸的配置下,又存在绿边问题,因此再降到480
if(isVIVOY66() && mMediaServerDied) {
return getCamcorderProfile(CamcorderProfile.QUALITY_480P);
}
//SM-C9000,在1280 x 720 分辨率时有一条绿边。网上有种说法是GPU对数据进行了优化,使得GPU产生的图像分辨率
//和常规分辨率存在微小差异,造成图像色彩混乱,修复后存在绿边问题。
//测试发现,降低分辨率或者升高分辨率都可以绕开这个问题。
if (VideoAdapt.MODEL_SM_C9000.equals(Build.MODEL)) {
return getCamcorderProfile(CamcorderProfile.QUALITY_HIGH);
}
// 优先选择 1080 P的配置
CamcorderProfile camcorderProfile = getCamcorderProfile(CamcorderProfile.QUALITY_1080P);
if (camcorderProfile == null) {
camcorderProfile = getCamcorderProfile(CamcorderProfile.QUALITY_720P);
}
// 某些机型上这个 QUALITY_HIGH 有点问题,可能通过这个参数拿到的配置是1080p,所以这里也可能拿不到
if (camcorderProfile == null) {
camcorderProfile = getCamcorderProfile(CamcorderProfile.QUALITY_HIGH);
}
// 兜底
if (camcorderProfile == null) {
camcorderProfile = getCamcorderProfile(CamcorderProfile.QUALITY_480P);
}
视频合成
我们的视频拍摄有段落拍摄这种场景,商家可根据事先下载的模板进行分段拍摄,最后会对每一段的视频做拼接,拼接成一个完整的mp4文件。mp4由若干个Box组成,所有数据都封装在Box中,且Box可再包含Box的被称为Container Box。mp4中Track表示一个视频或音频序列,是Sample的集合,而Sample又可分为Video Smaple和Audio Sample。Video Smaple代表一帧或一组连续视频帧,Audio Sample即为一段连续的压缩音频数据。(详见mp4文件结构。)
基于上面的业务场景需要,视频合成的基础能力我们采用mp4parser技术实现(也可用FFmpeg等其他手段)。mp4parser在拼接视频时,先将视频的音轨和视频轨进行分离,然后进行视频和音频轨的追加,最终将合成后的视频轨和音频轨放入容器里(这里的容器就是mp4的Box)。采用mp4parser技术简单高效,API设计简洁清晰,满足需求。
但我们发现某些被编码或处理过的mp4文件可能会存在特殊的Box,并且mp4parser是不支持的。经过源码分析和原因推导,发现当遇到这种特殊格式的Box时,会申请分配一个比较大的空间用来存放数据,很容易造成OOM(内存溢出),见下图所示。于是,我们对这种拼接场景下做了有效规避,仅在段落拍摄下使用mp4parser的拼接功能,保证我们处理过的文件不会包含这种特殊的Box。
视频裁剪
我们刚开始采用mp4parser技术完成视频裁剪,在实践中发现其精度误差存在很大的问题,甚至会影响正常的业务需求。比如我们禁止裁剪出3s以下的视频,但是由于mp4parser产生的精度误差,导致4-5s的视频很容易裁剪出少于3s的视频。究其原因,mp4parser只能在关键帧(又称I帧,在视频编码中是一种自带全部信息的独立帧)进行切割,这样就可能存在一些问题。比如在视频截取的起始时间位置并不是关键帧,因此会造成误差,无法保证精度而且是秒级误差。以下为mp4parser裁剪的关键代码:
public static double correctTimeToSyncSample(Track track, double cutHere, boolean next) {
double[] timeOfSyncSamples = new double[track.getSyncSamples().length];
long currentSample = 0;
double currentTime = 0;
for (int i = 0; i < track.getSampleDurations().length; i++) {
long delta = track.getSampleDurations()[i];
int index = Arrays.binarySearch(track.getSyncSamples(), currentSample + 1);
if (index >= 0) {
timeOfSyncSamples[index] = currentTime;
}
currentTime += ((double) delta / (double) track.getTrackMetaData().getTimescale());
currentSample++;
}
double previous = 0;
for (double timeOfSyncSample : timeOfSyncSamples) {
if (timeOfSyncSample > cutHere) {
if (next) {
return timeOfSyncSample;
} else {
return previous;
}
}
previous = timeOfSyncSample;
}
return timeOfSyncSamples[timeOfSyncSamples.length - 1];
}
为了解决精度问题,我们废弃了mp4parser,采用MediaCodec的方案,虽然该方案会增加复杂度,但是误差精度大大降低。
方案具体实施如下:先获得目标时间的上一帧信息,对视频解码,然后根据起始时间和截取时长进行切割,最后将裁剪后的音视频信息进行压缩编码,再封装进mp4容器中,这样我们的裁剪精度从秒级误差降低到微秒级误差,大大提高了容错率。
视频处理
视频处理是整个视频能力最核心的部分,会涉及硬编解码(遵循OpenMAX框架)、OpenGL、音频处理等相关能力。
下图是视频处理的核心流程,会先将音视频做分离,并行处理音视频的编解码,并加入特效处理,最后合成进一个mp4文件中。
在实践过程中,我们遇到了一些需要特别注意的问题,比如开发时遇到的坑,严重的兼容性问题(包括硬件兼容性和系统版本兼容性问题)等。下面重点讲几个有代表性的问题。
1.偶数宽高的编解码器
视频经过编码后输出特定宽高的视频文件时出现了如下错误,信息里仅提示了Colorformat错误,具体如下:
查阅大量资料,也没能解释清楚这个异常的存在。基于日志错误信息,并通过系统源码定位,也只是发现了是和设置的参数不兼容导致的。经过反复的试错,最后确认是部分编解码器只支持偶数的视频宽高,所以我们对视频的宽高做了偶数限制。引起该问题的核心代码如下:
status_t ACodec::setupVideoEncoder(const char *mime, const sp<AMessage> &msg,
sp<AMessage> &outputFormat, sp<AMessage> &inputFormat) {
if (!msg->findInt32("color-format", &tmp)) {
return INVALID_OPERATION;
}
OMX_COLOR_FORMATTYPE colorFormat =
static_cast<OMX_COLOR_FORMATTYPE>(tmp);
status_t err = setVideoPortFormatType(
kPortIndexInput, OMX_VIDEO_CodingUnused, colorFormat);
if (err != OK) {
ALOGE("[%s] does not support color format %d",
mComponentName.c_str(), colorFormat);
return err;
}
.......
}
status_t ACodec::setVideoPortFormatType(OMX_U32 portIndex,OMX_VIDEO_CODINGTYPE compressionFormat,
OMX_COLOR_FORMATTYPE colorFormat,bool usingNativeBuffers) {
......
for (OMX_U32 index = 0; index <= kMaxIndicesToCheck; ++index) {
format.nIndex = index;
status_t err = mOMX->getParameter(
mNode, OMX_IndexParamVideoPortFormat,
&format, sizeof(format));
if (err != OK) {
return err;
}
......
}
2. 颜色格式
我们在处理视频帧的时候,一开始获得的是从Camera读取到的基本的YUV格式数据,如果给编码器设置YUV帧格式,需要考虑YUV的颜色格式。这是因为YUV根据其采样比例,UV分量的排列顺序有很多种不同的颜色格式,Android也支持不同的YUV格式,如果颜色格式不对,会导致花屏等问题。
3. 16位对齐
这也是硬编码中老生常谈的问题了,因为H264编码需要16*16的编码块大小。如果一开始设置输出的视频宽高没有进行16字节对齐,在某些设备(华为,三星等)就会出现绿边,或者花屏。
4. 二次渲染
4.1 视频旋转
在最后的视频处理阶段,用户可以实时的看到加滤镜后的视频效果。这就需要对原始的视频帧进行二次处理,然后在播放器的Surface上渲染。首先我们需要OpenGL 的渲染环境(通过OpenGL的固有流程创建),渲染环境完成后就可以对视频的帧数据进行二次处理了。通过SurfaceTexture的updateTexImage接口,可将视频流中最新的帧数据更新到对应的GL纹理,再操作GL纹理进行滤镜、动画等处理。在处理视频帧数据的时候,首先遇到的是角度问题。在正常播放下(不利用OpenGL处理情况下)通过设置TextureView的角度(和视频的角度做转换)就可以解决,但是加了滤镜后这一方案就失效了。原因是视频的原始数据经过纹理处理再渲染到Surface上,单纯设置TextureView的角度就失效了,解决方案就是对OpenGL传入的纹理坐标做相应的旋转(依据视频的本身的角度)。
4.2 渲染停滞
视频在二次渲染后会出现偶现的画面停滞现象,主要是SurfaceTexture的OnFrameAvailableListener不返回数据了。该问题的根本原因是GPU的渲染和视频帧的读取不同步,进而导致SurfaceTexture的底层核心BufferQueue读取Buffer出了问题。下面我们通过BufferQueue的机制和核心源码深入研究下:
首先从二次渲染的工作流程入手。从图像流(来自Camera预览、视频解码、GL绘制场景等)中获得帧数据,此时OnFrameAvailableListener会回调。再调用updateTexImage(),会根据内容流中最近的图像更新SurfaceTexture对应的GL纹理对象。我们再对纹理对象做处理,比如添加滤镜等效果。SurfaceTexture底层核心管理者是BufferQueue,本身基于生产者消费者模式。
BufferQueue管理的Buffer状态分为:FREE,DEQUEUED,QUEUED,ACQUIRED,SHARED。当Producer需要填充数据时,需要先Dequeue一个Free状态的Buffer,此时Buffer的状态为DEQUEUED,成功后持有者为Producer。随后Producer填充数据完毕后,进行Queue操作,Buffer状态流转为QUEUED,且Owner变为BufferQueue,同时会回调BufferQueue持有的ConsumerListener的onFrameAvailable,进而通知Consumer可对数据进行二次处理了。Consumer先通过Acquire操作,获取处于QUEUED状态的Buffer,此时Owner为Consumer。当Consumer消费完Buffer后,会执行Release,该Buffer会流转回BufferQueue以便重用。BufferQueue核心数据为GraphicBuffer,而GraphicBuffer会根据场景、申请的内存大小、申请方式等的不同而有所不同。
SurfaceTexture的核心流程如下图:
通过上图可知,我们的Producer是Video,填充视频帧后,再对纹理进行特效处理(滤镜等),最后再渲染出来。前面我们分析了BufferQueue的工作流程,但是在Producer要填充数据,执行dequeueBuffer操作时,如果有Buffer已经QUEUED,且申请的dequeuedCount大于mMaxDequeuedBufferCount,就不会再继续申请Free Buffer了,Producer就无法DequeueBuffer,也就导致onFrameAvailable无法最终调用,核心源码如下:
status_t BufferQueueProducer::dequeueBuffer(int *outSlot,sp<android::Fence> *outFence, uint32_t width, uint32_t height,
PixelFormat format, uint32_t usage,FrameEventHistoryDelta* outTimestamps) {
......
int found = BufferItem::INVALID_BUFFER_SLOT;
while (found == BufferItem::INVALID_BUFFER_SLOT) {
status_t status = waitForFreeSlotThenRelock(FreeSlotCaller::Dequeue,
& found);
if (status != NO_ERROR) {
return status;
}
}
......
}
status_t BufferQueueProducer::waitForFreeSlotThenRelock(FreeSlotCaller caller,
int*found) const{
......
while (tryAgain) {
int dequeuedCount = 0;
int acquiredCount = 0;
for (int s : mCore -> mActiveBuffers) {
if (mSlots[s].mBufferState.isDequeued()) {
++dequeuedCount;
}
if (mSlots[s].mBufferState.isAcquired()) {
++acquiredCount;
}
}
// Producers are not allowed to dequeue more than
// mMaxDequeuedBufferCount buffers.
// This check is only done if a buffer has already been queued
if (mCore -> mBufferHasBeenQueued &&
dequeuedCount >= mCore -> mMaxDequeuedBufferCount) {
BQ_LOGE("%s: attempting to exceed the max dequeued buffer count "
"(%d)", callerString, mCore -> mMaxDequeuedBufferCount);
return INVALID_OPERATION;
}
}
.......
}
5. 码流适配
视频的监控体系发现,Android 9.0的系统出现大量的编解码失败问题,错误信息都是相同的。在MediaCodec的Configure时候出异常了,主要原因是我们强制使用了CQ码流,Android 9.0以前并无问题,但9.0及以后对CQ码流增加了新的校验机制而我们没有适配。核心流程代码如下:
status_t ACodec::configureCodec(
const char *mime, const sp<AMessage> &msg) {
.......
if (encoder) {
if (mIsVideo || mIsImage) {
if (!findVideoBitrateControlInfo(msg, &bitrateMode, &bitrate, &quality)) {
return INVALID_OPERATION;
}
} else if (strcasecmp(mime, MEDIA_MIMETYPE_AUDIO_FLAC)
&& !msg->findInt32("bitrate", &bitrate)) {
return INVALID_OPERATION;
}
}
.......
}
static bool findVideoBitrateControlInfo(const sp<AMessage> &msg,
OMX_VIDEO_CONTROLRATETYPE *mode, int32_t *bitrate, int32_t *quality) {
*mode = getVideoBitrateMode(msg);
bool isCQ = (*mode == OMX_Video_ControlRateConstantQuality);
return (!isCQ && msg->findInt32("bitrate", bitrate))
|| (isCQ && msg->findInt32("quality", quality));
}
9.0前并无对CQ码流的强校验,如果不支持该码流也会使用默认支持的码流,
static OMX_VIDEO_CONTROLRATETYPE getBitrateMode(const sp<AMessage> &msg) {
int32_t tmp;
if (!msg->findInt32("bitrate-mode", &tmp)) {
return OMX_Video_ControlRateVariable;
}
return static_cast<OMX_VIDEO_CONTROLRATETYPE>(tmp);
}
关于码流还有个问题就是如果通过系统的接口isBitrateModeSupported(int mode),判断是否支持该码流可能会出现误判,究其原因是framework层写死了该返回值,而并没有从硬件层或从media_codecs.xml去获取该值。关于码流各硬件厂商支持的差异性,可能谷歌也认为码流的兼容性太碎片化,不建议用非默认的码流。
6. 音频处理
音频处理还括对音频的混音,消声等操作。在混音操作的时候,还要注意音频文件的单声道转换等问题。
其实视频问题总结起来,大部分是都会牵扯到编解码(尤其是使用硬编码),需要大量的适配工作(以上也只是部分问题,碎片化还是很严峻的),所以就需要兜底容错方案,比如加入软编。
线上监控
视频功能引入了埋点,日志,链路监控等技术手段进行线上的监控,我们可以针对监控结果进行降级或维护更新。埋点更多的是产品维度的数据收集,日志是辅助定位问题的,而链路监控则可以做到监控预警。我们加了拍摄流程,音视频处理,视频上传流程的全链路监控,整个链路如果任何一个节点出问题都认为是整个链路的失败,若失败次数超过阈值就会通过大象或邮件进行报警,我们在适配Andorid 9.0码流问题时,最早发现也是由于链路监控的预警。所有全链路的成功率目标值均为98%,若成功率低于92%的目标阈值就会触发报警,我们会根据报警的信息和日志定位分析,该异常的影响范围,再根据影响范围确定是否热修复或者降级。
我们以拍摄流程为例,来看看链路各核心节点的监控,如下图:
容灾降级
视频功能目前只支持粗粒度的降级策略。我们在视频入口处做了开关控制,关掉后所有的视频功能都无法使用。我们通过线上监控到视频的稳定性和成功率在特定机型无法保证,导致影响用户正常的使用商家端App,我们支持针对特定设备做降级。后续我们可以做更细粒度的降级策略,比如根据P0级功能做降级,或者编解码策略的降级等
维护更新
视频功能上线后,经历了几个稳定的版本,保持着较高的成功率,但近期收到了Sniffer(美团内部监控系统)的邮件报警,发现视频处理链路的失败次数明显增多,通过Sniffer收集的信息发现大部分都是Android 9.0的问题(也就是上面讲的Android 9.0码流适配的问题),我们在商家端5.2版本进行了修复,该问题解决后我们的视频处理链路成功率也恢复到了98%以上。
总结和规划
视频功能上线后,稳定性、内存、CPU等一些相关指标数据比较理想,我们建设的视频监控体系,也支撑着视频核心业务的监控,一些异常报警也让我们及时发现问题并迅速对异常进行维护更新,但视频技术栈也是远比本文介绍的要庞大,怎么提高秒播率,怎么提高编解码效率,还有硬编解码过程中可能造成的花屏,绿边等问题都是挑战,需要更深入的研究解决。
未来我们会继续致力于提高视频处理的兼容性和效率,优化现有流程,我们会对音频和视频处理合并处理,也会引入软编和自定义编解码算法。
美团外卖大前端团队将来也会继续致力于提高用户的体验,并且会将在实践过程中遇到的问题进行总结,沉底技术,积极的和大家分享,如果你也对视频感兴趣,欢迎加入我们。
参考资料
- Android开发者官网
- Google CTS
- Grafika
- BufferQueue原理介绍
- MediaCodec原理
- 微信Android 视频编码爬过的坑
- mp4文件结构(一)、(二)、(三)、(四)
- AndroidVideoCache 代理策略
- ijkplayer
- mp4parser
- GPUImage
作者简介
金辉、李琼,美团外卖商家终端研发工程师。
招聘信息
美团外卖商家终端研发团队的主要职责是为商家提供稳定可靠的生产经营工具,在保障稳定的需求迭代的基础之上,持续优化APP、PC和H5的性能和用户体验,并不断优化提升团队的研发效率。团队主要负责的业务主要包括外卖订单、商品管理、门店装修、服务市场、门店运营、三方会话、蓝牙打印、自动接单、视频、语音和实时消息触达等基础业务,支撑整个外卖链路的高可用性及稳定发展。
团队通过架构演进及平台化体系化建设,有效支撑业务发展,提升了业务的可靠性和安全性;通过大规模落地跨平台和动态化技术,加快了业务迭代效率,帮助产品(PM)加快产品方案的落地及上线;通过监控容灾体系建设,有效保障业务的高可用性和稳定性;通过性能优化建设,保证APP的流畅性和良好用户体验。团队开发的技术栈包括Android、iOS、React、Flutter和React Native。
美团外卖商家端研发团队长期招聘Android、iOS、和前端工程师,欢迎有兴趣的同学投简历至:tech@meituan.com(邮件标题注明:美团外卖商家端)
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。