html @p2play-js/p2p-game — دستاویزات
دستاویزات
🌐 زبان:
مقامی جامد HTML @p2play-js/p2p-game تھیم
⚠️ انتباہ: یہ دستاویز انگریزی سے خودکار ترجمہ کی گئی ہے۔ اس میں غلطیاں ہو سکتی ہیں۔ اصل انگریزی ورژن

تعارف

@p2play-js/p2p-game برائوزر پر مبنی P2P (WebRTC) ملٹی پلیئر گیمز بنانے کے لیے ایک ماڈولر TypeScript لائبریری ہے۔ یہ فراہم کرتی ہے حالت کی ہم آہنگی (مکمل/ڈیلٹا)، مطابقت کی حکمت عملیاں (ٹائم اسٹیمپ)، کم سے کم WebSocket سگنلنگ اڈاپٹر، موومنٹ ہیلپرز، ہوسٹ انتخاب/منتقلی، اور ایک پنگ اوورلے۔

فوری شروعات

npm install @p2play-js/p2p-game
import { P2PGameLibrary, WebSocketSignaling } from "@p2play-js/p2p-game";

const signaling = new WebSocketSignaling("کھلاڑیA", "کمرہ-42", "wss://آپ-کا-ws.مثال");
const multiP2PGame = new P2PGameLibrary({
  signaling,
  maxPlayers: 4,
  conflictResolution: "timestamp",
});

await multiP2PGame.start();

multiP2PGame.on("playerMove", (id, pos) => {/* رینڈر */});

ڈیموز

فن تعمیر

  • لائبریری WebSocket سگنلنگ سرور استعمال کرتی ہے تاکہ کمرے سنبھالے، کھلاڑیوں کے شناختی نمبرز کی فہرست رکھے، اور مخصوص پیئرز تک SDP/ICE پیغامات پہنچائے۔
  • پیئرز فل‑میش بناتے ہیں: ہر جوڑی میں وہ پیئر جو playerId کے لغوی ترتیب میں چھوٹا ہو، WebRTC آفّر بناتا ہے — ڈبل آفّر ٹکراؤ سے بچاؤ۔
  • DataChannel قائم ہو جانے کے بعد گیم کے پیغامات پیئر‑ٹو‑پیئر چلتے ہیں؛ سگنلنگ سرور اب ایپ ٹریفک ریلے نہیں کرتا۔
  • ہوسٹ کا انتخاب متعین ہے: سب سے چھوٹا playerId ہوسٹ بنتا ہے۔ جب ہوسٹ نکلے تو اگلا چھوٹا منتخب ہوتا ہے اور نیا مکمل اسنیپ شاٹ بھیجتا ہے۔
سگنلنگ کیا ہے؟
براؤزر WebRTC کنکشن کھولنے سے پہلے میٹاڈیٹا (SDP offers/answers اور ICE candidates) کسی بیرونی چینل سے بدلے بغیر نہیں جڑ سکتے۔ سگنلنگ سرور یہی تبادلہ کرتا ہے اور کمرے کی فہرست رکھتا ہے؛ DataChannel کھلنے کے بعد گیم پلے ٹریفک آگے نہیں بھیجتا۔

سگنلنگ ترتیب

sequenceDiagram participant A as کلائنٹ A participant S as WS سگنلنگ participant B as کلائنٹ B A->>S: register {roomId, from, announce} B->>S: register {roomId, from, announce} S-->>A: sys: roster [A,B] S-->>B: sys: roster [A,B] note over A,B: سب سے چھوٹا playerId آفّر شروع کرتا ہے A->>S: kind: desc (offer), to: B S->>B: kind: desc (offer) from A B->>S: kind: desc (answer), to: A S->>A: kind: desc (answer) from B A->>S: kind: ice, to: B B->>S: kind: ice, to: A note over A,B: DataChannel کھلا → گیم پلے P2P ہو گیا

فل‑میش ٹوپولوجی

graph LR A[کھلاڑی A] --- B[کھلاڑی B] A --- C[کھلاڑی C] B --- C classDef host fill:#2b79c2,stroke:#2a3150,color:#fff; class A host;

حالت کی ہم آہنگی

  • مکمل اسنیپ شاٹس: شامل ہونے/منتقلی پر، اصلاحی ری‑سِنک۔
  • ڈیلٹا اپ ڈیٹس: راستہ‑بنیاد ہدفی تبدیلیاں (عملی طور پر عموماً ہائبرڈ طریقہ)۔
مکمل بمقابلہ ڈیلٹا
مکمل اسنیپ شاٹس سادہ اور مضبوط مگر بھاری؛ ڈیلٹا مختصر اور موثر مگر مستحکم اسٹیٹ اسکیما درکار۔ عملی طور پر زیادہ تر ڈیلٹا بھیجیں، اور پیئر کے شامل ہونے یا ہوسٹ منتقلی کے بعد مکمل اسنیپ شاٹ۔

مطابقت

  • Timestamp (ڈیفالٹ): ہر بھیجنے والے کی سیکوئنس پر Last-Writer-Wins (LWW)۔
گیمز میں مطابقت
timestamp موڈ میں ہر بھیجنے والے کی تازہ ترین کارروائی قبول کی جاتی ہے (Last-Writer-Wins, LWW): جس پیغام کا seq اس بھیجنے والے کے آخری دیکھی گئی قدر سے کم ہو، اسے نظر انداز کر دیا جاتا ہے۔

حرکت

ہدف یہ ہے کہ نیٹ ورک جِٹر میں بھی ہموار مگر پیش گوئی کے قابل حرکت ملے۔ اس کے لیے انٹرپولیشن اور حد بند ایکسٹراپولیشن (مختصر پیش گوئی کھڑکیاں) ملائی جاتی ہیں تاکہ تاخیر سے آنے والی اپ ڈیٹس چھپیں اور حقیقت سے دوری نہ بڑھے۔

