【数字人】项目总结

216 阅读8分钟

最近数字人很火,公司是做保险行业有关的,最近一直在抢占市场做数字人相关的项目,vue3开发的h5页面,内嵌小程序使用。本人主要负责前端开发,记录一下开发过程中所遇问题和知识点。

1. canvas绘制图片问题

1.1 canvas转base64图片时透明区域变成黑色背景

描述:canvas绘制带透明背景的图片和文字后,转成base64图片,透明背景变成黑色。

image.png

原因分析:背景图是png格式,toDataURL(type, quality)转换的时候转成jpeg格式的了,应该统一png:

cannvas.toDataURL("image/png");

1.2 *canvas清除画布内容后重新绘制透明底图片,边边会带阴影

image.png

问题定位发现第一次不会,clearRect()清除后重新绘制就会有,一开始以为逻辑问题:画布没清干净图片绘制了两次的原因,实际并未如此,猜想是不是文字影响,注释后发现果然如此!应该是画布内容清除了,但是文字携带的阴影还是存在没清除干净,重绘时清除画布的同时顺便清除阴影就可以了!

clearCanvas(){
        this.ctx.clearRect(0, 0, this.$canvas.width,  this.$canvas.height)
        this.ctx.shadowColor = 'rgba(0, 0, 0, 0)';
        this.ctx.shadowOffsetX = 0;
        this.ctx.shadowOffsetY = 0;
        this.ctx.shadowBlur = 0;
}

1.3 小程序回退内嵌h5有绘制canvas页面出现白屏

一进页面会调用接口获取详情页信息,拿到返回的图片再进行canvas绘制,直接写在<setup>里面调用,相当于是在created生命周期中使用。

但是:canvas应该在mounted的生命周期中初始化,在created和updated中都是无效的。

所以应该写在onMounted()生命周期中,或加个定时器。

<setup>
const getDetail = async () => {
   let res = await getTmplDetailApi({ id: tmpId });
   ...
   drawCanvas();
}
getDetail();
</setup>

2. 关于上传的问题

2.1 使用axios上传base64编码图片到七牛云,指定目录且带后缀

原代码返回的路径在根目录下且没带格式后缀,代码如下:

image.png

可在url后面设置key的值来指定存放目录和文件后缀。官网

image.png

canvas绘制后生成的图片base64 url为data:image/jpeg;base64,/9j/4AAQSkZJRg...,修改后的代码:

import { Base64 } from 'js-base64';
const qiniuImageHead = "https://static.xxxxx.com";
export const uploadBase64ToQiniu = async(file) => {
    if(!file) {
      toast("未获取到资源文件");
      return
    }
    try {
        const token = await getQiniuToken();
        //  解析 Data URI,获取文件信息
        const dataUriParts = file.split(';');
        const mimeType = dataUriParts[0].split(':')[1]; // 获取 MIME 类型
        const base64Data = dataUriParts[1].replace('base64,', ''); // 获取 Base64 编码的图像数据
        // 生成一个唯一的文件名,例如使用时间戳
        const timestamp = new Date().getTime();
        const userInfo = await getUserInfo();
        let userId = userInfo.user_id || 0;
        const fileName = `/digitalman/image_${userId}_${timestamp}.${mimeType.split('/')[1]}`;
         // 安全的 base64编码
        const destination_file_name = Base64.encode(fileName).replace('+', '-').replace('/', '_');
        // 输出文件信息
        // console.log('文件名:', fileName);
        // console.log('文件类型:', mimeType);
        let url = 'https://upload.qiniup.com/putb64/-1/key/' + destination_file_name;
        const qiniuRes = await ajax.post(url, base64Data, {
            headers: {
                "Content-Type": "application/octet-stream",
                "Authorization": "UpToken "+token,
                "Accept": "*/*",
            },
            processData: false
        });
        let theLongPicurl = qiniuImageHead + "/" + qiniuRes.data.key;
        console.log('theLongPicurl: ',theLongPicurl);
        return qiniuImageHead + "/" + qiniuRes.data.key;
    }catch(err) {
        toastError(err);
    }
}

2.2 使用vant4的Uploader组件上传图片后,获取图片的尺寸宽高

