踩坑之Web实现Gif多图轮播

4,335 阅读6分钟

一、背景

最近在开发一个热点功能,整体是类似微博内容的短图文排版,但在开发如下图所示的Gif图片的展示轮播时遇到了一些坑,记录下来希望能和大家分享讨论~

weibo

二、需求与问题

首先分析下要实现的具体播放形式:

  1. 从第一张Gif开始轮流播放,同一时间内有且仅有一张图片在播放
  2. 图片播放结束后返回第一帧
  3. 所有图片播放结束后从第一张重新开始播放

看起来实现思路也比较简单:监听Gif图片的播放结束事件,一张Gif播放完再播放下一张,循环往复。

但问题在于GIF图片虽然有“动”的性质,但在Web中被一视同仁地做为图片处理,没有提供任何特殊待遇API,所以无法控制Gif图片的播放、暂停、结束监听等事件。思路中的必要条件——播放结束监听——完全无法达成。

三、思路与方案

既然原生无法控制,那么只好考虑从“替换”入手了。可以将gif替换成其它可控的动态元素,比如video或者canvas,经过一番调研后的两种方案简要如下:

方案对比

四、实现与坑

下面介绍详细方案和具体实现过程,附上相关代码以参考,欢迎一起讨论

A. Gif转Video方案

一、前置条件

1.需要服务端支持提供转换后的video 2. 需要客户端支持video的自动播放及play()调用

二、最大缺陷

video标签在移动端的兼容很麻烦,不仅IOS和Android不同,甚至各平台都有自己的套路(微信重灾区),如果是做分享类页面不推荐该方案

三、实现环境

X5内核

四、核心问题

(一)禁止Ios播放全屏

  1. 配置参数 playsinlinewebkit-playsinline 为 true

(二)首次可自动播放或调用play()函数播放

  1. 配置 mutedautoplay 为 true;Android需要客户端配置IX5WebViewExtensioninvokeMiscMethod(String method, Bundle bundle) 以允许自动播放;
  2. 各平台兼容性不同,最好添加兜底方案:将play()绑定到touchstart事件(模拟点击事件无效)

PS:

  1. 早期的IOS和Android都是禁止自动播放的,play()函数也必须由真实用户交互触发; IOS从版本10以后放宽了规则,无音源或静音的video元素在屏幕可见后允许自动播放; 安卓的chrome 53 后设置 autoplaymuted才允许自动播放;
  2. X5内核作为webview,具备自动播放能力,但这能力的控制权交由TBS宿主控制;
  3. 目前安卓QQ浏览器允许wifi下自动播放、手机QQ允许自动播放、微信不允许自动播放。

(三)禁止用户与视频交互&去除视频控件样式

  1. 不配置controls或者配置为false

PS:如果平台或者webview不支持自动播放,即使配置为false后也可能会出现控件,可尝试下面的CSS方案

video::-webkit-media-controls,
video::-webkit-media-controls-enclosure {
    display: none !important;
}

video::-webkit-media-controls-play-button,
video::-webkit-media-controls-panel,
video::-webkit-media-controls-panel-container,
video::-webkit-media-controls-start-playback-button {
    display: none !important;
    -webkit-appearance: none;
}

五、实现结果

  1. 没有采用此方案所以只有测试数据
  2. 在IOS10以上和绝大多数Android手机均可完美实现轮播需求,测试的10+部不同品牌手机中仅有一台Android5版本的低端vivo无法自动播放
  3. 页面的首次观察到动画时间基本在1s内:Chrome环境下,以原大小为7.5M、平均传输时间在2.5s的Gif图片为例,转成video之后体积仅为2.2M、平均传输时间为1.2s体积缩小70%以上,传输速度提升50%以上);且video支持边传输边播放,感官上等待时间较短
  4. 下图是平均体积2M的6图GIF转换成video之后的播放示例,开头一闪而过的是页面加载状态标志,可以看到无论是页面加载速度还是动起来的速度都较快
    video

