From 16fa2653ab00899d420d004068e027a13a504153 Mon Sep 17 00:00:00 2001 From: Madhur Gupta Date: Mon, 18 Dec 2023 15:28:44 +0530 Subject: [PATCH 01/32] feat(video-v2): add highlevel video class --- packages/restapi/src/lib/pushapi/PushAPI.ts | 8 + .../restapi/src/lib/pushapi/pushAPITypes.ts | 18 +- packages/restapi/src/lib/pushapi/video.ts | 69 ++++++ packages/restapi/src/lib/types/index.ts | 1 + packages/restapi/src/lib/types/videoTypes.ts | 8 + packages/restapi/src/lib/video/VideoV2.ts | 209 ++++++++++++++++++ .../src/lib/video/helpers/validatePeerInfo.ts | 19 ++ 7 files changed, 330 insertions(+), 2 deletions(-) create mode 100644 packages/restapi/src/lib/pushapi/video.ts create mode 100644 packages/restapi/src/lib/types/videoTypes.ts create mode 100644 packages/restapi/src/lib/video/VideoV2.ts create mode 100644 packages/restapi/src/lib/video/helpers/validatePeerInfo.ts diff --git a/packages/restapi/src/lib/pushapi/PushAPI.ts b/packages/restapi/src/lib/pushapi/PushAPI.ts index 0549b5643..1c9f71c1e 100644 --- a/packages/restapi/src/lib/pushapi/PushAPI.ts +++ b/packages/restapi/src/lib/pushapi/PushAPI.ts @@ -16,6 +16,7 @@ import { STREAM, } from '../pushstream/pushStreamTypes'; import { ALPHA_FEATURE_CONFIG } from '../config'; +import { Video } from './video'; export class PushAPI { private signer?: SignerType; @@ -28,6 +29,8 @@ export class PushAPI { private progressHook?: (progress: ProgressHookType) => void; public chat: Chat; // Public instances to be accessed from outside the class + public video: Video; + public profile: Profile; public encryption: Encryption; private user: User; @@ -81,6 +84,11 @@ export class PushAPI { this.progressHook ); this.user = new User(this.account, this.env); + + this.video = new Video(this.account, + this.env, + this.decryptedPgpPvtKey, + this.signer) } // Overloaded initialize method signatures static async initialize( diff --git a/packages/restapi/src/lib/pushapi/pushAPITypes.ts b/packages/restapi/src/lib/pushapi/pushAPITypes.ts index 278e43250..76300613d 100644 --- a/packages/restapi/src/lib/pushapi/pushAPITypes.ts +++ b/packages/restapi/src/lib/pushapi/pushAPITypes.ts @@ -1,5 +1,5 @@ import Constants, { ENV } from '../constants'; -import { ChatMemberCounts, ChatMemberProfile, ChatStatus, ProgressHookType, Rules } from '../types'; +import { ChatStatus, ProgressHookType, Rules, SignerType } from '../types'; export enum ChatListType { CHATS = 'CHATS', @@ -66,4 +66,18 @@ export interface ParticipantStatus { pending: boolean; role: 'ADMIN' | 'MEMBER'; participant: boolean; -} \ No newline at end of file +} + +export interface VideoInitializeOptions { + /* + - If the signer and decryptedPgpPvtKey were not provided during the initialization of the PushAPI class, + - They can be provided when initializing the video. + */ + signer?: SignerType; + decryptedPgpPvtKey?: string; + media: { + video?: boolean; + audio?: boolean; + }; + stream?: MediaStream; +} diff --git a/packages/restapi/src/lib/pushapi/video.ts b/packages/restapi/src/lib/pushapi/video.ts new file mode 100644 index 000000000..5a5167adc --- /dev/null +++ b/packages/restapi/src/lib/pushapi/video.ts @@ -0,0 +1,69 @@ +import { ENV } from '../constants'; +import { SignerType, VideoCallData } from '../types'; + +import { Video as VideoV1 } from '../video/Video'; +import { VideoV2 } from '../video/VideoV2'; +import { VideoInitializeOptions } from './pushAPITypes'; + +export class Video { + constructor( + private account: string, + private env: ENV, + private decryptedPgpPvtKey?: string, + private signer?: SignerType + ) {} + + async initialize( + setVideoData: (fn: (data: VideoCallData) => VideoCallData) => void, + options?: VideoInitializeOptions + ) { + const { media, signer, decryptedPgpPvtKey, stream } = options || {}; + + const chainId = await this.signer?.getChainId(); + + if (!chainId) { + throw new Error('Chain Id not retrievable from signer'); + } + + if (!this.signer && !signer) { + throw new Error('Signer is required for push video'); + } + + if (!this.decryptedPgpPvtKey && !decryptedPgpPvtKey) { + throw new Error('Decrypted PGP private key is required for push video'); + } + + this.decryptedPgpPvtKey ??= decryptedPgpPvtKey; + this.signer ??= signer; + + // Initialize the video instance with the provided options + const videoV1Instance = new VideoV1({ + signer: this.signer!, + chainId, + pgpPrivateKey: this.decryptedPgpPvtKey!, + env: this.env, + setData: setVideoData, + }); + + // Create the media stream with the provided options + await videoV1Instance.create({ + ...(stream && { + stream, + }), + ...(media?.audio && { + audio: media.audio, + }), + ...(media?.video && { + video: media.video, + }), + }); + + // Return an instance of the video v2 class + return new VideoV2({ + videoV1Instance, + account: this.account, + decryptedPgpPvtKey: this.decryptedPgpPvtKey!, + env: this.env, + }); + } +} diff --git a/packages/restapi/src/lib/types/index.ts b/packages/restapi/src/lib/types/index.ts index d091d909f..0435bfc05 100644 --- a/packages/restapi/src/lib/types/index.ts +++ b/packages/restapi/src/lib/types/index.ts @@ -12,6 +12,7 @@ import { ENV, MessageType } from '../constants'; import { EthEncryptedData } from '@metamask/eth-sig-util'; import { Message, MessageObj } from './messageTypes'; export * from './messageTypes'; +export * from './videoTypes'; export type Env = typeof ENV[keyof typeof ENV]; diff --git a/packages/restapi/src/lib/types/videoTypes.ts b/packages/restapi/src/lib/types/videoTypes.ts new file mode 100644 index 000000000..aed9fae26 --- /dev/null +++ b/packages/restapi/src/lib/types/videoTypes.ts @@ -0,0 +1,8 @@ +export type VideoPeerInfo = { + address: string; + signal: any; + meta: { + // TODO: replace this type once old PR is merged + rules: any; + }; +}; diff --git a/packages/restapi/src/lib/video/VideoV2.ts b/packages/restapi/src/lib/video/VideoV2.ts new file mode 100644 index 000000000..9bba2b2f6 --- /dev/null +++ b/packages/restapi/src/lib/video/VideoV2.ts @@ -0,0 +1,209 @@ +import { chats } from '../chat'; +import { ENV } from '../constants'; +import { + isValidETHAddress, + pCAIP10ToWallet, + walletToPCAIP10, +} from '../helpers'; +import { VideoPeerInfo } from '../types'; +import { Video as VideoV1 } from './Video'; +import { validatePeerInfo } from './helpers/validatePeerInfo'; + +/** + * VideoV2 class + */ +export class VideoV2 { + private account: string; + private decryptedPgpPvtKey: string; + private env: ENV; + + private videoInstance: VideoV1; + + /** + * VideoV2 constructor + * @param {object} params - The constructor parameters + * @param {VideoV1} params.videoV1Instance - The VideoV1 instance + * @param {string} params.account - The account + * @param {string} params.decryptedPgpPvtKey - The decrypted PGP private key + * @param {ENV} params.env - The environment + */ + constructor({ + videoV1Instance, + account, + decryptedPgpPvtKey, + env, + }: { + videoV1Instance: VideoV1; + account: string; + decryptedPgpPvtKey: string; + env: ENV; + }) { + this.videoInstance = videoV1Instance; + this.account = account; + this.decryptedPgpPvtKey = decryptedPgpPvtKey; + this.env = env; + } + + /** + * Request a video call + * @param {string[]} recipients - The recipients of the video call + * @param {object} options - The options for the video call + * @param {object} options.rules - The rules for the video call + * @param {object} options.rules.access - The access rules for the video call + * @param {string} options.rules.access.type - The type of the video call + * @param {object} options.rules.access.data - The data for the video call + * @param {string} options.rules.access.data.chatId - The chat ID for the video call + */ + async request( + recipients: string[], + options?: { + rules: { + access: { + // TODO: Replace type once the initial video PR is merged + type: ''; + data: { + chatId?: string; + }; + }; + }; + } + ) { + const { rules } = options || {}; + + for (const recipient of recipients) { + if (!isValidETHAddress(recipient)) { + throw new Error('Invalid recipient address found'); + } + } + + if (recipients.length === 0) { + throw new Error( + 'Alteast one recipient address is required for a video call' + ); + } + + // TODO: Update the rules type after the PR is merged, type should be push chat + if ( + recipients.length > 1 && + rules?.access.type === '' && + !rules.access.data.chatId + ) { + throw new Error( + 'For multiple recipient addresses, chatId is required for a video call' + ); + } + + // If chatId is not passed, find a w2w chat between the addresses and use the chatId from there + let retrievedChatId = ''; + if (!rules?.access.data.chatId) { + let page = 1; + const limit = 30; + while (!retrievedChatId) { + const response = await chats({ + account: this.account, + toDecrypt: true, + pgpPrivateKey: this.decryptedPgpPvtKey, + env: this.env, + page, + limit, + }); + + if (response.length === 0) break; + + response.forEach((chat) => { + if (chat.did === walletToPCAIP10(recipients[0]) && chat.chatId) { + retrievedChatId = chat.chatId; + } + }); + + page++; + } + + if (!retrievedChatId) { + throw new Error( + `ChatId not found between local user (${this.account}) and recipient (${recipients[0]}).` + ); + } + } + + await this.videoInstance.request({ + senderAddress: pCAIP10ToWallet(this.account), + recipientAddress: recipients.map((recipient) => + pCAIP10ToWallet(recipient) + ), + chatId: rules?.access.data.chatId ?? retrievedChatId, + }); + } + + /** + * Approve a video call + * @param {VideoPeerInfo} peerInfo - The peer information + */ + async approve(peerInfo: VideoPeerInfo) { + validatePeerInfo(peerInfo); + + const { signal, address, meta } = peerInfo; + + await this.videoInstance.acceptRequest({ + senderAddress: pCAIP10ToWallet(this.account), + recipientAddress: pCAIP10ToWallet(address), + signalData: signal, + chatId: meta.rules.access.data, + }); + } + + /** + * Deny a video call + * @param {VideoPeerInfo} peerInfo - The peer information + */ + async deny(peerInfo: VideoPeerInfo) { + validatePeerInfo(peerInfo); + + const { address } = peerInfo; + + await this.videoInstance.disconnect({ + peerAddress: pCAIP10ToWallet(address), + }); + } + + /** + * Connect to a video call + * @param {VideoPeerInfo} peerInfo - The peer information + */ + async connect(peerInfo: VideoPeerInfo) { + validatePeerInfo(peerInfo); + + const { signal, address } = peerInfo; + + await this.videoInstance.connect({ + peerAddress: address, + signalData: signal, + }); + } + + /** + * Disconnect from a video call + * @param {string} address - The address to disconnect from + */ + async disconnect(address: string) { + await this.videoInstance.disconnect({ + peerAddress: pCAIP10ToWallet(address), + }); + } + + /** + * Enable or disable media + * @param {object} params - The parameters + * @param {boolean} params.video - The video state + * @param {boolean} params.audio - The audio state + */ + media({ video, audio }: { video?: boolean; audio?: boolean }) { + if (typeof video === 'boolean') { + this.videoInstance.enableVideo({ state: video }); + } + + if (typeof audio === 'boolean') { + this.videoInstance.enableAudio({ state: audio }); + } + } +} diff --git a/packages/restapi/src/lib/video/helpers/validatePeerInfo.ts b/packages/restapi/src/lib/video/helpers/validatePeerInfo.ts new file mode 100644 index 000000000..c793026ac --- /dev/null +++ b/packages/restapi/src/lib/video/helpers/validatePeerInfo.ts @@ -0,0 +1,19 @@ +import { isValidETHAddress } from '../../helpers'; +import { VideoPeerInfo } from '../../types'; + +export const validatePeerInfo = (peerInfo: VideoPeerInfo) => { + const { signal, address, meta } = peerInfo; + + if (!signal) { + throw new Error('Invalid signal data received'); + } + + if (!isValidETHAddress(address)) { + throw new Error('Invalid address received'); + } + + // TODO: comparing type should be PUSH_CHAT + if (meta.rules.access.type === '' && !meta.rules.access.data.chatId) { + throw new Error('ChatId not found in meta.rules'); + } +}; From ee8f2f9a9786f47722f17104e38cf4dbbc922603 Mon Sep 17 00:00:00 2001 From: Madhur Gupta Date: Thu, 21 Dec 2023 12:00:19 +0530 Subject: [PATCH 02/32] feat(video): add video stream --- .../src/lib/pushstream/DataModifier.ts | 56 +++++++++++++++++++ .../restapi/src/lib/pushstream/PushStream.ts | 53 ++++++++++++++---- .../src/lib/pushstream/pushStreamTypes.ts | 20 ++++++- 3 files changed, 117 insertions(+), 12 deletions(-) diff --git a/packages/restapi/src/lib/pushstream/DataModifier.ts b/packages/restapi/src/lib/pushstream/DataModifier.ts index 8b18e8cf1..182b1fefe 100644 --- a/packages/restapi/src/lib/pushstream/DataModifier.ts +++ b/packages/restapi/src/lib/pushstream/DataModifier.ts @@ -17,7 +17,12 @@ import { NotificationType, NOTIFICATION, ProposedEventNames, + VideoEventType, + MessageOrigin, + VideoEvent, } from './pushStreamTypes'; +import { VideoCallStatus, VideoPeerInfo } from '../types'; +import { VideoDataType } from '../video'; export class DataModifier { public static handleChatGroupEvent(data: any, includeRaw = false): any { @@ -388,4 +393,55 @@ export class DataModifier { break; } } + + public static convertToProposedNameForVideo( + currentVideoStatus: VideoCallStatus + ): VideoEventType { + switch (currentVideoStatus) { + case VideoCallStatus.INITIALIZED: + return VideoEventType.RequestVideo; + case VideoCallStatus.RECEIVED: + return VideoEventType.ApproveVideo; + case VideoCallStatus.DISCONNECTED: + return VideoEventType.DenyVideo; + default: + throw new Error(`Unknown video call status: ${currentVideoStatus}`); + } + } + + public static mapToVideoEvent( + data: any, + origin: MessageOrigin, + includeRaw = false + ): VideoEvent { + const { senderAddress, signalData, status }: VideoDataType = JSON.parse( + data.payload.data.additionalMeta?.data + ); + + const peerInfo: VideoPeerInfo = { + address: senderAddress, + signal: signalData, + meta: { + rules: data.payload.rules, + }, + }; + + const videoEventType: VideoEventType = + DataModifier.convertToProposedNameForVideo(status); + + const videoEvent: VideoEvent = { + event: videoEventType, + origin: origin, + timestamp: data.epoch, + peerInfo, + }; + + if (includeRaw) { + videoEvent.raw = { + verificationProof: data.payload.verificationProof, + }; + } + + return videoEvent; + } } diff --git a/packages/restapi/src/lib/pushstream/PushStream.ts b/packages/restapi/src/lib/pushstream/PushStream.ts index 192dd42a5..0323b60f6 100644 --- a/packages/restapi/src/lib/pushstream/PushStream.ts +++ b/packages/restapi/src/lib/pushstream/PushStream.ts @@ -4,15 +4,17 @@ import { ENV, PACKAGE_BUILD } from '../constants'; import { GroupEventType, MessageEventType, + MessageOrigin, NotificationEventType, PushStreamInitializeProps, - STREAM, + STREAM } from './pushStreamTypes'; import { DataModifier } from './DataModifier'; import { pCAIP10ToWallet, walletToPCAIP10 } from '../helpers'; import { Chat } from '../pushapi/chat'; import { ProgressHookType, SignerType } from '../types'; import { ALPHA_FEATURE_CONFIG } from '../config'; +import { ADDITIONAL_META_TYPE } from '../payloads'; export class PushStream extends EventEmitter { private pushChatSocket: any; @@ -102,7 +104,8 @@ export class PushStream extends EventEmitter { !this.listen || this.listen.length === 0 || this.listen.includes(STREAM.NOTIF) || - this.listen.includes(STREAM.NOTIF_OPS); + this.listen.includes(STREAM.NOTIF_OPS) || + this.listen.includes(STREAM.VIDEO); let isChatSocketConnected = false; let isNotifSocketConnected = false; @@ -312,16 +315,33 @@ export class PushStream extends EventEmitter { this.pushNotificationSocket.on(EVENTS.USER_FEEDS, (data: any) => { try { - const modifiedData = DataModifier.mapToNotificationEvent( - data, - NotificationEventType.INBOX, - this.account === data.sender ? 'self' : 'other', - this.raw - ); + if ( + data.payload.data.additionalMeta?.type === + `${ADDITIONAL_META_TYPE.PUSH_VIDEO}+1` && + shouldEmit(STREAM.VIDEO) && + this.shouldEmitVideo(data.sender) + ) { + // Video Notification + const modifiedData = DataModifier.mapToVideoEvent( + data, + this.account === data.sender ? MessageOrigin.Self : MessageOrigin.Other, + this.raw + ); - if (this.shouldEmitChannel(modifiedData.from)) { - if (shouldEmit(STREAM.NOTIF)) { - this.emit(STREAM.NOTIF, modifiedData); + this.emit(STREAM.VIDEO, modifiedData); + } else { + // Channel Notification + const modifiedData = DataModifier.mapToNotificationEvent( + data, + NotificationEventType.INBOX, + this.account === data.sender ? 'self' : 'other', + this.raw + ); + + if (this.shouldEmitChannel(modifiedData.from)) { + if (shouldEmit(STREAM.NOTIF)) { + this.emit(STREAM.NOTIF, modifiedData); + } } } } catch (error) { @@ -396,4 +416,15 @@ export class PushStream extends EventEmitter { } return this.options.filter.channels.includes(dataChannelId); } + + private shouldEmitVideo(dataVideoId: string): boolean { + if ( + !this.options.filter?.video || + this.options.filter.video.length === 0 || + this.options.filter.video.includes('*') + ) { + return true; + } + return this.options.filter.video.includes(dataVideoId); + } } diff --git a/packages/restapi/src/lib/pushstream/pushStreamTypes.ts b/packages/restapi/src/lib/pushstream/pushStreamTypes.ts index 7e0cbbad9..84e30792b 100644 --- a/packages/restapi/src/lib/pushstream/pushStreamTypes.ts +++ b/packages/restapi/src/lib/pushstream/pushStreamTypes.ts @@ -1,10 +1,11 @@ -import { Rules } from '../types'; +import { Rules, VideoPeerInfo } from '../types'; import { ENV } from '../constants'; export type PushStreamInitializeProps = { filter?: { channels?: string[]; chats?: string[]; + video?: string[]; }; connection?: { auto?: boolean; @@ -22,6 +23,7 @@ export enum STREAM { NOTIF_OPS = 'STREAM.NOTIF_OPS', CHAT = 'STREAM.CHAT', CHAT_OPS = 'STREAM.CHAT_OPS', + VIDEO = 'STREAM.VIDEO', CONNECT = 'STREAM.CONNECT', DISCONNECT = 'STREAM.DISCONNECT', } @@ -51,6 +53,14 @@ export enum GroupEventType { Remove = 'remove', } +export enum VideoEventType { + RequestVideo = 'video.request', + ApproveVideo = 'video.approve', + DenyVideo = 'video.deny', + ConnectVideo = 'video.connect', + DisconnectVideo = 'video.disconnect', +} + export enum ProposedEventNames { Message = 'chat.message', Request = 'chat.request', @@ -224,3 +234,11 @@ export interface MessageRawData { verificationProof: string; previousReference: string; } + +export interface VideoEvent { + event: VideoEventType; + origin: MessageOrigin; + timestamp: string; + peerInfo: VideoPeerInfo; + raw?: GroupEventRawData; +} From c8b2a0784f2c5abf77a22113a22bc51a886c49d4 Mon Sep 17 00:00:00 2001 From: Madhur Gupta Date: Thu, 21 Dec 2023 16:12:27 +0530 Subject: [PATCH 03/32] fix(sendnotification): modify rules.access.data to be an object & code cleanup --- .../src/lib/payloads/sendNotifications.ts | 4 ++-- .../restapi/src/lib/pushapi/pushAPITypes.ts | 5 ++--- packages/restapi/src/lib/pushapi/video.ts | 5 ++--- packages/restapi/src/lib/types/index.ts | 4 +++- packages/restapi/src/lib/types/videoTypes.ts | 5 +++-- packages/restapi/src/lib/video/Video.ts | 15 ++++++++++---- packages/restapi/src/lib/video/VideoV2.ts | 20 ++++++------------- .../helpers/sendVideoCallNotification.ts | 2 +- .../src/lib/video/helpers/validatePeerInfo.ts | 7 +++++-- .../lib/video/helpers/validateVideoRules.ts | 13 ++++++++++++ .../lib/video/sendVideoNotification.test.ts | 2 +- 11 files changed, 49 insertions(+), 33 deletions(-) create mode 100644 packages/restapi/src/lib/video/helpers/validateVideoRules.ts diff --git a/packages/restapi/src/lib/payloads/sendNotifications.ts b/packages/restapi/src/lib/payloads/sendNotifications.ts index 6101d6a54..09fd65085 100644 --- a/packages/restapi/src/lib/payloads/sendNotifications.ts +++ b/packages/restapi/src/lib/payloads/sendNotifications.ts @@ -185,7 +185,7 @@ export async function sendNotification(options: ISendNotificationInputOptions) { uuid, // for the pgpv2 verfication proof chatId: - rules?.access.data ?? // for backwards compatibilty with 'chatId' param + rules?.access.data.chatId ?? // for backwards compatibilty with 'chatId' param chatId, pgpPrivateKey, }); @@ -231,7 +231,7 @@ export async function sendNotification(options: ISendNotificationInputOptions) { ? { rules: rules ?? { access: { - data: chatId, + data: { chatId }, type: VIDEO_NOTIFICATION_ACCESS_TYPE.PUSH_CHAT, }, }, diff --git a/packages/restapi/src/lib/pushapi/pushAPITypes.ts b/packages/restapi/src/lib/pushapi/pushAPITypes.ts index 76300613d..04790642d 100644 --- a/packages/restapi/src/lib/pushapi/pushAPITypes.ts +++ b/packages/restapi/src/lib/pushapi/pushAPITypes.ts @@ -70,11 +70,10 @@ export interface ParticipantStatus { export interface VideoInitializeOptions { /* - - If the signer and decryptedPgpPvtKey were not provided during the initialization of the PushAPI class, - - They can be provided when initializing the video. + - If the signer was not provided during the initialization of the PushAPI class, + - It can be provided when initializing video. */ signer?: SignerType; - decryptedPgpPvtKey?: string; media: { video?: boolean; audio?: boolean; diff --git a/packages/restapi/src/lib/pushapi/video.ts b/packages/restapi/src/lib/pushapi/video.ts index 5a5167adc..a1146a668 100644 --- a/packages/restapi/src/lib/pushapi/video.ts +++ b/packages/restapi/src/lib/pushapi/video.ts @@ -17,7 +17,7 @@ export class Video { setVideoData: (fn: (data: VideoCallData) => VideoCallData) => void, options?: VideoInitializeOptions ) { - const { media, signer, decryptedPgpPvtKey, stream } = options || {}; + const { media, signer, stream } = options || {}; const chainId = await this.signer?.getChainId(); @@ -29,11 +29,10 @@ export class Video { throw new Error('Signer is required for push video'); } - if (!this.decryptedPgpPvtKey && !decryptedPgpPvtKey) { + if (!this.decryptedPgpPvtKey) { throw new Error('Decrypted PGP private key is required for push video'); } - this.decryptedPgpPvtKey ??= decryptedPgpPvtKey; this.signer ??= signer; // Initialize the video instance with the provided options diff --git a/packages/restapi/src/lib/types/index.ts b/packages/restapi/src/lib/types/index.ts index 54fa77a9a..0a04ca344 100644 --- a/packages/restapi/src/lib/types/index.ts +++ b/packages/restapi/src/lib/types/index.ts @@ -87,7 +87,9 @@ export type ParsedResponseType = { export interface VideNotificationRules { access: { type: VIDEO_NOTIFICATION_ACCESS_TYPE; - data: string; + data: { + chatId?: string; + }; }; } diff --git a/packages/restapi/src/lib/types/videoTypes.ts b/packages/restapi/src/lib/types/videoTypes.ts index aed9fae26..1e9904d1f 100644 --- a/packages/restapi/src/lib/types/videoTypes.ts +++ b/packages/restapi/src/lib/types/videoTypes.ts @@ -1,8 +1,9 @@ +import { VideNotificationRules } from "."; + export type VideoPeerInfo = { address: string; signal: any; meta: { - // TODO: replace this type once old PR is merged - rules: any; + rules: VideNotificationRules; }; }; diff --git a/packages/restapi/src/lib/video/Video.ts b/packages/restapi/src/lib/video/Video.ts index 380e3c507..07da6605b 100644 --- a/packages/restapi/src/lib/video/Video.ts +++ b/packages/restapi/src/lib/video/Video.ts @@ -35,6 +35,7 @@ import { SPACE_REQUEST_TYPE, VIDEO_CALL_TYPE, } from '../payloads/constants'; +import { validateVideoRules } from './helpers/validateVideoRules'; export const initVideoCallData: VideoCallData = { meta: { @@ -171,6 +172,9 @@ export class Video { details, } = options || {}; + // If rules object is passed, validate it + rules && validateVideoRules(rules); + const recipientAddresses = Array.isArray(recipientAddress) ? recipientAddress : [recipientAddress]; @@ -181,7 +185,7 @@ export class Video { this.setData((oldData) => { return produce(oldData, (draft) => { draft.local.address = senderAddress; - draft.meta.chatId = chatId ?? rules!.access.data; + draft.meta.chatId = chatId ?? rules!.access.data.chatId!; draft.meta.initiator.address = senderAddress; const incomingIndex = getIncomingIndexFromAddress( @@ -382,7 +386,7 @@ export class Video { this.setData(() => initVideoCallData); } } - } else if(onReceiveMessage) { + } else if (onReceiveMessage) { onReceiveMessage(data); } }); @@ -424,6 +428,9 @@ export class Video { details, } = options || {}; + // If rules object is passed, validate it + rules && validateVideoRules(rules); + try { // if peerInstance is not null -> acceptRequest/request was called before if (this.peerInstances[recipientAddress]) { @@ -448,7 +455,7 @@ export class Video { this.setData((oldData) => { return produce(oldData, (draft) => { draft.local.address = senderAddress; - draft.meta.chatId = chatId ?? rules!.access.data; + draft.meta.chatId = chatId ?? rules!.access.data.chatId!; draft.meta.initiator.address = senderAddress; const incomingIndex = getIncomingIndexFromAddress( @@ -674,7 +681,7 @@ export class Video { this.setData(() => initVideoCallData); } } - } else if(onReceiveMessage) { + } else if (onReceiveMessage) { onReceiveMessage(data); } }); diff --git a/packages/restapi/src/lib/video/VideoV2.ts b/packages/restapi/src/lib/video/VideoV2.ts index 9bba2b2f6..213b9a88a 100644 --- a/packages/restapi/src/lib/video/VideoV2.ts +++ b/packages/restapi/src/lib/video/VideoV2.ts @@ -5,7 +5,8 @@ import { pCAIP10ToWallet, walletToPCAIP10, } from '../helpers'; -import { VideoPeerInfo } from '../types'; +import { VIDEO_NOTIFICATION_ACCESS_TYPE } from '../payloads/constants'; +import { VideNotificationRules, VideoPeerInfo } from '../types'; import { Video as VideoV1 } from './Video'; import { validatePeerInfo } from './helpers/validatePeerInfo'; @@ -57,15 +58,7 @@ export class VideoV2 { async request( recipients: string[], options?: { - rules: { - access: { - // TODO: Replace type once the initial video PR is merged - type: ''; - data: { - chatId?: string; - }; - }; - }; + rules: VideNotificationRules; } ) { const { rules } = options || {}; @@ -82,10 +75,9 @@ export class VideoV2 { ); } - // TODO: Update the rules type after the PR is merged, type should be push chat if ( recipients.length > 1 && - rules?.access.type === '' && + rules?.access.type === VIDEO_NOTIFICATION_ACCESS_TYPE.PUSH_CHAT && !rules.access.data.chatId ) { throw new Error( @@ -148,7 +140,7 @@ export class VideoV2 { senderAddress: pCAIP10ToWallet(this.account), recipientAddress: pCAIP10ToWallet(address), signalData: signal, - chatId: meta.rules.access.data, + chatId: meta.rules.access.data.chatId, }); } @@ -192,7 +184,7 @@ export class VideoV2 { } /** - * Enable or disable media + * Enable or disable media (video, audio) * @param {object} params - The parameters * @param {boolean} params.video - The video state * @param {boolean} params.audio - The audio state diff --git a/packages/restapi/src/lib/video/helpers/sendVideoCallNotification.ts b/packages/restapi/src/lib/video/helpers/sendVideoCallNotification.ts index 4675e5be4..f6d10a501 100644 --- a/packages/restapi/src/lib/video/helpers/sendVideoCallNotification.ts +++ b/packages/restapi/src/lib/video/helpers/sendVideoCallNotification.ts @@ -60,7 +60,7 @@ const sendVideoCallNotification = async ( const videoData: VideoDataType = { recipientAddress, senderAddress, - chatId: rules?.access.data ?? chatId, + chatId: rules?.access.data.chatId ?? chatId, signalData, status, callDetails, diff --git a/packages/restapi/src/lib/video/helpers/validatePeerInfo.ts b/packages/restapi/src/lib/video/helpers/validatePeerInfo.ts index c793026ac..b72f20a4b 100644 --- a/packages/restapi/src/lib/video/helpers/validatePeerInfo.ts +++ b/packages/restapi/src/lib/video/helpers/validatePeerInfo.ts @@ -1,4 +1,5 @@ import { isValidETHAddress } from '../../helpers'; +import { VIDEO_NOTIFICATION_ACCESS_TYPE } from '../../payloads/constants'; import { VideoPeerInfo } from '../../types'; export const validatePeerInfo = (peerInfo: VideoPeerInfo) => { @@ -12,8 +13,10 @@ export const validatePeerInfo = (peerInfo: VideoPeerInfo) => { throw new Error('Invalid address received'); } - // TODO: comparing type should be PUSH_CHAT - if (meta.rules.access.type === '' && !meta.rules.access.data.chatId) { + if ( + meta.rules.access.type === VIDEO_NOTIFICATION_ACCESS_TYPE.PUSH_CHAT && + !meta.rules.access.data.chatId + ) { throw new Error('ChatId not found in meta.rules'); } }; diff --git a/packages/restapi/src/lib/video/helpers/validateVideoRules.ts b/packages/restapi/src/lib/video/helpers/validateVideoRules.ts new file mode 100644 index 000000000..f5c8a40a6 --- /dev/null +++ b/packages/restapi/src/lib/video/helpers/validateVideoRules.ts @@ -0,0 +1,13 @@ +import { VIDEO_NOTIFICATION_ACCESS_TYPE } from '../../payloads/constants'; +import { VideNotificationRules } from '../../types'; + +export const validateVideoRules = (rules: VideNotificationRules) => { + if ( + rules.access.type === VIDEO_NOTIFICATION_ACCESS_TYPE.PUSH_CHAT && + (!rules.access.data.chatId || rules.access.data.chatId === '') + ) { + throw new Error( + 'Invalid rules object recieved. For access as Push Chat, chatId is required!' + ); + } +}; diff --git a/packages/restapi/tests/lib/video/sendVideoNotification.test.ts b/packages/restapi/tests/lib/video/sendVideoNotification.test.ts index d45055b25..bbc8c73e3 100644 --- a/packages/restapi/tests/lib/video/sendVideoNotification.test.ts +++ b/packages/restapi/tests/lib/video/sendVideoNotification.test.ts @@ -113,7 +113,7 @@ describe('sendNotification functionality for video calls', () => { rules: { access: { type: VIDEO_NOTIFICATION_ACCESS_TYPE.PUSH_CHAT, - data: chatId, + data: { chatId }, }, }, notification: { From 0fff331c32a5b3f4de2edbfe8686d9a039e199c6 Mon Sep 17 00:00:00 2001 From: Madhur Gupta Date: Fri, 22 Dec 2023 16:26:37 +0530 Subject: [PATCH 04/32] fix(video): remove signer from input, throw err if signer, decrypted pgp key not found --- packages/restapi/src/lib/pushapi/pushAPITypes.ts | 5 ----- packages/restapi/src/lib/pushapi/video.ts | 8 +++----- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/packages/restapi/src/lib/pushapi/pushAPITypes.ts b/packages/restapi/src/lib/pushapi/pushAPITypes.ts index 04790642d..d64852592 100644 --- a/packages/restapi/src/lib/pushapi/pushAPITypes.ts +++ b/packages/restapi/src/lib/pushapi/pushAPITypes.ts @@ -69,11 +69,6 @@ export interface ParticipantStatus { } export interface VideoInitializeOptions { - /* - - If the signer was not provided during the initialization of the PushAPI class, - - It can be provided when initializing video. - */ - signer?: SignerType; media: { video?: boolean; audio?: boolean; diff --git a/packages/restapi/src/lib/pushapi/video.ts b/packages/restapi/src/lib/pushapi/video.ts index a1146a668..edf45775f 100644 --- a/packages/restapi/src/lib/pushapi/video.ts +++ b/packages/restapi/src/lib/pushapi/video.ts @@ -17,7 +17,7 @@ export class Video { setVideoData: (fn: (data: VideoCallData) => VideoCallData) => void, options?: VideoInitializeOptions ) { - const { media, signer, stream } = options || {}; + const { media, stream } = options || {}; const chainId = await this.signer?.getChainId(); @@ -25,16 +25,14 @@ export class Video { throw new Error('Chain Id not retrievable from signer'); } - if (!this.signer && !signer) { + if (!this.signer) { throw new Error('Signer is required for push video'); } if (!this.decryptedPgpPvtKey) { - throw new Error('Decrypted PGP private key is required for push video'); + throw new Error('PushSDK was initialized in readonly mode. Video functionality is not available.'); } - this.signer ??= signer; - // Initialize the video instance with the provided options const videoV1Instance = new VideoV1({ signer: this.signer!, From e96a9b728668bd81d9c6fbf64cec98533e1e93b1 Mon Sep 17 00:00:00 2001 From: Madhur Gupta Date: Sat, 23 Dec 2023 01:03:02 +0530 Subject: [PATCH 05/32] feat(video): add sendNotification calls in connect, disconnect methods --- .../src/lib/pushstream/DataModifier.ts | 4 + packages/restapi/src/lib/types/index.ts | 1 + packages/restapi/src/lib/video/Video.ts | 79 +++++++++++++------ 3 files changed, 61 insertions(+), 23 deletions(-) diff --git a/packages/restapi/src/lib/pushstream/DataModifier.ts b/packages/restapi/src/lib/pushstream/DataModifier.ts index 182b1fefe..dfeceb167 100644 --- a/packages/restapi/src/lib/pushstream/DataModifier.ts +++ b/packages/restapi/src/lib/pushstream/DataModifier.ts @@ -402,6 +402,10 @@ export class DataModifier { return VideoEventType.RequestVideo; case VideoCallStatus.RECEIVED: return VideoEventType.ApproveVideo; + case VideoCallStatus.CONNECTED: + return VideoEventType.ConnectVideo; + case VideoCallStatus.ENDED: + return VideoEventType.DisconnectVideo case VideoCallStatus.DISCONNECTED: return VideoEventType.DenyVideo; default: diff --git a/packages/restapi/src/lib/types/index.ts b/packages/restapi/src/lib/types/index.ts index 0a04ca344..9e086f623 100644 --- a/packages/restapi/src/lib/types/index.ts +++ b/packages/restapi/src/lib/types/index.ts @@ -766,6 +766,7 @@ export enum VideoCallStatus { RECEIVED, CONNECTED, DISCONNECTED, + ENDED, RETRY_INITIALIZED, RETRY_RECEIVED, } diff --git a/packages/restapi/src/lib/video/Video.ts b/packages/restapi/src/lib/video/Video.ts index 07da6605b..31226337a 100644 --- a/packages/restapi/src/lib/video/Video.ts +++ b/packages/restapi/src/lib/video/Video.ts @@ -34,6 +34,7 @@ import { SPACE_DISCONNECT_TYPE, SPACE_REQUEST_TYPE, VIDEO_CALL_TYPE, + VIDEO_NOTIFICATION_ACCESS_TYPE, } from '../payloads/constants'; import { validateVideoRules } from './helpers/validateVideoRules'; @@ -756,6 +757,32 @@ export class Video { draft.incoming[incomingIndex].status = VideoCallStatus.CONNECTED; }); }); + + // Notifying the recipient that the video call is now connected + sendVideoCallNotification( + { + signer: this.signer, + chainId: this.chainId, + pgpPrivateKey: this.pgpPrivateKey, + }, + { + senderAddress: this.data.local.address, + recipientAddress: peerAddress + ? peerAddress + : this.data.incoming[0].address, + status: VideoCallStatus.CONNECTED, + rules: { + access: { + type: VIDEO_NOTIFICATION_ACCESS_TYPE.PUSH_CHAT, + data: { + chatId: this.data.meta.chatId, + }, + }, + }, + signalData, + env: this.env, + } + ); } catch (err) { console.error('error in connect', err); } @@ -773,37 +800,43 @@ export class Video { ? getIncomingIndexFromAddress(this.data.incoming, peerAddress) : 0; - if ( - this.data.incoming[incomingIndex].status === VideoCallStatus.CONNECTED - ) { + const isCallConnected = + this.data.incoming[incomingIndex].status === VideoCallStatus.CONNECTED; + + if (isCallConnected) { this.peerInstances[ peerAddress ? peerAddress : this.data.incoming[0].address ]?.send(JSON.stringify({ type: 'endCall', value: true, details })); this.peerInstances[ peerAddress ? peerAddress : this.data.incoming[0].address ]?.destroy(); - } else { - // for disconnecting during status INITIALIZED, RECEIVED, RETRY_INITIALIZED, RETRY_RECEIVED - // send a notif to the other user signaling status = DISCONNECTED - sendVideoCallNotification( - { - signer: this.signer, - chainId: this.chainId, - pgpPrivateKey: this.pgpPrivateKey, - }, - { - senderAddress: this.data.local.address, - recipientAddress: this.data.incoming[incomingIndex].address, - status: VideoCallStatus.DISCONNECTED, - chatId: this.data.meta.chatId, - signalData: null, - env: this.env, - callType: this.callType, - callDetails: details, - } - ); } + /* + * Send a notification to the other user signaling: + * status = ENDED if the call was connected + * status = DISCONNECTED otherwise. + */ + sendVideoCallNotification( + { + signer: this.signer, + chainId: this.chainId, + pgpPrivateKey: this.pgpPrivateKey, + }, + { + senderAddress: this.data.local.address, + recipientAddress: this.data.incoming[incomingIndex].address, + status: isCallConnected + ? VideoCallStatus.ENDED + : VideoCallStatus.DISCONNECTED, + chatId: this.data.meta.chatId, + signalData: null, + env: this.env, + callType: this.callType, + callDetails: details, + } + ); + // destroy the peerInstance this.peerInstances[ peerAddress ? peerAddress : this.data.incoming[0].address From 3cf48a4ae54549fde8a91a19d5532596b2c80ee9 Mon Sep 17 00:00:00 2001 From: Madhur Gupta Date: Tue, 2 Jan 2024 14:43:11 +0530 Subject: [PATCH 06/32] fix(videonotificationrules): typo in VideoNotificationRules interface --- packages/restapi/src/lib/payloads/helpers.ts | 4 ++-- packages/restapi/src/lib/types/index.ts | 8 ++++---- packages/restapi/src/lib/types/videoTypes.ts | 4 ++-- packages/restapi/src/lib/video/VideoV2.ts | 4 ++-- .../src/lib/video/helpers/sendVideoCallNotification.ts | 4 ++-- .../restapi/src/lib/video/helpers/validateVideoRules.ts | 4 ++-- 6 files changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/restapi/src/lib/payloads/helpers.ts b/packages/restapi/src/lib/payloads/helpers.ts index 779154e6f..44f1594fd 100644 --- a/packages/restapi/src/lib/payloads/helpers.ts +++ b/packages/restapi/src/lib/payloads/helpers.ts @@ -7,7 +7,7 @@ import { ISendNotificationInputOptions, INotificationPayload, walletType, - VideNotificationRules, + VideoNotificationRules, } from '../types'; import { IDENTITY_TYPE, @@ -212,7 +212,7 @@ export async function getVerificationProof({ wallet?: walletType; pgpPrivateKey?: string; env?: ENV; - rules?:VideNotificationRules; + rules?:VideoNotificationRules; }) { let message = null; let verificationProof = null; diff --git a/packages/restapi/src/lib/types/index.ts b/packages/restapi/src/lib/types/index.ts index 9e086f623..b408d9fca 100644 --- a/packages/restapi/src/lib/types/index.ts +++ b/packages/restapi/src/lib/types/index.ts @@ -84,7 +84,7 @@ export type ParsedResponseType = { }; }; -export interface VideNotificationRules { +export interface VideoNotificationRules { access: { type: VIDEO_NOTIFICATION_ACCESS_TYPE; data: { @@ -94,7 +94,7 @@ export interface VideNotificationRules { } // SendNotificationRules can be extended in the future for other use cases -export type SendNotificationRules = VideNotificationRules; +export type SendNotificationRules = VideoNotificationRules; export interface ISendNotificationInputOptions { senderType?: 0 | 1; @@ -813,7 +813,7 @@ export type VideoRequestInputOptions = { recipientAddress: string | string[]; /** @deprecated - Use `rules` object instead */ chatId?: string; - rules?: VideNotificationRules; + rules?: VideoNotificationRules; onReceiveMessage?: (message: string) => void; retry?: boolean; details?: { @@ -828,7 +828,7 @@ export type VideoAcceptRequestInputOptions = { recipientAddress: string; /** @deprecated - Use `rules` object instead */ chatId?: string; - rules?: VideNotificationRules; + rules?: VideoNotificationRules; onReceiveMessage?: (message: string) => void; retry?: boolean; details?: { diff --git a/packages/restapi/src/lib/types/videoTypes.ts b/packages/restapi/src/lib/types/videoTypes.ts index 1e9904d1f..e6f018a2c 100644 --- a/packages/restapi/src/lib/types/videoTypes.ts +++ b/packages/restapi/src/lib/types/videoTypes.ts @@ -1,9 +1,9 @@ -import { VideNotificationRules } from "."; +import { VideoNotificationRules } from "."; export type VideoPeerInfo = { address: string; signal: any; meta: { - rules: VideNotificationRules; + rules: VideoNotificationRules; }; }; diff --git a/packages/restapi/src/lib/video/VideoV2.ts b/packages/restapi/src/lib/video/VideoV2.ts index 213b9a88a..38cfaddfa 100644 --- a/packages/restapi/src/lib/video/VideoV2.ts +++ b/packages/restapi/src/lib/video/VideoV2.ts @@ -6,7 +6,7 @@ import { walletToPCAIP10, } from '../helpers'; import { VIDEO_NOTIFICATION_ACCESS_TYPE } from '../payloads/constants'; -import { VideNotificationRules, VideoPeerInfo } from '../types'; +import { VideoNotificationRules, VideoPeerInfo } from '../types'; import { Video as VideoV1 } from './Video'; import { validatePeerInfo } from './helpers/validatePeerInfo'; @@ -58,7 +58,7 @@ export class VideoV2 { async request( recipients: string[], options?: { - rules: VideNotificationRules; + rules: VideoNotificationRules; } ) { const { rules } = options || {}; diff --git a/packages/restapi/src/lib/video/helpers/sendVideoCallNotification.ts b/packages/restapi/src/lib/video/helpers/sendVideoCallNotification.ts index f6d10a501..5408a8c6b 100644 --- a/packages/restapi/src/lib/video/helpers/sendVideoCallNotification.ts +++ b/packages/restapi/src/lib/video/helpers/sendVideoCallNotification.ts @@ -13,7 +13,7 @@ import { EnvOptionsType, SignerType, VideoCallStatus, - VideNotificationRules, + VideoNotificationRules, } from '../../types'; interface CallDetailsType { @@ -33,7 +33,7 @@ export interface VideoDataType { interface VideoCallInfoType extends VideoDataType, EnvOptionsType { callType?: VIDEO_CALL_TYPE; - rules?: VideNotificationRules; + rules?: VideoNotificationRules; } interface UserInfoType { diff --git a/packages/restapi/src/lib/video/helpers/validateVideoRules.ts b/packages/restapi/src/lib/video/helpers/validateVideoRules.ts index f5c8a40a6..bb185fe8a 100644 --- a/packages/restapi/src/lib/video/helpers/validateVideoRules.ts +++ b/packages/restapi/src/lib/video/helpers/validateVideoRules.ts @@ -1,7 +1,7 @@ import { VIDEO_NOTIFICATION_ACCESS_TYPE } from '../../payloads/constants'; -import { VideNotificationRules } from '../../types'; +import { VideoNotificationRules } from '../../types'; -export const validateVideoRules = (rules: VideNotificationRules) => { +export const validateVideoRules = (rules: VideoNotificationRules) => { if ( rules.access.type === VIDEO_NOTIFICATION_ACCESS_TYPE.PUSH_CHAT && (!rules.access.data.chatId || rules.access.data.chatId === '') From 4088dfc68f35a69aca68177140c7c840bc5f551a Mon Sep 17 00:00:00 2001 From: Siddesh Sankhya <79219618+Siddesh7@users.noreply.github.com> Date: Fri, 5 Jan 2024 17:03:33 +0530 Subject: [PATCH 07/32] add: useStream.ts video-v2-example-app --- .../examples/sdk-frontend/hooks/useStream.ts | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 packages/examples/sdk-frontend/hooks/useStream.ts diff --git a/packages/examples/sdk-frontend/hooks/useStream.ts b/packages/examples/sdk-frontend/hooks/useStream.ts new file mode 100644 index 000000000..4da48a2d5 --- /dev/null +++ b/packages/examples/sdk-frontend/hooks/useStream.ts @@ -0,0 +1,57 @@ +import { useEffect, useState } from 'react'; +import { CONSTANTS, VideoEvent } from '@pushprotocol/restapi'; + +export const useStream = ({ streamObject }: any) => { + const [stream, setStream] = useState(); + const [latestFeedItem, setLatestFeedItem] = useState(null); + const [isPushSocketConnected, setIsPushSocketConnected] = useState(false); + + const addSocketEvents = () => { + console.log(stream); + stream?.on(CONSTANTS.STREAM.CONNECT, () => { + console.log('CONNECTED: '); + setIsPushSocketConnected(true); + }); + stream?.on(CONSTANTS.STREAM.DISCONNECT, () => { + console.log('DISCONNECT: '); + setIsPushSocketConnected(false); + }); + + stream?.on(CONSTANTS.STREAM.VIDEO, (data: VideoEvent) => { + console.log('RECEIVED FEED ITEM: ', data); + setLatestFeedItem(data); + }); + }; + + const removeSocketEvents = () => { + stream?.off(CONSTANTS.STREAM.CONNECT); + stream?.off(CONSTANTS.STREAM.DISCONNECT); + stream?.off(CONSTANTS.STREAM.VIDEO); + }; + + useEffect(() => { + if (stream) { + addSocketEvents(); + stream.connect(); + } + + return () => { + if (stream) { + removeSocketEvents(); + } + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [stream]); + + // Update stream state when streamObject changes + useEffect(() => { + if (!streamObject) return; + setStream(streamObject); + }, [streamObject]); + + return { + stream, + isPushSocketConnected, + latestFeedItem, + }; +}; From 36780b88c4994beb761b17c527f0b08db6071c8c Mon Sep 17 00:00:00 2001 From: Siddesh Sankhya <79219618+Siddesh7@users.noreply.github.com> Date: Fri, 5 Jan 2024 17:04:43 +0530 Subject: [PATCH 08/32] add: videoV2.tsx in video-v2-example-app --- .../examples/sdk-frontend/pages/videoV2.tsx | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 packages/examples/sdk-frontend/pages/videoV2.tsx diff --git a/packages/examples/sdk-frontend/pages/videoV2.tsx b/packages/examples/sdk-frontend/pages/videoV2.tsx new file mode 100644 index 000000000..8991c87bf --- /dev/null +++ b/packages/examples/sdk-frontend/pages/videoV2.tsx @@ -0,0 +1,93 @@ +import type { NextPage } from 'next'; + +import { + PushAPI, + CONSTANTS, + VideoEvent, + VideoEventType, +} from '@pushprotocol/restapi'; +import { useAccount, useNetwork, useSigner } from 'wagmi'; +import styled from 'styled-components'; + +import { useEffect, useRef, useState } from 'react'; +import { VIDEO_NOTIFICATION_ACCESS_TYPE } from '@pushprotocol/restapi/src/lib/payloads/constants'; +import { useStream } from '../hooks/useStream'; + +const VideoV2: NextPage = () => { + const { address, isConnected } = useAccount(); + const { chain } = useNetwork(); + const { data: signer } = useSigner(); + + const aliceVideoCall = useRef(); + const [aliceStream, setAliceStream] = useState(); + const { stream, isPushSocketConnected, latestFeedItem } = useStream({ + streamObject: aliceStream, + }); + + const [data, setData] = useState(); + + useEffect(() => { + if (!signer) return; + const initializePushAPI = async () => { + const userAlice = await PushAPI.initialize(signer, { + env: CONSTANTS.ENV.DEV, + }); + + aliceVideoCall.current = await userAlice.video.initialize(setData, { + media: { + video: true, + audio: true, + }, + }); + const astream = await userAlice.initStream([CONSTANTS.STREAM.CHAT]); + setAliceStream(astream); + }; + + initializePushAPI(); + }, [signer]); + useEffect(() => { + console.log('stream', stream); + }, [stream]); + useEffect(() => { + isPushSocketConnected; + console.log('latestFeedItem', latestFeedItem); + }, [isPushSocketConnected, latestFeedItem]); + const setRequestVideoCall = async () => { + await aliceVideoCall.current.request( + ['0xb73923eCcfbd6975BFd66CD1C76FA6b883E30365'], + { + rules: { + access: { + type: VIDEO_NOTIFICATION_ACCESS_TYPE.PUSH_CHAT, + data: { + chatId: + '252395e6b5d0ae0796e05e648240f7950f7a50a80906cdf6accdf7079e311dea', + }, + }, + }, + } + ); + }; + + return ( +
+ Push Video SDK Demo + + {isConnected ? ( +
+ +
+ ) : ( + 'Please connect your wallet.' + )} +
+ ); +}; + +const Heading = styled.h1` + margin: 20px 40px; +`; + +// ... (rest of your styled components) + +export default VideoV2; From 10d05a9014029675b1f8b45db6f6476404df1038 Mon Sep 17 00:00:00 2001 From: Madhur Gupta Date: Fri, 5 Jan 2024 17:04:57 +0530 Subject: [PATCH 09/32] feat(video stream): handle connect, retry internally from the SDK --- .../restapi/src/lib/pushapi/pushAPITypes.ts | 4 +- packages/restapi/src/lib/pushapi/video.ts | 57 ++++++++++++++++++- .../src/lib/pushstream/DataModifier.ts | 6 +- .../src/lib/pushstream/pushStreamTypes.ts | 3 + packages/restapi/src/lib/video/Video.ts | 2 +- 5 files changed, 66 insertions(+), 6 deletions(-) diff --git a/packages/restapi/src/lib/pushapi/pushAPITypes.ts b/packages/restapi/src/lib/pushapi/pushAPITypes.ts index b33e6c406..3c8363546 100644 --- a/packages/restapi/src/lib/pushapi/pushAPITypes.ts +++ b/packages/restapi/src/lib/pushapi/pushAPITypes.ts @@ -1,5 +1,6 @@ import Constants, { ENV } from '../constants'; -import { ChatStatus, ProgressHookType, Rules, SignerType } from '../types'; +import type { PushStream } from '../pushstream/PushStream'; +import { ChatStatus, ProgressHookType, Rules } from '../types'; export enum ChatListType { CHATS = 'CHATS', @@ -68,6 +69,7 @@ export interface ParticipantStatus { } export interface VideoInitializeOptions { + socketStream: PushStream; media: { video?: boolean; audio?: boolean; diff --git a/packages/restapi/src/lib/pushapi/video.ts b/packages/restapi/src/lib/pushapi/video.ts index edf45775f..11aaddd8d 100644 --- a/packages/restapi/src/lib/pushapi/video.ts +++ b/packages/restapi/src/lib/pushapi/video.ts @@ -1,9 +1,11 @@ import { ENV } from '../constants'; +import CONSTANTS from '../constantsV2'; import { SignerType, VideoCallData } from '../types'; import { Video as VideoV1 } from '../video/Video'; import { VideoV2 } from '../video/VideoV2'; import { VideoInitializeOptions } from './pushAPITypes'; +import { VideoEvent, VideoEventType } from '../pushstream/pushStreamTypes'; export class Video { constructor( @@ -15,9 +17,9 @@ export class Video { async initialize( setVideoData: (fn: (data: VideoCallData) => VideoCallData) => void, - options?: VideoInitializeOptions + options: VideoInitializeOptions ) { - const { media, stream } = options || {}; + const { socketStream, media, stream } = options; const chainId = await this.signer?.getChainId(); @@ -30,7 +32,9 @@ export class Video { } if (!this.decryptedPgpPvtKey) { - throw new Error('PushSDK was initialized in readonly mode. Video functionality is not available.'); + throw new Error( + 'PushSDK was initialized in readonly mode. Video functionality is not available.' + ); } // Initialize the video instance with the provided options @@ -55,6 +59,53 @@ export class Video { }), }); + // Setup video event handlers + socketStream.on(CONSTANTS.STREAM.VIDEO, (data: VideoEvent) => { + const { + address, + signal, + meta: { rules }, + } = data.peerInfo; + + // Check if the chatId from the incoming video event matches the chatId of the current video instance + if (rules.access.data.chatId === videoV1Instance.data.meta.chatId) { + // If the event is a ConnectVideo or RetryApproveVideo event, connect to the video + if ( + data.event === VideoEventType.ConnectVideo || + data.event === VideoEventType.RetryApproveVideo + ) { + videoV1Instance.connect({ peerAddress: address, signalData: signal }); + } + + // If the event is a RetryRequestVideo event and the current instance is the initiator, send a request + if ( + data.event === VideoEventType.RetryRequestVideo && + videoV1Instance.isInitiator() + ) { + videoV1Instance.request({ + senderAddress: this.account, + recipientAddress: address, + chatId: rules.access.data.chatId, + retry: true, + }); + } + + // If the event is a RetryRequestVideo event and the current instance is not the initiator, accept the request + if ( + data.event === VideoEventType.RetryRequestVideo && + !videoV1Instance.isInitiator() + ) { + videoV1Instance.acceptRequest({ + signalData: signal, + senderAddress: this.account, + recipientAddress: address, + chatId: rules.access.data.chatId, + retry: true, + }); + } + } + }); + // Return an instance of the video v2 class return new VideoV2({ videoV1Instance, diff --git a/packages/restapi/src/lib/pushstream/DataModifier.ts b/packages/restapi/src/lib/pushstream/DataModifier.ts index dfeceb167..6aedbc092 100644 --- a/packages/restapi/src/lib/pushstream/DataModifier.ts +++ b/packages/restapi/src/lib/pushstream/DataModifier.ts @@ -405,9 +405,13 @@ export class DataModifier { case VideoCallStatus.CONNECTED: return VideoEventType.ConnectVideo; case VideoCallStatus.ENDED: - return VideoEventType.DisconnectVideo + return VideoEventType.DisconnectVideo; case VideoCallStatus.DISCONNECTED: return VideoEventType.DenyVideo; + case VideoCallStatus.RETRY_INITIALIZED: + return VideoEventType.RetryRequestVideo; + case VideoCallStatus.RETRY_RECEIVED: + return VideoEventType.RetryApproveVideo; default: throw new Error(`Unknown video call status: ${currentVideoStatus}`); } diff --git a/packages/restapi/src/lib/pushstream/pushStreamTypes.ts b/packages/restapi/src/lib/pushstream/pushStreamTypes.ts index c4e5f587b..01cf61e7b 100644 --- a/packages/restapi/src/lib/pushstream/pushStreamTypes.ts +++ b/packages/restapi/src/lib/pushstream/pushStreamTypes.ts @@ -59,6 +59,9 @@ export enum VideoEventType { DenyVideo = 'video.deny', ConnectVideo = 'video.connect', DisconnectVideo = 'video.disconnect', + // retry events + RetryRequestVideo = 'video.retry.request', + RetryApproveVideo = 'video.retry.approve' } export enum ProposedEventNames { diff --git a/packages/restapi/src/lib/video/Video.ts b/packages/restapi/src/lib/video/Video.ts index 31226337a..66870c62d 100644 --- a/packages/restapi/src/lib/video/Video.ts +++ b/packages/restapi/src/lib/video/Video.ts @@ -88,7 +88,7 @@ export class Video { [key: string]: any; } = {}; - protected data: VideoCallData; + data: VideoCallData; setData: (fn: (data: VideoCallData) => VideoCallData) => void; constructor({ From 3dcace4d026b1286ad6ed950609c83ef2457974b Mon Sep 17 00:00:00 2001 From: Madhur Gupta Date: Fri, 5 Jan 2024 18:01:04 +0530 Subject: [PATCH 10/32] feat(video connect): remove the connect method from the videov2 SDK --- packages/restapi/src/lib/video/VideoV2.ts | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/packages/restapi/src/lib/video/VideoV2.ts b/packages/restapi/src/lib/video/VideoV2.ts index 38cfaddfa..479dee5f1 100644 --- a/packages/restapi/src/lib/video/VideoV2.ts +++ b/packages/restapi/src/lib/video/VideoV2.ts @@ -158,21 +158,6 @@ export class VideoV2 { }); } - /** - * Connect to a video call - * @param {VideoPeerInfo} peerInfo - The peer information - */ - async connect(peerInfo: VideoPeerInfo) { - validatePeerInfo(peerInfo); - - const { signal, address } = peerInfo; - - await this.videoInstance.connect({ - peerAddress: address, - signalData: signal, - }); - } - /** * Disconnect from a video call * @param {string} address - The address to disconnect from From 194e06481400893e8d7e21e25a201c037592a257 Mon Sep 17 00:00:00 2001 From: Madhur Gupta Date: Mon, 8 Jan 2024 15:19:55 +0530 Subject: [PATCH 11/32] fix(videov2): create push signer instance to get chain id in initialize() --- packages/restapi/src/lib/pushapi/video.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/restapi/src/lib/pushapi/video.ts b/packages/restapi/src/lib/pushapi/video.ts index 11aaddd8d..5051cf052 100644 --- a/packages/restapi/src/lib/pushapi/video.ts +++ b/packages/restapi/src/lib/pushapi/video.ts @@ -1,6 +1,7 @@ import { ENV } from '../constants'; import CONSTANTS from '../constantsV2'; import { SignerType, VideoCallData } from '../types'; +import { Signer as PushSigner } from '../helpers'; import { Video as VideoV1 } from '../video/Video'; import { VideoV2 } from '../video/VideoV2'; @@ -21,12 +22,6 @@ export class Video { ) { const { socketStream, media, stream } = options; - const chainId = await this.signer?.getChainId(); - - if (!chainId) { - throw new Error('Chain Id not retrievable from signer'); - } - if (!this.signer) { throw new Error('Signer is required for push video'); } @@ -37,6 +32,12 @@ export class Video { ); } + const chainId = await new PushSigner(this.signer).getChainId(); + + if (!chainId) { + throw new Error('Chain Id not retrievable from signer'); + } + // Initialize the video instance with the provided options const videoV1Instance = new VideoV1({ signer: this.signer!, From e05e8b840d439680c7861bd92e24596d8e547f3f Mon Sep 17 00:00:00 2001 From: Madhur Gupta Date: Tue, 9 Jan 2024 12:34:55 +0530 Subject: [PATCH 12/32] fix(videov2-example): refactor react example component & remove useStream hook --- packages/examples/automated-chat/package.json | 2 +- .../examples/sdk-frontend/hooks/useStream.ts | 57 ------------------ packages/examples/sdk-frontend/package.json | 2 +- .../pages/{videoV2.tsx => video-v2.tsx} | 60 ++++++++++++++----- .../examples/sdk-frontend/pages/video.tsx | 16 +++-- .../sdk-frontend/video/pages/spaces/index.tsx | 0 6 files changed, 58 insertions(+), 79 deletions(-) delete mode 100644 packages/examples/sdk-frontend/hooks/useStream.ts rename packages/examples/sdk-frontend/pages/{videoV2.tsx => video-v2.tsx} (55%) delete mode 100644 packages/examples/sdk-frontend/video/pages/spaces/index.tsx diff --git a/packages/examples/automated-chat/package.json b/packages/examples/automated-chat/package.json index 734de43c1..ef781b48f 100644 --- a/packages/examples/automated-chat/package.json +++ b/packages/examples/automated-chat/package.json @@ -14,7 +14,7 @@ "author": "", "license": "ISC", "dependencies": { - "@pushprotocol/restapi": "1.4.35", + "@pushprotocol/restapi": "1.5.5", "@pushprotocol/socket": "0.5.2", "ethers": "5.7.2" }, diff --git a/packages/examples/sdk-frontend/hooks/useStream.ts b/packages/examples/sdk-frontend/hooks/useStream.ts deleted file mode 100644 index 4da48a2d5..000000000 --- a/packages/examples/sdk-frontend/hooks/useStream.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { useEffect, useState } from 'react'; -import { CONSTANTS, VideoEvent } from '@pushprotocol/restapi'; - -export const useStream = ({ streamObject }: any) => { - const [stream, setStream] = useState(); - const [latestFeedItem, setLatestFeedItem] = useState(null); - const [isPushSocketConnected, setIsPushSocketConnected] = useState(false); - - const addSocketEvents = () => { - console.log(stream); - stream?.on(CONSTANTS.STREAM.CONNECT, () => { - console.log('CONNECTED: '); - setIsPushSocketConnected(true); - }); - stream?.on(CONSTANTS.STREAM.DISCONNECT, () => { - console.log('DISCONNECT: '); - setIsPushSocketConnected(false); - }); - - stream?.on(CONSTANTS.STREAM.VIDEO, (data: VideoEvent) => { - console.log('RECEIVED FEED ITEM: ', data); - setLatestFeedItem(data); - }); - }; - - const removeSocketEvents = () => { - stream?.off(CONSTANTS.STREAM.CONNECT); - stream?.off(CONSTANTS.STREAM.DISCONNECT); - stream?.off(CONSTANTS.STREAM.VIDEO); - }; - - useEffect(() => { - if (stream) { - addSocketEvents(); - stream.connect(); - } - - return () => { - if (stream) { - removeSocketEvents(); - } - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [stream]); - - // Update stream state when streamObject changes - useEffect(() => { - if (!streamObject) return; - setStream(streamObject); - }, [streamObject]); - - return { - stream, - isPushSocketConnected, - latestFeedItem, - }; -}; diff --git a/packages/examples/sdk-frontend/package.json b/packages/examples/sdk-frontend/package.json index bff36c8a5..7c85f6475 100644 --- a/packages/examples/sdk-frontend/package.json +++ b/packages/examples/sdk-frontend/package.json @@ -9,7 +9,7 @@ "lint": "next lint" }, "dependencies": { - "@pushprotocol/restapi": "^1.4.46", + "@pushprotocol/restapi": "^1.5.5", "@pushprotocol/socket": "^0.5.1", "@pushprotocol/uiweb": "^1.1.8", "@rainbow-me/rainbowkit": "0.12.14", diff --git a/packages/examples/sdk-frontend/pages/videoV2.tsx b/packages/examples/sdk-frontend/pages/video-v2.tsx similarity index 55% rename from packages/examples/sdk-frontend/pages/videoV2.tsx rename to packages/examples/sdk-frontend/pages/video-v2.tsx index 8991c87bf..32c933b93 100644 --- a/packages/examples/sdk-frontend/pages/videoV2.tsx +++ b/packages/examples/sdk-frontend/pages/video-v2.tsx @@ -3,15 +3,15 @@ import type { NextPage } from 'next'; import { PushAPI, CONSTANTS, + VideoCallData, VideoEvent, - VideoEventType, } from '@pushprotocol/restapi'; import { useAccount, useNetwork, useSigner } from 'wagmi'; import styled from 'styled-components'; import { useEffect, useRef, useState } from 'react'; import { VIDEO_NOTIFICATION_ACCESS_TYPE } from '@pushprotocol/restapi/src/lib/payloads/constants'; -import { useStream } from '../hooks/useStream'; +import { initVideoCallData } from '@pushprotocol/restapi/src/lib/video'; const VideoV2: NextPage = () => { const { address, isConnected } = useAccount(); @@ -19,39 +19,67 @@ const VideoV2: NextPage = () => { const { data: signer } = useSigner(); const aliceVideoCall = useRef(); - const [aliceStream, setAliceStream] = useState(); - const { stream, isPushSocketConnected, latestFeedItem } = useStream({ - streamObject: aliceStream, - }); + const [latestVideoEvent, setLatestVideoEvent] = useState( + null + ); + const [isPushStreamConnected, setIsPushStreamConnected] = useState(false); - const [data, setData] = useState(); + const [data, setData] = useState(initVideoCallData); useEffect(() => { if (!signer) return; + const initializePushAPI = async () => { + console.log('initializePushAPI'); + const userAlice = await PushAPI.initialize(signer, { env: CONSTANTS.ENV.DEV, }); + const createdStream = await userAlice.initStream([ + CONSTANTS.STREAM.VIDEO, + CONSTANTS.STREAM.CONNECT, + CONSTANTS.STREAM.DISCONNECT, + ]); + + createdStream.on(CONSTANTS.STREAM.CONNECT, () => { + console.log('PUSH STREAM CONNECTED: '); + setIsPushStreamConnected(true); + }); + + createdStream.on(CONSTANTS.STREAM.DISCONNECT, () => { + console.log('PUSH STREAM DISCONNECTED: '); + setIsPushStreamConnected(false); + }); + + createdStream.on(CONSTANTS.STREAM.VIDEO, (data: VideoEvent) => { + console.log('RECEIVED VIDEO EVENT: ', data); + if (data) { + setLatestVideoEvent(data); + } + }); + + await createdStream.connect(); + aliceVideoCall.current = await userAlice.video.initialize(setData, { + socketStream: createdStream, media: { video: true, audio: true, }, }); - const astream = await userAlice.initStream([CONSTANTS.STREAM.CHAT]); - setAliceStream(astream); }; initializePushAPI(); }, [signer]); + useEffect(() => { - console.log('stream', stream); - }, [stream]); - useEffect(() => { - isPushSocketConnected; - console.log('latestFeedItem', latestFeedItem); - }, [isPushSocketConnected, latestFeedItem]); + console.log('isPushStreamConnected', isPushStreamConnected); + if (isPushStreamConnected) { + console.log('latestVideoEvent', latestVideoEvent); + } + }, [isPushStreamConnected, latestVideoEvent]); + const setRequestVideoCall = async () => { await aliceVideoCall.current.request( ['0xb73923eCcfbd6975BFd66CD1C76FA6b883E30365'], @@ -71,7 +99,7 @@ const VideoV2: NextPage = () => { return (
- Push Video SDK Demo + Push Video v2 SDK Demo {isConnected ? (
diff --git a/packages/examples/sdk-frontend/pages/video.tsx b/packages/examples/sdk-frontend/pages/video.tsx index 1c7587433..c3c79ee6c 100644 --- a/packages/examples/sdk-frontend/pages/video.tsx +++ b/packages/examples/sdk-frontend/pages/video.tsx @@ -113,7 +113,9 @@ const Home: NextPage = () => { rules: { access: { type: VIDEO_NOTIFICATION_ACCESS_TYPE.PUSH_CHAT, - data: data.meta.chatId + data: { + chatId: data.meta.chatId + } } } }); @@ -173,7 +175,9 @@ const Home: NextPage = () => { rules: { access: { type: VIDEO_NOTIFICATION_ACCESS_TYPE.PUSH_CHAT, - data: data.meta.chatId + data: { + chatId: data.meta.chatId + } } } }); @@ -231,7 +235,9 @@ const Home: NextPage = () => { rules: { access: { type: VIDEO_NOTIFICATION_ACCESS_TYPE.PUSH_CHAT, - data: data.meta.chatId + data: { + chatId: data.meta.chatId + } } }, retry: true, @@ -247,7 +253,9 @@ const Home: NextPage = () => { rules: { access: { type: VIDEO_NOTIFICATION_ACCESS_TYPE.PUSH_CHAT, - data: data.meta.chatId + data: { + chatId: data.meta.chatId + } } }, retry: true, diff --git a/packages/examples/sdk-frontend/video/pages/spaces/index.tsx b/packages/examples/sdk-frontend/video/pages/spaces/index.tsx deleted file mode 100644 index e69de29bb..000000000 From df4b6f8a9d61e7d670806465bc066516c1995eb9 Mon Sep 17 00:00:00 2001 From: Madhur Gupta Date: Wed, 10 Jan 2024 11:57:41 +0530 Subject: [PATCH 13/32] fix(video stream): add backwards compatibilty to rules object for older SDK versions --- packages/restapi/src/lib/pushstream/DataModifier.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/restapi/src/lib/pushstream/DataModifier.ts b/packages/restapi/src/lib/pushstream/DataModifier.ts index 6aedbc092..172b13f74 100644 --- a/packages/restapi/src/lib/pushstream/DataModifier.ts +++ b/packages/restapi/src/lib/pushstream/DataModifier.ts @@ -23,6 +23,7 @@ import { } from './pushStreamTypes'; import { VideoCallStatus, VideoPeerInfo } from '../types'; import { VideoDataType } from '../video'; +import { VIDEO_NOTIFICATION_ACCESS_TYPE } from '../payloads/constants'; export class DataModifier { public static handleChatGroupEvent(data: any, includeRaw = false): any { @@ -426,11 +427,21 @@ export class DataModifier { data.payload.data.additionalMeta?.data ); + // add backward compatibility to rules object + const rules = data.payload.rules ?? { + access: { + type: VIDEO_NOTIFICATION_ACCESS_TYPE.PUSH_CHAT, + data: { + chatId: data.payloads.chatId, + }, + }, + }; + const peerInfo: VideoPeerInfo = { address: senderAddress, signal: signalData, meta: { - rules: data.payload.rules, + rules, }, }; From 272667b1bb01e8742d89e742b1f410ba58b83d53 Mon Sep 17 00:00:00 2001 From: Madhur Gupta Date: Wed, 10 Jan 2024 12:15:29 +0530 Subject: [PATCH 14/32] fix(video stream): fix bug in rules object creation --- packages/restapi/src/lib/pushstream/DataModifier.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/restapi/src/lib/pushstream/DataModifier.ts b/packages/restapi/src/lib/pushstream/DataModifier.ts index 172b13f74..2c4cb62cd 100644 --- a/packages/restapi/src/lib/pushstream/DataModifier.ts +++ b/packages/restapi/src/lib/pushstream/DataModifier.ts @@ -423,16 +423,16 @@ export class DataModifier { origin: MessageOrigin, includeRaw = false ): VideoEvent { - const { senderAddress, signalData, status }: VideoDataType = JSON.parse( - data.payload.data.additionalMeta?.data - ); + const { senderAddress, signalData, status, chatId }: VideoDataType = + JSON.parse(data.payload.data.additionalMeta?.data); - // add backward compatibility to rules object + // To maintain backward compatibility, if the rules object is not present in the payload, + // we create a new rules object with chatId from additionalMeta.data const rules = data.payload.rules ?? { access: { type: VIDEO_NOTIFICATION_ACCESS_TYPE.PUSH_CHAT, data: { - chatId: data.payloads.chatId, + chatId, }, }, }; From a6ef376270c882552b4b434b05d30542f4de6931 Mon Sep 17 00:00:00 2001 From: Madhur Gupta Date: Wed, 10 Jan 2024 12:56:19 +0530 Subject: [PATCH 15/32] fix(video stream): call connect() for ApproveVideo event --- packages/restapi/src/lib/pushapi/video.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/restapi/src/lib/pushapi/video.ts b/packages/restapi/src/lib/pushapi/video.ts index 5051cf052..7d9531114 100644 --- a/packages/restapi/src/lib/pushapi/video.ts +++ b/packages/restapi/src/lib/pushapi/video.ts @@ -70,9 +70,9 @@ export class Video { // Check if the chatId from the incoming video event matches the chatId of the current video instance if (rules.access.data.chatId === videoV1Instance.data.meta.chatId) { - // If the event is a ConnectVideo or RetryApproveVideo event, connect to the video + // If the event is a ApproveVideo or RetryApproveVideo event, connect to the video if ( - data.event === VideoEventType.ConnectVideo || + data.event === VideoEventType.ApproveVideo || data.event === VideoEventType.RetryApproveVideo ) { videoV1Instance.connect({ peerAddress: address, signalData: signal }); From 019c8a6f3cb04f3bfd24494a182094964ef60f0d Mon Sep 17 00:00:00 2001 From: Siddesh Date: Wed, 10 Jan 2024 13:12:47 +0530 Subject: [PATCH 16/32] feat(example-video-1.5): video v2 example app implementation --- .../components/IncomingVideoModal.tsx | 57 +++++++ .../sdk-frontend/components/Toast.tsx | 38 +++++ .../examples/sdk-frontend/pages/video-v2.tsx | 147 +++++++++++++++--- 3 files changed, 217 insertions(+), 25 deletions(-) create mode 100644 packages/examples/sdk-frontend/components/IncomingVideoModal.tsx create mode 100644 packages/examples/sdk-frontend/components/Toast.tsx diff --git a/packages/examples/sdk-frontend/components/IncomingVideoModal.tsx b/packages/examples/sdk-frontend/components/IncomingVideoModal.tsx new file mode 100644 index 000000000..9df3c1af5 --- /dev/null +++ b/packages/examples/sdk-frontend/components/IncomingVideoModal.tsx @@ -0,0 +1,57 @@ +// Import necessary libraries +import React from 'react'; +import styled from 'styled-components'; + +// Styled components for the CallControl component +const IncomingVideoModalWrapper = styled.div` + display: flex; + flex-direction: column; + align-items: center; + padding: 20px; + width: 300px; + border-radius: 5px; + background-color: black; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); +`; + +const CallerID = styled.div` + font-size: 18px; + margin-bottom: 20px; + color: #fff; +`; + +const Button = styled.button` + padding: 10px 20px; + margin: 5px; + font-size: 16px; + cursor: pointer; + border: none; + border-radius: 5px; + outline: none; +`; + +const GreenButton = styled(Button)` + background-color: #4caf50; + color: #fff; +`; + +const RedButton = styled(Button)` + background-color: #f44336; + color: #fff; +`; + +// CallControl component +const IncomingVideoModal = ({ callerID, onAccept, onReject }) => { + return ( + + {callerID} + Accept + Reject + + ); +}; + +export default IncomingVideoModal; diff --git a/packages/examples/sdk-frontend/components/Toast.tsx b/packages/examples/sdk-frontend/components/Toast.tsx new file mode 100644 index 000000000..d6d484ab0 --- /dev/null +++ b/packages/examples/sdk-frontend/components/Toast.tsx @@ -0,0 +1,38 @@ +import React, { useState, useEffect } from 'react'; +import styled from 'styled-components'; + +interface ToastProps { + message: string; +} + +const ToastContainer = styled.div` + position: fixed; + bottom: 20px; + left: 50%; + transform: translateX(-50%); + background-color: #333; + color: #fff; + padding: 10px 20px; + border-radius: 5px; + opacity: ${(props) => (props.visible ? '1' : '0')}; + transition: opacity 1.5s ease-in-out; + z-index: 1000; +`; + +const Toast: React.FC = ({ message }) => { + const [visible, setVisible] = useState(true); + + useEffect(() => { + const timer = setTimeout(() => { + setVisible(false); + }, 3000); + + return () => { + clearTimeout(timer); + }; + }, []); + + return {message}; +}; + +export default Toast; diff --git a/packages/examples/sdk-frontend/pages/video-v2.tsx b/packages/examples/sdk-frontend/pages/video-v2.tsx index 32c933b93..d0d61433e 100644 --- a/packages/examples/sdk-frontend/pages/video-v2.tsx +++ b/packages/examples/sdk-frontend/pages/video-v2.tsx @@ -5,13 +5,17 @@ import { CONSTANTS, VideoCallData, VideoEvent, + VideoEventType, } from '@pushprotocol/restapi'; import { useAccount, useNetwork, useSigner } from 'wagmi'; import styled from 'styled-components'; import { useEffect, useRef, useState } from 'react'; -import { VIDEO_NOTIFICATION_ACCESS_TYPE } from '@pushprotocol/restapi/src/lib/payloads/constants'; import { initVideoCallData } from '@pushprotocol/restapi/src/lib/video'; +import IncomingVideoModal from '../components/IncomingVideoModal'; +import Toast from '../components/Toast'; +import VideoPlayer from '../components/VideoPlayer'; +import { VIDEO_NOTIFICATION_ACCESS_TYPE } from '@pushprotocol/restapi/src/lib/payloads/constants'; const VideoV2: NextPage = () => { const { address, isConnected } = useAccount(); @@ -25,7 +29,13 @@ const VideoV2: NextPage = () => { const [isPushStreamConnected, setIsPushStreamConnected] = useState(false); const [data, setData] = useState(initVideoCallData); - + const [recipientAddress, setRecipientAddress] = useState(); + const [showIncomingVideoModal, setShowIncomingVideoModal] = useState(false); + const [showCallDisconnectedToast, setShowCallDisconnectedToast] = + useState(false); + useEffect(() => { + console.log('data', data); + }, [data]); useEffect(() => { if (!signer) return; @@ -52,14 +62,38 @@ const VideoV2: NextPage = () => { setIsPushStreamConnected(false); }); - createdStream.on(CONSTANTS.STREAM.VIDEO, (data: VideoEvent) => { - console.log('RECEIVED VIDEO EVENT: ', data); - if (data) { + createdStream.on(CONSTANTS.STREAM.VIDEO, async (data: VideoEvent) => { + // Handle incoming call, when the type is RequestVideo + if (data.event === VideoEventType.RequestVideo) { setLatestVideoEvent(data); + setShowIncomingVideoModal(true); } - }); - await createdStream.connect(); + // If the received status is ApproveVideo that means we can connect the video call + if (data.event === VideoEventType.ApproveVideo) { + // setLatestVideoEvent(data); + + // // connecting the call using received peerInfo + // aliceVideoCall.current.connect(data.peerInfo); + console.log('ApproveVideo', data); + } + + // If the received status is DenyVideo that means the call has ended + if (data.event === VideoEventType.DenyVideo) { + // here you can do a window reload + } + + // If the received status is ConnectVideo that means the call was connected + if (data.event === VideoEventType.ConnectVideo) { + // can update the ui with a toast or something that the call is connected + console.log('ConnectVideo', data); + } + + // If the received status is DisconnectVideo that means the call has ended/someone hung up after it was connected + if (data.event === VideoEventType.DisconnectVideo) { + setShowCallDisconnectedToast(true); + } + }); aliceVideoCall.current = await userAlice.video.initialize(setData, { socketStream: createdStream, @@ -68,6 +102,7 @@ const VideoV2: NextPage = () => { audio: true, }, }); + await createdStream.connect(); }; initializePushAPI(); @@ -80,20 +115,37 @@ const VideoV2: NextPage = () => { } }, [isPushStreamConnected, latestVideoEvent]); - const setRequestVideoCall = async () => { - await aliceVideoCall.current.request( - ['0xb73923eCcfbd6975BFd66CD1C76FA6b883E30365'], - { - rules: { - access: { - type: VIDEO_NOTIFICATION_ACCESS_TYPE.PUSH_CHAT, - data: { - chatId: - '252395e6b5d0ae0796e05e648240f7950f7a50a80906cdf6accdf7079e311dea', - }, - }, - }, - } + const requestVideoCall = async (recipient: string) => { + console.log(recipient); + await aliceVideoCall.current.request([recipient]); + }; + + // const requestVideoCall = async (recipient: string) => { + // console.log(recipient); + // await aliceVideoCall.current.request([recipient], { + // rules: { + // access: { + // type: VIDEO_NOTIFICATION_ACCESS_TYPE.PUSH_CHAT, + // data: { + // chatId: + // '252395e6b5d0ae0796e05e648240f7950f7a50a80906cdf6accdf7079e311dea', + // }, + // }, + // }, + // }); + // }; + const acceptIncomingCall = async () => { + console.log(latestVideoEvent); + await aliceVideoCall.current.approve(latestVideoEvent?.peerInfo); + setShowIncomingVideoModal(false); + }; + const denyIncomingCall = async () => { + await aliceVideoCall.current.deny(latestVideoEvent?.peerInfo); + setShowIncomingVideoModal(false); + }; + const endCall = async () => { + await aliceVideoCall.current.disconnect( + latestVideoEvent?.peerInfo?.address ); }; @@ -103,7 +155,43 @@ const VideoV2: NextPage = () => { {isConnected ? (
- + {showCallDisconnectedToast && } + + setRecipientAddress(e.target.value)} + value={recipientAddress} + placeholder="recipient address" + type="text" + /> + + + {showIncomingVideoModal && ( + + )} + + + +

Local Video

+ +
+ {data.incoming[1]?.stream && ( + +

Incoming Video

+ +
+ )} +
) : ( 'Please connect your wallet.' @@ -115,7 +203,16 @@ const VideoV2: NextPage = () => { const Heading = styled.h1` margin: 20px 40px; `; - -// ... (rest of your styled components) - +const HContainer = styled.div` + display: flex; + gap: 20px; + margin: 20px 40px; +`; +const VContainer = styled.div` + display: flex; + gap: 10px; + flex-direction: column; + width: fit-content; + height: fit-content; +`; export default VideoV2; From 8bff3c484feedd11ea003d883b7ddf0884ebe9a4 Mon Sep 17 00:00:00 2001 From: Siddesh Date: Wed, 10 Jan 2024 15:12:06 +0530 Subject: [PATCH 17/32] feat(sdk-frontend): few ui changes --- .../components/IncomingVideoModal.tsx | 27 ++++-- .../examples/sdk-frontend/pages/index.tsx | 3 + .../examples/sdk-frontend/pages/video-v2.tsx | 85 +++++++++++-------- 3 files changed, 72 insertions(+), 43 deletions(-) diff --git a/packages/examples/sdk-frontend/components/IncomingVideoModal.tsx b/packages/examples/sdk-frontend/components/IncomingVideoModal.tsx index 9df3c1af5..f9d91ea9c 100644 --- a/packages/examples/sdk-frontend/components/IncomingVideoModal.tsx +++ b/packages/examples/sdk-frontend/components/IncomingVideoModal.tsx @@ -2,13 +2,19 @@ import React from 'react'; import styled from 'styled-components'; +type IncomingVideoModalProps = { + callerID: string | undefined; + onAccept: () => void; + onReject: () => void; +}; + // Styled components for the CallControl component const IncomingVideoModalWrapper = styled.div` display: flex; flex-direction: column; align-items: center; padding: 20px; - width: 300px; + width: 400px; border-radius: 5px; background-color: black; position: absolute; @@ -18,7 +24,7 @@ const IncomingVideoModalWrapper = styled.div` `; const CallerID = styled.div` - font-size: 18px; + font-size: 14px; margin-bottom: 20px; color: #fff; `; @@ -42,14 +48,23 @@ const RedButton = styled(Button)` background-color: #f44336; color: #fff; `; - +const ButtonContainer = styled.div` + display: flex; + justify-content: space-around; +`; // CallControl component -const IncomingVideoModal = ({ callerID, onAccept, onReject }) => { +const IncomingVideoModal = ({ + callerID, + onAccept, + onReject, +}: IncomingVideoModalProps) => { return ( {callerID} - Accept - Reject + + Accept + Reject + ); }; diff --git a/packages/examples/sdk-frontend/pages/index.tsx b/packages/examples/sdk-frontend/pages/index.tsx index e95310d1b..08378deb4 100644 --- a/packages/examples/sdk-frontend/pages/index.tsx +++ b/packages/examples/sdk-frontend/pages/index.tsx @@ -11,6 +11,9 @@ const Index: NextPage = () => { + {/* */} diff --git a/packages/examples/sdk-frontend/pages/video-v2.tsx b/packages/examples/sdk-frontend/pages/video-v2.tsx index d0d61433e..47a2e81d3 100644 --- a/packages/examples/sdk-frontend/pages/video-v2.tsx +++ b/packages/examples/sdk-frontend/pages/video-v2.tsx @@ -33,6 +33,7 @@ const VideoV2: NextPage = () => { const [showIncomingVideoModal, setShowIncomingVideoModal] = useState(false); const [showCallDisconnectedToast, setShowCallDisconnectedToast] = useState(false); + const [showCallConnectedToast, setShowCallConnectedToast] = useState(false); useEffect(() => { console.log('data', data); }, [data]); @@ -76,6 +77,7 @@ const VideoV2: NextPage = () => { // // connecting the call using received peerInfo // aliceVideoCall.current.connect(data.peerInfo); console.log('ApproveVideo', data); + setShowCallConnectedToast(true); } // If the received status is DenyVideo that means the call has ended @@ -87,11 +89,15 @@ const VideoV2: NextPage = () => { if (data.event === VideoEventType.ConnectVideo) { // can update the ui with a toast or something that the call is connected console.log('ConnectVideo', data); + setShowCallConnectedToast(true); } // If the received status is DisconnectVideo that means the call has ended/someone hung up after it was connected if (data.event === VideoEventType.DisconnectVideo) { setShowCallDisconnectedToast(true); + setTimeout(() => { + window.location.reload(); + }, 5000); } }); @@ -110,43 +116,26 @@ const VideoV2: NextPage = () => { useEffect(() => { console.log('isPushStreamConnected', isPushStreamConnected); - if (isPushStreamConnected) { + if (isPushStreamConnected) console.log('latestVideoEvent', latestVideoEvent); - } }, [isPushStreamConnected, latestVideoEvent]); const requestVideoCall = async (recipient: string) => { - console.log(recipient); await aliceVideoCall.current.request([recipient]); }; - // const requestVideoCall = async (recipient: string) => { - // console.log(recipient); - // await aliceVideoCall.current.request([recipient], { - // rules: { - // access: { - // type: VIDEO_NOTIFICATION_ACCESS_TYPE.PUSH_CHAT, - // data: { - // chatId: - // '252395e6b5d0ae0796e05e648240f7950f7a50a80906cdf6accdf7079e311dea', - // }, - // }, - // }, - // }); - // }; const acceptIncomingCall = async () => { - console.log(latestVideoEvent); await aliceVideoCall.current.approve(latestVideoEvent?.peerInfo); setShowIncomingVideoModal(false); }; const denyIncomingCall = async () => { + console.log('denyIncomingCall', latestVideoEvent?.peerInfo); await aliceVideoCall.current.deny(latestVideoEvent?.peerInfo); setShowIncomingVideoModal(false); }; const endCall = async () => { - await aliceVideoCall.current.disconnect( - latestVideoEvent?.peerInfo?.address - ); + console.log(recipientAddress); + await aliceVideoCall.current.disconnect(recipientAddress); }; return ( @@ -156,6 +145,7 @@ const VideoV2: NextPage = () => { {isConnected ? (
{showCallDisconnectedToast && } + {showCallConnectedToast && } setRecipientAddress(e.target.value)} @@ -164,22 +154,43 @@ const VideoV2: NextPage = () => { type="text" /> - - {showIncomingVideoModal && ( - - )} - + + + + + + + {showIncomingVideoModal && ( + + )} +

Local Video

From 6e73ba7a4df6212728e1bf1d1806bc813bd18f7b Mon Sep 17 00:00:00 2001 From: Siddesh Date: Wed, 10 Jan 2024 18:57:13 +0530 Subject: [PATCH 18/32] fix(video-v2-example-app): initialisevdeo call after a call ends --- .../examples/sdk-frontend/pages/video-v2.tsx | 153 +++++++++--------- 1 file changed, 78 insertions(+), 75 deletions(-) diff --git a/packages/examples/sdk-frontend/pages/video-v2.tsx b/packages/examples/sdk-frontend/pages/video-v2.tsx index 47a2e81d3..e4631b3d2 100644 --- a/packages/examples/sdk-frontend/pages/video-v2.tsx +++ b/packages/examples/sdk-frontend/pages/video-v2.tsx @@ -34,82 +34,82 @@ const VideoV2: NextPage = () => { const [showCallDisconnectedToast, setShowCallDisconnectedToast] = useState(false); const [showCallConnectedToast, setShowCallConnectedToast] = useState(false); - useEffect(() => { - console.log('data', data); - }, [data]); + const [callEndedReinitialize, setCallEndedReinitialize] = useState(false); + const initializePushAPI = async () => { + console.log('initializePushAPI'); + setCallEndedReinitialize(false); + const userAlice = await PushAPI.initialize(signer, { + env: CONSTANTS.ENV.DEV, + }); + + const createdStream = await userAlice.initStream([ + CONSTANTS.STREAM.VIDEO, + CONSTANTS.STREAM.CONNECT, + CONSTANTS.STREAM.DISCONNECT, + ]); + + createdStream.on(CONSTANTS.STREAM.CONNECT, () => { + console.log('PUSH STREAM CONNECTED: '); + setIsPushStreamConnected(true); + }); + + createdStream.on(CONSTANTS.STREAM.DISCONNECT, () => { + console.log('PUSH STREAM DISCONNECTED: '); + setIsPushStreamConnected(false); + }); + + createdStream.on(CONSTANTS.STREAM.VIDEO, async (data: VideoEvent) => { + // Handle incoming call, when the type is RequestVideo + if (data.event === VideoEventType.RequestVideo) { + setLatestVideoEvent(data); + setShowIncomingVideoModal(true); + } + + // If the received status is ApproveVideo that means we can connect the video call + if (data.event === VideoEventType.ApproveVideo) { + // setLatestVideoEvent(data); + + // // connecting the call using received peerInfo + // aliceVideoCall.current.connect(data.peerInfo); + console.log('ApproveVideo', data); + setShowCallConnectedToast(true); + } + + // If the received status is DenyVideo that means the call has ended + if (data.event === VideoEventType.DenyVideo) { + // here you can do a window reload + } + + // If the received status is ConnectVideo that means the call was connected + if (data.event === VideoEventType.ConnectVideo) { + // can update the ui with a toast or something that the call is connected + console.log('ConnectVideo', data); + setShowCallConnectedToast(true); + } + + // If the received status is DisconnectVideo that means the call has ended/someone hung up after it was connected + if (data.event === VideoEventType.DisconnectVideo) { + setShowCallDisconnectedToast(true); + setCallEndedReinitialize(true); + } + }); + + aliceVideoCall.current = await userAlice.video.initialize(setData, { + socketStream: createdStream, + media: { + video: true, + audio: true, + }, + }); + await createdStream.connect(); + }; useEffect(() => { if (!signer) return; - const initializePushAPI = async () => { - console.log('initializePushAPI'); - - const userAlice = await PushAPI.initialize(signer, { - env: CONSTANTS.ENV.DEV, - }); - - const createdStream = await userAlice.initStream([ - CONSTANTS.STREAM.VIDEO, - CONSTANTS.STREAM.CONNECT, - CONSTANTS.STREAM.DISCONNECT, - ]); - - createdStream.on(CONSTANTS.STREAM.CONNECT, () => { - console.log('PUSH STREAM CONNECTED: '); - setIsPushStreamConnected(true); - }); - - createdStream.on(CONSTANTS.STREAM.DISCONNECT, () => { - console.log('PUSH STREAM DISCONNECTED: '); - setIsPushStreamConnected(false); - }); - - createdStream.on(CONSTANTS.STREAM.VIDEO, async (data: VideoEvent) => { - // Handle incoming call, when the type is RequestVideo - if (data.event === VideoEventType.RequestVideo) { - setLatestVideoEvent(data); - setShowIncomingVideoModal(true); - } - - // If the received status is ApproveVideo that means we can connect the video call - if (data.event === VideoEventType.ApproveVideo) { - // setLatestVideoEvent(data); - - // // connecting the call using received peerInfo - // aliceVideoCall.current.connect(data.peerInfo); - console.log('ApproveVideo', data); - setShowCallConnectedToast(true); - } - - // If the received status is DenyVideo that means the call has ended - if (data.event === VideoEventType.DenyVideo) { - // here you can do a window reload - } - - // If the received status is ConnectVideo that means the call was connected - if (data.event === VideoEventType.ConnectVideo) { - // can update the ui with a toast or something that the call is connected - console.log('ConnectVideo', data); - setShowCallConnectedToast(true); - } - - // If the received status is DisconnectVideo that means the call has ended/someone hung up after it was connected - if (data.event === VideoEventType.DisconnectVideo) { - setShowCallDisconnectedToast(true); - setTimeout(() => { - window.location.reload(); - }, 5000); - } - }); - - aliceVideoCall.current = await userAlice.video.initialize(setData, { - socketStream: createdStream, - media: { - video: true, - audio: true, - }, - }); - await createdStream.connect(); - }; + initializePushAPI(); + }, [callEndedReinitialize]); + useEffect(() => { + if (!signer) return; initializePushAPI(); }, [signer]); @@ -134,8 +134,11 @@ const VideoV2: NextPage = () => { setShowIncomingVideoModal(false); }; const endCall = async () => { - console.log(recipientAddress); - await aliceVideoCall.current.disconnect(recipientAddress); + console.log(latestVideoEvent?.peerInfo?.address); + await aliceVideoCall.current.disconnect( + latestVideoEvent?.peerInfo?.address + ); + setCallEndedReinitialize(true); }; return ( From aefd75402deae952ade68b5d9b5ac9f998757624 Mon Sep 17 00:00:00 2001 From: Madhur Gupta Date: Wed, 10 Jan 2024 20:22:48 +0530 Subject: [PATCH 19/32] feat(video stream): add request, deny evennt handlers --- .../examples/sdk-frontend/pages/video-v2.tsx | 26 +++++------ packages/restapi/src/lib/pushapi/video.ts | 44 +++++++++++++++---- packages/restapi/src/lib/video/VideoV2.ts | 29 ++++++++++-- 3 files changed, 73 insertions(+), 26 deletions(-) diff --git a/packages/examples/sdk-frontend/pages/video-v2.tsx b/packages/examples/sdk-frontend/pages/video-v2.tsx index e4631b3d2..d61dcc4b7 100644 --- a/packages/examples/sdk-frontend/pages/video-v2.tsx +++ b/packages/examples/sdk-frontend/pages/video-v2.tsx @@ -15,7 +15,6 @@ import { initVideoCallData } from '@pushprotocol/restapi/src/lib/video'; import IncomingVideoModal from '../components/IncomingVideoModal'; import Toast from '../components/Toast'; import VideoPlayer from '../components/VideoPlayer'; -import { VIDEO_NOTIFICATION_ACCESS_TYPE } from '@pushprotocol/restapi/src/lib/payloads/constants'; const VideoV2: NextPage = () => { const { address, isConnected } = useAccount(); @@ -35,6 +34,7 @@ const VideoV2: NextPage = () => { useState(false); const [showCallConnectedToast, setShowCallConnectedToast] = useState(false); const [callEndedReinitialize, setCallEndedReinitialize] = useState(false); + const initializePushAPI = async () => { console.log('initializePushAPI'); setCallEndedReinitialize(false); @@ -101,18 +101,15 @@ const VideoV2: NextPage = () => { audio: true, }, }); + await createdStream.connect(); }; - useEffect(() => { - if (!signer) return; - initializePushAPI(); - }, [callEndedReinitialize]); useEffect(() => { if (!signer) return; initializePushAPI(); - }, [signer]); + }, [callEndedReinitialize]); useEffect(() => { console.log('isPushStreamConnected', isPushStreamConnected); @@ -166,11 +163,11 @@ const VideoV2: NextPage = () => { > Request Video Call -
) : ( diff --git a/packages/restapi/src/lib/pushapi/video.ts b/packages/restapi/src/lib/pushapi/video.ts index 7d9531114..b3036684c 100644 --- a/packages/restapi/src/lib/pushapi/video.ts +++ b/packages/restapi/src/lib/pushapi/video.ts @@ -1,12 +1,14 @@ import { ENV } from '../constants'; import CONSTANTS from '../constantsV2'; -import { SignerType, VideoCallData } from '../types'; +import { SignerType, VideoCallData, VideoCallStatus } from '../types'; import { Signer as PushSigner } from '../helpers'; -import { Video as VideoV1 } from '../video/Video'; +import { Video as VideoV1, initVideoCallData } from '../video/Video'; import { VideoV2 } from '../video/VideoV2'; import { VideoInitializeOptions } from './pushAPITypes'; import { VideoEvent, VideoEventType } from '../pushstream/pushStreamTypes'; +import { produce } from 'immer'; +import { endStream } from '../video/helpers/mediaToggle'; export class Video { constructor( @@ -68,9 +70,35 @@ export class Video { meta: { rules }, } = data.peerInfo; + const chatId = rules.access.data.chatId; + + // If the event is RequestVideo, update the video call 'data' state with the incoming call data + if (data.event === VideoEventType.RequestVideo) { + videoV1Instance.setData((oldData) => { + return produce(oldData, (draft) => { + draft.local.address = this.account; + draft.incoming[0].address = address; + draft.incoming[0].status = VideoCallStatus.RECEIVED; + draft.meta.chatId = chatId!; + draft.meta.initiator.address = address; + draft.meta.initiator.signal = signal; + }); + }); + } + // Check if the chatId from the incoming video event matches the chatId of the current video instance - if (rules.access.data.chatId === videoV1Instance.data.meta.chatId) { - // If the event is a ApproveVideo or RetryApproveVideo event, connect to the video + if (chatId && chatId === videoV1Instance.data.meta.chatId) { + // If the event is DenyVideo, destroy the local stream & reset the video call data + if (data.event === VideoEventType.DenyVideo) { + // destroy the local stream + if (videoV1Instance.data.local.stream) { + endStream(videoV1Instance.data.local.stream); + } + + videoV1Instance.setData(() => initVideoCallData); + } + + // If the event is ApproveVideo or RetryApproveVideo, connect to the video if ( data.event === VideoEventType.ApproveVideo || data.event === VideoEventType.RetryApproveVideo @@ -78,7 +106,7 @@ export class Video { videoV1Instance.connect({ peerAddress: address, signalData: signal }); } - // If the event is a RetryRequestVideo event and the current instance is the initiator, send a request + // If the event is RetryRequestVideo and the current instance is the initiator, send a request if ( data.event === VideoEventType.RetryRequestVideo && videoV1Instance.isInitiator() @@ -86,12 +114,12 @@ export class Video { videoV1Instance.request({ senderAddress: this.account, recipientAddress: address, - chatId: rules.access.data.chatId, + rules, retry: true, }); } - // If the event is a RetryRequestVideo event and the current instance is not the initiator, accept the request + // If the event is RetryRequestVideo and the current instance is not the initiator, accept the request if ( data.event === VideoEventType.RetryRequestVideo && !videoV1Instance.isInitiator() @@ -100,7 +128,7 @@ export class Video { signalData: signal, senderAddress: this.account, recipientAddress: address, - chatId: rules.access.data.chatId, + rules, retry: true, }); } diff --git a/packages/restapi/src/lib/video/VideoV2.ts b/packages/restapi/src/lib/video/VideoV2.ts index 479dee5f1..cad064e07 100644 --- a/packages/restapi/src/lib/video/VideoV2.ts +++ b/packages/restapi/src/lib/video/VideoV2.ts @@ -1,3 +1,4 @@ +import { produce } from 'immer'; import { chats } from '../chat'; import { ENV } from '../constants'; import { @@ -6,7 +7,11 @@ import { walletToPCAIP10, } from '../helpers'; import { VIDEO_NOTIFICATION_ACCESS_TYPE } from '../payloads/constants'; -import { VideoNotificationRules, VideoPeerInfo } from '../types'; +import { + VideoCallStatus, + VideoNotificationRules, + VideoPeerInfo, +} from '../types'; import { Video as VideoV1 } from './Video'; import { validatePeerInfo } from './helpers/validatePeerInfo'; @@ -118,12 +123,30 @@ export class VideoV2 { } } + this.videoInstance.setData((oldData) => { + return produce(oldData, (draft: any) => { + draft.local.address = this.account; + draft.incoming = recipients.map((recipient) => ({ + address: pCAIP10ToWallet(recipient), + status: VideoCallStatus.INITIALIZED, + })); + draft.meta.chatId = rules?.access.data.chatId ?? retrievedChatId; + }); + }); + await this.videoInstance.request({ senderAddress: pCAIP10ToWallet(this.account), recipientAddress: recipients.map((recipient) => pCAIP10ToWallet(recipient) ), - chatId: rules?.access.data.chatId ?? retrievedChatId, + rules: rules ?? { + access: { + type: VIDEO_NOTIFICATION_ACCESS_TYPE.PUSH_CHAT, + data: { + chatId: retrievedChatId, + }, + }, + }, }); } @@ -140,7 +163,7 @@ export class VideoV2 { senderAddress: pCAIP10ToWallet(this.account), recipientAddress: pCAIP10ToWallet(address), signalData: signal, - chatId: meta.rules.access.data.chatId, + rules: meta.rules, }); } From e3f048f13337596f7e4227d87016cdf1eaa5b1cb Mon Sep 17 00:00:00 2001 From: Siddesh Date: Thu, 11 Jan 2024 12:06:31 +0530 Subject: [PATCH 20/32] feat(sdk-frontend and sdk-frontend-react): added video-v2 example implementation --- .../src/app/Video/index.tsx | 239 ++++++++++++++++++ .../sdk-frontend-react/src/app/app.tsx | 33 +-- .../src/app/components/IncomingVideoModal.tsx | 75 ++++++ .../src/app/components/Toast.tsx | 43 ++++ .../src/app/components/VideoPlayer.tsx | 27 ++ .../components/IncomingVideoModal.tsx | 5 +- .../sdk-frontend/components/Toast.tsx | 11 +- .../examples/sdk-frontend/pages/video-v2.tsx | 31 ++- 8 files changed, 432 insertions(+), 32 deletions(-) create mode 100644 packages/examples/sdk-frontend-react/src/app/Video/index.tsx create mode 100644 packages/examples/sdk-frontend-react/src/app/components/IncomingVideoModal.tsx create mode 100644 packages/examples/sdk-frontend-react/src/app/components/Toast.tsx create mode 100644 packages/examples/sdk-frontend-react/src/app/components/VideoPlayer.tsx diff --git a/packages/examples/sdk-frontend-react/src/app/Video/index.tsx b/packages/examples/sdk-frontend-react/src/app/Video/index.tsx new file mode 100644 index 000000000..fceb717d0 --- /dev/null +++ b/packages/examples/sdk-frontend-react/src/app/Video/index.tsx @@ -0,0 +1,239 @@ +import type { NextPage } from 'next'; + +import { + PushAPI, + CONSTANTS, + VideoCallData, + VideoEvent, + VideoEventType, +} from '@pushprotocol/restapi'; + +import styled from 'styled-components'; + +import { useContext, useEffect, useRef, useState } from 'react'; +import IncomingVideoModal from '../components/IncomingVideoModal'; +import Toast from '../components/Toast'; +import VideoPlayer from '../components/VideoPlayer'; +import { EnvContext, Web3Context } from '../context'; + +const VideoV2: NextPage = () => { + const { account, library } = useContext(Web3Context); + const { env } = useContext(EnvContext); + const librarySigner = library.getSigner(); + const aliceVideoCall = useRef(); + const [latestVideoEvent, setLatestVideoEvent] = useState( + null + ); + const [isPushStreamConnected, setIsPushStreamConnected] = useState(false); + + const [data, setData] = useState(); + const [recipientAddress, setRecipientAddress] = useState(); + const [showIncomingVideoModal, setShowIncomingVideoModal] = useState(false); + const [showCallDisconnectedToast, setShowCallDisconnectedToast] = + useState(false); + const [showCallConnectedToast, setShowCallConnectedToast] = useState(false); + const [userDeniedCallStatus, setUserDeniedCallStatus] = useState(false); + + const initializePushAPI = async () => { + const userAlice = await PushAPI.initialize(librarySigner, { + env: env, + }); + + const createdStream = await userAlice.initStream([ + CONSTANTS.STREAM.VIDEO, + CONSTANTS.STREAM.CONNECT, + CONSTANTS.STREAM.DISCONNECT, + ]); + + createdStream.on(CONSTANTS.STREAM.CONNECT, () => { + console.log('PUSH STREAM CONNECTED: '); + setIsPushStreamConnected(true); + }); + + createdStream.on(CONSTANTS.STREAM.DISCONNECT, () => { + console.log('PUSH STREAM DISCONNECTED: '); + setIsPushStreamConnected(false); + }); + + createdStream.on(CONSTANTS.STREAM.VIDEO, async (data: VideoEvent) => { + // Handle incoming call, when the type is RequestVideo + if (data.event === VideoEventType.RequestVideo) { + setLatestVideoEvent(data); + setShowIncomingVideoModal(true); + } + + // If the received status is ApproveVideo that means we can connect the video call + if (data.event === VideoEventType.ApproveVideo) { + console.log('ApproveVideo', data); + setShowCallConnectedToast(true); + setLatestVideoEvent(data); + } + + // If the received status is DenyVideo that means the call has ended + if (data.event === VideoEventType.DenyVideo) { + // here you can do a window reload + console.log('DenyVideo', data); + setUserDeniedCallStatus(true); + initializePushAPI(); + } + + // If the received status is ConnectVideo that means the call was connected + if (data.event === VideoEventType.ConnectVideo) { + // can update the ui with a toast or something that the call is connected + console.log('ConnectVideo', data); + setShowCallConnectedToast(true); + } + + // If the received status is DisconnectVideo that means the call has ended/someone hung up after it was connected + if (data.event === VideoEventType.DisconnectVideo) { + console.log('DisconnectVideo', data); + setShowCallDisconnectedToast(true); + initializePushAPI(); + } + }); + + aliceVideoCall.current = await userAlice.video.initialize(setData, { + socketStream: createdStream, + media: { + video: true, + audio: true, + }, + }); + + await createdStream.connect(); + }; + + useEffect(() => { + if (!librarySigner) return; + console.log('env', env); + initializePushAPI(); + }, [env, library]); + + useEffect(() => { + console.log('isPushStreamConnected', isPushStreamConnected); + if (isPushStreamConnected) + console.log('latestVideoEvent', latestVideoEvent); + }, [isPushStreamConnected, latestVideoEvent]); + + const requestVideoCall = async (recipient: string) => { + console.log('requestVideoCall', recipient); + await aliceVideoCall.current.request([recipient]); + }; + + const acceptIncomingCall = async () => { + await aliceVideoCall.current.approve(latestVideoEvent?.peerInfo); + setShowIncomingVideoModal(false); + }; + const denyIncomingCall = async () => { + await aliceVideoCall.current.deny(latestVideoEvent?.peerInfo); + setShowIncomingVideoModal(false); + initializePushAPI(); + }; + const endCall = async () => { + console.log(latestVideoEvent?.peerInfo?.address); + await aliceVideoCall.current.disconnect( + latestVideoEvent?.peerInfo?.address + ); + + initializePushAPI(); + }; + + return ( +
+ {account ? ( +
+ {showCallDisconnectedToast && ( + + )} + {showCallConnectedToast && ( + + )} + {userDeniedCallStatus && ( + + )} + + setRecipientAddress(e.target.value)} + value={recipientAddress} + placeholder="recipient address" + type="text" + /> + + + + + + + + {showIncomingVideoModal && ( + + )} + + +

LOCAL VIDEO: {data?.local.video ? 'TRUE' : 'FALSE'}

+

LOCAL AUDIO: {data?.local.audio ? 'TRUE' : 'FALSE'}

+

INCOMING VIDEO: {data?.incoming[0]?.video ? 'TRUE' : 'FALSE'}

+

INCOMING AUDIO: {data?.incoming[0]?.audio ? 'TRUE' : 'FALSE'}

+
+ + +

Local Video

+ +
+ + +

Incoming Video

+ +
+
+
+ ) : ( + 'Please connect your wallet.' + )} +
+ ); +}; + +const Heading = styled.h1` + margin: 20px 40px; +`; +const HContainer = styled.div` + display: flex; + gap: 20px; + margin: 20px 40px; +`; +const VContainer = styled.div` + display: flex; + gap: 10px; + flex-direction: column; + width: fit-content; + height: fit-content; +`; +export default VideoV2; diff --git a/packages/examples/sdk-frontend-react/src/app/app.tsx b/packages/examples/sdk-frontend-react/src/app/app.tsx index 1f60c2ad4..402d723c0 100644 --- a/packages/examples/sdk-frontend-react/src/app/app.tsx +++ b/packages/examples/sdk-frontend-react/src/app/app.tsx @@ -92,7 +92,7 @@ import { ChatSupportTest } from './ChatSupportTest'; import GetGroupMemberCountTest from './ChatTest/GetGroupMemberCountTest'; import GetGroupInfoTest from './ChatTest/GetGroupInfoTest'; import GetGroupMembersTest from './ChatTest/GetGroupMembersTest'; - +import VideoV2 from './Video'; window.Buffer = window.Buffer || Buffer; @@ -317,7 +317,11 @@ export function App() { - + CHAT UI + + VIDEO + SPACE @@ -380,6 +387,7 @@ export function App() { } /> } /> } /> + } /> } /> {/* chat method routes */} } /> @@ -575,19 +583,14 @@ export function App() { } - /> - } - /> } - /> - - {/* */} - {/* */} - - + /> + } />{' '} + } /> + + {/* */} + {/* */} + + diff --git a/packages/examples/sdk-frontend-react/src/app/components/IncomingVideoModal.tsx b/packages/examples/sdk-frontend-react/src/app/components/IncomingVideoModal.tsx new file mode 100644 index 000000000..d7dafeb1a --- /dev/null +++ b/packages/examples/sdk-frontend-react/src/app/components/IncomingVideoModal.tsx @@ -0,0 +1,75 @@ +// Import necessary libraries +import React from 'react'; +import styled from 'styled-components'; + +type IncomingVideoModalProps = { + callerID: string | undefined; + onAccept: () => void; + onReject: () => void; +}; + +// Styled components for the CallControl component +const IncomingVideoModalWrapper = styled.div` + display: flex; + flex-direction: column; + align-items: center; + padding: 20px; + width: 400px; + border-radius: 5px; + background-color: black; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + z-index: 100; +`; + +const CallerID = styled.div` + font-size: 14px; + margin-bottom: 20px; + color: #fff; +`; + +const Button = styled.button` + padding: 10px 20px; + margin: 5px; + font-size: 16px; + cursor: pointer; + border: none; + border-radius: 5px; + outline: none; +`; + +const GreenButton = styled(Button)` + background-color: #4caf50; + color: #fff; + cursor: pointer; +`; + +const RedButton = styled(Button)` + background-color: #f44336; + color: #fff; + cursor: pointer; +`; +const ButtonContainer = styled.div` + display: flex; + justify-content: space-around; +`; +// CallControl component +const IncomingVideoModal = ({ + callerID, + onAccept, + onReject, +}: IncomingVideoModalProps) => { + return ( + + {callerID} is calling... + + Accept + Reject + + + ); +}; + +export default IncomingVideoModal; diff --git a/packages/examples/sdk-frontend-react/src/app/components/Toast.tsx b/packages/examples/sdk-frontend-react/src/app/components/Toast.tsx new file mode 100644 index 000000000..5898d854a --- /dev/null +++ b/packages/examples/sdk-frontend-react/src/app/components/Toast.tsx @@ -0,0 +1,43 @@ +import React, { useState, useEffect } from 'react'; +import styled from 'styled-components'; + +interface ToastProps { + message: string; + bg: string; +} + +const ToastContainer = styled.div` + position: fixed; + bottom: 20px; + left: 50%; + transform: translateX(-50%); + background-color: ${(props) => props.bg}; + color: #fff; + padding: 10px 20px; + border-radius: 5px; + opacity: ${(props) => (props.visible ? '1' : '0')}; + transition: opacity 1.5s ease-in-out; + z-index: 1000; +`; + +const Toast: React.FC = ({ message, bg }) => { + const [visible, setVisible] = useState(true); + + useEffect(() => { + const timer = setTimeout(() => { + setVisible(false); + }, 3000); + + return () => { + clearTimeout(timer); + }; + }, []); + + return ( + + {message} + + ); +}; + +export default Toast; diff --git a/packages/examples/sdk-frontend-react/src/app/components/VideoPlayer.tsx b/packages/examples/sdk-frontend-react/src/app/components/VideoPlayer.tsx new file mode 100644 index 000000000..200f10357 --- /dev/null +++ b/packages/examples/sdk-frontend-react/src/app/components/VideoPlayer.tsx @@ -0,0 +1,27 @@ +import { useEffect, useRef } from 'react'; +import styled from 'styled-components'; + +type VideoPlayerPropsType = { + stream: MediaStream | null; + isMuted: boolean; +}; + +const VideoPlayer = ({ stream, isMuted }: VideoPlayerPropsType) => { + const videoRef = useRef(null); + + useEffect(() => { + if (videoRef.current) { + videoRef.current.srcObject = stream; + } + }, [videoRef, stream]); + + return