210 lines
9.9 KiB
JavaScript
210 lines
9.9 KiB
JavaScript
// voice_bridge_v7.js — Voice Bridge with Audio Injection
|
|
// Hook point: libantaudio.so MFAntAudio3AV2Filter::process(micIn, spkRef, out, size, &result)
|
|
// TCP :18901
|
|
// Frame: 4-byte len + 1-byte type + payload
|
|
// type 0: speaker/AI audio (spkRef, downstream to client)
|
|
// type 1: text/JSON command
|
|
// type 2: mic audio (micIn, downstream to client)
|
|
// type 3: inject audio (upstream from client, replaces micIn)
|
|
|
|
var voiceActive = false;
|
|
var clientOS = null;
|
|
var capturedSpk = 0, capturedMic = 0, spkBytes = 0, micBytes = 0;
|
|
var injectMode = false; // true = replace mic with injected audio
|
|
var injectQueue = []; // queue of PCM frames to inject
|
|
|
|
function wf(os, type, jArr) {
|
|
try {
|
|
var len = jArr.length;
|
|
var h = Java.array("byte", [(len>>24)&0xFF,(len>>16)&0xFF,(len>>8)&0xFF,len&0xFF, type]);
|
|
os.write(h); os.write(jArr); os.flush();
|
|
} catch(e) {}
|
|
}
|
|
function wt(os, text) {
|
|
wf(os, 1, Java.use("java.lang.String").$new(text).getBytes("UTF-8"));
|
|
}
|
|
|
|
// === Hook libantaudio.so ===
|
|
var hooked = false;
|
|
function tryHook() {
|
|
if (hooked) return;
|
|
var m = Process.findModuleByName("libantaudio.so");
|
|
if (!m) return;
|
|
var addr = m.findExportByName("_ZN8antaudio20MFAntAudio3AV2Filter7processEPhS1_S1_iRi");
|
|
if (!addr) return;
|
|
hooked = true;
|
|
|
|
Interceptor.attach(addr, {
|
|
onEnter: function(args) {
|
|
if (!voiceActive || !clientOS) return;
|
|
var size = args[4].toInt32();
|
|
if (size <= 0) return;
|
|
|
|
try {
|
|
// If inject mode, replace micIn with queued or silence
|
|
if (injectMode) {
|
|
if (injectQueue.length > 0) {
|
|
var frame = injectQueue.shift();
|
|
// Only write if frame size matches expected size
|
|
if (frame.byteLength === size) {
|
|
args[1].writeByteArray(frame);
|
|
} else if (frame.byteLength > 0) {
|
|
// Size mismatch — pad or truncate
|
|
var buf = new ArrayBuffer(size);
|
|
var dst = new Uint8Array(buf);
|
|
var src = new Uint8Array(frame);
|
|
var copyLen = Math.min(size, frame.byteLength);
|
|
for (var k = 0; k < copyLen; k++) dst[k] = src[k];
|
|
args[1].writeByteArray(buf);
|
|
}
|
|
} else {
|
|
// No data queued — inject silence to avoid mic leak
|
|
var silence = new ArrayBuffer(size);
|
|
args[1].writeByteArray(silence);
|
|
}
|
|
}
|
|
|
|
// Always capture speaker/AI output (type 0)
|
|
var spkPcm = args[2].readByteArray(size);
|
|
var spkArr = Java.array("byte", Array.from(new Uint8Array(spkPcm)));
|
|
wf(clientOS, 0, spkArr);
|
|
capturedSpk++; spkBytes += size;
|
|
|
|
// Capture mic (type 2) only when not injecting
|
|
if (!injectMode) {
|
|
var micPcm = args[1].readByteArray(size);
|
|
var micArr = Java.array("byte", Array.from(new Uint8Array(micPcm)));
|
|
wf(clientOS, 2, micArr);
|
|
}
|
|
capturedMic++; micBytes += size;
|
|
|
|
if (capturedMic <= 3 || capturedMic % 500 === 0)
|
|
console.log("[VOICE] mic=" + capturedMic + " spk=" + capturedSpk + " inject=" + injectQueue.length);
|
|
} catch(e) {}
|
|
}
|
|
});
|
|
console.log("[VOICE] 3AV2Filter.process hooked @ " + addr);
|
|
}
|
|
|
|
[0, 1000, 3000, 5000, 10000, 15000, 20000].forEach(function(ms) { setTimeout(tryHook, ms); });
|
|
try {
|
|
new ApiResolver("module").enumerateMatches("exports:linker*!*dlopen*").forEach(function(d) {
|
|
Interceptor.attach(d.address, { onLeave: function() { setTimeout(tryHook, 500); } });
|
|
});
|
|
} catch(e) {}
|
|
|
|
// === TCP Server ===
|
|
Java.perform(function() {
|
|
var SS = Java.use("java.net.ServerSocket");
|
|
var JS = Java.use("java.lang.String");
|
|
var server = SS.$new(18901);
|
|
console.log("[VOICE] Listening :18901");
|
|
|
|
function openVoice(os) {
|
|
Java.scheduleOnMainThread(function() {
|
|
try {
|
|
Java.choose("com.antgroup.aijk.android.ijklauncher.biz.activity.IJKActivity", {
|
|
onMatch: function(a) {
|
|
var fm = a.getSupportFragmentManager();
|
|
var f = Java.use("com.antgroup.aijk.android.ijkchat.biz.voicechat.IjkVoiceChatFragment").$new();
|
|
f.show(fm, "v");
|
|
console.log("[VOICE] Opened");
|
|
}, onComplete: function() {}
|
|
});
|
|
setTimeout(function() { wt(os, JSON.stringify({event:"voice_opened"})); }, 2000);
|
|
} catch(e) { wt(os, JSON.stringify({event:"error",msg:""+e})); }
|
|
});
|
|
}
|
|
|
|
function closeVoice(os) {
|
|
Java.scheduleOnMainThread(function() {
|
|
try {
|
|
Java.choose("com.antgroup.aijk.android.ijkchat.biz.voicechat.IjkVoiceChatFragment", {
|
|
onMatch: function(f) { f.dismiss(); console.log("[VOICE] Closed"); },
|
|
onComplete: function() {}
|
|
});
|
|
setTimeout(function() { wt(os, JSON.stringify({event:"voice_closed"})); }, 1000);
|
|
} catch(e) { wt(os, JSON.stringify({event:"error",msg:""+e})); }
|
|
});
|
|
}
|
|
|
|
var Srv = Java.registerClass({
|
|
name: "com.antaf.voice.S7",
|
|
implements: [Java.use("java.lang.Runnable")],
|
|
methods: {
|
|
run: function() {
|
|
while (true) {
|
|
try {
|
|
console.log("[VOICE] Waiting...");
|
|
var c = server.accept();
|
|
var is = c.getInputStream();
|
|
var os = c.getOutputStream();
|
|
clientOS = os;
|
|
console.log("[VOICE] Connected");
|
|
wt(os, JSON.stringify({
|
|
event:"connected", protocol:"antaf-voice-v8",
|
|
commands:["open_voice","close_voice","start","stop","status","inject_on","inject_off"],
|
|
audio:"pcm-16bit-960b-frames",
|
|
frameTypes:{0:"spk_ai",1:"text",2:"mic",3:"inject"}
|
|
}));
|
|
|
|
while (true) {
|
|
var hb = [];
|
|
for (var i=0;i<5;i++) { var b=is.read(); if(b<0) throw "EOF"; hb.push(b); }
|
|
var fl=(hb[0]<<24)|(hb[1]<<16)|(hb[2]<<8)|hb[3], ft=hb[4];
|
|
if (fl>1048576) break;
|
|
var pb = [];
|
|
for (var i=0;i<fl;i++) { var b=is.read(); if(b<0) throw "EOF"; pb.push(b&0xFF); }
|
|
|
|
if (ft === 3) {
|
|
// type 3: inject audio frame into micIn
|
|
var arr = new ArrayBuffer(pb.length);
|
|
var view = new Uint8Array(arr);
|
|
for (var j=0;j<pb.length;j++) view[j] = pb[j];
|
|
injectQueue.push(arr);
|
|
}
|
|
else if (ft === 1) {
|
|
var pl = Java.array("byte", pb);
|
|
var cmd = JSON.parse(JS.$new(pl,"UTF-8").toString());
|
|
console.log("[VOICE] Cmd: " + JSON.stringify(cmd));
|
|
if (cmd.cmd === "open_voice") openVoice(os);
|
|
else if (cmd.cmd === "close_voice") closeVoice(os);
|
|
else if (cmd.cmd === "start") {
|
|
voiceActive = true;
|
|
capturedSpk=0;capturedMic=0;spkBytes=0;micBytes=0;
|
|
injectQueue = [];
|
|
wt(os, JSON.stringify({event:"started",hooked:hooked}));
|
|
}
|
|
else if (cmd.cmd === "stop") {
|
|
voiceActive = false;
|
|
injectMode = false;
|
|
injectQueue = [];
|
|
wt(os, JSON.stringify({event:"stopped",spk:{frames:capturedSpk,bytes:spkBytes},mic:{frames:capturedMic,bytes:micBytes}}));
|
|
}
|
|
else if (cmd.cmd === "inject_on") {
|
|
injectMode = true;
|
|
injectQueue = [];
|
|
wt(os, JSON.stringify({event:"inject_on"}));
|
|
console.log("[VOICE] Inject mode ON");
|
|
}
|
|
else if (cmd.cmd === "inject_off") {
|
|
injectMode = false;
|
|
injectQueue = [];
|
|
wt(os, JSON.stringify({event:"inject_off"}));
|
|
console.log("[VOICE] Inject mode OFF");
|
|
}
|
|
else if (cmd.cmd === "status") {
|
|
wt(os, JSON.stringify({event:"status",active:voiceActive,hooked:hooked,inject:injectMode,queue:injectQueue.length,spk:{frames:capturedSpk,bytes:spkBytes},mic:{frames:capturedMic,bytes:micBytes}}));
|
|
}
|
|
}
|
|
}
|
|
} catch(e) { console.log("[VOICE] Ended: "+e); }
|
|
finally { voiceActive=false; clientOS=null; injectMode=false; injectQueue=[]; }
|
|
}
|
|
}
|
|
}
|
|
});
|
|
Java.use("java.lang.Thread").$new(Srv.$new()).start();
|
|
console.log("[VOICE] Ready (v7 + inject)");
|
|
});
|