基于腾讯云实时音视频(TRTC)的web端 多人人脸识别小游戏
一个双人视频互动的小游戏,连接后,可以实时看到对方的视频情况, 根据对方的视频情况实时进行游戏操作摇头进行控制挡板 不让球掉落。眨眼发球。初期准备基于信息进行游戏的同步。但是没有注意到关于sei的注意事项。因为sei有丢失的可能, 需要去增加发送次数, 然后在接收端去重来保证消息的 确定性。也就导致并不适合,游戏场景的实时数据同步。后面修改方案,通过推流到房主端。房主端进行人脸识别用户操作, 同步
场景介绍
一个双人视频互动的小游戏, 连接后,可以实时看到对方的视频情况, 根据对方的视频情况实时进行游戏操作
摇头进行控制挡板 不让球掉落。 眨眼发球。
准备工作
获取应用 SDKAppID 和 应用秘钥
登录腾讯云 搜索 实时音视频 TRTC
创建应用
在创建的应用上, 点击配置管理
在 应用概览 -》 应用信息 中获取 SDKAppID
在 快速上手 -》 第二步 中获取 应用秘钥
编码
项目设计
- 同步房主和玩家状态, 确定都处于加入房间
- 第一步 先推流 玩家视频
- 第二步 游戏画面 做辅流 和房主画面做主流 推流
- 第三步 识别玩家操作
代码
发送消息
音视频web端没有提供定制消息的功能, 但是提供了 sei message 进行消息发送。 原理是视频帧的头部有一个叫做 SEI 的头部数据块, 我们将数据放在这个地方 自定义消息格式, 便可以进行信息发送。
注意
其中的 sendSEIMessage 接收ArrayBuffer, 而且消息有 丢失问题。
消息格式处理, 将字符串 和 ArrayBuffer 互相转换
const string2arrayBuffer = (string: String) => {
const buf = new ArrayBuffer(string.length * 2);
let uint16 = new Uint16Array(buf);
// 使用charCodeAt字符转为二进制编码
for (var i = 0; i < string.length; i++) {
uint16[i] = string.charCodeAt(i);
}
return uint16;
};
const arrayBuffer2string = (buff: ArrayBuffer) => {
return String.fromCharCode.apply(
null,
new Uint16Array(buff) as unknown as Array<number>
);
};
发送端多次发送, 接收端去重
// 通知
const broadcast(obj: any) {
if (!this.client) return;
try {
const string = generateRoomCode(16) + JSON.stringify(obj);
const fn = (n: number) => {
if (n <= 0) return;
client.sendSEIMessage(string2arrayBuffer(string).buffer, {
seiPayloadType: 5,
});
setTimeout(() => {
fn(n - 1);
}, 1000 / 4);
};
fn(4);
} catch (e) {
console.log(e);
}
}
// 接收消息
let getMsgList = new Set()
const handlerData = (event: any) => {
let data;
try {
const tmpData = arrayBuffer2string(event.data);
const id = tmpData.substring(0, 15);
if (getMsgList.has(id)) return [];
this.getMsgList.add(id);
data = JSON.parse(tmpData.substring(16, tmpData.length));
} catch (e) {
console.log(e);
}
return data;
};
const roomCodeOptions = "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890";
const generateRoomCode = (len = 8) => {
let code = "";
for (let i = 0; i < len; i++) {
const ndx = Math.floor(Math.random() * roomCodeOptions.length);
code += roomCodeOptions[ndx];
}
return code;
};
因为发送 sei message
需要先进行推流, 但是在准备阶段, 直接推送视频在流程上不太友好。 可以自定义流,我这边通过cavans 创建一个空白的流进行推送
const publishBlack = async(userId) => {
if (!client) return;
try {
const isCanvasCapturingSupported = () =>
"captureStream" in HTMLCanvasElement.prototype;
// 检测您当前的浏览器是否支持从 canvas 元素采集 stream
if (!isCanvasCapturingSupported()) throw new Error("浏览器不支持");
const width = 640,
height = 480;
const canvas = Object.assign(document.createElement("canvas"), {
width,
height,
}) as any;
// TRTC 需要检测到画面变化
const timer = setInterval(() => {
canvas.getContext("2d").fillRect(0, 0, width, height);
}, 100);
const stream = canvas.captureStream();
const blackSteam = TRTC.createStream({
userId,
videoSource: stream.getVideoTracks()[0],
});
blackSteam.setVideoProfile("480p");
await blackSteam.initialize();
await client.publish(blackSteam);
} catch (e) {
console.log(e);
}
}
推主流 + 辅流
这个功能是SDK v4.15.0+ 版本的功能, 如果在这个版本之前 需要创建额外的Client来进行处理
推送辅流只需要 publish
加上 isAuxiliary: true
参数就可以。
推送游戏只需要游戏画面,最开始想基于屏幕共享 然后切割画面, 但是发现基于TRTC屏幕共享的类 获取到的track 并没有 cropTo方法。
直接用游戏画面的canvas 自定义流推送
createCustomStream = async (userId) => {
let customStream;
try {
const isVideoCapturingSupported = () =>
"captureStream" in HTMLVideoElement.prototype;
if (!isVideoCapturingSupported()) throw new Error("浏览器不支持");
const isCanvasCapturingSupported = () =>
"captureStream" in HTMLCanvasElement.prototype;
// 检测您当前的浏览器是否支持从 canvas 元素采集 stream
if (!isCanvasCapturingSupported()) throw new Error("浏览器不支持");
const stream = dom.captureStream();
customStream = TRTC.createStream({
userId,
videoSource: stream.getVideoTracks()[0],
});
await customStream.initialize();
await client.publish(customStream, { isAuxiliary: true });
} catch (e) {
console.log(e);
}
return customStream;
}
人脸识别
这边是直接基于开源的模型 https://github.com/tensorflow/tfjs-models/tree/master/face-landmarks-detection
使用模型后,会获得上面图上的坐标。 放大图片可以看清点位坐标。
左右摇头
我们取额头和下巴的坐标, 计算这条直线的角度。
const isSwivel = (face: any) => {
if (!face || !face.keypoints) return 0;
const place1 = face.keypoints[10]; //额头位置
const place2 = face.keypoints[152]; //下巴位置
return (
Math.round(
(Math.atan2(place1.y - place2.y, place1.x - place2.x) * 180) / Math.PI
) + 90
);
};
眨眼
我们取眼睛的上下左右, 计算 眼睛长宽比, 调整合适的参数 进行眨眼判断
const calculateDistance = ({ x: x1, y: y1 }: any, { x: x2, y: y2 }: any) => {
return Math.sqrt(Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2));
};
let maxLeft = 0,
maxRight = 0;
const detectarBlink = (face: any) => {
const keypoints = face.keypoints;
// 左眼上下距离
const leftVertical = calculateDistance(keypoints[386], keypoints[374]);
// 左眼左右距离
const leftHorizontal = calculateDistance(keypoints[263], keypoints[362]);
const eyeLeft = leftVertical / (2 * leftHorizontal);
// 右眼上下距离
const rightVertical = calculateDistance(keypoints[159], keypoints[145]);
// 右眼左右距离
const rightHorizontal = calculateDistance(keypoints[133], keypoints[33]);
const eyeRight = rightVertical / (2 * rightHorizontal);
// TODO 参数需要调整下
const baseCloseEye = 0.095;
const limitOpenEye = 0.14;
if (maxLeft < eyeLeft) maxLeft = eyeLeft;
if (maxRight < eyeRight) maxRight = eyeRight;
let result = false;
if (maxLeft > limitOpenEye && maxRight > limitOpenEye) {
if (eyeLeft < baseCloseEye || eyeRight < baseCloseEye) {
result = true;
}
}
return result;
};
// 摇头判断
const isSwivel = (face: any) => {
if (!face || !face.keypoints) return 0;
const place1 = face.keypoints[10]; //额头位置
const place2 = face.keypoints[152]; //下巴位置
return (
Math.round(
(Math.atan2(place1.y - place2.y, place1.x - place2.x) * 180) / Math.PI
) + 90
);
};
自此场景中的关键点就基本解决了。 完整代码上传到gitee上 https://gitee.com/my_zend/faceball/tree/csdn/
最终效果:
总结
关于实践
初期准备基于 SEI Message
信息进行游戏的同步。但是没有注意到关于sei的注意事项。
因为sei有丢失的可能, 需要去增加发送次数, 然后在接收端去重来保证消息的 确定性。也就导致 SEI Message
并不适合,游戏场景的实时数据同步。
后面修改方案,通过推流到房主端。 房主端进行人脸识别用户操作, 同步到游戏上, 同时双流推送 房主视频和游戏视频到 玩家端。最后虽然场景勉强完成了, 但是因为房主端需要同时处理推双流、识别两个玩家操作,对于房主端的设备性能要求较高。
关于TRTC
虽然场景并不算太成功, 但是腾讯云实时音视频的api还是操作非常方便的, 其中连接处理、事件处理 大大减少了代码量,断开重连等相关功能也使得产品非常容易上手。同时还有很多云上处理, 比如云端混流 就可以缓解上面场景的人脸识别的性能问题。还有很多的功能处理, 来丰富我们的业务需求。
同时文档也是非常丰富, 跟着文档上一步一步操作, 基本不会出现太大问题。 中间遇到过的一些莫名其妙的报错,也都可以在文档上找到, 比如我在 已授权的情况下操作文件共享的时候出现 NotAllowedError: Permission denied by system
报错也能在文档上找到
但是也存在一些问题。 比如文档和代码上的一些不同步,在文档上存在 option, 但是 类型提示上没有
比如文档上没有统一描述roomId:
但是这些也只是一些小问题不没有太大影响。 而且文档也可以操作选中文本 进行文档反馈。 总体还是非常方便的
相关链接
更多推荐
所有评论(0)