Merge pull request #29 from daily-demos/dailyjs/active-speaker

Active speaker layout and participant sidebar
This commit is contained in:
Jon Taylor 2022-01-04 13:52:57 +00:00 committed by GitHub
commit 8508b48998
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 352 additions and 0 deletions

View File

@ -0,0 +1,4 @@
{
"presets": ["next/babel"],
"plugins": ["inline-react-svg"]
}

View File

@ -0,0 +1,31 @@
# Active Speaker
![Active speaker](./image.png)
### Live example
**[See it in action here ➡️](https://dailyjs-active-speaker.vercel.app)**
---
## What does this demo do?
- Uses an active speaker view mode that shows the currently talking participant (or active screen share) in a larger tile
- Introduces the `ParticipantBar` column that virtually scrolls through all call participants
- Uses manual subscriptions to paginate between tiles that are currently in view. For more information about how this works, please refer to the [pagination demo](../pagination)
Please note: this demo is not currently mobile optimised
### Getting started
```
# set both DAILY_API_KEY and DAILY_DOMAIN
mv env.example .env.local
yarn
yarn workspace @dailyjs/active-speaker dev
```
## Deploy your own on Vercel
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/daily-co/clone-flow?repository-url=https%3A%2F%2Fgithub.com%2Fdaily-demos%2Fexamples.git&env=DAILY_DOMAIN%2CDAILY_API_KEY&envDescription=Your%20Daily%20domain%20and%20API%20key%20can%20be%20found%20on%20your%20account%20dashboard&envLink=https%3A%2F%2Fdashboard.daily.co&project-name=daily-examples&repo-name=daily-examples)

View File

@ -0,0 +1,15 @@
import React from 'react';
import App from '@dailyjs/basic-call/components/App';
import { Room } from '../Room';
// Extend our basic call app component with our custom Room componenet
export const AppWithSpeakerViewRoom = () => (
<App
customComponentForState={{
room: () => <Room />,
}}
/>
);
export default AppWithSpeakerViewRoom;

View File

@ -0,0 +1 @@
export { AppWithSpeakerViewRoom as default } from './App';

View File

@ -0,0 +1,7 @@
import React from 'react';
import { SpeakerView } from '../SpeakerView';
export const Room = () => <SpeakerView />;
export default Room;

View File

@ -0,0 +1,2 @@
export { Room as default } from './Room';
export { Room } from './Room';

View File

@ -0,0 +1,72 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { Tile } from '@dailyjs/shared/components/Tile';
import { DEFAULT_ASPECT_RATIO } from '@dailyjs/shared/constants';
import { useResize } from '@dailyjs/shared/hooks/useResize';
import PropTypes from 'prop-types';
const MAX_RATIO = DEFAULT_ASPECT_RATIO;
const MIN_RATIO = 4 / 3;
export const SpeakerTile = ({ participant, screenRef }) => {
const [ratio, setRatio] = useState(MAX_RATIO);
const [nativeAspectRatio, setNativeAspectRatio] = useState(null);
const [screenHeight, setScreenHeight] = useState(1);
const updateRatio = useCallback(() => {
const rect = screenRef.current?.getBoundingClientRect();
setRatio(rect.width / rect.height);
setScreenHeight(rect.height);
}, [screenRef]);
useResize(() => {
updateRatio();
}, [updateRatio]);
useEffect(() => updateRatio(), [updateRatio]);
/**
* Only use the video's native aspect ratio if it's in portrait mode
* (e.g. mobile) to update how we crop videos. Otherwise, use landscape
* defaults.
*/
const handleNativeAspectRatio = (r) => {
const isPortrait = r < 1;
setNativeAspectRatio(isPortrait ? r : null);
};
const { height, finalRatio, videoFit } = useMemo(
() =>
// Avoid cropping mobile videos, which have the nativeAspectRatio set
({
height: (nativeAspectRatio ?? ratio) >= MIN_RATIO ? '100%' : null,
finalRatio:
nativeAspectRatio || (ratio <= MIN_RATIO ? MIN_RATIO : MAX_RATIO),
videoFit: ratio >= MAX_RATIO || nativeAspectRatio ? 'contain' : 'cover',
}),
[nativeAspectRatio, ratio]
);
const style = {
height,
maxWidth: screenHeight * finalRatio,
overflow: 'hidden',
};
return (
<Tile
aspectRatio={finalRatio}
participant={participant}
style={style}
videoFit={videoFit}
showActiveSpeaker={false}
onVideoResize={handleNativeAspectRatio}
/>
);
};
SpeakerTile.propTypes = {
participant: PropTypes.object,
screenRef: PropTypes.object,
};
export default SpeakerTile;

