Skip to content

Commit

Permalink
feat: handle camera preview
Browse files Browse the repository at this point in the history
  • Loading branch information
Jozwiaczek committed Sep 13, 2021
1 parent 15e1e22 commit 8bb911b
Show file tree
Hide file tree
Showing 25 changed files with 634 additions and 34 deletions.
1 change: 1 addition & 0 deletions packages/api/src/enums/webSocketEvent.enum.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export enum WebSocketEvent {
CHECK_DEVICE_CONNECTION = 'checkDeviceConnection',
TOGGLE_GATE = 'toggleGate',
SET_NGROK_DATA = 'setNgrokData',
}
2 changes: 2 additions & 0 deletions packages/api/src/modules/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Module } from '@nestjs/common';
import { ThrottlerModule } from '@nestjs/throttler';

import { AuthModule } from './auth/auth.module';
import { CameraModule } from './camera/camera.module';
import { Config } from './config/config';
import { ConfigModule } from './config/config.module';
import { ConfigurationModule } from './configuration/configuration.module';
Expand Down Expand Up @@ -41,6 +42,7 @@ import { WebsocketModule } from './websocket/websocket.module';
PushNotificationsModule,
ExternalIntegrationsModule,
ConfigurationModule,
CameraModule,
],
})
export class AppModule {}
83 changes: 83 additions & 0 deletions packages/api/src/modules/camera/camera.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import {
BadGatewayException,
CACHE_MANAGER,
Controller,
Get,
Inject,
Logger,
Res,
} from '@nestjs/common';
import { Cache } from 'cache-manager';
import { Response } from 'express';
import https from 'https';

import { Auth } from '../auth/decorators/auth.decorator';

export interface NgrokData {
url: string;
auth: string;
}

@Auth()
@Controller('camera')
export class CameraController {
private readonly logger: Logger = new Logger(CameraController.name);

constructor(@Inject(CACHE_MANAGER) private readonly cacheManager: Cache) {}

@Get()
async proxyCameraRequest(@Res() res: Response) {
const ngrokData = (await this.cacheManager.get('ngrokData')) as NgrokData;
if (!ngrokData?.url || !ngrokData?.auth) {
throw new BadGatewayException();
}

res.set({
'Content-Type': 'multipart/x-mixed-replace; boundary=BoundaryString',
'Cache-Control': 'no-cache, private',
'Max-Age': 0,
Expires: 0,
Connection: 'close',
Pragma: 'no-cache',
});

const ngrokRequest = https
.request(ngrokData.url, { auth: ngrokData.auth }, (httpRes) => {
res.on('close', () => {
ngrokRequest.destroy(new Error('Client disconnected'));
});

httpRes.on('data', (dataBuffer) => {
if (httpRes.statusCode !== 200) {
return;
}
res.write(dataBuffer);
});

httpRes.on('close', () => {
if (httpRes.statusCode !== 200) {
this.logger.error(
`Ngrok connection closed with status code: ${
httpRes.statusCode?.toString() as string
}`,
);
res.status(httpRes.statusCode ?? 500).end();
return;
}

this.logger.log(
`Ngrok connection closed with status code: ${httpRes.statusCode?.toString()}`,
);
res.status(httpRes.statusCode ?? 200).end();
});
})
.on('error', (e) => {
if (e.message === 'Client disconnected') {
// Info about disconnect handled above in ngrok request
return;
}
this.logger.error(e.message);
});
ngrokRequest.end();
}
}
13 changes: 13 additions & 0 deletions packages/api/src/modules/camera/camera.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { CacheModule, Module } from '@nestjs/common';

import { AuthModule } from '../auth/auth.module';
import { TokenModule } from '../auth/token/token.module';
import { RepositoryModule } from '../repository/repository.module';
import { UsersModule } from '../users/users.module';
import { CameraController } from './camera.controller';

