Skip to content

Commit 523c135

Browse files
Support added for multiple video feeds
1 parent 0465f5b commit 523c135

File tree

8 files changed

+101
-59
lines changed

8 files changed

+101
-59
lines changed

backend/src/libs/base_camera.py

Lines changed: 49 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
1-
import time
1+
# -*- coding: utf-8 -*-
2+
# From https://github.com/miguelgrinberg/flask-video-streaming
3+
import logging
24
import threading
5+
import time
6+
37
try:
48
from greenlet import getcurrent as get_ident
59
except ImportError:
@@ -8,6 +12,8 @@
812
except ImportError:
913
from _thread import get_ident
1014

15+
logger = logging.getLogger(__name__)
16+
1117

1218
class CameraEvent(object):
1319
"""An Event-like class that signals all active clients when a new frame is
@@ -52,64 +58,73 @@ def clear(self):
5258

5359

5460
class BaseCamera(object):
55-
thread = None # background thread that reads frames from camera
56-
frame = None # current frame is stored here by background thread
57-
last_access = 0 # time of last client access to the camera
58-
stopped = False
59-
event = CameraEvent()
61+
thread = {} # background thread that reads frames from camera
62+
frame = {} # current frame is stored here by background thread
63+
last_access = {} # time of last client access to the camera
64+
event = {}
65+
running = {}
6066

61-
def __init__(self):
67+
def __init__(self, unique_id=None):
6268
"""Start the background camera thread if it isn't running yet."""
63-
self.stopped = False
64-
if BaseCamera.thread is None:
65-
BaseCamera.last_access = time.time()
69+
self.unique_id = unique_id
70+
BaseCamera.event[self.unique_id] = CameraEvent()
71+
BaseCamera.running[self.unique_id] = True
72+
73+
if self.unique_id not in BaseCamera.thread:
74+
BaseCamera.thread[self.unique_id] = None
75+
76+
if BaseCamera.thread[self.unique_id] is None:
77+
BaseCamera.last_access[self.unique_id] = time.time()
6678

6779
# start background frame thread
68-
BaseCamera.thread = threading.Thread(target=self._thread)
69-
BaseCamera.thread.start()
80+
BaseCamera.thread[self.unique_id] = threading.Thread(target=self._thread, args=(self.unique_id,))
81+
BaseCamera.thread[self.unique_id].start()
7082

7183
# wait until frames are available
7284
while self.get_frame() is None:
7385
time.sleep(0)
7486

7587
def get_frame(self):
7688
"""Return the current camera frame."""
77-
BaseCamera.last_access = time.time()
89+
BaseCamera.last_access[self.unique_id] = time.time()
7890

7991
# wait for a signal from the camera thread
80-
BaseCamera.event.wait()
81-
BaseCamera.event.clear()
92+
BaseCamera.event[self.unique_id].wait()
93+
BaseCamera.event[self.unique_id].clear()
8294

83-
return BaseCamera.frame
95+
return BaseCamera.frame[self.unique_id]
8496

8597
@staticmethod
8698
def frames():
8799
""""Generator that returns frames from the camera."""
88-
raise RuntimeError('Must be implemented by subclasses.')
100+
raise RuntimeError('Must be implemented by subclasses')
89101

90102
@classmethod
91-
def _thread(cls):
103+
def _thread(cls, unique_id):
92104
"""Camera background thread."""
93-
print('Starting camera thread.')
94-
# get new frames
105+
print('Starting camera thread')
95106
frames_iterator = cls.frames()
96107
for frame in frames_iterator:
97-
BaseCamera.frame = frame
98-
BaseCamera.event.set() # send signal to clients
108+
BaseCamera.frame[unique_id] = frame
109+
BaseCamera.event[unique_id].set() # send signal to clients
99110
time.sleep(0)
100-
# if there hasn't been any clients asking for frames in
111+
# if there haven't been any clients asking for frames in
101112
# the last 10 seconds then stop the thread
102-
if time.time() - BaseCamera.last_access > 10:
113+
# if time.time() - BaseCamera.last_access[unique_id] > 10:
114+
# frames_iterator.close()
115+
# print(f"[{unique_id}] Stopping camera thread due to inactivity")
116+
# break
117+
if not BaseCamera.running[unique_id]:
103118
frames_iterator.close()
104-
print('Stopping camera thread due to inactivity.')
119+
print(f"[{unique_id}] Camera thread instructed to shut down")
105120
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
111-
BaseCamera.thread = None
121+
BaseCamera.thread[unique_id] = None
122+
BaseCamera.running[unique_id] = False
112123

