Just.Call / index.html
tuhbooh's picture
Update index.html
6922dce verified
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<title>VoiceConnect Pro - Hi-Fi Audio (Spotify Quality)</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/peerjs@1.5.2/dist/peerjs.min.js"></script>
<script src="https://unpkg.com/lucide@latest"></script>
<style>
@import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap');
body {
font-family: 'Plus Jakarta Sans', sans-serif;
background: #020617;
color: #f8fafc;
margin: 0;
overflow: hidden;
touch-action: manipulation;
}
.call-overlay {
position: fixed;
inset: 0;
background: radial-gradient(circle at center, #1e293b 0%, #020617 100%);
z-index: 9999;
transition: transform 0.4s cubic-bezier(0.16, 1, 0.3, 1);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.key-btn {
background: rgba(30, 41, 59, 0.6);
backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.1);
aspect-ratio: 1/1;
border-radius: 1.5rem;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
transition: 0.15s;
}
.key-btn:active { transform: scale(0.92); background: rgba(59, 130, 246, 0.4); }
.settings-panel {
position: fixed;
inset: 0;
background: #020617;
z-index: 10001;
transform: translateX(100%);
transition: 0.3s cubic-bezier(0.16, 1, 0.3, 1);
overflow-y: auto;
}
.settings-panel.open { transform: translateX(0); }
.infinite-bar {
background: linear-gradient(90deg, #8b5cf6 0%, #3b82f6 50%, #8b5cf6 100%);
background-size: 200% 100%;
animation: move-gradient 2s linear infinite;
}
@keyframes move-gradient {
0% { background-position: 200% 0%; }
100% { background-position: 0% 0%; }
}
.audio-pulse {
width: 140px;
height: 140px;
background: rgba(139, 92, 246, 0.2);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
.audio-pulse::before, .audio-pulse::after {
content: '';
position: absolute;
border: 2px solid #8b5cf6;
inset: 0;
border-radius: 50%;
animation: pulse-ring 2s cubic-bezier(0.21, 0.61, 0.35, 1) infinite;
}
.audio-pulse::after { animation-delay: 1s; }
@keyframes pulse-ring {
0% { transform: scale(0.8); opacity: 0.8; }
100% { transform: scale(2.8); opacity: 0; }
}
</style>
</head>
<body>
<div class="min-h-screen flex flex-col p-6 max-w-md mx-auto relative z-10 pb-10">
<header class="mb-6">
<div class="flex justify-between items-center mb-4">
<h1 class="font-bold flex items-center gap-2 text-purple-400 text-xl italic">VoiceConnect <span class="text-[9px] bg-purple-600 px-2 py-0.5 rounded-full text-white not-italic">HI-FI</span></h1>
<div class="flex items-center gap-3">
<div id="my-id-display" class="font-mono text-purple-400 font-bold bg-purple-500/10 px-3 py-1 rounded-lg text-sm border border-purple-500/20 italic tracking-tighter">Connecting...</div>
<button onclick="toggleSettings()" class="w-10 h-10 flex items-center justify-center bg-slate-800 rounded-2xl border border-white/5 shadow-lg active:scale-90 transition-all"><i data-lucide="settings" size="20"></i></button>
</div>
</div>
<div class="bg-slate-900/80 p-5 rounded-[2.5rem] border border-purple-500/20 shadow-2xl backdrop-blur-md">
<div class="flex justify-between items-end mb-3">
<span class="text-[10px] text-slate-400 font-bold uppercase tracking-widest">CHẤT LƯỢNG: LOSSLESS RAW</span>
<span id="data-status" class="text-2xl font-black text-white tracking-tighter flex items-center gap-2">
<span id="data-used">0 MB</span>
<span class="text-purple-500 text-3xl"></span>
</span>
</div>
<div class="w-full bg-slate-800 h-2 rounded-full overflow-hidden">
<div class="infinite-bar h-full w-full"></div>
</div>
</div>
</header>
<main class="flex-1 flex flex-col items-center">
<input type="text" id="target-id-input" readonly class="w-full bg-transparent text-center text-6xl font-black text-white py-6 outline-none tracking-tighter" placeholder="0000">
<div class="grid grid-cols-3 gap-4 w-full mb-8" id="keypad"></div>
<button id="call-btn" class="w-24 h-24 bg-purple-600 rounded-full flex items-center justify-center shadow-[0_20px_50px_rgba(139,92,246,0.4)] active:scale-95 transition-all mb-10">
<i data-lucide="headphones" class="text-white" size="40"></i>
</button>
</main>
</div>
<!-- Màn hình cuộc gọi Hi-Fi -->
<div id="call-overlay" class="call-overlay translate-y-full">
<audio id="remote-audio" autoplay></audio>
<div class="flex-1 flex flex-col items-center justify-center pt-20">
<div id="status-animation" class="audio-pulse mb-12 hidden">
<div class="w-16 h-16 bg-purple-600 rounded-full flex items-center justify-center z-10 shadow-[0_0_30px_rgba(139,92,246,0.5)]">
<i data-lucide="music" class="text-white animate-bounce"></i>
</div>
</div>
<h2 id="display-peer-id" class="text-5xl font-black text-white mb-2 tracking-tighter">---</h2>
<p id="call-status" class="text-purple-400 font-bold text-xs uppercase tracking-[0.4em]">Streaming Raw Audio</p>
</div>
<div class="w-full p-10 pb-20 space-y-6">
<div class="flex justify-center gap-8 mb-4">
<button id="toggle-mic" class="w-20 h-20 rounded-full bg-white/10 backdrop-blur-2xl flex items-center justify-center border border-white/20 text-white active:bg-purple-600 transition-colors">
<i data-lucide="mic" size="28"></i>
</button>
</div>
<div class="flex gap-4">
<button id="accept-btn" class="flex-1 py-7 bg-emerald-500 text-white rounded-[2.5rem] hidden font-black text-xl shadow-2xl">TRẢ LỜI</button>
<button id="hangup-btn" class="flex-1 py-7 bg-red-500 text-white rounded-[2.5rem] font-black text-xl shadow-2xl">NGẮT KẾT NỐI</button>
</div>
</div>
</div>
<!-- Cài đặt -->
<div id="settings-panel" class="settings-panel p-8 flex flex-col">
<div class="flex items-center justify-between mb-8">
<button onclick="toggleSettings()" class="w-12 h-12 bg-slate-800 rounded-2xl flex items-center justify-center active:scale-90"><i data-lucide="arrow-left"></i></button>
<h2 class="text-2xl font-black uppercase tracking-tighter text-purple-400">Hi-Fi Setup</h2>
<div class="w-12"></div>
</div>
<div class="space-y-6">
<div class="bg-slate-900/50 p-6 rounded-[2rem] border border-white/5">
<label class="text-[10px] font-bold text-slate-500 uppercase mb-3 block tracking-widest">Phone ID</label>
<input type="number" id="setting-phone-input" class="w-full bg-slate-950 border border-purple-500/30 p-4 rounded-2xl text-white font-bold text-2xl outline-none text-center" placeholder="0000">
</div>
<div class="bg-slate-900/50 p-6 rounded-[2rem] border border-white/5">
<label class="text-[10px] font-bold text-slate-500 uppercase mb-3 block tracking-widest">Input Device</label>
<select id="mic-select" class="w-full bg-slate-950 border border-white/10 p-4 rounded-2xl text-white outline-none text-sm"></select>
</div>
<div class="p-6 border border-purple-500/20 rounded-[2rem] bg-purple-500/5">
<h4 class="text-purple-300 font-bold text-xs uppercase mb-2 tracking-widest">Studio Mode Active</h4>
<ul class="text-[10px] text-slate-400 space-y-1">
<li>• Sample Rate: 48,000Hz</li>
<li>• Channel: Stereo (Raw Capture)</li>
<li>• Bitrate: Uncapped (Dynamic Max)</li>
<li>• DSP Filters: Disabled</li>
</ul>
</div>
</div>
<button onclick="saveSettings()" class="w-full bg-purple-600 py-6 rounded-[2rem] font-black text-xl mt-10 shadow-2xl active:scale-95 transition-all">LƯU CẤU HÌNH</button>
</div>
<script>
lucide.createIcons();
let dataUsed = parseFloat(localStorage.getItem('voice_data_infinite') || 0);
let myPhone = localStorage.getItem('user_fixed_phone');
let selectedMic = localStorage.getItem('selected_mic') || '';
let peer, localStream, currentCall, wakeLock = null;
let isMuted = false;
async function loadDevices() {
try {
await navigator.mediaDevices.getUserMedia({ audio: true });
const devices = await navigator.mediaDevices.enumerateDevices();
const micSelect = document.getElementById('mic-select');
micSelect.innerHTML = '';
devices.forEach(device => {
if (device.kind === 'audioinput') {
const opt = document.createElement('option');
opt.value = device.deviceId;
opt.text = device.label || `Mic (${device.deviceId.slice(0,5)})`;
if (device.deviceId === selectedMic) opt.selected = true;
micSelect.appendChild(opt);
}
});
} catch (e) { console.error("Permission denied", e); }
}
async function getAudioStream() {
if (localStream) localStream.getTracks().forEach(t => t.stop());
// Thiết lập Audio "Lossless Raw"
localStream = await navigator.mediaDevices.getUserMedia({
audio: {
deviceId: selectedMic ? { exact: selectedMic } : undefined,
echoCancellation: false,
noiseSuppression: false,
autoGainControl: false,
// Ép chất lượng cao nhất cho WebRTC
sampleRate: 48000,
sampleSize: 16,
channelCount: 2 // Stereo if supported
},
video: false
});
return localStream;
}
if(!myPhone) { setTimeout(() => toggleSettings(), 500); } else { initPeer(myPhone); }
loadDevices();
function initPeer(phone) {
if(peer) peer.destroy();
peer = new Peer(phone, {
config: {
'iceServers': [{ urls: 'stun:stun.l.google.com:19302' }],
'sdpSemantics': 'unified-plan'
}
});
peer.on('open', id => {
document.getElementById('my-id-display').innerText = id;
});
peer.on('call', async call => {
if(currentCall) return;
currentCall = call;
showOverlay(call.peer, "Incoming Studio Stream...");
document.getElementById('accept-btn').style.display = 'block';
if ('wakeLock' in navigator) wakeLock = await navigator.wakeLock.request('screen');
});
}
document.getElementById('call-btn').onclick = async () => {
const id = document.getElementById('target-id-input').value;
if (id.length < 3) return;
showOverlay(id, "Initializing Hi-Fi...");
try {
localStream = await getAudioStream();
currentCall = peer.call(id, localStream);
setupCall(currentCall);
} catch (e) { alert("Mic Error!"); }
};
document.getElementById('accept-btn').onclick = async () => {
try {
localStream = await getAudioStream();
currentCall.answer(localStream);
setupCall(currentCall);
document.getElementById('accept-btn').style.display = 'none';
} catch (e) { alert("Mic Error!"); }
};
function setupCall(call) {
currentCall = call;
// Logic để mở khóa băng thông tối đa
const pc = call.peerConnection;
pc.onicecandidate = () => {
const senders = pc.getSenders();
senders.forEach(sender => {
if (sender.track.kind === 'audio') {
const parameters = sender.getParameters();
if (!parameters.encodings) parameters.encodings = [{}];
// Xóa bỏ giới hạn maxBitrate để WebRTC tự do truyền tải
delete parameters.encodings[0].maxBitrate;
sender.setParameters(parameters);
}
});
};
call.on('stream', stream => {
const remoteAudio = document.getElementById('remote-audio');
remoteAudio.srcObject = stream;
// Đảm bảo audio playback không bị xử lý lại
remoteAudio.play();
document.getElementById('call-status').innerText = "LIVE • FULL QUALITY";
document.getElementById('status-animation').classList.remove('hidden');
startUsage();
});
call.on('close', hangup);
call.on('error', hangup);
}
function hangup() {
if (currentCall) currentCall.close();
if (localStream) localStream.getTracks().forEach(t => t.stop());
currentCall = null;
document.getElementById('call-overlay').classList.add('translate-y-full');
document.getElementById('remote-audio').srcObject = null;
document.getElementById('status-animation').classList.add('hidden');
clearInterval(window.usageInt);
if (wakeLock) wakeLock.release().then(() => wakeLock = null);
}
document.getElementById('toggle-mic').onclick = () => {
isMuted = !isMuted;
if (localStream) {
localStream.getAudioTracks()[0].enabled = !isMuted;
document.getElementById('toggle-mic').classList.toggle('bg-red-500', isMuted);
document.getElementById('toggle-mic').innerHTML = isMuted ? '<i data-lucide="mic-off" size="28"></i>' : '<i data-lucide="mic" size="28"></i>';
lucide.createIcons();
}
};
const keys = ['1','2','3','4','5','6','7','8','9','X','0','D'];
keys.forEach(k => {
const b = document.createElement('button');
b.className = 'key-btn text-white text-3xl font-black shadow-lg';
b.innerHTML = k === 'X' ? '<i data-lucide="trash-2" class="text-red-400"></i>' : k === 'D' ? '<i data-lucide="delete"></i>' : k;
b.onclick = () => {
const input = document.getElementById('target-id-input');
if(k==='X') input.value = '';
else if(k==='D') input.value = input.value.slice(0,-1);
else if(input.value.length < 11) input.value += k;
lucide.createIcons();
};
document.getElementById('keypad').appendChild(b);
});
function showOverlay(id, status) {
document.getElementById('display-peer-id').innerText = id;
document.getElementById('call-status').innerText = status;
document.getElementById('call-overlay').classList.remove('translate-y-full');
}
function startUsage() {
if(window.usageInt) clearInterval(window.usageInt);
window.usageInt = setInterval(() => {
// Hi-Fi Audio Raw có thể ngốn từ 128kbps đến 510kbps (~1MB/phút)
dataUsed += (1.0 / 60);
localStorage.setItem('voice_data_infinite', dataUsed);
document.getElementById('data-used').innerText = dataUsed.toFixed(2) + " MB";
}, 1000);
}
function toggleSettings() {
document.getElementById('settings-panel').classList.toggle('open');
if(document.getElementById('settings-panel').classList.contains('open')) loadDevices();
}
function saveSettings() {
const v = document.getElementById('setting-phone-input').value;
const mic = document.getElementById('mic-select').value;
if(v.length < 3) return alert("ID too short!");
localStorage.setItem('user_fixed_phone', v);
localStorage.setItem('selected_mic', mic);
location.reload();
}
document.getElementById('hangup-btn').onclick = hangup;
</script>
</body>
</html>