Custom Templates Guide
Mycelian allows you to create custom HTML templates that integrate with your streaming services through WebSocket communication. This guide covers template creation, data integration, and advanced features.
Template System Overview
Template Architecture
HTML Templates: Stored in
templates/directoryTemplate Configs: JSON configuration files in
templates/template_configs/Assets: Static files (images, sounds, fonts) in
assets/directoryWebSocket Communication: Real-time data via Socket.IO on
http://localhost:5000Auto-Registration: Templates automatically become available as routes
File Structure
templates/
├── my_custom_template.html # Your HTML template
├── template_configs/
│ └── my_custom_template.json # Configuration file
└── assets/
├── my_custom_template/ # Template-specific assets
│ ├── images/
│ ├── sounds/
│ └── styles/
└── default_assets/ # Shared assets
├── fonts/
├── images/
└── sounds/
Creating Your First Template
Basic HTML Template
Create templates/my_alerts.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My Custom Alerts</title>
<script src="https://cdn.socket.io/4.0.0/socket.io.min.js"></script>
<style>
body {
margin: 0;
padding: 0;
background: transparent;
font-family: 'Arial', sans-serif;
overflow: hidden;
}
.alert-container {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
opacity: 0;
transition: opacity 0.5s ease-in-out;
}
.alert-container.show {
opacity: 1;
}
.alert-title {
font-size: 36px;
font-weight: bold;
color: #ffffff;
text-shadow: 2px 2px 4px rgba(0,0,0,0.5);
margin-bottom: 10px;
}
.alert-message {
font-size: 24px;
color: #cccccc;
text-shadow: 1px 1px 2px rgba(0,0,0,0.5);
}
</style>
</head>
<body>
<div id="alertContainer" class="alert-container">
<div id="alertTitle" class="alert-title"></div>
<div id="alertMessage" class="alert-message"></div>
</div>
<script>
// Connect to WebSocket
const socket = io();
// Handle new alerts
socket.on('next_alert', function(data) {
showAlert(data);
});
function showAlert(alertData) {
const container = document.getElementById('alertContainer');
const title = document.getElementById('alertTitle');
const message = document.getElementById('alertMessage');
// Set alert content
title.textContent = alertData.alert_type.toUpperCase();
message.textContent = alertData.username + ' - ' + alertData.message;
// Show alert
container.classList.add('show');
// Hide after duration
setTimeout(() => {
container.classList.remove('show');
// Notify completion
socket.emit('alert_complete');
}, alertData.duration * 1000 || 5000);
}
</script>
</body>
</html>
Creating JSON Configuration Files
Template configuration files define the customizable settings for your templates. They are stored in templates/template_configs/ and follow a specific structure.
Basic Structure:
{
"template_name": "my_template",
"elements": [
{
"type": "separator",
"label": "Section Title"
},
{
"type": "text",
"id": "setting_id",
"label": "Setting Display Name",
"value": "default_value",
"description": "Description of what this setting does"
}
]
}
Available Element Types:
Text Input:
{
"type": "text",
"id": "custom_text",
"label": "Custom Text",
"value": "Default text here",
"description": "Enter custom text to display"
}
Number Input:
{
"type": "number",
"id": "animation_duration",
"label": "Animation Duration",
"value": 5,
"min": 1,
"max": 30,
"step": 0.5,
"description": "Duration in seconds"
}
Slider Input:
{
"type": "slider",
"id": "opacity_level",
"label": "Opacity Level",
"value": 80,
"min": 0,
"max": 100,
"step": 5,
"description": "Opacity percentage"
}
Checkbox (Boolean):
{
"type": "checkbox",
"id": "enable_feature",
"label": "Enable Feature",
"value": true,
"description": "Check to enable this feature"
}
Toggle (Alternative Boolean):
{
"type": "toggle",
"id": "compact_mode",
"label": "Compact Mode",
"value": false,
"description": "Use compact layout"
}
Color Selector:
{
"type": "select",
"id": "background_color",
"label": "Background Color",
"value": "#7c3aed",
"description": "Choose background color",
"display": "color_grid",
"transparent": true,
"options": [
"#e11d48",
"#dc2626",
"#ea580c",
"#d97706",
"#ca8a04",
"#65a30d",
"#16a34a",
"#059669",
"#0891b2",
"#0284c7",
"#2563eb",
"#4f46e5",
"#7c3aed",
"#9333ea",
"#c026d3",
"#db2777",
"#ffffff",
"#f8fafc",
"#f1f5f9",
"#e2e8f0",
"#cbd5e1",
"#94a3b8",
"#64748b",
"#475569",
"#334155",
"#1e293b",
"#0f172a",
"#1a1a1a",
"#000000",
"transparent"
]
}
Dropdown Select:
{
"type": "select",
"id": "font_weight",
"label": "Font Weight",
"value": "bold",
"description": "Choose font weight",
"options": [
"normal",
"bold",
"bolder",
"lighter",
"100",
"200",
"300",
"400",
"500",
"600",
"700",
"800",
"900"
]
}
Section Separator:
{
"type": "separator",
"label": "Visual Settings"
}
Complete Example Configuration:
{
"template_name": "my_alerts",
"elements": [
{
"type": "separator",
"label": "General Settings"
},
{
"type": "checkbox",
"id": "enabled",
"label": "Enable Template",
"value": true,
"description": "Enable or disable this template entirely"
},
{
"type": "text",
"id": "alert_title",
"label": "Alert Title",
"value": "New Alert!",
"description": "Text shown as the alert title"
},
{
"type": "separator",
"label": "Visual Settings"
},
{
"type": "select",
"id": "title_color",
"label": "Title Color",
"value": "#ffffff",
"description": "Color of the alert title text",
"display": "color_grid",
"transparent": false,
"options": [
"#ffffff",
"#000000",
"#e11d48",
"#dc2626",
"#ea580c",
"#d97706",
"#ca8a04",
"#65a30d",
"#16a34a",
"#059669",
"#0891b2",
"#0284c7",
"#2563eb",
"#4f46e5",
"#7c3aed",
"#9333ea",
"#c026d3",
"#db2777"
]
},
{
"type": "number",
"id": "font_size",
"label": "Font Size",
"value": 24,
"min": 12,
"max": 48,
"step": 2,
"description": "Font size in pixels"
},
{
"type": "slider",
"id": "opacity",
"label": "Background Opacity",
"value": 80,
"min": 0,
"max": 100,
"step": 5,
"description": "Background opacity percentage"
},
{
"type": "separator",
"label": "Animation Settings"
},
{
"type": "number",
"id": "alert_duration",
"label": "Alert Duration",
"value": 5.0,
"min": 1.0,
"max": 30.0,
"step": 0.5,
"description": "How long alerts are displayed in seconds"
},
{
"type": "select",
"id": "animation_easing",
"label": "Animation Easing",
"value": "ease-out",
"description": "Easing function for animations",
"options": [
"linear",
"ease",
"ease-in",
"ease-out",
"ease-in-out"
]
},
{
"type": "toggle",
"id": "enable_sound",
"label": "Enable Sound Effects",
"value": true,
"description": "Play sound effects with alerts"
}
]
}
Element Properties Reference:
Common Properties (all elements):
- type: Element type (required)
- id: Unique identifier for accessing the value (required)
- label: Display name in the UI (required)
- description: Help text explaining the setting (optional)
Value Properties:
- value: Default value (required for most types)
Number/Slider Properties:
- min: Minimum allowed value
- max: Maximum allowed value
- step: Increment step size
Select Properties:
- options: Array of available choices (required)
- display: Display style ("color_grid" for color pickers)
- transparent: Whether to include transparent option for colors
Best Practices:
Use descriptive IDs: Make IDs clear and consistent (e.g.,
title_color,animation_duration)Group related settings: Use separators to organize settings logically
Provide helpful descriptions: Explain what each setting does and its effects
Set sensible defaults: Choose default values that work well out of the box
Use appropriate ranges: Set reasonable min/max values for numbers and sliders
Color grid organization: Order colors logically (light to dark, or by hue)
Consistent naming: Use consistent naming patterns across your configuration
Template Configuration File Example:
Save as templates/template_configs/my_alerts.json:
WebSocket Integration
Connection Setup
All templates must connect to the WebSocket server:
// Connect to Socket.IO server
const socket = io();
// Handle connection events
socket.on('connect', function() {
console.log('Connected to Mycelian');
});
socket.on('disconnect', function() {
console.log('Disconnected from Mycelian');
});
Alert System Integration
Receiving Alerts:
// Listen for new alerts
socket.on('next_alert', function(alertData) {
console.log('New alert:', alertData);
// Alert data structure:
// {
// alert_type: "follow|sub|bits|donation|raid|points",
// username: "viewer_name",
// message: "alert message",
// duration: 5,
// volume: 100,
// tier: 1, // For subs (1, 2, 3)
// amt_cheered: 100, // For bits
// donation_amount: 10.00, // For donations
// raider_count: 25, // For raids
// point_cost: 500, // For channel points
// gif_dir: "assets/alerts/", // GIF directory
// gif_name: "follow.gif", // GIF filename
// single_audio_dir: "assets/sounds/",
// single_audio_name: "alert.mp3",
// user_message: "Thanks for the follow!" // User's message
// }
displayAlert(alertData);
});
// Notify when alert is complete
function alertComplete() {
socket.emit('alert_complete');
}
Pause/Resume Status:
// Listen for pause status changes
socket.on('pause_status_update', function(data) {
if (data.paused) {
console.log('Alerts are paused');
// Hide alert interface or show paused state
} else {
console.log('Alerts are resumed');
// Show alert interface
}
});
// Request current pause status
socket.emit('get_pause_status');
Service Data Integration
Twitch Integration
Getting Streamer Information:
// Request streamer info
socket.emit('get-streamer-info');
socket.on('streamer-info', function(data) {
console.log('Streamer data:', data);
// Data structure:
// {
// user_id: "123456789",
// streamer_name: "MyStreamer",
// current_category: "Just Chatting"
// }
});
Twitch API Requests:
// Make Twitch API requests through proxy
socket.emit('twitch-api-request', {
endpoint: 'https://api.twitch.tv/helix/chat/emotes',
method: 'GET',
requestId: 'emotes_request_1'
});
socket.on('twitch-api-response', function(data) {
if (data.requestId === 'emotes_request_1' && data.success) {
console.log('Emotes data:', data.data);
}
});
Chat Integration:
// Listen for new chat messages
socket.on('new-message', function(messageData) {
console.log('New chat message:', messageData);
// Message data structure:
// {
// username: "viewer123",
// message: "Hello stream!",
// badges: ["subscriber", "vip"],
// emotes: { /* emote data */ },
// timestamp: "2024-01-01T12:00:00Z"
// }
});
// Listen for chat clear events
socket.on('clear-chat', function() {
// Clear all displayed messages
});
socket.on('remove-messages', function(messageIds) {
// Remove specific messages by ID
});
Spotify Integration
Getting Now Playing Data:
// Request current Spotify data
socket.emit('get_spotify_data');
socket.on('spotify_data_update', function(spotifyData) {
console.log('Now playing:', spotifyData);
// Spotify data structure:
// {
// track_name: "Song Title",
// artist_name: "Artist Name",
// album_name: "Album Name",
// album_art_url: "https://...",
// is_playing: true,
// progress_ms: 120000,
// duration_ms: 240000,
// external_url: "https://open.spotify.com/..."
// }
updateNowPlaying(spotifyData);
});
PlayStation Network Integration
Getting PSN Data:
// Request PSN data
socket.emit('get_psn_data');
socket.on('psn_data_update', function(psnData) {
console.log('PSN data:', psnData);
// PSN data structure:
// {
// current_game_name: "Game Title",
// current_game_art_url: "https://...",
// current_game_trophies: [
// {
// trophy_name: "Achievement Name",
// trophy_type: "bronze|silver|gold|platinum",
// earned_date: "2024-01-01T12:00:00Z"
// }
// ],
// trophy_counts: {
// platinum: 5,
// gold: 25,
// silver: 100,
// bronze: 500
// }
// }
});
Activity Feed Integration
Getting Historical Alerts:
// Request paginated stored alerts
socket.emit('get_stored_alerts_paginated', {
page: 1,
limit: 25
});
socket.on('stored_alerts_paginated', function(data) {
console.log('Historical alerts:', data);
// Response structure:
// {
// alerts: [ /* array of alert objects */ ],
// page: 1,
// total_pages: 10,
// total_count: 250,
// has_prev: false,
// has_next: true
// }
});
Activity Feed Events:
// Listen for new activity feed alerts
socket.on('activity_feed_alert', function(alertData) {
// Display alert in activity feed format
addToActivityFeed(alertData);
});
Accessing Template Configuration
Loading Configuration via API
Templates can dynamically load their JSON configuration files through the web API. This allows real-time updates without requiring template reloads:
// Load all template configurations
async function loadTemplateConfig() {
try {
const response = await fetch('/api/all-template-configs');
if (response.ok) {
const allConfigs = await response.json();
console.log('Loaded all template configs:', Object.keys(allConfigs));
// Get your specific template config
const myTemplateConfig = allConfigs.my_template_name;
if (myTemplateConfig && myTemplateConfig.elements) {
console.log('Found template config:', myTemplateConfig);
// Parse configuration elements into usable values
const config = {};
myTemplateConfig.elements.forEach(element => {
if (element.type !== 'section') {
config[element.id] = element.value;
}
});
console.log('Parsed config values:', config);
applyTemplateConfig(config);
} else {
console.warn('No template config found');
}
} else {
console.error('Failed to load template configs:', response.status);
}
} catch (error) {
console.error('Error loading template config:', error);
}
}
Applying Configuration Settings
Once loaded, configuration values can be applied to your template elements:
function applyTemplateConfig(config) {
console.log('Applying template config:', config);
// Apply styling configurations
const container = document.querySelector('.my-container');
// Position and size settings
if (config.ContainerLeft !== undefined) {
container.style.left = config.ContainerLeft + 'px';
}
if (config.ContainerTop !== undefined) {
container.style.top = config.ContainerTop + 'px';
}
if (config.ContainerWidth !== undefined) {
container.style.width = config.ContainerWidth + 'px';
}
if (config.ContainerHeight !== undefined) {
container.style.height = config.ContainerHeight + 'px';
}
// Font and color settings
const titleElement = document.getElementById('title');
if (titleElement) {
if (config.TitleFontFamily) {
titleElement.style.fontFamily = config.TitleFontFamily;
}
if (config.TitleFontSize !== undefined) {
titleElement.style.fontSize = config.TitleFontSize + 'px';
}
if (config.TitleColor) {
titleElement.style.color = config.TitleColor;
}
}
// Background settings with conditional application
if (config.EnableBackground) {
container.style.backgroundColor = config.BackgroundColor || 'rgba(0,0,0,0.5)';
container.style.padding = (config.BackgroundPadding || 10) + 'px';
container.style.borderRadius = (config.BackgroundBorderRadius || 5) + 'px';
if (config.BackgroundOpacity !== undefined) {
container.style.opacity = config.BackgroundOpacity;
}
} else {
container.style.backgroundColor = 'transparent';
container.style.padding = '0';
container.style.borderRadius = '0';
}
// Apply behavioral settings to global variables
if (config.AnimationDuration !== undefined) {
window.animationDuration = config.AnimationDuration;
}
if (config.EnableSound !== undefined) {
window.soundEnabled = config.EnableSound;
}
// Update audio source if configured
if (config.AudioFile !== undefined) {
const audio = document.getElementById('myAudio');
if (audio && audio.firstElementChild) {
audio.firstElementChild.src = config.AudioFile;
audio.load(); // Reload audio with new source
}
}
}
Real-time Configuration Updates
Set up periodic reloading to pick up configuration changes:
// Load config on startup
setTimeout(function() {
loadTemplateConfig();
}, 1000);
// Periodically reload config (every 30 seconds)
setInterval(loadTemplateConfig, 30000);
Configuration-Driven Initialization
Use configuration values during template initialization:
// Global configuration object
var templateConfig = {};
var configLoaded = false;
// Initialize template after config is loaded
async function initializeTemplate() {
// Load configuration first
await loadTemplateConfig();
configLoaded = true;
// Set up template with config values
setupEventListeners();
initializeDisplay();
startPeriodicUpdates();
}
function setupEventListeners() {
const socket = io();
socket.on('next_alert', function(alertData) {
if (!configLoaded) {
console.warn('Config not loaded yet, skipping alert');
return;
}
// Use config values in alert processing
const duration = templateConfig.AlertDuration || 5000;
const volume = templateConfig.AudioVolume || 1.0;
processAlert(alertData, duration, volume);
});
}
Configuration Element Types
Handle different configuration element types properly:
function parseConfigElement(element) {
switch (element.type) {
case 'text':
return element.value || '';
case 'number':
return parseFloat(element.value) || 0;
case 'boolean':
return Boolean(element.value);
case 'color':
return element.value || '#000000';
case 'select':
return element.value || element.options[0];
case 'file':
return element.value || '';
case 'font':
return element.value || 'Arial';
case 'section':
// Skip section headers
return null;
default:
return element.value;
}
}
// Enhanced config parsing
function parseTemplateConfig(configData) {
const config = {};
if (configData && configData.elements) {
configData.elements.forEach(element => {
const value = parseConfigElement(element);
if (value !== null) {
config[element.id] = value;
}
});
}
return config;
}
Error Handling for Configuration
Implement robust error handling for configuration loading:
async function loadTemplateConfigSafely() {
try {
const response = await fetch('/api/all-template-configs');
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const allConfigs = await response.json();
const myConfig = allConfigs.my_template_name;
if (!myConfig) {
console.warn('Template config not found, using defaults');
return applyDefaultConfig();
}
const parsedConfig = parseTemplateConfig(myConfig);
applyTemplateConfig(parsedConfig);
return true;
} catch (error) {
console.error('Failed to load template config:', error);
// Fallback to default configuration
applyDefaultConfig();
return false;
}
}
function applyDefaultConfig() {
const defaultConfig = {
ContainerWidth: 400,
ContainerHeight: 200,
TitleColor: '#ffffff',
TitleFontSize: 24,
AnimationDuration: 5,
EnableSound: true
};
applyTemplateConfig(defaultConfig);
console.log('Applied default configuration');
}
Template Controls Integration
Dynamic Controls
Add interactive controls to your templates by defining them in your configuration:
Configuration (template_configs/interactive_counter.json):
{
"template_name": "interactive_counter",
"dynamic_controls": {
"elements": [
{
"type": "counter_control",
"id": "main_counter",
"label": "Main Counter",
"action_increment": "counter_increment",
"action_decrement": "counter_decrement",
"action_reset": "counter_reset"
},
{
"type": "button",
"id": "special_action",
"label": "Special Action",
"button_text": "Trigger Effect",
"action": "special_effect"
},
{
"type": "text_input",
"id": "message_input",
"label": "Send Message",
"action": "send_message",
"placeholder": "Enter message..."
}
]
}
}
Template JavaScript:
// Listen for control events
socket.on('interactive_counter_counter_increment', function() {
incrementCounter();
});
socket.on('interactive_counter_counter_decrement', function() {
decrementCounter();
});
socket.on('interactive_counter_counter_reset', function() {
resetCounter();
});
socket.on('interactive_counter_special_effect', function() {
triggerSpecialEffect();
});
socket.on('interactive_counter_send_message', function(data) {
displayMessage(data.text);
});
Database Integration
Setting Data:
// Save data to database
socket.emit('set_data', {
path: 'custom_templates/my_template/settings',
data: {
counter_value: 42,
last_update: new Date().toISOString(),
user_preferences: {
theme: 'dark',
animations: true
}
}
});
Getting Data:
// Retrieve data from database
socket.emit('get_data', {
path: 'custom_templates/my_template/settings'
});
socket.on('get_data', function(response) {
if (response.error) {
console.error('Database error:', response.error);
} else {
console.log('Retrieved data:', response);
// Use the data to initialize your template
}
});
Updating Data:
// Update existing data
socket.emit('update_data', {
path: 'custom_templates/my_template/settings',
data: {
counter_value: 43 // Only update specific fields
}
});
Connector Automation Integration
Templates can be fully integrated with the Connector automation system to enable automated control through trigger-action workflows. This allows your templates to respond to stream events automatically.
Defining Connector Actions
To make your template compatible with the Connector system, add a connector_actions section to your template configuration file. This defines what actions connectors can perform on your template.
Configuration (template_configs/my_template.json):
{
"template_name": "my_template",
"elements": [
// Regular configuration elements...
],
"connector_actions": {
"increment_counter": {
"action_name": "Increment Counter",
"elements": [
{
"type": "number",
"id": "increment_amount",
"label": "Increment Amount",
"value": 1,
"min": 1,
"max": 100,
"description": "How much to increment by"
}
]
},
"set_text": {
"action_name": "Set Text",
"elements": [
{
"type": "text",
"id": "new_text",
"label": "Text Content",
"value": "Default text",
"placeholder": "Enter text here...",
"description": "Text to display in the template"
}
]
},
"trigger_animation": {
"action_name": "Trigger Animation",
"elements": [
{
"type": "select",
"id": "animation_type",
"label": "Animation Type",
"value": "fade",
"options": ["fade", "slide", "bounce", "spin"],
"description": "Type of animation to trigger"
},
{
"type": "number",
"id": "animation_duration",
"label": "Duration (seconds)",
"value": 2.0,
"min": 0.1,
"max": 10.0,
"step": 0.1,
"description": "How long the animation should last"
}
]
}
}
}
Action Element Types:
The elements array in each connector action supports all the same element types as regular template configuration:
text- Text input fieldsnumber- Numeric input with validationselect- Dropdown selection with optionscheckbox- Boolean true/false valuesslider- Numeric slider with rangeseparator- Section headers for organization
Listening for Connector Events
In your template JavaScript, listen for connector-triggered events using the format: {template_name}_{action_id}
const socket = io();
// Listen for connector-triggered actions
socket.on('my_template_increment_counter', function(data) {
console.log('Connector triggered increment:', data);
// data includes:
// - increment_amount: Value from connector action configuration
// - trigger_id: ID of the trigger that fired this action
// - event_type: Type of original event (e.g., "twitch_bits")
// - timestamp: When the trigger fired
// - event data from the original trigger
incrementCounter(data.increment_amount || 1);
});
socket.on('my_template_set_text', function(data) {
console.log('Connector triggered text update:', data);
// data.new_text contains the configured text (with placeholders replaced)
updateDisplayText(data.new_text);
});
socket.on('my_template_trigger_animation', function(data) {
console.log('Connector triggered animation:', data);
// Use the configured animation parameters
triggerAnimation(data.animation_type, data.animation_duration);
});
Placeholder Support in Connector Actions
Connector actions support placeholder replacement, allowing dynamic content based on the triggering event:
Example Configuration with Placeholders:
{
"connector_actions": {
"show_donation_message": {
"action_name": "Show Donation Message",
"elements": [
{
"type": "text",
"id": "message_template",
"label": "Message Template",
"value": "Thanks {{username}} for the ${{amount}} donation!",
"description": "Use {{username}}, {{amount}}, {{message}} placeholders"
},
{
"type": "number",
"id": "display_duration",
"label": "Display Duration (seconds)",
"value": 5,
"min": 1,
"max": 30
}
]
}
}
}
Template JavaScript:
socket.on('my_template_show_donation_message', function(data) {
// data.message_template will have placeholders replaced
// e.g., "Thanks StreamViewer for the $10.00 donation!"
showMessage(data.message_template, data.display_duration * 1000);
});
function showMessage(text, duration) {
const messageEl = document.getElementById('message');
messageEl.textContent = text;
messageEl.style.display = 'block';
setTimeout(() => {
messageEl.style.display = 'none';
}, duration);
}
Available Placeholders:
Common placeholders available in connector actions:
{{username}}- User who triggered the event{{amount}}- Numeric amount (bits, donation, viewer count){{message}}- Message content (chat, donation message){{timestamp}}- When the event occurred{{event_type}}- Type of triggering event{{tier}}- Subscription tier (for sub events){{months}}- Subscription months (for sub events){{command}}- Chat command (for command events){{reward_title}}- Channel point reward name
Advanced Connector Integration
Dynamic Control Data:
Your template can provide dynamic data back to the connector system for use in conditions:
// Emit template state for connector conditions
function updateConnectorState() {
socket.emit('template_state_update', {
template_name: 'my_template',
state: {
counter_value: currentCounter,
last_action: lastActionType,
is_active: isTemplateActive,
current_mode: displayMode
}
});
}
// Call this whenever your template state changes
incrementCounter = function(amount) {
currentCounter += amount;
updateDisplay();
updateConnectorState(); // Notify connector system
};
Complex Action Handlers:
Handle complex connector actions with multiple configuration parameters:
socket.on('my_template_complex_action', function(data) {
console.log('Complex action triggered:', data);
// Access all configured parameters
const config = {
primaryColor: data.primary_color || '#ffffff',
secondaryColor: data.secondary_color || '#000000',
animationSpeed: data.animation_speed || 1.0,
enableSound: data.enable_sound || false,
customText: data.custom_text || 'Default text',
repeatCount: data.repeat_count || 1
};
// Execute complex template behavior
executeComplexAnimation(config);
});
Manual Connector Triggers
Templates can trigger manual connector events for testing or special interactions:
// Trigger manual connectors from template
function triggerManualConnector(customData) {
socket.emit('trigger_manual_connector', {
event_data: {
username: 'TemplateUser',
source: 'template_manual',
action_type: 'button_click',
template_name: 'my_template',
...customData
}
});
}
// Example: Button in template triggers manual connectors
document.getElementById('testButton').addEventListener('click', function() {
triggerManualConnector({
button_id: 'test_button',
click_count: Math.floor(Math.random() * 100)
});
});
Template Action Categories
Organize your connector actions into logical categories:
{
"connector_actions": {
// Counter Actions
"counter_increment": {
"action_name": "Increment Counter",
"category": "Counter",
"elements": [...]
},
"counter_decrement": {
"action_name": "Decrement Counter",
"category": "Counter",
"elements": [...]
},
"counter_reset": {
"action_name": "Reset Counter",
"category": "Counter",
"elements": []
},
// Visual Actions
"change_theme": {
"action_name": "Change Theme",
"category": "Visual",
"elements": [...]
},
"trigger_effect": {
"action_name": "Trigger Effect",
"category": "Visual",
"elements": [...]
},
// Text Actions
"update_title": {
"action_name": "Update Title",
"category": "Text",
"elements": [...]
}
}
}
Best Practices for Connector Integration
Descriptive Action Names: Use clear, descriptive names for connector actions that explain what they do.
Logical Grouping: Organize related actions and use categories to group them in the UI.
Sensible Defaults: Provide good default values for action parameters that work out of the box.
Parameter Validation: Set appropriate min/max values and validate input ranges.
Comprehensive Placeholders: Support relevant placeholders for the events that might trigger your actions.
Error Handling: Gracefully handle missing or invalid connector data.
State Reporting: Update connector state when your template changes to enable condition-based automation.
Testing Support: Provide ways to manually trigger actions for testing connector workflows.
Example: Complete Connector-Enabled Template
Template Configuration (template_configs/smart_counter.json):
{
"template_name": "smart_counter",
"elements": [
{
"type": "separator",
"label": "Display Settings"
},
{
"type": "select",
"id": "counter_color",
"label": "Counter Color",
"value": "#ffffff",
"display": "color_grid",
"options": ["#ffffff", "#ff0000", "#00ff00", "#0000ff"]
},
{
"type": "number",
"id": "font_size",
"label": "Font Size",
"value": 48,
"min": 12,
"max": 128
}
],
"connector_actions": {
"increment": {
"action_name": "Increment Counter",
"elements": [
{
"type": "number",
"id": "amount",
"label": "Increment Amount",
"value": 1,
"min": 1,
"max": 1000
}
]
},
"set_value": {
"action_name": "Set Counter Value",
"elements": [
{
"type": "number",
"id": "new_value",
"label": "New Value",
"value": 0,
"min": 0,
"max": 999999
}
]
},
"show_message": {
"action_name": "Show Message",
"elements": [
{
"type": "text",
"id": "message_text",
"label": "Message",
"value": "Thanks {{username}}!",
"placeholder": "Enter message with {{placeholders}}"
},
{
"type": "number",
"id": "duration",
"label": "Duration (seconds)",
"value": 3,
"min": 1,
"max": 30
}
]
}
}
}
Template HTML/JavaScript (templates/smart_counter.html):
<!DOCTYPE html>
<html>
<head>
<title>Smart Counter</title>
<script src="https://cdn.socket.io/4.0.0/socket.io.min.js"></script>
<style>
body {
margin: 0;
background: transparent;
font-family: Arial, sans-serif;
}
.counter {
font-size: 48px;
font-weight: bold;
color: #ffffff;
text-align: center;
padding: 20px;
}
.message {
font-size: 24px;
color: #ffff00;
text-align: center;
display: none;
margin-top: 10px;
}
</style>
</head>
<body>
<div id="counter" class="counter">0</div>
<div id="message" class="message"></div>
<script>
const socket = io();
let counterValue = 0;
let config = {};
// Load template configuration
async function loadConfig() {
try {
const response = await fetch('/api/all-template-configs');
const allConfigs = await response.json();
const myConfig = allConfigs.smart_counter;
if (myConfig && myConfig.elements) {
myConfig.elements.forEach(element => {
if (element.id) {
config[element.id] = element.value;
}
});
applyConfig();
}
} catch (error) {
console.error('Failed to load config:', error);
}
}
function applyConfig() {
const counter = document.getElementById('counter');
if (config.counter_color) {
counter.style.color = config.counter_color;
}
if (config.font_size) {
counter.style.fontSize = config.font_size + 'px';
}
}
// Connector action handlers
socket.on('smart_counter_increment', function(data) {
console.log('Increment triggered by connector:', data);
counterValue += data.amount || 1;
updateDisplay();
updateConnectorState();
});
socket.on('smart_counter_set_value', function(data) {
console.log('Set value triggered by connector:', data);
counterValue = data.new_value || 0;
updateDisplay();
updateConnectorState();
});
socket.on('smart_counter_show_message', function(data) {
console.log('Show message triggered by connector:', data);
showMessage(data.message_text, data.duration * 1000);
});
function updateDisplay() {
document.getElementById('counter').textContent = counterValue;
}
function showMessage(text, duration) {
const messageEl = document.getElementById('message');
messageEl.textContent = text;
messageEl.style.display = 'block';
setTimeout(() => {
messageEl.style.display = 'none';
}, duration);
}
function updateConnectorState() {
socket.emit('template_state_update', {
template_name: 'smart_counter',
state: {
counter_value: counterValue,
last_updated: new Date().toISOString()
}
});
}
// Initialize
loadConfig();
setTimeout(() => loadConfig(), 1000); // Reload periodically
</script>
</body>
</html>
Advanced Features
Audio Integration
Getting Available Audio Files:
// Get audio files from a specific folder
socket.emit('get_audio_files', {
folder: 'alerts/follows',
request_id: 'audio_request_1'
});
socket.on('audio_files_response', function(data) {
if (data.request_id === 'audio_request_1' && data.success) {
console.log('Available audio files:', data.files);
// data.files contains array of filenames
}
});
Playing Audio:
function playAlertSound(audioPath, volume = 1.0) {
const audio = new Audio(`/assets/${audioPath}`);
audio.volume = volume;
audio.play().catch(e => console.error('Audio play error:', e));
}
Asset Management
Loading Images:
function loadAlertGif(gifPath) {
const img = document.createElement('img');
img.src = `/assets/${gifPath}`;
img.onload = function() {
// GIF loaded successfully
document.body.appendChild(img);
};
img.onerror = function() {
console.error('Failed to load GIF:', gifPath);
};
}
Asset URLs:
Template-specific assets:
/assets/template_name/file.extShared assets:
/assets/default_assets/file.extFonts:
/assets/default_assets/fonts/font.ttfSounds:
/assets/sounds/sound.mp3
Template Examples
Donation Goal Tracker
<!DOCTYPE html>
<html>
<head>
<title>Donation Goal</title>
<script src="https://cdn.socket.io/4.0.0/socket.io.min.js"></script>
<style>
.goal-container {
width: 400px;
background: rgba(0,0,0,0.8);
border-radius: 10px;
padding: 20px;
color: white;
font-family: Arial, sans-serif;
}
.progress-bar {
width: 100%;
height: 30px;
background: #333;
border-radius: 15px;
overflow: hidden;
margin: 10px 0;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #4CAF50, #45a049);
transition: width 1s ease;
border-radius: 15px;
}
</style>
</head>
<body>
<div class="goal-container">
<h2 id="goalTitle">Donation Goal</h2>
<div class="progress-bar">
<div id="progressFill" class="progress-fill" style="width: 0%"></div>
</div>
<p id="goalText">$0 / $500</p>
</div>
<script>
const socket = io();
let currentAmount = 0;
const goalAmount = 500;
// Listen for donation alerts
socket.on('next_alert', function(data) {
if (data.alert_type === 'donation') {
currentAmount += parseFloat(data.donation_amount);
updateGoal();
}
});
function updateGoal() {
const percentage = Math.min((currentAmount / goalAmount) * 100, 100);
document.getElementById('progressFill').style.width = percentage + '%';
document.getElementById('goalText').textContent =
`$${currentAmount.toFixed(2)} / $${goalAmount}`;
if (percentage >= 100) {
document.getElementById('goalTitle').textContent = 'Goal Reached! 🎉';
}
}
// Load saved progress
socket.emit('get_data', {path: 'donation_goal/current_amount'});
socket.on('get_data', function(response) {
if (!response.error && response.data) {
currentAmount = response.data || 0;
updateGoal();
}
});
</script>
</body>
</html>
Real-time Follower Count
<!DOCTYPE html>
<html>
<head>
<title>Follower Count</title>
<script src="https://cdn.socket.io/4.0.0/socket.io.min.js"></script>
<style>
.counter {
font-size: 48px;
font-weight: bold;
color: #9146ff;
text-align: center;
text-shadow: 2px 2px 4px rgba(0,0,0,0.5);
font-family: 'Arial Black', sans-serif;
}
</style>
</head>
<body>
<div class="counter" id="followerCount">0 Followers</div>
<script>
const socket = io();
let followerCount = 0;
// Update count when someone follows
socket.on('next_alert', function(data) {
if (data.alert_type === 'follow') {
followerCount++;
updateDisplay();
// Animate the update
document.getElementById('followerCount').style.transform = 'scale(1.2)';
setTimeout(() => {
document.getElementById('followerCount').style.transform = 'scale(1)';
}, 300);
}
});
function updateDisplay() {
document.getElementById('followerCount').textContent =
`${followerCount} Followers`;
}
// Get initial follower count via Twitch API
socket.emit('twitch-api-request', {
endpoint: 'https://api.twitch.tv/helix/channels/followers',
requestId: 'follower_count'
});
socket.on('twitch-api-response', function(data) {
if (data.requestId === 'follower_count' && data.success) {
followerCount = data.data.total || 0;
updateDisplay();
}
});
</script>
</body>
</html>
Best Practices
Template Development
Always include Socket.IO:
<script src="https://cdn.socket.io/4.0.0/socket.io.min.js"></script>Handle connection events: Listen for connect/disconnect events
Use transparent backgrounds: For overlay compatibility
Implement error handling: Gracefully handle missing data
Test with different data: Ensure your template works with various alert types
Optimize performance: Avoid heavy animations that could impact stream performance
Configuration Management
Use descriptive labels: Make configuration options clear for users
Provide sensible defaults: Templates should work out of the box
Include descriptions: Help users understand what each setting does
Group related settings: Use logical organization in your config
Validate input ranges: Set appropriate min/max values for numbers
Asset Organization
Create template-specific folders: Keep assets organized by template
Use appropriate file formats: Optimize images and audio for web use
Provide fallbacks: Handle missing assets gracefully
Consider file sizes: Large assets can impact loading performance
Use relative paths: Always reference assets via
/assets/URLs
Error Handling
// Always handle WebSocket errors
socket.on('connect_error', function(error) {
console.error('Connection error:', error);
// Show offline state or retry logic
});
// Handle missing data gracefully
socket.on('next_alert', function(data) {
try {
if (!data || !data.alert_type) {
console.warn('Invalid alert data received');
return;
}
// Process alert...
} catch (error) {
console.error('Error processing alert:', error);
}
});
Debugging Templates
Use browser console: Check for JavaScript errors
Test WebSocket connection: Verify events are being received
Validate JSON configs: Ensure configuration files are valid
Check asset paths: Verify all assets load correctly
Monitor network requests: Use browser dev tools to debug loading issues
Template Testing
Test with real data: Use actual alerts to verify functionality
Test edge cases: Handle missing or unusual data
Test different screen sizes: Ensure compatibility with various resolutions
Test performance: Monitor CPU/memory usage during heavy activity
Test in OBS: Verify the template works correctly as a browser source