유튜브와 같이 온라인 영상 서비스들에서 원하는 영상을 재생하려고 하면,
전부 다운로드 후 재생이 아닌 일부만 조금씩 다운로드 후 재생하고 그 이후에 추가로 다운로드 하는걸 알 수 있습니다.
Spring Boot에서 영상 파일을 스트리밍 하는 방법을 알아보겠습니다.
환경
- Spring Boot 3.1.3
- React 18.2.0
- React-Player 2.12.0
구조
- 영상을 가져와서 프론트에 출력하는 구조는 아래와 같습니다.
영상을 재생하기 위해 순서대로 정리해보겠습니다.
- React에서 영상 데이터 요청
- 파일 정보(이름, 위치 등)을 Spring Boot에 요청합니다.
- React-Player를 사용하기 때문에 요청을 url={videoLoader()} 라는 파라미터를 통해 영상을 받아옵니다.
- Spring Boot에서 영상 파일 탐색
- 파일 정보를 받는 파라미터와 HettpHeaders를 가져오는 파라미터 이렇게 두개를 받는 API를 만듭니다.
- 그리고 파일 정보를 통해(이름, 위치) 파일의 존재를 확인 합니다.
- Spring Boot에서 영상 파일 데이터를 분할 가공
- 파일이 존재한다면 ResourceRegion과 HttpRange를 통해 원하는 시간만큼 잘라 냅니다.
- 영상 데이터 리턴
- 가공한 영상 byte 데이터를 리턴합니다.
코드를 통해 실행해 보겠습니다.
React
import { useState } from 'react';
import ReactPlayer from "react-player";
import axios from 'axios';
export default function ImageModal(): JSX.Element {
const [isPlaying, setIsPlaying] = useState<boolean>(false);
const [info, setInfo] = useState('파일 정보');
const videoLoader = () => {
return 'http://localhost:8080/file/streamingPublicVideo/'+info;
}
return (
<div>
<ReactPlayer
className="player"
url={videoLoader}
controls={true}
playing={isPlaying}
width='100%'
height='100%'
/>
</div>
);
}
- videoLoader라는 함수를 생성하고 영상 파일 데이터를 제공하는 API 주소를 return합니다.
- info에 들어갈 내용은 저는 파일 위치로 하였습니다.
- ex : C:\Users\SonJuHy\Videos\video.mp4
Spring Boot
- SpringBoot는 두 파트로 나뉘어서 코드가 작성되어 있습니다.
- RestAPI를 받는 Controller, 로직을 실행하는 Service로 나뉘어져있습니다.
Controller
package com.myhome.server.api.controller;
import com.myhome.server.api.service.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.InputStreamResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.ResourceRegion;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController()
@RequestMapping("/file")
public class FileServerController {
@Autowired
FileServerService service;
@GetMapping("/streamingPublicVideo/{info}")
public ResponseEntity<ResourceRegion> streamingPublicVideo(@RequestHeader HttpHeaders httpHeaders, @PathVariable String info){
return service.streamingPublicVideo(httpHeaders, uuid);
}
}
- HttpHeaders와 파일 정보(위치)를 받아서 서비스로 넘기는 작업을 합니다.
Service
package com.myhome.server.api.service;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.InputStreamResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.ResourceRegion;
import org.springframework.http.*;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
@Service
public class FileServerPublicServiceImpl implements FileServerPublicService {
@Override
public ResponseEntity<ResourceRegion> streamingPublicVideo(HttpHeaders httpHeaders, String pathStr) {
try{
Path path = Paths.get(pathStr);
Resource resource = new FileSystemResource(path);
long chunkSize = 1024*1024;
long contentLength = resource.contentLength();
ResourceRegion resourceRegion;
try{
HttpRange httpRange;
if(httpHeaders.getRange().stream().findFirst().isPresent()){
httpRange = httpHeaders.getRange().stream().findFirst().get();
long start = httpRange.getRangeStart(contentLength);
long end = httpRange.getRangeEnd(contentLength);
long rangeLength = Long.min(chunkSize, end-start+1);
resourceRegion = new ResourceRegion(resource, start, rangeLength);
}
else{
resourceRegion = new ResourceRegion(resource, 0, Long.min(chunkSize, resource.contentLength()));
}
}
catch(Exception e){
long rangeLength = Long.min(chunkSize, resource.contentLength());
resourceRegion = new ResourceRegion(resource, 0, rangeLength);
}
return ResponseEntity.status(HttpStatus.PARTIAL_CONTENT)
.cacheControl(CacheControl.maxAge(10, TimeUnit.MINUTES)) // 10분
.contentType(MediaTypeFactory.getMediaType(resource).orElse(MediaType.APPLICATION_OCTET_STREAM))
.header(HttpHeaders.ACCEPT_RANGES, "bytes")
.body(resourceRegion);
} catch (IOException e) {
return new ResponseEntity<>(null, HttpStatus.OK);
}
return new ResponseEntity<>(null, HttpStatus.OK);
}
}
파일의 존재를 확인하고 그 정보를 불러옵니다.
Path path = Paths.get(pathStr);
헤더로부터 값을 읽습니다.
영상의 시작지점이 0이 아니라면 그 뒤를 이어서 정해진 길이 만큼 (rangeLength) 데이터를 가공합니다.
만약 시작지점이 0이라면 처음부터 정해진 길이 만큼 데이터를 가공합니다.
HttpRange httpRange;
if(httpHeaders.getRange().stream().findFirst().isPresent()){
httpRange = httpHeaders.getRange().stream().findFirst().get();
long start = httpRange.getRangeStart(contentLength);
long end = httpRange.getRangeEnd(contentLength);
long rangeLength = Long.min(chunkSize, end-start+1);
System.out.println("contentLength : "+contentLength+", start : "+start+", end : "+end+", rangeLength : "+rangeLength);
resourceRegion = new ResourceRegion(resource, start, rangeLength);
}
else{
resourceRegion = new ResourceRegion(resource, 0, Long.min(chunkSize, resource.contentLength()));
}
스트리밍을 위해 다운로드 받는 영상 캐시의 지속시간은 10분으로 설정.
데이터 타입은 bytes라고 헤더에 입력
body에 가공한 데이터를 담아서 리턴
return ResponseEntity.status(HttpStatus.PARTIAL_CONTENT)
.cacheControl(CacheControl.maxAge(10, TimeUnit.MINUTES)) // 10분
.contentType(MediaTypeFactory.getMediaType(resource).orElse(MediaType.APPLICATION_OCTET_STREAM))
.header(HttpHeaders.ACCEPT_RANGES, "bytes")
.body(resourceRegion);
'개발잡담 > Back-End' 카테고리의 다른 글
CMD로 Django 프로젝트 생성하기 (0) | 2024.07.08 |
---|---|
Spring Boot에서 파일 다운로드 (feat. React) (0) | 2024.02.02 |
Spring batch 5.1.0 간단 사용 (0) | 2024.01.31 |
로그가 필요해 - 서론 (0) | 2023.11.02 |
Spring은 어떻게 여러 개의 요청을 동시에 처리할까? (0) | 2023.09.26 |