본문 바로가기
블로그 이미지
Dev-RiQ
Back-end Developer Studying Record
✉️ lwk525678@gmail.com
Back-End/Spring Boot

[Spring Boot] FFmpeg + 스프링부트 연동 및 사용하기

by Dev-RiQ 2025. 5. 12.

이미지, 영상, 음원 등의 파일 인코딩을 변경하고 압축하기 위해
FFmpeg를 이용하기

 

  • FFmpeg 다운로드
https://ffmpeg.org/

 

먼저, ffmpeg를 다운로드하여 원하는 경로에 설치한다. win, linux 설정이 다르기 때문에 잘 확인!

 

  • FFmpeg yml 설정
ffmpeg:
  exe:
    location: [ffmpeg 실행 파일 절대 경로] (ex. /usr/bin/ffmpeg)
ffprobe:
  exe:
    location: [ffprobe 실행 파일 절대 경로] (ex. /usr/bin/ffprobe)
file:
  path: [encoding 파일 저장 절대 경로] (ex. /home/ubuntu/app/)

 

설치한 경로 내부의 ffmpeg, ffprobe 실행파일 및 실행 이후 인코딩된 파일의 저장 경로를 설정해준다. 이후 ffmpeg 전용 폴더를 따로 생성해준다. (윈도우의 경우 ffpmeg 실행 파일 절대 경로 뒤에 .exe 붙여주기)

 

  • Gradle Dependency 추가
implementation("net.bramp.ffmpeg:ffmpeg:0.7.0")

 

  • FFmpegConfig 설정
@Configuration
public class FFmpegConfig {

    @Value("${ffmpeg.exe.location}")
    private String ffmpegLocation;
    @Value("${ffprobe.exe.location}")
    private String ffprobeLocation;

    private final String osName = System.getProperty("os.name");

    @Bean(name = "ffmpeg")
    public FFmpeg ffMpeg() throws IOException {
        if (osName.toLowerCase().contains("win")) {
            return new FFmpeg(new ClassPathResource(ffmpegLocation).getURL().getPath());
        } else {
            return new FFmpeg(ffmpegLocation);
        }
    }

    @Bean(name = "ffprobe")
    public FFprobe ffProbe() throws IOException {
        if (osName.toLowerCase().contains("win")) {
            return new FFprobe(new ClassPathResource(ffprobeLocation).getURL().getPath());
        } else {
            return new FFprobe(ffprobeLocation);
        }
    }

    @Bean
    public FFmpegExecutor ffmpegExecutor(FFmpeg fFmpeg, FFprobe fFprobe) {
        return new FFmpegExecutor(fFmpeg, fFprobe);
    }
}

 

해당 설정에서 실행 파일의 경로를 불러와 스프링부트 내부에서 ffmpeg, ffprobe를 실행할 수 있도록 os별로 설정을 적용해준다.

이후, Image, Video, Audio 등의 필요한 파일별로 EncoderRepository를 생성한다.

 

  • EncoderRepository

Repository 설정에 앞서 사전 설명이 필요한 부분이 있다.

 

1. FFmpeg는 multipartFile을 바로 변환할 수 없어 File객체로 임시 저장이 필요하다.

2. 인코딩 완료 시 임시 저장 파일과 인코딩 파일 두개가 저장되기 때문에 인코딩 후 임시 저장 파일을 삭제할 로직이 필요하다.

    (AWS S3를 이용시 파일 저장 directory 하위 파일 모두 삭제 -> 필자의 경우 해당 방식 차용)

3. 파일을 저장 후 해당 파일의 절대경로를 포함한 이름을 반환한다. (소모를 줄이기 위해서 추후 Util을 생성하여 DB 저장은 filename만 저장, 불러올 시 Util의 절대경로를 붙여서 불러오도록 설정하길 추천)

 

그렇다면 각각의 인코딩 설정을 살펴보자.

 

  • FileType
@Getter
@AllArgsConstructor
public enum FileType {
    IMAGE,
    VIDEO,
    AUDIO;
}

 

