npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@3dsource/angular-unreal-module

v0.0.194

Published

Angular integration for Unreal Engine Pixel Streaming

Readme

@3dsource/angular-unreal-module

Standalone Angular integration for Unreal Engine Pixel Streaming. The package provides the scene component, WebRTC and signalling lifecycle, NgRx state, command/callback APIs, reconnection, file transfer and telemetry.

Requirements

  • Angular, Angular CDK and Angular Forms >=19.0.0 <23.0.0
  • NgRx Store and Effects >=19.0.0 <23.0.0
  • RxJS >=7.8.0 <8.0.0
  • @3dsource/types-unreal >=0.0.14 <0.1.0
  • provideHttpClient() in the host application

Installation

pnpm add @3dsource/angular-unreal-module @3dsource/types-unreal

The package is standalone and does not expose an NgModule.

Entry points

| Entry point | Contains | | ---------------------------------------- | ------------------------------------------------------------------------------------ | | @3dsource/angular-unreal-module | Everything — scene component, services, NgRx state, providers, helpers | | @3dsource/angular-unreal-module/config | UNREAL_CONFIG, UnrealInitialConfig, UnrealMode only — no engine, 266 B of FESM |

Import the token from /config in the application root and keep every other import behind a lazy route — see Setup for why.

Styling

The package ships its own styles and requires no UI library. Everything it draws on top of the video is customisable through three levels — take the cheapest one that solves your case.

| Need | Level | Cost | | ---------------------------------- | ------------- | -------------------------- | | different colour / radius / font | design tokens | 3 lines of CSS | | your own look for a whole block | class hooks | one class in global styles | | a different set of elements inside | slots | one template |

The three levels compose: tokens recolour what stays, a class hook restyles one block, a slot replaces what is inside it. A slot makes the class hook for the same part irrelevant — the markup inside is then entirely yours.

What you can customise

These are the blocks the scene draws over the video. Nothing else is themed — the video itself, and the dev-only overlays, are out of scope.

| Block | When the user sees it | Token-only | Class hook parts | Slot | | -------------- | ---------------------------------- | ---------- | ------------------------------------------ | ------------------ | | Loading status | while connecting, shows percentage | ✓ | statusCard, statusMessage | unrealStatusSlot | | Resume / Start | stream paused or not yet started | ✓ | resumeCard, resumeText, resumeButton | unrealResumeSlot | | AFK timeout | inactivity countdown before drop | ✓ | afkScrim, afkCard, afkButton | unrealAfkSlot |

Error dialogs (unreal-error-modal, webrtc-error-modal) are not part of this contract: they have no class hook, no slot, and they read the @3dsource/source-ui-native tokens (--src-*) rather than --unreal-*. They also render outside the scene DOM — see the note under Design tokens.

1. Design tokens

Set the --unreal-* variables on app-unreal-scene — custom properties inherit through the DOM and reach every block inside.

app-unreal-scene {
  --unreal-surface-card: #101014;
  --unreal-text-main: #f5f5f7;
  --unreal-radius-card: 16px;
}

| Token | Default | | ------------------------- | -------------------------------------------------------- | | --unreal-surface-card | #fff | | --unreal-surface-screen | #fff | | --unreal-surface-scrim | rgba(100, 100, 100, .7) | | --unreal-text-main | #1f2937 | | --unreal-text-secondary | #6b7280 | | --unreal-font-family | system-ui, sans-serif | | --unreal-radius-card | 8px | | --unreal-radius-pill | 9999px | | --unreal-shadow-card | 0 26px 80px 0 rgba(0,0,0,.2), 0 0 1px 0 rgba(0,0,0,.2) | | --unreal-accent | #017bff | | --unreal-accent-hover | #016fe6 | | --unreal-accent-text | #fff |

If @3dsource/source-ui-native is loaded, its tokens are used automatically wherever you do not set an --unreal-* value.

Anything opened through CDK Dialog is out of reach. The error dialogs render in a separate overlay container, outside the scene DOM, so variables set on app-unreal-scene never reach them — declare those on :root. Note that those dialogs read --src-*, not --unreal-*.

