Skip to content

Commit

Permalink
feat: add video SDK v2 and video stream (#1017)
Browse files Browse the repository at this point in the history
* add: ethers support (#952)

* fix: fix viem support, add ethers support

* fix: remove @pushprotocol/socket dependency from restapi

* fix: changed ethers fn

* fix: fix ether changes

* fix: fix subscribev2

* chore: readme changes

* fix: fix ethers provider issue

* fix: fix channel.update

* feat(video-v2): add video v2 class and stream (#930)

* feat(video-v2): add highlevel video class

* feat(video): add video stream

* fix(sendnotification): modify rules.access.data to be an object & code cleanup

* fix(video): remove signer from input, throw err if signer, decrypted pgp key  not found

* feat(video): add sendNotification calls in connect, disconnect methods

* fix(videonotificationrules): typo in VideoNotificationRules interface

* feat(video stream): handle connect, retry internally from the SDK

* feat(video connect): remove the connect method from the videov2 SDK

* fix(videov2): create push signer instance to get chain id in initialize()

* fix(video stream): add backwards compatibilty to rules object for older SDK versions

* fix(video stream): fix bug in rules object creation

* feat(video): update param names for video.initialize()

* feat(video): add internal event handlers for stream in video SDK

---------

Co-authored-by: Mohammed S <shoaibmohammed92@gmail.com>

---------

Co-authored-by: Aman Gupta <guptaaman200115@gmail.com>
Co-authored-by: Mohammed S <shoaibmohammed92@gmail.com>
  • Loading branch information
3 people authored Jan 15, 2024
1 parent 8e2cec9 commit 6ae974b
Show file tree
Hide file tree
Showing 17 changed files with 650 additions and 56 deletions.
4 changes: 2 additions & 2 deletions packages/restapi/src/lib/payloads/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
ISendNotificationInputOptions,
INotificationPayload,
walletType,
VideNotificationRules,
VideoNotificationRules,
} from '../types';
import {
IDENTITY_TYPE,
Expand Down Expand Up @@ -212,7 +212,7 @@ export async function getVerificationProof({
wallet?: walletType;
pgpPrivateKey?: string;
env?: ENV;
rules?:VideNotificationRules;
rules?:VideoNotificationRules;
}) {
let message = null;
let verificationProof = null;
Expand Down
4 changes: 2 additions & 2 deletions packages/restapi/src/lib/payloads/sendNotifications.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
Expand Down Expand Up @@ -231,7 +231,7 @@ export async function sendNotification(options: ISendNotificationInputOptions) {
? {
rules: rules ?? {
access: {
data: chatId,
data: { chatId },
type: VIDEO_NOTIFICATION_ACCESS_TYPE.PUSH_CHAT,
},
},
Expand Down
10 changes: 10 additions & 0 deletions packages/restapi/src/lib/pushapi/PushAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
STREAM,
} from '../pushstream/pushStreamTypes';
import { ALPHA_FEATURE_CONFIG } from '../config';
import { Video } from './video';
import { isValidCAIP10NFTAddress } from '../helpers';

export class PushAPI {
Expand All @@ -29,6 +30,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;
Expand Down Expand Up @@ -86,6 +89,13 @@ 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
);

this.errors = initializationErrors || [];
}
// Overloaded initialize method signatures
Expand Down
10 changes: 10 additions & 0 deletions packages/restapi/src/lib/pushapi/pushAPITypes.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Constants, { ENV } from '../constants';
import type { PushStream } from '../pushstream/PushStream';
import { ChatStatus, ProgressHookType, Rules } from '../types';

export enum ChatListType {
Expand Down Expand Up @@ -66,3 +67,12 @@ export interface ParticipantStatus {
role: 'admin' | 'member';
participant: boolean;
}

export interface VideoInitializeOptions {
stream: PushStream;
config: {
video?: boolean;
audio?: boolean;
};
media?: MediaStream;
}
146 changes: 146 additions & 0 deletions packages/restapi/src/lib/pushapi/video.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import { ENV } from '../constants';
import CONSTANTS from '../constantsV2';
import { SignerType, VideoCallData, VideoCallStatus } from '../types';
import { Signer as PushSigner } from '../helpers';

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(
private account: string,
private env: ENV,
private decryptedPgpPvtKey?: string,
private signer?: SignerType
) {}

async initialize(
onChange: (fn: (data: VideoCallData) => VideoCallData) => void,
options: VideoInitializeOptions
) {
const { stream, config, media } = options;

if (!this.signer) {
throw new Error('Signer is required for push video');
}

if (!this.decryptedPgpPvtKey) {
throw new Error(
'PushSDK was initialized in readonly mode. Video functionality is not available.'
);
}

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!,
chainId,
pgpPrivateKey: this.decryptedPgpPvtKey!,
env: this.env,
setData: onChange,
});

// Create the media stream with the provided options
await videoV1Instance.create({
...(media && {
stream: media,
}),
...(config?.audio && {
audio: config.audio,
}),
...(config?.video && {
video: config.video,
}),
});

// Setup video event handlers
stream.on(CONSTANTS.STREAM.VIDEO, (data: VideoEvent) => {
const {
address,
signal,
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 (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
) {
videoV1Instance.connect({ peerAddress: address, signalData: signal });
}

// If the event is RetryRequestVideo and the current instance is the initiator, send a request
if (
data.event === VideoEventType.RetryRequestVideo &&
videoV1Instance.isInitiator()
) {
videoV1Instance.request({
senderAddress: this.account,
recipientAddress: address,
rules,
retry: true,
});
}

// If the event is RetryRequestVideo 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,
rules,
retry: true,
});
}
}
});

// Return an instance of the video v2 class
return new VideoV2({
videoV1Instance,
account: this.account,
decryptedPgpPvtKey: this.decryptedPgpPvtKey!,
env: this.env,
});
}
}
75 changes: 75 additions & 0 deletions packages/restapi/src/lib/pushstream/DataModifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,13 @@ import {
NotificationType,
NOTIFICATION,
ProposedEventNames,
VideoEventType,
MessageOrigin,
VideoEvent,
} 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 {
Expand Down Expand Up @@ -388,4 +394,73 @@ 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.CONNECTED:
return VideoEventType.ConnectVideo;
case VideoCallStatus.ENDED:
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}`);
}
}

public static mapToVideoEvent(
data: any,
origin: MessageOrigin,
includeRaw = false
): VideoEvent {
const { senderAddress, signalData, status, chatId }: VideoDataType =
JSON.parse(data.payload.data.additionalMeta?.data);

// 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,
},
},
};

const peerInfo: VideoPeerInfo = {
address: senderAddress,
signal: signalData,
meta: {
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;
}
}
Loading

0 comments on commit 6ae974b

Please sign in to comment.