Add setting for minimum recording length
If a recording is shorter than x seconds, it gets deleted
This commit is contained in:
parent
560e73c1dd
commit
ceb7c07aa8
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
|
@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue