diff --git a/src/backend/signaling_server/src/main/java/com/jootalkpia/signaling_server/SignalingServerApplication.java b/src/backend/signaling_server/src/main/java/com/jootalkpia/signaling_server/SignalingServerApplication.java index a9218e9c..f81a1600 100644 --- a/src/backend/signaling_server/src/main/java/com/jootalkpia/signaling_server/SignalingServerApplication.java +++ b/src/backend/signaling_server/src/main/java/com/jootalkpia/signaling_server/SignalingServerApplication.java @@ -3,7 +3,7 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -@SpringBootApplication +@SpringBootApplication(scanBasePackages = "com.jootalkpia.signaling_server") public class SignalingServerApplication { public static void main(String[] args) { diff --git a/src/backend/signaling_server/src/main/java/com/jootalkpia/signaling_server/config/RedisConfig.java b/src/backend/signaling_server/src/main/java/com/jootalkpia/signaling_server/config/RedisConfig.java index 3c3d6348..c1d9ca84 100644 --- a/src/backend/signaling_server/src/main/java/com/jootalkpia/signaling_server/config/RedisConfig.java +++ b/src/backend/signaling_server/src/main/java/com/jootalkpia/signaling_server/config/RedisConfig.java @@ -1,57 +1,57 @@ -package com.jootalkpia.signaling_server.config; - -import com.jootalkpia.signaling_server.model.Huddle; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.data.redis.connection.RedisConnectionFactory; -import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; -import org.springframework.data.redis.connection.RedisStandaloneConfiguration; -import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.data.redis.core.StringRedisTemplate; -import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; -import org.springframework.data.redis.serializer.StringRedisSerializer; - -@Configuration -public class RedisConfig { - - @Value("${spring.data.redis.host}") - private String redisHost; - - @Value("${spring.data.redis.port}") - private int redisPort; - - @Bean - public RedisConnectionFactory redisConnectionFactory() { - RedisStandaloneConfiguration config = new RedisStandaloneConfiguration(); - config.setHostName(redisHost); - config.setPort(redisPort); - - return new LettuceConnectionFactory(config); - } - - @Bean - public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) { - RedisTemplate template = new RedisTemplate<>(); - template.setConnectionFactory(redisConnectionFactory); - template.setKeySerializer(new StringRedisSerializer()); - template.setValueSerializer(new Jackson2JsonRedisSerializer<>(Huddle.class)); - template.afterPropertiesSet(); - return template; - } - - @Bean - public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) { - return new StringRedisTemplate(redisConnectionFactory); - } - - @Bean - public RedisTemplate longRedisTemplate(RedisConnectionFactory redisConnectionFactory) { - RedisTemplate template = new RedisTemplate<>(); - template.setConnectionFactory(redisConnectionFactory); - template.setKeySerializer(new StringRedisSerializer()); - template.setValueSerializer(new Jackson2JsonRedisSerializer<>(Long.class)); - template.afterPropertiesSet(); - return template; - } -} +//package com.jootalkpia.signaling_server.config; +// +//import com.jootalkpia.signaling_server.model.Huddle; +//import org.springframework.beans.factory.annotation.Value; +//import org.springframework.context.annotation.Bean; +//import org.springframework.context.annotation.Configuration; +//import org.springframework.data.redis.connection.RedisConnectionFactory; +//import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +//import org.springframework.data.redis.connection.RedisStandaloneConfiguration; +//import org.springframework.data.redis.core.RedisTemplate; +//import org.springframework.data.redis.core.StringRedisTemplate; +//import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; +//import org.springframework.data.redis.serializer.StringRedisSerializer; +// +//@Configuration +//public class RedisConfig { +// +// @Value("${spring.data.redis.host}") +// private String redisHost; +// +// @Value("${spring.data.redis.port}") +// private int redisPort; +// +// @Bean +// public RedisConnectionFactory redisConnectionFactory() { +// RedisStandaloneConfiguration config = new RedisStandaloneConfiguration(); +// config.setHostName(redisHost); +// config.setPort(redisPort); +// +// return new LettuceConnectionFactory(config); +// } +// +// @Bean +// public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) { +// RedisTemplate template = new RedisTemplate<>(); +// template.setConnectionFactory(redisConnectionFactory); +// template.setKeySerializer(new StringRedisSerializer()); +// template.setValueSerializer(new Jackson2JsonRedisSerializer<>(Huddle.class)); +// template.afterPropertiesSet(); +// return template; +// } +// +// @Bean +// public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) { +// return new StringRedisTemplate(redisConnectionFactory); +// } +// +// @Bean +// public RedisTemplate longRedisTemplate(RedisConnectionFactory redisConnectionFactory) { +// RedisTemplate template = new RedisTemplate<>(); +// template.setConnectionFactory(redisConnectionFactory); +// template.setKeySerializer(new StringRedisSerializer()); +// template.setValueSerializer(new Jackson2JsonRedisSerializer<>(Long.class)); +// template.afterPropertiesSet(); +// return template; +// } +//} diff --git a/src/backend/signaling_server/src/main/java/com/jootalkpia/signaling_server/config/WebRtcConfig.java b/src/backend/signaling_server/src/main/java/com/jootalkpia/signaling_server/config/WebRtcConfig.java index 0c5426ae..ad257075 100644 --- a/src/backend/signaling_server/src/main/java/com/jootalkpia/signaling_server/config/WebRtcConfig.java +++ b/src/backend/signaling_server/src/main/java/com/jootalkpia/signaling_server/config/WebRtcConfig.java @@ -1,26 +1,26 @@ -package com.jootalkpia.signaling_server.config; - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.kurento.client.KurentoClient; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.web.socket.config.annotation.EnableWebSocket; -import org.springframework.web.socket.config.annotation.WebSocketConfigurer; -import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; -import com.jootalkpia.signaling_server.rtc.KurentoHandler; - -@Configuration -@EnableWebSocket -@RequiredArgsConstructor -@Slf4j -public class WebRtcConfig implements WebSocketConfigurer { - - private final KurentoHandler kurentoHandler; - - @Override - public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { - registry.addHandler(kurentoHandler, "/signal").setAllowedOrigins("*"); - } -} +//package com.jootalkpia.signaling_server.config; +// +//import lombok.RequiredArgsConstructor; +//import lombok.extern.slf4j.Slf4j; +//import org.kurento.client.KurentoClient; +//import org.springframework.beans.factory.annotation.Value; +//import org.springframework.context.annotation.Bean; +//import org.springframework.context.annotation.Configuration; +//import org.springframework.web.socket.config.annotation.EnableWebSocket; +//import org.springframework.web.socket.config.annotation.WebSocketConfigurer; +//import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; +//import com.jootalkpia.signaling_server.rtc.KurentoHandler; +// +//@Configuration +//@EnableWebSocket +//@RequiredArgsConstructor +//@Slf4j +//public class WebRtcConfig implements WebSocketConfigurer { +// +// private final KurentoHandler kurentoHandler; +// +// @Override +// public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { +// registry.addHandler(kurentoHandler, "/signal").setAllowedOrigins("*"); +// } +//} diff --git a/src/backend/signaling_server/src/main/java/com/jootalkpia/signaling_server/config/WebSocketConfig.java b/src/backend/signaling_server/src/main/java/com/jootalkpia/signaling_server/config/WebSocketConfig.java index a8ae353c..2d969dd2 100644 --- a/src/backend/signaling_server/src/main/java/com/jootalkpia/signaling_server/config/WebSocketConfig.java +++ b/src/backend/signaling_server/src/main/java/com/jootalkpia/signaling_server/config/WebSocketConfig.java @@ -12,7 +12,8 @@ public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { @Override public void configureMessageBroker(MessageBrokerRegistry config) { - config.enableSimpleBroker("/topic"); // 클라이언트가 구독하는 메시지 브로커 경로 + config.enableSimpleBroker("/topic", "/queue"); // /topic: 브로드캐스트, /queue: 1:1 메시지 + config.setUserDestinationPrefix("/user"); config.setApplicationDestinationPrefixes("/app"); // 클라이언트에서 메시지를 보낼 때 사용 } diff --git a/src/backend/signaling_server/src/main/java/com/jootalkpia/signaling_server/model/Huddle.java b/src/backend/signaling_server/src/main/java/com/jootalkpia/signaling_server/model/Huddle.java index 586e7a56..8db5037d 100644 --- a/src/backend/signaling_server/src/main/java/com/jootalkpia/signaling_server/model/Huddle.java +++ b/src/backend/signaling_server/src/main/java/com/jootalkpia/signaling_server/model/Huddle.java @@ -1,14 +1,14 @@ -package com.jootalkpia.signaling_server.model; - -import java.time.LocalDateTime; - -public record Huddle( - String huddleId, - Long channelId, - Long createdByUserId, - LocalDateTime createdAt -) { - public Huddle(String huddleId, Long channelId, Long createdByUserId) { - this(huddleId, channelId, createdByUserId, LocalDateTime.now()); - } -} +//package com.jootalkpia.signaling_server.model; +// +//import java.time.LocalDateTime; +// +//public record Huddle( +// String huddleId, +// Long channelId, +// Long createdByUserId, +// LocalDateTime createdAt +//) { +// public Huddle(String huddleId, Long channelId, Long createdByUserId) { +// this(huddleId, channelId, createdByUserId, LocalDateTime.now()); +// } +//} diff --git a/src/backend/signaling_server/src/main/java/com/jootalkpia/signaling_server/model/KurentoUserSession.java b/src/backend/signaling_server/src/main/java/com/jootalkpia/signaling_server/model/KurentoUserSession.java index ed202da5..15de3653 100644 --- a/src/backend/signaling_server/src/main/java/com/jootalkpia/signaling_server/model/KurentoUserSession.java +++ b/src/backend/signaling_server/src/main/java/com/jootalkpia/signaling_server/model/KurentoUserSession.java @@ -1,20 +1,20 @@ -package com.jootalkpia.signaling_server.model; - -import lombok.Getter; -import lombok.RequiredArgsConstructor; -import org.kurento.client.IceCandidate; -import org.kurento.client.WebRtcEndpoint; -import org.springframework.web.socket.WebSocketSession; - -@RequiredArgsConstructor -@Getter -public class KurentoUserSession { - private final Long userId; - private final String huddleId; - private final WebSocketSession session; - private final WebRtcEndpoint webRtcEndpoint; - - public void addIceCandidate(IceCandidate candidate) { - webRtcEndpoint.addIceCandidate(candidate); - } -} +//package com.jootalkpia.signaling_server.model; +// +//import lombok.Getter; +//import lombok.RequiredArgsConstructor; +//import org.kurento.client.IceCandidate; +//import org.kurento.client.WebRtcEndpoint; +//import org.springframework.web.socket.WebSocketSession; +// +//@RequiredArgsConstructor +//@Getter +//public class KurentoUserSession { +// private final Long userId; +// private final String huddleId; +// private final WebSocketSession session; +// private final WebRtcEndpoint webRtcEndpoint; +// +// public void addIceCandidate(IceCandidate candidate) { +// webRtcEndpoint.addIceCandidate(candidate); +// } +//} diff --git a/src/backend/signaling_server/src/main/java/com/jootalkpia/signaling_server/repository/ChannelHuddleRepository.java b/src/backend/signaling_server/src/main/java/com/jootalkpia/signaling_server/repository/ChannelHuddleRepository.java index d3e0f83a..b21ad9da 100644 --- a/src/backend/signaling_server/src/main/java/com/jootalkpia/signaling_server/repository/ChannelHuddleRepository.java +++ b/src/backend/signaling_server/src/main/java/com/jootalkpia/signaling_server/repository/ChannelHuddleRepository.java @@ -1,29 +1,29 @@ -package com.jootalkpia.signaling_server.repository; - -import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.stereotype.Repository; -import org.springframework.data.redis.core.ValueOperations; - -@Repository -public class ChannelHuddleRepository { - private final RedisTemplate redisTemplate; - private final ValueOperations valueOps; - - public ChannelHuddleRepository(RedisTemplate redisTemplate) { - this.redisTemplate = redisTemplate; - this.valueOps = redisTemplate.opsForValue(); - } - - public void saveChannelHuddle(Long channelId, String huddleId) { - valueOps.set("channel:" + channelId, huddleId); - } - - public String getHuddleByChannel(Long channelId) { - return valueOps.get("channel:" + channelId); - } - - public void deleteChannelHuddle(Long channelId) { - redisTemplate.delete("channel:" + channelId); - } -} - +//package com.jootalkpia.signaling_server.repository; +// +//import org.springframework.data.redis.core.RedisTemplate; +//import org.springframework.stereotype.Repository; +//import org.springframework.data.redis.core.ValueOperations; +// +//@Repository +//public class ChannelHuddleRepository { +// private final RedisTemplate redisTemplate; +// private final ValueOperations valueOps; +// +// public ChannelHuddleRepository(RedisTemplate redisTemplate) { +// this.redisTemplate = redisTemplate; +// this.valueOps = redisTemplate.opsForValue(); +// } +// +// public void saveChannelHuddle(Long channelId, String huddleId) { +// valueOps.set("channel:" + channelId, huddleId); +// } +// +// public String getHuddleByChannel(Long channelId) { +// return valueOps.get("channel:" + channelId); +// } +// +// public void deleteChannelHuddle(Long channelId) { +// redisTemplate.delete("channel:" + channelId); +// } +//} +// diff --git a/src/backend/signaling_server/src/main/java/com/jootalkpia/signaling_server/repository/HuddleCacheRepository.java b/src/backend/signaling_server/src/main/java/com/jootalkpia/signaling_server/repository/HuddleCacheRepository.java index ae5c5bd7..41c17001 100644 --- a/src/backend/signaling_server/src/main/java/com/jootalkpia/signaling_server/repository/HuddleCacheRepository.java +++ b/src/backend/signaling_server/src/main/java/com/jootalkpia/signaling_server/repository/HuddleCacheRepository.java @@ -1,42 +1,42 @@ -package com.jootalkpia.signaling_server.repository; - -import com.jootalkpia.signaling_server.model.Huddle; -import org.springframework.data.redis.core.HashOperations; -import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.stereotype.Repository; -import java.util.Map; - -@Repository -public class HuddleCacheRepository { - private final HashOperations hashOps; - - public HuddleCacheRepository(RedisTemplate redisTemplate) { - this.hashOps = redisTemplate.opsForHash(); - } - - public void saveHuddle(Huddle huddle) { - String key = "huddle:" + huddle.huddleId(); - hashOps.put(key, "huddleId", huddle.huddleId()); - hashOps.put(key, "channelId", huddle.channelId().toString()); - hashOps.put(key, "createdByUserId", huddle.createdByUserId().toString()); - hashOps.put(key, "createdAt", huddle.createdAt().toString()); - } - - public Huddle getHuddleById(String huddleId) { - String key = "huddle:" + huddleId; - Map huddleData = hashOps.entries(key); - if (huddleData.isEmpty()) { - return null; - } - - return new Huddle( - huddleData.get("huddleId"), - Long.parseLong(huddleData.get("channelId")), - Long.parseLong(huddleData.get("createdByUserId")) - ); - } - - public void deleteHuddle(String huddleId) { - hashOps.getOperations().delete("huddle:" + huddleId); - } -} +//package com.jootalkpia.signaling_server.repository; +// +//import com.jootalkpia.signaling_server.model.Huddle; +//import org.springframework.data.redis.core.HashOperations; +//import org.springframework.data.redis.core.RedisTemplate; +//import org.springframework.stereotype.Repository; +//import java.util.Map; +// +//@Repository +//public class HuddleCacheRepository { +// private final HashOperations hashOps; +// +// public HuddleCacheRepository(RedisTemplate redisTemplate) { +// this.hashOps = redisTemplate.opsForHash(); +// } +// +// public void saveHuddle(Huddle huddle) { +// String key = "huddle:" + huddle.huddleId(); +// hashOps.put(key, "huddleId", huddle.huddleId()); +// hashOps.put(key, "channelId", huddle.channelId().toString()); +// hashOps.put(key, "createdByUserId", huddle.createdByUserId().toString()); +// hashOps.put(key, "createdAt", huddle.createdAt().toString()); +// } +// +// public Huddle getHuddleById(String huddleId) { +// String key = "huddle:" + huddleId; +// Map huddleData = hashOps.entries(key); +// if (huddleData.isEmpty()) { +// return null; +// } +// +// return new Huddle( +// huddleData.get("huddleId"), +// Long.parseLong(huddleData.get("channelId")), +// Long.parseLong(huddleData.get("createdByUserId")) +// ); +// } +// +// public void deleteHuddle(String huddleId) { +// hashOps.getOperations().delete("huddle:" + huddleId); +// } +//} diff --git a/src/backend/signaling_server/src/main/java/com/jootalkpia/signaling_server/repository/HuddleParticipantsRepository.java b/src/backend/signaling_server/src/main/java/com/jootalkpia/signaling_server/repository/HuddleParticipantsRepository.java index 08e45989..b149735b 100644 --- a/src/backend/signaling_server/src/main/java/com/jootalkpia/signaling_server/repository/HuddleParticipantsRepository.java +++ b/src/backend/signaling_server/src/main/java/com/jootalkpia/signaling_server/repository/HuddleParticipantsRepository.java @@ -1,77 +1,77 @@ -package com.jootalkpia.signaling_server.repository; - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.data.redis.core.HashOperations; -import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.data.redis.core.SetOperations; -import org.springframework.stereotype.Repository; -import java.util.Set; - -@Repository -@Slf4j -public class HuddleParticipantsRepository { - private final SetOperations setOps; - private final HashOperations hashOps; - private final RedisTemplate redisTemplate; - private final UserHuddleRepository userHuddleRepository; // 추가 - - public HuddleParticipantsRepository( - @Qualifier("longRedisTemplate") RedisTemplate redisTemplate, - @Qualifier("stringRedisTemplate") RedisTemplate stringRedisTemplate, - UserHuddleRepository userHuddleRepository) { // 추가 - this.setOps = redisTemplate.opsForSet(); - this.hashOps = stringRedisTemplate.opsForHash(); - this.redisTemplate = redisTemplate; - this.userHuddleRepository = userHuddleRepository; // 추가 - } - - // 허들 참가자 추가 및 유저-허들 매핑 업데이트 - public void addParticipant(String huddleId, Long userId) { - String key = "huddle:" + huddleId + ":participants"; - setOps.add(key, userId); - } - - // 허들에서 참가자 제거 및 유저-허들 매핑 삭제 - public void removeParticipant(String huddleId, Long userId) { - setOps.remove("huddle:" + huddleId + ":participants", userId); - userHuddleRepository.removeUserHuddle(userId); // 유저-허들 매핑 삭제 - removeUserEndpoint(huddleId, userId); - } - - // 허들 내 참가자 목록 조회 - public Set getParticipants(String huddleId) { - return setOps.members("huddle:" + huddleId + ":participants"); - } - - // WebRTC 엔드포인트 정보 저장 - public void saveUserEndpoint(String huddleId, Long userId, String endpointId) { - String key = "huddle:" + huddleId + ":endpoints"; - hashOps.put(key, userId.toString(), endpointId); - - // 저장 확인 로그 추가 - log.info("WebRTC 엔드포인트 저장 완료: huddleId={}, userId={}, endpointId={}", huddleId, userId, endpointId); - - // Redis에서 즉시 확인 - String savedEndpoint = hashOps.get(key, userId.toString()); - if (savedEndpoint == null) { - log.error("저장 후 조회 실패: huddleId={}, userId={}", huddleId, userId); - } else { - log.info("Redis에 저장된 엔드포인트 확인됨: {}", savedEndpoint); - } - } - - - // WebRTC 엔드포인트 정보 조회 - public String getUserEndpoint(String huddleId, Long userId) { - String key = "huddle:" + huddleId + ":endpoints"; - return hashOps.get(key, userId.toString()); - } - - // WebRTC 엔드포인트 삭제 - public void removeUserEndpoint(String huddleId, Long userId) { - String key = "huddle:" + huddleId + ":endpoints"; - hashOps.delete(key, userId.toString()); - } -} +//package com.jootalkpia.signaling_server.repository; +// +//import lombok.RequiredArgsConstructor; +//import lombok.extern.slf4j.Slf4j; +//import org.springframework.beans.factory.annotation.Qualifier; +//import org.springframework.data.redis.core.HashOperations; +//import org.springframework.data.redis.core.RedisTemplate; +//import org.springframework.data.redis.core.SetOperations; +//import org.springframework.stereotype.Repository; +//import java.util.Set; +// +//@Repository +//@Slf4j +//public class HuddleParticipantsRepository { +// private final SetOperations setOps; +// private final HashOperations hashOps; +// private final RedisTemplate redisTemplate; +// private final UserHuddleRepository userHuddleRepository; // 추가 +// +// public HuddleParticipantsRepository( +// @Qualifier("longRedisTemplate") RedisTemplate redisTemplate, +// @Qualifier("stringRedisTemplate") RedisTemplate stringRedisTemplate, +// UserHuddleRepository userHuddleRepository) { // 추가 +// this.setOps = redisTemplate.opsForSet(); +// this.hashOps = stringRedisTemplate.opsForHash(); +// this.redisTemplate = redisTemplate; +// this.userHuddleRepository = userHuddleRepository; // 추가 +// } +// +// // 허들 참가자 추가 및 유저-허들 매핑 업데이트 +// public void addParticipant(String huddleId, Long userId) { +// String key = "huddle:" + huddleId + ":participants"; +// setOps.add(key, userId); +// } +// +// // 허들에서 참가자 제거 및 유저-허들 매핑 삭제 +// public void removeParticipant(String huddleId, Long userId) { +// setOps.remove("huddle:" + huddleId + ":participants", userId); +// userHuddleRepository.removeUserHuddle(userId); // 유저-허들 매핑 삭제 +// removeUserEndpoint(huddleId, userId); +// } +// +// // 허들 내 참가자 목록 조회 +// public Set getParticipants(String huddleId) { +// return setOps.members("huddle:" + huddleId + ":participants"); +// } +// +// // WebRTC 엔드포인트 정보 저장 +// public void saveUserEndpoint(String huddleId, Long userId, String endpointId) { +// String key = "huddle:" + huddleId + ":endpoints"; +// hashOps.put(key, userId.toString(), endpointId); +// +// // 저장 확인 로그 추가 +// log.info("WebRTC 엔드포인트 저장 완료: huddleId={}, userId={}, endpointId={}", huddleId, userId, endpointId); +// +// // Redis에서 즉시 확인 +// String savedEndpoint = hashOps.get(key, userId.toString()); +// if (savedEndpoint == null) { +// log.error("저장 후 조회 실패: huddleId={}, userId={}", huddleId, userId); +// } else { +// log.info("Redis에 저장된 엔드포인트 확인됨: {}", savedEndpoint); +// } +// } +// +// +// // WebRTC 엔드포인트 정보 조회 +// public String getUserEndpoint(String huddleId, Long userId) { +// String key = "huddle:" + huddleId + ":endpoints"; +// return hashOps.get(key, userId.toString()); +// } +// +// // WebRTC 엔드포인트 삭제 +// public void removeUserEndpoint(String huddleId, Long userId) { +// String key = "huddle:" + huddleId + ":endpoints"; +// hashOps.delete(key, userId.toString()); +// } +//} diff --git a/src/backend/signaling_server/src/main/java/com/jootalkpia/signaling_server/repository/HuddlePipelineRepository.java b/src/backend/signaling_server/src/main/java/com/jootalkpia/signaling_server/repository/HuddlePipelineRepository.java index 03f93a54..3fe8bd39 100644 --- a/src/backend/signaling_server/src/main/java/com/jootalkpia/signaling_server/repository/HuddlePipelineRepository.java +++ b/src/backend/signaling_server/src/main/java/com/jootalkpia/signaling_server/repository/HuddlePipelineRepository.java @@ -1,71 +1,71 @@ -package com.jootalkpia.signaling_server.repository; - -import com.jootalkpia.signaling_server.exception.common.CustomException; -import com.jootalkpia.signaling_server.exception.common.ErrorCode; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.kurento.client.KurentoClient; -import org.kurento.client.MediaPipeline; -import org.springframework.data.redis.core.StringRedisTemplate; -import org.springframework.stereotype.Repository; - -import java.util.Set; - -@Repository -@RequiredArgsConstructor -@Slf4j -public class HuddlePipelineRepository { - - private final StringRedisTemplate redisTemplate; - private final KurentoClient kurentoClient; - private final UserHuddleRepository userHuddleRepository; - - // HuddleId와 pipelineId 매핑을 Redis에 저장 - public void saveHuddlePipeline(String huddleId, String pipelineId) { - try { - String key = "huddle:" + huddleId + ":pipeline"; - redisTemplate.opsForValue().set(key, pipelineId); - - log.info("허들 파이프라인 저장 완료: huddleId={}, pipelineId={}", huddleId, pipelineId); - } catch (Exception e) { - log.error("Redis 저장 오류: Huddle-Pipeline 매핑 저장 실패", e); - } - } - - // Redis에서 HuddlePipeline 가져와서 MediaPipeline 복원 - public MediaPipeline getPipeline(String huddleId) { - try { - String pipelineId = getPipelineId(huddleId); - - // pipelineId를 이용해서 MediaPipeline을 다시 가져옴 - return kurentoClient.getById(pipelineId, MediaPipeline.class); - } catch (Exception e) { - return null; - } - } - - public String getPipelineId(String huddleId) { +//package com.jootalkpia.signaling_server.repository; +// +//import com.jootalkpia.signaling_server.exception.common.CustomException; +//import com.jootalkpia.signaling_server.exception.common.ErrorCode; +//import lombok.RequiredArgsConstructor; +//import lombok.extern.slf4j.Slf4j; +//import org.kurento.client.KurentoClient; +//import org.kurento.client.MediaPipeline; +//import org.springframework.data.redis.core.StringRedisTemplate; +//import org.springframework.stereotype.Repository; +// +//import java.util.Set; +// +//@Repository +//@RequiredArgsConstructor +//@Slf4j +//public class HuddlePipelineRepository { +// +// private final StringRedisTemplate redisTemplate; +// private final KurentoClient kurentoClient; +// private final UserHuddleRepository userHuddleRepository; +// +// // HuddleId와 pipelineId 매핑을 Redis에 저장 +// public void saveHuddlePipeline(String huddleId, String pipelineId) { // try { - String key = "huddle:" + huddleId + ":pipeline"; - String pipelineId = redisTemplate.opsForValue().get(key); - if (pipelineId == null) { - log.warn("해당 huddleId={}에 대한 pipelineId 없음", huddleId); -// throw new CustomException(ErrorCode.PIPELINE_NOT_FOUND.getCode(), ErrorCode.PIPELINE_NOT_FOUND.getMsg()); - } - return pipelineId; +// String key = "huddle:" + huddleId + ":pipeline"; +// redisTemplate.opsForValue().set(key, pipelineId); +// +// log.info("허들 파이프라인 저장 완료: huddleId={}, pipelineId={}", huddleId, pipelineId); // } catch (Exception e) { -// throw new CustomException(ErrorCode.PIPELINE_NOT_FOUND.getCode(), ErrorCode.PIPELINE_NOT_FOUND.getMsg()); +// log.error("Redis 저장 오류: Huddle-Pipeline 매핑 저장 실패", e); // } - } - - // 허들 삭제 (Redis에서 pipeline 매핑 제거) - public void deleteHuddlePipeline(String huddleId) { - try { - String key = "huddle:" + huddleId + ":pipeline"; - redisTemplate.delete(key); - log.info("허들 삭제 완료: huddleId={}", huddleId); - } catch (Exception e) { - log.error("Redis 삭제 오류: Huddle-Pipeline 삭제 실패", e); - } - } -} +// } +// +// // Redis에서 HuddlePipeline 가져와서 MediaPipeline 복원 +// public MediaPipeline getPipeline(String huddleId) { +// try { +// String pipelineId = getPipelineId(huddleId); +// +// // pipelineId를 이용해서 MediaPipeline을 다시 가져옴 +// return kurentoClient.getById(pipelineId, MediaPipeline.class); +// } catch (Exception e) { +// return null; +// } +// } +// +// public String getPipelineId(String huddleId) { +//// try { +// String key = "huddle:" + huddleId + ":pipeline"; +// String pipelineId = redisTemplate.opsForValue().get(key); +// if (pipelineId == null) { +// log.warn("해당 huddleId={}에 대한 pipelineId 없음", huddleId); +//// throw new CustomException(ErrorCode.PIPELINE_NOT_FOUND.getCode(), ErrorCode.PIPELINE_NOT_FOUND.getMsg()); +// } +// return pipelineId; +//// } catch (Exception e) { +//// throw new CustomException(ErrorCode.PIPELINE_NOT_FOUND.getCode(), ErrorCode.PIPELINE_NOT_FOUND.getMsg()); +//// } +// } +// +// // 허들 삭제 (Redis에서 pipeline 매핑 제거) +// public void deleteHuddlePipeline(String huddleId) { +// try { +// String key = "huddle:" + huddleId + ":pipeline"; +// redisTemplate.delete(key); +// log.info("허들 삭제 완료: huddleId={}", huddleId); +// } catch (Exception e) { +// log.error("Redis 삭제 오류: Huddle-Pipeline 삭제 실패", e); +// } +// } +//} diff --git a/src/backend/signaling_server/src/main/java/com/jootalkpia/signaling_server/repository/UserHuddleRepository.java b/src/backend/signaling_server/src/main/java/com/jootalkpia/signaling_server/repository/UserHuddleRepository.java index 57147bcc..ad396d32 100644 --- a/src/backend/signaling_server/src/main/java/com/jootalkpia/signaling_server/repository/UserHuddleRepository.java +++ b/src/backend/signaling_server/src/main/java/com/jootalkpia/signaling_server/repository/UserHuddleRepository.java @@ -1,34 +1,34 @@ -package com.jootalkpia.signaling_server.repository; - -import lombok.RequiredArgsConstructor; -import org.springframework.data.redis.core.StringRedisTemplate; -import org.springframework.data.redis.core.ValueOperations; -import org.springframework.stereotype.Repository; - -@Repository -public class UserHuddleRepository { - private final ValueOperations valueOps; - - public UserHuddleRepository(StringRedisTemplate redisTemplate) { - this.valueOps = redisTemplate.opsForValue(); - } - - private String getUserHuddleKey(Long userId) { - return "huddle:user:" + userId; - } - - // 유저의 현재 허들 저장 - public void saveUserHuddle(Long userId, String huddleId) { - valueOps.set(getUserHuddleKey(userId), huddleId); - } - - // 유저의 현재 허들 조회 - public String getUserHuddle(Long userId) { - return valueOps.get(getUserHuddleKey(userId)); - } - - // 유저의 허들 삭제 (퇴장 처리) - public void removeUserHuddle(Long userId) { - valueOps.getAndDelete(getUserHuddleKey(userId)); - } -} +//package com.jootalkpia.signaling_server.repository; +// +//import lombok.RequiredArgsConstructor; +//import org.springframework.data.redis.core.StringRedisTemplate; +//import org.springframework.data.redis.core.ValueOperations; +//import org.springframework.stereotype.Repository; +// +//@Repository +//public class UserHuddleRepository { +// private final ValueOperations valueOps; +// +// public UserHuddleRepository(StringRedisTemplate redisTemplate) { +// this.valueOps = redisTemplate.opsForValue(); +// } +// +// private String getUserHuddleKey(Long userId) { +// return "huddle:user:" + userId; +// } +// +// // 유저의 현재 허들 저장 +// public void saveUserHuddle(Long userId, String huddleId) { +// valueOps.set(getUserHuddleKey(userId), huddleId); +// } +// +// // 유저의 현재 허들 조회 +// public String getUserHuddle(Long userId) { +// return valueOps.get(getUserHuddleKey(userId)); +// } +// +// // 유저의 허들 삭제 (퇴장 처리) +// public void removeUserHuddle(Long userId) { +// valueOps.getAndDelete(getUserHuddleKey(userId)); +// } +//} diff --git a/src/backend/signaling_server/src/main/java/com/jootalkpia/signaling_server/rtc/CallHandler.java b/src/backend/signaling_server/src/main/java/com/jootalkpia/signaling_server/rtc/CallHandler.java new file mode 100644 index 00000000..7d761236 --- /dev/null +++ b/src/backend/signaling_server/src/main/java/com/jootalkpia/signaling_server/rtc/CallHandler.java @@ -0,0 +1,80 @@ +package com.jootalkpia.signaling_server.rtc; + +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.kurento.client.IceCandidate; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.messaging.handler.annotation.MessageMapping; +import org.springframework.messaging.handler.annotation.Payload; +import org.springframework.messaging.simp.stomp.StompHeaderAccessor; +import org.springframework.stereotype.Controller; + +@Controller +@Slf4j +@RequiredArgsConstructor +public class CallHandler { + + @Qualifier("customUserRegistry") + private final UserRegistry registry; + private final HuddleManager huddleManager; + + @MessageMapping("/signal") + public void handleSignalMessage(StompHeaderAccessor headerAccessor, @Payload String message) { + final JsonObject jsonMessage = JsonParser.parseString(message).getAsJsonObject(); + + String sessionId = headerAccessor.getSessionId(); + UserSession userSession = registry.getBySessionId(sessionId); + + if (userSession != null) { + log.debug("Incoming message from user '{}': {}", userSession.getUserId(), jsonMessage); + } else { + log.debug("Incoming message from new user: {}", jsonMessage); + } + + switch (jsonMessage.get("id").getAsString()) { + case "joinHuddle": + joinHuddle(jsonMessage, sessionId); + break; + case "receiveVideoFrom": + final Long senderId = jsonMessage.get("sender").getAsLong(); + final UserSession sender = registry.getByUserId(senderId); + final String sdpOffer = jsonMessage.get("sdpOffer").getAsString(); + userSession.receiveVideoFrom(sender, sdpOffer); + break; + case "leaveRoom": + leaveRoom(userSession); + break; + case "onIceCandidate": + JsonObject candidate = jsonMessage.get("candidate").getAsJsonObject(); + IceCandidate cand = new IceCandidate( + candidate.get("candidate").getAsString(), + candidate.get("sdpMid").getAsString(), + candidate.get("sdpMLineIndex").getAsInt() + ); + userSession.addCandidate(cand, jsonMessage.get("sender").getAsLong()); + break; + default: + break; + } + } + + private void joinHuddle(JsonObject jsonMessage, String sessionId) { + final Long channelId = jsonMessage.get("channelId").getAsLong(); + final Long userId = jsonMessage.get("userId").getAsLong(); + log.info("PARTICIPANT {}: trying to join room {}", userId, channelId); + + Huddle huddle = huddleManager.getHuddle(channelId); + final UserSession userSession = huddle.join(userId, sessionId); + registry.register(userSession); + } + + private void leaveRoom(UserSession userSession) { + final Huddle huddle = huddleManager.getHuddle(userSession.getChannelId()); + huddle.leave(userSession); + if (huddle.getParticipants().isEmpty()) { + huddleManager.removeHuddle(huddle); + } + } +} diff --git a/src/backend/signaling_server/src/main/java/com/jootalkpia/signaling_server/rtc/Huddle.java b/src/backend/signaling_server/src/main/java/com/jootalkpia/signaling_server/rtc/Huddle.java new file mode 100644 index 00000000..192ad780 --- /dev/null +++ b/src/backend/signaling_server/src/main/java/com/jootalkpia/signaling_server/rtc/Huddle.java @@ -0,0 +1,151 @@ +package com.jootalkpia.signaling_server.rtc; + +import java.io.Closeable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import javax.annotation.PreDestroy; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.kurento.client.Continuation; +import org.kurento.client.MediaPipeline; +import org.springframework.messaging.simp.SimpMessagingTemplate; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; + +@Slf4j +public class Huddle implements Closeable { + + @Getter + private final Long channelId; + private final ConcurrentMap participants = new ConcurrentHashMap<>(); + private final MediaPipeline pipeline; + private final SimpMessagingTemplate messagingTemplate; + + public Huddle(Long channelId, MediaPipeline pipeline, SimpMessagingTemplate messagingTemplate) { + this.channelId = channelId; + this.pipeline = pipeline; + this.messagingTemplate = messagingTemplate; + log.info("ROOM {} has been created", channelId); + } + + @PreDestroy + private void shutdown() { + this.close(); + } + + public UserSession join(Long userId, String sessionId) { + log.info("ROOM {}: adding participant {}", this.channelId, userId); + final UserSession participant = new UserSession(userId, this.channelId, sessionId, this.pipeline, this.messagingTemplate); + joinRoom(participant); + participants.put(participant.getUserId(), participant); + sendParticipantNames(participant); + return participant; + } + + public void leave(UserSession user) { + log.debug("PARTICIPANT {}: Leaving room {}", user.getUserId(), this.channelId); + this.removeParticipant(user.getUserId()); + user.close(); + } + + private Collection joinRoom(UserSession newParticipant) { + final JsonObject newParticipantMsg = new JsonObject(); + newParticipantMsg.addProperty("id", "newParticipantArrived"); + newParticipantMsg.addProperty("name", String.valueOf(newParticipant.getUserId())); + + final List participantsList = new ArrayList<>(participants.values().size()); + log.debug("ROOM {}: notifying other participants of new participant {}", channelId, + newParticipant.getUserId()); + + for (final UserSession participant : participants.values()) { + + participant.sendPrivateMessage(participant.getUserId(), newParticipantMsg); + + participantsList.add(participant.getUserId()); + } + + return participantsList; + } + + private void removeParticipant(Long userId) { + participants.remove(userId); + + log.debug("ROOM {}: notifying all users that {} is leaving the room", this.channelId, userId); + + final List unnotifiedParticipants = new ArrayList<>(); + final JsonObject participantLeftJson = new JsonObject(); + participantLeftJson.addProperty("id", "participantLeft"); + participantLeftJson.addProperty("name", userId); + + for (final UserSession participant : participants.values()) { + participant.cancelVideoFrom(userId); + participant.sendMessage(participantLeftJson); + } + + if (!unnotifiedParticipants.isEmpty()) { + log.debug("ROOM {}: The users {} could not be notified that {} left the room", this.channelId, + unnotifiedParticipants, userId); + } + } + + + public void sendParticipantNames(UserSession user) { + + final JsonArray participantsArray = new JsonArray(); + for (final UserSession participant : this.getParticipants()) { + if (!participant.equals(user)) { + final JsonElement participantName = new JsonPrimitive(participant.getUserId()); + participantsArray.add(participantName); + } + } + + final JsonObject existingParticipantsMsg = new JsonObject(); + existingParticipantsMsg.addProperty("id", "existingParticipants"); + existingParticipantsMsg.add("data", participantsArray); + + log.debug("PARTICIPANT {}: sending a list of {} participants", user.getUserId(), participantsArray.size()); + + user.sendPrivateMessage(user.getUserId(), existingParticipantsMsg); + } + + public Collection getParticipants() { + return participants.values(); + } + + public UserSession getParticipant(Long userId) { + return participants.get(userId); + } + + @Override + public void close() { + for (final UserSession user : participants.values()) { + user.close(); + } + + participants.clear(); + + pipeline.release(new Continuation() { + + @Override + public void onSuccess(Void result) throws Exception { + log.trace("ROOM {}: Released Pipeline", Huddle.this.channelId); + } + + @Override + public void onError(Throwable cause) throws Exception { + log.warn("PARTICIPANT {}: Could not release Pipeline", Huddle.this.channelId); + } + }); + + log.debug("Room {} closed", this.channelId); + } + +} diff --git a/src/backend/signaling_server/src/main/java/com/jootalkpia/signaling_server/rtc/HuddleManager.java b/src/backend/signaling_server/src/main/java/com/jootalkpia/signaling_server/rtc/HuddleManager.java new file mode 100644 index 00000000..5414595a --- /dev/null +++ b/src/backend/signaling_server/src/main/java/com/jootalkpia/signaling_server/rtc/HuddleManager.java @@ -0,0 +1,40 @@ +package com.jootalkpia.signaling_server.rtc; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.kurento.client.KurentoClient; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +@Slf4j +public class HuddleManager { + + private final KurentoClient kurento; + private final SimpMessagingTemplate messagingTemplate; + private final ConcurrentMap huddles = new ConcurrentHashMap<>(); + + public Huddle getHuddle(Long channelId) { + log.debug("Searching for channelId {}", channelId); + Huddle huddle = huddles.get(channelId); + + if (huddle == null) { + log.debug("channelId {} not existent. Will create now!", channelId); + huddle = new Huddle(channelId, kurento.createMediaPipeline(), messagingTemplate); + huddles.put(channelId, huddle); + } + log.debug("channelId {} found!", channelId); + return huddle; + } + + public void removeHuddle(Huddle huddle) { + this.huddles.remove(huddle.getChannelId()); + huddle.close(); + log.info("channelId {} removed and closed", huddle.getChannelId()); + } + +} \ No newline at end of file diff --git a/src/backend/signaling_server/src/main/java/com/jootalkpia/signaling_server/rtc/KurentoHandler.java b/src/backend/signaling_server/src/main/java/com/jootalkpia/signaling_server/rtc/KurentoHandler.java index 74b1dd53..e977c833 100644 --- a/src/backend/signaling_server/src/main/java/com/jootalkpia/signaling_server/rtc/KurentoHandler.java +++ b/src/backend/signaling_server/src/main/java/com/jootalkpia/signaling_server/rtc/KurentoHandler.java @@ -1,316 +1,314 @@ -package com.jootalkpia.signaling_server.rtc; - -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.ToNumberPolicy; -import com.jootalkpia.signaling_server.exception.common.CustomException; -import com.jootalkpia.signaling_server.exception.common.ErrorCode; -import com.jootalkpia.signaling_server.model.Huddle; -import com.jootalkpia.signaling_server.repository.ChannelHuddleRepository; -import com.jootalkpia.signaling_server.repository.HuddleParticipantsRepository; -import com.jootalkpia.signaling_server.service.HuddleService; -import com.jootalkpia.signaling_server.service.KurentoService; -import com.jootalkpia.signaling_server.util.ValidationUtils; -import java.util.Set; -import java.util.Timer; -import java.util.TimerTask; -import java.util.stream.Collectors; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.kurento.client.IceCandidate; -import org.kurento.client.WebRtcEndpoint; -import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.messaging.simp.SimpMessagingTemplate; -import org.springframework.stereotype.Component; -import org.springframework.web.socket.*; -import org.springframework.web.socket.handler.TextWebSocketHandler; - -import java.io.IOException; -import java.util.Map; - -@Slf4j -@Component -@RequiredArgsConstructor -public class KurentoHandler extends TextWebSocketHandler { - - private final Gson gson = new GsonBuilder().setObjectToNumberStrategy(ToNumberPolicy.LONG_OR_DOUBLE).create(); - private final HuddleService huddleService; - private final KurentoService kurentoService; - private final ValidationUtils validationUtils; - private final RedisTemplate redisTemplate; - private final SimpMessagingTemplate messagingTemplate; - - - @Override - public void handleTextMessage(WebSocketSession session, TextMessage message) throws IOException { - Map json = gson.fromJson(message.getPayload(), Map.class); - String id = (String) json.get("id"); - - switch (id) { - case "createRoom" -> handleCreateRoom(session, json); - case "joinRoom" -> handleJoinRoom(session, json); - case "leaveRoom" -> handleLeaveRoom(session, json); - case "offer" -> handleOffer(session, json); - case "iceCandidate" -> handleIceCandidate(session, json); - default -> log.warn("Unknown message type received: {}", id); - } - } - - private Long getLongValueFromJson(Map json, String key) { - Object value = json.get(key); - if (value == null) { - log.error("Missing required parameter: {}", key); - throw new IllegalArgumentException("Missing required parameter: " + key); - } - return (value instanceof Number) ? ((Number) value).longValue() : Long.parseLong(value.toString()); - } - - // 허들 생성 - private void handleCreateRoom(WebSocketSession session, Map json) throws IOException { - try { - Long channelId = getLongValueFromJson(json, "channelId"); - Long userId = getLongValueFromJson(json, "userId"); - - // TODO: ValidationUtils.validateUserId(), ValidationUtils.validateChannelId() - - // 허들 메타데이터 저장 - Huddle newHuddle = huddleService.createHuddle(channelId, userId); - - // 채널과 허들 매핑 - huddleService.saveHuddleChannel(channelId, newHuddle.huddleId()); - - // 파이프라인 생성 및 허들과 파이프라인 매핑 - kurentoService.createPipeline(newHuddle.huddleId()); - - session.sendMessage(new TextMessage(gson.toJson(Map.of("id", "roomCreated", "huddleId", newHuddle.huddleId())))); - -// 일정 시간 내 참가 없으면 삭제 -// scheduleHuddleDeletion(newHuddle.huddleId()); - - // 자동으로 허들 입장 처리 - handleJoinRoom(session, Map.of( - "id", "joinRoom", - "channelId", channelId, - "userId", userId - )); - } catch (Exception e) { - log.error("Error creating room", e); - session.sendMessage(new TextMessage(gson.toJson(Map.of("id", "error", "message", "Failed to create room")))); - } - } - - private void scheduleHuddleDeletion(String huddleId) { - - } - - // 허들 입장 (새로운 참가자가 들어올 때 기존 참가자들에게 알림) - private void handleJoinRoom(WebSocketSession session, Map json) throws IOException { - Long userId = getLongValueFromJson(json, "userId"); - Long channelId = getLongValueFromJson(json, "channelId"); - - // TODO: ValidationUtils.validateUserId(), ValidationUtils.validateChannelId() - - try { - String huddleId = validationUtils.isHuddleInChannel(channelId); - if (huddleId == null) { - throw new CustomException(ErrorCode.HUDDLE_NOT_FOUND.getCode(), "해당 채널에 매핑된 허들이 없습니다."); - } - - validationUtils.canUserJoinHuddle(huddleId, userId); - validationUtils.isHuddleValid(huddleId); - validationUtils.isPipelineInChannel(huddleId); - - // WebRTC 엔드포인트 생성 및 저장 - WebRtcEndpoint newUserEndpoint = kurentoService.addParticipantToRoom(huddleId, userId); - - // 유저:허들 저장 - huddleService.addUserHuddle(userId, huddleId); - - session.sendMessage(new TextMessage(gson.toJson(Map.of("id", "joinedRoom", "huddleId", huddleId)))); - - // 새로운 참가자가 들어왔음을 기존 참가자들에게 알림 - notifyExistingParticipants(huddleId, userId, newUserEndpoint); - - // 새로운 참가자가 기존 참가자들의 스트림을 구독하도록 SDP Offer 전송 요청 - subscribeToExistingParticipants(huddleId, userId); - - } catch (Exception e) { - log.error("Error joining room", e); - - // 오류 발생 시 롤백 처리 - huddleService.recoverIfErrorJoining(userId, channelId); - - session.sendMessage(new TextMessage(gson.toJson(Map.of("id", "error", "message", "Failed to join room")))); - } - } - - // 새로운 참가자가 입장하면 기존 참가자들에게 구독하라고 SDP Offer 전송 요청 - private void notifyExistingParticipants(String huddleId, Long newUserId, WebRtcEndpoint newUserEndpoint) { - Set participantIds = redisTemplate.opsForSet().members("huddle:" + huddleId + ":participants"); - - for (Long participantId : participantIds) { - if (!participantId.equals(newUserId)) { - try { - WebRtcEndpoint existingEndpoint = kurentoService.getParticipantEndpoint(huddleId, participantId); - if (existingEndpoint != null) { - // 새로운 참가자가 기존 참가자에게 SDP Offer 요청 - String sdpOffer = newUserEndpoint.generateOffer(); - - messagingTemplate.convertAndSend("/topic/huddle/" + huddleId + "/subscribe", gson.toJson(Map.of( - "id", "subscribe", - "huddleId", huddleId, - "newUserId", newUserId, - "targetUserId", participantId, // 구독 대상 추가 - "sdpOffer", sdpOffer - ))); - } - } catch (Exception e) { - log.error("Error notifying existing participant {} about new participant {}", participantId, newUserId, e); - } - } - } - } - - // 새로운 참가자가 기존 참가자들을 구독하도록 SDP Offer 전송 요청 - private void subscribeToExistingParticipants(String huddleId, Long newUserId) { - Set participantIds = redisTemplate.opsForSet().members("huddle:" + huddleId + ":participants"); - - for (Long participantId : participantIds) { - if (!participantId.equals(newUserId)) { - try { - WebRtcEndpoint newUserEndpoint = kurentoService.getParticipantEndpoint(huddleId, newUserId); - if (newUserEndpoint != null) { - String sdpOffer = newUserEndpoint.generateOffer(); - - messagingTemplate.convertAndSend("/topic/huddle/" + huddleId + "/subscribe", gson.toJson(Map.of( - "id", "subscribe", - "huddleId", huddleId, - "newUserId", newUserId, - "targetUserId", participantId, - "sdpOffer", sdpOffer - ))); - } - } catch (Exception e) { - log.error("Error notifying new participant {} about existing participant {}", newUserId, participantId, e); - } - } - } - } - - - // 허들 나감 - private void handleLeaveRoom(WebSocketSession session, Map json) throws IOException { - try { - Long userId = getLongValueFromJson(json, "userId"); - String huddleId = (String) json.get("huddleId"); - - // Kurento에서 WebRTC 엔드포인트 삭제 - kurentoService.removeParticipantFromRoom(huddleId, userId); - - // 허들에서 유저 제거 - validationUtils.canUserExitHuddle(huddleId, userId); - - session.sendMessage(new TextMessage(gson.toJson(Map.of("id", "leftRoom", "huddleId", huddleId)))); - } catch (Exception e) { - log.error("Error leaving room", e); - } - } - - // SDP Offer 처리 - private void handleOffer(WebSocketSession session, Map json) throws IOException { - try { - Long userId = getLongValueFromJson(json, "userId"); - String huddleId = (String) json.get("huddleId"); - String sdpOffer = (String) json.get("sdpOffer"); - - WebRtcEndpoint webRtcEndpoint = kurentoService.getParticipantEndpoint(huddleId, userId); - - // 허들에 참여 중이 아닌 경우 Offer 처리 안함 - if (webRtcEndpoint == null) { - log.warn("엔드포인트가 널!! 허들에 참여 중이지 않은 유저입니다: userId={}", userId); - return; - } - - webRtcEndpoint.addIceCandidateFoundListener(event -> { - IceCandidate candidate = event.getCandidate(); - sendIceCandidate(huddleId, userId, userId, candidate); - }); - - // offer 에 대한 answer 생성 - String sdpAnswer = webRtcEndpoint.processOffer(sdpOffer); - - // 쿠렌토가 후보를 찾는 과정 - webRtcEndpoint.gatherCandidates(); - - session.sendMessage(new TextMessage(gson.toJson(Map.of( - "id", "answer", - "huddleId", huddleId, - "userId", userId, - "sdpAnswer", sdpAnswer - )))); - - } catch (Exception e) { - log.error("Error handling offer", e); - } - } - - // ICE Candidate 처리 - private void handleIceCandidate(WebSocketSession session, Map json) { - try { - Long userId = getLongValueFromJson(json, "userId"); - String huddleId = (String) json.get("huddleId"); - Long targetUserId = getLongValueFromJson(json, "targetUserId"); - - Object candidateObj = json.get("candidate"); - String candidate; - String sdpMid = ""; - int sdpMLineIndex = 0; - - if (candidateObj instanceof String) { - candidate = (String) candidateObj; - } else if (candidateObj instanceof Map) { - Map candidateMap = (Map) candidateObj; - candidate = (String) candidateMap.get("candidate"); - sdpMid = (String) candidateMap.getOrDefault("sdpMid", ""); - sdpMLineIndex = ((Number) candidateMap.getOrDefault("sdpMLineIndex", 0)).intValue(); - } else { - log.error("Invalid ICE Candidate format: {}", candidateObj); - return; - } - - WebRtcEndpoint targetEndpoint = kurentoService.getParticipantEndpoint(huddleId, targetUserId); - if (targetEndpoint == null) { - log.warn("Target user {} is not in huddle {}", targetUserId, huddleId); - return; - } - - targetEndpoint.addIceCandidate(new IceCandidate(candidate, sdpMid, sdpMLineIndex)); - - // 상대방에게 ICE Candidate 전송 - sendIceCandidate(huddleId, targetUserId, userId, new IceCandidate(candidate, sdpMid, sdpMLineIndex)); - - } catch (Exception e) { - log.error("Error handling ICE candidate", e); - } - } - - // ICE Candidate 전송 공통 메서드 - private final Object webSocketLock = new Object(); // 동기화용 Lock 객체 - private void sendIceCandidate(String huddleId, Long targetUserId, Long senderId, IceCandidate candidate) { - Map candidateJson = Map.of( - "id", "iceCandidate", - "huddleId", huddleId, - "userId", targetUserId, - "senderId", senderId, - "candidate", Map.of( - "candidate", candidate.getCandidate(), - "sdpMid", candidate.getSdpMid(), - "sdpMLineIndex", candidate.getSdpMLineIndex() - ) - ); - - messagingTemplate.convertAndSend("/topic/huddle/" + huddleId + "/iceCandidate", gson.toJson(candidateJson)); - log.info("📡 Sent ICE candidate to user {} in huddle {}: {}", targetUserId, huddleId, candidateJson); - } - -} +//package com.jootalkpia.signaling_server.rtc; +// +//import com.google.gson.Gson; +//import com.google.gson.GsonBuilder; +//import com.google.gson.ToNumberPolicy; +//import com.jootalkpia.signaling_server.exception.common.CustomException; +//import com.jootalkpia.signaling_server.exception.common.ErrorCode; +//import com.jootalkpia.signaling_server.model.Huddle; +//import com.jootalkpia.signaling_server.rtc.ValidationUtils; +//import com.jootalkpia.signaling_server.repository.ChannelHuddleRepository; +//import com.jootalkpia.signaling_server.repository.HuddleParticipantsRepository; +//import com.jootalkpia.signaling_server.service.HuddleService; +//import com.jootalkpia.signaling_server.service.KurentoService; +//import java.util.Set; +//import lombok.RequiredArgsConstructor; +//import lombok.extern.slf4j.Slf4j; +//import org.kurento.client.IceCandidate; +//import org.kurento.client.WebRtcEndpoint; +//import org.springframework.data.redis.core.RedisTemplate; +//import org.springframework.messaging.simp.SimpMessagingTemplate; +//import org.springframework.stereotype.Component; +//import org.springframework.web.socket.*; +//import org.springframework.web.socket.handler.TextWebSocketHandler; +// +//import java.io.IOException; +//import java.util.Map; +// +//@Slf4j +//@Component +//@RequiredArgsConstructor +//public class KurentoHandler extends TextWebSocketHandler { +// +// private final Gson gson = new GsonBuilder().setObjectToNumberStrategy(ToNumberPolicy.LONG_OR_DOUBLE).create(); +// private final HuddleService huddleService; +// private final KurentoService kurentoService; +// private final ValidationUtils validationUtils; +// private final RedisTemplate redisTemplate; +// private final SimpMessagingTemplate messagingTemplate; +// private final ValidationUtils validatorUtils; +// +// +// @Override +// public void handleTextMessage(WebSocketSession session, TextMessage message) throws IOException { +// Map json = gson.fromJson(message.getPayload(), Map.class); +// String id = (String) json.get("id"); +// +// switch (id) { +// case "createRoom" -> handleCreateRoom(session, json); +// case "joinRoom" -> handleJoinRoom(session, json); +// case "leaveRoom" -> handleLeaveRoom(session, json); +// case "offer" -> handleOffer(session, json); +// case "iceCandidate" -> handleIceCandidate(session, json); +// default -> log.warn("Unknown message type received: {}", id); +// } +// } +// +// private Long getLongValueFromJson(Map json, String key) { +// Object value = json.get(key); +// if (value == null) { +// log.error("Missing required parameter: {}", key); +// throw new IllegalArgumentException("Missing required parameter: " + key); +// } +// return (value instanceof Number) ? ((Number) value).longValue() : Long.parseLong(value.toString()); +// } +// +// // 허들 생성 +// private void handleCreateRoom(WebSocketSession session, Map json) throws IOException { +// try { +// Long channelId = getLongValueFromJson(json, "channelId"); +// Long userId = getLongValueFromJson(json, "userId"); +// +// // TODO: ValidationUtils.validateUserId(), ValidationUtils.validateChannelId() +// +// // 허들 메타데이터 저장 +// Huddle newHuddle = huddleService.createHuddle(channelId, userId); +// +// // 채널과 허들 매핑 +// huddleService.saveHuddleChannel(channelId, newHuddle.huddleId()); +// +// // 파이프라인 생성 및 허들과 파이프라인 매핑 +// kurentoService.createPipeline(newHuddle.huddleId()); +// +// session.sendMessage(new TextMessage(gson.toJson(Map.of("id", "roomCreated", "huddleId", newHuddle.huddleId())))); +// +//// 일정 시간 내 참가 없으면 삭제 +//// scheduleHuddleDeletion(newHuddle.huddleId()); +// +// // 자동으로 허들 입장 처리 +// handleJoinRoom(session, Map.of( +// "id", "joinRoom", +// "channelId", channelId, +// "userId", userId +// )); +// } catch (Exception e) { +// log.error("Error creating room", e); +// session.sendMessage(new TextMessage(gson.toJson(Map.of("id", "error", "message", "Failed to create room")))); +// } +// } +// +// private void scheduleHuddleDeletion(String huddleId) { +// +// } +// +// // 허들 입장 (새로운 참가자가 들어올 때 기존 참가자들에게 알림) +// private void handleJoinRoom(WebSocketSession session, Map json) throws IOException { +// Long userId = getLongValueFromJson(json, "userId"); +// Long channelId = getLongValueFromJson(json, "channelId"); +// +// // TODO: ValidationUtils.validateUserId(), ValidationUtils.validateChannelId() +// +// try { +// String huddleId = validationUtils.isHuddleInChannel(channelId); +// if (huddleId == null) { +// throw new CustomException(ErrorCode.HUDDLE_NOT_FOUND.getCode(), "해당 채널에 매핑된 허들이 없습니다."); +// } +// +// validationUtils.canUserJoinHuddle(huddleId, userId); +// validationUtils.isHuddleValid(huddleId); +// validationUtils.isPipelineInChannel(huddleId); +// +// // WebRTC 엔드포인트 생성 및 저장 +// WebRtcEndpoint newUserEndpoint = kurentoService.addParticipantToRoom(huddleId, userId); +// +// // 유저:허들 저장 +// huddleService.addUserHuddle(userId, huddleId); +// +// session.sendMessage(new TextMessage(gson.toJson(Map.of("id", "joinedRoom", "huddleId", huddleId)))); +// +// // 새로운 참가자가 들어왔음을 기존 참가자들에게 알림 +// notifyExistingParticipants(huddleId, userId, newUserEndpoint); +// +// // 새로운 참가자가 기존 참가자들의 스트림을 구독하도록 SDP Offer 전송 요청 +// subscribeToExistingParticipants(huddleId, userId); +// +// } catch (Exception e) { +// log.error("Error joining room", e); +// +// // 오류 발생 시 롤백 처리 +// huddleService.recoverIfErrorJoining(userId, channelId); +// +// session.sendMessage(new TextMessage(gson.toJson(Map.of("id", "error", "message", "Failed to join room")))); +// } +// } +// +// // 새로운 참가자가 입장하면 기존 참가자들에게 구독하라고 SDP Offer 전송 요청 +// private void notifyExistingParticipants(String huddleId, Long newUserId, WebRtcEndpoint newUserEndpoint) { +// Set participantIds = redisTemplate.opsForSet().members("huddle:" + huddleId + ":participants"); +// +// for (Long participantId : participantIds) { +// if (!participantId.equals(newUserId)) { +// try { +// WebRtcEndpoint existingEndpoint = kurentoService.getParticipantEndpoint(huddleId, participantId); +// if (existingEndpoint != null) { +// // 새로운 참가자가 기존 참가자에게 SDP Offer 요청 +// String sdpOffer = newUserEndpoint.generateOffer(); +// +// messagingTemplate.convertAndSend("/topic/huddle/" + huddleId + "/subscribe", gson.toJson(Map.of( +// "id", "subscribe", +// "huddleId", huddleId, +// "newUserId", newUserId, +// "targetUserId", participantId, // 구독 대상 추가 +// "sdpOffer", sdpOffer +// ))); +// } +// } catch (Exception e) { +// log.error("Error notifying existing participant {} about new participant {}", participantId, newUserId, e); +// } +// } +// } +// } +// +// // 새로운 참가자가 기존 참가자들을 구독하도록 SDP Offer 전송 요청 +// private void subscribeToExistingParticipants(String huddleId, Long newUserId) { +// Set participantIds = redisTemplate.opsForSet().members("huddle:" + huddleId + ":participants"); +// +// for (Long participantId : participantIds) { +// if (!participantId.equals(newUserId)) { +// try { +// WebRtcEndpoint newUserEndpoint = kurentoService.getParticipantEndpoint(huddleId, newUserId); +// if (newUserEndpoint != null) { +// String sdpOffer = newUserEndpoint.generateOffer(); +// +// messagingTemplate.convertAndSend("/topic/huddle/" + huddleId + "/subscribe", gson.toJson(Map.of( +// "id", "subscribe", +// "huddleId", huddleId, +// "newUserId", newUserId, +// "targetUserId", participantId, +// "sdpOffer", sdpOffer +// ))); +// } +// } catch (Exception e) { +// log.error("Error notifying new participant {} about existing participant {}", newUserId, participantId, e); +// } +// } +// } +// } +// +// +// // 허들 나감 +// private void handleLeaveRoom(WebSocketSession session, Map json) throws IOException { +// try { +// Long userId = getLongValueFromJson(json, "userId"); +// String huddleId = (String) json.get("huddleId"); +// +// // Kurento에서 WebRTC 엔드포인트 삭제 +// kurentoService.removeParticipantFromRoom(huddleId, userId); +// +// // 허들에서 유저 제거 +// validationUtils.canUserExitHuddle(huddleId, userId); +// +// session.sendMessage(new TextMessage(gson.toJson(Map.of("id", "leftRoom", "huddleId", huddleId)))); +// } catch (Exception e) { +// log.error("Error leaving room", e); +// } +// } +// +// // SDP Offer 처리 +// private void handleOffer(WebSocketSession session, Map json) throws IOException { +// try { +// Long userId = getLongValueFromJson(json, "userId"); +// String huddleId = (String) json.get("huddleId"); +// String sdpOffer = (String) json.get("sdpOffer"); +// +// WebRtcEndpoint webRtcEndpoint = kurentoService.getParticipantEndpoint(huddleId, userId); +// +// // 허들에 참여 중이 아닌 경우 Offer 처리 안함 +// if (webRtcEndpoint == null) { +// log.warn("엔드포인트가 널!! 허들에 참여 중이지 않은 유저입니다: userId={}", userId); +// return; +// } +// +// webRtcEndpoint.addIceCandidateFoundListener(event -> { +// IceCandidate candidate = event.getCandidate(); +// sendIceCandidate(huddleId, userId, userId, candidate); +// }); +// +// // offer 에 대한 answer 생성 +// String sdpAnswer = webRtcEndpoint.processOffer(sdpOffer); +// +// // 쿠렌토가 후보를 찾는 과정 +// webRtcEndpoint.gatherCandidates(); +// +// session.sendMessage(new TextMessage(gson.toJson(Map.of( +// "id", "answer", +// "huddleId", huddleId, +// "userId", userId, +// "sdpAnswer", sdpAnswer +// )))); +// +// } catch (Exception e) { +// log.error("Error handling offer", e); +// } +// } +// +// // ICE Candidate 처리 +// private void handleIceCandidate(WebSocketSession session, Map json) { +// try { +// Long userId = getLongValueFromJson(json, "userId"); +// String huddleId = (String) json.get("huddleId"); +// Long targetUserId = getLongValueFromJson(json, "targetUserId"); +// +// Object candidateObj = json.get("candidate"); +// String candidate; +// String sdpMid = ""; +// int sdpMLineIndex = 0; +// +// if (candidateObj instanceof String) { +// candidate = (String) candidateObj; +// } else if (candidateObj instanceof Map) { +// Map candidateMap = (Map) candidateObj; +// candidate = (String) candidateMap.get("candidate"); +// sdpMid = (String) candidateMap.getOrDefault("sdpMid", ""); +// sdpMLineIndex = ((Number) candidateMap.getOrDefault("sdpMLineIndex", 0)).intValue(); +// } else { +// log.error("Invalid ICE Candidate format: {}", candidateObj); +// return; +// } +// +// WebRtcEndpoint targetEndpoint = kurentoService.getParticipantEndpoint(huddleId, targetUserId); +// if (targetEndpoint == null) { +// log.warn("Target user {} is not in huddle {}", targetUserId, huddleId); +// return; +// } +// +// targetEndpoint.addIceCandidate(new IceCandidate(candidate, sdpMid, sdpMLineIndex)); +// +// // 상대방에게 ICE Candidate 전송 +// sendIceCandidate(huddleId, targetUserId, userId, new IceCandidate(candidate, sdpMid, sdpMLineIndex)); +// +// } catch (Exception e) { +// log.error("Error handling ICE candidate", e); +// } +// } +// +// // ICE Candidate 전송 공통 메서드 +// private final Object webSocketLock = new Object(); // 동기화용 Lock 객체 +// private void sendIceCandidate(String huddleId, Long targetUserId, Long senderId, IceCandidate candidate) { +// Map candidateJson = Map.of( +// "id", "iceCandidate", +// "huddleId", huddleId, +// "userId", targetUserId, +// "senderId", senderId, +// "candidate", Map.of( +// "candidate", candidate.getCandidate(), +// "sdpMid", candidate.getSdpMid(), +// "sdpMLineIndex", candidate.getSdpMLineIndex() +// ) +// ); +// +// messagingTemplate.convertAndSend("/topic/huddle/" + huddleId + "/iceCandidate", gson.toJson(candidateJson)); +// log.info("📡 Sent ICE candidate to user {} in huddle {}: {}", targetUserId, huddleId, candidateJson); +// } +// +//} diff --git a/src/backend/signaling_server/src/main/java/com/jootalkpia/signaling_server/rtc/UserRegistry.java b/src/backend/signaling_server/src/main/java/com/jootalkpia/signaling_server/rtc/UserRegistry.java new file mode 100644 index 00000000..d4659e7a --- /dev/null +++ b/src/backend/signaling_server/src/main/java/com/jootalkpia/signaling_server/rtc/UserRegistry.java @@ -0,0 +1,40 @@ +package com.jootalkpia.signaling_server.rtc; + +import java.util.concurrent.ConcurrentHashMap; + +import org.springframework.stereotype.Component; +import org.springframework.web.socket.WebSocketSession; + +@Component("customUserRegistry") +public class UserRegistry { + + private final ConcurrentHashMap usersById = new ConcurrentHashMap<>(); + private final ConcurrentHashMap usersBySessionId = new ConcurrentHashMap<>(); + + + public void register(UserSession user) { +// UserSession user = getBySessionId(sessionId); + usersById.put(user.getUserId(), user); + usersBySessionId.put(user.getSessionId(), user); + } + + public UserSession getByUserId(Long userId) { + return usersById.get(userId); + } + + public UserSession getBySessionId(String sessionId) { + return usersBySessionId.get(sessionId); + } + + public boolean exists(String name) { + return usersById.keySet().contains(name); + } + + public UserSession removeBySession(String sessionId) { + final UserSession user = getBySessionId(sessionId); + usersById.remove(user.getUserId()); + usersBySessionId.remove(sessionId); + return user; + } + +} \ No newline at end of file diff --git a/src/backend/signaling_server/src/main/java/com/jootalkpia/signaling_server/rtc/UserSession.java b/src/backend/signaling_server/src/main/java/com/jootalkpia/signaling_server/rtc/UserSession.java new file mode 100644 index 00000000..e6f07edd --- /dev/null +++ b/src/backend/signaling_server/src/main/java/com/jootalkpia/signaling_server/rtc/UserSession.java @@ -0,0 +1,243 @@ +package com.jootalkpia.signaling_server.rtc; + +import com.google.gson.JsonArray; +import java.io.Closeable; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.kurento.client.Continuation; +import org.kurento.client.EventListener; +import org.kurento.client.IceCandidate; +import org.kurento.client.IceCandidateFoundEvent; +import org.kurento.client.MediaPipeline; +import org.kurento.client.WebRtcEndpoint; +import org.kurento.jsonrpc.JsonUtils; +import org.springframework.messaging.simp.SimpMessagingTemplate; + +import com.google.gson.JsonObject; + +@Slf4j +@RequiredArgsConstructor +@Getter +public class UserSession implements Closeable { + + private final Long userId; + private final Long channelId; + private final String sessionId; + private final SimpMessagingTemplate messagingTemplate; + private final MediaPipeline pipeline; + private final WebRtcEndpoint outgoingMedia; + private final ConcurrentMap incomingMedia = new ConcurrentHashMap<>(); + + public UserSession(final Long userId, Long channelId, String sessionId, + MediaPipeline pipeline, SimpMessagingTemplate messagingTemplate) { + + this.pipeline = pipeline; + this.userId = userId; + this.sessionId = sessionId; + this.channelId = channelId; + this.outgoingMedia = new WebRtcEndpoint.Builder(pipeline).build(); + this.messagingTemplate = messagingTemplate; + + this.outgoingMedia.addIceCandidateFoundListener(new EventListener() { + + @Override + public void onEvent(IceCandidateFoundEvent event) { + JsonObject response = new JsonObject(); + response.addProperty("id", "iceCandidate"); + response.addProperty("senderId", userId); + response.add("candidate", JsonUtils.toJsonObject(event.getCandidate())); + + sendMessage(response); + } + }); + } + + public void receiveVideoFrom(UserSession sender, String sdpOffer) { + log.info("USER {}: connecting with {} in room {}", this.userId, sender.getUserId(), this.channelId); + + log.trace("USER {}: SdpOffer for {} is {}", this.userId, sender.getUserId(), sdpOffer); + + final String ipSdpAnswer = this.getEndpointForUser(sender).processOffer(sdpOffer); + final JsonObject scParams = new JsonObject(); + scParams.addProperty("id", "receiveVideoAnswer"); + scParams.addProperty("senderId", sender.getUserId()); + scParams.addProperty("sdpAnswer", ipSdpAnswer); + + log.trace("USER {}: SdpAnswer for {} is {}", this.userId, sender.getUserId(), ipSdpAnswer); + this.sendPrivateMessage(userId, scParams); + log.debug("gather candidates"); + this.getEndpointForUser(sender).gatherCandidates(); + } + + private WebRtcEndpoint getEndpointForUser(final UserSession sender) { + if (sender.getUserId().equals(userId)) { + log.debug("PARTICIPANT {}: configuring loopback", this.userId); + return outgoingMedia; + } + + log.debug("PARTICIPANT {}: receiving video from {}", this.userId, sender.getUserId()); + + WebRtcEndpoint incoming = incomingMedia.get(sender.getUserId()); + if (incoming == null) { + log.debug("PARTICIPANT {}: creating new endpoint for {}", this.userId, sender.getUserId()); + incoming = new WebRtcEndpoint.Builder(pipeline).build(); + + incoming.addIceCandidateFoundListener(new EventListener() { + + @Override + public void onEvent(IceCandidateFoundEvent event) { + JsonObject response = new JsonObject(); + response.addProperty("id", "iceCandidate"); + response.addProperty("senderId", sender.getUserId()); + response.add("candidate", JsonUtils.toJsonObject(event.getCandidate())); + sendMessage(response); + } + }); + + incomingMedia.put(sender.getUserId(), incoming); + } + + log.debug("PARTICIPANT {}: obtained endpoint for {}", this.userId, sender.getUserId()); + sender.getOutgoingMedia().connect(incoming); + + return incoming; + } + + public void cancelVideoFrom(final UserSession sender) { + this.cancelVideoFrom(sender.getUserId()); + } + + public void cancelVideoFrom(final Long senderId) { + log.debug("PARTICIPANT {}: canceling video reception from {}", this.userId, senderId); + final WebRtcEndpoint incoming = incomingMedia.remove(senderId); + + log.debug("PARTICIPANT {}: removing endpoint for {}", this.userId, senderId); + incoming.release(new Continuation() { + @Override + public void onSuccess(Void result) throws Exception { + log.trace("PARTICIPANT {}: Released successfully incoming EP for {}", + UserSession.this.userId, senderId); + } + + @Override + public void onError(Throwable cause) throws Exception { + log.warn("PARTICIPANT {}: Could not release incoming EP for {}", UserSession.this.userId, + senderId); + } + }); + } + + @Override + public void close() { + log.debug("PARTICIPANT {}: Releasing resources", this.userId); + for (final Long remoteParticipantName : incomingMedia.keySet()) { + + log.trace("PARTICIPANT {}: Released incoming EP for {}", this.userId, remoteParticipantName); + + final WebRtcEndpoint ep = this.incomingMedia.get(remoteParticipantName); + + ep.release(new Continuation() { + + @Override + public void onSuccess(Void result) throws Exception { + log.trace("PARTICIPANT {}: Released successfully incoming EP for {}", + UserSession.this.userId, remoteParticipantName); + } + + @Override + public void onError(Throwable cause) throws Exception { + log.warn("PARTICIPANT {}: Could not release incoming EP for {}", UserSession.this.userId, + remoteParticipantName); + } + }); + } + + outgoingMedia.release(new Continuation() { + + @Override + public void onSuccess(Void result) throws Exception { + log.trace("PARTICIPANT {}: Released outgoing EP", UserSession.this.userId); + } + + @Override + public void onError(Throwable cause) throws Exception { + log.warn("USER {}: Could not release outgoing EP", UserSession.this.userId); + } + }); + } + + public void sendMessage(JsonObject message) { + String destination = "/topic/huddle/" + channelId; + messagingTemplate.convertAndSend(destination, message.toString()); + log.info("📡 Sent STOMP message to {}: {}", destination, message); + } + + // 개별 사용자에게 응답 전송 (경로에 userId를 붙임) + public void sendPrivateMessage(Long userId, JsonObject message) { + String destination = "/queue/private/" + userId; + messagingTemplate.convertAndSend(destination, message.toString()); + log.info("📡 Sent private STOMP message to {}: {}", destination, message); + } + + + public void sendParticipantMessage(Long userId, JsonArray participantsArray) { + String destination = "/topic/huddle/" + channelId; + + final JsonObject existingParticipantsMsg = new JsonObject(); + existingParticipantsMsg.addProperty("id", "existingParticipants"); + existingParticipantsMsg.add("data", participantsArray); + + log.debug("PARTICIPANT {}: sending a list of {} participants", userId, participantsArray.size()); + + messagingTemplate.convertAndSend(destination, existingParticipantsMsg.toString()); + } + + + public void addCandidate(IceCandidate candidate, Long userId) { + if (this.userId.compareTo(userId) == 0) { + outgoingMedia.addIceCandidate(candidate); + } else { + WebRtcEndpoint webRtc = incomingMedia.get(userId); + if (webRtc != null) { + webRtc.addIceCandidate(candidate); + } + } + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(Object obj) { + + if (this == obj) { + return true; + } + if (obj == null || !(obj instanceof UserSession)) { + return false; + } + UserSession other = (UserSession) obj; + boolean eq = userId.equals(other.userId); + eq &= channelId.equals(other.channelId); + return eq; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + int result = 1; + result = 31 * result + userId.hashCode(); + result = 31 * result + channelId.hashCode(); + return result; + } +} diff --git a/src/backend/signaling_server/src/main/java/com/jootalkpia/signaling_server/rtc/ValidationUtils.java b/src/backend/signaling_server/src/main/java/com/jootalkpia/signaling_server/rtc/ValidationUtils.java new file mode 100644 index 00000000..635341ec --- /dev/null +++ b/src/backend/signaling_server/src/main/java/com/jootalkpia/signaling_server/rtc/ValidationUtils.java @@ -0,0 +1,136 @@ +//package com.jootalkpia.signaling_server.rtc; +// +//import com.jootalkpia.signaling_server.exception.common.CustomException; +//import com.jootalkpia.signaling_server.exception.common.ErrorCode; +//import com.jootalkpia.signaling_server.repository.ChannelHuddleRepository; +//import com.jootalkpia.signaling_server.repository.HuddleCacheRepository; +//import com.jootalkpia.signaling_server.repository.HuddleParticipantsRepository; +//import com.jootalkpia.signaling_server.repository.HuddlePipelineRepository; +//import com.jootalkpia.signaling_server.repository.UserHuddleRepository; +//import java.util.Set; +//import lombok.RequiredArgsConstructor; +//import lombok.extern.slf4j.Slf4j; +//import org.springframework.stereotype.Component; +// +//@Component +//@RequiredArgsConstructor +//@Slf4j +//public class ValidationUtils { +// private final UserHuddleRepository userHuddleRepository; +// private final HuddleParticipantsRepository huddleParticipantsRepository; +// private final HuddleCacheRepository huddleCacheRepository; +// private final HuddlePipelineRepository huddlePipelineRepository; +// private final ChannelHuddleRepository channelHuddleRepository; +// +// public void canUserJoinHuddle(String huddleId, Long userId) { +// try { +// if (userHuddleRepository.getUserHuddle(userId) != null) { +// throw new CustomException(ErrorCode.VALIDATION_FAILED.getCode(), "유저는 하나의 허들에만 참여할 수 있습니다."); +// } +// if (huddleCacheRepository.getHuddleById(huddleId) == null) { +// throw new CustomException(ErrorCode.HUDDLE_NOT_FOUND.getCode(), ErrorCode.HUDDLE_NOT_FOUND.getMsg()); +// } +// } catch (Exception e) { +// throw new CustomException(ErrorCode.UNEXPECTED_ERROR.getCode(), "유저가 허들 참여 가능한지 검증 중 오류 발생"); +// } +// } +// +// public void isHuddleValid(String huddleId) { +// try { +// if (huddleCacheRepository.getHuddleById(huddleId) == null) { +// throw new CustomException(ErrorCode.HUDDLE_NOT_FOUND.getCode(), ErrorCode.HUDDLE_NOT_FOUND.getMsg()); +// } +// } catch (Exception e) { +// throw new CustomException(ErrorCode.UNEXPECTED_ERROR.getCode(), "허들 검증 중 오류 발생"); +// } +// } +// +// public String isHuddleInChannel(Long channelId) { +//// try { +// String huddleId = channelHuddleRepository.getHuddleByChannel(channelId); +// if (huddleId == null) { +// throw new CustomException(ErrorCode.HUDDLE_NOT_IN_CHANNEL.getCode(), ErrorCode.HUDDLE_NOT_IN_CHANNEL.getMsg()); +// } +// return huddleId; +//// } catch (Exception e) { +//// throw new CustomException(ErrorCode.UNEXPECTED_ERROR.getCode(), "채널-허들 검증 중 오류 발생"); +//// } +// } +// +// public void isUserInHuddle(String huddleId, Long userId) { +// try { +// Set participants = huddleParticipantsRepository.getParticipants(huddleId); +// if (participants == null || !participants.contains(userId)) { +// throw new CustomException(ErrorCode.USER_NOT_FOUND.getCode(), "해당 허들에 유저가 존재하지 않습니다."); +// } +// } catch (Exception e) { +// throw new CustomException(ErrorCode.UNEXPECTED_ERROR.getCode(), "허들 내 유저 검증 중 오류 발생"); +// } +// } +// +// public void canUserExitHuddle(String huddleId, Long userId) { +// try { +// if (huddleCacheRepository.getHuddleById(huddleId) == null) { +// throw new CustomException(ErrorCode.HUDDLE_NOT_FOUND.getCode(), ErrorCode.HUDDLE_NOT_FOUND.getMsg()); +// } +// } catch (Exception e) { +// throw new CustomException(ErrorCode.UNEXPECTED_ERROR.getCode(), "허들 나가기 검증 중 오류 발생"); +// } +// } +// +// public void isPipelineInChannel(String huddleId) { +// try { +// if (huddlePipelineRepository.getPipeline(huddleId) == null) { +// throw new CustomException(ErrorCode.PIPELINE_NOT_FOUND.getCode(), ErrorCode.PIPELINE_NOT_FOUND.getMsg()); +// } +// } catch (Exception e) { +// throw new CustomException(ErrorCode.UNEXPECTED_ERROR.getCode(), "파이프라인 검증 중 오류 발생"); +// } +// } +// +// // 허들:참여자 확인 후 삭제 +// public void removeParticipantIfExists(String huddleId, Long userId) { +// try { +// Set participants = huddleParticipantsRepository.getParticipants(huddleId); +// if (participants == null || !participants.contains(userId)) { +// log.warn("허들에 존재하지 않는 참가자 제거 시도: huddleId={}, userId={}", huddleId, userId); +// return; +// } +// huddleParticipantsRepository.removeParticipant(huddleId, userId); +// log.info("허들 참가자 삭제 완료: huddleId={}, userId={}", huddleId, userId); +// } catch (Exception e) { +// log.error("허들 참가자 삭제 중 오류 발생 (무시됨): huddleId={}, userId={}", huddleId, userId, e); +// } +// } +// +// // 허들:엔드포인트 확인 후 삭제 +// public void removeUserEndpointIfExists(String huddleId, Long userId) { +// try { +// String endpointId = huddleParticipantsRepository.getUserEndpoint(huddleId, userId); +// if (endpointId == null) { +// log.warn("해당 유저의 WebRTC 엔드포인트가 존재하지 않음: huddleId={}, userId={}", huddleId, userId); +// return; +// } +// huddleParticipantsRepository.removeUserEndpoint(huddleId, userId); +// log.info("WebRTC 엔드포인트 삭제 완료: huddleId={}, userId={}, endpointId={}", huddleId, userId, endpointId); +// } catch (Exception e) { +// log.error("WebRTC 엔드포인트 삭제 중 오류 발생 (무시됨): huddleId={}, userId={}", huddleId, userId, e); +// } +// } +// +// // 유저:허들 확인 후 삭제 +// public void removeUserHuddleIfExists(Long userId) { +// try { +// String huddleId = userHuddleRepository.getUserHuddle(userId); +// if (huddleId == null) { +// log.warn("해당 유저가 속한 허들이 존재하지 않음: userId={}", userId); +// return; +// } +// userHuddleRepository.removeUserHuddle(userId); +// log.info("유저의 허들 삭제 완료: userId={}, huddleId={}", userId, huddleId); +// } catch (Exception e) { +// log.error("유저 허들 삭제 중 오류 발생 (무시됨): userId={}", userId, e); +// } +// } +// +//} diff --git a/src/backend/signaling_server/src/main/java/com/jootalkpia/signaling_server/service/HuddleService.java b/src/backend/signaling_server/src/main/java/com/jootalkpia/signaling_server/service/HuddleService.java index 072594c2..da2fdddc 100644 --- a/src/backend/signaling_server/src/main/java/com/jootalkpia/signaling_server/service/HuddleService.java +++ b/src/backend/signaling_server/src/main/java/com/jootalkpia/signaling_server/service/HuddleService.java @@ -1,130 +1,130 @@ -package com.jootalkpia.signaling_server.service; - -import com.jootalkpia.signaling_server.exception.common.CustomException; -import com.jootalkpia.signaling_server.exception.common.ErrorCode; -import com.jootalkpia.signaling_server.model.Huddle; -import com.jootalkpia.signaling_server.repository.HuddleCacheRepository; -import com.jootalkpia.signaling_server.repository.HuddleParticipantsRepository; -import com.jootalkpia.signaling_server.repository.ChannelHuddleRepository; -import com.jootalkpia.signaling_server.repository.HuddlePipelineRepository; -import com.jootalkpia.signaling_server.repository.UserHuddleRepository; -import com.jootalkpia.signaling_server.util.ValidationUtils; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; - -import java.time.LocalDateTime; -import java.util.Set; - -@Service -@RequiredArgsConstructor -@Slf4j -public class HuddleService { - - private final HuddleCacheRepository huddleCacheRepository; - private final HuddleParticipantsRepository huddleParticipantsRepository; - private final ChannelHuddleRepository channelHuddleRepository; - private final UserHuddleRepository userHuddleRepository; - private final HuddlePipelineRepository huddlePipelineRepository; - private final ValidationUtils validationUtils; - - public Huddle createHuddle(Long channelId, Long userId) { - String existingHuddleId = channelHuddleRepository.getHuddleByChannel(channelId); - if (existingHuddleId != null) { - throw new IllegalStateException("해당 채널에 이미 허들이 존재합니다."); - } - - String huddleId = "huddle-" + System.currentTimeMillis(); - Huddle huddle = new Huddle(huddleId, channelId, userId, LocalDateTime.now()); - - // 레디스에 허들 메타데이터(허들아이디, 채널아이디, 만든유저아이디, 생성시간) 저장 - huddleCacheRepository.saveHuddle(huddle); - - return huddle; - } - - public void saveHuddleChannel(Long channelId, String huddleId) { - channelHuddleRepository.saveChannelHuddle(channelId, huddleId); - } - - public void addUserHuddle(Long userId, String huddleId) { - userHuddleRepository.saveUserHuddle(userId, huddleId); - } - - public void saveHuddleParticipant(Long userId, String huddleId) { - huddleParticipantsRepository.addParticipant(huddleId, userId); - } - - public void deleteHuddle(String huddleId) { - Huddle huddle = huddleCacheRepository.getHuddleById(huddleId); - if (huddle == null) { - log.warn("삭제하려는 허들이 이미 없음: huddleId={}", huddleId); - return; - } - - log.info("허들 삭제 진행: {}", huddleId); - - // Redis에서 허들 삭제 - huddleCacheRepository.deleteHuddle(huddleId); - - // 채널-허들 매핑 삭제 (채널 ID는 Long 타입이어야 함) - if (huddle.channelId() != null) { - channelHuddleRepository.deleteChannelHuddle(huddle.channelId()); // Long 타입 전달 - } - } - - public int getParticipantCount(String huddleId) { - Set participants = huddleParticipantsRepository.getParticipants(huddleId); - log.info("참가자 수: {}", participants.size()); - - return participants == null ? 0 : participants.size(); - } - - public void recoverIfErrorJoining(Long userId, Long channelId) { - try { - // 채널이 허들에 존재하는지 확인하고 허들 ID 가져오기 - String huddleId = validationUtils.isHuddleInChannel(channelId); - - // 허들:참여자 확인 후 삭제 - validationUtils.removeParticipantIfExists(huddleId, userId); - - // 허들:엔드포인트 확인 후 삭제 - validationUtils.removeUserEndpointIfExists(huddleId, userId); - - // 유저:허들 확인 후 삭제 - validationUtils.removeUserHuddleIfExists(userId); - - // 허들에 남은 참가자가 없으면 삭제 - if (huddleParticipantsRepository.getParticipants(huddleId).isEmpty()) { - // 허들 데이터 확인 후 삭제 - String storedHuddleId = channelHuddleRepository.getHuddleByChannel(channelId); - if (storedHuddleId != null && storedHuddleId.equals(huddleId)) { - channelHuddleRepository.deleteChannelHuddle(channelId); - log.info("채널-허들 매핑 삭제 완료: channelId={}, huddleId={}", channelId, huddleId); - } - - // 허들:파이프라인 확인 후 삭제 - String pipelineId = huddlePipelineRepository.getPipelineId(huddleId); - if (pipelineId != null) { - huddlePipelineRepository.deleteHuddlePipeline(huddleId); - log.info("허들-파이프라인 삭제 완료: huddleId={}, pipelineId={}", huddleId, pipelineId); - } - - // 채널:허들 확인 후 삭제 - String storedChannelHuddle = channelHuddleRepository.getHuddleByChannel(channelId); - if (storedChannelHuddle != null && storedChannelHuddle.equals(huddleId)) { - channelHuddleRepository.deleteChannelHuddle(channelId); - log.info("채널-허들 데이터 삭제 완료: channelId={}", channelId); - } - - // 허들에 남은 참가자가 없으면 최종 삭제 - if (getParticipantCount(huddleId) == 0) { - deleteHuddle(huddleId); - log.info("참여자가 없어 허들 삭제: {}", huddleId); - } - } - } catch (Exception e) { - throw new CustomException(ErrorCode.UNEXPECTED_ERROR.getCode(), "허들 복구 과정 중 오류 발생"); - } - } -} +//package com.jootalkpia.signaling_server.service; +// +//import com.jootalkpia.signaling_server.exception.common.CustomException; +//import com.jootalkpia.signaling_server.exception.common.ErrorCode; +//import com.jootalkpia.signaling_server.model.Huddle; +//import com.jootalkpia.signaling_server.repository.HuddleCacheRepository; +//import com.jootalkpia.signaling_server.repository.HuddleParticipantsRepository; +//import com.jootalkpia.signaling_server.repository.ChannelHuddleRepository; +//import com.jootalkpia.signaling_server.repository.HuddlePipelineRepository; +//import com.jootalkpia.signaling_server.repository.UserHuddleRepository; +//import com.jootalkpia.signaling_server.rtc.ValidationUtils; +//import lombok.RequiredArgsConstructor; +//import lombok.extern.slf4j.Slf4j; +//import org.springframework.stereotype.Service; +// +//import java.time.LocalDateTime; +//import java.util.Set; +// +//@Service +//@RequiredArgsConstructor +//@Slf4j +//public class HuddleService { +// +// private final HuddleCacheRepository huddleCacheRepository; +// private final HuddleParticipantsRepository huddleParticipantsRepository; +// private final ChannelHuddleRepository channelHuddleRepository; +// private final UserHuddleRepository userHuddleRepository; +// private final HuddlePipelineRepository huddlePipelineRepository; +// private final ValidationUtils validationUtils; +// +// public Huddle createHuddle(Long channelId, Long userId) { +// String existingHuddleId = channelHuddleRepository.getHuddleByChannel(channelId); +// if (existingHuddleId != null) { +// throw new IllegalStateException("해당 채널에 이미 허들이 존재합니다."); +// } +// +// String huddleId = "huddle-" + System.currentTimeMillis(); +// Huddle huddle = new Huddle(huddleId, channelId, userId, LocalDateTime.now()); +// +// // 레디스에 허들 메타데이터(허들아이디, 채널아이디, 만든유저아이디, 생성시간) 저장 +// huddleCacheRepository.saveHuddle(huddle); +// +// return huddle; +// } +// +// public void saveHuddleChannel(Long channelId, String huddleId) { +// channelHuddleRepository.saveChannelHuddle(channelId, huddleId); +// } +// +// public void addUserHuddle(Long userId, String huddleId) { +// userHuddleRepository.saveUserHuddle(userId, huddleId); +// } +// +// public void saveHuddleParticipant(Long userId, String huddleId) { +// huddleParticipantsRepository.addParticipant(huddleId, userId); +// } +// +// public void deleteHuddle(String huddleId) { +// Huddle huddle = huddleCacheRepository.getHuddleById(huddleId); +// if (huddle == null) { +// log.warn("삭제하려는 허들이 이미 없음: huddleId={}", huddleId); +// return; +// } +// +// log.info("허들 삭제 진행: {}", huddleId); +// +// // Redis에서 허들 삭제 +// huddleCacheRepository.deleteHuddle(huddleId); +// +// // 채널-허들 매핑 삭제 (채널 ID는 Long 타입이어야 함) +// if (huddle.channelId() != null) { +// channelHuddleRepository.deleteChannelHuddle(huddle.channelId()); // Long 타입 전달 +// } +// } +// +// public int getParticipantCount(String huddleId) { +// Set participants = huddleParticipantsRepository.getParticipants(huddleId); +// log.info("참가자 수: {}", participants.size()); +// +// return participants == null ? 0 : participants.size(); +// } +// +// public void recoverIfErrorJoining(Long userId, Long channelId) { +// try { +// // 채널이 허들에 존재하는지 확인하고 허들 ID 가져오기 +// String huddleId = validationUtils.isHuddleInChannel(channelId); +// +// // 허들:참여자 확인 후 삭제 +// validationUtils.removeParticipantIfExists(huddleId, userId); +// +// // 허들:엔드포인트 확인 후 삭제 +// validationUtils.removeUserEndpointIfExists(huddleId, userId); +// +// // 유저:허들 확인 후 삭제 +// validationUtils.removeUserHuddleIfExists(userId); +// +// // 허들에 남은 참가자가 없으면 삭제 +// if (huddleParticipantsRepository.getParticipants(huddleId).isEmpty()) { +// // 허들 데이터 확인 후 삭제 +// String storedHuddleId = channelHuddleRepository.getHuddleByChannel(channelId); +// if (storedHuddleId != null && storedHuddleId.equals(huddleId)) { +// channelHuddleRepository.deleteChannelHuddle(channelId); +// log.info("채널-허들 매핑 삭제 완료: channelId={}, huddleId={}", channelId, huddleId); +// } +// +// // 허들:파이프라인 확인 후 삭제 +// String pipelineId = huddlePipelineRepository.getPipelineId(huddleId); +// if (pipelineId != null) { +// huddlePipelineRepository.deleteHuddlePipeline(huddleId); +// log.info("허들-파이프라인 삭제 완료: huddleId={}, pipelineId={}", huddleId, pipelineId); +// } +// +// // 채널:허들 확인 후 삭제 +// String storedChannelHuddle = channelHuddleRepository.getHuddleByChannel(channelId); +// if (storedChannelHuddle != null && storedChannelHuddle.equals(huddleId)) { +// channelHuddleRepository.deleteChannelHuddle(channelId); +// log.info("채널-허들 데이터 삭제 완료: channelId={}", channelId); +// } +// +// // 허들에 남은 참가자가 없으면 최종 삭제 +// if (getParticipantCount(huddleId) == 0) { +// deleteHuddle(huddleId); +// log.info("참여자가 없어 허들 삭제: {}", huddleId); +// } +// } +// } catch (Exception e) { +// throw new CustomException(ErrorCode.UNEXPECTED_ERROR.getCode(), "허들 복구 과정 중 오류 발생"); +// } +// } +//} diff --git a/src/backend/signaling_server/src/main/java/com/jootalkpia/signaling_server/service/KurentoService.java b/src/backend/signaling_server/src/main/java/com/jootalkpia/signaling_server/service/KurentoService.java index fa889837..7bfd60e4 100644 --- a/src/backend/signaling_server/src/main/java/com/jootalkpia/signaling_server/service/KurentoService.java +++ b/src/backend/signaling_server/src/main/java/com/jootalkpia/signaling_server/service/KurentoService.java @@ -1,227 +1,227 @@ -package com.jootalkpia.signaling_server.service; - -import com.jootalkpia.signaling_server.exception.common.CustomException; -import com.jootalkpia.signaling_server.exception.common.ErrorCode; -import com.jootalkpia.signaling_server.repository.HuddleParticipantsRepository; -import com.jootalkpia.signaling_server.repository.HuddlePipelineRepository; -import java.util.Collection; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import javax.print.attribute.standard.Media; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.kurento.client.IceCandidate; -import org.kurento.client.KurentoClient; -import org.kurento.client.MediaElement; -import org.kurento.client.MediaObject; -import org.kurento.client.MediaPipeline; -import org.kurento.client.WebRtcEndpoint; -import org.springframework.data.redis.core.StringRedisTemplate; -import org.springframework.stereotype.Service; - -import java.util.Set; - -@Service -@RequiredArgsConstructor -@Slf4j -public class KurentoService { - - private final KurentoClient kurentoClient; - private final HuddleService huddleService; - private final HuddleParticipantsRepository huddleParticipantsRepository; - private final HuddlePipelineRepository huddlePipelineRepository; - private final StringRedisTemplate redisTemplate; - - // KurentoRoom 생성 - public void createPipeline(String huddleId) { - if (huddlePipelineRepository.getPipeline(huddleId) != null) { - throw new IllegalStateException("이미 허들-파이프라인이 존재합니다."); - } - - String pipelineId = kurentoClient.createMediaPipeline().getId(); - huddlePipelineRepository.saveHuddlePipeline(huddleId, pipelineId); - - // Redis에 올바르게 저장되었는지 검증 - String savedPipelineId = huddlePipelineRepository.getPipelineId(huddleId); - if (savedPipelineId == null || !savedPipelineId.equals(pipelineId)) { - throw new CustomException(ErrorCode.PIPELINE_NOT_FOUND.getCode(), "파이프라인이 정상적으로 저장되지 않았습니다."); - } - } - - // 방 정보 조회 - public MediaPipeline getPipeline(String huddleId) { - return huddlePipelineRepository.getPipeline(huddleId); - } - - // 참가자 추가 - public WebRtcEndpoint addParticipantToRoom(String huddleId, Long userId) { - log.info("add participant to room in kurento service"); - // 해당 허들의 pipelineId 가져오기 - String pipelineId = redisTemplate.opsForValue().get("huddle:" + huddleId + ":pipeline"); - - if (pipelineId == null) { - throw new IllegalStateException("해당 huddleId=" + huddleId + "에 대한 MediaPipeline ID를 찾을 수 없습니다."); - } - - // pipelineId를 이용하여 MediaPipeline 복원 - MediaPipeline pipeline = kurentoClient.getById(pipelineId, MediaPipeline.class); - if (pipeline == null) { - throw new IllegalStateException("Kurento에서 pipelineId=" + pipelineId + "를 찾을 수 없습니다."); - } - - log.info("🍎🍎🍎🍎🍎🍎"); - - // WebRTC 엔드포인트 생성 및 해당 파이프라인에 추가 - WebRtcEndpoint webRtcEndpoint = new WebRtcEndpoint.Builder(pipeline).build(); - - log.info("add participant to room in kurento service: 엔포 생성 및 파이프라인에 추가"); - - List mediaObjects = pipeline.getChildren(); - int webRtcEndpointCount = 0; - - for (MediaObject obj : mediaObjects) { - if (obj instanceof MediaElement) { // MediaElement인지 확인 - MediaElement element = (MediaElement) obj; // 안전한 다운캐스팅 - if (element instanceof WebRtcEndpoint) { - webRtcEndpointCount++; - } - } - } - - log.info("😄😄😄😄😄현재 허들 " + huddleId + "에 연결된 WebRtcEndpoint 개수: " + webRtcEndpointCount); - - - // 허들:참가자 저장 - huddleService.saveHuddleParticipant(userId, huddleId); - - log.info("add participant to room in kurento service: 허들:참가자 저장"); - - // 허들:엔드포인트 저장 - huddleParticipantsRepository.saveUserEndpoint(huddleId, userId, webRtcEndpoint.getId()); - log.info("add participant to room in kurento service: 허들 엔포 저장"); - - return webRtcEndpoint; - } - - - public WebRtcEndpoint getParticipantEndpoint(String huddleId, Long userId) { - if (huddleId == null) { - throw new CustomException(ErrorCode.HUDDLE_NOT_FOUND.getCode(), "허들 ID가 null입니다."); - } - - // 저장된 엔드포인트 ID 가져오기 - String endpointId = huddleParticipantsRepository.getUserEndpoint(huddleId, userId); - - if (endpointId == null) { - log.error("Redis에서 엔드포인트 조회 실패: userId={}", userId); - throw new CustomException(ErrorCode.ENDPOINT_NOT_FOUND.getCode(), ErrorCode.ENDPOINT_NOT_FOUND.getMsg()); - } - - // 엔드포인트 ID를 이용하여 WebRtcEndpoint 복원 - WebRtcEndpoint endpoint = kurentoClient.getById(endpointId, WebRtcEndpoint.class); - - if (endpoint == null) { - log.warn("Kurento에서 엔드포인트 ID={} 를 찾을 수 없음: huddleId={}, userId={}", endpointId, huddleId, userId); - throw new CustomException(ErrorCode.ENDPOINT_NOT_FOUND.getCode(), ErrorCode.ENDPOINT_NOT_FOUND.getMsg()); - } - - // ICE Candidate 감지 이벤트 리스너 추가 - endpoint.addIceCandidateFoundListener(event -> { - IceCandidate candidate = event.getCandidate(); - log.info("ICE Candidate found for user {} in huddle {}: {}", userId, huddleId, candidate.getCandidate()); - }); - - // 대신 ICE Candidate 감지 이벤트 리스너 추가 - endpoint.addIceCandidateFoundListener(event -> { - log.info("ICE Candidate found for user {}: {}", userId, event.getCandidate().getCandidate()); - }); - - return endpoint; - } - - public Map getParticipants(String huddleId) { - Map participantsMap = new HashMap<>(); - - // 해당 huddleId의 참가자 목록 조회 - Set participantsIds = huddleParticipantsRepository.getParticipants(huddleId); - - for (Long userId : participantsIds) { - // 유저의 엔드포인트 ID 조회 - String endpointId = huddleParticipantsRepository.getUserEndpoint(huddleId, userId); - - if (endpointId != null) { - // 엔드포인트 ID를 이용하여 WebRtcEndpoint 복원 - WebRtcEndpoint endpoint = kurentoClient.getById(endpointId, WebRtcEndpoint.class); - - if (endpoint != null) { - participantsMap.put(userId, endpoint); - } else { - log.warn("WebRtcEndpoint not found for user {} in huddle {}", userId, huddleId); - } - } else { - log.warn("Endpoint ID not found for user {} in huddle {}", userId, huddleId); - } - } - - return participantsMap; - } - - - - // 참가자 제거 - public void removeParticipantFromRoom(String huddleId, Long userId) { - MediaPipeline pipeline = getPipeline(huddleId); - if (pipeline == null) { - log.warn("파이프라인을 찾을 수 없습니다: huddleId={}", huddleId); - return; - } - - // 유효한 참가자인지 확인 - Set participants = huddleParticipantsRepository.getParticipants(huddleId); - if (participants == null || !participants.contains(userId)) { - log.warn("유효하지 않은 참가자 제거 시도: huddleId={}, userId={}", huddleId, userId); - return; - } - - // WebRTC 엔드포인트 제거 - huddleParticipantsRepository.removeUserEndpoint(huddleId, userId); - - // 참가자 정보 삭제 - huddleParticipantsRepository.removeParticipant(huddleId, userId); - huddleParticipantsRepository.removeUserEndpoint(huddleId, userId); - log.info("참가자 제거 완료: huddleId={}, userId={}", huddleId, userId); - - // 허들에 남아 있는 참가자 수 확인 후 방 삭제 - if (huddleParticipantsRepository.getParticipants(huddleId).isEmpty()) { - removeRoom(huddleId); - } - } - - // 방 삭제, 파이프라인 해제 - public void removeRoom(String huddleId) { - MediaPipeline pipeline = getPipeline(huddleId); - if (pipeline == null) { - log.warn("파이프라인을 찾을 수 없습니다: huddleId={}", huddleId); - return; - } - - Set participants = huddleParticipantsRepository.getParticipants(huddleId); - if (participants != null) { - for (Long userId : participants) { - huddleParticipantsRepository.removeUserEndpoint(huddleId, userId); - } - } - - // MediaPipeline 해제 - try { - pipeline.release(); - log.info("MediaPipeline 삭제 완료: huddleId={}, pipelineId={}", huddleId, pipeline.getId()); - } catch (Exception e) { - log.error("파이프라인 삭제 중 오류 발생: huddleId={}, pipelineId={}", huddleId, pipeline.getId(), e); - } - - log.info("KurentoRoom 삭제 완료: huddleId={}", huddleId); - } - -} +//package com.jootalkpia.signaling_server.service; +// +//import com.jootalkpia.signaling_server.exception.common.CustomException; +//import com.jootalkpia.signaling_server.exception.common.ErrorCode; +//import com.jootalkpia.signaling_server.repository.HuddleParticipantsRepository; +//import com.jootalkpia.signaling_server.repository.HuddlePipelineRepository; +//import java.util.Collection; +//import java.util.HashMap; +//import java.util.List; +//import java.util.Map; +//import javax.print.attribute.standard.Media; +//import lombok.RequiredArgsConstructor; +//import lombok.extern.slf4j.Slf4j; +//import org.kurento.client.IceCandidate; +//import org.kurento.client.KurentoClient; +//import org.kurento.client.MediaElement; +//import org.kurento.client.MediaObject; +//import org.kurento.client.MediaPipeline; +//import org.kurento.client.WebRtcEndpoint; +//import org.springframework.data.redis.core.StringRedisTemplate; +//import org.springframework.stereotype.Service; +// +//import java.util.Set; +// +//@Service +//@RequiredArgsConstructor +//@Slf4j +//public class KurentoService { +// +// private final KurentoClient kurentoClient; +// private final HuddleService huddleService; +// private final HuddleParticipantsRepository huddleParticipantsRepository; +// private final HuddlePipelineRepository huddlePipelineRepository; +// private final StringRedisTemplate redisTemplate; +// +// // KurentoRoom 생성 +// public void createPipeline(String huddleId) { +// if (huddlePipelineRepository.getPipeline(huddleId) != null) { +// throw new IllegalStateException("이미 허들-파이프라인이 존재합니다."); +// } +// +// String pipelineId = kurentoClient.createMediaPipeline().getId(); +// huddlePipelineRepository.saveHuddlePipeline(huddleId, pipelineId); +// +// // Redis에 올바르게 저장되었는지 검증 +// String savedPipelineId = huddlePipelineRepository.getPipelineId(huddleId); +// if (savedPipelineId == null || !savedPipelineId.equals(pipelineId)) { +// throw new CustomException(ErrorCode.PIPELINE_NOT_FOUND.getCode(), "파이프라인이 정상적으로 저장되지 않았습니다."); +// } +// } +// +// // 방 정보 조회 +// public MediaPipeline getPipeline(String huddleId) { +// return huddlePipelineRepository.getPipeline(huddleId); +// } +// +// // 참가자 추가 +// public WebRtcEndpoint addParticipantToRoom(String huddleId, Long userId) { +// log.info("add participant to room in kurento service"); +// // 해당 허들의 pipelineId 가져오기 +// String pipelineId = redisTemplate.opsForValue().get("huddle:" + huddleId + ":pipeline"); +// +// if (pipelineId == null) { +// throw new IllegalStateException("해당 huddleId=" + huddleId + "에 대한 MediaPipeline ID를 찾을 수 없습니다."); +// } +// +// // pipelineId를 이용하여 MediaPipeline 복원 +// MediaPipeline pipeline = kurentoClient.getById(pipelineId, MediaPipeline.class); +// if (pipeline == null) { +// throw new IllegalStateException("Kurento에서 pipelineId=" + pipelineId + "를 찾을 수 없습니다."); +// } +// +// log.info("🍎🍎🍎🍎🍎🍎"); +// +// // WebRTC 엔드포인트 생성 및 해당 파이프라인에 추가 +// WebRtcEndpoint webRtcEndpoint = new WebRtcEndpoint.Builder(pipeline).build(); +// +// log.info("add participant to room in kurento service: 엔포 생성 및 파이프라인에 추가"); +// +// List mediaObjects = pipeline.getChildren(); +// int webRtcEndpointCount = 0; +// +// for (MediaObject obj : mediaObjects) { +// if (obj instanceof MediaElement) { // MediaElement인지 확인 +// MediaElement element = (MediaElement) obj; // 안전한 다운캐스팅 +// if (element instanceof WebRtcEndpoint) { +// webRtcEndpointCount++; +// } +// } +// } +// +// log.info("😄😄😄😄😄현재 허들 " + huddleId + "에 연결된 WebRtcEndpoint 개수: " + webRtcEndpointCount); +// +// +// // 허들:참가자 저장 +// huddleService.saveHuddleParticipant(userId, huddleId); +// +// log.info("add participant to room in kurento service: 허들:참가자 저장"); +// +// // 허들:엔드포인트 저장 +// huddleParticipantsRepository.saveUserEndpoint(huddleId, userId, webRtcEndpoint.getId()); +// log.info("add participant to room in kurento service: 허들 엔포 저장"); +// +// return webRtcEndpoint; +// } +// +// +// public WebRtcEndpoint getParticipantEndpoint(String huddleId, Long userId) { +// if (huddleId == null) { +// throw new CustomException(ErrorCode.HUDDLE_NOT_FOUND.getCode(), "허들 ID가 null입니다."); +// } +// +// // 저장된 엔드포인트 ID 가져오기 +// String endpointId = huddleParticipantsRepository.getUserEndpoint(huddleId, userId); +// +// if (endpointId == null) { +// log.error("Redis에서 엔드포인트 조회 실패: userId={}", userId); +// throw new CustomException(ErrorCode.ENDPOINT_NOT_FOUND.getCode(), ErrorCode.ENDPOINT_NOT_FOUND.getMsg()); +// } +// +// // 엔드포인트 ID를 이용하여 WebRtcEndpoint 복원 +// WebRtcEndpoint endpoint = kurentoClient.getById(endpointId, WebRtcEndpoint.class); +// +// if (endpoint == null) { +// log.warn("Kurento에서 엔드포인트 ID={} 를 찾을 수 없음: huddleId={}, userId={}", endpointId, huddleId, userId); +// throw new CustomException(ErrorCode.ENDPOINT_NOT_FOUND.getCode(), ErrorCode.ENDPOINT_NOT_FOUND.getMsg()); +// } +// +// // ICE Candidate 감지 이벤트 리스너 추가 +// endpoint.addIceCandidateFoundListener(event -> { +// IceCandidate candidate = event.getCandidate(); +// log.info("ICE Candidate found for user {} in huddle {}: {}", userId, huddleId, candidate.getCandidate()); +// }); +// +// // 대신 ICE Candidate 감지 이벤트 리스너 추가 +// endpoint.addIceCandidateFoundListener(event -> { +// log.info("ICE Candidate found for user {}: {}", userId, event.getCandidate().getCandidate()); +// }); +// +// return endpoint; +// } +// +// public Map getParticipants(String huddleId) { +// Map participantsMap = new HashMap<>(); +// +// // 해당 huddleId의 참가자 목록 조회 +// Set participantsIds = huddleParticipantsRepository.getParticipants(huddleId); +// +// for (Long userId : participantsIds) { +// // 유저의 엔드포인트 ID 조회 +// String endpointId = huddleParticipantsRepository.getUserEndpoint(huddleId, userId); +// +// if (endpointId != null) { +// // 엔드포인트 ID를 이용하여 WebRtcEndpoint 복원 +// WebRtcEndpoint endpoint = kurentoClient.getById(endpointId, WebRtcEndpoint.class); +// +// if (endpoint != null) { +// participantsMap.put(userId, endpoint); +// } else { +// log.warn("WebRtcEndpoint not found for user {} in huddle {}", userId, huddleId); +// } +// } else { +// log.warn("Endpoint ID not found for user {} in huddle {}", userId, huddleId); +// } +// } +// +// return participantsMap; +// } +// +// +// +// // 참가자 제거 +// public void removeParticipantFromRoom(String huddleId, Long userId) { +// MediaPipeline pipeline = getPipeline(huddleId); +// if (pipeline == null) { +// log.warn("파이프라인을 찾을 수 없습니다: huddleId={}", huddleId); +// return; +// } +// +// // 유효한 참가자인지 확인 +// Set participants = huddleParticipantsRepository.getParticipants(huddleId); +// if (participants == null || !participants.contains(userId)) { +// log.warn("유효하지 않은 참가자 제거 시도: huddleId={}, userId={}", huddleId, userId); +// return; +// } +// +// // WebRTC 엔드포인트 제거 +// huddleParticipantsRepository.removeUserEndpoint(huddleId, userId); +// +// // 참가자 정보 삭제 +// huddleParticipantsRepository.removeParticipant(huddleId, userId); +// huddleParticipantsRepository.removeUserEndpoint(huddleId, userId); +// log.info("참가자 제거 완료: huddleId={}, userId={}", huddleId, userId); +// +// // 허들에 남아 있는 참가자 수 확인 후 방 삭제 +// if (huddleParticipantsRepository.getParticipants(huddleId).isEmpty()) { +// removeRoom(huddleId); +// } +// } +// +// // 방 삭제, 파이프라인 해제 +// public void removeRoom(String huddleId) { +// MediaPipeline pipeline = getPipeline(huddleId); +// if (pipeline == null) { +// log.warn("파이프라인을 찾을 수 없습니다: huddleId={}", huddleId); +// return; +// } +// +// Set participants = huddleParticipantsRepository.getParticipants(huddleId); +// if (participants != null) { +// for (Long userId : participants) { +// huddleParticipantsRepository.removeUserEndpoint(huddleId, userId); +// } +// } +// +// // MediaPipeline 해제 +// try { +// pipeline.release(); +// log.info("MediaPipeline 삭제 완료: huddleId={}, pipelineId={}", huddleId, pipeline.getId()); +// } catch (Exception e) { +// log.error("파이프라인 삭제 중 오류 발생: huddleId={}, pipelineId={}", huddleId, pipeline.getId(), e); +// } +// +// log.info("KurentoRoom 삭제 완료: huddleId={}", huddleId); +// } +// +//} diff --git a/src/backend/signaling_server/src/main/java/com/jootalkpia/signaling_server/util/ValidationUtils.java b/src/backend/signaling_server/src/main/java/com/jootalkpia/signaling_server/util/ValidationUtils.java deleted file mode 100644 index c50061bb..00000000 --- a/src/backend/signaling_server/src/main/java/com/jootalkpia/signaling_server/util/ValidationUtils.java +++ /dev/null @@ -1,136 +0,0 @@ -package com.jootalkpia.signaling_server.util; - -import com.jootalkpia.signaling_server.exception.common.CustomException; -import com.jootalkpia.signaling_server.exception.common.ErrorCode; -import com.jootalkpia.signaling_server.repository.ChannelHuddleRepository; -import com.jootalkpia.signaling_server.repository.HuddleCacheRepository; -import com.jootalkpia.signaling_server.repository.HuddleParticipantsRepository; -import com.jootalkpia.signaling_server.repository.HuddlePipelineRepository; -import com.jootalkpia.signaling_server.repository.UserHuddleRepository; -import java.util.Set; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; - -@Component -@RequiredArgsConstructor -@Slf4j -public class ValidationUtils { - private final UserHuddleRepository userHuddleRepository; - private final HuddleParticipantsRepository huddleParticipantsRepository; - private final HuddleCacheRepository huddleCacheRepository; - private final HuddlePipelineRepository huddlePipelineRepository; - private final ChannelHuddleRepository channelHuddleRepository; - - public void canUserJoinHuddle(String huddleId, Long userId) { - try { - if (userHuddleRepository.getUserHuddle(userId) != null) { - throw new CustomException(ErrorCode.VALIDATION_FAILED.getCode(), "유저는 하나의 허들에만 참여할 수 있습니다."); - } - if (huddleCacheRepository.getHuddleById(huddleId) == null) { - throw new CustomException(ErrorCode.HUDDLE_NOT_FOUND.getCode(), ErrorCode.HUDDLE_NOT_FOUND.getMsg()); - } - } catch (Exception e) { - throw new CustomException(ErrorCode.UNEXPECTED_ERROR.getCode(), "유저가 허들 참여 가능한지 검증 중 오류 발생"); - } - } - - public void isHuddleValid(String huddleId) { - try { - if (huddleCacheRepository.getHuddleById(huddleId) == null) { - throw new CustomException(ErrorCode.HUDDLE_NOT_FOUND.getCode(), ErrorCode.HUDDLE_NOT_FOUND.getMsg()); - } - } catch (Exception e) { - throw new CustomException(ErrorCode.UNEXPECTED_ERROR.getCode(), "허들 검증 중 오류 발생"); - } - } - - public String isHuddleInChannel(Long channelId) { -// try { - String huddleId = channelHuddleRepository.getHuddleByChannel(channelId); - if (huddleId == null) { - throw new CustomException(ErrorCode.HUDDLE_NOT_IN_CHANNEL.getCode(), ErrorCode.HUDDLE_NOT_IN_CHANNEL.getMsg()); - } - return huddleId; -// } catch (Exception e) { -// throw new CustomException(ErrorCode.UNEXPECTED_ERROR.getCode(), "채널-허들 검증 중 오류 발생"); -// } - } - - public void isUserInHuddle(String huddleId, Long userId) { - try { - Set participants = huddleParticipantsRepository.getParticipants(huddleId); - if (participants == null || !participants.contains(userId)) { - throw new CustomException(ErrorCode.USER_NOT_FOUND.getCode(), "해당 허들에 유저가 존재하지 않습니다."); - } - } catch (Exception e) { - throw new CustomException(ErrorCode.UNEXPECTED_ERROR.getCode(), "허들 내 유저 검증 중 오류 발생"); - } - } - - public void canUserExitHuddle(String huddleId, Long userId) { - try { - if (huddleCacheRepository.getHuddleById(huddleId) == null) { - throw new CustomException(ErrorCode.HUDDLE_NOT_FOUND.getCode(), ErrorCode.HUDDLE_NOT_FOUND.getMsg()); - } - } catch (Exception e) { - throw new CustomException(ErrorCode.UNEXPECTED_ERROR.getCode(), "허들 나가기 검증 중 오류 발생"); - } - } - - public void isPipelineInChannel(String huddleId) { - try { - if (huddlePipelineRepository.getPipeline(huddleId) == null) { - throw new CustomException(ErrorCode.PIPELINE_NOT_FOUND.getCode(), ErrorCode.PIPELINE_NOT_FOUND.getMsg()); - } - } catch (Exception e) { - throw new CustomException(ErrorCode.UNEXPECTED_ERROR.getCode(), "파이프라인 검증 중 오류 발생"); - } - } - - // 허들:참여자 확인 후 삭제 - public void removeParticipantIfExists(String huddleId, Long userId) { - try { - Set participants = huddleParticipantsRepository.getParticipants(huddleId); - if (participants == null || !participants.contains(userId)) { - log.warn("허들에 존재하지 않는 참가자 제거 시도: huddleId={}, userId={}", huddleId, userId); - return; - } - huddleParticipantsRepository.removeParticipant(huddleId, userId); - log.info("허들 참가자 삭제 완료: huddleId={}, userId={}", huddleId, userId); - } catch (Exception e) { - log.error("허들 참가자 삭제 중 오류 발생 (무시됨): huddleId={}, userId={}", huddleId, userId, e); - } - } - - // 허들:엔드포인트 확인 후 삭제 - public void removeUserEndpointIfExists(String huddleId, Long userId) { - try { - String endpointId = huddleParticipantsRepository.getUserEndpoint(huddleId, userId); - if (endpointId == null) { - log.warn("해당 유저의 WebRTC 엔드포인트가 존재하지 않음: huddleId={}, userId={}", huddleId, userId); - return; - } - huddleParticipantsRepository.removeUserEndpoint(huddleId, userId); - log.info("WebRTC 엔드포인트 삭제 완료: huddleId={}, userId={}, endpointId={}", huddleId, userId, endpointId); - } catch (Exception e) { - log.error("WebRTC 엔드포인트 삭제 중 오류 발생 (무시됨): huddleId={}, userId={}", huddleId, userId, e); - } - } - - // 유저:허들 확인 후 삭제 - public void removeUserHuddleIfExists(Long userId) { - try { - String huddleId = userHuddleRepository.getUserHuddle(userId); - if (huddleId == null) { - log.warn("해당 유저가 속한 허들이 존재하지 않음: userId={}", userId); - return; - } - userHuddleRepository.removeUserHuddle(userId); - log.info("유저의 허들 삭제 완료: userId={}, huddleId={}", userId, huddleId); - } catch (Exception e) { - log.error("유저 허들 삭제 중 오류 발생 (무시됨): userId={}", userId, e); - } - } - -}