Skip to content

Commit 0465f5b

Browse files
#12 Added Video Feed Frontend Component & few UI tweaks
1 parent 569affd commit 0465f5b

27 files changed

+584
-114
lines changed

backend/src/app.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88
from src.resources.teacher import Teacher, TeacherRegister, TeacherLogin
99
from src.resources.student import StudentList, StudentAdd, StudentDelete
1010
from src.resources.attendance import AttendanceList
11-
from src.resources.video_feed import VideoFeed
11+
from src.resources.video_feed import (
12+
VideoFeedList, VideoFeedAdd, VideoFeed, VideoFeedPreview, VideoFeedStop, VideoFeedStart, VideoFeedDelete
13+
)
1214

1315

1416
app = Flask(__name__)
@@ -23,12 +25,27 @@ def handle_marshmallow_validation(err):
2325
return jsonify(err.messages), 400
2426

2527

28+
# /teacher
2629
api.add_resource(Teacher, "/teacher/<int:teacher_id>")
2730
api.add_resource(TeacherRegister, "/register")
2831
api.add_resource(TeacherLogin, "/login")
32+
33+
# /dashboard
2934
api.add_resource(Dashboard, "/dashboard")
30-
api.add_resource(VideoFeed, "/video_feed")
35+
36+
# /video_feed
37+
api.add_resource(VideoFeedList, "/video_feeds")
38+
api.add_resource(VideoFeedAdd, "/video_feeds/add")
39+
api.add_resource(VideoFeed, "/video_feeds/<string:feed_id>")
40+
api.add_resource(VideoFeedPreview, "/video_feeds/preview/<string:feed_id>")
41+
api.add_resource(VideoFeedStop, "/video_feeds/stop/<string:feed_id>")
42+
api.add_resource(VideoFeedStart, "/video_feeds/start/<string:feed_id>")
43+
api.add_resource(VideoFeedDelete, "/video_feeds/delete/<string:feed_id>")
44+
45+
# /students
3146
api.add_resource(StudentList, "/students")
3247
api.add_resource(StudentAdd, "/students/add")
3348
api.add_resource(StudentDelete, "/students/delete/<int:student_id>")
49+
50+
# /attendance
3451
api.add_resource(AttendanceList, "/attendance")

backend/src/libs/base_camera.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,12 @@ class BaseCamera(object):
5555
thread = None # background thread that reads frames from camera
5656
frame = None # current frame is stored here by background thread
5757
last_access = 0 # time of last client access to the camera
58+
stopped = False
5859
event = CameraEvent()
5960

6061
def __init__(self):
6162
"""Start the background camera thread if it isn't running yet."""
63+
self.stopped = False
6264
if BaseCamera.thread is None:
6365
BaseCamera.last_access = time.time()
6466

@@ -89,16 +91,25 @@ def frames():
8991
def _thread(cls):
9092
"""Camera background thread."""
9193
print('Starting camera thread.')
94+
# get new frames
9295
frames_iterator = cls.frames()
9396
for frame in frames_iterator:
9497
BaseCamera.frame = frame
9598
BaseCamera.event.set() # send signal to clients
9699
time.sleep(0)
97-
98100
# if there hasn't been any clients asking for frames in
99101
# the last 10 seconds then stop the thread
100102
if time.time() - BaseCamera.last_access > 10:
101103
frames_iterator.close()
102104
print('Stopping camera thread due to inactivity.')
103105
break
106+
# stop the video feed by changing VIDEO_FEED global variable to false
107+
if cls.stopped:
108+
frames_iterator.close()
109+
print('Camera manually stopped.')
110+
break
104111
BaseCamera.thread = None
112+
113+
@classmethod
114+
def stop_feed(cls):
115+
cls.stopped = True

backend/src/libs/web_utils.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
ENCODINGS_FILE
1515
)
1616
from src.libs.base_camera import BaseCamera
17-
from src.models import StudentModel, AttendanceModel
17+
from src.models import StudentModel, AttendanceModel, VideoFeedModel
1818

1919

2020
class DetectionCamera(BaseCamera):
@@ -47,11 +47,15 @@ def frames(cls):
4747

