Creating the Widget
Creating a Widget
There are two sides to creating a widget:
- Widget Side (The .jar that runs in Caffeinated)
- Stream Side (The .html that runs in OBS or is embedded elsewhere)
Messaging #
Widget Side and Stream Side can communicate with eachother over a Json based event system, this is done by implementing Widget#onNewInstance() and registering listeners using WidgetInstance#on() and sending messages back by using WidgetInstance#emit().
In our case, we just want to log some basic info for the purpose of demonstrating how the system works, so that's what we'll do:
@Override
public void onNewInstance(@NonNull WidgetInstance instance) {
instance.on("message", (JsonElement e) -> {
// We just print the raw json.
FastLogger.logStatic("Received message from a widget instance: %s", e);
// instance.emit("mytype", mymessage);
});
}Initializing Stream Side #
We first need to load in our widget html, this is done by implementing Widget#getWidgetBasePath() and telling Caffeinated what resource it needs to load. Next, we implement CaffeinatedPlugin#getResource() and actually load the file from our Jar.
@Override
public @NonNull String getWidgetBasePath(WidgetInstanceMode mode) {
return "/chat";
}@Override
public @Nullable Pair<String, String> getResource(String resource) throws IOException {
// Figure out the mime tpye of the requested file.
String mimeType = "application/octet-stream";
String[] split = resource.split("\\.");
if (split.length > 1) {
mimeType = MimeTypes.getMimeForType(split[split.length - 1]);
}
// Read the file from our jar's resources. More advanced widgets can override
// this behavior to serve a modern "site" such as React or Vue or SvelteKit.
try (InputStream in = MyFirstPlugin.class.getClassLoader().getResourceAsStream(resource)) {
return new Pair<>(IOUtil.readInputStreamString(in, StandardCharsets.UTF_8), mimeType);
}
}Next, we need to make our widget html. The example provided just writes a simple chatlog on the screen (with some basic customization), but you can do anything you want.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Custom Widget</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/webfont/1.6.28/webfontloader.js"></script>
</head>
<body>
</body>
<footer>
<script>
const chatLog = document.getElementById("chatlog");
let oldConfig = {};
// We use webfont loader because it's easy, that's it.
function changeFont(fontname) {
fontname = fontname || "Poppins";
document.documentElement.style = "font-family: '" + fontname + "';";
WebFont.load({
google: {
families: [fontname],
},
});
}
changeFont("Poppins");
Widget.on("init", () => {
console.log("Init!");
// Manually trigger an update
Widget.broadcast("update");
});
Widget.on("update", () => {
changeFont(Widget.getSetting("chat_style.font"));
chatDiv.style.color = Widget.getSetting("chat_style.text_color");
chatDiv.style.fontSize =
Widget.getSetting("chat_style.font_size") + "px";
});
// When we get chat messages we want to append them to the chat log.
Koi.on("rich_message", (event) => {
// escapeHtml is a helper function that escapes HTML characters. It's provided by the loader.
// Additionally, we replace all spaces with non-breaking spaces so they don't get broken up.
const html = `${event.sender.displayname} > ${event.html}<br />`;
// Append it.
chatLog.innerHTML += html;
});
</script>
</footer>
</html>After creatring a new instance of this widget and loading it into OBS (or just a normal browser) you will see a basic chat history on the screen.

Note about events #
Koi events and music events are already sent to the Stream Side, so you don't need to implement your own event system.
Note about frameworks #
Casterlabs places no restrictions on what can be loaded by your plugin, just keep in mind that if you use a framework you will need to set it's base path to /$caffeinated-sdk-root$ otherwise the app will not know how to serve the contents (that string gets replaced on every loaded file). Example SvelteKit config here .