/*
** 通过上传的图片file对象,获取图片的尺寸(宽 * 高)
*/
export const getImageSize = (file) => {
    return new Promise((resolve, reject)=> {
        try{
            var arr = file.content.split(','), mime = arr[0].match(/:(.*?);/)[1],
            bstr = atob(arr[1]), n = bstr.length, u8arr = new Uint8Array(n);
            while (n--) {
                u8arr[n] = bstr.charCodeAt(n);
            }
            var f = new Blob([u8arr], { type: mime });
            
            var reader = new FileReader();  
            reader.onload = function (e) {  
                var data = e.target.result;  
                //加载图片获取图片真实宽度和高度  
                var image = new Image();  
                image.onload=function(){  
                    var width = image.width;  
                    var height = image.height;   
                    resolve({ width, height })
                };  
                image.src= data;  
            };  
            reader.readAsDataURL(f);  
        }catch(err) {
            reject(err)
        }
    })
}

2.3 *微信浏览器h5 ios监听loadedmetadata获取视频时长不成功没反应

使用loadedmetadata,当指定的音频/视频的元数据已加载时,会发生loadedmetadata事件

由于ios对audio和video的一系列限制,包括是提前load和自动播放等。所以只有当audio的play触发事件,才可以监听到duration,所以我们需默认触发play事件,播放音频。ios播放时默认会打开全屏,可以设置 playsinline 属性禁止全屏播放。

export const getVideoWidthHeight = (file) => {
    return new Promise((resolve, reject)=> {
        try{
            // 创建一个video元素
            const video = document.createElement('video');
            
            videoElement.setAttribute("src", file.objectUrl);
            // 设置 playsinline 属性
            videoElement.setAttribute("playsinline", "true");
            videoElement.setAttribute("webkit-playsinline", "true");

            // 在视频加载完成后获取视频的宽度和高度
            video.addEventListener('loadedmetadata', function() {
                const width = video.videoWidth;
                const height = video.videoHeight;
                const duration = video.duration;
                resolve({ width, height, duration })
                console.log('视频宽度:', width);
                console.log('视频高度:', height);
                console.log("时长:", duration)
            });
             //   console.log("ios----",isIos())
            if (isIos()) {
              showDialog({
                title: "读取成功,开始上传",
                confirmButtonText: "确定",
                showConfirmButton: true,
              })
                .then(() => {
                  console.log("play1");
                  videoElement.play().then(() => videoElement.pause());
                })
                .catch(() => {
                  console.log("cancle");
                });

            }

        }catch(err) {
            reject(err)
        }
    })
}

3. 运行问题

3.1 (mac)运行vscode脚本node版本不一致

使用nvm来管理node版本,默认版本号14.15.0,因为有用到pnmp包管理器,需node版本号在16.14以上,但明明已经切换到18.15.0了,运行脚本还是提示在14.15.0,且总是变为14.15.0,运行报错提示版本低。

image.png

image.png

解决方法:使用命令行nvm alias default <version>将默认版本改成18.15.0

3.2 node版本过低报错,运行项目报错

image.png

原因:Vite 需要 Node.js 版本 14.18+,16+。且有些模板需要依赖更高的 Node 版本才能正常运行。

3.3 报错TypeError: Assignment to constant variable.

image.png

原因:使用const定义的常量被改动,将const改成let

3.4 按需引入使用Toast组件,引用 showToast 时出现编译报错的解决方案

image.png

解决办法:参考

4. 关于小程序

4.1 小程序内嵌h5使用wx.miniProgram.postMessage发送信息获取失败

需求:H5内嵌小程序实现视频保存到相册

// =====H5代码====
const testSave = () => {
    wx.miniProgram.postMessage({type:'saveVideoRequest', videoUrl:'https://duobao-1256871399.cos.ap-guangzhou.myqcloud.com/15-202306/168767589938893.mp4'})
}
//====小程序代码====
<web-view bindmessage="bindGetMsg"></web-view>


