The complete reference for the Eagle 3D Streaming Web SDK: how a session is authorised and started, every call you can make on a running stream, every callback it will make back to you, and the mistakes that are expensive to make late.
A Streaming API key from your Control Panel, an
application uploaded and playable there, and the
connector domain your account was issued (something like
connector.eagle3dstreaming.com). All three appear in the
dashboard. If the app does not play from the Control Panel it will not play
from the SDK either — fix that first, because it removes half the
variables.
Both approaches put a running Unreal application on your web page. The difference is who owns the page.
| iframe | SDK | |
|---|---|---|
| What you add | One <iframe> tag |
A script, a configuration file, and code you write |
| The stream lives | In a separate document, isolated from your page | In your page, as an element you position and style |
| You talk to it by | postMessage across the frame boundary |
Direct function calls on e3ds_controller |
| Best for | Dropping a stream into an existing site quickly | Applications where the stream is part of the interface |
Choose the SDK when the stream and your interface have to behave as one thing — when your own controls drive the application, when you need the stream's lifecycle events to change what your page shows, or when the stream must sit inside a layout that resizes with it. Choose the iframe when you want a stream on a page and nothing more.
Understanding this sequence explains almost every error message you will encounter, so it is worth reading once even if you only intend to copy the sample.
Your API key buys a token. A request goes to the token endpoint carrying your key and a description of which application to launch. It comes back with a short-lived session token and the address of a signalling server.
The token starts a session. You hand it to
e3ds_controller.main(). The player connects to the signalling
server, which finds a machine, launches your application on it, and
negotiates a WebRTC connection back to the browser.
Video arrives, then the data channel opens. These are two separate events and they do not arrive together. Video can be playing before you are able to send anything to the application.
The session ends — because you called
terminate(), because the session's time limit expired, or
because the connection was lost. Each of these tells you through a
different callback.
The sample is a plain static site — no build step, no package manager. Every file is one you can open and read.
| File | What it is for | Do you edit it? |
|---|---|---|
scripts/dist/e3dsCore.min_ns.js |
The SDK itself. Defines e3ds_controller. |
Never |
scripts/sdk-config.js |
Your API key and which application to launch. | Yes — this is the only file you must edit |
scripts/sdk-callbacks.js |
What your page does as the session progresses. | Yes, once you are past the sample |
scripts/demo-ui-controls.js, scripts/demo-ui-connection-setup.js |
The sample's control panel. An example, not part of the SDK. | Replace it with your own interface |
scripts/sdk-token.js |
Requests the token and starts the stream. | Yes, when you move token requests to your server |
<script defer src="./scripts/dist/e3dsCore.min_ns.js"></script>
<script defer src="./scripts/sdk-config.js"></script>
<script defer src="./scripts/sdk-callbacks.js"></script>
<script defer src="./scripts/demo-ui-connection-setup.js"></script>
<script defer src="./scripts/demo-ui-controls.js"></script>
<script defer src="./scripts/sdk-token.js"></script>
The SDK is first because everything after it uses e3ds_controller.
Configuration is next because the rest reads it. The file that
starts the stream is last, so that everything it depends on —
your callbacks in particular — is already in place when the session
begins. Move it earlier and you will lose the earliest events, which are the
ones that report progress while the application is still starting.
Everything in sdk-config.js. Every field under
application must match your dashboard exactly, capitalisation
included — a mismatched name is rejected by the token endpoint, not by
the browser, so the failure arrives as a refusal rather than as a typo.
| Field | What it does |
|---|---|
STREAMING_API_KEY |
Your key. See the security section — in production this does not belong here. |
tokenExpiryMs |
How long the token stays valid, in milliseconds. It only has to survive long enough for the page to start its session, so short is correct. 60000 is a sensible default. |
application.domain |
The connector your account was issued. |
application.userName |
The account that owns the application, and the name the session is recorded against. One username, used for both — the separate clientUserName field was removed in SDK 4.23. |
application.appName |
The application's name, exactly as the dashboard shows it. |
application.configurationName |
Which stored configuration to launch with. |
application.version |
A specific version, or "latest" to always use the newest. |
configurationToOverride |
Per-session overrides of the stored configuration. Applies to this session only and never changes what is saved. Leave it empty to use the dashboard's settings as they are. |
The fix is to move the token request to your own server. Your server holds the key; the browser only ever receives a token that expires in a minute.
// On YOUR server. The key lives here and never reaches the browser.
app.post("/api/stream-token", async (req, res) => {
const response = await fetch(
"https://token.eagle3dstreaming.com/api/v2/token/create",
{
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": "Auth " + process.env.E3DS_API_KEY
},
body: JSON.stringify({
object: {
core: {
domain: "connector.eagle3dstreaming.com",
userName: "your-account",
appName: "YourApp",
configurationName: "default",
version: "latest"
},
configurationToOverride: {}
},
expiry: 60000,
// Identify the viewer from YOUR session, not from the request
// body - otherwise a caller can stream as somebody else.
client: req.user.id
})
}
);
res.json(await response.json());
});
async function startStream() {
const response = await fetch("/api/stream-token", { method: "POST" });
const tokenData = await response.json();
e3ds_controller.main(tokenData);
}
Open your live page, view source, and search for your key. If it appears, you have not finished. Search the network tab as well — a key sent in a request from the browser is just as exposed as one written in the page.
main() requires three fields on the object you give it:
token, socket_url and
userName (the account username the token was
created for; client is accepted as the older spelling). Anything
else is carried along and used where it applies, so a field your token
endpoint starts returning later becomes available without any change on this
side.
const result = e3ds_controller.main({
token: tokenData.token,
socket_url: tokenData.socket_url,
client: "the-viewer-id"
});
A missing field is a setup mistake, not an exceptional condition, and throwing would take down whatever called it. So it returns instead:
const result = e3ds_controller.main(tokenData);
if (result && result.ok === false) {
// result.error -> "missing_fields"
// result.missing -> ["token"]
// result.message -> a sentence naming exactly what is absent
showYourOwnError(result.message);
}
Three calls, and they are not interchangeable. The distinction is who defined the message.
sendDataToUE(data) — messages you definedAnything your own Blueprints or C++ listen for. The object is delivered as-is, and its shape is entirely up to you.
e3ds_controller.sendDataToUE({ Character: "Aurora" });
e3ds_controller.sendDataToUE({ action: "openDoor", id: 42 });
sendToUnreal(data) is the same function under a second name. Pick
one and be consistent across your codebase.
sendCommandToUE(command) — platform commandsCommands the streaming platform itself understands, rather than ones you implemented. These work without any support in your application.
e3ds_controller.sendCommandToUE({ "Resolution.Width": 1920,
"Resolution.Height": 1080 });
sendConsoleCommandToUE(command) — Unreal console commandsStandard Unreal console commands, as you would type them into the editor.
e3ds_controller.sendConsoleCommandToUE("stat fps");
e3ds_controller.sendConsoleCommandToUE("r.ScreenPercentage 80");
Anything your application sends back arrives at one callback.
e3ds_controller.callbacks.onResponseFromUnreal = function (descriptor) {
// descriptor is whatever your application sent.
if (descriptor.type === "inventoryChanged") {
renderInventory(descriptor.items);
}
};
Quality is expressed as a quantisation parameter. It runs roughly from 0 to 51 and it is inverted: lower means better quality and more bandwidth.
// Hold one fixed quality. The picture stays consistent, and a poor
// connection shows up as stutter rather than as softening.
e3ds_controller.setQualityPoint(20);
// Allow it to move within a range. The picture softens under pressure
// instead of stuttering.
e3ds_controller.setAdaptiveQualityPoint(20, 40);
Fixed for anything where appearance is being judged — a product configurator, a design review — because a frame that quietly softens misrepresents the thing on screen. Adaptive for anything interactive, where a stutter costs more than a soft frame.
// Pin the stream to a fixed size.
const r = e3ds_controller.setResolution(1920, 1080);
// { ok: false, error: "invalid_resolution", ... } if the numbers are unusable
// Go back to following the size of the element on the page.
e3ds_controller.useViewportResolution();
Following the viewport is the right default: the stream renders at the size it is actually displayed, so nothing is spent on pixels that get scaled away. Pin it when you need a known size regardless of the window — a fixed-size embed, or a capture you intend to store.
Features API › Fullscreen: the same from your own page (iframe or Web SDK) or from your Unreal app, with demos and troubleshooting.
e3ds_controller.toggleFullscreen();
e3ds_controller.switchMouseControlSchemeTo("HoveringMouse"); // cursor is visible
e3ds_controller.switchMouseControlSchemeTo("LockedMouse"); // pointer captured
e3ds_controller.setTouchInputEnabled(false);
Features API › Mouse mode: locked or hovering: the same from your own page (iframe or Web SDK) or from your Unreal app, with demos and troubleshooting.
Locked capture suits first-person navigation, where the pointer should not leave the frame. Hovering suits anything with an interface the viewer clicks. Turning touch off is worth doing when your own page handles gestures that would otherwise be swallowed by the stream.
For an on-screen control that should behave like a key press — a
touch-screen movement pad, an action button, a menu shortcut — the
platform can emulate keyboard input. It is not on
e3ds_controller yet. The commands exist and work, but
only over the iframe bridge:
sending keys to the app.
Said here rather than left out, because the alternative is worse: looking for a method that does not exist and concluding the feature does not either. If you need it from the SDK, tell support — the plumbing is already in the player and exposing it is small.
Features API › Volume: the same from your own page (iframe or Web SDK) or from your Unreal app, with demos and troubleshooting.
e3ds_controller.setVolume(0.5); // 0 = silent, 1 = full
// Values run from 0 to 1. Anything above 1 is treated as 1, so convert a
// percentage yourself: 75% is 0.75.
e3ds_controller.setVolume(75 / 100);
Values outside 0–1 are clamped; a value that is not a number is refused
and reported as false rather than being applied as zero.
e3ds_controller.captureScreenshot();
// captureScreenShot() is the same call under an older spelling.
This captures the current frame and hands it to the browser as a download.
Assign these to e3ds_controller.callbacks before the session
starts. Each one is optional; the SDK simply does not call what you have not
assigned.
| Callback | When it fires | What to do with it |
|---|---|---|
onConfigAcquire() |
The configuration has been fetched and startup is beginning. | Show that something is happening. |
onReceivingAppAcquiringProgress(percent) |
A machine is being acquired. | First stage of your progress display. |
onReceivingAppPreparationProgress(percent) |
The application is being prepared on that machine. | Second stage. This is the longest one on a cold start. |
onReceivingAppStartingProgress(percent) |
The application is launching. | Final stage before video. |
onDataChannelOpen() |
Two-way communication is available. | Send your first message from here, and enable controls that talk to the app. |
onResponseFromUnreal(descriptor) |
Your application sent something. | Your side of your own protocol. |
onDataChannelClose() |
Communication ended. | Disable controls that send. Video may still be on screen. |
onStreamerDisconnected() |
The application stopped sending. | Do not build on this one. It is unreliable, and
onSessionEnding already covers a crash — with text
explaining it. |
onSessionExpired() |
The session reached its time limit. | Offer to start a new one. Nothing is wrong. |
onSessionEnding(message) |
The session is ending with an explanation. | Show message in your own interface — see below. |
Acquiring, preparing and starting are reported separately because they take very different amounts of time and fail for different reasons. Collapsing them into a single bar throws away the information that tells a waiting viewer whether anything is actually happening.
e3ds_controller.callbacks.onReceivingAppAcquiringProgress = function (percent) {
setStatus("Finding a machine", percent);
};
e3ds_controller.callbacks.onReceivingAppPreparationProgress = function (percent) {
setStatus("Preparing the application", percent);
};
e3ds_controller.callbacks.onReceivingAppStartingProgress = function (percent) {
setStatus("Starting up", percent);
};
Features API › End the session: the same from your own page (iframe or Web SDK) or from your Unreal app, with demos and troubleshooting.
By default the player replaces the page with a message when a session ends.
In an application that is usually wrong — it destroys the interface
around the stream. Handle onSessionEnding and it becomes
yours to present.
e3ds_controller.callbacks.onSessionEnding = function (message) {
showOverlay({
title: "The session has ended",
body: message,
action: { label: "Start again", onClick: startStream }
});
};
e3ds_controller.terminate();
This ends the session and releases the machine. Call it when the viewer navigates away from the stream, and not only when they close the tab — a session nobody is watching still occupies a machine and is still billed.
Fetch the replacement token before ending the session that is running. If the request fails, the viewer keeps what they already had instead of being left with nothing.
async function restartStream() {
const response = await fetch("/api/stream-token", { method: "POST" });
const tokenData = await response.json();
if (!tokenData || !tokenData.token) {
showYourOwnError("Could not start a new session. The current one is still running.");
return; // the existing stream is untouched
}
e3ds_controller.main(tokenData); // ends the old session and starts the new
}
Beside the callbacks above, the SDK accepts five names that are never called. They are listed here so you do not wire one up and then spend an afternoon debugging why it never fires — which is the only thing that can happen, because a callback that is never invoked reports nothing at all.
| Never fires | Use instead |
|---|---|
onError | onSessionEnding — it carries the reason |
onEnding | onSessionEnding |
onHtmlBind | onDataChannelOpen |
onFrame | — |
preventErrorRedirect | — the redirect cannot be prevented today |
| What you see | What it usually is |
|---|---|
| Nothing happens, no errors | The token request failed. Check the network tab: a rejection carries a body explaining which field the server did not accept. |
| The server refuses the token request | A field in application does not match the dashboard. Capitalisation counts. |
| Video plays but messages do nothing | You are sending before onDataChannelOpen, or the application is not listening for that message name. |
| Console commands are ignored | The command is not one the application accepts, or it was sent before onDataChannelOpen. Unknown commands are dropped without an error. |
| No sound until something is clicked | Expected. The browser blocks audio that begins without a user gesture. |
| Fullscreen does nothing | It was not called from a click or tap. |
| Screenshots never arrive on a phone | iOS and in-app browsers block downloads a page generates. Nothing is wrong with the call. |
| It works locally, fails when deployed | Usually the token request: check the network tab for the token endpoint's reply. If the app uses the microphone, the page must also be served over HTTPS, which browsers require before granting it. |
The server-side token pattern in full, and how to verify you have done it.
Last updated