阶段4:格式自动识别 + flv.js 替换为 mpegts.js + 移动端 5 项修复 + 自动化冒烟测试

- 按 URL 扩展名自动识别插件(.m3u8→hls.js,.flv/.ts/.m2ts→mpegts.js),iOS 上 m3u8 仍走原生 HLS
- flv 播放改由 mpegts.js 承担,移除 flv.js 依赖,plug:'flv.js' 保留兼容别名
- 修复 plug:'dash.js' 调用不存在函数的死代码 bug
- mpegts 能力判断由 mseLivePlayback 放宽为 isSupported()(修复部分浏览器点播被误杀)
- 移动端:iOS 原生全屏状态同步、iPad/iPod UA 识别、X5 同层播放属性、进度/音量条触摸拖动、触摸坐标滚动偏移修正
- 新增 Playwright 冒烟测试(npm test)与支持 Range 的本地服务器(npm run serve)
- 文档说明:测试禁用 python http.server(不支持 Range,mp4 无法 seek)
This commit is contained in:
王阳
2026-07-21 16:43:51 +08:00
parent 7e5fc46126
commit be445f2899
14 changed files with 420 additions and 280 deletions
+108 -18
View File
@@ -59,6 +59,8 @@ var language = languageDefault;
var CT=null,CK=null,CM=null,CV=null;//CT=播放器容器,页面中已存的,CT>CK>CM>CV>video
var pugPlayer=null;//插件播放器
var hls=null;//播放hls
var plugAuto='';//自动识别设置的插件名,用于与用户显式指定的plug区分
var videoNativeFull=false;//video元素是否处于原生全屏(iOS Safari的webkitEnterFullscreen)
var loadMeta=true;//第一次加载到元数据
var app='';//平台类型
var nowRotate=0;//当前视频旋转角度
@@ -339,6 +341,8 @@ var language = languageDefault;
video.attr('playsinline','true');
video.attr('webkit-playsinline','true');
video.attr('x5-playsinline','true');
video.attr('x5-video-player-type','h5');//X5内核启用同层H5播放
video.attr('x5-video-player-fullscreen','true');//X5内核全屏时使用同层播放器
}
if(vars['crossOrigin']){
video.useCORS=true;//解决跨域
@@ -466,6 +470,14 @@ var language = languageDefault;
return;
}
else{
/*
* 未指定插件(或插件为上次自动识别的结果)时,按视频地址扩展名自动识别插件
* 仅填写vars['plug'],后续canPlay分流逻辑不变:iOS等原生支持m3u8的环境仍走原生播放
*/
if(!vars['plug'] || vars['plug']==plugAuto){
vars['plug']=getPlugByUrl(vstr);
plugAuto=vars['plug'];
}
if(vars['plug'] && !canPlay(vstr)){
plugPlayer(vstr);
}
@@ -568,9 +580,6 @@ var language = languageDefault;
case 'mpegts.js':
mpegtsPlayer(url);
break;
case 'dash.js':
dashPlayer(url);
break;
default:
if(valType(vars['plug'])=='function'){
vars['plug'](video,url);
@@ -626,12 +635,12 @@ var language = languageDefault;
},
/*
* flvPlayer
* 功能:使用flv.js插件进行播放
* 功能:播放flv格式视频(基于mpegts.js库,plug:'flv.js'为兼容旧配置的别名)
*/
flvPlayer=function (url){
var path=getPath('flv.js')+'flv.min.js';
var path=getPath('mpegts.js')+'mpegts.js';
loadJs(path,function(){
if (typeof(flvjs)!='undefined' && !isUndefined(flvjs) && flvjs.isSupported()) {
if (typeof(mpegts)!='undefined' && !isUndefined(mpegts) && mpegts.isSupported()) {
var config={
type: 'flv',
url: url
@@ -649,13 +658,13 @@ var language = languageDefault;
pugPlayer.destroy();
pugPlayer=null;
}
pugPlayer = flvjs.createPlayer(config);
pugPlayer = mpegts.createPlayer(config);
pugPlayer.attachMediaElement(video);
pugPlayer.load();
pugPlayer.on(flvjs.Events.METADATA_ARRIVED, function(res){
pugPlayer.on(mpegts.Events.METADATA_ARRIVED, function(res){
videoHandler.loadedMetaData();
});
pugPlayer.on(flvjs.Events.ERROR, function(errorType, errorDetail, errorInfo){
pugPlayer.on(mpegts.Events.ERROR, function(errorType, errorDetail, errorInfo){
CT.error={code:errorInfo['code'],message:errorInfo['msg']};
eventTarget('error',CT.error);//注册监听error
showError();
@@ -670,7 +679,7 @@ var language = languageDefault;
mpegtsPlayer=function (url){
var path=getPath('mpegts.js')+'mpegts.js';
loadJs(path,function(){
if (typeof(mpegts)!='undefined' && !isUndefined(mpegts) && mpegts.getFeatureList().mseLivePlayback) {
if (typeof(mpegts)!='undefined' && !isUndefined(mpegts) && mpegts.isSupported()) {
var config={
type: 'mse',
url: url
@@ -703,6 +712,29 @@ var language = languageDefault;
}
});
},
/*
* getPlugByUrl
* 功能:根据视频地址的扩展名自动识别需要使用的插件
* 识别不到时返回''(维持原生播放路径),URL支持携带query/hash参数
*/
getPlugByUrl=function(url){
if(valType(url)!='string'){
return '';
}
var purePath=url.split('?')[0].split('#')[0];
var ext=purePath.substring(purePath.lastIndexOf('.')+1).toLowerCase();
switch(ext){
case 'm3u8':
return 'hls.js';
case 'flv':
return 'mpegts.js';
case 'ts':
case 'm2ts':
return 'mpegts.js';
default:
return '';
}
},
/*
* canPlay
* 功能:判断是否能支持相关视频格式
@@ -742,7 +774,9 @@ var language = languageDefault;
return sup;
};
var getExtension=function(filepath) {
return filepath.replace(/.+\./, '');
//先剥离query与hash,避免 xxx.m3u8?token=... 之类地址无法识别扩展名
var purePath=filepath.split('?')[0].split('#')[0];
return purePath.replace(/.+\./, '');
};
if(isUndefined(num)){
if(valType(vStr)=='string'){
@@ -1225,6 +1259,24 @@ var language = languageDefault;
CM.doubleClick(player.fullOrExit);//监听视频双击
$(document).addListener('keydown',videoHandler.keydown);//监听键盘按键
addListener(window, 'resize', videoHandler.resize);//监听窗口尺寸变化
/*
* 监听video元素原生全屏的进出(iOS Safari),同步内部全屏状态
*/
video.addListener('webkitbeginfullscreen',function(){
videoNativeFull=true;
videoHandler.resize();
});
video.addListener('webkitendfullscreen',function(){
videoNativeFull=false;
videoHandler.resize();
});
/*
* 监听标准全屏状态变化(桌面端),保持全屏状态准确
*/
addListener(document,'fullscreenchange',videoHandler.resize);
addListener(document,'webkitfullscreenchange',videoHandler.resize);
addListener(document,'mozfullscreenchange',videoHandler.resize);
addListener(document,'MSFullscreenChange',videoHandler.resize);
if(!isUndefined(vars['smallWindows'])){
if(valType(vars['smallWindows'])=='boolean' && vars['smallWindows']){
addListener(window, 'scroll', windowScroll);//监听窗口滚动
@@ -1769,7 +1821,7 @@ var language = languageDefault;
replaceInformation('volume',parseInt(this.volume*100));
},
resize:function(){
var fullState = document.fullScreen || document.mozFullScreen || document.webkitIsFullScreen || document.msFullscreenElement;
var fullState = document.fullScreen || document.mozFullScreen || document.webkitIsFullScreen || document.msFullscreenElement || videoNativeFull;
if(fullState) {
C['bar']['fullAndExit']['exitFull'].show();
C['bar']['fullAndExit']['full'].hide();
@@ -2892,6 +2944,11 @@ var language = languageDefault;
* 功能: 退出全屏
*/
exitFull:function(){
//iOS Safari上video处于原生全屏时,通过video.webkitExitFullscreen退出
if(videoNativeFull && !isUndefined(video.webkitExitFullscreen)){
video.webkitExitFullscreen();
return player;
}
var exitFullFun = document.exitFullscreen || //W3C
document.mozCancelFullScreen || //FireFox
document.webkitExitFullscreen || //Chrome等
@@ -2913,7 +2970,7 @@ var language = languageDefault;
* 功能:在全屏和退出全屏之间进行切换
*/
fullOrExit:function(){
var fullState = document.fullScreen || document.mozFullScreen || document.webkitIsFullScreen || document.msFullscreenElement;
var fullState = document.fullScreen || document.mozFullScreen || document.webkitIsFullScreen || document.msFullscreenElement || videoNativeFull;
if(fullState){
player.exitFull();
}
@@ -4928,6 +4985,16 @@ var language = languageDefault;
bg.mousemove(bgMouseMove);
bg.mouseout(bgMouseOut);
mouseLine.mouseover(clearTime);
/*
* 移动端:支持在进度条上按住拖动seek,复用bgMouseDown,preventDefault防止页面滚动
*/
var bgTouchSeek=function(e){
e.preventDefault && e.preventDefault();
bgMouseDown(e);
};
bg.touchstart(bgTouchSeek);
bg.touchmove(bgTouchSeek);
bg.touchend(bgTouchSeek);
},
/*
* volumeDragY
@@ -5003,6 +5070,16 @@ var language = languageDefault;
player.volume(vol);
};
bg.mousedown(bgMouseDown);
/*
* 移动端:支持在音量条上按住拖动调节音量,复用bgMouseDown,preventDefault防止页面滚动
*/
var bgTouchVolume=function(e){
e.preventDefault && e.preventDefault();
bgMouseDown(e);
};
bg.touchstart(bgTouchVolume);
bg.touchmove(bgTouchVolume);
bg.touchend(bgTouchVolume);
},
/*
* changeProgress
@@ -6315,11 +6392,24 @@ var language = languageDefault;
}
var x=eve.clientX + (document.documentElement.scrollLeft || $('body').scrollLeft),
y=eve.clientY + (document.documentElement.scrollTop || $('body').scrollTop);
if(isUndefined(x.toString()) || x.toString()=='NaN'){
x=eve.touches[0].clientX
}
if(isUndefined(y.toString()) || y.toString()=='NaN'){
y=eve.touches[0].clientY
if(isUndefined(x.toString()) || x.toString()=='NaN' || isUndefined(y.toString()) || y.toString()=='NaN'){
//触摸事件:touchstart/touchmove取touches;touchend时touches为空,取changedTouches兜底
var touch=null;
if(!isUndefined(eve.touches) && eve.touches.length>0){
touch=eve.touches[0];
}
else if(!isUndefined(eve.changedTouches) && eve.changedTouches.length>0){
touch=eve.changedTouches[0];
}
if(touch){
//与mouse路径对齐,补上页面滚动偏移
x=touch.clientX + (window.pageXOffset || document.documentElement.scrollLeft || 0);
y=touch.clientY + (window.pageYOffset || document.documentElement.scrollTop || 0);
}
else{
x=0;
y=0;
}
}
if(x.toString()=='NaN'){
x=0;
+6 -2
View File
@@ -173,11 +173,15 @@ import { addListener } from './dom.js';
/*
* getApp
* 功能,获取平台类型是否是iphone
* 功能,获取平台类型是否是iphone(含ipod/ipad,统一返回'iphone'以保持现有分支兼容)
*/
function getApp(){
var u = navigator.userAgent.toLowerCase();
if(u.indexOf('iphone')>-1){
if(u.indexOf('iphone')>-1 || u.indexOf('ipod')>-1 || u.indexOf('ipad')>-1){
return 'iphone';
}
//iPadOS 13+默认使用桌面UA(Macintosh),通过MacIntel平台+触屏点数判断
if(u.indexOf('macintosh')>-1 && navigator.platform=='MacIntel' && navigator.maxTouchPoints>1){
return 'iphone';
}
}