Skip to content

Commit

Permalink
GUI is working
Browse files Browse the repository at this point in the history
  • Loading branch information
GermanBluefox committed Dec 26, 2024
1 parent 34b9ee1 commit 9bfaa11
Show file tree
Hide file tree
Showing 12 changed files with 828 additions and 6,587 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,5 @@ build
.vscode
/src-admin/node_modules/
/src-admin/build/
/src-admin/package-lock.json
/package-lock.json
8 changes: 8 additions & 0 deletions io-package.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,14 @@
"role": "state"
},
"native": {}
},
{
"_id": "persons",
"type": "folder",
"common": {
"name": "Persons"
},
"native": {}
}
]
}
6,462 changes: 0 additions & 6,462 deletions package-lock.json

This file was deleted.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,9 @@
"npm": "npm i && cd src-admin && npm i",
"lint": "eslint -c eslint.config.mjs",
"lint-frontend": "cd src-admin && eslint -c eslint.config.mjs",
"build": "npm run build:ts && npm run build:gui",
"build": "npm run build:backend && npm run build:gui",
"build:gui": "node tasks.js --build",
"build:ts": "tsc -p tsconfig.build.json && node tasks.js --copy-i18n",
"build:backend": "tsc -p tsconfig.build.json",
"release": "release-script",
"release-patch": "release-script patch --yes",
"release-minor": "release-script minor --yes",
Expand Down
1 change: 1 addition & 0 deletions src-admin/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ class App extends GenericApp<GenericAppProps, AppState> {
native={this.state.native as FaceAdapterConfig}
instance={this.instance}
showToast={(text: string) => this.showToast(text)}
themeType={this.state.themeType}
/>
);
}
Expand Down
390 changes: 318 additions & 72 deletions src-admin/src/Tabs/Persons.tsx

Large diffs are not rendered by default.

227 changes: 187 additions & 40 deletions src-admin/src/components/Camera.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
import React from 'react';

import { Button, MenuItem, Select } from '@mui/material';
import { Button, IconButton, MenuItem, Select, Checkbox, FormControlLabel } from '@mui/material';
import { Delete } from '@mui/icons-material';

import { I18n } from '@iobroker/adapter-react-v5';

interface CameraProps {
width: number;
height: number;
id: string;
onImagesUpdate: (images: string[]) => void;
disabled?: boolean;
verifyAllPersons?: boolean;
onVerifyAllPersonsChanged?: (verifyAllPersons: boolean) => void;
}

