How to Share Screen Presentations
In this tutorial a participant shares their screen, an application window or a browser tab, and it fills the whole stream for everyone else in the session. When they are done, the stream returns to the normal layout and the presenter's camera comes back. You need two pieces for that, and this tutorial combines them.
Presentation mode decides whose video source fills the stream. It does not decide what that source shows. While a presentation runs, all other participants see only the presenter's video source, so when the presenter's browser sends a screen capture instead of the camera, everyone sees the screen. The Eyeson JS SDK makes that switch.
Prerequisites
The presenter takes part through a browser application built with the Eyeson JS SDK, because the screen switch happens on their device. Once the session is set up, the presentation flow works with a participant's access key alone. None of its steps needs your API key, so the presenter's browser can send the presentation requests itself.
- A running session. Each participant gets their own
access_keyfrom the Start Meeting request - A browser application with the Eyeson JS SDK, installed with
npm i --save @eyeson/js - A browser that supports screen capture. Step 2 checks this
- A secure page. Screen capture and camera access only work over HTTPS or on
http://localhost, not from a page opened as a file
Set up the session
Start the meeting with options[sfu_mode]=disabled. Without it, a meeting with two
participants runs in
SFU mode,
and nobody sees themselves in the stream. A presentation still switches to SFU mode while
it runs, and the stream returns to the composited layout afterwards.
curl -X POST \
-H "Authorization: $API_KEY" \
-d "user[name]=Presenter" \
-d "options[sfu_mode]=disabled" \
"https://api.eyeson.team/rooms"
The response contains the presenter's access_key. A second participant joins with the
guest link from links.guest_join, or your application registers them with
POST /guests/GUEST_TOKEN, which returns their own access_key. For a room you reuse
every time, create a Permalink
with the same options instead.
Room options are fixed when a meeting starts, so a meeting that is already running keeps its options until it ends. The same applies to a permalink: its options are set when you create it.
The presentation itself needs only access keys. Your API key is needed to start the session, and for server-side features such as the Meeting Observer or force-stopping a meeting. Keep these on your backend, so the key never reaches the browser.
Step 1. Connect presenter
The presenter joins the session from your browser application with their own access
key. Start with camera and microphone, as in any session. The accept event delivers
the composited stream, which you attach to a video element so the presenter sees what
everyone sees.
import eyeson from "@eyeson/js";
eyeson.onEvent((event) => {
if (event.type === "accept") {
// Some iOS devices need the autoplay attribute on the video element.
const video = document.querySelector("video");
video.srcObject = event.remoteStream;
video.play();
}
});
eyeson.start("ACCESS_KEY", { audio: true, video: true });
Keep the access key at hand. The same key starts and ends the presentation in Step 3 and Step 4.
For a quick test without a bundler, load the SDK as a module from a CDN in a plain HTML page:
<script type="module">
import eyeson, { FeatureDetector, StreamHelpers } from "https://cdn.jsdelivr.net/npm/@eyeson/js@2.2.1/+esm";
</script>
If the camera cannot be opened, for example because another application uses it, joining
fails with an error event and no accept follows. Handle it as described in
Handle errors, or the application waits forever.
Step 2. Share the screen
The start_stream command with screen: true replaces the presenter's camera with a
screen capture. The browser then shows its own picker, where the presenter chooses the
screen, a window or a tab. surface is optional: where the browser supports it, it
pre-selects one of these. FeatureDetector tells you what the current browser can do.
import eyeson, { FeatureDetector } from "@eyeson/js";
function shareScreen() {
if (!FeatureDetector.canScreenCapture()) {
alert("This browser cannot share the screen.");
return;
}
eyeson.send({
type: "start_stream",
audio: true,
screen: true,
// Pre-select browser tabs where supported, otherwise show the browser's default picker.
surface: FeatureDetector.canChooseDisplaySurface() ? "browser" : null,
});
}
eyeson.onEvent((event) => {
if (event.type === "screen_capture_error") {
const message =
event.name === "error_Screen_NotAllowedError"
? "Allow screen access in your browser."
: "Unable to start screen capture";
alert(message);
}
});
Call shareScreen() from a button. Use "monitor" for the entire screen or "window"
for an application window instead of "browser", or leave surface out. Enabling the
screen turns the camera off, because the two are mutually exclusive. If the presenter
allows audio in the browser's picker, the screen's audio is sent along with the
microphone. While the screen is shared, don't send change_stream with video: true,
because that replaces the screen with the camera again.
Step 3. Start presentation
Send a POST to /rooms/ACCESS_KEY/presentation with the presenter's access key, which also makes that
participant the presenter. The request has no body. The stream switches to
SFU mode, the presenter's video source
fills the whole stream, and all other participants see only that source while the
presentation runs.
export ACCESS_KEY=...
curl -i -X POST "https://api.eyeson.team/rooms/$ACCESS_KEY/presentation"
Send this request only once the screen capture is live, which the SDK reports with a
stream_update event after start_stream. If the presenter cancels the browser's
picker, screen_capture_error arrives instead, and you should not start the
presentation, or everyone would see the camera.
A successful start returns 201 Created. The response has no documented body. Only one
presentation can run at a time, see Handle errors.
To show who is presenting, and to disable your start button while someone else presents,
listen for presentation_update. In the browser, use the SDK's event, which needs no API
key. On your backend, the
Meeting Observer sends the
same event, and it needs your API key. presentation holds the running presentation, or
null when it ends.
eyeson.onEvent((event) => {
if (event.type === "presentation_update") {
const isRunning = Boolean(event.presentation);
document.querySelector("#start-presentation").disabled = isRunning;
}
});
Step 4. End presentation
Ending takes two actions in the reverse order. Send a DELETE to the same path to end
presentation mode, then switch the presenter's browser back to the camera. Any
participant can end the presentation with their own access key, so a moderator can stop
it too.
export ACCESS_KEY=...
curl -i -X DELETE "https://api.eyeson.team/rooms/$ACCESS_KEY/presentation"
A successful end returns 200 OK. Then bring the camera back in the presenter's
browser. Restore the presenter's previous camera and microphone state rather than
switching both on, so that a muted presenter stays muted.
Two more cases end a presentation without the presenter's stop button:
- The presenter stops sharing with the browser's own "Stop sharing" button. The SDK then
sends
screen_video_ended, and your application should end the presentation and restore the camera the same way. - Another participant ends the presentation. The presenter's browser then receives
presentation_updatewithpresentation: nullwhile it still sends the screen. Restore the camera without sending a secondDELETE.
import eyeson from "@eyeson/js";
const accessKey = "ACCESS_KEY";
let presenting = true;
let micOn = true;
let camOn = true;
function restoreCamera() {
eyeson.send({ type: "start_stream", audio: micOn, video: camOn });
}
async function stopPresenting() {
presenting = false;
const res = await fetch(`https://api.eyeson.team/rooms/${accessKey}/presentation`, {
method: "DELETE",
});
if (!res.ok) console.warn(`Ending the presentation failed with ${res.status}`);
restoreCamera();
}
eyeson.onEvent((event) => {
if (event.type === "screen_video_ended" && presenting) stopPresenting();
// Someone else ended the presentation.
if (event.type === "presentation_update" && !event.presentation && presenting) {
presenting = false;
restoreCamera();
}
});
Step 5. Verify presentation
Test with two participants, because the result is only visible to the others. Join the second participant with the guest link from Set up the session. After the session is set up, the presentation flow works with access keys, so no backend or API key is involved.
To test on one computer, use two profiles of the same browser. Two different browsers usually cannot use the same camera at once.
Check three things in this order.
- The requests succeed. The start in Step 3 returns
201 Created, and the end in Step 4 returns200 OK. - The others see the screen. While the presentation runs, the second participant sees only the presenter's shared screen, filling the whole stream, and hears the presenter.
- Presentation mode is active. Send the start request from Step 3 once more with the second participant's access key. It returns
409 Conflict, because only one presentation can run at a time. After Step 4, the same request succeeds, which shows the stream is free again.
After Step 4, both participants see the normal layout again and the presenter's camera is back.
A participant can now share a screen, window or tab, it fills the stream for everyone, and the session returns to normal when they stop, whichever way they stop.
Handle errors
Most errors come from two places: the presentation endpoint and the browser's screen
capture. A conflict on start is part of normal use when two people want to present, so
your interface should expect it. Joining can also fail when the camera or microphone
cannot be opened, which the SDK reports with an error event. The table lists every case
the documentation names.
| Where | Signal | Meaning | What to do |
|---|---|---|---|
| Start request | 409 Conflict | Another participant is already presenting | End the current presentation with Step 4, then start again |
| Start or end request | 404 Not Found | The path is wrong, for example a typo such as /presentatio | Check the path against /rooms/ACCESS_KEY/presentation |
| Start or end request | 401 Unauthorized | The access key is wrong | Check the participant's access key |
| Browser | screen_capture_error with name error_Screen_NotAllowedError | The presenter or the browser denied screen access | Ask the presenter to allow screen access |
| Browser | screen_video_ended | The presenter stopped sharing from the browser | End the presentation and restore the camera, as in Step 4 |
| Browser | presentation_update with presentation: null while presenting | Another participant ended the presentation | Restore the camera, as in Step 4 |
| Joining | error with name not_readable or devices | The camera is used by another application, or is missing. A virtual camera such as OBS can be listed first and fail when its application isn't running | Join without video, or let the user choose another camera |
| Joining | error with name permission | The user or the browser denied camera or microphone access | Ask the user to allow access in the browser |
| Joining | error with name meeting_locked or session_in_use | The meeting is locked, or the user already joined in another tab | Show a message to the user |
| Browser | disconnect from the presenter | The presenter left without ending the presentation | End the presentation from another participant, as in Step 4 |
Next Steps
Screen presentations combine a browser feature with a layout change. The pages below go deeper into each part and into what else the presenter's stream can do.
- JS SDK stream control, including
change_screen_videoto switch screens without stopping - Screen Cam guide, on switching between camera and screen
- Presentation reference with both endpoints