如何使用 Amazon Interactive Video Service 和 VRoid 构建 VTubing 应用程序
支持亚马逊云科技免费套餐
您是否曾想过通过虚拟角色的形象以更有创意的方式表达自己?想象一下,如果您能创建或召唤一个您喜爱的虚拟角色形象,让它在镜头前模仿您的肢体动作,并向成千上万的观众直播。在本教程中,您将学习如何通过创建自己的 VTubing 应用程序来实现这一目标。
虚拟 YouTubing,简称 V-Tubing,是指直播者使用虚拟角色形象,通过面部摄像头以外的方式展示自己的品牌、个性或身份。
在这一背景下,我将逐步指导您构建一个 Web 应用程序,从 VRoid Hub 渲染一个 3D 角色,根据您的肢体动作为其制作动画,并通过 Amazon Interactive Video Service (IVS) 进行直播。VRoid Hub 是 Pixiv 创建的一个在线平台,用于托管和共享 3D 角色模型。您将使用我们创建并上传到 VRoid Hub 的模型作为您的角色形象。Pixiv 提供的 Three VRM SDK 将用于渲染数字角色。VRM 是一种用于处理 3D 角色模型的文件格式。为了让您的角色形象模仿您的肢体动作并为其制作动画,您将使用 MediaPipe Holistic 库和 Kalidokit。
学习内容
- 如何渲染 3D 虚拟角色
- 如何用自己的肢体动作为 3D 虚拟角色制作动画
- 如何将 3D 虚拟角色通过 Amazon IVS 进行直播
解决方案概述
本教程由 5 个部分组成:
- 第 1 部分 - 下载您的 3D 角色
- 第 2 部分 - 设置 HTML 显示摄像机画面和直播控件
- 第 3 部分 - 使用 Three VRM SDK 渲染虚拟角色
- 第 4 部分 - 用您的肢体动作为虚拟角色制作动画
- 第 5 部分 - 将您的虚拟角色通过 Amazon IVS 进行直播
方便起见,本教程将只关注加载虚拟角色形象、为其制作动画和通过虚拟角色形象直播所需的关键步骤。完整的代码示例可在 Github 上获取。
第 1 部分 - 下载您的 3D 角色
在本教程中,我们使用 VRoid Studio 制作了自己的 3D 角色。VRoid Studio 是一款 3D 角色创建工具,可让您在本地导出 VRM 文件,或将其上传到 VRoid Hub 与公众共享。3D 角色创建完成后,我们将其保存为 VRM 格式。您可以从此处或者 VRoid Hub 获取用于此教程。VRM 是一种用于处理 3D 角色模型的文件格式。
您可以选择集成 VRoid Hub API,通过编写代码从 VRoid Hub 下载和使用其他 3D 角色。
第 2 部分 - 设置 HTML 显示摄像机画面和直播控件
在 index.html 中创建以下 HTML。在 <body> 元素中,我们首先添加用于显示前置摄像机画面的 <video> 元素。这将有助于观察我们的角色形象模仿我们自己的动作的效果。此外,还可以添加按钮,用于加入 Amazon IVS 中的 Stage(舞台)。Stage 是参与者交换音频和/或视频的虚拟空间。加入 Stage 后,我们就可以向 Stage 中的观众或其他参与者直播我们的角色形象。我们还将添加一个包含添加参与者令牌表单的模态框。参与者令牌可以被视为加入 Stage 所需的密码。它还可以用于向 Amazon IVS 告知某人想要加入哪个 Stage。本教程稍后将介绍如何创建 Stage 和参与者令牌。在 <head> 标签中,我们添加了一些 CSS 样式文件,您可以在 GitHub 存储库上找到这些文件。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Simple VTuber Demo for Amazon IVS</title>
<meta name="description" content="Simple VTuber Demo for Amazon IVS" />
<link rel="stylesheet" href="style.css" />
<link rel="stylesheet" href="modal.css" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.css" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/milligram/1.4.1/milligram.css" />
</head>
<body>
<div class="preview">
<video class="input_video" width="1280px" height="720px" autoplay muted playsinline></video>
</div>
<nav>
<button id="settings-btn" class="button-open"" class="button button-outline">Settings</button>
<button id="join-stage-btn" class="button button-outline">Join stage</button>
</nav>
<section class="modal hidden">
<div>
<h3>Settings</h3>
</div>
<input type="text" id="participant-token" placeholder="Enter a participant token" />
<button class="btn" id="submit-btn">Submit</button>
</section>
<div class="overlay hidden"></div>
</body>
</html>
第 3 部分 - 使用 Three VRM SDK 渲染虚拟角色
下一步是将 VRM 文件中的数字角色渲染到画布上。要将 VRM 文件呈现到画布上,我们将使用 Three-VRM 库及其依赖库,包括 Three.js 和 GLTFLoader。Three.js 是一个流行的 JavaScript 3D 库。GLFTLoader 是 Three.js 中的一个组件,用于加载 glTF(GL 传输格式)格式的 3D 模型,这是一种用于三维场景和模型的标准文件格式。我们还将使用一个名为 OrbitControls 的 Three.js 附加组件,以便在旋转角色形象时旋转形象视图。在 < head > 元素中添加以下 < script > 元素,以使用这些库。
<script src="https://unpkg.com/three@0.133.0/build/three.js"></script>
<script src="https://unpkg.com/three@0.133.0/examples/js/loaders/GLTFLoader.js"></script>
<script src="https://unpkg.com/@pixiv/three-vrm@0.6.7/lib/three-vrm.js"></script>
<script src="https://unpkg.com/three@0.133.0/examples/js/controls/OrbitControls.js"></script>
导入这些库后,我们创建一个 JavaScript 文件 app.js,以编写使用这些库和本教程其余部分的代码。将该文件导入结尾的 </body> 标记之前,如下所示。
<script src="app.js"></script>
</body>
接下来,在 app.js 中初始化一个 WebGLRenderer 实例,我们将使用该实例在 HTML 中动态添加一个 <canvas> 元素。该画布元素将用于渲染我们的角色形象。currrentVrm 变量稍后将在我们制作角色形象动画时使用。
let currentVrm;
const renderer = new THREE.WebGLRenderer({ alpha: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
document.body.appendChild(renderer.domElement);
接下来,创建一个 PerspectiveCamera 实例,定义在屏幕上能看到角色形象的多大部分(以度为单位)、角色形象的宽高比,以及如果将角色形象移到离摄像机更远的地方,在屏幕上能看到角色形象的多大部分。我们还创建了 OrbitControls 的实例,这样,我们只需点击和拖动角色形象即可旋转角色形象的视图。
const orbitCamera = new THREE.PerspectiveCamera(
35,
window.innerWidth / window.innerHeight,
0.1,
1000
);
orbitCamera.position.set(0.0, 1.4, 0.7);
const orbitControls = new THREE.OrbitControls(orbitCamera, renderer.domElement);
orbitControls.screenSpacePanning = true;
orbitControls.target.set(0.0, 1.4, 0.0);
orbitControls.update();
接下来,我们使用 Three.js 库加载一个场景实例。场景就像一个虚拟舞台,我们的角色形象将放置在这里进行渲染。我们还将创建一个 DirectionalLight 实例,为场景添加一些光线。最后,创建一个 Three.Clock 实例,以便稍后使用它来管理和同步角色形象的动画。
const scene = new THREE.Scene();
const light = new THREE.DirectionalLight(0xffffff);
light.position.set(1.0, 1.0, 1.0).normalize();
scene.add(light);
const clock = new THREE.Clock();
接下来,我们使用之前下载的 VRM 文件将数字角色加载到场景中。在以下代码片段中,我们使用 Three-VRM 库及 Three.js 中的 GLTFLoader 来实现这一目的。
// Load our 3D character from a VRM file via a Cloudfront distribution
const loader = new THREE.GLTFLoader();
loader.crossOrigin = "anonymous";
loader.load(
// Replace this with the URL to your own VRM file
"https://d1l5n2avb89axj.cloudfront.net/avatar-first.vrm",
(gltf) => {
THREE.VRMUtils.removeUnnecessaryJoints(gltf.scene);
THREE.VRM.from(gltf).then((vrm) => {
scene.add(vrm.scene);
currentVrm = vrm;
currentVrm.scene.rotation.y = Math.PI;
});
},
(progress) =>
console.log(
"Loading model...",
100.0 * (progress.loaded / progress.total),
"%"
),
(error) => console.error(error)
);
第 4 部分 - 用您的肢体动作为虚拟角色制作动画
要开始制作虚拟角色动画,请将用于 Kalidokit 库、MediaPipe Holistic 库以及来自 MediaPipe 的摄像机实用工具模块的 < script > 元素添加到 index.html 中的 < head > 元素内。MediaPipe Holistic 是一种计算机视觉工具,用于跟踪用户的肢体动作、面部表情和手势。这对于制作数字角色形象动画以模仿自己的动作非常有用。Kalidokit 包括使用 Blendshape 制作面部动画和使用运动学求解器制作肢体动作,以创建更逼真的数字角色形象。Blendshape 是角色动画中的一种技术,用于制作各种面部动画。运动学求解器是一种算法,用于计算角色形象肢体的位置和方向。在制作角色形象动画(又称角色绑定)时,运动学求解器有助于确定角色的关节和骨骼应如何移动,以实现所需的姿势或动画效果。简而言之,MediaPipe Holistic 会跟踪您的身体动作,而 Kalidokit 则会将这些动作作为输入,为您的角色形象制作动画。MediaPipe 的摄像头实用工具模块将简化向 MediaPipe Holistic 提供前置摄像机输入的过程。MediaPipe Holistic 需要此摄像机输入来进行手部、面部和身体动作跟踪。
<script
src="https://cdn.jsdelivr.net/npm/@mediapipe/holistic@0.5.1635989137/holistic.js"
crossorigin="anonymous"
></script>
<script src="https://cdn.jsdelivr.net/npm/kalidokit@1.1/dist/kalidokit.umd.js"></script>
<script
src="https://cdn.jsdelivr.net/npm/@mediapipe/camera_utils/camera_utils.js"
crossorigin="anonymous"
></script>
下面,我们在 app.js 中定义一个 animate 函数来初始化我们的动画。在该函数中,我们调用 Kalidokit 库提供的 requestAnimationFrame。该函数用于根据浏览器的刷新率同步更新和渲染我们角色形象的动画。它能确保流畅地跟踪和应用从摄像机捕捉到的实时面部、身体和手部动作。定义后,我们还要确保在加载 app.js 时调用它。
function animate() {
requestAnimationFrame(animate);
if (currentVrm) {
currentVrm.update(clock.getDelta());
}
renderer.render(scene, orbitCamera);
}
animate();
接下来,我们为角色形象绑定逻辑添加代码,这是为我们的角色形象创建灵活骨架的过程。我们将调用这些辅助函数来协助制作角色形象的动画。这些函数负责为我们的角色形象形成数字骨架的不同部分,并映射 MediaPipe Holistic 库提供的实时坐标数据。坐标数据由坐标组成,当我们面对摄像机时,MediaPipe Holistic 库会精确定位我们身体、面部和手部的具体位置。通过这些数据,我们可以准确地将身体动作转化为角色形象动画。rigRotation 辅助函数涉及调整角色形象数字骨架中关节或骨骼的角度,使其与我们的动作相匹配。这包括转动头部或弯曲肘部等动作。rigPosition 辅助函数用于在场景中移动整个角色或角色的某些部分,以跟随我们自己的位置移动。这可能包括左右移动等动作。rigFace 辅助函数用于调整角色形象的面部结构,以反映我们自己的面部动作,如眨眼和说话时的嘴部动作。
const rigRotation = (
name,
rotation = { x: 0, y: 0, z: 0 },
dampener = 1,
lerpAmount = 0.3
) => {
if (!currentVrm) {
return;
}
const Part = currentVrm.humanoid.getBoneNode(
THREE.VRMSchema.HumanoidBoneName[name]
);
if (!Part) {
return;
}
let euler = new THREE.Euler(
rotation.x * dampener,
rotation.y * dampener,
rotation.z * dampener
);
let quaternion = new THREE.Quaternion().setFromEuler(euler);
Part.quaternion.slerp(quaternion, lerpAmount);
};
const rigPosition = (
name,
position = { x: 0, y: 0, z: 0 },
dampener = 1,
lerpAmount = 0.3
) => {
if (!currentVrm) {
return;
}
const Part = currentVrm.humanoid.getBoneNode(
THREE.VRMSchema.HumanoidBoneName[name]
);
if (!Part) {
return;
}
let vector = new THREE.Vector3(
position.x * dampener,
position.y * dampener,
position.z * dampener
);
Part.position.lerp(vector, lerpAmount);
};
let oldLookTarget = new THREE.Euler();
const rigFace = (riggedFace) => {
if (!currentVrm) {
return;
}
rigRotation("Neck", riggedFace.head, 0.7);
const Blendshape = currentVrm.blendShapeProxy;
const PresetName = THREE.VRMSchema.BlendShapePresetName;
riggedFace.eye.l = lerp(
clamp(1 - riggedFace.eye.l, 0, 1),
Blendshape.getValue(PresetName.Blink),
0.5
);
riggedFace.eye.r = lerp(
clamp(1 - riggedFace.eye.r, 0, 1),
Blendshape.getValue(PresetName.Blink),
0.5
);
riggedFace.eye = Kalidokit.Face.stabilizeBlink(
riggedFace.eye,
riggedFace.head.y
);
Blendshape.setValue(PresetName.Blink, riggedFace.eye.l);
Blendshape.setValue(
PresetName.I,
lerp(riggedFace.mouth.shape.I, Blendshape.getValue(PresetName.I), 0.5)
);
Blendshape.setValue(
PresetName.A,
lerp(riggedFace.mouth.shape.A, Blendshape.getValue(PresetName.A), 0.5)
);
Blendshape.setValue(
PresetName.E,
lerp(riggedFace.mouth.shape.E, Blendshape.getValue(PresetName.E), 0.5)
);
Blendshape.setValue(
PresetName.O,
lerp(riggedFace.mouth.shape.O, Blendshape.getValue(PresetName.O), 0.5)
);
Blendshape.setValue(
PresetName.U,
lerp(riggedFace.mouth.shape.U, Blendshape.getValue(PresetName.U), 0.5)
);
let lookTarget = new THREE.Euler(
lerp(oldLookTarget.x, riggedFace.pupil.y, 0.4),
lerp(oldLookTarget.y, riggedFace.pupil.x, 0.4),
0,
"XYZ"
);
oldLookTarget.copy(lookTarget);
currentVrm.lookAt.applyer.lookAt(lookTarget);
};
接下来,创建 animateVRM 函数,该函数将通过 results 参数接收来自 MediaPipe Holistic 库的实时坐标数据。利用这些坐标数据,我们可以将其传递给 Kalidokit,让它为我们角色形象的相应身体部位制作动画。获得坐标数据后,我们就可以调用刚刚创建的绑定辅助函数来制作角色形象的动画。
const animateVRM = (vrm, results) => {
if (!vrm) {
return;
}
let riggedPose, riggedLeftHand, riggedRightHand, riggedFace;
const faceLandmarks = results.faceLandmarks;
const pose3DLandmarks = results.ea;
const pose2DLandmarks = results.poseLandmarks;
const leftHandLandmarks = results.rightHandLandmarks;
const rightHandLandmarks = results.leftHandLandmarks;
// Animate Face
if (faceLandmarks) {
riggedFace = Kalidokit.Face.solve(faceLandmarks, {
runtime: "mediapipe",
video: videoElement,
});
rigFace(riggedFace);
}
// Animate Pose
if (pose2DLandmarks && pose3DLandmarks) {
riggedPose = Kalidokit.Pose.solve(pose3DLandmarks, pose2DLandmarks, {
runtime: "mediapipe",
video: videoElement,
});
rigRotation("Hips", riggedPose.Hips.rotation, 0.7);
rigPosition(
"Hips",
{
x: -riggedPose.Hips.position.x, // Reverse direction
y: riggedPose.Hips.position.y + 1, // Add a bit of height
z: -riggedPose.Hips.position.z, // Reverse direction
},
1,
0.07
);
rigRotation("Chest", riggedPose.Spine, 0.25, 0.3);
rigRotation("Spine", riggedPose.Spine, 0.45, 0.3);
rigRotation("RightUpperArm", riggedPose.RightUpperArm, 1, 0.3);
rigRotation("RightLowerArm", riggedPose.RightLowerArm, 1, 0.3);
rigRotation("LeftUpperArm", riggedPose.LeftUpperArm, 1, 0.3);
rigRotation("LeftLowerArm", riggedPose.LeftLowerArm, 1, 0.3);
rigRotation("LeftUpperLeg", riggedPose.LeftUpperLeg, 1, 0.3);
rigRotation("LeftLowerLeg", riggedPose.LeftLowerLeg, 1, 0.3);
rigRotation("RightUpperLeg", riggedPose.RightUpperLeg, 1, 0.3);
rigRotation("RightLowerLeg", riggedPose.RightLowerLeg, 1, 0.3);
}
// Animate Hands
if (leftHandLandmarks) {
riggedLeftHand = Kalidokit.Hand.solve(leftHandLandmarks, "Left");
rigRotation("LeftHand", {
z: riggedPose.LeftHand.z,
y: riggedLeftHand.LeftWrist.y,
x: riggedLeftHand.LeftWrist.x,
});
rigRotation("LeftRingProximal", riggedLeftHand.LeftRingProximal);
rigRotation("LeftRingIntermediate", riggedLeftHand.LeftRingIntermediate);
rigRotation("LeftRingDistal", riggedLeftHand.LeftRingDistal);
rigRotation("LeftIndexProximal", riggedLeftHand.LeftIndexProximal);
rigRotation("LeftIndexIntermediate", riggedLeftHand.LeftIndexIntermediate);
rigRotation("LeftIndexDistal", riggedLeftHand.LeftIndexDistal);
rigRotation("LeftMiddleProximal", riggedLeftHand.LeftMiddleProximal);
rigRotation(
"LeftMiddleIntermediate",
riggedLeftHand.LeftMiddleIntermediate
);
rigRotation("LeftMiddleDistal", riggedLeftHand.LeftMiddleDistal);
rigRotation("LeftThumbProximal", riggedLeftHand.LeftThumbProximal);
rigRotation("LeftThumbIntermediate", riggedLeftHand.LeftThumbIntermediate);
rigRotation("LeftThumbDistal", riggedLeftHand.LeftThumbDistal);
rigRotation("LeftLittleProximal", riggedLeftHand.LeftLittleProximal);
rigRotation(
"LeftLittleIntermediate",
riggedLeftHand.LeftLittleIntermediate
);
rigRotation("LeftLittleDistal", riggedLeftHand.LeftLittleDistal);
}
if (rightHandLandmarks) {
riggedRightHand = Kalidokit.Hand.solve(rightHandLandmarks, "Right");
rigRotation("RightHand", {
z: riggedPose.RightHand.z,
y: riggedRightHand.RightWrist.y,
x: riggedRightHand.RightWrist.x,
});
rigRotation("RightRingProximal", riggedRightHand.RightRingProximal);
rigRotation("RightRingIntermediate", riggedRightHand.RightRingIntermediate);
rigRotation("RightRingDistal", riggedRightHand.RightRingDistal);
rigRotation("RightIndexProximal", riggedRightHand.RightIndexProximal);
rigRotation(
"RightIndexIntermediate",
riggedRightHand.RightIndexIntermediate
);
rigRotation("RightIndexDistal", riggedRightHand.RightIndexDistal);
rigRotation("RightMiddleProximal", riggedRightHand.RightMiddleProximal);
rigRotation(
"RightMiddleIntermediate",
riggedRightHand.RightMiddleIntermediate
);
rigRotation("RightMiddleDistal", riggedRightHand.RightMiddleDistal);
rigRotation("RightThumbProximal", riggedRightHand.RightThumbProximal);
rigRotation(
"RightThumbIntermediate",
riggedRightHand.RightThumbIntermediate
);
rigRotation("RightThumbDistal", riggedRightHand.RightThumbDistal);
rigRotation("RightLittleProximal", riggedRightHand.RightLittleProximal);
rigRotation(
"RightLittleIntermediate",
riggedRightHand.RightLittleIntermediate
);
rigRotation("RightLittleDistal", riggedRightHand.RightLittleDistal);
}
};
最后,我们来创建并配置一个 MediaPipe Holistic 库实例。首先,我们需要使用 MediaPipe 摄像机实用工具模块获取摄像机画面,并将其渲染到 <video> 元素中。然后,我们将 HTML 中的 <video> 元素传递给 MediaPipe Holistic,以便它进行处理并提供坐标数据。一旦 Holistic 完成了对 <video> 元素中摄像机数据的处理,它就会调用一个回调函数,并提供由此产生的坐标数据。然后,这些结果将被传递给我们之前创建的 animateVRM 函数,以便为我们的角色形象制作动画。
let videoElement = document.querySelector(".input_video");
const onResults = (results) => {
// Animate model
animateVRM(currentVrm, results);
};
const holistic = new Holistic({
locateFile: (file) => {
return `https://cdn.jsdelivr.net/npm/@mediapipe/holistic@0.5.1635989137/${file}`;
},
});
holistic.setOptions({
modelComplexity: 1,
smoothLandmarks: true,
minDetectionConfidence: 0.7,
minTrackingConfidence: 0.7,
refineFaceLandmarks: true,
});
// Pass holistic a callback function
holistic.onResults(onResults);
// Use `Mediapipe` utils to get camera
const camera = new Camera(videoElement, {
onFrame: async () => {
await holistic.send({ image: videoElement });
},
width: 640,
height: 480,
});
camera.start();
第 5 部分 - 将您的虚拟角色通过 Amazon IVS 进行直播
现在,我们已经在画布上绘制了一个虚拟角色形象,它可以模仿我们的上半身动作。下面,让我们将 Amazon IVS Web Broadcast SDK 与 Web 应用程序集成来实现直播,让全世界都能看到我们的角色形象。在开始直播之前,我们需要理解三个核心概念,这些概念是实现实时直播的关键。
- Stage:参与者交换音频或视频的虚拟空间。Stage 类是主机应用程序与 SDK 之间的主要交互点。
- StageStrategy:为主机应用程序向 SDK 传达所需的 Stage 状态的接口
- Events(事件):您可以使用 Stage 实例来传达状态变化,如某人离开或加入 Stage 等事件。
要让观众看到我们的画面,我们需要从画布元素中捕获 MediaStream,并在名为 init() 的函数中将其分配给 avatarStream。我们使用 Canvas API 的 captureStream 方法来实现这一点。
const init = async () => {
const avatarStream = renderer.domElement.captureStream();
};
一旦我们有了想要发布给观众的 MediaStream,我们就需要加入一个 Stage。加入 Stage 后,我们就可以向 Stage 中的观众或其他参与者直播视频。如果我们不想再进行直播,可以离开 Stage。我们添加事件监听器,当最终用户点击“加入 Stage”或“离开 Stage”按钮时监听点击事件,并实现相应的逻辑。
const init = async () => {
const avatarStream = renderer.domElement.captureStream();
joinBtn.addEventListener("click", () => {
if (tokenInput.value.length === 0) {
openModal();
} else {
joinStage(avatarStream);
}
});
};
接下来,我们添加 joinStage 函数的逻辑。在此函数中,我们将从用户的麦克风中获取 MediaStream,以便将其发布至 Stage。发布是指将音频和/或视频发送到 Stage,以便其他参与者可以看到已加入的参与者或听到其声音。
在此函数中,我们还需要使用麦克风和画布中的 MediaStream 实例来创建 LocalStageStream 实例。使用这些 LocalStageStream 实例,我们可以在 StageStrategy 接口上实现 stageStreamsToPublish 函数。在 stageStreamsToPublish 函数中,我们只需返回数组中的 LocalStageStream 实例,这样观众就能听到我们的音频并看到我们的角色形象了。
同时,我们还需要实现 shouldPublishParticipant 并返回 true。这表示特定的参与者是否应该发布。此外,我们还需要实现 shouldSubscribeToParticipant,以表示我们的应用程序应该只订阅远程参与者的音频、同时订阅音频和视频,还是什么都不订阅。
最后,新建一个 Stage 对象,将我们之前设置的参与者令牌和策略对象作为参数传入其中。参与者令牌用于向 Stage 进行身份验证,以及识别我们要加入的 Stage。在控制台中创建一个 Stage,然后在该 Stage 中创建一个参与者令牌,即可获得参与者令牌。策略对象定义了我们加入 Stage 后要发布给观众看的内容。稍后,我们将调用 Stage 对象上的 join 方法来加入 Stage。
const joinStage = async (avatarStream) => {
if (connected || joining) {
return;
}
joining = true;
joinBtn.addEventListener("click", () => {
leaveStage();
joinBtn.innerText = "Leave Stage";
});
const token = tokenInput.value;
if (!token) {
window.alert("Please enter a participant token");
joining = false;
return;
}
localMic = await navigator.mediaDevices.getUserMedia({
video: false,
audio: true,
});
avatarStageStream = new LocalStageStream(avatarStream.getVideoTracks()[0]);
micStageStream = new LocalStageStream(localMic.getAudioTracks()[0]);
const strategy = {
stageStreamsToPublish() {
return [avatarStageStream, micStageStream];
},
shouldPublishParticipant() {
return true;
},
shouldSubscribeToParticipant() {
return SubscribeType.AUDIO_VIDEO;
},
};
stage = new Stage(token, strategy);
};
最后,我们添加一些逻辑来监听 Stage 事件。当您已加入的 Stage 状态发生变化,例如有人加入或离开时,就会发生这些事件。利用这些事件,您可以动态更新 HTML 代码,以便在新参与者加入时显示其视频画面,或在其离开时使其不再显示。setupParticipant 和 teardownParticipant 函数分别执行其中每项操作。下一步,我们调用 Stage 对象上的 join 方法来加入 Stage。
// Other available events:
// https://aws.github.io/amazon-ivs-web-broadcast/docs/sdk-guides/stages#events
stage.on(StageEvents.STAGE_CONNECTION_STATE_CHANGED, (state) => {
connected = state === ConnectionState.CONNECTED;
if (connected) {
joining = false;
}
});
const leaveStage = async () => {
stage.leave();
joining = false;
connected = false;
};
stage.on(StageEvents.STAGE_PARTICIPANT_JOINED, (participant) => {
console.log("Participant Joined:", participant);
});
stage.on(
StageEvents.STAGE_PARTICIPANT_STREAMS_ADDED,
(participant, streams) => {
console.log("Participant Media Added: ", participant, streams);
}
);
stage.on(StageEvents.STAGE_PARTICIPANT_LEFT, (participant) => {
console.log("Participant Left: ", participant);
});
try {
await stage.join();
} catch (err) {
joining = false;
connected = false;
console.error(err.message);
}
此时,我们要将模仿我们肢体动作的实时角色形象画面广播到 Stage 上。要测试加入 Stage 的其他人能否看到您的角色形象,请在另一个浏览器窗口中打开 Amazon IVS 实时流处理 Web 示例,创建另一个参与者令牌,在此浏览器窗口中提供该令牌,然后点击“加入 Stage ”。现在,您应该能看到角色形象随着您在摄像机上的移动而移动。延迟可以达到秒级,最低可达 300 毫秒。加入 Stage 的其他观众也会看到您角色形象以这种方式移动。
总结
在本教程中,您利用 Pixiv 的 SDK 创建了一个 VTubing 应用程序,以显示虚拟角色形象角色,并使用 Amazon IVS 进行直播。VTubing 为您打开了通往精彩虚拟内容创作世界的大门。按照本教程中概述的步骤操作,您就能获得必要的知识和工具,让您独一无二的虚拟角色栩栩如生。
关于作者
Tony Vu 是 Twitch 的高级合作伙伴工程师。他专门负责评估与 Amazon Interactive Video Service (IVS) 集成的合作伙伴技术,旨在为我们的 IVS 客户开发和提供全面的联合解决方案。