간단한 프로젝트에서 동영상 서비스를 해야 하는 경우가 생겼다.

프로젝트는 스프링 부트 설정이였고 접속자는 많지 않을것으로 예상.

보통은 스트리밍 서버를 사용하지만 그정도 규모의 프로젝트가 아닌 경우에는

그냥 웹서버에 파일을 올리거나

아니면 자바에서 파일을 내려주는 형식으로 스트리밍 서비스를 하는 경우도 있다.

물론 사용자가 많지 않아서 부하가 적다는 가정하에서 말이다.

 

아래는 자바에서 동영상 서비스를 하는 여러가지를 정리했다.

 

1. 웹경로에 파일을 올려서 바로 바로 스트리밍 하는 방법

2. 자바에서 파일을 읽어 내려주는 방법

3. 자바에서 파일을 읽어 내려주는 방법(Resource 객체로 내림)

4. 자바에서 파일의 일부분만 읽어서 부분적으로 파일을 내려주는 방법

5. HLS를 이용하여 스트리밍을 지원하는 방법

 

video.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<video controls="controls" th:src="${videoUrl}" width="400" autoplay="autoplay"></video>
</body>
</html>

 

 

package com.test2.web.video;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.concurrent.TimeUnit;

import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

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.CacheControl;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpRange;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.MediaTypeFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;

import lombok.extern.slf4j.Slf4j;

@Slf4j
@Controller
@RequestMapping("/video")
public class VideoController {

	private final String TEMPLATE_DIR = "video/";
	private final String UPLOAD_DIR = "D:\\upload\\";
	
	// 1. 웹경로에 의한 스트리밍
    // 스프링프로젝트의 /resources/static/video 폴터에 동영상을 넣어 서비스
    @GetMapping("")
	public String videoIndex(Model model) {
		log.debug("************** class = {}, function = {}", this.getClass().getName(), new Object(){}.getClass().getEnclosingMethod().getName());
		model.addAttribute("videoUrl", "/video/video.mp4");
		return TEMPLATE_DIR + "video";
	}

	
    // 2. 다운로드 방식으로 서비스
    // byte array 로 내려주는 경우 스트리밍이 아닌 다운로드 방식이므로 전체 파일이 다운로드 된 이후 플레이 된다.
    // 크롬인 경우 플에이 이후 시간 설정이 되지 않는다.
	@GetMapping("/download")
	public String videoDownload(Model model) {
		log.debug("************** class = {}, function = {}", this.getClass().getName(), new Object(){}.getClass().getEnclosingMethod().getName());
		model.addAttribute("videoUrl", "/video/download/video.mp4");
		return TEMPLATE_DIR + "video";
	}
    
	@GetMapping("/download/{fileName}")
	public void vidoeDownloadFileName(@PathVariable String fileName, HttpServletRequest req, HttpServletResponse res) throws IOException {
		log.debug("************** class = {}, function = {}", this.getClass().getName(), new Object(){}.getClass().getEnclosingMethod().getName());


		String fileFullPath = UPLOAD_DIR + fileName;
		File downFile = new File(fileFullPath);  //파일 객체 생성
		if(downFile.isFile()){  // 파일이 존재하면
			int fSize = (int)downFile.length();
			res.setBufferSize(fSize);
			res.setContentType("application/octet-stream");
			res.setHeader("Content-Disposition", "attachment; filename="+fileName+";");
			res.setContentLength(fSize);  // 헤더정보 입력
			FileInputStream in  = new FileInputStream(downFile);
			ServletOutputStream out = res.getOutputStream();
			try
			{
				byte[] buf=new byte[8192];  // 8Kbyte 로 쪼개서 보낸다.
				int bytesread = 0, bytesBuffered = 0;
				while( (bytesread = in.read( buf )) > -1 ) {
					out.write( buf, 0, bytesread );
					bytesBuffered += bytesread;
					if (bytesBuffered > 1024 * 1024) { //아웃풋스트림이 1MB 가 넘어가면 flush 해준다.
						bytesBuffered = 0;
						out.flush();
					}
				}
			}
			finally {
				if (out != null) {
					out.flush();
					out.close();
				}
				if (in != null) {
					in.close();
				}
				//에러가 나더라도 아웃풋 flush와 close를 실행한다.
			}
		}
		
	}
	
    // 3. 동영상 파일을 Resource 객체로 내려주는 경우
    // 파일이 전체 내려오지 않아도 동영상 플레이가 시작된다. 단 중간에 플레이 구간을 선택 시 다시 파일을 받기 시작한다.
	@GetMapping("/resource")
	public String videoResource(Model model) {
		log.debug("************** class = {}, function = {}", this.getClass().getName(), new Object(){}.getClass().getEnclosingMethod().getName());
		model.addAttribute("videoUrl", "/video/resource/video.mp4");
		return TEMPLATE_DIR + "video";
	}
	
	@GetMapping("/resource/{fileName}")
	public ResponseEntity<Resource> vidoeResourceFileName(@PathVariable String fileName) throws FileNotFoundException {
		log.debug("************** class = {}, function = {}", this.getClass().getName(), new Object(){}.getClass().getEnclosingMethod().getName());
		String fileFullPath = UPLOAD_DIR + fileName;
		Resource resource = new FileSystemResource(fileFullPath); 
		HttpHeaders headers = new HttpHeaders();
		headers.set(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + fileName + "");
		headers.setContentType(MediaType.parseMediaType("video/mp4"));
		return new ResponseEntity<Resource>(resource, headers, HttpStatus.OK);
	}
	
	
	// 4. 범위설정을 하여 파일의 일부분만 내려주는 형식
    // 동영상 시작시 설정된 파일 크기를 내리고 video 태그에서 호출하는 단위 크기만큼만 계속 내려주는 형식	
    // 플레이 구간을 선택 시 해당하는 구간부터 파일을 내려받는다.
	@GetMapping("/region")
	public String videoRegion(Model model) {
		log.debug("************** class = {}, function = {}", this.getClass().getName(), new Object(){}.getClass().getEnclosingMethod().getName());
		model.addAttribute("videoUrl", "/video/region/video.mp4");
		return TEMPLATE_DIR + "video";
	}
	
	@GetMapping("/region/{fileName}")
	public ResponseEntity<ResourceRegion> vidoeRegionFileName(@PathVariable String fileName, @RequestHeader HttpHeaders headers) throws IOException {
		log.debug("************** class = {}, function = {}", this.getClass().getName(), new Object(){}.getClass().getEnclosingMethod().getName());
		String fileFullPath = UPLOAD_DIR + fileName;
		Resource resource = new FileSystemResource(fileFullPath);
		
		final long chunkSize = 1024 * 1024 * 1;
		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);
			region = new ResourceRegion(resource, start, rangeLength);

			log.debug("**********  httpRange :: rangeLength ==> {}", rangeLength);
		} catch (Exception e) {
			long rangeLength = Long.min(chunkSize, contentLength);
			region = new ResourceRegion(resource, 0, rangeLength);
			log.debug("**********  httpRange error :: rangeLength ==> {}", 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(fileName) // IE 부분 호출을 위해서 설정
        		.body(region);
	}

	
}

 

 

 

+ Recent posts