클라이언트 측에서 데이터 전송 시 위 세가지의 파일을 한 서비스에서 처리하기 위해 Type을 함께 받아준다.

 

  • ImageEncoderRepository
@Repository
@RequiredArgsConstructor
public class ImageEncoderRepository {

    private final FFmpeg ffmpeg;
    private final FFprobe ffprobe;
    @Value("${file.path}")
    private String path;

    public List<File> imageEncoding(List<MultipartFile> multipartFiles) throws Exception {
    
        // 변환된 파일들을 담을 배열 생성
        List<File> files = new ArrayList<>();
        
        // 받아온 multipartFile을 File 객체로 저장할 폴더 생성 (없을 시 생성)
        Path paths = Paths.get(path);
        Files.createDirectories(paths);
        
        // 받아온 multipartFile 변환 및 저장
        for (MultipartFile multipartFile : multipartFiles) {
        
            // UUID 이용 파일이름 겹치지 않도록 처리
            String saveFileName = changedFileName(multipartFile.getOriginalFilename());
            
            // multipartFile을 원하는 인코딩으로 변환 후 파일 리스트에 담기
            files.add(getConvertImage(multipartFile, saveFileName));
        }
        return files;
    }

    private File getConvertImage(MultipartFile multipartFile, String saveFileName) throws Exception{
    
        // multipartFile의 이미지 정보를 지정 경로에 파일 형식으로 저장
        // FFmpeg는 File 객체를 받아서 변환하기 때문
        BufferedImage bi = ImageIO.read(multipartFile.getInputStream());
        File file = new File(path + saveFileName);
        // 임시로 png 파일로 저장 (jpg로 설정 시 png파일 전송 시 쓰기 오류)
        // png 파일 지정 시 jpg, jpeg, png 쓰기 가능함 확인 (이외 확인 안해봄)
        ImageIO.write(bi,"png", file);
        
        // webp로 변환 후 파일 객체 리턴 (확장자는 변경가능)
        return new File(convertToWebp(file.getParentFile()+"/"+file.getName()));
    }

    public String convertToWebp(String filename) throws Exception {
    
        // 앞서 임시 저장된 png파일을 ffprobe를 이용하여 가져오기 (필수)
        FFmpegProbeResult ffmpegProbeResult = ffprobe.probe(filename);
        // 파일 저장 확장자를 .webp로 변경 (다른 확장자도 사용 가능)
        String name = filename.substring(0, filename.lastIndexOf(".")) + ".webp";
        // ffmpeg에 명령어를 전달하기 위한 빌더 설정
        FFmpegBuilder builder = new FFmpegBuilder()
                .setInput(ffmpegProbeResult) // 기존 임시 저장 파일
                .addOutput(name) // 저장할 이름
                .setFormat("webp") // 저장할 인코딩 포맷
                .done();

        // 완성된 변환 명령어를 수행하는 구간
        FFmpegExecutor executable = new FFmpegExecutor(ffmpeg, ffprobe);
        FFmpegJob ffmpegjob = executable.createJob(builder);
        ffmpegjob.run();
        
        return name;
    }
    
    private String changedFileName(String originName) {
        String random = UUID.randomUUID().toString();
        return random + "_" + originName;
    }

}

 

  • VideoEncoderRepository
@Repository
@RequiredArgsConstructor
public class VideoEncoderRepository {

    private final FFmpeg ffmpeg;
    private final FFprobe ffprobe;
    private final ImageEncoderRepository imageEncoderRepository;
    @Value("${file.path}")
    private String path;

    public List<File> videoEncoding(List<MultipartFile> multipartFiles) throws Exception {
        List<File> files = new ArrayList<>();
        Path paths = Paths.get(path);
        Files.createDirectories(paths);
        
        for (MultipartFile multipartFile : multipartFiles) {
            
            // 파일 이름 UUID 붙여서 변경
            String saveFileName = changedFileName(multipartFile.getOriginalFilename());
            
            // multipartFile -> File 객체
            File file = new File(path + saveFileName);
            Path filePath = file.toPath();
            multipartFile.transferTo(filePath);
            
            // FFprobe 설정
            FFmpegProbeResult ffmpegProbeResult = ffprobe.probe(path + saveFileName);
            files.add(new File(convertVideo(ffmpegProbeResult, saveFileName)));
            
            // 필요 시 썸네일 추가 저장
            String thumbnailName = getThumbnail(ffmpegProbeResult, saveFileName);
            // jpg 썸네일 저장 이후 기존의 imageEncoderRepository이용 webp 변환
            files.add( new File(imageEncoderRepository.convertToWebp(thumbnailName)));
        }
        return files;
    }

