들어가기 전
- 원인을 읽기 전에 정리를 먼저 본 다음 보시는 걸 추천한다.
문제 발생
실행한 코드
private String encodingToHls(File file) throws IOException {
String uploadedFileUrl = uploadPath + "/master.m3u8";
FFmpegProbeResult probeResult = fFprobe.probe(file.getAbsolutePath());
FFmpegBuilder fFmpegBuilder = new FFmpegBuilder()
.setInput(file.getAbsolutePath())
.overrideOutputFiles(true)
.addOutput(uploadedFileUrl)
.setFormat("hls")
.addExtraArgs("-hls_time", "10")
.addExtraArgs("-hls_list_size", "0")
.addExtraArgs("-hls_segment_filename", uploadPath + File.separator + "master_%08d.ts")
.done();
FFmpegExecutor executor = new FFmpegExecutor(fFmpeg, fFprobe);
FFmpegJob job = executor
.createJob(fFmpegBuilder, progress -> {
log.info("progress ==> {}", progress);
if (progress.status.equals(Progress.Status.END)) {
log.info("===== JOB FINISHED =====");
}
});
job.run();
if (job.getState() == FFmpegJob.State.FINISHED) {
log.info("FINISH!!");
}
return uploadedFileUrl;
예외 발생
2024-10-17T17:05:43.016+09:00 INFO 27071 --- [nio-8080-exec-1] net.bramp.ffmpeg.RunProcessFunction : /opt/homebrew/bin/ffmpeg -y -v error -progress tcp://127.0.0.1:60771 -i /temp/테스트_동영상_202410171704165790.mov -f hls -hls_time 10 -hls_list_size 0 -hls_segment_filename /media/master_%08d.ts /media/master.m3u8
Exception in thread "TcpProgressParser(tcp://127.0.0.1:60771)" java.lang.IllegalArgumentException: invalid time 'N/A'
at net.bramp.ffmpeg.FFmpegUtils.fromTimecode(FFmpegUtils.java:83)
at net.bramp.ffmpeg.progress.Progress.parseLine(Progress.java:164)
at net.bramp.ffmpeg.progress.StreamProgressParser.processReader(StreamProgressParser.java:41)
at net.bramp.ffmpeg.progress.StreamProgressParser.processStream(StreamProgressParser.java:32)
at net.bramp.ffmpeg.progress.TcpProgressParserRunnable.run(TcpProgressParserRunnable.java:35)
at java.base/java.lang.Thread.run(Thread.java:840)
시도
- ffmpeg에
-ss
옵션을 추가해보자 - ⇒ ffmpeg 문제가 아니었음
- 디버깅해서 하나씩 따라가보기
- FFmpegUtils.fromTimecode(String time)에서 입력으로 들어오는 time이 “N/A” 이기 때문에 예외처리가 되버림
원인
ffmpeg-cli-wrapper 라이브러리에서는 내가 코드로 추가한 ffmpeg 옵션 이외에도 아래의 옵션이 자동으로 입력된다.
- -y
- 묻지 않고 출력 파일을 덮어쓴다.
- -v [flags+]
- 라이브러리에서 사용하는 로깅 수준과 플래그를 설정한다.
- quiet, error, warning, info, debug 등
- -progress url
- 프로그램 진행 상황 정보를 url로 보낸다.
- 진행률 정보는 주기적으로 그리고 인코딩 프로세스가 끝날 때 작성된다.
- “key=value” 줄로 구성되어 있고, 키는 영숫자로만 구성된다.
- 진행률 정보 시퀀스의 마지막 키는 “progress”이다. 값은 “continue”와 “end”가 있다.
*그 외에 ffmpeg 옵션을 궁금하다면 ffmpeg Documentation 참고 (https://www.ffmpeg.org/ffmpeg-all.html)
ffmepg-cli-wrapper의 개발자들은 ffmpeg가 실행되는 동안 출력을 분석하고 진행 상황을 출력해주는 기능을 추가하기 위해 아래와 같은 의견을 나누었다.
ffmpeg의 출력은 -progress /dev/stdout 옵션을 통해 현재 터미널에서 진행상황을 출력해보면 아래와 같은 결과가 출력된다.
(짧게 이해해보기로는) 출력을 분석하고 진행상황을 출력하는 기능이 동작하는 방식은 아래와 같다. (실제로는 엄청나게 많은 과정을 거친다. 고작 이것이 자바다를 1회독 한 내 자바 실력으로는 제대로 이해하기에는 어림도 없다.. 오픈소스 개발자들은 대단하다)
- Runnable을 상속받은 TcpProgressParserRunnable을 실행시킨다. (스레드 A)
- -progress 옵션을 통해 스레드A에 출력 결과를 보낸다.
- StreamProgressParser.processReader()를 통해 출력 결과를 읽어온다.
- Progress.parserLine()으로 출력 결과가 없을 때까지 읽으면서 파싱을 진행한다.
at net.bramp.ffmpeg.FFmpegUtils.fromTimecode(FFmpegUtils.java:83)
at net.bramp.ffmpeg.progress.Progress.parseLine(Progress.java:164)
at net.bramp.ffmpeg.progress.StreamProgressParser.processReader(StreamProgressParser.java:41)
at net.bramp.ffmpeg.progress.StreamProgressParser.processStream(StreamProgressParser.java:32)
at net.bramp.ffmpeg.progress.TcpProgressParserRunnable.run(TcpProgressParserRunnable.java:35)
at java.base/java.lang.Thread.run(Thread.java:840)
여기에서 진행상황 중에 out_time 이라는 키가 포함되어 있는데, 왜인지 모르겠지만 그 값이 out_time=N/A로 들어온다. 여기서 문제가 발생하는데 bitrate나 total_size는 값이 N/A인 경우, -1L을 저장하게 되어 있는데 out_time은 그런게 없이 냅다 Exception을 반환해버린다. 그렇기 때문에 Exception in thread "TcpProgressParser(tcp://127.0.0.1:60771)" java.lang.IllegalArgumentException: invalid time 'N/A' 라는 예외가 발생해버린 것이다.
아래는 Progress의 parseLine 메서드이다.
아래는 FFmpegUtils의 fromTimecode 메서드이다.
이 부분에서 문제가 생기는데 Mathcher를 통해 time을 분석하고, find()를 통해 값을 정규식에 일치하는 값이 있는지 확인하는데 out_time=N/A로 입력이 들어왔기 때문에 예외를 발생시킨다.
원인을 찾았으니 누군가 관련된 이슈를 올렸는지 깃허브 레포지토리에서 확인해봤는데, 이미 관련된 주제로 이슈가 올라와있었고 해당 이슈가 closed가 된 상태였다.
- https://github.com/bramp/ffmpeg-cli-wrapper/issues/302
- https://github.com/bramp/ffmpeg-cli-wrapper/pull/315
게다가 해당 이슈에 연결된 PR이 있고, Merged가 되어있었고, time이 N/A
인 경우, -1을 반환하게끔 코드도 변경되어 있었다.
정리
정리하자면 ffmepg-cli-wrapper의 개발자들이 ffmepg가 실행되는 동안 출력을 파싱하고 진행 상황을 출력하는 기능을 추가했다. 이때, 파싱하는 과정에서 특정 부분(out_time)에 N/A인 상태가 되어있고, out_time=N/A가 입력됐을 때, 별도의 처리가 안된 상태라서 예외가 발생하는 것이다.
이 문제는 이미 이슈화 됐고, 코드 변경도 되었지만 maven central repository에는 배포가 안된 상태였기 때문에 발생한 문제였다.
아래 스크린샷에서도 확인할 수 있듯이 spring boot의 의존성에 가장 최신 버전인 'net.bramp.ffmpeg:ffmpeg:0.8.0'를 추가해봐도 관련 코드는 업데이트가 되어있지 않다.
해결 방법
두 가지 방법을 생각할 수 있겠다.
- Progress 클래스 관련된 코드를 지우고 사용하는 방법 (우선,이 방법으로 진행할 예정)
- 깃허브 코드를 직접 가져오는 방법 (사용해보지 않았다)
실제로 createJob을 할 때, listener 부분을 비우고 사용하면 해당 예외가 발생하지 않는다. 대신 진행사항을 로깅할 수 없다.
아래는 변경된 코드이다.
private String encodingToHls(File file) throws IOException {
File destinationUrl = new File(uploadPath);
if (!destinationUrl.exists()) {
destinationUrl.mkdir();
}
String uploadedFileUrl = uploadPath + "/master.m3u8";
FFmpegProbeResult probeResult = fFprobe.probe(file.getAbsolutePath());
FFmpegBuilder fFmpegBuilder = new FFmpegBuilder()
.setInput(file.getAbsolutePath())
.overrideOutputFiles(true)
.addOutput(uploadedFileUrl)
.setFormat("hls")
.addExtraArgs("-hls_time", "10")
.addExtraArgs("-hls_list_size", "0")
.addExtraArgs("-hls_segment_filename", uploadPath + File.separator + "master_%08d.ts")
.done();
FFmpegExecutor executor = new FFmpegExecutor(fFmpeg, fFprobe);
FFmpegJob job = executor
.createJob(fFmpegBuilder);
job.run();
return uploadedFileUrl;
}
추가
아래는 -progress 옵션과 -v 옵션을 제외하고 실행했을 때, 출력되는 결과이다.
ffmpeg version 7.1 Copyright (c) 2000-2024 the FFmpeg developers
built with Apple clang version 15.0.0 (clang-1500.3.9.4)
configuration: --prefix=/opt/homebrew/Cellar/ffmpeg/7.1 --enable-shared --enable-pthreads --enable-version3 --cc=clang --host-cflags= --host-ldflags='-Wl,-ld_classic' --enable-ffplay --enable-gnutls --enable-gpl --enable-libaom --enable-libaribb24 --enable-libbluray --enable-libdav1d --enable-libharfbuzz --enable-libjxl --enable-libmp3lame --enable-libopus --enable-librav1e --enable-librist --enable-librubberband --enable-libsnappy --enable-libsrt --enable-libssh --enable-libsvtav1 --enable-libtesseract --enable-libtheora --enable-libvidstab --enable-libvmaf --enable-libvorbis --enable-libvpx --enable-libwebp --enable-libx264 --enable-libx265 --enable-libxml2 --enable-libxvid --enable-lzma --enable-libfontconfig --enable-libfreetype --enable-frei0r --enable-libass --enable-libopencore-amrnb --enable-libopencore-amrwb --enable-libopenjpeg --enable-libspeex --enable-libsoxr --enable-libzmq --enable-libzimg --disable-libjack --disable-indev=jack --enable-videotoolbox --enable-audiotoolbox --enable-neon
libavutil 59. 39.100 / 59. 39.100
libavcodec 61. 19.100 / 61. 19.100
libavformat 61. 7.100 / 61. 7.100
libavdevice 61. 3.100 / 61. 3.100
libavfilter 10. 4.100 / 10. 4.100
libswscale 8. 3.100 / 8. 3.100
libswresample 5. 3.100 / 5. 3.100
libpostproc 58. 3.100 / 58. 3.100
Input #0, mov,mp4,m4a,3gp,3g2,mj2, from '/temp/테스트_동영상_202410192131057090.mov':
Metadata:
major_brand : qt
minor_version : 0
compatible_brands: qt
creation_time : 2024-10-17T03:20:12.000000Z
com.apple.quicktime.make: Apple
com.apple.quicktime.model: MacBookAir10,1
com.apple.quicktime.software: macOS 14.6.1 (23G93)
com.apple.quicktime.creationdate: 2024-10-17T12:20:10+0900
Duration: 00:00:28.32, start: 0.000000, bitrate: 48577 kb/s
Stream #0:0[0x1](und): Video: h264 (Main) (avc1 / 0x31637661), yuv420p(tv, bt709, progressive), 2880x1800, 47025 kb/s, 60 fps, 60 tbr, 6k tbn (default)
Metadata:
creation_time : 2024-10-17T03:20:12.000000Z
handler_name : Core Media Video
vendor_id : [0][0][0][0]
encoder : H.264
Stream #0:1[0x2](und): Audio: aac (LC) (mp4a / 0x6134706D), 48000 Hz, stereo, fltp, 248 kb/s (default)
Metadata:
creation_time : 2024-10-17T03:20:12.000000Z
handler_name : Core Media Audio
vendor_id : [0][0][0][0]
Stream mapping:
Stream #0:0 -> #0:0 (h264 (native) -> h264 (libx264))
Stream #0:1 -> #0:1 (aac (native) -> aac (native))
Press [q] to stop, [?] for help
[libx264 @ 0x14070fba0] using cpu capabilities: ARMv8 NEON
[libx264 @ 0x14070fba0] profile High, level 5.2, 4:2:0, 8-bit
[libx264 @ 0x14070fba0] 264 - core 164 r3108 31e19f9 - H.264/MPEG-4 AVC codec - Copyleft 2003-2023 - http://www.videolan.org/x264.html - options: cabac=1 ref=3 deblock=1:0:0 analyse=0x3:0x113 me=hex subme=7 psy=1 psy_rd=1.00:0.00 mixed_ref=1 me_range=16 chroma_me=1 trellis=1 8x8dct=1 cqm=0 deadzone=21,11 fast_pskip=1 chroma_qp_offset=-2 threads=12 lookahead_threads=2 sliced_threads=0 nr=0 decimate=1 interlaced=0 bluray_compat=0 constrained_intra=0 bframes=3 b_pyramid=2 b_adapt=1 b_bias=0 direct=1 weightb=1 open_gop=0 weightp=2 keyint=250 keyint_min=25 scenecut=40 intra_refresh=0 rc_lookahead=40 rc=crf mbtree=1 crf=23.0 qcomp=0.60 qpmin=0 qpmax=69 qpstep=4 ip_ratio=1.40 aq=1:1.00
Output #0, hls, to '/media/master.m3u8':
Metadata:
major_brand : qt
minor_version : 0
compatible_brands: qt
com.apple.quicktime.creationdate: 2024-10-17T12:20:10+0900
com.apple.quicktime.make: Apple
com.apple.quicktime.model: MacBookAir10,1
com.apple.quicktime.software: macOS 14.6.1 (23G93)
encoder : Lavf61.7.100
Stream #0:0(und): Video: h264, yuv420p(tv, bt709, progressive), 2880x1800, q=2-31, 60 fps, 90k tbn (default)
Metadata:
creation_time : 2024-10-17T03:20:12.000000Z
handler_name : Core Media Video
vendor_id : [0][0][0][0]
encoder : Lavc61.19.100 libx264
Side data:
cpb: bitrate max/min/avg: 0/0/0 buffer size: 0 vbv_delay: N/A
Stream #0:1(und): Audio: aac (LC), 48000 Hz, stereo, fltp, 128 kb/s (default)
Metadata:
creation_time : 2024-10-17T03:20:12.000000Z
handler_name : Core Media Audio
vendor_id : [0][0][0][0]
encoder : Lavc61.19.100 aac
[hls @ 0x14070f2f0] Opening '/media/master_00000000.ts' for writing
[hls @ 0x14070f2f0] Opening '/media/master.m3u8.tmp' for writing
[hls @ 0x14070f2f0] Opening '/media/master_00000001.ts' for writing
[hls @ 0x14070f2f0] Opening '/media/master.m3u8.tmp' for writing
[hls @ 0x14070f2f0] Opening '/media/master_00000002.ts' for writing
[hls @ 0x14070f2f0] Opening '/media/master.m3u8.tmp' for writing
[out#0/hls @ 0x600000620300] video:12403KiB audio:444KiB subtitle:0KiB other streams:0KiB global headers:0KiB muxing overhead: unknown
frame= 1699 fps= 51 q=-1.0 Lsize=N/A time=00:00:28.28 bitrate=N/A speed=0.842x
[libx264 @ 0x14070fba0] frame I:7 Avg QP:18.96 size:435551
[libx264 @ 0x14070fba0] frame P:434 Avg QP:22.52 size: 15884
[libx264 @ 0x14070fba0] frame B:1258 Avg QP:25.27 size: 2192
[libx264 @ 0x14070fba0] consecutive B-frames: 0.8% 1.3% 0.7% 97.2%
[libx264 @ 0x14070fba0] mb I I16..4: 22.1% 44.5% 33.3%
[libx264 @ 0x14070fba0] mb P I16..4: 0.2% 0.8% 0.3% P16..4: 6.5% 1.9% 1.3% 0.0% 0.0% skip:89.0%
[libx264 @ 0x14070fba0] mb B I16..4: 0.0% 0.1% 0.0% B16..8: 6.8% 0.4% 0.1% direct: 0.2% skip:92.5% L0:53.0% L1:46.4% BI: 0.6%
[libx264 @ 0x14070fba0] 8x8 transform intra:52.9% inter:47.3%
[libx264 @ 0x14070fba0] coded y,uvDC,uvAC intra: 37.9% 30.2% 17.2% inter: 1.0% 0.5% 0.0%
[libx264 @ 0x14070fba0] i16 v,h,dc,p: 40% 48% 9% 3%
[libx264 @ 0x14070fba0] i8 v,h,dc,ddl,ddr,vr,hd,vl,hu: 27% 14% 42% 2% 4% 3% 3% 2% 3%
[libx264 @ 0x14070fba0] i4 v,h,dc,ddl,ddr,vr,hd,vl,hu: 29% 23% 23% 3% 5% 5% 5% 3% 4%
[libx264 @ 0x14070fba0] i8c dc,h,v,p: 65% 20% 12% 2%
[libx264 @ 0x14070fba0] Weighted P-Frames: Y:0.0% UV:0.0%
[libx264 @ 0x14070fba0] ref P L0: 66.1% 8.7% 16.5% 8.7%
[libx264 @ 0x14070fba0] ref B L0: 90.1% 8.8% 1.2%
[libx264 @ 0x14070fba0] ref B L1: 95.5% 4.5%
[libx264 @ 0x14070fba0] kb/s:3587.99
[aac @ 0x140757250] Qavg: 513.980
참고 자료
- https://github.com/bramp/ffmpeg-cli-wrapper/issues/21
- https://github.com/bramp/ffmpeg-cli-wrapper/issues/302
- https://github.com/bramp/ffmpeg-cli-wrapper/pull/315
- https://github.com/bramp/ffmpeg-cli-wrapper/issues/21#issuecomment-226985803
- [Github | bramp/ffmpeg-cli-wrapper] TcpProgressParserRunnable.java
- [Github | bramp/ffmpeg-cli-wrapper] StreamProgressParser.java
- [Github | bramp/ffmpeg-cli-wrapper] Progress.java
- [Github] bramp/ffmpeg/FFmpegUtils.java
- ffmpeg Documentation
댓글