113-
@classmethod
114-
def stop_feed(cls):
115-
cls.stopped = True
124+
@staticmethod
125+
def stop(unique_id):
126+
BaseCamera.running[unique_id] = False
127+
128+
@staticmethod
129+
def is_running(unique_id):
130+
return BaseCamera.running[unique_id]

backend/src/libs/web_utils.py

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -50,13 +50,10 @@ class RecognitionCamera(BaseCamera):
5050
# this class variable will help to process every other frame of video to save time
5151
process_this_frame = True
5252

53-
def __init__(self, video_feed: VideoFeedModel):
54-
if VIDEO_SOURCE:
55-
self.video_feed = video_feed
56-
print(video_feed.id)
57-
RecognitionCamera.set_video_source(VIDEO_SOURCE)
58-
# RecognitionCamera.set_video_source(video_feed.url)
59-
super(RecognitionCamera, self).__init__()
53+
# def __init__(self, unique_id=None):
54+
# if VIDEO_SOURCE:
55+
# RecognitionCamera.set_video_source(VIDEO_SOURCE)
56+
# super(RecognitionCamera, self).__init__(unique_id)
6057

6158
@classmethod
6259
def set_video_source(cls, source):
@@ -66,6 +63,7 @@ def set_video_source(cls, source):
6663
def frames(cls):
6764
print("[INFO] starting video stream...")
6865
camera = cv2.VideoCapture(cls.video_source)
66+
6967
# store input video stream in camera variable
7068
if not camera.isOpened():
7169
raise RuntimeError('Could not start camera.')
@@ -91,7 +89,6 @@ def frames(cls):
9189
@classmethod
9290
def recognize_n_attendance(cls, frame: np.ndarray, attendance: AttendanceModel,
9391
data: Dict, known_students: Dict) -> bytes:
94-
print("playing...")
9592
# convert the input frame from BGR to RGB then resize it to have
9693
# a width of 750px (to speedup processing)
9794
rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)

backend/src/resources/video_feed.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from flask import Response, request
22
from flask_restful import Resource
3-
from flask_jwt_extended import jwt_required
3+
from flask_jwt_extended import jwt_required, get_jwt_identity
44

55
from src.db import Session
66
from src.libs.strings import gettext
@@ -23,6 +23,7 @@ def get(cls):
2323

2424
class VideoFeed(Resource):
2525
@classmethod
26+
@jwt_required
2627
def get(cls, feed_id: str):
2728
"""Video streaming route "/video_feed". Put this in the src attribute of an img tag."""
2829
video_feed = VideoFeedModel.find_by_id(feed_id)
@@ -33,18 +34,23 @@ def get(cls, feed_id: str):
3334
return {"message": gettext('video_feed_not_found')}, 404
3435

3536

36-
# TODO: make this get() to work with @jwt_required
37+
# TODO: make this get() to work with @jwt_required by sending response with Flask-RESTful instead of Response()
3738
class VideoFeedPreview(Resource):
3839
@classmethod
3940
def get(cls, feed_id: str):
4041
"""Video streaming route. Put this route in the src attribute of an img tag."""
4142
video_feed = VideoFeedModel.find_by_id(feed_id)
42-
43+
feed_url = video_feed.url
44+
camera_stream = RecognitionCamera
45+
if feed_url == "0":
46+
feed_url = 0
47+
camera_stream.set_video_source(feed_url)
4348
if video_feed:
44-
return Response(
45-
cls.gen_frame(RecognitionCamera(video_feed)),
49+
resp = Response(
50+
cls.gen_frame(camera_stream(unique_id=feed_id)),
4651
mimetype='multipart/x-mixed-replace; boundary=frame'
4752
)
53+
return resp
4854

4955
return {"message": gettext('video_feed_not_found')}, 404
5056

@@ -83,7 +89,7 @@ class VideoFeedStop(Resource):
8389
def get(cls, feed_id: str):
8490
video_feed = VideoFeedModel.find_by_id(feed_id)
8591
if video_feed:
86-
RecognitionCamera.stop_feed()
92+
RecognitionCamera.stop(feed_id)
8793
try:
8894
video_feed.is_active = False
8995
video_feed.save_to_db()

frontend/src/app/components/video-feed-list/video-feed-list.component.html

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,11 @@
5252
<tr *ngFor="let feed of feeds; index as i">
5353
<th scope="row">{{ feed.id }}</th>
5454
<td>
55-
<div *ngIf="!feed.is_active">Inactive</div>
56-
<div *ngIf="feed.is_active" style="color:red">
55+
<div *ngIf="!feed.is_active">
56+
<fa-icon [icon]="circleIcon" style="color:red" size="xs"></fa-icon>&nbsp;
57+
Inactive
58+
</div>
59+
<div *ngIf="feed.is_active" style="color:green">
5760
<fa-icon [icon]="circleIcon" size="xs"></fa-icon>&nbsp;
5861
<a class="nav-link" style="padding:0rem;display:inline" [routerLink]="generatePreviewUrl(feed.id)">Live</a>
5962
</div>

frontend/src/app/components/video-feed-list/video-feed-list.component.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Component, OnInit } from '@angular/core';
22
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
3+
import { Router } from '@angular/router';
34

