From ceb7c07aa8b115b395a4abaf91dd2cc34d65b7a4 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Thu, 13 Dec 2018 20:54:10 +0100 Subject: [PATCH] Add setting for minimum recording length If a recording is shorter than x seconds, it gets deleted --- .../java/ctbrec/ui/settings/SettingsTab.java | 21 ++++++ common/src/main/java/ctbrec/MpegUtil.java | 73 +++++++++++++++++++ common/src/main/java/ctbrec/Settings.java | 1 + .../java/ctbrec/recorder/LocalRecorder.java | 73 +++++++++++++++++++ .../ctbrec/recorder/PlaylistGenerator.java | 55 +------------- 5 files changed, 171 insertions(+), 52 deletions(-) create mode 100644 common/src/main/java/ctbrec/MpegUtil.java diff --git a/client/src/main/java/ctbrec/ui/settings/SettingsTab.java b/client/src/main/java/ctbrec/ui/settings/SettingsTab.java index d9630ff9..c06733b2 100644 --- a/client/src/main/java/ctbrec/ui/settings/SettingsTab.java +++ b/client/src/main/java/ctbrec/ui/settings/SettingsTab.java @@ -58,6 +58,7 @@ public class SettingsTab extends Tab implements TabSelectionListener { private TextField port; private TextField onlineCheckIntervalInSecs; private TextField leaveSpaceOnDevice; + private TextField minimumLengthInSecs; private CheckBox loadResolution; private CheckBox secureCommunication = new CheckBox(); private CheckBox chooseStreamQuality = new CheckBox(); @@ -360,6 +361,26 @@ public class SettingsTab extends Tab implements TabSelectionListener { GridPane.setMargin(leaveSpaceOnDevice, new Insets(0, 0, 0, CHECKBOX_MARGIN)); layout.add(leaveSpaceOnDevice, 1, row++); + tt = new Tooltip("Delete recordings, which are shorter than x seconds. 0 to disable."); + l = new Label("Delete recordings shorter than (secs)"); + l.setTooltip(tt); + layout.add(l, 0, row); + int minimumLengthInSeconds = Config.getInstance().getSettings().minimumLengthInSeconds; + minimumLengthInSecs = new TextField(Integer.toString(minimumLengthInSeconds)); + minimumLengthInSecs.setTooltip(tt); + minimumLengthInSecs.textProperty().addListener((observable, oldValue, newValue) -> { + if (!newValue.matches("\\d*")) { + minimumLengthInSecs.setText(newValue.replaceAll("[^\\d]", "")); + } + if(!minimumLengthInSecs.getText().isEmpty()) { + int minimumLength = Integer.parseInt(minimumLengthInSecs.getText()); + Config.getInstance().getSettings().minimumLengthInSeconds = minimumLength; + saveConfig(); + } + }); + GridPane.setMargin(minimumLengthInSecs, new Insets(0, 0, 0, CHECKBOX_MARGIN)); + layout.add(minimumLengthInSecs, 1, row++); + TitledPane locations = new TitledPane("Recorder", layout); locations.setCollapsible(false); return locations; diff --git a/common/src/main/java/ctbrec/MpegUtil.java b/common/src/main/java/ctbrec/MpegUtil.java new file mode 100644 index 00000000..4ef37a53 --- /dev/null +++ b/common/src/main/java/ctbrec/MpegUtil.java @@ -0,0 +1,73 @@ +package ctbrec; + +import java.io.File; +import java.io.IOException; +import java.nio.channels.ReadableByteChannel; +import java.time.Duration; +import java.util.Set; + +import org.jcodec.common.Demuxer; +import org.jcodec.common.DemuxerTrack; +import org.jcodec.common.TrackType; +import org.jcodec.common.Tuple; +import org.jcodec.common.Tuple._2; +import org.jcodec.common.io.FileChannelWrapper; +import org.jcodec.common.io.NIOUtils; +import org.jcodec.common.model.Packet; +import org.jcodec.containers.mps.MPSDemuxer; +import org.jcodec.containers.mps.MTSDemuxer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class MpegUtil { + private static final transient Logger LOG = LoggerFactory.getLogger(MpegUtil.class); + + public static void main(String[] args) throws IOException { + readFile(new File("../../test-recs/ff.ts")); + } + + public static void readFile(File file) throws IOException { + System.out.println(file.getCanonicalPath()); + double duration = MpegUtil.getFileDuration(file); + System.out.println(Duration.ofSeconds((long) duration)); + } + + public static double getFileDuration(File file) throws IOException { + try(FileChannelWrapper ch = NIOUtils.readableChannel(file)) { + _2 m2tsDemuxer = createM2TSDemuxer(ch, TrackType.VIDEO); + Demuxer demuxer = m2tsDemuxer.v1; + DemuxerTrack videoDemux = demuxer.getTracks().get(0); + Packet videoFrame = null; + double totalDuration = 0; + while( (videoFrame = videoDemux.nextFrame()) != null) { + totalDuration += videoFrame.getDurationD(); + } + return totalDuration; + } + } + + public static _2 createM2TSDemuxer(FileChannelWrapper ch, TrackType targetTrack) throws IOException { + MTSDemuxer mts = new MTSDemuxer(ch); + Set programs = mts.getPrograms(); + if (programs.size() == 0) { + LOG.error("The MPEG TS stream contains no programs"); + return null; + } + Tuple._2 found = null; + for (Integer pid : programs) { + ReadableByteChannel program = mts.getProgram(pid); + if (found != null) { + program.close(); + continue; + } + MPSDemuxer demuxer = new MPSDemuxer(program); + if (targetTrack == TrackType.AUDIO && demuxer.getAudioTracks().size() > 0 + || targetTrack == TrackType.VIDEO && demuxer.getVideoTracks().size() > 0) { + found = org.jcodec.common.Tuple._2(pid, (Demuxer) demuxer); + } else { + program.close(); + } + } + return found; + } +} diff --git a/common/src/main/java/ctbrec/Settings.java b/common/src/main/java/ctbrec/Settings.java index 384ea432..043b55af 100644 --- a/common/src/main/java/ctbrec/Settings.java +++ b/common/src/main/java/ctbrec/Settings.java @@ -41,6 +41,7 @@ public class Settings { public String recordingsDir = System.getProperty("user.home") + File.separator + "ctbrec"; public DirectoryStructure recordingsDirStructure = DirectoryStructure.FLAT; public long minimumSpaceLeftInBytes = 0; + public int minimumLengthInSeconds = 0; public String mediaPlayer = "/usr/bin/mpv"; public String postProcessing = ""; public String username = ""; // chaturbate username TODO maybe rename this onetime diff --git a/common/src/main/java/ctbrec/recorder/LocalRecorder.java b/common/src/main/java/ctbrec/recorder/LocalRecorder.java index 0732220b..6e16c809 100644 --- a/common/src/main/java/ctbrec/recorder/LocalRecorder.java +++ b/common/src/main/java/ctbrec/recorder/LocalRecorder.java @@ -4,6 +4,8 @@ import static ctbrec.Recording.State.*; import static ctbrec.event.Event.Type.*; import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; import java.io.FilenameFilter; import java.io.IOException; import java.nio.file.FileStore; @@ -11,6 +13,7 @@ import java.nio.file.Files; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.text.SimpleDateFormat; +import java.time.Duration; import java.time.Instant; import java.util.ArrayList; import java.util.Arrays; @@ -34,11 +37,19 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.eventbus.Subscribe; +import com.iheartradio.m3u8.Encoding; +import com.iheartradio.m3u8.Format; import com.iheartradio.m3u8.ParseException; +import com.iheartradio.m3u8.ParsingMode; import com.iheartradio.m3u8.PlaylistException; +import com.iheartradio.m3u8.PlaylistParser; +import com.iheartradio.m3u8.data.MediaPlaylist; +import com.iheartradio.m3u8.data.Playlist; +import com.iheartradio.m3u8.data.TrackData; import ctbrec.Config; import ctbrec.Model; +import ctbrec.MpegUtil; import ctbrec.OS; import ctbrec.Recording; import ctbrec.Recording.State; @@ -740,9 +751,71 @@ public class LocalRecorder implements Recorder { fireRecordingStateChanged(download.getTarget(), GENERATING_PLAYLIST, download.getModel(), download.getStartTime()); generatePlaylist(download.getTarget()); } + boolean deleted = deleteIfTooShort(download); + if(deleted) { + // recording was too short. stop here and don't do post-processing + return; + } fireRecordingStateChanged(download.getTarget(), POST_PROCESSING, download.getModel(), download.getStartTime()); postprocess(download); fireRecordingStateChanged(download.getTarget(), FINISHED, download.getModel(), download.getStartTime()); }; } + + + // TODO maybe get file size and bitrate and check, if the values are plausible + // we could also compare the length with the time elapsed since starting the recording + private boolean deleteIfTooShort(Download download) { + long minimumLengthInSeconds = Config.getInstance().getSettings().minimumLengthInSeconds; + if(minimumLengthInSeconds <= 0) { + return false; + } + + try { + LOG.debug("Determining video length for {}", download.getTarget()); + File target = download.getTarget(); + double duration = 0; + if(target.isDirectory()) { + File playlist = new File(target, "playlist.m3u8"); + duration = getPlaylistLength(playlist); + } else { + duration = MpegUtil.getFileDuration(target); + } + Duration minLength = Duration.ofSeconds(minimumLengthInSeconds); + Duration videoLength = Duration.ofSeconds((long) duration); + LOG.debug("Recording started at:{}. Video length is {}", download.getStartTime(), videoLength); + if(videoLength.minus(minLength).isNegative()) { + LOG.debug("Video too short {} {}", videoLength, download.getTarget()); + LOG.debug("Deleting {}", target); + if(target.isDirectory()) { + deleteDirectory(target); + deleteEmptyParents(target); + } else { + Files.delete(target.toPath()); + deleteEmptyParents(target.getParentFile()); + } + return true; + } else { + return false; + } + } catch (Exception e) { + LOG.error("Couldn't check video length", e); + return false; + } + } + + private double getPlaylistLength(File playlist) throws IOException, ParseException, PlaylistException { + if(playlist.exists()) { + PlaylistParser playlistParser = new PlaylistParser(new FileInputStream(playlist), Format.EXT_M3U, Encoding.UTF_8, ParsingMode.LENIENT); + Playlist m3u = playlistParser.parse(); + MediaPlaylist mediaPlaylist = m3u.getMediaPlaylist(); + double length = 0; + for (TrackData trackData : mediaPlaylist.getTracks()) { + length += trackData.getTrackInfo().duration; + } + return length; + } else { + throw new FileNotFoundException(playlist.getAbsolutePath() + " does not exist"); + } + } } diff --git a/common/src/main/java/ctbrec/recorder/PlaylistGenerator.java b/common/src/main/java/ctbrec/recorder/PlaylistGenerator.java index 2fec113c..a4180765 100644 --- a/common/src/main/java/ctbrec/recorder/PlaylistGenerator.java +++ b/common/src/main/java/ctbrec/recorder/PlaylistGenerator.java @@ -6,24 +6,12 @@ import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.FilenameFilter; import java.io.IOException; -import java.nio.channels.ReadableByteChannel; import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; -import org.jcodec.common.Demuxer; -import org.jcodec.common.DemuxerTrack; -import org.jcodec.common.TrackType; -import org.jcodec.common.Tuple; -import org.jcodec.common.Tuple._2; -import org.jcodec.common.io.FileChannelWrapper; -import org.jcodec.common.io.NIOUtils; -import org.jcodec.common.model.Packet; -import org.jcodec.containers.mps.MPSDemuxer; -import org.jcodec.containers.mps.MTSDemuxer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -40,6 +28,8 @@ import com.iheartradio.m3u8.data.PlaylistType; import com.iheartradio.m3u8.data.TrackData; import com.iheartradio.m3u8.data.TrackInfo; +import ctbrec.MpegUtil; + public class PlaylistGenerator { private static final transient Logger LOG = LoggerFactory.getLogger(PlaylistGenerator.class); @@ -72,7 +62,7 @@ public class PlaylistGenerator { try { track.add(new TrackData.Builder() .withUri(file.getName()) - .withTrackInfo(new TrackInfo((float) getFileDuration(file), file.getName())) + .withTrackInfo(new TrackInfo((float) MpegUtil.getFileDuration(file), file.getName())) .build()); } catch(Exception e) { LOG.warn("Couldn't determine duration for {}. Skipping this file.", file.getName()); @@ -141,45 +131,6 @@ public class PlaylistGenerator { return targetDuration; } - private double getFileDuration(File file) throws IOException { - try(FileChannelWrapper ch = NIOUtils.readableChannel(file)) { - _2 m2tsDemuxer = createM2TSDemuxer(ch, TrackType.VIDEO); - Demuxer demuxer = m2tsDemuxer.v1; - DemuxerTrack videoDemux = demuxer.getTracks().get(0); - Packet videoFrame = null; - double totalDuration = 0; - while( (videoFrame = videoDemux.nextFrame()) != null) { - totalDuration += videoFrame.getDurationD(); - } - return totalDuration; - } - } - - public static _2 createM2TSDemuxer(FileChannelWrapper ch, TrackType targetTrack) throws IOException { - MTSDemuxer mts = new MTSDemuxer(ch); - Set programs = mts.getPrograms(); - if (programs.size() == 0) { - LOG.error("The MPEG TS stream contains no programs"); - return null; - } - Tuple._2 found = null; - for (Integer pid : programs) { - ReadableByteChannel program = mts.getProgram(pid); - if (found != null) { - program.close(); - continue; - } - MPSDemuxer demuxer = new MPSDemuxer(program); - if (targetTrack == TrackType.AUDIO && demuxer.getAudioTracks().size() > 0 - || targetTrack == TrackType.VIDEO && demuxer.getVideoTracks().size() > 0) { - found = org.jcodec.common.Tuple._2(pid, (Demuxer) demuxer); - } else { - program.close(); - } - } - return found; - } - public void addProgressListener(ProgressListener l) { listeners.add(l); }