六、具体过程

  1. 参数配置:引入video,配置合理参数:去掉视频的控件样式&禁止播放全屏

    <li :class="['hotImgWrap', `imgCount${videos.length}`]" v-for="(v, i) in videos" :key="i">
      <video 
             :id="`hotGif${i}`" 
             class="hotImg" 
             :src="v" 
             :poster="imgUrls[i]"	//设置首帧图片覆盖,因为部分Android手机视频缓冲时会闪过灰色底和白色按钮
             :muted="true" //设置静音,否则无法自动播放+无法用代码触发首次播放
             :controls="false" //关闭用户控制和隐藏控件
             preload="auto"	//预加载
             :webkit-playsinline="true" 
             :playsinline="true"	//和上一行👆一起禁止IOS播放全屏
      ></video>
    </li>
    
  2. 循环播放:获取video元素,首次播放则添加播放结束的监听函数;调用play() 播放当前视频

    //this.currentVideoId: 当前要播放的视频id,在数据初始化时设为 视频数量-1
    //this.videoEles: 已播放过的视频数组,初始化为[], 用于存储 视频元素
    playVideoGif() {
      //获取当前要播放的视频id
      if (this.currentVideoId + 1 === this.videos.length) {
        this.currentVideoId = 0;
      } else {
        this.currentVideoId++;
      }
      if (!this.videoEles[this.currentVideoId]) {
        //如果数组中没有当前视频元素,则获取后加入
        const v = document.getElementById(`hotGif${this.currentVideoId}`);
        this.videoEles.push(v || {});
        
        //每个视频只需添加一次结束事件监听
        v.addEventListener("ended", e => {
          e.target.currentTime = 0;	//播放结束后返回第一帧
          this.playVideoGif();	//调用此播放函数,进行下次播放
        }, false);
      }
      //调用视频播放
      const playPromise = this.videoEles[this.currentVideoId].play && this.videoEles[this.currentVideoId].play();
      //输出调试确认播放结果
      if (playPromise) {
        playPromise.then(() => {
          console.log(`play ${this.currentVideoId} success`);
        }).catch(err => {
          console.log(`play ${this.currentVideoId} fail: `, err);
        });
      }
    }
    

B. Gif+Canvas方案

一、前置条件

图片通过HTTP请求需要跨域,所以需要图片服务器添加CORS配置

二、最大缺陷

很慢,如所用图片较大建议压缩或者换其它方案

  1. 该方案中Gif从请求到显示所需时间为传输时间+解析时间
  2. 其中传输时间略低于通过请求图片,解析则耗时很长。以大小为2M,平均传输时间为1.5s(标签请求)的Gif为例,通过HTPP请求平均耗时0.9s,平均解析时间为3s
  3. 虽然可以边解析边绘制,但在低端Android机上观察到闪动现象,不建议使用

三、核心问题

(一)如何在两天的开发时间中实现canvas对gif的读取、绘制、跳帧以及结束回调呢?

  1. libgif-js :上述功能均支持,有简要文档和示范,是实现的不二之选(主要也没有第二个选择 = =)

(二)canvas的开销很高,IOS测试中4个canvas同时加载已有明显卡顿,但gif图片数量可能到9甚至更多,页面初始化时严重卡顿+崩溃

  1. 首先获取所有GIF第一帧静态图(+缩略图)覆盖其上,目的是快速显示出所有图片
  2. 读取前2张Gif图片,完成后播放第一张;第一张播放结束后,第二张(基本已完成读取)即可播放,同时开始读取第三张,以此类推直到所有图片完成读取
  3. 首轮读取的过程中需要配合静态图的显示与消失

(三)基于问题二,如何获取Gif第一帧静态图or缩略图

  1. 如果图片服务器用的是云的话,大部分都支持通过url参数配置来缩放和转换类型,但是会延长图片请求响应中的等待时间:在Chrome上测试请求阿里云的图片,不加参数时TTFB占总时长为2%左右,偶尔会到最高10%;添加缩放参数后TTFB占比为12%左右,偶尔能到30%+;
  2. 所以图片较大时最好还是找服务端直接提供静态缩略图的url

