The BrightScript Engine implements the BrightScript language specification up to Roku OS 16.0. However, some features and components remain unsupported or only partially implemented. These limitations fall into three categories: features planned for future development, components that will remain as mock objects for compatibility purposes, and functionality considered outside the project's scope. The following sections detail each category and its current status.
- Roku SceneGraph framework support is implemented as an extension published separately as the brs-scenegraph NPM package. All SceneGraph nodes documented by Roku are available, however because the engine is a simulator and not a hardware emulator, expect rendering differences from a real Roku device, please open a GitHub issue if you find any problem or missing feature. These are the current known limitations:
- The
Tasknode is implemented but its behavior is limited:- For now only 30 concurrent task threads are supported per application
- Only one
portinstance can be used on Taskinit()to observe fields - Rendezvous is implemented, but may not work properly for all edge cases.
Tasknodes never spawn under the synchronousexecuteFile()execution model of the Node.js library — useexecuteApp()for Task support, see the Node.js library guide.
- The focus change native animation on grids, lists and panel nodes is not implemented yet.
- Some abstract base nodes are not implemented on their own and fall back to a plain
Group(like the Standard Dialog framework'sStdDlgAreaBase,StdDlgItemGroupandStdDlgGraphicItem). - Component Libraries are supported, both declared in XML (
<ComponentLibrary id="..." uri="..."/>) and created at runtime viaCreateObject("roSGNode", "ComponentLibrary")with theuriassigned in code. The component namespace is the library manifest'ssg_component_libs_providedvalue (falling back to the nodeidwhen not declared), so its components are referenced asLibraryName:ComponentName. Theurimay point to a local volume (pkg:/,tmp:/,ext1:/) or a remotehttp(s)://package (the configured CORS proxy is honored). Current limitations:- Libraries are fetched and compiled synchronously (the BrightScript event loop never yields, so an asynchronous load could not complete mid-execution). The node reports
loadStatus = "loading"immediately and the"ready"/"failed"transition is emitted on the next render frame, soobserveField("loadStatus", ...)callbacks are notified as on a real device — provided the app is pumping its screen message loop (wait(...)). - Inside a library,
pkg:/still resolves to the host app's package (not the library's own); reference a library's bundled scripts with relativeuripaths in its component XML. - Library
source/global functions and intra-library component inheritance (a library component extending another library component) are not supported yet. - Encrypted/signed libraries are not supported (plain
.ziponly).
- Libraries are fetched and compiled synchronously (the BrightScript event loop never yields, so an asynchronous load could not complete mid-execution). The node reports
- The
- Node identity, cloning and render-thread-only APIs differ from a device in three measured cases (probes:
test/simulator/probes/node-container-copy-probe,clone-and-setref-probe,clone-basetype-probe):roUtils.IsSameObject()compares the engine's node instance, so it returnstruefor two values that refer to the same node. A device mints a fresh BrightScript handle on every node retrieval (anodefield read,getField,findNode,getChild,getParent, even a node returned byroUtils.DeepCopy), so it answersfalsein almost every real case. UseisSameNode()for node identity — it matches a device. Copy behavior itself is correct: reading anassocarray/arrayfield,roUtils.DeepCopyandmoveIntoFieldall carry the node over unchanged, andcloneshares node-valued fields; only the identity report differs.clone()never re-runs a component'sinit(), so the clone's script scope is empty and acallFuncon it readsinvalidfor anything the component cached inm— this matches a device. Note the device quirk the engine now reproduces: a custom component whose root built-in base isNodeclones to a bareNode(subtype, declared andaddFieldfields, and functions all dropped, leaving onlyNode's four fields with their current values); rooted at any other built-in —Group,Label,ContentNode— the clone keeps its subtype, fields and functions (a component extending another custom component follows its root).- The engine runs
source/code and the SceneGraph tree on one thread, so it has no main-vs-render ScriptEngine split. APIs a device restricts to the render thread are therefore accepted fromMain— notablysetRef(),getRef()andcanGetRef(), which a device fails (returningfalse, leaving the field unassigned) when called from the main thread. Their render-thread semantics match a device exactly.
- The complete Roku OS file system is available and shared among threads.
- Supports all volumes:
pkg:,common:,tmp:,cachefs:andext1:. - By default, the external volume (
ext1:) and the writeable volumes (tmp:cachefs:) are limited to 32 MB each. - The writable volumes
tmp:andcachefs:limits are configurable, see customization documentation; - The
ext1:limit is fixed. - As on a device,
tmp:is emptied on every app launch, whilecachefs:survives app relaunches for the session (browser page or Node.js process) — reloading the page or exiting the process acts as a device reboot and clears it. Unlike a device,cachefs:is a single volume shared by all apps, not segregated by developer ID. The CLI (brs-cli) runs one app per process, so it always starts with an emptycachefs:. When an app launches withcachefs:over 75% full, the oldest-modified files are evicted until it is back to half, as a device frees cache space. Eviction only happens at launch: while an app runs, a write that doesn't fit fails (and leaves no file behind), and space freed by deleting or rewriting files is only reclaimed on the next launch. A file being written when the app is terminated may be left incomplete.
- Supports all volumes:
- Audio playback is implemented via
roAudioResources,roAudioPlayercomponents andSoundEffect,Audionodes, but with some limitations:- Only one instance of
roAudioPlayercomponent orAudionode should be used, if more are created those will share the content playlist. - If the
roAudioPlayercomponent orAudionode instance is destroyed the audio keeps playing, make sure to stop the playback before discarding the object. - No
Timed Metadatasupport.
- Only one instance of
- Video playback is implemented via
roVideoPlayercomponent andVideonode, but with some limitations:- If the instance of
roVideoPlayerorVideois destroyed the video keeps playing, make sure tostopbefore discarding the object. - The following
roVideoPlayermethods are not supported, implemented as mock:setCGMS,setMaxVideoDecodeResolution,setTimedMetadataForKeys,getCaptionRenderer - Check what formats (container and codec) can be used on each browser, using
roDeviceInfo.canDecodeVideo(), to make sure your video can be played. - Under Node.js/CLI there is no audio or video output:
autoPlayEnabledis forced off and media events are only simulated, so apps behave as if playback never starts. - Subtitles are only supported for HLS and DASH streams and only in
Videonode, theroVideoPlayerdoes not support subtitles untilroCaptionRendereris implemented. - BIF (Base Index Frames) thumbnails are not yet supported.
- If the instance of
- The component
roUrlTransferis implemented with basic functionality but with the following limitations:- To make a web app access urls from domains other than the one it is hosted, the Cross-Origin Resource Sharing (CORS) browser policy requires the server called to respond with the header
Access-Control-Allow-Origin, read more.- A simple way to overcome this limitation is to use a CORS proxy like the cors-anywhere, see customization documentation to learn how to configure
brs-engineto use it. - If you are using a Chromium based browser (Chrome, Edge, Brave, etc) you can install this extension to bypass CORS.
- A simple way to overcome this limitation is to use a CORS proxy like the cors-anywhere, see customization documentation to learn how to configure
- The async methods (
AsyncGetToString(),AsyncGetToFile(),AsyncHead(),AsyncPostFromString(),AsyncPostFromFile(),AsyncPostFromFileToFile()) run on a background worker thread while the app keeps running, and theirroUrlEventarrives on the message port when the transfer completes, as on a device. They fall back to running synchronously (on the nextWait()/GetMessage()) in three cases:- The background worker is unavailable: on the browser package, the page is not cross-origin isolated (no
SharedArrayBuffer); on the Node.js library, the app runs with the synchronousexecuteFile()instead ofexecuteApp(). - The background worker stops responding for 15 seconds, for example while busy decoding a huge image. New transfers go back to it once it responds again.
- The transfer would fail before reaching the network: an
httpsURL with a missing certificates file, or a missing body file forAsyncPostFromFileToFile(). The synchronous path reports these errors.
- The background worker is unavailable: on the browser package, the page is not cross-origin isolated (no
- Custom/Self-Signed SSL certificates are not supported, the engine will use default browser client certificate database.
- As custom certificates are not supported these methods are just mocked and do nothing:
EnablePeerVerification,EnableHostVerification,SetCertificatesDepth. - Cookies are only partially supported, if
EnableCookiesis called andEnableFreshConnectionis set tofalse, then Cookies from previous calls will be preserved. - The other Cookies related methods are just mocked and do nothing:
GetCookies,AddCookies,ClearCookies. - The following methods are also only mocked but do nothing:
EnableResume,SetHttpVersionandSetMinimumTransferRate. - Server-Sent Events (SSE) (Roku OS 16.0) are supported via
AsyncGetSSEvents(), delivering real streamingroSSEvents on both packages: on Node.js/CLI the request runs in a small per-transfer helper process, and on the browser package thefetch()runs on the main thread, bridged to the worker via shared memory (so the CORS limitation above applies too). On the browser package, request headers that browsers forbid script to set (seeAddHeader()) are silently dropped.
- To make a web app access urls from domains other than the one it is hosted, the Cross-Origin Resource Sharing (CORS) browser policy requires the server called to respond with the header
- The component
roTextureManagerdecodes and downloads asynchronous texture requests on a background worker thread, so the app keeps running while images load, as on a device. Requests for the same URI share one load. Limitations:- Like asynchronous
roUrlTransfertransfers, requests load synchronously (when the message port is read) when the background worker is unavailable or not responding (see above). Requests made beforeSetMessagePort()also load synchronously. - Requests with
SetAsync(false)load synchronously, as documented. - Delivering a loaded image still copies its pixels on the BrightScript thread, about 2 ms for a 2048×1024 image.
- The SceneGraph
Posternode and other nodes that load images from aurifield still download and decode them synchronously, stalling the frame that loads them.
- Like asynchronous
- The
roStreamSocket(TCP) component performs real network I/O, on the Node.js/CLI package only.- Supports listening, connecting, accepting connections, and sending/receiving stream data, including
roSocketEventdelivery viaNotifyReadable()/Wait(), backed by a small helper process per listener/connection. Connect()uses a blocking request with an 8-second timeout rather than modeling Roku's async-per-message-port connect semantics.SetLinger/SetMaxSeg(ifSocketConnectionOption) are accepted and stored but not enforced — Node'snetmodule exposes no publicSO_LINGER/TCP_MAXSEGhook.- On the browser package
roStreamSocketremains mocked, as browsers cannot open raw sockets.
- Supports listening, connecting, accepting connections, and sending/receiving stream data, including
- The
roDataGramSocket(UDP) component performs real network I/O, on the Node.js/CLI package only.- Supports binding, broadcast, send and receive, including
roSocketEventdelivery viaNotifyReadable()/Wait(), backed by a small per-socket helper process. - Real multicast group membership (
JoinGroup/DropGroup) is not implemented on either platform. - On the browser package
roDataGramSocketremains mocked, as browsers cannot open raw sockets.
- Supports binding, broadcast, send and receive, including
- The new
roWebSocketcomponent (Roku OS 16.0) performs real WebSocket network I/O on both packages, unlike the raw-socket components above:- On the Node.js/CLI package, a per-socket helper process (backed by the
wspackage) gives full protocol fidelity, including real Ping/Pong control frames (SendPing/SendPong/PingTest/SetAutoPingReply), custom handshake headers, HTTP Basic-Auth credentials (SetUserAndPassword), and TLS peer/host verification toggles. - On the browser package, the real
WebSocketconnection is opened on the main thread (the interpreter's own thread cannot service its callbacks) and bridged to the worker via shared memory. Browsers give script no way to set handshake headers or credentials and no way to observe or originate Ping/Pong control frames, so on that package:SetUserAndPassword, customifHttpAgentheaders, andEnablePeerVerification/EnableHostVerificationare accepted but have no effect;SendPing/SendPong/PingTestare no-ops (the browser still auto-replies to an incoming Ping, invisibly to script, regardless ofSetAutoPingReply). GetBuffered()always returns0andSetFragmentSize()/GetMsgSendBufferSize()/GetMsgRecvBufferSize()are stored but not enforced — neither platform's transport models outgoing buffering or manual frame fragmentation.
- On the Node.js/CLI package, a per-socket helper process (backed by the
- The new
roAnimatedImage(BrightScript component) andAnimatedImage(SceneGraph node), added by Roku OS 15.3, now follow Roku's published specification (field names, defaults, and thecontrol/statevocabulary).- Animated WebP frames are decoded and composited (including
dispose/blendsemantics) directly by the engine. - Lottie/Bodymovin JSON is rendered via the third-party
lottie.jslibrary (pre-1.0, "covers a growing subset of the format" per its own documentation) — not every Lottie feature (precomps, mattes, masks, expressions) is confirmed supported. video/mp4is a documentedmimeType, but MP4-encoded animated content is not yet decoded — content that sniffs or declares as MP4 fails to load.loadingBitmapUri/failedBitmapUri(placeholder image swap while loading/on failure) are accepted fields but not yet functional.
- Animated WebP frames are decoded and composited (including
- The
roInputdeep link events are supported, but the events related to Voice Commands are not available yet. - The component
roAudioMetadataonly supports MP3 (for now). - The component
roImageMetadataonly supports JPEG images (for now). - The component
roAppMemoryMonitorwill only return measured data in Node.JS and Chromium browsers. For browsers the memory heap info only accounts for the main thread, as WebWorkers do not have support forperformance.memoryAPI. TheroAppMemoryMonitorEventis not yet implemented. roRegexpatterns are compiled by the JavaScriptRegExpengine, not PCRE. Common PCRE-only syntax is translated: inline options ((?i),(?-i),(?i:...),(?s),(?m),(?x)), possessive quantifiers, atomic groups,(?#...)comments,(?P<n>...)/(?P=n),\A/\z/\Z, POSIX classes,\p{..},\Q...\Eand\x{...}. Other PCRE-only syntax (recursion, conditionals,\K,\G, backtracking verbs) makesCreateObject("roRegex", ...)printroRegex: Invalid patternand returninvalid, where a device would accept the pattern. Case-insensitive matching of back references inside an(?i)scope is not emulated.- The global functions
Eval(),GetLastRunCompileError()andGetLastRunRuntimeError()are not available. - The string
modcannot be used as variable or function parameter name, because it conflicts with remainder operatorMod(Roku devices allows that). - Screensaver functionality is not yet implemented.
- SDK 1.0 deprecated components are not supported, but may be implemented in the future as a new extension, allowing legacy apps to be executed.
- RAF (Roku Ads Framework) library that exposes
Roku_Adsobject is mocked with the most common methods available returning static values. - RED (Roku Event Dispatcher) and Google IMA3 libraries are also mocked.
- Channel Store components (
ChannelStore,roChannelStoreandroChannelStoreEvent) are mocked with support for thefakeServer()feature: every documented command is implemented, with catalog, purchase, order and account data coming from thecsfake/*.xmltest files (or a canned account) only whilefakeServeris enabled. Two exceptions work regardless offakeServer:- The channel credential store (
storeChannelCredData/getChannelCred) holds the app's own artifact in memory, though the data does not persist between runs or across devices. - The
GetRokuCustomerIdcommand (Roku OS 16.0), sent through theChannelStorenode's generic request framework (request/requestStatusfields), returns thecustomerIdconfigured in the device information (see customization). The generic request framework only supports this command, and only on the node: Roku documents noroChannelStoremethod for it.
- The channel credential store (
- The Text to Speech components (
roAudioGuide,roTextToSpeechandroMicrophone) and the Signing Algorithm components (roDSAandroRSA) are mocked: they expose their documented methods returning static values, but perform no real speech, recording or cryptographic signing. - Several components have their methods and events mocked, they return constant values to prevent crash. Those are mostly related to device behaviors that are not possible to replicate in a browser environment or simply not applicable to the engine.
- Roku OS User Interface.
- Roku Channel Store features.
- Video playback with Ads.