이전 글에 이어서 한 클라이언트가 producer를 하면 다른 유저는 그 내용을 consume을 하기 위해서 producer가 새로 생겼다는 내용을 socket을 통해서 전달을 한다. 그럼 produce를 하지 않은 다른 클라이언트는 'new-producer'라는 이벤트를 수신하게 되고 수신된 정보를 기반으로 새로운 recv consume을 만들어 달라고 서버에 요청을 한다.
마찬가지로 새로운 consumer를 만들기 위해서는 rtpCapabilities라는 정보가 필요하고 이 정보를 서버에 요청을 보내서 서버 쪽에서 먼저 consumer를 만들고 그다음에 다시 만들어진 정보를 기반으로 클라이언트 측에서도 consumer를 생성한다.
// server.js
socket.on(
'transport-send-produce',
() => {
// 그 외 동일
socket.emit('new-producer',producer.id, producer.appData);
})
새로운 producer가 생길 경우에 해당 producer의 아이디와 producer에 저장된 appData의 데이터를 다른 클라이언트에게 전달을 한다.
useEffect(() => {
//...
socket.on('new-producer',handleConsumeNewProducer);
//
},[]);
function handleConsumeNewProducer(producerId: string, appData: AppData) {
// 이미 존재하는 경우 넘어감
if (isAlreadyConsume(consumers, producerId)) return;
try {
const rtpCapabilities = getRtpCapabilitiesFromDevice();
socket.emit(
"transport-recv-consume",
{ rtpCapabilities, producerId, appData, playerId: currentPlayerId },
handleCreatedConsumer);
// handle created consumer는 콜백함수이다. 핑퐁이 많기 때문에 의도적으로 함수로 분리를 했다.
}
'new-producer'이벤트를 수신받은 클라이언트는 producerId와 appData를 가지고 이미 consume하고 있는지를 확인을 하고 이미 consume하고 있지 않다면 서버 측에 consumer를 만들어 달라고 요청을 한다. 이때 rtp capabilities의 정보를 넘겨 준다.
//server.js
socket.on(
"transport-recv-consume",
async ({ rtpCapabilities, producerId, appData, playerId }, callback) => {
const client = clients[playerId];
try {
const recvTransport = client[RECV_TRANSPORT_KEY];
if (!isCanConsumeWithRouter(producerId, rtpCapabilities)) {
console.error("can not consume");
return;
}
const consumer = await recvTransport.consume({
producerId,
rtpCapabilities,
paused: true,
appData,
});
consumer.on("transportclose", () => {
recvTransport.close();
});
consumer.on("producerclose", () => {
client.consumers = client.consumers.filter(
(c) => c.id !== consumer.id
);
consumer.close();
});
const params = {
id: consumer.id,
producerId,
kind: consumer.kind,
rtpParameters: consumer.rtpParameters,
};
client.consumers = [...client.consumers, consumer];
callback(params);
} catch (error) {
console.error("while consume recv transport error:", error);
}
}
);
서버 측에서 정상적으로 consumer를 만든 이후에는 해당 내용을 다시 클라이언트에게도 보내서 클라이언트에서도 매칭될 수 있는 consumer를 만들 수 있도록 한다.
function handleCreatedConsumer(
data : {
id: string;
producerId: string;
kind: "audio" : "video";
rtpParamters:RtpParameters;
}
) {
const { id, producerId, kind, rtpParameters } = data;
const consumer = await recvTransport.current?.consume({
id,
producerId,
kind,
rtpParameters,
appData,
});
consumer?.on("@close", () => {
console.log("basic close consumer");
});
consumer?.observer.on("close", () => {
console.log("close consumer");
});
if (!consumer) {
throw new Error("consumer가 없다...있어야 하는데...");
}
addConsumer(consumer);
socket.emit("consumer-resume", {
consumerId: consumer.id,
playerId: currentPlayerId,
});
}
클라이언트 측에서도 consumer를 만든 이후에 다시 소켓에 consumer-resume이라는 이벤트를 보내는데 해당 이벤트는 처음에 서버에서 만들 때 클라이언트에서 받고 난 이후에 정상적으로 consume을 하기 위해서 있는 코드로 서버 측 코드는 다음과 같다
socket.on('consumer-resume', ({consumerId, playerId}) => {
try {
const client = clients[playerId];
const consumer = client.consumers.find((consumer) => consumer.id === consumerId);
consumer.resume();
} catch(error) {
console.log(error);
}
});
이렇게까지 모두 통신이 되면 정상적으로 연결이 되어 상대방이 공유한 화면을 볼 수 있다.
처음에는 엄청 난잡했는데 많이 코드를 보면서 어느정도 익숙해진 느낌이 크다. 그리고 스스로 최대한 직관적으로 리팩토링을 했다고 생각했는데 아직 더 할 요소들이 많은 것 같아서 반성하고 일단 기능 구현을 우선시 하고 코드를 계속해서 재정비 해나가야겠다.