四、实现结果

  1. 可实现轮播需求,兼容性良好,性能要求不高,在测试和线上环境中暂未收到无法显示或页面崩溃的报告(希望这不是个Flag)
  2. 首张图有明显等待时间,例如2M大小的Gif在页面初始化完成后要等待大概3s(解析时间)后才能动起来
  3. 由于前一张播放时下一张已经在加载,所以后续动图一般无需等待,可流畅播放
  4. 下图为平均体积2M的6图GIF,相比较video方案页面加载时间首张GIF动起来的时间都更长
    canvas

五、具体过程

  1. 引入libgif.js

  2. 获取所有GIF的第一帧静态图(+缩略图)覆盖其上

    <li :class="['hotImgWrap', `imgCount${feedInfo.imageInfos.length}`]" v-for="(img, i) in feedInfo.imageInfos" :key="i" >
      <img v-if="img.gif" :id="`hotGif${img.gifId}`" class="hotImg" :rel:animated_src="getFormatUrl(img)" />	<!--用于渲染GIF-->
      <img class="hotImg" v-show="!img.gif || img.show" :data-index="i" v-lazy="getFormatUrl(img, img.gif)" /> <!--用于快速显示的GIF静态缩略图-->
    </li>
    
  3. 初始化前2张Gif为SuperGif类型并load(),并组成supGifs数组

    const arr = [];
    for (let i = 0; i < 2; i++) {
      const sup = new SuperGif({ 
        gif: document.getElementById(`hotGif${i}`), 
        loop_mode: false, 
        on_end: this.onGifEnd, 
        show_progress_bar: false,
        auto_play: i === 0 ? 1 : 0,	//第一张图load()后自动开始播放
      });
      sup.load(() => {	//GIF load完成后去掉覆盖的缩略图
        this.setImgShow(this.imgGifs[i], false);
      });
      arr.push(sup);
    }
    this.supGifs = arr;
    this.nextGifId++;
    
  4. 初始化时添加播放结束的回调函数on_end

    onGifEnd() {
      //播放结束重置回第一帧
      this.supGifs[this.nextGifId > 0 ? this.nextGifId-1 : this.imgGifs.length-1].move_to(0);
      //下一张开始播放
      this.supGifs[this.nextGifId++].play();
      if (this.nextGifId === this.imgGifs.length) {
        this.nextGifId = 0;
      }
      //判断是否是首轮播放(首轮播放需要初始化和load)
      if (this.supGifs.length < this.imgGifs.length) {
        const len = this.supGifs.length;
        const sup = new SuperGif({ 
          gif: document.getElementById(`hotGif${len}`), 
          loop_mode: false, 
          on_end: this.onGifEnd,
          show_progress_bar: false,
          auto_play: 0
        });
        sup.load(() => {
          this.setImgShow(this.imgGifs[len], false);
        });
        this.supGifs.push(sup);
      }
    },
    

五、总结

这两种方案各有优缺点,这里我去掉了页面加载时间(canvas方案的页面加载时间也更长)可以更直观的对比出两种方案首张图动起来的快慢(上video方案,下canvas方案):

对比

总体来看,video方案体验好但是有兼容性问题且需要服务端支持;canvas方案自由度高、兼容性好,但是在图片体积大的时候,首张图动起来的速度慢,如果没有动图和加载标识的话用户很可能会以为这是张静态图……好在图片多的时候以小图形式展示,缩放后的体积控制在500k以内,最终效果也勉强能接受。 总体来看两种方案都只能算可行,中间也在思考查找其它方案但无果(甚至想找服务端直接加上每张Gif的时长然后用定时器控制)。希望这篇踩坑经历有所帮助,也欢迎随时留言交流想法~感谢阅读

参考文档

X5内核视频问答汇总 视频播放踩坑小计 libgif-js