// 获取h5传参
bindGetMsg: function (e) {
    let articleData;
    console.log("李哈哈---",e)
    if (e.detail.data && e.detail.data.length > 0) {
      articleData = e.detail.data.pop();
    }
   if(articleData.type == "saveVideoRequest"){
      // 视频下载
      if (articleData.videoUrl) {
        let self = this;
        wx.downloadFile({
          url: articleData.videoUrl,
          success(res) {
            wx.hideLoading();
            // console.log(res);
            if (res.statusCode === 200) {
              let path = res.tempFilePath;
              wx.saveVideoToPhotosAlbum({
                filePath: path,
                success(res) {
                  self.setData({
                    showSucess: true
                  });
                },
                fail(res) {
                  util.toast('save视频保存失败');
                }
              })
            }
          },
          fail(res) {
            wx.hideLoading();
            console.log('视频保存失败', res);
            util.toast('视频保存失败');
          }
        })
      }
}

解决办法:传递过去的数据必须写在data对象里面,修改后可以获取成功,但是仍然报错{errMsg: "invokeMiniProgramAPI"},且小程序那边没反应。

分析原因是网页向小程序postMessage时,只有在特定时机触发并接收到消息。

image.png

image.png

最终修改代码如下,最终成功将视频保存到手机相册。

// =====H5代码====
const testSave = () => {
    wx.miniProgram.navigateBack({delta: 1}); //注:必须在前,否则触发不了接收不到信息
    wx.miniProgram.postMessage({
        data: {type:'saveVideoRequest', videoUrl:'https://duobao-1256871399.cos.ap-guangzhou.myqcloud.com/15-202306/168767589938893.mp4'}
    })
}

4.2 移动端页面可以手动放大

<!-- <meta name="viewport" content="width=device-width, initial-scale=1.0" 修改如下:/> -->
<meta name="viewport"
    content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=0,viewport-fit=cover" />

4.3 微信小程序开发AudioContext与InnerAudioContext的区别

image.png

微信官方文档:

音频。1.6.0版本开始,该组件不再维护。建议使用能力更强的 wx.createInnerAudioContext 接口

AudioContext 通过 id 跟一个 audio 组件绑定,操作对应的 audio 组件。

所以说你不能用audio了,而audiocontext需要和audio绑定的,你都没有audio了,那也就不能用AudioContext了。

4.4 小程序打开授权页没看到麦克风授权

image.png

解决办法:在小程序中使用敏感权限(如麦克风、摄像头等)时.json进行相应的配置

"permission": {
    "scope.record": {
      "desc": "你的录音功能将用于小程序语音输入"
    }
  },

5. 数据处理

5.1 *签名验签(通信安全问题)

数字签名:在客户端和服务端的通信过程中,会遇到很多的安全问题,无法确认收到的是不是被修改过,这个时候数字签名就发挥作用了。

验签:顾名思义,就是一个对数字进行验证的操作。

步骤:

  1. 运营需求:动态生成小程序二维码,扫码进入到h5页面弹出免费领取弹窗,以生成的时间为准,超过24小时二维码失效。
  2. 思路:在跳转链接后面加个参数(生成二维码的时间戳),页面获取参数判断是否已过24小时。问题是如果有人直接修改时间戳也不能判断。于是对参数进行数字签名+验签。
  3. 处理:传2个参数:时间戳 + 验证码(定义一个只有自己知道的固定key字符串,将时间戳和key连接的字符串进行MD5加密得到一长字符串,截取10位作为验证码参数)。页面获取参数,将获取到参数时间戳和key连接进行MD5加密并截取10位,与获取到的验证码参数进行比较,就可判断链接参数有没有被修改过。

代码如下:

生成二维码页参数设置:

let createdAt = moment().unix();
let key = "realmanlmx";
let signStr = MD5(createdAt + key);
createdAt = createdAt + 'sign' + signStr.substring(0, 10);

获取的链接:http://localhost:3000/digital/invite?ctime=1704367048signb89c0c4666

弹窗也获取参数进行验证:

const isExpired = ref(true); //小程序码是否过期(超24h)
const checkQrcodeAuth = () => {
  let createAt = route.query.ctime || ""; //小程序码创建的时间戳(秒)
  if(!createAt) {
    pageStart();
    return
  }
  let signArr = createAt ? createAt.split("sign") : ""; //['时间戳', '验证码']
  let key = "realmanlmx";
  let signStr = MD5(signArr[0] + key); //校验码
  if(signStr.substring(0, 10)== signArr[1]){
    if(parseInt(nowTimestamp)< parseInt(signArr[0]) + 24*3600){
      isExpired.value = false;
    }
  }
}