diff options
Diffstat (limited to 'src/tools/camera-recorder/camera-recorder.vue')
-rw-r--r-- | src/tools/camera-recorder/camera-recorder.vue | 202 |
1 files changed, 202 insertions, 0 deletions
diff --git a/src/tools/camera-recorder/camera-recorder.vue b/src/tools/camera-recorder/camera-recorder.vue new file mode 100644 index 0000000..81fec42 --- /dev/null +++ b/src/tools/camera-recorder/camera-recorder.vue @@ -0,0 +1,202 @@ +<template> + <div> + <c-card v-if="!isSupported"> Your browser does not support recording video from camera </c-card> + + <c-card v-else-if="!permissionGranted" text-center> + You need to grant permission to use your camera and microphone + + <c-alert v-if="permissionCannotBePrompted" mt-4 text-left> + Your browser has blocked permission request or does not support it. You need to grant permission manually in + your browser settings (usually the lock icon in the address bar). + </c-alert> + + <div v-else mt-4 flex justify-center> + <c-button @click="requestPermissions">Grant permission</c-button> + </div> + </c-card> + + <c-card v-else> + <div flex gap-2> + <div flex-1> + <div>Video</div> + <n-select + v-model:value="currentCamera" + :options="cameras.map(({ deviceId, label }) => ({ value: deviceId, label }))" + placeholder="Select camera" + /> + </div> + <div v-if="currentMicrophone && microphones.length > 0" flex-1> + <div>Audio</div> + <n-select + v-model:value="currentMicrophone" + :options="microphones.map(({ deviceId, label }) => ({ value: deviceId, label }))" + placeholder="Select microphone" + /> + </div> + </div> + + <div v-if="!isMediaStreamAvailable" mt-3 flex justify-center> + <c-button type="primary" @click="start">Start webcam</c-button> + </div> + + <div v-else> + <div my-2> + <video ref="video" autoplay controls playsinline max-h-full w-full /> + </div> + + <div flex items-center justify-between gap-2> + <c-button :disabled="!isMediaStreamAvailable" @click="takeScreenshot"> + <span mr-2> <icon-mdi-camera /></span> + Take screenshot + </c-button> + + <div v-if="isRecordingSupported" flex justify-center gap-2> + <c-button v-if="recordingState === 'stopped'" @click="startRecording"> + <span mr-2> <icon-mdi-video /></span> + Start recording + </c-button> + + <c-button v-if="recordingState === 'recording'" @click="pauseRecording"> + <span mr-2> <icon-mdi-pause /></span> + Pause + </c-button> + + <c-button v-if="recordingState === 'paused'" @click="resumeRecording"> + <span mr-2> <icon-mdi-play /></span> + Resume + </c-button> + + <c-button v-if="recordingState !== 'stopped'" type="error" @click="stopRecording"> + <span mr-2> <icon-mdi-record /></span> + Stop + </c-button> + </div> + <div v-else italic op-60>Video recording is not supported in your browser</div> + </div> + </div> + </c-card> + + <div grid grid-cols-2 mt-5 gap-2> + <c-card v-for="({ type, value, createdAt }, index) in medias" :key="index"> + <img v-if="type === 'image'" :src="value" max-h-full w-full alt="screenshot" /> + + <video v-else :src="value" controls max-h-full w-full /> + + <div flex items-center justify-between> + <div font-bold>{{ type === 'image' ? 'Screenshot' : 'Video' }}</div> + + <div flex gap-2> + <c-button @click="downloadMedia({ type, value, createdAt })"> + <icon-mdi-download /> + </c-button> + + <c-button @click="medias = medias.filter((_ignored, i) => i !== index)"> + <icon-mdi-delete-outline /> + </c-button> + </div> + </div> + </c-card> + </div> + </div> +</template> + +<script setup lang="ts"> +import _ from 'lodash'; + +import { useMediaRecorder } from './useMediaRecorder'; + +type 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 { + stream, + start, + enabled: isMediaStreamAvailable, +} = useUserMedia({ + constraints: computed(() => ({ + video: { deviceId: currentCamera.value }, + ...(currentMicrophone.value ? { audio: { deviceId: currentMicrophone.value } } : {}), + })), + 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() { + console.log('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; +}); + +async function requestPermissions() { + try { + await ensurePermissions(); + } catch (e) { + permissionCannotBePrompted.value = true; + } +} + +function downloadMedia({ type, value, createdAt }: Media) { + const link = document.createElement('a'); + link.href = value; + link.download = `${type}-${createdAt.getTime()}.${type === 'image' ? 'png' : 'webm'}`; + link.click(); +} +</script> + +<style lang="less" scoped></style> |