Skip to content

Commit

Permalink
Refactors
Browse files Browse the repository at this point in the history
  • Loading branch information
yihong1120 committed May 25, 2024
1 parent 6c962a7 commit 3d6584e
Show file tree
Hide file tree
Showing 6 changed files with 222 additions and 137 deletions.
144 changes: 7 additions & 137 deletions examples/Stream-Web/app.py
Original file line number Diff line number Diff line change
@@ -1,153 +1,23 @@
import base64
from flask import Flask, abort, render_template, make_response, Response
import threading
from flask import Flask
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
import redis
from flask_socketio import SocketIO, emit
from flask_socketio import SocketIO
from flask_cors import CORS
from typing import List, Tuple

# Connect to Redis
r = redis.Redis(host='localhost', port=6379, db=0)

app = Flask(__name__)
CORS(app) # Allow cross-origin requests from any domain
socketio = SocketIO(app, cors_allowed_origins="*") # Allow all origins for WebSocket connections
limiter = Limiter(key_func=get_remote_address)

CACHE_DURATION = 60 # Cache duration in seconds
from .routes import register_routes
from .sockets import register_sockets

def get_labels() -> List[str]:
"""
Retrieve and decode all unique labels from Redis keys.
Returns:
list: Sorted list of unique labels.
"""
cursor, keys = r.scan()
decoded_keys = [key.decode('utf-8') for key in keys]
labels = {key.split('_')[0] for key in decoded_keys if key.count('_') == 1 and not key.startswith('_') and not key.endswith('_')}
return sorted(labels)

def get_image_data(label: str) -> List[Tuple[str, str]]:
"""
Retrieve and process image data for a specific label.
Args:
label (str): The label/category of the images.
Returns:
list: List of tuples containing base64 encoded images and their names.
"""
cursor, keys = r.scan(match=f'{label}_*')
image_data = [(base64.b64encode(r.get(key)).decode('utf-8'), key.decode('utf-8').split('_')[1]) for key in keys]
return sorted(image_data, key=lambda x: x[1])

@app.route('/')
@limiter.limit("60 per minute")
def index() -> str:
"""
Serve the index page with a dynamically generated list of labels from Redis.
Returns:
str: The rendered 'index.html' page.
"""
labels = get_labels()
return render_template('index.html', labels=labels)

@app.route('/label/<label>', methods=['GET'])
@limiter.limit("60 per minute")
def label_page(label: str) -> str:
"""
Serve a page for each label, displaying images under that label.
Args:
label (str): The label/category of the images.
Returns:
str: The rendered 'label.html' page for the specific label.
"""
image_data = get_image_data(label)
return render_template('label.html', label=label, image_data=image_data)

@socketio.on('connect')
def handle_connect() -> None:
"""
Handle client connection to the WebSocket.
"""
emit('message', {'data': 'Connected'})
socketio.start_background_task(update_images)

@socketio.on('disconnect')
def handle_disconnect() -> None:
"""
Handle client disconnection from the WebSocket.
"""
print('Client disconnected')

@socketio.on('error')
def handle_error(e: Exception) -> None:
"""
Handle errors in WebSocket communication.
Args:
e (Exception): The exception that occurred.
"""
print(f'Error: {str(e)}')

def update_images() -> None:
"""
Update images every 10 seconds by scanning Redis and emitting updates to clients.
"""
while True:
socketio.sleep(10) # Execute every 20 seconds
labels = get_labels()
for label in labels:
image_data = get_image_data(label)
socketio.emit('update', {'label': label, 'images': [img for img, _ in image_data], 'image_names': [name for _, name in image_data]})

@app.route('/image/<label>/<filename>.png')
@limiter.limit("60 per minute")
def image(label: str, filename: str) -> Response:
"""
Serve an image file from Redis.
Args:
label (str): The label/category of the image.
filename (str): The filename of the image.
Returns:
Response: The image file as a response.
"""
redis_key = f"{label}_{filename}"
img_encoded = r.get(redis_key)

if img_encoded is None:
abort(404, description="Resource not found")

response = make_response(img_encoded)
response.headers.set('Content-Type', 'image/png')
response.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate'
response.headers['Pragma'] = 'no-cache'
response.headers['Expires'] = '0'

return response

@app.route('/camera/<label>/<camera_id>')
@limiter.limit("60 per minute")
def camera_page(label: str, camera_id: str) -> str:
"""
Serve a page for viewing the camera stream.
Args:
label (str): The label/category of the camera.
camera_id (str): The specific ID of the camera to view.
Returns:
str: The rendered 'camera.html' page for the specific camera.
"""
return render_template('camera.html', label=label, camera_id=camera_id)
register_routes(app, limiter, r)
register_sockets(socketio, r)

if __name__ == '__main__':
socketio.start_background_task(target=update_images)
socketio.run(app, host='127.0.0.1', port=8000, debug=False)
73 changes: 73 additions & 0 deletions examples/Stream-Web/routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
from flask import Flask, render_template, abort, make_response, Response
from flask_limiter import Limiter
from .utils import get_labels, get_image_data

def register_routes(app: Flask, limiter: Limiter, r) -> None:
@app.route('/')
@limiter.limit("60 per minute")
def index() -> str:
"""
Serve the index page with a dynamically generated list of labels from Redis.
Returns:
str: The rendered 'index.html' page.
"""
labels = get_labels(r)
return render_template('index.html', labels=labels)

@app.route('/label/<label>', methods=['GET'])
@limiter.limit("60 per minute")
def label_page(label: str) -> str:
"""
Serve a page for each label, displaying images under that label.
Args:
label (str): The label/category of the images.
Returns:
str: The rendered 'label.html' page for the specific label.
"""
image_data = get_image_data(r, label)
return render_template('label.html', label=label, image_data=image_data)

@app.route('/image/<label>/<filename>.png')
@limiter.limit("60 per minute")
def image(label: str, filename: str) -> Response:
"""
Serve an image file from Redis.
Args:
label (str): The label/category of the image.
filename (str): The filename of the image.
Returns:
Response: The image file as a response.
"""
redis_key = f"{label}_{filename}"
img_encoded = r.get(redis_key)

if img_encoded is None:
abort(404, description="Resource not found")

response = make_response(img_encoded)
response.headers.set('Content-Type', 'image/png')
response.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate'
response.headers['Pragma'] = 'no-cache'
response.headers['Expires'] = '0'

return response

@app.route('/camera/<label>/<camera_id>')
@limiter.limit("60 per minute")
def camera_page(label: str, camera_id: str) -> str:
"""
Serve a page for viewing the camera stream.
Args:
label (str): The label/category of the camera.
camera_id (str): The specific ID of the camera to view.
Returns:
str: The rendered 'camera.html' page for the specific camera.
"""
return render_template('camera.html', label=label, camera_id=camera_id)
44 changes: 44 additions & 0 deletions examples/Stream-Web/sockets.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
from flask_socketio import SocketIO, emit
from typing import Any
from .utils import get_labels, get_image_data

def register_sockets(socketio: SocketIO, r: Any) -> None:
@socketio.on('connect')
def handle_connect() -> None:
"""
Handle client connection to the WebSocket.
"""
emit('message', {'data': 'Connected'})
socketio.start_background_task(update_images, socketio, r)

@socketio.on('disconnect')
def handle_disconnect() -> None:
"""
Handle client disconnection from the WebSocket.
"""
print('Client disconnected')

@socketio.on('error')
def handle_error(e: Exception) -> None:
"""
Handle errors in WebSocket communication.
Args:
e (Exception): The exception that occurred.
"""
print(f'Error: {str(e)}')

def update_images(socketio: SocketIO, r: Any) -> None:
"""
Update images every 10 seconds by scanning Redis and emitting updates to clients.
Args:
socketio (SocketIO): The SocketIO instance.
r (Any): The Redis connection.
"""
while True:
socketio.sleep(10)
labels = get_labels(r)
for label in labels:
image_data = get_image_data(r, label)
socketio.emit('update', {'label': label, 'images': [img for img, _ in image_data], 'image_names': [name for _, name in image_data]})
43 changes: 43 additions & 0 deletions examples/Stream-Web/static/js/label.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
$(document).ready(() => {
// 自动检测当前页面协议,以决定 ws 还是 wss
const protocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
// 创建 WebSocket 连接,并配置重连策略
const socket = io.connect(protocol + document.domain + ':' + location.port, {
transports: ['websocket'],
reconnectionAttempts: 5, // 最多重连尝试 5 次
reconnectionDelay: 2000 // 重连间隔为 2000 毫秒
});

// 获取当前页面的标签名
const currentPageLabel = $('h1').text(); // 假设页面的 <h1> 标签包含了当前的标签名称

socket.on('connect', () => {
console.log('WebSocket connected!');
});

socket.on('connect_error', (error) => {
console.error('WebSocket connection error:', error);
});

socket.on('reconnect_attempt', () => {
console.log('Attempting to reconnect...');
});

socket.on('update', (data) => {
// 检查接收到的数据是否适用于当前页面的标签
if (data.label === currentPageLabel) {
console.log('Received update for current label:', data.label);
const fragment = document.createDocumentFragment();
data.images.forEach((image, index) => {
const cameraDiv = $('<div>').addClass('camera');
const title = $('<h2>').text(data.image_names[index]);
const img = $('<img>').attr('src', `data:image/png;base64,${image}`).attr('alt', `${data.label} image`);
cameraDiv.append(title).append(img);
fragment.appendChild(cameraDiv[0]);
});
$('.camera-grid').empty().append(fragment);
} else {
console.log('Received update for different label:', data.label);
}
});
});
22 changes: 22 additions & 0 deletions examples/Stream-Web/templates/label.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{{ label }}</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/3.1.3/socket.io.min.js"></script>
<script src="{{ url_for('static', filename='js/label.js') }}"></script>
</head>
<body>
<h1>{{ label }}</h1>
<div class="camera-grid">
{% for image, image_name in image_data %}
<div class="camera">
<h2>{{ image_name }}</h2>
<img src="data:image/png;base64,{{ image }}" alt="{{ label }} image">
</div>
{% endfor %}
</div>
</body>
</html>
33 changes: 33 additions & 0 deletions examples/Stream-Web/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import base64
import redis
from typing import List, Tuple

def get_labels(r: redis.Redis) -> List[str]:
"""
Retrieve and decode all unique labels from Redis keys, excluding the label 'test'.
Args:
r (redis.Redis): The Redis connection.
Returns:
list: Sorted list of unique labels.
"""
cursor, keys = r.scan()
decoded_keys = [key.decode('utf-8') for key in keys]
labels = {key.split('_')[0] for key in decoded_keys if key.count('_') == 1 and not key.startswith('_') and not key.endswith('_') and key.split('_')[0] != 'test'}
return sorted(labels)

def get_image_data(r: redis.Redis, label: str) -> List[Tuple[str, str]]:
"""
Retrieve and process image data for a specific label.
Args:
r (redis.Redis): The Redis connection.
label (str): The label/category of the images.
Returns:
list: List of tuples containing base64 encoded images and their names.
"""
cursor, keys = r.scan(match=f'{label}_*')
image_data = [(base64.b64encode(r.get(key)).decode('utf-8'), key.decode('utf-8').split('_')[1]) for key in keys]
return sorted(image_data, key=lambda x: x[1])

0 comments on commit 3d6584e

Please sign in to comment.