diff --git a/packages/restapi/src/lib/space/acceptPromotionRequest.ts b/packages/restapi/src/lib/space/acceptPromotionRequest.ts index 0a5ea4ea3..6ed125b25 100644 --- a/packages/restapi/src/lib/space/acceptPromotionRequest.ts +++ b/packages/restapi/src/lib/space/acceptPromotionRequest.ts @@ -36,14 +36,14 @@ export async function acceptPromotionRequest( }); // accept the promotion request - this.acceptRequest({ - signalData, - senderAddress: this.data.local.address, - recipientAddress: pCAIP10ToWallet(promoteeAddress), - chatId: spaceId, - details: { - type: SPACE_ACCEPT_REQUEST_TYPE.ACCEPT_PROMOTION, - data: {}, - }, - }); + // this.acceptRequest({ + // signalData, + // senderAddress: this.data.local.address, + // recipientAddress: pCAIP10ToWallet(promoteeAddress), + // chatId: spaceId, + // details: { + // type: SPACE_ACCEPT_REQUEST_TYPE.ACCEPT_PROMOTION, + // data: {}, + // }, + // }); } diff --git a/packages/restapi/src/lib/space/start.ts b/packages/restapi/src/lib/space/start.ts index 6a14791e7..23ac20bc9 100644 --- a/packages/restapi/src/lib/space/start.ts +++ b/packages/restapi/src/lib/space/start.ts @@ -31,14 +31,15 @@ type StartType = { livepeerApiKey: string; }; -export async function start(this: Space, options: StartType): Promise { - const { livepeerApiKey } = options || {}; +// export async function start(this: Space, options: StartType): Promise { +export async function start(this: Space): Promise { + // const { livepeerApiKey } = options || {}; try { // host should have there audio stream - if (!this.data.local.stream) { - throw new Error('Local audio stream not found'); - } + // if (!this.data.local.stream) { + // throw new Error('Local audio stream not found'); + // } const space = await get({ spaceId: this.spaceSpecificData.spaceId, @@ -115,109 +116,109 @@ export async function start(this: Space, options: StartType): Promise { }); }); - // start the livepeer playback and store the playback URL group meta - // send a notification/meta message to all the added listeners (members) telling the space has started - - // create the mergeStream object - const mergedStream = getMergeStreamObject(this.data.local.stream); - // store the mergeStreamObject - this.mergedStream = mergedStream; - - const url = 'https://livepeer.studio/api/stream'; - const data = { - name: this.spaceSpecificData.spaceName, - record: true, - }; - - const { data: responseData } = await axios.post(url, data, { - headers: { - Authorization: 'Bearer ' + livepeerApiKey, - }, - }); - - const { streamKey, playbackId } = responseData; - - console.log('livepeer details', streamKey, playbackId); - - this.update({ meta: playbackId }); - - let redirectUrl; - try { - console.log('Ignore the following error'); - - // the redirect URL from the above GET request - await axios.get(`https://livepeer.studio/webrtc/${streamKey}`); - } catch (err: any) { - console.log('redirectUrl error', err); - redirectUrl = err.request.responseURL; - } - - // we use the host from the redirect URL in the ICE server configuration - const host = new URL(redirectUrl).host; - - const iceServers = [ - { - urls: `stun:${host}`, - }, - { - urls: `turn:${host}`, - username: 'livepeer', - credential: 'livepeer', - }, - ]; - - const peerConnection = new RTCPeerConnection({ iceServers }); - - const newAudioTrack = mergedStream.result?.getAudioTracks?.()?.[0] ?? null; - - if (newAudioTrack) { - peerConnection?.addTransceiver(newAudioTrack, { - direction: 'sendonly', - }); - } - - /** - * https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/createOffer - * We create an SDP offer here which will be shared with the server - */ - const offer = await peerConnection.createOffer(); - /** https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/setLocalDescription */ - await peerConnection.setLocalDescription(offer); - - /** Wait for ICE gathering to complete */ - const ofr = await new Promise((resolve) => { - /** Wait at most five seconds for ICE gathering. */ - setTimeout(() => { - resolve(peerConnection.localDescription); - }, 5000); - peerConnection.onicegatheringstatechange = (_ev) => { - if (peerConnection.iceGatheringState === 'complete') { - resolve(peerConnection.localDescription); - } - }; - }); - if (!ofr) { - throw Error('failed to gather ICE candidates for offer'); - } - /** - * This response contains the server's SDP offer. - * This specifies how the client should communicate, - * and what kind of media client and server have negotiated to exchange. - */ - const sdpResponse = await fetch(redirectUrl, { - method: 'POST', - mode: 'cors', - headers: { - 'content-type': 'application/sdp', - }, - body: ofr.sdp, - }); - if (sdpResponse.ok) { - const answerSDP = await sdpResponse.text(); - await peerConnection.setRemoteDescription( - new RTCSessionDescription({ type: 'answer', sdp: answerSDP }) - ); - } + // // start the livepeer playback and store the playback URL group meta + // // send a notification/meta message to all the added listeners (members) telling the space has started + + // // create the mergeStream object + // const mergedStream = getMergeStreamObject(this.data.local.stream); + // // store the mergeStreamObject + // this.mergedStream = mergedStream; + + // const url = 'https://livepeer.studio/api/stream'; + // const data = { + // name: this.spaceSpecificData.spaceName, + // record: true, + // }; + + // const { data: responseData } = await axios.post(url, data, { + // headers: { + // Authorization: 'Bearer ' + livepeerApiKey, + // }, + // }); + + // const { streamKey, playbackId } = responseData; + + // console.log('livepeer details', streamKey, playbackId); + + // this.update({ meta: playbackId }); + + // let redirectUrl; + // try { + // console.log('Ignore the following error'); + + // // the redirect URL from the above GET request + // await axios.get(`https://livepeer.studio/webrtc/${streamKey}`); + // } catch (err: any) { + // console.log('redirectUrl error', err); + // redirectUrl = err.request.responseURL; + // } + + // // we use the host from the redirect URL in the ICE server configuration + // const host = new URL(redirectUrl).host; + + // const iceServers = [ + // { + // urls: `stun:${host}`, + // }, + // { + // urls: `turn:${host}`, + // username: 'livepeer', + // credential: 'livepeer', + // }, + // ]; + + // const peerConnection = new RTCPeerConnection({ iceServers }); + + // const newAudioTrack = mergedStream.result?.getAudioTracks?.()?.[0] ?? null; + + // if (newAudioTrack) { + // peerConnection?.addTransceiver(newAudioTrack, { + // direction: 'sendonly', + // }); + // } + + // /** + // * https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/createOffer + // * We create an SDP offer here which will be shared with the server + // */ + // const offer = await peerConnection.createOffer(); + // /** https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/setLocalDescription */ + // await peerConnection.setLocalDescription(offer); + + // /** Wait for ICE gathering to complete */ + // const ofr = await new Promise((resolve) => { + // /** Wait at most five seconds for ICE gathering. */ + // setTimeout(() => { + // resolve(peerConnection.localDescription); + // }, 5000); + // peerConnection.onicegatheringstatechange = (_ev) => { + // if (peerConnection.iceGatheringState === 'complete') { + // resolve(peerConnection.localDescription); + // } + // }; + // }); + // if (!ofr) { + // throw Error('failed to gather ICE candidates for offer'); + // } + // /** + // * This response contains the server's SDP offer. + // * This specifies how the client should communicate, + // * and what kind of media client and server have negotiated to exchange. + // */ + // const sdpResponse = await fetch(redirectUrl, { + // method: 'POST', + // mode: 'cors', + // headers: { + // 'content-type': 'application/sdp', + // }, + // body: ofr.sdp, + // }); + // if (sdpResponse.ok) { + // const answerSDP = await sdpResponse.text(); + // await peerConnection.setRemoteDescription( + // new RTCSessionDescription({ type: 'answer', sdp: answerSDP }) + // ); + // } console.log('Live Stream started'); } catch (err) { diff --git a/packages/uiweb/package.json b/packages/uiweb/package.json index 10f3afd3d..6645e2d69 100644 --- a/packages/uiweb/package.json +++ b/packages/uiweb/package.json @@ -5,6 +5,8 @@ "registry": "https://registry.npmjs.org/" }, "dependencies": { + "@livekit/components-react": "^1.2.2", + "@livekit/components-styles": "^1.0.6", "@livepeer/react": "^2.6.0", "@pushprotocol/socket": "^0.5.0", "@unstoppabledomains/resolution": "^8.5.0", @@ -20,10 +22,12 @@ "font-awesome": "^4.7.0", "gif-picker-react": "^1.1.0", "html-react-parser": "^1.4.13", + "livekit-client": "^1.13.3", "moment": "^2.29.4", "react-icons": "^4.10.1", "react-toastify": "^9.1.3", - "react-twitter-embed": "^4.0.4" + "react-twitter-embed": "^4.0.4", + "uuid": "^9.0.1" }, "peerDependencies": { "@pushprotocol/restapi": "^1.2.15", diff --git a/packages/uiweb/src/lib/components/space/SpaceWidget/LiveWidgetContent.tsx b/packages/uiweb/src/lib/components/space/SpaceWidget/LiveWidgetContent.tsx index 2e996e542..a4b8fcf06 100644 --- a/packages/uiweb/src/lib/components/space/SpaceWidget/LiveWidgetContent.tsx +++ b/packages/uiweb/src/lib/components/space/SpaceWidget/LiveWidgetContent.tsx @@ -1,9 +1,13 @@ -import React, { useEffect, useState, useContext } from 'react'; +import React, { useEffect, useState, useContext, useMemo } from 'react'; import styled, { ThemeProvider } from 'styled-components'; import { Player } from '@livepeer/react'; import * as PushAPI from '@pushprotocol/restapi'; import { SpaceDTO } from '@pushprotocol/restapi'; +// livekit imports +import { LiveKitRoom, ConnectionState, RoomAudioRenderer, TrackToggle, ConnectionStateToast } from '@livekit/components-react'; +import { Room, Track } from 'livekit-client'; + import { LiveSpaceProfileContainer } from './LiveSpaceProfileContainer'; import { SpaceMembersSectionModal } from './SpaceMembersSectionModal'; @@ -12,7 +16,7 @@ import { ThemeContext } from '../theme/ThemeProvider'; import CircularProgressSpinner from '../../loader/loader'; -import { Button, Image, Item, Text } from '../../../config'; +import { Button, Image, Item, LIVEKIT_SERVER_URL, Text } from '../../../config'; import MicOnIcon from '../../../icons/micon.svg'; import MicEngagedIcon from '../../../icons/MicEngage.svg'; import MuteIcon from '../../../icons/Muted.svg'; @@ -21,18 +25,22 @@ import MembersIcon from '../../../icons/Members.svg'; import { useSpaceData } from '../../../hooks'; import { SpaceStatus } from './WidgetContent'; import { pCAIP10ToWallet } from '../../../helpers'; +import { getLivekitRoomToken, performAction } from '../../../services'; +import Microphone from './Microphone'; interface LiveWidgetContentProps { spaceData?: SpaceDTO; // temp props only for testing demo purpose for now isHost?: boolean; setSpaceStatusState: React.Dispatch>; + account: string | undefined; } export const LiveWidgetContent: React.FC = ({ spaceData, isHost, setSpaceStatusState, + account }) => { const [showMembersModal, setShowMembersModal] = useState(false); const [playBackUrl, setPlayBackUrl] = useState(''); @@ -41,6 +49,7 @@ export const LiveWidgetContent: React.FC = ({ const [isRequestedForMic, setIsRequestedForMic] = useState(false); const [promotedListener, setPromotedListener] = useState(''); + const [livekitToken, setLivekitToken] = useState(null); const theme = useContext(ThemeContext); @@ -57,12 +66,27 @@ export const LiveWidgetContent: React.FC = ({ const isMicOn = spaceObjectData?.connectionData?.local?.audio; - const numberOfRequests = spaceObjectData.liveSpaceData.listeners.filter((listener: any) => listener.handRaised).length; + // const numberOfRequests = spaceObjectData.liveSpaceData.listeners.filter((listener: any) => listener.handRaised).length; const handleMicState = async () => { await spacesObjectRef?.current?.enableAudio?.({ state: !isMicOn }); }; + useEffect(() => { + (async function () { + const removeEIP155 = (input: string) => input.substring(7); + const nonEIPAddress = removeEIP155(account as string); + + if ((isHost || isSpeaker) && spaceData?.spaceId) { + const livekitToken = await getLivekitRoomToken({ userType: "sender", roomId: spaceData?.spaceId, userId: nonEIPAddress }); + setLivekitToken(livekitToken.data); + } else if (isListener && spaceData?.spaceId) { + const livekitToken = await getLivekitRoomToken({ userType: "receiver", roomId: spaceData?.spaceId, userId: nonEIPAddress }); + setLivekitToken(livekitToken.data); + } + })(); + }, [isListener, isHost, spaceData]); + useEffect(() => { if (!spaceObjectData?.connectionData?.local?.stream || !isRequestedForMic) return; @@ -83,11 +107,13 @@ export const LiveWidgetContent: React.FC = ({ }; useEffect(() => { - if (!spaceObjectData?.connectionData?.local?.stream || promotedListener.length === 0) + // if (!spaceObjectData?.connectionData?.local?.stream || promotedListener.length === 0) + // return; + if (promotedListener.length === 0) return; const options = { - signalData: raisedHandInfo[promotedListener].signalData, + // signalData: raisedHandInfo[promotedListener].signalData, promoteeAddress: pCAIP10ToWallet( raisedHandInfo[promotedListener].senderAddress ), @@ -102,14 +128,16 @@ export const LiveWidgetContent: React.FC = ({ }, [promotedListener]); const handleAcceptPromotion = async (requesterAddress: any) => { - await spacesObjectRef?.current?.createAudioStream?.(); + // await spacesObjectRef?.current?.createAudioStream?.(); setPromotedListener(requesterAddress); + await performAction({ roomId: spaceData?.spaceId, userId: requesterAddress, canPublish: true }); }; const handleRejectPromotion = async (requesterAddress: any) => { await spacesObjectRef?.current?.rejectPromotionRequest?.({ promoteeAddress: pCAIP10ToWallet(requesterAddress), }); + await performAction({ roomId: spaceData?.spaceId, userId: requesterAddress, canPublish: false }); }; const handleJoinSpace = async () => { @@ -204,6 +232,8 @@ export const LiveWidgetContent: React.FC = ({ setPlayBackUrl(spaceObjectData?.meta); }, [spaceObjectData?.meta]); + const livekitRoom = useMemo(() => new Room(), []); + return ( = ({ justifyContent={'space-between'} padding={'6px 8px'} > - - isHost || isSpeaker ? handleMicState() : handleRequest() - } - > - Mic Icon - + + {isHost || isSpeaker - ? isMicOn - ? 'Speaking' - : 'Muted' - : isRequestedForMic - ? 'Requested' - : 'Request' + ? + + + + : + handleRequest()} + > + Mic Icon + + { + isRequestedForMic + ? 'Requested' + : 'Request' + } + + } - - + + )} + {/* + isHost || isSpeaker ? handleMicState() : handleRequest() + } + > + Mic Icon + + {isHost || isSpeaker + ? isMicOn + ? 'Speaking' + : 'Muted' + : isRequestedForMic + ? 'Requested' + : 'Request' + } + + */} - { + {/* { isHost && numberOfRequests ? {numberOfRequests} : null - } + } */} = ({ {!isHost ? 'Leave' : 'End space'} - {isListener && !isHost && playBackUrl.length > 0 && ( + {/* {isListener && !isHost && playBackUrl.length > 0 && ( - )} + )} */} ) : (