Add setting to run post-processing script

The post-processing script is executed, after a local recording
is finished. The script is executed in the directory of the recording
with the following parameters in given order: directory (absolute path),
file (absolute path), model name, site name, unixtime
This commit is contained in:
0xboobface 2018-11-07 23:05:06 +01:00
parent 77a1b4f3ac
commit 8ee3d8b588
8 changed files with 190 additions and 25 deletions

View File

@ -21,6 +21,7 @@ public class Settings {
public String httpServer = "localhost"; public String httpServer = "localhost";
public String recordingsDir = System.getProperty("user.home") + File.separator + "ctbrec"; public String recordingsDir = System.getProperty("user.home") + File.separator + "ctbrec";
public String mediaPlayer = "/usr/bin/mpv"; public String mediaPlayer = "/usr/bin/mpv";
public String postProcessing = "";
public String username = ""; // chaturbate username TODO maybe rename this onetime public String username = ""; // chaturbate username TODO maybe rename this onetime
public String password = ""; // chaturbate password TODO maybe rename this onetime public String password = ""; // chaturbate password TODO maybe rename this onetime
public String bongaUsername = ""; public String bongaUsername = "";

View File

@ -10,6 +10,7 @@ import java.security.NoSuchAlgorithmException;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.time.Instant; import java.time.Instant;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.Date; import java.util.Date;
import java.util.HashMap; import java.util.HashMap;
@ -28,7 +29,9 @@ import com.iheartradio.m3u8.PlaylistException;
import ctbrec.Config; import ctbrec.Config;
import ctbrec.Model; import ctbrec.Model;
import ctbrec.OS;
import ctbrec.Recording; import ctbrec.Recording;
import ctbrec.io.StreamRedirectThread;
import ctbrec.recorder.PlaylistGenerator.InvalidPlaylistException; import ctbrec.recorder.PlaylistGenerator.InvalidPlaylistException;
import ctbrec.recorder.download.Download; import ctbrec.recorder.download.Download;
import ctbrec.recorder.download.HlsDownload; import ctbrec.recorder.download.HlsDownload;
@ -46,7 +49,7 @@ public class LocalRecorder implements Recorder {
private Config config; private Config config;
private ProcessMonitor processMonitor; private ProcessMonitor processMonitor;
private OnlineMonitor onlineMonitor; private OnlineMonitor onlineMonitor;
private PlaylistGeneratorTrigger playlistGenTrigger; private PostProcessingTrigger postProcessingTrigger;
private volatile boolean recording = true; private volatile boolean recording = true;
private List<File> deleteInProgress = Collections.synchronizedList(new ArrayList<>()); private List<File> deleteInProgress = Collections.synchronizedList(new ArrayList<>());
private RecorderHttpClient client = new RecorderHttpClient(); private RecorderHttpClient client = new RecorderHttpClient();
@ -68,9 +71,9 @@ public class LocalRecorder implements Recorder {
onlineMonitor = new OnlineMonitor(); onlineMonitor = new OnlineMonitor();
onlineMonitor.start(); onlineMonitor.start();
playlistGenTrigger = new PlaylistGeneratorTrigger(); postProcessingTrigger = new PostProcessingTrigger();
if(Config.getInstance().isServerMode()) { if(Config.getInstance().isServerMode()) {
playlistGenTrigger.start(); postProcessingTrigger.start();
} }
LOG.debug("Recorder initialized"); LOG.debug("Recorder initialized");
@ -153,10 +156,51 @@ public class LocalRecorder implements Recorder {
}.start(); }.start();
} }
private void stopRecordingProcess(Model model) throws IOException { private void stopRecordingProcess(Model model) {
Download download = recordingProcesses.get(model); Download download = recordingProcesses.get(model);
download.stop(); download.stop();
recordingProcesses.remove(model); 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 @Override
@ -202,7 +246,7 @@ public class LocalRecorder implements Recorder {
LOG.debug("Stopping monitor threads"); LOG.debug("Stopping monitor threads");
onlineMonitor.running = false; onlineMonitor.running = false;
processMonitor.running = false; processMonitor.running = false;
playlistGenTrigger.running = false; postProcessingTrigger.running = false;
LOG.debug("Stopping all recording processes"); LOG.debug("Stopping all recording processes");
stopRecordingProcesses(); stopRecordingProcesses();
client.shutdown(); client.shutdown();
@ -267,10 +311,14 @@ public class LocalRecorder implements Recorder {
LOG.debug("Recording terminated for model {}", m.getName()); LOG.debug("Recording terminated for model {}", m.getName());
iterator.remove(); iterator.remove();
restart.add(m); restart.add(m);
try { if(config.isServerMode()) {
finishRecording(d.getDirectory()); try {
} catch(Exception e) { finishRecording(d.getDirectory());
LOG.error("Error while finishing recording for model {}", m.getName(), e); } 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) { private void finishRecording(File directory) {
Thread t = new Thread() { if(Config.getInstance().isServerMode()) {
@Override Thread t = new Thread() {
public void run() { @Override
if(Config.getInstance().isServerMode()) { public void run() {
generatePlaylist(directory); generatePlaylist(directory);
} }
} };
}; t.setDaemon(true);
t.setDaemon(true); t.setName("Post-Processing " + directory.toString());
t.setName("Postprocessing" + directory.toString()); t.start();
t.start(); }
} }
private void generatePlaylist(File recDir) { 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; private volatile boolean running = false;
public PlaylistGeneratorTrigger() { public PostProcessingTrigger() {
setName("PlaylistGeneratorTrigger"); setName("PostProcessingTrigger");
setDaemon(true); setDaemon(true);
} }
@ -386,7 +434,7 @@ public class LocalRecorder implements Recorder {
} }
if (!recordingProcessFound) { if (!recordingProcessFound) {
if (deleteInProgress.contains(recDir)) { 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 { } else {
finishRecording(recDir); finishRecording(recDir);
} }
@ -569,8 +617,7 @@ public class LocalRecorder implements Recorder {
Download download = recordingProcesses.get(model); Download download = recordingProcesses.get(model);
if(download != null) { if(download != null) {
download.stop(); stopRecordingProcess(model);
recordingProcesses.remove(model);
} }
} }

View File

@ -5,6 +5,7 @@ import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.net.URL; import java.net.URL;
import java.nio.file.Path; import java.nio.file.Path;
import java.time.Instant;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.Iterator; import java.util.Iterator;
@ -41,6 +42,8 @@ public abstract class AbstractHlsDownload implements Download {
volatile boolean running = false; volatile boolean running = false;
volatile boolean alive = true; volatile boolean alive = true;
Path downloadDir; Path downloadDir;
Instant startTime;
Model model;
public AbstractHlsDownload(HttpClient client) { public AbstractHlsDownload(HttpClient client) {
this.client = client; this.client = client;
@ -117,6 +120,16 @@ public abstract class AbstractHlsDownload implements Download {
return downloadDir.toFile(); return downloadDir.toFile();
} }
@Override
public Instant getStartTime() {
return startTime;
}
@Override
public Model getModel() {
return model;
}
public static class SegmentPlaylist { public static class SegmentPlaylist {
public int seq = 0; public int seq = 0;
public float totalDuration = 0; public float totalDuration = 0;

View File

@ -2,6 +2,7 @@ package ctbrec.recorder.download;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.time.Instant;
import ctbrec.Config; import ctbrec.Config;
import ctbrec.Model; import ctbrec.Model;
@ -11,4 +12,6 @@ public interface Download {
public void stop(); public void stop();
public boolean isAlive(); public boolean isAlive();
public File getDirectory(); public File getDirectory();
public Model getModel();
public Instant getStartTime();
} }

View File

@ -12,6 +12,7 @@ import java.nio.file.Files;
import java.nio.file.LinkOption; import java.nio.file.LinkOption;
import java.nio.file.Path; import java.nio.file.Path;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.time.Instant;
import java.util.Date; import java.util.Date;
import java.util.concurrent.Callable; import java.util.concurrent.Callable;
@ -39,6 +40,8 @@ public class HlsDownload extends AbstractHlsDownload {
public void start(Model model, Config config) throws IOException { public void start(Model model, Config config) throws IOException {
try { try {
running = true; running = true;
startTime = Instant.now();
super.model = model;
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd_HH-mm"); SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd_HH-mm");
String startTime = sdf.format(new Date()); String startTime = sdf.format(new Date());
Path modelDir = FileSystems.getDefault().getPath(config.getSettings().recordingsDir, model.getName()); Path modelDir = FileSystems.getDefault().getPath(config.getSettings().recordingsDir, model.getName());

View File

@ -16,6 +16,7 @@ import java.nio.file.Path;
import java.text.DecimalFormat; import java.text.DecimalFormat;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.time.Duration; import java.time.Duration;
import java.time.Instant;
import java.time.ZonedDateTime; import java.time.ZonedDateTime;
import java.util.Date; import java.util.Date;
import java.util.LinkedList; import java.util.LinkedList;
@ -58,9 +59,14 @@ public class MergedHlsDownload extends AbstractHlsDownload {
super(client); super(client);
} }
public File getTargetFile() {
return targetFile;
}
public void start(String segmentPlaylistUri, File targetFile, ProgressListener progressListener) throws IOException { public void start(String segmentPlaylistUri, File targetFile, ProgressListener progressListener) throws IOException {
try { try {
running = true; running = true;
super.startTime = Instant.now();
downloadDir = targetFile.getParentFile().toPath(); downloadDir = targetFile.getParentFile().toPath();
mergeThread = createMergeThread(targetFile, progressListener, false); mergeThread = createMergeThread(targetFile, progressListener, false);
mergeThread.start(); mergeThread.start();
@ -75,7 +81,7 @@ public class MergedHlsDownload extends AbstractHlsDownload {
} finally { } finally {
alive = false; alive = false;
streamer.stop(); 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; this.config = config;
try { try {
running = true; running = true;
super.startTime = Instant.now();
super.model = model;
startTime = ZonedDateTime.now(); startTime = ZonedDateTime.now();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd_HH-mm"); SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd_HH-mm");
String startTime = sdf.format(new Date()); String startTime = sdf.format(new Date());

View File

@ -56,7 +56,9 @@ public class SettingsTab extends Tab implements TabSelectionListener {
public static final int CHECKBOX_MARGIN = 6; public static final int CHECKBOX_MARGIN = 6;
private TextField recordingsDirectory; private TextField recordingsDirectory;
private Button recordingsDirectoryButton; private Button recordingsDirectoryButton;
private Button postProcessingDirectoryButton;
private TextField mediaPlayer; private TextField mediaPlayer;
private TextField postProcessing;
private TextField server; private TextField server;
private TextField port; private TextField port;
private CheckBox loadResolution; private CheckBox loadResolution;
@ -267,6 +269,17 @@ public class SettingsTab extends Tab implements TabSelectionListener {
layout.add(mediaPlayer, 1, 1); layout.add(mediaPlayer, 1, 1);
layout.add(createMpvBrowseButton(), 3, 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); TitledPane locations = new TitledPane("Locations", layout);
locations.setCollapsible(false); locations.setCollapsible(false);
return locations; return locations;
@ -380,6 +393,8 @@ public class SettingsTab extends Tab implements TabSelectionListener {
recordingsDirectoryButton.setDisable(!local); recordingsDirectoryButton.setDisable(!local);
splitAfter.setDisable(!local); splitAfter.setDisable(!local);
maxResolution.setDisable(!local); maxResolution.setDisable(!local);
postProcessing.setDisable(!local);
postProcessingDirectoryButton.setDisable(!local);
} }
private ChangeListener<? super Boolean> createRecordingsDirectoryFocusListener() { private ChangeListener<? super Boolean> createRecordingsDirectoryFocusListener() {
@ -414,6 +429,22 @@ public class SettingsTab extends Tab implements TabSelectionListener {
}; };
} }
private ChangeListener<? super Boolean> createPostProcessingFocusListener() {
return new ChangeListener<Boolean>() {
@Override
public void changed(ObservableValue<? extends Boolean> 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) { private void setMpv(File program) {
String msg = validateProgram(program); String msg = validateProgram(program);
if (msg != null) { 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) { private String validateProgram(File program) {
if (program == null || !program.exists()) { if (program == null || !program.exists()) {
return "File does not exist"; return "File does not exist";
@ -470,6 +511,27 @@ public class SettingsTab extends Tab implements TabSelectionListener {
return button; 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) { private void setRecordingsDir(File dir) {
if (dir != null && dir.isDirectory()) { if (dir != null && dir.isDirectory()) {
try { try {

View File

@ -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