2. Class hooks

Supply your own class for a part; it replaces the built-in decoration class. The positioning class stays, so the scene layout cannot break.

<app-unreal-scene [uiClasses]="{ resumeCard: 'my-card', statusCard: 'my-pill' }" />

Parts: resumeCard, resumeText, resumeButton, afkScrim, afkCard, afkButton, statusCard, statusMessage.

Your classes must live in global styles (styles.scss). Declared in a component's own SCSS, Angular scopes them and they never reach the scene.

The two button parts behave differently. resumeButton and afkButton put your class on the <app-unreal-button> host, but the <button> inside is a library component whose styles are scoped — and a scoped .unreal-button selector (0,2,0) outweighs a global .my-cta button (0,1,1). So the hook is good for the button's outer box (width, margin, alignment); to change the button itself, use the --unreal-accent* tokens, or a slot if you want different markup altogether.

afkScrim and --unreal-surface-scrim have one caveat. The package also ships a legacy partial, src/lib/styles/unreal.scss, which is not part of the published bundle and is not meant to be imported. It contains .frame #videoPlayOverlay { background-color: … } — an id selector that outweighs any class. If you import that partial anyway, it wins over both the class hook and the token for the AFK overlay background.

3. Slots

Replace the contents of a block with your own markup. The module keeps the wrapper and the visibility logic; you get the data through the template context.

<app-unreal-scene>
  <ng-template unrealResumeSlot let-ctx>
    <button (click)="ctx.start()" class="my-cta">{{ ctx.isSecondStart() ? 'Resume' : 'Start' }}</button>
  </ng-template>

  <ng-template unrealStatusSlot let-ctx>
    <my-progress [value]="ctx.percents()" />
  </ng-template>
</app-unreal-scene>

| Directive | Context | | ------------------ | --------------------------------------------- | | unrealResumeSlot | isSecondStart, isAfkDisconnect, start() | | unrealAfkSlot | countdown (seconds left), reset() | | unrealStatusSlot | percents, message |

All context values except methods are Signals — call them in the template. percents is undefined until the first value arrives — guard it if you render it raw.

The context object is created once and never replaced; what changes are the signals inside it. So let-ctx is stable and you can pass ctx around freely.

Markup inside a slot belongs to you, not to the library. Angular scopes a template to the component that declares it, and your slot template is declared in your component. Two consequences:

  • The library's own classes do nothing there. Writing class="resume-box__text" inside a slot will not pick up the module's styling — that class is scoped to the module.
  • Style slot content the way you style any of your own markup: your component's SCSS applies normally (no ::ng-deep, no global stylesheet needed — unlike class hooks).

The --unreal-* tokens do reach inside, because CSS custom properties inherit through the DOM. Read them if you want your markup to follow the same theme:

