Template Syntax

Mustache template syntax reference for custom plugins.

Overview

Custom plugin HTML templates use Mustache syntax for dynamic content. This provides a simple, logic-less templating system that keeps complex logic in JavaScript where it belongs.

Why Mustache?
Mustache is logic-less by design, encouraging separation of concerns. Complex conditionals and data transformations belong in JavaScript.
Template Syntax is for HTML & CSS Only!

⚠️ The {{variable}} template syntax ONLY works in HTML and CSS. It does NOT work in JavaScript!

✅ HTML:<h1>{{title}}</h1>
✅ CSS:.card { background: {{backgroundColor}}; }
❌ JavaScript:const title = "{{title}}";(doesn't work!)
In JavaScript, use:const config = window.mp4e?.getConfig();
const title = config.title;

Variable Interpolation

Use double curly braces to insert config values into your template:

Basic interpolation
1<!-- Basic interpolation -->
2<h1>{{title}}</h1>
3<p>{{description}}</p>
4
5<!-- Nested properties -->
6<span class="price">{{product.price}}</span>
7<span class="currency">{{product.currency}}</span>
8
9<!-- Array access -->
10<p>First item: {{items.0}}</p>
11
12<!-- With default values (via JS) -->
13<span>{{title}}</span> <!-- Falls back to empty string if undefined -->

Playback State Variables

The engine injects playback state as system variables, available in any {{...}} expression. These update automatically every frame during playback.

VariableTypeDescription
{{currentFrame}}numberCurrent frame number (1-based)
{{currentTime}}numberCurrent playback time in seconds
{{duration}}numberTotal video duration in seconds
{{totalFrames}}numberTotal number of frames
{{fps}}numberVideo frame rate
{{isPlaying}}booleanWhether video is currently playing
{{playbackRate}}numberCurrent playback speed (1 = normal)
{{volume}}numberCurrent volume level (0-1)
{{isMuted}}booleanWhether audio is muted
{{isFullscreen}}booleanWhether player is in fullscreen mode
Playback state usage
1<!-- Playback state variables are injected by the engine and update every frame -->
2
3<!-- Current position -->
4<span>Frame: {{currentFrame}}</span>
5<span>Time: {{currentTime}}s</span>
6
7<!-- Video metadata -->
8<span>Duration: {{duration}}s</span>
9<span>Total Frames: {{totalFrames}}</span>
10<span>FPS: {{fps}}</span>
11
12<!-- Playback state -->
13<span>Playing: {{isPlaying}}</span>
14<span>Rate: {{playbackRate}}x</span>
15<span>Volume: {{volume}}</span>
16<span>Muted: {{isMuted}}</span>
17<span>Fullscreen: {{isFullscreen}}</span>
18
19<!-- CSS custom properties driven by playback state -->
20<style>
21 .progress { width: calc({{currentTime}} / {{duration}} * 100%); }
22</style>
Frame-Accurate Updates
For replacement zones and overlay configs, playback state variables are re-interpolated on every frame during playback. This makes them ideal for frame-driven animations like color cycling or progress indicators.

Math Expressions

Template expressions support inline arithmetic. You can use +, -, *, /, % (modulo) and parentheses to build calculated values:

Math expressions in templates
1<!-- Inline arithmetic inside {{...}} templates -->
2<!-- Supported operators: + - * / % (modulo) and parentheses -->
3
4<!-- Cycle hue through 360 degrees based on frame -->
5<div style="filter: hue-rotate({{(currentFrame * 4) % 360}}deg)">
6 Color-cycling content
7</div>
8
9<!-- Calculate percentage -->
10<span>{{score * 100 / maxScore}}%</span>
11
12<!-- Combine variables with math -->
13<span>Total: {{price * quantity + shippingCost}}</span>
14
15<!-- Nested parentheses -->
16<span>{{(baseScore + bonus) * multiplier}}</span>
17
18<!-- Mix playback state with project variables -->
19<span>Frame offset: {{currentFrame - startFrame}}</span>
Expression Builder
The Studio's Expression Builder (click the fx button on any template input) provides clickable operators, functions, and variable chips to help you construct expressions without remembering syntax.

Object Data Variables

For object display plugins (tooltips, product cards attached to detected objects), you can access the object's properties and custom data fields:

Object data access
1<!-- Object Display Plugins: Access object data -->
2
3<!-- Built-in object properties -->
4<h1>{{object.label}}</h1> <!-- AI-detected label: "chair", "person" -->
5<h2>{{object.userLabel}}</h2> <!-- User-defined label -->
6<span>ID: {{object.id}}</span> <!-- Unique object ID -->
7
8<!-- Custom data fields (defined in Group Data Schema) -->
9<h3>{{object.data.title}}</h3>
10<p>{{object.data.description}}</p>
11<span class="price">${{object.data.price}}</span>
12<span class="currency">{{object.data.currency}}</span>
13<a href="{{object.data.url}}">Buy Now</a>
14<img src="{{object.data.imageUrl}}" alt="{{object.data.title}}">
15
16<!-- Fallback operator: use first non-empty value -->
17<h1>{{object.data.title || object.userLabel || object.label}}</h1>
18
19<!-- Example: Product tooltip for detected object -->
20<div class="product-tooltip">
21 <img src="{{object.data.imageUrl}}" alt="{{object.data.title}}">
22 <div class="info">
23 <h3>{{object.data.title || object.userLabel}}</h3>
24 <p>{{object.data.description}}</p>
25 <span class="price">${{object.data.price}} {{object.data.currency}}</span>
26 </div>
27</div>
Custom Data Schema
Custom fields like object.data.title are defined in the Group Settings → Data Schema tab. Each group can define its own fields (e.g., e-commerce groups might have price, URL; educational groups might have difficulty, duration).

Conditionals

Sections render content based on truthiness. Use # for truthy and ^ for falsy:

Conditional rendering
1<!-- Section: Renders if truthy -->
2{{#showImage}}
3<img src="{{imageUrl}}" alt="{{title}}">
4{{/showImage}}
5
6<!-- Inverted section: Renders if falsy -->
7{{^showImage}}
8<div class="placeholder">
9 <span>No image available</span>
10</div>
11{{/showImage}}
12
13<!-- Boolean checks -->
14{{#isOnSale}}
15<span class="badge sale">SALE</span>
16{{/isOnSale}}
17
18{{^isOnSale}}
19<span class="badge">Regular Price</span>
20{{/isOnSale}}
21
22<!-- Existence check (renders if defined and non-empty) -->
23{{#discount}}
24<span class="discount">-{{discount}}%</span>
25{{/discount}}
26
27<!-- Nested conditionals -->
28{{#product}}
29 {{#product.inStock}}
30 <button>Add to Cart</button>
31 {{/product.inStock}}
32 {{^product.inStock}}
33 <button disabled>Out of Stock</button>
34 {{/product.inStock}}
35{{/product}}

Loops & Iteration

Iterate over arrays using sections. Inside the loop, properties are accessible directly:

Array iteration
1<!-- Iterating over arrays -->
2<ul class="features">
3 {{#features}}
4 <li>{{.}}</li>
5 {{/features}}
6</ul>
7
8<!-- Array of objects -->
9<div class="products">
10 {{#products}}
11 <div class="product-card">
12 <h3>{{name}}</h3>
13 <p>{{description}}</p>
14 <span class="price">${{price}}</span>
15 </div>
16 {{/products}}
17</div>
18
19<!-- Empty array fallback -->
20{{#products}}
21<div class="product">{{name}}</div>
22{{/products}}
23{{^products}}
24<p class="empty">No products available</p>
25{{/products}}
26
27<!-- Nested loops -->
28{{#categories}}
29<div class="category">
30 <h2>{{name}}</h2>
31 <ul>
32 {{#items}}
33 <li>{{title}} - {{price}}</li>
34 {{/items}}
35 </ul>
36</div>
37{{/categories}}
38
39<!-- Current item reference with . -->
40{{#tags}}
41<span class="tag">{{.}}</span>
42{{/tags}}

HTML Escaping

By default, values are HTML-escaped to prevent XSS. Use triple braces for trusted HTML:

Escaping behavior
1<!-- Default: HTML-escaped (safe) -->
2<p>{{userInput}}</p>
3<!-- Input: "<script>alert('xss')</script>" -->
4<!-- Output: &lt;script&gt;alert('xss')&lt;/script&gt; -->
5
6<!-- Triple mustache: Unescaped HTML (use carefully!) -->
7<div class="rich-content">{{{richHtml}}}</div>
8<!-- Input: "<strong>Bold text</strong>" -->
9<!-- Output: <strong>Bold text</strong> -->
10
11<!-- When to use unescaped -->
12<!-- SAFE: Content from your CMS/database that you control -->
13{{{trustedHtmlContent}}}
14
15<!-- DANGEROUS: Never use with user input! -->
16<!-- {{{userProvidedHtml}}} <-- DON'T DO THIS -->
17
18<!-- Alternative: Use JS for dynamic content -->
19<div id="content"></div>
20<!-- In JS: document.getElementById('content').textContent = userInput; -->
Security Warning
Never use triple braces {{{}}} with user-provided content. Only use it for trusted HTML from your own systems.

Context Variables

Access player context through the special _context variable:

Context access
1<!-- Playback state variables (engine-injected, available everywhere) -->
2<span>Time: {{currentTime}}s</span>
3<span>Frame: {{currentFrame}}</span>
4<span>Duration: {{duration}}s</span>
5
6<!-- Conditionals with playback state -->
7{{#isPlaying}}
8<span class="status">Playing</span>
9{{/isPlaying}}
10{{^isPlaying}}
11<span class="status">Paused</span>
12{{/isPlaying}}
13
14<!-- Project variables (user-defined) -->
15<span>Cart: {{cartCount}} items</span>
16<span>Score: {{quizScore}}</span>
17
18<!-- Object context (when overlay is bound to an object) -->
19<div class="object-info">
20 <span>{{object.label}}</span>
21 <span>{{object.userLabel}}</span>
22</div>
23
24<!-- Math expressions with variables -->
25<div style="opacity: {{currentTime / duration}}">
26 Fading content
27</div>

Helper Functions

Use JavaScript helper functions for formatting and transformations:

Helper functions
1// Helper functions available in JavaScript
2
3// Format numbers
4mp4e.helpers.formatNumber(1234.56); // "1,234.56"
5mp4e.helpers.formatCurrency(99.99, 'USD'); // "$99.99"
6mp4e.helpers.formatPercent(0.156); // "15.6%"
7
8// Format dates
9mp4e.helpers.formatDate(date, 'short'); // "12/13/2025"
10mp4e.helpers.formatDate(date, 'long'); // "December 13, 2025"
11mp4e.helpers.formatTime(seconds); // "1:23" or "1:23:45"
12mp4e.helpers.formatRelativeTime(date); // "2 hours ago"
13
14// String helpers
15mp4e.helpers.truncate(text, 50); // Truncate to 50 chars
16mp4e.helpers.capitalize(text); // Capitalize first letter
17mp4e.helpers.slugify(text); // "Hello World" -> "hello-world"
18
19// Array helpers
20mp4e.helpers.first(array); // First element
21mp4e.helpers.last(array); // Last element
22mp4e.helpers.take(array, 3); // First 3 elements
23mp4e.helpers.shuffle(array); // Randomize order
24
25// Using helpers to update DOM
26var cfg = mp4e.getConfig();
27var priceEl = document.getElementById('price');
28priceEl.textContent = mp4e.helpers.formatCurrency(cfg.price, cfg.currency);
29
30// Listen for variable changes to update UI
31mp4e.onVariableChange(function(variables, changedIds) {
32 // Update only when relevant variable changes
33 if (changedIds && changedIds.indexOf('currentTime') < 0) return;
34 var timeEl = document.getElementById('video-time');
35 timeEl.textContent = mp4e.helpers.formatTime(variables.currentTime);
36});

Complete Examples

A comprehensive product card plugin using all template features:

Product card plugin
1<!-- Product Card Template -->
2{
3 template: {
4 html: `
5 <div class="product-card">
6 {{#imageUrl}}
7 <img src="{{imageUrl}}" alt="{{title}}" class="product-image">
8 {{/imageUrl}}
9 {{^imageUrl}}
10 <div class="product-image placeholder">
11 <svg viewBox="0 0 24 24" fill="currentColor">
12 <path d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"/>
13 </svg>
14 </div>
15 {{/imageUrl}}
16
17 <div class="product-content">
18 <h3 class="product-title">{{title}}</h3>
19
20 {{#subtitle}}
21 <p class="product-subtitle">{{subtitle}}</p>
22 {{/subtitle}}
23
24 <div class="product-pricing">
25 {{#originalPrice}}
26 <span class="original-price">${{originalPrice}}</span>
27 {{/originalPrice}}
28 <span class="current-price">${{price}}</span>
29 {{#discount}}
30 <span class="discount-badge">-{{discount}}%</span>
31 {{/discount}}
32 </div>
33
34 {{#rating}}
35 <div class="product-rating">
36 <div class="stars" style="--rating: {{rating}}"></div>
37 <span class="rating-count">({{reviewCount}})</span>
38 </div>
39 {{/rating}}
40
41 {{#tags}}
42 <div class="product-tags">
43 {{#tags}}
44 <span class="tag">{{.}}</span>
45 {{/tags}}
46 </div>
47 {{/tags}}
48
49 <button id="add-to-cart" class="btn-primary">
50 {{#inStock}}
51 Add to Cart
52 {{/inStock}}
53 {{^inStock}}
54 Out of Stock
55 {{/inStock}}
56 </button>
57 </div>
58 </div>
59 `,
60 css: `
61 .product-card {
62 background: white;
63 border-radius: 12px;
64 overflow: hidden;
65 box-shadow: 0 4px 12px rgba(0,0,0,0.1);
66 max-width: 300px;
67 }
68 .product-image {
69 width: 100%;
70 height: 200px;
71 object-fit: cover;
72 }
73 .product-image.placeholder {
74 background: #f3f4f6;
75 display: flex;
76 align-items: center;
77 justify-content: center;
78 color: #9ca3af;
79 }
80 .product-image.placeholder svg {
81 width: 48px;
82 height: 48px;
83 }
84 .product-content {
85 padding: 16px;
86 }
87 .product-title {
88 margin: 0 0 4px;
89 font-size: 18px;
90 font-weight: 600;
91 color: #111827;
92 }
93 .product-subtitle {
94 margin: 0 0 12px;
95 font-size: 14px;
96 color: #6b7280;
97 }
98 .product-pricing {
99 display: flex;
100 align-items: center;
101 gap: 8px;
102 margin-bottom: 12px;
103 }
104 .original-price {
105 text-decoration: line-through;
106 color: #9ca3af;
107 font-size: 14px;
108 }
109 .current-price {
110 font-size: 24px;
111 font-weight: 700;
112 color: #111827;
113 }
114 .discount-badge {
115 background: #fef2f2;
116 color: #ef4444;
117 padding: 2px 8px;
118 border-radius: 4px;
119 font-size: 12px;
120 font-weight: 600;
121 }
122 .product-rating {
123 display: flex;
124 align-items: center;
125 gap: 8px;
126 margin-bottom: 12px;
127 }
128 .stars {
129 --percent: calc(var(--rating) / 5 * 100%);
130 display: inline-block;
131 font-size: 14px;
132 line-height: 1;
133 background: linear-gradient(90deg, #fbbf24 var(--percent), #e5e7eb var(--percent));
134 -webkit-background-clip: text;
135 background-clip: text;
136 }
137 .stars::before {
138 content: '★★★★★';
139 -webkit-text-fill-color: transparent;
140 }
141 .rating-count {
142 font-size: 12px;
143 color: #6b7280;
144 }
145 .product-tags {
146 display: flex;
147 flex-wrap: wrap;
148 gap: 6px;
149 margin-bottom: 12px;
150 }
151 .tag {
152 background: #f3f4f6;
153 color: #4b5563;
154 padding: 4px 8px;
155 border-radius: 4px;
156 font-size: 12px;
157 }
158 .btn-primary {
159 width: 100%;
160 padding: 12px 16px;
161 background: #3b82f6;
162 color: white;
163 border: none;
164 border-radius: 8px;
165 font-size: 16px;
166 font-weight: 600;
167 cursor: pointer;
168 transition: background 0.2s;
169 }
170 .btn-primary:hover {
171 background: #2563eb;
172 }
173 .btn-primary:disabled {
174 background: #9ca3af;
175 cursor: not-allowed;
176 }
177 `,
178 js: `
179 var button = document.getElementById('add-to-cart');
180 var cfg = mp4e.getConfig();
181
182 if (!cfg.inStock) {
183 button.disabled = true;
184 }
185
186 button.addEventListener('click', function() {
187 mp4e.emit('onAddToCart', {
188 productId: cfg.productId,
189 title: cfg.title,
190 price: cfg.price
191 });
192
193 mp4e.executeActions([
194 { type: 'setVariable', variableId: 'cartCount', operation: 'increment', value: 1 },
195 { type: 'showNotification', message: 'Added to cart!' }
196 ]);
197
198 // Visual feedback
199 button.textContent = 'Added!';
200 button.style.background = '#22c55e';
201 setTimeout(() => {
202 button.textContent = 'Add to Cart';
203 button.style.background = '';
204 }, 2000);
205 });
206 `
207 }
208}