视频直播项目

moment.js

官方文档

问题1:切换国家地区

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

javascript 复制代码
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 复制代码
// 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'

html 复制代码
<input type="file" accept="*">

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

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

问题3: google登录问题

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

优雅的桥接方式

传统的桥接方式:

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

新的桥接方式:

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

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

大概思路:

  1. native与h5约定以postMessage方式桥接,方式名为flutter_methods
  2. 约定传递的JSON数据格式,并要求native端如果有数据需要回传则通过callback回传。
json 复制代码
{
    "event_name": "事件名称",
    "data": "事件所传递的数据",
    "callback": "为native需要执行的js代码,通过它将native数据传递给h5"
}
  1. 将桥接方式进行封装并命名为flutter_methods,并返回一个promise,pormise包含了桥接后的数据。
js 复制代码
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.knCallbackscallbackId上声明一个函数方法,该函数方法会接收native传递过来的数据,再通过执行reslove函数将数据传递给封装的桥接方法flutter_methods。
js 复制代码
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}`;
}

总代码

js 复制代码
// 封装
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. 各种页面与组件的懒加载、预加载
  2. 图片cdn缓存
  3. 骨架屏(loading图)

其中最明显的优化效果就是骨架屏了,在index.html上写入一个loading的div元素(注意不要写在id=root的div上),然后在框架的根组件上监听生命周期,待监听到根组件渲染完成后,隐藏这个loading元素。

google上架问题

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

实现的逻辑是:先上一版正常的APP(卖的是羊头),后上一版非正常的APP(卖狗肉)。后一版APP打开的时候会弹出一个webview,这个webview的url是服务返回的。

app发请求的时候会将广告渠道的标识通过两次BASE64加密后传输。如果是非广告进来(google审核人员)所下载的APP,则服务会根据标识返回隐私页的HTML内容。如果是广告渠道(目标用户)所下载的APP,服务则会返回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没有那么智能,不会因为调用栈问题直接锁死,导致代码也上不了。

内存泄漏问题

根据浏览器的垃圾回收原理:当作用域消失后,其作用域所使用的内存将会被回收。

起因是在flutter里面,webview所打开的h5在观看视频的过程中,超过了十几分钟就崩溃了。一开始是想用传统的方法用google浏览器提供的 google浏览器-更多工具-开发者工具-内存进行记录的,但是它录制不了很长的时间,超过五分钟就不行,如果用五分钟去录制又判断不出究竟有没有内存泄漏。后面换了一种方法 google浏览器-更多工具-任务管理器

看了十几分钟,发现在直播间里面,它的内存占用空间与JS使用的内存会一直上升,从而确定自己的H5代码出现内存泄漏了。(注意:用这个看的时候最好不要打开google的开发者工具,它会导致任务管理器提供的信息很杂)

测试代码

通过blob进行图片上传

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

js 复制代码
// 举例: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。

相关推荐
weixin_382395232 小时前
为小工厂量身打造:本地部署的物料管理系统带缺料计算
前端·制造
__zRainy__3 小时前
解决pnpm v10+不自动构建
前端·pnpm·工程化
猫猫不是喵喵.3 小时前
Vue3 Props 属性
前端·javascript·vue.js
醉城夜风~4 小时前
CSS元素显示模式(display)
前端·css
AI大模型-小华5 小时前
Codex 三方充值快速入门指南
java·前端·数据库·chatgpt·ai编程·codex·chatgpt pro
做前端的娜娜子7 小时前
同一链接实现 PC Web 与移动 H5 自适应
前端·掘金·金石计划
小帅不太帅7 小时前
架构没变、规模没变,DeepSeek V4 Flash 正式版凭什么暴涨 47 分?
前端·aigc·deepseek
jarvisuni8 小时前
DeepSeekFlash前端依旧拉垮,而且变慢了很多!
前端·javascript·算法
卷福同学9 小时前
AI编程出海第二步:验证关键词能否做站
前端·人工智能·后端