@Module({
imports: [UsersModule, AuthModule, RepositoryModule, TokenModule, CacheModule.register()],
controllers: [CameraController],
})
export class CameraModule {}
21 changes: 20 additions & 1 deletion packages/api/src/modules/websocket/websocket.gateway.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import { Injectable, Logger, ServiceUnavailableException } from '@nestjs/common';
import {
CACHE_MANAGER,
Inject,
Injectable,
Logger,
MethodNotAllowedException,
ServiceUnavailableException,
} from '@nestjs/common';
import {
OnGatewayConnection,
OnGatewayDisconnect,
Expand All @@ -7,9 +14,11 @@ import {
WebSocketGateway,
WebSocketServer,
} from '@nestjs/websockets';
import { Cache } from 'cache-manager';
import { Server, Socket } from 'socket.io';

import { WebSocketEvent } from '../../enums/webSocketEvent.enum';
import { NgrokData } from '../camera/camera.controller';
import { TicketService } from '../ticket/ticket.service';
import { WebsocketConfigService } from './config/websocket-config.service';

Expand All @@ -21,6 +30,7 @@ import { WebsocketConfigService } from './config/websocket-config.service';
})
export class Websocket implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect {
constructor(
@Inject(CACHE_MANAGER) private readonly cacheManager: Cache,
private readonly ticketService: TicketService,
private readonly websocketConfigService: WebsocketConfigService,
) {}
Expand Down Expand Up @@ -68,6 +78,15 @@ export class Websocket implements OnGatewayInit, OnGatewayConnection, OnGatewayD
this.deviceClient.send(WebSocketEvent.TOGGLE_GATE);
}

@SubscribeMessage(WebSocketEvent.SET_NGROK_DATA)
async setNgrokData(client: Socket, ngrokData: NgrokData): Promise<void> {
if (this.deviceClient && client.id === this.deviceClient.id) {
await this.cacheManager.set('ngrokData', ngrokData, { ttl: 60 * 60 * 24 }); // Expires every 1 day
} else {
throw new MethodNotAllowedException();
}
}

@SubscribeMessage(WebSocketEvent.CHECK_DEVICE_CONNECTION)
checkDeviceConnection(client: Socket): void {
this.logger.log(`client:${this.clients.get(client.id) ?? ''}`);
Expand Down
4 changes: 2 additions & 2 deletions packages/api/src/modules/websocket/websocket.module.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { Module } from '@nestjs/common';
import { CacheModule, Module } from '@nestjs/common';

import { TicketModule } from '../ticket/ticket.module';
import { WebsocketConfigModule } from './config/websocket-config.module';
import { Websocket } from './websocket.gateway';

@Module({
imports: [TicketModule, WebsocketConfigModule],
imports: [CacheModule.register(), TicketModule, WebsocketConfigModule],
providers: [Websocket],
exports: [Websocket],
})
Expand Down
4 changes: 4 additions & 0 deletions packages/client/src/i18n/resources/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,10 @@ const en = {
dashboard: {
title: 'Dashboard',
sections: {
camera: {
title: 'Camera preview',
loadingPreview: 'Loading camera preview...',
},
toggling: {
swipeUpToToggle: 'Swipe up to toggle gate',
toggleSuccess: 'Successfully toggled',
Expand Down
4 changes: 4 additions & 0 deletions packages/client/src/i18n/resources/pl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,10 @@ const pl: TranslationStructure = {
dashboard: {
title: 'Pulpit',
sections: {
camera: {
title: 'Podgląd kamery',
loadingPreview: 'Ładowanie podglądu...',
},
toggling: {
swipeUpToToggle: 'Przesuń w górę aby aktywować bramę',
toggleSuccess: 'Aktywowano bramę',
Expand Down
19 changes: 19 additions & 0 deletions packages/client/src/pages/authorized/Dashboard/Dashboard.styled.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import styled, { css } from 'styled-components';

export const RowSection = styled.div(
({ theme: { breakpoints, down } }) => css`
display: flex;
flex-wrap: wrap;
gap: 100px;
margin: 80px 0 20px;
${down(breakpoints.md)} {
gap: 0;
align-items: center;
flex-direction: column-reverse;
& > :not(:first-child) {
margin-bottom: 50px;
}
}
`,
);
7 changes: 6 additions & 1 deletion packages/client/src/pages/authorized/Dashboard/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { useTranslation } from 'react-i18next';
import { useAxios } from '../../../hooks';
import registerWebPush from '../../../utils/registerWebPush';
import { Title } from '../AuthorizedPages.styled';
import { RowSection } from './Dashboard.styled';
import CameraPreviewSection from './sections/CameraPreviewSection';
import TogglingSection from './sections/TogglingSection';

const Dashboard = () => {
Expand All @@ -17,7 +19,10 @@ const Dashboard = () => {
return (
<>
<Title data-testid="dashboard-title">{t('routes.dashboard.title')}</Title>
<TogglingSection />
<RowSection>
<TogglingSection />
<CameraPreviewSection />
</RowSection>
</>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import styled, { css } from 'styled-components';

export const LoadingContainer = styled.div(
({ theme: { palette, sizes, down, breakpoints } }) => css`
position: relative;
width: 520px;
height: 345px;
border-radius: ${sizes.borderRadius};
background: ${palette.background.paper};
${down(breakpoints.sm)} {
width: 100%;
padding-bottom: 66.66%;
}
`,
);

export const LoadingContent = styled.div`
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
display: flex;
justify-content: center;
align-items: center;
text-align: center;
`;

export const CameraPreview = styled.img<CameraPreviewProps>(
({ isLoaded, theme: { sizes, down, breakpoints } }) => css`
border-radius: ${sizes.borderRadius};
width: 520px;
height: 345px;
${!isLoaded &&
css`
visibility: hidden;
`};
${down(breakpoints.sm)} {
width: 100%;
height: unset;
}
`,
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
interface CameraPreviewProps {
isLoaded: boolean;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import React, { useCallback, useLayoutEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';

import { CameraPreview, LoadingContainer, LoadingContent } from './CameraPreviewSection.styled';

const CameraPreviewSection = () => {
const cameraURL = process.env.REACT_APP_API_URL && `${process.env.REACT_APP_API_URL}/camera`;
const previewRef = useRef<HTMLImageElement>(null);
const [isPreviewLoaded, setIsPreviewLoaded] = useState(false);
const { t } = useTranslation();

const reloadCameraPreview = useCallback(() => {
if (previewRef.current && cameraURL) {
const requestPreviewTime = new Date().getTime();
previewRef.current.src = `${cameraURL}/?t=${requestPreviewTime}`;
}
}, [cameraURL]);

useLayoutEffect(() => {
reloadCameraPreview();
window.addEventListener('focus', reloadCameraPreview);
return () => {
window.removeEventListener('focus', reloadCameraPreview);
};
}, [cameraURL, reloadCameraPreview]);

return (
<div>
{!isPreviewLoaded && (
<LoadingContainer>
<LoadingContent>
<h3>{t('routes.dashboard.sections.camera.loadingPreview')}</h3>
</LoadingContent>
</LoadingContainer>
)}
<CameraPreview
ref={previewRef}
isLoaded={isPreviewLoaded}
alt={t('routes.dashboard.sections.camera.title')}
onLoad={() => {
setIsPreviewLoaded(true);
}}
/>
</div>
);
};

export default CameraPreviewSection;
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { ITheme } from '../../../../../theme/Theme';

export const ToggleSliderWrapper = styled.div(
({ theme: { down, breakpoints } }) => css`
margin: 80px 0 20px;
display: inline-block;
${down(breakpoints.sm)} {
display: flex;
Expand Down
8 changes: 7 additions & 1 deletion packages/device/.env.example
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
#API
# API
API_URL=http://localhost:3030
AUTH_TICKET=ticket

# Camera
CAMERA_ENABLED=false
CAMERA_USE_WIRED=true
NGROK_LOCAL_CAMERA_ADDRESS=http://localhost:8081
NGROK_REGION=eu
1 change: 1 addition & 0 deletions packages/device/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"dependencies": {
"chalk": "^4.1.2",
"dotenv": "^10.0.0",
"ngrok": "^4.2.2",
"rpio": "^2.4.2",
"socket.io-client": "^4.1.3"
},
Expand Down
Loading

0 comments on commit 8bb911b

Please sign in to comment.