问题:React单页面应用加载ali-playerSDK的一种方式
原因
使用ali-player,需要加载其js库以及css库,同时ali-player仍不支持npm包的引入方式。所以最简单的方式是在document.ejs(或其他入口html)中标签引入:
<link
rel="stylesheet"
href="https://g.alicdn.com/de/prismplayer/2.9.3/skins/default/aliplayer-min.css"
/>
<script
type="text/javascript"
charset="utf-8"
src="https://g.alicdn.com/de/prismplayer/2.9.3/aliplayer-min.js"
></script>
但是这样就会引入一个新的问题,由于项目是单页面应用,所有的页面都会主动去请求这两个文件,但是我只是在其中的一个详情页面需要用到ali-player,就造成了一定的资源浪费。所以我期望只在页面进入到详情页面的时候,再去加载ali-player。
解决
思路是,在页面didMount的时候,去动态往页面上插入标签,异步加载完成后,然后初始化播放器:
// async AliplayerSDK inject to html
const loadSDK = () => {
const loaded = !!window.Aliplayer;
return (
loaded
? Promise.resolve()
: Promise.all([
addStyleLink(playerCDN.stylelink),
addScript(playerCDN.script),
])
).catch((err) => {
window.console.log(err);
});
};
const playerCDN = {
stylelink:
'https://g.alicdn.com/de/prismplayer/2.9.3/skins/default/aliplayer-min.css',
script: 'https://g.alicdn.com/de/prismplayer/2.9.3/aliplayer-min.js',
};
function addScript(source: string): Promise<void> {
return new Promise((resolve, reject) => {
const s = document.createElement('script');
s.src = source;
s.setAttribute('charset', 'utf-8');
document.body.appendChild(s);
s.onload = () => {
resolve();
};
s.onerror = () => {
reject();
};
});
}
function addStyleLink(source: string): Promise<void> {
return new Promise((resolve, reject) => {
const l = document.createElement('link');
l.type = 'text/css';
l.rel = 'stylesheet';
l.href = source;
document.head.appendChild(l);
l.onload = () => {
resolve();
};
l.onerror = () => {
reject();
};
});
}
在我们使用到ali-player的页面中,didMount时,动态插入标签:
const [sdkLoaing, { setFalse }] = useBoolean(true);
useEffect(() => {
Promise.all([
loadSDK(),
]).then(setFalse);
}, []);
return sdkLoading ? null : <Ali-player />
问题:android手机的webview中,视频无法唤起全屏、退出全屏。
原因
由于项目是一组H5网页,使用了andriod端的webview容器。唤起全屏(requestFullScreen)需要webview容器支持,要在原生端增加适配代码。同时,点击退出全屏按钮(exitFullScreen )的时候,要通过JS桥接发消息给原生端,由app来控制webivew退出全屏。
解决
android原生端处理,前端只需要监听退出全屏事件,发送消息给原生端,例如 ali-player 中:
let isFullScreen = false;
_player.on('requestFullScreen', (e) => {
if (!isFullScreen) {
isFullScreen = true;
}
});
_player.on('cancelFullScreen', (e) => {
if (isFullScreen) {
isFullScreen = false;
Utils.cancelFullScreen(); // 通过js桥接的通知app端来退出webview的全屏
}
});
问题:ios下,使用轮播组件下的ali-player,视频底部控制条被视频盖住。
原因
轮播组件,例如(antd-mobile-Carousel或者swiper.js),都是使用css动画中的transform属性来实现的,在和播放器底部控制条使用的 position(relative/absolute)混合的话,会导致z-index在某些浏览器下表现奇怪,在safari内核的浏览器中,就会出现被盖住的情况。
解决
给播放器控制条增加样式,使用 transform:translateZ(100px),将控制条也 transform 上来。ali-player中的处理方式:
.prism-controlbar {
// 解决:ios 视频覆盖控制条问题
transform: translateZ(100px);
}
问题:使用轮播组件下的ali-player,拖动视频底部控制条的视频进度条会触发轮播手势。
原因
与轮播组件产生了手势冲突。
解决
由于轮播组件使用的是swiper/react,可以通过noSwiping属性设置不可拖动块,拖动视频控制条的时,禁止轮播手势。同时,在触摸控制条的边缘的时候,也会触发轮播手势,可以通过给控制条新增一个伪元素,来增加操作热区。
// prism-controlbar 是 ali-player 控制条的类名
<Swiper
spaceBetween={20}
slidesPerView={1}
onSwiper={setSwiperController}
onSlideChange={onSlideChange}
pagination={{ clickable: false }}
// noSwiping when operating the controlbar
noSwiping
noSwipingClass="prism-controlbar"
>
{list.map((item, index) => {
return (
<SwiperSlide key={index}>
<Player
id={`player-${item.id || index}`}
ref={(ref) => {
if (ref) playerRefs.current[index] = ref;
}}
config={config}
/>
</div>
</SwiperSlide>
);
})}
</Swiper>
通过伪元素来增大操作热区,覆盖ali-player的样式:
.prism-controlbar {
// 扩大热区
position: relative;
&::after {
position: absolute;
width: 100%;
height: 10px;
left: 0;
top: -10px;
content: '';
background: transparent;
z-index: 9;
pointer-events: none;
}
}
问题:部分视频在safari内核浏览器下加载失败,无法播放。
原因
是和safari浏览器采用的策略有关。safari在请求视频或者这类文件的时,期望用分段的方式来获取视频文件,而不是整个视频文件都请求下来,这种策略是为了节约流量以及提高响应速度。分段请求文件则是通过http的请求头range,以及响应头content-range来实现的。如果服务端不支持处理这两个头,就会导致视频无法播放。
chrome浏览器,兼容性比较好,无论你有没有实现分段请求,都能正确处理。
safari分段请求视频文件的具体流程是这样的:
- 首先会发送第一个请求,其中包含请求头 range:bytes=0-1,期望获取第一个字节的视频流以及整个视频文件的总字节数。这里浏览器其实是为了拿到视频文件的总字节数,来为后面分几段来请求视频做准备。
- 服务端支持分段请求的话,就要解析range请求头,状态码设置为206(表示部分返回),同时返回 content-range: bytes 0-1/6990051,其中6990051是该视频的总字节数,0-1则是此次响应返回第一个字节的文件流,放置到body中返回。
- safari拿到总字节数后,会根据大小再次发送请求,例如range:bytes=0-6990050,获取一段视频后开始播放。
解决
以下是nodejs作为http服务端的处理方式:
// ./app.js
// run: node app.js
// 根目录下,需要 test.mp4 视频文件用于测试。
const { createServer } = require('http');
const fs = require('fs');
// bytes=n-m => [n,m]
function getRange(range, stats) {
const r = range.match(/=(\d+)-(\d+)?/);
const start = r[1];
const end = r[2] || stats.size - 1;
return [parseInt(start), parseInt(end)];
}
createServer((req, res) => {
const { headers } = req;
let { range } = headers; // 获取请求头 range, bytes=n-m,获取n到m个字节的数据
if (typeof range === 'undefined') {
range = 'bytes=0-1'; // 未发送请求头,设置一个默认值
}
if (req.url.includes('/test.mp4')) {
fs.stat('./test.mp4', (err, stats) => {
const [start, end] = getRange(range, stats); // 获取当前片段的范围
res.setHeader('Content-Range', `bytes ${start}-${end}/${stats.size}`); // 响应头 - Content-Range: bytes 0-1/6990051
res.setHeader('Content-Type', 'video/mp4'); // 响应头 - 文件类型
res.setHeader('Content-Length', end == start ? 0 : end - start + 1); // 响应头 - 返回的字节长度
res.writeHead(206); // 206 - Partial Content 部分内容
fs.createReadStream('./test.mp4', { start, end }).pipe(res); // 写入body
});
} else {
res.end();
}
}).listen(3000, () => {
console.log(`server listen on localhost:3000`);
// safari 浏览器访问 localhost:3000/test.mp4 就能看到视频了
});
问题:部分android机型下,video标签无法被其他标签覆盖,始终在最顶层。

