Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,15 @@ Sign a build by exporting `XTV_CCL_*` env (see `docs/signing.md`) before `build`
7. **Ports & adapters** for platform capabilities (see `libs/muting`): a
platform-agnostic controller depends on an interface; each platform supplies an
adapter. Add a platform = new adapter, controller untouched.
8. **UI render layer = Blits** (`@lightningjs/blits`, LightningJS canvas). `libs/core`
`Blits.Launch(App, "app", …)` mounts the root Blits Application (`libs/core/src/app.ts`);
widgets are Blits components (`libs/widgets/src/components/*.component.ts`). The old
DOM `layout`/`widget-registry` path is retained (still typechecks, feeds the
`CustomerLayout` type) but is **not** the render path — it will be reworked into a
Blits-native dynamic layout engine. **Transitional:** the foundation renders one
known widget (hero) from config; fully config-driven multi-widget + feature-gated
Blits layout, keymap→Blits input, and Blits-reactive hot-apply (currently a soft
reload) are follow-ups.

## How to…

Expand Down Expand Up @@ -126,6 +135,15 @@ Sign a build by exporting `XTV_CCL_*` env (see `docs/signing.md`) before `build`
- **LG/Android per-cruiseline certs** are procurement-pending; the pluggable design
keeps builds green meanwhile.
- On this dev Mac: `tizen` + `ares-package` are installed, `gradle` is not.
- **Blits build quirks:** (a) never put `.blits.` in a `.ts` filename — the Blits
Vite converter treats the import as a `.blits` SFC and fails; name Blits component
files `*.component.ts`. (b) each `apps/<p>-tv/` needs a `public/` dir (Blits'
msdfGenerator scans it) — keep the `.gitkeep`. (c) Blits' Vite plugin is the
**default array export** of `@lightningjs/blits/vite` — spread it: `plugins: [...blits]`.
- **Tizen firmware SDK is runtime-injected**, not a static `index.html` tag —
`apps/samsung-tv/src/main.ts` appends the `$WEBAPIS/$B2BAPIS` scripts before
bootstrap (static tags can't be bundled by Vite). Guarded adapters fall back if
absent.
- **Platform SDK globals are firmware-provided, not vendored.** `apps/samsung-tv/index.html`
loads Tizen `webapis.js` / `avplayextension.js` / `b2bapis.js` via `$WEBAPIS`/`$B2BAPIS`
script tags (resolved on-device; 404 harmlessly in a browser). LG uses the
Expand Down
Empty file added apps/android-tv/public/.gitkeep
Empty file.
2 changes: 2 additions & 0 deletions apps/android-tv/vite.config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import blits from "@lightningjs/blits/vite";
import { defineConfig } from "vite";
import { createXtvAliases } from "../../tools/vite/xtv-aliases";

Expand All @@ -11,6 +12,7 @@ export default defineConfig({
// Relative base so bundle assets resolve under file:///android_asset/.
base: "./",
cacheDir: "../../node_modules/.vite/apps/android-tv",
plugins: [...blits],
resolve: {
alias: createXtvAliases(workspaceRoot, process.env.VITE_XTV_CUSTOMER),
},
Expand Down
Empty file added apps/lg-tv/public/.gitkeep
Empty file.
2 changes: 2 additions & 0 deletions apps/lg-tv/vite.config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import blits from "@lightningjs/blits/vite";
import { defineConfig } from "vite";
import { createXtvAliases } from "../../tools/vite/xtv-aliases";

Expand All @@ -11,6 +12,7 @@ export default defineConfig({
// Relative base so bundle assets resolve under file:// (webOS package root).
base: "./",
cacheDir: "../../node_modules/.vite/apps/lg-tv",
plugins: [...blits],
resolve: {
alias: createXtvAliases(workspaceRoot, process.env.VITE_XTV_CUSTOMER),
},
Expand Down
12 changes: 4 additions & 8 deletions apps/samsung-tv/index.html
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
<!--
Tizen firmware SDK globals. Classic (non-module) scripts run before the deferred
module bundle, so webapis.avplay / tizen.tvaudiocontrol exist at bootstrap. The
$WEBAPIS / $B2BAPIS tokens are resolved by the TV at runtime; in a browser they
404 harmlessly and the adapters fall back. Samsung only — LG uses firmware HCAP,
Android uses the native bridge.
Tizen firmware SDK globals (webapis.js / avplayextension.js / b2bapis.js at
$WEBAPIS / $B2BAPIS) are injected at RUNTIME by the Samsung entry before the
Blits app boots — static tags here can't be bundled by Vite (unresolvable
tokens). See apps/samsung-tv/src/main.ts.
-->
<script src="$WEBAPIS/webapis/webapis.js"></script>
<script src="$WEBAPIS/avplayextension/avplayextension.js"></script>
<script src="$B2BAPIS/b2bapis/b2bapis.js"></script>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
Empty file added apps/samsung-tv/public/.gitkeep
Empty file.
20 changes: 20 additions & 0 deletions apps/samsung-tv/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,25 @@
import { bootstrapTvPlatform } from "@x-tv/core";

// Inject the Tizen firmware SDK globals at runtime. Static <script> tags in
// index.html can't be bundled by Vite ($WEBAPIS/$B2BAPIS are TV-resolved tokens,
// not real files). On a real TV these resolve and create webapis.avplay /
// tizen.tvaudiocontrol; in a browser they 404 and the guarded adapters fall back.
function injectTizenSdk(): void {
const sources = [
"$WEBAPIS/webapis/webapis.js",
"$WEBAPIS/avplayextension/avplayextension.js",
"$B2BAPIS/b2bapis/b2bapis.js",
];
for (const src of sources) {
const script = document.createElement("script");
script.src = src;
script.async = false;
document.head.appendChild(script);
}
}

injectTizenSdk();

void bootstrapTvPlatform({
appId: "samsung-tv",
platformId: "samsung",
Expand Down
2 changes: 2 additions & 0 deletions apps/samsung-tv/vite.config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import blits from "@lightningjs/blits/vite";
import { defineConfig } from "vite";
import { createXtvAliases } from "../../tools/vite/xtv-aliases";

Expand All @@ -12,6 +13,7 @@ export default defineConfig({
// This is the LightningJS analog of CCL's XC_WEB_ROOT path-portability fix.
base: "./",
cacheDir: "../../node_modules/.vite/apps/samsung-tv",
plugins: [...blits],
resolve: {
alias: createXtvAliases(workspaceRoot, process.env.VITE_XTV_CUSTOMER),
},
Expand Down
33 changes: 33 additions & 0 deletions libs/core/src/app.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import Blits from "@lightningjs/blits";
import type { CustomerLayout } from "@x-tv/layout";
import { cclTheme } from "@x-tv/themes";
import { HeroBanner } from "@x-tv/widgets";
import { getBootConfig } from "./boot-config";

// Root Blits Application. Reads the resolved tenant config once and renders the
// hero from the active layout. This is the foundation: a single known widget.
// The fully config-driven, feature-gated, multi-widget Blits layout engine
// (porting @x-tv/layout to resolve widgets by type) is the next step.
export default Blits.Application({
components: { HeroBanner },
template: `
<Element w="1920" h="1080" color="$background">
<HeroBanner title="$title" subtitle="$subtitle" background="$background" />
</Element>
`,
state() {
const config = getBootConfig();
const hero = findHeroProps(config.layout);
return {
title: hero.title,
subtitle: hero.subtitle,
background: cclTheme.colors.background,
};
},
});

function findHeroProps(layout: CustomerLayout): { title: string; subtitle: string } {
const node = layout.root.children?.find((child) => child.widget === "hero-banner");
const props = (node?.props ?? {}) as { title?: string; subtitle?: string };
return { title: props.title ?? "", subtitle: props.subtitle ?? "" };
}
17 changes: 17 additions & 0 deletions libs/core/src/boot-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { RuntimeConfig } from "@x-tv/runtime-config";

// Bridges the resolved RuntimeConfig into Blits component state. Blits.Launch
// mounts the root Application which reads this in state(); we set it just before
// launch. (A Blits plugin is the richer option later; this keeps the seam small.)
let current: RuntimeConfig | undefined;

export function setBootConfig(config: RuntimeConfig): void {
current = config;
}

export function getBootConfig(): RuntimeConfig {
if (!current) {
throw new Error("Boot config accessed before setBootConfig().");
}
return current;
}
59 changes: 18 additions & 41 deletions libs/core/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import Blits from "@lightningjs/blits";
import {
captureConsoleLogs,
createDiagnosticsOverlay,
createLogBuffer,
readDeviceInfo,
} from "@x-tv/diagnostics";
import { createLayoutRenderer } from "@x-tv/layout";
import { createAudioController, createMutingController } from "@x-tv/muting";
import { createNavigationEngine } from "@x-tv/navigation";
import { type RuntimeConfig, createRuntimeConfigLoader } from "@x-tv/runtime-config";
import { createRuntimeConfigLoader } from "@x-tv/runtime-config";
import { createServiceGateway } from "@x-tv/service-gateway";
import { createWebsocketEventBus } from "@x-tv/websocket";
import { createDefaultWidgetRegistry } from "@x-tv/widget-registry";
import App from "./app";
import { setBootConfig } from "./boot-config";

export type PlatformId = "samsung" | "lg" | "android";

Expand All @@ -36,7 +36,7 @@ export async function bootstrapTvPlatform(
platformId: options.platformId,
defaultProfile: options.defaultProfile,
});
let runtimeConfig = await loader.load();
const runtimeConfig = await loader.load();
const deviceInfo = readDeviceInfo({
appId: options.appId,
customer: runtimeConfig.customer,
Expand All @@ -48,50 +48,26 @@ export async function bootstrapTvPlatform(
logBuffer,
config: runtimeConfig.diagnostics,
});
const registry = createDefaultWidgetRegistry();
// ONE navigation engine + renderer for the app's lifetime. Re-rendering reuses
// them; the keymap is hot-swapped via setKeymap so a config change can remap the
// remote with no reboot and no leaked listener.
const navigation = createNavigationEngine({
platform: runtimeConfig.platform.platform,
keymapOverride: runtimeConfig.keymapOverride,
});
const renderer = createLayoutRenderer({ registry, navigation });

// Renders the whole tree from a config snapshot. Called at boot and again on
// every hot config apply — the renderer clears #app, navigation.attach is
// idempotent, and setKeymap swaps the remote mapping live.
async function applyAndRender(config: RuntimeConfig): Promise<void> {
const services = createServiceGateway(config.services);
const activeLayout = await services.layout.getActiveLayout(config.layout);
navigation.setKeymap(config.keymapOverride);
await renderer.render(activeLayout, {
customer: config.customer,
locale: config.locale,
platform: config.platform.platform,
theme: config.theme,
features: config.features,
});
}
// Resolve the active layout (local or head-end/remote) and hand the config to
// the Blits app via the boot-config bridge.
const services = createServiceGateway(runtimeConfig.services);
runtimeConfig.layout = await services.layout.getActiveLayout(runtimeConfig.layout);
setBootConfig(runtimeConfig);

// Head-end can push {"type":"config.updated"} to re-pull config and hot-apply
// it with NO TV reboot. Falls back to a soft reload if re-apply throws.
// Head-end can push {"type":"config.updated"} to re-pull config. With the Blits
// canvas we soft-reload to re-launch (Blits-reactive in-place hot-apply — update
// app state instead of reload — is the next step).
function connectLiveConfig(): void {
const wsUrl = runtimeConfig.realtime.websocketUrl;
if (!runtimeConfig.features.websocketEvents || !wsUrl) {
return;
}
const bus = createWebsocketEventBus();
bus.connect(wsUrl);
bus.on("config.updated", async () => {
try {
runtimeConfig = await loader.load();
await applyAndRender(runtimeConfig);
console.info("xTV config hot-applied");
} catch (error) {
console.warn("Hot config apply failed; falling back to soft reload.", error);
globalThis.location?.reload();
}
bus.on("config.updated", () => {
console.info("xTV config.updated — reloading");
globalThis.location?.reload();
});
}

Expand All @@ -114,7 +90,8 @@ export async function bootstrapTvPlatform(
const runtime: TvPlatformRuntime = {
appId: options.appId,
async start() {
await applyAndRender(runtimeConfig);
// Launch the Blits (LightningJS canvas) app into #app.
Blits.Launch(App, "app", { w: 1920, h: 1080, debugLevel: 1 });
if (runtimeConfig.diagnostics.enabled) {
diagnostics.mount();
}
Expand Down
2 changes: 1 addition & 1 deletion libs/runtime-config/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ import androidTv12 from "../../../platforms/android/profiles/android-tv-12.json"
import webos3 from "../../../platforms/lg/profiles/webos3.json";
import webos5 from "../../../platforms/lg/profiles/webos5.json";
import webos6 from "../../../platforms/lg/profiles/webos6.json";
import tizen10 from "../../../platforms/samsung/profiles/tizen10.json";
import tizen6 from "../../../platforms/samsung/profiles/tizen6.json";
import tizen7 from "../../../platforms/samsung/profiles/tizen7.json";
import tizen9 from "../../../platforms/samsung/profiles/tizen9.json";
import tizen10 from "../../../platforms/samsung/profiles/tizen10.json";

export interface PlatformProfile {
id: string;
Expand Down
15 changes: 15 additions & 0 deletions libs/widgets/src/components/hero-banner.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import Blits from "@lightningjs/blits";

// Blits (LightningJS canvas) hero banner. Props are passed from the layout node's
// props by the parent Application. Replaces the old DOM preview component.
export default Blits.Component("HeroBanner", {
template: `
<Element w="1920" h="1080" color="$background">
<Element x="96" y="380">
<Text content="$title" size="72" color="#f5f8fb" font="regular" />
<Text content="$subtitle" y="120" size="30" color="#9db1c7" font="regular" />
</Element>
</Element>
`,
props: ["title", "subtitle", "background"],
});
77 changes: 0 additions & 77 deletions libs/widgets/src/components/hero-banner.lightning.ts

This file was deleted.

6 changes: 6 additions & 0 deletions libs/widgets/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { createHeroBannerElement } from "./components/hero-banner";

// Blits (canvas) components — the production render path used by the Blits app.
export { default as HeroBanner } from "./components/hero-banner.component";

// NOTE: the DOM WidgetDefinition/registry/layout path below is retained (still
// consumed by @x-tv/layout + @x-tv/widget-registry types) but is NOT the render
// path anymore. It will be reworked into a Blits-native dynamic layout engine.
export interface WidgetRenderInput {
id: string;
props: Record<string, unknown>;
Expand Down
Loading
Loading