[WIP] Update around webrtc.
This commit is contained in:
		@@ -5,13 +5,13 @@ import { SidebarVariant, SidebarVariantSchema } from './Sidebar';
 | 
			
		||||
import { z } from 'zod';
 | 
			
		||||
import { LiveCollaborationTrigger } from '@excalidraw/excalidraw';
 | 
			
		||||
 | 
			
		||||
export const RightTopUIPropsScheme = z.object({
 | 
			
		||||
export const RightTopUIPropsSchema = z.object({
 | 
			
		||||
  setSidebarVariant: z.function().args(SidebarVariantSchema).returns(z.void()),
 | 
			
		||||
  setToggleState: z.function().args(z.boolean()).returns(z.void()),
 | 
			
		||||
  toggleState: z.boolean(),
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export type RightTopUIProps = z.infer<typeof RightTopUIPropsScheme> & {
 | 
			
		||||
export type RightTopUIProps = z.infer<typeof RightTopUIPropsSchema> & {
 | 
			
		||||
  excalidrawAPI: ExcalidrawImperativeAPI;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,16 @@
 | 
			
		||||
import { FC } from 'react';
 | 
			
		||||
import { FC, useCallback, useEffect, useState } from 'react';
 | 
			
		||||
import { Button, SidebarHeader } from '@/Components/Utilities';
 | 
			
		||||
import { z } from 'zod';
 | 
			
		||||
import SidebarProviderBase, { SidebarBasePropsSchema } from './Base';
 | 
			
		||||
import { startCollaboration } from '@/webrtc/collaboration';
 | 
			
		||||
import {
 | 
			
		||||
  SignalingData,
 | 
			
		||||
  decodeSignalingData,
 | 
			
		||||
  encodeSignalingData,
 | 
			
		||||
} from '@/webrtc/utilities';
 | 
			
		||||
import { Input, Link } from '@mui/material';
 | 
			
		||||
import { fromUrl, toUrl } from '@/utilities/url';
 | 
			
		||||
import { copyToClipboard } from '@/utilities/clipboard';
 | 
			
		||||
 | 
			
		||||
export type CollaborationSidebarProps = z.infer<
 | 
			
		||||
  typeof CollaborationSidebarPropsSchema
 | 
			
		||||
@@ -12,10 +21,72 @@ export const CollaborationSidebarPropsSchema = z
 | 
			
		||||
  .merge(SidebarBasePropsSchema);
 | 
			
		||||
 | 
			
		||||
const CollaborationSidebar: FC<CollaborationSidebarProps> = (props) => {
 | 
			
		||||
  const signalingDataCallback = useCallback((data: SignalingData) => {
 | 
			
		||||
    const encodedData = encodeSignalingData(data);
 | 
			
		||||
    const url = toUrl({ signalingData: encodedData });
 | 
			
		||||
 | 
			
		||||
    setCollaborationUrl(url);
 | 
			
		||||
 | 
			
		||||
    console.log(data);
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  const [collaborationUrl, setCollaborationUrl] = useState<URL | null>(null);
 | 
			
		||||
  const [isCollaborationStarted, setIsCollaborationStarted] = useState(false);
 | 
			
		||||
  const [providedCollaborationUrl, setProvidedCollaborationUrl] =
 | 
			
		||||
    useState<URL | null>(null);
 | 
			
		||||
 | 
			
		||||
  const onMessage = useCallback((event: MessageEvent) => {
 | 
			
		||||
    console.log(event);
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  const startCollaborationWrapper = useCallback(async () => {
 | 
			
		||||
    console.log('start collaboration');
 | 
			
		||||
 | 
			
		||||
    const signalingData = providedCollaborationUrl
 | 
			
		||||
      ? decodeSignalingData(fromUrl(providedCollaborationUrl).signalingData!)
 | 
			
		||||
      : undefined;
 | 
			
		||||
 | 
			
		||||
    console.log(signalingData);
 | 
			
		||||
 | 
			
		||||
    await startCollaboration(signalingData, signalingDataCallback, {
 | 
			
		||||
      onMessage,
 | 
			
		||||
    });
 | 
			
		||||
  }, [signalingDataCallback, onMessage, providedCollaborationUrl]);
 | 
			
		||||
 | 
			
		||||
  const copyCollaborationUrl = useCallback(async () => {
 | 
			
		||||
    console.log('copy collaboration url');
 | 
			
		||||
    console.log(collaborationUrl?.toString());
 | 
			
		||||
    await copyToClipboard(collaborationUrl ? collaborationUrl?.toString() : '');
 | 
			
		||||
  }, [collaborationUrl]);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (isCollaborationStarted || providedCollaborationUrl) {
 | 
			
		||||
      startCollaborationWrapper().catch((error) => console.error(error));
 | 
			
		||||
    }
 | 
			
		||||
  }, [
 | 
			
		||||
    isCollaborationStarted,
 | 
			
		||||
    startCollaborationWrapper,
 | 
			
		||||
    providedCollaborationUrl,
 | 
			
		||||
  ]);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <SidebarProviderBase onClose={props.onClose}>
 | 
			
		||||
      <SidebarHeader>Collaboration Settings</SidebarHeader>
 | 
			
		||||
      <Button>Button</Button>
 | 
			
		||||
      <SidebarHeader>Collaboration</SidebarHeader>
 | 
			
		||||
      <Button onClick={() => setIsCollaborationStarted(true)}>
 | 
			
		||||
        Start Collaboration
 | 
			
		||||
      </Button>
 | 
			
		||||
      <Link>{collaborationUrl?.toString()}</Link>
 | 
			
		||||
      <Button
 | 
			
		||||
        onClick={() => {
 | 
			
		||||
          copyCollaborationUrl().catch((e) => console.error(e));
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        Copy
 | 
			
		||||
      </Button>
 | 
			
		||||
      <Input
 | 
			
		||||
        value={providedCollaborationUrl ?? ''}
 | 
			
		||||
        onChange={(e) => setProvidedCollaborationUrl(new URL(e.target.value))}
 | 
			
		||||
      ></Input>
 | 
			
		||||
    </SidebarProviderBase>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										3
									
								
								src/utilities/clipboard.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								src/utilities/clipboard.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,3 @@
 | 
			
		||||
export const copyToClipboard = async (text: string) => {
 | 
			
		||||
  await navigator.clipboard.writeText(text);
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										25
									
								
								src/utilities/url.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								src/utilities/url.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,25 @@
 | 
			
		||||
import { z } from 'zod';
 | 
			
		||||
 | 
			
		||||
export const AvailableURLParametersSchema = z.object({
 | 
			
		||||
  signalingData: z.string().optional(),
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export type AvailableURLParameters = z.infer<
 | 
			
		||||
  typeof AvailableURLParametersSchema
 | 
			
		||||
>;
 | 
			
		||||
 | 
			
		||||
export const toUrl = (params: AvailableURLParameters) => {
 | 
			
		||||
  const url = new URL(window.location.href);
 | 
			
		||||
 | 
			
		||||
  Object.entries(params).forEach(([key, value]) => {
 | 
			
		||||
    url.searchParams.set(key, value);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  return url;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const fromUrl = (url: URL): AvailableURLParameters => {
 | 
			
		||||
  const params = Object.fromEntries(url.searchParams.entries());
 | 
			
		||||
 | 
			
		||||
  return AvailableURLParametersSchema.parse(params);
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										28
									
								
								src/webrtc/collaboration.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								src/webrtc/collaboration.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,28 @@
 | 
			
		||||
import establishConnection from './connection';
 | 
			
		||||
import type { Connection, DataChannelCallbacks } from './connection';
 | 
			
		||||
import type { SignalingData } from './utilities';
 | 
			
		||||
 | 
			
		||||
export type SignalingDataCallback = (signalingData: SignalingData) => void;
 | 
			
		||||
 | 
			
		||||
export type OnAnswerCallback = (answer: RTCSessionDescription) => void;
 | 
			
		||||
export type OnOfferCallback = (offer: RTCSessionDescription) => void;
 | 
			
		||||
 | 
			
		||||
export const startCollaboration = async (
 | 
			
		||||
  signalingData: SignalingData | undefined,
 | 
			
		||||
  onSignalingData: SignalingDataCallback,
 | 
			
		||||
  dataChannelCallbacks: DataChannelCallbacks
 | 
			
		||||
): Promise<void> => {
 | 
			
		||||
  let connection: Connection = {
 | 
			
		||||
    connectionState: 'noConnection',
 | 
			
		||||
    dataChannelCallbacks,
 | 
			
		||||
    signalingData,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  while (connection?.connectionState !== 'established') {
 | 
			
		||||
    connection = await establishConnection(connection);
 | 
			
		||||
 | 
			
		||||
    if (connection.signalingData) {
 | 
			
		||||
      onSignalingData(connection.signalingData);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										148
									
								
								src/webrtc/connection.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										148
									
								
								src/webrtc/connection.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,148 @@
 | 
			
		||||
import { match } from 'ts-pattern';
 | 
			
		||||
import { SignalingDataType, SignalingData } from './utilities';
 | 
			
		||||
import { z } from 'zod';
 | 
			
		||||
 | 
			
		||||
export const PeerConfiguration: RTCConfiguration = {
 | 
			
		||||
  iceServers: [{ urls: 'stun:stun.l.google.com:19302' }],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const EstablishingConnectionStateSchema = z.enum([
 | 
			
		||||
  'noConnection',
 | 
			
		||||
  'waiting',
 | 
			
		||||
  'established',
 | 
			
		||||
]);
 | 
			
		||||
 | 
			
		||||
export type EstablishingConnectionState = z.infer<
 | 
			
		||||
  typeof EstablishingConnectionStateSchema
 | 
			
		||||
>;
 | 
			
		||||
 | 
			
		||||
export type Connection = {
 | 
			
		||||
  connectionState: EstablishingConnectionState;
 | 
			
		||||
  signalingData?: SignalingData;
 | 
			
		||||
  dataChannelCallbacks?: DataChannelCallbacks;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const establishConnection = async ({
 | 
			
		||||
  connectionState,
 | 
			
		||||
  signalingData,
 | 
			
		||||
  dataChannelCallbacks,
 | 
			
		||||
}: Connection): Promise<Connection> => {
 | 
			
		||||
  const peerConnection = createPeerConnection();
 | 
			
		||||
 | 
			
		||||
  return await match(connectionState)
 | 
			
		||||
    .with('noConnection', async () => {
 | 
			
		||||
      const _signalingData = await createOffer(peerConnection);
 | 
			
		||||
 | 
			
		||||
      return {
 | 
			
		||||
        connectionState: 'waiting',
 | 
			
		||||
        signalingData: _signalingData,
 | 
			
		||||
        dataChannelCallbacks,
 | 
			
		||||
      } as Connection;
 | 
			
		||||
    })
 | 
			
		||||
    .with('waiting', async () => {
 | 
			
		||||
      if (!dataChannelCallbacks) {
 | 
			
		||||
        throw new Error('Data channel callbacks are not set');
 | 
			
		||||
      }
 | 
			
		||||
      if (!signalingData) {
 | 
			
		||||
        throw new Error('Signaling data is not set');
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      await peerConnection.setRemoteDescription(signalingData.sdp);
 | 
			
		||||
 | 
			
		||||
      const connection: Connection = await match(signalingData.type)
 | 
			
		||||
        .with('offer', async () => {
 | 
			
		||||
          const answer = await createAnswer(peerConnection);
 | 
			
		||||
          return {
 | 
			
		||||
            connectionState: 'waiting',
 | 
			
		||||
            signalingData: answer,
 | 
			
		||||
            dataChannelCallbacks,
 | 
			
		||||
          } as Connection;
 | 
			
		||||
        })
 | 
			
		||||
        .with('answer', () => {
 | 
			
		||||
          return {
 | 
			
		||||
            connectionState: 'established',
 | 
			
		||||
            signalingData: undefined,
 | 
			
		||||
            dataChannelCallbacks,
 | 
			
		||||
          } as Connection;
 | 
			
		||||
        })
 | 
			
		||||
        .exhaustive();
 | 
			
		||||
 | 
			
		||||
      dataChannelHandler(
 | 
			
		||||
        signalingData.type,
 | 
			
		||||
        peerConnection,
 | 
			
		||||
        dataChannelCallbacks
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      return connection;
 | 
			
		||||
    })
 | 
			
		||||
    .with('established', () => {
 | 
			
		||||
      throw new Error(
 | 
			
		||||
        "Connection can't be established because it is already established"
 | 
			
		||||
      );
 | 
			
		||||
    })
 | 
			
		||||
    .exhaustive();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default establishConnection;
 | 
			
		||||
 | 
			
		||||
export const createPeerConnection = () => {
 | 
			
		||||
  const configuration = PeerConfiguration;
 | 
			
		||||
 | 
			
		||||
  return new RTCPeerConnection(configuration);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const createOffer = async (peerConnection: RTCPeerConnection) => {
 | 
			
		||||
  const offer = await peerConnection.createOffer();
 | 
			
		||||
  await peerConnection.setLocalDescription(offer);
 | 
			
		||||
 | 
			
		||||
  if (peerConnection.localDescription) {
 | 
			
		||||
    const signalingData: SignalingData = {
 | 
			
		||||
      type: 'offer',
 | 
			
		||||
      sdp: peerConnection.localDescription,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    return signalingData;
 | 
			
		||||
  } else {
 | 
			
		||||
    throw new Error('Local description is not set');
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type DataChannelCallbacks = {
 | 
			
		||||
  onMessage: (event: MessageEvent) => void;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const dataChannelHandler = (
 | 
			
		||||
  type: SignalingDataType,
 | 
			
		||||
  peerConnection: RTCPeerConnection,
 | 
			
		||||
  DataChannelCallbacks: DataChannelCallbacks
 | 
			
		||||
) => {
 | 
			
		||||
  const { onMessage } = DataChannelCallbacks;
 | 
			
		||||
 | 
			
		||||
  match(type)
 | 
			
		||||
    .with('offer', () => {
 | 
			
		||||
      const dataChannel = peerConnection.createDataChannel('dataChannel');
 | 
			
		||||
      dataChannel.onmessage = onMessage;
 | 
			
		||||
    })
 | 
			
		||||
    .with('answer', () => {
 | 
			
		||||
      peerConnection.ondatachannel = (event) => {
 | 
			
		||||
        event.channel.onmessage = onMessage;
 | 
			
		||||
      };
 | 
			
		||||
    })
 | 
			
		||||
    .exhaustive();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const createAnswer = async (peerConnection: RTCPeerConnection) => {
 | 
			
		||||
  const answer = await peerConnection.createAnswer();
 | 
			
		||||
  await peerConnection.setLocalDescription(answer);
 | 
			
		||||
 | 
			
		||||
  if (peerConnection.localDescription) {
 | 
			
		||||
    const signalingData: SignalingData = {
 | 
			
		||||
      type: 'answer',
 | 
			
		||||
      sdp: peerConnection.localDescription,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    return signalingData;
 | 
			
		||||
  } else {
 | 
			
		||||
    throw new Error('Local description is not set');
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										24
									
								
								src/webrtc/utilities.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								src/webrtc/utilities.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,24 @@
 | 
			
		||||
import { z } from 'zod';
 | 
			
		||||
 | 
			
		||||
export const encodeSignalingData = (
 | 
			
		||||
  data: SignalingData
 | 
			
		||||
): EncodedSignalingData => {
 | 
			
		||||
  return encodeURIComponent(JSON.stringify(data));
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const decodeSignalingData = (encodedData: string) => {
 | 
			
		||||
  return JSON.parse(decodeURIComponent(encodedData)) as SignalingData;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const SignanlingDataTypeSchema = z.enum(['offer', 'answer']);
 | 
			
		||||
 | 
			
		||||
export type SignalingDataType = z.infer<typeof SignanlingDataTypeSchema>;
 | 
			
		||||
 | 
			
		||||
export type SignalingData = {
 | 
			
		||||
  type: SignalingDataType;
 | 
			
		||||
  sdp: RTCSessionDescription;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const EncodedSignalingDataSchema = z.string();
 | 
			
		||||
 | 
			
		||||
export type EncodedSignalingData = z.infer<typeof EncodedSignalingDataSchema>;
 | 
			
		||||
		Reference in New Issue
	
	Block a user