45
import { faVideo, faCircle, faPlay, faStop } from '@fortawesome/free-solid-svg-icons';
56
import { faTrashAlt } from '@fortawesome/free-regular-svg-icons';
@@ -27,7 +28,7 @@ export class VideoFeedListComponent implements OnInit {
2728
addResponseMsg = '';
2829
delResponseMsg = '';
2930

30-
constructor(private _videoFeedService: VideoFeedService, private fb: FormBuilder) { }
31+
constructor(private _videoFeedService: VideoFeedService, private fb: FormBuilder, private router: Router) { }
3132

3233
ngOnInit(): void {
3334
this.videoFeedForm = this.fb.group({
@@ -99,6 +100,7 @@ export class VideoFeedListComponent implements OnInit {
99100
res => console.log(res),
100101
err => console.log(err)
101102
);
103+
this.router.navigate(['/video_feeds/preview', feed.id]);
102104
return feed.is_active = true;
103105
}
104106
}

frontend/src/app/components/video-feed-preview/video-feed-preview.component.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<div class="row">
33
<!-- <div class="col-lg-8 offset-lg-2"> -->
44
<div class=" mt-4 col-lg-9">
5-
<img *ngIf="get_is_active()" style="-webkit-user-select: none;" [src]="_feedUrl" width="1037" height="583">
5+
<img *ngIf="is_active" style="-webkit-user-select: none;" [src]="_feedUrl" width="1037" height="583">
66
</div>
77
</div>
8-
</div>
8+
</div>
Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Component, OnInit } from '@angular/core';
2+
import { ActivatedRoute } from '@angular/router';
23

34
import { VideoFeedService } from 'src/app/services/video-feed.service';
45

@@ -9,16 +10,29 @@ import { VideoFeedService } from 'src/app/services/video-feed.service';
910
})
1011
export class VideoFeedPreviewComponent implements OnInit {
1112

12-
public _feedUrl = "http://localhost:5000/video_feeds/1"; // + "/" + _id.toString()
13+
public feed_id: string;
14+
public _feedUrl: string;
15+
public is_active: boolean;
1316

14-
constructor(private _videoFeedService: VideoFeedService) { }
17+
constructor(private _videoFeedService: VideoFeedService, private route: ActivatedRoute) { }
1518

1619
ngOnInit(): void {
17-
}
20+
let id = this.route.snapshot.paramMap.get('feed_id');
21+
this.feed_id = id;
22+
this._videoFeedService.getVideoFeed(id).subscribe(
23+
res => {
24+
console.log(res);
25+
this.is_active = res.is_active;
26+
// preview feed as it is
27+
// this._feedUrl = res.url;
28+
// run recognization & preview
29+
this._feedUrl = this._videoFeedService._feedPreviewUrl + "/" + id;
1830

19-
get_is_active(){
20-
// get is_active of the given id parameter in the route
21-
return true;
31+
},
32+
err => {
33+
// console.log(err);
34+
}
35+
);
2236
}
2337

2438
}

frontend/src/app/services/video-feed.service.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { Observable } from 'rxjs';
99
export class VideoFeedService {
1010
_feedListUrl = "http://localhost:5000/video_feeds";
1111
_feedAddUrl = "http://localhost:5000/video_feeds/add"
12+
_feedPreviewUrl = "http://localhost:5000/video_feeds/preview";
1213
_feedStartUrl = "http://localhost:5000/video_feeds/start";
1314
_feedStopUrl = "http://localhost:5000/video_feeds/stop";
1415
_feedDeleteUrl = "http://localhost:5000/video_feeds/delete";
@@ -23,6 +24,10 @@ export class VideoFeedService {
2324
return this.http.post<any>(this._feedAddUrl, _feed);
2425
}
2526

27+
getVideoFeed(feed_id){
28+
return this.http.get<any>(this._feedListUrl + "/" + feed_id.toString());
29+
}
30+
2631
deleteVideoFeed(feed_id){
2732
return this.http.delete<any>(this._feedDeleteUrl + "/" + feed_id);
2833
}

0 commit comments

Comments
 (0)