    public String convertVideo(FFmpegProbeResult probeResult, String saveFileName) {
        FFmpegStream file = probeResult.getStreams().get(0);
        String filename = path + saveFileName.substring(0, saveFileName.lastIndexOf(".")) + "_encoded.mp4";
        FFmpegBuilder builder = new FFmpegBuilder()
                .setInput(probeResult)
                .addOutput(filename)
                .setAudioCodec("aac") // 영상 오디오 확장자 코덱
                .setAudioBitRate(128000) // 오디오 BitRate
                .setAudioChannels(2) // 오디오 Channels (2 = 스테레오, 1 = 모노)
                .setAudioSampleRate(44100)
                .setFormat("mp4") // 원하는 변경 확장자
                .setVideoCodec("h264") // 비디오 코덱
                .setVideoBitRate(2500000) // 비디오 비트레이트 (720p 기준으로 설정, 1080p는 2배정도)
                .setVideoFrameRate(30) // 프레임 (보통 30)
                .setVideoResolution(file.width, file.height) // 크기 변경 필요 시 변경 (현재 기존 크기)
                .setStrict(FFmpegBuilder.Strict.EXPERIMENTAL)
                .done();

        FFmpegExecutor executable = new FFmpegExecutor(ffmpeg, ffprobe);
        executable.createJob(builder).run();

        return filename;
    }

    // 썸네일 저장하기 (필요 시 적용, 현재 jpg 임시파일로 저장후 webp로 변환)
    private String getThumbnail(FFmpegProbeResult probeResult, String saveFileName) {
        FFmpegStream file = probeResult.getStreams().get(0);
        String filename = path + saveFileName.substring(0, saveFileName.lastIndexOf(".")) + "_thumbnail.jpg";
        FFmpegBuilder builder = new FFmpegBuilder()
                .setInput(probeResult)
                .addOutput(filename)
                .addExtraArgs("-ss","00:00:00") // 원하는 썸네일 구간 설정 (현재 제일 앞부분)
                .addExtraArgs("-vframes","1") // 썸네일은 1프레임만 저장하면 됨
                .setVideoResolution(file.width, file.height) // 크기 설정 (현재 파일 기본 크기)
                .setStrict(FFmpegBuilder.Strict.EXPERIMENTAL)
                .done();

        FFmpegExecutor executable = new FFmpegExecutor(ffmpeg, ffprobe);
        executable.createJob(builder).run();

        return filename;
    }
    
    private String changedFileName(String originName) {
        String random = UUID.randomUUID().toString();
        return random + "_" + originName;
    }
}

 

  • AudioEncoderRepository
@Repository
@RequiredArgsConstructor
public class AudioEncoderRepository {

    private final FFmpeg ffmpeg;
    private final FFprobe ffprobe;
    @Value("${file.path}")
    private String path;

    public List<File> audioEncoding(List<MultipartFile> multipartFiles) throws Exception {
        List<File> files = new ArrayList<>();
        Path paths = Paths.get(path + FileType.AUDIO.getType());
        Files.createDirectories(paths);
        for (MultipartFile multipartFile : multipartFiles) {
            String saveFileName = changedFileName(multipartFile.getOriginalFilename());
            
            // 임시 파일 생성
            File file = new File(path + saveFileName);
            Path filePath = file.toPath();
            multipartFile.transferTo(filePath);
            
            // FFprobe에 담아 Audio convert
            FFmpegProbeResult ffmpegProbeResult = ffprobe.probe(path + saveFileName);
            files.add(new File(convertAudio(ffmpegProbeResult, saveFileName)));
        }
        return files;
    }

