diff --git a/dailyjs/active-speaker/.babelrc b/dailyjs/active-speaker/.babelrc new file mode 100644 index 0000000..a6f4434 --- /dev/null +++ b/dailyjs/active-speaker/.babelrc @@ -0,0 +1,4 @@ +{ + "presets": ["next/babel"], + "plugins": ["inline-react-svg"] +} diff --git a/dailyjs/active-speaker/README.md b/dailyjs/active-speaker/README.md new file mode 100644 index 0000000..4656997 --- /dev/null +++ b/dailyjs/active-speaker/README.md @@ -0,0 +1,31 @@ +# Active Speaker + +![Active speaker](./image.png) + +### Live example + +**[See it in action here ➡️](https://dailyjs-active-speaker.vercel.app)** + +--- + +## What does this demo do? + +- Uses an active speaker view mode that shows the currently talking participant (or active screen share) in a larger tile +- Introduces the `ParticipantBar` column that virtually scrolls through all call participants +- Uses manual subscriptions to paginate between tiles that are currently in view. For more information about how this works, please refer to the [pagination demo](../pagination) + +Please note: this demo is not currently mobile optimised + +### Getting started + +``` +# set both DAILY_API_KEY and DAILY_DOMAIN +mv env.example .env.local + +yarn +yarn workspace @dailyjs/active-speaker dev +``` + +## Deploy your own on Vercel + +[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/daily-co/clone-flow?repository-url=https%3A%2F%2Fgithub.com%2Fdaily-demos%2Fexamples.git&env=DAILY_DOMAIN%2CDAILY_API_KEY&envDescription=Your%20Daily%20domain%20and%20API%20key%20can%20be%20found%20on%20your%20account%20dashboard&envLink=https%3A%2F%2Fdashboard.daily.co&project-name=daily-examples&repo-name=daily-examples) diff --git a/dailyjs/active-speaker/components/App/App.js b/dailyjs/active-speaker/components/App/App.js new file mode 100644 index 0000000..5689315 --- /dev/null +++ b/dailyjs/active-speaker/components/App/App.js @@ -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 = () => ( + , + }} + /> +); + +export default AppWithSpeakerViewRoom; diff --git a/dailyjs/active-speaker/components/App/index.js b/dailyjs/active-speaker/components/App/index.js new file mode 100644 index 0000000..19427e3 --- /dev/null +++ b/dailyjs/active-speaker/components/App/index.js @@ -0,0 +1 @@ +export { AppWithSpeakerViewRoom as default } from './App'; diff --git a/dailyjs/active-speaker/components/Room/Room.js b/dailyjs/active-speaker/components/Room/Room.js new file mode 100644 index 0000000..527564c --- /dev/null +++ b/dailyjs/active-speaker/components/Room/Room.js @@ -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 = () => ( + + + + + +); + +export default Room; diff --git a/dailyjs/active-speaker/components/Room/index.js b/dailyjs/active-speaker/components/Room/index.js new file mode 100644 index 0000000..d26c647 --- /dev/null +++ b/dailyjs/active-speaker/components/Room/index.js @@ -0,0 +1,2 @@ +export { Room as default } from './Room'; +export { Room } from './Room'; diff --git a/dailyjs/active-speaker/components/SpeakerView/SpeakerTile/SpeakerTile.js b/dailyjs/active-speaker/components/SpeakerView/SpeakerTile/SpeakerTile.js new file mode 100644 index 0000000..d931cb0 --- /dev/null +++ b/dailyjs/active-speaker/components/SpeakerView/SpeakerTile/SpeakerTile.js @@ -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 ( + + ); +}; + +SpeakerTile.propTypes = { + participant: PropTypes.object, + screenRef: PropTypes.object, +}; + +export default SpeakerTile; diff --git a/dailyjs/active-speaker/components/SpeakerView/SpeakerTile/index.js b/dailyjs/active-speaker/components/SpeakerView/SpeakerTile/index.js new file mode 100644 index 0000000..84639df --- /dev/null +++ b/dailyjs/active-speaker/components/SpeakerView/SpeakerTile/index.js @@ -0,0 +1,2 @@ +export { SpeakerTile as default } from './SpeakerTile'; +export { SpeakerTile } from './SpeakerTile'; diff --git a/dailyjs/active-speaker/components/SpeakerView/SpeakerView.js b/dailyjs/active-speaker/components/SpeakerView/SpeakerView.js new file mode 100644 index 0000000..bb69d9d --- /dev/null +++ b/dailyjs/active-speaker/components/SpeakerView/SpeakerView.js @@ -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 ( +
+ + +
+ ); +}; + +export default SpeakerView; diff --git a/dailyjs/active-speaker/components/SpeakerView/index.js b/dailyjs/active-speaker/components/SpeakerView/index.js new file mode 100644 index 0000000..aa06afe --- /dev/null +++ b/dailyjs/active-speaker/components/SpeakerView/index.js @@ -0,0 +1,2 @@ +export { SpeakerView as default } from './SpeakerView'; +export { SpeakerView } from './SpeakerView'; diff --git a/dailyjs/active-speaker/env.example b/dailyjs/active-speaker/env.example new file mode 100644 index 0000000..5ab7e03 --- /dev/null +++ b/dailyjs/active-speaker/env.example @@ -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 diff --git a/dailyjs/active-speaker/image.png b/dailyjs/active-speaker/image.png new file mode 100644 index 0000000..592ea98 Binary files /dev/null and b/dailyjs/active-speaker/image.png differ diff --git a/dailyjs/active-speaker/index.js b/dailyjs/active-speaker/index.js new file mode 100644 index 0000000..9044efc --- /dev/null +++ b/dailyjs/active-speaker/index.js @@ -0,0 +1 @@ +// Note: I am here because next-transpile-modules requires a mainfile diff --git a/dailyjs/active-speaker/next.config.js b/dailyjs/active-speaker/next.config.js new file mode 100644 index 0000000..9a0a6ee --- /dev/null +++ b/dailyjs/active-speaker/next.config.js @@ -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, + }, +}); diff --git a/dailyjs/active-speaker/package.json b/dailyjs/active-speaker/package.json new file mode 100644 index 0000000..959883e --- /dev/null +++ b/dailyjs/active-speaker/package.json @@ -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" + } +} diff --git a/dailyjs/active-speaker/pages/_app.js b/dailyjs/active-speaker/pages/_app.js new file mode 100644 index 0000000..4fcae14 --- /dev/null +++ b/dailyjs/active-speaker/pages/_app.js @@ -0,0 +1,7 @@ +import React from 'react'; +import App from '@dailyjs/basic-call/pages/_app'; +import AppWithSpeakerViewRoom from '../components/App'; + +App.customAppComponent = ; + +export default App; diff --git a/dailyjs/active-speaker/pages/api b/dailyjs/active-speaker/pages/api new file mode 120000 index 0000000..999f604 --- /dev/null +++ b/dailyjs/active-speaker/pages/api @@ -0,0 +1 @@ +../../basic-call/pages/api \ No newline at end of file diff --git a/dailyjs/active-speaker/pages/index.js b/dailyjs/active-speaker/pages/index.js new file mode 100644 index 0000000..d25e77e --- /dev/null +++ b/dailyjs/active-speaker/pages/index.js @@ -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; diff --git a/dailyjs/active-speaker/public b/dailyjs/active-speaker/public new file mode 120000 index 0000000..33a6e67 --- /dev/null +++ b/dailyjs/active-speaker/public @@ -0,0 +1 @@ +../basic-call/public \ No newline at end of file diff --git a/dailyjs/basic-call/components/Room/Room.js b/dailyjs/basic-call/components/Room/Room.js index a21247f..8efc0bb 100644 --- a/dailyjs/basic-call/components/Room/Room.js +++ b/dailyjs/basic-call/components/Room/Room.js @@ -1,4 +1,5 @@ import React from 'react'; +import VideoContainer from '@dailyjs/shared/components/VideoContainer/VideoContainer'; import { VideoGrid } from '../VideoGrid'; import { Header } from './Header'; @@ -7,9 +8,9 @@ import { RoomContainer } from './RoomContainer'; export const Room = () => (
-
+ -
+ ); diff --git a/dailyjs/basic-call/components/Room/RoomContainer.js b/dailyjs/basic-call/components/Room/RoomContainer.js index 998ecaf..b3fed79 100644 --- a/dailyjs/basic-call/components/Room/RoomContainer.js +++ b/dailyjs/basic-call/components/Room/RoomContainer.js @@ -37,16 +37,6 @@ export const RoomContainer = ({ children }) => { height: 100%; 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; - } `} ); diff --git a/dailyjs/live-fitness/components/Room/Room.js b/dailyjs/live-fitness/components/Room/Room.js index 43960af..f6aa109 100644 --- a/dailyjs/live-fitness/components/Room/Room.js +++ b/dailyjs/live-fitness/components/Room/Room.js @@ -1,12 +1,13 @@ import React from 'react'; import { RoomContainer } from '@dailyjs/basic-call/components/Room/RoomContainer'; +import { VideoContainer } from '@dailyjs/shared/components/VideoContainer'; import { Header } from './Header'; export const Room = () => (
-
Hello
+ Hello ); diff --git a/dailyjs/live-streaming/components/App/App.js b/dailyjs/live-streaming/components/App/App.js new file mode 100644 index 0000000..4749551 --- /dev/null +++ b/dailyjs/live-streaming/components/App/App.js @@ -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 = () => ( + + + +); + +export default AppWithLiveStreaming; diff --git a/dailyjs/live-streaming/components/App/index.js b/dailyjs/live-streaming/components/App/index.js new file mode 100644 index 0000000..c46acf2 --- /dev/null +++ b/dailyjs/live-streaming/components/App/index.js @@ -0,0 +1 @@ +export { AppWithLiveStreaming as default } from './App'; diff --git a/dailyjs/shared/assets/assets b/dailyjs/shared/assets/assets deleted file mode 120000 index 951b6e8..0000000 --- a/dailyjs/shared/assets/assets +++ /dev/null @@ -1 +0,0 @@ -../../shared/assets \ No newline at end of file diff --git a/dailyjs/shared/components/HairCheck/HairCheck.js b/dailyjs/shared/components/HairCheck/HairCheck.js index f6f1e15..e07307e 100644 --- a/dailyjs/shared/components/HairCheck/HairCheck.js +++ b/dailyjs/shared/components/HairCheck/HairCheck.js @@ -208,7 +208,7 @@ export const HairCheck = () => { justify-content: center; height: 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%; } diff --git a/dailyjs/shared/components/Tile/Tile.js b/dailyjs/shared/components/Tile/Tile.js index 53afe11..ac49960 100644 --- a/dailyjs/shared/components/Tile/Tile.js +++ b/dailyjs/shared/components/Tile/Tile.js @@ -1,4 +1,4 @@ -import React, { useRef } from 'react'; +import React, { useState, useEffect, useRef } from 'react'; import useVideoTrack from '@dailyjs/shared/hooks/useVideoTrack'; import { ReactComponent as IconMicMute } from '@dailyjs/shared/icons/mic-off-sm.svg'; import classNames from 'classnames'; @@ -14,12 +14,45 @@ export const Tile = React.memo( showName = true, showAvatar = true, aspectRatio = DEFAULT_ASPECT_RATIO, + onVideoResize, + videoFit = 'contain', ...props }) => { const videoTrack = useVideoTrack(participant); 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, avatar: showAvatar && !videoTrack, active: participant.isActiveSpeaker, @@ -46,19 +79,14 @@ export const Tile = React.memo( + +); + +VideoContainer.propTypes = { + children: PropTypes.node, +}; +export default VideoContainer; diff --git a/dailyjs/shared/components/VideoContainer/index.js b/dailyjs/shared/components/VideoContainer/index.js new file mode 100644 index 0000000..e69de29 diff --git a/dailyjs/shared/contexts/UIStateProvider.js b/dailyjs/shared/contexts/UIStateProvider.js index 2e5bab5..01d7e92 100644 --- a/dailyjs/shared/contexts/UIStateProvider.js +++ b/dailyjs/shared/contexts/UIStateProvider.js @@ -68,6 +68,7 @@ export const UIStateProvider = ({ asides, modals, customTrayComponent, + viewMode, openModal, closeModal, closeAside, diff --git a/dailyjs/shared/hooks/useResize.js b/dailyjs/shared/hooks/useResize.js new file mode 100644 index 0000000..bed711d --- /dev/null +++ b/dailyjs/shared/hooks/useResize.js @@ -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;