在这个静谧的夜晚,当城市的灯火阑珊,万籁俱寂之时,我悄然回到了自己的小天地。心中那份对技术的炽热与不懈追求,如同夜空中最亮的星,指引着我前行。我轻轻按下电脑的电源键,屏幕逐渐亮起,却没有被即刻的娱乐或休憩所吸引,而是直接跳转到了那个凝聚了我无数心血与创意的在线工具——“网络摄像机在线录像器”。
这个网页应用,是我对便捷性、高效性以及即时性追求的集中体现。它摒弃了传统软件的繁琐安装流程,只需一个简单的URL,便能瞬间激活用户的摄像头与声卡,开启一场高清、流畅的录像与截图盛宴。无论是温馨的家庭聚会,紧张的远程工作讨论,还是生动的在线教育课堂,它都能成为用户手中最趁手的记录工具,捕捉每一个值得铭记的瞬间。
我满怀期待地点击了“开始录像”的按钮,只见屏幕中央迅速浮现出一个清晰无比的预览框,那是摄像头捕捉到的鲜活画面,仿佛整个世界都被微缩于此。与此同时,底部的状态栏精准地显示着录音的音量信息,确保声音与画面的完美同步,让每一个细节都得以忠实记录。
在录制的过程中,我时不时会按下“截图”键,将那些稍纵即逝的美好画面定格下来。这些截图如同时间的碎片,被精心收集并展示在网页的侧边栏中,它们静静地诉说着过去的故事,让人不禁沉醉其中。
当录制结束时,我满意地看着视频与截图整齐地排列在网页上,它们就像是我亲手编织的记忆之网,记录着生活的点点滴滴。点击下载按钮,这些珍贵的作品便会被安全地保存到本地设备中,随时等待着我或他人的翻阅与分享。
然而,我也清醒地认识到,“网络摄像机在线录像器”并非完美无缺。它有一个显著的局限性——一旦网页被刷新或关闭,所有的视频与截图都将不复存在。这既是它的一种设计哲学,也是我对用户的一种温柔提醒:生活中的每一个瞬间都是独一无二的,值得我们用心去珍惜与把握。
为了让更多人能够发现并爱上这款工具,我还投入了大量的精力进行SEO优化。通过深入研究搜索引擎的工作原理与用户的搜索习惯,我精心挑选了关键词、优化了网页结构、提升了加载速度,并积极与相关领域的博主和社区合作,共同推广“网络摄像机在线录像器”。这些努力逐渐取得了成效,网站的访问量稳步增长,用户的反馈也愈发积极。
在完成了对“网络摄像机在线录像器”的自我审视与SEO优化后,我缓缓关闭了电脑,准备进入梦乡。但我的思绪并未因此停歇,反而在梦中继续翱翔于技术的海洋之中。我梦想着如何进一步优化这款工具的功能与界面设计,如何让它更加符合用户的需求与期待。因为我知道,“网络摄像机在线录像器”不仅仅是一个网页应用那么简单,它更是我对技术、对生活无尽热爱与追求的象征。
<template>
<tool-info :tool="tool" :readme="VueComponent">
<template #body>
<el-card shadow="never">
<el-row v-if="!isSupported" justify="center">
<el-col :xs="24" :sm="16" :md="16">
<el-alert :title="$t('tools.camera-recorder.alert.video')" type="info" show-icon :closable="false"/>
</el-col>
</el-row>
<el-row v-else-if="!permissionGranted" justify="center">
<el-col :xs="24" :sm="16" :md="16">
<el-row>
<el-col :span="24">
<el-alert v-if="permissionCannotBePrompted" :title="$t('tools.camera-recorder.alert.microphone.title')" type="info" :description="$t('tools.camera-recorder.alert.microphone.description')" show-icon :closable="false" />
<el-alert v-else :title="$t('tools.camera-recorder.alert.permission')" type="info" show-icon :closable="false"/>
</el-col>
</el-row>
<el-row>
<el-col :span="24">
<el-button @click="requestPermissions">{{$t('tools.camera-recorder.consent')}}</el-button>
</el-col>
</el-row>
</el-col>
</el-row>
<el-row v-else justify="center">
<el-col :xs="24" :sm="16" :md="16">
<el-form label-width="auto">
<el-form-item :label="$t('tools.camera-recorder.video')">
<el-select v-model="currentCamera" >
<el-option v-for="item in cameras.map(({ deviceId, label }) => ({ value: deviceId, label }))":label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item :label="$t('tools.camera-recorder.audio')" v-if="currentMicrophone && microphones.length > 0">
<el-select v-model="currentMicrophone">
<el-option v-for="item in microphones.map(({ deviceId, label }) => ({ value: deviceId, label }))":label="item.label" :value="item.value"/>
</el-select>
</el-form-item>
</el-form>
<el-row v-if="!isMediaStreamAvailable">
<el-col :span="24" align="center">
<el-button type="primary" @click="start"> {{$t('tools.camera-recorder.button.start')}} </el-button>
</el-col>
</el-row>
<el-row v-else>
<el-col :span="24">
<el-row>
<el-col :span="24">
<video ref="video" autoplay controls playsinline max-h-full w-full style="width: 100%;"/>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-button :disabled="!isMediaStreamAvailable" @click="takeScreenshot">{{$t('tools.camera-recorder.button.screen-shot')}}</el-button>
</el-col>
<el-col :span="12" v-if="isRecordingSupported" style="text-align: right;" >
<el-button v-if="recordingState === 'stopped'" @click="startRecording">{{$t('tools.camera-recorder.button.record')}}</el-button>
<el-button v-if="recordingState === 'recording'" @click="pauseRecording">{{$t('tools.camera-recorder.button.pause')}} </el-button>
<el-button v-if="recordingState === 'paused'" @click="resumeRecording"> {{$t('tools.camera-recorder.button.resume')}}</el-button>
<el-button v-if="recordingState !== 'stopped'" type="error" @click="stopRecording">{{$t('tools.camera-recorder.button.stop')}}</el-button>
</el-col>
<el-col :span="12" v-else>
<el-alert :title="$t('tools.camera-recorder.alert.not')" type="info" show-icon :closable="false"/>
</el-col>
</el-row>
</el-col>
</el-row>
<el-row>
<el-col :span="12" v-for="({ type, value, createdAt }, index) in medias" :key="index">
<el-divider/>
<el-card shadow="never" style="max-height: 400px;">
<template #default>
<img v-if="type === 'image'" :src="value" style="width: 100%" alt="screenshot">
<video v-else :src="value" controls max-h-full w-full style="width: 100%"/>
</template>
<template #footer>
<el-text style="margin-right: 20px;"> {{ type === 'image' ? $t('tools.camera-recorder.type.iamge') : $t('tools.camera-recorder.type.video') }}</el-text>
<el-button @click="downloadMedia({ type, value, createdAt })">{{$t('tools.camera-recorder.button.download') }}</el-button>
<el-button icon="Delete" @click="medias = medias.filter((_ignored, i) => i !== index)">{{$t('tools.camera-recorder.button.delete') }}</el-button>
</template>
</el-card>
</el-col>
</el-row>
</el-col>
</el-row>
</el-card>
</template>
</tool-info>
</template>
<script setup lang="ts">
import { tool } from './index';
import ToolInfo from '@/components/ToolInfo.vue';
import {VueComponent} from './README.md';
import _ from 'lodash';
import { useDevicesList, useUserMedia } from '@vueuse/core';
import { useMediaRecorder } from './useMediaRecorder';
import { ref, computed,watchEffect ,onBeforeUnmount} from 'vue';
interface Media { type: 'image' | 'video'; value: string; createdAt: Date }
const {
videoInputs: cameras,
audioInputs: microphones,
permissionGranted,
isSupported,
ensurePermissions,
} = useDevicesList({
requestPermissions: true,
constraints: { video: true, audio: true },
onUpdated() {
refreshCurrentDevices();
},
});
const video = ref<HTMLVideoElement>();
const medias = ref<Media[]>([]);
const currentCamera = ref(cameras.value[0]?.deviceId);
const currentMicrophone = ref(microphones.value[0]?.deviceId);
const permissionCannotBePrompted = ref(false);
const constraint = computed<MediaStreamConstraints>(() => ({
video: { deviceId: currentCamera.value },
...(currentMicrophone.value ? { audio: { deviceId: currentMicrophone.value } } : {}),
}))
const {
stream,
start,
stop,
enabled: isMediaStreamAvailable,
} = useUserMedia({
constraints: constraint,
autoSwitch: true,
});
const {
isRecordingSupported,
onRecordAvailable,
startRecording,
stopRecording,
pauseRecording,
recordingState,
resumeRecording,
} = useMediaRecorder({
stream,
});
onRecordAvailable((value) => {
medias.value.unshift({ type: 'video', value, createdAt: new Date() });
});
function refreshCurrentDevices() {
if (_.isNil(currentCamera) || !cameras.value.find(i => i.deviceId === currentCamera.value)) {
currentCamera.value = cameras.value[0]?.deviceId;
}
if (_.isNil(microphones) || !microphones.value.find(i => i.deviceId === currentMicrophone.value)) {
currentMicrophone.value = microphones.value[0]?.deviceId;
}
}
function takeScreenshot() {
if (!video.value) {
return;
}
const canvas = document.createElement('canvas');
canvas.width = video.value.videoWidth;
canvas.height = video.value.videoHeight;
canvas.getContext('2d')?.drawImage(video.value, 0, 0);
const image = canvas.toDataURL('image/png');
medias.value.unshift({ type: 'image', value: image, createdAt: new Date() });
}
watchEffect(() => {
if (video.value && stream.value) {
video.value.srcObject = stream.value;
}
});
onBeforeUnmount(() => stop());
async function requestPermissions() {
try {
await ensurePermissions();
}
catch (e) {
permissionCannotBePrompted.value = true;
}
}
function downloadMedia({ type, value, createdAt }: Media) {
if(typeof document === 'undefined'){
return;
}
const link = document.createElement('a');
link.href = value;
link.download = `${type}-${createdAt.getTime()}.${type === 'image' ? 'png' : 'webm'}`;
link.click();
}
</script>