انٹرپولیشن

نئی ریموٹ پوزیشن آتے ہی جھٹکا نہیں دیتے؛ ہر فریم باقی فاصلہ کا حصہ طے کرتے ہیں۔ smoothing اسی حصے (0..1) کو کنٹرول کرتا ہے — بڑا مان قدرے کم لَیگ مگر کبھی "تیرتا" احساس دے سکتا ہے۔

// Pseudocode
const clampedVx = clamp(velocity.x, -maxSpeed, maxSpeed);
const clampedVy = clamp(velocity.y, -maxSpeed, maxSpeed);
// allowedDtSec ایکسٹراپولیشن حد کو مدنظر رکھتا ہے
position.x += clampedVx * allowedDtSec * smoothing;
position.y += clampedVy * allowedDtSec * smoothing;
// اختیاری Z محور
اسموتھنگ کی ٹیوننگ
0.2–0.3 سے آغاز کریں۔ حرکت اگر ان پٹ سے پیچھے رہتی ہو تو بڑھائیں؛ جھولا/اوور شوٹ ہو تو گھٹا دیں۔

ایکسٹراپولیشن (حد کے ساتھ)

جس فریم میں نئی اپ ڈیٹ نہ ہو، عارضی طور پر آخری معلوم رفتار سے پروجیکٹ کریں؛ extrapolationMs (مثلاً 120–140ms) سے کھڑکی محدود رکھیں — بجٹ ختم ہو تو رک جائیں اور اگلی اپ ڈیٹ کا انتظار کریں۔

حد کیوں؟
بہت دیر تک پیش گوئی سے نمایاں غلطیاں جنم لیتی ہیں (دیوار پار، تصحیح پر ٹیلے پورٹ)۔ مختصر حد جِٹر چھپاتی اور منظر کو حقیقت کے قریب رکھتی ہے۔

2D بمقابلہ 3D

ڈیفالٹ 2D؛ سادہ 3D کے لیے z شامل کریں۔ اگر worldBounds.depth دیا ہو تو Z پر بھی کلیمپ لگتا ہے۔

ورلڈ باؤنڈز بمقابلہ اوپن ورلڈ

worldBounds کے ساتھ پوزیشنز [0..width] اور [0..height] (اور Z کے لیے [0..depth]) تک محدود؛ اوپن‑ورلڈ میں ignoreWorldBounds: true کریں (صرف کھلاڑی‑بنام‑کھلاڑی تصادم رہتے ہیں)۔

تصادم (دائرہ/کُرہ)

تصادم برابر رداس کے دائروں (2D)/کُروں (3D) استعمال کرتے ہیں۔ صرف لوکل کھلاڑی کی پوزیشن دوبارہ سیٹ ہوتی ہے: ریموٹ پوزیشنز نیٹ ورک کا ماخذِ حقیقت ہیں اور کبھی مقامی طور پر تبدیل نہیں ہوتیں۔ جب لوکل کھلاڑی کسی ریموٹ کھلاڑی سے اوورلیپ کرے تو دونوں کے بیچ نورملائزڈ ویکٹر نکال کر لوکل کھلاڑی کو پوری اوورلیپ مسافت سے دور دھکیلا جاتا ہے۔ یوں ریموٹ حالت بااختیار رہتی ہے اور لوکل کھلاڑی دوسروں میں نہیں گھستا۔

// لوکل کھلاڑی L بمقابلہ ریموٹ کھلاڑی R، دونوں کا رداس r
const dx = R.x - L.x, dy = R.y - L.y, dz = (R.z||0) - (L.z||0);
const dist = Math.max(1e-6, Math.hypot(dx, dy, dz));
const overlap = Math.max(0, 2*r - dist);
const nx = dx / dist, ny = dy / dist, nz = dz / dist;
// صرف L کو R سے دور، پوری اوورلیپ مسافت سے، حرکت دی جاتی ہے:
L.x -= nx * overlap; L.y -= ny * overlap; L.z = (L.z||0) - nz * overlap;
پیچیدگی نوٹ
سادہ الگورتھم تمام جوڑوں کو چیک کرتا ہے (O(n²)) — چھوٹے کمروں میں ٹھیک؛ بھیڑ میں گرڈ/کواڈ ٹری جیسے ڈھانچے شامل کریں۔

فلو: موومنٹ اسٹیپ

flowchart TD A[آخری معلوم حالت] --> B{کیا نئی نیٹ اپ ڈیٹ؟} B -- ہاں --> C[ٹائم اسٹیمپس ری سیٹ؛ انٹرپولیشن] B -- نہیں --> D{کیا ایکسٹراپولیشن بجٹ باقی؟} D -- ہاں --> E[آخری رفتار * smoothing سے پروجیکٹ] D -- نہیں --> F[پوزیشن برقرار] C --> G{ورلڈ باؤنڈز؟} E --> G G -- کلیمپ --> H[کلیمپس لگائیں] G -- اوپن ورلڈ --> I[کلیمپس چھوڑیں] H --> J[تصادم حل] I --> J J --> K[رینڈر]

نیٹ ورکنگ کی تفصیل

  • بیک پریشر حکمت عملیاں: لبریز چینلز کے لیے مرج/ڈراپس۔
  • گنجائش: maxPlayers نفاذ + maxCapacityReached ایونٹ۔
  • STUN/TURN: سخت نیٹ ورکس کے لیے TURN؛ سگنلنگ کے لیے WSS۔
NAT اور TURN
بہت سے ادارہ/ہوٹل نیٹ ورکس براہ راست P2P روکتے ہیں۔ TURN سرور ٹریفک ریلے کرتا ہے تاکہ پیئرز جڑ سکیں، مگر تاخیر/بینڈوڈتھ کی قیمت پر۔ پروڈکشن میں اعتبار کے لیے TURN اسناد فراہم کریں۔