4848
class RecognitionCamera(BaseCamera):
4949
video_source = 0
50+
# this class variable will help to process every other frame of video to save time
5051
process_this_frame = True
5152

52-
def __init__(self):
53+
def __init__(self, video_feed: VideoFeedModel):
5354
if VIDEO_SOURCE:
55+
self.video_feed = video_feed
56+
print(video_feed.id)
5457
RecognitionCamera.set_video_source(VIDEO_SOURCE)
58+
# RecognitionCamera.set_video_source(video_feed.url)
5559
super(RecognitionCamera, self).__init__()
5660

5761
@classmethod
@@ -61,8 +65,8 @@ def set_video_source(cls, source):
6165
@classmethod
6266
def frames(cls):
6367
print("[INFO] starting video stream...")
64-
# store input video stream in camera variable
6568
camera = cv2.VideoCapture(cls.video_source)
69+
# store input video stream in camera variable
6670
if not camera.isOpened():
6771
raise RuntimeError('Could not start camera.')
6872

@@ -82,12 +86,12 @@ def frames(cls):
8286
while True:
8387
# read current frame
8488
_, img = camera.read()
85-
8689
yield cls.recognize_n_attendance(img, attendance, data, known_students)
8790

8891
@classmethod
8992
def recognize_n_attendance(cls, frame: np.ndarray, attendance: AttendanceModel,
9093
data: Dict, known_students: Dict) -> bytes:
94+
print("playing...")
9195
# convert the input frame from BGR to RGB then resize it to have
9296
# a width of 750px (to speedup processing)
9397
rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)

backend/src/models.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
from typing import List
2+
from uuid import uuid4
23
from datetime import date as dt, datetime as dtime
34

4-
from sqlalchemy import Column, Integer, String, Date, TIMESTAMP, ForeignKey
5+
from sqlalchemy import Column, Integer, String, Boolean, Date, TIMESTAMP, ForeignKey
56
from sqlalchemy.ext.declarative import declarative_base
67
from sqlalchemy.orm import relationship, backref
78

@@ -118,3 +119,31 @@ def save_to_db(self) -> None:
118119
def delete_from_db(self) -> None:
119120
Session.delete(self)
120121
Session.commit()
122+
123+
124+
class VideoFeedModel(Base):
125+
__tablename__ = "video_feeds"
126+
127+
id = Column(String(30), nullable=False, primary_key=True)
128+
is_active = Column(Boolean, default=False)
129+
url = Column(String, nullable=False)
130+
131+
@classmethod
132+
def find_by_id(cls, _id: str) -> "VideoFeedModel":
133+
return Session.query(cls).filter_by(id=_id).first()
134+
135+
@classmethod
136+
def find_by_url(cls, url: str) -> "VideoFeedModel":
137+
return Session.query(cls).filter_by(url=url).first()
138+
139+
@classmethod
140+
def find_all(cls) -> List["VideoFeedModel"]:
141+
return Session.query(cls).all()
142+
143+
def save_to_db(self) -> None:
144+
Session.add(self)
145+
Session.commit()
146+
147+
def delete_from_db(self) -> None:
148+
Session.delete(self)
149+
Session.commit()
Lines changed: 106 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,52 @@
1-
from flask import Response
1+
from flask import Response, request
22
from flask_restful import Resource
3+
from flask_jwt_extended import jwt_required
34

5+
from src.db import Session
6+
from src.libs.strings import gettext
7+
from src.models import VideoFeedModel
8+
from src.schemas import VideoFeedSchema
49
from src.libs.web_utils import RecognitionCamera
510

611

