diff --git a/back/build.gradle.kts b/back/build.gradle.kts index ed959ef9..506d7da5 100644 --- a/back/build.gradle.kts +++ b/back/build.gradle.kts @@ -66,6 +66,8 @@ dependencies { testRuntimeOnly("org.junit.platform:junit-platform-launcher") implementation ("software.amazon.awssdk:s3:2.25.0") + + implementation ("org.springframework.kafka:spring-kafka") } tasks.withType { diff --git a/back/src/main/java/com/back/domain/file/video/controller/VideoController.java b/back/src/main/java/com/back/domain/file/video/controller/VideoController.java index 88d41e72..73189046 100644 --- a/back/src/main/java/com/back/domain/file/video/controller/VideoController.java +++ b/back/src/main/java/com/back/domain/file/video/controller/VideoController.java @@ -4,6 +4,7 @@ import com.back.domain.file.video.dto.service.PresignedUrlResponse; import com.back.domain.file.video.service.FileManager; import com.back.global.rsData.RsData; +import io.swagger.v3.oas.annotations.Operation; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; @@ -15,16 +16,18 @@ public class VideoController { private final FileManager fileManager; @GetMapping("/videos/upload") + @Operation(summary="업로드용 URL 요청", description="파일 업로드를 위한 Presigned URL을 발급받습니다.") public RsData getUploadUrl() { PresignedUrlResponse uploadUrl = fileManager.getUploadUrl(); - UploadUrlGetResponse response = new UploadUrlGetResponse(uploadUrl.url(), uploadUrl.expiresAt()); + UploadUrlGetResponse response = new UploadUrlGetResponse(uploadUrl.url().toString(), uploadUrl.expiresAt()); return new RsData<>("200", "업로드용 URL 요청완료", response); } @GetMapping("/videos/download") + @Operation(summary="다운로드용 URL 요청", description="파일 다운로드를 위한 Presigned URL을 발급받습니다.") public RsData getDownloadUrls(@RequestParam String objectKey) { PresignedUrlResponse downloadUrl = fileManager.getDownloadUrl(objectKey); - UploadUrlGetResponse response = new UploadUrlGetResponse(downloadUrl.url(), downloadUrl.expiresAt()); + UploadUrlGetResponse response = new UploadUrlGetResponse(downloadUrl.url().toString(), downloadUrl.expiresAt()); return new RsData<>("200", "다운로드용 URL 요청완료", response); } } \ No newline at end of file diff --git a/back/src/main/java/com/back/domain/file/video/dto/controller/UploadUrlGetResponse.java b/back/src/main/java/com/back/domain/file/video/dto/controller/UploadUrlGetResponse.java index faff0422..dc8a7ab2 100644 --- a/back/src/main/java/com/back/domain/file/video/dto/controller/UploadUrlGetResponse.java +++ b/back/src/main/java/com/back/domain/file/video/dto/controller/UploadUrlGetResponse.java @@ -1,10 +1,9 @@ package com.back.domain.file.video.dto.controller; -import java.net.URL; import java.time.LocalDateTime; public record UploadUrlGetResponse( - URL url, + String url, LocalDateTime expiresAt ) { } \ No newline at end of file diff --git a/back/src/main/java/com/back/domain/file/video/entity/Video.java b/back/src/main/java/com/back/domain/file/video/entity/Video.java index 4f73d56a..1711ed18 100644 --- a/back/src/main/java/com/back/domain/file/video/entity/Video.java +++ b/back/src/main/java/com/back/domain/file/video/entity/Video.java @@ -24,18 +24,15 @@ public class Video extends BaseEntity { private Integer duration; - private Long fileSize; - @Builder(access = AccessLevel.PRIVATE) - private Video(String uuid, String status, String path, Integer duration, Long fileSize) { + private Video(String uuid, String status, String path, Integer duration) { this.uuid = uuid; this.status = status; this.path = path; this.duration = duration; - this.fileSize = fileSize; } - public static Video create(String uuid, String status, String path, Integer duration, Long fileSize) { + public static Video create(String uuid, String status, String path, Integer duration) { if (uuid == null || uuid.isBlank()) { throw new IllegalArgumentException("uuid cannot be null or empty"); } @@ -48,7 +45,6 @@ public static Video create(String uuid, String status, String path, Integer dura .status(status) .path(path) .duration(duration) - .fileSize(fileSize) .build(); } public void updateStatus(String status) { diff --git a/back/src/main/java/com/back/domain/file/video/service/FileManager.java b/back/src/main/java/com/back/domain/file/video/service/FileManager.java index 27afbe3a..a7b9a022 100644 --- a/back/src/main/java/com/back/domain/file/video/service/FileManager.java +++ b/back/src/main/java/com/back/domain/file/video/service/FileManager.java @@ -29,4 +29,13 @@ public PresignedUrlResponse getDownloadUrl(String objectKey) { LocalDateTime expiresAt = LocalDateTime.now().plusMinutes(expires); return new PresignedUrlResponse(url, expiresAt); } + + //TODO : 테스트 작성필요 + public void updateVideoStatus(String videoId, String status) { + try { + videoService.updateStatus(videoId, status); + } catch (Exception e) { + videoService.createVideo(videoId, status, "/", 0); + } + } } diff --git a/back/src/main/java/com/back/domain/file/video/service/VideoService.java b/back/src/main/java/com/back/domain/file/video/service/VideoService.java index ddafd8de..51cd2a8b 100644 --- a/back/src/main/java/com/back/domain/file/video/service/VideoService.java +++ b/back/src/main/java/com/back/domain/file/video/service/VideoService.java @@ -11,8 +11,8 @@ public class VideoService { private final VideoRepository videoRepository; - public Video createVideo(String uuid, String status, String path, Integer duration, Long fileSize) { - Video video = Video.create(uuid, status, path, duration, fileSize); + public Video createVideo(String uuid, String status, String path, Integer duration) { + Video video = Video.create(uuid, status, path, duration); return videoRepository.save(video); } diff --git a/back/src/main/java/com/back/domain/news/news/controller/NewsController.java b/back/src/main/java/com/back/domain/news/news/controller/NewsController.java index f951698d..6ef455f3 100644 --- a/back/src/main/java/com/back/domain/news/news/controller/NewsController.java +++ b/back/src/main/java/com/back/domain/news/news/controller/NewsController.java @@ -45,6 +45,7 @@ public RsData createNews(@RequestBody NewsCreateRequest requ @Operation(summary = "뉴스 단건 조회", description = "특정 ID의 뉴스를 읽어옵니다.") public RsData getNews(@PathVariable("newsId") Long newsId) { News news = newsService.getNewsById(newsId); + newsService.incrementViews(news); NewsGetResponse response = new NewsGetResponse(news); return new RsData<>("200", "뉴스 읽어오기 완료", response); } diff --git a/back/src/main/java/com/back/domain/news/news/entity/News.java b/back/src/main/java/com/back/domain/news/news/entity/News.java index ae69a8b8..ffdbfe7e 100644 --- a/back/src/main/java/com/back/domain/news/news/entity/News.java +++ b/back/src/main/java/com/back/domain/news/news/entity/News.java @@ -24,17 +24,19 @@ public class News extends BaseEntity { @OneToOne private Video video; private String content; + private Integer views; @OneToMany(mappedBy = "news", cascade = CascadeType.ALL, orphanRemoval = true) private List newsComment; @OneToMany(mappedBy = "news", cascade = CascadeType.ALL, orphanRemoval = true) private List newsLikes = new ArrayList<>(); @Builder(access = AccessLevel.PRIVATE) - private News(Member member, String title, Video video, String content, List newsComment, List newsLikes) { + private News(Member member, String title, Video video, String content, Integer views, List newsComment, List newsLikes) { this.member = member; this.title = title; this.video = video; this.content = content; + this.views = views; this.newsComment = newsComment; this.newsLikes = newsLikes; } @@ -57,6 +59,7 @@ public static News create(Member member, String title, Video video, String conte .title(title) .video(video) .content(content) + .views(0) .newsComment(new ArrayList<>()) .newsLikes(new ArrayList<>()) .build(); @@ -80,6 +83,10 @@ public void unLike(NewsLike newsLike) { this.newsLikes.remove(newsLike); } + public void incrementViews() { + this.views += 1; + } + public void addComment(NewsComment newsComment) { if (newsComment == null) { throw new IllegalArgumentException("Comment cannot be null"); diff --git a/back/src/main/java/com/back/domain/news/news/service/NewsService.java b/back/src/main/java/com/back/domain/news/news/service/NewsService.java index c8c38e96..10b778bc 100644 --- a/back/src/main/java/com/back/domain/news/news/service/NewsService.java +++ b/back/src/main/java/com/back/domain/news/news/service/NewsService.java @@ -34,15 +34,20 @@ public News getNewsById(Long id) { public News updateNews(Member member, News news, String title, Video video, String content) { if (!(member.getRole() == Member.Role.ADMIN)) { - throw new ServiceException("403","수정 권한이 없습니다."); + throw new ServiceException("403", "수정 권한이 없습니다."); } news.update(title, video, content); return newsRepository.save(news); } + public News incrementViews(News news) { + news.incrementViews(); + return newsRepository.save(news); + } + public void deleteNews(Member member, News news) { if (!(member.getRole() == Member.Role.ADMIN)) { - throw new ServiceException("403","수정 권한이 없습니다."); + throw new ServiceException("403", "수정 권한이 없습니다."); } newsRepository.delete(news); } diff --git a/back/src/main/java/com/back/global/kafka/KafkaConfig.java b/back/src/main/java/com/back/global/kafka/KafkaConfig.java new file mode 100644 index 00000000..6a24b0e9 --- /dev/null +++ b/back/src/main/java/com/back/global/kafka/KafkaConfig.java @@ -0,0 +1,38 @@ +package com.back.global.kafka; + +import org.apache.kafka.clients.consumer.ConsumerConfig; +import org.apache.kafka.common.serialization.StringDeserializer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.kafka.annotation.EnableKafka; +import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory; +import org.springframework.kafka.core.ConsumerFactory; +import org.springframework.kafka.core.DefaultKafkaConsumerFactory; + +import java.util.HashMap; +import java.util.Map; + +@EnableKafka +@Configuration +public class KafkaConfig { + + @Bean + public ConsumerFactory consumerFactory() { + Map props = new HashMap<>(); + + props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092"); + props.put(ConsumerConfig.GROUP_ID_CONFIG, "spring-boot-group"); + props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest"); + props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class); + props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class); + + return new DefaultKafkaConsumerFactory<>(props); + } + + @Bean + public ConcurrentKafkaListenerContainerFactory kafkaListenerContainerFactory() { + ConcurrentKafkaListenerContainerFactory factory = new ConcurrentKafkaListenerContainerFactory<>(); + factory.setConsumerFactory(consumerFactory()); + return factory; + } +} diff --git a/back/src/main/java/com/back/global/kafka/KafkaConsumer.java b/back/src/main/java/com/back/global/kafka/KafkaConsumer.java new file mode 100644 index 00000000..ee4d9790 --- /dev/null +++ b/back/src/main/java/com/back/global/kafka/KafkaConsumer.java @@ -0,0 +1,31 @@ +package com.back.global.kafka; + +import com.back.domain.file.video.service.FileManager; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import org.springframework.kafka.annotation.KafkaListener; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class KafkaConsumer { + private final FileManager fileManager; + private final ObjectMapper mapper; + + @KafkaListener(topics = "transcoding-status") + public void consume(String transcodingStatusMessage) { + + try { + JsonNode rootNode = mapper.readTree(transcodingStatusMessage); + + String uuid = rootNode.get("key").asText(); + String status = rootNode.get("qualities").toString(); + //현재 duration이 0인 문제가 있음 영상 길이가 필요한게 아니라면 제거할 예정 + fileManager.updateVideoStatus(uuid, status); + + } catch (Exception e) { + e.printStackTrace(); + } + } +} diff --git a/back/src/main/java/com/back/global/kafka/KafkaProducer.java b/back/src/main/java/com/back/global/kafka/KafkaProducer.java new file mode 100644 index 00000000..ba8d3f5a --- /dev/null +++ b/back/src/main/java/com/back/global/kafka/KafkaProducer.java @@ -0,0 +1,4 @@ +package com.back.global.kafka; + +public class KafkaProducer { +} diff --git a/back/src/test/java/com/back/domain/file/entity/VideoTest.java b/back/src/test/java/com/back/domain/file/entity/VideoTest.java index 1645776b..5fe1b7cb 100644 --- a/back/src/test/java/com/back/domain/file/entity/VideoTest.java +++ b/back/src/test/java/com/back/domain/file/entity/VideoTest.java @@ -15,16 +15,14 @@ void videoCreationTest() { String status = "{\"status\":\"done\"}"; String originalPath = "/videos/sample.mp4"; Integer duration = 120; - Long fileSize = 1024L; - Video video = Video.create(uuid, status, originalPath, duration, fileSize); + Video video = Video.create(uuid, status, originalPath, duration); assertThat(video).isNotNull(); assertThat(video.getUuid()).isEqualTo(uuid); assertThat(video.getStatus()).isEqualTo(status); assertThat(video.getPath()).isEqualTo(originalPath); assertThat(video.getDuration()).isEqualTo(duration); - assertThat(video.getFileSize()).isEqualTo(fileSize); } @Test @@ -34,13 +32,13 @@ void videoCreationTestWithInvalidUuid() { String originalPath = "/videos/sample.mp4"; try { - Video.create(null, status, originalPath, 100, 1000L); + Video.create(null, status, originalPath, 100); } catch (Exception e) { assertThat(e).isInstanceOf(IllegalArgumentException.class); } try { - Video.create("", status, originalPath, 100, 1000L); + Video.create("", status, originalPath, 100); } catch (Exception e) { assertThat(e).isInstanceOf(IllegalArgumentException.class); } @@ -53,13 +51,13 @@ void videoCreationTestWithInvalidOriginalPath() { String status = "{}"; try { - Video.create(uuid, status, null, 100, 1000L); + Video.create(uuid, status, null, 100); } catch (Exception e) { assertThat(e).isInstanceOf(IllegalArgumentException.class); } try { - Video.create(uuid, status, "", 100, 1000L); + Video.create(uuid, status, "", 100); } catch (Exception e) { assertThat(e).isInstanceOf(IllegalArgumentException.class); } @@ -74,7 +72,7 @@ void videoUpdateStatusTest() { Integer duration = 120; Long fileSize = 1024L; - Video video = Video.create(uuid, status, originalPath, duration, fileSize); + Video video = Video.create(uuid, status, originalPath, duration); assertThat(video.getStatus()).isEqualTo(status); String newStatus = "{\"status\":\"done\"}"; @@ -91,7 +89,7 @@ void videoUpdateStatusTestWithInvalidStatus() { Integer duration = 120; Long fileSize = 1024L; - Video video = Video.create(uuid, status, originalPath, duration, fileSize); + Video video = Video.create(uuid, status, originalPath, duration); try { video.updateStatus(null); diff --git a/back/src/test/java/com/back/domain/file/service/VideoServiceTest.java b/back/src/test/java/com/back/domain/file/service/VideoServiceTest.java index e0c8bdd1..7d371177 100644 --- a/back/src/test/java/com/back/domain/file/service/VideoServiceTest.java +++ b/back/src/test/java/com/back/domain/file/service/VideoServiceTest.java @@ -27,7 +27,7 @@ class VideoServiceTest { private VideoService videoService; @Test - @DisplayName("transcodingResults, originalPath, originalFileName, duration, fileSize로 Video 객체 생성") + @DisplayName("transcodingResults, originalPath, originalFileName, duration로 Video 객체 생성") void videoCreationTest() { String uuid = UUID.randomUUID().toString(); String transcodingResults = """ @@ -51,18 +51,16 @@ void videoCreationTest() { """; String originalPath = "/videos/sample.mp4"; Integer duration = 120; - Long fileSize = 1024L; - Video video = VideoFixture.create(uuid, transcodingResults, originalPath, duration, fileSize); + Video video = VideoFixture.create(uuid, transcodingResults, originalPath, duration); when(videoRepository.save(any(Video.class))).thenReturn(video); - Video createdVideo = videoService.createVideo(uuid, transcodingResults, originalPath, duration, fileSize); + Video createdVideo = videoService.createVideo(uuid, transcodingResults, originalPath, duration); assertThat(createdVideo).isNotNull(); assertThat(createdVideo.getUuid()).isEqualTo(uuid); assertThat(createdVideo.getStatus()).isEqualTo(transcodingResults); assertThat(createdVideo.getPath()).isEqualTo(originalPath); assertThat(createdVideo.getDuration()).isEqualTo(duration); - assertThat(createdVideo.getFileSize()).isEqualTo(fileSize); } @Test diff --git a/back/src/test/java/com/back/domain/news/news/entity/NewsTest.java b/back/src/test/java/com/back/domain/news/news/entity/NewsTest.java index e8f0fdf8..6d3dbaf9 100644 --- a/back/src/test/java/com/back/domain/news/news/entity/NewsTest.java +++ b/back/src/test/java/com/back/domain/news/news/entity/NewsTest.java @@ -291,4 +291,22 @@ void removeNullCommentTest() { assertThat(news.getNewsComment().size()).isEqualTo(0); } + + @Test + @DisplayName("뉴스 조회수를 증가시킬 수 있다.") + void incrementViewsTest() { + Member member = MemberFixture.createDefault(); + String title = "Sample News Title"; + String content = "This is a sample news content."; + Video video = VideoFixture.createDefault(); + News news = News.create(member, title, video, content); + + assertThat(news.getViews()).isEqualTo(0); + + news.incrementViews(); + assertThat(news.getViews()).isEqualTo(1); + + news.incrementViews(); + assertThat(news.getViews()).isEqualTo(2); + } } \ No newline at end of file diff --git a/back/src/test/java/com/back/domain/news/news/service/NewsServiceTest.java b/back/src/test/java/com/back/domain/news/news/service/NewsServiceTest.java index be11556e..743116ab 100644 --- a/back/src/test/java/com/back/domain/news/news/service/NewsServiceTest.java +++ b/back/src/test/java/com/back/domain/news/news/service/NewsServiceTest.java @@ -175,4 +175,21 @@ void deleteNews_NoPermission_ThrowsException() { }); verify(newsRepository, never()).delete(any(News.class)); } + + @Test + @DisplayName("뉴스 조회수 증가 성공") + void incrementViews_Success() { + // given + News news = NewsFixture.createDefault(); + int initialViews = news.getViews(); + when(newsRepository.save(any(News.class))).thenReturn(news); + + // when + News updatedNews = newsService.incrementViews(news); + + // then + assertThat(updatedNews).isNotNull(); + assertThat(updatedNews.getViews()).isEqualTo(initialViews + 1); + verify(newsRepository, times(1)).save(news); + } } \ No newline at end of file diff --git a/back/src/test/java/com/back/fixture/NewsFixture.java b/back/src/test/java/com/back/fixture/NewsFixture.java index b3da8c24..57653a7e 100644 --- a/back/src/test/java/com/back/fixture/NewsFixture.java +++ b/back/src/test/java/com/back/fixture/NewsFixture.java @@ -17,7 +17,6 @@ public class NewsFixture { private Video video = VideoFixture.createDefault(); private String content = "This is a sample news content."; private List newsComments = new ArrayList<>(); - private Integer likes = 0; private static NewsFixture builder() { return new NewsFixture(); @@ -58,11 +57,6 @@ public NewsFixture withContent(String content) { return this; } - public NewsFixture withLikes(Integer likes) { - this.likes = likes; - return this; - } - public NewsFixture withComments(List newsComments) { this.newsComments = newsComments; return this; diff --git a/back/src/test/java/com/back/fixture/VideoFixture.java b/back/src/test/java/com/back/fixture/VideoFixture.java index 622d0345..59787d91 100644 --- a/back/src/test/java/com/back/fixture/VideoFixture.java +++ b/back/src/test/java/com/back/fixture/VideoFixture.java @@ -27,7 +27,6 @@ public class VideoFixture { """; private String originalPath = "/videos/original.mp4"; private Integer duration = 120; - private Long fileSize = 1024L * 1024L * 10; // 10MB private static VideoFixture builder() { return new VideoFixture(); @@ -36,13 +35,12 @@ private static VideoFixture builder() { public static Video createDefault() { return builder().build(); } - public static Video create(String uuid, String transcodingResults, String originalPath, Integer duration, Long fileSize) { + public static Video create(String uuid, String transcodingResults, String originalPath, Integer duration) { return builder() .withUuid(uuid) .withTranscodingResults(transcodingResults) .withOriginalPath(originalPath) .withDuration(duration) - .withFileSize(fileSize) .build(); } @@ -66,18 +64,12 @@ public VideoFixture withDuration(Integer duration) { return this; } - public VideoFixture withFileSize(Long fileSize) { - this.fileSize = fileSize; - return this; - } - public Video build() { return Video.create( uuid, transcodingResults, originalPath, - duration, - fileSize + duration ); } } \ No newline at end of file diff --git a/consumer.py b/consumer.py index 26f93402..ed22e4ff 100644 --- a/consumer.py +++ b/consumer.py @@ -2,7 +2,7 @@ import json import time import subprocess -from kafka import KafkaConsumer +from kafka import KafkaConsumer, KafkaProducer from kafka.errors import NoBrokersAvailable import boto3 @@ -12,6 +12,7 @@ # Kafka KAFKA_TOPIC = os.getenv("KAFKA_TOPIC", "s3-events") +KAFKA_TRANSCODING_STATUS_TOPIC = os.getenv("KAFKA_TRANSCODING_STATUS_TOPIC", "transcoding-status") KAFKA_BOOTSTRAP = os.getenv("KAFKA_BOOTSTRAP", "kafka:29092") # Docker 내부 네트워크용 GROUP_ID = os.getenv("GROUP_ID", "minio-consumer") @@ -35,6 +36,35 @@ aws_secret_access_key=MINIO_SECRET_KEY ) +# ========================= +# Kafka Producer 연결 재시도 +# ========================= +while True: + try: + producer = KafkaProducer( + bootstrap_servers=KAFKA_BOOTSTRAP, + value_serializer=lambda v: json.dumps(v).encode('utf-8') + ) + print("Kafka Producer 연결 성공...") + break + except NoBrokersAvailable: + print("Kafka Producer 연결 실패, 5초 후 재시도...") + time.sleep(5) + +# ========================= +# Kafka 메시지 전송 함수 +# ========================= +def send_kafka_message(producer, topic, bucket, key, qualities_status): + message = { + "bucket": bucket, + "key": key, + "qualities": qualities_status + } + producer.send(topic, message) + producer.flush() + print(f"Kafka 메시지 전송: {json.dumps(message)}") + + # ========================= # DASH 트랜스코딩 함수 # ========================= @@ -53,19 +83,30 @@ def is_video(file_path): except Exception: return False -def encode_dash_multi_quality(input_file, output_dir): +def encode_dash_multi_quality(input_file, output_dir, producer, topic, bucket, key): os.makedirs(output_dir, exist_ok=True) - # 화질별 설정 (폴더 이름, 해상도, 비트레이트) qualities = [ ("1080p", "1920x1080", "5000k"), ("720p", "1280x720", "3000k"), ("480p", "854x480", "1500k") ] + qualities_status = { + q[0]: {"file_path": None, "status": "PENDING", "size_mb": 0} for q in qualities + } + + # 트랜스코딩 시작 메시지 + send_kafka_message(producer, topic, bucket, key, qualities_status) + for name, resolution, bitrate in qualities: quality_dir = os.path.join(output_dir, name) os.makedirs(quality_dir, exist_ok=True) + manifest_path = os.path.join(quality_dir, "manifest.mpd") + + # 특정 화질 트랜스코딩 진행중 + qualities_status[name]["status"] = "IN_PROGRESS" + send_kafka_message(producer, topic, bucket, key, qualities_status) cmd = [ "ffmpeg", @@ -75,12 +116,36 @@ def encode_dash_multi_quality(input_file, output_dir): "-s", resolution, "-c:a", "aac", "-f", "dash", - os.path.join(quality_dir, "manifest.mpd") + manifest_path ] print(f"{name} 트랜스코딩 시작: {input_file} → {quality_dir}") - subprocess.run(cmd, check=True) - print(f"{name} 트랜스코딩 완료: {quality_dir}") + try: + subprocess.run(cmd, check=True) + print(f"{name} 트랜스코딩 완료: {quality_dir}") + + # 트랜스코딩 완료 + qualities_status[name]["status"] = "COMPLETED" + qualities_status[name]["file_path"] = f"{key}/{name}/manifest.mpd" + + # DASH 폴더의 모든 파일 크기 합산 + total_size_bytes = 0 + for root, _, files in os.walk(quality_dir): + for file in files: + total_size_bytes += os.path.getsize(os.path.join(root, file)) + qualities_status[name]["size_mb"] = round(total_size_bytes / (1024 * 1024), 2) + + send_kafka_message(producer, topic, bucket, key, qualities_status) + + except subprocess.CalledProcessError as e: + print(f"{name} 트랜스코딩 실패: {e}") + qualities_status[name]["status"] = "FAILED" + send_kafka_message(producer, topic, bucket, key, qualities_status) + + # 모든 화질에 대한 처리가 끝났음을 알리는 최종 메시지 + print("전체 트랜스코딩 과정 완료.") + send_kafka_message(producer, topic, bucket, key, qualities_status) + # ========================= # DASH 폴더 업로드 함수 @@ -136,12 +201,21 @@ def upload_folder_to_minio(local_folder, bucket_name, s3_prefix=""): # 영상 확인 후 DASH 인코딩 (3화질) if is_video(download_path): - dash_output_dir = os.path.join(DOWNLOAD_DIR, "dash_" + os.path.splitext(key)[0]) - encode_dash_multi_quality(download_path, dash_output_dir) + object_key_without_ext = os.path.splitext(key)[0] + dash_output_dir = os.path.join(DOWNLOAD_DIR, "dash_" + object_key_without_ext) + + encode_dash_multi_quality( + download_path, + dash_output_dir, + producer, + KAFKA_TRANSCODING_STATUS_TOPIC, + bucket, + object_key_without_ext + ) print(f"DASH 인코딩 완료: {dash_output_dir}") # DASH 결과 재업로드 - upload_folder_to_minio(dash_output_dir, REUPLOAD_BUCKET, s3_prefix=os.path.splitext(key)[0]) + upload_folder_to_minio(dash_output_dir, REUPLOAD_BUCKET, s3_prefix=object_key_without_ext) else: print(f"영상 아님, 인코딩 스킵: {download_path}")