乐趣区

美团外卖商家端视频探索之旅

背景

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


自视频功能上线后,每周视频样本量及使用视频的商家量大幅增加,视频录制成功率达 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 等一些相关指标数据比较理想,我们建设的视频监控体系,也支撑着视频核心业务的监控,一些异常报警也让我们及时发现问题并迅速对异常进行维护更新,但视频技术栈也是远比本文介绍的要庞大,怎么提高秒播率,怎么提高编解码效率,还有硬编解码过程中可能造成的花屏,绿边等问题都是挑战,需要更深入的研究解决。

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

美团外卖大前端团队将来也会继续致力于提高用户的体验,并且会将在实践过程中遇到的问题进行总结,沉底技术,积极的和大家分享,如果你也对视频感兴趣,欢迎加入我们。

参考资料

  1. Android 开发者官网
  2. Google CTS
  3. Grafika
  4. BufferQueue 原理介绍
  5. MediaCodec 原理
  6. 微信 Android 视频编码爬过的坑
  7. mp4 文件结构(一)、(二)、(三)、(四)
  8. AndroidVideoCache 代理策略
  9. ijkplayer
  10. mp4parser
  11. GPUImage

作者简介

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

招聘信息

美团外卖商家终端研发团队的主要职责是为商家提供稳定可靠的生产经营工具,在保障稳定的需求迭代的基础之上,持续优化 APP、PC 和 H5 的性能和用户体验,并不断优化提升团队的研发效率。团队主要负责的业务主要包括外卖订单、商品管理、门店装修、服务市场、门店运营、三方会话、蓝牙打印、自动接单、视频、语音和实时消息触达等基础业务,支撑整个外卖链路的高可用性及稳定发展。
团队通过架构演进及平台化体系化建设,有效支撑业务发展,提升了业务的可靠性和安全性;通过大规模落地跨平台和动态化技术,加快了业务迭代效率,帮助产品(PM)加快产品方案的落地及上线;通过监控容灾体系建设,有效保障业务的高可用性和稳定性;通过性能优化建设,保证 APP 的流畅性和良好用户体验。团队开发的技术栈包括 Android、iOS、React、Flutter 和 React Native。
美团外卖商家端研发团队长期招聘 Android、iOS、和前端工程师,欢迎有兴趣的同学投简历至:tech@meituan.com(邮件标题注明:美团外卖商家端)

退出移动版