Aa
This commit is contained in:
@@ -7,69 +7,99 @@ let currentStream = null;
|
|||||||
// 메인 프로세스로부터 명령 수신 (IPC)
|
// 메인 프로세스로부터 명령 수신 (IPC)
|
||||||
console.log('electronAPI:', window.electronAPI);
|
console.log('electronAPI:', window.electronAPI);
|
||||||
console.log('desktopCapturer:', window.desktopCapturer);
|
console.log('desktopCapturer:', window.desktopCapturer);
|
||||||
|
|
||||||
|
window.electronAPI.send('displays', { types: ['screen'] });
|
||||||
window.electronAPI.receive('start-webrtc', async (payload) => {
|
window.electronAPI.receive('start-webrtc', async (payload) => {
|
||||||
try {
|
try {
|
||||||
const { offer, displayId, signalingServer, employeeId } = payload;
|
const { offer, displayId, signalingServer, targetId } = payload;
|
||||||
console.log('start webrtc');
|
console.log('start webrtc');
|
||||||
// 1. Socket 연결 (렌더러에서도 가능)
|
// 1. Socket 연결 (렌더러에서도 가능)
|
||||||
socket = io(signalingServer);
|
socket = io(signalingServer);
|
||||||
|
socket.on('availableDisplays', async (sources) => {
|
||||||
|
|
||||||
|
const source = sources.find(s => {
|
||||||
|
console.log(s.id, displayId)
|
||||||
|
return s.id === displayId
|
||||||
|
});
|
||||||
|
if (!source) throw new Error('디스플레이 없음');
|
||||||
|
|
||||||
|
const stream = await navigator.mediaDevices.getUserMedia({
|
||||||
|
audio: false,
|
||||||
|
video: {
|
||||||
|
mandatory: {
|
||||||
|
chromeMediaSource: 'desktop',
|
||||||
|
chromeMediaSourceId: source.id,
|
||||||
|
minWidth: 1920,
|
||||||
|
minHeight: 1080
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
currentStream = stream;
|
||||||
|
|
||||||
|
|
||||||
|
// 3. WebRTC 연결
|
||||||
|
peerConnection = new RTCPeerConnection({
|
||||||
|
iceServers: [{urls: 'stun:stun.l.google.com:19302'}]
|
||||||
|
});
|
||||||
|
|
||||||
|
stream.getTracks().forEach(track => {
|
||||||
|
console.log('track', track, stream);
|
||||||
|
peerConnection.addTrack(track, stream)
|
||||||
|
});
|
||||||
|
await peerConnection.setRemoteDescription(new RTCSessionDescription(offer));
|
||||||
|
const answer = await peerConnection.createAnswer();
|
||||||
|
await peerConnection.setLocalDescription(answer);
|
||||||
|
|
||||||
|
// 4. Answer 전송
|
||||||
|
socket.emit('webrtcSignal', {
|
||||||
|
targetId: targetId,
|
||||||
|
type: 'answer',
|
||||||
|
answer
|
||||||
|
});
|
||||||
|
|
||||||
|
// 5. ICE candidate
|
||||||
|
peerConnection.onicecandidate = (e) => {
|
||||||
|
if (e.candidate) {
|
||||||
|
socket.emit('webrtcSignal', {
|
||||||
|
targetId: targetId,
|
||||||
|
type: 'icecandidate',
|
||||||
|
candidate: e.candidate
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 6. 입력 이벤트 수신 (robotjs는 메인 프로세스에서 실행 권장)
|
||||||
|
socket.on('inputEvent', (data) => {
|
||||||
|
window.electronApi.send('input-event-from-renderer', data);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
console.log('✅ WebRTC 시작됨 (렌더러)');
|
||||||
|
})
|
||||||
|
|
||||||
// 2. 화면 스트림 캡처
|
// 2. 화면 스트림 캡처
|
||||||
const sources = await window.desktopCapturer.getSources({ types: ['screen'] });
|
socket.emit('requestDisplays', 'psn14020')
|
||||||
const source = sources.find(s => s.id === displayId);
|
|
||||||
if (!source) throw new Error('디스플레이 없음');
|
|
||||||
|
|
||||||
const stream = await navigator.mediaDevices.getUserMedia({
|
// const sources = await window.desktopCapturer.getSources({ types: ['screen'] });
|
||||||
audio: false,
|
|
||||||
video: {
|
|
||||||
mandatory: {
|
|
||||||
chromeMediaSource: 'desktop',
|
|
||||||
chromeMediaSourceId: source.id,
|
|
||||||
minWidth: 1280,
|
|
||||||
minHeight: 720
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
currentStream = stream;
|
|
||||||
|
|
||||||
// 3. WebRTC 연결
|
|
||||||
peerConnection = new RTCPeerConnection({
|
|
||||||
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
|
|
||||||
});
|
|
||||||
|
|
||||||
stream.getTracks().forEach(track => peerConnection.addTrack(track, stream));
|
|
||||||
|
|
||||||
await peerConnection.setRemoteDescription(new RTCSessionDescription(offer));
|
|
||||||
const answer = await peerConnection.createAnswer();
|
|
||||||
await peerConnection.setLocalDescription(answer);
|
|
||||||
|
|
||||||
// 4. Answer 전송
|
|
||||||
socket.emit('webrtcSignal', {
|
|
||||||
targetId: employeeId,
|
|
||||||
answer
|
|
||||||
});
|
|
||||||
|
|
||||||
// 5. ICE candidate
|
|
||||||
peerConnection.onicecandidate = (e) => {
|
|
||||||
if (e.candidate) {
|
|
||||||
socket.emit('webrtcSignal', {
|
|
||||||
targetId: employeeId,
|
|
||||||
type: 'icecandidate', candidate: e.candidate
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 6. 입력 이벤트 수신 (robotjs는 메인 프로세스에서 실행 권장)
|
|
||||||
socket.on('inputEvent', (data) => {
|
|
||||||
window.electronApi.send('input-event-from-renderer', data);
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('✅ WebRTC 시작됨 (렌더러)');
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('❌ WebRTC 실패:', err);
|
console.error('❌ WebRTC 실패:', err);
|
||||||
window.electronApi.send('webrtc-error', err.message);
|
window.electronApi.send('webrtc-error', err.message);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
window.electronAPI.receive('webrtcSignal', async (data) => {
|
||||||
|
try {
|
||||||
|
console.log('webrtcSignal', data)
|
||||||
|
if (!peerConnection) return;
|
||||||
|
if (data.type === 'icecandidate' && data.candidate) {
|
||||||
|
await peerConnection.addIceCandidate(new RTCIceCandidate(data.candidate));
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('ICE candidate 처리 오류:', err);
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
// desktopCapturer 사용을 위해 preload 필요
|
// desktopCapturer 사용을 위해 preload 필요
|
||||||
// → 다음 단계에서 preload.js 설정
|
// → 다음 단계에서 preload.js 설정
|
||||||
96
main.js
96
main.js
@@ -14,6 +14,7 @@ let tray = null;
|
|||||||
let mainWindow = null;
|
let mainWindow = null;
|
||||||
let socket = null;
|
let socket = null;
|
||||||
let peerConnection = null;
|
let peerConnection = null;
|
||||||
|
let displayId = null;
|
||||||
|
|
||||||
// 자동 시작 설정
|
// 자동 시작 설정
|
||||||
app.setLoginItemSettings({
|
app.setLoginItemSettings({
|
||||||
@@ -53,6 +54,9 @@ function createTray() {
|
|||||||
async function sendAvailableDisplays() {
|
async function sendAvailableDisplays() {
|
||||||
try {
|
try {
|
||||||
const sources = await desktopCapturer.getSources({ types: ['screen'] });
|
const sources = await desktopCapturer.getSources({ types: ['screen'] });
|
||||||
|
const source = sources[0];
|
||||||
|
displayId = source.id
|
||||||
|
console.log('sources',sources)
|
||||||
const displays = sources.map(source => ({
|
const displays = sources.map(source => ({
|
||||||
id: source.id,
|
id: source.id,
|
||||||
name: source.name,
|
name: source.name,
|
||||||
@@ -65,76 +69,6 @@ async function sendAvailableDisplays() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🔁 WebRTC 연결 설정 (offer 수신 후 호출)
|
|
||||||
async function setupWebRTCConnection(offer, requestedDisplayId) {
|
|
||||||
try {
|
|
||||||
// 기존 연결 정리
|
|
||||||
if (peerConnection) {
|
|
||||||
peerConnection.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
peerConnection = new RTCPeerConnection({
|
|
||||||
iceServers: [
|
|
||||||
{ urls: 'stun:stun.l.google.com:19302' }
|
|
||||||
// 프로덕션: TURN 서버 추가 권장
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
// 🖼️ 화면 스트림 캡처
|
|
||||||
const sources = await desktopCapturer.getSources({ types: ['screen'] });
|
|
||||||
const source = sources.find(s => s.id === requestedDisplayId);
|
|
||||||
if (!source) throw new Error(`디스플레이 ${requestedDisplayId}를 찾을 수 없습니다.`);
|
|
||||||
|
|
||||||
const stream = await navigator.mediaDevices.getUserMedia({
|
|
||||||
audio: false,
|
|
||||||
video: {
|
|
||||||
mandatory: {
|
|
||||||
chromeMediaSource: 'desktop',
|
|
||||||
chromeMediaSourceId: source.id,
|
|
||||||
minWidth: 1280,
|
|
||||||
minHeight: 720,
|
|
||||||
maxWidth: 1920,
|
|
||||||
maxHeight: 1080
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 스트림을 PeerConnection에 추가
|
|
||||||
stream.getTracks().forEach(track => {
|
|
||||||
peerConnection.addTrack(track, stream);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Offer 설정
|
|
||||||
await peerConnection.setRemoteDescription(new RTCSessionDescription(offer));
|
|
||||||
|
|
||||||
// ✅ Answer SDP 생성
|
|
||||||
const answer = await peerConnection.createAnswer();
|
|
||||||
await peerConnection.setLocalDescription(answer);
|
|
||||||
|
|
||||||
// Answer 전송
|
|
||||||
socket.emit('webrtcSignal', {
|
|
||||||
targetId: EMPLOYEE_ID,
|
|
||||||
answer
|
|
||||||
});
|
|
||||||
|
|
||||||
// ICE candidate 전송
|
|
||||||
peerConnection.onicecandidate = (event) => {
|
|
||||||
if (event.candidate) {
|
|
||||||
socket.emit('webrtcSignal', {
|
|
||||||
targetId: EMPLOYEE_ID,
|
|
||||||
type: 'icecandidate',
|
|
||||||
candidate: event.candidate
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log('✅ WebRTC 연결 설정 완료. 화면 공유 시작됨.');
|
|
||||||
} catch (err) {
|
|
||||||
console.error('❌ WebRTC 설정 실패:', err);
|
|
||||||
dialog.showErrorBox('오류', '화면 공유 시작에 실패했습니다:\n' + err.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 🖱️ 입력 이벤트 처리 (robotjs 필요)
|
// 🖱️ 입력 이벤트 처리 (robotjs 필요)
|
||||||
function setupInputHandler() {
|
function setupInputHandler() {
|
||||||
// robotjs는 별도 설치 및 권한 필요
|
// robotjs는 별도 설치 및 권한 필요
|
||||||
@@ -175,11 +109,11 @@ function connectToSignaling() {
|
|||||||
sendAvailableDisplays(); // 연결 시 디스플레이 목록 전송
|
sendAvailableDisplays(); // 연결 시 디스플레이 목록 전송
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('controlRequest', async ({ offer, displayId }) => {
|
socket.on('controlRequest', async (data) => {
|
||||||
const result = await dialog.showMessageBox({
|
const result = await dialog.showMessageBox({
|
||||||
type: 'question',
|
type: 'question',
|
||||||
title: '원격 연결 요청',
|
title: '원격 연결 요청',
|
||||||
message: `관리자로부터 원격 제어 요청이 왔습니다.\n허용하시겠습니까?`,
|
message: `관리자로부터 원격 제어 요청이 왔습니다.\n허용하시겠습니까? ${displayId}`,
|
||||||
buttons: ['허용', '거부'],
|
buttons: ['허용', '거부'],
|
||||||
defaultId: 1,
|
defaultId: 1,
|
||||||
cancelId: 1
|
cancelId: 1
|
||||||
@@ -188,26 +122,19 @@ function connectToSignaling() {
|
|||||||
if (result.response === 0) {
|
if (result.response === 0) {
|
||||||
// ✅ 렌더러 프로세스에 WebRTC 시작 명령 전달
|
// ✅ 렌더러 프로세스에 WebRTC 시작 명령 전달
|
||||||
rendererWindow.webContents.send('start-webrtc', {
|
rendererWindow.webContents.send('start-webrtc', {
|
||||||
offer,
|
offer: data.offer,
|
||||||
displayId,
|
displayId,
|
||||||
signalingServer: SIGNALING_SERVER,
|
signalingServer: SIGNALING_SERVER,
|
||||||
employeeId: EMPLOYEE_ID
|
targetId: data.from
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
socket.emit('webrtcSignal', { targetId: EMPLOYEE_ID, data: { type: 'reject' } });
|
socket.emit('webrtcSignal', { targetId: data.from, data: { type: 'reject' } });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// WebRTC 신호 수신 (ICE candidate 등)
|
// WebRTC 신호 수신 (ICE candidate 등)
|
||||||
socket.on('webrtcSignal', async ({ data }) => {
|
socket.on('webrtcSignal', async (data) => {
|
||||||
if (!peerConnection) return;
|
rendererWindow.webContents.send('webrtcSignal', data);
|
||||||
try {
|
|
||||||
if (data.type === 'icecandidate' && data.candidate) {
|
|
||||||
await peerConnection.addIceCandidate(new RTCIceCandidate(data.candidate));
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('ICE candidate 처리 오류:', err);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('disconnect', () => {
|
socket.on('disconnect', () => {
|
||||||
@@ -256,3 +183,4 @@ ipcMain.on('input-event-from-renderer', (event, data) => {
|
|||||||
console.error('robotjs 오류:', e);
|
console.error('robotjs 오류:', e);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
// preload.js
|
// preload.js
|
||||||
const { contextBridge, ipcRenderer, desktopCapturer } = require('electron');
|
const { contextBridge, ipcRenderer } = require('electron');
|
||||||
|
const { desktopCapturer } = require('electron');
|
||||||
|
|
||||||
|
console.log('Preload script loaded'); // 이 로그가 안 찍히면 preload 미로드
|
||||||
|
|
||||||
|
|
||||||
contextBridge.exposeInMainWorld('agentAPI', {
|
contextBridge.exposeInMainWorld('agentAPI', {
|
||||||
getEmployeeId: () => ipcRenderer.invoke('get-employee-id')
|
getEmployeeId: () => ipcRenderer.invoke('get-employee-id')
|
||||||
|
|||||||
Reference in New Issue
Block a user