视频直播项目

390 阅读16分钟

moment.js

官方文档

问题1:切换国家地区

切换国际环境,需要引入对应的国家文件。它默认自带英文环境。

import moment from 'moment'
import 'moment/locale/zh-cn.js'
moment.locale('zh-cn')

音视频播放

问题1:拉流(音视频的容器格式)

android支持: flv

ios支持:m3u8

问题2:视频播放问题

1、无交互的视频自动播放:需要静音。

2、有交互的但是延时播放的策略:

背景:为了视频达到秒开又不浪费流量,开发会在一个图层里面同时渲染三个直播间(每个直播间带有一个视频播放器),分别为上、中、下直播间,中间的直播间是正常播放的,上和下直播间是暂停的。当用户切换直播间的时候,下一个直播间播放视频,如果是无声音则是能够自动播放的,但是有声音的播放在ios上则不行。通过测试可以知道通过交互,在移动端ios上video.play()方法需要在交互事件的0.9S内触发才行,android则可以更长。

解决方案:通过调研与测试,发现:当通过交互让一个有声音的视频播放后再暂停,后续就可以通过异步的方法成功触发video.play()。参考资料

3、组件重新激活 或者 熄灭屏幕重新唤起 视频都会自动暂停,需要调用video.play()方法让视频继续播放。

sdk接入遇到的问题

火山引擎

问题1:拉流失败没有重新拉取

视频拉流过程中,会出现卡住不再播放问题,需要根据sdk提供的错误码,监听当错误出现时,重新调用sdk方法强制去拉流解决这个问题。

webview问题

H5在webview打开后遇到的一系列问题。

问题1:权限

h5在webview上能请求的权限就两个,相机与麦克风。mdn介绍

// js请求 权限的代码, 权限只能在localhost 或者 域名上才生效,
// ip地址访问会报错
navigator.mediaDevices
  .getUserMedia({ audio: true, video: true })
  .then(function(stream) {
    /* 使用这个 stream stream */
  })
  .catch(function(err) {
    console.error(err)
  })

浏览器上的权限获取流程:h5 -> 浏览器 -> 系统 -> 用户

webview上的权限获取流程:h5 -> webview -> native(app) -> 系统 -> 用户

需要注意的地方则是:h5在webview上获取权限是需要native的研发同学支持的,native的研发同学通过监听拦截到h5的权限请求,再进行赋予操作,否则h5是无法调用权限相关的方法的。

还有一个地方:h5在webview android上是不能一次性请求相机与麦克风权限的,现阶段在webview_flutter android上一次性获取,它那边只能监听拦截到麦克风权限。故此 js 需要分两次去获取,一次视频、一次音频。

问题2:组件input.type='file'

<input type="file" accept="*">

这个控件在浏览器上点击的时候会自动获取照相机和文件读取权限,不需要用户授权,但是在webview上是需要向系统获取照相机权限的,否则只能进行文件读取操作,故此解决方案则是,H5判断环境,如果是webview打开的H5,在触发这个控件前,需要触发照相机权限。

navigator.mediaDevices
  .getUserMedia({ audio: false, video: true })
  .then(function(stream) {
    /* 使用这个 stream stream */
  })
  .catch(function(err) {
    console.error(err)
  })

问题3: google登录问题

webview上不能使用google登录,只能通过桥接方法,让app进行google登录。

优雅的桥接方式

传统的桥接方式:

1h5通过调native注册的方法把数据传递给native
2、native通过调h5在window上注册的方法把数据传给h5

新的桥接方式:

native只注册一个controler,
h5通过postMessage的方式告诉native事件名称、传递的数据与回调函数(需要执行的JS代码),
native通过回调函数把数据传给h5。

将h5请求native端的数据以类似发送请求的形式获取

大概思路:

  1. native与h5约定以postMessage方式桥接,方式名为flutter_methods
  2. 约定传递的JSON数据格式,并要求native端如果有数据需要回传则通过callback回传。
{
    "event_name": "事件名称",
    "data": "事件所传递的数据",
    "callback": "为native需要执行的js代码,通过它将native数据传递给h5"
}
  1. 将桥接方式进行封装并命名为flutter_methods,并返回一个promise,pormise包含了桥接后的数据。
