[KoPring] 스트리밍 서비스 구현해보기 2 (feat. Chunk로 나눠서 업로드)

Sihwan Kim·2024년 3월 3일

streaming

목록 보기
2/14

💡 동영상을 업로드할 때 용량이 크기때문에 한번에 올리게 되면 얼마나 업로드되었는지, 얼마나 남았는지 알 수없었다.
서버에서도 한번의 요청에 한번에 응답만 가기때문에 얼마나 진행되었는지 알려줄 수 없기 때문이다.
그래서 Chunk개념을 이용해보기로 했다.

HTTP 206코드

HTTP 상태코드에는 206 Partial Content라는 코드가 존재한다. 전체 Content가 다 온것은 아닌지만 일정부분만을 보냈고 그거에 대한 응답이 처리되었을 때 이 코드를 보낸다.
이 코드를 활용해서 구현할 생각이다.

🥞 클라이언트에서 파일을 같은 용량 여러chunk로 나누기 => 하나씩 요청을 보냄 => 서버는 마지막인지 체크해서 마지막이면 200, 아니면 206을 반환 => 클라이언트는 응답이 206이면 다음 chunk를 보냄

요청 DTO

data class UploadVideoPartDTO(
    val title : String,
    val chunkNumber : Int,
    val totalChunk : Int,
)

Controller 구현

    @PostMapping("/video")
    fun uploadVideo(@RequestPart(name = "video") video: MultipartFile,
                    @RequestPart(name = "videoData")videoData: UploadVideoPartDTO): ResponseEntity<Any> {

        return uploadService.uploadVideoPart(video, videoData)


    }

Service구현

    fun uploadVideoPart(video: MultipartFile, videoData: UploadVideoPartDTO): ResponseEntity<Any> {

        uploadRepository.uploadVideoPart(video, videoData.chunkNumber) //S3에 업로드

        if (videoData.totalChunk - 1 == videoData.chunkNumber) {
        	//여러 part를 하나의 파일로 만들기
            val inputFilePath = Paths.get(UUID.randomUUID().toString() + ".mp4")
            Files.createFile(inputFilePath)
            
            for (i: Int in 0 until videoData.totalChunk) {
                val videoPart = uploadRepository.getPart(bucketUrl, video.originalFilename, i) ?: return ResponseEntity(
                    HttpStatus.BAD_REQUEST
                ) //S3에서 i번째 파트 가져오기
                Files.write(inputFilePath, videoPart.readAllBytes(), StandardOpenOption.APPEND)
                uploadRepository.deletePart(video.originalFilename, i)//i번째 파트 S3에서 제거
            }
            
                        //mp4 to ts
            mp4ToTs(inputFilePath, tsFilePath)

            //ts 분할
            divideTsFile(tsFilePath)

            // 여러 TS들을 S3에 업로드
            uploadRepository.uploadVideoTs(tsFilePath)
            
            return ResponseEntity(HttpStatus.OK)
        } else {
            return ResponseEntity(HttpStatus.PARTIAL_CONTENT)
        }
    private fun divideTsFile(tsFilePath: String) {
        val segmentBuilder =
            FFmpegBuilder().setInput(tsFilePath)
                .addOutput("${tsFilePath}_%03d.ts")
                .addExtraArgs("-c", "copy")
                .addExtraArgs("-map", "0")
                .addExtraArgs("-segment_time", "5")
                .addExtraArgs("-f", "segment")
                .addExtraArgs("-reset_timestamps", "1")
                .setStrict(FFmpegBuilder.Strict.EXPERIMENTAL)
                .done()
        FFmpegExecutor(ffmpeg, ffprobe).createJob(segmentBuilder).run()
        File(tsFilePath).delete()
    }

    private fun mp4ToTs(inputFilePath: Path, tsFilePath: String) {
        val builder = FFmpegBuilder()
            .setInput(inputFilePath.toString())
            .addOutput(tsFilePath).addExtraArgs("-c", "copy")
            .addExtraArgs("-bsf:v", "h264_mp4toannexb")
            .addExtraArgs("-f", "mpegts")
            .setStrict(FFmpegBuilder.Strict.EXPERIMENTAL).done()
        FFmpegExecutor(ffmpeg, ffprobe).createJob(builder).run()
        File(inputFilePath.toString()).delete()
    }

결과

❓️ 현재까지 한 생각은 MP4파일을 여러 .ts로 변환하기 위해서는 우선 MP4파일을 저장해야하는데, 이걸 로컬에 저장하면, 사용자가 매우 늘어났을 때 문제가 생길 것으로 판단했다.
그래서 S3에 각 Chunk를 저장하고 모든 Chunk를 다 받으면 하나씩 불러와서 MP4로 저장후 바로 ts로 변경 및 분할후 삭제하는 방식을 채택하였다. 최대한 로컬 머신에 저장되는시간을 줄이려는 판단이었는데.. 이보다 더 좋은 방법을 찾아볼 예정이다.

0개의 댓글