본문 바로가기

Develop/Back-End

JAVA 에서 FFMPEG를 통한 영상 정보 읽기 및 영상 인코딩(압축) 처리 도전기

FFMPEG 로고


이번에 내가 맡게 된 업무는 배너 이미지(소재) 만 등록 되던 기능에

운영중인 서비스 환경에 동영상 업로드 및 압축 기능을 추가하는거였다.

 

개발을 잘 모르는 사람들은 "아니 이미 파일 업로드 기능이 있는데? 그냥 기존에 업로드 하던 부분 조금만

손보면 되는거 아니야?" 이렇게 말을 할 수도 있지만

 

개발은 운영중인 서비스에 어떤 기능을 도입하는게

해당 기능을 새로 만드는 것보다 어렵다.. 🤨

 

그러니 운영 중인 서비스에 어느 기능을 넣을 땐 최대한 

많이 검토하는 습관을 길러야한다!!

 

제일 힘들었던건 ffmpeg가 국내에 정보가 많이 없는 것 같다. 🤬

그래서 따라하기 좋은 예제를 github 이슈에 모아놓았다..

 

github 링크는 아래 레퍼런스에 남김 :)



FFmpeg
FFmpeg는 오디오와 비디오를 다루기 위한 오픈 소스 멀티미디어 프레임워크😁.

이 도구는 다양한 형식의 미디어 파일을 변환, 편집, 인코딩, 디코딩하는 데 사용되고 FFmpeg는 커맨드 라인 인터페이스를 제공하며, 다양한 옵션을 통해 다양한 작업을 수행할 수 있다😎

사용 예시 커맨드
1. 비디오 변환:

ffmpeg -i input.mp4 -c:v libx264 -c:a aac output.mp4


이 커맨드는 input.mp4` 파일을 H.264 비디오 코덱과 AAC 오디오 코덱을 사용하여 `output.mp4`로 변환하는 커맨드

2. 오디오 추출:

ffmpeg -i input.mp4 -vn output.mp3


이 커맨드는 input.mp4` 파일에서 오디오를 추출하여 `output.mp3` 파일로 저장하는 커맨드

3. 비디오 자르기:

ffmpeg -i input.mp4 -ss 00:00:10 -t 00:00:20 -c:v copy -c:a copy output.mp4


