Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
Lemonreds opened this issue Jun 16, 2021 · 0 comments
Open
Labels

Comments

@Lemonreds
Copy link
Owner

问题: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);
        }
     }

@Lemonreds Lemonreds added the H5 label Jun 28, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant