
IMPORTANT: This document analyzes ONLY the client-side code found in this repository. Sections marked with π are direct code analysis. Sections marked with π‘ are educated guesses based on client behavior.
Table of Contents
- What We Have (Client Code)
- Client Architecture
- Communication Protocol
- WebRTC Implementation
- DataChannel Message Protocol
- Input Handling System
- Game Configuration System
- CloudMoon Infrastructure
What We Have (Client Code)
π Files in This Repository
/run-site/
βββ index.html # Main HTML entry point
βββ js/
β βββ app.js # Vue instance initialization (149 lines)
β βββ app-data.js # Application state (148 lines)
β βββ app-methods.js # Core logic (1973 lines) β MAIN FILE
β βββ app-utils.js # Utility functions
β βββ app-component.js # Vue components (ad system)
β βββ gameConfig.js # Game registry (79 lines)
β βββ keyToKeyCodeMap.js # Keyboard mapping
β βββ adapter-latest.js # WebRTC adapter polyfill
β βββ loading.js # Loading screen
β βββ loglevel.min.js # Logging
β βββ com_*.js (28 files) # Per-game configurations
βββ css/
β βββ main.css # Styling
βββ images/
βββ favicon.png
βββ logo.png
βββ send.png
π Tech Stack (Client-Side)
From index.html:
<!-- Core Framework -->
<script src="https://s3.ap-southeast-1.amazonaws.com/cdn.cloudbatata.com/ajax/libs/vue/2.6.12/vue.min.js"></script>
<script src="https://s3.ap-southeast-1.amazonaws.com/cdn.cloudbatata.com/ajax/libs/socket.io/2.4.0/socket.io.js"></script>
<script src="https://s3.ap-southeast-1.amazonaws.com/cdn.cloudbatata.com/ajax/libs/axios/0.26.0/axios.min.js"></script>
<script src="https://s3.ap-southeast-1.amazonaws.com/cdn.cloudbatata.com/ajax/libs/vConsole/3.3.4/vconsole.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11.26.2/dist/sweetalert2.all.min.js"></script>
<!-- Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-Q30NXCCZNN"></script>
<!-- Ad Network -->
<script src="https://hb.vntsm.com/v4/live/vms/sites/cloudmoonapp.com/index.js" data-mode="scan"></script>
Summary:
- Vue.js 2.6.12 (reactive UI)
- Socket.IO 2.4.0 (WebSocket signaling)
- Axios 0.26.0 (HTTP client)
- WebRTC (native browser API)
- SweetAlert2 (dialogs)
- vConsole (mobile debugging)
Client Architecture
π Component Overview
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Vue.js App β
β ββββββββββββββββ ββββββββββββββββ ββββββββββββββββ β
β β Socket.IO β β WebRTC β β Input β β
β β Client β βPeerConnectionβ β Capture β β
β β β β β β β β
β β - Signaling β β - Video recv β β - Touch β β
β β - Room mgmt β β - DataChannelβ β - Keyboard β β
β β - ICE relay β β - Stats β β - Mouse β β
β ββββββββββββββββ ββββββββββββββββ ββββββββββββββββ β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β β β
βΌ βΌ βΌ
Signaling Video Stream Input Commands
Server (H.264/VP8) (DataChannel)
π State Management (app-data.js)
// app-data.js - Complete application state
const params = new URLSearchParams(window.location.search);
export const AppData = {
// Connection params
token: params.get("token"),
email: params.get("email"),
game: params.get("game"), // e.g., "com.roblox.client"
userId: params.get("userid"),
androidInstanceId: params.get("android_instance_id"), // Base64 encoded
quality: params.get("quality"), // HD, SD, LD, ULD
codec: params.get("codec"), // H264 or VP8
// WebRTC
pc: null, // RTCPeerConnection
dataChannel: null, // RTCDataChannel
remoteStream: null,
// Video dimensions
screenWidth: 720, // Default
screenHeight: 1280,
videoWidth: 720,
videoHeight: 1280,
// Android instance info (decoded from base64)
androidInstance: {}, // { androidIp, coorUrl, ... }
// Game config
currentGameConfig: {}, // Loaded per-game config
supportPointerLock: false,
supportLeftRocker: false,
supportRightRocker: false,
// UI state
showKeyboard: true,
isEditKeyboard: false,
cursorLock: false,
openMenu: false,
// Performance
dataChannelDelay: 0, // RTT in ms
videoStats: null,
// Kubernetes pod info
podName: "",
podLocale: 0,
// ... many more properties
};
Communication Protocol
π Phase 1: Initial Connection
From app-methods.js lines 873-898:
connectSocket() {
let socketUrl = this.getSocketUrl(); // Returns coordinator URL
this.socket = io(socketUrl, {
reconnection: true,
reconnectionAttempts: 5,
reconnectionDelay: 1000,
path: '/client/socket.io', // WebSocket path
pingInterval: 10000,
pingTimeout: 20000,
transports: ['websocket'] // WebSocket only, no polling
});
this.socketBind(); // Setup event handlers
}
Socket URL determination (app-methods.js lines 695-719):
getSocketUrl() {
const params = new URLSearchParams(window.location.search);
// Try to decode android_instance_id (base64 JSON)
if (params.has("android_instance_id")) {
this.androidInstanceId = params.get("android_instance_id");
try {
this.androidInstance = JSON.parse(atob(this.androidInstanceId));
// androidInstance = { coorUrl, androidIp, ... }
if (this.androidInstance.coorUrl) {
return this.androidInstance.coorUrl;
}
} catch (e) {}
}
// Fallback to current hostname
return `https://${window.location.hostname}`;
}
π Phase 2: Socket.IO Signaling
Event handlers (app-methods.js lines 900-1000):
socketBind() {
// Server says hello
this.socket.on('hello_server', () => {
setTimeout(() => this.joinRoom(), 1000);
});
// Connected to signaling server
this.socket.on('connect', () => {
gtag('event', 'socket_connected');
});
// Server ready, send registration
this.socket.on('action_done2', (data) => {
this.socket.emit('register', {
roomid: data.signalingRoomId,
clientid: '1111111'
});
});
// WebRTC messages
this.socket.on('msg', (msg) => {
switch(msg.type) {
case 'offer':
// Receive SDP offer from game server
this.pc.setRemoteDescription(new RTCSessionDescription(msg));
this.createAnswer();
break;
case 'candidate':
// Receive ICE candidate
this.pc.addIceCandidate(new RTCIceCandidate({
sdpMLineIndex: msg.label,
candidate: msg.candidate
}));
break;
}
});
// Session status
this.socket.on('getout', (data) => {
data = JSON.parse(data);
if (data.errorCode === 10) {
// Success! Stream connected
this.streamingOn = true;
this.socket.close();
} else if (data.errorCode === 1) {
// Server busy
}
// ... more error codes
});
}
Client registration (app-methods.js lines 1010-1035):
joinRoom() {
let data = {
screenRes: this.screenWidth + 'x' + this.screenHeight,
game: this.game, // Package name
userId: this.userId,
slotId: this.userId + '@' + this.game,
instanceId: this.androidInstanceId // Base64 encoded
};
this.socket.emit('client_register', data);
// Timeout if no response
this.joinRoomTimer = setTimeout(() => {
if (this.joinRoomCount > 2) {
swal.fire({ title: 'Connection failed' });
} else {
this.joinRoomCount++;
this.joinRoom(); // Retry
}
}, 3000);
}
π Phase 3: WebRTC Setup
Create peer connection (app-methods.js lines 1251-1289):
createPeerConnection() {
// ACTUAL ICE servers from their code
this.pc = new RTCPeerConnection({
iceServers: [
{ urls: 'stun:stun.nenly.cn:53006' }, // Their STUN
{ urls: 'stun:stun.l.google.com:19302' } // Google STUN
],
bundlePolicy: 'max-bundle',
rtcpMuxPolicy: 'require'
});
// Event handlers
this.pc.onicecandidate = this.handleIceCandidate;
this.pc.ontrack = this.handleTrack;
this.pc.oniceconnectionstatechange = function() {
gtag('event', `ice_connection_state_${this.pc.iceConnectionState}`);
};
// Create data channel for input
this.createDataChannel();
}
DataChannel creation (app-methods.js lines 1037-1066):
createDataChannel() {
let channel = this.pc.createDataChannel('config', {
ordered: true,
reliable: true
});
channel.onopen = () => {
// Send initial resolution
this.dataChannel.send('res>' + this.screenWidth + 'x' + this.screenHeight);
// Start statistics polling
this.getStats();
};
channel.onmessage = (event) => {
this.handleDCMessage(event.data);
};
channel.onclose = () => {
// Connection lost
this.pc.close();
this.streamingOn = false;
};
this.dataChannel = channel;
}
Create answer (app-methods.js lines 1349-1377):
createAnswer() {
this.pc.createAnswer().then(answer => {
// Optional bitrate constraint
if (this.bitRate > 0) {
let lines = answer.sdp.split('\r\n');
lines.forEach((line, idx) => {
if (/^a=fmtp:\d*/.test(line)) {
lines[idx] = line +
`;x-google-max-bitrate=${this.bitRate}` +
`;x-google-min-bitrate=1000`;
}
});
answer.sdp = lines.join('\r\n');
}
this.pc.setLocalDescription(answer);
this.sendMessage(answer); // Send via Socket.IO
});
}
WebRTC Implementation
π Video Track Handling
From app-methods.js lines 1203-1236:
handleTrack(event) {
event.track.onunmute = () => {
if (event.track.kind === 'video') {
// Bind video stream to <video> element
this.$refs.remoteVideo.srcObject = event.streams[0];
this.remoteStream = event.streams[0];
// Get receivers for jitter buffer tuning
this.receivers = this.pc.getReceivers();
} else if (event.track.kind === 'audio') {
this.$refs.remoteAudio.srcObject = event.streams[0];
}
};
}
π Statistics & Adaptive Quality
From app-methods.js lines 762-792:
getStats() {
this.pc.getStats().then(stats => {
stats.forEach(report => {
// Video stats
if (report.type === 'inbound-rtp' && report.mediaType === 'video') {
this.videoStats = report;
// Adjust jitter buffer based on network conditions
this.adjustPlayoutDelayFromStats(report);
}
// RTT (latency)
if (report.type === 'candidate-pair' && report.nominated) {
this.dataChannelDelay = 1000 * report.currentRoundTripTime;
// Cap at 999ms
if (this.dataChannelDelay > 999) {
this.dataChannelDelay = 999;
}
}
});
});
setTimeout(() => this.getStats(), 1000); // Poll every second
}
Adaptive jitter buffer (app-methods.js lines 742-760):
adjustPlayoutDelayFromStats(stats) {
const jitter = stats.jitter ?? 1;
const packetLoss = (stats.retransmittedPacketsReceived ?? 0) /
(stats.packetsReceived ?? 1);
const avgJitterDelay = (stats.jitterBufferMinimumDelay ?? 0) /
(stats.jitterBufferEmittedCount ?? 1) * 1000;
let targetDelay = 0.2; // Default 200ms
// Ultra low latency
if (jitter < 0.005 && packetLoss < 0.05 && avgJitterDelay < 70) {
targetDelay = 0.04; // 40ms
}
// Low latency
else if (jitter < 0.01 && packetLoss < 0.1 && avgJitterDelay < 100) {
targetDelay = 0.1; // 100ms
}
// Apply to all receivers
for (const receiver of this.pc.getReceivers()) {
if (receiver.jitterBufferTarget !== undefined) {
receiver.jitterBufferTarget = targetDelay;
}
}
}
DataChannel Message Protocol
π Message Format
All messages: {type}>{data}
π Client β Server Messages
From actual code usage:
| Message | Example | Source Code |
|---|---|---|
res> | res>1280x720 | app-methods.js:1051 |
touch> | touch>Touch start:10,640,360 | app-methods.js:1721 |
touch> | touch>Mouse down:0,640,360 | app-methods.js:1546 |
keyboard> | keyboard>66,0 | Not directly used |
keycode> | keycode>4 | index.html (BACK button) |
sensor> | sensor>accelerate:9.55_0.40_1.64 | app-methods.js:1333 |
softinput> | softinput>switch:remote | app-methods.js:554 |
softinput> | softinput>text_change:0:0,999,Hello | app-methods.js:652 |
softinput> | softinput>action:4 | app-methods.js:637 |
game> | game>restart | index.html (Restart button) |
stats> | stats> | app-methods.js:737 (request stats) |
adBlock> | adBlock>true | app-methods.js:116 |
π Server β Client Messages
From handleDCMessage (app-methods.js lines 1087-1166):
handleDCMessage(msg) {
msg = msg.split('>');
let type = msg[0];
let data = msg[1];
switch(type) {
case 'rotation':
// Screen orientation (0=portrait, 1=landscape)
this.rotation = parseInt(data);
this.rotateHandler();
break;
case 'keyboard':
// show, hide, verify
if (data.startsWith('show')) {
this.inputTextShow = true;
this.$refs.input.focus();
}
break;
case 'stats':
// Performance metrics
// Format: "bwe:12345,rtt:50,fps:60,..."
let metrics = data.split(',');
// Parse and update UI
break;
case 'cloudData':
// Custom game data (JSON)
let gameData = JSON.parse(data);
if (gameData.loading_done) {
this.loadingDoneTime = Date.now();
}
break;
case 'pod_name':
// Kubernetes pod identifier
this.podName = data;
break;
case 'locale':
// Datacenter region
this.podLocale = data;
break;
case 'kickout':
// Session terminated
this.getout = true;
this.socket.close();
break;
case 'hide_cursor':
// 0 or 1
this.cursorHide = (data === '1');
break;
case 'touchState':
// true or false
this.enableTouch = (data === 'true');
break;
}
}
Input Handling System
π Coordinate Transformation
Critical for mapping browser coordinates to Android (app-methods.js lines 1690-1718):
getMousePos(pageX, pageY) {
// 1. Convert to container-relative
let rect = this.$refs.containerDiv.getBoundingClientRect();
pageX -= rect.left;
pageY -= rect.top;
// 2. Center adjustment
let videoW = this.direction === GAME_VIEW_PORTRAIT ?
this.videoWidth : this.videoHeight;
let videoH = this.direction === GAME_VIEW_PORTRAIT ?
this.videoHeight : this.videoWidth;
pageX -= (containerWidth - videoW) / 2;
pageY -= (containerHeight - videoH) / 2;
// 3. Scale to Android resolution
let scale = this.screenWidth / this.$refs.remoteVideo.offsetWidth;
pageX = Math.floor(pageX * scale);
pageY = Math.floor(pageY * scale);
// 4. Handle rotation
if (this.rotation === ANDROID_LANDSCAPE &&
this.direction === GAME_VIEW_PORTRAIT) {
// Rotate coordinates 90 degrees
return {
x: Math.abs(pageY),
y: Math.abs(pageX - this.screenWidth)
};
}
return { x: pageX, y: pageY };
}
π Touch Input
From app-methods.js lines 1720-1739:
touchstart(event) {
let msg = 'touch>Touch start' + this.getChangedTouchesString(event);
this.sendDataChannelMessage(msg);
}
touchmove(event) {
let msg = 'touch>Touch move' + this.getChangedTouchesString(event);
this.sendDataChannelMessage(msg);
}
touchend(event) {
let msg = 'touch>Touch end' + this.getChangedTouchesString(event);
this.sendDataChannelMessage(msg);
}
getChangedTouchesString(event) {
let result = '';
for (let i = 0; i < event.changedTouches.length; i++) {
let touch = event.changedTouches[i];
let pos = this.getMousePos(touch.pageX, touch.pageY);
let id = this.getTouchCounter(touch.identifier);
// Format: :id,x,y
result += `:${id},${pos.x},${pos.y}`;
}
return result;
// Example: "touch>Touch start:10,640,360:11,200,300"
}
// Persistent touch ID mapping
getTouchCounter(identifier) {
if (!this.touchIdMap.has(identifier)) {
this.touchIdMap.set(identifier, this.touchCounter++);
}
return this.touchIdMap.get(identifier);
}
π Mouse Input
Normal mouse (app-methods.js lines 1505-1549):
mousedown(event) {
const canvas = document.getElementById('canvasCoc');
const isPointerLocked = document.pointerLockElement === canvas;
if (isPointerLocked) {
// Pointer lock mode (FPS games)
if (event.button === 0) { // Left click
let coord = '1060,550'; // Fixed position
this.sendDataChannelMessage(`touch>Touch start:5,${coord}`);
}
} else {
// Normal mode - send actual coordinates
this.sendMove = true;
let msg = `touch>Mouse down:${event.button},` +
`${this.resizeHandler(event.offsetX)},` +
`${this.resizeHandler(event.offsetY)}`;
this.sendDataChannelMessage(msg);
}
}
Pointer lock mode (app-methods.js lines 1551-1585):
mousemove(event) {
const isPointerLocked = document.pointerLockElement === canvas;
if (isPointerLocked) {
// Accumulate movement deltas
this.moveX += event.movementX * this.mouseSensitivity;
this.moveY += event.movementY * this.mouseSensitivity;
// Clamp to screen bounds
let maxX = this.$refs.remoteVideo.offsetWidth / 2;
let maxY = this.$refs.remoteVideo.offsetHeight / 2;
this.moveX = Math.max(-maxX, Math.min(maxX, this.moveX));
this.moveY = Math.max(-maxY, Math.min(maxY, this.moveY));
let center = { x: 640, y: 360 };
let dx = this.resizeHandler(this.moveX);
let dy = this.resizeHandler(this.moveY);
this.sendDataChannelMessage(
`touch>Touch move:8,${center.x + dx},${center.y + dy}`
);
// Auto-release after 200ms of no movement
clearTimeout(this.resetMoveTimer);
this.resetMoveTimer = setTimeout(() => {
this.sendDataChannelMessage(
`touch>Touch end:8,${center.x + dx},${center.y + dy}`
);
this.moveX = 0;
this.moveY = 0;
}, 200);
}
}
π Virtual Joystick (WASD)
Continuous update loop (app-methods.js lines 494-550):
gameLoop() {
this.updateLeftPosition(); // WASD joystick
this.updateRightPosition(); // ZXCV joystick (if game supports)
requestAnimationFrame(this.gameLoop);
}
updateLeftPosition() {
if (!this.supportLeftRocker) return;
let dx = 0, dy = 0;
let step = 4; // Pixels per frame
// Calculate movement vector based on pressed keys
if (this.leftRockerPressedKeys.w) dy -= step;
if (this.leftRockerPressedKeys.s) dy += step;
if (this.leftRockerPressedKeys.a) dx -= step;
if (this.leftRockerPressedKeys.d) dx += step;
// Update accumulated position
this.leftRockerPosition.x += dx;
this.leftRockerPosition.y += dy;
// Clamp to circular boundary (radius = 120px)
let radius = 120;
let distance = Math.sqrt(
this.leftRockerPosition.x ** 2 +
this.leftRockerPosition.y ** 2
);
if (distance > radius) {
let angle = Math.atan2(this.leftRockerPosition.y,
this.leftRockerPosition.x);
this.leftRockerPosition.x = radius * Math.cos(angle);
this.leftRockerPosition.y = radius * Math.sin(angle);
}
// Send touch events if keys are pressed
if (Object.keys(this.leftRockerPressedKeys).length > 0) {
if (!this.leftRockerStart) {
// Initial touch
let msg = `touch>Touch start:10,` +
`${this.center.x + this.leftRockerPosition.x},` +
`${this.center.y + this.leftRockerPosition.y}`;
this.sendDataChannelMessage(msg);
this.leftRockerStart = true;
}
// Continuous movement (60fps)
let msg = `touch>Touch move:10,` +
`${this.center.x + Math.floor(this.leftRockerPosition.x)},` +
`${this.center.y + Math.floor(this.leftRockerPosition.y)}`;
this.sendDataChannelMessage(msg);
}
}
π Keyboard Handling
Key down (app-methods.js lines 1825-1857):
listenKeyDown(event) {
event.preventDefault();
// Ctrl+M toggles pointer lock
if (event.ctrlKey && event.keyCode === 77) {
if (this.currentGameConfig?.pointerLock) {
this.pointerLock();
}
return;
}
if (this.keyDownMap[event.key]) return; // Already pressed
this.keyDownMap[event.key] = true;
let key = event.key.toLowerCase();
// Virtual joystick keys
if (this.supportLeftRocker && ['w','a','s','d'].includes(key)) {
this.leftRockerPressedKeys[key] = true;
return; // Handled by gameLoop
}
// Game-specific key binding
let config = this.currentGameConfig.keyBoard?.[key];
if (config && config.coordinate !== '0,0') {
let touchId = this.getTouchCounter(key);
this.sendDataChannelMessage(
`touch>Touch start:${touchId},${config.coordinate}`
);
// Visual feedback
document.querySelector(`.keyboard-key-${this.keyToKeyCode(key)}`)
?.classList.add('keyboard-key-press');
}
}
Game Configuration System
π Configuration Structure
Example: Roblox (com_roblox_client.js):
const comRobloxClientConfig = {
supportLeftRocker: true, // Enable WASD joystick
supportRightClick: true, // Enable right-click camera
pointerLock: false, // Disable FPS mouse lock
center: { x: 162, y: 575 }, // Left joystick center
keyBoardDirection: 1, // 0=portrait, 1=landscape
keyBoard: {
' ': {
str: 'Space',
key: 'space',
coordinate: '1142,575', // Android screen coords
size: 'small'
},
'shift': {
str: 'Shift',
key: 'shift',
coordinate: '1050,575',
size: 'small'
},
'escape': {
str: 'Esc',
key: 'escape',
coordinate: '45,45',
size: 'small'
}
}
};
π Configuration Loading
From app.js lines 50-72:
async mounted() {
// Try to fetch remote config first
const response = await fetch(
`https://api.prod.cloudmoonapp.com/web/game_config?pkg=${this.checkGame}`
);
let config = {};
if (response.code === 0) {
config = response.data.config; // Use server config
} else {
// Fallback to local bundled config
config = this.gameConfig[this.checkGame] ?? {};
}
this.currentGameConfig = config;
this.supportPointerLock = config?.pointerLock ?? false;
this.supportLeftRocker = config?.supportLeftRocker ?? false;
this.center = config?.center ?? { x: 0, y: 0 };
// Load user customizations from localStorage
this.loadKeyboardLayout();
}
π Custom Layout Persistence
From app-methods.js lines 391-440:
saveKeyboardLayout() {
localStorage.setItem(
`keyboardLayout:${this.selectLayout}:${this.checkGame}:center`,
JSON.stringify(this.currentGameConfig.center)
);
localStorage.setItem(
`keyboardLayout:${this.selectLayout}:${this.checkGame}:newKeyboard`,
JSON.stringify(this.currentGameConfig.keyBoard)
);
}
loadKeyboardLayout() {
// Load saved keyboard (3 layouts: layout1, layout2, layout3)
let saved = localStorage.getItem(
`keyboardLayout:${this.selectLayout}:${this.checkGame}:newKeyboard`
);
if (saved) {
this.currentGameConfig.keyBoard = JSON.parse(saved);
} else if (this.defaultKeyboard) {
// Use default from config
this.currentGameConfig.keyBoard =
JSON.parse(JSON.stringify(this.defaultKeyboard));
}
}
π Supported Games (28 total)
From gameConfig.js:
export default {
"com.miHoYo.GenshinImpact": comMiHoYoGenshinImpactConfig,
"com.roblox.client": comRobloxClientConfig,
"com.HoYoverse.hkrpgoversea": comHoYoverseHkrpgoverseaConfig,
"com.kurogame.wutheringwaves.global": comKurogameWutheringwavesGlobalConfig,
"com.HoYoverse.Nap": comHoYoverseNapConfig,
"com.devsisters.ck": comDevsistersCkClientConfig,
"com.netease.idv.googleplay": comNeteaseIdvGoogleplayConfig,
"com.infoldgames.infinitynikkien": comInfoldgamesInfinitynikkienConfig,
"com.papegames.lysk.en": comPapegamesLyskEnConfig,
"com.netmarble.sololv": comNetmarbleSololvConfig,
"com.proximabeta.nikke": comProximabetaNikkeClientConfig,
"com.miHoYo.bh3oversea": comMiHoYoBh3overseaConfig,
"com.kurogame.gplay.punishing.grayraven.en": comKurogameGplayPunishingGrayravenEnConfig,
"com.netease.dfjssea": comNeteaseDfjsseaConfig,
"jp.konami.pesam": jpKonamiPesamConfig,
"com.epicgames.fortnite": comEpicgamesFortniteConfig,
"com.tgc.sky.android": comTgcSkyAndroidConfig,
"com.h73.jhqyna": comH73JhqynaConfig,
"com.garena.game.df": comGarenaGameDfConfig,
"com.supercell.brawlstars": comSupercellBrawlstarsConfig,
"com.levelinfinite.sgameGlobal": comLevelinfiniteSgameGlobalConfig,
"com.garena.game.kgth": comGarenaGameKgthConfig,
"com.garena.game.kgvn": comGarenaGameKgvnConfig,
"com.sega.persona5.the.phantomx.en": comSegaPersona5ThePhantomxEnConfig,
"com.blackstorm.rememento.android.google": comBlackstormRemementoAndroidGoogleConfig,
"com.xd.etheria.gp.glb": comXdEtheriaGpGlbConfig,
"com.tencent.ig": comTencentIgConfig,
"com.kakaogames.gdts": comKakaogamesGdtsConfig,
"com.mobile.legends": comMobileLegendsConfig,
"com.dts.freefire": comDtsFreefireConfig,
"com.kitkagames.fallbuddies": comKitkagamesFallbuddiesConfig,
"com.innersloth.spacemafia": comInnerslothSpacemafiaConfig,
"com.supercell.squad": comSupercellSquadConfig,
"com.YoStarEN.AzurLane": comYoStarENAzurLaneConfig,
"YZGAME": YZGAMEConfig // Custom game support
};
CloudMoon Infrastructure
π Official API Endpoints (from code)
REST API:
https://api.prod.cloudmoonapp.com/
βββ /web/game_config?pkg={package} # Game configuration
βββ /web/ad # Ad placement check
βββ /phone/action # Quality/instance control
POST {
android_id: userId,
game_name: game,
instance_id: androidInstanceId,
key: 'quality',
value: 'HD|SD|LD|ULD'
}
Shop/Premium:
https://shop.cloudmoonapp.com/shop
WebSocket Signaling:
wss://{coordinator_host}/client/socket.io
coordinator_hostis dynamic, extracted fromandroidInstanceId(base64)- Fallback: current page hostname
STUN Servers:
stun:stun.nenly.cn:53006 # Their custom STUN
stun:stun.l.google.com:19302 # Google STUN (fallback)
π CDN & External Services
Library CDN:
https://s3.ap-southeast-1.amazonaws.com/cdn.cloudbatata.com/ajax/libs/
βββ vue/2.6.12/vue.min.js
βββ socket.io/2.4.0/socket.io.js
βββ axios/0.26.0/axios.min.js
βββ vConsole/3.3.4/vconsole.min.js
Other CDNs:
https://cdn.jsdelivr.net/npm/
βββ sweetalert2@11.26.2/
Analytics:
https://www.googletagmanager.com/gtag/js?id=G-Q30NXCCZNN
Ad Network:
https://hb.vntsm.com/v4/live/vms/sites/cloudmoonapp.com/index.js
π‘ Inferred Server Architecture
Based on code evidence:
- Kubernetes-based (from
podName,podLocaleDataChannel messages) - Containerized Android instances (each has
androidIp, isolated) - Regional deployment (
podLocalesuggests multiple datacenters) - Dynamic coordinator routing (
coorUrlin androidInstance)
Ping display shows (index.html line 5):
/ {{userId}} / {{podLocale}} / {{androidInstance?.androidIp}} / {{utcDate}}
This suggests:
- Each user gets a dedicated Android instance
- Instance has its own IP address on internal network
- Pod locale indicates datacenter region
- Instances are ephemeral (allocated per session)
π Quality Settings
From app-methods.js:
// Quality options
"HD" - High Definition
"SD" - Standard Definition
"LD" - Low Definition
"ULD" - Ultra Low Definition
// Sent to server via:
POST https://api.prod.cloudmoonapp.com/phone/action
{
key: 'quality',
value: 'HD' // etc
}
// Server adjusts encoder bitrate accordingly
π Analytics Events
From code (gtag events):
gtag('event', 'socket_connecting');
gtag('event', 'socket_connected');
gtag('event', 'receive_offer');
gtag('event', 'receive_stream');
gtag('event', 'ice_connection_state_${state}');
gtag('event', 'user_show_ad');
gtag('event', 'user_block_ad');
gtag('event', 'play_for_${minutes}_min'); // 1-30 minutes
Key Insights
β What We Know for Certain
- Client is Vue.js 2.6 with Socket.IO for signaling
- WebRTC uses H.264/VP8 codecs
- All input goes through DataChannel (not WebSocket)
- Adaptive jitter buffer (40-200ms based on network)
- WASD joystick updates at 60fps via requestAnimationFrame
- 28+ games supported with per-game configs
- 3 keyboard layouts per game (user customizable)
- Pointer lock mode for FPS games
- Real-time latency display (RTT from ICE candidate pairs)
- Custom STUN server at
stun.nenly.cn:53006
π‘ Educated Guesses
- ARM-based Android containers (likely Redroid or similar)
- Evidence:
androidIpfield,podNameKubernetes terminology
- Evidence:
- Global edge deployment
- Evidence:
podLocale, dynamiccoorUrl
- Evidence:
- GPU-accelerated encoding
- Evidence: H.264 hardware encoding, low latency
- Session-based billing
- Evidence: Time tracking, premium upgrade prompts
β Unknown (Server-Side Implementation)
- Exact Android virtualization technology
- Video encoding pipeline details
- Input injection mechanism
- Scaling/orchestration specifics
- Billing/subscription system
Complete Server & Endpoint Reference
π All URLs Found in Code
CloudMoon Services:
https://api.prod.cloudmoonapp.com/web/game_config
https://api.prod.cloudmoonapp.com/web/ad
https://api.prod.cloudmoonapp.com/phone/action
https://shop.cloudmoonapp.com/shop
https://web.cloudmoonapp.com/
wss://{dynamic}/client/socket.io
stun:stun.nenly.cn:53006
CDN & Libraries:
https://s3.ap-southeast-1.amazonaws.com/cdn.cloudbatata.com/ajax/libs/
- vue/2.6.12/vue.min.js
- socket.io/2.4.0/socket.io.js
- axios/0.26.0/axios.min.js
- vConsole/3.3.4/vconsole.min.js
https://cdn.jsdelivr.net/npm/
- sweetalert2@11.26.2/dist/sweetalert2.all.min.js
- sweetalert2@11.26.2/dist/sweetalert2.min.css
- sweetalert2@11.26.2/themes/material-ui.css
Third-Party Services:
https://www.googletagmanager.com/gtag/js?id=G-Q30NXCCZNN (Analytics)
https://hb.vntsm.com/v4/live/vms/sites/cloudmoonapp.com/ (Ad network)
stun:stun.l.google.com:19302 (Google STUN)
Document Version: 2.0 (Code Analysis Only) Based on: CloudMoon Web Client (build_time=1764914045706) Analysis Date: 2025-12-13 Files Analyzed: 35 JavaScript files, 1 HTML file, 1 CSS file
