自我探索之路:我的网络摄像机在线录像器1.0版本之路

27 阅读4分钟

在这个静谧的夜晚,当城市的灯火阑珊,万籁俱寂之时,我悄然回到了自己的小天地。心中那份对技术的炽热与不懈追求,如同夜空中最亮的星,指引着我前行。我轻轻按下电脑的电源键,屏幕逐渐亮起,却没有被即刻的娱乐或休憩所吸引,而是直接跳转到了那个凝聚了我无数心血与创意的在线工具——“网络摄像机在线录像器”。

这个网页应用,是我对便捷性、高效性以及即时性追求的集中体现。它摒弃了传统软件的繁琐安装流程,只需一个简单的URL,便能瞬间激活用户的摄像头与声卡,开启一场高清、流畅的录像与截图盛宴。无论是温馨的家庭聚会,紧张的远程工作讨论,还是生动的在线教育课堂,它都能成为用户手中最趁手的记录工具,捕捉每一个值得铭记的瞬间。

我满怀期待地点击了“开始录像”的按钮,只见屏幕中央迅速浮现出一个清晰无比的预览框,那是摄像头捕捉到的鲜活画面,仿佛整个世界都被微缩于此。与此同时,底部的状态栏精准地显示着录音的音量信息,确保声音与画面的完美同步,让每一个细节都得以忠实记录。

在录制的过程中,我时不时会按下“截图”键,将那些稍纵即逝的美好画面定格下来。这些截图如同时间的碎片,被精心收集并展示在网页的侧边栏中,它们静静地诉说着过去的故事,让人不禁沉醉其中。

当录制结束时,我满意地看着视频与截图整齐地排列在网页上,它们就像是我亲手编织的记忆之网,记录着生活的点点滴滴。点击下载按钮,这些珍贵的作品便会被安全地保存到本地设备中,随时等待着我或他人的翻阅与分享。

然而,我也清醒地认识到,“网络摄像机在线录像器”并非完美无缺。它有一个显著的局限性——一旦网页被刷新或关闭,所有的视频与截图都将不复存在。这既是它的一种设计哲学,也是我对用户的一种温柔提醒:生活中的每一个瞬间都是独一无二的,值得我们用心去珍惜与把握。

为了让更多人能够发现并爱上这款工具,我还投入了大量的精力进行SEO优化。通过深入研究搜索引擎的工作原理与用户的搜索习惯,我精心挑选了关键词、优化了网页结构、提升了加载速度,并积极与相关领域的博主和社区合作,共同推广“网络摄像机在线录像器”。这些努力逐渐取得了成效,网站的访问量稳步增长,用户的反馈也愈发积极。

在完成了对“网络摄像机在线录像器”的自我审视与SEO优化后,我缓缓关闭了电脑,准备进入梦乡。但我的思绪并未因此停歇,反而在梦中继续翱翔于技术的海洋之中。我梦想着如何进一步优化这款工具的功能与界面设计,如何让它更加符合用户的需求与期待。因为我知道,“网络摄像机在线录像器”不仅仅是一个网页应用那么简单,它更是我对技术、对生活无尽热爱与追求的象征。

v2-7bad07e122d302405b98959a2107f0ca_1440w.png

v2-66963638f5044cec5cdb88c637c738bc_1440w.png

<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>