写点什么

Android 视频技术探索之旅:美团外卖商家端的实践

  • 2019-09-23
  • 本文字数:10442 字

    阅读完需:约 34 分钟

Android视频技术探索之旅:美团外卖商家端的实践

背景

2013 年美团外卖成立,至今一直迅猛发展。随着外卖业务量级与日俱增,单一的文字和图片已无法满足商家的需求,商家迫切需要更丰富的商品描述手段吸引用户,增加流量,进而提高下单转化率和下单量。商品视频的引入,在一定程度上可以提升商品信息描述丰富度,以更加直观的方式为商家引流,增加收益。为此,商家端引入了视频功能,进行了一系列视频功能开发,核心功能包含视频处理(混音,滤镜,加水印,动画等)、视频拍摄、合成等,最终效果图如下所示:




自视频功能上线后,每周视频样本量及使用视频的商家量大幅增加,视频录制成功率达 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,在保证预览图像不拉伸的情况下,只能对完整的采集区域做裁剪,这无形增加了开发难度和挑战。通过大量的资料分析,重点调研了有两种方案:


  1. Camera+AudioRecord+MediaCodec+Surface

  2. 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 等一些相关指标数据比较理想。我们建设的监控体系,覆盖了视频核心业务,一些异常报警让我们能够及时发现问题并迅速对异常进行维护更新。但视频技术栈远比本文介绍的要庞大,怎么提高秒播率,怎么提高编解码效率,还有硬编解码过程中可能造成的花屏、绿边等问题都是挑战,需要更深入的研究解决。


未来我们会继续致力于提高视频处理的兼容性和效率,优化现有流程,我们会对音频和视频处理合并处理,也会引入软编和自定义编解码算法。


美团外卖大前端团队将来也会继续致力于提高用户的体验,将在实践过程中遇到的问题进行总结,继续和大家分享。


敬请关注。如果你也对视频技术感兴趣,欢迎加入我们。

参考资料


作者介绍


金辉、李琼,美团外卖商家终端研发工程师。


本文转载自公众号美团技术团队(ID:meituantech)


原文链接


https://mp.weixin.qq.com/s?__biz=MjM5NjQ5MTI5OA==&mid=2651750633&idx=2&sn=6037630fc5d11511d4333ca44426c479&chksm=bd1259a48a65d0b2c0ce444c586643b1ac4dd52fc080df21d617036c108b7f79e019565d0ef9&scene=27#wechat_redirect


2019-09-23 08:002395

评论 1 条评论

发布
用户头像
牛逼了。
2020-04-08 15:42
回复
没有更多了
发现更多内容

作业:架构实战营模块1

Poplar89

「架构实战营」

我所理解的微服务

gevin

微服务 微服务架构

消费类电子线上问题定位,分析和解决落地

wood

硬件产品 28天写作 线上故障

记录-今年最骄傲的一件事(2)

将军-技术演讲力教练

如何验证你的产品创意?

石云升

产品思维 28天写作 12月日更

TypeScript 之 Class(下)

冴羽

JavaScript typescript 翻译 大前端

我粗心,有救吗?

Justin

心理学 成长 28天写作

架构实战营三期--模块一作业

木几丶

架构实战营 #架构实战营

微信业务架构图&学生管理系统毕业架构设计

Spring

架构实战营

模块一作业

whoami

「架构实战营」

彻底弄懂死锁

李子捌

Java、 28天写作 12月日更

「从0到1如何快速实现cli工具」

速冻鱼

大前端 cli JavaScrip 签约计划第二季 12月日更

Spring AOP(一) AOP基本概念

程序员历小冰

spring aop 28天写作 12月日更

微信业务架构图&&“学生管理系统”毕业架构设计

guodongq

「架构实战营」

学习总结

Anlumina

「架构实战营」

GrowingIO Terraform 实践

GrowingIO技术专栏

运维 SRE Terraform 项目实践 资源编排

Python Qt GUI设计:菜单栏、工具栏和状态栏的使用方法(拓展篇—2)

不脱发的程序猿

Python qt GUI设计 Qt Creator 菜单栏、工具栏、状态栏

架构实战营-模块1-作业

Pyel

「架构实战营」

Hoo虎符研究院 | Arweave调研报告

区块链前沿News

Arweave Hoo虎符 虎符交易所 虎符研究院 去中心化存储

第一周作业

lv

技术架构演进的思考

gevin

架构演进

Week1学习总结

guodongq

「架构实战营」

从实习到秋招成为一名安全工程师,我经历了什么

网络安全学海

面试 网络安全 信息安全 渗透测试 WEB安全

Git 报错:unable to update local ref

liuzhen007

28天写作 12月日更

架构实战营模块1课后作业

墨宝

透过全球首个知识增强千亿大模型,看到中国AI差异化发展之路

脑极体

Rust 元宇宙 15 —— 细节和重构

Miracle

rust 元宇宙

毕业总结

小智

架构训练营

日本公司诚招IT开发技术者

马农驾驾驾

Java c++ php Python 日语

第一模块作业

Anlumina

「架构实战营」

基于云的技术架构设计实践-第5篇

hackstoic

数据分析 云原生 数据可视化 业务分析 签约计划第二季

Android视频技术探索之旅:美团外卖商家端的实践_文化 & 方法_李琼_InfoQ精选文章