بیک پریشر

بیک پریشر DataChannel کو اوورلوڈ سے بچاتا ہے۔ جب RTCDataChannel.bufferedAmount حد سے اوپر جائے، تو عارضی طور پر بھیجنا روکیں، کم‑قدر پیغامات چھوڑیں، یا کئی اپ ڈیٹس کو تازہ ترین میں سمیٹیں۔

کیسے کام کرتا ہے
ہر send() bufferedAmount بڑھاتا ہے جب تک براؤزر نیٹ ورک پر فِلش نہ کر دے؛ اگر بھیجنا ترسیل سے تیز ہو تو تاخیر بڑھے گی اور ایپ اٹکے گی۔

حکمت عملیاں

  • off: کوئی حفاظتی اقدام نہیں — چھوٹے/کم میعادی پیغامات کے لیے۔
  • drop-moves: حد سے اوپر نئے move چھوڑ دیں۔
  • coalesce-moves: ہر پیئر کا صرف تازہ ترین move قطار میں رکھیں۔
const multiP2PGame = new P2PGameLibrary({
    signaling,
    backpressure: { strategy: 'coalesce-moves', thresholdBytes: 256 * 1024 }
  });
  
سفارش کردہ حدیں
256–512 KB سے آغاز کریں؛ اگر بار بار حد لگے تو فریکوئنسی/سائز گھٹا دیں (ڈیلٹا، binary‑min، ویکٹر کوانٹائز)۔

Events & API (selection)

  • on('playerMove'), on('inventoryUpdate'), on('objectTransfer')
  • on('stateSync'), on('stateDelta'), on('hostChange'), on('ping')
  • broadcastMove(), updateInventory(), transferItem()
  • broadcastPayload(), sendPayload()
  • setStateAndBroadcast(), announcePresence(), getHostId()

Events overview

Event Signature Description
playerMove(playerId, position)Movement applied
inventoryUpdate(playerId, items)Inventory updated
objectTransfer(from, to, item)Object transferred
sharedPayload(from, payload, channel?)Generic payload received
stateSync(state)Full snapshot received
stateDelta(delta)State delta received
peerJoin(playerId)Peer connected
peerLeave(playerId)Peer disconnected
hostChange(hostId)New host
ping(playerId, ms)RTT to peer
maxCapacityReached(maxPlayers)Capacity reached; new connections refused

Lifecycle & presence

  • Presence: call announcePresence(playerId) early to emit an initial move so peers render the player immediately.
  • peerJoin/peerLeave: the UI can show/hide entities. Host‑side cleanup can be automated by enabling cleanupOnPeerLeave: true in P2PGameLibrary options: the host removes the leaving player's entries and broadcasts a delta accordingly.
  • Capacity limit: set maxPlayers to cap the room size. When capacity is reached, the library will not initiate new connections and will ignore incoming offers; it emits maxCapacityReached(maxPlayers) so you can inform the user/UI.

Types Reference

GameLibOptions

type SerializationStrategy = "json" | "binary-min";
type ConflictResolution = "timestamp";

interface BackpressureOptions {
  strategy?: "off" | "drop-moves" | "coalesce-moves";
  thresholdBytes?: number; // default ~256KB
}

interface DebugOptions {
  enabled?: boolean;
  onSend?: (info: {
    type: "broadcast" | "send";
    to: string | "all";
    payloadBytes: number;
    delivered: number;
    queued: number;
    channel: "reliable" | "unreliable";
    serialization: SerializationStrategy;
    timestamp: number;
  }) => void;
}

interface MovementOptions {
  maxSpeed?: number;
  smoothing?: number; // 0..1
  extrapolationMs?: number;
  worldBounds?: { width: number; height: number; depth?: number };
  ignoreWorldBounds?: boolean;
  playerRadius?: number;
}

interface GameLibOptions {
  maxPlayers?: number;
  conflictResolution?: ConflictResolution;
  serialization?: SerializationStrategy;
  iceServers?: RTCIceServer[];
  cleanupOnPeerLeave?: boolean;
  debug?: DebugOptions;
  backpressure?: BackpressureOptions;
  timing?: { pendingOfferTimeoutMs?: number; pingIntervalMs?: number };
  pingOverlay?: { enabled?: boolean; position?: "top-left"|"top-right"|"bottom-left"|"bottom-right"; canvas?: HTMLCanvasElement | null };
  movement?: MovementOptions;
}

Events

type EventMap = {
  playerMove: (playerId: string, position: { x:number; y:number; z?:number }) => void;
  inventoryUpdate: (playerId: string, items: Array<{ id:string; type:string; quantity:number }>) => void;
  objectTransfer: (fromId: string, toId: string, item: { id:string; type:string; quantity:number }) => void;
  stateSync: (state: GlobalGameState) => void;
  stateDelta: (delta: StateDelta) => void;
  peerJoin: (playerId: string) => void;
  peerLeave: (playerId: string) => void;
  hostChange: (hostId: string) => void;
  ping: (playerId: string, ms: number) => void;
  sharedPayload: (from: string, payload: unknown, channel?: string) => void;
  maxCapacityReached: (maxPlayers: number) => void;
};

interface GlobalGameState {
  players: Record<string, { id:string; position:{x:number;y:number;z?:number}; velocity?:{x:number;y:number;z?:number} }>;
  inventories: Record<string, Array<{ id:string; type:string; quantity:number }>>;
  objects: Record<string, { id:string; kind:string; data:Record<string,unknown> }>;
  tick: number;
}

interface StateDelta { tick:number; changes: Array<{ path:string; value:unknown }> }

وائس کی اقسام (v0.3.0 سے)

type VoiceMode = "talk" | "listen";
type VoiceState = "connecting" | "connected" | "disconnected" | "failed";

