Skip to content

[2021-06-16]: 移动端video标签以及ali-player遇到的问题 #19

@Lemonreds

Description

@Lemonreds

问题: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分段请求视频文件的具体流程是这样的:

  1. 首先会发送第一个请求,其中包含请求头 range:bytes=0-1,期望获取第一个字节的视频流以及整个视频文件的总字节数。这里浏览器其实是为了拿到视频文件的总字节数,来为后面分几段来请求视频做准备。
  2. 服务端支持分段请求的话,就要解析range请求头,状态码设置为206(表示部分返回),同时返回 content-range: bytes 0-1/6990051,其中6990051是该视频的总字节数,0-1则是此次响应返回第一个字节的文件流,放置到body中返回。
  3. 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标签无法被其他标签覆盖,始终在最顶层。

imag.png

原因

和浏览器有关,即便是z-index,也无法改变video的层级。

解决

  1. 如果是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>
  1. 弹出层单独设计成另一个页面,不覆盖在视频上。

截屏2021-06-16上午9.55.23.png

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

截屏2021-06-16上午9.58.27.png
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);
        }
     }

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions