diff --git a/config-fe/package-lock.json b/config-fe/package-lock.json index c4637ea..84eee81 100644 --- a/config-fe/package-lock.json +++ b/config-fe/package-lock.json @@ -3575,6 +3575,11 @@ "color-convert": "^1.9.0" } }, + "any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha1-q8av7tzqUugJzcA3au0845Y10X8=" + }, "anymatch": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", @@ -7935,6 +7940,14 @@ "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=" }, + "get-video-dimensions": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/get-video-dimensions/-/get-video-dimensions-1.0.0.tgz", + "integrity": "sha1-/H5ayBw5JEH1uG1Q3XeDiptTFHo=", + "requires": { + "mz": "^1.2.1" + } + }, "getpass": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", @@ -12570,6 +12583,16 @@ "integrity": "sha512-5VmmjADBqS4++8pTI6poSRJ+chHdaoI4XErcQPM5w4QfwaDl+FQlSI0iOgWbYDn6CBCbDRKaSCcEiN2K5aHNGQ==", "dev": true }, + "mz": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-1.3.0.tgz", + "integrity": "sha1-BvCT/dmVagbTfhsegTROJ0eMQvA=", + "requires": { + "native-or-bluebird": "1", + "thenify": "3", + "thenify-all": "1" + } + }, "nan": { "version": "2.14.2", "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz", @@ -12606,6 +12629,11 @@ } } }, + "native-or-bluebird": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/native-or-bluebird/-/native-or-bluebird-1.2.0.tgz", + "integrity": "sha1-OcR7/Xgl0fuf+tMiEK4l2q3xAck=" + }, "native-url": { "version": "0.2.6", "resolved": "https://registry.npmjs.org/native-url/-/native-url-0.2.6.tgz", @@ -17294,6 +17322,22 @@ "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=" }, + "thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "requires": { + "any-promise": "^1.0.0" + } + }, + "thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha1-GhkY1ALY/D+Y+/I02wvMjMEOlyY=", + "requires": { + "thenify": ">= 3.1.0 < 4" + } + }, "throat": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/throat/-/throat-5.0.0.tgz", diff --git a/config-fe/package.json b/config-fe/package.json index 47afecf..70b2347 100644 --- a/config-fe/package.json +++ b/config-fe/package.json @@ -22,6 +22,7 @@ "clsx": "^1.1.1", "cors": "^2.8.5", "fontsource-roboto": "^3.0.3", + "get-video-dimensions": "^1.0.0", "konva": "^7.2.0", "line-reader": "^0.4.0", "prop-types": "^15.7.2", diff --git a/config-fe/src/api/FeedApi.tsx b/config-fe/src/api/FeedApi.tsx index e0a8d81..36f83a7 100644 --- a/config-fe/src/api/FeedApi.tsx +++ b/config-fe/src/api/FeedApi.tsx @@ -43,9 +43,13 @@ export async function createFeed(feed: Feed) { return data; } +export async function updateFeed(feed: Feed) { + let { data } = await axios.put(`/feeds/${feed.id}`, feed); + return data; +} + export async function deleteFeed(feed: Feed) { let response = await axios.delete(`/feeds/${feed.id}`) - return response.status === 204; } diff --git a/config-fe/src/api/axios.js b/config-fe/src/api/axios.js index cc99a93..ef2dcdf 100644 --- a/config-fe/src/api/axios.js +++ b/config-fe/src/api/axios.js @@ -2,7 +2,7 @@ import axios from "axios"; import * as qs from "qs"; import { PathLike } from "fs"; -let API_URL = process.env.API_URL || "http://localhost:5050"; +let API_URL = process.env.API_URL || "http://localhost:5000"; const apiConfig = { returnRejectedPromiseOnError: true, diff --git a/config-fe/src/features/card/customVideoCard.jsx b/config-fe/src/features/card/customVideoCard.jsx index dacd693..58eb297 100644 --- a/config-fe/src/features/card/customVideoCard.jsx +++ b/config-fe/src/features/card/customVideoCard.jsx @@ -29,7 +29,7 @@ const styles = theme => ({ class customVideoCard extends React.Component { FetchUrlObjectForBlob(blob) { - if(blob) { + if (blob) { let url = URL.createObjectURL(blob); return url; } else { @@ -66,18 +66,18 @@ class customVideoCard extends React.Component { - + {(this.props.Active) ? - this.props.OnStartStop(this.props.Id)}> - - - : - this.props.OnStartStop(this.props.Id)}> - - + this.props.OnStartStop(this.props.Id)}> + + + : + this.props.OnStartStop(this.props.Id)}> + + } diff --git a/config-fe/src/features/custom/Popup.jsx b/config-fe/src/features/custom/Popup.jsx index 4d08474..8fa0fa6 100644 --- a/config-fe/src/features/custom/Popup.jsx +++ b/config-fe/src/features/custom/Popup.jsx @@ -26,6 +26,18 @@ const useStyles = makeStyles({ marginBottom: 15, textAlign: "left" }, + successMessage: { + fontSize: 14, + fontWeight: 400, + marginBottom: 15, + paddingLeft: 10, + textAlign: "left", + backgroundColor: "#57c22d", + color: "white", + borderRadius: 5, + lineHeight: 3, + + }, pos: { marginBottom: 12 }, @@ -134,6 +146,7 @@ function Popup(props) { const pulseclasses = pulseStyles(); const [value, setValue] = React.useState(0); const [open, setOpen] = React.useState(false); + const [success, setSuccess] = React.useState(false); const [newFeed, setNewFeed] = React.useState({ name: "", location: "", @@ -174,8 +187,39 @@ function Popup(props) { const onFormSubmit = e => { e.preventDefault(); console.log(newFeed) - props.AddVideoSource(newFeed) + //get video resolution + getVideoDimensionsOf(newFeed.url) + .then(({ width, height }) => { + console.log("Video width: " + width); + console.log("Video height: " + height); + }); + + props.AddVideoSource(newFeed); + + setSuccess(true); + } + + function getVideoDimensionsOf(url) { + return new Promise(function (resolve) { + // create the video element + let video = document.createElement('video'); + + // place a listener on it + video.addEventListener("loadedmetadata", function () { + // retrieve dimensions + let height = this.videoHeight; + let width = this.videoWidth; + // send back result + resolve({ + height: height, + width: width + }); + }, false); + + // start download meta-datas + video.src = url; + }); } return ( @@ -269,6 +313,11 @@ function Popup(props) { + + {success ? + Video successfully added! + : null} + New feed config: @@ -277,6 +326,7 @@ function Popup(props) { editName(e.target)} /> @@ -290,12 +340,14 @@ function Popup(props) { className={classes.urlField} id="outlined-basic" label="Description" + required="Required" onInput={e => editDescription(e.target)} /> editUrl(e.target)} /> diff --git a/config-fe/src/pages/Configuration.jsx b/config-fe/src/pages/Configuration.jsx index 38fc442..a850a9b 100644 --- a/config-fe/src/pages/Configuration.jsx +++ b/config-fe/src/pages/Configuration.jsx @@ -3,6 +3,7 @@ import { withStyles } from "@material-ui/core/styles"; import { Grid, Typography, Card, TextField, FormGroup, Chip, FormControlLabel, Checkbox, Button, CardContent, FormControl, MenuItem, InputLabel, Box } from "@material-ui/core"; import Canvas from "../features/custom/Canvas"; import { SaveConfig, LoadConfig, UpdateConfig } from "../store/modules/configuration/configSlice"; +import { UpdateVideoSource, GetVideoSource } from "../store/modules/video_sources/sourcesSlice"; import { withRouter, Link } from 'react-router-dom'; import { connect } from 'react-redux'; import Detections from '../Model/Detections'; @@ -75,10 +76,11 @@ const useStyles = (theme) => ({ const mapStateToProps = state => ({ config: state.config.configSource, + feed: state.sources.singleVideoSource, snapshots: state.sources.snapshots }); -const mapDispatch = { LoadConfig, SaveConfig, UpdateConfig } +const mapDispatch = { LoadConfig, SaveConfig, UpdateConfig, UpdateVideoSource, GetVideoSource } class Configuration extends React.Component { @@ -106,12 +108,15 @@ class Configuration extends React.Component { value: [], detectionArray: [], fixedOptions: [], + feed: "", } } async componentDidMount() { await this.props.LoadConfig(this.props.match.params.id); - if(this.props.config != null) { + await this.props.GetVideoSource(this.props.match.params.id); + console.log(this.props.feed); + if (this.props.config != null) { this.setState({ existingConfig: true }); this.configuration.name = this.props.config.name; this.configuration.resolution = this.props.config.resolution; @@ -120,14 +125,18 @@ class Configuration extends React.Component { this.state.fixedOptions.push(object.detectionType); }); } + if (this.props.feed != null) { + this.setState({ feed: this.props.feed }); + + } this.setState({ isLoading: false }); } SnapshotAvailable() { - if(this.props.snapshots && this.props.snapshots.length > 0) { + if (this.props.snapshots && this.props.snapshots.length > 0) { let blob = this.props.snapshots.find(x => x.feed_id == this.props.match.params.id)?.snapshot; - if(blob) { + if (blob) { return true; } else { return false; @@ -138,9 +147,9 @@ class Configuration extends React.Component { } FetchBlobPreview() { - if(this.props.snapshots && this.props.snapshots.length > 0) { + if (this.props.snapshots && this.props.snapshots.length > 0) { let blob = this.props.snapshots.find(x => x.feed_id == this.props.match.params.id)?.snapshot; - if(blob) { + if (blob) { let url = URL.createObjectURL(blob); return url; } else { @@ -167,11 +176,17 @@ class Configuration extends React.Component { e.preventDefault(); //check if we are updating an existing config or creating a new one. - if(this.state.existingConfig) { + if (this.state.existingConfig) { this.configuration.id = this.props.config.id; + const feed = { ...this.state.feed, name: this.configuration.name } + this.setState(() => ({ feed })) this.props.UpdateConfig(this.configuration) + this.props.UpdateVideoSource(feed); } else { + const feed = { ...this.state.feed, name: this.configuration.name } + this.setState(() => ({ feed })) this.props.SaveConfig(this.configuration) + this.props.UpdateVideoSource(feed); } this.configuration.detection_types.map(object => { @@ -199,8 +214,8 @@ class Configuration extends React.Component { - Configuratie - Camera X - + Configuratie - {this.state.feed.name} + @@ -217,7 +232,7 @@ class Configuration extends React.Component { > this.configuration.name = e.target.value} /> @@ -233,6 +248,7 @@ class Configuration extends React.Component { > @@ -245,10 +261,11 @@ class Configuration extends React.Component { className={classes.title2} color="textSecondary" gutterBottom + > @@ -298,7 +315,7 @@ class Configuration extends React.Component { @@ -427,7 +444,7 @@ class Configuration extends React.Component { variant="contained" color="default" className={classes.Buttons} - + > Cancel @@ -443,7 +460,7 @@ class Configuration extends React.Component { Camera Preview - + diff --git a/config-fe/src/pages/Dashboard.jsx b/config-fe/src/pages/Dashboard.jsx index b260260..1fca4ef 100644 --- a/config-fe/src/pages/Dashboard.jsx +++ b/config-fe/src/pages/Dashboard.jsx @@ -58,6 +58,7 @@ class Dashboard extends React.Component { Active={value.active} Snapshot={(this.props.snapshots && this.props.snapshots.length > 0) ? this.props.snapshots.find(x => x.feed_id == value.id)?.snapshot : null} OnStartStop={this.OnChangeFeedActive} + Feed={value} /> ) })} diff --git a/config-fe/src/pages/Livefeed.jsx b/config-fe/src/pages/Livefeed.jsx index 7d11bfa..7d07a43 100644 --- a/config-fe/src/pages/Livefeed.jsx +++ b/config-fe/src/pages/Livefeed.jsx @@ -4,7 +4,7 @@ import stompClient from 'rabbitMQ/rabbitMQ' import { Client, Message } from '@stomp/stompjs'; import { Container } from '@material-ui/core'; -import { withStyles } from '@material-ui/core/styles'; +import { withStyles } from '@material-ui/core/styles'; const styles = theme => ({ videoPlayer: { @@ -35,7 +35,7 @@ class Livefeed extends React.Component { //attempt to connect to rabbitmq this.stompClient.activate(); - this.stompClient.onConnect = (frame) => { + this.stompClient.onConnect = (frame) => { console.log("CONNECTED TO RABBITMQ") this.stompClient.subscribe("/queue/video-queue", onQueueMessage); @@ -46,6 +46,7 @@ class Livefeed extends React.Component { var onQueueMessage = (message) => { let blob = new Blob([message.binaryBody]); let url = URL.createObjectURL(blob); + console.log(url) document.querySelector("#image").src = url; } } @@ -56,12 +57,12 @@ class Livefeed extends React.Component { render() { const { classes } = this.props; - return( + return ( - + ) } } -export default withStyles(styles, {withTheme: true})(Livefeed); \ No newline at end of file +export default withStyles(styles, { withTheme: true })(Livefeed); \ No newline at end of file diff --git a/config-fe/src/store/modules/video_sources/sourcesSlice.tsx b/config-fe/src/store/modules/video_sources/sourcesSlice.tsx index 61b11f9..a79e956 100644 --- a/config-fe/src/store/modules/video_sources/sourcesSlice.tsx +++ b/config-fe/src/store/modules/video_sources/sourcesSlice.tsx @@ -1,41 +1,46 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; -import { Feed, getFeeds, getFeedDetails, createFeed, deleteFeed, Snapshot, FeedChangeEvent, ChangeFeed } from 'api/FeedApi'; +import { Feed, getFeeds, getFeedDetails, createFeed, updateFeed, deleteFeed, Snapshot, FeedChangeEvent, ChangeFeed } from 'api/FeedApi'; import stompClient from 'rabbitMQ/rabbitMQ' import store from 'store/store'; -const initialState: { videoSources: Feed[], snapshots: Snapshot[] } = { +const initialState: { videoSources: Feed[], singleVideoSource: (null | Feed), snapshots: Snapshot[] } = { videoSources: [], + singleVideoSource: null, snapshots: [] }; export const sourcesSlice = createSlice({ - name: 'sources', - initialState, - reducers: { - LoadSources: (state, action : PayloadAction) => { - //set sources loaded from API to local state. - state.videoSources = action.payload; - }, - AddSource: (state, action : PayloadAction) => { - //add video source to local state array - state.videoSources.push(action.payload); - }, - RemoveSource: (state, action : PayloadAction) => { - //delete from local state array. - var indexOfVideoToDelete = state.videoSources.indexOf(action.payload); - state.videoSources.splice(indexOfVideoToDelete, 1); - }, - ChangeFeedActive: (state, action : PayloadAction) => { - var foundIndex = state.videoSources.findIndex(x => x.id == action.payload.id); - state.videoSources[foundIndex] = action.payload; - }, - LoadSnapshots: (state, action: PayloadAction) => { - state.snapshots = action.payload; - } + name: 'sources', + initialState, + reducers: { + LoadSources: (state, action: PayloadAction) => { + //set sources loaded from API to local state. + state.videoSources = action.payload; + }, + GetSource: (state, action: PayloadAction) => { + //set sources loaded from API to local state. + state.singleVideoSource = action.payload; + }, + AddSource: (state, action: PayloadAction) => { + //add video source to local state array + state.videoSources.push(action.payload); + }, + RemoveSource: (state, action: PayloadAction) => { + //delete from local state array. + var indexOfVideoToDelete = state.videoSources.indexOf(action.payload); + state.videoSources.splice(indexOfVideoToDelete, 1); }, - }); - -export const { LoadSources, AddSource, RemoveSource, LoadSnapshots, ChangeFeedActive } = sourcesSlice.actions; + ChangeFeedActive: (state, action: PayloadAction) => { + var foundIndex = state.videoSources.findIndex(x => x.id == action.payload.id); + state.videoSources[foundIndex] = action.payload; + }, + LoadSnapshots: (state, action: PayloadAction) => { + state.snapshots = action.payload; + } + }, +}); + +export const { LoadSources, GetSource, AddSource, RemoveSource, LoadSnapshots, ChangeFeedActive } = sourcesSlice.actions; // The function below is called a selector and allows us to select a value from // the state. Selectors can also be defined inline where they're used instead of @@ -50,11 +55,22 @@ export const LoadVideoSources = () => async (dispatch: any) => { dispatch(LoadSources(feeds)); } +export const GetVideoSource = (id: number) => async (dispatch: any) => { + let feed = await getFeedDetails(id); + dispatch(GetSource(feed)); +} + export const AddVideoSource = (videoSource: Feed) => async (dispatch: any) => { let feed = await createFeed(videoSource); dispatch(AddSource(feed)); } +export const UpdateVideoSource = (videoSource: Feed) => async (dispatch: any) => { + let feed = await updateFeed(videoSource); + console.log(feed); + dispatch(AddSource(feed)); +} + export const RemoveVideoSource = (videoSource: Feed) => async (dispatch: any) => { let deleted = await deleteFeed(videoSource); @@ -65,12 +81,12 @@ export const RemoveVideoSource = (videoSource: Feed) => async (dispatch: any) => } } -export const StartStopVideoSource = (FeedChangeEvent: FeedChangeEvent) => async (dispatch : any) => { +export const StartStopVideoSource = (FeedChangeEvent: FeedChangeEvent) => async (dispatch: any) => { let updatedfeed = await ChangeFeed(FeedChangeEvent); dispatch(ChangeFeedActive(updatedfeed)); } -export const FetchSnapshots = () => async (dispatch : any) => { +export const FetchSnapshots = () => async (dispatch: any) => { let tempSnapshots = [] as Snapshot[]; //subscriptions storing the client subscribe messages. let subscriptions = [] as any[]; @@ -82,7 +98,7 @@ export const FetchSnapshots = () => async (dispatch : any) => { let sub = subscriptions[indexOfSub]; let blob = new Blob([message.binaryBody]); - let snapshot: Snapshot = { feed_id: sub.feed_id, snapshot: blob}; + let snapshot: Snapshot = { feed_id: sub.feed_id, snapshot: blob }; tempSnapshots.push(snapshot); //finally unsubscribe from the subscription so we only receive one frame and remove the sub from the list. @@ -99,13 +115,13 @@ export const FetchSnapshots = () => async (dispatch : any) => { //attempt to connect to rabbitmq client.activate(); - client.onConnect = (frame) => { - for(let feed of store.getState().sources.videoSources) { + client.onConnect = (frame) => { + for (let feed of store.getState().sources.videoSources) { //only fetch for active feeds. - if(feed.active) { + if (feed.active) { //subscribe to the queue for messages. - var sub = client.subscribe("/queue/video-queue", onQueueMessage, {'ack': 'client-individual', 'prefetch-count': 1 }); - + var sub = client.subscribe("/queue/video-queue", onQueueMessage, { 'ack': 'client-individual', 'prefetch-count': 1 }); + subscriptions.push({ subscription: sub, feed_id: feed.id @@ -115,7 +131,7 @@ export const FetchSnapshots = () => async (dispatch : any) => { }; //wait 3 seconds then deactivate the client to give the client time to receive a single frame. - setTimeout(function() { + setTimeout(function () { client.deactivate(); //in the end, pass all the results to the snapshot state.