188js MG---是假的还是真的

GraphQL 确实并没有『火起来』,我觉得是这么几个因素:&br&&br&1. 要在前端爽爽地使用 GraphQL,必须要在服务端搭建符合 GraphQL spec 的接口,基本上是整个改写服务端暴露数据的方式。目前 FB 官方就只有一个 Node.js 的 reference implementation,其他语言都是社区爱好者自己搞的。另外,GraphQL 在前端如何与视图层、状态管理方案结合,目前也只有 React/Relay 这个一个官方方案。换句话说,如果你不是已经在用 Node + React 这个技术栈,引入 GraphQL 成本略高,风险也不小,这就很大程度上限制了受众。&br&&br&2. GraphQL 的 field resolve 如果按照 naive 的方式来写,每一个 field 都对数据库直接跑一个 query,会产生大量冗余 query,虽然网络层面的请求数被优化了,但数据库查询可能会成为性能瓶颈,这里面有很大的优化空间,但并不是那么容易做。FB 本身没有这个问题,因为他们内部数据库这一层也是抽象掉的,写 GraphQL 接口的人不需要顾虑 query 优化的问题。&br&&br&3. 这个事情到底由谁来做?GraphQL 的利好主要是在于前端的开发效率,但落地却需要服务端的全力配合。如果是小公司或者整个公司都是全栈,那可能可以做,但在很多前后端分工比较明确的团队里,要推动 GraphQL 还是会遇到各种协作上的阻力。这可能是没火起来的根本原因。
GraphQL 确实并没有『火起来』,我觉得是这么几个因素: 1. 要在前端爽爽地使用 GraphQL,必须要在服务端搭建符合 GraphQL spec 的接口,基本上是整个改写服务端暴露数据的方式。目前 FB 官方就只有一个 Node.js 的 reference implementation,其他语言都是…
&p&之前很长的一段时间,一直负责在做阿里云人工智能产品 &a href=&///?target=https%3A///index& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&ET&i class=&icon-external&&&/i&&/a& &/p&&p&为了一名前端工程师,参与了当中的一些工程工作,分享出来,希望对大家有所帮助。&/p&&p&&br&&/p&&p&前端工程师在人工智能的团队到底能做什么,能体现怎么的价值?对此,可以先下图的一个总结,然后再会逐条结合实际以及业界的发展情况做一些分析&/p&&img src=&/v2-8adb9ef80a467aed0ce5a62_b.png& data-rawwidth=&1364& data-rawheight=&498& class=&origin_image zh-lightbox-thumb& width=&1364& data-original=&/v2-8adb9ef80a467aed0ce5a62_r.png&&&p&&br&&/p&&p&从我们的实践看,要完成一个完整的人工智能项目,三种东西是不可或缺的:&b&算法,数据和工程&/b&。&/p&&p&而前端在这三个方向种,最容易参与进去,同时也最容易做出彩的地方就是在工程方面,我们把这块内容叫做大前端。&/p&&p&具体的大致可以分为五块内容:&b&人机交互,数据可视化,产品Web, 计算,模型训练和算法执行&/b&。&/p&&p&对于前三点偏重交互的领域,毋庸置疑用前端做起来驾轻就熟,&/p&&p&而后面偏重计算的领域,前端是否合适做,或者说前端该怎么去做是有可以探讨的。&/p&&p&&br&&/p&&p&一. 人机机互&/p&&p&这个应该前端这几年发力的重点,而且取得不错进展的地方。&/p&&p&特别是随着HTML5技术,特别是移动互联网的普及,浏览器对硬件的控制越来越好。&/p&&p&在AI的项目中,很多时候需要获取麦克风和摄像头的权限,好实现“听”,“说”, “看”的功能。&/p&&p&具体可以参考 &a href=&///?target=https%3A//developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&MediaDevices.getUserMedia&i class=&icon-external&&&/i&&/a& 的 H5 文档,里面对这块有详细的介绍。&/p&&p&对于图片的处理,之前网上已经不少的用 Canvas 例子,我就不做过多的介绍。&/p&&p&这里重点对语音处理的内容,这块由于需要很多专业方面的知识,之前处理前端处理起来还是挺痛苦的,不过现在 &a href=&///?target=https%3A//developer.mozilla.org/zh-CN/docs/Web/API/Web_Audio_API& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&Web Audio API&i class=&icon-external&&&/i&&/a& 很好的解决了这个问题。&/p&&p&它提供了在Web上控制音频的一个非常有效通用的系统,允许开发者来自选音频源,对音频添加特效,使音频可视化,添加空间效果 等等。&/p&&p&更有甚者,Chrome中已经自动集成了语音识别的基础 SDK: &a href=&///?target=https%3A///web/updates/2013/01/Voice-Driven-Web-Apps-Introduction-to-the-Web-Speech-API& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&Web Speech API&i class=&icon-external&&&/i&&/a&&/p&&img src=&/v2-2e89ffc5b4abd_b.png& data-rawwidth=&1746& data-rawheight=&412& class=&origin_image zh-lightbox-thumb& width=&1746& data-original=&/v2-2e89ffc5b4abd_r.png&&&p&&br&&/p&&p&二. 数据可视化&/p&&p&数据可视化 可以是前几年特别火的一个方向,特别是大数据风起云涌的时代&/p&&p&而这些年明显的趋势就是人工智能,就是AI,在这里其实也有很多可视化的工作&/p&&p&比如我们在 ET 项目中就需要做很多声音的可视化内容&/p&&img src=&/v2-b9f37b52cd4c1d_b.jpg& data-rawwidth=&701& data-rawheight=&127& class=&origin_image zh-lightbox-thumb& width=&701& data-original=&/v2-b9f37b52cd4c1d_r.jpg&&&p&&br&&/p&&p&以及现在外面在做的一些人脸可视化的内容&/p&&img src=&/v2-926f6a7107e93cfd08a5c447d15b61b0_b.jpg& data-rawwidth=&779& data-rawheight=&541& class=&origin_image zh-lightbox-thumb& width=&779& data-original=&/v2-926f6a7107e93cfd08a5c447d15b61b0_r.jpg&&&p&地址:&a href=&///?target=https%3A///en/& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&PREDICTIVE_WORLD, the program that predicts your future/&i class=&icon-external&&&/i&&/a&&/p&&p&&br&&/p&&p&三. 产品Web&/p&&p&任何人工智能的技术最终一定需要转化成实际的产品或者项目&/p&&p&这样的话,往往少不了Portal和控制台。&/p&&p&这些工作,前端的工作也是在所难免。&/p&&p&这是常规的工作,这里就不再过多描述了&/p&&p&&br&&/p&&p&&br&&/p&&p&四. 算法执行&/p&&p&算法执行顾名思义,其实就是执行算法逻辑,比如人脸识别,语音识别
…&/p&&p&前几年有些大家对前端的认知还停留在纯浏览器端,但随着 V8 引擎在2008 年发布, Node.js 在2009 年 发布,前端的领地就扩展到服务器端,桌面应用。&/p&&p&这些算法执行的原先都需要后端同学开发,现在也可以由前端同学才完成。&/p&&p&我们很多AI的项目,很多时候往往就是算法的同学提供给我们一些动态链接库或者C的代码,我们通过Nodejs驱动这些服务提供 http接口,浏览器通过ajax来调用这些接口。&/p&&p&更有甚者,现在PC性能提升,V8对JS执行的优化,特别WebGL 在各个浏览器端的普及&/p&&p&很多算法执行不一定并不一定需要在后端执行,浏览器也可以胜任。&/p&&p&比如:&/p&&p&&a href=&///?target=https%3A///& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&Tranck.js&i class=&icon-external&&&/i&&/a&:就是纯浏览器的图像算法库,通过javascript计算来执行算法逻辑&/p&&img src=&/v2-fe6fecc8d16e6d42cc40c70dbdb38888_b.png& data-rawwidth=&1566& data-rawheight=&622& class=&origin_image zh-lightbox-thumb& width=&1566& data-original=&/v2-fe6fecc8d16e6d42cc40c70dbdb38888_r.png&&&p&&br&&/p&&p&&a href=&///?target=https%3A///Erkaman/regl-cnn& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&regl-cnn&i class=&icon-external&&&/i&&/a&: 浏览器端的数字识别类库,与track.js 不同的是,它利用浏览器的WebGL 来操纵GPU,
实现了CNN&/p&&p&&br&&/p&&img src=&/v2-71e844aaca1e88ecd778a7_b.jpg& data-rawwidth=&324& data-rawheight=&204& class=&content_image& width=&324&&&p&&br&&/p&&p&五. 模型训练&/p&&p&虽然现在阶段也出现了像 ConvNetJS 这种在浏览器端做深度学习算法训练的工具,&/p&&p&但整体而言,前端在这块还是非常欠缺的,同时缺少非常成功的实践。&/p&&p&究其原因,还是因为跨了领域,特别是专业类库往往都不是javascript写的,造成更大的隔阂&/p&&p&但就像谷歌的TensorFlow机器学习框架底层大部分使用 C++实现,但选择了 Python 作为应用层的编程语言。&/p&&p&Javascript 在各个端,特别是web端的优势,也是一门非常优秀的应用开发语言。&/p&&p&可喜的是看到挺多同学在往这个方向走,我们拭目以待&/p&&p&&a href=&///?target=http%3A//cs.stanford.edu/people/karpathy/convnetjs/& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&ConvNetJS&i class=&icon-external&&&/i&&/a&:Deep Learning in your browser&/p&&p&&/p&&p&&/p&&p&&/p&&p&&/p&&p&&/p&&p&&/p&&p&&/p&
之前很长的一段时间,一直负责在做阿里云人工智能产品
为了一名前端工程师,参与了当中的一些工程工作,分享出来,希望对大家有所帮助。 前端工程师在人工智能的团队到底能做什么,能体现怎么的价值?对此,可以先下图的一个总结,然后再会逐条结合实际…
&blockquote&&p&本文来自“小时光茶社(Tech Teahouse)”公众号&/p&
&/blockquote&
&p&&strong&作者简介:&/strong&&br& 文赫,2015年加入腾讯,作为前端开发工程师参与过手Q游戏公会,游戏中心,企鹅电竞等项目,具有丰富的移动端开发和直播开发经验。&/p&
&h2&导语&/h2&
&p& 企鹅电竞项目,直播和视频播放是其中的核心。面对着产品同学不断的询问:为什么h5的体验这么差?为什么不能和app的播放体验保持一致?我们对着h5不明确的文档和不同浏览器的怪异表现欲哭无泪。 经过一系列的调研爬坑,斩荆披棘,我们一步步提升了体验,做到了和app基本一致的体验。在摸索优化背后,我们也想把这些问题和解决方法总结下来,让其他同学做到直播的时候可以自豪的说,这就是我们的h5直播体验&/p&
&h2&1. 自动播放问题&/h2&
&ul&&li&&p&&strong&通过autoplay属性&/strong&&br&视频的自动播放需要在video标签上添加autoplay属性, 如:&/p&
&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&&video autoplay&&video/&
&/code&&/pre&&/div&&p&但是在很多浏览器里,如iOS下并不支持这个属性,在iOS下必须给webview设置&/p&
&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&self.wView.allowsInlineMediaPlayback = YES;self.wView.mediaPlaybackRequiresUserAction = NO;
&/code&&/pre&&/div&&p&才能让这个属性生效从而让用户一进入页面就开始视频的自动播放&/p&
&li&&p&&strong&通过直接调用video.play()方法&/strong&&br&在一些情况下我们想加入一些判断逻辑,如判断用户网络环境,在wifi下自动播放,在4g环境下给出提示,这中情况下就适合直接选中video并调用video.play来播放视频&/p&
&/ul&&h2&2. 页面内联播放问题&/h2&
&p& 在iOS Safari和一些安卓的一些浏览器下播放视频的时候,不能在h5页面中播放视频,系统会自动接管视频&/p&
&p& 如果需要在h5页面内播放视频,需要在视频标签上加上 webkit-playsinline,在iOS10以后,需要加上playsinline,建议同时加上这两个属性,同时需要app支持这种模式,手Q和微信都支持这种模式&/p&
&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&
&video id=&player& webkit-playsinline playsinline &
//在app内设置webview属性
webview.allowsInlineMediaPlayback = YES;
&/code&&/pre&&/div&&h2&3. 视频的高度问题&/h2&
&p& 在安卓下,一些浏览器如QQ浏览器和UC浏览器,系统会把视频的层级调到最高,所以如果想在页面上显示dom元素,都会被视频盖住,单纯的设置该dom的z-index是无效的,如图所示&/p&
&img src=&/v2-dedfd46e3b5d317ac81c51d4_b.png& data-rawwidth=&400& data-rawheight=&705& class=&content_image& width=&400&&&p&&strong&解决方案:&/strong&&/p&
&ol&&li&在弹出会显示在视频上方dom的时候暂停视频播放&/li&
&li&将视频所在的dom的父元素的高度设为1&/li&
&li&处理完弹出的事件后将视频所在的父元素高度还原&/li&
&/ol&&h2&4. 视频的默认播放图标&/h2&
&p& 在iOS下会有一个默认的播放图标,如图所示&/p&
&img src=&/v2-ce89ea9f36dbcd3fbcf43e_b.png& data-rawwidth=&400& data-rawheight=&671& class=&content_image& width=&400&&&p& 在iOS都会默认显示,不能通过js控制,但是可以通过css样式将其隐藏&/p&
&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&
video::-webkit-media-controls-start-playback-button {
&/code&&/pre&&/div&&h2&5. 视频的控制栏&/h2&
&p& 在h5播放的时候,如果在video标签上设置了controls属性,则会在视频里显示控制栏&/p&
&img src=&/v2-7c5186fed14cb36ca319caf980a6f7d1_b.png& data-rawwidth=&400& data-rawheight=&708& class=&content_image& width=&400&&&p& 需要注意的是这个控制栏是系统webview自带的,无法通过css控制其样式,建议不要使用这个属性而是自己通过dom自己制作一套控制条&/p&
&h2&6. 视频的刷新&/h2&
&p& 我们知道video暴露了play和pause方法来提供视频的播放和暂停,但是h5没有标准的刷新方法,如果我们想实现视频的刷新,则需要通过js实现&/p&
&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span& var player = $('#player')[0];
player.load();
setTimeout(function () {
player.play();
&/code&&/pre&&/div&&h2&7. 视频的全屏问题&/h2&
&h4&1)全屏api&/h4&
&p& h5暴露了一个webkitRequestFullScreen方法,可以让每个dom都请求全屏,当然video标签也可以使用。&/p&
&p& 但是在测试中发现,一些安卓机不支持该属性,如小米手机,所以需要在调用的时候进行一下判断&/p&
&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span& var player = $('#player')[0];if (player.webkitSupportsFullscreen) {
player.webkitEnterFullscreen();
player.webkitRequestFullScreen();
&/code&&/pre&&/div&&h4&2)系统接管播放&/h4&
&p& 我们上边说道调用h5的webkitRequestFullScreen方法来进入视频的全屏,那么这个方法会使浏览器完全接管视频播放,如图所示&/p&
&img src=&/v2-2db1d23befb_b.png& data-rawwidth=&400& data-rawheight=&711& class=&content_image& width=&400&&&p& 这种接管的后果是这时的我们是没有办法控制视频的播放,也没有办法在上面浮动我们的dom元素,如弹幕,礼物这些,会完全被视频盖在下面,所以我们的目标即是解决这种系统接管的问题&/p&
&h4&3)使用伪全屏(样式全屏)&/h4&
&p& 样式全屏的核心是设置video标签的宽高,使其撑满整个webview,看上去像全屏一样&/p&
&p& 但是因为视频一般都是16:9的宽高比,所以在竖屏情况下不能很好的做到铺满整个屏幕&/p&
&img src=&/v2-6dafbe23bdedf8daa071dd04_b.png& data-rawwidth=&400& data-rawheight=&711& class=&content_image& width=&400&&&p& 而一般用户进入页面基本都是竖屏,所以我们就要考虑怎么让用户在竖屏点击全屏按钮时,能体验到像终端app一样自动进入横屏全屏的体验,下面有两种方案&/p&
&p&&strong&1.在用户点击全屏时候,通过css3属性旋转屏幕&/strong&&/p&
&p& 通过css的transform,我们可以把dom元素旋转显示&/p&
&p& 通过-webkit-transform: rotate(90deg)并设置video的高度为当前webview的宽度,video的宽度为当前webview的高度来实现旋转全屏.&/p&
&p& 这种模式的显示没有太大问题,但因为是通过css控制的页面dom显示,对于原生的空间不能很好的控制,如系统的键盘&/p&
&img src=&/v2-ce5eb1d14d351a1196191cf_b.png& data-rawwidth=&400& data-rawheight=&707& class=&content_image& width=&400&&&p& 在拉起键盘输入弹幕的时候,键盘不受控制还是竖屏显示了&br& 如果页面不涉及与原生组件的交互,那么这种方案是一种很可行且兼容性比较好的方案&/p&
&p&&strong&2.用户在点击全屏时,通过js api来控制webview旋转横屏&/strong&&/p&
&p& 在手Q里,我们和终端的同学合作添加了控制webview横竖屏的接口&br& 在用户点击全屏的时候,先判断当前是否横屏&/p&
&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&
* 是否横屏
function isHorizontal() {
if (window.orientation != undefined) {
return (window.orientation == 90 || window.orientation == -90);
return window.innerWidth & window.innerH
&/code&&/pre&&/div&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span& //设置webview为横竖屏
mqq.ui.setWebViewBehavior({
orientation: 0 //0是竖屏,1是横屏
&/code&&/pre&&/div&&p& 如果是竖屏则强制webview旋转进入横屏,同时监听页面的resize方法,页面横竖屏变化的时候会触发这个方法,在这个方法里再动态的调整video的宽高来铺满整个屏幕&/p&
&img src=&/v2-55ebd8f3fc9a_b.jpg& data-rawwidth=&711& data-rawheight=&400& class=&origin_image zh-lightbox-thumb& width=&711& data-original=&/v2-55ebd8f3fc9a_r.jpg&&&p& 注:&/p&
&p& 之前我们发现x5插入了一段js来劫持视频的全屏的事件&/p&
&img src=&/v2-6c896b1e85f536a02c6fa8_b.png& data-rawwidth=&1345& data-rawheight=&400& class=&origin_image zh-lightbox-thumb& width=&1345& data-original=&/v2-6c896b1e85f536a02c6fa8_r.png&&&p& 满足条件的video标签全屏时都会被X5接管,另外调用webkitEnterFullscreen方法时,X5也会接管播放器。&/p&
&h2&总结:&/h2&
&p& 在经历过各种优化和调整后,我们可以在h5直播页做到看直播,看弹幕,发弹幕,发送礼物,表情,查看排名等各种功能,再配合上手Q里的隐藏titlebar的_wv=,可以实现全屏播放效果,做到了媲美原生播放的体验。&/p&
&p& 现在的h5的播放还有很多的表现和兼容性的问题,希望这份指南可以帮你在遇到同样的坑时能尽快爬出来,并优化你的h5播放体验,吸引到更多的用户 : D&/p&
&p&更多精彩内容欢迎关注&a href=&/?target=https%3A///& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&bugly&i class=&icon-external&&&/i&&/a&的微信公众账号:&/p&&img src=&/3f2c1b1ff77fcedf3fb54616_b.jpg& class=&content_image&&&p&&a href=&/?target=https%3A///& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&腾讯 Bugly&i class=&icon-external&&&/i&&/a&是一款专为移动开发者打造的质量监控工具,帮助开发者快速,便捷的定位线上应用崩溃的情况以及解决方案。智能合并功能帮助开发同学把每天上报的数千条 &a href=&/?target=https%3A///& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&Crash&i class=&icon-external&&&/i&&/a& 根据根因合并分类,每日日报会列出影响用户数最多的崩溃,精准定位功能帮助开发同学定位到出问题的代码行,实时上报可以在发布后快速的了解应用的质量情况,适配最新的 iOS, Android 官方操作系统,鹅厂的工程师都在使用,快来加入我们吧!&/p&
本文来自“小时光茶社(Tech Teahouse)”公众号
作者简介: 文赫,2015年加入腾讯,作为前端开发工程师参与过手Q游戏公会,游戏中心,企鹅电竞等项目,具有丰富的移动端开发和直播开发经验。
企鹅电竞项目,直播和视频播放是其中的核心。面对着产品…
&p&(原答案有问题,以下为修改的答案)&/p&&p&form 提交之后会更新页面,而假如更新到指定 iframe/window 中,如果是跨域的,父页面访问其内容一样受到 same origin 策略限制。&/p&
(原答案有问题,以下为修改的答案)form 提交之后会更新页面,而假如更新到指定 iframe/window 中,如果是跨域的,父页面访问其内容一样受到 same origin 策略限制。
&p&JS字符串的长度受到下标限制。理论最大长度是2^53-1(即js中可表达的最大安全整数)。&/p&&p&2^53是多大呢?大约9PB。根据统计,中国2014年所有出版物(不计复本)不到2000亿字,也就是400GB而已。&/p&&p&按此推算,不要说存一个txt了,中国自有甲骨文以来所有的书、各类出版物字数加在一起估计也不可能超过100TB,也就是0.1PB。&/p&&br&&p&当然啦,实际引擎是不可能允许分配那么大的字符串的,你的电脑也没那么大存储不是。V8的heap上限只有2GB不到,允许分配的单个字符串大小上限更只有大约是512MB不到。JS字符串是UTF16编码保存,所以也就是2.68亿个字符。FF大约也是这个数字。&/p&&p&根据&a href=&/question//answer/& class=&internal&&最长的网络小说是哪部? - 知乎&/a&,目前最长的网络小说大概2000万字。所以还是绰绰有余的。&/p&&br&&p&《道藏》大约7000万字,《大藏经》大约1亿字,也是存得下的。&/p&&br&&p&不过《永乐大典》有3.7亿字,《四库全书》有8亿字,V8/FF的一个字符串就存不下喽。&/p&&p&然而IE11貌似可以存4GB的字符串,即21亿字。&/p&&br&&p&以上。&/p&
JS字符串的长度受到下标限制。理论最大长度是2^53-1(即js中可表达的最大安全整数)。2^53是多大呢?大约9PB。根据统计,中国2014年所有出版物(不计复本)不到2000亿字,也就是400GB而已。按此推算,不要说存一个txt了,中国自有甲骨文以来所有的书、各类…
&p&Myers' Diff Algorithm: &/p&&a href=&///?target=http%3A//www.xmailserver.org/diff2.pdf& class=& external& target=&_blank& rel=&nofollow noreferrer&&&span class=&invisible&&http://www.&/span&&span class=&visible&&xmailserver.org/diff2.p&/span&&span class=&invisible&&df&/span&&span class=&ellipsis&&&/span&&i class=&icon-external&&&/i&&/a&&br&&br&实现:&a href=&///?target=https%3A///chenshuo/google-diff-match-patch& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&chenshuo/google-diff-match-patch&i class=&icon-external&&&/i&&/a&
Myers' Diff Algorithm:
&img src=&/v2-f2ca632f946b0b9d2ea22_b.jpg& data-rawwidth=&1280& data-rawheight=&622& class=&origin_image zh-lightbox-thumb& width=&1280& data-original=&/v2-f2ca632f946b0b9d2ea22_r.jpg&&&h2&&b&这是一个特殊的 worker&/b&&/h2&
&p&浏览器一般有三类 web Worker:&/p&
&ol&&li&Dedicated Worker:专用的 worker,只能被创建它的 JS 访问,创建它的页面关闭,它的生命周期就结束了。&/li&
&li&Shared Worker:共享的 worker,可以被同一域名下的 JS 访问,关联的页面都关闭时,它的生命周期就结束了。&/li&
&li&ServiceWorker:是事件驱动的 worker,生命周期与页面无关,关联页面未关闭时,它也可以退出,没有关联页面时,它也可以启动,&/li&
&/ol&&p&这三者有什么区别呢?众所周知,JShted 的执行线程,同一时刻内只会有一段代码在执行。Web worker 目的是为 JS 是单线程的,即一个浏览器进程中只有一个 JS 创造多线程环境,允许主线程将一些任务分配给子线程。Web Worker 一般是用于在后台执行一些耗时较长的 JS,避免影响 UI 线程的响应速度。&/p&
&p&Dedicated worker 或 shared worker 最主要的能力,一是后台运行 JS,不影响 UI 线程,二是使用消息机制实现并行,可以监听 onmessage 事件。所以 dedicated worker 和 shared worker 专注于解决“耗时的 JS 执行影响 UI 响应”的问题,而 service worker 则是为解决“Web App 的用户体验不如 Native App”的普遍问题而提供的一系列技术集合,必然部分处理逻辑会牵扯到 UI 线程,从而在启动 service worker 的时候,UI 线程的繁忙也会影响其启动性能。&/p&
&p&显然 service worker 的使命更加远大,虽然规范把它定义为 web worker,但它已不是一个普通的 worker了。&/p&
&h2&&b&每一部分的作用&/b&&/h2&
&p&Google 官方入门文档提到,它能提供丰富的离线体验,周期的后台同步,消息推送通知,拦截和处理网络请求,以及管理资源缓存。这每个能力各自都有什么作用呢?&/p&
&p&&b&1. 丰富的离线体验&/b&&/p&
&p&首先,一提到 service worker,很多人都会想到离线访问,而且不少文章都会提到,service worker 能提供丰富的离线体验,但实际情况来说,需要离线访问的场景很少,毕竟 web 最大的特点在于可传播性,所以 service worker 的离线体验主要还是在于解决页面加载的可靠性,让用户能够完整地打开整个页面,比如页面的白屏时间过长,网络不稳定造成的加载中断导致页面不可用。&/p&
&p&有实际意义的离线,一般不是指断开网络能访问,而是指在用户想访问之前,能提前把资源加载回来。离线并不是一直都断开网络,而是在网络连接良好的情况下,能把需要的资源都加载回来。一些比较糟糕的做法是在 WIFI 网络下把整个 App 客户端的资源都拉下来,这样其实很多资源是用户不需要的,浪费了用户的网络和存储。Service worker 提供了更好更丰富的离线技术,Push / Fetch / Cache 这些技术的结合,能够提供非常完美的离线体验。比如,在小程序页面发版时,推送消息给客户端,客户端唤起页面的 service worker,去将需要用到的资源提前加载回来。&/p&
&p&&b&2. 消息推送通知&/b&&/p&
&p&Service worker 的消息推送,其实是提供了一种服务器与页面交互的技术。消息推送在 Native App 或 Hybird App 已经比较常见。很多 Hybird App 里面其实还会有一些 H5 页面,在没有实现 service worker 消息推送之前,消息是推送不到页面的。消息能推送到页面,意味着页面提前知道要发生的一些事情,把这些事情做好,比如,提前准备好页面需要的资源。Push 的推送服务器,Chromium 默认使用 GCM / FCM,在国内都不能访问,无法使用。浏览器厂商自己搭建 Push 服务器,成本也不低,目前国内还未有浏览器厂商支持标准的Push 服务。从 API 的使用规范来看,消息推送与通知弹窗的关联比较密切,基本上使用的业务场景仅限制在消息通知范围。&/p&
&p&&b&3. 管理资源缓存&/b&&/p&
&p&浏览器提供了很多存储相关的 H5 API,比如 application cache、localStorage,但都不是非常好用,主要是给予页端的控制权太少,限制太多,页端不能完全控制每一个资源请求的存储逻辑,或多或少会有一些趟不过的坑。Service worker Cache API 的出现彻底改变了这一局面,赋予了页端强大的灵活性,更大的存储空间。如何灵活地控制缓存,可以参考 Google 官方文章 《&a href=&/?target=https%3A///web/fundamentals/instant-and-offline/offline-cookbook/& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&The Offline Cookbook&i class=&icon-external&&&/i&&/a&》。&/p&
&p&&b&4. 网络请求&/b&&/p&
&p&在 Fetch 出现之前,页面 JS 一般通过 XHR 发起网络资源请求,但 XHR 有一定的局限性,比如,它不像普通请求那样支持 Request 和 Response 对象,也不支持 streaming response,一些跨域的场景也限制较多。而现在,Fetch API 支持 Request 和 Response 对象,也支持 streaming response,Foreign Fetch 还具备跨域的能力。&/p&
&p&一般来说,基于 webview 的客户端拦截网络请求,都会基于 WebViewClient 的标准的 shouldInterceptRequest 接口。那么 service worker 的请求在 webview 还能不能拦截呢?WebViewClient 的标准的 shouldInterceptRequest 接口是拦截不了 service worker 的请求了,但 Chrome 49.0 提供了新的 ServiceWorkerController 可以拦截所有 service worker 的请求。另外,页端 JS 可以监听 Fetch 事件,通过 FetchEvent.respondWith 返回符合期望的 Response,即页端也能拦截 Response。&/p&
&p&&b&尴尬的处境&/b&&/p&
&p&Service worker 的理想看起来很美好,现实却很骨感,为什么这么说呢?GCM / FCM 服务被墙不说,强大的 Background Sync 功能也需要依赖 Google Play,而国内 Android 手机厂商自带的 ROM 基本上都把 Google Play 干掉了,并且还被墙了,略尴尬。比这更尴尬的是,Apple iOS 团队对 Service Worker 的态度很不明朗,现在是,将来可能也是,所以现在很多特性在 iOS 上都不支持。&/p&
&h2&&b&启动分析&/b&&/h2&
&p&Service worker 线程的整个启动流程可划分为五个步骤:&/p&
&p&&b&1. 触发启动流程&/b&&/p&
&p&一般来说,我们在访问一个含有 service worker 的 HTML 文档时,在发起主文档请求之前,它会先派发一个 Fetch 事件,这个事件会触发该页面 service worker 的启动流程。&/p&
&p&&b&2. 分派进程(多进程模式)/ 线程(单进程模式)&/b&&/p&
&p&Service worker 启动之前,它必须先向浏览器 UI 线程申请分派一个线程,再回到 IO 线程继续执行 service worker 线程的启动流程:&/p&&p&content::EmbeddedWorkerInstance::Start&br&--& content::EmbeddedWorkerInstance::RunProcessAllocated
&br&--& ServiceWorkerProcessManager::AllocateWorkerProcess
// from IO thread&br&--& ServiceWorkerProcessManager::AllocateWorkerProcess
// PostTask to UI thread&br&--& ServiceWorkerProcessManager::AllocateWorkerProcess
// from UI thread&br&--& content::EmbeddedWorkerInstance::ProcessAllocated
// from IO thread&br&--& content::EmbeddedWorkerInstance::RegisterToWorkerDevToolsManager
// from IO thread&br&--& content::EmbeddedWorkerInstance::RegisterToWorkerDevToolsManager
// PostTask to UI thread&br&--& content::EmbeddedWorkerInstance::RegisterToWorkerDevToolsManager
// from UI thread&br&--& content::EmbeddedWorkerInstance::SendStartWorker
// from IO thread&br&--& content::EmbeddedWorkerRegistry::SendStartWorker&br&--& content::EmbeddedWorkerDispatcher::OnStartWorker&/p&
&p&这个过程中,由于频繁的 IO 与 UI 的线程切换,导致 service worker 启动过程中,存在一定的性能开销。&/p&
&p&&b&3. 加载 service worker js 文件&/b&&/p&
&p&分派了 service worker 线程之后,就会继续执行 service worker js 文件的加载流程:&/p&
&p&content::EmbeddedWorkerDispatcher::OnStartWorker&br&--& blink::WebEmbeddedWorkerImpl::startWorkerContext&br&--& blink::WebEmbeddedWorkerImpl::loadShadowPage
// 加载一个与 Service Worker js 文件相同 URL 的空白影子文档&br&--& blink::FrameLoader::load
// 触发空白文档的加载&br&--& ... ...&br&--& blink::WebEmbeddedWorkerImpl::didFinishDocumentLoad&br&--& blink::WebEmbeddedWorkerImpl::Loader::load
// 触发 Service Worker js 文件的加载&br&--& content::ResourceDispatcherHostImpl::BeginRequest&br&--& content::ServiceWorkerReadFromCacheJob::Start&br&--& content::ServiceWorkerReadFromCacheJob::OnReadComplete&br&--& ResourceLoader::didFinishLoading
// 完成 Service Worker js 文件的加载&/p&
&p&这个过程中,它会先加载一个空白影子文档,再去加载 service worker js 文件,也就是说,会走两次完整的加载流程。&/p&
&p&&b&4. 启动 Service Worker 线程&/b&&/p&
&p&Service worker js 文件加载完成之后,就会触发 service worker 线程的启动流程。这个过程中,主要包括创建 ServiceWorkerGlobalScope,初始化上下文( WorkerScriptController::initializeContextIfNeeded )和执行 JS 代码( WorkerScriptController::evaluate )。&/p&
&p&&b&5. 回调通知启动完成&/b&&/p&
&p&Service worker 线程启动完成之后,回调通知 ServiceWorkerVersion。至此,service worker 线程启动完成。&/p&
&p&WebEmbeddedWorkerImpl::startWorkerThread
// 启动 Service Worker 线程&br&--& new ServiceWorkerThread::ServiceWorkerThread&br&--& content::ServiceWorkerDispatcherHost::OnWorkerStarted&br&--& content::EmbeddedWorkerRegistry::OnWorkerStarted&br&--& content::EmbeddedWorkerInstance::OnStarted&br&--& content::ServiceWorkerVersion::OnStarted
// 启动 Service Worker 线程完成&/p&
&p&从上面 5 个步骤可以看到,service worker 的启动流程极其复杂,这么复杂的启动流程,会带来怎样的性能消耗呢?我们通过本地测试 Chromium 57 内核版本,初步得出几个结论:&/p&
&ul&&li&分派 service worker 进程/线程的过程中,有频繁的不同类型线程转换,IO --& UI --& IO --& UI --& IO,这个过程中 UI 线程如果非常繁忙,耗时将会非常大,甚至可以超过 200ms。&/li&
&li&加载 service worker js 文件,首次加载需要创建 https 连接并等待服务器响应,耗时可以超过 700ms,但在非首次的场景下,可以从缓存读取,一般能在 50ms 以内完成。&/li&
&li&手机锁屏开屏的场景下,浏览器大部分内存都会被清除,会极大的影响缓存读取以及对象创建的时间,比如创建 v8 isolate,一般能在 10ms 完成,但锁屏之后要 80ms 才能完成。&/li&
&/ul&&p&Google 官方文档《&a href=&/?target=https%3A///web/updates/2017/02/navigation-preload& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&Speed up Service Worker with Navigation Preloads&i class=&icon-external&&&/i&&/a&》提到:&/p&
&blockquote&The bootup time depends on the device and conditions. It's usually around 50ms. On mobile it's more like 250ms. In extreme cases (slow devices, CPU in distress) it can be over 500ms. However, since the service worker stays awake for a browser-determined time between events, you only get this delay occasionally, such as when the user navigates to your site from a fresh tab, or another site.&br&&/blockquote&
&p&Service worker 的启动时间与用户设备条件有关,在 PC 上一般为 50ms,手机上大概为 250ms。在极端的场景下,如低端手机且 CPU 压力较大时,可能会超出 500ms。Chromium 团队已尝试使用多种方式来减少 service worker 的启动时间, 比如: &/p&
&ul&&li&使用 V8 Code Cache(using code-caching in V8 &a href=&/?target=https%3A//v8project.blogspot.hk/2015/07/code-caching.html& class=& external& target=&_blank& rel=&nofollow noreferrer&&&span class=&invisible&&https://&/span&&span class=&visible&&v8project.blogspot.hk/2&/span&&span class=&invisible&&015/07/code-caching.html&/span&&span class=&ellipsis&&&/span&&i class=&icon-external&&&/i&&/a&)。&/li&
&li&在没有注册监听 Fetch 事件的页面允许先发网络请求(skipping service workers that don't have a fetch event &a href=&/?target=https%3A//bugs.chromium.org/p/chromium/issues/detail%3Fid%3D605844& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&605844 - Optimization when no fetch handler is registered - chromium - Monorail&i class=&icon-external&&&/i&&/a&)。&/li&
&li&在特定情境下(比如 mouse / touch 事件), 预先启动 service worker(launching service workers speculatively &a href=&/?target=https%3A//codereview.chromium.org/& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&Issue : Speculatively launch Service Workers on mouse/touch events. [4/5]&i class=&icon-external&&&/i&&/a&)。&/li&
&li&使用 Navigation Preloads &a href=&/?target=https%3A///feature/8448& class=& external& target=&_blank& rel=&nofollow noreferrer&&&span class=&invisible&&https://www.&/span&&span class=&visible&&/featur&/span&&span class=&invisible&&e/8448&/span&&span class=&ellipsis&&&/span&&i class=&icon-external&&&/i&&/a& 技术, 允许 Fetch 请求在 service worker 还未启动完成时就可发出, 从而减少启动时间对总体性能的影响。&/li&
&li&从我们的测试数据来看,service worker 线程的启动耗时一般在 100-300ms,与 Chromium 官方的数据相近。所以我们能够得出一个大概的推论,service worker 线程的启动是有较大成本的,一般在 100-300ms。&/li&
&/ul&&h2&&b&生命周期与状态&/b&&/h2&
&p&&b&1. 生命周期&/b&&/p&
&p&Google 官方文档 《&a href=&/?target=https%3A///web/fundamentals/instant-and-offline/service-worker/lifecycle& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&The Service Worker Lifecycle&i class=&icon-external&&&/i&&/a&》 提到:&/p&
&blockquote&&p&Service worker 生命周期的目的:&/p&&ul&&li&实现离线优先。&/li&&li&允许新服务工作线程自行做好运行准备,无需中断当前的服务工作线程。&/li&&li&确保整个过程中作用域页面由同一个服务工作线程(或者没有服务工作线程)控制。&/li&&li&确保每次只运行网站的一个版本。&/li&&/ul&&/blockquote&&p&整个生命周期的运作方式,官方文档已经说得很清楚,这里不再多说,我们来看看状态管理的机制是怎样的。&/p&
&p&&b&2. 状态管理&/b&&/p&
&p&Service worker 在浏览器内核有两类状态,一类是 service worker 线程的运行状态,另一类是 service worker 脚本版本的状态。&/p&
&p&&b&1) Service worker 线程的运行状态, 一般对应 service worker 线程的状态,这类状态只保存在内存中。&/b&&/p&
&ul&&li&STOPPED:已停止,EmbeddedWorkerInstance::OnStopped 时设置。&/li&
&li&STARTING:正在启动,EmbeddedWorkerInstance::Start 时设置。&/li&
&li&RUNNING:正在运行,EmbeddedWorkerInstance::OnStarted 时设置。&/li&
&li&STOPPING:正在停止,EmbeddedWorkerInstance::Stop --& EmbeddedWorkerRegistry::StopWorker 返回 status 为 SERVICE_WORKER_OK 时设置。&/li&
&/ul&&p&&b&2) Service worker 脚本版本(即注册函数中指定的 service worker js 文件)的状态,这类状态中的 INSTALLED 和 ACTIVATED 可以被持久化存储。&/b&&/p&
&ul&&li&NEW:浏览器内核的 ServiceWorkerVersion 已创建,属于一个初始值。&/li&
&li&INSTALLING:Install 事件被派发和处理,一般在 service worker 线程启动后,即 ServiceWorkerVersion::StartWorker 返回 status 为 SERVICE_WORKER_OK 时设置。&/li&
&li&INSTALLED:Install 事件已处理完成,准备进入 ACTIVATING 状态。一般在注册信息已存储到数据库,即 ServiceWorkerStorage::StoreRegistration 返回 status 为 SERVICE_WORKER_OK 时设置。&/li&
&li&ACTIVATING:Activate 事件被派发和处理。一般在当前 scope 下没有 active ServiceWorker 或 INSTALLED 状态的 service worker 调用了 skipWaiting,service worker 就会从 INSTALLED 状态转为 ACTIVATING 状态。&/li&
&li&ACTIVATED:Activate 事件已处理完成,已正式开始控制页面,可处理各类功能事件。一般在 activate 事件处理完成后就会转为 ACTIVATED 状态,此时 service worker 就可以控制页面行为,可以处理功能事件,比如 fetch、push。&/li&
&li&REDUNDANT:ServiceWorkerVersion 已失效,一般是因为执行了 unregister 操作或已被新 service worker 更新替换。&/li&
&/ul&&p&需要注意的是:&/p&
&ul&&li&Service worker 规范中提到的 &service workers may be started and killed many times a second&,指的是 service worker 线程随时可以被 Started 和 Killed。在关联文档未关闭时,Service worker 线程可以处于 Stopped 状态。在全部关联文档都已关闭时,service worker 线程也可以处于 Running 状态。&/li&
&li&Service worker 脚本版本的状态,也是独立于文档生命周期的,与 service worker 线程的运行状态无关,service worker 线程关闭时,service worker 脚本版本也可处于 ACTIVATED 状态。&/li&
&li&Service worker 脚本版本的状态,INSTALLED 和 ACTIVATED 是稳定的状态,service worker 线程启动之后一般是进入这两种状态之一。INSTALLING 和 ACTIVATING 是中间状态,一般只会在 service worker 新注册或更新时触发一次,刷新页面一般不会触发。INSTALLING 成功就转入 INSTALLED,失败就转入 REDUNDANT。ACTIVATING 成功就转入 ACTIVATED,失败就转入 REDUNDANT。&/li&
&li&如果 service worker 脚本版本处于 ACTIVATED 状态,功能事件处理完之后,service worker 线程会被 Stop,当再次有功能事件时,service worker 线程又会被 Start,Start 完成后 service worker 就可以立即进入 ACTIVATED 状态。&/li&
&/ul&&p&浏览器内核会管理三种 service worker 脚本版本:&/p&
&ul&&li&installing_version:处于 INSTALLING 状态的版本&/li&
&li&waiting_version:处于 INSTALLED 状态的版本&/li&
&li&active_version:处于 ACTIVATED 状态的版本&/li&
&/ul&&p&installing_version 一般是在 service worker 线程启动后,即 ServiceWorkerVersion::StartWorker 返回 status 为 SERVICE_WORKER_OK 时,处于此版本状态,这是一个中间版本,在正确安装完成后会转入 waiting_version。&/p&
&p&waiting_version 一般在注册信息已存储到数据库,即 ServiceWorkerStorage::StoreRegistration 返回 status 为 SERVICE_WORKER_OK 时,处于此版本状态。或者在再次打开 service worker 页面时,检查到 service worker 脚本版本的状态为 INSTALLED,也会进入此版本状态。waiting_version 的存在确保了当前 scope 下只有一个 active service worker。&/p&
&p&active_version 一般在 activate 事件处理完成后,就会处于此版本状态,同一 scope 下只有一个 active Service Worker。需要特别注意的是,当前页面已有 active worker 控制,刷新页面时,新版本 Waiting(Installed) 状态的 service worker 并不能转入 active 状态。&/p&
&p&Service worker 可以从 waiting_version 转入 active_version 的条件:&/p&
&ul&&li&当前 scope 下没有 active service worker 在运行。&/li&
&li&页面 JS 调用 self.skipWaiting 跳过 waiting 状态。&/li&
&li&用户关闭页面,释放了当前处于 active 状态的 service worker。&/li&
&li&浏览器周期性检测,发现 active service worker 处于 idle 状态,就会释放当前处于 active 状态的 service worker。&/li&
&/ul&&p&&b&3. 更新机制&/b&&/p&
&p&Service worker 注册函数中指定的 scriptURL(比如 serviceworker.js),一般有两种更新方式:&/p&
&p&1) 强制更新&/p&
&p&距离上一次更新检查已超过 24 小时,会忽略浏览器缓存,强制到服务器更新一次。&/p&
&p&2) 检查更新(&a href=&/?target=https%3A//w3c.github.io/ServiceWorker/%23soft-update-algorithm& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&Soft Update&i class=&icon-external&&&/i&&/a&)&/p&
&p&一般在下面情况会检查更新,&/p&
&ul&&li&第一次访问 scope 里的页面。&/li&
&li&距离上一次更新检查已超过 24 小时。&/li&
&li&有功能性事件发生,比如 push、sync。&/li&
&li&在 service worker URL 发生变化时调用了 register 方法。&/li&
&li&Service worker JS 资源文件的缓存时间已超出其头部的 max-age 设置的时间(注:max-age 大于 24 小时,会使用 24 小时作为其值)。&/li&
&li&Service worker JS 资源文件的代码只要有一个字节发生了变化,就会触发更新,包括其引入的脚本发生了变化。&/li&
&/ul&&p&我们看看浏览器内核是怎样实现周期性的检查更新的,service worker schedule update 代码如下:&/p&
&p&ServiceWorkerControlleeRequestHandler::~ServiceWorkerControlleeRequestHandler &br&// Navigation triggers an update to occur shortly after the page and its initial subresources load。&br&--& ServiceWorkerVersion::ScheduleUpdate
// if (is_main_resource_load_)&br&--& ServiceWorkerVersion::StartUpdate&/p&
&p&从上述代码流程可以看到,service worker 页面主文档加载完成时,就会触发 active_version 的一次检查更新,如果距离上一次脚本更新的时间超过了 24 小时,就会设置 LOAD_BYPASS_CACHE 的标记,忽略浏览器缓存,直接从网络加载。&/p&
&p&上一次脚本更新的时间,一般在 service worker 安装完成时会更新为当前时间,或者检查到脚本超过 24 小时都没有发生变化也会更新为当前时间,这样就能保证 service worker 在安装完成之后,每隔 24 小时,至少会更新一次。&/p&
&p&&b&4. 线程退出&/b&&/p&
&p&Service worker 线程一般在什么情况下会被停止呢?&/p&
&br&&ul&&li&Service worker JS 资源文件有任何异常,都会导致
service worker 线程退出。包括但不限于如 JS 文件存在语法错误、service worker 安装失败或激活失败、service worker JS 执行时出现未捕获的异常。&/li&&li&Service worker 功能事件处理完成,处于空闲状态,Service Worker 线程会自动退出。&/li&&li&Service worker JS 执行时间过长,service worker 线程会自动退出。比如 service worker JS 执行时间超过 30 秒,或 Fetch 请求在 5 分钟内还未完成。&/li&&li&浏览器会周期性检查各个 service worker 线程是否可以退出,一般在启动 service worker 线程时会检查一次。&/li&&li&为了方便开发者调试,Chromium 进行了特殊处理,在连上 devtools 之后,service worker 线程不会退出。&/li&&/ul&&p&&b&5. 消息通信机制&/b&&/p&
&p&我们知道,在 worker 中无法直接操作 DOM,service worker 也不例外,那么它如何与其控制的页面(至少一个)进行通信呢?接下来我们来看 service worker 与其控制的页面之间的通信机制到底是怎样的。&/p&
&p&&b&单向通信&/b&&/p&
&p&1) 页面使用 ServiceWorker.postMessage 发送消息给 service worker。&/p&
&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&function oneWayCommunication() {
if (navigator.serviceWorker.controller) {
navigator.serviceWorker.controller.postMessage({
command: 'oneWayCommunication',
message: 'Hi, SW'
&/code&&/pre&&/div&&p&2) Service worker 监听 onmessage 事件,即可获取到页面发过来的消息。&br&&/p&
&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&self.addEventListener('message', function(event) {
const data = event.
if (mand === 'oneWayCommunication') {
console.log(`Message from the Page : ${data.message}`);
&/code&&/pre&&/div&&p&单向通信模式下,页面可以向 service worker 发送消息,但是 service worker 不能回复消息响应给页面。&br&&/p&
&p&&b&双向通信&/b&&/p&
&p&1) 页面建立 MessageChannel,使用 MessageChannel.port1 监听来自 service worker 的消息。使用 ServiceWorker.postMessage 发送消息给 service worker,并且将MessageChannel.port2 也一起传递给 service worker。&/p&
&div class=&highlight&&&pre&&code class=&language-js&&&span&&/span&&span class=&kd&&function&/span& &span class=&nx&&twoWayCommunication&/span&&span class=&p&&()&/span& &span class=&p&&{&/span&
&span class=&k&&if&/span& &span class=&p&&(&/span&&span class=&nx&&navigator&/span&&span class=&p&&.&/span&&span class=&nx&&serviceWorker&/span&&span class=&p&&.&/span&&span class=&nx&&controller&/span&&span class=&p&&)&/span& &span class=&p&&{&/span&
&span class=&kr&&const&/span& &span class=&nx&&messageChannel&/span& &span class=&o&&=&/span& &span class=&k&&new&/span& &span class=&nx&&MessageChannel&/span&&span class=&p&&();&/span&
&span class=&nx&&messageChannel&/span&&span class=&p&&.&/span&&span class=&nx&&port1&/span&&span class=&p&&.&/span&&span class=&nx&&onmessage&/span& &span class=&o&&=&/span& &span class=&kd&&function&/span&&span class=&p&&(&/span&&span class=&nx&&event&/span&&span class=&p&&)&/span& &span class=&p&&{&/span&
&span class=&nx&&console&/span&&span class=&p&&.&/span&&span class=&nx&&log&/span&&span class=&p&&(&/span&&span class=&sb&&`Response from the SW : &/span&&span class=&si&&${&/span&&span class=&nx&&event&/span&&span class=&p&&.&/span&&span class=&nx&&data&/span&&span class=&p&&.&/span&&span class=&nx&&message&/span&&span class=&si&&}&/span&&span class=&sb&&`&/span&&span class=&p&&);&/span&
&span class=&p&&}&/span&
&span class=&nx&&navigator&/span&&span class=&p&&.&/span&&span class=&nx&&serviceWorker&/span&&span class=&p&&.&/span&&span class=&nx&&controller&/span&&span class=&p&&.&/span&&span class=&nx&&postMessage&/span&&span class=&p&&({&/span&
&span class=&nx&&command&/span&&span class=&o&&:&/span& &span class=&s1&&'twoWayCommunication'&/span&&span class=&p&&,&/span&
&span class=&nx&&message&/span&&span class=&o&&:&/span& &span class=&s1&&'Hi, SW'&/span&
&span class=&p&&},&/span& &span class=&p&&[&/span&&span class=&nx&&messageChannel&/span&&span class=&p&&.&/span&&span class=&nx&&port2&/span&&span class=&p&&]);&/span&
&span class=&p&&}&/span&
&span class=&p&&}&/span&
&/code&&/pre&&/div&&p&2. Service worker 监听 onmessage 事件,即可获取到页面发过来的消息。同时,它可使用页面传递过来的 MessageChannel.port2(即 event.ports[0])的 postMessage 方法回复消息给页面。&br&&/p&
&div class=&highlight&&&pre&&code class=&language-js&&&span&&/span&&span class=&nx&&self&/span&&span class=&p&&.&/span&&span class=&nx&&addEventListener&/span&&span class=&p&&(&/span&&span class=&s1&&'message'&/span&&span class=&p&&,&/span& &span class=&kd&&function&/span&&span class=&p&&(&/span&&span class=&nx&&event&/span&&span class=&p&&)&/span& &span class=&p&&{&/span&
&span class=&kr&&const&/span& &span class=&nx&&data&/span& &span class=&o&&=&/span& &span class=&nx&&event&/span&&span class=&p&&.&/span&&span class=&nx&&data&/span&&span class=&p&&;&/span&
&span class=&k&&if&/span& &span class=&p&&(&/span&&span class=&nx&&data&/span&&span class=&p&&.&/span&&span class=&nx&&command&/span& &span class=&o&&===&/span& &span class=&s1&&'twoWayCommunication'&/span&&span class=&p&&)&/span& &span class=&p&&{&/span&
&span class=&nx&&event&/span&&span class=&p&&.&/span&&span class=&nx&&ports&/span&&span class=&p&&[&/span&&span class=&mi&&0&/span&&span class=&p&&].&/span&&span class=&nx&&postMessage&/span&&span class=&p&&({&/span&
&span class=&nx&&message&/span&&span class=&o&&:&/span& &span class=&s1&&'Hi, Page'&/span&
&span class=&p&&});&/span&
&span class=&p&&}&/span&
&span class=&p&&});&/span&
&/code&&/pre&&/div&
&p&&b&广播通信&/b&&/p&
&p&1) 页面使用 ServiceWorker.postMessage 发送消息给 service worker,要求它向所有 Client 广播消息。同时,注册 onmessage 事件以监听来自 service worker 的广播消息。&/p&
&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&function registerBroadcastReceiver() {
navigator.serviceWorker.onmessage = function(event) {
const data = event.
if (mand === 'broadcastOnRequest') {
console.log(`Broadcasted message from the ServiceWorker : ${data.message}`);
function requestBroadcast() {
registerBroadcastReceiver();
if (navigator.serviceWorker.controller) {
navigator.serviceWorker.controller.postMessage({
command: 'broadcast'
&/code&&/pre&&/div&&p&2) Service worker 监听 onmessage 事件,获取到页面发过来的广播请求。Service worker 遍历所有的 Client,并使用 Client.postMessage 发送消息给每一个 Client,从而实现消息广播。&br&&/p&
&div class=&highlight&&&pre&&code class=&language-js&&&span&&/span&&span class=&nx&&self&/span&&span class=&p&&.&/span&&span class=&nx&&addEventListener&/span&&span class=&p&&(&/span&&span class=&s1&&'message'&/span&&span class=&p&&,&/span& &span class=&kd&&function&/span&&span class=&p&&(&/span&&span class=&nx&&event&/span&&span class=&p&&)&/span& &span class=&p&&{&/span&
&span class=&kr&&const&/span& &span class=&nx&&data&/span& &span class=&o&&=&/span& &span class=&nx&&event&/span&&span class=&p&&.&/span&&span class=&nx&&data&/span&&span class=&p&&;&/span&
&span class=&k&&if&/span& &span class=&p&&(&/span&&span class=&nx&&data&/span&&span class=&p&&.&/span&&span class=&nx&&command&/span& &span class=&o&&===&/span& &span class=&s1&&'broadcast'&/span&&span class=&p&&)&/span& &span class=&p&&{&/span&
&span class=&nx&&self&/span&&span class=&p&&.&/span&&span class=&nx&&clients&/span&&span class=&p&&.&/span&&span class=&nx&&matchAll&/span&&span class=&p&&().&/span&&span class=&nx&&then&/span&&span class=&p&&(&/span&&span class=&kd&&function&/span&&span class=&p&&(&/span&&span class=&nx&&clients&/span&&span class=&p&&)&/span& &span class=&p&&{&/span&
&span class=&nx&&clients&/span&&span class=&p&&.&/span&&span class=&nx&&forEach&/span&&span class=&p&&(&/span&&span class=&kd&&function&/span&&span class=&p&&(&/span&&span class=&nx&&client&/span&&span class=&p&&)&/span& &span class=&p&&{&/span&
&span class=&nx&&client&/span&&span class=&p&&.&/span&&span class=&nx&&postMessage&/span&&span class=&p&&({&/span&
&span class=&nx&&command&/span&&span class=&o&&:&/span& &span class=&s1&&'broadcastOnRequest'&/span&&span class=&p&&,&/span&
&span class=&nx&&message&/span&&span class=&o&&:&/span& &span class=&s1&&'This is a broadcast on request from the SW'&/span&
&span class=&p&&});&/span&
&span class=&p&&})&/span&
&span class=&p&&})&/span&
&span class=&p&&}&/span&
&span class=&p&&});&/span&
&/code&&/pre&&/div&&p&&b&存在的问题&/b&&br&&/p&
&p&这里我们重点探讨下 MessageChannel,理解它的原理和可能存在的问题。原理我们描述一下:&/p&
&ul&&li&页面实例化 MessageChannel 对象,浏览器内核在创建 MessageChannel 的过程中,同时会创建两个 MessagePort,一个用于监听来自 service worker 的消息,另外一个传递给 service worker,service worker 可使用它来回复消息。页面使用 ServiceWorker.postMessage 向 service worker 发送消息,而 service worker 使用 port2 回复消息。&/li&
&li&Service worker 的 StopWorker 会触发 MessagePort::close, MessageChannel 会关闭,MessagePort 在 close 之后就不能收发消息了,而且 service worker 再次重启之后也无法重建原来的 Messagechannel,最新的 Chromium 版本存在同样的问题。这就意味着,在 service worker stop之后,整个双向通信的通道就完全不能使用了。按照 service worker 规范的说明,浏览器可以在任意需要的时候关闭和重启 service worker,这也等同于 service worker 与其控制页面建立的 MessageChannel 随时会断掉,而且无法重建。&/li&
&/ul&&p&解决方案有两种思路:&/p&
&ul&&li&思路一:从上面分析可以看到,service worker 的 stop 方法会破坏 MessageChannel 的通信通道,那么如果 service worker 不会 Stop,即在页面不关闭时保持不退出呢?理论上 MessageChannel 也可以继续保持正常,这是一个解决思路,但这种思路与规范约定的 service worker 的生命周期存在冲突。&/li&
&li&思路二:service worker 的 stop 会破坏 MessageChannel,那么如果我们每次发送消息都新建 MessageChannel 呢?理论上也是可行的,且 Google 官方的 DEMO (《Service Worker postMessage() Sample》 &a href=&/?target=https%3A//googlechrome.github.io/samples/service-worker/post-message/& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&Service Worker postMessage() Sample&i class=&icon-external&&&/i&&/a&
就是使用了这种方式。它实现一个 sendMessage 方法,通过该方法与 service worker 进行通信,其中每次调用该方法都会创建新的 MessageChannel。缺点是每次消息通信都需要新建 MessageChannel 实例,这样它与单向通信相比,优势就不明显了。&/li&
&/ul&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&function sendMessage(message) {
// This wraps the message posting/response in a promise, which will resolve if the response doesn't
// contain an error, and reject with the error if it does. If you'd prefer, it's possible to call
// controller.postMessage() and set up the onmessage handler independently of a promise, but this is
// a convenient wrapper.
return new Promise(function(resolve, reject) {
const messageChannel = new MessageChannel();
messageChannel.port1.onmessage = function(event) {
if (event.data.error) {
reject(event.data.error);
resolve(event.data);
// This sends the message data as well as transferring messageChannel.port2 to the service worker.
// The service worker can then use the transferred port to reply via postMessage(), which
// will in turn trigger the onmessage handler on messageChannel.port1.
// See https://html.spec.whatwg.org/multipage/workers.html#dom-worker-postmessage
navigator.serviceWorker.controller.postMessage(message, [messageChannel.port2]);
&/code&&/pre&&/div&
&h2&&b&异常处理机制&/b&&/h2&
&p&&b&1. 线程退出时机&/b&&/p&
&p&Service worker 规范中提到:“Service workers may be started by user agents without an attached document and may be killed by the user agent at nearly any time”,即 Service Worker 线程可能在任意时间被浏览器停止,即使关联的文档还未关闭 service worker 线程也有可能已被停止。这种设计主要是为了降低 Service Worker 对资源(比如浏览器内存、手机电量等)的消耗。所以,Service Worker 线程一般在什么情况下会被停止?(WTF )&/p&&ul&&li&Service worker JS 有任何异常,都会导致 service worker 线程退出。包括但不限于 JS 文件存在语法错误、service worker 安装失败或激活失败、service worker JS 执行时出现未被捕获的异常。&/li&&li&Service worker 功能事件处理完成,处于空闲状态,service worker 线程会自动退出。&/li&&li&Service worker JS 执行时间过长,service worker 线程会自动退出。比如 service worker JS 执行时间超过 30 秒,或 Fetch 请求在 5 分钟内还未完成。&/li&&li&浏览器会周期性检查各个 service worker 线程是否可以退出, 一般在启动 service worker 线程时会检查一次。&/li&&li&为了方便开发者调试, Chromium 进行了特殊处理, 在连上 devtools 之后,service worker 线程不会退出。&a href=&/?target=https%3A//bugs.chromium.org/p/chromium/issues/detail%3Fid%3D429582& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&Keep a serviceworker alive when devtools is attached - chromium - Monorail&i class=&icon-external&&&/i&&/a&&/li&&/ul&&p&所以,service worker 线程退出时会带来什么坑呢?&/p&
&ul&&li&Service worker JS 里面不能使用全局变量,如果需要全局状态,必须自己进行持久化,比如使用 IndexedDB API。&/li&
&li&Service worker 注册过程中出现异常,无法连上 devtools,无法从 devtools 获取异常信息。&/li&
&/ul&&p&&b&2. 异常案例&/b&&/p&
&p&Service worker 线程在启动或执行代码的过程中,一般会有下面几类异常:&/p&
&p&1) Service worker JS 文件存在语法错误,如 Uncaught SyntaxError: Unexpected token function,这种情况,一般在启动 WorkerThread 的时候,initialize 初始化时,会调用 ScriptController::evaluate 去执行 service worker 的 JS 代码,检查到语法错误时,会引起 service worker 注册失败。&/p&
&p&2) Service worker 安装或激活的事件回调函数执行代码存在异常,引起 service worker 线程退出。ScriptPromise 本身会捕获异常,它仅仅返回 Rejected/Fulfilled,并不会再将 JS 异常往上抛,很多时候前端开发同学仅仅能看到 Promise Rejected 了,但并不清楚是什么原因。同样,WaitUntilObserver 也一样,它也只返回 Rejected/Fulfilled,没有进一步将 JS 异常往上抛,很多时候前端开发同学仅仅能看到 WaitUntil Rejected 了,也并不清楚是什么原因。&/p&
&p&3) 功能事件处理出错, 如 Fetch ResponseWith 出错。举个例子,下面 service worker js 文件的 fetch 事件处理函数中,如果 strategies.networkFallbackToCache 执行出错了,会出现什么问题?&/p&&div class=&highlight&&&pre&&code class=&language-js&&&span&&/span&&span class=&nx&&self&/span&&span class=&p&&.&/span&&span class=&nx&&addEventListener&/span&&span class=&p&&(&/span&&span class=&s1&&'fetch'&/span&&span class=&p&&,&/span& &span class=&kd&&function&/span&&span class=&p&&(&/span&&span class=&nx&&e&/span&&span class=&p&&)&/span& &span class=&p&&{&/span&
&span class=&k&&return&/span& &span class=&nx&&e&/span&&span class=&p&&.&/span&&span class=&nx&&respondWith&/span&&span class=&p&&(&/span&&span class=&nx&&strategies&/span&&span class=&p&&.&/span&&span class=&nx&&networkFallbackToCache&/span&&span class=&p&&(&/span&&span class=&nx&&e&/span&&span class=&p&&.&/span&&span class=&nx&&request&/span&&span class=&p&&));&/span&
&span class=&p&&});&/span&
&/code&&/pre&&/div&
&p&这种情况,respondWith 会 Rejected,但并不会抛异常, 表现为资源请求失败了,很可能造成页面白屏或者排版显示异常。这类问题非常难跟进, 只能是一步一步的修改页面,然后使用devtools 等工具进行调试。&/p&
这是一个特殊的 worker
浏览器一般有三类 web Worker:
Dedicated Worker:专用的 worker,只能被创建它的 JS 访问,创建它的页面关闭,它的生命周期就结束了。
Shared Worker:共享的 worker,可以被同一域名下的 JS 访问,关联的页面都关闭时,它的生命周…
&img src=&/v2-641da9a0a124dcf2f1b29ce_b.png& data-rawwidth=&781& data-rawheight=&533& class=&origin_image zh-lightbox-thumb& width=&781& data-original=&/v2-641da9a0a124dcf2f1b29ce_r.png&&&blockquote&&h2&前言&/h2&&p&近年来,随着HTML5技术普及、移动设备迅猛增长和流量资费不断下降,视频已成为人们了解世界、获取知识的主要途径之一。现在音视频播放器已随处可见,但这个过程到底如何实现?播放器的原理又是什么?或作为生产者,应该依据哪些标准来选择合适自己的播放器?今天这篇文章将给你答案。&/p&&/blockquote&&br&&h2&一. 播放器功能架构&/h2&&p&首先来看常规播放器的功能架构:&/p&&img src=&/v2-641da9a0a124dcf2f1b29ce_b.png& data-rawwidth=&781& data-rawheight=&533& class=&origin_image zh-lightbox-thumb& width=&781& data-original=&/v2-641da9a0a124dcf2f1b29ce_r.png&&&blockquote&&b&应用层:&/b&UI/统计/DRM(数字版权保护)/多码率/弹幕/广告等。&br&&b&底层:&/b&数据接收模块/解复用模块/音视频解码、滤镜、渲染模块/字幕/应用层结合功能:DRM、多码率/统计等。&/blockquote&&p&播放器的功能比较多,本文就不再一一赘述,将重点讲解多媒体引擎模块。&/p&&br&&br&&h2&二. 多媒体引擎&/h2&&p&作为播放器的核心部分,多媒体引擎主要负责音视频数据的加载、处理和展现。以FFmpeg为例,它的基本运作流程如下图所示:&br&&/p&&img src=&/v2-ba7e1d0458bad94dd509fe_b.png& data-rawwidth=&766& data-rawheight=&670& class=&origin_image zh-lightbox-thumb& width=&766& data-original=&/v2-ba7e1d0458bad94dd509fe_r.png&&&br&&p&我们详细了解一下每个环节:&/p&&p&&b&1、数据接收(Source)&/b&&/p&&p&作为数据入口,这里的文件可以通过本地文件(File://协议开头)输入,也可以通过网络协议(如HTTP、RTMP、RTSP等)输入。在找到对应的传输协议之后,FFmpeg会根据协议特性与本机或服务器建立连接并获取到流数据。&/p&&p&&b&2、解复用(Demux)&/b&&/p&&p&通过对文件的特征码分析,可以找到文件的封装格式,如常见的MP4、FLV、TS、AVI等。根据封装格式的标准对其拆封,可以得到编码的音视频数据,一般称之为“packet”。&/p&&p&&b&3、解码(Decode)&/b&&/p&&p&解码器初始化时,利用之前源数据分析获得的音视频信息,分别设置对应的音频解码器和视频解码器。目前互联网中比较常见的音频编码方式有AAC(Advanced Audio Coding)、MP3,视频编码方式有H.264、H.265。对packet分别进行解码后,音频解码获得的数据是PCM(Pulse Code Modulation,脉冲编码调制)采样数据,一般称为“sample”。视频解码获得的数据是一幅YUV或RGB图像数据,一般称为“picture”。&/p&&p&&b&4、音视频同步(Synchronizing)&/b&&/p&&p&音视频解码时是两个独立线程,因此获得的音视频数据是分开的,并无任何关联。理想状态下,音视频按照自己固有频率渲染输出就能达到音视频同步的效果。但是在现实中,断网、弱网、丢帧、缓冲、音视频不同的解码耗时等情况都会妨碍实现同步,很难达到预期效果,所以要保证视频内容和音频内容对得上,必须做音视频同步。&/p&&p&&b&5、音视频渲染(Render)&/b&&/p&&p&经过音视频同步调整之后,需要把sample和picture分别输送给声卡和显卡,这部分工作建议由成熟的库来完成。常见的音频库有SDL、OpenAL/OpenAL ES、DirectSound、ALSA(Advanced Linux Sound Architecture)、OSS(Open Source System)等;视频库有SDL、OpenGL/OpenGL ES、DirectDraw、FrameBuffer等。&/p&&br&&p&经过以上5个步骤,基本的播放流程就完成了。&/p&&br&&br&&h2&三. 各平台视频技术概况&/h2&&p&要实现全平台覆盖的播放器,自然少不了技术支撑。这里主要讨论Web、Android、iOS上的音视频技术,讲述一部分音视频常用方案。&/p&&br&&p&&b&1、Web-HTML5&/b&&/p&&br&&p&技术语言:JavaScript,HTML/CSS&/p&&br&&p&使用环境:所有可以支持HTML5 Video标签的终端浏览器(PC Windows/Mac/Linux/Unix、Android、iOS)即可播放视频,目前覆盖率已达95%。&/p&&img src=&/v2-804fef177c8_b.png& data-rawwidth=&1920& data-rawheight=&887& class=&origin_image zh-lightbox-thumb& width=&1920& data-original=&/v2-804fef177c8_r.png&&&p&(图:Video element覆盖率)&/p&&blockquote&&b&相关方案:&/b&&/blockquote&&p&&b&1.1 &video&标签&/b&&/p&&br&&img src=&/v2-9e18cdf6bb8a910a94b2_b.png& data-rawwidth=&707& data-rawheight=&115& class=&origin_image zh-lightbox-thumb& width=&707& data-original=&/v2-9e18cdf6bb8a910a94b2_r.png&&&img src=&/v2-b59ddb0db9fe83cf9f8ed_b.png& data-rawwidth=&546& data-rawheight=&519& class=&origin_image zh-lightbox-thumb& width=&546& data-original=&/v2-b59ddb0db9fe83cf9f8ed_r.png&&&p&从代码示例来看,HTML5的Video标签非常简单易用,controls= & controls &可以使用浏览器默认的播放器UI(不同浏览器UI不一样),如果不对controls赋值,则可以通过CSS和Video消息来实现自定义UI及控制。&/p&&br&&p&&b&1.2 MSE(Media Source Extensions)&/b&&/p&&br&&p&Media Source Extensions (MSE) 是一个主流浏览器支持的新Web API,而且是W3C标准,允许JavaScript动态构建 &video& 和 &audio& 媒体流。它定义了对象, 允许JavaScript传输媒体流片段到一个HTMLMediaElement。&/p&&br&&p&通过使用MSE,可以动态地修改媒体流而不需要任何插件。这使前端JavaScript可以做更多事情, 比如可以在JavaScript进行转封装、处理,甚至转码,因此像ABR(Adaptive Bitrate)、hls.js、dash.js、flv.js也是基于这点来实现的。&/p&&br&&p&虽然MSE不能让流直接传输到media tags上, 但是MSE提供了构建跨浏览器播放器的核心技术, 让浏览器通过JavaScript API来推音视频到media tags上,目前MSE的覆盖率已达77.93%。&/p&&img src=&/v2-c78b00fb2e17cfb565bc0e2e61a80353_b.png& data-rawwidth=&2306& data-rawheight=&902& class=&origin_image zh-lightbox-thumb& width=&2306& data-original=&/v2-c78b00fb2e17cfb565bc0e2e61a80353_r.png&&&p&(图:MSE覆盖率)&/p&&br&&br&&p&&b&2、Web-Flash&/b&&/p&&br&&p&技术语言:ActionStript&/p&&br&&p&使用环境:安装了FlashPlayer的浏览器(PC Windows/Mac/Linux/Unix、Android、iOS)即可播放视频。曾经是互联网时代早期富媒体老大的Web-Flash,因HTML5普及已慢慢淡出人们的视野,现在更多是作为HTML5播放器的互补方案来使用。&/p&&br&&blockquote&&b&相关方案:&/b&&/blockquote&&p&&b&2.1 FLVPlayBack&/b&&/p&&br&&p&Flash自带封装好的播放器,跟HTML5的浏览器默认播放器差不多,系统了一些播放器UI样式供选择,简单几句代码就可以快速构建精美的播放器,但是灵活度较差。&/p&&br&&p&&b&2.2 NetStream&/b&&/p&&br&&p&Flash处理多媒体的核心类,它提供了appendbytes这个API,允许外部传入FLV封装格式的数据进来播放,类似于HTML5的MSE。同理,可以用此接口实现视频的转封装、处理、转码等操作,再配合RTMFP协议,还可以实现P2P的视频传输播放。&/p&&br&&p&&b&2.3 CrossBridge & FFmpeg&/b&&/p&&br&&p&曾几何时,Flash还有个叫CrossBridge(又叫Alchemy、Flascc)的黑科技,允许在Flash规定范围内跑自己编写的C/C++代码,这样就彻底提升了运算效率,如果配合FFmpeg的多媒体处理能力,可能会有意想不到的效果。&/p&&br&&br&&p&&b&3、Andorid&/b&&/p&&br&&p&技术语言:Java、JNI(C/C++)&/p&&br&&p&使用环境:Andorid系统(Andorid phone、Android pad、Andorid TV、Android Box等)。&/p&&br&&blockquote&&b&相关方案:&/b&&/blockquote&&p&&b&3.1 MediaPlayer&/b&&/p&&br&&p&Android提供的默认多媒体播放器,可以用来播放本地或网络上的音频、视频数据,主要负责解协议、解码,但不包含显示部分。显示部分需要配合SurfaceView或者SurfaceTexture来处理。&/p&&br&&p&&b&3.2 MeidaCodec (API 16+ ,Jelly Bean 4.1.x)&/b&&/p&&br&&p&Android提供的硬编解码类,第一次使用是在Android 4.1版本(API16),主要对音视频数据进行编码或解码操作。在Android 4.3(API18)中,MediaCodec被扩展为包含一种通过 Surface 提供输入的方法(即createInputSurface),这允许输入来自于相机的预览或是经过OpenGL ES呈现。&/p&&br&&p&&b&3.3 JNI & FFmpeg&/b&&/p&&br&&p&JNI是Java Native Interface的缩写,一开始是为了本地已编译语言,尤其是C和C++而设计的,但并不妨碍使用其他编程语言,只要调用约定受支持就可以了。使用java与本地已编译的代码交互,通常会丧失平台可移植性,但有些情况下这样做是可以接受的,甚至是必须的。例如,使用一些旧的库与硬件、操作系统进行交互,或者为了提高程序的性能。JNI配合FFmpeg这个强大引擎,使Android平台具备了较好的多媒体处理能力。&/p&&br&&br&&p&&b&4、iOS&/b&&/p&&br&&p&技术语言:Objective C、C/C++、Swift&/p&&br&&p&使用环境:iOS系统(iphone、ipad)&/p&&br&&blockquote&&b&相关方案:&/b&&/blockquote&&p&&b&4.1 AVFoundation.framework:AVPlayer、AVPlayerLayer&/b&&/p&&br&&p&AVPlayer类只包含音视频的数据接收、解码、处理工作,不包含显示部分,需要配合AVPlayerLayer来使用,可定制化强度大、灵活,消息也比较丰富,官方推荐使用,取代了MPMoviePlayerController的位置。&/p&&br&&p&&b&4.2 AVFoundation.framework:AVPlayerViewController&/b&&/p&&br&&p&提供了默认的可视化控制界面,整合了一个完整的播放器,可以作为控制器进行操作显示。官方推荐使用,取代了MPMoviePlayerViewController的位置。&/p&&br&&p&&b&4.3 MediaPlayer.framework:MPMoviePlayerController&/b&&/p&&br&&p&iOS的基础播放器,使用几个简单的API就能完成视频文件播放,其内部已包含数据接收、解码功能,如果要在UI中展示视频,需要将view属性添加到界面中,播放器UI方面需要自行开发。iOS 9.0以后,苹果官方已弃用。&/p&&br&&p&&b&4.4 MediaPlayer.framework:MPMoviePlayerViewController&/b&&/p&&br&&p&iOS的基础播放器视图控制器,MPMoviePlayerViewController在MPMoviePlayerController基础上封装了一层View,默认全屏模式展示,弹出后自动播放,作为模态窗口展示时如果点击“Done”按钮会自动退出模态窗口等,有一套自己的UI。iOS 9.0以后苹果官方已弃用。&/p&&br&&p&&b&4.5 VideoToolbox.framework&/b&&/p&&br&&p&VideoToolbox是一个底层框架,iOS 8.0以后,官方正式放开了硬件编解码API,它为视频压缩和解压缩提供服务,并存储在缓冲区corevideo像素栅格图像格式之中。这些服务以会话对象的形式提供(压缩、解压,和像素传输),应用程序不需要直接访问硬件编码器和解码器相关内容,而只需要直接使用videotoolbox即可实现编解码。iOS设备在硬件编解码这块的质量有一定保证,可以优先使用硬编解码,FFmpeg软解码为互补方案。&/p&&br&&p&&b&4.6 FFmpeg&/b&&/p&&br&&p&因Objective-C支持 C/C++混合编译,所以很容易就可以把FFmpeg使用起来,让iOS具备强大的多媒体处理能力。&/p&&br&&br&&p&&b&5、小结&/b&&/p&&br&&p&从HTML5、Flash、iOS、Android四个平台上看,它们似乎都具备音视频播放能力,但它们之间到底有些什么差别?我们应如何选择合适自己的方案?&/p&&br&&p&先对比一下播放方面的技术差异:&/p&&br&&img src=&/v2-95cc88b25bca811c6c19_b.jpg& data-rawwidth=&640& data-rawheight=&796& class=&origin_image zh-lightbox-thumb& width=&640& data-original=&/v2-95cc88b25bca811c6c19_r.jpg&&&p&我们尝试把产品场景分为四种,以下是对各自的建议:&/p&&br&&img src=&/v2-69ef3bfc1d10bb9bc1617_b.jpg& data-rawwidth=&640& data-rawheight=&331& class=&origin_image zh-lightbox-thumb& width=&640& data-original=&/v2-69ef3bfc1d10bb9bc1617_r.jpg&&&br&&h2&四. 几个常见问题&/h2&&p&做一个播放器远没有想象的那么简单,来看一下播放器的几个常见问题。&/p&&br&&p&&b&1、 怎么做音视频同步?&/b&&/p&&br&&p&对于系统封装好的播放器,则无须关心这个问题,但如果要实现一个完全可控的播放器,从数据接收-&解复用-&解码-&音视频同步-&渲染都需要去干预。处理音视频同步的原因已在前面章节讲过了,这里着重讲实现原理。&/p&&br&&p&做音视频同步之前,先补充个知识点,DTS与PTS(I帧、P帧、B帧请自行补充)。&/p&&br&&p&DTS(Decode Time Stamp):解码时间戳,这个时间戳的意义在于告诉播放器该在什么时候解码这一帧的数据。&/p&&br&&p&PTS(Presentation Time Stamp):显示时间戳,这个时间戳用来告诉播放器该在什么时候显示这一帧的数据。&/p&&br&&p&视频编码中必存在I帧,可能存在B帧、P帧,B帧是双向参考帧,因此会打乱帧的解码和显示顺序,因此引入了DTS和PTS来应对这种情况。普遍情况下B帧压缩率较I帧、P帧高,在点播视频中比较常见,直播视频中一般没有B帧,在此情况下,DTS和PTS应该相同。音频编码因为没有参考帧的概念,是不需要DTS的。&/p&&img src=&/v2-c2e8cbfa15bb4cc9fc35a83dc9a93363_b.png& data-rawwidth=&984& data-rawheight=&652& class=&origin_image zh-lightbox-thumb& width=&984& data-original=&/v2-c2e8cbfa15bb4cc9fc35a83dc9a93363_r.png&&&p&(图:I、P、B、DTS、PTS关系)&/p&&br&&p&在清楚了DTS和PTS之后,可见PTS对音视频同步起关键作用,音视频常见的同步方式有三种,分别是:以音频为主线同步、以视频为主线同步和以外部时钟同步。第一种最为常见,也是比较符合人类视觉听觉感官的方式(音频变速会导致变声),同时音频的播放速率比较固定,比较容易作为主线管理,其同步过程总结如下:&/p&&br&&p&a)解码器从输入的数据封装中解出音频PTS、视频PTS;&/p&&br&&p&b)创建音频播放线程,音频按固有的采样频率播放;&/p&&br&&p&c)创建视频刷新线程,按一定频率(10ms)定时检测;&/p&&br&&p&d)通过音频、视频的时间基(time_base)换算成相同时间基准的PTS;&/p&&br&&p&e)在视频刷新线程中计算当前真实的音频PTS、视频PTS:&/p&&br&&p&
currentRealVideoPTS = 当前准备显示的视频帧PTS + 当前视频帧流逝系统时间;&/p&&br&&p&
currentRealAudioPTS = 最新已播放的音频帧PTS + 当前音频帧流逝系统时间;&/p&&br&&p&f)计算currentRealVideoPTS - currentRealAudioPTS的差值diff;&/p&&br&&p&g)若diff &= 0,说明视频显示慢了,这时需要马上显示该帧数据;&/p&&br&&p&
若diff & 0,则视频酌情做适当的延时显示;&/p&&br&&p&h)一帧数据的音视频同步检测就此结束。&/p&&br&&p&以上方法的核心理念是通过延时评估算法来控制视频帧慢放或快放,以此做音视频在合理范围内保持同步。&/p&&br&&br&&p&&b&2、 怎么做到视频秒开?&/b&&/p&&br&&p&有以下几点可以优化:&/p&&br&&p&a)服务器端下发关键帧数据,保证播放器第一时间解码到有用的数据;&/p&&br&&p&b)播放器可以在选择音视频解码器环节做优化,通过外部预设解码器相关参数来减少解码器类型探测耗时;&/p&&br&&p&c)在业务层面上优化,使用HTTP-DNS服务做到精准调度,选择最优服务器获取播放数据;&/p&&br&&p&d)提升音视频服务节点质量;&/p&&br&&br&&p&&b&3、 怎么做到低延时?&/b&&/p&&br&&p&延时这词是针对直播而言,先看直播系统的架构图:&/p&&img src=&/v2-99ddb5d50942edb258a4a0_b.png& data-rawwidth=&809& data-rawheight=&400& class=&origin_image zh-lightbox-thumb& width=&809& data-original=&/v2-99ddb5d50942edb258a4a0_r.png&&&p&(图:UCloud直播系统架构图)&/p&&br&&br&&p&从图中可以看出,可能产生延时的环节有:&/p&&br&&p&a)主播端推流至上传节点的延时;&/p&&br&&p&可以通过接入HTTP-DNS为主播选择最优节点,减少传输延时。&/p&&br&&p&b)主播端发包的延时;&/p&&br&&p&主播端应尽快发送本地数据至上传节点,对于弱网环境,应该有合理的控制策略,尽量避免大量过期数据阻塞在本地。&/p&&br&&p&c)上传节点到服务器集群的数据分发、转封装、转码等耗时;&/p&&br&&p&分发的层级越多,延时则越大,这块也主要依赖于服务器间的网络状况。&/p&&br&&p&d)观众端到下载节点的延时;&/p&&br&&p&可以通过接入HTTP-DNS为观众选择最优节点,减少传输延时。&/p&&br&&p&e)播放器本地存在buffer;&/p&&br&&p&常规的播放器都会有本地缓存,合理的缓存设置可以为用户带来较好的观看体验,减少视频的卡顿次数。如果对延时有较高要求,可以减少buffer的缓冲时长,甚至可以把buffer去掉。&/p&&br&&br&&p&&b&4、 播放器卡顿的原因有哪些?&/b&&/p&&br&&p&再看图6,卡顿可能的原因有:&/p&&br&&p&a) 主播端推流与上传节点传输不顺畅;&/p&&br&&p&b) 服务器间传输出了问题;&/p&&br&&p&c) 观众与下载节点传输不顺畅。&/p&&br&&br&&h2&五. 总结&/h2&&p&看完这些,相信大家对基础的播放流程及各平台上音视频处理技术已有初步了解,但是要做好多个平台的播放器并非易事,要做大量兼容性适配、容灾、性能调优、播放质量统计反馈、新功能迭代等工作。因此,一款好的播放器需要不断打磨,UCloud也将持续投入更多资源去支持该领域的运作。&/p&&br&&p&_______________&/p&&blockquote&&p&&b&作者简介&/b&&/p&&br&&p&&b&刘宇峰 | &/b&现任UCloud流媒体终端研发部经理,热爱多媒体行业和专注于Web、Android、iOS方面的技术,拥有近十年的互联网和多媒体领域的研发经验,现主要负责UCloud视频云SDK、直播云SDK的架构设计、研发管理和运营工作。此前,曾任职于腾讯,主要负责Web前端多媒体方面的研发工作。&/p&&/blockquote&&br&&h2&相关阅读推荐:&/h2&&a href=&/p/& class=&internal&&关于直播,所有的技术细节都在这里了(三) &/a&&br&&a href=&/p/& class=&internal&&关于直播,所有的技术细节都在这里了(二)&/a&&br&&a href=&/p/& class=&i}

我要回帖

更多关于 js判断法定节假日 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信