原因
和浏览器有关,即便是z-index,也无法改变video的层级。
解决
- 如果是x5内核(微信内置浏览器、华为mate30内置浏览器、手机QQ浏览器等),可以使用x5的同层播放,如:
<video
loop
playsinline="true"
webkit-playsinline="true"
x-webkit-airplay="allow"
airplay="allow"
autoplay
x5-video-player-type="h5"
x5-video-player-fullscreen="false"
x5-video-orientation="portrait"
></video>
- 弹出层单独设计成另一个页面,不覆盖在视频上。

- 弹出层打开的时候,隐藏视频标签(设置width:0,height:0,或者style.left = '9999px')使用广告页覆盖原页面。

ali-player的示例代码:
const meta = {
videoWidth: 0,// 原视频的宽度
videoHeight: 0,// 原视频的高度
};
block: () => {
if (!isAndroid) {
return;
}
// 获取video的dom标签
const i = player.current;
const video = i.tag;
meta.videoWidth = video.offsetWidth;
meta.videoHeight = video.offsetHeight;
// 暂停视频,设置视频的宽度高度为0
i.pause();
i.setPlayerSize(0, 0);
// 新建一个占位符dom,替换原来的位置
const ele = document.createElement('div');
ele.style.width = `${meta.videoWidth}px`;
ele.style.height = `${meta.videoHeight}px`;
ele.setAttribute('id', `block-${id}`);
ele.setAttribute('class', 'visible');
meta.childId = `block-${id}`;
// 如果视频有占位图,设置背景图片
if (config.cover) {
ele.style.backgroundImage = `url( ${config.cover})`;
ele.style.backgroundRepeat = 'no-repeat';
ele.style.backgroundPosition = 'center';
ele.style.backgroundSize = 'cover';
}
// 增加样式,将占位div加到页面上
addClassName(containerRef.current, 'hidden-container');
containerRef.current.appendChild(ele);
},
unblock: () => {
if (!isAndroid) {
return;
}
// 将原视频复原
const i = player.current;
i.setPlayerSize(`${meta.videoWidth}px`, `${meta.videoHeight}px`);
removeClassName(containerRef.current, 'hidden-container');
const child = document.getElementById(meta.childId);
if (child) {
containerRef.current.removeChild(child);
}
}
问题:React单页面应用加载ali-playerSDK的一种方式
原因
使用ali-player,需要加载其js库以及css库,同时ali-player仍不支持npm包的引入方式。所以最简单的方式是在document.ejs(或其他入口html)中标签引入:
但是这样就会引入一个新的问题,由于项目是单页面应用,所有的页面都会主动去请求这两个文件,但是我只是在其中的一个详情页面需要用到ali-player,就造成了一定的资源浪费。所以我期望只在页面进入到详情页面的时候,再去加载ali-player。
解决
思路是,在页面didMount的时候,去动态往页面上插入标签,异步加载完成后,然后初始化播放器:
在我们使用到ali-player的页面中,didMount时,动态插入标签:
问题:android手机的webview中,视频无法唤起全屏、退出全屏。
原因
由于项目是一组H5网页,使用了andriod端的webview容器。唤起全屏(requestFullScreen)需要webview容器支持,要在原生端增加适配代码。同时,点击退出全屏按钮(exitFullScreen )的时候,要通过JS桥接发消息给原生端,由app来控制webivew退出全屏。
解决
android原生端处理,前端只需要监听退出全屏事件,发送消息给原生端,例如 ali-player 中:
问题:ios下,使用轮播组件下的ali-player,视频底部控制条被视频盖住。
原因
轮播组件,例如(antd-mobile-Carousel或者swiper.js),都是使用css动画中的transform属性来实现的,在和播放器底部控制条使用的 position(relative/absolute)混合的话,会导致z-index在某些浏览器下表现奇怪,在safari内核的浏览器中,就会出现被盖住的情况。
解决
给播放器控制条增加样式,使用 transform:translateZ(100px),将控制条也 transform 上来。ali-player中的处理方式:
问题:使用轮播组件下的ali-player,拖动视频底部控制条的视频进度条会触发轮播手势。
原因
与轮播组件产生了手势冲突。
解决
由于轮播组件使用的是swiper/react,可以通过
noSwiping属性设置不可拖动块,拖动视频控制条的时,禁止轮播手势。同时,在触摸控制条的边缘的时候,也会触发轮播手势,可以通过给控制条新增一个伪元素,来增加操作热区。通过伪元素来增大操作热区,覆盖ali-player的样式:
问题:部分视频在safari内核浏览器下加载失败,无法播放。
原因
是和safari浏览器采用的策略有关。safari在请求视频或者这类文件的时,期望用分段的方式来获取视频文件,而不是整个视频文件都请求下来,这种策略是为了节约流量以及提高响应速度。分段请求文件则是通过http的请求头range,以及响应头content-range来实现的。如果服务端不支持处理这两个头,就会导致视频无法播放。
chrome浏览器,兼容性比较好,无论你有没有实现分段请求,都能正确处理。
safari分段请求视频文件的具体流程是这样的:
解决
以下是nodejs作为http服务端的处理方式:
问题:部分android机型下,video标签无法被其他标签覆盖,始终在最顶层。
原因
和浏览器有关,即便是z-index,也无法改变video的层级。
解决
ali-player的示例代码: