Merge branch 'main' of github.com:daily-demos/examples
|
|
@ -0,0 +1,35 @@
|
|||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
node_modules
|
||||
.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
.next
|
||||
out
|
||||
|
||||
# production
|
||||
build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# local env files
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
|
@ -20,6 +20,10 @@ Record a call video and audio using both cloud and local modes
|
|||
|
||||
Send emoji reactions to all clients using sendAppMessage
|
||||
|
||||
### [📃 Pagination](./pagination)
|
||||
|
||||
Demonstrates using manual track management to support larger call sizes
|
||||
|
||||
---
|
||||
|
||||
## Getting started
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"presets": ["next/babel"],
|
||||
"plugins": ["inline-react-svg"]
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
# Active Speaker
|
||||
|
||||

|
||||
|
||||
### 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
|
||||
|
||||
[](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)
|
||||
|
|
@ -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;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { AppWithSpeakerViewRoom as default } from './App';
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
import React from 'react';
|
||||
|
||||
import { SpeakerView } from '../SpeakerView';
|
||||
|
||||
export const Room = () => <SpeakerView />;
|
||||
|
||||
export default Room;
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
export { Room as default } from './Room';
|
||||
export { Room } from './Room';
|
||||
|
|
@ -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;
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
export { SpeakerTile as default } from './SpeakerTile';
|
||||
export { SpeakerTile } from './SpeakerTile';
|
||||
|
|
@ -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;
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
export { SpeakerView as default } from './SpeakerView';
|
||||
export { SpeakerView } from './SpeakerView';
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
# 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
|
||||
|
After Width: | Height: | Size: 143 KiB |
|
|
@ -0,0 +1 @@
|
|||
// Note: I am here because next-transpile-modules requires a mainfile
|
||||
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -0,0 +1 @@
|
|||
../../basic-call/pages/api
|
||||
|
|
@ -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;
|
||||
|
|
@ -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 |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 50 KiB |
|
|
@ -1,76 +1,51 @@
|
|||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import ExpiryTimer from '@dailyjs/shared/components/ExpiryTimer';
|
||||
import { useCallState } from '@dailyjs/shared/contexts/CallProvider';
|
||||
import { useCallUI } from '@dailyjs/shared/hooks/useCallUI';
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import Room from '../Room';
|
||||
import { Asides } from './Asides';
|
||||
import { Modals } from './Modals';
|
||||
|
||||
export const App = () => {
|
||||
export const App = ({ customComponentForState }) => {
|
||||
const { roomExp, state } = useCallState();
|
||||
const [secs, setSecs] = useState();
|
||||
|
||||
// If room has an expiry time, we'll calculate how many seconds until expiry
|
||||
useEffect(() => {
|
||||
if (!roomExp) {
|
||||
return false;
|
||||
}
|
||||
const i = setInterval(() => {
|
||||
const timeLeft = Math.round((roomExp - Date.now()) / 1000);
|
||||
setSecs(`${Math.floor(timeLeft / 60)}:${`0${timeLeft % 60}`.slice(-2)}`);
|
||||
}, 1000);
|
||||
return () => clearInterval(i);
|
||||
}, [roomExp]);
|
||||
|
||||
const componentForState = useCallUI({
|
||||
state,
|
||||
room: () => <Room />,
|
||||
...customComponentForState,
|
||||
});
|
||||
|
||||
// Memoize children to avoid unnecassary renders from HOC
|
||||
const memoizedApp = useMemo(
|
||||
return useMemo(
|
||||
() => (
|
||||
<div className="app">
|
||||
{componentForState()}
|
||||
<Modals />
|
||||
<Asides />
|
||||
<style jsx>{`
|
||||
color: white;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
<>
|
||||
{roomExp && <ExpiryTimer expiry={roomExp} />}
|
||||
<div className="app">
|
||||
{componentForState()}
|
||||
<Modals />
|
||||
<Asides />
|
||||
<style jsx>{`
|
||||
color: white;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.loader {
|
||||
margin: 0 auto;
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
.loader {
|
||||
margin: 0 auto;
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
</>
|
||||
),
|
||||
[componentForState]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{roomExp && <div className="countdown">{secs}</div>} {memoizedApp}
|
||||
<style jsx>{`
|
||||
.countdown {
|
||||
position: fixed;
|
||||
top: 0px;
|
||||
right: 0px;
|
||||
width: 48px;
|
||||
text-align: center;
|
||||
padding: 4px 0;
|
||||
font-size: 0.875rem;
|
||||
font-weight: var(--weight-medium);
|
||||
border-radius: 0 0 0 var(--radius-sm);
|
||||
background: var(--blue-dark);
|
||||
color: white;
|
||||
z-index: 999;
|
||||
}
|
||||
`}</style>
|
||||
</>
|
||||
[componentForState, roomExp]
|
||||
);
|
||||
};
|
||||
|
||||
App.propTypes = {
|
||||
customComponentForState: PropTypes.any,
|
||||
};
|
||||
|
||||
export default App;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import React, { useMemo } from 'react';
|
||||
import HeaderCapsule from '@dailyjs/shared/components/HeaderCapsule';
|
||||
import { useParticipants } from '@dailyjs/shared/contexts/ParticipantsProvider';
|
||||
import { useUIState } from '@dailyjs/shared/contexts/UIStateProvider';
|
||||
|
||||
|
|
@ -9,18 +10,19 @@ export const Header = () => {
|
|||
return useMemo(
|
||||
() => (
|
||||
<header className="room-header">
|
||||
<img src="images/daily-logo.svg" alt="Daily" className="logo" />
|
||||
<div className="capsule">Basic call demo</div>
|
||||
<div className="capsule">
|
||||
<img src="assets/daily-logo.svg" alt="Daily" className="logo" />
|
||||
|
||||
<HeaderCapsule>Basic call demo</HeaderCapsule>
|
||||
<HeaderCapsule>
|
||||
{`${participantCount} ${
|
||||
participantCount > 1 ? 'participants' : 'participant'
|
||||
participantCount === 1 ? 'participant' : 'participants'
|
||||
}`}
|
||||
</div>
|
||||
</HeaderCapsule>
|
||||
{customCapsule && (
|
||||
<div className={`capsule ${customCapsule.variant}`}>
|
||||
<HeaderCapsule variant={customCapsule.variant}>
|
||||
{customCapsule.variant === 'recording' && <span />}
|
||||
{customCapsule.label}
|
||||
</div>
|
||||
</HeaderCapsule>
|
||||
)}
|
||||
|
||||
<style jsx>{`
|
||||
|
|
@ -37,44 +39,6 @@ export const Header = () => {
|
|||
.logo {
|
||||
margin-right: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.capsule {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-xxxs);
|
||||
background-color: var(--blue-dark);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: var(--spacing-xxs) var(--spacing-xs);
|
||||
box-sizing: border-box;
|
||||
line-height: 1;
|
||||
font-weight: var(--weight-medium);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.capsule.recording {
|
||||
background: var(--secondary-default);
|
||||
}
|
||||
|
||||
.capsule.recording span {
|
||||
display: block;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
animation: capsulePulse 2s infinite linear;
|
||||
}
|
||||
|
||||
@keyframes capsulePulse {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.25;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
</header>
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,64 +1,17 @@
|
|||
import React from 'react';
|
||||
import { Audio } from '@dailyjs/shared/components/Audio';
|
||||
import { BasicTray } from '@dailyjs/shared/components/Tray';
|
||||
import {
|
||||
WaitingRoomModal,
|
||||
WaitingRoomNotification,
|
||||
} from '@dailyjs/shared/components/WaitingRoom';
|
||||
import { useParticipants } from '@dailyjs/shared/contexts/ParticipantsProvider';
|
||||
import { useWaitingRoom } from '@dailyjs/shared/contexts/WaitingRoomProvider';
|
||||
import useJoinSound from '@dailyjs/shared/hooks/useJoinSound';
|
||||
import VideoContainer from '@dailyjs/shared/components/VideoContainer/VideoContainer';
|
||||
|
||||
import { VideoGrid } from '../VideoGrid';
|
||||
import { Header } from './Header';
|
||||
import { RoomContainer } from './RoomContainer';
|
||||
|
||||
export const Room = () => {
|
||||
const { setShowModal, showModal } = useWaitingRoom();
|
||||
const { localParticipant } = useParticipants();
|
||||
|
||||
useJoinSound();
|
||||
|
||||
return (
|
||||
<div className="room">
|
||||
<Header />
|
||||
|
||||
<main>
|
||||
<VideoGrid />
|
||||
</main>
|
||||
|
||||
{/* Show waiting room notification & modal if call owner */}
|
||||
{localParticipant?.isOwner && (
|
||||
<>
|
||||
<WaitingRoomNotification />
|
||||
{showModal && (
|
||||
<WaitingRoomModal onClose={() => setShowModal(false)} />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<BasicTray />
|
||||
<Audio />
|
||||
|
||||
<style jsx>{`
|
||||
.room {
|
||||
flex-flow: column nowrap;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
main {
|
||||
flex: 1 1 auto;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
min-height: 0px;
|
||||
height: 100%;
|
||||
padding: var(--spacing-xxxs);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export const Room = () => (
|
||||
<RoomContainer>
|
||||
<Header />
|
||||
<VideoContainer>
|
||||
<VideoGrid />
|
||||
</VideoContainer>
|
||||
</RoomContainer>
|
||||
);
|
||||
|
||||
export default Room;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,49 @@
|
|||
import React, { useMemo } from 'react';
|
||||
import { Audio } from '@dailyjs/shared/components/Audio';
|
||||
import { BasicTray } from '@dailyjs/shared/components/Tray';
|
||||
import { useParticipants } from '@dailyjs/shared/contexts/ParticipantsProvider';
|
||||
import useJoinSound from '@dailyjs/shared/hooks/useJoinSound';
|
||||
import PropTypes from 'prop-types';
|
||||
import WaitingRoom from '../WaitingRoom';
|
||||
|
||||
export const RoomContainer = ({ children }) => {
|
||||
const { isOwner } = useParticipants();
|
||||
|
||||
useJoinSound();
|
||||
|
||||
const roomComponents = useMemo(
|
||||
() => (
|
||||
<>
|
||||
{/* Show waiting room notification & modal if call owner */}
|
||||
{isOwner && <WaitingRoom />}
|
||||
{/* Tray buttons */}
|
||||
<BasicTray />
|
||||
{/* Audio tags */}
|
||||
<Audio />
|
||||
</>
|
||||
),
|
||||
[isOwner]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="room">
|
||||
{children}
|
||||
{roomComponents}
|
||||
|
||||
<style jsx>{`
|
||||
.room {
|
||||
flex-flow: column nowrap;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
RoomContainer.propTypes = {
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
export default RoomContainer;
|
||||
|
|
@ -1 +1,2 @@
|
|||
export { Room as default } from './Room';
|
||||
export { RoomContainer } from './RoomContainer';
|
||||
|
|
|
|||
|
|
@ -2,19 +2,29 @@ import React, { useState, useMemo, useEffect, useRef } from 'react';
|
|||
import Tile from '@dailyjs/shared/components/Tile';
|
||||
import { DEFAULT_ASPECT_RATIO } from '@dailyjs/shared/constants';
|
||||
import { useParticipants } from '@dailyjs/shared/contexts/ParticipantsProvider';
|
||||
import { useTracks } from '@dailyjs/shared/contexts/TracksProvider';
|
||||
import { useDeepCompareMemo } from 'use-deep-compare';
|
||||
|
||||
/**
|
||||
* Basic unpaginated video tile grid, scaled by aspect ratio
|
||||
*
|
||||
* Note: this component is designed to work with automated track subscriptions
|
||||
* and is only suitable for small call sizes as it will show all participants
|
||||
* and not paginate.
|
||||
*
|
||||
* Note: this grid does not show screenshares (just participant cams)
|
||||
*
|
||||
* Note: this grid does not sort participants
|
||||
*/
|
||||
export const VideoGrid = React.memo(
|
||||
() => {
|
||||
const containerRef = useRef();
|
||||
const { allParticipants } = useParticipants();
|
||||
const { resumeVideoTrack } = useTracks();
|
||||
const { participants } = useParticipants();
|
||||
const [dimensions, setDimensions] = useState({
|
||||
width: 1,
|
||||
height: 1,
|
||||
});
|
||||
|
||||
// Keep a reference to the width and height of the page, so we can repack
|
||||
useEffect(() => {
|
||||
let frame;
|
||||
const handleResize = () => {
|
||||
|
|
@ -35,9 +45,10 @@ export const VideoGrid = React.memo(
|
|||
};
|
||||
}, []);
|
||||
|
||||
// Basic brute-force packing algo
|
||||
const layout = useMemo(() => {
|
||||
const aspectRatio = DEFAULT_ASPECT_RATIO;
|
||||
const tileCount = allParticipants.length || 0;
|
||||
const tileCount = participants.length || 0;
|
||||
const w = dimensions.width;
|
||||
const h = dimensions.height;
|
||||
|
||||
|
|
@ -76,11 +87,12 @@ export const VideoGrid = React.memo(
|
|||
}
|
||||
|
||||
return bestLayout;
|
||||
}, [dimensions, allParticipants]);
|
||||
}, [dimensions, participants]);
|
||||
|
||||
// Memoize our tile list to avoid unnecassary re-renders
|
||||
const tiles = useDeepCompareMemo(
|
||||
() =>
|
||||
allParticipants.map((p) => (
|
||||
participants.map((p) => (
|
||||
<Tile
|
||||
participant={p}
|
||||
key={p.id}
|
||||
|
|
@ -88,45 +100,10 @@ export const VideoGrid = React.memo(
|
|||
style={{ maxWidth: layout.width, maxHeight: layout.height }}
|
||||
/>
|
||||
)),
|
||||
[layout, allParticipants]
|
||||
[layout, participants]
|
||||
);
|
||||
|
||||
/**
|
||||
* Set bandwidth layer based on amount of visible participants
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (
|
||||
typeof rtcpeers === 'undefined' ||
|
||||
// eslint-disable-next-line no-undef
|
||||
rtcpeers?.getCurrentType() !== 'sfu'
|
||||
)
|
||||
return;
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
const sfu = rtcpeers.soup;
|
||||
const count = allParticipants.length;
|
||||
|
||||
allParticipants.forEach(({ id }) => {
|
||||
if (count < 5) {
|
||||
// High quality video for calls with < 5 people per page
|
||||
sfu.setPreferredLayerForTrack(id, 'cam-video', 2);
|
||||
} else if (count < 10) {
|
||||
// Medium quality video for calls with < 10 people per page
|
||||
sfu.setPreferredLayerForTrack(id, 'cam-video', 1);
|
||||
} else {
|
||||
// Low quality video for calls with 10 or more people per page
|
||||
sfu.setPreferredLayerForTrack(id, 'cam-video', 0);
|
||||
}
|
||||
});
|
||||
}, [allParticipants]);
|
||||
|
||||
useEffect(() => {
|
||||
allParticipants.forEach(
|
||||
(p) => p.id !== 'local' && resumeVideoTrack(p.id)
|
||||
);
|
||||
}, [allParticipants, resumeVideoTrack]);
|
||||
|
||||
if (!allParticipants.length) {
|
||||
if (!participants.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
import React from 'react';
|
||||
import {
|
||||
WaitingRoomModal,
|
||||
WaitingRoomNotification,
|
||||
} from '@dailyjs/shared/components/WaitingRoom';
|
||||
import { useWaitingRoom } from '@dailyjs/shared/contexts/WaitingRoomProvider';
|
||||
|
||||
export const WaitingRoom = () => {
|
||||
const { setShowModal, showModal } = useWaitingRoom();
|
||||
return (
|
||||
<>
|
||||
<WaitingRoomNotification />
|
||||
{showModal && <WaitingRoomModal onClose={() => setShowModal(false)} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default WaitingRoom;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { WaitingRoom as default } from './WaitingRoom';
|
||||
|
|
@ -20,8 +20,5 @@
|
|||
"babel-plugin-module-resolver": "^4.1.0",
|
||||
"next-compose-plugins": "^2.2.1",
|
||||
"next-transpile-modules": "^8.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.12"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
export default async function handler(req, res) {
|
||||
const { privacy, expiryMinutes } = req.body;
|
||||
|
||||
if (req.method === 'POST') {
|
||||
console.log(`Creating room on domain ${process.env.DAILY_DOMAIN}`);
|
||||
|
||||
|
|
@ -9,9 +11,12 @@ export default async function handler(req, res) {
|
|||
Authorization: `Bearer ${process.env.DAILY_API_KEY}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
privacy: privacy || 'public',
|
||||
properties: {
|
||||
exp: Math.round(Date.now() / 1000) + 5 * 60, // expire in 5 minutes
|
||||
exp: Math.round(Date.now() / 1000) + (expiryMinutes || 5) * 60, // expire in x minutes
|
||||
eject_at_room_exp: true,
|
||||
enable_knocking: privacy !== 'public',
|
||||
enable_recording: 'local',
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ export default function Index({
|
|||
predefinedRoom = '',
|
||||
forceFetchToken = false,
|
||||
forceOwner = false,
|
||||
subscribeToTracksAutomatically = true,
|
||||
demoMode = false,
|
||||
asides,
|
||||
modals,
|
||||
|
|
@ -114,7 +115,12 @@ export default function Index({
|
|||
modals={modals}
|
||||
customTrayComponent={customTrayComponent}
|
||||
>
|
||||
<CallProvider domain={domain} room={roomName} token={token}>
|
||||
<CallProvider
|
||||
domain={domain}
|
||||
room={roomName}
|
||||
token={token}
|
||||
subscribeToTracksAutomatically={subscribeToTracksAutomatically}
|
||||
>
|
||||
<ParticipantsProvider>
|
||||
<TracksProvider>
|
||||
<MediaDeviceProvider>
|
||||
|
|
@ -139,12 +145,12 @@ Index.propTypes = {
|
|||
customAppComponent: PropTypes.node,
|
||||
forceFetchToken: PropTypes.bool,
|
||||
forceOwner: PropTypes.bool,
|
||||
subscribeToTracksAutomatically: PropTypes.bool,
|
||||
demoMode: PropTypes.bool,
|
||||
};
|
||||
|
||||
export async function getStaticProps() {
|
||||
const defaultProps = getDemoProps();
|
||||
|
||||
return {
|
||||
props: defaultProps,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 |
|
|
@ -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 |
|
After Width: | Height: | Size: 50 KiB |
|
|
@ -81,7 +81,7 @@ export const FlyingEmojisOverlay = () => {
|
|||
// Remove all event listeners on unmount to prevent console warnings
|
||||
useEffect(
|
||||
() => () =>
|
||||
overlayRef.current.childNodes.forEach((n) =>
|
||||
overlayRef.current?.childNodes.forEach((n) =>
|
||||
n.removeEventListener('animationend', handleRemoveFlyingEmoji)
|
||||
),
|
||||
[handleRemoveFlyingEmoji]
|
||||
|
|
@ -94,8 +94,8 @@ export const FlyingEmojisOverlay = () => {
|
|||
position: fixed;
|
||||
top: 0px;
|
||||
bottom: 0px;
|
||||
left: 24px;
|
||||
right: 24px;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
// Note: I am here because next-transpile-modules requires a mainfile
|
||||
|
|
@ -1 +0,0 @@
|
|||
../basic-call/public
|
||||
|
|
@ -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 |
|
|
@ -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 |
|
After Width: | Height: | Size: 50 KiB |
|
|
@ -12,15 +12,13 @@ export const Tray = () => {
|
|||
const { isStreaming } = useLiveStreaming();
|
||||
|
||||
return (
|
||||
<>
|
||||
<TrayButton
|
||||
label={isStreaming ? 'Live' : 'Stream'}
|
||||
orange={isStreaming}
|
||||
onClick={() => openModal(LIVE_STREAMING_MODAL)}
|
||||
>
|
||||
<IconStream />
|
||||
</TrayButton>
|
||||
</>
|
||||
<TrayButton
|
||||
label={isStreaming ? 'Live' : 'Stream'}
|
||||
orange={isStreaming}
|
||||
onClick={() => openModal(LIVE_STREAMING_MODAL)}
|
||||
>
|
||||
<IconStream />
|
||||
</TrayButton>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
// Note: I am here because next-transpile-modules requires a mainfile
|
||||
|
|
@ -1 +0,0 @@
|
|||
../basic-call/public
|
||||
|
|
@ -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 |
|
|
@ -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 |
|
After Width: | Height: | Size: 50 KiB |
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"presets": ["next/babel"],
|
||||
"plugins": ["inline-react-svg"]
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
# Pagination, Sorting & Track Management
|
||||
|
||||

|
||||
|
||||
### Live example
|
||||
|
||||
**[See it in action here ➡️](https://dailyjs-pagination.vercel.app)**
|
||||
|
||||
---
|
||||
|
||||
## What does this demo do?
|
||||
|
||||
- Switches to [manual track subscriptions](https://docs.daily.co/reference#%EF%B8%8F-setsubscribetotracksautomatically) to pause / resume video tracks as they are paged in and out of view
|
||||
- Introduces a new video grid component that manages pagination and sorts participant tiles based on their active speaker status
|
||||
|
||||
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/live-streaming dev
|
||||
```
|
||||
|
||||
Note: this example uses an additional env `MANUAL_TRACK_SUBS=1` that will disable [automatic track management](https://docs.daily.co/reference#%EF%B8%8F-setsubscribetotracksautomatically).
|
||||
|
||||
## How does this example work?
|
||||
|
||||
When call sizes exceed a certain volume (~12 or more participants) it's important to start optimising for both bandwidth and CPU. Using manual track subscriptions allows each client to specify which participants they want to receive video and/or audio from, reducing how much data needs to be downloaded as well as the number of connections our servers maintain (subsequently supporting increased participant counts.)
|
||||
|
||||
This demo introduces a new paginated grid component that subscribes to any tiles that are in view. Our subscription API allows for the subscribing, pausing, resuming and unsubscribing of tracks. The grid component will:
|
||||
|
||||
1. Subscribe to all participants on the current and adjacent pages.
|
||||
2. Pause participants video if they are not in view (i.e. on the current page.) Pausing is optimal over unsubscribing in this particular use case since unsubscribing a track results in a full teardown of the data stream. Re-subscribing to a track is perceivably slower than pausing and resuming.
|
||||
3. Play / resume participant's video when they are on the current page.
|
||||
4. Unsubscribe from a participant's video if they are not on an adjacent page (explained below.)
|
||||
|
||||
When you pause a track, you are keeping the connection for that track open and connected to the SFU but stopping any bytes from flowing across that connection. Therefore, this simple approach of pausing a track when it is offscreen rather than completely unsubscribing (and tearing down that connection) speeds up the process of showing/hiding participant videos while also cutting out the processing and bandwidth required for those tracks.
|
||||
|
||||
It is important to note that a subscription and the underlying connections it entails does still result in some overhead and this approach breaks down once you get to even larger calls (e.g. ~50 or more depending on device, bandwidth, geolocation etc). In those scenarios it is best to take advantage of both pausing and unsubscribing to maximize both quickly showing videos and minimizing connections/processing/cpu. This example showcases how to do this by subscribing to the current page's videos (all videos resumed) as well as the adjacent pages' videos (all videos paused) and unsubscribing to any other pages' videos. This has the affect of minimizing the overall number of subscriptions while still having any video which may be displayed shortly, subscribed with their connections ready to be resumed as soon as the user pages over.
|
||||
|
||||
## Deploy your own on Vercel
|
||||
|
||||
[](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)
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
import React from 'react';
|
||||
|
||||
import App from '@dailyjs/basic-call/components/App';
|
||||
import Room from '@dailyjs/basic-call/components/Room';
|
||||
import PaginatedVideoGrid from '../PaginatedVideoGrid';
|
||||
|
||||
export const AppWithPagination = () => (
|
||||
<App
|
||||
customComponentForState={{
|
||||
room: () => <Room MainComponent={PaginatedVideoGrid} />,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
export default AppWithPagination;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { AppWithPagination as default } from './App';
|
||||
|
|
@ -0,0 +1,367 @@
|
|||
import React, {
|
||||
useRef,
|
||||
useCallback,
|
||||
useMemo,
|
||||
useEffect,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { Button } from '@dailyjs/shared/components/Button';
|
||||
import Tile from '@dailyjs/shared/components/Tile';
|
||||
import {
|
||||
DEFAULT_ASPECT_RATIO,
|
||||
MEETING_STATE_JOINED,
|
||||
} from '@dailyjs/shared/constants';
|
||||
import { useCallState } from '@dailyjs/shared/contexts/CallProvider';
|
||||
import { useParticipants } from '@dailyjs/shared/contexts/ParticipantsProvider';
|
||||
import { isLocalId } from '@dailyjs/shared/contexts/participantsState';
|
||||
import { useActiveSpeaker } from '@dailyjs/shared/hooks/useActiveSpeaker';
|
||||
import { useCamSubscriptions } from '@dailyjs/shared/hooks/useCamSubscriptions';
|
||||
import { ReactComponent as IconArrow } from '@dailyjs/shared/icons/raquo-md.svg';
|
||||
import sortByKey from '@dailyjs/shared/lib/sortByKey';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useDeepCompareMemo } from 'use-deep-compare';
|
||||
|
||||
// --- Constants
|
||||
|
||||
const MIN_TILE_WIDTH = 280;
|
||||
const MAX_TILES_PER_PAGE = 12;
|
||||
|
||||
export const PaginatedVideoGrid = ({
|
||||
maxTilesPerPage = MAX_TILES_PER_PAGE,
|
||||
}) => {
|
||||
const { callObject } = useCallState();
|
||||
const {
|
||||
activeParticipant,
|
||||
participantCount,
|
||||
participants,
|
||||
swapParticipantPosition,
|
||||
} = useParticipants();
|
||||
const activeSpeakerId = useActiveSpeaker();
|
||||
|
||||
// Memoized participant count (does not include screen shares)
|
||||
const displayableParticipantCount = useMemo(
|
||||
() => participantCount,
|
||||
[participantCount]
|
||||
);
|
||||
|
||||
// Grid size (dictated by screen size)
|
||||
const [dimensions, setDimensions] = useState({
|
||||
width: 1,
|
||||
height: 1,
|
||||
});
|
||||
const [page, setPage] = useState(1);
|
||||
const [pages, setPages] = useState(1);
|
||||
|
||||
const gridRef = useRef(null);
|
||||
|
||||
// -- Layout / UI
|
||||
|
||||
// Update width and height of grid when window is resized
|
||||
useEffect(() => {
|
||||
let frame;
|
||||
const handleResize = () => {
|
||||
if (frame) cancelAnimationFrame(frame);
|
||||
frame = requestAnimationFrame(() => {
|
||||
const width = gridRef.current?.clientWidth;
|
||||
const height = gridRef.current?.clientHeight;
|
||||
setDimensions({ width, height });
|
||||
});
|
||||
};
|
||||
handleResize();
|
||||
window.addEventListener('resize', handleResize);
|
||||
window.addEventListener('orientationchange', handleResize);
|
||||
return () => {
|
||||
window.removeEventListener('resize', handleResize);
|
||||
window.removeEventListener('orientationchange', handleResize);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Memoized reference to the max columns and rows possible given screen size
|
||||
const [maxColumns, maxRows] = useMemo(() => {
|
||||
const { width, height } = dimensions;
|
||||
const columns = Math.max(1, Math.floor(width / MIN_TILE_WIDTH));
|
||||
const widthPerTile = width / columns;
|
||||
const rows = Math.max(1, Math.floor(height / (widthPerTile * (9 / 16))));
|
||||
return [columns, rows];
|
||||
}, [dimensions]);
|
||||
|
||||
// Memoized count of how many tiles can we show per page
|
||||
const pageSize = useMemo(
|
||||
() => Math.min(maxColumns * maxRows, maxTilesPerPage),
|
||||
[maxColumns, maxRows, maxTilesPerPage]
|
||||
);
|
||||
|
||||
// Calc and set the total number of pages as participant count mutates
|
||||
useEffect(() => {
|
||||
setPages(Math.ceil(displayableParticipantCount / pageSize));
|
||||
}, [pageSize, displayableParticipantCount]);
|
||||
|
||||
// Make sure we never see a blank page (if we're on the last page and people leave)
|
||||
useEffect(() => {
|
||||
if (page <= pages) return;
|
||||
setPage(pages);
|
||||
}, [page, pages]);
|
||||
|
||||
// Brutishly calculate the dimensions of each tile given the size of the grid
|
||||
const [tileWidth, tileHeight] = useMemo(() => {
|
||||
const { width, height } = dimensions;
|
||||
const n = Math.min(pageSize, displayableParticipantCount);
|
||||
if (n === 0) return [width, height];
|
||||
const dims = [];
|
||||
for (let i = 1; i <= n; i += 1) {
|
||||
let maxWidthPerTile = (width - (i - 1)) / i;
|
||||
let maxHeightPerTile = maxWidthPerTile / DEFAULT_ASPECT_RATIO;
|
||||
const rows = Math.ceil(n / i);
|
||||
if (rows * maxHeightPerTile > height) {
|
||||
maxHeightPerTile = (height - (rows - 1)) / rows;
|
||||
maxWidthPerTile = maxHeightPerTile * DEFAULT_ASPECT_RATIO;
|
||||
dims.push([maxWidthPerTile, maxHeightPerTile]);
|
||||
} else {
|
||||
dims.push([maxWidthPerTile, maxHeightPerTile]);
|
||||
}
|
||||
}
|
||||
return dims.reduce(
|
||||
([rw, rh], [w, h]) => {
|
||||
if (w * h < rw * rh) return [rw, rh];
|
||||
return [w, h];
|
||||
},
|
||||
[0, 0]
|
||||
);
|
||||
}, [dimensions, pageSize, displayableParticipantCount]);
|
||||
|
||||
// -- Track subscriptions
|
||||
|
||||
// Memoized array of participants on the current page (those we can see)
|
||||
const visibleParticipants = useMemo(
|
||||
() =>
|
||||
participants.length - page * pageSize > 0
|
||||
? participants.slice((page - 1) * pageSize, page * pageSize)
|
||||
: participants.slice(-pageSize),
|
||||
[page, pageSize, participants]
|
||||
);
|
||||
|
||||
/**
|
||||
* Play / pause tracks based on pagination
|
||||
* Note: we pause adjacent page tracks and unsubscribe from everything else
|
||||
*/
|
||||
const camSubscriptions = useMemo(() => {
|
||||
const maxSubs = 3 * pageSize;
|
||||
|
||||
// Determine participant ids to subscribe to or stage, based on page
|
||||
let renderedOrBufferedIds = [];
|
||||
switch (page) {
|
||||
// First page
|
||||
case 1:
|
||||
renderedOrBufferedIds = participants
|
||||
.slice(0, Math.min(maxSubs, 2 * pageSize))
|
||||
.map((p) => p.id);
|
||||
break;
|
||||
// Last page
|
||||
case Math.ceil(participants.length / pageSize):
|
||||
renderedOrBufferedIds = participants
|
||||
.slice(-Math.min(maxSubs, 2 * pageSize))
|
||||
.map((p) => p.id);
|
||||
break;
|
||||
// Any other page
|
||||
default:
|
||||
{
|
||||
const buffer = (maxSubs - pageSize) / 2;
|
||||
const min = (page - 1) * pageSize - buffer;
|
||||
const max = page * pageSize + buffer;
|
||||
renderedOrBufferedIds = participants.slice(min, max).map((p) => p.id);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
const subscribedIds = [];
|
||||
const stagedIds = [];
|
||||
|
||||
// Decide whether to subscribe to or stage participants'
|
||||
// track based on visibility
|
||||
renderedOrBufferedIds.forEach((id) => {
|
||||
if (id !== isLocalId()) {
|
||||
if (visibleParticipants.some((vp) => vp.id === id)) {
|
||||
subscribedIds.push(id);
|
||||
} else {
|
||||
stagedIds.push(id);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
subscribedIds,
|
||||
stagedIds,
|
||||
};
|
||||
}, [page, pageSize, participants, visibleParticipants]);
|
||||
|
||||
useCamSubscriptions(
|
||||
camSubscriptions?.subscribedIds,
|
||||
camSubscriptions?.pausedIds
|
||||
);
|
||||
|
||||
/**
|
||||
* Set bandwidth layer based on amount of visible participants
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (!(callObject && callObject.meetingState() === MEETING_STATE_JOINED))
|
||||
return;
|
||||
const count = visibleParticipants.length;
|
||||
|
||||
let layer;
|
||||
if (count < 5) {
|
||||
// highest quality layer
|
||||
layer = 2;
|
||||
} else if (count < 10) {
|
||||
// mid quality layer
|
||||
layer = 1;
|
||||
} else {
|
||||
// low qualtiy layer
|
||||
layer = 0;
|
||||
}
|
||||
|
||||
const receiveSettings = visibleParticipants.reduce(
|
||||
(settings, participant) => {
|
||||
if (isLocalId(participant.id)) return settings;
|
||||
return { ...settings, [participant.id]: { video: { layer } } };
|
||||
},
|
||||
{}
|
||||
);
|
||||
callObject.updateReceiveSettings(receiveSettings);
|
||||
}, [visibleParticipants, callObject]);
|
||||
|
||||
// -- Active speaker
|
||||
|
||||
/**
|
||||
* Handle position updates based on active speaker events
|
||||
*/
|
||||
const handleActiveSpeakerChange = useCallback(
|
||||
(peerId) => {
|
||||
if (!peerId) return;
|
||||
// active participant is already visible
|
||||
if (visibleParticipants.some(({ id }) => id === peerId)) return;
|
||||
// ignore repositioning when viewing page > 1
|
||||
if (page > 1) return;
|
||||
|
||||
/**
|
||||
* We can now assume that
|
||||
* a) the user is looking at page 1
|
||||
* b) the most recent active participant is not visible on page 1
|
||||
* c) we'll have to promote the most recent participant's position to page 1
|
||||
*
|
||||
* To achieve that, we'll have to
|
||||
* - find the least recent active participant on page 1
|
||||
* - swap least & most recent active participant's position via setParticipantPosition
|
||||
*/
|
||||
const sortedVisibleRemoteParticipants = visibleParticipants
|
||||
.filter(({ isLocal }) => !isLocal)
|
||||
.sort((a, b) => sortByKey(a, b, 'lastActiveDate'));
|
||||
|
||||
if (!sortedVisibleRemoteParticipants.length) return;
|
||||
|
||||
swapParticipantPosition(sortedVisibleRemoteParticipants[0].id, peerId);
|
||||
},
|
||||
[page, swapParticipantPosition, visibleParticipants]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (page > 1 || !activeSpeakerId) return;
|
||||
handleActiveSpeakerChange(activeSpeakerId);
|
||||
}, [activeSpeakerId, handleActiveSpeakerChange, page]);
|
||||
|
||||
const tiles = useDeepCompareMemo(
|
||||
() =>
|
||||
visibleParticipants.map((p) => (
|
||||
<Tile
|
||||
participant={p}
|
||||
mirrored
|
||||
key={p.id}
|
||||
style={{
|
||||
maxHeight: tileHeight,
|
||||
maxWidth: tileWidth,
|
||||
}}
|
||||
/>
|
||||
)),
|
||||
[
|
||||
activeParticipant,
|
||||
participantCount,
|
||||
tileWidth,
|
||||
tileHeight,
|
||||
visibleParticipants,
|
||||
]
|
||||
);
|
||||
|
||||
const handlePrevClick = () => setPage((p) => p - 1);
|
||||
const handleNextClick = () => setPage((p) => p + 1);
|
||||
|
||||
return (
|
||||
<div ref={gridRef} className="grid">
|
||||
<Button
|
||||
className="page-button prev"
|
||||
disabled={!(pages > 1 && page > 1)}
|
||||
type="button"
|
||||
onClick={handlePrevClick}
|
||||
>
|
||||
<IconArrow />
|
||||
</Button>
|
||||
|
||||
<div className="tiles">{tiles}</div>
|
||||
|
||||
<Button
|
||||
className="page-button next"
|
||||
disabled={!(pages > 1 && page < pages)}
|
||||
type="button"
|
||||
onClick={handleNextClick}
|
||||
>
|
||||
<IconArrow />
|
||||
</Button>
|
||||
|
||||
<style jsx>{`
|
||||
.grid {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.grid .tiles {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
gap: 1px;
|
||||
max-height: 100%;
|
||||
justify-content: center;
|
||||
margin: auto;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.grid :global(.page-button) {
|
||||
border-radius: var(--radius-sm) 0 0 var(--radius-sm);
|
||||
height: 84px;
|
||||
padding: 0px var(--spacing-xxxs) 0px var(--spacing-xxs);
|
||||
background-color: var(--blue-default);
|
||||
color: white;
|
||||
border-right: 0px;
|
||||
}
|
||||
|
||||
.grid :global(.page-button):disabled {
|
||||
color: var(--blue-dark);
|
||||
background-color: var(--blue-light);
|
||||
border-color: var(--blue-light);
|
||||
}
|
||||
|
||||
.grid :global(.page-button.prev) {
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
PaginatedVideoGrid.propTypes = {
|
||||
maxTilesPerPage: PropTypes.number,
|
||||
};
|
||||
|
||||
export default PaginatedVideoGrid;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { PaginatedVideoGrid as default } from './PaginatedVideoGrid';
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
import React from 'react';
|
||||
|
||||
import { TrayButton } from '@dailyjs/shared/components/Tray';
|
||||
import { useCallState } from '@dailyjs/shared/contexts/CallProvider';
|
||||
import { ReactComponent as IconAdd } from '@dailyjs/shared/icons/add-md.svg';
|
||||
|
||||
export const Tray = () => {
|
||||
const { callObject } = useCallState();
|
||||
|
||||
return (
|
||||
<>
|
||||
<TrayButton
|
||||
label="Add Fake"
|
||||
onClick={() => {
|
||||
callObject.addFakeParticipant();
|
||||
}}
|
||||
>
|
||||
<IconAdd />
|
||||
</TrayButton>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Tray;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { Tray as default } from './Tray';
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
DAILY_DOMAIN=
|
||||
DAILY_API_KEY=
|
||||
DAILY_REST_DOMAIN=https://api.daily.co/v1
|
||||
DAILY_ROOM=
|
||||
MANUAL_TRACK_SUBS=1
|
||||
|
After Width: | Height: | Size: 714 KiB |
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"name": "@dailyjs/pagination",
|
||||
"description": "Basic Call + Pagination",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dailyjs/shared": "*",
|
||||
"@dailyjs/basic-call": "*",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
import React from 'react';
|
||||
|
||||
import App from '@dailyjs/basic-call/pages/_app';
|
||||
import AppWithPagination from '../components/App';
|
||||
|
||||
import Tray from '../components/Tray';
|
||||
|
||||
App.customTrayComponent = <Tray />;
|
||||
App.customAppComponent = <AppWithPagination />;
|
||||
|
||||
export default App;
|
||||
|
|
@ -0,0 +1 @@
|
|||
../../basic-call/pages/api
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
import Index from '@dailyjs/basic-call/pages';
|
||||
import getDemoProps from '@dailyjs/shared/lib/demoProps';
|
||||
|
||||
export async function getStaticProps() {
|
||||
const defaultProps = getDemoProps();
|
||||
|
||||
return {
|
||||
props: {
|
||||
...defaultProps,
|
||||
forceFetchToken: true,
|
||||
forceOwner: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default Index;
|
||||
|
|
@ -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 |
|
|
@ -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 |
|
After Width: | Height: | Size: 50 KiB |
|
|
@ -32,15 +32,13 @@ export const Tray = () => {
|
|||
].includes(recordingState);
|
||||
|
||||
return (
|
||||
<>
|
||||
<TrayButton
|
||||
label={isRecording ? 'Recording' : 'Record'}
|
||||
orange={isRecording}
|
||||
onClick={() => openModal(RECORDING_MODAL)}
|
||||
>
|
||||
<IconRecord />
|
||||
</TrayButton>
|
||||
</>
|
||||
<TrayButton
|
||||
label={isRecording ? 'Recording' : 'Record'}
|
||||
orange={isRecording}
|
||||
onClick={() => openModal(RECORDING_MODAL)}
|
||||
>
|
||||
<IconRecord />
|
||||
</TrayButton>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
// Note: I am here because next-transpile-modules requires a mainfile
|
||||
|
|
@ -1 +0,0 @@
|
|||
../basic-call/public
|
||||
|
|
@ -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 |
|
|
@ -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 |
|
After Width: | Height: | Size: 50 KiB |
|
|
@ -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 |
|
|
@ -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 |
|
After Width: | Height: | Size: 50 KiB |
|
|
@ -11,7 +11,7 @@ export const Aside = ({ onClose, children }) => (
|
|||
<div className="close">
|
||||
<Button
|
||||
size="small-square"
|
||||
variant="dark"
|
||||
variant="white"
|
||||
className="closeButton"
|
||||
onClick={onClose}
|
||||
>
|
||||
|
|
@ -42,9 +42,15 @@ export const Aside = ({ onClose, children }) => (
|
|||
.call-aside .close {
|
||||
position: absolute;
|
||||
top: var(--spacing-xxs);
|
||||
left: calc(-48px - var(--spacing-xxs));
|
||||
left: calc(-43px);
|
||||
border-right: 1px solid var(--gray-wash);
|
||||
z-index: 99;
|
||||
}
|
||||
|
||||
.call-aside :global(.closeButton) {
|
||||
border-radius: var(--radius-sm) 0 0 var(--radius-sm);
|
||||
height: 48px;
|
||||
}
|
||||
`}</style>
|
||||
</aside>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ const PersonRow = ({ participant, isOwner = false }) => (
|
|||
margin-bottom: var(--spacing-xxxs);
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.person-row .name {
|
||||
|
|
@ -104,49 +105,57 @@ export const PeopleAside = () => {
|
|||
|
||||
return (
|
||||
<Aside onClose={() => setShowAside(false)}>
|
||||
{isOwner && (
|
||||
<div className="owner-actions">
|
||||
<Button
|
||||
fullWidth
|
||||
size="tiny"
|
||||
variant="outline-gray"
|
||||
onClick={() =>
|
||||
callObject.updateParticipants({ '*': { setAudio: false } })
|
||||
}
|
||||
>
|
||||
Mute all mics
|
||||
</Button>
|
||||
<Button
|
||||
fullWidth
|
||||
size="tiny"
|
||||
variant="outline-gray"
|
||||
onClick={() =>
|
||||
callObject.updateParticipants({ '*': { setVideo: false } })
|
||||
}
|
||||
>
|
||||
Mute all cams
|
||||
</Button>
|
||||
<div className="people-aside">
|
||||
{isOwner && (
|
||||
<div className="owner-actions">
|
||||
<Button
|
||||
fullWidth
|
||||
size="tiny"
|
||||
variant="outline-gray"
|
||||
onClick={() =>
|
||||
callObject.updateParticipants({ '*': { setAudio: false } })
|
||||
}
|
||||
>
|
||||
Mute all mics
|
||||
</Button>
|
||||
<Button
|
||||
fullWidth
|
||||
size="tiny"
|
||||
variant="outline-gray"
|
||||
onClick={() =>
|
||||
callObject.updateParticipants({ '*': { setVideo: false } })
|
||||
}
|
||||
>
|
||||
Mute all cams
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<div className="rows">
|
||||
{allParticipants.map((p) => (
|
||||
<PersonRow participant={p} key={p.id} isOwner={isOwner} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className="rows">
|
||||
{allParticipants.map((p) => (
|
||||
<PersonRow participant={p} key={p.id} isOwner={isOwner} />
|
||||
))}
|
||||
</div>
|
||||
<style jsx>
|
||||
{`
|
||||
.owner-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-xxxs);
|
||||
margin: var(--spacing-xs) var(--spacing-xxs);
|
||||
}
|
||||
<style jsx>
|
||||
{`
|
||||
.people-aside {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.rows {
|
||||
margin: var(--spacing-xxs);
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
.owner-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-xxxs);
|
||||
margin: var(--spacing-xs) var(--spacing-xxs);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.rows {
|
||||
margin: var(--spacing-xxs);
|
||||
flex: 1;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</div>
|
||||
</Aside>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,79 +1,76 @@
|
|||
/**
|
||||
* Audio
|
||||
* ---
|
||||
* Renders audio tags for each audible participant / screen share in the call
|
||||
* Note: it's very important to minimise DOM mutates for audio components
|
||||
* as iOS / Safari do a lot of browser 'magic' that may result in muted
|
||||
* tracks. We heavily memoize this component to avoid unnecassary re-renders.
|
||||
* When working with audio elements it's very important to avoid mutating
|
||||
* the DOM elements as much as possible to avoid audio pops and crackles.
|
||||
* This component addresses to known browser quirks; Safari autoplay
|
||||
* and Chrome's maximum media elements. On Chrome we add all audio tracks
|
||||
* into into a single audio node using the CombinedAudioTrack component
|
||||
*/
|
||||
import React, { useRef, useEffect } from 'react';
|
||||
import { useParticipants } from '@dailyjs/shared/contexts/ParticipantsProvider';
|
||||
import useAudioTrack from '@dailyjs/shared/hooks/useAudioTrack';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import { useTracks } from '@dailyjs/shared/contexts/TracksProvider';
|
||||
import Bowser from 'bowser';
|
||||
import { Portal } from 'react-portal';
|
||||
import AudioTrack from './AudioTrack';
|
||||
import CombinedAudioTrack from './CombinedAudioTrack';
|
||||
|
||||
const AudioItem = React.memo(
|
||||
({ participant }) => {
|
||||
const audioRef = useRef(null);
|
||||
const audioTrack = useAudioTrack(participant);
|
||||
export const Audio = () => {
|
||||
const { audioTracks } = useTracks();
|
||||
|
||||
useEffect(() => {
|
||||
if (!audioTrack || !audioRef.current) return;
|
||||
const renderedTracks = useMemo(
|
||||
() =>
|
||||
Object.entries(audioTracks).reduce(
|
||||
(tracks, [id, track]) => ({ ...tracks, [id]: track }),
|
||||
{}
|
||||
),
|
||||
[audioTracks]
|
||||
);
|
||||
|
||||
// quick sanity to check to make sure this is an audio track...
|
||||
if (audioTrack.kind !== 'audio') return;
|
||||
// On iOS safari, when headphones are disconnected, all audio elements are paused.
|
||||
// This means that when a user disconnects their headphones, that user will not
|
||||
// be able to hear any other users until they mute/unmute their mics.
|
||||
// To fix that, we call `play` on each audio track on all devicechange events.
|
||||
useEffect(() => {
|
||||
const playTracks = () => {
|
||||
document.querySelectorAll('.audioTracks audio').forEach(async (audio) => {
|
||||
try {
|
||||
if (audio.paused && audio.readyState === audio.HAVE_ENOUGH_DATA) {
|
||||
await audio?.play();
|
||||
}
|
||||
} catch (e) {
|
||||
// Auto play failed
|
||||
}
|
||||
});
|
||||
};
|
||||
navigator.mediaDevices.addEventListener('devicechange', playTracks);
|
||||
return () => {
|
||||
navigator.mediaDevices.removeEventListener('devicechange', playTracks);
|
||||
};
|
||||
}, []);
|
||||
|
||||
audioRef.current.srcObject = new MediaStream([audioTrack]);
|
||||
}, [audioTrack]);
|
||||
|
||||
useEffect(() => {
|
||||
// On iOS safari, when headphones are disconnected, all audio elements are paused.
|
||||
// This means that when a user disconnects their headphones, that user will not
|
||||
// be able to hear any other users until they mute/unmute their mics.
|
||||
// To fix that, we call `play` on each audio track on all devicechange events.
|
||||
if (audioRef.currenet) {
|
||||
return false;
|
||||
}
|
||||
const startPlayingTrack = () => {
|
||||
audioRef.current?.play();
|
||||
};
|
||||
|
||||
navigator.mediaDevices.addEventListener(
|
||||
'devicechange',
|
||||
startPlayingTrack
|
||||
);
|
||||
|
||||
return () =>
|
||||
navigator.mediaDevices.removeEventListener(
|
||||
'devicechange',
|
||||
startPlayingTrack
|
||||
);
|
||||
}, [audioRef]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<audio autoPlay playsInline ref={audioRef}>
|
||||
<track kind="captions" />
|
||||
</audio>
|
||||
</>
|
||||
);
|
||||
},
|
||||
() => true
|
||||
);
|
||||
|
||||
AudioItem.propTypes = {
|
||||
participant: PropTypes.object,
|
||||
};
|
||||
|
||||
export const Audio = React.memo(() => {
|
||||
const { allParticipants } = useParticipants();
|
||||
const tracksComponent = useMemo(() => {
|
||||
const { browser } = Bowser.parse(navigator.userAgent);
|
||||
if (browser.name === 'Chrome' && parseInt(browser.version, 10) >= 92) {
|
||||
return <CombinedAudioTrack tracks={renderedTracks} />;
|
||||
}
|
||||
return Object.entries(renderedTracks).map(([id, track]) => (
|
||||
<AudioTrack key={id} track={track.persistentTrack} />
|
||||
));
|
||||
}, [renderedTracks]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{allParticipants.map(
|
||||
(p) => !p.isLocal && <AudioItem participant={p} key={p.id} />
|
||||
)}
|
||||
</>
|
||||
<Portal key="AudioTracks">
|
||||
<div className="audioTracks">
|
||||
{tracksComponent}
|
||||
<style jsx>{`
|
||||
.audioTracks {
|
||||
position: absolute;
|
||||
visibility: hidden;
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
</Portal>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
export default Audio;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,45 @@
|
|||
import React, { useRef, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const AudioTrack = React.memo(
|
||||
({ track }) => {
|
||||
const audioRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!audioRef.current) return false;
|
||||
let playTimeout;
|
||||
|
||||
const handleCanPlay = () => {
|
||||
playTimeout = setTimeout(() => {
|
||||
console.log('Unable to autoplay audio element');
|
||||
}, 1500);
|
||||
};
|
||||
const handlePlay = () => {
|
||||
clearTimeout(playTimeout);
|
||||
};
|
||||
audioRef.current.addEventListener('canplay', handleCanPlay);
|
||||
audioRef.current.addEventListener('play', handlePlay);
|
||||
audioRef.current.srcObject = new MediaStream([track]);
|
||||
|
||||
const audioEl = audioRef.current;
|
||||
|
||||
return () => {
|
||||
audioEl?.removeEventListener('canplay', handleCanPlay);
|
||||
audioEl?.removeEventListener('play', handlePlay);
|
||||
};
|
||||
}, [track]);
|
||||
|
||||
return track ? (
|
||||
<audio autoPlay playsInline ref={audioRef}>
|
||||
<track kind="captions" />
|
||||
</audio>
|
||||
) : null;
|
||||
},
|
||||
() => true
|
||||
);
|
||||
|
||||
AudioTrack.propTypes = {
|
||||
track: PropTypes.object,
|
||||
};
|
||||
|
||||
export default AudioTrack;
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
import React, { useEffect, useRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useDeepCompareEffect, useDeepCompareMemo } from 'use-deep-compare';
|
||||
|
||||
const CombinedAudioTrack = ({ tracks }) => {
|
||||
const audioEl = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!audioEl) return;
|
||||
audioEl.current.srcObject = new MediaStream();
|
||||
}, []);
|
||||
|
||||
const trackIds = useDeepCompareMemo(
|
||||
() => Object.values(tracks).map((t) => t?.persistentTrack?.id),
|
||||
[tracks]
|
||||
);
|
||||
|
||||
useDeepCompareEffect(() => {
|
||||
const audio = audioEl.current;
|
||||
if (!audio || !audio.srcObject) return;
|
||||
|
||||
const stream = audio.srcObject;
|
||||
const allTracks = Object.values(tracks);
|
||||
|
||||
allTracks.forEach((track) => {
|
||||
const persistentTrack = track?.persistentTrack;
|
||||
if (persistentTrack) {
|
||||
persistentTrack.addEventListener(
|
||||
'ended',
|
||||
(ev) => stream.removeTrack(ev.target),
|
||||
{ once: true }
|
||||
);
|
||||
stream.addTrack(persistentTrack);
|
||||
}
|
||||
});
|
||||
|
||||
const playAudio = async () => {
|
||||
try {
|
||||
if (
|
||||
stream
|
||||
.getAudioTracks()
|
||||
.some((t) => t.enabled && t.readyState === 'live') &&
|
||||
audio.paused
|
||||
) {
|
||||
await audio.play();
|
||||
}
|
||||
} catch {
|
||||
// ...
|
||||
}
|
||||
};
|
||||
|
||||
audio.load();
|
||||
playAudio();
|
||||
}, [tracks, trackIds]);
|
||||
|
||||
return (
|
||||
<audio autoPlay playsInline ref={audioEl}>
|
||||
<track kind="captions" />
|
||||
</audio>
|
||||
);
|
||||
};
|
||||
|
||||
CombinedAudioTrack.propTypes = {
|
||||
tracks: PropTypes.object,
|
||||
};
|
||||
|
||||
export default CombinedAudioTrack;
|
||||
|
|
@ -195,7 +195,7 @@ export const Button = forwardRef(
|
|||
}
|
||||
|
||||
.button.tiny {
|
||||
height: 32px;
|
||||
height: 28px;
|
||||
font-size: 11px;
|
||||
border-radius: var(--radius-xs);
|
||||
text-transform: uppercase;
|
||||
|
|
@ -290,6 +290,24 @@ export const Button = forwardRef(
|
|||
opacity: 0.35;
|
||||
}
|
||||
|
||||
.button.white {
|
||||
background: white;
|
||||
border: 0px;
|
||||
}
|
||||
.button.white:hover,
|
||||
.button.white:focus,
|
||||
.button.white:active {
|
||||
background: var(--gray-wash);
|
||||
border: 0px;
|
||||
color: var(--primary-default);
|
||||
}
|
||||
.button.white:focus {
|
||||
box-shadow: 0 0 0px 3px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
.button.white:disabled {
|
||||
opacity: 0.35;
|
||||
}
|
||||
|
||||
.button.outline {
|
||||
background: transparent;
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
|
|
@ -320,6 +338,22 @@ export const Button = forwardRef(
|
|||
box-shadow: 0 0 0px 3px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.button.outline-dark {
|
||||
background: transparent;
|
||||
border: 1px solid var(--blue-light);
|
||||
color: var(--text-light);
|
||||
}
|
||||
|
||||
.button.outline-dark:hover,
|
||||
.button.outline-dark:focus,
|
||||
.button.outline-dark:active {
|
||||
border: 1px solid var(--primary-default);
|
||||
box-shadow: none;
|
||||
}
|
||||
.button.outline-dark:focus {
|
||||
box-shadow: 0 0 0px 3px ${hexa(theme.primary.default, 0.35)};
|
||||
}
|
||||
|
||||
.button.muted {
|
||||
color: var(--red-default);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,8 +18,6 @@ export const DeviceSelect = () => {
|
|||
return <div>Loading devices...</div>;
|
||||
}
|
||||
|
||||
console.log(currentDevices);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Field label="Select camera:">
|
||||
|
|
|
|||
|
|
@ -0,0 +1,54 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export const ExpiryTimer = ({ expiry }) => {
|
||||
const [secs, setSecs] = useState('--:--');
|
||||
|
||||
// If room has an expiry time, we'll calculate how many seconds until expiry
|
||||
useEffect(() => {
|
||||
if (!expiry) {
|
||||
return false;
|
||||
}
|
||||
const i = setInterval(() => {
|
||||
const timeLeft = Math.round((expiry - Date.now()) / 1000);
|
||||
if (timeLeft < 0) {
|
||||
return setSecs(null);
|
||||
}
|
||||
setSecs(`${Math.floor(timeLeft / 60)}:${`0${timeLeft % 60}`.slice(-2)}`);
|
||||
}, 1000);
|
||||
|
||||
return () => clearInterval(i);
|
||||
}, [expiry]);
|
||||
|
||||
if (!secs) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="countdown">
|
||||
{secs}
|
||||
<style jsx>{`
|
||||
.countdown {
|
||||
position: fixed;
|
||||
top: 0px;
|
||||
right: 0px;
|
||||
width: 48px;
|
||||
text-align: center;
|
||||
padding: 4px 0;
|
||||
font-size: 0.875rem;
|
||||
font-weight: var(--weight-medium);
|
||||
border-radius: 0 0 0 var(--radius-sm);
|
||||
background: var(--secondary-dark);
|
||||
color: white;
|
||||
z-index: 999;
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ExpiryTimer.propTypes = {
|
||||
expiry: PropTypes.number,
|
||||
};
|
||||
|
||||
export default ExpiryTimer;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { ExpiryTimer as default } from './ExpiryTimer';
|
||||
|
|
@ -5,7 +5,7 @@ export const GlobalHead = () => (
|
|||
<Head>
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Rubik:wght@400;500;600&display=swap"
|
||||
href="https://fonts.googleapis.com/css2?family=Rubik:wght@400;500;600&display=optional"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
</Head>
|
||||
|
|
|
|||