Kaigi-ийг JavaScript -ийн хэрэгсэлд багтааж байна
Kaigi нэвтрүүлэг нь санхүүжилтээр дэмжлэг үзүүлсэн нэгээс нэг аудио / видео уулзалтыг бий болгох боломжийг олгодог. Iroha. Бrowser нь хэвлэл мэдээллийн хэрэгслийг WebRTC, Цаашид Torii болон Kaigi заавар нь тогтвортой уулзалтын бүртгэл, шифрлэгдсэн сигналын мета өгөгдөл, хувийн жагсаалтыг дэмжих болон хэрэглээний үйл явдлыг бүрдүүлэх.
Энэ сургалт нь Iroha Demo JavaScript апп ашигладаг хамгийн бага интеграцийн загварыг харуулж байна:
- дамжуулагч WebRTC санал болгож, хариулт өгдөг
- Kaigi гүйлгээний өргөдлийн гүүр тэмдэглэж, өргөн мэдүүлсэн
- компакт уриалтын холбоосууд зөвхөн ID уриалтыг борлуулж, нууц уриалж байна.
- зохион байгуулагч Torii нь оролцогчдын нууцлагдсан хариултыг ажиглаж байна.
Тус жишээ нь TypeScript -ийг ашигладаг бөгөөд тэд Electron, аюулгүй бэкэндтэй хөтөч, эсвэл хөрөнгийн өргөжүүлэл бүхий вэб аппликейшнүүдээр ажиллуулах боломжтой гэж бичигддэг.
Урьдчилсан шаардлага
Та хэрэгтэй:
- Kaigi -ийн хүчин чадалтай Torii төгсгөлийн цэг
- зочид буудлын болон зочдын бүртгэл
- бүртгэлийн гарын үсэг зурах түлхэгийг аюулгүй аппликейшнээр дамжуулан ашиглах боломжтой
- Бrowser-ийн камер/микрофонны зөвшөөрөл
- Node.js 20+ хэрэв та шууд JavaScript дэмо эсвэл үндсэн
@iroha/iroha-jsхолболт ашиглаж байгаа бол
Хөдөлмөрийн бүрэн сүлжээнд Iroha эх үүсвэрийн шалгаруулалтын хамт демог клонируулъя:
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 эх үүсвэрийн хадгаламжаас @iroha/iroha-js-ийн дэмог ашигла. түүний file: -ээс хамаарах нь төлбөрийг шууд шийддэг. Тус үндсэн холболт өөрчлөгдөж байгаа бол iroha/javascript/iroha_js -д дахин бариарай; цэвэр багцын захиалгаар npm run build:native шаардлагатай ачаалалтын ажлын орон зай байхгүй
TAIRA дээр шууд уулзалтыг явуулахын өмнө дэмо нь хамаардаг олон нийтийн Torii гарааг шалгаарай:
TAIRA=https://taira.sora.org
curl -fsS "$TAIRA/health"
curl -fsS "$TAIRA/v1/kaigi/relays"
curl -fsS "$TAIRA/v1/kaigi/relays/health"Эдгээр команд нь TAIRA идэвхтэй байгааг баталгаажуулж, Kaigi ээлжит телеметрийн хэрэгслийг ашиглаж байна. Тэд Kaigi гүйлгээг өргөн мэдүүлэхгүй. Үнэн CreateKaigi эсвэл JoinKaigi туршилт TAIRA дансанд санхүүжилт олгох шаардлагатай бөгөөд демонстратын гүүр болон хөрөнгийн мөнгөөр батлагдсан бусад гүүрээр гарын үсэг зурах хэрэгтэй.
Архитектура
Kaigi нэгтгэлийг гурван давхаргаар хувааж байх:
| Сургууль | Хариуцлага |
|---|---|
| UI | Акаунтын сонгон шалгаруулалт, уулзалтын бланк, уриалтын сүлжээний дэлгэц, хэвлэл мэдээллийн хэрэгслийн удирдлага |
| WebRTC | RTCPeerConnection, орон нутгийн хэвлэл мэдээллийн хэрэгсэл, санал, хариултын тодорхойлолтууд |
| Iroha гүүр | гарын үсэг зурах, CreateKaigi, JoinKaigi, EndKaigi, сигналын санал асуулга |
Хэрэглэлийн гүүр нь Electron Preload API, Wallet extension, эсвэл Backend Endpoint байж болно. Энэ нь 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 }>;
};Демо аппликейшнээр эдгээр буудлын арга барилыг @iroha/iroha-js, орон нутгийн гарын үсэг зурах, шифрлэсэн Kaigi метабараа болон Torii дуудлагаар хэрэгжүүлж байна.
Тэтгэгчдийг дуудлаа
Torii-тай нийцсэн дуудлага IDs нь domain.dataspace:meeting хэлбэрээр ашиглана. Демо нь үүсгэсэн уулзалтад kaigi.universal:<call-name> ашиглана.
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 Хөдөлмөрийн туслагчид
Үйлчлөгч санал авна, CreateKaigi -д хадгалж, зочноос хариулт авахын тулд цонхны нээлттэй байлгана. Зочид шифрлэгдсэн саналыг аваад хариултыг бий болгож, хариулгыг JoinKaigi -ээр байрлуулна.
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 };
}UI урсгалыг энгийн видео элементээр холбох:
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);
});
}Үйлчлүүлэгч: Хуралдааны холбоосыг байлгаарай
Үйлчилгээний урсгал:
- нээлттэй камера, микрофон
- Kaigi сигналын товчлогын хосуулгыг бий болгох
- WebRTC санал гаргах
CreateKaigi- товч уриалтын холбоосыг хуваалцаарай
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 -ийг таны UI -д үзнэ үү. Хэрэглэгчид үүнийг нунтаглаж, өөр гарын үсэгт нээж, эсвэл:
export function inviteRoute(inviteLink: string): string {
const invite = parseInviteLink(inviteLink);
return `/kaigi?call=${encodeURIComponent(invite.callId)}&secret=${encodeURIComponent(
invite.inviteSecretBase64Url,
)}`;
}Зохиолч: Хуралдаанд оролцох
зочид буудлын урсгал:
- уриалгыг судлах
- Torii газраас шифрлэгдсэн дуудлагын санал авна
- WebRTC хариуг бий болгох
JoinKaigi-г шифрлэгдсэн хариултын метадэтгэлтэй хүргүүлнэ
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 };
}Хэрэв уулзалт ил тод байгаа бол нэгдсэн хүсэлтэд хөрөнгийн дэлгэцийн шугам багтааж болно. Хувийн уулзалтаар walletIdentity -ийг нээлттэй байлгаарай, хэрэв хэрэглэгчид үүнийг тодорхой илэрхийлэхыг сонгохгүй бол.
Зохиолч: Аялагчийн хариултыг ашигла
Амьд уулзалтыг бий болгосны дараа хөтлөгч Kaigi үйл явдлыг үзэх, нууцалагдсан хариулт сигналын талаар санал асуулга явуулах ёстой.
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();
}
},
);
}Буцаасан захиалгыг хадгалах ID Тиймээс таны UI Үйлчлөгч нь хаасан эсвэл алдах үед байцаагчийг зогсоож болно.
Хуралдааны төгсгөл
Харилцааг бүтээсэн хостинг хуудсаас дуусгах:
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(),
});
}Хувийн хэв маягийн санхүүжилт
Хувийн Kaigi бүтээх, нэгжүүлэх, дуусгах үйл ажиллагаа нь хувийн нэвтрүүлгийн цэгтийн төлбөрийн хувьд хамгаалалттай XOR шаарддаг. Таны апп энэ алдааг олж авч, дахин туршиж үзэхээс өмнө өөрөө хамгаалах үйлдлийг санал болгодог.
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-д UI нь хэрэглэгчийг өөрийгөө хамгаалахыг шаардаж, дараа нь анхны бүтээх эсвэл нэгдсэн үйлдлийг дахин туршиж үзэх болно.
Хөдөлмөрийн эргэлт
Автомат дамжуулалт нь Kaigi-ийн үйл ажиллагаа явуулж буй Torii чиглэл, хувийн хэвшлийн баталгаажуулалтад хамааралтай. Хөгжлийн болон хязгаарлагдмал орчинд дасан зохицуулах:
CreateKaigiамжилтгүй бол санал болгож буй гарын үсэг бүхий уриалгыг үзнэ үү.JoinKaigiалдаатай бол түүхий эдийн хариултын багц үзүүлнэ- Үйлчлөгч хариуны багцыг татаж,
setRemoteDescriptionдуудлаа.
Хөдөлмөрийн дутагдал WebRTC-ийн засварын хяналт тавихэд ашигтай боловч шууд Kaigi урсгалын адил хувийн зангилааны сигналжуулах баталгааг олгодоггүй.
Тэсний шалгалтын жагсаалт
Нэгжийн шинжилгээний хувьд гүүртэй ойлгуулж, UI төхөөрөмж нь хүлээсэн Kaigi ашиг ачааллыг давсан гэдгийг баталгаажуулна.
- Үйлчлүүлэгч орон нутгийн хэвлэл мэдээллийн хэрэгслийг бий болгож,
createKaigiMeetingхүргүүлнэ - Үйлчлүүлэгч
iroha://kaigi/join?call=...&secret=...уриалгыг илтгэж байна - зочид уриалгыг шалгаж,
getKaigiCallдуудлага өгч,joinKaigiMeetingөргөн мэдүүлнэ - санал асуулгыг зохион байгуулж, хариултын сигналыг ажиглаж, хариултыг ашигладаг
- XOR гүнзгийрсэн үед өөрийгөө хамгаалах хувийн хэвшлийн илтгэлийг үгүйсгэнэ
- Амьдралын сигнал байхгүй бол дасан зохицуулалттай унадаг.
Бүхэл бүтэн хяналтын туршилтын цогцолборын хувьд демо аппликейшнүүдийн Kaigi үзлэг болон өмнөх ачааллын цахилгаан туршилтыг үзнэ үү:
npm test -- tests/kaigiView.spec.ts tests/preloadKaigiBridge.spec.ts
npm run e2e:uiUI дулааны шинжилгээ нь /kaigi замын үр дүнг баталгаажуулдаг. Үнэн хэвлэл мэдээллийн шинжилгэээнд аливаа хоёр санхүүжүүлсэн хөрөнгийн мөнгөн тэмдэгт болон хоёр ширээ эсвэл төхөөрөмж хэрэгтэй байдаг, учир нь транзакцын гарын үсэг зуралт, камер, микрофон, WebRTC зөвшөөрөл гүйлтийн цаг хугацаагаар өөрчлөх боломжтой.
Хэрэв та TAIRA-ийн эсрэг шинжилгээ хийж байгаа бол дуудлага тусгай замын чиглэл 404 -ийг буцааж байгаа бол хамгийн түрүүнд хүлээн авагч хөрөнгийг амжилттай ирүүлсэн эсэхийг баталгаажуулCreateKaigi. Релегийн эрүүл мэндийн эцсийн цэг нь тухайн дуудлага үүсэхээс өмнө ашиглах боломжтой.
Дараагийн алхам
RecordKaigiUsage-тай хэрэглээний бүртгэлийг нэмнэ, хэрэв таны аппликейшн сессийн хугацааг найдвартай тооцоолдог бол./v1/kaigi/relays-ийн дамжуулалт дамжуулалт явуулж, хяналт тавьж байна.- Үйлчлөгчийн гар утасны
KaigiRosterSummary,KaigiUsageSummary,KaigiRelayHealthUpdatedүйл явдлууд.