diff --git a/src/main/java/ctbrec/Settings.java b/src/main/java/ctbrec/Settings.java index a92327a4..2d5f7866 100644 --- a/src/main/java/ctbrec/Settings.java +++ b/src/main/java/ctbrec/Settings.java @@ -21,6 +21,7 @@ public class Settings { public String httpServer = "localhost"; public String recordingsDir = System.getProperty("user.home") + File.separator + "ctbrec"; public String mediaPlayer = "/usr/bin/mpv"; + public String postProcessing = ""; public String username = ""; // chaturbate username TODO maybe rename this onetime public String password = ""; // chaturbate password TODO maybe rename this onetime public String bongaUsername = ""; diff --git a/src/main/java/ctbrec/recorder/LocalRecorder.java b/src/main/java/ctbrec/recorder/LocalRecorder.java index 15c63e69..a35f2f56 100644 --- a/src/main/java/ctbrec/recorder/LocalRecorder.java +++ b/src/main/java/ctbrec/recorder/LocalRecorder.java @@ -10,6 +10,7 @@ import java.security.NoSuchAlgorithmException; import java.text.SimpleDateFormat; import java.time.Instant; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.Date; import java.util.HashMap; @@ -28,7 +29,9 @@ import com.iheartradio.m3u8.PlaylistException; import ctbrec.Config; import ctbrec.Model; +import ctbrec.OS; import ctbrec.Recording; +import ctbrec.io.StreamRedirectThread; import ctbrec.recorder.PlaylistGenerator.InvalidPlaylistException; import ctbrec.recorder.download.Download; import ctbrec.recorder.download.HlsDownload; @@ -46,7 +49,7 @@ public class LocalRecorder implements Recorder { private Config config; private ProcessMonitor processMonitor; private OnlineMonitor onlineMonitor; - private PlaylistGeneratorTrigger playlistGenTrigger; + private PostProcessingTrigger postProcessingTrigger; private volatile boolean recording = true; private List deleteInProgress = Collections.synchronizedList(new ArrayList<>()); private RecorderHttpClient client = new RecorderHttpClient(); @@ -68,9 +71,9 @@ public class LocalRecorder implements Recorder { onlineMonitor = new OnlineMonitor(); onlineMonitor.start(); - playlistGenTrigger = new PlaylistGeneratorTrigger(); + postProcessingTrigger = new PostProcessingTrigger(); if(Config.getInstance().isServerMode()) { - playlistGenTrigger.start(); + postProcessingTrigger.start(); } LOG.debug("Recorder initialized"); @@ -153,10 +156,51 @@ public class LocalRecorder implements Recorder { }.start(); } - private void stopRecordingProcess(Model model) throws IOException { + private void stopRecordingProcess(Model model) { Download download = recordingProcesses.get(model); download.stop(); recordingProcesses.remove(model); + if(!Config.getInstance().isServerMode()) { + postprocess(download); + } + } + + private void postprocess(Download download) { + if(!(download instanceof MergedHlsDownload)) { + throw new IllegalArgumentException("Download should be of type MergedHlsDownload"); + } + String postProcessing = Config.getInstance().getSettings().postProcessing; + if (postProcessing != null && !postProcessing.isEmpty()) { + new Thread(() -> { + Runtime rt = Runtime.getRuntime(); + try { + MergedHlsDownload d = (MergedHlsDownload) download; + String[] args = new String[] { + postProcessing, + d.getDirectory().getAbsolutePath(), + d.getTargetFile().getAbsolutePath(), + d.getModel().getName(), + d.getModel().getSite().getName(), + Long.toString(download.getStartTime().getEpochSecond()) + }; + LOG.debug("Running {}", Arrays.toString(args)); + Process process = rt.exec(args, OS.getEnvironment(), download.getDirectory()); + Thread std = new Thread(new StreamRedirectThread(process.getInputStream(), System.out)); + std.setName("Process stdout pipe"); + std.setDaemon(true); + std.start(); + Thread err = new Thread(new StreamRedirectThread(process.getErrorStream(), System.err)); + err.setName("Process stderr pipe"); + err.setDaemon(true); + err.start(); + + process.waitFor(); + LOG.debug("Process finished."); + } catch (Exception e) { + LOG.error("Error in process thread", e); + } + }).start(); + } } @Override @@ -202,7 +246,7 @@ public class LocalRecorder implements Recorder { LOG.debug("Stopping monitor threads"); onlineMonitor.running = false; processMonitor.running = false; - playlistGenTrigger.running = false; + postProcessingTrigger.running = false; LOG.debug("Stopping all recording processes"); stopRecordingProcesses(); client.shutdown(); @@ -267,10 +311,14 @@ public class LocalRecorder implements Recorder { LOG.debug("Recording terminated for model {}", m.getName()); iterator.remove(); restart.add(m); - try { - finishRecording(d.getDirectory()); - } catch(Exception e) { - LOG.error("Error while finishing recording for model {}", m.getName(), e); + if(config.isServerMode()) { + try { + finishRecording(d.getDirectory()); + } catch(Exception e) { + LOG.error("Error while finishing recording for model {}", m.getName(), e); + } + } else { + postprocess(d); } } } @@ -290,17 +338,17 @@ public class LocalRecorder implements Recorder { } private void finishRecording(File directory) { - Thread t = new Thread() { - @Override - public void run() { - if(Config.getInstance().isServerMode()) { + if(Config.getInstance().isServerMode()) { + Thread t = new Thread() { + @Override + public void run() { generatePlaylist(directory); } - } - }; - t.setDaemon(true); - t.setName("Postprocessing" + directory.toString()); - t.start(); + }; + t.setDaemon(true); + t.setName("Post-Processing " + directory.toString()); + t.start(); + } } private void generatePlaylist(File recDir) { @@ -360,11 +408,11 @@ public class LocalRecorder implements Recorder { } } - private class PlaylistGeneratorTrigger extends Thread { + private class PostProcessingTrigger extends Thread { private volatile boolean running = false; - public PlaylistGeneratorTrigger() { - setName("PlaylistGeneratorTrigger"); + public PostProcessingTrigger() { + setName("PostProcessingTrigger"); setDaemon(true); } @@ -386,7 +434,7 @@ public class LocalRecorder implements Recorder { } if (!recordingProcessFound) { if (deleteInProgress.contains(recDir)) { - LOG.debug("{} is being deleted. Not going to generate a playlist", recDir); + LOG.debug("{} is being deleted. Not going to start post-processing", recDir); } else { finishRecording(recDir); } @@ -569,8 +617,7 @@ public class LocalRecorder implements Recorder { Download download = recordingProcesses.get(model); if(download != null) { - download.stop(); - recordingProcesses.remove(model); + stopRecordingProcess(model); } } diff --git a/src/main/java/ctbrec/recorder/download/AbstractHlsDownload.java b/src/main/java/ctbrec/recorder/download/AbstractHlsDownload.java index 84a3744d..62a8f897 100644 --- a/src/main/java/ctbrec/recorder/download/AbstractHlsDownload.java +++ b/src/main/java/ctbrec/recorder/download/AbstractHlsDownload.java @@ -5,6 +5,7 @@ import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.nio.file.Path; +import java.time.Instant; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; @@ -41,6 +42,8 @@ public abstract class AbstractHlsDownload implements Download { volatile boolean running = false; volatile boolean alive = true; Path downloadDir; + Instant startTime; + Model model; public AbstractHlsDownload(HttpClient client) { this.client = client; @@ -117,6 +120,16 @@ public abstract class AbstractHlsDownload implements Download { return downloadDir.toFile(); } + @Override + public Instant getStartTime() { + return startTime; + } + + @Override + public Model getModel() { + return model; + } + public static class SegmentPlaylist { public int seq = 0; public float totalDuration = 0; diff --git a/src/main/java/ctbrec/recorder/download/Download.java b/src/main/java/ctbrec/recorder/download/Download.java index 4148a362..76a71f0e 100644 --- a/src/main/java/ctbrec/recorder/download/Download.java +++ b/src/main/java/ctbrec/recorder/download/Download.java @@ -2,6 +2,7 @@ package ctbrec.recorder.download; import java.io.File; import java.io.IOException; +import java.time.Instant; import ctbrec.Config; import ctbrec.Model; @@ -11,4 +12,6 @@ public interface Download { public void stop(); public boolean isAlive(); public File getDirectory(); + public Model getModel(); + public Instant getStartTime(); } diff --git a/src/main/java/ctbrec/recorder/download/HlsDownload.java b/src/main/java/ctbrec/recorder/download/HlsDownload.java index 6974056a..ab1804b6 100644 --- a/src/main/java/ctbrec/recorder/download/HlsDownload.java +++ b/src/main/java/ctbrec/recorder/download/HlsDownload.java @@ -12,6 +12,7 @@ import java.nio.file.Files; import java.nio.file.LinkOption; import java.nio.file.Path; import java.text.SimpleDateFormat; +import java.time.Instant; import java.util.Date; import java.util.concurrent.Callable; @@ -39,6 +40,8 @@ public class HlsDownload extends AbstractHlsDownload { public void start(Model model, Config config) throws IOException { try { running = true; + startTime = Instant.now(); + super.model = model; SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd_HH-mm"); String startTime = sdf.format(new Date()); Path modelDir = FileSystems.getDefault().getPath(config.getSettings().recordingsDir, model.getName()); diff --git a/src/main/java/ctbrec/recorder/download/MergedHlsDownload.java b/src/main/java/ctbrec/recorder/download/MergedHlsDownload.java index 51e7b422..59668dbd 100644 --- a/src/main/java/ctbrec/recorder/download/MergedHlsDownload.java +++ b/src/main/java/ctbrec/recorder/download/MergedHlsDownload.java @@ -16,6 +16,7 @@ import java.nio.file.Path; import java.text.DecimalFormat; import java.text.SimpleDateFormat; import java.time.Duration; +import java.time.Instant; import java.time.ZonedDateTime; import java.util.Date; import java.util.LinkedList; @@ -58,9 +59,14 @@ public class MergedHlsDownload extends AbstractHlsDownload { super(client); } + public File getTargetFile() { + return targetFile; + } + public void start(String segmentPlaylistUri, File targetFile, ProgressListener progressListener) throws IOException { try { running = true; + super.startTime = Instant.now(); downloadDir = targetFile.getParentFile().toPath(); mergeThread = createMergeThread(targetFile, progressListener, false); mergeThread.start(); @@ -75,7 +81,7 @@ public class MergedHlsDownload extends AbstractHlsDownload { } finally { alive = false; streamer.stop(); - LOG.debug("Download for terminated"); + LOG.debug("Download terminated for {}", segmentPlaylistUri); } } @@ -84,6 +90,8 @@ public class MergedHlsDownload extends AbstractHlsDownload { this.config = config; try { running = true; + super.startTime = Instant.now(); + super.model = model; startTime = ZonedDateTime.now(); SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd_HH-mm"); String startTime = sdf.format(new Date()); diff --git a/src/main/java/ctbrec/ui/SettingsTab.java b/src/main/java/ctbrec/ui/SettingsTab.java index 2c9bb604..9cc910d7 100644 --- a/src/main/java/ctbrec/ui/SettingsTab.java +++ b/src/main/java/ctbrec/ui/SettingsTab.java @@ -56,7 +56,9 @@ public class SettingsTab extends Tab implements TabSelectionListener { public static final int CHECKBOX_MARGIN = 6; private TextField recordingsDirectory; private Button recordingsDirectoryButton; + private Button postProcessingDirectoryButton; private TextField mediaPlayer; + private TextField postProcessing; private TextField server; private TextField port; private CheckBox loadResolution; @@ -267,6 +269,17 @@ public class SettingsTab extends Tab implements TabSelectionListener { layout.add(mediaPlayer, 1, 1); layout.add(createMpvBrowseButton(), 3, 1); + layout.add(new Label("Post-Processing"), 0, 2); + postProcessing = new TextField(Config.getInstance().getSettings().postProcessing); + postProcessing.focusedProperty().addListener(createPostProcessingFocusListener()); + GridPane.setFillWidth(postProcessing, true); + GridPane.setHgrow(postProcessing, Priority.ALWAYS); + GridPane.setColumnSpan(postProcessing, 2); + GridPane.setMargin(postProcessing, new Insets(0, 0, 0, CHECKBOX_MARGIN)); + layout.add(postProcessing, 1, 2); + postProcessingDirectoryButton = createPostProcessingBrowseButton(); + layout.add(postProcessingDirectoryButton, 3, 2); + TitledPane locations = new TitledPane("Locations", layout); locations.setCollapsible(false); return locations; @@ -380,6 +393,8 @@ public class SettingsTab extends Tab implements TabSelectionListener { recordingsDirectoryButton.setDisable(!local); splitAfter.setDisable(!local); maxResolution.setDisable(!local); + postProcessing.setDisable(!local); + postProcessingDirectoryButton.setDisable(!local); } private ChangeListener createRecordingsDirectoryFocusListener() { @@ -414,6 +429,22 @@ public class SettingsTab extends Tab implements TabSelectionListener { }; } + private ChangeListener createPostProcessingFocusListener() { + return new ChangeListener() { + @Override + public void changed(ObservableValue arg0, Boolean oldPropertyValue, Boolean newPropertyValue) { + if (newPropertyValue) { + postProcessing.setBorder(Border.EMPTY); + postProcessing.setTooltip(null); + } else { + String input = postProcessing.getText(); + File program = new File(input); + setPostProcessing(program); + } + } + }; + } + private void setMpv(File program) { String msg = validateProgram(program); if (msg != null) { @@ -424,6 +455,16 @@ public class SettingsTab extends Tab implements TabSelectionListener { } } + private void setPostProcessing(File program) { + String msg = validateProgram(program); + if (msg != null) { + postProcessing.setBorder(new Border(new BorderStroke(Color.RED, BorderStrokeStyle.DASHED, new CornerRadii(2), new BorderWidths(2)))); + postProcessing.setTooltip(new Tooltip(msg)); + } else { + Config.getInstance().getSettings().postProcessing = postProcessing.getText(); + } + } + private String validateProgram(File program) { if (program == null || !program.exists()) { return "File does not exist"; @@ -470,6 +511,27 @@ public class SettingsTab extends Tab implements TabSelectionListener { return button; } + private Button createPostProcessingBrowseButton() { + Button button = new Button("Select"); + button.setOnAction((e) -> { + FileChooser chooser = new FileChooser(); + File program = chooser.showOpenDialog(null); + if(program != null) { + try { + postProcessing.setText(program.getCanonicalPath()); + } catch (IOException e1) { + LOG.error("Couldn't determine path", e1); + Alert alert = new AutosizeAlert(Alert.AlertType.ERROR); + alert.setTitle("Whoopsie"); + alert.setContentText("Couldn't determine path"); + alert.showAndWait(); + } + setPostProcessing(program); + } + }); + return button; + } + private void setRecordingsDir(File dir) { if (dir != null && dir.isDirectory()) { try { diff --git a/src/main/resources/pp_example.sh b/src/main/resources/pp_example.sh new file mode 100755 index 00000000..b4e829e4 --- /dev/null +++ b/src/main/resources/pp_example.sh @@ -0,0 +1,28 @@ +#!/bin/bash + +# $1 directory (absolute path) +# $2 file (absolute path) +# $3 model name +# $4 site name +# $5 unixtime + +# get the filename without path +FILE=`basename $2` + +# format unixtime to human readable +TIME=$(date --date="@$5" +%d.%m.%Y_%H:%M) + +# define filename of end result +MP4=$(echo "$1/$4_$3_$TIME.mp4") + +# remux ts to mp4 +ffmpeg -i $2 -c:v copy -c:a copy -f mp4 $MP4 + +# move mp4 to target directory +mv $MP4 /tmp + +# delete the original .ts file +rm $2 + +# delete the directory of the recording +rm -r $1