
지난번에는 스트리밍할 동영상업로드를 구현하면서 Chunk별 분할 업로드를 구현했었다.
여기서 생긴 성능에 대한 궁금증들을 실험해보는 과정들을 적어보겠다.
소스코드 https://github.com/van1164/video-streaming

html코드에서 로그를 찍어보니 모든 Chunk를 업로드하는데 걸리는 시간이 약 2.5초 정도 걸리고 최종적으로 걸리는 시간은 10초정도로 나왔다. 그렇다면 s3에 접근해서 모든 Chunk를 mp4로 변환해서 hls파일로 변경후 다시 S3에 업로드하는 과정에서 대략 8초정도 걸린 것을 알 수 있다.



기존에는 Chunk들을 순차적으로 보내는 방식을 사용했었다. 이번에는 모든 Chunk에 대한 요청을 동시에 보내고 Promise를 통해 다 보내지면 그 다음 동작을 하도록 나누어 보았다.
그에 맞게 서버 API도 새로운 구성을 추가했다.






fun uploadVideoPartLast(video: MultipartFile, videoData: UploadVideoPartDTO): String {
val futureList = mutableListOf<CompletableFuture<ByteArray>>()
//여러 part를 하나의 파일로 만들기
val stopWatch = StopWatch()
stopWatch.start("mp4로 만드는데 걸린 시간")
//val mp4start = System.currentTimeMillis()
val inputFilePath = Paths.get(UUID.randomUUID().toString() + ".mp4")
runBlocking {
Files.createFile(inputFilePath)
}
for (i: Int in 0 until videoData.totalChunk) {
futureList.add(CompletableFuture.supplyAsync {
return@supplyAsync uploadRepository.getPartByteArray(
bucketUrl,
video.originalFilename,
i
)
})
//val videoPart = uploadRepository.getPart(bucketUrl, video.originalFilename, i)
}
return CompletableFuture.allOf(*futureList.toTypedArray())
.thenApply {
// ts -> mp4
futureList.forEach{videoPart ->
Files.write(inputFilePath, videoPart.get(), StandardOpenOption.APPEND)
}
stopWatch.stop()
}.thenApplyAsync {
val outputUUID = UUID.randomUUID().toString()
val m3u8Path = "$outputUUID.m3u8"
val thumbNailPath = UUID.randomUUID().toString() + ".jpg"
val deleteChunkFuture = CompletableFuture.runAsync{deleteChunkFiles(videoData, video)}
val thumbNailFuture = CompletableFuture.runAsync{createThumbNail(inputFilePath, thumbNailPath)}
val saveDataFuture = CompletableFuture.runAsync{saveVideoData(outputUUID, videoData, thumbNailPath)}
val mp4ToHlsFuture = CompletableFuture.runAsync{mp4ToHls(inputFilePath, m3u8Path, outputUUID)}
CompletableFuture.allOf(deleteChunkFuture,thumbNailFuture,saveDataFuture,mp4ToHlsFuture).get()
return@thenApplyAsync outputUUID
}.get()
}
private fun mp4ToHls(
inputFilePath: Path,
m3u8Path: String,
outputUUID: String
) {
logger.info("hls시작")
val stopWatch = StopWatch()
stopWatch.start("mp4를 hls로 바꾸고 업로드하는 데 걸린 시간")
//mp4 to ts
mp4ToM3U8(inputFilePath, m3u8Path, outputUUID)
// 여러 TS들을 S3에 업로드
uploadVideoTs(outputUUID)
uploadRepository.uploadM3U8(m3u8Path)
stopWatch.stop()
println(stopWatch.prettyPrint())
}
private fun createThumbNail(
inputFilePath: Path,
thumbNailPath: String
) {
logger.info("썸네일")
val stopWatch = StopWatch()
stopWatch.start("썸네일 만들고 업로드하는 데 걸린 시간")
//thumbnail by ffmpeg
extractThumbnail(inputFilePath.toString(), thumbNailPath)
//uploadThumbnail
uploadRepository.uploadThumbnail(thumbNailPath)
stopWatch.stop()
println(stopWatch.prettyPrint())
}
private fun deleteChunkFiles(
videoData: UploadVideoPartDTO,
video: MultipartFile
) {
logger.info("DELETE")
val futures = (0 until videoData.totalChunk).map {
CompletableFuture.runAsync { uploadRepository.deletePart(video.originalFilename, it) }
}
CompletableFuture.allOf(*futures.toTypedArray()).get()
}
예시 코드)
val stopWatch =StopWatch()
stopWatch.start("mp4로 만드는데 걸린 시간")
//코드
stopWatch.stop()

S3 Object를 여러 개 동시에 읽어올 때 생길 수있는 문제
amazonS3 S3Object를 close해주지 않았기 때문에 다음과 같은 오류가 발생하였다. S3Object는 Closeable을 implements하고 있기 때문에 try-with-resources를 사용할 수있다.
try-with-resources란 AutoCloseable 인터페이스를 구현하고 있는 자원에 대해 try안에 그 자원을 넣으면 작업이 끝나면 자동으로 close해주는 것을 말한다.

코틀린에서는 use 고차함수를 통해 사용할 수 있다.
fun readFirstLine(path: String): String {
BufferedReader(FileReader(path)).use { br ->
return br.readLine()
}
}
https://blog.naver.com/dnjung/221168057295
https://aws.amazon.com/ko/blogs/developer/closeable-s3objects/
https://shinjekim.github.io/kotlin/2019/11/01/Kotlin-%EC%9E%90%EB%B0%94%EC%9D%98-try-with-resource-%EA%B5%AC%EB%AC%B8%EA%B3%BC-%EC%BD%94%ED%8B%80%EB%A6%B0%EC%9D%98-use-%ED%95%A8%EC%88%98/