Kaigi-ni JavaScript tətbiqetməsinə daxil etmək
Kaigi bir tətbiqə pullu dəstəkləyən bir-bir audio / video görüşləri yaratmağa imkan verir ki, onların həyat dövrü Iroha vasitəsilə qeyd olunur. Brauzer hələ də medianı WebRTC ilə idarə edir, Torii və Kaigi təlimatları davamlı toplantı qeydini, şifrələnmiş siqnal metadatalarını təmin edir. özəl siyahı dəstək və istifadə hadisələri.
Bu təlimat Iroha Demo JavaScript tətbiqi tərəfindən istifadə olunan minimal inteqrasiya nümunəsini göstərir:
- Renderer WebRTC təklifləri və cavabları yaradır
- Ərizə köprüni işarələyir və Kaigi əməliyyatları təqdim edir.
- Kompakt dəvət linkləri yalnız ID çağırışı və gizli dəvət alır
- aparıcı Torii şifreli iştirakçı cavabları üçün izləyir
nümunələr TypeScript istifadə edir və Electron, etibarlı bir arka uçlu brauzerdə və ya cüzdan uzantısı olan veb tətbiqetməsində işləmək üçün yazılıblar.
Əvvəlki şərtlər
Sənə lazım:
- Kaigi -ə malik olan Torii son nöqtəsi
- Ev sahibi və qonaq hesabı.
- Hər hesabın imza açarına təhlükəsiz bir tətbiq köprü və ya cüzdan vasitəsilə daxil olmaq
- brauzer kamerası/mikrofon icazələri
- Node.js 20+ if you're using the JavaScript demo or native
@iroha/iroha-jsbinding directly
Tam işləmə istinadı üçün Iroha mənbə verilişinin yanındakı demo klonlayın:
mkdir iroha-wallet-workspace
cd iroha-wallet-workspace
git clone https://github.com/hyperledger-iroha/iroha.git
git clone https://github.com/soramitsu/iroha-demo-javascript.git
cd iroha/javascript/iroha_js
npm install
npm run build:native
npm run build:dist
cd ../../../iroha-demo-javascript
npm install
npm run devIroha mənbə anbarından olan @iroha/iroha-js ilə demo istifadə edin. Onun file: asılılığı birbaşa yoxlanışı həll edir. Doğrudan bağlama dəyişsə, onu iroha/javascript/iroha_js altında yenidən qurun; təmiz paketlər dizaynında npm run build:native üçün lazım olan yük iş məkanı yoxdur.
TAIRA şəbəkəsində canlı iclas keçirməzdən əvvəl demo-dan asılı olan ictimai Torii səthini yoxlayın:
TAIRA=https://taira.sora.org
curl -fsS "$TAIRA/health"
curl -fsS "$TAIRA/v1/kaigi/relays"
curl -fsS "$TAIRA/v1/kaigi/relays/health"Bu əmrlər təsdiq edir ki, TAIRA canlıdır və bu Kaigi relay telemetriyası mövcuddur. Kaigi real əməliyyatlar. CreateKaigi və ya JoinKaigi sınaq ehtiyacları maliyyələşdirilir TAIRA Hesablar və demo körpüsü vasitəsilə imzalanma və ya başqa bir cüzdan dəstəkləyən körpü.
Memarlıq
Kaigi inteqrasiyasını üç təbəqədə bölün:
| Layer | Məsuliyyət |
|---|---|
| UI | Hesab seçimi, iclas forması, dəvət linklərinin göstərilməsi, media nəzarətləri |
| WebRTC | RTCPeerConnection, yerli media, təklif və cavab təsvirləri |
| Iroha köprü | İmzalama, CreateKaigi, JoinKaigi, EndKaigi, siqnal səsverməsi |
Tətbiq köprü elektron yüklənmə ola bilər API, bir cüzdan uzantısı və ya arka plan son nöqtəsi olmalıdır. UI:
type KaigiMeetingPrivacy = "private" | "transparent";
type KaigiPeerIdentityReveal = "Hidden" | "RevealAfterJoin";
type KaigiSignalKeyPair = {
publicKeyBase64Url: string;
privateKeyBase64Url: string;
};
type KaigiDescription = {
type: "offer" | "answer";
sdp: string;
};
type KaigiMeeting = {
callId: string;
meetingCode: string;
title?: string;
hostAccountId?: string;
hostDisplayName?: string;
hostParticipantId?: string;
hostKaigiPublicKeyBase64Url: string;
scheduledStartMs: number;
expiresAtMs: number;
live: boolean;
ended: boolean;
privacyMode: KaigiMeetingPrivacy;
peerIdentityReveal: KaigiPeerIdentityReveal;
rosterRootHex: string;
offerDescription: { type: "offer"; sdp: string };
};
type KaigiSignal = {
entrypointHash: string;
callId: string;
participantId: string;
participantName: string;
createdAtMs: number;
answerDescription: { type: "answer"; sdp: string };
};
type KaigiBridge = {
generateKaigiSignalKeyPair(): KaigiSignalKeyPair;
createKaigiMeeting(input: {
toriiUrl: string;
chainId: string;
hostAccountId: string;
callId: string;
title?: string;
scheduledStartMs: number;
meetingCode: string;
inviteSecretBase64Url: string;
hostDisplayName: string;
hostParticipantId: string;
hostKaigiPublicKeyBase64Url: string;
offerDescription: { type: "offer"; sdp: string };
privacyMode: KaigiMeetingPrivacy;
peerIdentityReveal: KaigiPeerIdentityReveal;
}): Promise<{ hash: string }>;
getKaigiCall(input: {
toriiUrl: string;
callId: string;
inviteSecretBase64Url: string;
}): Promise<KaigiMeeting>;
joinKaigiMeeting(input: {
toriiUrl: string;
chainId: string;
participantAccountId: string;
callId: string;
hostAccountId?: string;
hostKaigiPublicKeyBase64Url: string;
participantId: string;
participantName: string;
walletIdentity?: string;
roomId: string;
privacyMode: KaigiMeetingPrivacy;
rosterRootHex: string;
answerDescription: { type: "answer"; sdp: string };
}): Promise<{ hash: string }>;
pollKaigiMeetingSignals(input: {
toriiUrl: string;
accountId: string;
callId: string;
hostKaigiKeys: KaigiSignalKeyPair;
afterTimestampMs?: number;
}): Promise<KaigiSignal[]>;
watchKaigiCallEvents(
input: { toriiUrl: string; callId: string },
onEvent: (event: { kind: string; callId: string }) => void | Promise<void>,
): Promise<string>;
endKaigiMeeting(input: {
toriiUrl: string;
chainId: string;
hostAccountId: string;
callId: string;
endedAtMs?: number;
}): Promise<{ hash: string }>;
};Demo tətbiqində bu köprü üsulları @iroha/iroha-js, yerli imzalanma, şifrələnmiş Kaigi metadata və Torii zənglərlə həyata keçirilir.
Yardımçıları dəvət edin
Torii-ə uyğun zəng IDs istifadə edin domain.dataspace:meeting formasında. Demo iclaslar üçün kaigi.universal:<call-name> istifadə edir.
const KAIGI_WINDOW_MS = 24 * 60 * 60 * 1000;
const base64Url = (bytes: Uint8Array): string =>
btoa(String.fromCharCode(...bytes))
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=+$/g, "");
export function createInviteSecret(): string {
const bytes = new Uint8Array(24);
crypto.getRandomValues(bytes);
return base64Url(bytes);
}
export function createMeetingCode(): string {
const bytes = new Uint8Array(8);
crypto.getRandomValues(bytes);
return base64Url(bytes).toLowerCase();
}
export function buildKaigiCallId(domain: string, meetingCode: string): string {
const qualifiedDomain = domain.includes(".") ? domain : `${domain}.universal`;
const safeCode = meetingCode
.toLowerCase()
.replace(/[^a-z0-9-]+/g, "-")
.replace(/^-|-$/g, "");
return `${qualifiedDomain}:kaigi-${safeCode || "meeting"}`;
}
export function buildInviteLink(input: {
callId: string;
inviteSecretBase64Url: string;
}): string {
const call = encodeURIComponent(input.callId);
const secret = encodeURIComponent(input.inviteSecretBase64Url);
return `iroha://kaigi/join?call=${call}&secret=${secret}`;
}
export function parseInviteLink(link: string): {
callId: string;
inviteSecretBase64Url: string;
} {
const url = new URL(link);
const callId = url.searchParams.get("call")?.trim();
const inviteSecretBase64Url = url.searchParams.get("secret")?.trim();
if (!callId || !inviteSecretBase64Url) {
throw new Error("Kaigi invite link is missing call or secret.");
}
return { callId, inviteSecretBase64Url };
}WebRTC Yardımçılar
Qonaq bir təklif yaradır, onu CreateKaigi vasitəsilə saxlayır və qonağın cavabını tətbiq etmək üçün pəncərəni açıq saxlayır. Qonaq şifrələnmiş təklifi alır, bir cavab yaratır və cavabı JoinKaigi ilə göndərir.
const rtcConfig: RTCConfiguration = {
iceServers: [{ urls: "stun:stun.l.google.com:19302" }],
};
export async function openLocalMedia(): Promise<MediaStream> {
return navigator.mediaDevices.getUserMedia({
audio: true,
video: {
width: { ideal: 1280 },
height: { ideal: 720 },
frameRate: { ideal: 24, max: 30 },
},
});
}
export function createPeer(localStream: MediaStream): RTCPeerConnection {
const peer = new RTCPeerConnection(rtcConfig);
for (const track of localStream.getTracks()) {
peer.addTrack(track, localStream);
}
return peer;
}
async function waitForIceGathering(peer: RTCPeerConnection): Promise<void> {
if (peer.iceGatheringState === "complete") {
return;
}
await new Promise<void>((resolve) => {
const done = () => {
if (peer.iceGatheringState === "complete") {
peer.removeEventListener("icegatheringstatechange", done);
resolve();
}
};
peer.addEventListener("icegatheringstatechange", done);
});
}
export async function createOfferDescription(
peer: RTCPeerConnection,
): Promise<{ type: "offer"; sdp: string }> {
const offer = await peer.createOffer();
await peer.setLocalDescription(offer);
await waitForIceGathering(peer);
const local = peer.localDescription;
if (!local?.sdp || local.type !== "offer") {
throw new Error("WebRTC offer was not created.");
}
return { type: "offer", sdp: local.sdp };
}
export async function createAnswerDescription(
peer: RTCPeerConnection,
offer: { type: "offer"; sdp: string },
): Promise<{ type: "answer"; sdp: string }> {
await peer.setRemoteDescription(offer);
const answer = await peer.createAnswer();
await peer.setLocalDescription(answer);
await waitForIceGathering(peer);
const local = peer.localDescription;
if (!local?.sdp || local.type !== "answer") {
throw new Error("WebRTC answer was not created.");
}
return { type: "answer", sdp: local.sdp };
}Axını UI ilə adi video elementləri ilə bağlayın:
export function attachKaigiMedia(input: {
peer: RTCPeerConnection;
localStream: MediaStream;
localVideo: HTMLVideoElement;
remoteVideo: HTMLVideoElement;
}): void {
input.localVideo.srcObject = input.localStream;
const remoteStream = new MediaStream();
input.remoteVideo.srcObject = remoteStream;
input.peer.addEventListener("track", (event) => {
if (event.streams[0]) {
input.remoteVideo.srcObject = event.streams[0];
return;
}
remoteStream.addTrack(event.track);
});
}Ev sahibi: Toplantı ilə əlaqə qurun
Ev sahibi axını:
- Açıq kamera və mikrofon
- bir Kaigi siqnal açar cütü yaratmaq
- WebRTC təklifini yaratmaq
CreateKaigitəqdim etmək- kompakt bir dəvət linkini paylaşın
type AccountContext = {
accountId: string;
displayName: string;
};
type KaigiContext = {
bridge: KaigiBridge;
toriiUrl: string;
chainId: string;
};
export async function hostKaigiMeeting(input: {
context: KaigiContext;
account: AccountContext;
title?: string;
privacyMode?: KaigiMeetingPrivacy;
}): Promise<{
callId: string;
inviteLink: string;
peer: RTCPeerConnection;
localStream: MediaStream;
hostKaigiKeys: KaigiSignalKeyPair;
createdAtMs: number;
}> {
const { bridge, toriiUrl, chainId } = input.context;
const privacyMode = input.privacyMode ?? "private";
const scheduledStartMs = Date.now();
const meetingCode = createMeetingCode();
const callId = buildKaigiCallId("kaigi", meetingCode);
const inviteSecretBase64Url = createInviteSecret();
const hostKaigiKeys = bridge.generateKaigiSignalKeyPair();
const localStream = await openLocalMedia();
const peer = createPeer(localStream);
const offerDescription = await createOfferDescription(peer);
await bridge.createKaigiMeeting({
toriiUrl,
chainId,
hostAccountId: input.account.accountId,
callId,
title: input.title,
scheduledStartMs,
meetingCode,
inviteSecretBase64Url,
hostDisplayName: input.account.displayName,
hostParticipantId: "host",
hostKaigiPublicKeyBase64Url: hostKaigiKeys.publicKeyBase64Url,
offerDescription,
privacyMode,
peerIdentityReveal: "Hidden",
});
return {
callId,
inviteLink: buildInviteLink({ callId, inviteSecretBase64Url }),
peer,
localStream,
hostKaigiKeys,
createdAtMs: scheduledStartMs,
};
}inviteLink nümayiş etdirin UI. İstifadəçi onu nüsxələyə, başqa cüzdanda aça və ya tətbiqi yolu kimi çevirə bilər:
export function inviteRoute(inviteLink: string): string {
const invite = parseInviteLink(inviteLink);
return `/kaigi?call=${encodeURIComponent(invite.callId)}&secret=${encodeURIComponent(
invite.inviteSecretBase64Url,
)}`;
}Qonaq: İclasda iştirak edin
Qonaq axını:
- dəvətnaməni təhlil edin
- Torii -dən şifrələnmiş çağırış təklifini alın.
- WebRTC cavabını yaratın
JoinKaigişifrələnmiş cavab metadataları ilə təqdim edin
export async function joinKaigiMeetingFromInvite(input: {
context: KaigiContext;
account: AccountContext;
inviteLink: string;
}): Promise<{
callId: string;
peer: RTCPeerConnection;
localStream: MediaStream;
}> {
const { bridge, toriiUrl, chainId } = input.context;
const { callId, inviteSecretBase64Url } = parseInviteLink(input.inviteLink);
const meeting = await bridge.getKaigiCall({
toriiUrl,
callId,
inviteSecretBase64Url,
});
if (meeting.ended) {
throw new Error("This Kaigi meeting has already ended.");
}
if (Date.now() > meeting.expiresAtMs) {
throw new Error("This Kaigi invite has expired.");
}
const localStream = await openLocalMedia();
const peer = createPeer(localStream);
const answerDescription = await createAnswerDescription(
peer,
meeting.offerDescription,
);
await bridge.joinKaigiMeeting({
toriiUrl,
chainId,
participantAccountId: input.account.accountId,
callId: meeting.callId,
hostAccountId: meeting.hostAccountId,
hostKaigiPublicKeyBase64Url: meeting.hostKaigiPublicKeyBase64Url,
participantId: "guest",
participantName: input.account.displayName,
roomId: meeting.callId,
privacyMode: meeting.privacyMode,
rosterRootHex: meeting.rosterRootHex,
answerDescription,
});
return { callId: meeting.callId, peer, localStream };
}Əgər toplantı şəffafdırsa, birləşmə tələbinə cüzdan nümayiş etdirmə xətti daxil edə bilərsiniz. walletIdentity İstifadəçinin açıq şəkildə ifadə etməsini seçmədiyi təqdirdə, müəyyənləşdirilməz.
Ev sahibi: Qonağın cavabını tətbiq edin
Canlı bir görüş yaratdıqdan sonra aparıcı Kaigi hadisələrini izləməlidir və şifrələnmiş cavab siqnalları üçün sorğu verməlidir. İlk etibarlı cavabı aparıcının həmyaşıd əlaqəsi ilə tətbiq edin.
export async function watchForKaigiAnswer(input: {
context: KaigiContext;
hostAccountId: string;
callId: string;
hostKaigiKeys: KaigiSignalKeyPair;
createdAtMs: number;
peer: RTCPeerConnection;
onParticipant?: (signal: KaigiSignal) => void;
}): Promise<string | null> {
const { bridge, toriiUrl } = input.context;
const seenSignals = new Set<string>();
let lastSignalAtMs = input.createdAtMs;
const checkSignals = async (): Promise<boolean> => {
const signals = await bridge.pollKaigiMeetingSignals({
toriiUrl,
accountId: input.hostAccountId,
callId: input.callId,
hostKaigiKeys: input.hostKaigiKeys,
afterTimestampMs: lastSignalAtMs,
});
const next = signals.find(
(signal) => !seenSignals.has(signal.entrypointHash),
);
if (!next) {
return false;
}
seenSignals.add(next.entrypointHash);
lastSignalAtMs = Math.max(lastSignalAtMs, next.createdAtMs);
await input.peer.setRemoteDescription(next.answerDescription);
input.onParticipant?.(next);
return true;
};
if (await checkSignals()) {
return null;
}
return bridge.watchKaigiCallEvents(
{ toriiUrl, callId: input.callId },
async (event) => {
if (event.kind !== "ended") {
await checkSignals();
}
},
);
}Geri qaytarılan abunə ID saxlayın ki, sizin UI aparıcısı bağlandıqda və ya uzaqlaşdıqda izləyicini dayandıra bilər.
Görüşün bitməsi
Çıxışı yaratmış eyni ev sahibi hesabından bitirin:
export async function endKaigi(input: {
context: KaigiContext;
hostAccountId: string;
callId: string;
peer?: RTCPeerConnection;
localStream?: MediaStream;
}): Promise<void> {
input.peer?.close();
input.localStream?.getTracks().forEach((track) => track.stop());
await input.context.bridge.endKaigiMeeting({
toriiUrl: input.context.toriiUrl,
chainId: input.context.chainId,
hostAccountId: input.hostAccountId,
callId: input.callId,
endedAtMs: Date.now(),
});
}Özəl rejimdə maliyyələşdirmə
Xüsusi Kaigi yaratmaq, qoşulmaq və bitirmə əməliyyatları xüsusi giriş nöqtəsi haqqı üçün qorunmuş XOR tələb edə bilər. Tətbiqiniz bu səhvini aşkar etməlidir və yenidən cəhd etməkdən əvvəl öz-özünə qorunma hərəkəti təklif edir.
type PrivateKaigiFundingBridge = KaigiBridge & {
getPrivateKaigiConfidentialXorState(input: {
toriiUrl: string;
accountId: string;
}): Promise<{
shieldedBalance: string | null;
transparentBalance: string;
canSelfShield: boolean;
message?: string;
}>;
selfShieldPrivateKaigiXor(input: {
toriiUrl: string;
chainId: string;
accountId: string;
amount: string;
}): Promise<{ hash: string }>;
};
export async function selfShieldForPrivateKaigi(input: {
context: Omit<KaigiContext, "bridge"> & {
bridge: PrivateKaigiFundingBridge;
};
accountId: string;
amount: string;
}): Promise<void> {
const { bridge, toriiUrl, chainId } = input.context;
const state = await bridge.getPrivateKaigiConfidentialXorState({
toriiUrl,
accountId: input.accountId,
});
if (!state.canSelfShield) {
throw new Error(
state.message || "This account cannot self-shield XOR for private Kaigi.",
);
}
await bridge.selfShieldPrivateKaigiXor({
toriiUrl,
chainId,
accountId: input.accountId,
amount: input.amount,
});
}Demo-da, UI istifadəçini özünü qorumağa çağırır və sonra orijinal yaratmaq və ya qoşulma hərəkətini yenidən sınayır.
Əlyazmalar
Avtomatik siqnallaşdırma canlı bir cüzdan, Kaigi -ə malik olan Torii marşrutlardan və şəxsi rejimdə sübut istehsalından asılıdır. İnkişaf və məhdud mühitlər üçün əl ilə geri çəkilmə saxlayın:
CreateKaigiuğursuz olarsa, təklif daxil olan əl dəvəti göstərin- Əgər
JoinKaigiuğursuz olsa, xam cavab paketini göstərin - Ev sahibi cavab paketini yapışdırsın və
setRemoteDescriptionçağırın.
Manual fallback WebRTC debug etmək üçün faydalıdır, lakin canlı Kaigi axını ilə eyni şəxsi silsilədə siqnallaşdırma zəmanətləri təmin etmir.
Test yoxlama siyahısı
Birlik sınaqları üçün körpüdən istifadə edin və UI planlaşdırılan Kaigi paylı yüklərin keçdiyini təsdiqləyin:
- Ev sahibi yerli media yaratır və
createKaigiMeetingtəqdim edir. - Ev sahibi
iroha://kaigi/join?call=...&secret=...dəvətnaməsini göstərir - qonaq dəvətnaməni təhlil edir,
getKaigiCallçağırır vəjoinKaigiMeetingtəqdim edir. - cavab siqnalları üçün aparıcı səsvermələr və ya saatlar və cavabı tətbiq edir;
- Gizli rejimdə XOR qorunma zamanı özünü qorumaq üçün təyinatlar yoxdur
- canlı siqnal tapılmadıqda əl ilə geri çəkilmə görünür
Tam bir istinad sınaq kompleksi üçün demo tətbiqinin Kaigi görünüşünü və yüklənmədən əvvəlki körpü testlərini baxın:
npm test -- tests/kaigiView.spec.ts tests/preloadKaigiBridge.spec.ts
npm run e2e:uiUI duman testi /kaigi marşrutunun işlədiyini təsdiqləyir. Həqiqi media testi hələ də iki maliyyələşdirilmiş cüzdan və ya iki pəncərə və ya qurğuya ehtiyac duyur, çünki əməliyyat imzalanması, kamera, mikrofon və WebRTC icazələri icra vaxtı ilə dəyişir.
Əgər TAIRA ilə mübarizə aparırsınızsa və çağırış üçün xüsusi bir marşrut 404 qaytarırsa, əvvəlcə ev sahibi cüzdanın uğurla təqdim olunduğunu təsdiqləyin CreateKaigi.
Növbəti addımlar
- Tətbiqinizin etibarlı seans müddətinin hesablanması olduğu zaman
RecordKaigiUsageilə istifadə qeydini əlavə edin. - Relay manifestlərindən istifadə edərkən
/v1/kaigi/relaysvasitəsilə relayləri qeydə alıb izləyin. - Əməliyyatçının idarəetmə cədvəlindəki səth
KaigiRosterSummary,KaigiUsageSummaryvəKaigiRelayHealthUpdatedhadisələri.