forked from j62/ctbrec
1
0
Fork 0

Fix post-processing timestamp problems

This commit is contained in:
0xboobface 2019-12-25 17:55:33 +01:00
parent b8cdb2200e
commit 6cc8fd9cc2
11 changed files with 124 additions and 97 deletions

View File

@ -8,6 +8,10 @@ import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.StandardCopyOption; import java.nio.file.StandardCopyOption;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
@ -142,23 +146,22 @@ public class Config {
return configDir; return configDir;
} }
public File getFileForRecording(Model model, String suffix) { public File getFileForRecording(Model model, String suffix, Instant startTime) {
File dirForRecording = getDirForRecording(model); DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(RECORDING_DATE_FORMAT);
SimpleDateFormat sdf = new SimpleDateFormat(RECORDING_DATE_FORMAT); LocalDateTime startDateTime = LocalDateTime.ofInstant(startTime, ZoneId.systemDefault());
String startTime = sdf.format(new Date()); String formattedDate = dateTimeFormatter.format(startDateTime);
File targetFile = new File(dirForRecording, model.getSanitizedNamed() + '_' + startTime + '.' + suffix); File dirForRecording = getDirForRecording(model, formattedDate);
File targetFile = new File(dirForRecording, model.getSanitizedNamed() + '_' + formattedDate + '.' + suffix);
return targetFile; return targetFile;
} }
private File getDirForRecording(Model model) { private File getDirForRecording(Model model, String formattedDate) {
switch(getSettings().recordingsDirStructure) { switch(getSettings().recordingsDirStructure) {
case ONE_PER_MODEL: case ONE_PER_MODEL:
return new File(getSettings().recordingsDir, model.getSanitizedNamed()); return new File(getSettings().recordingsDir, model.getSanitizedNamed());
case ONE_PER_RECORDING: case ONE_PER_RECORDING:
File modelDir = new File(getSettings().recordingsDir, model.getSanitizedNamed()); File modelDir = new File(getSettings().recordingsDir, model.getSanitizedNamed());
SimpleDateFormat sdf = new SimpleDateFormat(RECORDING_DATE_FORMAT); return new File(modelDir, formattedDate);
String startTime = sdf.format(new Date());
return new File(modelDir, startTime);
case FLAT: case FLAT:
default: default:
return new File(getSettings().recordingsDir); return new File(getSettings().recordingsDir);

View File

@ -16,6 +16,7 @@ import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.NoSuchElementException; import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.BlockingQueue; import java.util.concurrent.BlockingQueue;
@ -95,6 +96,21 @@ public class NextGenLocalRecorder implements Recorder {
LOG.info("Models to record: {}", models); LOG.info("Models to record: {}", models);
LOG.info("Saving recordings in {}", config.getSettings().recordingsDir); LOG.info("Saving recordings in {}", config.getSettings().recordingsDir);
startCompletionHandler();
scheduler.scheduleWithFixedDelay(() -> {
try {
if (!recordingProcesses.isEmpty() && !enoughSpaceForRecording()) {
LOG.info("No space left -> Stopping all recordings");
stopRecordingProcesses();
}
} catch (IOException e) {
LOG.error("Couldn't check space left on device", e);
}
}, 1, 1, TimeUnit.SECONDS);
}
private void startCompletionHandler() {
Thread completionHandler = new Thread(() -> { Thread completionHandler = new Thread(() -> {
while (!Thread.interrupted()) { while (!Thread.interrupted()) {
try { try {
@ -127,17 +143,6 @@ public class NextGenLocalRecorder implements Recorder {
completionHandler.setName("CompletionHandler"); completionHandler.setName("CompletionHandler");
completionHandler.setDaemon(true); completionHandler.setDaemon(true);
completionHandler.start(); completionHandler.start();
scheduler.scheduleWithFixedDelay(() -> {
try {
if (!recordingProcesses.isEmpty() && !enoughSpaceForRecording()) {
LOG.info("No space left -> Stopping all recordings");
stopRecordingProcesses();
}
} catch (IOException e) {
LOG.error("Couldn't check space left on device", e);
}
}, 1, 1, TimeUnit.SECONDS);
} }
private void submitPostProcessingJob(Recording recording) { private void submitPostProcessingJob(Recording recording) {
@ -150,6 +155,9 @@ public class NextGenLocalRecorder implements Recorder {
recordingManager.saveRecording(recording); recordingManager.saveRecording(recording);
deleteIfTooShort(recording); deleteIfTooShort(recording);
} catch (Exception e) { } catch (Exception e) {
if (e instanceof InterruptedException) { // NOSONAR
Thread.currentThread().interrupt();
}
LOG.error("Error while post-processing recording {}", recording, e); LOG.error("Error while post-processing recording {}", recording, e);
recording.setStatus(State.FAILED); recording.setStatus(State.FAILED);
try { try {
@ -240,14 +248,16 @@ public class NextGenLocalRecorder implements Recorder {
LOG.debug("Starting recording for model {}", model.getName()); LOG.debug("Starting recording for model {}", model.getName());
Download download = model.createDownload(); Download download = model.createDownload();
download.init(config, model); download.init(config, model, Instant.now());
Objects.requireNonNull(download.getStartTime(),
"At this point the download should have set a startTime. Make sure to set a startTime in " + download.getClass() + ".init()");
LOG.debug("Downloading with {}", download.getClass().getSimpleName()); LOG.debug("Downloading with {}", download.getClass().getSimpleName());
Recording rec = new Recording(); Recording rec = new Recording();
rec.setDownload(download); rec.setDownload(download);
rec.setPath(download.getPath(model).replaceAll("\\\\", "/")); rec.setPath(download.getPath(model).replaceAll("\\\\", "/"));
rec.setModel(model); rec.setModel(model);
rec.setStartDate(Instant.ofEpochMilli(System.currentTimeMillis())); rec.setStartDate(download.getStartTime());
recordingProcesses.put(model, rec); recordingProcesses.put(model, rec);
recordingManager.add(rec); recordingManager.add(rec);
completionService.submit(() -> { completionService.submit(() -> {
@ -621,7 +631,7 @@ public class NextGenLocalRecorder implements Recorder {
for (Recording other : recordings) { for (Recording other : recordings) {
if(other.equals(recording)) { if(other.equals(recording)) {
Download download = other.getModel().createDownload(); Download download = other.getModel().createDownload();
download.init(Config.getInstance(), other.getModel()); download.init(Config.getInstance(), other.getModel(), other.getStartDate());
other.setDownload(download); other.setDownload(download);
submitPostProcessingJob(other); submitPostProcessingJob(other);
return; return;

View File

@ -1,6 +1,8 @@
package ctbrec.recorder.download; package ctbrec.recorder.download;
import java.io.File; import java.io.File;
import java.io.IOException;
import java.time.Instant;
import java.util.Arrays; import java.util.Arrays;
import org.slf4j.Logger; import org.slf4j.Logger;
@ -15,12 +17,13 @@ public abstract class AbstractDownload implements Download {
private static final Logger LOG = LoggerFactory.getLogger(AbstractDownload.class); private static final Logger LOG = LoggerFactory.getLogger(AbstractDownload.class);
protected void runPostProcessingScript(Recording recording) { protected Instant startTime;
protected void runPostProcessingScript(Recording recording) throws IOException, InterruptedException {
String postProcessing = Config.getInstance().getSettings().postProcessing; String postProcessing = Config.getInstance().getSettings().postProcessing;
if (postProcessing != null && !postProcessing.isEmpty()) { if (postProcessing != null && !postProcessing.isEmpty()) {
File target = recording.getAbsoluteFile(); File target = recording.getAbsoluteFile();
Runtime rt = Runtime.getRuntime(); Runtime rt = Runtime.getRuntime();
try {
String[] args = new String[] { String[] args = new String[] {
postProcessing, postProcessing,
target.getParentFile().getAbsolutePath(), target.getParentFile().getAbsolutePath(),
@ -45,9 +48,11 @@ public abstract class AbstractDownload implements Download {
process.waitFor(); process.waitFor();
LOG.debug("Process finished."); LOG.debug("Process finished.");
} catch (Exception e) {
LOG.error("Error in process thread", e);
} }
} }
@Override
public Instant getStartTime() {
return startTime;
} }
} }

View File

@ -10,7 +10,7 @@ import ctbrec.Model;
import ctbrec.Recording; import ctbrec.Recording;
public interface Download { public interface Download {
public void init(Config config, Model model); public void init(Config config, Model model, Instant startTime);
public void start() throws IOException; public void start() throws IOException;
public void stop(); public void stop();
public Model getModel(); public Model getModel();

View File

@ -0,0 +1,7 @@
package ctbrec.recorder.download;
public class ProcessExitedUncleanException extends RuntimeException {
public ProcessExitedUncleanException(String msg) {
super(msg);
}
}

View File

@ -32,10 +32,11 @@ import org.slf4j.LoggerFactory;
import ctbrec.Config; import ctbrec.Config;
import ctbrec.Model; import ctbrec.Model;
import ctbrec.Recording; import ctbrec.Recording;
import ctbrec.Recording.State;
import ctbrec.io.HttpClient; import ctbrec.io.HttpClient;
import ctbrec.io.HttpException;
import ctbrec.recorder.download.AbstractDownload; import ctbrec.recorder.download.AbstractDownload;
import ctbrec.recorder.download.dash.SegmentTimelineType.S; import ctbrec.recorder.download.dash.SegmentTimelineType.S;
import ctbrec.recorder.download.hls.PostProcessingException;
import okhttp3.Request; import okhttp3.Request;
import okhttp3.Response; import okhttp3.Response;
@ -53,7 +54,6 @@ public class DashDownload extends AbstractDownload {
private HttpClient httpClient; private HttpClient httpClient;
private Config config; private Config config;
private Model model; private Model model;
private Instant startTime;
private Instant endTime; private Instant endTime;
private Path downloadDir; private Path downloadDir;
private String manifestUrl; private String manifestUrl;
@ -80,7 +80,11 @@ public class DashDownload extends AbstractDownload {
.build(); // @formatter:on .build(); // @formatter:on
LOG.trace("Loading manifest {}", url); LOG.trace("Loading manifest {}", url);
try (Response response = httpClient.execute(request)) { try (Response response = httpClient.execute(request)) {
if (response.isSuccessful()) {
return response.body().string(); return response.body().string();
} else {
throw new HttpException(response.code(), "Couldn't load manifest: " + response.message());
}
} }
} }
@ -181,7 +185,7 @@ public class DashDownload extends AbstractDownload {
File segmentFile = new File(dir, prefix + '_' + df.format(c) + '_' + new File(absFile).getName()); File segmentFile = new File(dir, prefix + '_' + df.format(c) + '_' + new File(absFile).getName());
while (tries <= 10) { while (tries <= 10) {
if (!segmentFile.exists() || segmentFile.length() == 0) { if (!segmentFile.exists() || segmentFile.length() == 0) {
if (tries > 1) { if (tries == 10) {
LOG.debug("Loading segment, try {}, {} {} {}", tries, response.code(), response.headers().values("Content-Length"), url); LOG.debug("Loading segment, try {}, {} {} {}", tries, response.code(), response.headers().values("Content-Length"), url);
} else { } else {
LOG.trace("Loading segment, try {}, {} {} {}", tries, response.code(), response.headers().values("Content-Length"), url); LOG.trace("Loading segment, try {}, {} {} {}", tries, response.code(), response.headers().values("Content-Length"), url);
@ -204,11 +208,11 @@ public class DashDownload extends AbstractDownload {
} }
@Override @Override
public void init(Config config, Model model) { public void init(Config config, Model model, Instant startTime) {
this.config = config; this.config = config;
this.model = model; this.model = model;
startTime = Instant.now(); this.startTime = startTime;
File finalFile = Config.getInstance().getFileForRecording(model, "mp4"); File finalFile = Config.getInstance().getFileForRecording(model, "mp4", startTime);
targetFile = new File(finalFile.getParentFile(), finalFile.getName() + ".part"); targetFile = new File(finalFile.getParentFile(), finalFile.getName() + ".part");
downloadDir = targetFile.toPath(); downloadDir = targetFile.toPath();
} }
@ -241,10 +245,10 @@ public class DashDownload extends AbstractDownload {
} }
private boolean splitRecording() { private boolean splitRecording() {
if(config.getSettings().splitRecordings > 0) { if (config.getSettings().splitRecordings > 0) {
Duration recordingDuration = Duration.between(splitRecStartTime, ZonedDateTime.now()); Duration recordingDuration = Duration.between(splitRecStartTime, ZonedDateTime.now());
long seconds = recordingDuration.getSeconds(); long seconds = recordingDuration.getSeconds();
if(seconds >= config.getSettings().splitRecordings) { if (seconds >= config.getSettings().splitRecordings) {
internalStop(); internalStop();
return true; return true;
} }
@ -331,11 +335,6 @@ public class DashDownload extends AbstractDownload {
return model; return model;
} }
@Override
public Instant getStartTime() {
return startTime;
}
@Override @Override
public Duration getLength() { public Duration getLength() {
return Duration.between(startTime, Optional.ofNullable(endTime).orElse(Instant.now())); return Duration.between(startTime, Optional.ofNullable(endTime).orElse(Instant.now()));
@ -352,9 +351,8 @@ public class DashDownload extends AbstractDownload {
targetFile = file; targetFile = file;
recording.setPath(path.substring(0, path.length() - 5)); recording.setPath(path.substring(0, path.length() - 5));
runPostProcessingScript(recording); runPostProcessingScript(recording);
} catch (IOException e) { } catch (Exception e) {
LOG.error("Error while merging dash segments", e); throw new PostProcessingException(e);
recording.setStatus(State.FAILED);
} }
} }

View File

@ -12,6 +12,7 @@ import org.slf4j.LoggerFactory;
import ctbrec.OS; import ctbrec.OS;
import ctbrec.io.StreamRedirectThread; import ctbrec.io.StreamRedirectThread;
import ctbrec.recorder.download.ProcessExitedUncleanException;
public class FfmpegMuxer { public class FfmpegMuxer {
private static final Logger LOG = LoggerFactory.getLogger(FfmpegMuxer.class); private static final Logger LOG = LoggerFactory.getLogger(FfmpegMuxer.class);
@ -96,10 +97,4 @@ public class FfmpegMuxer {
return 1; return 1;
} }
} }
public static class ProcessExitedUncleanException extends RuntimeException {
public ProcessExitedUncleanException(String msg) {
super(msg);
}
}
} }

View File

@ -4,7 +4,6 @@ import java.io.ByteArrayInputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.net.URL; import java.net.URL;
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;
@ -51,7 +50,6 @@ public abstract class AbstractHlsDownload extends AbstractDownload {
protected HttpClient client; protected HttpClient client;
protected volatile boolean running = false; protected volatile boolean running = false;
protected Instant startTime;
protected Model model = new UnknownModel(); protected Model model = new UnknownModel();
protected BlockingQueue<Runnable> downloadQueue = new LinkedBlockingQueue<>(50); protected BlockingQueue<Runnable> downloadQueue = new LinkedBlockingQueue<>(50);
protected ExecutorService downloadThreadPool = new ThreadPoolExecutor(5, 5, 2, TimeUnit.MINUTES, downloadQueue, createThreadFactory()); protected ExecutorService downloadThreadPool = new ThreadPoolExecutor(5, 5, 2, TimeUnit.MINUTES, downloadQueue, createThreadFactory());
@ -180,11 +178,6 @@ public abstract class AbstractHlsDownload extends AbstractDownload {
abstract void internalStop(); abstract void internalStop();
@Override
public Instant getStartTime() {
return startTime;
}
@Override @Override
public Model getModel() { public Model getModel() {
return model; return model;

View File

@ -64,14 +64,14 @@ public class HlsDownload extends AbstractHlsDownload {
} }
@Override @Override
public void init(Config config, Model model) { public void init(Config config, Model model, Instant startTime) {
this.config = config; this.config = config;
super.model = model; super.model = model;
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(Config.RECORDING_DATE_FORMAT); DateTimeFormatter formatter = DateTimeFormatter.ofPattern(Config.RECORDING_DATE_FORMAT);
this.startTime = Instant.now(); this.startTime = startTime;
String startTime = formatter.format(ZonedDateTime.ofInstant(this.startTime, ZoneId.systemDefault())); String formattedStartTime = formatter.format(ZonedDateTime.ofInstant(this.startTime, ZoneId.systemDefault()));
Path modelDir = FileSystems.getDefault().getPath(config.getSettings().recordingsDir, model.getSanitizedNamed()); Path modelDir = FileSystems.getDefault().getPath(config.getSettings().recordingsDir, model.getSanitizedNamed());
downloadDir = FileSystems.getDefault().getPath(modelDir.toString(), startTime); downloadDir = FileSystems.getDefault().getPath(modelDir.toString(), formattedStartTime);
} }
@Override @Override
@ -179,7 +179,11 @@ public class HlsDownload extends AbstractHlsDownload {
recording.setStatusWithEvent(State.GENERATING_PLAYLIST); recording.setStatusWithEvent(State.GENERATING_PLAYLIST);
generatePlaylist(recording); generatePlaylist(recording);
recording.setStatusWithEvent(State.POST_PROCESSING); recording.setStatusWithEvent(State.POST_PROCESSING);
try {
runPostProcessingScript(recording); runPostProcessingScript(recording);
} catch (Exception e) {
throw new PostProcessingException(e);
}
} }
protected File generatePlaylist(Recording recording) { protected File generatePlaylist(Recording recording) {

View File

@ -9,6 +9,7 @@ import java.nio.file.Files;
import java.security.InvalidKeyException; import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.time.Duration; import java.time.Duration;
import java.time.Instant;
import java.util.Arrays; import java.util.Arrays;
import org.jcodec.containers.mp4.MP4Util; import org.jcodec.containers.mp4.MP4Util;
@ -27,6 +28,7 @@ import ctbrec.io.HttpClient;
import ctbrec.io.StreamRedirectThread; import ctbrec.io.StreamRedirectThread;
import ctbrec.recorder.ProgressListener; import ctbrec.recorder.ProgressListener;
import ctbrec.recorder.RecordingManager; import ctbrec.recorder.RecordingManager;
import ctbrec.recorder.download.ProcessExitedUncleanException;
import okhttp3.Request; import okhttp3.Request;
import okhttp3.Response; import okhttp3.Response;
@ -40,9 +42,15 @@ public class MergedHlsDownload extends HlsDownload {
} }
@Override @Override
public void init(Config config, Model model) { public void init(Config config, Model model, Instant startTime) {
super.init(config, model); super.init(config, model, startTime);
finalFile = Config.getInstance().getFileForRecording(model, "mp4"); try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
finalFile = Config.getInstance().getFileForRecording(model, "mp4", startTime);
downloadDir = finalFile.getParentFile().toPath();
} }
@Override @Override
@ -60,12 +68,12 @@ public class MergedHlsDownload extends HlsDownload {
} }
runPostProcessingScript(recording); runPostProcessingScript(recording);
} catch (PostProcessingException | IOException e) { } catch (Exception e) {
LOG.error("An error occurred during post-processing", e); throw new PostProcessingException(e);
} }
} }
private void postprocess(File playlist, File target) throws PostProcessingException { private void postprocess(File playlist, File target) {
try { try {
File dir = playlist.getParentFile(); File dir = playlist.getParentFile();
// @formatter:off // @formatter:off
@ -90,7 +98,7 @@ public class MergedHlsDownload extends HlsDownload {
Files.delete(segment.toPath()); Files.delete(segment.toPath());
} }
} else { } else {
throw new PostProcessingException("FFmpeg exit code was " + exitCode); throw new ProcessExitedUncleanException("FFmpeg exit code was " + exitCode);
} }
} catch (InterruptedException e) { } catch (InterruptedException e) {
Thread.currentThread().interrupt(); Thread.currentThread().interrupt();
@ -101,7 +109,7 @@ public class MergedHlsDownload extends HlsDownload {
} }
public void downloadFinishedRecording(String segmentPlaylistUri, File target, ProgressListener progressListener) public void downloadFinishedRecording(String segmentPlaylistUri, File target, ProgressListener progressListener)
throws IOException, ParseException, PlaylistException, InvalidKeyException, NoSuchAlgorithmException, PostProcessingException { throws IOException, ParseException, PlaylistException, InvalidKeyException, NoSuchAlgorithmException {
if (Config.getInstance().getSettings().requireAuthentication) { if (Config.getInstance().getSettings().requireAuthentication) {
URL u = new URL(segmentPlaylistUri); URL u = new URL(segmentPlaylistUri);
String path = u.getPath(); String path = u.getPath();

View File

@ -1,9 +1,13 @@
package ctbrec.recorder.download.hls; package ctbrec.recorder.download.hls;
public class PostProcessingException extends Exception { public class PostProcessingException extends RuntimeException {
public PostProcessingException(String msg) { public PostProcessingException(String msg) {
super(msg); super(msg);
} }
public PostProcessingException(Exception cause) {
super(cause);
}
} }