export const flutter_methods = async ({
  event_name,
  data,
}, isNeedDataFromNative) => {
  return await new Promise((resolve, reject) => {
    if (window?.flutter_methods) {
      window?.flutter_methods.postMessage(JSON.stringify({
        event_name,
        data,
        callback: isNeedDataFromNative ? registerCallback(resolve) : ''
      }))
      if (!isNeedDataFromNative) {
        resolve('')
      }
    } else {
      reject(new Error('get data from native error!'))
    }
  })
}
  1. 声明一个函数方法registerCallback(接受一个reslove函数),该函数方法会在window.knCallbacks[callbackId]上声明一个函数方法,该函数方法会接收native传递过来的数据,再通过执行reslove函数将数据传递给封装的桥接方法flutter_methods。
function makeRandomId(func) {
  return `${func.name || 'anonymous'}_${Date.now()}`;
}

export const registerCallback = (resolve, keepAlive) => {
  if (!callback) {
    return null;
  }

  const callbackId = makeRandomId(callback);
  window.knCallbacks[callbackId] = (dataFromNative) => {
    let result;
    if (typeof dataFromNative === 'object') {
      result = dataFromNative;
    } else {
      try {
        result = JSON.parse(dataFromNative);
      } catch (e) {
        result = dataFromNative;
      }
    }
    callback(result);
    if (!keepAlive) {
      delete window.knCallbacks[callbackId];
    }
  };

  return `knCallbacks.${callbackId}`;
}

总代码

// 封装
function makeRandomId(func) {
  return `${func.name || 'anonymous'}_${Date.now()}`;
}

export const registerCallback = (resolve, keepAlive) => {
  if (!callback) {
    return null;
  }

  const callbackId = makeRandomId(callback);
  window.knCallbacks[callbackId] = (dataFromNative) => {
    let result;
    if (typeof dataFromNative === 'object') {
      result = dataFromNative;
    } else {
      try {
        result = JSON.parse(dataFromNative);
      } catch (e) {
        result = dataFromNative;
      }
    }
    resolve(result);
    if (!keepAlive) {
      delete window.knCallbacks[callbackId];
    }
  };

  return `knCallbacks.${callbackId}`;
}

export const flutter_methods = async ({
  event_name,
  data,
}, isNeedDataFromNative) => {
  return await new Promise((resolve, reject) => {
    if (window?.flutter_methods) {
      window?.flutter_methods.postMessage(JSON.stringify({
        event_name,
        data,
        callback: isNeedDataFromNative ? registerCallback(resolve) : ''
      }))
      if (!isNeedDataFromNative) {
        resolve('')
      }
    } else {
      reject(new Error('get data from native error!'))
    }
  })
}

// 业务执行
flutter_methods({
        event_name: 'googleLogin',
      }, true).then((data: FlutterGoogleMsg) => {
        console.log(data)
        if (data.success !== '1') {
          return
        }
        handleLoginByGoogle(data.idToken)
      }).catch((err) => {
        console.error(err)
      }).finally(() => {
        dispatch(setIsShowLoginLoading(false))
      })

白屏问题

背景:webview打开H5的时候,会有一段时间的白屏,而且多页面入口中一个简简单单的落地页资源白屏时间也很长。

调研问题

  1. 用google开发者的Lighthouse报告
  2. 引入webpack-bundle-analyzer分析页面的打包后的情况。

先简单查看一个落地页的情况

优化前

Lighthouse报告情况如下:

image.png

image.png webpack-bundle-analyzer分析情况如下:

image.png image.png

优化过程

1、可以发现有一个文件特别大,@sentry/react的使用,这个库是检测代码质量的,对于一些简单的落地页无需使用。

2、antd-mobile这个库,有几个疑问,我只是简单的使用了其中的两个组件ToastModal,为什么会把整个组件库引入进去?我在测试的过程中,尝试在我的业务代码里面把toast的引用去除,但是发现没有什么用。后面通过从构建webpakc脚本一层一层往上查找,发现了在通用的函数方法里面有个文件引用了antd-mobile,将其注释后成功的把此库的引用去除了。

