개발 창고/Web

[Spring] Movie Streaming 처리

로이제로 2020. 8. 10. 16:17
반응형

최근에 카페24에 스프링 프레임워크를 설치하고 운영중에 버그가 한개 발생했었습니다.

그 버그는 동일 서버내에서 영상 재생을 다이렉트로 하면, Stack Overflow가 발생하면서 재생동안 에러로그가 쌓여 카페24에서 할당받은 용량을 catalina.out이 꽉 차면서 용량이 사용할 수 없는 문제였습니다.

 

확인해보니, 컨트롤러단에서 스트리밍 링크를 직접 연결해주면 해당 문제가 해결되는것을 확인했습니다.

    private final String FOLDER_MOVIE = "{폴더경로}";
    
    /**
     *  @reference : http://aodis.egloos.com/5962812 
     *  @modified  : whiteduck
     */
    @RequestMapping(value="/stream/{video_name:.+}", method= RequestMethod.GET) 
    public String stream(@PathVariable("video_name") String video_name, HttpServletRequest request, HttpServletResponse response) throws UnsupportedEncodingException, IOException {
        // 확장자 확인 //
        String[] filename_seperate = video_name.split("\\.");
        String exp;
        if(filename_seperate.length <= 1 ) {
            // 확장자 에러  //
            throw new RuntimeException("Wrong file name. You need to include expand file name");
            // response.getOutputStream().write(resultMsg.getBytes(SystemInfo.ENCODING));
            // response.getOutputStream().flush();
            // return null;
        } else {
            exp = filename_seperate[1];
        }
        
        // Progressbar 에서 특정 위치를 클릭하거나 해서 임의 위치의 내용을 요청할 수 있으므로
        // 파일의 임의의 위치에서 읽어오기 위해 RandomAccessFile 클래스를 사용한다.
        // 해당 파일이 없을 경우 예외 발생
        File file = new File(FOLDER_MOVIE + video_name);
        if(!file.exists())    throw new FileNotFoundException();
        
        RandomAccessFile randomFile = new RandomAccessFile(file, "r");
        long rangeStart = 0;    // 요청 범위의 시작 위치
        long rangeEnd = 0;      // 요청 범위의 끝 위치
        boolean isPart = false; //부분 요청일 경우 true, 전체 요청의 경우 false
        
        // randomFile 을 클로즈 하기 위하여 try~finally 사용
        try{
            // 동영상 파일 크기
            long movieSize = randomFile.length(); 
            // 스트림 요청 범위, request의 헤더에서 range를 읽는다.
            String range = request.getHeader("range");
            
            // 브라우저에 따라 range 형식이 다른데, 기본 형식은 "bytes={start}-{end}" 형식이다.
            // range가 null이거나, reqStart가  0이고 end가 없을 경우 전체 요청이다.
            // 요청 범위를 구한다.
            if(range != null) {
                // 처리의 편의를 위해 요청 range에 end 값이 없을 경우 넣어줌
                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{
                //range가 null인 경우 동영상 전체 크기로 초기값을 넣어줌. 0부터 시작하므로 -1
                rangeStart = 0;
                rangeEnd = movieSize - 1;
            }
            
            // 전송 파일 크기
            long partSize = rangeEnd - rangeStart + 1;
            
            // 전송시작
            response.reset();
            
            // 전체 요청일 경우 200, 부분 요청일 경우 206을 반환상태 코드로 지정
            response.setStatus(isPart ? 206 : 200);
            
            // mime type 지정
            response.setContentType("video/mp4");
            
            // 전송 내용을 헤드에 넣어준다. 마지막에 파일 전체 크기를 넣는다.
            response.setHeader("Content-Range", "bytes "+rangeStart+"-"+rangeEnd+"/"+movieSize);
            response.setHeader("Accept-Ranges", "bytes");
            response.setHeader("Content-Length", ""+partSize);
            
            OutputStream out = response.getOutputStream();
            // 동영상 파일의 전송시작 위치 지정
            randomFile.seek(rangeStart);
            
            // 파일 전송...  java io는 1회 전송 byte수가 int로 지정됨
            // 동영상 파일의 경우 int형으로는 처리 안되는 크기의 파일이 있으므로
            // 8kb로 잘라서 파일의 크기가 크더라도 문제가 되지 않도록 구현
            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);
        }catch(IOException e){
            // 전송 중에 브라우저를 닫거나, 화면을 전환한 경우 종료해야 하므로 전송취소.
            // progressBar를 클릭한 경우에는 클릭한 위치값으로 재요청이 들어오므로 전송 취소.
        }finally{
             randomFile.close();
        }
        return null;
    }

 

해당 소스는 이글루 whiteduck님의 aodis.egloos.com/5962812 소스를 재구성한 소스입니다.

 

이렇게 소스를 구현하고 html에서 아래와 같이 호출하게 되면

<video width="640" height="344" controls autoplay="autoplay">
	<source src="http://localhost:8080/stream/sample.mp4" type="video/mp4"/>
</video>

FOLDER_MOVIE에서 지정한 폴더 냉에서 sample.mp4파일을 호출해서 재생하게 됩니다.

 

반응형