Shad Ansari | 30a2373 | 2021-09-29 23:07:21 -0700 | [diff] [blame] | 1 | import time |
| 2 | import threading |
| 3 | try: |
| 4 | from greenlet import getcurrent as get_ident |
| 5 | except ImportError: |
| 6 | try: |
| 7 | from thread import get_ident |
| 8 | except ImportError: |
| 9 | from _thread import get_ident |
| 10 | |
| 11 | |
| 12 | class CameraEvent(object): |
| 13 | """An Event-like class that signals all active clients when a new frame is |
| 14 | available. |
| 15 | """ |
| 16 | def __init__(self): |
| 17 | self.events = {} |
| 18 | |
| 19 | def wait(self): |
| 20 | """Invoked from each client's thread to wait for the next frame.""" |
| 21 | ident = get_ident() |
| 22 | if ident not in self.events: |
| 23 | # this is a new client |
| 24 | # add an entry for it in the self.events dict |
| 25 | # each entry has two elements, a threading.Event() and a timestamp |
| 26 | self.events[ident] = [threading.Event(), time.time()] |
| 27 | return self.events[ident][0].wait() |
| 28 | |
| 29 | def set(self): |
| 30 | """Invoked by the camera thread when a new frame is available.""" |
| 31 | now = time.time() |
| 32 | remove = None |
| 33 | for ident, event in self.events.items(): |
| 34 | if not event[0].isSet(): |
| 35 | # if this client's event is not set, then set it |
| 36 | # also update the last set timestamp to now |
| 37 | event[0].set() |
| 38 | event[1] = now |
| 39 | else: |
| 40 | # if the client's event is already set, it means the client |
| 41 | # did not process a previous frame |
| 42 | # if the event stays set for more than 5 seconds, then assume |
| 43 | # the client is gone and remove it |
| 44 | if now - event[1] > 5: |
| 45 | remove = ident |
| 46 | if remove: |
| 47 | del self.events[remove] |
| 48 | |
| 49 | def clear(self): |
| 50 | """Invoked from each client's thread after a frame was processed.""" |
| 51 | self.events[get_ident()][0].clear() |
| 52 | |
| 53 | |
| 54 | class BaseCamera(object): |
Shad Ansari | c0726e6 | 2021-10-04 22:38:53 +0000 | [diff] [blame] | 55 | thread = {} # background thread that reads frames from camera |
| 56 | frame = {} # current frame is stored here by background thread |
| 57 | last_access = {} # time of last client access to the camera |
| 58 | event = {} |
Shad Ansari | 4ae1168 | 2021-10-22 18:51:53 +0000 | [diff] [blame^] | 59 | idle = False # if True, stops thread if no client connected |
Shad Ansari | 30a2373 | 2021-09-29 23:07:21 -0700 | [diff] [blame] | 60 | |
Shad Ansari | 4ae1168 | 2021-10-22 18:51:53 +0000 | [diff] [blame^] | 61 | def __init__(self, device=None, idle=False): |
Shad Ansari | 30a2373 | 2021-09-29 23:07:21 -0700 | [diff] [blame] | 62 | """Start the background camera thread if it isn't running yet.""" |
Shad Ansari | c0726e6 | 2021-10-04 22:38:53 +0000 | [diff] [blame] | 63 | self.device = device |
Shad Ansari | 4ae1168 | 2021-10-22 18:51:53 +0000 | [diff] [blame^] | 64 | self.idle = idle |
Shad Ansari | c0726e6 | 2021-10-04 22:38:53 +0000 | [diff] [blame] | 65 | BaseCamera.event[self.device] = CameraEvent() |
| 66 | if self.device not in BaseCamera.thread: |
| 67 | BaseCamera.thread[self.device] = None |
| 68 | if BaseCamera.thread[self.device] is None: |
| 69 | BaseCamera.last_access[self.device] = time.time() |
Shad Ansari | 30a2373 | 2021-09-29 23:07:21 -0700 | [diff] [blame] | 70 | |
| 71 | # start background frame thread |
Shad Ansari | c0726e6 | 2021-10-04 22:38:53 +0000 | [diff] [blame] | 72 | BaseCamera.thread[self.device] = threading.Thread(target=self._thread, args=(self.device)) |
| 73 | BaseCamera.thread[self.device].start() |
Shad Ansari | 30a2373 | 2021-09-29 23:07:21 -0700 | [diff] [blame] | 74 | |
| 75 | # wait until frames are available |
| 76 | while self.get_frame() is None: |
| 77 | time.sleep(0) |
| 78 | |
| 79 | def get_frame(self): |
| 80 | """Return the current camera frame.""" |
Shad Ansari | c0726e6 | 2021-10-04 22:38:53 +0000 | [diff] [blame] | 81 | BaseCamera.last_access[self.device] = time.time() |
Shad Ansari | 30a2373 | 2021-09-29 23:07:21 -0700 | [diff] [blame] | 82 | |
| 83 | # wait for a signal from the camera thread |
Shad Ansari | c0726e6 | 2021-10-04 22:38:53 +0000 | [diff] [blame] | 84 | BaseCamera.event[self.device].wait() |
| 85 | BaseCamera.event[self.device].clear() |
Shad Ansari | 30a2373 | 2021-09-29 23:07:21 -0700 | [diff] [blame] | 86 | |
Shad Ansari | c0726e6 | 2021-10-04 22:38:53 +0000 | [diff] [blame] | 87 | return BaseCamera.frame[self.device] |
Shad Ansari | 30a2373 | 2021-09-29 23:07:21 -0700 | [diff] [blame] | 88 | |
Shad Ansari | 341ca3a | 2021-09-30 12:10:00 -0700 | [diff] [blame] | 89 | def frames(self): |
Shad Ansari | 30a2373 | 2021-09-29 23:07:21 -0700 | [diff] [blame] | 90 | """"Generator that returns frames from the camera.""" |
Shad Ansari | 341ca3a | 2021-09-30 12:10:00 -0700 | [diff] [blame] | 91 | raise NotImplementedError('Must be implemented by subclasses.') |
Shad Ansari | 30a2373 | 2021-09-29 23:07:21 -0700 | [diff] [blame] | 92 | |
Shad Ansari | c0726e6 | 2021-10-04 22:38:53 +0000 | [diff] [blame] | 93 | def _thread(self, device): |
Shad Ansari | 30a2373 | 2021-09-29 23:07:21 -0700 | [diff] [blame] | 94 | """Camera background thread.""" |
| 95 | frames_iterator = self.frames() |
| 96 | for frame in frames_iterator: |
Shad Ansari | c0726e6 | 2021-10-04 22:38:53 +0000 | [diff] [blame] | 97 | BaseCamera.frame[device] = frame |
| 98 | BaseCamera.event[device].set() # send signal to clients |
Shad Ansari | 30a2373 | 2021-09-29 23:07:21 -0700 | [diff] [blame] | 99 | time.sleep(0) |
| 100 | |
Shad Ansari | 4ae1168 | 2021-10-22 18:51:53 +0000 | [diff] [blame^] | 101 | if self.idle: |
| 102 | # if there hasn't been any clients asking for frames in |
| 103 | # the last 10 seconds then stop the thread |
| 104 | if time.time() - BaseCamera.last_access[device] > 10: |
| 105 | frames_iterator.close() |
| 106 | print('Stopping camera thread due to inactivity.') |
| 107 | break |
| 108 | |
Shad Ansari | c0726e6 | 2021-10-04 22:38:53 +0000 | [diff] [blame] | 109 | BaseCamera.thread[device] = None |