image.png

现在存在的问题:我只是简单的使用了其中的两个组件ToastModal,为什么会把整个组件库引入进去?

答:通过比对ant-mobile组件的增加与减少,发现项目确实按需引入,有treeShaking,只是这两个组件就是这么大。

3、我的落地页明明没有用到moment,但是却将其打包进去,发现是因为这个框架是多页面入口,会在入口文件将一些通用的方法都注册进reat里面。比如util/indes里面引入了moment,可以将使用到moment的方法抽离到别的文件,或者自定义的文件。

image.png 这里引出思考:如果用的是公司的模板,里面一些定义好的方法,特别是index.js文件,建议不要在它的里面扩充三方库的功能,说不定就被不需要用到的落地页打包进去。

4、当前项目是多页面入口,这时候将所有多页面入口放开,进行打包构建的时候,对单独的一个落地页进行查看的时候,发现每个落地页都有common.js文件,并且特别大(400k+)。通过定位webpack,发现是webpack这个配置的问题splitChunks:

image.png

它的主要功能是将用到两次以上的组件都打包到同一个文件里面,可以有效减少整个项目的体积,避免了同个组件被重复打包进代码里面,但是!它合并的资源会在首屏的时候进行加载,导致白屏时间变长,多页面入口的话,即使项目的一些落地页没有用到common.js,但是却会去加载它,导致一些简单的落地页白屏时间很长。

解决方法: 对于编译多个页面入口,将此功能关闭,对单个页面入口,此功能放开。或者开发模式开启这个功能,生产环境关闭这个功能。

优化后

image.png

image.png

image.png

image.png

总结
  1. 优化项目白屏从哪入手可以用Lighthousewebpack-bundle-analyzer
  2. 对于用公司的项目模板,前人的智慧肯定是有进行优化的,但是你可能会在一些入口文件里面引入了三方库,导致项目打包后变得很大,故此,避免的方法就是,尽量不用在已经存在的文件里面引入三方库,特别是indes.js结尾的文件,尽量自己新建文件。

开始优化主项目

优化前的数据

image.png

image.png

image.png

优化过程
  1. common.js文件很大,这个项目很大,所以common.js文件很大能够理解,但是!它是作为关键资源也就是首屏资源来进行加载的,会大大增加首屏的加载时间,故此,对于主项目,要将commons.js的优化项给注释掉。

image.png

  1. 首屏加载会加载很多非首屏的js组件,通过查看router的配置,发现使用了react-lazy-with-preload对一些一级页面做了preload的处理,现在先暂时去除一部分。

image.png image.png

image.png

3、通过lighthouse发现,vender.js这个资源加载了三方sdkIM,这个im高达138kb,通过定位代码,发现是在index.jsx里面通过hooks引用了这个IM,定位到引用的初始地方,发现是直接通过

import TIM from 'tim-js-sdk'

这种方式引入,考虑用import ('tim-js-sdk')这种异步方式引入。

优化后

image.png

总结
  1. 三方sdk使用考虑使用异步imort的导入方式,避免三方sdk被打包到入口文件里面。
  2. 非首屏组件,react要使用异步导入的方式,两个apilazySuspense
  3. 将多个地方引用的同一个组件抽离出来进行打包可以大大减少项目大小,但是会影响到首屏加载,建议设置当有4个地方以上到这个组件的时候再进行抽离会比较好。
// webpack.config.js
...
{
    optimization: {
        ...
        splitChunks: {
          default: {
            minSize: 0,
            minChunks: 2, // 这个是判断有n个地方引用到了同一个组件,则抽离打包
            priority: -20,
            reuseExistingChunk: true,
            name: 'common',
          },
        }
    }
}
...

