mirror of
https://github.com/CorentinTh/it-tools.git
synced 2025-04-26 09:46:15 -04:00
214 lines
6.1 KiB
Vue
214 lines
6.1 KiB
Vue
<script setup lang="ts">
|
|
import _ from 'lodash';
|
|
|
|
import { useMediaRecorder } from './useMediaRecorder';
|
|
|
|
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 {
|
|
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() {
|
|
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>
|
|
|
|
<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 flex-col gap-2>
|
|
<c-select
|
|
v-model:value="currentCamera"
|
|
label-position="left"
|
|
label-width="60px"
|
|
label="Video:"
|
|
:options="cameras.map(({ deviceId, label }) => ({ value: deviceId, label }))"
|
|
placeholder="Select camera"
|
|
/>
|
|
<c-select
|
|
v-if="currentMicrophone && microphones.length > 0"
|
|
v-model:value="currentMicrophone"
|
|
label="Audio:"
|
|
label-position="left"
|
|
label-width="60px"
|
|
:options="microphones.map(({ deviceId, label }) => ({ value: deviceId, label }))"
|
|
placeholder="Select microphone"
|
|
/>
|
|
</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>
|