7-
class VideoFeed(Resource):
12+
video_feed_schema = VideoFeedSchema()
13+
video_feed_list_schema = VideoFeedSchema(many=True)
14+
15+
16+
# TODO: change this to support multiple feeds
17+
class VideoFeedList(Resource):
818
@classmethod
19+
@jwt_required
920
def get(cls):
21+
return video_feed_list_schema.dump(VideoFeedModel.find_all()), 200
22+
23+
24+
class VideoFeed(Resource):
25+
@classmethod
26+
def get(cls, feed_id: str):
1027
"""Video streaming route "/video_feed". Put this in the src attribute of an img tag."""
11-
return Response(
12-
VideoFeed.gen_frame(RecognitionCamera()),
13-
mimetype='multipart/x-mixed-replace; boundary=frame'
14-
)
28+
video_feed = VideoFeedModel.find_by_id(feed_id)
29+
30+
if video_feed:
31+
return video_feed_schema.dump(video_feed), 200
32+
33+
return {"message": gettext('video_feed_not_found')}, 404
34+
35+
36+
# TODO: make this get() to work with @jwt_required
37+
class VideoFeedPreview(Resource):
38+
@classmethod
39+
def get(cls, feed_id: str):
40+
"""Video streaming route. Put this route in the src attribute of an img tag."""
41+
video_feed = VideoFeedModel.find_by_id(feed_id)
42+
43+
if video_feed:
44+
return Response(
45+
cls.gen_frame(RecognitionCamera(video_feed)),
46+
mimetype='multipart/x-mixed-replace; boundary=frame'
47+
)
48+
49+
return {"message": gettext('video_feed_not_found')}, 404
1550

1651
@classmethod
1752
def gen_frame(cls, camera):
@@ -22,3 +57,68 @@ def gen_frame(cls, camera):
2257
b'--frame\r\n'
2358
b'Content-Type: image/jpeg\r\n\r\n' + frame + b'\r\n'
2459
) # concat frame one by one and show result
60+
61+
62+
# TODO: VideoFeedAdd Resource
63+
class VideoFeedAdd(Resource):
64+
"""Adds a video feed to `feeds` table in the database"""
65+
@classmethod
66+
@jwt_required
67+
def post(cls):
68+
video_feed_json = request.get_json()
69+
70+
video_feed = video_feed_schema.load(video_feed_json, session=Session)
71+
72+
try:
73+
video_feed.save_to_db()
74+
except:
75+
return {"message": gettext('error_inserting')}, 500
76+
77+
return video_feed_schema.dump(video_feed), 201
78+
79+
80+
class VideoFeedStop(Resource):
81+
@classmethod
82+
@jwt_required
83+
def get(cls, feed_id: str):
84+
video_feed = VideoFeedModel.find_by_id(feed_id)
85+
if video_feed:
86+
RecognitionCamera.stop_feed()
87+
try:
88+
video_feed.is_active = False
89+
video_feed.save_to_db()
90+
except:
91+
return {"message": gettext('internal_server_error')}, 500
92+
return {"message": gettext('video_feed_stopped')}, 200
93+
94+
return {"message": gettext('video_feed_not_found')}, 404
95+
96+
97+
class VideoFeedStart(Resource):
98+
"""Restart video feed for specific feed given by its feed_id"""
99+
@classmethod
100+
@jwt_required
101+
def get(cls, feed_id: str):
102+
video_feed = VideoFeedModel.find_by_id(feed_id)
103+
if video_feed:
104+
# RecognitionCamera.start_feed()
105+
try:
106+
video_feed.is_active = True
107+
video_feed.save_to_db()
108+
except:
109+
return {"message": gettext('internal_server_error')}, 500
110+
return {"message": gettext('video_feed_stopped')}, 200
111+
112+
return {"message": gettext('video_feed_not_found')}, 404
113+
114+
115+
class VideoFeedDelete(Resource):
116+
@classmethod
117+
@jwt_required
118+
def delete(cls, feed_id: str):
119+
video_feed = VideoFeedModel.find_by_id(feed_id)
120+
if video_feed:
121+
video_feed.delete_from_db()
122+
return {"message": gettext('video_feed_deleted').format(video_feed.id)}, 200
123+
124+
return {"message": gettext('video_feed_not_found')}, 404

backend/src/schemas.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from marshmallow_sqlalchemy import SQLAlchemyAutoSchema, auto_field
22
from marshmallow_sqlalchemy.fields import Nested
33

4-
from src.models import TeacherModel, StudentModel, AttendanceModel
4+
from src.models import TeacherModel, StudentModel, AttendanceModel, VideoFeedModel
55