总结

  1. 优化项目白屏从哪入手可以用Lighthousewebpack-bundle-analyzer
  2. 对于用公司的项目模板,前人的智慧肯定是有进行优化的,但是你可能会在一些入口文件里面引入了三方库,导致项目打包后变得很大,故此,避免的方法就是,尽量不用在已经存在的文件里面引入三方库,特别是indes.js结尾的文件,尽量自己新建文件。
  3. 三方sdk使用考虑使用异步imort的导入方式,避免三方sdk被打包到入口文件里面。
const getSdk = async () => {
    let sdk = (await import('moment')).default
}
  1. 非首屏组件,react要使用异步导入的方式,两个apilazySuspense
  2. 将多个地方引用的同一个组件抽离出来进行打包可以大大减少项目大小,但是会影响到首屏加载,建议设置当有4个地方以上到这个组件的时候再进行抽离会比较好。
// webpack.config.js
...
{
    optimization: {
        ...
        splitChunks: {
          default: {
            minSize: 0,
            minChunks: 2, // 这个是判断有n个地方引用到了同一个组件,则抽离打包
            priority: -20,
            reuseExistingChunk: true,
            name: 'common',
          },
        }
    }
}
...

google上架问题

挂羊头,卖狗肉。狗肉则是H5。

实现的逻辑是:先上一版正常的APP(卖的是羊头),后上一版非正常的APP(卖狗肉)。后一版APP打开的时候会弹出一个webview隐私页面,这个webview的url会携带上广告渠道的标识(rf,通过google提供的sdk可以获取,进行base64加密)这个参数。rf的可以区分出用户下载app的来源(广告进来后在google商店上的下载或者google商店上搜到下载的)。如果是广告进来的服务则会返回302状态码,并将实际H5链接放在header里面,让浏览器重定向,如果是google商店上搜到下载的则返回正常的隐私页面。

注意核心点:

  1. app的测试不能有google服务(华为手机)。
  2. app不能用if else 等逻辑判断。
  3. app的桥接使用flutter的插件库实现。
  4. app的google登录使用flutter的插件库实现。
  5. h5所使用到的权限,APP都需要有对应的功能。
  6. app用flutter开发是因为google的审核对flutter没有那么智能,不会因为调用栈问题直接锁死,导致代码也上不了。

内存泄漏问题

如何通过goole浏览器判断是否内存泄漏

打开goole浏览器,进入开发者模式,点击内存tab。

操作如下 image.png

堆快照: 可以记录此时的内存使用情况

时间轴上的分配插桩: 可以记录一整段短时间内的内存使用情况

分配采样: 可以记录一整段长时间内的内存使用情况

如果发现内存一直在上涨,那么就是内存泄漏了!

能够让内存进行回收的原理

根据《node.js深入浅出》可知到,v8引擎在64位的cpu上最大的内存为1.5GB,在32位的CPU上是0.75GB。

能够使内存进行回收的条件只有一个:引用变量的作用域消失。

作用域分为两种:全局作用域函数作用域

全局作用域的变量是不会被垃圾回收的,函数作用域的变量则会。

函数里所使用的变量,从变量找到变量声明的地方,此过程为函数作用域链,这是声明阶段就确认的,不是执行阶段!所以要记住,闭包的变量查找过程。

闭包导致的内存泄漏

有人说,使用闭包,容易出现内存泄漏,那么怎么样的使用才会导致内存泄漏呢,很简单。

闭包的引用闭包函数的变量所在的作用域为全局作用域则会导致内存泄漏!

<body>
    <button onclick="addBibaoMemory()">点击给闭包添加内存</button>
    <button onclick="addBibaoMemory2()">点击给闭包添加内存</button>
    <button onclick="deleteVarUseBibao()">删除引用闭包的变量</button>
</body>
<script>
    function fatherOfBibao() {
        let list = []
        return () => {
            alert(list.length)
            list.push(new Array(1000000).fill(1))
        }
    }
    let addBibaoMemory = fatherOfBibao() // 每次返回的函数引用地址是不一样的,这就意味着这个闭包变量只有这个函数能用
    let addBibaoMemory2 = fatherOfBibao() // 每次返回的函数引用地址是不一样的,这就意味着这个闭包变量只有这个函数能用

    function deleteVarUseBibao() {
        console.log('删除')
        addBibaoMemory = undefined // 删除了addBibaoMemory闭包所产生的内存,但是addBibaoMemory2的所产生的内存还在
    }