interface VoiceOptions {
  mode: VoiceMode;
  /** صرف talk — کسٹم آؤٹ گوئنگ اسٹریم؛ listen کے لیے نظر انداز۔ ڈیفالٹ: getUserMedia({ audio: true })۔ */
  localStream?: MediaStream;
}

type VoiceEventMap = {
  remoteStream: (peerId: string, stream: MediaStream) => void;
  remoteStreamRemoved: (peerId: string) => void;
  state: (peerId: string, state: VoiceState) => void;
  error: (peerId: string, error: Error) => void;
};

ڈیلٹا راستوں کے قواعد

  • راستے dot‑separated آبجیکٹ کیز ہیں (ارے انڈیکس معاون نہیں)۔
  • ہدفی اپ ڈیٹس کے لیے ساخت کو کم گہرا اور keyed رکھیں (جیسے objects.chest.42)؛ گہرے ارریز سے گریز کریں۔
// اچھا: آبجیکٹ میپ
{ path: 'objects.chest.42', value: { id:'chest.42', kind:'chest', data:{ opened:true } } }

// معاون نہیں: 'objects[3]' یا 'players.list.0' جیسے ارے انڈیکس راستے

P2PGameLibrary

Constructor

new P2PGameLibrary(options: GameLibOptions & { signaling: WebSocketSignaling | SignalingAdapter })

Lifecycle

await start(): Promise<void>
stop(): void
on<N extends keyof EventMap>(name: N, handler: EventMap[N]): () => void
getState(): GlobalGameState
getHostId(): string | undefined
setPingOverlayEnabled(enabled: boolean): void
tick(now?: number): void // apply interpolation/collisions once

State utilities

setStateAndBroadcast(selfId: string, changes: Array<{ path:string; value:unknown }>): string[]
broadcastFullState(selfId: string): void
broadcastDelta(selfId: string, paths: string[]): void

Gameplay APIs

announcePresence(selfId: string, position = { x:0, y:0 }): void
broadcastMove(selfId: string, position: {x:number;y:number;z?:number}, velocity?: {x:number;y:number;z?:number}): void
updateInventory(selfId: string, items: Array<{ id:string; type:string; quantity:number }>): void
transferItem(selfId: string, to: string, item: { id:string; type:string; quantity:number }): void

Payload APIs

broadcastPayload(selfId: string, payload: unknown, channel?: string): void
sendPayload(selfId: string, to: string, payload: unknown, channel?: string): void

وائس API (v0.3.0 سے)

readonly voice: VoiceManager // «وائس» سیکشن دیکھیں
voice.start(remotePeerId: string, options: VoiceOptions): Promise<void>
voice.stop(remotePeerId: string): void
voice.stopAll(): void
voice.setMuted(muted: boolean): void
voice.isMuted(): boolean
voice.getActiveLinks(): Array<{ peerId: string; mode: VoiceMode; state: VoiceState }>
voice.on(event, handler): () => void // remoteStream | remoteStreamRemoved | state | error

منتخب پیئر وائس (talk / listen)

v0.3.0 سے، لائبریری انہی RTCPeerConnections پر منتخب پیئر‑ٹو‑پیئر آڈیو کی حمایت کرتی ہے جو پہلے ہی DataChannels کے لیے کھلے ہوتے ہیں، معیاری SDP ری‑نیگوشی ایشن کے ذریعے۔ فی جوڑا کوئی دوسرا کنکشن نہیں، کوئی میڈیا سرور (SFU) نہیں، اور سگنلنگ پروٹوکول میں صفر تبدیلیاں: ری‑نیگوشی ایشن کے offer/answer موجودہ desc/ice اینویلپس کو دوبارہ استعمال کرتے ہیں، اس لیے کسٹم SignalingAdapter عمل درآمد اور ریفرنس WS سرور بغیر تبدیلی کام کرتے ہیں (بشمول STRICT_ENVELOPES=1

لنک موڈز

موڈلوکل (کالر)ریموٹاستعمال
talksendrecvsendrecvدو طرفہ کال
listenrecvonlysendonlyیک طرفہ سماعت (تماشائی، ریفری...)

فوری آغاز

// دو طرفہ کال — صرف کالر start() کال کرتا ہے؛
// ریموٹ لائبریری خودکار طور پر ری‑نیگوشی ایشن کا جواب دیتی ہے (ایپ سائیڈ کال کی ضرورت نہیں)۔
await multiplayer.voice.start("playerB", { mode: "talk" });

// یک طرفہ: آڈیو بھیجے بغیر playerB کو سنیں
await multiplayer.voice.start("playerB", { mode: "listen" });

multiplayer.voice.on("remoteStream", (peerId, stream) => { audioEl.srcObject = stream; });
multiplayer.voice.on("remoteStreamRemoved", (peerId) => { /* آڈیو الگ کریں */ });
multiplayer.voice.on("state", (peerId, state) => { /* connecting | connected | disconnected | failed */ });
multiplayer.voice.on("error", (peerId, error) => { /* مثلاً مائیک اجازت مسترد */ });

multiplayer.voice.setMuted(true);   // تمام آؤٹ گوئنگ آڈیو بند کریں، بغیر ری‑نیگوشی ایشن
multiplayer.voice.stop("playerB");  // ریموٹ کو remoteStreamRemoved + state: disconnected ملتا ہے
multiplayer.voice.stopAll();

یہ کیسے کام کرتا ہے (SDP‑ڈرون)

sequenceDiagram participant App as App (caller) participant VA as VoiceManager A participant PA as PeerManager A participant S as WS Signaling participant PB as PeerManager B participant VB as VoiceManager B App->>VA: voice.start(B, {mode}) VA->>VA: getUserMedia if talk (first call) VA->>PA: addTransceiver(direction) + renegotiate(B) PA->>S: kind: desc (offer), to: B S->>PB: offer from A (existing envelope) note over PB: Peer connected → renegotiation branch PB->>VB: onRemoteOfferApplied (reads SDP audio direction) VB->>VB: attach mic if the link requests our audio PB->>S: kind: desc (answer), to: A S->>PA: answer from B PA->>VA: negotiation settled (stable) VA->>App: start() resolves PB-->>VA: audio track (RTP) VA->>App: remoteStream event note over PA,PB: DataChannels untouched throughout

ڈیزائن فیصلے

موضوعبرتاؤ
ریموٹ ہینڈ شیکلائبریری خودکار طور پر سنبھالتی ہے: صرف voice.start() کا کالر آغاز کرتا ہے۔ ریموٹ ری‑نیگوشی ایشن کا جواب دیتا ہے اور جب لنک اس کا آڈیو مانگے تو اپنا مائیک منسلک کرتا ہے (براؤزر اجازت پرامپٹ ہی رضامندی کا دروازہ ہے)۔ انکار پر نیگوشی ایشن آڈیو کے بغیر مکمل ہوتی ہے اور error ایونٹ خارج ہوتا ہے۔
start() کا حلجب SDP ری‑نیگوشی ایشن مکمل ہوتی ہے (سگنلنگ موافق نیگوشی ایٹڈ سمت کے ساتھ stable پر واپس) تو حل ہوتا ہے۔ ICE ٹرانسپورٹ دوبارہ استعمال ہوتا ہے — پہلے سے منسلک۔ ریموٹ ٹریک پہنچنے پر remoteStream الگ سے خارج ہوتا ہے۔
stop() کی توازنمقامی طور پر آڈیو transceiver کو غیر فعال کرتا ہے (direction: "inactive"، ٹریک الگ) اور ری‑نیگوشی ایٹ کرتا ہے؛ ریموٹ SDP کے ذریعے تبدیلی محسوس کرکے remoteStreamRemoved + state: "disconnected" خارج کرتا ہے۔ اگلے start پر transceivers دوبارہ استعمال ہوتے ہیں (کوئی m‑line اضافہ نہیں)۔
دہرایا گیا start()فعال جوڑے پر دوبارہ start() کال کرنے سے پچھلا موڈ بدل جاتا ہے (idempotent، کوئی خرابی نہیں)۔
GlarePerfect negotiation: شائستگی اسی تعینیاتی id ترتیب سے اخذ ہوتی ہے جو ہوسٹ انتخاب میں ہے؛ شائستہ پیئر rollback کرتا ہے، جواب دیتا ہے، پھر اپنا offer دوبارہ بھیجتا ہے۔
مائیکروفون لائف سائیکلتمام کنکشنز پر ایک مشترکہ MediaStreamTrack (کوئی کلوننگ نہیں)۔ جیسے ہی کوئی لنک آڈیو نہیں بھیجتا، لائبریری کی ملکیتی مائیک روک دی جاتی ہے (براؤزر اشارہ بند)۔ ایپ کے فراہم کردہ localStream کو لائبریری کبھی نہیں روکتی۔ setMuted track.enabled کو ٹوگل کرتا ہے — بغیر ری‑نیگوشی ایشن۔
ICE / TURNوائس P2PGameLibrary آپشنز کے iceServers کو دوبارہ استعمال کرتی ہے۔ سخت نیٹ ورکس کے لیے TURN سرور تجویز کیا جاتا ہے (آڈیو بینڈوڈتھ گیم ڈیٹا سے زیادہ ہوتی ہے)۔
صفائیpeerLeave، stop() اور P2PGameLibrary.stop() وائس حالت کو بغیر لیک کے ختم کرتے ہیں۔

سماعت کا منظر (یک طرفہ آغاز)

A اور B ایک talk کال میں ہیں۔ C — تنہا — voice.start(A, { mode: "listen" }) اور voice.start(B, { mode: "listen" }) کال کرتا ہے۔ A اور B اپنے پہلے سے فعال مائیک ٹریک کو دوبارہ استعمال کرتے ہوئے خودکار طور پر sendonly جواب دیتے ہیں: C دونوں کو سنتا ہے بغیر سنے جائے، اور A↔B دو طرفہ رہتا ہے۔ A یا B پر کسی ایپ سائیڈ کال کی ضرورت نہیں۔

مسائل کا حل

  • کوئی آواز نہیں چلتی: براؤزر آڈیو پلے بیک سے پہلے صارف کے اشارے کا تقاضا کرتے ہیں (autoplay پالیسی)۔ ریموٹ اسٹریم کو کلک ہینڈلر سے <audio autoplay> عنصر سے منسلک کریں۔
  • مائیک اجازت مسترد: start(talk) مسترد ہوتا ہے اور error خارج ہوتا ہے؛ ریموٹ سائیڈ پر نیگوشی ایشن اس کے آڈیو کے بغیر مکمل ہوتی ہے۔ DataChannels کبھی متاثر نہیں ہوتے۔
  • Safari: ری‑نیگوشی ایشن معاون ہے، مگر autoplay اور مائیک‑اجازت فلو خاص طور پر آزمائیں؛ Safari اشاروں کے بارے میں زیادہ سخت ہے۔
  • سخت NAT کے پیچھے یک طرفہ آڈیو: iceServers میں TURN سرور کنفیگر کریں۔

Messages (transport)

// NetMessage union (selected)
type NetMessage =
  | { t:"move"; from:string; ts:number; seq?:number; position:{x:number;y:number;z?:number}; velocity?:{x:number;y:number;z?:number} }
  | { t:"inventory"; from:string; ts:number; seq?:number; items:Array<{id:string;type:string;quantity:number}> }
  | { t:"transfer"; from:string; ts:number; seq?:number; to:string; item:{id:string;type:string;quantity:number} }
  | { t:"state_full"; from:string; ts:number; seq?:number; state: GlobalGameState }
  | { t:"state_delta"; from:string; ts:number; seq?:number; delta: StateDelta }
  | { t:"payload"; from:string; ts:number; seq?:number; payload: unknown; channel?: string };

// Serialization
// strategy: "json" (string frames) or "binary-min" (ArrayBuffer UTF-8 JSON)

Signaling Adapter

Abstraction used by the library to exchange SDP/ICE via any backend (WebSocket, REST, etc.).

interface SignalingAdapter {
  localId: string;
  roomId?: string;
  register(): Promise<void>; // join room and receive roster
  announce(desc: RTCSessionDescriptionInit, to?: string): Promise<void>;
  onRemoteDescription(cb: (desc: RTCSessionDescriptionInit, from: string) => void): void;
  onIceCandidate(cb: (candidate: RTCIceCandidateInit, from: string) => void): void;
  onRoster(cb: (roster: string[]) => void): void;
  sendIceCandidate(candidate: RTCIceCandidateInit, to?: string): Promise<void>;
}

Example: minimal custom adapter (WebSocket)

A tiny implementation of the interface using a plain WebSocket signaling server.

class SimpleWsSignaling implements SignalingAdapter {
  constructor(public localId: string, public roomId: string, private url: string) {
    this.ws = new WebSocket(this.url);
  }
  private ws: WebSocket;
  private rosterCb?: (list: string[]) => void;
  private descCb?: (d: RTCSessionDescriptionInit, from: string) => void;
  private iceCb?: (c: RTCIceCandidateInit, from: string) => void;

  async register(): Promise<void> {
    await new Promise<void>((resolve) => {
      this.ws.addEventListener("open", () => {
        this.ws.send(JSON.stringify({ kind: "register", roomId: this.roomId, from: this.localId, announce: true }));
        resolve();
      });
    });
    this.ws.addEventListener("message", (ev) => {
      const msg = JSON.parse(ev.data);
      if (msg.sys === "roster" && this.rosterCb) this.rosterCb(msg.roster);
      if (msg.kind === "desc" && this.descCb) this.descCb(msg.payload, msg.from);
      if (msg.kind === "ice" && this.iceCb) this.iceCb(msg.payload, msg.from);
    });
  }

  onRoster(cb: (roster: string[]) => void){ this.rosterCb = cb; }
  onRemoteDescription(cb: (desc: RTCSessionDescriptionInit, from: string) => void){ this.descCb = cb; }
  onIceCandidate(cb: (candidate: RTCIceCandidateInit, from: string) => void){ this.iceCb = cb; }

  async announce(desc: RTCSessionDescriptionInit, to?: string): Promise<void> {
    this.ws.send(JSON.stringify({ kind: "desc", roomId: this.roomId, from: this.localId, to, payload: desc }));
  }
  async sendIceCandidate(candidate: RTCIceCandidateInit, to?: string): Promise<void> {
    this.ws.send(JSON.stringify({ kind: "ice", roomId: this.roomId, from: this.localId, to, payload: candidate }));
  }
}

// Usage with the library
const signaling = new SimpleWsSignaling("alice", "room-1", "wss://your-signal.example");
await signaling.register();
const game = new P2PGameLibrary({ signaling });
await game.start();

Example: REST + long‑polling adapter

For environments without WebSockets, use HTTP endpoints and a polling loop to receive messages.

class RestPollingSignaling implements SignalingAdapter {
  constructor(public localId: string, public roomId: string, private baseUrl: string) {}
  private rosterCb?: (list: string[]) => void;
  private descCb?: (d: RTCSessionDescriptionInit, from: string) => void;
  private iceCb?: (c: RTCIceCandidateInit, from: string) => void;
  private polling = false;

  async register(): Promise<void> {
    await fetch(`${this.baseUrl}/register`, {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({ roomId: this.roomId, from: this.localId, announce: true }),
    });
    this.polling = true;
    void this.poll();
  }

  private async poll(): Promise<void> {
    while (this.polling) {
      try {
        const res = await fetch(`${this.baseUrl}/poll?roomId=${encodeURIComponent(this.roomId)}&from=${encodeURIComponent(this.localId)}`);
        if (!res.ok) { await new Promise((r) => setTimeout(r, 1000)); continue; }
        const msgs = await res.json();
        for (const msg of msgs) {
          if (msg.sys === "roster" && this.rosterCb) this.rosterCb(msg.roster);
          if (msg.kind === "desc" && this.descCb) this.descCb(msg.payload, msg.from);
          if (msg.kind === "ice" && this.iceCb) this.iceCb(msg.payload, msg.from);
        }
      } catch {
        await new Promise((r) => setTimeout(r, 1000));
      }
    }
  }

  onRoster(cb: (roster: string[]) => void){ this.rosterCb = cb; }
  onRemoteDescription(cb: (desc: RTCSessionDescriptionInit, from: string) => void){ this.descCb = cb; }
  onIceCandidate(cb: (candidate: RTCIceCandidateInit, from: string) => void){ this.iceCb = cb; }

  async announce(desc: RTCSessionDescriptionInit, to?: string): Promise<void> {
    await fetch(`${this.baseUrl}/send`, {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({ kind: "desc", roomId: this.roomId, from: this.localId, to, payload: desc }),
    });
  }
  async sendIceCandidate(candidate: RTCIceCandidateInit, to?: string): Promise<void> {
    await fetch(`${this.baseUrl}/send`, {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({ kind: "ice", roomId: this.roomId, from: this.localId, to, payload: candidate }),
    });
  }
}

// Usage
const restSignaling = new RestPollingSignaling("alice", "room-1", "https://your-signal.example");
await restSignaling.register();
const game2 = new P2PGameLibrary({ signaling: restSignaling });
await game2.start();

WebSocketSignaling

Reference implementation used in examples; protocol: { sys:'roster', roster:string[] } broadcasts; targeted messages via to.

new WebSocketSignaling(localId: string, roomId: string, serverUrl: string)
await signaling.register();
signaling.onRoster((list) => {/* update UI */});
signaling.onRemoteDescription((desc, from) => {/* pass to PeerManager */});
signaling.onIceCandidate((cand, from) => {/* pass to PeerManager */});

Message shapes

// Client → server (register)
{ roomId: string, from: string, announce: true, kind: 'register' }

// Server → clients (roster broadcast)
{ sys: 'roster', roomId: string, roster: string[] }

// Client → server (SDP/ICE, targeted or broadcast in-room)
{ kind: 'desc'|'ice', roomId: string, from: string, to?: string, payload: any, announce?: true }

See examples/server/ws-server.mjs.

PeerManager (internal)

  • It maintains one RTCPeerConnection and one RTCDataChannel per peer, wiring the necessary callbacks.
  • For each pair of peers, the peer with the lexicographically smaller playerId initiates the connection by creating the offer; the other answers. This avoids simultaneous offers.
  • It emits peerJoin, peerLeave, hostChange, and ping events, and it forwards decoded network messages as netMessage.
  • Backpressure:
    • off: always send if channel is open.
    • drop-moves: if bufferedAmount exceeds threshold, drop new move messages.
    • coalesce-moves: replace the older queued move with the most recent one.
  • Capacity: enforces maxPlayers (no new inits; ignore extra offers) and emits maxCapacityReached(maxPlayers).

EventBus (internal)

class EventBus {
  on<N extends keyof EventMap>(name: N, fn: EventMap[N]): () => void
  off<N extends keyof EventMap>(name: N, fn: EventMap[N]): void
  emit<N extends keyof EventMap>(name: N, ...args: Parameters<EventMap[N]>): void
}

You usually subscribe through P2PGameLibrary.on(), which delegates to the internal bus.

PingOverlay

The overlay renders a tiny dashboard on top of your page that tracks round‑trip times (RTT) to each connected peer. It listens to ping events emitted by the network layer and keeps a short rolling history (up to ~60 samples). Use it in development to spot spikes, verify TURN usage, and compare peers.

Options

{
  enabled?: boolean; // default false
  position?: 'top-left'|'top-right'|'bottom-left'|'bottom-right'; // default 'top-right'
  canvas?: HTMLCanvasElement | null; // provide your own canvas, or let the overlay create one
}

Usage

const multiP2PGame = new P2PGameLibrary({ signaling, pingOverlay: { enabled: true, position: 'top-right' } });
// Toggle on/off at runtime
multiP2PGame.setPingOverlayEnabled(false);
Reading the chart
Each colored line is one peer. Flat low values are good; saw‑tooth patterns or sudden jumps suggest congestion or relay (TURN). If one peer is consistently higher, consider re‑balancing roles (e.g., avoid making them host).

Serialization

  • Strategies: json (string frames) or binary-min (ArrayBuffer UTF‑8 JSON).
  • Unknown strategies throw an error.
interface Serializer {
  encode(msg: NetMessage): string | ArrayBuffer;
  decode(data: string | ArrayBuffer): NetMessage;
}

function createSerializer(strategy: 'json'|'binary-min' = 'json'): Serializer

Examples

Host-validated intents (application pattern)

const isHost = () => multiP2PGame.getHostId() === localId;
multiP2PGame.on("sharedPayload", (from, payload, channel) => {
  if (!isHost()) return;
  if (channel === "move-intent" && typeof payload === "object") {
    const p = payload as { pos:{x:number;y:number}; vel?:{x:number;y:number} };
    multiP2PGame.broadcastMove(multiP2PGame.getHostId()!, p.pos, p.vel);
  }
});

Persisting ephemeral payloads into shared state

multiP2PGame.on("sharedPayload", (from, payload, channel) => {
  if (channel !== "status") return;
  if (payload && typeof payload === "object" && "hp" in (payload as any)) {
    multiP2PGame.setStateAndBroadcast(multiP2PGame.getHostId()!, [
      { path: `objects.playerStatus.${from}`, value: { id:`playerStatus.${from}`, kind:"playerStatus", data:{ hp:(payload as any).hp } } }
    ]);
  }
});

Selective delta updates

const paths = multiP2PGame.setStateAndBroadcast(localId, [
  { path:"objects.chest.42", value:{ id:"chest.42", kind:"chest", data:{ opened:true } } }
]);
// paths == ["objects.chest.42"]

Event reference

playerMove

game.on('playerMove', (playerId, position) => {
    drawAvatar(playerId, position);
  });
  

inventoryUpdate

game.on('inventoryUpdate', (playerId, items) => {
    ui.updateInventory(playerId, items);
  });
  

objectTransfer

game.on('objectTransfer', (from, to, item) => {
    ui.toast(`${from} gave ${item.id} to ${to}`);
  });
  

sharedPayload

game.on('sharedPayload', (from, payload, channel) => {
    if (channel === 'chat') chat.add(from, (payload as any).text);
  });
  

stateSync

game.on('stateSync', (state) => {
    world.hydrate(state);
  });
  

stateDelta

game.on('stateDelta', (delta) => {
    world.applyDelta(delta);
  });
  

peerJoin / peerLeave

game.on('peerJoin', (id) => ui.addPeer(id));
  game.on('peerLeave', (id) => ui.removePeer(id));
  

hostChange

game.on('hostChange', (hostId) => ui.setHost(hostId));
  

ping

game.on('ping', (id, ms) => ui.setPing(id, ms));
  

maxCapacityReached

game.on('maxCapacityReached', (max) => ui.alert(`Room is full (${max})`));
  

Production notes

  • Provision ICE (TURN) and secure signaling (WSS).
  • Monitor RTCDataChannel.bufferedAmount and tune backpressure.

Reconnect & UX checklist

  • Show reconnecting UI when peers drop; rely on roster to detect returns.
  • Host sends a fresh state_full after migration to realign clients.
  • Optionally enable cleanupOnPeerLeave to prune state upon leave (host only).

Debugging

const game = new P2PGameLibrary({
  signaling,
  debug: {
    enabled: true,
    onSend(info){
      console.log('[send]', info.type, 'to', info.to, 'bytes=', info.payloadBytes, 'queued=', info.queued);
    }
  }
});

Browser compatibility

  • Recent Chrome/Firefox/Edge/Safari support DataChannels; Safari requires HTTPS/WSS in production.
  • Deploy TURN for enterprise/hotel networks; expect higher latency when relayed.

Troubleshooting

WebRTC connection fails to establish

  • Mixed content: ensure your page and signaling use HTTPS/WSS (browsers block WS from HTTPS pages).
  • TURN missing: on enterprise/hotel networks, direct P2P is blocked. Provide TURN credentials (username/credential) in iceServers.
  • CORS/firewall: your signaling endpoint must accept the origin; verify reverse proxy rules and open ports (TLS 443).

DataChannel stalls (high latency, inputs delayed)

  • Backpressure: enable coalesce-moves or drop-moves and tune thresholdBytes (start at 256–512 KB).
  • Reduce message size: prefer deltas; compress payloads (binary‑min); quantize vectors (e.g., mm → cm).
  • Lower send rate: throttle movement broadcasts (e.g., 30–60 Hz) and rely on interpolation to fill frames.

Peers out of sync after host change

  • Ensure the new host broadcasts a state_full (the library triggers this automatically on host change).
  • Clients should apply the full snapshot and clear local caches (let interpolation settle for a few frames).

Safari specific issues

  • Requires HTTPS/WSS for WebRTC outside localhost.
  • Check that STUN/TURN URLs include transport parameters (e.g., ?transport=udp) if needed by your relay.

Game workflows

End‑to‑end patterns to wire networking, consistency and state for different game genres.

1) Real‑time arena (action/shooter)

  • Sync: deltas for steady‑state; occasional full snapshot on host migration.
  • Backpressure: coalesce-moves to keep only the latest movement.