View File

@ -0,0 +1,2 @@
export { SpeakerTile as default } from './SpeakerTile';
export { SpeakerTile } from './SpeakerTile';

View File

@ -0,0 +1,116 @@
import React, { useEffect, useMemo, useRef } from 'react';
import RoomContainer from '@dailyjs/basic-call/components/Room/RoomContainer';
import ParticipantBar from '@dailyjs/shared/components/ParticipantBar/ParticipantBar';
import VideoContainer from '@dailyjs/shared/components/VideoContainer/VideoContainer';
import { useCallState } from '@dailyjs/shared/contexts/CallProvider';
import { useParticipants } from '@dailyjs/shared/contexts/ParticipantsProvider';
import { useTracks } from '@dailyjs/shared/contexts/TracksProvider';
import { useUIState } from '@dailyjs/shared/contexts/UIStateProvider';
import { isScreenId } from '@dailyjs/shared/contexts/participantsState';
import { SpeakerTile } from './SpeakerTile';
const SIDEBAR_WIDTH = 186;
export const SpeakerView = () => {
const { currentSpeaker, localParticipant, participants, screens } =
useParticipants();
const { updateCamSubscriptions } = useTracks();
const { showLocalVideo } = useCallState();
const { pinnedId } = useUIState();
const activeRef = useRef();
const screensAndPinned = useMemo(
() => [...screens, ...participants.filter(({ id }) => id === pinnedId)],
[participants, pinnedId, screens]
);
const otherParticipants = useMemo(
() => participants.filter(({ isLocal }) => !isLocal),
[participants]
);
const showSidebar = useMemo(() => {
const hasScreenshares = screens.length > 0;
if (isScreenId(pinnedId)) {
return false;
}
return participants.length > 1 || hasScreenshares;
}, [participants, pinnedId, screens]);
/* const screenShareTiles = useMemo(
() => <ScreensAndPins items={screensAndPinned} />,
[screensAndPinned]
); */
const hasScreenshares = useMemo(() => screens.length > 0, [screens]);
const fixedItems = useMemo(() => {
const items = [];
if (showLocalVideo) {
items.push(localParticipant);
}
if (hasScreenshares && otherParticipants.length > 0) {
items.push(otherParticipants[0]);
}
return items;
}, [hasScreenshares, localParticipant, otherParticipants, showLocalVideo]);
const otherItems = useMemo(() => {
if (otherParticipants.length > 1) {
return otherParticipants.slice(hasScreenshares ? 1 : 0);
}
return [];
}, [hasScreenshares, otherParticipants]);
/**
* Update cam subscriptions, in case ParticipantBar is not shown.
*/
useEffect(() => {
// Sidebar takes care of cam subscriptions for all displayed participants.
if (showSidebar) return;
updateCamSubscriptions([
currentSpeaker?.id,
...screensAndPinned.map((p) => p.id),
]);
}, [currentSpeaker, screensAndPinned, showSidebar, updateCamSubscriptions]);
return (
<div className="speaker-view">
<RoomContainer>
<VideoContainer>
<div ref={activeRef} className="active">
<SpeakerTile participant={currentSpeaker} screenRef={activeRef} />
</div>
</VideoContainer>
</RoomContainer>
{showSidebar && (
<ParticipantBar
fixed={fixedItems}
others={otherItems}
width={SIDEBAR_WIDTH}
/>
)}
<style jsx>{`
.speaker-view {
display: flex;
height: 100%;
width: 100%;
position: relative;
}
.active {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
width: 100%;
}
`}</style>
</div>
);
};
export default SpeakerView;