</script>

addBibaoMemory这个变量所在的作用域就是全局作用域了。

通过快照功能可以发现有内存泄漏,触发垃圾回收的机制也简单,就是将引用变量重新设置为undefined或者null

注意:开发者模式下使用console.log会一直占用内存,即使作用域上已经消失,但是关闭开发者模式就好了。

image.png

通过浅层大小 -> 构造函数 -> array -> 点击进去,一步步追踪可以找到对应的增加所使用的函数与变量名!

通过blob进行图片上传

通过裁剪工具裁剪完成后获取到的blob是一个地址,则时候如果要获取blob所对应的file,需要走本地接口

// 举例:blob:http://192.168.28.38:8000/202f8d0a-af47-41de-adce-36e2b878b059
export const getFileFromBlob = (
  file,
  type = 'image/jpeg',
  quality = 0.5,
) => {
  const fileName = moment().unix() + '.jpeg'
  return new Promise((resolve, reject) => {
    fetch(file)
      .then(response => response.blob())
      .then(blobData => {
      // 创建一个 File 对象
        const file = new File([blobData], fileName, { type });

        // 现在,'file' 就是表示文件的 File 对象
        resolve(file);
      })
      .catch(error => reject(error));
  })
}

// 这时候获取到file 上传文件的话,需要上传二进制数据,这时候file转成二进制数据再上传接口
const fileReader = new FileReader()
fileReader.readAsArrayBuffer(currentFile)
fileReader.onloadend = async function () {
  const binaryData = fileReader.result
  setIsLoading(true)
  try {
    let { data } = await Apis.baseUploadImage(binaryData)
    dispatch(
      updateUserInfo({
        avatar: data?.url,
      }),
    )
    setData(val => ({
      ...val,
      avatar: data?.url,
    }))
  } catch (error) {
    console.log('error', error)
  }
  setIsLoading(false)
}

android设备上进行swiper切换问题

背景:swiper中的swiper-slide滑动切换的时候,如果滑动的区域刚好有滚动元素,则会出现滑动异常问题,如下:

解决方法: 给滚动元素绑定touchuStart、touchMove、touchEnd事件,touchuStart时禁止swiper滚动,并记录滑动的x位置,touchMove时获取此时的x与记录的x之间的差值,如大于一定数值,则调用方法滚动swiper-slide。

引用高斯模糊问题

背景:游戏卡片中引用了模糊的css样式,当加载的游戏卡片很多的时候,就会爆卡。

原因:css中的filter:blur这个属性很吃gpu性能。

解决方法:

  1. 不用这个功能
  2. 使用虚拟滚动react-virtualized

弹框后阻止后面的元素滚动

背景

h5项目在移动端上如果弹窗后,滑动弹窗后,弹窗后面的元素也会滚动。

解决方法1

当弹窗出现的时候,给body设置事件,禁止默认事件的触发,弹窗消失则消除事件。react代码如下:

  useEffect(() => {
    const handleMove = e => {
      e.preventDefault()
    }
    document.body.addEventListener('touchmove', handleMove, {
      passive: false,  // 一定要设置passive: false
    })
    return () => {
      document.body.removeEventListener('touchmove', handleMove)
    }
  }, [])

缺陷:如果弹窗内容有滚动元素,会导致无法滚动。

解决方法2

当弹窗出现的时候,给body设置style,height = '100vh'、overflow = 'hidden',弹窗消失则恢复原样。react代码如下:

  useEffect(() => {
    const style = document.body.getAttribute('style') || ''
    document.body.style.height = '100vh'
    document.body.style.overflow = 'hidden'
    return () => {
      document.body.setAttribute('style', style)
    }
  }, [])

缺陷:ios设备无效。

解决方法3

弹窗元素与弹窗后面的元素在html的结构上是同一层级。

缺陷:移动端浏览器向下滚动元素的时候,无法触发浏览器自动关闭导航的功能,触发浏览器自动关闭导航的关键是body元素的高度超过浏览器的高度