// Setup
const multiP2PGame = new P2PGameLibrary({ signaling, conflictResolution: 'timestamp', backpressure: { strategy: 'coalesce-moves' }, movement: { smoothing: 0.2, extrapolationMs: 120 } });
await multiP2PGame.start();

// Local input → broadcast movement (client prediction handled by your renderer)
function onInput(vec){
  const pos = getPredictedPosition(vec);
  multiP2PGame.broadcastMove(localId, pos, vec);
}

multiP2PGame.on('playerMove', (id, pos) => renderPlayer(id, pos));

2) Co‑op RPG (inventory, host applies intents)

  • Protocol: clients send intents (payloads); host mutates shared state and broadcasts deltas.
// Client sends intents to host only
function usePotion(){
  multiP2PGame.sendPayload(localId, multiP2PGame.getHostId()!, { action: 'use-item', itemId: 'potion' }, 'intent');
}

// Host handles intents and mutates state
const isHost = () => multiP2PGame.getHostId() === localId;
multiP2PGame.on('sharedPayload', (from, payload, channel) => {
  if (!isHost() || channel !== 'intent') return;
  if ((payload as any).action === 'use-item') {
    const inv = getInventoryAfterUse(from, (payload as any).itemId);
    multiP2PGame.setStateAndBroadcast(localId, [ { path: `inventories.${from}`, value: inv } ]);
  }
});