.my-cta {
  background: var(--unreal-accent, #017bff);
  border-radius: var(--unreal-radius-card, 8px);
}

Recipes

Dark theme, nothing else. One rule, no TypeScript:

app-unreal-scene {
  --unreal-surface-card: #101014;
  --unreal-surface-screen: #0b0b0f;
  --unreal-text-main: #f5f5f7;
  --unreal-text-secondary: #a1a1aa;
  --unreal-shadow-card: 0 20px 60px 0 rgb(0 0 0 / 60%);
}

Your brand's accent on the buttons. Buttons are recoloured with tokens, not with a class hook — see the note above:

app-unreal-scene {
  --unreal-accent: #ff5f6d;
  --unreal-accent-hover: #f0454f;
  --unreal-accent-text: #fff;
}

If you need a button that is shaped differently, not just recoloured, replace the whole block through unrealResumeSlot and render your own control.

A completely different loading status. Structure, not just skin — so this is a slot. Visibility is still decided by the module:

<app-unreal-scene>
  <ng-template unrealStatusSlot let-ctx>
    <my-brand-progress [value]="ctx.percents() ?? 0" />
  </ng-template>
</app-unreal-scene>

What you cannot change

  • When a block appears. Visibility stays with the module — the conditions are non-trivial (connection state, reconnect flags, AFK timers) and duplicating them in a host app is a reliable source of bugs.
  • Where a block sits in the scene. Each customisable element keeps a positioning class that a host class never replaces, so the scene layout cannot be broken from outside. Move the block by restyling its container instead.
  • The video element and dev overlays (video-stats, stat-graph).

Setup

1. Register state and configuration once

Add the Unreal feature state, HTTP client and configuration at the application root. UNREAL_CONFIG is required, although all its fields are optional.

import { provideHttpClient } from '@angular/common/http';
import type { ApplicationConfig } from '@angular/core';
import { provideStore } from '@ngrx/store';
import { provideUnrealState } from '@3dsource/angular-unreal-module';
import {
  UNREAL_CONFIG,
  type UnrealInitialConfig,
} from '@3dsource/angular-unreal-module/config';

const unrealConfig = {
  regionsPingUrl: 'https://datacenter.3dsource.com/regions/',
  dataChannelConnectionTimeout: 8000,
  fpsMonitor: false,
  autoHighResolution: false,
} satisfies UnrealInitialConfig;

export const appConfig: ApplicationConfig = {
  providers: [
    provideHttpClient(),
    provideStore(),
    provideUnrealState(),
    { provide: UNREAL_CONFIG, useValue: unrealConfig },
  ],
};

Omit provideStore() when the root NgRx store is already configured.

UNREAL_CONFIG comes from the /config entry point on purpose. The token has to be provided in the root injector, because the module's services are providedIn: 'root' singletons and cannot see a route-level provider. The package itself ships as one FESM, and esbuild places a module in the common-ancestor chunk of its importers — so importing the token from the package root here pulls the whole engine into the application's eager bundle. On a metabox production build that was 180 kB raw (49 kB gzip) of main spent on pages that never stream.

This pays off only while nothing else in the eager graph imports the package root. Following the example above literally does not qualify — the provideUnrealState() import next to it brings the FESM back. An app that wants the engine out of its initial bundle moves provideUnrealState() to the lazy route alongside provideUnrealModule() (step 2) and leaves only the UNREAL_CONFIG provider at the root.

Available configuration fields:

| Field | Purpose | | ------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- | | regionsPingUrl | Region latency endpoint | | dataChannelConnectionTimeout | DataChannel connection timeout in ms | | customErrorsEndpoint | Custom error reporting endpoint | | streamTelemetryV2Url | Stream lifecycle telemetry endpoint | | screenLockerContainerId | Container used by the screen-locker overlay | | mode | 'metabox' (default) — full Metabox command protocol; 'default' — stock Pixel Streaming app, the module sends no Metabox commands of its own | | fpsMonitor | Enables FPS monitoring (currently off — the service is not instantiated) | | autoHighResolution | Raises resolution after the scene becomes idle | | playwright | Enables the test-specific service behaviour |

Use { provide: UNREAL_CONFIG, useValue: {} } for the minimal configuration.

2. Boot the engine on a lazy route

Lazy-load the route file from the application router:

import type { Routes } from '@angular/router';

export const APP_ROUTES: Routes = [
  {
    path: 'stream',
    loadChildren: () =>
      import('./stream/stream.routes').then((module) => module.STREAM_ROUTES),
  },
];

Register provideUnrealModule() inside that lazy route file:

import type { Routes } from '@angular/router';
import { provideUnrealModule } from '@3dsource/angular-unreal-module';
import { StreamComponent } from './stream.component';

export const STREAM_ROUTES: Routes = [
  {
    path: '',
    component: StreamComponent,
    providers: [provideUnrealModule()],
  },
];

Keep provideUnrealState() and UNREAL_CONFIG at the application root. The loadChildren() boundary keeps the streaming engine out of the initial bundle. Route-scoping provideUnrealModule() tears down its effects when the route is left.

UNREAL_CONFIG on a route's providers does not work: the engine services are providedIn: 'root', so they are created by the root injector and read the token from there. A route-level value is invisible to them and inject(UNREAL_CONFIG, { optional: true }) resolves to null — e.g. the post-connection region re-ping never runs (empty regionsPingUrl), so without the prefetch script no region is ever cached and every request goes out without one. Importing only the token at the root does not pull the module into the initial bundle (the package is sideEffects: false).

3. Render the scene

import { ChangeDetectionStrategy, Component } from '@angular/core';
import { UnrealSceneComponent } from '@3dsource/angular-unreal-module';

@Component({
  selector: 'app-stream',
  imports: [UnrealSceneComponent],
  template: `<app-unreal-scene />`,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class StreamComponent {}

UnrealSceneComponent also accepts isStudio, useContainerAsSizeProvider and resolutionSize inputs, and emits changeMouseOverScene.

Main API

  • provideUnrealState() — registers the unrealFeature NgRx state.
  • provideUnrealModule() — registers effects and boots streaming services.
  • UnrealSceneComponent — renders and manages the Pixel Streaming scene.
  • UnrealCommunicatorService — sends commands and UI interactions.
  • UnrealCallbackService — observes Unreal callbacks and command responses.
  • unrealFeature, exported selectors and actions — expose connection and scene lifecycle state.

Command packet types are provided by @3dsource/types-unreal.

That package ships two type contours, prod and QA, and your application and this package must resolve the same one — the selection is a tsconfig path override on the bare specifier, never an import. A bundler that ignores tsconfig paths (plain Vite or webpack without a matching resolve.alias) takes the types from one contour and the enum values from the other, which fails at runtime rather than at compile time. See "Two contours: prod and QA" in the @3dsource/types-unreal README.

Run pnpm demo:start from the repository root to see the scene component in the demo application.

Optional prefetch scripts

The package publishes two dependency-free scripts for use in the document <head> before Angular starts:

  • region-ping-prefetch.js measures regions early, caches a full result and publishes its live answer. The connection never waits for it: the request takes the cached region, else the prefetch's answer if it is already certain, else no region at all (the orchestrator then picks by geo-IP). Load it first in <head>; without it the cache is filled after the first connection.
  • stream-prefetch.js opens and parks an eligible WebRTC connection so Angular can adopt it after bootstrap.
<script
  src="https://cdn.jsdelivr.net/npm/@3dsource/angular-unreal-module/js/region-ping-prefetch.js"
  async
></script>
<script
  src="https://cdn.jsdelivr.net/npm/@3dsource/angular-unreal-module/js/stream-prefetch.js"
  async
></script>

stream-prefetch.js runs only on metabox-configurator/modular/{configuratorId} routes — the configurator id comes from the route and there is no attribute to supply one, so the script cannot be enabled for an arbitrary page.

It reads the same-origin assets/config.json and assets/features.json, then the configurator's cached record from {cdn}/cache/configurator/{id}.json — the same document the host app reads — so it polls under the identity the app will use rather than inventing one. The newOrchestration feature flag picks the transport: signalling (HTTP polling) or newSignalling (control socket + requestStream). The orchestration-issued streamRequestId is forwarded to Cirrus on the WebSocket URL (the session connectionId) and parked for adoption.

window.__stream is both the parked stream and the lock: whichever of the script and the Angular module writes it first owns the connection, so a page never builds two. Load the script and the package from the same version — an older script does not know about the lock.

Optional data-*: data-config-url, data-features-url, data-ws-timeout, data-poll-timeout.

Pin an exact package version in production when deterministic CDN assets are required.

Repository development

Run commands from the repository root:

pnpm unreal-module:build
pnpm unreal-module:build:watch
pnpm unreal-module:lint
pnpm unreal-module:test
pnpm unreal-module:test:watch
pnpm unreal-module:test:signalling
pnpm unreal-module:test:signalling:leaks

unreal-module:build builds the local types-unreal dependency before this package.

Release commands publish only this package:

pnpm unreal-module:release:patch
pnpm unreal-module:release:dev

After a successful publish, the release automatically purges the matching jsDelivr tag. Retry a failed purge without rerunning the release:

pnpm unreal-module:purge-cdn -- latest
pnpm unreal-module:purge-cdn -- dev

Repository tooling requires Node.js 24.16.0 or newer and pnpm last version.