|
@@ -1,191 +1,62 @@
|
|
|
<script setup lang="ts">
|
|
|
-import { withResolvers } from '@/tools';
|
|
|
-import { getMediaStream } from '@/tools/camera.tool';
|
|
|
-import { tryOnMounted, tryOnUnmounted, useElementSize, useEventListener } from '@vueuse/core';
|
|
|
+import Camera from '@/assets/camera.html?url';
|
|
|
|
|
|
|
|
|
-let _stream: MediaStream;
|
|
|
+const { width, height, zoom, preview = true } = defineProps<{
|
|
|
+ width: number;
|
|
|
+ height: number;
|
|
|
+ zoom?: number;
|
|
|
+ preview?: boolean
|
|
|
+}>();
|
|
|
+const emits = defineEmits<{ loaded: [] }>();
|
|
|
+const style = computed(() => `width: ${ width }px;height: ${ height }px;`);
|
|
|
|
|
|
+const snapshot = ref<string | void>();
|
|
|
|
|
|
-const { constraints = {} } = defineProps<{ constraints?: MediaTrackConstraints, multiple?: number; }>();
|
|
|
-const width = defineModel('width', { default: 0 });
|
|
|
-const height = defineModel('height', { default: 0 });
|
|
|
-const multiple = defineModel('multiple', { default: 1 });
|
|
|
-
|
|
|
-const offsetX = ref(0);
|
|
|
-const offsetY = ref(0);
|
|
|
-
|
|
|
-watch([ width, height, multiple ], ([ w, h, m ]) => {
|
|
|
- applyConstraints({ width: w * m, height: h * m });
|
|
|
-});
|
|
|
-
|
|
|
-
|
|
|
-const styleValue = computed(() => {
|
|
|
- return width.value && height.value
|
|
|
- ? { width: `${ width.value }px`, height: `${ height.value }px` }
|
|
|
- : void 0;
|
|
|
-});
|
|
|
-
|
|
|
-const containerStyleValue = reactive({
|
|
|
- width: `${ width.value }px`,
|
|
|
- height: `${ height.value }px`,
|
|
|
-});
|
|
|
-
|
|
|
-const container = useTemplateRef<HTMLElement>('camera-container');
|
|
|
-const videoRef = ref<HTMLVideoElement | null>(null);
|
|
|
-const canvasRef = ref<HTMLCanvasElement | null>(null);
|
|
|
-
|
|
|
-/* 根容器 宽高 */
|
|
|
-const root = useElementSize(() => container.value?.parentElement);
|
|
|
-
|
|
|
-const getVideoTrack = () => {
|
|
|
- return videoRef.value?.srcObject instanceof MediaStream
|
|
|
- ? videoRef.value.srcObject.getVideoTracks()[ 0 ]
|
|
|
- : new MediaStreamTrack();
|
|
|
-};
|
|
|
-
|
|
|
-useEventListener(videoRef, 'canplay', (event) => {
|
|
|
- const video = event.target as HTMLVideoElement;
|
|
|
- const canvas = canvasRef.value;
|
|
|
-
|
|
|
- const track = getVideoTrack();
|
|
|
-
|
|
|
- const settings = track.getSettings();
|
|
|
- const { width: w = 1, height: h = 1, aspectRatio = w / h } = settings;
|
|
|
- const reverse = aspectRatio > 1;
|
|
|
- const _width = reverse ? h : w;
|
|
|
- const _height = reverse ? w : h;
|
|
|
- video.setAttribute('width', `${ _width }`);
|
|
|
- video.setAttribute('height', `${ _height }`);
|
|
|
-
|
|
|
- containerStyleValue.width = `${ _width }px`;
|
|
|
- containerStyleValue.height = `${ _height }px`;
|
|
|
-
|
|
|
- offsetX.value = (
|
|
|
- _width - width.value
|
|
|
- ) / 2;
|
|
|
- offsetY.value = (
|
|
|
- _height - height.value
|
|
|
- ) / 2;
|
|
|
-
|
|
|
- video.play().then(
|
|
|
- () => {},
|
|
|
- // (_) => (error.value = _)
|
|
|
- );
|
|
|
- console.group('[log] camera:', '流媒体可播放');
|
|
|
- console.log(`width=${ _width }, height=${ _height }`);
|
|
|
- console.log(`浏览器支持: 可缩放 ${ !!(
|
|
|
- <any> navigator.mediaDevices.getSupportedConstraints()
|
|
|
- ).zoom }`);
|
|
|
- console.log(`摄像头支持: 可缩放 ${ !!(
|
|
|
- <any> track.getCapabilities()
|
|
|
- )[ 'zoom' ] }`);
|
|
|
- console.log(`当前设置缩放: ${ multiple.value }`);
|
|
|
- console.log(`当前实际缩放: (${ _width / width.value }, ${ _height / height.value })`);
|
|
|
- console.log(`图象偏移距离: (${ offsetX.value }, ${ offsetY.value })`);
|
|
|
- console.groupEnd();
|
|
|
-});
|
|
|
-
|
|
|
-tryOnMounted(() => {
|
|
|
- const _container = container.value!;
|
|
|
- canvasRef.value = container.value!.querySelector('canvas');
|
|
|
- videoRef.value = container.value!.querySelector('video');
|
|
|
-
|
|
|
- const style = getComputedStyle(_container);
|
|
|
- const _width = width.value || Number.parseInt(style.width);
|
|
|
- const _height = height.value || Number.parseInt(style.height);
|
|
|
- const _multiple = multiple.value ?? 1;
|
|
|
-
|
|
|
- canvasRef.value!.width = _width;
|
|
|
- canvasRef.value!.height = _height;
|
|
|
-
|
|
|
- applyConstraints({ ...constraints, width: _width * _multiple, height: _height * _multiple });
|
|
|
-});
|
|
|
-
|
|
|
-tryOnUnmounted(() => {
|
|
|
- destroy();
|
|
|
- videoRef.value = null;
|
|
|
- canvasRef.value = null;
|
|
|
-});
|
|
|
-
|
|
|
-async function applyConstraints(constraints: Partial<MediaTrackConstraints>) {
|
|
|
- const { width, height, ..._constraints } = constraints;
|
|
|
- // @ts-ignore
|
|
|
- if ( !!width && !!height ) {
|
|
|
- destroy();
|
|
|
- _stream = await getMediaStream(<any> { ..._constraints, width, height, facingMode: 'user' });
|
|
|
- videoRef.value!.srcObject = _stream;
|
|
|
- } else {
|
|
|
- await _stream.getVideoTracks()[ 0 ]?.applyConstraints(constraints);
|
|
|
+const cameraFrameRef = useTemplateRef<HTMLIFrameElement & {
|
|
|
+ contentWindow: {
|
|
|
+ loadCamera(props: { width: number, height: number, zoom?: number }): Promise<void>;
|
|
|
+ handle(promise?: Promise<void>): string;
|
|
|
}
|
|
|
-}
|
|
|
-
|
|
|
-function destroy() {
|
|
|
- const tracks = _stream?.getTracks() ?? [];
|
|
|
- for ( const track of tracks ) track.stop();
|
|
|
- _stream = void 0 as any;
|
|
|
- if ( videoRef.value ) {
|
|
|
- videoRef.value.pause();
|
|
|
- videoRef.value.srcObject = null as any;
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-
|
|
|
-const handle = async (promise: Promise<boolean>) => {
|
|
|
- const video = videoRef.value;
|
|
|
- const canvas = canvasRef.value;
|
|
|
- if ( !video || !canvas ) return;
|
|
|
- if ( video.paused ) await video.play();
|
|
|
-
|
|
|
- const context = canvas.getContext('2d')!;
|
|
|
- context.clearRect(0, 0, canvas.width, canvas.height);
|
|
|
- context.drawImage(video, offsetX.value, offsetY.value, video.width, video.height, 0, 0, canvas.width, canvas.height);
|
|
|
- video.pause();
|
|
|
+}>('camera-frame');
|
|
|
|
|
|
- promise.then(value => video.paused && value ? video.play() : void 0);
|
|
|
-};
|
|
|
-
|
|
|
-const get = async (type: 'base64' | 'blob', mime = 'image/png') => {
|
|
|
- const video = videoRef.value;
|
|
|
- const canvas = canvasRef.value;
|
|
|
- if ( !video || !canvas ) return;
|
|
|
- if ( !video.paused ) await handle(Promise.resolve(true));
|
|
|
-
|
|
|
- switch ( type ) {
|
|
|
- case 'blob':
|
|
|
- const { promise, resolve } = withResolvers<Blob>();
|
|
|
- canvas.toBlob(<any> resolve, mime, 0.98);
|
|
|
- return promise;
|
|
|
- case 'base64':
|
|
|
- return canvas.toDataURL(mime);
|
|
|
- }
|
|
|
+const loadCamera = async () => {
|
|
|
+ await cameraFrameRef.value?.contentWindow.loadCamera?.({ width, height, zoom });
|
|
|
+ emits('loaded');
|
|
|
};
|
|
|
|
|
|
defineExpose({
|
|
|
- handle, get,
|
|
|
+ handle() {
|
|
|
+ if ( !preview || !snapshot.value ) {
|
|
|
+ snapshot.value = cameraFrameRef.value?.contentWindow.handle?.();
|
|
|
+ } else {
|
|
|
+ snapshot.value = void 0;
|
|
|
+ }
|
|
|
+ return snapshot.value;
|
|
|
+ },
|
|
|
});
|
|
|
</script>
|
|
|
<template>
|
|
|
- <div class="camera-container size-full" ref="camera-container" :style="containerStyleValue">
|
|
|
- <canvas style="display: none;"></canvas>
|
|
|
- <video></video>
|
|
|
- <slot :style="styleValue" :offsetX="offsetX" :offsetY="offsetY"></slot>
|
|
|
+ <div class="relative camera-container" :style="style">
|
|
|
+ <iframe ref="camera-frame" :src="Camera" @load="loadCamera()"></iframe>
|
|
|
+ <img v-if="snapshot" :src="snapshot" alt="图像" />
|
|
|
+ <slot name="shade" :style="style" :width="width" :height="height"></slot>
|
|
|
</div>
|
|
|
</template>
|
|
|
<style scoped lang="scss">
|
|
|
.camera-container {
|
|
|
- position: absolute;
|
|
|
- z-index: 0;
|
|
|
- display: flex;
|
|
|
- justify-content: center;
|
|
|
- align-items: center;
|
|
|
+ iframe {
|
|
|
+ clip-path: url("#shade");
|
|
|
+ }
|
|
|
|
|
|
- > * {
|
|
|
- position: absolute;
|
|
|
+ img {
|
|
|
+ object-fit: scale-down;
|
|
|
}
|
|
|
|
|
|
- video {
|
|
|
- clip-path: url("#shade");
|
|
|
+ > * {
|
|
|
+ position: absolute;
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
}
|
|
|
}
|
|
|
</style>
|