444 lines
12 KiB
JavaScript
444 lines
12 KiB
JavaScript
'use strict';
|
|
|
|
const heygen_API = {
|
|
apiKey: 'Nzc0ODg1OTQ5ODU1NDRhNDg5OWVjMzc3MGIxNDVhNzItMTc0MzYxNDU5NA==',
|
|
serverUrl: 'https://api.heygen.com',
|
|
};
|
|
|
|
const statusElement = document.querySelector('#status');
|
|
const apiKey = heygen_API.apiKey;
|
|
const SERVER_URL = heygen_API.serverUrl;
|
|
|
|
if (apiKey === 'YourApiKey' || SERVER_URL === '') {
|
|
alert('Please enter your API key and server URL in the api.json file');
|
|
}
|
|
|
|
let sessionInfo = null;
|
|
let peerConnection = null;
|
|
|
|
function updateStatus(statusElement, message) {
|
|
statusElement.innerHTML += message + '<br>';
|
|
statusElement.scrollTop = statusElement.scrollHeight;
|
|
}
|
|
|
|
updateStatus(statusElement, 'Please click the new button to create the stream first.');
|
|
|
|
function onMessage(event) {
|
|
const message = event.data;
|
|
console.log('Received message:', message);
|
|
}
|
|
|
|
// Create a new WebRTC session when clicking the "New" button
|
|
async function createNewSession() {
|
|
updateStatus(statusElement, 'Creating new session... please wait');
|
|
|
|
const avatar = avatarID.value;
|
|
const voice = voiceID.value;
|
|
|
|
// call the new interface to get the server's offer SDP and ICE server to create a new RTCPeerConnection
|
|
sessionInfo = await newSession('low', avatar, voice);
|
|
const { sdp: serverSdp, ice_servers2: iceServers } = sessionInfo;
|
|
|
|
// Create a new RTCPeerConnection
|
|
peerConnection = new RTCPeerConnection({ iceServers: iceServers });
|
|
|
|
// When audio and video streams are received, display them in the video element
|
|
peerConnection.ontrack = (event) => {
|
|
console.log('Received the track');
|
|
if (event.track.kind === 'audio' || event.track.kind === 'video') {
|
|
mediaElement.srcObject = event.streams[0];
|
|
}
|
|
};
|
|
|
|
// When receiving a message, display it in the status element
|
|
peerConnection.ondatachannel = (event) => {
|
|
const dataChannel = event.channel;
|
|
dataChannel.onmessage = onMessage;
|
|
};
|
|
|
|
// Set server's SDP as remote description
|
|
const remoteDescription = new RTCSessionDescription(serverSdp);
|
|
await peerConnection.setRemoteDescription(remoteDescription);
|
|
|
|
updateStatus(statusElement, 'Session creation completed');
|
|
updateStatus(statusElement, 'Now.You can click the start button to start the stream');
|
|
}
|
|
|
|
// Start session and display audio and video when clicking the "Start" button
|
|
async function startAndDisplaySession() {
|
|
if (!sessionInfo) {
|
|
updateStatus(statusElement, 'Please create a connection first');
|
|
return;
|
|
}
|
|
|
|
updateStatus(statusElement, 'Starting session... please wait');
|
|
|
|
// Create and set local SDP description
|
|
const localDescription = await peerConnection.createAnswer();
|
|
await peerConnection.setLocalDescription(localDescription);
|
|
|
|
// When ICE candidate is available, send to the server
|
|
peerConnection.onicecandidate = ({ candidate }) => {
|
|
console.log('Received ICE candidate:', candidate);
|
|
if (candidate) {
|
|
handleICE(sessionInfo.session_id, candidate.toJSON());
|
|
}
|
|
};
|
|
|
|
// When ICE connection state changes, display the new state
|
|
peerConnection.oniceconnectionstatechange = (event) => {
|
|
updateStatus(
|
|
statusElement,
|
|
`ICE connection state changed to: ${peerConnection.iceConnectionState}`,
|
|
);
|
|
};
|
|
|
|
|
|
|
|
// Start session
|
|
await startSession(sessionInfo.session_id, localDescription);
|
|
|
|
var receivers = peerConnection.getReceivers();
|
|
|
|
receivers.forEach((receiver) => {
|
|
receiver.jitterBufferTarget = 500
|
|
});
|
|
|
|
updateStatus(statusElement, 'Session started successfully');
|
|
}
|
|
|
|
const taskInput = document.querySelector('#taskInput');
|
|
|
|
// When clicking the "Send Task" button, get the content from the input field, then send the tas
|
|
async function repeatHandler() {
|
|
if (!sessionInfo) {
|
|
updateStatus(statusElement, 'Please create a connection first');
|
|
|
|
return;
|
|
}
|
|
updateStatus(statusElement, 'Sending task... please wait');
|
|
const text = taskInput.value;
|
|
if (text.trim() === '') {
|
|
alert('Please enter a task');
|
|
return;
|
|
}
|
|
|
|
const resp = await repeat(sessionInfo.session_id, text);
|
|
|
|
updateStatus(statusElement, 'Task sent successfully');
|
|
}
|
|
|
|
async function talkHandler() {
|
|
if (!sessionInfo) {
|
|
updateStatus(statusElement, 'Please create a connection first');
|
|
return;
|
|
}
|
|
const prompt = taskInput.value; // Using the same input for simplicity
|
|
if (prompt.trim() === '') {
|
|
alert('Please enter a prompt for the LLM');
|
|
return;
|
|
}
|
|
|
|
updateStatus(statusElement, 'Talking to LLM... please wait');
|
|
|
|
try {
|
|
const text = await talkToOpenAI(prompt)
|
|
|
|
if (text) {
|
|
// Send the AI's response to Heygen's streaming.task API
|
|
const resp = await repeat(sessionInfo.session_id, text);
|
|
updateStatus(statusElement, 'LLM response sent successfully');
|
|
} else {
|
|
updateStatus(statusElement, 'Failed to get a response from AI');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error talking to AI:', error);
|
|
updateStatus(statusElement, 'Error talking to AI');
|
|
}
|
|
}
|
|
|
|
|
|
// when clicking the "Close" button, close the connection
|
|
async function closeConnectionHandler() {
|
|
if (!sessionInfo) {
|
|
updateStatus(statusElement, 'Please create a connection first');
|
|
return;
|
|
}
|
|
|
|
renderID++;
|
|
hideElement(canvasElement);
|
|
hideElement(bgCheckboxWrap);
|
|
mediaCanPlay = false;
|
|
|
|
updateStatus(statusElement, 'Closing connection... please wait');
|
|
try {
|
|
// Close local connection
|
|
peerConnection.close();
|
|
// Call the close interface
|
|
const resp = await stopSession(sessionInfo.session_id);
|
|
|
|
console.log(resp);
|
|
} catch (err) {
|
|
console.error('Failed to close the connection:', err);
|
|
}
|
|
updateStatus(statusElement, 'Connection closed successfully');
|
|
}
|
|
|
|
document.querySelector('#newBtn').addEventListener('click', createNewSession);
|
|
document.querySelector('#startBtn').addEventListener('click', startAndDisplaySession);
|
|
document.querySelector('#repeatBtn').addEventListener('click', repeatHandler);
|
|
document.querySelector('#closeBtn').addEventListener('click', closeConnectionHandler);
|
|
document.querySelector('#talkBtn').addEventListener('click', talkHandler);
|
|
|
|
|
|
// new session
|
|
async function newSession(quality, avatar_name, voice_id) {
|
|
const response = await fetch(`${SERVER_URL}/v1/streaming.new`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-Api-Key': apiKey,
|
|
},
|
|
body: JSON.stringify({
|
|
quality,
|
|
avatar_name,
|
|
voice: {
|
|
voice_id: voice_id,
|
|
},
|
|
}),
|
|
});
|
|
if (response.status === 500) {
|
|
console.error('Server error');
|
|
updateStatus(
|
|
statusElement,
|
|
'Server Error. Please ask the staff if the service has been turned on',
|
|
);
|
|
|
|
throw new Error('Server error');
|
|
} else {
|
|
const data = await response.json();
|
|
console.log(data.data);
|
|
return data.data;
|
|
}
|
|
}
|
|
|
|
// start the session
|
|
async function startSession(session_id, sdp) {
|
|
const response = await fetch(`${SERVER_URL}/v1/streaming.start`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-Api-Key': apiKey,
|
|
},
|
|
body: JSON.stringify({ session_id, sdp }),
|
|
});
|
|
if (response.status === 500) {
|
|
console.error('Server error');
|
|
updateStatus(
|
|
statusElement,
|
|
'Server Error. Please ask the staff if the service has been turned on',
|
|
);
|
|
throw new Error('Server error');
|
|
} else {
|
|
const data = await response.json();
|
|
return data.data;
|
|
}
|
|
}
|
|
|
|
// submit the ICE candidate
|
|
async function handleICE(session_id, candidate) {
|
|
const response = await fetch(`${SERVER_URL}/v1/streaming.ice`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-Api-Key': apiKey,
|
|
},
|
|
body: JSON.stringify({ session_id, candidate }),
|
|
});
|
|
if (response.status === 500) {
|
|
console.error('Server error');
|
|
updateStatus(
|
|
statusElement,
|
|
'Server Error. Please ask the staff if the service has been turned on',
|
|
);
|
|
throw new Error('Server error');
|
|
} else {
|
|
const data = await response.json();
|
|
return data;
|
|
}
|
|
}
|
|
|
|
async function talkToOpenAI(prompt) {
|
|
const response = await fetch(`http://localhost:3000/openai/complete`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({ prompt }),
|
|
});
|
|
if (response.status === 500) {
|
|
console.error('Server error');
|
|
updateStatus(
|
|
statusElement,
|
|
'Server Error. Please make sure to set the openai api key',
|
|
);
|
|
throw new Error('Server error');
|
|
} else {
|
|
const data = await response.json();
|
|
return data.text;
|
|
}
|
|
}
|
|
|
|
// repeat the text
|
|
async function repeat(session_id, text) {
|
|
const response = await fetch(`${SERVER_URL}/v1/streaming.task`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-Api-Key': apiKey,
|
|
},
|
|
body: JSON.stringify({ session_id, text }),
|
|
});
|
|
if (response.status === 500) {
|
|
console.error('Server error');
|
|
updateStatus(
|
|
statusElement,
|
|
'Server Error. Please ask the staff if the service has been turned on',
|
|
);
|
|
throw new Error('Server error');
|
|
} else {
|
|
const data = await response.json();
|
|
return data.data;
|
|
}
|
|
}
|
|
|
|
// stop session
|
|
async function stopSession(session_id) {
|
|
const response = await fetch(`${SERVER_URL}/v1/streaming.stop`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-Api-Key': apiKey,
|
|
},
|
|
body: JSON.stringify({ session_id }),
|
|
});
|
|
if (response.status === 500) {
|
|
console.error('Server error');
|
|
updateStatus(statusElement, 'Server Error. Please ask the staff for help');
|
|
throw new Error('Server error');
|
|
} else {
|
|
const data = await response.json();
|
|
return data.data;
|
|
}
|
|
}
|
|
|
|
const removeBGCheckbox = document.querySelector('#removeBGCheckbox');
|
|
removeBGCheckbox.addEventListener('click', () => {
|
|
const isChecked = removeBGCheckbox.checked; // status after click
|
|
|
|
if (isChecked && !sessionInfo) {
|
|
updateStatus(statusElement, 'Please create a connection first');
|
|
removeBGCheckbox.checked = false;
|
|
return;
|
|
}
|
|
|
|
if (isChecked && !mediaCanPlay) {
|
|
updateStatus(statusElement, 'Please wait for the video to load');
|
|
removeBGCheckbox.checked = false;
|
|
return;
|
|
}
|
|
|
|
if (isChecked) {
|
|
hideElement(mediaElement);
|
|
showElement(canvasElement);
|
|
|
|
renderCanvas();
|
|
} else {
|
|
hideElement(canvasElement);
|
|
showElement(mediaElement);
|
|
|
|
renderID++;
|
|
}
|
|
});
|
|
|
|
let renderID = 0;
|
|
function renderCanvas() {
|
|
if (!removeBGCheckbox.checked) return;
|
|
hideElement(mediaElement);
|
|
showElement(canvasElement);
|
|
|
|
canvasElement.classList.add('show');
|
|
|
|
const curRenderID = Math.trunc(Math.random() * 1000000000);
|
|
renderID = curRenderID;
|
|
|
|
const ctx = canvasElement.getContext('2d', { willReadFrequently: true });
|
|
|
|
if (bgInput.value) {
|
|
canvasElement.parentElement.style.background = bgInput.value?.trim();
|
|
}
|
|
|
|
function processFrame() {
|
|
if (!removeBGCheckbox.checked) return;
|
|
if (curRenderID !== renderID) return;
|
|
|
|
canvasElement.width = mediaElement.videoWidth;
|
|
canvasElement.height = mediaElement.videoHeight;
|
|
|
|
ctx.drawImage(mediaElement, 0, 0, canvasElement.width, canvasElement.height);
|
|
ctx.getContextAttributes().willReadFrequently = true;
|
|
const imageData = ctx.getImageData(0, 0, canvasElement.width, canvasElement.height);
|
|
const data = imageData.data;
|
|
|
|
for (let i = 0; i < data.length; i += 4) {
|
|
const red = data[i];
|
|
const green = data[i + 1];
|
|
const blue = data[i + 2];
|
|
|
|
// You can implement your own logic here
|
|
if (isCloseToGreen([red, green, blue])) {
|
|
// if (isCloseToGray([red, green, blue])) {
|
|
data[i + 3] = 0;
|
|
}
|
|
}
|
|
|
|
ctx.putImageData(imageData, 0, 0);
|
|
|
|
requestAnimationFrame(processFrame);
|
|
}
|
|
|
|
processFrame();
|
|
}
|
|
|
|
function isCloseToGreen(color) {
|
|
const [red, green, blue] = color;
|
|
return green > 90 && red < 90 && blue < 90;
|
|
}
|
|
|
|
function hideElement(element) {
|
|
element.classList.add('hide');
|
|
element.classList.remove('show');
|
|
}
|
|
function showElement(element) {
|
|
element.classList.add('show');
|
|
element.classList.remove('hide');
|
|
}
|
|
|
|
const mediaElement = document.querySelector('#mediaElement');
|
|
let mediaCanPlay = false;
|
|
mediaElement.onloadedmetadata = () => {
|
|
mediaCanPlay = true;
|
|
mediaElement.play();
|
|
|
|
showElement(bgCheckboxWrap);
|
|
};
|
|
const canvasElement = document.querySelector('#canvasElement');
|
|
|
|
const bgCheckboxWrap = document.querySelector('#bgCheckboxWrap');
|
|
const bgInput = document.querySelector('#bgInput');
|
|
bgInput.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Enter') {
|
|
renderCanvas();
|
|
}
|
|
});
|