Skip to content

Commit 38d260e

Browse files
authored
Merge pull request #92 from klaraloreen/main
Added flag for switching the camera control status
2 parents 6b6d23b + 7fbfc38 commit 38d260e

File tree

5 files changed

+144
-12
lines changed

5 files changed

+144
-12
lines changed

camera-control.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ camera_update_frequency: 300
3939
# password: password to access the API (optional)
4040
# preset_active: Camera preset to move to when recording (default: 1)
4141
# preset_inactive: Camera preset to move to when not recording (default: 10)
42+
# control: Flag for switching between automatic and manual camera control
43+
# (default: "automatic")
44+
4245
camera:
4346
test-agent:
4447
- url: http://camera-panasonic.example.com

occameracontrol/__main__.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626
from occameracontrol.camera import Camera
2727
from occameracontrol.metrics import start_metrics_exporter, RequestErrorHandler
2828

29+
from occameracontrol.camera_control_server import start_camera_control_server
30+
2931

3032
logger = logging.getLogger(__name__)
3133

@@ -57,7 +59,11 @@ def control_camera(camera: Camera):
5759
f'Failed to communicate with camera {camera}')
5860
while True:
5961
with error_handler:
60-
camera.update_position()
62+
if camera.control == "automatic":
63+
camera.update_position()
64+
else:
65+
camera.activate_camera()
66+
camera.check_calendar()
6167
time.sleep(1)
6268

6369

@@ -100,14 +106,18 @@ def main():
100106
agent_update.start()
101107

102108
for camera in cameras:
103-
logger.info('Starting camera control for %s', camera)
109+
logger.info('Starting camera control for %s with control status %s',
110+
camera, getattr(camera, 'control'))
104111
control_thread = Thread(target=control_camera, args=(camera,))
105112
threads.append(control_thread)
106113
control_thread.start()
107114

108115
# Start delivering metrics
109116
start_metrics_exporter()
110117

118+
# Start camera control server
119+
start_camera_control_server(cameras=cameras)
120+
111121
try:
112122
for thread in threads:
113123
thread.join()

occameracontrol/camera.py

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,15 @@ class Camera:
5252
preset_inactive: int = 10
5353
last_updated: float = 0.0
5454
update_frequency: int = 300
55+
# Flag for switching between automatic and manual camera control
56+
# automatic = The corresponding camera will be controlled automatically,
57+
# i.e. the camera position will be adjusted
58+
# according to the agent's state and the values given
59+
# in preset_active and preset_inactive
60+
# manual = The corresponding camera will be controlled manually.
61+
# Values in preset_active and preset_inactive will
62+
# be ignored as well as the agent's status
63+
control: str = "automatic"
5564