    public String convertAudio(FFmpegProbeResult probeResult, String saveFileName) {
        String filename = saveFileName.substring(0, saveFileName.lastIndexOf(".")) + "_encoded.mp3";
        FFmpegBuilder builder = new FFmpegBuilder()
                .setInput(probeResult)
                .addOutput(path + filename)
//                .addExtraArgs("-ss","00:01:00") // 시작시간 (구획 설정 시 필요)
//                .addExtraArgs("-to","00:01:30") // 종료시간 (구획 설정 시 필요)
                .setAudioBitRate(128000)
                .setAudioChannels(2)
                .setAudioSampleRate(44100)
                .setFormat("mp3") // 원하는 인코딩 포멧
                .setStrict(FFmpegBuilder.Strict.EXPERIMENTAL)
                .done();
        FFmpegExecutor executable = new FFmpegExecutor(ffmpeg, ffprobe);
        executable.createJob(builder).run();

        return filename;
    }
    
    private String changedFileName(String originName) {
        String random = UUID.randomUUID().toString();
        return random + "_" + originName;
    }

}

 

 

  • FileEncoderService
@Service
@RequiredArgsConstructor
public class FileEncoderService {

    private final ImageEncoderRepository imageEncoderRepository;
    private final VideoEncoderRepository videoEncoderRepository;
    private final AudioEncoderRepository audioEncoderRepository;

    public List<File> changeEncoding(List<MultipartFile> multipartFiles, FileType type) {
        List<File> files = null;
        try{
            switch (type) {
                case IMAGE -> files = imageEncoderRepository.imageEncoding(multipartFiles);
                case VIDEO -> files = videoEncoderRepository.videoEncoding(multipartFiles);
                case AUDIO -> files = audioEncoderRepository.audioEncoding(multipartFiles);
            }
        }catch (Exception e){
            throw new CustomException(ErrorCode.UNSUCCESSFUL_FILE_UPLOAD);
        }
        return files;
    }

}

 

Service에서는 request를 받아올 때 IMAGE / VIDEO / AUDIO 별로 타입을 명시하여 받아와 처리해주었다.

 

  • FileEncoderController
@Component
@RequiredArgsConstructor
public class FileEncoderController {

    private final FileEncoderService fileEncoderService;

    public List<File> fileEncoding(List<MultipartFile> multipartFiles, FileType type) {
        return fileEncoderService.changeEncoding(multipartFiles, type);
    }
}

 

마지막으로 Controller를 셋팅해주면 마무리.

 

이제 인코딩이 필요한 부분에서 해당 컨트롤러를 호출해주며 multipartFile 리스트 객체와 Type을 전달해주면 바로 사용할 수 있다.

public List<File> upload(List<MultipartFile> multipartFiles, FileType fileType) {
    return fileEncoderController.fileEncoding(multipartFiles, fileType);
}

 

 

  • 주의 사항

1. 클라이언트 측에서 multipart를 1개만 업로드하여 전송하면 List 객체에 담기지 않기에 1개라도 Array 형태로 request를 전송해 주어야 한다.

2. 현재 포스트에서는 File 객체로 리턴을 마무리 하지만, 실제로는 이미 파일이 저장되었고, 보편적으로 파일 이름만 DB에 저장하거나 경로를 기준으로 S3로 전송하기 때문에 Filename 혹은 절대 경로를 리턴하는 List<String>으로 변환할 필요가 있다.

3. 해당 포스트의 FileType은 IMAGE, VIDEO, AUDIO 세가지를 가지는 Enum 클래스이다. Enum 없이 단순 String Parameter로 받아 구분해도 무방하다.

4. multipart를 클라이언트 측에서 받아올 때, header 설정에 (Content-Type: multipart/form-data) 는 필수다.

5. 인코딩 설정은 필자의 임의 설정이기에 속도 / 압축률이 좋지 않을 수 있다. 별도 검색을 통해 변경하길 바란다.

 

 

블로그 이미지
Dev-RiQ
Back-end Developer Studying Record
✉️ lwk525678@gmail.com