Compare commits
2 commits
dd5c8e3d57
...
25ee3dd4f2
Author | SHA1 | Date | |
---|---|---|---|
|
25ee3dd4f2 | ||
|
32f91f7ee3 |
4 changed files with 175 additions and 228 deletions
|
@ -37,11 +37,6 @@
|
||||||
<b-icon-camera-video></b-icon-camera-video>
|
<b-icon-camera-video></b-icon-camera-video>
|
||||||
</div>
|
</div>
|
||||||
</webcam-file-source>
|
</webcam-file-source>
|
||||||
<webcam-file-source-legacy @input="addFiles">
|
|
||||||
<div class="img-thumbnail btn btn-outline-primary">
|
|
||||||
<b-icon-camera-video></b-icon-camera-video>
|
|
||||||
</div>
|
|
||||||
</webcam-file-source-legacy>
|
|
||||||
<label class="img-thumbnail btn btn-outline-primary" for="file-dropdown">
|
<label class="img-thumbnail btn btn-outline-primary" for="file-dropdown">
|
||||||
<b-icon-plus></b-icon-plus>
|
<b-icon-plus></b-icon-plus>
|
||||||
</label>
|
</label>
|
||||||
|
@ -111,13 +106,11 @@ import CameraFileSource from "@/components/CameraFileSource.vue";
|
||||||
import AuthenticatedImage from "@/components/AuthenticatedImage.vue";
|
import AuthenticatedImage from "@/components/AuthenticatedImage.vue";
|
||||||
import DeletableWrapper from "@/components/DeletableWrapper.vue";
|
import DeletableWrapper from "@/components/DeletableWrapper.vue";
|
||||||
import WebcamFileSource from "@/components/WebcamFileSource.vue";
|
import WebcamFileSource from "@/components/WebcamFileSource.vue";
|
||||||
import WebcamFileSourceLegacy from "@/components/WebcamFileSourceLegacy.vue";
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "CombinedFileField",
|
name: "CombinedFileField",
|
||||||
components: {
|
components: {
|
||||||
WebcamFileSource,
|
WebcamFileSource,
|
||||||
WebcamFileSourceLegacy,
|
|
||||||
...BIcons,
|
...BIcons,
|
||||||
AuthenticatedImage,
|
AuthenticatedImage,
|
||||||
DeletableWrapper,
|
DeletableWrapper,
|
||||||
|
|
|
@ -1,29 +1,71 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="d-inline-block">
|
<div class="d-inline-block">
|
||||||
<label @click="show_modal = true">
|
<label @click="open">
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</label>
|
</label>
|
||||||
<div class="modal">
|
<div class="modal" :class="{'d-block': show_modal}" tabindex="-1">
|
||||||
<div class="modal-dialog modal-fullscreen">
|
<div class="modal-dialog modal-fullscreen">
|
||||||
<div class="modal-content">
|
<div class="modal-content p-2">
|
||||||
<select v-model="selectedCamera" v-on:change="openStream" class="form-select position-relative w-75 mx-auto mt-4" aria-label="Select Camera Source">
|
<button type="button" class="btn-close position-absolute fixed-top m-2" style="right: 1rem; left: auto;"
|
||||||
<option disabled value="">Select Camera Source</option>
|
@click="close"></button>
|
||||||
<option v-for="camera in availableCameras" :key="camera.deviceId" :value="camera">
|
<div v-if="error" class="alert alert-danger" role="alert">
|
||||||
{{ camera.label }}
|
{{ lastError }}
|
||||||
</option>
|
</div>
|
||||||
</select>
|
<div class="row" v-if="capturing && !streaming">
|
||||||
<video
|
<div class="spinner-grow text-danger mx-auto" role="status">
|
||||||
v-if="capturing"
|
<span class="sr-only">Loading...</span>
|
||||||
ref="video"
|
|
||||||
class="img-fluid rounded mx-auto d-block mb-3 img-preview w-100"
|
|
||||||
>
|
|
||||||
Video stream not available.
|
|
||||||
</video>
|
|
||||||
<canvas ref="canvas" class="img-fluid d-none img-preview"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<img
|
||||||
|
v-if="!capturing && dataImage"
|
||||||
|
class="img-fluid rounded mx-auto d-block w-75"
|
||||||
|
:src="dataImage"
|
||||||
|
alt="Image not available."
|
||||||
|
/>
|
||||||
|
<video
|
||||||
|
ref="video"
|
||||||
|
class="img-fluid rounded d-block img-preview w-75 mx-auto"
|
||||||
|
:class="{'d-none': !capturing}"
|
||||||
|
>
|
||||||
|
Video stream not available.
|
||||||
|
</video>
|
||||||
|
<canvas ref="canvas" class="img-fluid d-none img-preview"/>
|
||||||
|
<div class="position-absolute fixed-bottom text-center">
|
||||||
|
<div class="btn-group shadow">
|
||||||
|
<button
|
||||||
|
class="btn btn-success"
|
||||||
|
:class="{'disabled': dataImage}"
|
||||||
|
@click="(!dataImage) && captureVideoImage()">
|
||||||
|
<b-icon-camera></b-icon-camera> Capture
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="btn btn-danger"
|
||||||
|
:class="{'disabled': !dataImage}"
|
||||||
|
@click="(dataImage) && retake()"
|
||||||
|
>
|
||||||
|
<b-icon-trash></b-icon-trash> Retake
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="btn btn-primary"
|
||||||
|
:class="{'disabled': !dataImage}"
|
||||||
|
@click="(dataImage) && save()"
|
||||||
|
>
|
||||||
|
<b-icon-save></b-icon-save> Save
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<select v-model="selectedCamera" v-on:change="onUserSelect"
|
||||||
|
class="form-select position-relative w-50 mx-auto mb-5 mt-2 shadow"
|
||||||
|
aria-label="Select Camera Source">
|
||||||
|
<option disabled value="">Select Camera Source</option>
|
||||||
|
<option v-for="camera in availableCameras" :key="camera.deviceId" :value="camera">
|
||||||
|
{{ camera.label }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
@ -32,43 +74,117 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
name: "WebcamFileSource",
|
name: "WebcamFileSource",
|
||||||
data: () => ({
|
data: () => ({
|
||||||
show_modal: false,
|
lastError: undefined,
|
||||||
availableCameras: [],
|
error: false,
|
||||||
selectedCamera: undefined,
|
show_modal: false,
|
||||||
capturing: false,
|
availableCameras: [],
|
||||||
streaming: false,
|
selectedCamera: undefined,
|
||||||
stream: undefined,
|
capturing: false,
|
||||||
dataImage: undefined
|
streaming: false,
|
||||||
}),
|
stream: undefined,
|
||||||
methods: {
|
dataImage: undefined
|
||||||
async openStream() {
|
}),
|
||||||
if (!this.capturing) {
|
methods: {
|
||||||
this.capturing = true;
|
async attemptGetUserMedia(constraints) {
|
||||||
this.streaming = false;
|
this.stream = await navigator.mediaDevices.getUserMedia({
|
||||||
this.stream = await navigator.mediaDevices.getUserMedia({video: {deviceId: this.selectedCamera.deviceId}, audio: false});
|
audio: false,
|
||||||
const {video} = this.$refs;
|
video: constraints
|
||||||
video.srcObject = this.stream;
|
}).catch((error) => {
|
||||||
video.play();
|
console.error(error);
|
||||||
video.addEventListener('canplay', () => {
|
if (error.name === "NotAllowedError") this.lastError = "Camera Permission Not Granted";
|
||||||
this.streaming = true;
|
if (error.name === "NotReadableError") this.lastError = "Camera Hardware Error";
|
||||||
}, false);
|
this.lastError = "Unknown Error"
|
||||||
}
|
});
|
||||||
},
|
if (this.stream) this.allowed = true;
|
||||||
async open() {
|
|
||||||
await navigator.mediaDevices.getUserMedia({video: true});
|
|
||||||
const devices = await navigator.mediaDevices.enumerateDevices();
|
|
||||||
this.availableCameras = devices.filter(device => device.kind === "videoinput");
|
|
||||||
if (this.availableCameras.length === 0) return;
|
|
||||||
if (this.availableCameras.length === 1) {
|
|
||||||
this.selectedCamera = this.availableCameras[0];
|
|
||||||
await this.openStream();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
mounted() {
|
async assignStream() {
|
||||||
this.open();
|
this.capturing = true;
|
||||||
|
this.streaming = false;
|
||||||
|
const {video} = this.$refs;
|
||||||
|
video.srcObject = this.stream;
|
||||||
|
video.addEventListener('canplay', () => {
|
||||||
|
this.streaming = true;
|
||||||
|
localStorage.setItem("WebcamFileSource#previousDevice", this.selectedCamera.deviceId);
|
||||||
|
}, false);
|
||||||
|
await video.play();
|
||||||
|
},
|
||||||
|
closeStream() {
|
||||||
|
if (this.capturing) {
|
||||||
|
this.stream.getTracks().forEach(s => s.stop());
|
||||||
|
this.streaming = false;
|
||||||
|
this.stream = undefined;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async enumerateCameras() {
|
||||||
|
const devices = await navigator.mediaDevices.enumerateDevices();
|
||||||
|
this.availableCameras = devices.filter(device => device.kind === "videoinput");
|
||||||
|
},
|
||||||
|
async open() {
|
||||||
|
this.show_modal = true;
|
||||||
|
const previousDevice = localStorage.getItem("WebcamFileSource#previousDevice");
|
||||||
|
if (previousDevice) await this.attemptGetUserMedia({deviceId: previousDevice});
|
||||||
|
if (!this.stream) await this.attemptGetUserMedia({facingMode: "environment"});
|
||||||
|
if (!this.stream) await this.attemptGetUserMedia(true);
|
||||||
|
if (!this.stream) this.error = true;
|
||||||
|
await this.enumerateCameras();
|
||||||
|
this.selectedCamera = this.availableCameras.find(({deviceId}) => deviceId === this.stream.getTracks()[0].getSettings().deviceId);
|
||||||
|
await this.assignStream();
|
||||||
|
},
|
||||||
|
async onUserSelect() {
|
||||||
|
this.closeStream();
|
||||||
|
if (!this.dataImage) {
|
||||||
|
await this.attemptGetUserMedia({deviceId: this.selectedCamera.deviceId})
|
||||||
|
await this.assignStream();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async close() {
|
||||||
|
this.closeStream();
|
||||||
|
this.capturing = false;
|
||||||
|
this.show_modal = false;
|
||||||
|
this.dataImage = undefined;
|
||||||
|
},
|
||||||
|
captureVideoImage() {
|
||||||
|
const {video, canvas} = this.$refs;
|
||||||
|
const context = canvas.getContext('2d');
|
||||||
|
const {videoWidth, videoHeight} = video;
|
||||||
|
canvas.width = videoWidth;
|
||||||
|
canvas.height = videoHeight;
|
||||||
|
context.drawImage(video, 0, 0, videoWidth, videoHeight);
|
||||||
|
this.dataImage = canvas.toDataURL('image/jpeg', 0.5);
|
||||||
|
this.closeStream();
|
||||||
|
this.capturing = false;
|
||||||
|
},
|
||||||
|
retake() {
|
||||||
|
this.dataImage = undefined;
|
||||||
|
this.open();
|
||||||
|
},
|
||||||
|
save() {
|
||||||
|
const mimeType = this.dataImage.split(';')[0].split(':')[1];
|
||||||
|
const data = this.dataImage.split(',')[1];
|
||||||
|
const raw_data = atob(data);
|
||||||
|
const hash = nacl.crypto_hash(raw_data).reduce((a, b) => a + b.toString(16).padStart(2, "0"), "");
|
||||||
|
const image = {
|
||||||
|
name: hash.slice(0, 12) + ".jpg",
|
||||||
|
size: raw_data.length,
|
||||||
|
mime_type: mimeType,
|
||||||
|
data: data,
|
||||||
|
hash: hash,
|
||||||
|
};
|
||||||
|
this.$emit('input', [image]);
|
||||||
|
this.close();
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
navigator.mediaDevices.addEventListener("devicechange", (event) => {
|
||||||
|
console.log("devicechange");
|
||||||
|
this.enumerateCameras();
|
||||||
|
if (this.availableCameras.findIndex(({deviceId}) => deviceId === this.selectedCamera.deviceId) === -1) {
|
||||||
|
this.closeStream();
|
||||||
|
this.open();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
|
@ -1,163 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="d-inline-block">
|
|
||||||
<label @click="show_modal = true">
|
|
||||||
<slot></slot>
|
|
||||||
</label>
|
|
||||||
<div class="modal" :class="{'d-block': show_modal}" tabindex="-1">
|
|
||||||
<div class="modal-dialog modal-fullscreen">
|
|
||||||
<div class="modal-content">
|
|
||||||
<img
|
|
||||||
v-if="!capturing"
|
|
||||||
class="img-fluid rounded mx-auto d-block mb-3"
|
|
||||||
:src="dataImage"
|
|
||||||
alt="Image not available."
|
|
||||||
/>
|
|
||||||
<video
|
|
||||||
v-if="capturing"
|
|
||||||
ref="video"
|
|
||||||
class="img-fluid rounded mx-auto d-block mb-3"
|
|
||||||
>
|
|
||||||
Video stream not available.
|
|
||||||
</video>
|
|
||||||
<canvas ref="canvas" class="img-fluid d-none img-preview"/>
|
|
||||||
<div class="row" v-if="capturing && !streaming">
|
|
||||||
<div class="spinner-grow text-danger mx-auto" role="status">
|
|
||||||
<span class="sr-only">Loading...</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row m-auto">
|
|
||||||
<button v-if="!capturing" class="btn my-2 ml-auto btn-secondary" @click="openStream()">
|
|
||||||
<b-icon-camera></b-icon-camera>
|
|
||||||
</button>
|
|
||||||
<div v-if="capturing" class="btn-group my-2 ml-auto">
|
|
||||||
<button class="btn btn-success" @click="captureVideoImage()">
|
|
||||||
<b-icon-camera></b-icon-camera> Capture
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="btn"
|
|
||||||
:class="(dataImage) ? 'btn-danger' : 'btn-secondary disabled'"
|
|
||||||
@click="(dataImage) && closeStream()"
|
|
||||||
>
|
|
||||||
<b-icon-stop-fill></b-icon-stop-fill> Abort
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<select v-model="selectedCamera" class="form-select" aria-label="Select Camera Source"
|
|
||||||
@change="chooseDevice(selectedCamera)">
|
|
||||||
<option disabled value="">Select Camera Source</option>
|
|
||||||
<option v-for="camera in availableCameras" :key="camera.deviceId" :value="camera">
|
|
||||||
{{ camera.label }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div> </div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import {mapMutations} from 'vuex';
|
|
||||||
import * as BIcons from "bootstrap-icons-vue";
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'WebcamFileSourceLegacy',
|
|
||||||
components: {
|
|
||||||
...BIcons
|
|
||||||
},
|
|
||||||
emits: ["input"],
|
|
||||||
data: () => ({
|
|
||||||
capturing: false,
|
|
||||||
streaming: false,
|
|
||||||
stream: undefined,
|
|
||||||
dataImage: undefined,
|
|
||||||
show_modal: false,
|
|
||||||
availableCameras: [],
|
|
||||||
selectedCamera: undefined,
|
|
||||||
}),
|
|
||||||
methods: {
|
|
||||||
async openStream() {
|
|
||||||
if (!this.capturing) {
|
|
||||||
//await navigator.mediaDevices.getUserMedia({video: true});
|
|
||||||
this.capturing = true;
|
|
||||||
this.streaming = false;
|
|
||||||
if (this.selectedCamera)
|
|
||||||
this.stream = await navigator.mediaDevices.getUserMedia({
|
|
||||||
video: {deviceId: this.selectedCamera.deviceId},
|
|
||||||
audio: false
|
|
||||||
});
|
|
||||||
else
|
|
||||||
this.stream = await navigator.mediaDevices.getUserMedia({
|
|
||||||
video: {facingMode: "environment"},
|
|
||||||
audio: false
|
|
||||||
})
|
|
||||||
const {video} = this.$refs;
|
|
||||||
video.srcObject = this.stream;
|
|
||||||
video.addEventListener('canplay', () => {
|
|
||||||
this.streaming = true;
|
|
||||||
}, false);
|
|
||||||
await video.play();
|
|
||||||
const devices = await navigator.mediaDevices.enumerateDevices();
|
|
||||||
this.availableCameras = devices.filter(device => device.kind === "videoinput");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
captureVideoImage() {
|
|
||||||
const {video, canvas} = this.$refs;
|
|
||||||
const context = canvas.getContext('2d');
|
|
||||||
const {videoWidth, videoHeight} = video;
|
|
||||||
canvas.width = videoWidth;
|
|
||||||
canvas.height = videoHeight;
|
|
||||||
context.drawImage(video, 0, 0, videoWidth, videoHeight);
|
|
||||||
this.dataImage = canvas.toDataURL('image/jpeg', 0.5);
|
|
||||||
const mimeType = this.dataImage.split(';')[0].split(':')[1];
|
|
||||||
const data = this.dataImage.split(',')[1];
|
|
||||||
const raw_data = atob(data);
|
|
||||||
const hash = nacl.crypto_hash(raw_data).reduce((a, b) => a + b.toString(16).padStart(2, "0"), "");
|
|
||||||
const image = {
|
|
||||||
name: hash.slice(0, 12) + ".jpg",
|
|
||||||
size: raw_data.length,
|
|
||||||
mime_type: mimeType,
|
|
||||||
data: data,
|
|
||||||
hash: hash,
|
|
||||||
};
|
|
||||||
this.show_modal = false;
|
|
||||||
this.$emit('input', [image]);
|
|
||||||
this.closeStream();
|
|
||||||
},
|
|
||||||
chooseDevice(device) {
|
|
||||||
this.closeStream();
|
|
||||||
this.selectedCamera = device;
|
|
||||||
this.openStream();
|
|
||||||
},
|
|
||||||
closeStream() {
|
|
||||||
if (this.capturing) {
|
|
||||||
this.stream.getTracks().forEach(s => s.stop());
|
|
||||||
this.capturing = false;
|
|
||||||
this.streaming = false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
|
|
||||||
this.openStream();
|
|
||||||
},
|
|
||||||
beforeDestroy() {
|
|
||||||
this.closeStream();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
|
|
||||||
.camera-modal {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
z-index: 9999;
|
|
||||||
width: 100vw;
|
|
||||||
height: 100vh;
|
|
||||||
background-color: rgba(0, 0, 0, .5);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -36,6 +36,7 @@ export default defineConfig({
|
||||||
},
|
},
|
||||||
'^/auth/': {
|
'^/auth/': {
|
||||||
target: "https://toolshed.j3d1.de:8000/",
|
target: "https://toolshed.j3d1.de:8000/",
|
||||||
|
secure: false,
|
||||||
},
|
},
|
||||||
'^/docs/': {
|
'^/docs/': {
|
||||||
target: "https://toolshed.j3d1.de:8000/",
|
target: "https://toolshed.j3d1.de:8000/",
|
||||||
|
|
Loading…
Reference in a new issue