Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c7bf395c1d |
@ -2,14 +2,14 @@ import { videoCodecs } from 'livekit-client';
|
|||||||
import { VideoConferenceClientImpl } from './VideoConferenceClientImpl';
|
import { VideoConferenceClientImpl } from './VideoConferenceClientImpl';
|
||||||
import { isVideoCodec } from '@/lib/types';
|
import { isVideoCodec } from '@/lib/types';
|
||||||
|
|
||||||
export default function CustomRoomConnection(props: {
|
export default async function CustomRoomConnection(props: {
|
||||||
searchParams: {
|
searchParams: Promise<{
|
||||||
liveKitUrl?: string;
|
liveKitUrl?: string;
|
||||||
token?: string;
|
token?: string;
|
||||||
codec?: string;
|
codec?: string;
|
||||||
};
|
}>;
|
||||||
}) {
|
}) {
|
||||||
const { liveKitUrl, token, codec } = props.searchParams;
|
const { liveKitUrl, token, codec } = await props.searchParams;
|
||||||
if (typeof liveKitUrl !== 'string') {
|
if (typeof liveKitUrl !== 'string') {
|
||||||
return <h2>Missing LiveKit URL</h2>;
|
return <h2>Missing LiveKit URL</h2>;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,25 +2,32 @@ import * as React from 'react';
|
|||||||
import { PageClientImpl } from './PageClientImpl';
|
import { PageClientImpl } from './PageClientImpl';
|
||||||
import { isVideoCodec } from '@/lib/types';
|
import { isVideoCodec } from '@/lib/types';
|
||||||
|
|
||||||
export default function Page({
|
export default async function Page({
|
||||||
params,
|
params,
|
||||||
searchParams,
|
searchParams,
|
||||||
}: {
|
}: {
|
||||||
params: { roomName: string };
|
params: Promise<{ roomName: string }>;
|
||||||
searchParams: {
|
searchParams: Promise<{
|
||||||
// FIXME: We should not allow values for regions if in playground mode.
|
// FIXME: We should not allow values for regions if in playground mode.
|
||||||
region?: string;
|
region?: string;
|
||||||
hq?: string;
|
hq?: string;
|
||||||
codec?: string;
|
codec?: string;
|
||||||
};
|
}>;
|
||||||
}) {
|
}) {
|
||||||
|
const _searchParams = await searchParams;
|
||||||
|
const _params = await params;
|
||||||
const codec =
|
const codec =
|
||||||
typeof searchParams.codec === 'string' && isVideoCodec(searchParams.codec)
|
typeof _searchParams.codec === 'string' && isVideoCodec(_searchParams.codec)
|
||||||
? searchParams.codec
|
? _searchParams.codec
|
||||||
: 'vp9';
|
: 'vp9';
|
||||||
const hq = searchParams.hq === 'true' ? true : false;
|
const hq = _searchParams.hq === 'true' ? true : false;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageClientImpl roomName={params.roomName} region={searchParams.region} hq={hq} codec={codec} />
|
<PageClientImpl
|
||||||
|
roomName={_params.roomName}
|
||||||
|
region={_searchParams.region}
|
||||||
|
hq={hq}
|
||||||
|
codec={codec}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
112
lib/pip.tsx
Normal file
112
lib/pip.tsx
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
type PiPContextType = {
|
||||||
|
isSupported: boolean;
|
||||||
|
pipWindow: Window | null;
|
||||||
|
requestPipWindow: (width: number, height: number) => Promise<void>;
|
||||||
|
closePipWindow: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const PiPContext = React.createContext<PiPContextType | undefined>(undefined);
|
||||||
|
|
||||||
|
export function usePiPWindow(): PiPContextType {
|
||||||
|
const context = React.useContext(PiPContext);
|
||||||
|
|
||||||
|
if (context === undefined) {
|
||||||
|
throw new Error('usePiPWindow must be used within a PiPContext');
|
||||||
|
}
|
||||||
|
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
type PiPProviderProps = {
|
||||||
|
children: React.ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function PiPProvider({ children }: PiPProviderProps) {
|
||||||
|
// Detect if the feature is available.
|
||||||
|
const isSupported = typeof window !== 'undefined' && 'documentPictureInPicture' in window;
|
||||||
|
|
||||||
|
// Expose pipWindow that is currently active
|
||||||
|
const [pipWindow, setPipWindow] = React.useState<Window | null>(null);
|
||||||
|
|
||||||
|
// Close pipWidnow programmatically
|
||||||
|
const closePipWindow = React.useCallback(() => {
|
||||||
|
if (pipWindow != null) {
|
||||||
|
pipWindow.close();
|
||||||
|
setPipWindow(null);
|
||||||
|
}
|
||||||
|
}, [pipWindow]);
|
||||||
|
|
||||||
|
// Open new pipWindow
|
||||||
|
const requestPipWindow = React.useCallback(
|
||||||
|
async (width: number, height: number) => {
|
||||||
|
// We don't want to allow multiple requests.
|
||||||
|
if (pipWindow != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
const pip = await window.documentPictureInPicture.requestWindow({
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Detect when window is closed by user
|
||||||
|
pip.addEventListener('pagehide', () => {
|
||||||
|
setPipWindow(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
// It is important to copy all parent widnow styles. Otherwise, there would be no CSS available at all
|
||||||
|
// https://developer.chrome.com/docs/web-platform/document-picture-in-picture/#copy-style-sheets-to-the-picture-in-picture-window
|
||||||
|
[...document.styleSheets].forEach((styleSheet) => {
|
||||||
|
try {
|
||||||
|
const cssRules = [...styleSheet.cssRules].map((rule) => rule.cssText).join('');
|
||||||
|
const style = document.createElement('style');
|
||||||
|
|
||||||
|
style.textContent = cssRules;
|
||||||
|
pip.document.head.appendChild(style);
|
||||||
|
} catch (e) {
|
||||||
|
const link = document.createElement('link');
|
||||||
|
if (styleSheet.href == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
link.rel = 'stylesheet';
|
||||||
|
link.type = styleSheet.type;
|
||||||
|
// @ts-ignore
|
||||||
|
link.media = styleSheet.media;
|
||||||
|
link.href = styleSheet.href;
|
||||||
|
pip.document.head.appendChild(link);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
setPipWindow(pip);
|
||||||
|
},
|
||||||
|
[pipWindow],
|
||||||
|
);
|
||||||
|
|
||||||
|
const value = React.useMemo(() => {
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
isSupported,
|
||||||
|
pipWindow,
|
||||||
|
requestPipWindow,
|
||||||
|
closePipWindow,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, [closePipWindow, isSupported, pipWindow, requestPipWindow]);
|
||||||
|
|
||||||
|
return <PiPContext.Provider value={value}>{children}</PiPContext.Provider>;
|
||||||
|
}
|
||||||
|
|
||||||
|
import { createPortal } from 'react-dom';
|
||||||
|
|
||||||
|
type PiPWindowProps = {
|
||||||
|
pipWindow: Window;
|
||||||
|
children: React.ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function PiPWindow({ pipWindow, children }: PiPWindowProps) {
|
||||||
|
return createPortal(children, pipWindow.document.body);
|
||||||
|
}
|
||||||
2
next-env.d.ts
vendored
2
next-env.d.ts
vendored
@ -2,4 +2,4 @@
|
|||||||
/// <reference types="next/image-types/global" />
|
/// <reference types="next/image-types/global" />
|
||||||
|
|
||||||
// NOTE: This file should not be edited
|
// NOTE: This file should not be edited
|
||||||
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
|
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||||
|
|||||||
@ -15,7 +15,7 @@
|
|||||||
"@livekit/krisp-noise-filter": "0.2.14",
|
"@livekit/krisp-noise-filter": "0.2.14",
|
||||||
"livekit-client": "2.8.1",
|
"livekit-client": "2.8.1",
|
||||||
"livekit-server-sdk": "2.9.7",
|
"livekit-server-sdk": "2.9.7",
|
||||||
"next": "14.2.23",
|
"next": "^15",
|
||||||
"react": "18.3.1",
|
"react": "18.3.1",
|
||||||
"react-dom": "18.3.1",
|
"react-dom": "18.3.1",
|
||||||
"tinykeys": "^3.0.0"
|
"tinykeys": "^3.0.0"
|
||||||
|
|||||||
389
pnpm-lock.yaml
generated
389
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user