众所周知,16年无疑是直播行业的春天,同时也是H5的一次高潮。
so,到现在用H5技术在移动端做网页直播也是见怪不怪了,但是!!!
今天我们的主角是webApp下播放视频
参考文献:
1)HTML5+CSS3+ JQuery 打造自定义视频播放器
简介
HTML5的<video>标签已经被目前大多数主流浏览器所支持,包括还未正式发布的IE9也声明将支持<video>标签,利用浏览器原生特性嵌入视频有很多好处,所以很多开发者想尽快用上,但是真正使用前还有些问题要考虑,尤其是 Opera/Firefox 和IE/Safari浏览器所支持的视频编码不同的问题,Google几个月前发布的开源视频编码VP8有望能解决这一问题,另外Google还发布了开放网络媒体项目WebM,旨在帮助开发者为开放网络制作出世界级媒体格式,Opera, Firefox, Chrome和IE9都将支持VP8,而且Flash Player也将可以播放VP8,这就意味着我们很快就可以只制作一个版本的视频然后在所有主流浏览器上播放了。另外一个主要的问题就是如何构建自定义的HTML5<video>播放器,这是目前Flash Player的优势所在,利用Flash的IDE所提供的接口可以很方便的构建一个个性化的视频播放器,那HTML5的<video>标签要怎样才能实现呢?这个问题就是本文所要解决的!我们将开发一个HTML5<video>视频播放器的jQuery插件,并且可以很方便的进行自定义,将分为以下几个部分:
1.视频控制工具条
2.视频控制按钮
3.打包成jQuery插件
4.外观和体验
5.自定义皮肤
视频控制工具条
做为一个专业的web开发人员,我们创建一个视频播放器时一定希望它的外观在各个浏览器中看起来一致(consistent),但是通过下面的图可以看到目前各个浏览器提供的视频控制工具条外观各不相同:
那就没办法了,我们得自己从头来创建这个控制工具条,利用HTML和CSS再加上一些图片实现起来并不算很难,另外通过HTML5多媒体元素提供的API我们可以很方便将创建的任何按钮与播放/暂停等事件进行绑定。
视频控制按钮
基本的视频控制工具条要包含一个播放/暂停按钮,一个进度条,一个计时器和一个音量控制按钮,我们将这些按钮放在<video>元素下面,并用一个div作为父容器:
Java代码
<div class=”ghinda-video-controls”>
<a class=”ghinda-video-play” title=”Play/Pause”></a>
<div class=”ghinda-video-seek”></div>
<div class=”ghinda-video-timer”>00:00</div>
<div class=”ghinda-volume-box”>
<div class=”ghinda-volume-slider”></div>
<a class=”ghinda-volume-button” title=”Mute/Unmute”></a>
</div>
</div>
复制代码
注意,我们使用元素的class属性来代替ID属性是为了方便在一个页面上使用多个播放器。
打包成jQuery插件
创建好控制按钮后我们需要配合多媒体元素的API来实现视频控制的目的,正如前面提到的一样我们将我们的播放器打包成jQuery插件,这样可以很好的实现复用,代码如下:
Java代码
$.fn.gVideo = function(options) {
// build main options before element iteration
var defaults = {
theme: ‘simpledark’,
childtheme: ”
};
var options = $.extend(defaults, options);
// iterate and reformat each matched element
return this.each(function() {
var $gVideo = $(this);
//create html structure
//main wrapper
var $video_wrap = $(‘<div></div>’).addClass(‘ghinda-video-player’).addClass(options.theme).addClass(options.childtheme);
//controls wraper
var $video_controls = $(‘<div class=”ghinda-video-controls”><a class=”ghinda-video-play” title=”Play/Pause”></a><div class=”ghinda-video-seek”></div><div class=”ghinda-video-timer”>00:00</div><div class=”ghinda-volume-box”><div class=”ghinda-volume-slider”></div><a class=”ghinda-volume-button” title=”Mute/Unmute”></a></div></div>’);
$gVideo.wrap($video_wrap);
$gVideo.after($video_controls);
这里先假设您了解jQuery并知道如何创建一个jQuery插件,因为这个不在本文的讨论范围之内,在上面这段脚本中我们使用jQuery动态创建视频控制工具条的元素,接下来为了绑定事件我们需要获取对应的元素:
Java代码
//get newly created elements
var $video_container = $gVideo.parent(‘.ghinda-video-player’);
var $video_controls = $(‘.ghinda-video-controls’, $video_container);
var $ghinda_play_btn = $(‘.ghinda-video-play’, $video_container);
var $ghinda_video_seek = $(‘.ghinda-video-seek’, $video_container);
var $ghinda_video_timer = $(‘.ghinda-video-timer’, $video_container);
var $ghinda_volume = $(‘.ghinda-volume-slider’, $video_container);
var $ghinda_volume_btn = $(‘.ghinda-volume-button’, $video_container);
$video_controls.hide(); // keep the controls hidden
这里我们通过className方式获取,先让工具条隐藏直到所有资源加载完成,现在来实现播放/暂停按钮:
Java代码
var gPlay = function() {
if($gVideo.attr(‘paused’) == false) {
$gVideo[0].pause();
} else {
$gVideo[0].play();
}
};
$ghinda_play_btn.click(gPlay);
$gVideo.click(gPlay);
$gVideo.bind(‘play’, function() {
$ghinda_play_btn.addClass(‘ghinda-paused-button’);
});
$gVideo.bind(‘pause’, function() {
$ghinda_play_btn.removeClass(‘ghinda-paused-button’);
});
$gVideo.bind(‘ended’, function() {
$ghinda_play_btn.removeClass(‘ghinda-paused-button’);
});
大多数浏览器在右键点击视频时会提供一个独立的菜单,它也提供了视频控制功能,如果用户通过这个右键菜单控制视频那就会跟我们的自定义控件冲突,所以为了避免这一点我们需要绑定视频播放器自身的“播放”,“暂停”和“结束”事件,在事件处理函数中处理播放/暂停按钮,控制按钮的样式。
为了创建进度条的拖动块,我们使用了jQuery UI的Slider组件:
Java代码
var createSeek = function() {
if($gVideo.attr(‘readyState’)) {
var video_duration = $gVideo.attr(‘duration’);
$ghinda_video_seek.slider({
value: 0,
step: 0.01,
orientation: “horizontal”,
range: “min”,
max: video_duration,
animate: true,
slide: function(){
seeksliding = true;
},
stop:function(e,ui){
seeksliding = false;
$gVideo.attr(“currentTime”,ui.value);
}
});
$video_controls.show();
} else {
setTimeout(createSeek, 150);
}
};
createSeek();
正如你所看到的,这里我们写了一个递归函数,通过循环比较video的readyState属性来判断视频是否已经准备好,否则我们就不能获得视频的时长也无法创建滑动块,当视频准备好后我们初始化滑动块并显示控制工具条,下一步我们通过绑定video元素的timeupdate事件实现计时器功能:
Java代码
var gTimeFormat=function(seconds){
var m=Math.floor(seconds/60)<10?”0″+Math.floor(seconds/60):Math.floor(seconds/60);
var s=Math.floor(seconds-(m*60))<10?”0″+Math.floor(seconds-(m*60)):Math.floor(seconds-(m*60));
return m+”:”+s;
};
var seekUpdate = function() {
var currenttime = $gVideo.attr(‘currentTime’);
if(!seeksliding) $ghinda_video_seek.slider(‘value’, currenttime);
$ghinda_video_timer.text(gTimeFormat(currenttime));
};
$gVideo.bind(‘timeupdate’, seekUpdate);
这里我们用seekUpdate函数获取video的currentTime属性值然后调用gTimeFormat函数进行格式化后得到当前播放的时间点。
至于音量控制控件我们还是利用jQuery UI的Slider组件然后利用自定义函数实现静音和取消静音的功能:
Java代码
$ghinda_volume.slider({
value: 1,
orientation: “vertical”,
range: “min”,
max: 1,
step: 0.05,
animate: true,
slide:function(e,ui){
$gVideo.attr(‘muted’,false);
video_volume = ui.value;
$gVideo.attr(‘volume’,ui.value);
}
});
var muteVolume = function() {
if($gVideo.attr(‘muted’)==true) {
$gVideo.attr(‘muted’, false);
$ghinda_volume.slider(‘value’, video_volume);
$ghinda_volume_btn.removeClass(‘ghinda-volume-mute’);
} else {
$gVideo.attr(‘muted’, true);
$ghinda_volume.slider(‘value’, ‘0’);
$ghinda_volume_btn.addClass(‘ghinda-volume-mute’);
};
};
$ghinda_volume_btn.click(muteVolume);
最后当我们自己的自定义视频控制工具条构造完成后需要移除<video>标签的controls属性,这样浏览器默认的工具条就被去掉了。
好了,我们的插件功能已经全部完成了,调用方法:
Java代码
$(‘video’).gVideo();
这会将我们的插件应用到页面上每一个video元素上。
外观和体验
好的,现在到了比较有意思的部分,也就是播放器的外观和体验了。当插件功能已经完成后利用一点CSS就可以很容易地自定义样式了,我们将全部使用CSS3来实现。
首先,我们给播放器主容器加一些样式:
Java代码
.ghinda-video-player {
float: left;
padding: 10px;
border: 5px solid #61625d;
-moz-border-radius: 5px; /* FF1+ */
-ms-border-radius: 5px; /* IE future proofing */
-webkit-border-radius: 5px; /* Saf3+, Chrome */
border-radius: 5px; /* Opera 10.5, IE 9 */
background: #000000;
background-image: -moz-linear-gradient(top, #313131, #000000); /* FF3.6 */
background-image: -webkit-gradient(linear,left top,left bottombottom,color-stop(0, #313131),color-stop(1, #000000)); /* Saf4+, Chrome */
box-shadow: inset 0 15px 35px #535353;
}
下一步,我们设置视频控制工具条左边浮动使它们水平对齐,利用CSS3的opacity和transitions我们给播放/暂停和静音/取消静音按钮添加了非常不错的悬浮效果:
Java代码
.ghinda-video-play {
display: block;
width: 22px;
height: 22px;
margin-right: 15px;
background: url(../images/play-icon.png) no-repeat;
opacity: 0.7;
-moz-transition: all 0.2s ease-in-out; /* Firefox */
-ms-transition: all 0.2s ease-in-out; /* IE future proofing */
-o-transition: all 0.2s ease-in-out; /* Opera */
-webkit-transition: all 0.2s ease-in-out; /* Safari and Chrome */
transition: all 0.2s ease-in-out;
}
.ghinda-paused-button {
background: url(../images/pause-icon.png) no-repeat;
}
.ghinda-video-play:hover {
opacity: 1;
}
如果您仔细看了前面那段根据视频播放状态(Playing/Paused)添加和移除播放/暂停按钮样式的JavaScript代码,就会明白为什么.ghinda-paused-button为什么要重写.ghinda-video-play的背景属性了。
现在轮到滑动块了,我们进度条和音量控制的滑动块的实现都是利用了jQuery UI的Slider组件,这个组件它本身自带了样式,定义在jQuery UI对应的css文件中,但是为了使滑动块和播放器其他控件外观保持一致我们全部重写了它的样式:
Java代码
.ghinda-video-seek .ui-slider-handle {
width: 15px;
height: 15px;
border: 1px solid #333;
top: -4px;
-moz-border-radius:10px;
-ms-border-radius:10px;
-webkit-border-radius:10px;
border-radius:10px;
background: #e6e6e6;
background-image: -moz-linear-gradient(top, #e6e6e6, #d5d5d5);
background-image: -webkit-gradient(linear,left top,left bottombottom,color-stop(0, #e6e6e6),color-stop(1, #d5d5d5));
box-shadow: inset 0 -3px 3px #d5d5d5;
}
.ghinda-video-seek .ui-slider-handle.ui-state-hover {
background: #fff;
}
.ghinda-video-seek .ui-slider-range {
-moz-border-radius:15px;
-ms-border-radius:15px;
-webkit-border-radius:15px;
border-radius:15px;
background: #4cbae8;
background-image: -moz-linear-gradient(top, #4cbae8, #39a2ce);
background-image: -webkit-gradient(linear,left top,left bottombottom,color-stop(0, #4cbae8),color-stop(1, #39a2ce));
box-shadow: inset 0 -3px 3px #39a2ce;
}
这时候音量控制的滑动块一直显示在音量按钮旁边,我们需要将它改成默认隐藏,当鼠标悬浮在音量按钮上再动态显示出来,使用transitions来实现这个效果会是个不错的的选择:
Java代码
.ghinda-volume-box {
height: 30px;
-moz-transition: all 0.1s ease-in-out; /* Firefox */
-ms-transition: all 0.1s ease-in-out; /* IE future proofing */
-o-transition: all 0.2s ease-in-out; /* Opera */
-webkit-transition: all 0.1s ease-in-out; /* Safari and Chrome */
transition: all 0.1s ease-in-out;
}
.ghinda-volume-box:hover {
height: 135px;
padding-top: 5px;
}
.ghinda-volume-slider {
visibility: hidden;
opacity: 0;
-moz-transition: all 0.1s ease-in-out; /* Firefox */
-ms-transition: all 0.1s ease-in-out; /* IE future proofing */
-o-transition: all 0.1s ease-in-out; /* Opera */
-webkit-transition: all 0.1s ease-in-out; /* Safari and Chrome */
transition: all 0.1s ease-in-out;
}
.ghinda-volume-box:hover .ghinda-volume-slider {
position: relative;
visibility: visible;
opacity: 1;
}
利用一些基础的CSS属性以及CSS3提供的新属性我们打造了一个全新的播放器外观,它看起来是这个样子:
自定义皮肤
可能您已经注意到,我们在编写插件的时候已经定义了一些默认选项,它们是theme和childtheme,可以在调用插件的时候根据需要方便的应用自定义皮肤。
这里解释下theme就是所有控件的一整套样式定义,childtheme就是在theme基础上重写某些样式,我们在调用插件的时候可以同时指定这两个选项或者其中的一个:
Java代码
$(‘video’).gVideo({
childtheme:’smalldark’
});
我们写了一个示例的皮肤smalldark,它只重写了部分的样式,显示效果是这样的:
总结
利用HTML5 video,JavaScript和CSS3打造自定义的视频播放器真的非常容易,t实现工具条功能用JavaScrip,外观和体验交给CSS3,我们得到了一个功能强大并且易于定制的解决方案!
enjoy!
2)mui Html5 Video 实现方案
前言: 最近项目中需要用到html5 视频播放功能,于是稍微研究了解了下,遇到了很多坑,特此记录下.
一、 Html5 Video
参考来源:
(这篇博文确实帮助很大)
1.1、 目的
将Html5 Video功能应用到实际项目中,针对不同的平台和环境,进行个性化处理。
基本只考虑webkit浏览器兼容问题
1.2、 Html5 Video支持格式
只支持: .mp4后缀(.h264编码格式),和.webm后缀(专用web视频格式),以及.ogg后缀(音频文件)
注意: Html5 Video 可以添加多个source源来进行兼容适配,这样,当第一个源读取出问题时会自动读取下一个源. 比如可以同时在前面加上.webm和.mp4源,这样一个出错时会自动读取另一个可用源(因为不同浏览器,支持的格式是不一样的)
但是,Hybird模式的 Android 下,有些机型只能读取第一个source来源(测试华为和联想都是),所以也就是说在这种情况下,要确保第一个source源是正确的
各大浏览器兼容如图所示:
见图1
1.3、 不同平台环境和对应实现方案
说明: 这里分为两大块,普通浏览器环境(pc和手机,主要是移动端,pc不做特别处理)和Hybird模式的APP环境(Android和iOS).
注: Html5 video可以播放本地视频或者网络视频
1.3.1、 普通浏览器环境
*用Html5 Video 自带的播放栏控件
*用 Video 视频统一处理方法处理后,点击图片手动隐藏图片,设置视频大小,手动播放视频.
注: 播放效果则由各大浏览器自行实现
手机端浏览器实现的不同效果,比如:
QQ浏览器(包括QQ客户端内置的浏览器):播放时会自动进入全屏
华为自带浏览器: 正常小窗口播放
1.3.2、 Hybird App环境
说明: 内联播放是指直接在video标签中播放视频,没有必要进入全屏
1.3.2.1、 Android内联播放
*用Html5 Video 自带的播放栏控件
*用 Video 视频统一处理方法处理后,点击图片手动隐藏图片,设置视频大小,手动播放视频.
*Android内联播放需要注意,必须开启硬件加速,由于有些Android手机 webview是默认关闭硬件加速的,所以必须在创建这个带视频播放的webview时手动添加 硬件加速属性才行.(详情见plus创建webview的style)
style.hardwareAccelerated = true;
1.3.2.2、 iOS内联播放
*用Html5 Video 自带的播放栏控件
*用 Video 视频统一处理方法处理后,点击图片手动隐藏图片,设置视频大小,手动播放视频.
*内联播放注意要点,由于iOS下默认是全屏播放的,所以需要经过设置才能正常内联播放
第一步:在项目的manifest里面配置允许webview内联播放
"plus": { "splashscreen": { "autoclose": true,/*是否自动关闭程序启动界面,true表示应用加载应用入口页面后自动关闭;false则需调plus.navigator.closeSplashscreen()关闭*/ "waiting": true/*是否在程序启动界面显示等待雪花,true表示显示,false表示不显示。*/ }, "allowsInlineMediaPlayback": true,/*设置ios下允许内联播放*/ "popGesture": "close"
第二步: 创建video标签时,手动加上内联播放的属性(iOS不支持preload)
<!-- 让ios支持内联播放,必须添加 webkit-playsinline 标签 --> <video webkit-playsinline id="videoMedia" controls="controls" preload>
这样iOS手机在播放的时候才会采用内联播放
1.3.2.3、 Android非内联播放
*通过NJS使用原生播放器来播放视频,传入的url可以是本地的或网络的地址
*用 Video 视频统一处理方法处理后,点击图片之后,图片保持不变(所以没有必要隐藏图片),直接获取视频的资源地址,传给原生播放器播放
注: 这种模式下,性能要比直接html5自带播放器播放高
1.3.2.4、 iOS非内联播放
*用Html5 Video 自带的播放栏控件(非内联播放需要去除特定内联属性”webkit-playsinline”,这样才能全屏播放)
if(!isInlinePlay){ //如果是非内敛,ios需要去除内联样式 mediaTarget.removeAttribute('webkit-playsinline'); }
*用 Video 视频统一处理方法处理后,点击图片之后,图片保持不变(所以没有必要隐藏图片),直接调用video.play()播放视频(这时候会用一个全屏播放器来播放视频)
1.3.3、 注意要点
如果采用NJS通过Android原生播放器播放视频,目前无法监听到视频的一些自定义事件.(如下载中,播放完毕,暂停,播放时间等)
而如果采用Html5 Video自带播放,这些是可以通过脚本控制的.
所以选定方案时需要进行衡量
*另外,在Html5 Video播放时,无法监听到规定的结束事件seeked,只能在timeUpdate里面判断,如果ended为true就代表结束了.
*在NJS通过Android原生播放器播放时,可以通过document监听resume和pause事件判断是否进入播放和退出播放
1.4、 Tips
1.4.1、 关于Video 视频统一处理的方案
说明: 由于将一个<Video>直接显示在页面中,会有各种五花八门的播放器效果,如图:
(这里引用了参考来源的图)
见图2
显然,体验效果并不好,所以现在的做法是用一张模拟播放的图片来替代<Video>所在的地方,而将Video元素设置为1*1像素大小.然后给图片设置点击监听,监听到点击时,播放视频.
注意:
*这里不要用{display: none}或者{width:0;height:0;}的方式,因为这样视频元素会处于未激活的状态,给后续的处理带来麻烦.
*这里没有考虑ios<6和一些低版本的Android的兼容性问题了(这些版本里,无法直接通过video.play()来播放),因为项目环境基本上要求Android>4.0 iOS 7.0以上的.
*关于点击图片播放视频后,如果是内联播放模式下(或者是普通浏览器),就应该将图片隐藏,然后将视频大小设置为本来的大小(一般为图片大小);如果是非内联播放模式(全屏模式),就没有必要隐藏图片了,因为iOS下会自动打开一个全屏播放器来播放视频,Android下考虑到Html5 video较卡,所以也会通过NJS使用原生播放器来全屏播放视频.
1.4.2、 Android NJS播放视屏的实现代码
说明: 这个是Dcloud论坛中有人分享的
//非内联模式下的plus下的android才用到 var Intent = plus.android.importClass("android.content.Intent"); var Uri = plus.android.importClass("android.net.Uri"); var main = plus.android.runtimeMainActivity(); var intent = new Intent(Intent.ACTION_VIEW); var uri = Uri.parse(url); intent.setDataAndType(uri, "video/*"); main.startActivity(intent);
1.4.3、 如何读取元素的宽高
*在获取视频的宽度时,发现用 video.style.width无法获取到宽度.
后来查了资料,发现dom.style.width(height)只能获取在stye直接赋予的值.而如果是通过css样式表赋予的值是无法直接获取的,只能通过dom.offsetWidth(offsetHeight)获取.
*设置元素宽和高是不要直接在style里设置,而是最好通过css样式表赋予
*读取元素宽和高时,用offsetWidth(offsetHeight)
1.4.4、 关于全屏播放的问题
在PC端webkit浏览器下,全屏代码如下:
进入全屏: videoContainer(对应的dom).webkitRequestFullscreen();
退出全屏: document.webkitCancelFullScreen();
*但是经测试,在手机浏览器和Hybird模式下的手机环境中都无法使用,
应该是手机浏览器中video 播放器的全屏模式和pc端的有区别,已经脱离了webkit的全屏组件,而是用原生自己实现的.
1.5、 遇到问题及解决方法
1.5.1、 Video.currentTime 设置值时设置无效,或者变为0
原因分析:
与测试的服务器和端口有关,测试环境是放在hbuild本地浏览器的,没有处理好视频快进问题,所以会导致每次快进后,视频都会重置-在某些测试服务器上,则出现快进无效,但不会重置
解决方法:
将网页用其它正式服务器打开均可正常,如tomcat,wampserver,甚至直接在本地打开也行.
1.5.2、 无法通过代码Video.src获取资源路径
原因分析:
本实例中,Video是通过source添加src的,无法直接读取video的src
解决方法:
可以通过读取到第一个source的标签,再获取source的src
注:本来这个方法有一个缺点就是有可能第一个source的src不可用.但是由于Android中第一个source必须有用才行.否则无法正常播放.所以在确保第一个source正确的情况下能这样用.
1.5.3、 部分Android机型无法退出全屏
描述:
在使用Html5 Video自带播放器播放时,部分Android机型(如联想K860点击全屏按钮进入全屏后,无法退出全屏-因为进入全屏后,全屏按钮不见了)
原因分析: 可能是手机厂商擅自劫持了浏览器或者篡改了浏览器实现方式
解决方法:
目前无法解决,在这类机型中,建议直接采用非内联模式播放或者是尽量不要手动进入全屏
3)移动端HTML5<video>视频播放优化实践
遇到的挑战
移动端HTML5使用原生<video>标签播放视频,要做到两个基本原则,速度快和体验佳,先来分析一下这两个问题。
下载速度
以一个8s短视频为例,wifi环境下提供的高清视频达到1000kbps,文件大小大约1MB;非wifi环境下提供的低码率视频是500kbps左右,文件大小大约500KB;参考QzoneTouch多普勒测速,2g网络的平均速度是14KB/s,那么下载一个低码率视频耗时35s;那么要想流畅播放视频,就需要一个加载等待的过程,这个过程要有明确的反馈,不能让用户有“坏掉了”的感觉。
多普勒测速数据参考
# | dns(s) | conn(s) | rtt(s) | tran(kb/s) |
---|---|---|---|---|
2g | 3.85785 | 2.33482 | 2.57478 | 14.0374 |
3g | 1.60643 | 0.743109 | 0.608047 | 60.1967 |
wifi | 0.986921 | 0.550208 | 0.444332 | 70.8728 |
用户体验
视频是否可以自动播放,是否能循环播放,是否能显示下载进度,播放的时候如何隐藏控制条,暂停的时候又能显示出来呢。这些问题看上去貌似简单,但是由于PC/iOS/Android这些不同平台、不同的浏览器内核、甚至相同内核的不同版本,所实现的<video>属性、方法和事件差异较大,解决兼容性问题又给开发造成了很大困扰。
分析原因
事件差异
下面是播放一个短视频,在不同平台触发事件和获取属性的差异表现。
PC
# | event | readyState | currentTime (s) | buffered (s) | duration (s) | 视频状态 |
---|---|---|---|---|---|---|
1 | loadstart | NOTHING | 0 | – | – | – |
2 | suspend | NOTHING | 0 | – | – | – |
3 | play | NOTHING | 0 | – | – | – |
4 | waiting | NOTHING | 0 | – | – | – |
5 | durationchange | METADATA | 0 | 5.35 | 7.91 | 获取到视频长度 |
6 | loadedmetadata | METADATA | 0 | 0.66 | 7.91 | 获取到元数据 |
7 | loadeddata | ENOUGHDATA | 0 | 0.66 | 7.91 | – |
8 | canplay | ENOUGH_DATA | 0 | 0.66 | 7.91 | – |
9 | playing | ENOUGH_DATA | 0 | 0.66 | 7.91 | 开始播放 |
10 | canplaythrough | ENOUGH_DATA | 0 | 0.66 | 7.91 | 可以流畅播放 |
11 | progress | ENOUGH_DATA | 0.11 | 3.68 | 7.91 | 持续下载 |
12 | timeupdate | ENOUGH_DATA | 0.14 | 4.44 | 7.91 | 播放进度变化 |
… | … | … | … | … | … | … |
23 | progress | ENOUGH_DATA | 1.77 | 7.91 | 7.91 | 下载完毕 |
24 | suspend | ENOUGH_DATA | 1.77 | 7.91 | 7.91 | – |
25 | timeupdate | ENOUGH_DATA | 1.9 | 7.91 | 7.91 | 继续播放中 |
… | … | … | … | … | … | … |
48 | timeupdate | ENOUGH_DATA | 7.7 | 7.91 | 7.91 | – |
49 | timeupdate | ENOUGH_DATA | 0 | 7.91 | 7.91 | – |
50 | seeking | METADATA | 0 | 7.91 | 7.91 | – |
51 | waiting | METADATA | 0 | 7.91 | 7.91 | – |
52 | timeupdate | ENOUGH_DATA | 0 | 7.91 | 7.91 | – |
53 | seeked | ENOUGH_DATA | 0 | 7.91 | 7.91 | 播放完毕进度回到起点 |
54 | canplay | ENOUGH_DATA | 0 | 7.91 | 7.91 | – |
55 | playing | ENOUGH_DATA | 0 | 7.91 | 7.91 | 循环播放 |
56 | canplaythrough | ENOUGH_DATA | 0 | 7.91 | 7.91 | – |
57 | timeupdate | ENOUGH_DATA | 0.19 | 7.91 | 7.91 | – |
… | … | … | … | … | … | … |
iOS
# | event | readyState | currentTime (s) | buffered (s) | duration (s) | 视频状态 |
---|---|---|---|---|---|---|
1 | loadstart | NOTHING | 0 | – | – | – |
2 | play | NOTHING | 0 | – | – | – |
3 | waiting | NOTHING | 0 | – | – | – |
4 | durationchange | METADATA | 0 | – | 7.91 | 获取到视频长度 |
5 | loadedmetadata | METADATA | 0 | – | 7.91 | 获取到元数据 |
6 | loadeddata | ENOUGHDATA | 0 | – | 7.91 | – |
7 | canplay | ENOUGH_DATA | 0 | 7.91 | 7.91 | – |
8 | canplaythrough | ENOUGH_DATA | 0 | 7.91 | 7.91 | 可以流畅播放 |
9 | playing | ENOUGH_DATA | 0 | 7.91 | 7.91 | 开始播放 |
10 | progress | ENOUGH_DATA | 0 | 7.91 | 7.91 | 下载完毕 |
11 | suspend | ENOUGH_DATA | 0 | 7.91 | 7.91 | – |
12 | timeupdate | ENOUGH_DATA | 0.02 | 7.91 | 7.91 | 播放进度变化 |
… | … | … | … | … | … | … |
43 | timeupdate | ENOUGH_DATA | 7.8 | 7.91 | 7.91 | – |
44 | timeupdate | ENOUGH_DATA | 0 | 7.91 | 7.91 | – |
45 | seeked | ENOUGH_DATA | 0 | 7.91 | 7.91 | 播放完毕进度回到起点 |
46 | timeupdate | ENOUGH_DATA | 0.22 | 7.91 | 7.91 | 循环播放 |
… | … | … | … | … | … | … |
Android
# | event | readyState | currentTime (s) | buffered (s) | duration (s) | 视频状态 |
---|---|---|---|---|---|---|
1 | loadstart | NOTHING | 0 | – | – | – |
2 | play | NOTHING | 0 | – | – | – |
3 | waiting | NOTHING | 0 | 0 | – | – |
4 | durationchange | ENOUGH_DATA | 0 | 0 | 0 | – |
5 | durationchange | ENOUGH_DATA | 0 | 0 | 7.91 | 获取到视频长度 |
6 | loadedmetadata | ENOUGH_DATA | 0 | 0 | 7.91 | 获取到元数据 |
7 | loadeddata | ENOUGHDATA | 0 | 0 | 7.91 | – |
8 | canplay | ENOUGH_DATA | 0 | 0 | 7.91 | – |
9 | canplaythrough | ENOUGH_DATA | 0 | 0 | 7.91 | – |
10 | playing | ENOUGH_DATA | 0 | 0 | 7.91 | – |
11 | timeupdate | ENOUGH_DATA | 0 | 0 | 7.91 | – |
12 | progress | ENOUGH_DATA | 0 | 3.57 | 7.91 | 下载中 |
13 | timeupdate | ENOUGH_DATA | 0.2 | 6.89 | 7.91 | 开始播放 |
14 | progress | ENOUGH_DATA | 0 | 7.91 | 7.91 | 下载完毕 |
… | … | … | … | … | … | … |
49 | timeupdate | ENOUGH_DATA | 7.79 | 7.91 | 7.91 | – |
50 | progress | ENOUGH_DATA | 7.87 | 7.91 | 7.91 | – |
51 | timeupdate | ENOUGH_DATA | 0 | 7.91 | 7.91 | – |
52 | seeking | ENOUGH_DATA | 0 | 7.91 | 7.91 | 播放完毕进度回到起点 |
53 | timeupdate | ENOUGH_DATA | 0 | 7.91 | 7.91 | – |
54 | seeked | ENOUGH_DATA | 0 | 7.91 | 7.91 | 循环播放失败卡住了 |
55 | progress | ENOUGH_DATA | 0 | 7.91 | 7.91 | – |
56 | stalled | ENOUGH_DATA | 0 | 7.91 | 7.91 | – |
一些常用且需要重点关注的<video>事件
event | iOS | Android |
---|---|---|
****************** | *********************************************** | *********************************************** |
play | 只是要播放视频,响应的是video.play()方法,并不代表已经开始播放 | 和iOS一样,仅是响应video.play()方法 |
durationchange | 会执行一次,一定会获取到视频的duration | 可能会执行多次,只有最后一次才能获取到真实的duration,前面的duration都是0;但低版本Android可能获取到的duration是0或1;(本文提到的低版本Android大部分是4.1以下) |
canplay | 可以认为是视频元素没有问题,可以运行,没有更多含义了,基本用不上 | 同iOS |
canplaythrough | 会有明确的缓冲,表示可以流畅播放了; | 没有什么用,视频仍然会卡住,数据可能还没有开始加载; |
playing | 明确表示播放开始了; | 依然没有用,视频可能并没有开始播放; |
progress | 有明确的下载,可以获取到当前的buffer,并且全部下载完毕后不在触发; | 不一定有明确的数据下载,并且全部下载完毕后依然继续触发; |
timeupdate | 会有明确的进度变化,可以获取到currentTime; | 进度不一定变化,currentTime可能总是0,但是第一次有currentTime变化的timeupdate事件一定代表了视频开始播放了; |
error | iOS中会有明确的错误抛出; | Android中某些浏览器会莫名其妙的抛出error; |
stalled | 网络状况不佳,导致视频下载中断; | 在没有play之前,也可能会抛出该事件。 |
属性差异
attributes | iOS | android |
---|---|---|
****************** | *********************************************** | *********************************************** |
poster 封面图片 | 支持,但是加载速度明显比在<img>中要慢; | 不一定支持(浏览器厂商的实现标准不统一); |
preload 预加载 | iPhone不支持; | 可能支持; |
autoplay 自动播放 | iPhone Safari中不支持,但在webview中可能被开启;iOS开发文档明确说明蜂窝网络下不允许autoplay; | 可能支持; |
loop 循环播放 | 支持 | 可能支持; |
controls 控制条 | 支持,但是需要开始播放了才显示 | 基本都支持显示或者不显示 |
width和height | 一定给出明确的属性设置,切不能为0; | 如果不设置,仅仅通过CSS样式去控制视频大小,可能会导致标签失效。 |
其他怪异bug和不友好表现
iOS | android |
---|---|
********************************************************* | ********************************************************* |
物理位置覆盖在<video>区域上的元素,click和touch等事件会失效,比如一个<a>链接如果覆盖在<video>上,那么点击后没有任何效果。 | – |
iOS8.0+中,单页面播放视频超过16个,再播放的视频全部MediaError解码异常无法播放。 | – |
iPhone的Safari会弹出一个全屏的播放器来播放视频,iPad则支持内联播放。iOS7+ 如果webview(比如微信)开启了 webview.allowsInlineMediaPlayback = YES; ,可以通过设置 webkit-playsinline 属性支持内联播放; | 支持内联播放,但某些厂商会用自己的播放器劫持原生的视频播放; |
下载视频时,会先发送一个2字节的请求来获取视频元数据(比如时长),然后再不断的发送分包续传(206)请求来下载视频,抓包显示请求数和请求量至少有一倍的冗余(x2),这个严重的bug在iOS8中有明显的修复,但是分包的206请求仍然会有冗余数据的下载,浪费了流量。 | 比iOS的处理方式好,没有第一个2字节请求,没有流量损耗; |
– | 低版本Android(<=4.0.4)中,<video>如果在有相对和决定定位的层中,可能会导致整个页面错位。 |
– | 某些浏览器厂商会劫持<video>,用其“自己”的播放器来播放视频,“破坏”了产品本身的播放体验,那么只能case by case的解决了。 |
加载视频时没有进度提示,视觉上看不出是播放完了还是卡住了; | 加载视频时,大都会显示一个自带的loading UI(菊花)。 |
最佳实践
视频初始化
如果将一个<video>直接显示在页面中,那么就会看到各种五花八门的播放器初始效果;
这显然不是一个好的视觉体验,那么通常的做法是制作一个模拟的视频播放视图,比如一个封面加一个播放按钮。
而真实的<video>视频元素要隐藏起来,如何隐藏呢?最好不要用 {display: none}
或者 {width:0;height:0;}
的方式,因为这样视频元素会处于未激活的状态,给后续的处理带来麻烦。最佳的方式是将视频设置成1×1像素大小,放在视觉边缘的位置。
1 2 3 4 5 | <!–iOS–> <video webkit-playsinline width=”1″height=”1″class=”vplayinside notaplink”x-webkit-airplay controls loop=”loop”src=”<%=src%>”></video> <!–Android–> <video width=”1″height=”1″controls loop=”loop”src=”<%=src%>”></video> |
自动播放
autoplay的支持依赖内核和网络状况,比如iPhone在蜂窝网络下明确禁用了autoplay;
经过试验,在没有明确的用户操作的情况下,直接通过 video.play()
也是无法激活播放的;
并且在产品设计上,自动播放也不是一个舒服的用户体验,所以产品设计上尽量避免使用自动播放。
点击播放
之前提到,视频最好通过1px大小隐藏起来,那么这时如何触发播放呢?
经过试验,当在明确的用户操作(touch、click)时,通过这些用户行为事件的回调函数,用 video.play()
是可以触发视频播放的,那么能否在用户操作后,再去同步的创建和播放视频呢?答案是肯定的,这无疑是一个视频元素初始化的最佳实践,但是有些差异需要注意。
iOS6+
可以在用户的touch时间中动态创建并播放视频。
iOS < 6
可以在用户的touch时间中动态创建视频,但不能播放;要再追加一个click事件来启动播放;也就是说,给伪造的视频播放按钮同时绑定tap和click事件,在tap的时候创建,在之后300毫秒的click中去播放。
Android
大部分高版本Android可以像iOS6+那样去处理,但是低版本的不行,必须要通过click事件去传递 video.play()
,为了保持兼容,最好是用帮tap和click两个事件来分别完成视频的初始化和播放。
我们还发现,有些低版本Android中,无法通过 video.play()
来播放视频,必须有真实的用户点击视频元素才能播放;这种情况,有一个技巧就是在tap的时候初始化并放大视频覆盖在播放视图中,让300毫秒后的真实点击行为穿透点击在视频元素上来实现播放。
循环播放
如果视频需要循环播放,那么就增加 loop
属性,是否能循环播放就看浏览器是否支持了,因为还没有找到hack技巧来强制循环播放;
即使,在不支持循环播放的Android中,通过监听 seeked
事件知道了播放进度到了终点或起点暂停了,此时也无法通过 video.play()
来让视频重新播放。
监控下载进度
如何获取视频时长和已经下载的时长?
1 2 3 4 5 6 7 8 9 10 11 12 13 | // 视频时长 varduration=video.duration // 获取视频已经下载的时长 functiongetEnd(video){ varend=0 try{ end=video.buffered.end(0)||0 end=parseInt(end*1000+1)/1000 }catch(e){ } returnend } |
progress事件表示视频在加载,但是它的触发频率和时机并不规律,最佳做法是通过一个定时器去实时获取end,当end >= duration时,表示已经下载完毕,再终止定时器。
1 2 3 4 5 6 7 8 9 10 | vartimer=setInterval(function(){ varend=getEnd(video), duration=video.duration if(end<duration){ return } clearInterval(timer) },1000) |
全部下载后再播放
假设播放短视频,如果网络不佳,会造成播放断断续续,在iOS中这种停顿还没有一个明确的等待提示,这不是一个好的体验,那么是否可以将视频全部下载完毕再播放呢?
在iOS中,可以在视频刚开始下载的时候马上暂停,此时下载还将继续,可以做一个loading的菊花告知视频正在加载,然后等到视频全部下载完再开始播放。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | $(video).one(‘loadeddata’,function(){ // 暂停,但下载还在继续 video.pause() // 启动定时器检测视频下载进度 vartimer=setInterval(function(){ varend=getEnd(video), duration=video.duration if(end<duration){ return } varwidth=$(video).parent().width() // 下载完了,开始播放吧 $(video).attr{ width:width, height:width } video.play() clearInterval(timer) },1000) }) |
缓冲播放——边下边播时,选择开始播放的最佳时间点
当视频越来越长或者网络慢时,等待视频全部下载完再播放也不是好的体验,最好能边下边播,缓冲到流畅状态就开始播放,那什么时候播放才是最佳时间点呢?
在iOS中,canplaythrough事件就是这个最佳时间点,它是通过动态计算缓冲量和下载速度得出的视频可以流畅播放的状态反馈。
注意:下载完再播放和缓冲播放只适用于iOS。
统计播放时间和播放次数
要统计实际的播放时间,要累加timeupdate事件变化的时间,再减去中间可能暂停的时间。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | $video.on(‘playing’,function(){ // 开始播放是打点 $video.attr(‘data-updateTime’,+newDate()) }) $video.on(‘pause’,function(){ // 暂停播放时清除打点 $video.removeAttr(‘data-updateTime’) }) // 累加播放时间 $video.on(‘timeupdate’,function(event){ var$video=$(event.target), updateTime=parseInt($video.attr(‘data-updateTime’)||0), playingTime=parseInt($video.attr(‘data-playingTime’)||0), times=parseInt($video.attr(‘data-times’)||0), newtimes=0, video=$video.get(0), duration=parseFloat($video.attr(‘data-duration’)||0), now=+newDate() // 播放时间 playingTime=playingTime+now-updateTime // 播放次数 newtimes=Math.ceil(playingTime/1000/duration) $video.attr(‘data-playingTime’,playingTime) $video.attr(‘data-updateTime’,now) }) |
异常处理
对error事件做详细的上报;
对stalled事件做统计上报,并提示用户网络慢等。
参考数据
微视触屏版iOS视频测速
网络环境 | 视频码率 | 获取到视频时长 时间点(s) | 开始流畅播放 时间点(s) | 全部下载完毕 时间点(s) | 视频长度(s) |
---|---|---|---|---|---|
wifi | 1000kbps | 2.86 | 3.97 | 5.85 | 8.69 |
非wifi | 500kbps | 4.56 | 8 | 10.62 | 8.67 |
搬好凳子看HTML
首先我们在HB下创建一个新的app项目,名称为 欠债
新建一个video.html
webkit-playsinline : 在ios中,加入此属性,可以关闭自动全屏播放
object-fit:fill : 视频充满video容器的大小
详细理由请看参考文献2or3
在此我们向项目里放置一个mp4格式的视频,视频内容不限,可以是小动画,也可以是
ps:要在meta中加上,否则视频会扩充变形哦
<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no" />
OK,现在布局已经完成,一个视频已经在页面中了
旁白:尼玛,点了没反应,那这怎么播放?
楼主:你们这群家伙看别的小视频等个1小时都行。。。
旁白:一个简单的播放器,至少要有 暂停/播放,进度条,视频时长,全屏等控件吧
楼主:来来来,不要急,先来个播放按钮写在video标签后面
<div class="bad-video"> <video class="" webkit-playsinline style="object-fit:fill;"> <source src='xx.mp4' type="video/mp4"></source> <p>设备不支持</p> <video> <img src="img/play.png"/> </div>
写好样式、
.bad-video { position: relative; overflow: hidden; background-color: #CCCCCC; } .bad-video .vplay{ position: absolute; width: 15%; z-index: 99; top: 50%; left: 50%; -webkit-transform: translate(-50%, -50%); transform: translate(-50%, -50%); }
楼主:当当当
再在后面加一个控制条
<img src="img/play.png" class="vplay" /> <div class="controls"> <div> <div class="progressBar"> <div class="timeBar"></div> </div> </div> <div><span class="current">00:00</span>/<span class="duration">00:00</span></div> <div><span class="fill">全屏</span></div> </div>
.bad-video .controls { width: 100%; height: 2rem; line-height: 2rem; font-size: 0.8rem; color: white; display: block; position: absolute; bottom: 0; background-color: rgba(0, 0, 0, .55); display: -webkit-flex; display: flex; }.bad-video .controls>* { flex: 1; }.bad-video .controls>*:nth-child(1) { flex: 6; }.bad-video .controls>*:nth-child(2) { flex: 2; text-align: center; }.bad-video .controls .progressBar { margin: .75rem 5%; position: relative; width: 90%; height: .5rem; background-color: rgba(200, 200, 200, .55); border-radius: 10px; }.bad-video .controls .timeBar { position: absolute; top: 0; left: 0; width: 0; height: 100%; background-color: rgba(99, 110, 225, .85); border-radius: 10px; }
总算有个看起来像样的了
旁白:楼主,可是还是不能播放啊
楼主:叫你别急,要不你先去撸一把,我写好了文字@你
旁白:好啊,早说嘛,我先走了,记得@我
楼主:你走,省的我精神分裂码两个人的字
好,现在Html元素已经基本上弄好啦,看起来不是那么low了