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

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

这个网页应用,是我对便捷性、高效性以及即时性追求的集中体现。它摒弃了传统软件的繁琐安装流程,只需一个简单的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>

```

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容