interface CameraState {
images: string[];
selectedCamera: string;
Expand All @@ -18,13 +24,20 @@ interface CameraState {
export class Camera extends React.Component<CameraProps, CameraState> {
private readonly refVideo: React.RefObject<HTMLVideoElement>;
private readonly refCanvas: React.RefObject<HTMLCanvasElement>;
private readonly refOverlay: React.RefObject<HTMLDivElement>;
private readonly refOverlayFrame: React.RefObject<HTMLDivElement>;
private context2: CanvasRenderingContext2D | null = null;
private initialized = '';
private processInterval: ReturnType<typeof setInterval> | null = null;
private noMotionTimer: ReturnType<typeof setTimeout> | null = null;
private noActivityTimer: ReturnType<typeof setTimeout> | null = null;

constructor(props: CameraProps) {
super(props);
this.refVideo = React.createRef();
this.refCanvas = React.createRef();
this.refOverlay = React.createRef();
this.refOverlayFrame = React.createRef();
this.state = {
images: [],
selectedCamera: window.localStorage.getItem('selectedCamera') || '',
Expand All @@ -36,36 +49,80 @@ export class Camera extends React.Component<CameraProps, CameraState> {
await this.init();
}

componentWillUnmount(): void {
if (this.processInterval) {
clearInterval(this.processInterval);
this.processInterval = null;
}
if (this.noMotionTimer) {
clearTimeout(this.noMotionTimer);
this.noMotionTimer = null;
}
if (this.noActivityTimer) {
clearTimeout(this.noActivityTimer);
this.noActivityTimer = null;
}
if (this.refVideo.current) {
this.refVideo.current.pause();
this.refVideo.current.src = '';
this.refVideo.current.load();
}
}

static async getVideoDevices(): Promise<MediaDeviceInfo[]> {
const devices = await navigator.mediaDevices.enumerateDevices();
return devices.filter(device => device.kind === 'videoinput');
const uniqueDevices: MediaDeviceInfo[] = [];
devices.forEach(device => {
if (device.kind === 'videoinput') {
if (!uniqueDevices.find(dev => dev.deviceId === device.deviceId)) {
uniqueDevices.push(device);
}
}
});
return uniqueDevices;
}

async init(): Promise<void> {
let selectedCamera = this.state.selectedCamera;
let selectedCamera = this.state.selectedCamera === 'default' ? '' : this.state.selectedCamera;

const cameras = this.state.cameras || (await Camera.getVideoDevices());
if (!selectedCamera || !cameras.find(camera => camera.deviceId === selectedCamera)) {
selectedCamera = cameras[0].deviceId;
}

if (this.refVideo.current && this.refCanvas.current && this.initialized !== selectedCamera) {
if (this.refOverlay.current) {
this.refOverlay.current.style.opacity = '0';
}
this.context2 = this.context2 || this.refCanvas.current.getContext('2d');
this.initialized = this.state.selectedCamera;

this.setState({ cameras, selectedCamera }, async () => {
this.setState({ cameras, selectedCamera: selectedCamera || 'default' }, async () => {
if (typeof window.navigator.mediaDevices?.getUserMedia === 'function') {
this.refVideo.current!.srcObject = await window.navigator.mediaDevices.getUserMedia({
const stream = await window.navigator.mediaDevices.getUserMedia({
audio: false,
video: {
deviceId: this.state.selectedCamera,
deviceId: selectedCamera,
facingMode: 'user',
height: { min: 480 },
},
});
this.refVideo.current!.onloadedmetadata = async () => {
await this.refVideo.current!.play();
console.log('Playing live media stream');
};

if (stream) {
this.refVideo.current!.srcObject = stream;
this.refVideo.current!.onloadedmetadata = async () => {
await this.refVideo.current!.play();
console.log(
`Playing live media stream: ${this.refVideo.current!.clientWidth}x${this.refVideo.current!.clientHeight}`,
);
if (this.refOverlayFrame.current && this.refOverlay.current) {
this.refOverlay.current.style.opacity = '1';
this.refOverlayFrame.current.style.width = `${Math.floor(
(this.refOverlay.current.clientHeight * 480) / 640,
)}px`;
}
};
}
}
});
}
Expand All @@ -78,69 +135,159 @@ export class Camera extends React.Component<CameraProps, CameraState> {
takeSnapshot(): void {
if (this.refVideo.current && this.context2 && this.refCanvas.current) {
const images = [...this.state.images];
this.context2.drawImage(this.refVideo.current, 0, 0, this.props.width, this.props.height);
this.refCanvas.current.height = this.refVideo.current.clientHeight;
this.refCanvas.current.width = Math.round((this.refVideo.current.clientHeight * 480) / 640);

// We need portrait
this.context2.drawImage(
this.refVideo.current,
(this.refCanvas.current.width - this.refVideo.current.clientWidth) / 2,
0,
this.refVideo.current.clientWidth,
this.refVideo.current.clientHeight,
);
images.push(this.refCanvas.current.toDataURL('image/jpeg'));
if (images.length > 4) {
images.splice(0, images.length - 4);
}
this.setState({ images });
this.setState({ images }, () => this.props.onImagesUpdate(this.state.images));
}
}

renderImage(index: number): React.JSX.Element {
return (
<div style={{ position: 'relative', width: 200, borderRadius: 5 }}>
<IconButton
style={{
position: 'absolute',
top: 4,
right: 4,
opacity: 0.7,
}}
disabled={!!this.props.disabled}
onClick={() => {
const images = [...this.state.images];
images.splice(index, 1);
this.setState({ images }, () => this.props.onImagesUpdate(this.state.images));
}}
>
<Delete />
</IconButton>

<img
style={{ width: '100%', height: 'auto' }}
key={index}
src={this.state.images[index]}
alt="screenshot"
/>
</div>
);
}

render(): React.JSX.Element {
return (
<div
style={{
width: '100%',
height: this.props.height + 48,
display: 'flex',
flexDirection: 'column',
flexDirection: 'row',
gap: 8,
}}
>
<canvas
style={{ opacity: 0, position: 'absolute', top: 0, left: 0, pointerEvents: 'none' }}
ref={this.refCanvas}
id={this.props.id}
width={100}
height={100}
></canvas>
<div style={{ width: this.props.width, height: '100%' }}>
<Select
fullWidth
style={{ marginBottom: 16 }}
variant="standard"
disabled={!this.state.cameras}
value={this.state.selectedCamera}
value={this.state.selectedCamera || 'default'}
onChange={e => {
this.setState({ selectedCamera: e.target.value }, async () => {
this.setState({ selectedCamera: e.target.value || 'default' }, async () => {
await this.init();
});
}}
>
{this.state.cameras?.map(camera => (
<MenuItem
key={camera.deviceId}
value={camera.deviceId}
key={camera.deviceId || 'default'}
value={camera.deviceId || 'default'}
>
{camera.deviceId}
{camera.label || I18n.t('default')}
</MenuItem>
))}
</Select>
<video
playsInline
ref={this.refVideo}
autoPlay
width={this.props.width}
height={this.props.height}
></video>
<Button onClick={() => this.takeSnapshot()}>{I18n.t('Screenshot')}</Button>
<div style={{ position: 'relative' }}>
<video
playsInline
ref={this.refVideo}
autoPlay
style={{
zIndex: 0,
width: 480,
}}
// width={this.props.width}
// height={this.props.height}
></video>
<div
ref={this.refOverlay}
style={{
position: 'absolute',
width: '100%',
height: 'calc(100% - 4px)',
top: 0,
left: 0,
zIndex: 1,
}}
>
<div
ref={this.refOverlayFrame}
style={{
height: '100%',
border: 'dashed 3px green',
opacity: 0.5,
boxSizing: 'border-box',
margin: 'auto',
}}
></div>
</div>
</div>
</div>
<div>
<div style={{ width: '100%' }}>
<Button
disabled={!!this.props.disabled}
onClick={() => this.takeSnapshot()}
variant="outlined"
>
{I18n.t('Screenshot')}
</Button>
{this.props.onVerifyAllPersonsChanged ? (
<FormControlLabel
control={
<Checkbox
checked={this.props.verifyAllPersons}
onChange={() =>
this.props.onVerifyAllPersonsChanged!(!this.props.verifyAllPersons)
}
/>
}
label={I18n.t('Check all persons')}
/>
) : null}
</div>

<div style={{ display: 'flex', gap: 4, marginTop: 12, flexWrap: 'wrap' }}>
{this.state.images.map((_image, i) => this.renderImage(i))}
</div>
</div>
<canvas
style={{ opacity: 0, position: 'absolute', top: 0, left: 0, pointerEvents: 'none' }}
ref={this.refCanvas}
id={this.props.id}
width={this.props.width}
height={this.props.height}
></canvas>
{this.state.images.map((image, i) => (
<img
key={i}
src={image}
alt="screenshot"
/>
))}
</div>
);
}
Expand Down
Loading

0 comments on commit 9bfaa11

Please sign in to comment.