3) Turn‑based tactics (deterministic, full snapshots)

  • Sync: broadcast a small delta per move; send a full snapshot every N turns for safety.
interface TurnMove { unitId:string; to:{x:number;y:number} }

multiP2PGame.on('sharedPayload', (from, payload, channel) => {
  if (channel !== 'turn-move' || multiP2PGame.getHostId() !== localId) return;
  const mv = payload as TurnMove;
  const ok = validateMove(currentState, from, mv);
  if (!ok) return; // reject illegal move
  applyMove(currentState, mv);
  multiP2PGame.setStateAndBroadcast(localId, [ { path: `players.${from}.lastMove`, value: mv } ]);
});

// Every 10 turns, send a full snapshot
if (currentState.tick % 10 === 0) multiP2PGame.broadcastFullState(localId);

4) Party game with lobby (capacity & migration)

  • Set maxPlayers to protect UX; handle maxCapacityReached to inform the user.
  • Use roster to present the lobby; auto‑migrate host on leave.
const multiP2PGame = new P2PGameLibrary({ signaling, maxPlayers: 8 });

multiP2PGame.on('maxCapacityReached', (max) => showToast(`Room is full (${max})`));

multiP2PGame.on('hostChange', (host) => updateLobbyHost(host));

5) Open‑world sandbox (no bounds, Z axis)

  • Disable world bounds; rely on player‑vs‑player collisions only.
  • Use binary-min for payload size wins if you ship frequent updates.
const multiP2PGame = new P2PGameLibrary({
  signaling,
  serialization: 'binary-min',
  movement: { ignoreWorldBounds: true, playerRadius: 20, smoothing: 0.25, extrapolationMs: 140 }
});

لغت

  • SDP: Session Description Protocol; WebRTC کے میڈیا/ڈیٹا سیشن کے پیرامیٹرز۔
  • ICE: Interactive Connectivity Establishment; پیئرز کے بیچ نیٹ ورک راستے (STUN/TURN کے ذریعے)۔
  • STUN: سرور جو کلائنٹ کو اس کا عوامی پتہ معلوم کرنے میں مدد دے؛ NAT ٹریورسل۔
  • TURN: ریلے سرور جو براہ راست P2P نہ ہو سکے تو ٹریفک آگے بڑھائے۔
  • DataChannel: WebRTC دو طرفہ ڈیٹا ٹرانسپورٹ برائے گیم پیغامات۔
  • LWW: Last‑Writer‑Wins; فی‑بھیجنے والا سلسلہ جس میں تازہ ترین اپ ڈیٹ غالب۔