이 커맨드는 input.mp4` 파일에서 10초부터 30초까지의 구간을 자르아 `output.mp4`로 저장하는 커맨드

FFprobe
FFprobe는 FFmpeg 프로젝트의 일부로서, 미디어 파일의 정보를 분석하는 도구인데

이를 통해 오디오와 비디오 파일의 코덱, 해상도, 비트레이트 등의 정보를 확인할 수 있다! 😆

사용 예시 커맨드

ffprobe input.mp4


이 커맨드는 `input.mp4` 파일에 대한 정보를 출력하는 커맨드 옵션으로는 json이나 xml 형식 문자열로 추출 가능

ffprobe -show_streams -select_streams v:0 -print_format json input.mp4


이 커맨드는 `input.mp4` 파일의 비디오 스트림에 대한 정보를 JSON 형식으로 출력합니다.

이 외에도 FFmpeg와 FFprobe에는 수많은 옵션과 기능이 있으며, 위의 예시는 일부에 불과하니 커맨드를 실행하기 전에 해당 도구의 공식문서를 꼭 참고하도록 해 그리고 위키도!


진행상황

dropzone.js를 커스터마이징 하여

동영상 업로드 및 썸네일 추출 이후 서버에 파일 및 데이터 처리 까지 완료

용량 5MB 초과 시 압축 로직

미디어 메타 인포 긁어오는 기능 추가 ( 해당 동영상에 비율로 업로드 가능 여부를 체크)


업무 중 이슈/고민 또는 해결내용

 

사실 이 업무를 하면서 여러 어려움을 겪었다.

 

1. 로컬(개발) 환경에서 이상 없던 코드가 운영 환경에 반영하니  ffmpeg ,ffprobe 권한이 없어

   접근 거부 급하게 개발하다보니 권한도 신경안쓰다가 운영환경에서 로그보고 뒤늦게 깨달았다..

  (이런 바보같은 실수를 한게 너무 창피하다 😭)

->   리눅스 서버 war 구동 중인 계정에 권한 변경으로 해결

 

2. 미디어 메타 인포 읽어 map으로 변환하여 클라이언트에서 비교하는 로직 

-> 아 이거 하면서 어질어질 했다 일단 추후에 나랑 비슷한걸 개발하게 되는 사람들에게 말해주면

   미디어 인포를 읽을 때는 ffmpeg가 아니라 ffprobe를 사용하자  아래 예제 코드에

   내가 적용한 코드를 첨부해놓았다 🤪 아래 코드에 ffprobe를 이용한 커맨드도 있음 😂 

2-1. 무해화 로직 거친 파일을 검증해야되었기 때문에 서버에 파일 보낼 때는 바이러스 검사 후 이상 없는 파일을 보내 temp 파일로 메타 인포만 읽고 삭제하였다.. (추후 용량 고려하여)

 


참고 레퍼런스

ffmpeg 공식 문서 

https://www.ffmpeg.org/ffmpeg.html

 

ffmpeg Documentation

Table of Contents ffmpeg [global_options] {[input_file_options] -i input_url} ... {[output_file_options] output_url} ... ffmpeg is a universal media converter. It can read a wide variety of inputs - including live grabbing/recording devices - filter, and t

www.ffmpeg.org

https://github.com/DongJu-Na/sbwvss-mvc

ffmpeg 이용하면서 여러 기능 추가해본 git

 

GitHub - DongJu-Na/sbwvss-mvc: Spring Boot WebMvc Video Streaming Server

Spring Boot WebMvc Video Streaming Server. Contribute to DongJu-Na/sbwvss-mvc development by creating an account on GitHub.

github.com

https://github.com/DongJu-Na/sbwvss-mvc/issues/1

효과나 사용할만한 예제 모아놓은 이슈 리스트

 

ffmpeg effect sample · Issue #1 · DongJu-Na/sbwvss-mvc

https://medium.com/numatic-ventures/top-17-ffmpeg-commands-of-video-processing-64b587325d9e ffmpeg17가지 예제 커맨드

github.com


 

Example Code

 

미디어 인포 읽어오는 기능 (BackEnd)

	@PostMapping(value = "/getVideoMetaInfo" , produces = "application/json")
	@CheckAuth(value = true)
	public @ResponseBody Map<String,Object>  getVideoInfo(@RequestParam(value = "file" , required = true) MultipartFile file
	){
		Map<String,Object> result = new HashMap<String,Object>();
		try {
			File tempFile = File.createTempFile("temp", ".mp4");
			file.transferTo(tempFile);
			String tempPath = tempFile.getAbsolutePath().replaceAll("\\\\", "/");

			ProcessBuilder processBuilder = new ProcessBuilder(ffprobe ,"-v","error","-show_format" ,"-show_streams" , "-of" ,"json" ,tempPath);
			processBuilder.redirectErrorStream(true);
			Process process = processBuilder.start();

			InputStream inputStream = process.getInputStream();
			InputStreamReader inputStreamReader = new InputStreamReader(inputStream);
			BufferedReader bufferedReader = new BufferedReader(inputStreamReader);

			String line;
			StringBuilder jsonOutput = new StringBuilder();
			Map<String,Object> jsonMap = null;
			while ((line = bufferedReader.readLine()) != null) {
				jsonOutput.append(line);
			}

			int exitCode = process.waitFor();

			if(jsonOutput.length() > 0 && exitCode == 0){
				ObjectMapper om = new ObjectMapper();
				jsonMap = om.readValue(jsonOutput.toString(), new TypeReference<Map<String,Object>>() {});
			}

			result.put("metaData",jsonMap);
			result.put("resultCode",exitCode);
			tempFile.delete();

		} catch (Exception e) { //IOException | InterruptedException
			e.printStackTrace();
			result.put("metaData",null);
			result.put("resultCode",1);
		}
		return result;
	}

미디어 메타 인포 읽어오기 FrontEnd

function getVideoMetaInfo(_file){
    var _result = null;
    var saveData = new FormData();
        saveData.append("file", _file);

     $.ajax({
            async: false,
            crossDomain: true,
            url: '/item/getVideoMetaInfo',
            method: "POST",
            type: "POST",
            data: saveData,
            mimeType: "multipart/form-data",
            xhrFields: {
                withCredentials: false
            },
            dataType: "json",
            processData: false,
            contentType: false,
            success: function(data) {
                _result = data;
            },
            error: function (request, status, error) {
                console.error(status,error);
            },
            cache: false
    });

    return _result;
}

 

동영상 압축 코드 예제

ProcessBuilder processBuilder = new ProcessBuilder(
						ffmpeg,
						"-y",
						"-v",
						"error",
						"-i",
						filePath + uploadPath,
						"-strict",
						"experimental",
						"-f",
						"mp4",
						"-vcodec",
						"libx264",
						"-s",
						"720x480",
						"-r",
						"24/1",
						"-acodec",
						"aac",
						"-ac",
						"1",
						"-ar",
						"48000",
						"-b:a",
						"32768",
						"-sn",
						filePath + "/" + formatFileName
				);

				Process process = processBuilder.start();
				int exitCode = process.waitFor();

				if (exitCode == 0) {
					if (!"local".equals(profileActive)) {
						Runtime.getRuntime().exec("chmod -R 777 " + filePath + "/" + formatFileName);
					}

					File compressedFile = new File(filePath + "/" + formatFileName);
					long compressedFileSizeInBytes = compressedFile.length();

					if(compressedFileSizeInBytes > fiveMegaBytes){
						uploadPath = "formatErrer-01";
						return uploadPath;
					}

					uploadPath = uploadPath.replace(newFileName,formatFileName);
				}else{
					uploadPath = "formatErrer-01";
				}

 

반응형