Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve default screen sharing FPS, limiting capturing surface #972

Merged
merged 5 commits into from
Jan 10, 2024
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
5 changes: 5 additions & 0 deletions .changeset/little-yaks-shop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'livekit-client': patch
---

Default screenshare capture resolution to 1080p
2 changes: 2 additions & 0 deletions example/sample.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
RoomConnectOptions,
RoomEvent,
RoomOptions,
ScreenSharePresets,
Track,
TrackPublication,
VideoCaptureOptions,
Expand Down Expand Up @@ -95,6 +96,7 @@ const appActions = {
dtx: true,
red: true,
forceStereo: false,
screenShareEncoding: ScreenSharePresets.h1080fps30.encoding,
},
videoCaptureDefaults: {
resolution: VideoPresets.h720.resolution,
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
"build:watch": "rollup --watch --config rollup.config.js",
"build-docs": "typedoc",
"proto": "protoc --es_out src/proto --es_opt target=ts -I./protocol ./protocol/livekit_rtc.proto ./protocol/livekit_models.proto",
"sample": "vite example -c vite.config.js",
"sample": "vite example -c vite.config.mjs",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks 🙏

"lint": "eslint src",
"test": "vitest run src",
"deploy": "gh-pages -d example/dist",
Expand Down
24 changes: 22 additions & 2 deletions src/room/participant/LocalParticipant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,24 @@ import type {
TrackPublishOptions,
VideoCaptureOptions,
} from '../track/options';
import { VideoPresets, isBackupCodec } from '../track/options';
import { ScreenSharePresets, VideoPresets, isBackupCodec } from '../track/options';
import {
constraintsForOptions,
mergeDefaultOptions,
mimeTypeToVideoCodecString,
screenCaptureToDisplayMediaStreamOptions,
} from '../track/utils';
import type { DataPublishOptions } from '../types';
import { Future, isFireFox, isSVCCodec, isSafari, isWeb, supportsAV1, supportsVP9 } from '../utils';
import {
Future,
isFireFox,
isSVCCodec,
isSafari,
isSafari17,
isWeb,
supportsAV1,
supportsVP9,
} from '../utils';
import Participant from './Participant';
import type { ParticipantTrackPermission } from './ParticipantTrackPermission';
import { trackPermissionToProto } from './ParticipantTrackPermission';
Expand Down Expand Up @@ -448,6 +457,13 @@ export default class LocalParticipant extends Participant {
throw new DeviceUnsupportedError('getDisplayMedia not supported');
}

if (options.resolution === undefined && !isSafari17()) {
// we need to constrain the dimensions, otherwise it could lead to low bitrate
// due to encoding a huge video. Encoding such large surfaces is really expensive
// unfortunately Safari 17 has a but and cannot be constrained by default
options.resolution = ScreenSharePresets.h1080fps30.resolution;
}

const constraints = screenCaptureToDisplayMediaStreamOptions(options);
const stream: MediaStream = await navigator.mediaDevices.getDisplayMedia(constraints);

Expand All @@ -457,6 +473,10 @@ export default class LocalParticipant extends Participant {
}
const screenVideo = new LocalVideoTrack(tracks[0], undefined, false);
screenVideo.source = Track.Source.ScreenShare;
if (options.contentHint) {
screenVideo.mediaStreamTrack.contentHint = options.contentHint;
}

const localTracks: Array<LocalTrack> = [screenVideo];
if (stream.getAudioTracks().length > 0) {
this.emit(ParticipantEvent.AudioStreamAcquired);
Expand Down
4 changes: 2 additions & 2 deletions src/room/participant/publishUtils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ describe('screenShareSimulcastDefaults', () => {
);
expect(defaultSimulcastLayers[0].width).toBe(640);
expect(defaultSimulcastLayers[0].height).toBe(360);
expect(defaultSimulcastLayers[0].encoding.maxFramerate).toBe(3);
expect(defaultSimulcastLayers[0].encoding.maxBitrate).toBe(150_000);
expect(defaultSimulcastLayers[0].encoding.maxFramerate).toBe(15);
expect(defaultSimulcastLayers[0].encoding.maxBitrate).toBe(375000);
});
});
5 changes: 3 additions & 2 deletions src/room/participant/publishUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export const defaultSimulcastPresets43 = [VideoPresets43.h180, VideoPresets43.h3

/* @internal */
export const computeDefaultScreenShareSimulcastPresets = (fromPreset: VideoPreset) => {
const layers = [{ scaleResolutionDownBy: 2, fps: 3 }];
const layers = [{ scaleResolutionDownBy: 2, fps: fromPreset.encoding.maxFramerate }];
return layers.map(
(t) =>
new VideoPreset(
Expand All @@ -54,7 +54,8 @@ export const computeDefaultScreenShareSimulcastPresets = (fromPreset: VideoPrese
150_000,
Math.floor(
fromPreset.encoding.maxBitrate /
(t.scaleResolutionDownBy ** 2 * ((fromPreset.encoding.maxFramerate ?? 30) / t.fps)),
(t.scaleResolutionDownBy ** 2 *
((fromPreset.encoding.maxFramerate ?? 30) / (t.fps ?? 30))),
),
),
t.fps,
Expand Down
7 changes: 4 additions & 3 deletions src/room/track/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,18 @@ import DeviceManager from '../DeviceManager';
import { audioDefaults, videoDefaults } from '../defaults';
import { DeviceUnsupportedError, TrackInvalidError } from '../errors';
import { mediaTrackToLocalTrack } from '../participant/publishUtils';
import { isSafari17 } from '../utils';
import LocalAudioTrack from './LocalAudioTrack';
import type LocalTrack from './LocalTrack';
import LocalVideoTrack from './LocalVideoTrack';
import { Track } from './Track';
import { ScreenSharePresets } from './options';
import type {
AudioCaptureOptions,
CreateLocalTracksOptions,
ScreenShareCaptureOptions,
VideoCaptureOptions,
} from './options';
import { ScreenSharePresets } from './options';
import {
constraintsForOptions,
mergeDefaultOptions,
Expand Down Expand Up @@ -116,8 +117,8 @@ export async function createLocalScreenTracks(
if (options === undefined) {
options = {};
}
if (options.resolution === undefined) {
options.resolution = ScreenSharePresets.h1080fps15.resolution;
if (options.resolution === undefined && !isSafari17()) {
options.resolution = ScreenSharePresets.h1080fps30.resolution;
}

if (navigator.mediaDevices.getDisplayMedia === undefined) {
Expand Down
17 changes: 12 additions & 5 deletions src/room/track/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,9 +169,10 @@ export interface ScreenShareCaptureOptions {
video?: true | { displaySurface?: 'window' | 'browser' | 'monitor' };

/**
* capture resolution, defaults to screen resolution
* NOTE: In Safari 17, specifying any resolution at all would lead to a low-resolution
* capture. https://bugs.webkit.org/show_bug.cgi?id=263015
* capture resolution, defaults to 1080 for all browsers other than Safari
* On Safari 17, default resolution is not capped, due to a bug, specifying
* any resolution at all would lead to a low-resolution capture.
* https://bugs.webkit.org/show_bug.cgi?id=263015
*/
resolution?: VideoResolution;

Expand All @@ -187,6 +188,9 @@ export interface ScreenShareCaptureOptions {
/** specifies whether the browser should include the system audio among the possible audio sources offered to the user */
systemAudio?: 'include' | 'exclude';

/** specify the type of content, see: https://www.w3.org/TR/mst-content-hint/#video-content-hints */
contentHint?: 'detail' | 'text' | 'motion';

/**
* Experimental option to control whether the audio playing in a tab will continue to be played out of a user's
* local speakers when the tab is captured.
Expand Down Expand Up @@ -367,9 +371,12 @@ export const VideoPresets43 = {

export const ScreenSharePresets = {
h360fps3: new VideoPreset(640, 360, 200_000, 3, 'medium'),
h720fps5: new VideoPreset(1280, 720, 400_000, 5, 'medium'),
h360fps15: new VideoPreset(640, 360, 400_000, 15, 'medium'),
h720fps5: new VideoPreset(1280, 720, 800_000, 5, 'medium'),
h720fps15: new VideoPreset(1280, 720, 1_500_000, 15, 'medium'),
h720fps30: new VideoPreset(1280, 720, 2_000_000, 30, 'medium'),
h1080fps15: new VideoPreset(1920, 1080, 2_500_000, 15, 'medium'),
h1080fps30: new VideoPreset(1920, 1080, 4_000_000, 30, 'medium'),
h1080fps30: new VideoPreset(1920, 1080, 5_000_000, 30, 'medium'),
// original resolution, without resizing
original: new VideoPreset(0, 0, 7_000_000, 30, 'medium'),
} as const;
3 changes: 2 additions & 1 deletion src/room/track/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,8 @@ export function screenCaptureToDisplayMediaStreamOptions(
options: ScreenShareCaptureOptions,
): DisplayMediaStreamOptions {
let videoConstraints: MediaTrackConstraints | boolean = options.video ?? true;
if (options.resolution) {
// treat 0 as uncapped
if (options.resolution && options.resolution.width > 0 && options.resolution.height > 0) {
videoConstraints = typeof videoConstraints === 'boolean' ? {} : videoConstraints;
if (isSafari()) {
videoConstraints = {
Expand Down
5 changes: 5 additions & 0 deletions src/room/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,11 @@ export function isSafari(): boolean {
return getBrowser()?.name === 'Safari';
}

export function isSafari17(): boolean {
const b = getBrowser();
return b?.name === 'Safari' && b.version.startsWith('17.');
}

export function isMobile(): boolean {
if (!isWeb()) return false;
return /Tablet|iPad|Mobile|Android|BlackBerry/.test(navigator.userAgent);
Expand Down
Loading