- persist client config from MQTT for display manager volume control - apply effective VLC volume from event volume and client multiplier - support live runtime volume updates for active video playback - make latest screenshot handoff atomic to avoid broken latest.jpg races - ignore broken screenshot pointers when selecting fallback images - fix screenshot size logging and document server-side volume control
13 KiB
Server-Side Volume Control Implementation
This document describes the server-side implementation steps for client-based video volume control in Infoscreen.
Architecture Overview
The system uses a two-level volume model:
-
Event Volume (server → client in event payload)
- Per-content loudness intent
- Set by the scheduler/content editor
- Range:
0.0to1.0 - Applies to all clients in the group
-
Client Volume Multiplier (dashboard → client via MQTT config)
- Room/device-specific loudness adjustment
- Set per client in the dashboard
- Range:
0.0to1.0(0% to 100%) - Applies to all videos on that client
Effective playback volume = event.video.volume × client_settings.audio.video_volume_multiplier
Example:
| Client | Location | Event Volume | Client Multiplier | Result |
|---|---|---|---|---|
| A | Hall | 0.8 | 1.0 | 80% |
| B | Library | 0.8 | 0.3 | 24% |
MQTT Payload Structure
The server sends client configuration via retained MQTT message to:
infoscreen/{client_id}/config
Minimal Payload
{
"audio": {
"video_volume_multiplier": 0.5
}
}
Full Payload (for future extensibility)
{
"audio": {
"video_volume_multiplier": 0.5,
"enabled": true,
"updated_at": "2025-03-12T14:30:00Z"
},
"display": {
"brightness": 0.9
}
}
Reset / Clear Setting
To reset to default, publish empty object or null:
{}
or
null
The client will delete the local config and use default multiplier 1.0.
Server Implementation Steps
Step 1: Database Schema
Add client audio settings to your clients table or config storage.
Option A: Dedicated field
ALTER TABLE clients ADD COLUMN video_volume_multiplier DECIMAL(3,2) DEFAULT 1.00;
Constraints:
CHECK (video_volume_multiplier >= 0.0 AND video_volume_multiplier <= 1.0)
Option B: JSON config storage (flexible)
If you already have a client_settings or config JSON column:
{
"audio": {
"video_volume_multiplier": 1.0
}
}
Step 2: API Endpoint
Create or extend an API to manage client audio settings.
Endpoint: PATCH /api/clients/{client_id}/settings
Request Body:
{
"audio": {
"video_volume_multiplier": 0.5
}
}
Response:
{
"status": "success",
"client_id": "550e8400-e29b-41d4-a716-446655440000",
"settings": {
"audio": {
"video_volume_multiplier": 0.5,
"updated_at": "2025-03-12T14:30:00Z"
}
}
}
Validation:
# Pseudo-code
def validate_video_volume_multiplier(value):
if not isinstance(value, (int, float)):
raise ValueError("Must be numeric")
if value < 0.0 or value > 1.0:
raise ValueError("Must be between 0.0 and 1.0")
return float(value)
Step 3: Service Logic
Create a service method to save the setting and publish via MQTT.
# Pseudo-code
class ClientSettingsService:
def set_video_volume_multiplier(self, client_id, multiplier, user_id):
# Validate
multiplier = validate_video_volume_multiplier(multiplier)
# Load current client
client = clients.get_by_id(client_id)
if not client:
raise ClientNotFound(client_id)
# Store previous value for audit
previous = client.video_volume_multiplier or 1.0
# Save to database
client.video_volume_multiplier = multiplier
client.save()
# Publish MQTT retained config
payload = {
"audio": {
"video_volume_multiplier": multiplier,
"updated_at": datetime.utcnow().isoformat()
}
}
mqtt_client.publish(
topic=f"infoscreen/{client_id}/config",
payload=json.dumps(payload),
retain=True,
qos=1
)
# Audit log
AuditLog.create(
client_id=client_id,
action="video_volume_multiplier_changed",
previous_value=previous,
new_value=multiplier,
user_id=user_id,
timestamp=datetime.utcnow()
)
return {
"status": "success",
"client_id": client_id,
"settings": payload
}
Step 4: Reset/Clear Setting
Create an endpoint to clear/reset the client setting.
Endpoint: DELETE /api/clients/{client_id}/settings/audio/video_volume_multiplier
Behavior:
def reset_video_volume_multiplier(client_id, user_id):
# Load client
client = clients.get_by_id(client_id)
if not client:
raise ClientNotFound(client_id)
# Clear database field
previous = client.video_volume_multiplier or 1.0
client.video_volume_multiplier = None
client.save()
# Publish empty config to MQTT to reset client
mqtt_client.publish(
topic=f"infoscreen/{client_id}/config",
payload="{}",
retain=True,
qos=1
)
# Audit log
AuditLog.create(
client_id=client_id,
action="video_volume_multiplier_reset",
previous_value=previous,
new_value=1.0, # Default
user_id=user_id,
timestamp=datetime.utcnow()
)
return {"status": "success", "client_id": client_id}
Step 5: Dashboard UI
In the client settings page, add a slider control.
UI Elements:
| Field | Type | Range | Default | Behavior |
|---|---|---|---|---|
| Client Volume Limit | Slider | 0% to 100% | 100% | Save on change, publish MQTT |
| Display Value | Text | 0.0 to 1.0 | 1.0 | Show decimal value |
| Description | Help Text | — | — | Explain purpose |
Suggested Help Text:
Client Volume Limit
Adjust the overall loudness for this display. Use this to adapt videos to different room environments (quiet library: 30%, noisy hall: 100%). The event volume determines the content-specific loudness; this multiplier applies on top.
Example: React-like pseudo code
function ClientVolumeSettings({ clientId, initialMultiplier }) {
const [multiplier, setMultiplier] = useState(initialMultiplier || 1.0);
const percentage = Math.round(multiplier * 100);
const handleChange = async (newValue) => {
setMultiplier(newValue);
// Save to server
try {
const response = await fetch(
`/api/clients/${clientId}/settings`,
{
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
audio: { video_volume_multiplier: newValue }
})
}
);
if (!response.ok) {
console.error("Failed to save setting");
setMultiplier(initialMultiplier); // Revert
}
} catch (error) {
console.error("Error:", error);
}
};
return (
<div>
<label>Client Volume Limit</label>
<input
type="range"
min="0"
max="100"
value={percentage}
onChange={(e) => handleChange(e.target.value / 100)}
/>
<span>{percentage}%</span>
</div>
);
}
Step 6: Retain Message Handling
Important: Always publish as retained message.
Why:
- When the client reconnects, it re-subscribes to
infoscreen/{client_id}/config - The broker immediately re-delivers the last retained message
- Client always has the latest setting, even after network outages
Best Practice: When changing a setting, overwrite the old retained message:
# New setting received
mqtt_client.publish(
topic=f"infoscreen/{client_id}/config",
payload=json.dumps(payload),
retain=True, # Mark as retained
qos=1 # Ensure delivery
)
Step 7: Validation & Error Handling
Server-side validation:
def validate_client_config(config_data):
"""Validate incoming client config from dashboard."""
if not isinstance(config_data, dict):
raise ValueError("Config must be an object")
audio = config_data.get("audio", {})
if not isinstance(audio, dict):
raise ValueError("audio must be an object")
multiplier = audio.get("video_volume_multiplier")
if multiplier is not None:
if not isinstance(multiplier, (int, float)):
raise ValueError("video_volume_multiplier must be numeric")
if multiplier < 0.0 or multiplier > 1.0:
raise ValueError("video_volume_multiplier must be between 0.0 and 1.0")
return config_data
Error responses:
{
"status": "error",
"code": "INVALID_VALUE",
"message": "video_volume_multiplier must be between 0.0 and 1.0",
"details": {
"field": "audio.video_volume_multiplier",
"received": 1.5,
"valid_range": [0.0, 1.0]
}
}
Step 8: Audit Logging
Store all changes for compliance and debugging.
Audit Log Schema:
CREATE TABLE audit_logs (
id UUID PRIMARY KEY,
client_id UUID NOT NULL REFERENCES clients(id),
action VARCHAR(255) NOT NULL,
previous_value DECIMAL(3,2),
new_value DECIMAL(3,2),
user_id UUID,
ip_address VARCHAR(45),
timestamp TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
details JSONB
);
Example Log Entry:
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"client_id": "550e8400-e29b-41d4-a716-446655440111",
"action": "video_volume_multiplier_changed",
"previous_value": 1.0,
"new_value": 0.3,
"user_id": "550e8400-e29b-41d4-a716-446655440222",
"ip_address": "192.168.1.100",
"timestamp": "2025-03-12T14:30:00Z",
"details": {
"location": "library",
"reason": "quiet environment"
}
}
Client Consumption
The client receives the config and applies it automatically. No manual steps needed on the client side; the code is already implemented.
Client flow:
- Client subscribes to
infoscreen/{client_id}/config - Server publishes retained config (persisted by broker)
- Client receives and saves to
src/config/client_settings.json - Display Manager reads the file and applies the multiplier during video playback
- Client reconnects → broker re-delivers retained message → client re-syncs
Testing Scenario
Test Case 1: Basic multiplier application
- Create client A and client B in the same group
- Set client A multiplier to
1.0(hall) - Set client B multiplier to
0.3(library) - Send a video event with
video.volume = 0.8to the group - Check client logs:
- Client A: "Video volume resolved: event=0.80 client=1.00 effective=80%"
- Client B: "Video volume resolved: event=0.80 client=0.30 effective=24%"
- Verify both clients play at correct perceived volumes
Test Case 2: Live multiplier update
- Start a video on client B (library)
- Change client B multiplier from
0.3to0.6in the dashboard - Check client logs:
- "Applied VLC volume for active video runtime update: 60%"
- Verify the video volume increases immediately without restarting the video
Test Case 3: Reconnect and retained delivery
- Set client C multiplier to
0.5 - Disconnect client C from MQTT
- Reconnect client C
- Verify client C immediately receives retained config and applies
0.5
Test Case 4: Reset to default
- Set client D multiplier to
0.2 - Click "Reset" in the dashboard
- Verify server publishes
{}to config topic - Check client log: "No client settings found, using default video multiplier: 1.00"
Recommended Multiplier Ranges
Based on environment and use case:
| Environment | Recommended Range | Example Use Cases |
|---|---|---|
| Quiet (Library, Museum) | 0.1 - 0.4 | Minimize noise, focus on learning |
| Normal (Classroom, Office) | 0.6 - 0.9 | Default comfortable level |
| Loud (Hall, Outdoor) | 0.9 - 1.0 | Compete with ambient noise |
| Mute / Subtitles Only | 0.0 | Accessible viewing (hearing impaired) |
Summary
Server implementation checklist:
- Add
video_volume_multiplierfield toclientstable or JSON config - Implement API endpoint
PATCH /api/clients/{client_id}/settings - Add validation: reject values outside
0.0to1.0 - Implement service to save and publish MQTT config
- Add retained message publishing to MQTT
- Add dashboard UI slider control
- Implement reset/clear endpoint
- Add audit logging
- Test client reconnect and retained message delivery
- Test live volume update during playback
- Document multiplier ranges and guidance for operators
Key contract:
{
"audio": {
"video_volume_multiplier": 0.5
}
}
Published to:
infoscreen/{client_id}/config
With retain=true and qos=1.