Add setting for minimum recording length

If a recording is shorter than x seconds, it gets deleted
This commit is contained in:
0xboobface 2018-12-13 20:54:10 +01:00
parent 560e73c1dd
commit ceb7c07aa8
5 changed files with 171 additions and 52 deletions

View File

@ -58,6 +58,7 @@ public class SettingsTab extends Tab implements TabSelectionListener {
private TextField port; private TextField port;
private TextField onlineCheckIntervalInSecs; private TextField onlineCheckIntervalInSecs;
private TextField leaveSpaceOnDevice; private TextField leaveSpaceOnDevice;
private TextField minimumLengthInSecs;
private CheckBox loadResolution; private CheckBox loadResolution;
private CheckBox secureCommunication = new CheckBox(); private CheckBox secureCommunication = new CheckBox();
private CheckBox chooseStreamQuality = 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)); GridPane.setMargin(leaveSpaceOnDevice, new Insets(0, 0, 0, CHECKBOX_MARGIN));
layout.add(leaveSpaceOnDevice, 1, row++); 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); TitledPane locations = new TitledPane("Recorder", layout);
locations.setCollapsible(false); locations.setCollapsible(false);
return locations; return locations;

View File

@ -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<Integer,Demuxer> 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<Integer, Demuxer> createM2TSDemuxer(FileChannelWrapper ch, TrackType targetTrack) throws IOException {
MTSDemuxer mts = new MTSDemuxer(ch);
Set<Integer> programs = mts.getPrograms();
if (programs.size() == 0) {
LOG.error("The MPEG TS stream contains no programs");
return null;
}
Tuple._2<Integer, Demuxer> 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;
}
}

View File

@ -41,6 +41,7 @@ public class Settings {
public String recordingsDir = System.getProperty("user.home") + File.separator + "ctbrec"; public String recordingsDir = System.getProperty("user.home") + File.separator + "ctbrec";
public DirectoryStructure recordingsDirStructure = DirectoryStructure.FLAT; public DirectoryStructure recordingsDirStructure = DirectoryStructure.FLAT;
public long minimumSpaceLeftInBytes = 0; public long minimumSpaceLeftInBytes = 0;
public int minimumLengthInSeconds = 0;
public String mediaPlayer = "/usr/bin/mpv"; public String mediaPlayer = "/usr/bin/mpv";
public String postProcessing = ""; public String postProcessing = "";
public String username = ""; // chaturbate username TODO maybe rename this onetime public String username = ""; // chaturbate username TODO maybe rename this onetime

View File

@ -4,6 +4,8 @@ import static ctbrec.Recording.State.*;
import static ctbrec.event.Event.Type.*; import static ctbrec.event.Event.Type.*;
import java.io.File; import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FilenameFilter; import java.io.FilenameFilter;
import java.io.IOException; import java.io.IOException;
import java.nio.file.FileStore; import java.nio.file.FileStore;
@ -11,6 +13,7 @@ import java.nio.file.Files;
import java.security.InvalidKeyException; import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.time.Duration;
import java.time.Instant; import java.time.Instant;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
@ -34,11 +37,19 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import com.google.common.eventbus.Subscribe; 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.ParseException;
import com.iheartradio.m3u8.ParsingMode;
import com.iheartradio.m3u8.PlaylistException; 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.Config;
import ctbrec.Model; import ctbrec.Model;
import ctbrec.MpegUtil;
import ctbrec.OS; import ctbrec.OS;
import ctbrec.Recording; import ctbrec.Recording;
import ctbrec.Recording.State; import ctbrec.Recording.State;
@ -740,9 +751,71 @@ public class LocalRecorder implements Recorder {
fireRecordingStateChanged(download.getTarget(), GENERATING_PLAYLIST, download.getModel(), download.getStartTime()); fireRecordingStateChanged(download.getTarget(), GENERATING_PLAYLIST, download.getModel(), download.getStartTime());
generatePlaylist(download.getTarget()); 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()); fireRecordingStateChanged(download.getTarget(), POST_PROCESSING, download.getModel(), download.getStartTime());
postprocess(download); postprocess(download);
fireRecordingStateChanged(download.getTarget(), FINISHED, download.getModel(), download.getStartTime()); 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");
}
}
} }

View File

@ -6,24 +6,12 @@ import java.io.FileNotFoundException;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.FilenameFilter; import java.io.FilenameFilter;
import java.io.IOException; import java.io.IOException;
import java.nio.channels.ReadableByteChannel;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Set;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; 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.Logger;
import org.slf4j.LoggerFactory; 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.TrackData;
import com.iheartradio.m3u8.data.TrackInfo; import com.iheartradio.m3u8.data.TrackInfo;
import ctbrec.MpegUtil;
public class PlaylistGenerator { public class PlaylistGenerator {
private static final transient Logger LOG = LoggerFactory.getLogger(PlaylistGenerator.class); private static final transient Logger LOG = LoggerFactory.getLogger(PlaylistGenerator.class);
@ -72,7 +62,7 @@ public class PlaylistGenerator {
try { try {
track.add(new TrackData.Builder() track.add(new TrackData.Builder()
.withUri(file.getName()) .withUri(file.getName())
.withTrackInfo(new TrackInfo((float) getFileDuration(file), file.getName())) .withTrackInfo(new TrackInfo((float) MpegUtil.getFileDuration(file), file.getName()))
.build()); .build());
} catch(Exception e) { } catch(Exception e) {
LOG.warn("Couldn't determine duration for {}. Skipping this file.", file.getName()); LOG.warn("Couldn't determine duration for {}. Skipping this file.", file.getName());
@ -141,45 +131,6 @@ public class PlaylistGenerator {
return targetDuration; return targetDuration;
} }
private double getFileDuration(File file) throws IOException {
try(FileChannelWrapper ch = NIOUtils.readableChannel(file)) {
_2<Integer,Demuxer> 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<Integer, Demuxer> createM2TSDemuxer(FileChannelWrapper ch, TrackType targetTrack) throws IOException {
MTSDemuxer mts = new MTSDemuxer(ch);
Set<Integer> programs = mts.getPrograms();
if (programs.size() == 0) {
LOG.error("The MPEG TS stream contains no programs");
return null;
}
Tuple._2<Integer, Demuxer> 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) { public void addProgressListener(ProgressListener l) {
listeners.add(l); listeners.add(l);
} }