66

77
class TeacherSchema(SQLAlchemyAutoSchema):
@@ -31,3 +31,11 @@ class Meta:
3131
StudentSchema,
3232
many=True
3333
)
34+
35+
36+
class VideoFeedSchema(SQLAlchemyAutoSchema):
37+
class Meta:
38+
model = VideoFeedModel
39+
# load_only = () # during deserialization dictionary -> object
40+
dump_only = ("is_active",) # during serialization object -> dictionary
41+
load_instance = True # Optional: deserialize to object/model instances

backend/strings/en-us.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,13 @@
99
"login_to_continue": "Failure! More data available if you log in",
1010

1111
"error_inserting": "An error occurred while inserting the item",
12+
1213
"student_deleted": "'{}' with id={} was removed",
13-
"student_not_found": "Student not found"
14+
"student_not_found": "Student not found",
15+
16+
"video_feed_deleted": "Video Feed for Classroom {} was removed",
17+
"video_feed_not_found": "Video Feed not found.",
18+
"video_feed_stopped": "Video Feed has been stopped",
19+
20+
"internal_server_error": "Internal Server Error"
1421
}

frontend/src/app/app-routing.module.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@ import { AuthGuard } from './guards/auth.guard';
77
import { DashboardComponent } from './components/dashboard/dashboard.component';
88
import { RegisterComponent } from './components/register/register.component';
99
import { LoginComponent } from './components/login/login.component';
10-
import { VideoFeedComponent } from './components/video-feed/video-feed.component';
11-
import { StudentComponent } from './components/student/student.component';
10+
import { VideoFeedPreviewComponent } from './components/video-feed-preview/video-feed-preview.component';
11+
import { VideoFeedListComponent } from './components/video-feed-list/video-feed-list.component';
12+
import { StudentListComponent } from './components/student-list/student-list.component';
1213
import { AttendanceComponent } from './components/attendance/attendance.component';
1314
import { PageNotFoundComponent } from './components/page-not-found/page-not-found.component';
1415

@@ -18,8 +19,9 @@ const routes: Routes = [
1819
{ path: 'dashboard', component: DashboardComponent },
1920
{ path: 'register', component: RegisterComponent },
2021
{ path: 'login', component: LoginComponent },
21-
{ path: 'video_feed', component: VideoFeedComponent, canActivate:[AuthGuard] },
22-
{ path: 'students', component: StudentComponent, canActivate:[AuthGuard] },
22+
{ path: 'video_feeds', component: VideoFeedListComponent, canActivate:[AuthGuard] },
23+
{ path: 'video_feeds/preview/:feed_id', component: VideoFeedPreviewComponent, canActivate:[AuthGuard] },
24+
{ path: 'students', component: StudentListComponent, canActivate:[AuthGuard] },
2325
{ path: 'attendance', component: AttendanceComponent, canActivate:[AuthGuard] },
2426
{ path: '**', component: PageNotFoundComponent }
2527
];
@@ -32,6 +34,8 @@ export class AppRoutingModule { }
3234
export const routingComponents = [
3335
DashboardComponent,
3436
RegisterComponent, LoginComponent,
35-
VideoFeedComponent, StudentComponent, AttendanceComponent,
37+
VideoFeedListComponent, VideoFeedPreviewComponent,
38+
StudentListComponent,
39+
AttendanceComponent,
3640
PageNotFoundComponent
3741
]

frontend/src/app/app.component.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
<div class="collapse navbar-collapse" id="navbarSupportedContent">
77
<ul class="navbar-nav mr-auto">
88
<li class="nav-item" *ngIf="loggedIn()">
9-
<a class="nav-link" routerLink="/video_feed" routerLinkActive="active">Video Feed</a>
9+
<a class="nav-link" routerLink="/video_feeds" routerLinkActive="active">Video Feeds</a>
1010
</li>
1111
<li class="nav-item" *ngIf="loggedIn()">
1212
<a class="nav-link" routerLink="/students" routerLinkActive="active">Students</a>

0 commit comments

Comments
 (0)