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 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 = "";

View File

@ -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<File> 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);
}
}

View File

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

View File

@ -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();
}

View File

@ -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());

View File

@ -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());

View File

@ -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<? 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) {
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 {

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