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:
parent
77a1b4f3ac
commit
8ee3d8b588
|
@ -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 = "";
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
Loading…
Reference in New Issue