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

feat(react-sdk): LivestreamLayout #1103

Merged
merged 6 commits into from
Sep 27, 2023
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
54 changes: 54 additions & 0 deletions packages/client/src/sorting/__tests__/presets.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { describe, expect, it } from 'vitest';
import * as TestData from './participant-data';
import { VisibilityState } from '../../types';
import { paginatedLayoutSortPreset } from '../presets';
import { TrackType } from '../../gen/video/sfu/models/models';

describe('presets', () => {
it('paginatedLayoutSortPreset', () => {
const ps = TestData.participants().map((p) => ({
...p,
viewportVisibilityState: {
videoTrack: VisibilityState.UNKNOWN,
screenShareTrack: VisibilityState.UNKNOWN,
},
}));

expect(ps.sort(paginatedLayoutSortPreset).map((p) => p.name)).toEqual([
'F',
'B',
'E',
'D',
'A',
'C',
]);

// server-pin C
ps.at(-1)!.pin = {
isLocalPin: false,
pinnedAt: Date.now(),
};

expect(ps.sort(paginatedLayoutSortPreset).map((p) => p.name)).toEqual([
'C',
'F',
'B',
'E',
'D',
'A',
]);

ps.at(-3)!.publishedTracks = [TrackType.AUDIO]; // E
ps.at(-2)!.isDominantSpeaker = false; // D
ps.at(-1)!.isDominantSpeaker = true; // A

expect(ps.sort(paginatedLayoutSortPreset).map((p) => p.name)).toEqual([
'C',
'F',
'B',
'A',
'E',
'D',
]);
});
});
28 changes: 28 additions & 0 deletions packages/client/src/sorting/presets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,19 @@ const ifInvisibleBy = conditional(
b.viewportVisibilityState?.videoTrack === VisibilityState.INVISIBLE,
);

/**
* A comparator that applies the decorated comparator when a participant is
* either invisible or its visibility state isn't known.
* For visible participants, it ensures stable sorting.
*/
const ifInvisibleOrUnknownBy = conditional(
(a: StreamVideoParticipant, b: StreamVideoParticipant) =>
a.viewportVisibilityState?.videoTrack === VisibilityState.INVISIBLE ||
a.viewportVisibilityState?.videoTrack === VisibilityState.UNKNOWN ||
b.viewportVisibilityState?.videoTrack === VisibilityState.INVISIBLE ||
b.viewportVisibilityState?.videoTrack === VisibilityState.UNKNOWN,
);

/**
* The default sorting preset.
*/
Expand Down Expand Up @@ -48,6 +61,21 @@ export const speakerLayoutSortPreset = combineComparators(
// ifInvisibleBy(name),
);

/**
* The sorting preset for layouts that don't render all participants but
* instead, render them in pages.
*/
export const paginatedLayoutSortPreset = combineComparators(
pinned,
screenSharing,
dominantSpeaker,
ifInvisibleOrUnknownBy(speaking),
ifInvisibleOrUnknownBy(reactionType('raised-hand')),
ifInvisibleOrUnknownBy(publishingVideo),
ifInvisibleOrUnknownBy(publishingAudio),
// ifInvisibleOrUnknownBy(name),
);

/**
* The sorting preset for livestreams and audio rooms.
*/
Expand Down
12 changes: 7 additions & 5 deletions packages/react-sdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,17 +54,19 @@ Here are some of the features we support:
## Roadmap

### 0.4 milestone
- [X] Enhanced device management API
- [X] Composite layout for streaming and recording
- [ ] Livestream Player

- [x] Enhanced device management API
- [x] Composite layout for streaming and recording
- [x] Livestream Player
- [ ] Screenshare Audio
- [ ] Screen-sharing resolution and FPS control
- [ ] Fast-reconnects
- [ ] New Device Management API
- [X] SFU retries
- [x] SFU retries
- [ ] Query call session endpoint

### 0.5 milestone

- [ ] Enhanced UI components and theming
- [ ] Enhanced SDK build system
- [ ] Typescript generics enhancements
Expand All @@ -76,14 +78,14 @@ Here are some of the features we support:
- [ ] E2E testing platform

### 0.6 milestone

- [ ] Break-out rooms
- [ ] Waiting rooms
- [ ] Transcriptions
- [ ] Closed captions
- [ ] Video and audio filters
- [ ] Dynascale: turn off incoming video when the browser is in the background


## Contributing

- How can I submit a sample app?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ const client = new StreamVideoClient({ apiKey, user, token });
const call = client.call('livestream', callId);
call.join({ create: true });