View File

@ -0,0 +1,2 @@
export { SpeakerView as default } from './SpeakerView';
export { SpeakerView } from './SpeakerView';

View File

@ -0,0 +1,12 @@
# Domain excluding 'https://' and 'daily.co' e.g. 'somedomain'
DAILY_DOMAIN=
# Obtained from https://dashboard.daily.co/developers
DAILY_API_KEY=
# Daily REST API endpoint
DAILY_REST_DOMAIN=https://api.daily.co/v1
# Enable manual track subscriptions
MANUAL_TRACK_SUBS=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

View File

@ -0,0 +1 @@
// Note: I am here because next-transpile-modules requires a mainfile

View File

@ -0,0 +1,13 @@
const withPlugins = require('next-compose-plugins');
const withTM = require('next-transpile-modules')([
'@dailyjs/shared',
'@dailyjs/basic-call',
]);
const packageJson = require('./package.json');
module.exports = withPlugins([withTM], {
env: {
PROJECT_TITLE: packageJson.description,
},
});

View File

@ -0,0 +1,25 @@
{
"name": "@dailyjs/active-speaker",
"description": "Basic Call + Active Speaker",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@dailyjs/basic-call": "*",
"@dailyjs/shared": "*",
"next": "^11.0.0",
"pluralize": "^8.0.0",
"react": "^17.0.2",
"react-dom": "^17.0.2"
},
"devDependencies": {
"babel-plugin-module-resolver": "^4.1.0",
"next-compose-plugins": "^2.2.1",
"next-transpile-modules": "^8.0.0"
}
}

View File

@ -0,0 +1,7 @@
import React from 'react';
import App from '@dailyjs/basic-call/pages/_app';
import AppWithSpeakerViewRoom from '../components/App';
App.customAppComponent = <AppWithSpeakerViewRoom />;
export default App;

View File

@ -0,0 +1 @@
../../basic-call/pages/api

View File

@ -0,0 +1,13 @@
import Index from '@dailyjs/basic-call/pages';
import getDemoProps from '@dailyjs/shared/lib/demoProps';
export async function getStaticProps() {
const defaultProps = getDemoProps();
// Pass through domain as prop
return {
props: defaultProps,
};
}
export default Index;

View File