5665
def __init__(self,
5766
agent: Agent,
@@ -60,7 +69,8 @@ def __init__(self,
6069
user: Optional[str] = None,
6170
password: Optional[str] = None,
6271
preset_active: int = 1,
63-
preset_inactive: int = 10):
72+
preset_inactive: int = 10,
73+
control: str = "automatic"):
6474
self.agent = agent
6575
self.url = url.rstrip('/')
6676
self.type = CameraType[type]
@@ -69,6 +79,7 @@ def __init__(self,
6979
self.preset_active = preset_active
7080
self.preset_inactive = preset_inactive
7181
self.update_frequency = config_t(int, 'camera_update_frequency') or 300
82+
self.control = control
7283

7384
def __str__(self) -> str:
7485
'''Returns a string representation of this camera
@@ -107,6 +118,7 @@ def activate_camera(self, on=True):
107118
def move_to_preset(self, preset: int):
108119
'''Move the PTZ camera to the specified preset position
109120
'''
121+
self.activate_camera()
110122
register_camera_expectation(self.url, preset)
111123
if self.type == CameraType.panasonic:
112124
params = {'cmd': f'#R{preset - 1:02}', 'res': 1}
@@ -142,11 +154,7 @@ def from_now(self, ts: float) -> str:
142154
seconds = int(ts - time.time()) # seconds are enough accuracy
143155
return str(datetime.timedelta(seconds=seconds))
144156

145-
def update_position(self):
146-
'''Check for currently active events with the camera's capture agent
147-
and move the camera to the appropriate (active, inactive) position if
148-
necessary.
149-
'''
157+
def check_calendar(self):
150158
agent_id = self.agent.agent_id
151159
level = logging.DEBUG if int(time.time()) % 60 else logging.INFO
152160

@@ -155,7 +163,6 @@ def update_position(self):
155163
time.sleep(1)
156164

157165
event = self.agent.next_event()
158-
159166
if event.future():
160167
logger.log(level, '[%s] Next event `%s` starts in %s',
161168
agent_id, event.title[:40], self.from_now(event.start))
@@ -165,22 +172,28 @@ def update_position(self):
165172
else:
166173
logger.log(level, '[%s] No planned events', agent_id)
167174

175+
return event
176+
177+
def update_position(self):
178+
'''Check for currently active events with the camera's capture agent
179+
and move the camera to the appropriate (active, inactive) position if
180+
necessary.
181+
'''
182+
agent_id = self.agent.agent_id
183+
event = self.check_calendar()
168184
if event.active(): # active event
169185
if self.position != self.preset_active:
170186
logger.info('[%s] Event `%s` started', agent_id, event.title)
171187
logger.info('[%s] Moving to preset %i', agent_id,
172188
self.preset_active)
173-
self.activate_camera()
174189
self.move_to_preset(self.preset_active)
175190
else: # No active event
176191
if self.position != self.preset_inactive:
177192
logger.info('[%s] Returning to preset %i', agent_id,
178193
self.preset_inactive)
179-
self.activate_camera()
180194
self.move_to_preset(self.preset_inactive)
181195

182196
if time.time() - self.last_updated >= self.update_frequency:
183197
logger.info('[%s] Re-sending preset %i to camera', agent_id,
184198
self.position)
185-
self.activate_camera()
186199
self.move_to_preset(self.position)
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
# Opencast Camera Control
2+
# Copyright 2024 Osnabrück University, virtUOS
3+
#
4+
# This program is free software: you can redistribute it and/or modify
5+
# it under the terms of the GNU General Public License as published by
6+
# the Free Software Foundation, either version 3 of the License, or
7+
# (at your option) any later version.
8+
#
9+
# This program is distributed in the hope that it will be useful,
10+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
# GNU General Public License for more details.
13+
#
14+
# You should have received a copy of the GNU General Public License
15+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
16+
17+
import logging
18+
19+
from prometheus_client import generate_latest, CONTENT_TYPE_LATEST
20+
from flask import Flask, Response
21+
22+
logger = logging.getLogger(__name__)
23+
24+
app = Flask(__name__)
25+
26+
27+
@app.route('/control/<string:status>/<string:req_camera_url>')
28+
# @requires_auth
29+
def activate_camera(status, req_camera_url):
30+
""" Endpoint for switching between manual and automatic camera control.
31+
The desired camera is identified by the passed req_camera_url.
32+
"""
33+
if status == "manual":
34+
logger.info('Camera with URL "[%s]" will be controlled manually and ' +
35+
'therefore it\'s position won\'t be updated automatically',
36+
req_camera_url)
37+
elif status == "automatic":
38+
logger.info('Camera with URL "[%s]" will switch to automatic ' +
39+
'controlling. The camera\'s position will be ' +
40+
'adjusted to the corresponding agent\'s status ' +
41+
'automatically.', req_camera_url)
42+
else:
43+
return 'ERROR<br/>Given status %s is invalid.', status
44+
45+
# Get rid of http or https prefixes to ensure reliable comparability
46+
sanitized_camera_url = str.replace(req_camera_url, 'http://', '')
47+
sanitized_camera_url = str.replace(req_camera_url, 'https://', '')
48+
cameras = app.config["cameras"]
49+
for camera in cameras:
50+
camera_url = str.replace(getattr(camera, 'url'), 'http://', '')
51+
camera_url = str.replace(getattr(camera, 'url'), 'https://', '')
52+
53+
if sanitized_camera_url == camera_url:
54+
setattr(camera, 'control', status)
55+
# Resets the current position of the camera
56+
setattr(camera, 'position', -1)
57+
return (
58+
f"Successfully set camera with url '{camera_url} "
59+
f"to control status <b>'{status}'</b>."
60+
)
61+
logger.info(f"Camera with url '{req_camera_url}' could not be found.")
62+
return f"ERROR<br/>Camera with url '{req_camera_url}' could not be found."
63+
64+
65+
@app.route('/control_status/<string:req_camera_url>')
66+
def view_current_camera_control_status(req_camera_url):
67+
""" Endpoint for requesting the current control status
68+
(manual or automatic) for a camera.
69+
The desired camera is identified by the passed camera url.
70+
"""
71+
# Get rid of http or https prefixes to ensure reliable comparability
72+
sanitized_camera_url = str.replace(req_camera_url, 'http://', '')
73+
sanitized_camera_url = str.replace(req_camera_url, 'https://', '')
74+
75+
cameras = app.config["cameras"]
76+
for camera in cameras:
77+
camera_url = str.replace(getattr(camera, 'url'), 'http://', '')
78+
camera_url = str.replace(getattr(camera, 'url'), 'https://', '')
79+
if sanitized_camera_url == camera_url:
80+
return (
81+
f"STATUS<br/>The control status of the camera "
82+
f"with url '{req_camera_url}' is "
83+
f"<b>{getattr(camera, 'control')}</b>"
84+
)
85+
logger.info(f"Camera with url '{req_camera_url}' could not be found.")
86+
return f"ERROR</br>Camera with url '{req_camera_url}' could not be found."
87+
88+
89+
# expose camera control metrics
90+
@app.route('/metrics')
91+
# @requires_auth
92+
def metrics():
93+
# registry = CollectorRegistry()
94+
# multiprocess.MultiProcessCollector(registry)
95+
return Response(generate_latest(), content_type=CONTENT_TYPE_LATEST)
96+
97+
98+
def start_camera_control_server(cameras):
99+
"""Start the flask server for managing the camera control
100+
"""
101+
logger.info('Starting camera control server')
102+
# start flask app
103+
# ToDo get host and port from config, default 127.0.0.1:8000
104+
app.config['cameras'] = cameras
105+
app.run(host='127.0.0.1', port=8080)

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ confygure >= 0.1.0
22
prometheus-client >= 0.13.1
33
python-dateutil
44
requests
5+
flask >= 3.1.0

0 commit comments

Comments
 (0)