export const App = () => {
export const MyApp = () => {
return (
<StreamVideo client={client}>
<StreamCall call={call}>
Expand Down Expand Up @@ -311,6 +311,9 @@ Compared to the current code in in `App.tsx` you:
- Don't render the local video, but instead render the remote videos - `useRemoteParticipants` instead of `useLocalParticipant`
- Typically include some small UI elements like viewer count, a button to mute etc.

Additionally, you can use our SDK provided `LivestreamLayout` that enables consuming WebRTC-based livestreams.
Read more about it here: [Watching a livestream (WebRTC)](../../ui-cookbook/watching-a-livestream).

### Step 6 - (Optional) Viewing a livestream with HLS

Another way to watch a livestream is using HLS. HLS tends to have a 10 to 20 seconds delay, while the above WebRTC approach is realtime.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
---
id: watching-a-livestream
title: Watching a livestream
---

As already described in our [livestream tutorial](../../tutorials/livestream), there are two ways how a livestream
can be watched: either via HLS or via WebRTC.

This guide describes how to watch a livestream via WebRTC.

Our React Video SDK provides a specialized `LivestreamLayout` component for this purpose.
Right next to playing the livestream, it also provides a standardized livestreaming experience:

- Shows a "Live" indicator badge
- Shows the current viewer count
- Shows the duration of the livestream
- Allows toggling between fullscreen and normal mode

Here is a preview of the `LivestreamLayout` component in video mode:
![Preview of the UI](../assets/06-ui-cookbook/15-watching-a-livestream/normal-mode.png)

And, in screen share mode:
![Preview of the UI](../assets/06-ui-cookbook/15-watching-a-livestream/screenshare-mode.png)

## Usage

```tsx
import {
LivestreamLayout,
StreamVideo,
StreamCall,
} from '@stream-io/video-react-sdk';

export const MyLivestreamApp = () => {
// init client and call here...
return (
<StreamVideo client={client}>
<StreamCall call={call}>
<LivestreamLayout />
</StreamCall>
</StreamVideo>
);
};
```

Note: Please make sure that the livestream is started and the call isn't in `backstage` mode before rendering this component.

### Customization options

The `LivestreamLayout` component provides a few customization options that can be passed as props:

- `muted` - a `boolean` flag that indicates whether the livestream should be muted or not. Defaults to `false`.
- `enableFullscreen` - a `boolean` flag that indicates whether the fullscreen button should be shown or not. Defaults to `true`.
- `showParticipantCount`- a `boolean` flag that indicates whether the current viewer count should be shown or not. Defaults to `true`.
- `showDuration` - a `boolean` flag that indicates whether the duration of the livestream should be shown or not. Defaults to `true`.
- `showLiveBadge`- a `boolean` flag that indicates whether the "Live" badge should be shown or not. Defaults to `true`.
- `showSpeakerName` - a `boolean` flag that indicates whether the speaker should be shown or not. Defaults to `false`.
- `floatingParticipantProps` - an optional object that contains props that should be passed to the "Floating Participant" component in screen-share mode.
They are identical to the props of the `LivestreamLayout` component props.
- `floatingParticipantProps.position` - a `string` that indicates the position of the "Floating Participant" component.
Can be either `top-left`, `top-right`, `bottom-left` or `bottom-right`. Defaults to `top-right`.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -211,11 +211,12 @@ export const ParticipantActionsContextMenu = ({
};

const toggleFullscreenMode = () => {
if (!fullscreenModeOn)
if (!fullscreenModeOn) {
return participantViewElement
?.requestFullscreen()
.then(() => setFullscreenModeOn(true))
.catch(console.error);
}

document
.exitFullscreen()
Expand Down
35 changes: 35 additions & 0 deletions packages/react-sdk/src/core/components/Audio/ParticipantsAudio.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { ComponentProps } from 'react';
import { SfuModels, StreamVideoParticipant } from '@stream-io/video-client';
import { Audio } from './Audio';

export type ParticipantsAudioProps = {
/**
* The participants to play audio for.
*/
participants: StreamVideoParticipant[];

/**
* Props to pass to the underlying `Audio` components.
*/
audioProps?: ComponentProps<typeof Audio>;
};

export const ParticipantsAudio = (props: ParticipantsAudioProps) => {
const { participants, audioProps } = props;
return (
<>
{participants.map(
(participant) =>
!participant.isLocalParticipant &&
participant.publishedTracks.includes(SfuModels.TrackType.AUDIO) &&
participant.audioStream && (
<Audio
{...audioProps}
participant={participant}
key={participant.sessionId}
/>
),
)}
</>
);
};
1 change: 1 addition & 0 deletions packages/react-sdk/src/core/components/Audio/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './Audio';
export * from './ParticipantsAudio';
Loading