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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,7 @@ lg/assets
lg/fonts
lg/*.ipk
lg/index.html
# The tvOS project under nativescript/ is its own pnpm root: its workspace file
# and lockfile are tracked, unlike the ones the rules above ignore.
!nativescript/pnpm-workspace.yaml
!nativescript/pnpm-lock.yaml
43 changes: 43 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,46 @@ pnpm start
```

If you're interested in using SolidTV with Web check out [Web Branch](https://github.com/solid-tv/solid-demo-app/tree/web) to see the setup.

## Apple TV

The app runs on tvOS without a browser through
[@solidtv/nativescript](https://github.com/solid-tv/nativescript): NativeScript
is the runtime, its canvas plugin the WebGL surface. The project is
`nativescript/`, its own pnpm root, and holds only the host: the boot file in
`app/app.ts`, the webpack config that bundles `../src` as it is, the
`App_Resources`, and a stand-in for a plugin package with no tvOS build.
Nothing in `src/` is written for it.

Prerequisites: macOS with Xcode 26.4 and the tvOS platform, the `xcodeproj`
gem (`gem install --user-install xcodeproj`), pnpm 10, and the TMDB token in
`src/api/key.ts` as for the web. A shell without a UTF-8 locale, a script's
say, needs `LC_ALL=en_US.UTF-8` on every `ns` command.

```
cd nativescript
pnpm install
LC_ALL=en_US.UTF-8 pnpm build:tvos # the Apple TV simulator build
LC_ALL=en_US.UTF-8 pnpm tvos # build, install and run on a booted simulator
```

For a paired Apple TV, put your team id in both `App_Resources/*/build.xcconfig`
files and run `ns run tvos --device <identifier> --no-hmr --no-watch`; the
first build of the bundle id needs Xcode to create its provisioning profile.
The device transport does not stream the console: the app mirrors every line
to `Library/Caches/demo-log.txt` in its container, which
`xcrun devicectl device copy from --device <identifier> --domain-type appDataContainer --domain-identifier dev.solidtv.demo --source Library/Caches/demo-log.txt --destination <file>`
fetches. The scripts under `nativescript/scripts/` run a simulator build,
press the Siri Remote's buttons through the XCUITest driver of the
`@solidtv/nativescript` harness, and run a background and foreground cycle.

The Siri Remote's arrows, Select, Menu and play/pause reach the app as the
keys the web build maps; Menu is Back. From a page it opens the nav, from
the nav it walks history back, and from the nav on the start page it is left
to the system, which returns to the Home screen: the one rule `src/` gained
for tvOS, in `LeftNavWrapper`, and one a browser never notices.

Out of scope there: the player (video is a native view, not a DOM element),
Firebolt, the renderer versions page (a link to GitHub Pages), texture
compression (the KTX assets are ETC2, the Apple TV HD's GPU takes PVRTC),
the mouse states, and the Destroy example's SVG. Their pages still bundle.
3 changes: 3 additions & 0 deletions nativescript/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
hooks/
node_modules/
platforms/
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"images": [
{
"idiom": "universal",
"platform": "ios",
"size": "1024x1024"
}
],
"info": {
"version": 1,
"author": "xcode"
}
}
6 changes: 6 additions & 0 deletions nativescript/App_Resources/iOS/Assets.xcassets/Contents.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"info": {
"version": 1,
"author": "xcode"
}
}
44 changes: 44 additions & 0 deletions nativescript/App_Resources/iOS/Info.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleDisplayName</key>
<string>${PRODUCT_NAME}</string>
<key>CFBundleExecutable</key>
<string>${EXECUTABLE_NAME}</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>${PRODUCT_NAME}</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0.0</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIRequiresFullScreen</key>
<true/>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>arm64</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
</dict>
</plist>
25 changes: 25 additions & 0 deletions nativescript/App_Resources/iOS/LaunchScreen.storyboard
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="21701" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<device id="retina6_12" orientation="portrait" appearance="light"/>
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21678"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
<color key="backgroundColor" red="0.0627" green="0.0784" blue="0.0941" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
</document>
3 changes: 3 additions & 0 deletions nativescript/App_Resources/iOS/build.xcconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
DEVELOPMENT_TEAM = EEG23Y94ED;
30 changes: 30 additions & 0 deletions nativescript/App_Resources/tvOS/Info.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleDisplayName</key>
<string>${PRODUCT_NAME}</string>
<key>CFBundleExecutable</key>
<string>${EXECUTABLE_NAME}</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>${PRODUCT_NAME}</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0.0</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>arm64</string>
</array>
</dict>
</plist>
2 changes: 2 additions & 0 deletions nativescript/App_Resources/tvOS/build.xcconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
TVOS_DEPLOYMENT_TARGET = 16.0;
DEVELOPMENT_TEAM = EEG23Y94ED;
142 changes: 142 additions & 0 deletions nativescript/app/app.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
// The polyfill first: the renderer and the router read browser globals at
// import. Then the shims, which give those globals the shape the demo's own
// entry expects: a file: location into the app folder, a working history for
// its HashRouter, and a document its focus manager can listen on.
import "@nativescript/canvas-polyfill";
import "@solidtv/nativescript/shims";
import { Application, Color, File, GridLayout, knownFolders, path } from "@nativescript/core";
import { isTvOS } from "@nativescript/core/platform";
import { Canvas } from "@nativescript/canvas";
import type { FpsUpdatePayload, RendererMain } from "@solidtv/renderer";
import { Config, renderer } from "@solidtv/solid";
import {
KeyBridge,
bindCanvas,
bindLifecycle,
bindRemote,
mirrorConsole,
type PausableRenderer
} from "@solidtv/nativescript";

// The clock every measurement below counts from: the first line of the
// bundle, the same origin the web build's entry has in the browser.
const T0 = performance.now();

// On a device the CLI cannot stream the console: every line also goes to
// Library/Caches/demo-log.txt in the app's container, for
// `xcrun devicectl device copy from` (see the README).
mirrorConsole(File.fromPath(path.join(knownFolders.temp().path, "demo-log.txt")), { application: Application });
console.log("BOOT start");

const sinceBoot = (): number => Math.round(performance.now() - T0);

// The numbers the checkpoint asks for, as console lines: every renderer
// idle with the texture memory at that point (the first is the first frame,
// the ones after the posters have a texture count to show for it), the
// frame rate at the demo's own interval, and the lifecycle with whether the
// loop had stopped half a second after a suspend.
function attachMeasurements(main: RendererMain): void {
let frames = 0;
main.on("frameTick", () => {
frames++;
if (frames === 1) {
console.log("RENDER first frame t=" + sinceBoot());
}
});
let idles = 0;
main.on("idle", () => {
idles++;
if (idles > 12 && idles % 10 !== 0) {
return;
}
console.log(
"RENDER idle " +
JSON.stringify(Object.assign({ n: idles, t: sinceBoot() }, main.stage.txMemManager.getMemoryInfo()))
);
});
main.on("fpsUpdate", (_target: RendererMain, d: FpsUpdatePayload) => {
console.log(
"FPS " +
JSON.stringify({
t: sinceBoot(),
fps: d.fps,
rendered: d.renderedFrames,
idle: d.idleTicks
})
);
});
Application.on("suspend", () => {
console.log("LIFECYCLE suspend t=" + sinceBoot());
setTimeout(() => {
console.log("LIFECYCLE paused=" + String(main.isPaused));
}, 500);
});
Application.on("resume", () => {
console.log("LIFECYCLE resume t=" + sinceBoot());
});
}

// The demo creates its renderer on its own schedule, a frame after its
// entry runs; SolidTV's `renderer` binding is where it appears.
function whenRendererExists(callback: (main: RendererMain) => void): void {
const poll = setInterval(() => {
const main = renderer as unknown as RendererMain | undefined;
if (main !== undefined) {
clearInterval(poll);
console.log("RENDER created t=" + sinceBoot());
callback(main);
}
}, 20);
}

// A press a handler consumed stays with the app; a Menu press nothing
// handled goes to the system and leaves the app, as tvOS expects.
Config.preventDefaultOnHandledKeys = true;

function boot(canvas: Canvas): void {
// The demo's createRenderer() names no canvas and no platform: this view
// and TvPlatform stand in, with the settings the host corrects.
bindCanvas(canvas);
// The Siri Remote, into the bridge, onto document, where the demo's focus
// manager and its own key listener are.
const bridge = new KeyBridge({ target: document });
bindLifecycle(() => renderer as unknown as PausableRenderer | undefined, bridge, Application);
if (isTvOS) {
bindRemote(bridge, Application.ios.window);
}
whenRendererExists(attachMeasurements);
// The demo's own entry, unchanged. It reads the URL, sets Config, creates
// the renderer on the next frame and renders its routes.
console.log("ENTRY import t=" + sinceBoot());
import("../../src/index")
.then(() => {
console.log("ENTRY loaded t=" + sinceBoot());
})
.catch((error: unknown) => {
console.error("The demo's entry failed to load", error);
});
}

function createRootView(): GridLayout {
const root = new GridLayout();
root.backgroundColor = new Color("#000000");
const canvas = new Canvas();
// Percent sizes size both the layout and the backing surface, and the
// overflow keeps the canvas out from under tvOS's title-safe insets.
canvas.width = "100%";
canvas.height = "100%";
canvas.iosOverflowSafeArea = true;
canvas.on("ready", () => {
// The plugin's ready callback swallows exceptions: a throw here would be
// a silent blank screen.
try {
boot(canvas);
} catch (error: unknown) {
console.error("Boot failed", error);
}
});
root.addChild(canvas);
return root;
}

Application.run({ create: createRootView });
12 changes: 12 additions & 0 deletions nativescript/nativescript.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { NativeScriptConfig } from "@nativescript/core";

export default {
id: "dev.solidtv.demo",
projectName: "SolidTVDemo",
appPath: "app",
appResourcesPath: "App_Resources",
bundler: "webpack",
cli: {
packageManager: "pnpm"
}
} as NativeScriptConfig;
33 changes: 33 additions & 0 deletions nativescript/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"name": "solid-demo-app-tvos",
"version": "0.0.0",
"private": true,
"description": "The SolidTV demo app on Apple TV: the NativeScript project around the app in ../src, through @solidtv/nativescript",
"main": "app/app.ts",
"scripts": {
"tvos": "ns run tvos --emulator --no-hmr",
"build:tvos": "ns build tvos",
"clean": "ns clean"
},
"dependencies": {
"@nativescript/canvas": "3.0.0-alpha.10",
"@nativescript/canvas-polyfill": "3.0.0-alpha.10",
"@nativescript/core": "9.2.0-tvos.0",
"@solid-primitives/list": "^0.1.2",
"@solid-primitives/scheduled": "^1.5.2",
"@solidjs/router": "^0.16.1",
"@solidtv/nativescript": "link:../../nativescript",
"@solidtv/renderer": "link:../../renderer",
"@solidtv/solid": "link:../../solid",
"solid-js": "^1.9.9"
},
"devDependencies": {
"@nativescript/ios": "9.1.0",
"@nativescript/tvos": "9.1.0",
"@nativescript/types": "9.1.1",
"@nativescript/webpack": "5.0.39-tvos.0",
"nativescript": "9.2.0-tvos.0",
"typescript": "~6.0.0"
},
"packageManager": "pnpm@10.33.4"
}
Loading