精华内容
下载资源
问答
  • 美团外卖商家版电脑版是美团外卖商家后台,它是美团外卖商家在pc端运行的一个美团商家后台。美团是我们经常用来点外卖的一个手机软件,很多商家们也很喜欢从美团上面获取用户
  • 美团外卖商家版电脑版是美团外卖商家后台,它是美团外卖商家在pc端运行的一个美团商家后台。美团是我们经常用来点外卖的一个手机软件,很多商家们也很喜欢从美团上面获取用户订单,美团外卖商家版电脑版可谓是商家们...
  • 美团外卖商家版4.55

    2018-06-09 19:59:58
    美团订餐
  • 仿美团外卖商家百度地图, 包含反向解析地址、自动地图搜索、选择区域跳转、地图点击与拖拽定位、异步加载地图js
  • 仿美团外卖商家百度地图, 包含反向解析地址、自动地图搜索、选择区域跳转、地图点击与拖拽定位。调用相关库
  • 支持一台电脑同时登录两个到6个客户端,同时在线管理,解决了美团外卖商家PC端只能登一个来回切换的难题
  • 仿美团外卖商家点菜效果,能充分学习菜单的功能。
  • 美团外卖商家端视频探索之旅

    千次阅读 2019-09-17 08:58:26
    美团外卖商家端视频探索之旅 背景 美团外卖至今已迅猛发展了六年,随着外卖业务量级与日俱增,单一的文字和图片已无法满足商家的需求,商家迫切需要更丰富的商品描述手段吸引用户,增加流量,进而提高下单转化率和...

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

    背景

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

    自视频功能上线后,每周视频样本量及使用视频的商家量大幅增加,视频录制成功率达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。

    展开全文
  • 我想爬美团外卖某个地点的商家信息,比如北京北太平庄,网址如下:http://waimai.meituan.com/home/wx4ergy6qht3,查看网址源代码是能够看到商家信息的,但是用Python爬虫只能得到美团外卖首页的内容。是我要给网站...
  • 简而言之,该程序员在反编译完美团外卖商家Android后,发现其中有一个杀死后台运行进程的权限,通过Android开发者官方文档,他随之找到了killBackgroundProcesses(String packageName)的调用处,发现美团的Android...

    下午,一位程序员小伙伴在知乎上发了一则帖子技术揭幕贴,题目也是广为人知的知乎体「如何评价美团外卖强杀竞争对手的 App 进程?

    简而言之,该程序员在反编译完美团外卖商家Android版后,发现其中有一个杀死后台运行进程的权限,通过Android开发者官方文档,他随之找到了killBackgroundProcesses(String packageName)的调用处,发现美团的Android程序员直接用Hardcode写死了四款App的包名——饿了么商家版、点评商家版、淘点点商家版、百度外卖商家版。

    这位举报者指出,美团调用killBackgroundProcesses(String packageName)方法的类名是FriendUtil(这个类名倒是很厚道,呵呵),最后他表示美团大胆地用Hardcode封杀的行为,实在有点过了。

    小编也建议做「有节操」的Android 开发者,共同维护平台公平性,希望这次不要又拿实习生出来顶包。

    【更新】

    美团还是反应很快的。公司的副总王慧文在晚7点多时回应,9点多给出了详细的调查结果,“全量发版更新掉了这段代码”,并道歉。

    另外代码的编写者(已离开美团,前员工也能很快找到并回应,看人家这执行力)也给出了自己的说明,并道歉。两者的核心内容是一致的,商家App需要通过蓝牙打印,但Android手机蓝牙模块只能允许一个App与蓝牙设备连接,为了解决争用导致无法打印的问题,就直接写了关闭其他应用的代码。由于Android软件”基本上都会加上后台自启”,所以其他应用其实并不会真正失效。

    知乎上很多讨论来自“杀与被杀”当事双方,都有些意气用事,多数都没什么价值。但其中董涵的回答中,建议这种情况下,可以“使用断开后重连的方式来强制再次连接……毕竟你们使用的蓝牙打印机应该是不同的,可以判断出是否是自己应用连接上。实在不行,可以先disable后再enable。”

    十三郎还给出了代码:

    BluetoothAdapter.getDefaultAdapter().enable(); BluetoothAdapter.getDefaultAdapter().disable();

    对此,代码编写者回复:

    1. 商家那块一般只会放一台蓝牙打印机,所以当一个app连接上的时候,另外一个app必然不能连接
    2. 关于断开蓝牙重连的思路,这个早就尝试过,但是没有效果,原因据我猜测,同行的app监听的蓝牙广播,并且后台自启动,所以即使我们应用在前台,只要他们应用在后台运行着,蓝牙端口也会被抢先占用,所以才考虑kill后台进程的方案

    饿了么的产品经理sacho则从产品角度提出了一种解决方案:

    针对你说的蓝牙端口被其他 app 占用无法连接的问题,饿了么的产品经理会给出如下解决方案:弹窗提醒用户问题所在,请用户选择是否强行关闭其他 app。

    感觉比较合理。

    【头条点评】

    美团在代码中杀对手进程的做法当然是不对的,这个毋庸置疑,不管你初衷如何,影响多大,竞争情况下类似会引起冲突的任何举措都要特别谨慎。否则就是授人以柄,一旦被人抓住,你就等着唾面自干吧。

    但这次事件的性质与当年3Q大战还是有很大区别的:杀死会自动重启的进程与直接卸载对方的软件当然不可同日而语。

    从格调上也有区别,那时候是你死我活,两边都有很重的草莽气。现在虽然也有很多嘴仗(大部分没什么必要),但双方的应对都理性、绅士多了,很欣喜业界新兴公司的这种进步。

    现在问题来了,遇到这种情况,最合理的解决方案是什么呢?吵架没什么意思,欢迎大家继续讨论正事儿。

    展开全文
  • 总第354篇2019年 第32篇美美导读:移动互联网时代,4G的普及推动了移动视频的发展,丰富的视频内容满足了用户多样化的需求。美团外卖商家端也尝试引入了视频功能,旨在提...


    总第354篇

    2019年 第32篇

    美美导读:移动互联网时代,4G的普及推动了移动视频的发展,丰富的视频内容满足了用户多样化的需求。美团外卖商家端也尝试引入了视频功能,旨在提升商品信息描述的丰富度。本文总结了商家端视频功能的闭环全流程实践及部分踩坑经验。

    背景

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

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

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

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

    参考资料

      作者简介

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

    ----------  END  ----------

    招聘信息

    美团外卖商家终端研发团队的主要职责是为商家提供稳定可靠的生产经营工具,在保障稳定的需求迭代的基础之上,持续优化APP、PC和H5的性能和用户体验,并不断优化提升团队的研发效率。团队主要负责的业务主要包括外卖订单、商品管理、门店装修、服务市场、门店运营、三方会话、蓝牙打印、自动接单、视频、语音和实时消息触达等基础业务,支撑整个外卖链路的高可用性及稳定发展。

    团队通过架构演进及平台化体系化建设,有效支撑业务发展,提升了业务的可靠性和安全性;通过大规模落地跨平台和动态化技术,加快了业务迭代效率,帮助产品(PM)加快产品方案的落地及上线;通过监控容灾体系建设,有效保障业务的高可用性和稳定性;通过性能优化建设,保证APP的流畅性和良好用户体验。团队开发的技术栈包括Android、iOS、React、Flutter和React Native。

    美团外卖商家端研发团队长期招聘Android、iOS、和前端工程师,欢迎有兴趣的同学投简历至:tech@meituan.com(邮件标题注明:美团外卖商家端)

    也许你还想看

    美团外卖Android平台化的复用实践

    美团外卖Android平台化架构演进实践

    美团外卖Android Lint代码检查实践

    展开全文
  • 所需工具:findllerchrome获取外卖历史订单地址为:http://e.waimai.meituan.com/v2/order/history/r/query?getNewVo=1&wmOrderPayType=2&wmOrderStatus=-2&sortField=1&startDate=2017-05-30&...

    所需工具:

    findller

    chrome

    获取外卖历史订单地址为:

    http://e.waimai.meituan.com/v2/order/history/r/query?getNewVo=1&wmOrderPayType=2&wmOrderStatus=-2&sortField=1&startDate=2017-05-30&endDate=2017-05-30&lastLabel=&nextLabel=&signToken=05StD%7BKnLehoTpdt%3BjdsaJIg3tMxPAH%5B%40Mn%40luuu4hmFQeD%60Hu7Ie%3Bnd%7BFWr%60pNUD2KnwqI7cUOGZlM%3BSEuvA%60FmNLtujpdz%60AX-a%3B4o-uk)MKl3%7B5dXjUENrzN4rf1XDumEGM%60Vzws0)cA0%3A3RVHe%3F%3F&_token=eJx90FtvolAQAOD%252FwqtEzo2byT6g1iqpFi9QpekDwikcrFAPqGCz%252F32PrJL1ZQnJfBmGmcn8SHwSST0IxKPLUlkIE1ODqgaRAaFIhf%252FmMMS6yG25N5R67xBqmqwh9HHNLETiHWKR0TXjQ74RCyIi3mvNRJRISVl%252B9xSFds8B2wesu6esPAZZN8z3ygkpOY8oVxJWlDmvJfm%252F5ZJoul9dm6qQyJBAUa9CLENstjJaaY2QEG4EhcBVwJAhUls1X8Ux7tKEms5AfRC4CzbTAHlQMxfgB%252Bmtml0AagUf9HcXcBMxzVZiP0gaaa1wK3CXobfC9391rZkhTra7nkzE4BbLWyxYnEk9idpVlILyFO%252BsjeV0XvrVxLGH1dDdTdhgOXATfTOwKRrxz8JbeGnu5HyQmfTlyyT7qRb5VvhEybGfsz23Gcs4K%252BfWBVmHPqDea7EgMdeSsJ4PYE3rEDg1mb88ueF5aKrBxh5bgZGMLkfDnZ7cYHYpHY5jpMxY5kdPkwkx5%252BNtZ%252BjlW4sV1heNYzNIYZYEq2f%252Fkzn87bVGGYSHMb5Uqun3s9oLy8ErDXS63L2BacFZAfR0Napm6y0dZdGUTDuH2Vt2TM2td1gbxtI1o5G3Pq3wNF7aQ38%252B9jvpeUNG1cLJlCrIPt15BVKkq9t17X5fknNleRf%252FGcI%252BsDfr8vxL%252Bv0HGsLkRQ%253D%253D

    里面有一个signToken,我们该如何获取signToken的值呢?

    用chrome访问http://e.waimai.meituan.com/v2/order/history 这个页面后,发现有一个

    http://e.waimai.meituan.com/static/59229326/js/page/order/history.js 比较可疑,

    于是拿出来分析,通过查询发现里面有signToken

    经过分析,得到大概是下面这段js代码

    define(‘module/ajax_util‘,["module/interface"],function(e){var r=jQuery.ajax,t=$(".J-csrf-token"),n=$(".J-sign-token"),a=t.val(),o=n.val(),d=[e.order.getNewOrderFromInterval,e.order.queryProcessedOrderList,e.order.queryHisOrderList];t.remove(),n.remove();var i=function(r){return r&&r.indexOf(e.order.queryHisOrderList)>-1?100007:r&&r.indexOf(e.order.recipientPhone)>-1?100008:r&&r.indexOf(e.order.getPrintOrderInfo)>-1?100009:null},u=location.origin,s=function(e){var r=[];for(var t in e)r.push(t+"="+e[t]);return r.join("&")},f=function(e,r){for(var t=0,n=e.length;n>t;t++)if(r&&-1!==r.indexOf(e[t]))return!0},c=function(e){if(e){for(var r="",t=0,n=e.length;n>t;t++)

    r+=String.fromCharCode(2^e.charCodeAt(t));returnr

    }return""},l=c(o);

    $.ajax=function(e){e.type&&"post"===e.type.toLowerCase()&&(e.data&&"function"==typeof e.data.append?e.data.append("csrfToken",a):e.data=$.extend(e.data||{},{csrfToken:a}))

    ,f(d,e.url)&&(e.data&&"function"==typeof e.data.append?e.data.append("signToken",l):e.data=$.extend(e.data||{},{signToken:l}));var t=function(e,r,t){};e.success&&jQuery.isFunction(e.success)&&(t=e.success),e.success=function(e,r,n){4001===e.code?alert(e.msg):4002===e.code?top.location.reload():1017===e.code?alert(e.msg):t(e,r,n)};var n=function(e,r,t){};"function"==typeof e.error&&(n=e.error),e.error=function(r,t,a){n(r,t,a);({eventTime:Math.floor((new Date).getTime()/1e3),responseCode:r.status,url:e.url,param:e.data,msg:t})};

    var o=i(e.url);if(o){Rohr_Opt.Flag=o;var c=s(e.data?e.data:{});Rohr_Opt.reload(u+e.url+(c?(e.url.indexOf("?")>-1?"&":"?")+c:"")),e.data?e.data._token=rohrdata:e.data={_token:rohrdata}

    }returnr(e)

    }

    });

    signToken的取值:

    1.获取http://e.waimai.meituan.com/v2/order/history 页面隐藏表单域J-sign-token的值

    2.通过以下算法得到r的值,即为signToken

    for(var r="",t=0,n=e.length;n>t;t++)

    r+=String.fromCharCode(2^e.charCodeAt(t));

    展开全文
  • 2013年美团外卖成立,至今一直迅猛发展。随着外卖业务量级与日俱增,单一的文字和图片已无法满足商家的需求,商家迫切需要更丰富的商品描述手段吸引用户,增加流量,进而提高下单转化率和下单量。商品视频的引入,在...
  • 美团外卖管理信息系统分析 指导老师李蕴孙小晴 组长周琳惠 组员孙迎秋武少溥尚雪容史振蕾 分工规划和整理 第一部分周琳惠 第二部分孙迎秋武少溥 第三部分尚雪容史振蕾 第四部分周琳惠 完成时间2017- 一系统简介 1....
  • 这里,小编给大家带来一款刷美团外卖抵用券的软件,轻松一键刷取12张卷,满10块可以省7块,非常实惠,绝对让你物超所值。软件使用很简单,绝对一键领取,需要的朋友下载试试咯! 美团外卖抵用券一次可以用几张 美团...
  • 美团外卖源码

    2017-07-13 23:56:24
    美团外卖源码供学习参考
  • 经典美团外卖解决方案:商户自主管理后台,可以选平台配送员,达达,菜鸟等第三方配送,还是商家自送模式!并且可实现多平台小程序: 【更新机制】 当前应用属于乐高场景体,场景体本身不会经常更新。主要依赖于套餐...
  • 原标题:中午美团外卖出现大面积故障 付款后显示未支付现已修复退款中今天中午通过美团点外卖的小伙伴们可能不少都遇到了悲剧... 据悉,在今天中午11点半左右,美团外卖服务器出现了疑似出现瘫痪,有不少用户反映,...
  • 一、背景1.1 业务背景美团外卖商家端业务形态美团外卖商家端业务围绕数百万商家,在 PC 和 App 上分别提供了交易履约、运营、广告、营销等一系列功能,且经常有外投 H5 的场景(如外卖...
  • 应某客户的“开发定制”要求,基于智慧电商客A2,多商户,掌上配送三个功能,整整乐高了三天,才把《美团外卖》乐高过来!交付后效果还算满意!今天一折特价(相对开发定制费用的一折)发布到进云市场(事先征求过...
  • 主要为大家详细介绍了Android仿美团外卖菜单界面,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
  • 美团外卖的客户端监控体系,针对客户端监控体系的详细讲解。
  • 为您提供进云仿美团外卖源码下载,进云仿美团外卖源码是...经典美团外卖解决方案:商户自主管理后台,可以选平台配送员,达达,菜鸟等第三方配送,还是商家自送模式!并且可实现多平台小程序:【更新机制】当前应用属于
  • 为您提供进云仿美团外卖源码下载,进云仿美团外卖源码是...经典美团外卖解决方案:商户自主管理后台,可以选平台配送员,达达,菜鸟等第三方配送,还是商家自送模式!并且可实现多平台小程序:【更新机制】当前应用属于
  • 为您提供进云仿美团外卖源码下载,进云仿美团外卖源码是...经典美团外卖解决方案:商户自主管理后台,可以选平台配送员,达达,菜鸟等第三方配送,还是商家自送模式!并且可实现多平台小程序:【更新机制】当前应用属于
  • 美团外卖门店数据集.xls
  • 美团外卖Android平台化的复用实践

    千次阅读 2018-09-21 11:21:26
    美团外卖平台化复用主要是指多端代码复用,正如美团外卖iOS多端复用的推动、支撑与思考文章所述,多端包含有两层意思:其一是相同业务的多入口,指美团外卖业务需要在美团外卖App(下文简称外卖App)和美团App外卖...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 3,301
精华内容 1,320
关键字:

美团外卖商家版