js牛逼,不是白说的,本文就给大家分享一个浏览器端js生成视频的方案。
效果演示
方案概述
上面这个演示效果的实现主要用到了以下几点。
- Oasis: 在画布上绘制出3D内容并提供鼠标交互能力
- HTMLMediaElement.captureStream(): 获取canvas的 媒体流
- MediaRecorder:接收媒体流并能够将其录制下来生成视频
关键代码和讲解
<template>
<div class="box">
<a-button :disabled="isRecording" @click="play">开启录制</a-button>
<a-button :disabled="!isRecording" @click="stopRecord"
>停止录制并生成视频</a-button
>
<a-space style="display: flex">
<div>
<h1>画布</h1>
<canvas style="width: 50vw; height: 50vh" id="canvas" />
</div>
</a-space>
</div>
</template>
<script lang="ts" setup>
/**
* @title Lite Collision Detection
* @category Physics
*/
import {
WebGLEngine,
Vector3,
MeshRenderer,
PointLight,
Camera,
Entity,
GLTFResource,
BoundingBox,
SkinnedMeshRenderer,
} from "oasis-engine";
import { OrbitControl } from "@oasis-engine/controls";
import { LitePhysics } from "@oasis-engine/physics-lite";
import { onMounted, ref } from "vue";
import { onUnmounted } from "vue";
onMounted(() => {
createOasis();
});
let engine: WebGLEngine;
let rootEntity: Entity;
let camera: Camera;
// 初始化oasis引擎,创建画布内容
function createOasis() {
engine = new WebGLEngine("canvas", LitePhysics, {
alpha: false, // 控制背景是否透明
premultipliedAlpha: false,
});
engine.canvas.resizeByClientSize();
const scene = engine.sceneManager.activeScene;
scene.background.solidColor.setValue(0, 0, 0, 0);
rootEntity = scene.createRootEntity("root");
scene.ambientLight.diffuseSolidColor.setValue(1, 1, 1, 1);
scene.ambientLight.diffuseIntensity = 1.2;
// 初始化相机
const cameraEntity = rootEntity.createChild("camera");
camera = cameraEntity.addComponent(Camera);
cameraEntity.transform.setPosition(10, 10, 10);
let orbitControl = cameraEntity.addComponent(OrbitControl);
// 初始化点光源
const light = rootEntity.createChild("light");
light.transform.setPosition(0, 3, 0);
const pointLight = light.addComponent(PointLight);
pointLight.intensity = 0.3;
// 加载模型
let modelEntity = rootEntity.createChild("ModelEntity");
engine.resourceManager
.load<GLTFResource>("./models/a0ff1c60-5e21-49e5-946a-675515d98736.glb")
.then((asset) => {
const { defaultSceneRoot } = asset;
modelEntity.addChild(defaultSceneRoot);
setTargetCenter(modelEntity, orbitControl);
});
// Run engine
engine.run();
onUnmounted(() => scene.destroy());
}
/**根据模型尺寸 将其设置为中间 */
function setTargetCenter(modelEntity: Entity, orbitControl: OrbitControl) {
const meshRenderers = new Array<MeshRenderer>();
modelEntity.getComponentsIncludeChildren(MeshRenderer, meshRenderers);
const skinnedMeshRenderers: SkinnedMeshRenderer[] = [];
modelEntity.getComponentsIncludeChildren(
SkinnedMeshRenderer,
skinnedMeshRenderers
);
const renderers = meshRenderers.concat(skinnedMeshRenderers);
const boundingBox = new BoundingBox();
const center = new Vector3();
const extent = new Vector3();
boundingBox.min.setValue(0, 0, 0);
boundingBox.max.setValue(0, 0, 0);
renderers.forEach((renderer) => {
BoundingBox.merge(renderer.bounds, boundingBox, boundingBox);
});
boundingBox.getExtent(extent);
const size = extent.length();
boundingBox.getCenter(center);
orbitControl.target.setValue(center.x, center.y, center.z);
const cameraEntity = orbitControl.camera;
const camera = cameraEntity.getComponent(Camera);
cameraEntity.transform.setPosition(size * 2, size * 1.5, size * 4);
// 记录模型初始化视角
camera.farClipPlane = size * 12;
if (camera.nearClipPlane > size) {
camera.nearClipPlane = size / 10;
} else {
camera.nearClipPlane = 0.1;
}
}
let stream = ref<MediaStream>();
let recorder: MediaRecorder;
// 点击录制按钮触发
function play() {
if (!stream.value) {
// 将画布内容生成媒体流
stream.value = engine.canvas._webCanvas.captureStream(60);
// 生成媒体录制器对象并传入一个媒体流以便录制
stream.value &&
(recorder = new MediaRecorder(stream.value, {
mimeType: "video/webm;codecs=vp8",
}));
// 有可录制的媒体资源事件
recorder.ondataavailable = (event) => {
blobs.push(event.data);
};
// 媒体录制器停止录制事件
recorder.onstop = (event) => {
isRecording.value = false;
saveVideo();
};
}
// 媒体录制器开始录制
isRecording.value = true;
recorder.start();
}
let blobs: Blob[] = [];
let webm = ref<Blob>();
let isRecording = ref(false);
function saveVideo() {
webm.value = new Blob(blobs, { type: "video/mp4" });
open(URL.createObjectURL(webm.value));
blobs = [];
}
/**
* 停止录制
*/
function stopRecord() {
recorder.stop();
}
</script>
<style scoped lang="scss">
.box {
padding-top: 60px;
}
#canvas {
background-color: #222;
}
</style>