@ -0,0 +1,14 @@
<svg width="80" height="32" viewBox="0 0 80 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M21.5886 27.0149C23.8871 27.0149 25.7976 25.6716 26.6035 24.0896V26.6866H30.9021V4H26.6035V13.403C25.7379 11.8209 24.1856 10.7164 21.6782 10.7164C17.7677 10.7164 14.8125 13.7313 14.8125 18.8657V19.1045C14.8125 24.2985 17.7976 27.0149 21.5886 27.0149ZM22.8722 23.6418C20.7229 23.6418 19.2304 22.1194 19.2304 19.0149V18.7761C19.2304 15.6716 20.5737 14.0299 22.9916 14.0299C25.3498 14.0299 26.7229 15.6119 26.7229 18.7164V18.9552C26.7229 22.1194 25.1409 23.6418 22.8722 23.6418Z" fill="#121A24"/>
<path d="M37.9534 27.0149C40.4011 27.0149 41.7743 26.0597 42.6698 24.806V26.6866H46.8787V16.5075C46.8787 12.2687 44.1623 10.7164 40.3414 10.7164C36.5205 10.7164 33.5951 12.3582 33.3265 16.0597H37.416C37.5951 14.7164 38.3713 13.8507 40.0728 13.8507C42.0429 13.8507 42.6101 14.8657 42.6101 16.7164V17.3433H40.8489C36.0728 17.3433 32.7295 18.7164 32.7295 22.3582C32.7295 25.6418 35.1175 27.0149 37.9534 27.0149ZM39.2369 24C37.6548 24 36.9683 23.2537 36.9683 22.1194C36.9683 20.4478 38.431 19.9104 40.9384 19.9104H42.6101V21.2239C42.6101 22.9552 41.1474 24 39.2369 24Z" fill="#121A24"/>
<path d="M49.647 26.6866H53.9456V11.0746H49.647V26.6866ZM51.7665 8.95522C53.1694 8.95522 54.2441 7.9403 54.2441 6.59702C54.2441 5.25373 53.1694 4.23881 51.7665 4.23881C50.3933 4.23881 49.3187 5.25373 49.3187 6.59702C49.3187 7.9403 50.3933 8.95522 51.7665 8.95522Z" fill="#121A24"/>
<path d="M56.9765 26.6866H61.275V4H56.9765V26.6866Z" fill="#121A24"/>
<path d="M70.5634 32L78.9917 11.0746H74.8711L71.3499 20.4478L67.5589 11.0746H62.9021L69.1523 25.1953L66.3779 32H70.5634Z" fill="#121A24"/>
<path d="M0 32H4.1875L17.0766 0H12.9916L0 32Z" fill="url(#paint0_linear)"/>
<defs>
<linearGradient id="paint0_linear" x1="8.88727" y1="-0.222885" x2="8.88727" y2="30" gradientUnits="userSpaceOnUse">
<stop stop-color="#1BEBB9"/>
<stop offset="1" stop-color="#FF9254"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -0,0 +1,14 @@
<svg width="80" height="32" viewBox="0 0 80 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M21.5886 27.0149C23.8871 27.0149 25.7976 25.6716 26.6035 24.0896V26.6866H30.9021V4H26.6035V13.403C25.7379 11.8209 24.1856 10.7164 21.6782 10.7164C17.7677 10.7164 14.8125 13.7313 14.8125 18.8657V19.1045C14.8125 24.2985 17.7976 27.0149 21.5886 27.0149ZM22.8722 23.6418C20.7229 23.6418 19.2304 22.1194 19.2304 19.0149V18.7761C19.2304 15.6716 20.5737 14.0299 22.9916 14.0299C25.3498 14.0299 26.7229 15.6119 26.7229 18.7164V18.9552C26.7229 22.1194 25.1409 23.6418 22.8722 23.6418Z" fill="white"/>
<path d="M37.9534 27.0149C40.4011 27.0149 41.7743 26.0597 42.6698 24.806V26.6866H46.8787V16.5075C46.8787 12.2687 44.1623 10.7164 40.3414 10.7164C36.5205 10.7164 33.5951 12.3582 33.3265 16.0597H37.416C37.5951 14.7164 38.3713 13.8507 40.0728 13.8507C42.0429 13.8507 42.6101 14.8657 42.6101 16.7164V17.3433H40.8489C36.0728 17.3433 32.7295 18.7164 32.7295 22.3582C32.7295 25.6418 35.1175 27.0149 37.9534 27.0149ZM39.2369 24C37.6548 24 36.9683 23.2537 36.9683 22.1194C36.9683 20.4478 38.431 19.9104 40.9384 19.9104H42.6101V21.2239C42.6101 22.9552 41.1474 24 39.2369 24Z" fill="white"/>
<path d="M49.647 26.6866H53.9456V11.0746H49.647V26.6866ZM51.7665 8.95522C53.1694 8.95522 54.2441 7.9403 54.2441 6.59702C54.2441 5.25373 53.1694 4.23881 51.7665 4.23881C50.3933 4.23881 49.3187 5.25373 49.3187 6.59702C49.3187 7.9403 50.3933 8.95522 51.7665 8.95522Z" fill="white"/>
<path d="M56.9765 26.6866H61.275V4H56.9765V26.6866Z" fill="white"/>
<path d="M70.5634 32L78.9917 11.0746H74.8711L71.3499 20.4478L67.5589 11.0746H62.9021L69.1523 25.1953L66.3779 32H70.5634Z" fill="white"/>
<path d="M0 32H4.1875L17.0766 0H12.9916L0 32Z" fill="url(#paint0_linear)"/>
<defs>
<linearGradient id="paint0_linear" x1="8.88727" y1="-0.222885" x2="8.88727" y2="30" gradientUnits="userSpaceOnUse">
<stop stop-color="#1BEBB9"/>
<stop offset="1" stop-color="#FF9254"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB