diff --git a/common/src/main/java/ctbrec/MpegUtil.java b/common/src/main/java/ctbrec/MpegUtil.java index 3bb370be..b8a1b959 100644 --- a/common/src/main/java/ctbrec/MpegUtil.java +++ b/common/src/main/java/ctbrec/MpegUtil.java @@ -23,7 +23,7 @@ public class MpegUtil { private MpegUtil() {} - public static double getFileDuration(File file) throws IOException { + public static double getFileDurationInSecs(File file) throws IOException { try(FileChannelWrapper ch = NIOUtils.readableChannel(file)) { _2 m2tsDemuxer = createM2TSDemuxer(ch, TrackType.VIDEO); Demuxer demuxer = m2tsDemuxer.v1; diff --git a/common/src/main/java/ctbrec/recorder/PlaylistGenerator.java b/common/src/main/java/ctbrec/recorder/PlaylistGenerator.java index dd522d49..bfbc05a2 100644 --- a/common/src/main/java/ctbrec/recorder/PlaylistGenerator.java +++ b/common/src/main/java/ctbrec/recorder/PlaylistGenerator.java @@ -63,7 +63,7 @@ public class PlaylistGenerator { try { float duration = 0; if (file.getName().toLowerCase().endsWith(".ts")) { - duration = (float) MpegUtil.getFileDuration(file); + duration = (float) MpegUtil.getFileDurationInSecs(file); if (duration <= 0) { throw new InvalidTrackLengthException("Track has negative duration: " + file.getName()); } diff --git a/common/src/main/java/ctbrec/recorder/download/VideoLengthDetector.java b/common/src/main/java/ctbrec/recorder/download/VideoLengthDetector.java new file mode 100644 index 00000000..c06f0adc --- /dev/null +++ b/common/src/main/java/ctbrec/recorder/download/VideoLengthDetector.java @@ -0,0 +1,75 @@ +package ctbrec.recorder.download; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.time.Duration; +import java.util.Arrays; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ctbrec.OS; +import ctbrec.io.DevNull; +import ctbrec.io.StreamRedirectThread; + +public class VideoLengthDetector { + private static final Logger LOG = LoggerFactory.getLogger(VideoLengthDetector.class); + + /** + * Tries to determine the length of a video file by calling FFmpeg and parsing it's output + * + * @param videoFile + * @return the length as Duration object. The duration is negative, if the length couldn't be determined + */ + public static Duration getLength(File videoFile) { + try { + String[] args = { + "-hide_banner", + "-i", + videoFile.getCanonicalPath(), + "-f", + "ffmetadata", + "-" + }; + String[] cmdline = OS.getFFmpegCommand(args); + LOG.debug("Command line: {}", Arrays.toString(cmdline)); + Process ffmpeg = Runtime.getRuntime().exec(cmdline, new String[0], videoFile.getParentFile()); + int exitCode = 1; + ByteArrayOutputStream stdErrBuffer = new ByteArrayOutputStream(); + Thread stdout = new Thread(new StreamRedirectThread(ffmpeg.getInputStream(), new DevNull())); + Thread stderr = new Thread(new StreamRedirectThread(ffmpeg.getErrorStream(), stdErrBuffer)); + stdout.start(); + stderr.start(); + exitCode = ffmpeg.waitFor(); + LOG.debug("FFmpeg exited with code {}", exitCode); + stdout.join(); + stderr.join(); + String ffmpegStderr = new String(stdErrBuffer.toByteArray()); + return parseDuration(ffmpegStderr); + } catch (IOException | ProcessExitedUncleanException e) { + LOG.error("Error in FFMpeg thread", e); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + LOG.info("Interrupted while waiting for ffmpeg", e); + } + return Duration.ZERO.minusSeconds(1); + } + + private static Duration parseDuration(String ffmpegStderr) { + Matcher m = Pattern.compile("Duration: (\\d{2}):(\\d{2}):(\\d{2}).(\\d{2}),").matcher(ffmpegStderr); + if (m.find()) { + int hours = Integer.parseInt(m.group(1)); + int minutes = Integer.parseInt(m.group(2)); + int seconds = Integer.parseInt(m.group(3)); + int millis = Integer.parseInt(m.group(4)); + return Duration.ofHours(hours) + .plusMinutes(minutes) + .plusSeconds(seconds) + .plusMillis(millis); + } + return Duration.ofSeconds(-1); + } +} diff --git a/common/src/main/java/ctbrec/recorder/download/hls/HlsDownload.java b/common/src/main/java/ctbrec/recorder/download/hls/HlsDownload.java index 1d2f034b..adca2dc2 100644 --- a/common/src/main/java/ctbrec/recorder/download/hls/HlsDownload.java +++ b/common/src/main/java/ctbrec/recorder/download/hls/HlsDownload.java @@ -348,7 +348,7 @@ public class HlsDownload extends AbstractHlsDownload { } catch (IOException | ParseException | PlaylistException e) { LOG.error("Couldn't determine recording length", e); } - return Duration.ofSeconds(0); + return Duration.ofSeconds(-1); } private double getPlaylistLength(File playlist) throws IOException, ParseException, PlaylistException { diff --git a/common/src/main/java/ctbrec/recorder/download/hls/MergedFfmpegHlsDownload.java b/common/src/main/java/ctbrec/recorder/download/hls/MergedFfmpegHlsDownload.java index a3d263cd..b86b0d55 100644 --- a/common/src/main/java/ctbrec/recorder/download/hls/MergedFfmpegHlsDownload.java +++ b/common/src/main/java/ctbrec/recorder/download/hls/MergedFfmpegHlsDownload.java @@ -44,6 +44,7 @@ import ctbrec.io.StreamRedirectThread; import ctbrec.recorder.ProgressListener; import ctbrec.recorder.download.HttpHeaderFactory; import ctbrec.recorder.download.ProcessExitedUncleanException; +import ctbrec.recorder.download.VideoLengthDetector; import okhttp3.Request; import okhttp3.Request.Builder; import okhttp3.Response; @@ -546,7 +547,11 @@ public class MergedFfmpegHlsDownload extends AbstractHlsDownload { @Override public Duration getLength() { - return Duration.between(getStartTime(), Instant.now()); + try { + return VideoLengthDetector.getLength(targetFile); + } catch (Exception e) { + return Duration.between(getStartTime(), Instant.now()); + } } @Override diff --git a/common/src/main/java/ctbrec/recorder/postprocessing/DeleteTooShort.java b/common/src/main/java/ctbrec/recorder/postprocessing/DeleteTooShort.java index bdf38b4f..5d2ed49b 100644 --- a/common/src/main/java/ctbrec/recorder/postprocessing/DeleteTooShort.java +++ b/common/src/main/java/ctbrec/recorder/postprocessing/DeleteTooShort.java @@ -25,7 +25,7 @@ public class DeleteTooShort extends AbstractPostProcessor { Duration minimumLengthInSeconds = Duration.ofSeconds(Integer.parseInt(getConfig().getOrDefault(MIN_LEN_IN_SECS, "0"))); if (minimumLengthInSeconds.getSeconds() > 0) { Duration recordingLength = rec.getLength(); - if (recordingLength.compareTo(minimumLengthInSeconds) < 0) { + if (!(recordingLength.isNegative() || recordingLength.isZero()) && recordingLength.compareTo(minimumLengthInSeconds) < 0) { LOG.info("Deleting too short recording {} [{} < {}]", rec, recordingLength, minimumLengthInSeconds); recordingManager.delete(rec); }