공부를 하다 집중력이 떨어지면 유튜브 보는 것이 요즘 삶의 낙이다.
갑자기 번뜩 생각이 났다.
Spring boot로 스트리밍을 만들어 볼까?
첫번째로 생각한 방법은 video 태그에 spring boot statics 경로를 넣는 것이다.
다들 알다 시피, spring boot static은 정적으로 파일을 들을 넣는곳이다.
이런식으로 넣게 되면 포트 설정을 8080 으로 한다고 가정하에 localhost:8080/Dynamite.mp4 를 url에 치게 되면
이런형식으로 동영상이 나오게 된다.
우리가 원하는 건 원하는 위치에 동영상에 띄우게 하는 것이기 때문에 컨트롤러에 url를 넣고 video 태그에서 불러오는 방식으로 진행을 하였다.
컨트롤러를 위처럼 지정해주고 video 태그에서 요청해준다.
그냥 url을 지정해줄 수 도 있지만 화면이 뜬후, ajax요청 후, 넣어주는 식으로 수정 해 보았다.
결과화면은 밑에와 같다.
다른 방법은 없을까?
Spring에서 지원하는 Resource를 사용하는 방법이다.
Resource를 사용하려면 FileSystemResource를 통해 File Path를 쉽게 지정할 수 있다.
FileSystemResource 안에는 위와 같으니 참고해주시면 좋겠다.
위와 또 다른 점은 Header에 Content-Diposition이라는 http body에 뭐가 올것이 알려줘 성향을 미리 말해줘야 한다.
저는 파일이름과 mp4라는 것을 알려줬다.
제가 최종 작성한 코드는 밑에와 같다.
이번에도 결과를 확인하기 위해 video태그 src에 컨트롤러 url을 넣고 테스트를 해봤다.
static url과 지정한것과 같이 동영상이 나오고 있다.
이번에는 spring에서 사용하는 것 말고 Java io 기능 중 RandomAccessFile을 사용해보았다.
RandomAcessFile로 내가 필요한 부분 부터 먼저 동영상의 데이터를 가져오기로 하였다.
@RequestMapping(value = "/api/random", method = RequestMethod.GET)
public void viewStreaming(HttpServletRequest request, HttpServletResponse response) throws Exception {
File file = new File(URL + "/Dynamite.mp4");
long rangeStart = 0, rangeEnd = 0;
boolean isPart = false;
try ( RandomAccessFile randomFile = new RandomAccessFile(file, "r");
BufferedOutputStream out = new BufferedOutputStream(response.getOutputStream()); ) {
long movieSize = randomFile.length();
String range = request.getHeader("range");
if(range != null) {
if(range.endsWith("-")) {
range = range + (movieSize - 1);
}
int idxm = range.trim().indexOf("-");
rangeStart = Long.parseLong(range.substring(6, idxm));
rangeEnd = Long.parseLong(range.substring(idxm + 1));
if(rangeStart > 0) isPart = true;
} else {
rangeStart = 0;
rangeEnd = movieSize - 1;
}
long partSize = rangeEnd - rangeStart + 1;
response.reset();
response.setStatus(isPart ? 206: 200);
response.setContentType("video/mp4");
response.setHeader("Content-Range", "bytes " + rangeStart + "-" + rangeEnd + "/" + movieSize);
response.setHeader("Accept-Ranges", "bytes");
response.setHeader("Content-Length", String.valueOf(partSize));
randomFile.seek(rangeStart);
int bufferSize = 8 * 1024;
byte[] buf = new byte[bufferSize];
do{
int block = partSize > bufferSize ? bufferSize : (int)partSize;
int len = randomFile.read(buf, 0, block);
out.write(buf, 0, len);
partSize -= block;
}while(partSize > 0);
}
}
header에 오는 range가 처음시작점보다 멀리 갈 수록 늘어나기 때문에 제가 지정한 위치에서 ~ 끝 위치만 데이터를 가져온다.
마지막 방법은 Resource Region 을 사용하는 방법이다.
위에 RandomAcessFile도 좋은 방법이지만 소스코드가 너무 길다라는 단점이 있다.
스프링에서 제공해주는 라이브러리 들을 최대한 사용하여 코드를 줄여 볼 것이다.
일단 Header를 Controller @RequestHeader를 통해 받아올 것이다.
또한, stream을 사용하여 HttpRange를 이용하여 위에 RandomAcessFile에서 계산하던 것을 생략을 할 수가 있다.
region은 httpRange에서 가져온 start와 end를 가져와 잘라서 결과를 나타낸다.
또한, cacheControl를 사용하여 리소스에 유효기간을 정해주는 대신 디스크나 메모리에서만 데이터를 읽어 들여 비교적 빠르게 영상 범위를 가져온다.
저는 10초로 잡아둬서 10초마다 리소를 가져오게 수정을 하였다.
@RequestMapping(value = "/api/region", method = RequestMethod.GET)
public ResponseEntity<ResourceRegion> videoRegion(@RequestHeader HttpHeaders headers) throws Exception {
String path = URL + "/Dynamite.mp4";
Resource resource = new FileSystemResource(path);
long chunkSize = 1024 * 1024;
long contentLength = resource.contentLength();
ResourceRegion region;
try {
HttpRange httpRange = headers.getRange().stream().findFirst().get();
long start = httpRange.getRangeStart(contentLength);
long end = httpRange.getRangeEnd(contentLength);
long rangeLength = Long.min(chunkSize, end -start + 1);
log.info("start === {} , end == {}", start, end);
region = new ResourceRegion(resource, start, rangeLength);
} catch (Exception e) {
long rangeLength = Long.min(chunkSize, contentLength);
region = new ResourceRegion(resource, 0, rangeLength);
}
return ResponseEntity.status(HttpStatus.PARTIAL_CONTENT)
.cacheControl(CacheControl.maxAge(10, TimeUnit.MINUTES))
.contentType(MediaTypeFactory.getMediaType(resource).orElse(MediaType.APPLICATION_OCTET_STREAM))
.header("Accept-Ranges", "bytes")
.eTag(path)
.body(region);
}
결과적으로 잘 작동되는 걸 볼 수 있다.
Spring과 java를 활용한 스트리밍 연습을 하였다.
HLS 같은 프로토콜 같은 걸 이용해서 왓챠나 티빙 처럼 해보는 걸 나중에 해볼 생각이다.
'프로그래밍 > 스프링부트(springboot)' 카테고리의 다른 글
[스프링부트/springboot] 스프링 부트 JPA로 CRUD 해보기 (0) | 2020.12.14 |
---|---|
[스프링부트/springboot] 스프링 부트 JPA 설정해보기 (0) | 2020.12.13 |
[스프링부트/springboot] 스프링 부트 배너 변경하기 (0) | 2020.12.12 |
[스프링 부트/ springboot ] @Controller, @RestController로 컨트롤러 클래스 작성하기 (0) | 2020.05.20 |
[스프링 부트/springboot] 스프링 부트 시작하기 , Gradle Project ( 인텔리 제이[IntelliJ] 사용) (0) | 2020.05.19 |