@lumra/plugins
v0.2.4
Published
Lumra built-in plugins (analytics, paywall gate, autoplay)
Readme
@lumra/plugins
Official plugins for the Lumra media player — analytics, paywall gating, chapters, VAST ads, heatmap, and LUT colour grading.
Install
npm install @lumra/plugins @lumra/coreanalyticsPlugin
Fires milestone beacons at 25%, 50%, 75%, and 100% watched. Optionally POSTs to your analytics endpoint.
import { analyticsPlugin } from '@lumra/plugins'
player.use(analyticsPlugin({
resourceId: 'video-123',
endpoint: '/api/analytics', // your backend receives the milestone POST
onMilestone: (pct) => console.log(`${pct}% watched`),
}))What your endpoint receives — POST /api/analytics:
{ "resourceId": "video-123", "milestone": 50, "currentTime": 142.3, "duration": 284.6 }paywallGatePlugin
Blocks playback after a free preview period and fires a callback to show your paywall UI.
import { paywallGatePlugin } from '@lumra/plugins'
player.use(paywallGatePlugin({
previewDuration: 30,
onBlock: () => showPaywallOverlay(),
}))chaptersPlugin — Premium
The free version of chapters (passing a chapters array directly to the player) gives you seek bar markers and click-to-jump. That's all most people need — no plugin or license required.
The premium chaptersPlugin adds one extra thing: an onChapterChange(chapter, index) JavaScript callback that fires in your code every time the video crosses a chapter boundary during playback. Use this when you need to react to chapter changes — e.g. highlight the current chapter in a sidebar, log which chapters viewers reach in your analytics, show a pop-up as each chapter starts.
import { chaptersPlugin } from '@lumra/plugins'
player.use(chaptersPlugin({
licenseKey: 'LUMRA-CHAP-XXXXXXXX-XXXXXXXX',
verifyEndpoint: 'https://lumra.reelfoundry.au/api/verify',
verifyPublicKey: `-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA9S3mIScMh6vOGHCm48RQ
7VvdALKg7foE8hKK2OQF4LlIzZyxrNDkv1hklcPU62hf50qrmOBqC8h3KbIt8Tm+
kz4kJMQgwt93bALcAXfIWNClMUPQoJUryDB29cHjCi3VqFcVohkyQfk7Q6D+32wY
9SYidvDU4nFWONqCgzz3Tx55jYi+yTeM7SajlGpfFhwL733DHE5fkoPfrWzrHnNX
nb1X9xs76ZbHhbD1obJIxdpcFrNfZV7eGR0qmAaJIVAL1GF8q3rrn/blhrJNTAPS
WJzNwEuOJoa5lsAG7SSs4mNTxpwOPk1wPvXirBlITjNoVw4BHf1C6kkGaXhuBLcU
LwIDAQAB
-----END PUBLIC KEY-----`,
chapters: [
{ at: 0, title: 'Intro' },
{ at: 60, title: 'Act 1' },
{ at: 180, title: 'Climax' },
],
onChapterChange: (chapter, index) => {
// Fires each time the viewer enters a new chapter
if (chapter) console.log('Now in:', chapter.title) // update your UI here
},
}))To programmatically skip to a chapter: player.seek(chapter.at)
adPlugin — Premium
VAST 2.0/3.0 pre-roll, mid-roll, and post-roll ads. Parses VAST XML, fires impression and tracking beacons automatically. Requires a license key.
import { adPlugin } from '@lumra/plugins'
player.use(adPlugin({
licenseKey: 'LUMRA-ADS-XXXXXXXX-XXXXXXXX',
verifyEndpoint: 'https://lumra.reelfoundry.au/api/verify',
verifyPublicKey: `-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA9S3mIScMh6vOGHCm48RQ
7VvdALKg7foE8hKK2OQF4LlIzZyxrNDkv1hklcPU62hf50qrmOBqC8h3KbIt8Tm+
kz4kJMQgwt93bALcAXfIWNClMUPQoJUryDB29cHjCi3VqFcVohkyQfk7Q6D+32wY
9SYidvDU4nFWONqCgzz3Tx55jYi+yTeM7SajlGpfFhwL733DHE5fkoPfrWzrHnNX
nb1X9xs76ZbHhbD1obJIxdpcFrNfZV7eGR0qmAaJIVAL1GF8q3rrn/blhrJNTAPS
WJzNwEuOJoa5lsAG7SSs4mNTxpwOPk1wPvXirBlITjNoVw4BHf1C6kkGaXhuBLcU
LwIDAQAB
-----END PUBLIC KEY-----`,
preRoll: {
vast: 'https://pubads.g.doubleclick.net/...', // VAST tag URL
skipAfter: 5, // show Skip button after 5s (0 = not skippable)
},
midRoll: [
{ at: 120, vast: 'https://...', skipAfter: 0 },
],
postRoll: {
src: 'https://example.com/ads/post.mp4',
skipAfter: 0,
},
onAdStart: (info) => console.log('Ad started:', info.phase),
onAdEnd: () => console.log('Ad ended'),
onAdSkip: () => console.log('Ad skipped'),
// Optional: shown in the in-player "Get a license" banner when key is invalid
purchaseUrl: 'https://your-site.com/purchase',
}))heatmapPlugin — Premium
YouTube-style engagement heatmap on the seek bar. Your backend serves the view-count data; the plugin normalises it and renders it. Requires a license key.
import { heatmapPlugin } from '@lumra/plugins'
player.use(heatmapPlugin({
licenseKey: 'LUMRA-HEAT-XXXXXXXX-XXXXXXXX',
verifyEndpoint: 'https://lumra.reelfoundry.au/api/verify',
verifyPublicKey: `-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA9S3mIScMh6vOGHCm48RQ
7VvdALKg7foE8hKK2OQF4LlIzZyxrNDkv1hklcPU62hf50qrmOBqC8h3KbIt8Tm+
kz4kJMQgwt93bALcAXfIWNClMUPQoJUryDB29cHjCi3VqFcVohkyQfk7Q6D+32wY
9SYidvDU4nFWONqCgzz3Tx55jYi+yTeM7SajlGpfFhwL733DHE5fkoPfrWzrHnNX
nb1X9xs76ZbHhbD1obJIxdpcFrNfZV7eGR0qmAaJIVAL1GF8q3rrn/blhrJNTAPS
WJzNwEuOJoa5lsAG7SSs4mNTxpwOPk1wPvXirBlITjNoVw4BHf1C6kkGaXhuBLcU
LwIDAQAB
-----END PUBLIC KEY-----`,
data: 'https://yourapp.com/api/heatmap/video-123',
onData: (normalised) => renderHeatmap(normalised), // 0–1 array, one value per segment
}))What your endpoint must return — GET /api/heatmap/:id:
{ "data": [120, 450, 890, 670, 230, 90] }Each number is a view count for one time segment (one value per 5 seconds works well). The plugin divides by the maximum to produce a 0–1 overlay. Build this data by incrementing a counter per segment in your analytics webhook.
lutPlugin — Premium
Real-time WebGL colour grading via .cube LUT files. Requires a license key.
import { lutPlugin } from '@lumra/plugins'
import type { LutPluginHandle } from '@lumra/plugins'
const lut = lutPlugin({
licenseKey: 'LUMRA-LUT-XXXXXXXX-XXXXXXXX',
verifyEndpoint: 'https://lumra.reelfoundry.au/api/verify',
verifyPublicKey: `-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA9S3mIScMh6vOGHCm48RQ
7VvdALKg7foE8hKK2OQF4LlIzZyxrNDkv1hklcPU62hf50qrmOBqC8h3KbIt8Tm+
kz4kJMQgwt93bALcAXfIWNClMUPQoJUryDB29cHjCi3VqFcVohkyQfk7Q6D+32wY
9SYidvDU4nFWONqCgzz3Tx55jYi+yTeM7SajlGpfFhwL733DHE5fkoPfrWzrHnNX
nb1X9xs76ZbHhbD1obJIxdpcFrNfZV7eGR0qmAaJIVAL1GF8q3rrn/blhrJNTAPS
WJzNwEuOJoa5lsAG7SSs4mNTxpwOPk1wPvXirBlITjNoVw4BHf1C6kkGaXhuBLcU
LwIDAQAB
-----END PUBLIC KEY-----`,
url: '/luts/cinematic.cube',
intensity: 0.85,
// Optional: shown in the in-player "Get a license" banner when key is invalid
purchaseUrl: 'https://your-site.com/purchase',
}) as LutPluginHandle
player.use(lut)
// Hot-swap at runtime
await lut.setLut('/luts/vintage.cube')
lut.setIntensity(0.5) // 0 = original, 1 = fully gradedAbout verifyPublicKey
verifyPublicKey is optional on all premium plugins. Without it, the plugin still calls the license server and validates your key — it just skips the extra step of cryptographically verifying the server's JWT response locally in the browser.
The public key is the same for all customers — it's Lumra's license server key, shown in the examples above. Copy it as-is.
| | Without verifyPublicKey | With verifyPublicKey |
|---|---|---|
| Server validates key | Yes | Yes |
| Browser verifies JWT signature | No | Yes |
| Security | Good | Best |
Hiding your license key (proxy mode)
When using Lumra in a CDN/plain-HTML page your licenseKey is visible in the page source. Use a server-side proxy to keep it out of the browser entirely.
Frontend — no key in browser code:
<script>
Lumra.createPlayer('#player', {
lut: {
// No licenseKey here — the proxy adds it server-side
verifyEndpoint: '/api/lumra-verify',
url: '/luts/cinematic.cube',
},
})
</script>Your proxy endpoint (Node.js / Express):
app.post('/api/lumra-verify', async (req, res) => {
const resp = await fetch('https://lumra.reelfoundry.au/api/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ...req.body, key: process.env.LUMRA_LICENSE_KEY }),
})
res.json(await resp.json())
})PHP proxy:
<?php // /api/lumra-verify.php
$body = json_decode(file_get_contents('php://input'), true) ?? [];
$body['key'] = getenv('LUMRA_LICENSE_KEY');
$ch = curl_init('https://lumra.reelfoundry.au/api/verify');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($body),
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_RETURNTRANSFER => true,
]);
header('Content-Type: application/json');
echo curl_exec($ch);Next.js API Route:
// app/api/lumra-verify/route.ts
export async function POST(req: Request) {
const body = await req.json()
const resp = await fetch('https://lumra.reelfoundry.au/api/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ...body, key: process.env.LUMRA_LICENSE_KEY }),
})
return Response.json(await resp.json())
}The browser only ever sends product + origin to your proxy. The real key lives in process.env and never touches the client.
Getting a license key
Purchase a license at lumra.reelfoundry.au/purchase. Premium plugins (chaptersPlugin, adPlugin, heatmapPlugin, lutPlugin) each require their own key.
Use the demo key (LUMRA-LUT-DEMO0001-DEMO0001, LUMRA-ADS-DEMO0001-DEMO0001, etc.) for local development — demo keys only work on localhost and will show a purchase banner on any public domain.
© 2026 Reel Foundry AU. All rights reserved.
MIT License — see LICENSE
