added active speaker view
This commit is contained in:
parent
236e91d302
commit
0123e01487
|
|
@ -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,14 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { RoomContainer } from '@dailyjs/basic-call/components/Room';
|
||||||
|
import VideoContainer from '@dailyjs/shared/components/VideoContainer/VideoContainer';
|
||||||
|
import { SpeakerView } from '../SpeakerView';
|
||||||
|
|
||||||
|
export const Room = () => (
|
||||||
|
<RoomContainer>
|
||||||
|
<VideoContainer>
|
||||||
|
<SpeakerView />
|
||||||
|
</VideoContainer>
|
||||||
|
</RoomContainer>
|
||||||
|
);
|
||||||
|
|
||||||
|
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}
|
||||||
|
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,25 @@
|
||||||
|
import React, { useRef } from 'react';
|
||||||
|
import { useParticipants } from '@dailyjs/shared/contexts/ParticipantsProvider';
|
||||||
|
import SpeakerTile from './SpeakerTile';
|
||||||
|
|
||||||
|
export const SpeakerView = () => {
|
||||||
|
const { currentSpeaker } = useParticipants();
|
||||||
|
const activeRef = useRef();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={activeRef} className="active">
|
||||||
|
<SpeakerTile participant={currentSpeaker} screenRef={activeRef} />
|
||||||
|
<style jsx>{`
|
||||||
|
.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
|
||||||
Binary file not shown.
|
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 @@
|
||||||
|
../basic-call/public
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import VideoContainer from '@dailyjs/shared/components/VideoContainer/VideoContainer';
|
||||||
|
|
||||||
import { VideoGrid } from '../VideoGrid';
|
import { VideoGrid } from '../VideoGrid';
|
||||||
import { Header } from './Header';
|
import { Header } from './Header';
|
||||||
|
|
@ -7,9 +8,9 @@ import { RoomContainer } from './RoomContainer';
|
||||||
export const Room = () => (
|
export const Room = () => (
|
||||||
<RoomContainer>
|
<RoomContainer>
|
||||||
<Header />
|
<Header />
|
||||||
<main>
|
<VideoContainer>
|
||||||
<VideoGrid />
|
<VideoGrid />
|
||||||
</main>
|
</VideoContainer>
|
||||||
</RoomContainer>
|
</RoomContainer>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -37,16 +37,6 @@ export const RoomContainer = ({ children }) => {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
.room > :global(main) {
|
|
||||||
flex: 1 1 auto;
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
min-height: 0px;
|
|
||||||
height: 100%;
|
|
||||||
padding: var(--spacing-xxxs);
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
`}</style>
|
`}</style>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,13 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { RoomContainer } from '@dailyjs/basic-call/components/Room/RoomContainer';
|
import { RoomContainer } from '@dailyjs/basic-call/components/Room/RoomContainer';
|
||||||
|
import { VideoContainer } from '@dailyjs/shared/components/VideoContainer';
|
||||||
import { Header } from './Header';
|
import { Header } from './Header';
|
||||||
|
|
||||||
export const Room = () => (
|
export const Room = () => (
|
||||||
<RoomContainer>
|
<RoomContainer>
|
||||||
<Header />
|
<Header />
|
||||||
<main>Hello</main>
|
<VideoContainer>Hello</VideoContainer>
|
||||||
</RoomContainer>
|
</RoomContainer>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import App from '@dailyjs/basic-call/components/App';
|
||||||
|
import { LiveStreamingProvider } from '../../contexts/LiveStreamingProvider';
|
||||||
|
|
||||||
|
// Extend our basic call app component with the live streaming context
|
||||||
|
export const AppWithLiveStreaming = () => (
|
||||||
|
<LiveStreamingProvider>
|
||||||
|
<App />
|
||||||
|
</LiveStreamingProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default AppWithLiveStreaming;
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export { AppWithLiveStreaming as default } from './App';
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
../../shared/assets
|
|
||||||
|
|
@ -208,7 +208,7 @@ export const HairCheck = () => {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background: url('/images/pattern-bg.png') center center no-repeat;
|
background: url('/assets/pattern-bg.png') center center no-repeat;
|
||||||
background-size: 100%;
|
background-size: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useRef } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import useVideoTrack from '@dailyjs/shared/hooks/useVideoTrack';
|
import useVideoTrack from '@dailyjs/shared/hooks/useVideoTrack';
|
||||||
import { ReactComponent as IconMicMute } from '@dailyjs/shared/icons/mic-off-sm.svg';
|
import { ReactComponent as IconMicMute } from '@dailyjs/shared/icons/mic-off-sm.svg';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
@ -14,12 +14,45 @@ export const Tile = React.memo(
|
||||||
showName = true,
|
showName = true,
|
||||||
showAvatar = true,
|
showAvatar = true,
|
||||||
aspectRatio = DEFAULT_ASPECT_RATIO,
|
aspectRatio = DEFAULT_ASPECT_RATIO,
|
||||||
|
onVideoResize,
|
||||||
|
videoFit = 'contain',
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
const videoTrack = useVideoTrack(participant);
|
const videoTrack = useVideoTrack(participant);
|
||||||
const videoEl = useRef(null);
|
const videoEl = useRef(null);
|
||||||
|
const [tileAspectRatio, setTileAspectRatio] = useState(aspectRatio);
|
||||||
|
|
||||||
const cx = classNames('tile', {
|
/**
|
||||||
|
* Add optional event listener for resize event so the parent component
|
||||||
|
* can know the video's native aspect ratio.
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
const video = videoEl.current;
|
||||||
|
if (!onVideoResize || !video) return false;
|
||||||
|
|
||||||
|
const handleResize = () => {
|
||||||
|
if (!video) return;
|
||||||
|
const width = video?.videoWidth;
|
||||||
|
const height = video?.videoHeight;
|
||||||
|
if (width && height) {
|
||||||
|
// Return the video's aspect ratio to the parent's handler
|
||||||
|
onVideoResize(width / height);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
handleResize();
|
||||||
|
|
||||||
|
video?.addEventListener('resize', handleResize);
|
||||||
|
|
||||||
|
return () => video?.removeEventListener('resize', handleResize);
|
||||||
|
}, [onVideoResize, videoEl, participant]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (aspectRatio === tileAspectRatio) return;
|
||||||
|
setTileAspectRatio(aspectRatio);
|
||||||
|
}, [aspectRatio, tileAspectRatio]);
|
||||||
|
|
||||||
|
const cx = classNames('tile', videoFit, {
|
||||||
mirrored,
|
mirrored,
|
||||||
avatar: showAvatar && !videoTrack,
|
avatar: showAvatar && !videoTrack,
|
||||||
active: participant.isActiveSpeaker,
|
active: participant.isActiveSpeaker,
|
||||||
|
|
@ -46,19 +79,14 @@ export const Tile = React.memo(
|
||||||
</div>
|
</div>
|
||||||
<style jsx>{`
|
<style jsx>{`
|
||||||
.tile .content {
|
.tile .content {
|
||||||
padding-bottom: ${100 / aspectRatio}%;
|
padding-bottom: ${100 / tileAspectRatio}%;
|
||||||
}
|
|
||||||
@supports (aspect-ratio: 1 / 1) {
|
|
||||||
.tile .content {
|
|
||||||
aspect-ratio: ${aspectRatio};
|
|
||||||
padding-bottom: 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
`}</style>
|
`}</style>
|
||||||
<style jsx>{`
|
<style jsx>{`
|
||||||
.tile {
|
.tile {
|
||||||
background: var(--blue-dark);
|
background: var(--blue-dark);
|
||||||
min-width: 1px;
|
min-width: 1px;
|
||||||
|
overflow: hidden;
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
|
@ -68,10 +96,6 @@ export const Tile = React.memo(
|
||||||
border: 2px solid var(--primary-default);
|
border: 2px solid var(--primary-default);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tile.mirrored :global(video) {
|
|
||||||
transform: scale(-1, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tile .name {
|
.tile .name {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 0px;
|
bottom: 0px;
|
||||||
|
|
@ -92,16 +116,27 @@ export const Tile = React.memo(
|
||||||
}
|
}
|
||||||
|
|
||||||
.tile :global(video) {
|
.tile :global(video) {
|
||||||
|
height: calc(100% + 4px);
|
||||||
|
left: -2px;
|
||||||
object-position: center;
|
object-position: center;
|
||||||
object-fit: cover;
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0;
|
top: -2px;
|
||||||
top: 0;
|
width: calc(100% + 4px);
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tile.contain :global(video) {
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile.cover :global(video) {
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile.mirrored :global(video) {
|
||||||
|
transform: scale(-1, 1);
|
||||||
|
}
|
||||||
|
|
||||||
.tile .avatar {
|
.tile .avatar {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
@ -122,6 +157,8 @@ Tile.propTypes = {
|
||||||
showName: PropTypes.bool,
|
showName: PropTypes.bool,
|
||||||
showAvatar: PropTypes.bool,
|
showAvatar: PropTypes.bool,
|
||||||
aspectRatio: PropTypes.number,
|
aspectRatio: PropTypes.number,
|
||||||
|
onVideoResize: PropTypes.func,
|
||||||
|
videoFit: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Tile;
|
export default Tile;
|
||||||
|
|
|
||||||
|
|
@ -72,7 +72,8 @@ export const Tray = ({ children }) => (
|
||||||
<style jsx>{`
|
<style jsx>{`
|
||||||
footer {
|
footer {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
padding: var(--spacing-xs);
|
padding: var(--spacing-xxs) var(--spacing-xs) var(--spacing-xs)
|
||||||
|
var(--spacing-xs);
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
export const VideoContainer = ({ children }) => (
|
||||||
|
<main>
|
||||||
|
{children}
|
||||||
|
<style jsx>{`
|
||||||
|
main {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
min-height: 0px;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
padding: var(--spacing-xxxs);
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
|
||||||
|
VideoContainer.propTypes = {
|
||||||
|
children: PropTypes.node,
|
||||||
|
};
|
||||||
|
export default VideoContainer;
|
||||||
|
|
@ -68,6 +68,7 @@ export const UIStateProvider = ({
|
||||||
asides,
|
asides,
|
||||||
modals,
|
modals,
|
||||||
customTrayComponent,
|
customTrayComponent,
|
||||||
|
viewMode,
|
||||||
openModal,
|
openModal,
|
||||||
closeModal,
|
closeModal,
|
||||||
closeAside,
|
closeAside,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { useCallback, useEffect } from 'react';
|
||||||
|
|
||||||
|
export const useResize = (callback, deps = []) => {
|
||||||
|
let timeout;
|
||||||
|
const handleResize = useCallback(() => {
|
||||||
|
if (timeout) cancelAnimationFrame(timeout);
|
||||||
|
timeout = requestAnimationFrame(() => callback());
|
||||||
|
}, [callback]);
|
||||||
|
useEffect(() => {
|
||||||
|
window.addEventListener('resize', handleResize, { passive: true });
|
||||||
|
window.addEventListener('orientationchange', handleResize, {
|
||||||
|
passive: true,
|
||||||
|
});
|
||||||
|
callback();
|
||||||
|
return () => {
|
||||||
|
if (timeout) cancelAnimationFrame(timeout);
|
||||||
|
window.removeEventListener('resize', handleResize);
|
||||||
|
window.removeEventListener('orientationchange', handleResize);
|
||||||
|
};
|
||||||
|
}, deps);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useResize;
|
||||||
Loading…
Reference in New Issue