# 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: 1. **Event Volume** (server → client in event payload) - Per-content loudness intent - Set by the scheduler/content editor - Range: `0.0` to `1.0` - Applies to all clients in the group 2. **Client Volume Multiplier** (dashboard → client via MQTT config) - Room/device-specific loudness adjustment - Set per client in the dashboard - Range: `0.0` to `1.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 ```json { "audio": { "video_volume_multiplier": 0.5 } } ``` ### Full Payload (for future extensibility) ```json { "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: ```json {} ``` 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** ```sql ALTER TABLE clients ADD COLUMN video_volume_multiplier DECIMAL(3,2) DEFAULT 1.00; ``` Constraints: ```sql 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: ```json { "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:** ```json { "audio": { "video_volume_multiplier": 0.5 } } ``` **Response:** ```json { "status": "success", "client_id": "550e8400-e29b-41d4-a716-446655440000", "settings": { "audio": { "video_volume_multiplier": 0.5, "updated_at": "2025-03-12T14:30:00Z" } } } ``` **Validation:** ```python # 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. ```python # 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:** ```python 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** ```jsx 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 (
handleChange(e.target.value / 100)} /> {percentage}%
); } ``` ### 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: ```python # 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:** ```python 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:** ```json { "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:** ```sql 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:** ```json { "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:** 1. Client subscribes to `infoscreen/{client_id}/config` 2. Server publishes retained config (persisted by broker) 3. Client receives and saves to `src/config/client_settings.json` 4. Display Manager reads the file and applies the multiplier during video playback 5. Client reconnects → broker re-delivers retained message → client re-syncs ## Testing Scenario ### Test Case 1: Basic multiplier application 1. Create client A and client B in the same group 2. Set client A multiplier to `1.0` (hall) 3. Set client B multiplier to `0.3` (library) 4. Send a video event with `video.volume = 0.8` to the group 5. 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%" 6. Verify both clients play at correct perceived volumes ### Test Case 2: Live multiplier update 1. Start a video on client B (library) 2. Change client B multiplier from `0.3` to `0.6` in the dashboard 3. Check client logs: - "Applied VLC volume for active video runtime update: 60%" 4. Verify the video volume increases immediately without restarting the video ### Test Case 3: Reconnect and retained delivery 1. Set client C multiplier to `0.5` 2. Disconnect client C from MQTT 3. Reconnect client C 4. Verify client C immediately receives retained config and applies `0.5` ### Test Case 4: Reset to default 1. Set client D multiplier to `0.2` 2. Click "Reset" in the dashboard 3. Verify server publishes `{}` to config topic 4. 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_multiplier` field to `clients` table or JSON config - [ ] Implement API endpoint `PATCH /api/clients/{client_id}/settings` - [ ] Add validation: reject values outside `0.0` to `1.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:** ```json { "audio": { "video_volume_multiplier": 0.5 } } ``` Published to: ``` infoscreen/{client_id}/config ``` With `retain=true` and `qos=1`.