forked from j62/ctbrec
Improve thread interrupt and lock handling
This commit is contained in:
parent
84a02d7432
commit
011e8c2f29
|
@ -123,6 +123,7 @@ public class StreamPreview extends StackPane {
|
||||||
}
|
}
|
||||||
showTestImage();
|
showTestImage();
|
||||||
} catch (InterruptedException | InterruptedIOException e) {
|
} catch (InterruptedException | InterruptedIOException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
// future has been canceled, that's fine
|
// future has been canceled, that's fine
|
||||||
} catch (ExecutionException e) {
|
} catch (ExecutionException e) {
|
||||||
if(e.getCause() instanceof InterruptedException || e.getCause() instanceof InterruptedIOException) {
|
if(e.getCause() instanceof InterruptedException || e.getCause() instanceof InterruptedIOException) {
|
||||||
|
@ -180,6 +181,7 @@ public class StreamPreview extends StackPane {
|
||||||
|
|
||||||
private void checkInterrupt() throws InterruptedException {
|
private void checkInterrupt() throws InterruptedException {
|
||||||
if(Thread.interrupted()) {
|
if(Thread.interrupted()) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
throw new InterruptedException();
|
throw new InterruptedException();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,6 +40,7 @@ public final class Toast {
|
||||||
try {
|
try {
|
||||||
Thread.sleep(toastDelay);
|
Thread.sleep(toastDelay);
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
}
|
}
|
||||||
Timeline fadeOutTimeline = new Timeline();
|
Timeline fadeOutTimeline = new Timeline();
|
||||||
KeyFrame fadeOutKey1 = new KeyFrame(Duration.millis(fadeOutDelay), new KeyValue(toastStage.getScene().getRoot().opacityProperty(), 0));
|
KeyFrame fadeOutKey1 = new KeyFrame(Duration.millis(fadeOutDelay), new KeyValue(toastStage.getScene().getRoot().opacityProperty(), 0));
|
||||||
|
|
|
@ -53,7 +53,7 @@ import ctbrec.sites.Site;
|
||||||
|
|
||||||
public class NextGenLocalRecorder implements Recorder {
|
public class NextGenLocalRecorder implements Recorder {
|
||||||
|
|
||||||
private static final transient Logger LOG = LoggerFactory.getLogger(NextGenLocalRecorder.class);
|
private static final Logger LOG = LoggerFactory.getLogger(NextGenLocalRecorder.class);
|
||||||
private static final boolean IGNORE_CACHE = true;
|
private static final boolean IGNORE_CACHE = true;
|
||||||
private List<Model> models = Collections.synchronizedList(new ArrayList<>());
|
private List<Model> models = Collections.synchronizedList(new ArrayList<>());
|
||||||
private Config config;
|
private Config config;
|
||||||
|
@ -78,7 +78,7 @@ public class NextGenLocalRecorder implements Recorder {
|
||||||
public NextGenLocalRecorder(Config config, List<Site> sites) throws IOException {
|
public NextGenLocalRecorder(Config config, List<Site> sites) throws IOException {
|
||||||
this.config = config;
|
this.config = config;
|
||||||
recordingManager = new RecordingManager(config, sites);
|
recordingManager = new RecordingManager(config, sites);
|
||||||
config.getSettings().models.stream().forEach((m) -> {
|
config.getSettings().models.stream().forEach(m -> {
|
||||||
if (m.getSite() != null) {
|
if (m.getSite() != null) {
|
||||||
if (m.getSite().isEnabled()) {
|
if (m.getSite().isEnabled()) {
|
||||||
models.add(m);
|
models.add(m);
|
||||||
|
@ -92,9 +92,6 @@ public class NextGenLocalRecorder implements Recorder {
|
||||||
|
|
||||||
recording = true;
|
recording = true;
|
||||||
registerEventBusListener();
|
registerEventBusListener();
|
||||||
// if(Config.isServerMode()) {
|
|
||||||
// processUnfinishedRecordings();
|
|
||||||
// }
|
|
||||||
|
|
||||||
LOG.debug("Recorder initialized");
|
LOG.debug("Recorder initialized");
|
||||||
LOG.info("Models to record: {}", models);
|
LOG.info("Models to record: {}", models);
|
||||||
|
@ -104,24 +101,27 @@ public class NextGenLocalRecorder implements Recorder {
|
||||||
while (!Thread.interrupted()) {
|
while (!Thread.interrupted()) {
|
||||||
try {
|
try {
|
||||||
Future<Recording> result = completionService.take();
|
Future<Recording> result = completionService.take();
|
||||||
Recording recording = result.get();
|
Recording rec = result.get();
|
||||||
recordingsLock.lock();
|
recordingsLock.lock();
|
||||||
try {
|
try {
|
||||||
recordingProcesses.remove(recording.getModel());
|
recordingProcesses.remove(rec.getModel());
|
||||||
} finally {
|
} finally {
|
||||||
recordingsLock.unlock();
|
recordingsLock.unlock();
|
||||||
}
|
}
|
||||||
if (recording.getStatus() == State.WAITING) {
|
if (rec.getStatus() == State.WAITING) {
|
||||||
LOG.debug("Download finished for {} -> Starting post-processing", recording.getModel().getName());
|
LOG.debug("Download finished for {} -> Starting post-processing", rec.getModel().getName());
|
||||||
submitPostProcessingJob(recording);
|
submitPostProcessingJob(rec);
|
||||||
|
|
||||||
// check, if we have to restart the recording
|
// check, if we have to restart the recording
|
||||||
Model model = recording.getModel();
|
Model model = rec.getModel();
|
||||||
tryRestartRecording(model);
|
tryRestartRecording(model);
|
||||||
} else {
|
} else {
|
||||||
setRecordingStatus(recording, State.FAILED);
|
setRecordingStatus(rec, State.FAILED);
|
||||||
}
|
}
|
||||||
} catch (InterruptedException | ExecutionException | IllegalStateException e) {
|
} catch (ExecutionException | IllegalStateException e) {
|
||||||
|
LOG.error("Error while completing recording", e);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
LOG.error("Error while completing recording", e);
|
LOG.error("Error while completing recording", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -171,7 +171,7 @@ public class NextGenLocalRecorder implements Recorder {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void startRecording(Model model) throws IOException, InvalidKeyException, NoSuchAlgorithmException, IllegalStateException {
|
public void startRecording(Model model) throws IOException, InvalidKeyException, NoSuchAlgorithmException {
|
||||||
if (!models.contains(model)) {
|
if (!models.contains(model)) {
|
||||||
LOG.info("Model {} added", model);
|
LOG.info("Model {} added", model);
|
||||||
modelLock.lock();
|
modelLock.lock();
|
||||||
|
@ -190,7 +190,10 @@ public class NextGenLocalRecorder implements Recorder {
|
||||||
if (model.isOnline()) {
|
if (model.isOnline()) {
|
||||||
startRecordingProcess(model);
|
startRecordingProcess(model);
|
||||||
}
|
}
|
||||||
} catch (ExecutionException | InterruptedException e) {
|
} catch (ExecutionException e) {
|
||||||
|
// noop
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -267,7 +270,7 @@ public class NextGenLocalRecorder implements Recorder {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean deleteIfEmpty(Recording rec) throws IOException, ParseException, PlaylistException, InvalidKeyException, NoSuchAlgorithmException, IllegalStateException {
|
private boolean deleteIfEmpty(Recording rec) throws IOException, InvalidKeyException, NoSuchAlgorithmException {
|
||||||
rec.refresh();
|
rec.refresh();
|
||||||
long sizeInByte = rec.getSizeInByte();
|
long sizeInByte = rec.getSizeInByte();
|
||||||
if (sizeInByte == 0) {
|
if (sizeInByte == 0) {
|
||||||
|
@ -279,7 +282,7 @@ public class NextGenLocalRecorder implements Recorder {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean deleteIfTooShort(Recording rec) throws IOException, ParseException, PlaylistException, InvalidKeyException, NoSuchAlgorithmException, IllegalStateException {
|
private boolean deleteIfTooShort(Recording rec) throws IOException, ParseException, PlaylistException, InvalidKeyException, NoSuchAlgorithmException {
|
||||||
Duration minimumLengthInSeconds = Duration.ofSeconds(Config.getInstance().getSettings().minimumLengthInSeconds);
|
Duration minimumLengthInSeconds = Duration.ofSeconds(Config.getInstance().getSettings().minimumLengthInSeconds);
|
||||||
if (minimumLengthInSeconds.getSeconds() <= 0) {
|
if (minimumLengthInSeconds.getSeconds() <= 0) {
|
||||||
return false;
|
return false;
|
||||||
|
@ -296,7 +299,7 @@ public class NextGenLocalRecorder implements Recorder {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void stopRecording(Model model) throws IOException, InvalidKeyException, NoSuchAlgorithmException, IllegalStateException {
|
public void stopRecording(Model model) throws IOException, InvalidKeyException, NoSuchAlgorithmException {
|
||||||
modelLock.lock();
|
modelLock.lock();
|
||||||
try {
|
try {
|
||||||
if (models.contains(model)) {
|
if (models.contains(model)) {
|
||||||
|
@ -314,8 +317,8 @@ public class NextGenLocalRecorder implements Recorder {
|
||||||
recordingsLock.lock();
|
recordingsLock.lock();
|
||||||
try {
|
try {
|
||||||
if (recordingProcesses.containsKey(model)) {
|
if (recordingProcesses.containsKey(model)) {
|
||||||
Recording recording = recordingProcesses.get(model);
|
Recording rec = recordingProcesses.get(model);
|
||||||
recording.getDownload().stop();
|
rec.getDownload().stop();
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
recordingsLock.unlock();
|
recordingsLock.unlock();
|
||||||
|
@ -323,7 +326,7 @@ public class NextGenLocalRecorder implements Recorder {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void switchStreamSource(Model model) throws IOException, InvalidKeyException, NoSuchAlgorithmException, IllegalStateException {
|
public void switchStreamSource(Model model) throws IOException, InvalidKeyException, NoSuchAlgorithmException {
|
||||||
if (models.contains(model)) {
|
if (models.contains(model)) {
|
||||||
int index = models.indexOf(model);
|
int index = models.indexOf(model);
|
||||||
models.get(index).setStreamUrlIndex(model.getStreamUrlIndex());
|
models.get(index).setStreamUrlIndex(model.getStreamUrlIndex());
|
||||||
|
@ -331,8 +334,8 @@ public class NextGenLocalRecorder implements Recorder {
|
||||||
LOG.debug("Switching stream source to index {} for model {}", model.getStreamUrlIndex(), model.getName());
|
LOG.debug("Switching stream source to index {} for model {}", model.getStreamUrlIndex(), model.getName());
|
||||||
recordingsLock.lock();
|
recordingsLock.lock();
|
||||||
try {
|
try {
|
||||||
Recording recording = recordingProcesses.get(model);
|
Recording rec = recordingProcesses.get(model);
|
||||||
if (recording != null) {
|
if (rec != null) {
|
||||||
stopRecordingProcess(model);
|
stopRecordingProcess(model);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
|
@ -341,7 +344,6 @@ public class NextGenLocalRecorder implements Recorder {
|
||||||
tryRestartRecording(model);
|
tryRestartRecording(model);
|
||||||
} else {
|
} else {
|
||||||
LOG.warn("Couldn't switch stream source for model {}. Not found in list", model.getName());
|
LOG.warn("Couldn't switch stream source for model {}. Not found in list", model.getName());
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -349,9 +351,9 @@ public class NextGenLocalRecorder implements Recorder {
|
||||||
recordingsLock.lock();
|
recordingsLock.lock();
|
||||||
try {
|
try {
|
||||||
LOG.debug("Stopping recording for {}", model);
|
LOG.debug("Stopping recording for {}", model);
|
||||||
Recording recording = recordingProcesses.get(model);
|
Recording rec = recordingProcesses.get(model);
|
||||||
LOG.debug("Stopping download for {}", model);
|
LOG.debug("Stopping download for {}", model);
|
||||||
recording.getDownload().stop();
|
rec.getDownload().stop();
|
||||||
} finally {
|
} finally {
|
||||||
recordingsLock.unlock();
|
recordingsLock.unlock();
|
||||||
}
|
}
|
||||||
|
@ -360,8 +362,8 @@ public class NextGenLocalRecorder implements Recorder {
|
||||||
private void stopRecordingProcesses() {
|
private void stopRecordingProcesses() {
|
||||||
recordingsLock.lock();
|
recordingsLock.lock();
|
||||||
try {
|
try {
|
||||||
for (Recording recording : recordingProcesses.values()) {
|
for (Recording rec : recordingProcesses.values()) {
|
||||||
recording.getDownload().stop();
|
rec.getDownload().stop();
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
recordingsLock.unlock();
|
recordingsLock.unlock();
|
||||||
|
@ -389,12 +391,12 @@ public class NextGenLocalRecorder implements Recorder {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<Recording> getRecordings() throws IOException, InvalidKeyException, NoSuchAlgorithmException, IllegalStateException {
|
public List<Recording> getRecordings() throws IOException, InvalidKeyException, NoSuchAlgorithmException {
|
||||||
return recordingManager.getAll();
|
return recordingManager.getAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void delete(Recording recording) throws IOException, InvalidKeyException, NoSuchAlgorithmException, IllegalStateException {
|
public void delete(Recording recording) throws IOException, InvalidKeyException, NoSuchAlgorithmException {
|
||||||
recordingManager.delete(recording);
|
recordingManager.delete(recording);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -423,6 +425,7 @@ public class NextGenLocalRecorder implements Recorder {
|
||||||
try {
|
try {
|
||||||
Thread.sleep(1000);
|
Thread.sleep(1000);
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
LOG.error("Error while waiting for downloads to finish", e);
|
LOG.error("Error while waiting for downloads to finish", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -438,6 +441,7 @@ public class NextGenLocalRecorder implements Recorder {
|
||||||
LOG.info("Waiting for post-processing to finish");
|
LOG.info("Waiting for post-processing to finish");
|
||||||
ppPool.awaitTermination(10, TimeUnit.MINUTES);
|
ppPool.awaitTermination(10, TimeUnit.MINUTES);
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
LOG.error("Error while waiting for pools to finish", e);
|
LOG.error("Error while waiting for pools to finish", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -463,8 +467,8 @@ public class NextGenLocalRecorder implements Recorder {
|
||||||
|
|
||||||
recordingsLock.lock();
|
recordingsLock.lock();
|
||||||
try {
|
try {
|
||||||
Recording recording = recordingProcesses.get(model);
|
Recording rec = recordingProcesses.get(model);
|
||||||
Optional.ofNullable(recording).map(Recording::getDownload).ifPresent(Download::stop);
|
Optional.ofNullable(rec).map(Recording::getDownload).ifPresent(Download::stop);
|
||||||
} finally {
|
} finally {
|
||||||
recordingsLock.unlock();
|
recordingsLock.unlock();
|
||||||
}
|
}
|
||||||
|
@ -485,9 +489,11 @@ public class NextGenLocalRecorder implements Recorder {
|
||||||
config.save();
|
config.save();
|
||||||
} else {
|
} else {
|
||||||
LOG.warn("Couldn't resume model {}. Not found in list", model.getName());
|
LOG.warn("Couldn't resume model {}. Not found in list", model.getName());
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
} catch (ExecutionException | InterruptedException e) {
|
} catch (ExecutionException e) {
|
||||||
|
LOG.error("Couldn't check, if model {} is online", model.getName());
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
LOG.error("Couldn't check, if model {} is online", model.getName());
|
LOG.error("Couldn't check, if model {} is online", model.getName());
|
||||||
} finally {
|
} finally {
|
||||||
modelLock.unlock();
|
modelLock.unlock();
|
||||||
|
@ -515,7 +521,10 @@ public class NextGenLocalRecorder implements Recorder {
|
||||||
return getModels().stream().filter(m -> {
|
return getModels().stream().filter(m -> {
|
||||||
try {
|
try {
|
||||||
return m.isOnline();
|
return m.isOnline();
|
||||||
} catch (IOException | ExecutionException | InterruptedException e) {
|
} catch (IOException | ExecutionException e) {
|
||||||
|
return false;
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}).collect(Collectors.toList());
|
}).collect(Collectors.toList());
|
||||||
|
@ -600,14 +609,11 @@ public class NextGenLocalRecorder implements Recorder {
|
||||||
}
|
}
|
||||||
|
|
||||||
private ThreadFactory createThreadFactory(String name) {
|
private ThreadFactory createThreadFactory(String name) {
|
||||||
return new ThreadFactory() {
|
return r -> {
|
||||||
@Override
|
Thread t = new Thread(r);
|
||||||
public Thread newThread(Runnable r) {
|
t.setName(name + " " + UUID.randomUUID().toString().substring(0, 8));
|
||||||
Thread t = new Thread(r);
|
t.setDaemon(true);
|
||||||
t.setName(name + " " + UUID.randomUUID().toString().substring(0, 8));
|
return t;
|
||||||
t.setDaemon(true);
|
|
||||||
return t;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -23,7 +23,7 @@ import ctbrec.event.ModelStateChangedEvent;
|
||||||
import ctbrec.io.HttpException;
|
import ctbrec.io.HttpException;
|
||||||
|
|
||||||
public class OnlineMonitor extends Thread {
|
public class OnlineMonitor extends Thread {
|
||||||
private static final transient Logger LOG = LoggerFactory.getLogger(OnlineMonitor.class);
|
private static final Logger LOG = LoggerFactory.getLogger(OnlineMonitor.class);
|
||||||
private static final boolean IGNORE_CACHE = true;
|
private static final boolean IGNORE_CACHE = true;
|
||||||
|
|
||||||
private volatile boolean running = false;
|
private volatile boolean running = false;
|
||||||
|
@ -47,56 +47,74 @@ public class OnlineMonitor extends Thread {
|
||||||
List<Model> models = recorder.getModels();
|
List<Model> models = recorder.getModels();
|
||||||
|
|
||||||
// remove models, which are not recorded anymore
|
// remove models, which are not recorded anymore
|
||||||
for (Iterator<Model> iterator = states.keySet().iterator(); iterator.hasNext();) {
|
removeDeletedModels(models);
|
||||||
Model model = iterator.next();
|
|
||||||
if(!models.contains(model)) {
|
|
||||||
iterator.remove();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// update the currently recorded models
|
// update the currently recorded models
|
||||||
for (Model model : models) {
|
updateModels(models);
|
||||||
try {
|
|
||||||
if(model.isOnline(IGNORE_CACHE)) {
|
|
||||||
EventBusHolder.BUS.post(new ModelIsOnlineEvent(model));
|
|
||||||
}
|
|
||||||
Model.State state = model.getOnlineState(false);
|
|
||||||
Model.State oldState = states.getOrDefault(model, UNKNOWN);
|
|
||||||
states.put(model, state);
|
|
||||||
if(state != oldState) {
|
|
||||||
EventBusHolder.BUS.post(new ModelStateChangedEvent(model, oldState, state));
|
|
||||||
}
|
|
||||||
} catch (HttpException e) {
|
|
||||||
LOG.error("Couldn't check if model {} is online. HTTP Response: {} - {}",
|
|
||||||
model.getName(), e.getResponseCode(), e.getResponseMessage());
|
|
||||||
} catch (SocketTimeoutException e) {
|
|
||||||
LOG.error("Couldn't check if model {} is online. Request timed out", model.getName());
|
|
||||||
} catch (InterruptedException | InterruptedIOException e) {
|
|
||||||
if(running) {
|
|
||||||
LOG.error("Couldn't check if model {} is online", model.getName(), e);
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
LOG.error("Couldn't check if model {} is online", model.getName(), e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Instant end = Instant.now();
|
Instant end = Instant.now();
|
||||||
Duration timeCheckTook = Duration.between(begin, end);
|
Duration timeCheckTook = Duration.between(begin, end);
|
||||||
LOG.trace("Online check for {} models took {} seconds", models.size(), timeCheckTook.getSeconds());
|
suspendUntilNextIteration(models, timeCheckTook);
|
||||||
|
}
|
||||||
|
LOG.debug("{} terminated", getName());
|
||||||
|
}
|
||||||
|
|
||||||
long sleepTime = Config.getInstance().getSettings().onlineCheckIntervalInSecs;
|
private void removeDeletedModels(List<Model> models) {
|
||||||
if(timeCheckTook.getSeconds() < sleepTime) {
|
for (Iterator<Model> iterator = states.keySet().iterator(); iterator.hasNext();) {
|
||||||
try {
|
Model model = iterator.next();
|
||||||
if (running) {
|
if(!models.contains(model)) {
|
||||||
long millis = TimeUnit.SECONDS.toMillis(sleepTime - timeCheckTook.getSeconds());
|
iterator.remove();
|
||||||
LOG.trace("Sleeping {}ms", millis);
|
}
|
||||||
Thread.sleep(millis);
|
}
|
||||||
}
|
}
|
||||||
} catch (InterruptedException e) {
|
|
||||||
LOG.trace("Sleep interrupted");
|
private void updateModels(List<Model> models) {
|
||||||
}
|
for (Model model : models) {
|
||||||
|
updateModel(model);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateModel(Model model) {
|
||||||
|
try {
|
||||||
|
if(model.isOnline(IGNORE_CACHE)) {
|
||||||
|
EventBusHolder.BUS.post(new ModelIsOnlineEvent(model));
|
||||||
|
}
|
||||||
|
Model.State state = model.getOnlineState(false);
|
||||||
|
Model.State oldState = states.getOrDefault(model, UNKNOWN);
|
||||||
|
states.put(model, state);
|
||||||
|
if(state != oldState) {
|
||||||
|
EventBusHolder.BUS.post(new ModelStateChangedEvent(model, oldState, state));
|
||||||
|
}
|
||||||
|
} catch (HttpException e) {
|
||||||
|
LOG.error("Couldn't check if model {} is online. HTTP Response: {} - {}",
|
||||||
|
model.getName(), e.getResponseCode(), e.getResponseMessage());
|
||||||
|
} catch (SocketTimeoutException e) {
|
||||||
|
LOG.error("Couldn't check if model {} is online. Request timed out", model.getName());
|
||||||
|
} catch (InterruptedException | InterruptedIOException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
if(running) {
|
||||||
|
LOG.error("Couldn't check if model {} is online", model.getName(), e);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOG.error("Couldn't check if model {} is online", model.getName(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void suspendUntilNextIteration(List<Model> models, Duration timeCheckTook) {
|
||||||
|
LOG.trace("Online check for {} models took {} seconds", models.size(), timeCheckTook.getSeconds());
|
||||||
|
long sleepTime = Config.getInstance().getSettings().onlineCheckIntervalInSecs;
|
||||||
|
if(timeCheckTook.getSeconds() < sleepTime) {
|
||||||
|
try {
|
||||||
|
if (running) {
|
||||||
|
long millis = TimeUnit.SECONDS.toMillis(sleepTime - timeCheckTook.getSeconds());
|
||||||
|
LOG.trace("Sleeping {}ms", millis);
|
||||||
|
Thread.sleep(millis);
|
||||||
|
}
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
LOG.trace("Sleep interrupted");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
LOG.debug(getName() + " terminated");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void shutdown() {
|
public void shutdown() {
|
||||||
|
|
|
@ -37,13 +37,12 @@ import okhttp3.Response;
|
||||||
|
|
||||||
public class RemoteRecorder implements Recorder {
|
public class RemoteRecorder implements Recorder {
|
||||||
|
|
||||||
private static final transient Logger LOG = LoggerFactory.getLogger(RemoteRecorder.class);
|
private static final String SUCCESS = "success";
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(RemoteRecorder.class);
|
||||||
|
|
||||||
public static final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
|
public static final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
|
||||||
private Moshi moshi = new Moshi.Builder()
|
private Moshi moshi = new Moshi.Builder().add(Instant.class, new InstantJsonAdapter()).add(Model.class, new ModelJsonAdapter()).build();
|
||||||
.add(Instant.class, new InstantJsonAdapter())
|
|
||||||
.add(Model.class, new ModelJsonAdapter())
|
|
||||||
.build();
|
|
||||||
private JsonAdapter<ModelListResponse> modelListResponseAdapter = moshi.adapter(ModelListResponse.class);
|
private JsonAdapter<ModelListResponse> modelListResponseAdapter = moshi.adapter(ModelListResponse.class);
|
||||||
private JsonAdapter<RecordingListResponse> recordingListResponseAdapter = moshi.adapter(RecordingListResponse.class);
|
private JsonAdapter<RecordingListResponse> recordingListResponseAdapter = moshi.adapter(RecordingListResponse.class);
|
||||||
private JsonAdapter<ModelRequest> modelRequestAdapter = moshi.adapter(ModelRequest.class);
|
private JsonAdapter<ModelRequest> modelRequestAdapter = moshi.adapter(ModelRequest.class);
|
||||||
|
@ -62,7 +61,6 @@ public class RemoteRecorder implements Recorder {
|
||||||
private Instant lastSync = Instant.EPOCH;
|
private Instant lastSync = Instant.EPOCH;
|
||||||
private SyncThread syncThread;
|
private SyncThread syncThread;
|
||||||
|
|
||||||
|
|
||||||
public RemoteRecorder(Config config, HttpClient client, List<Site> sites) {
|
public RemoteRecorder(Config config, HttpClient client, List<Site> sites) {
|
||||||
this.config = config;
|
this.config = config;
|
||||||
this.client = client;
|
this.client = client;
|
||||||
|
@ -77,29 +75,27 @@ public class RemoteRecorder implements Recorder {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void startRecording(Model model) throws IOException, InvalidKeyException, NoSuchAlgorithmException, IllegalStateException {
|
public void startRecording(Model model) throws IOException, InvalidKeyException, NoSuchAlgorithmException {
|
||||||
sendRequest("start", model);
|
sendRequest("start", model);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void stopRecording(Model model) throws IOException, InvalidKeyException, NoSuchAlgorithmException, IllegalStateException {
|
public void stopRecording(Model model) throws IOException, InvalidKeyException, NoSuchAlgorithmException {
|
||||||
sendRequest("stop", model);
|
sendRequest("stop", model);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void sendRequest(String action, Model model) throws IOException, InvalidKeyException, NoSuchAlgorithmException, IllegalStateException {
|
private void sendRequest(String action, Model model) throws IOException, InvalidKeyException, NoSuchAlgorithmException {
|
||||||
String payload = modelRequestAdapter.toJson(new ModelRequest(action, model));
|
String payload = modelRequestAdapter.toJson(new ModelRequest(action, model));
|
||||||
LOG.debug("Sending request to recording server: {}", payload);
|
LOG.debug("Sending request to recording server: {}", payload);
|
||||||
RequestBody body = RequestBody.create(JSON, payload);
|
RequestBody body = RequestBody.create(JSON, payload);
|
||||||
Request.Builder builder = new Request.Builder()
|
Request.Builder builder = new Request.Builder().url(getRecordingEndpoint()).post(body);
|
||||||
.url(getRecordingEndpoint())
|
|
||||||
.post(body);
|
|
||||||
addHmacIfNeeded(payload, builder);
|
addHmacIfNeeded(payload, builder);
|
||||||
Request request = builder.build();
|
Request request = builder.build();
|
||||||
try (Response response = client.execute(request)) {
|
try (Response response = client.execute(request)) {
|
||||||
String json = response.body().string();
|
String json = response.body().string();
|
||||||
if (response.isSuccessful()) {
|
if (response.isSuccessful()) {
|
||||||
ModelListResponse resp = modelListResponseAdapter.fromJson(json);
|
ModelListResponse resp = modelListResponseAdapter.fromJson(json);
|
||||||
if (!resp.status.equals("success")) {
|
if (!resp.status.equals(SUCCESS)) {
|
||||||
throw new IOException("Server returned error " + resp.status + " " + resp.msg);
|
throw new IOException("Server returned error " + resp.status + " " + resp.msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -115,8 +111,8 @@ public class RemoteRecorder implements Recorder {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addHmacIfNeeded(String msg, Builder builder) throws InvalidKeyException, NoSuchAlgorithmException, IllegalStateException, UnsupportedEncodingException {
|
private void addHmacIfNeeded(String msg, Builder builder) throws InvalidKeyException, NoSuchAlgorithmException, UnsupportedEncodingException {
|
||||||
if(Config.getInstance().getSettings().requireAuthentication) {
|
if (Config.getInstance().getSettings().requireAuthentication) {
|
||||||
byte[] key = Config.getInstance().getSettings().key;
|
byte[] key = Config.getInstance().getSettings().key;
|
||||||
String hmac = Hmac.calculate(msg, key);
|
String hmac = Hmac.calculate(msg, key);
|
||||||
builder.addHeader("CTBREC-HMAC", hmac);
|
builder.addHeader("CTBREC-HMAC", hmac);
|
||||||
|
@ -131,7 +127,7 @@ public class RemoteRecorder implements Recorder {
|
||||||
@Override
|
@Override
|
||||||
public boolean isSuspended(Model model) {
|
public boolean isSuspended(Model model) {
|
||||||
int index = models.indexOf(model);
|
int index = models.indexOf(model);
|
||||||
if(index >= 0) {
|
if (index >= 0) {
|
||||||
Model m = models.get(index);
|
Model m = models.get(index);
|
||||||
return m.isSuspended();
|
return m.isSuspended();
|
||||||
} else {
|
} else {
|
||||||
|
@ -141,10 +137,10 @@ public class RemoteRecorder implements Recorder {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<Model> getModels() {
|
public List<Model> getModels() {
|
||||||
if(!lastSync.equals(Instant.EPOCH) && lastSync.isBefore(Instant.now().minusSeconds(60))) {
|
if (!lastSync.equals(Instant.EPOCH) && lastSync.isBefore(Instant.now().minusSeconds(60))) {
|
||||||
throw new RuntimeException("Last sync was over a minute ago");
|
throw new RuntimeException("Last sync was over a minute ago");
|
||||||
}
|
}
|
||||||
return new ArrayList<Model>(models);
|
return new ArrayList<>(models);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -163,7 +159,7 @@ public class RemoteRecorder implements Recorder {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
running = true;
|
running = true;
|
||||||
while(running) {
|
while (running) {
|
||||||
syncModels();
|
syncModels();
|
||||||
syncOnlineModels();
|
syncOnlineModels();
|
||||||
syncSpace();
|
syncSpace();
|
||||||
|
@ -176,14 +172,12 @@ public class RemoteRecorder implements Recorder {
|
||||||
try {
|
try {
|
||||||
String msg = "{\"action\": \"space\"}";
|
String msg = "{\"action\": \"space\"}";
|
||||||
RequestBody body = RequestBody.create(JSON, msg);
|
RequestBody body = RequestBody.create(JSON, msg);
|
||||||
Request.Builder builder = new Request.Builder()
|
Request.Builder builder = new Request.Builder().url(getRecordingEndpoint()).post(body);
|
||||||
.url(getRecordingEndpoint())
|
|
||||||
.post(body);
|
|
||||||
addHmacIfNeeded(msg, builder);
|
addHmacIfNeeded(msg, builder);
|
||||||
Request request = builder.build();
|
Request request = builder.build();
|
||||||
try(Response response = client.execute(request)) {
|
try (Response response = client.execute(request)) {
|
||||||
String json = response.body().string();
|
String json = response.body().string();
|
||||||
if(response.isSuccessful()) {
|
if (response.isSuccessful()) {
|
||||||
JSONObject resp = new JSONObject(json);
|
JSONObject resp = new JSONObject(json);
|
||||||
spaceTotal = resp.getLong("spaceTotal");
|
spaceTotal = resp.getLong("spaceTotal");
|
||||||
spaceFree = resp.getLong("spaceFree");
|
spaceFree = resp.getLong("spaceFree");
|
||||||
|
@ -200,20 +194,18 @@ public class RemoteRecorder implements Recorder {
|
||||||
try {
|
try {
|
||||||
String msg = "{\"action\": \"list\"}";
|
String msg = "{\"action\": \"list\"}";
|
||||||
RequestBody body = RequestBody.create(JSON, msg);
|
RequestBody body = RequestBody.create(JSON, msg);
|
||||||
Request.Builder builder = new Request.Builder()
|
Request.Builder builder = new Request.Builder().url(getRecordingEndpoint()).post(body);
|
||||||
.url(getRecordingEndpoint())
|
|
||||||
.post(body);
|
|
||||||
addHmacIfNeeded(msg, builder);
|
addHmacIfNeeded(msg, builder);
|
||||||
Request request = builder.build();
|
Request request = builder.build();
|
||||||
try(Response response = client.execute(request)) {
|
try (Response response = client.execute(request)) {
|
||||||
String json = response.body().string();
|
String json = response.body().string();
|
||||||
if(response.isSuccessful()) {
|
if (response.isSuccessful()) {
|
||||||
ModelListResponse resp = modelListResponseAdapter.fromJson(json);
|
ModelListResponse resp = modelListResponseAdapter.fromJson(json);
|
||||||
if(resp.status.equals("success")) {
|
if (resp.status.equals(SUCCESS)) {
|
||||||
models = resp.models;
|
models = resp.models;
|
||||||
for (Model model : models) {
|
for (Model model : models) {
|
||||||
for (Site site : sites) {
|
for (Site site : sites) {
|
||||||
if(site.isSiteForModel(model)) {
|
if (site.isSiteForModel(model)) {
|
||||||
model.setSite(site);
|
model.setSite(site);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -235,16 +227,14 @@ public class RemoteRecorder implements Recorder {
|
||||||
try {
|
try {
|
||||||
String msg = "{\"action\": \"listOnline\"}";
|
String msg = "{\"action\": \"listOnline\"}";
|
||||||
RequestBody body = RequestBody.create(JSON, msg);
|
RequestBody body = RequestBody.create(JSON, msg);
|
||||||
Request.Builder builder = new Request.Builder()
|
Request.Builder builder = new Request.Builder().url(getRecordingEndpoint()).post(body);
|
||||||
.url(getRecordingEndpoint())
|
|
||||||
.post(body);
|
|
||||||
addHmacIfNeeded(msg, builder);
|
addHmacIfNeeded(msg, builder);
|
||||||
Request request = builder.build();
|
Request request = builder.build();
|
||||||
try (Response response = client.execute(request)) {
|
try (Response response = client.execute(request)) {
|
||||||
String json = response.body().string();
|
String json = response.body().string();
|
||||||
if (response.isSuccessful()) {
|
if (response.isSuccessful()) {
|
||||||
ModelListResponse resp = modelListResponseAdapter.fromJson(json);
|
ModelListResponse resp = modelListResponseAdapter.fromJson(json);
|
||||||
if (resp.status.equals("success")) {
|
if (resp.status.equals(SUCCESS)) {
|
||||||
onlineModels = resp.models;
|
onlineModels = resp.models;
|
||||||
for (Model model : models) {
|
for (Model model : models) {
|
||||||
for (Site site : sites) {
|
for (Site site : sites) {
|
||||||
|
@ -269,27 +259,25 @@ public class RemoteRecorder implements Recorder {
|
||||||
try {
|
try {
|
||||||
String msg = "{\"action\": \"recordings\"}";
|
String msg = "{\"action\": \"recordings\"}";
|
||||||
RequestBody body = RequestBody.create(JSON, msg);
|
RequestBody body = RequestBody.create(JSON, msg);
|
||||||
Request.Builder builder = new Request.Builder()
|
Request.Builder builder = new Request.Builder().url(getRecordingEndpoint()).post(body);
|
||||||
.url(getRecordingEndpoint())
|
|
||||||
.post(body);
|
|
||||||
addHmacIfNeeded(msg, builder);
|
addHmacIfNeeded(msg, builder);
|
||||||
Request request = builder.build();
|
Request request = builder.build();
|
||||||
try (Response response = client.execute(request)) {
|
try (Response response = client.execute(request)) {
|
||||||
String json = response.body().string();
|
String json = response.body().string();
|
||||||
if (response.isSuccessful()) {
|
if (response.isSuccessful()) {
|
||||||
RecordingListResponse resp = recordingListResponseAdapter.fromJson(json);
|
RecordingListResponse resp = recordingListResponseAdapter.fromJson(json);
|
||||||
if (resp.status.equals("success")) {
|
if (resp.status.equals(SUCCESS)) {
|
||||||
List<Recording> newRecordings = resp.recordings;
|
List<Recording> newRecordings = resp.recordings;
|
||||||
// fire changed events
|
// fire changed events
|
||||||
for (Iterator<Recording> iterator = recordings.iterator(); iterator.hasNext();) {
|
for (Iterator<Recording> iterator = recordings.iterator(); iterator.hasNext();) {
|
||||||
Recording recording = iterator.next();
|
Recording recording = iterator.next();
|
||||||
if(newRecordings.contains(recording)) {
|
if (newRecordings.contains(recording)) {
|
||||||
int idx = newRecordings.indexOf(recording);
|
int idx = newRecordings.indexOf(recording);
|
||||||
Recording newRecording = newRecordings.get(idx);
|
Recording newRecording = newRecordings.get(idx);
|
||||||
if(newRecording.getStatus() != recording.getStatus()) {
|
if (newRecording.getStatus() != recording.getStatus()) {
|
||||||
File file = new File(recording.getPath());
|
File file = new File(recording.getPath());
|
||||||
RecordingStateChangedEvent evt = new RecordingStateChangedEvent(file,
|
RecordingStateChangedEvent evt = new RecordingStateChangedEvent(file, newRecording.getStatus(), recording.getModel(),
|
||||||
newRecording.getStatus(), recording.getModel(), recording.getStartDate());
|
recording.getStartDate());
|
||||||
EventBusHolder.BUS.post(evt);
|
EventBusHolder.BUS.post(evt);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -311,6 +299,7 @@ public class RemoteRecorder implements Recorder {
|
||||||
try {
|
try {
|
||||||
Thread.sleep(2000);
|
Thread.sleep(2000);
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
// interrupted, probably by stopThread
|
// interrupted, probably by stopThread
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -339,25 +328,23 @@ public class RemoteRecorder implements Recorder {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<Recording> getRecordings() throws IOException, InvalidKeyException, NoSuchAlgorithmException, IllegalStateException {
|
public List<Recording> getRecordings() throws IOException, InvalidKeyException, NoSuchAlgorithmException {
|
||||||
return recordings;
|
return recordings;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void delete(Recording recording) throws IOException, InvalidKeyException, NoSuchAlgorithmException, IllegalStateException {
|
public void delete(Recording recording) throws IOException, InvalidKeyException, NoSuchAlgorithmException {
|
||||||
RecordingRequest recReq = new RecordingRequest("delete", recording);
|
RecordingRequest recReq = new RecordingRequest("delete", recording);
|
||||||
String msg = recordingRequestAdapter.toJson(recReq);
|
String msg = recordingRequestAdapter.toJson(recReq);
|
||||||
RequestBody body = RequestBody.create(JSON, msg);
|
RequestBody body = RequestBody.create(JSON, msg);
|
||||||
Request.Builder builder = new Request.Builder()
|
Request.Builder builder = new Request.Builder().url(getRecordingEndpoint()).post(body);
|
||||||
.url(getRecordingEndpoint())
|
|
||||||
.post(body);
|
|
||||||
addHmacIfNeeded(msg, builder);
|
addHmacIfNeeded(msg, builder);
|
||||||
Request request = builder.build();
|
Request request = builder.build();
|
||||||
try (Response response = client.execute(request)) {
|
try (Response response = client.execute(request)) {
|
||||||
String json = response.body().string();
|
String json = response.body().string();
|
||||||
RecordingListResponse resp = recordingListResponseAdapter.fromJson(json);
|
RecordingListResponse resp = recordingListResponseAdapter.fromJson(json);
|
||||||
if (response.isSuccessful()) {
|
if (response.isSuccessful()) {
|
||||||
if (!resp.status.equals("success")) {
|
if (!resp.status.equals(SUCCESS)) {
|
||||||
throw new IOException("Couldn't delete recording: " + resp.msg);
|
throw new IOException("Couldn't delete recording: " + resp.msg);
|
||||||
} else {
|
} else {
|
||||||
recordings.remove(recording);
|
recordings.remove(recording);
|
||||||
|
@ -423,29 +410,29 @@ public class RemoteRecorder implements Recorder {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void switchStreamSource(Model model) throws IOException, InvalidKeyException, NoSuchAlgorithmException, IllegalStateException {
|
public void switchStreamSource(Model model) throws IOException, InvalidKeyException, NoSuchAlgorithmException {
|
||||||
sendRequest("switch", model);
|
sendRequest("switch", model);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void suspendRecording(Model model) throws InvalidKeyException, NoSuchAlgorithmException, IllegalStateException, IOException {
|
public void suspendRecording(Model model) throws InvalidKeyException, NoSuchAlgorithmException, IOException {
|
||||||
sendRequest("suspend", model);
|
sendRequest("suspend", model);
|
||||||
model.setSuspended(true);
|
model.setSuspended(true);
|
||||||
// update cached model
|
// update cached model
|
||||||
int index = models.indexOf(model);
|
int index = models.indexOf(model);
|
||||||
if(index >= 0) {
|
if (index >= 0) {
|
||||||
Model m = models.get(index);
|
Model m = models.get(index);
|
||||||
m.setSuspended(true);
|
m.setSuspended(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void resumeRecording(Model model) throws IOException, InvalidKeyException, NoSuchAlgorithmException, IllegalStateException {
|
public void resumeRecording(Model model) throws IOException, InvalidKeyException, NoSuchAlgorithmException {
|
||||||
sendRequest("resume", model);
|
sendRequest("resume", model);
|
||||||
model.setSuspended(false);
|
model.setSuspended(false);
|
||||||
// update cached model
|
// update cached model
|
||||||
int index = models.indexOf(model);
|
int index = models.indexOf(model);
|
||||||
if(index >= 0) {
|
if (index >= 0) {
|
||||||
Model m = models.get(index);
|
Model m = models.get(index);
|
||||||
m.setSuspended(false);
|
m.setSuspended(false);
|
||||||
}
|
}
|
||||||
|
@ -472,21 +459,19 @@ public class RemoteRecorder implements Recorder {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void rerunPostProcessing(Recording recording) throws IOException, InvalidKeyException, NoSuchAlgorithmException, IllegalStateException {
|
public void rerunPostProcessing(Recording recording) throws IOException, InvalidKeyException, NoSuchAlgorithmException {
|
||||||
RecordingRequest recReq = new RecordingRequest("rerunPostProcessing", recording);
|
RecordingRequest recReq = new RecordingRequest("rerunPostProcessing", recording);
|
||||||
String msg = recordingRequestAdapter.toJson(recReq);
|
String msg = recordingRequestAdapter.toJson(recReq);
|
||||||
LOG.debug(msg);
|
LOG.debug(msg);
|
||||||
RequestBody body = RequestBody.create(JSON, msg);
|
RequestBody body = RequestBody.create(JSON, msg);
|
||||||
Request.Builder builder = new Request.Builder()
|
Request.Builder builder = new Request.Builder().url(getRecordingEndpoint()).post(body);
|
||||||
.url(getRecordingEndpoint())
|
|
||||||
.post(body);
|
|
||||||
addHmacIfNeeded(msg, builder);
|
addHmacIfNeeded(msg, builder);
|
||||||
Request request = builder.build();
|
Request request = builder.build();
|
||||||
try (Response response = client.execute(request)) {
|
try (Response response = client.execute(request)) {
|
||||||
String json = response.body().string();
|
String json = response.body().string();
|
||||||
SimpleResponse resp = simpleResponseAdapter.fromJson(json);
|
SimpleResponse resp = simpleResponseAdapter.fromJson(json);
|
||||||
if (response.isSuccessful()) {
|
if (response.isSuccessful()) {
|
||||||
if (!resp.status.equals("success")) {
|
if (!resp.status.equals(SUCCESS)) {
|
||||||
throw new IOException("Couldn't start post-processing for recording: " + resp.msg);
|
throw new IOException("Couldn't start post-processing for recording: " + resp.msg);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -47,7 +47,7 @@ import okhttp3.Response;
|
||||||
|
|
||||||
public abstract class AbstractHlsDownload implements Download {
|
public abstract class AbstractHlsDownload implements Download {
|
||||||
|
|
||||||
private static final transient Logger LOG = LoggerFactory.getLogger(AbstractHlsDownload.class);
|
private static final Logger LOG = LoggerFactory.getLogger(AbstractHlsDownload.class);
|
||||||
private static int threadCounter = 0;
|
private static int threadCounter = 0;
|
||||||
|
|
||||||
protected HttpClient client;
|
protected HttpClient client;
|
||||||
|
@ -103,9 +103,9 @@ public abstract class AbstractHlsDownload implements Download {
|
||||||
for (TrackData trackData : tracks) {
|
for (TrackData trackData : tracks) {
|
||||||
String uri = trackData.getUri();
|
String uri = trackData.getUri();
|
||||||
if(!uri.startsWith("http")) {
|
if(!uri.startsWith("http")) {
|
||||||
String _url = segmentsUrl.toString();
|
String tmpurl = segmentsUrl.toString();
|
||||||
_url = _url.substring(0, _url.lastIndexOf('/') + 1);
|
tmpurl = tmpurl.substring(0, tmpurl.lastIndexOf('/') + 1);
|
||||||
uri = _url + uri;
|
uri = tmpurl + uri;
|
||||||
}
|
}
|
||||||
lsp.totalDuration += trackData.getTrackInfo().duration;
|
lsp.totalDuration += trackData.getTrackInfo().duration;
|
||||||
lsp.lastSegDuration = trackData.getTrackInfo().duration;
|
lsp.lastSegDuration = trackData.getTrackInfo().duration;
|
||||||
|
@ -168,6 +168,7 @@ public abstract class AbstractHlsDownload implements Download {
|
||||||
try {
|
try {
|
||||||
Thread.sleep(6000);
|
Thread.sleep(6000);
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
playlistEmptyCount = 0;
|
playlistEmptyCount = 0;
|
||||||
|
@ -209,7 +210,9 @@ public abstract class AbstractHlsDownload implements Download {
|
||||||
getModel().getSite().getName(),
|
getModel().getSite().getName(),
|
||||||
Long.toString(recording.getStartDate().getEpochSecond())
|
Long.toString(recording.getStartDate().getEpochSecond())
|
||||||
};
|
};
|
||||||
LOG.debug("Running {}", Arrays.toString(args));
|
if(LOG.isDebugEnabled()) {
|
||||||
|
LOG.debug("Running {}", Arrays.toString(args));
|
||||||
|
}
|
||||||
Process process = rt.exec(args, OS.getEnvironment());
|
Process process = rt.exec(args, OS.getEnvironment());
|
||||||
// TODO maybe write these to a separate log file, e.g. recname.ts.pp.log
|
// TODO maybe write these to a separate log file, e.g. recname.ts.pp.log
|
||||||
Thread std = new Thread(new StreamRedirectThread(process.getInputStream(), System.out));
|
Thread std = new Thread(new StreamRedirectThread(process.getInputStream(), System.out));
|
||||||
|
|
|
@ -10,17 +10,14 @@ import java.io.InputStream;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.nio.file.FileSystems;
|
import java.nio.file.FileSystems;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.LinkOption;
|
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.text.DecimalFormat;
|
import java.text.DecimalFormat;
|
||||||
import java.text.NumberFormat;
|
import java.text.NumberFormat;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.time.Instant;
|
|
||||||
import java.time.ZoneId;
|
import java.time.ZoneId;
|
||||||
import java.time.ZonedDateTime;
|
import java.time.ZonedDateTime;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
import java.util.concurrent.Callable;
|
import java.util.concurrent.Callable;
|
||||||
import java.util.concurrent.Future;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
@ -51,7 +48,7 @@ import okhttp3.Response;
|
||||||
|
|
||||||
public class HlsDownload extends AbstractHlsDownload {
|
public class HlsDownload extends AbstractHlsDownload {
|
||||||
|
|
||||||
private static final transient Logger LOG = LoggerFactory.getLogger(HlsDownload.class);
|
private static final Logger LOG = LoggerFactory.getLogger(HlsDownload.class);
|
||||||
|
|
||||||
protected Path downloadDir;
|
protected Path downloadDir;
|
||||||
|
|
||||||
|
@ -69,7 +66,6 @@ public class HlsDownload extends AbstractHlsDownload {
|
||||||
public void init(Config config, Model model) {
|
public void init(Config config, Model model) {
|
||||||
this.config = config;
|
this.config = config;
|
||||||
super.model = model;
|
super.model = model;
|
||||||
startTime = Instant.now();
|
|
||||||
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(Config.RECORDING_DATE_FORMAT);
|
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(Config.RECORDING_DATE_FORMAT);
|
||||||
String startTime = formatter.format(ZonedDateTime.ofInstant(this.startTime, ZoneId.systemDefault()));
|
String startTime = 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());
|
||||||
|
@ -88,7 +84,7 @@ public class HlsDownload extends AbstractHlsDownload {
|
||||||
|
|
||||||
String segments = getSegmentPlaylistUrl(model);
|
String segments = getSegmentPlaylistUrl(model);
|
||||||
if(segments != null) {
|
if(segments != null) {
|
||||||
if (!Files.exists(downloadDir, LinkOption.NOFOLLOW_LINKS)) {
|
if (!downloadDir.toFile().exists()) {
|
||||||
Files.createDirectories(downloadDir);
|
Files.createDirectories(downloadDir);
|
||||||
}
|
}
|
||||||
int lastSegmentNumber = 0;
|
int lastSegmentNumber = 0;
|
||||||
|
@ -103,7 +99,6 @@ public class HlsDownload extends AbstractHlsDownload {
|
||||||
LOG.warn("Missed segments {} < {} in download for {} - setting wait factor to 1/{}", nextSegmentNumber, playlist.seq, model, waitFactor);
|
LOG.warn("Missed segments {} < {} in download for {} - setting wait factor to 1/{}", nextSegmentNumber, playlist.seq, model, waitFactor);
|
||||||
}
|
}
|
||||||
int skip = nextSegmentNumber - playlist.seq;
|
int skip = nextSegmentNumber - playlist.seq;
|
||||||
Future<Boolean> lastSegmentDownload = null;
|
|
||||||
for (String segment : playlist.segments) {
|
for (String segment : playlist.segments) {
|
||||||
if(skip > 0) {
|
if(skip > 0) {
|
||||||
skip--;
|
skip--;
|
||||||
|
@ -111,13 +106,12 @@ public class HlsDownload extends AbstractHlsDownload {
|
||||||
URL segmentUrl = new URL(segment);
|
URL segmentUrl = new URL(segment);
|
||||||
String prefix = nf.format(segmentCounter++);
|
String prefix = nf.format(segmentCounter++);
|
||||||
SegmentDownload segmentDownload = new SegmentDownload(playlist, segmentUrl, downloadDir, client, prefix);
|
SegmentDownload segmentDownload = new SegmentDownload(playlist, segmentUrl, downloadDir, client, prefix);
|
||||||
lastSegmentDownload = downloadThreadPool.submit(segmentDownload);
|
downloadThreadPool.submit(segmentDownload);
|
||||||
//new SegmentDownload(segment, downloadDir).call();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// split recordings
|
// split recordings
|
||||||
boolean split = splitRecording(lastSegmentDownload);
|
boolean split = splitRecording();
|
||||||
if (split) {
|
if (split) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -129,13 +123,14 @@ public class HlsDownload extends AbstractHlsDownload {
|
||||||
LOG.trace("Playlist didn't change... waiting for {}ms", wait);
|
LOG.trace("Playlist didn't change... waiting for {}ms", wait);
|
||||||
} else {
|
} else {
|
||||||
// playlist did change -> wait for at least last segment duration
|
// playlist did change -> wait for at least last segment duration
|
||||||
wait = 1;//(long) lsp.lastSegDuration * 1000;
|
wait = 1;
|
||||||
LOG.trace("Playlist changed... waiting for {}ms", wait);
|
LOG.trace("Playlist changed... waiting for {}ms", wait);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Thread.sleep(wait);
|
Thread.sleep(wait);
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
if(running) {
|
if(running) {
|
||||||
LOG.error("Couldn't sleep between segment downloads. This might mess up the download!");
|
LOG.error("Couldn't sleep between segment downloads. This might mess up the download!");
|
||||||
}
|
}
|
||||||
|
@ -175,7 +170,9 @@ public class HlsDownload extends AbstractHlsDownload {
|
||||||
try {
|
try {
|
||||||
LOG.debug("Waiting for last segments for {}", model);
|
LOG.debug("Waiting for last segments for {}", model);
|
||||||
downloadThreadPool.awaitTermination(60, TimeUnit.SECONDS);
|
downloadThreadPool.awaitTermination(60, TimeUnit.SECONDS);
|
||||||
} catch (InterruptedException e) {}
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
}
|
||||||
synchronized (downloadFinished) {
|
synchronized (downloadFinished) {
|
||||||
downloadFinished.notifyAll();
|
downloadFinished.notifyAll();
|
||||||
}
|
}
|
||||||
|
@ -198,7 +195,7 @@ public class HlsDownload extends AbstractHlsDownload {
|
||||||
}
|
}
|
||||||
|
|
||||||
PlaylistGenerator playlistGenerator = new PlaylistGenerator();
|
PlaylistGenerator playlistGenerator = new PlaylistGenerator();
|
||||||
playlistGenerator.addProgressListener(percent -> recording.setProgress(percent));
|
playlistGenerator.addProgressListener(recording::setProgress);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
File playlist = playlistGenerator.generate(recDir);
|
File playlist = playlistGenerator.generate(recDir);
|
||||||
|
@ -214,17 +211,21 @@ public class HlsDownload extends AbstractHlsDownload {
|
||||||
} else {
|
} else {
|
||||||
LOG.error("Playlist contains errors");
|
LOG.error("Playlist contains errors");
|
||||||
for (PlaylistError error : e.getErrors()) {
|
for (PlaylistError error : e.getErrors()) {
|
||||||
LOG.error("Error: {}", error.toString());
|
LOG.error("Error: {}", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (InvalidPlaylistException e) {
|
} catch (InvalidPlaylistException e) {
|
||||||
LOG.error("Playlist is invalid and will be deleted", e);
|
LOG.error("Playlist is invalid and will be deleted", e);
|
||||||
File playlist = new File(recDir, "playlist.m3u8");
|
File playlist = new File(recDir, "playlist.m3u8");
|
||||||
playlist.delete();
|
try {
|
||||||
|
Files.delete(playlist.toPath());
|
||||||
|
} catch (IOException e1) {
|
||||||
|
LOG.error("Couldn't delete playlist {}", playlist, e1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean splitRecording(Future<Boolean> lastSegmentDownload) {
|
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();
|
||||||
|
@ -237,14 +238,15 @@ public class HlsDownload extends AbstractHlsDownload {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public synchronized void stop() {
|
public void stop() {
|
||||||
if (running) {
|
if (running) {
|
||||||
internalStop();
|
internalStop();
|
||||||
try {
|
try {
|
||||||
synchronized (downloadFinished) {
|
synchronized (downloadFinished) {
|
||||||
downloadFinished.wait();
|
downloadFinished.wait(TimeUnit.SECONDS.toMillis(60));
|
||||||
}
|
}
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
LOG.error("Couldn't wait for download to finish", e);
|
LOG.error("Couldn't wait for download to finish", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -271,7 +273,7 @@ public class HlsDownload extends AbstractHlsDownload {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Boolean call() throws Exception {
|
public Boolean call() throws Exception {
|
||||||
LOG.trace("Downloading segment to " + file);
|
LOG.trace("Downloading segment to {}", file);
|
||||||
int maxTries = 3;
|
int maxTries = 3;
|
||||||
for (int i = 1; i <= maxTries; i++) {
|
for (int i = 1; i <= maxTries; i++) {
|
||||||
Request request = new Request.Builder().url(url).addHeader("connection", "keep-alive").build();
|
Request request = new Request.Builder().url(url).addHeader("connection", "keep-alive").build();
|
||||||
|
@ -357,6 +359,8 @@ public class HlsDownload extends AbstractHlsDownload {
|
||||||
private void waitSomeTime() {
|
private void waitSomeTime() {
|
||||||
try {
|
try {
|
||||||
Thread.sleep(10_000);
|
Thread.sleep(10_000);
|
||||||
} catch (Exception e) {}
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,6 @@ import java.net.MalformedURLException;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.nio.channels.FileChannel;
|
import java.nio.channels.FileChannel;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.LinkOption;
|
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.security.InvalidKeyException;
|
import java.security.InvalidKeyException;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
@ -50,7 +49,7 @@ import okhttp3.Response;
|
||||||
|
|
||||||
public class MergedHlsDownload extends AbstractHlsDownload {
|
public class MergedHlsDownload extends AbstractHlsDownload {
|
||||||
|
|
||||||
private static final transient Logger LOG = LoggerFactory.getLogger(MergedHlsDownload.class);
|
private static final Logger LOG = LoggerFactory.getLogger(MergedHlsDownload.class);
|
||||||
private static final boolean IGNORE_CACHE = true;
|
private static final boolean IGNORE_CACHE = true;
|
||||||
private BlockingMultiMTSSource multiSource;
|
private BlockingMultiMTSSource multiSource;
|
||||||
private Thread mergeThread;
|
private Thread mergeThread;
|
||||||
|
@ -59,7 +58,7 @@ public class MergedHlsDownload extends AbstractHlsDownload {
|
||||||
private Config config;
|
private Config config;
|
||||||
private File targetFile;
|
private File targetFile;
|
||||||
private FileChannel fileChannel = null;
|
private FileChannel fileChannel = null;
|
||||||
private Object downloadFinished = new Object();
|
private boolean downloadFinished = false;
|
||||||
|
|
||||||
public MergedHlsDownload(HttpClient client) {
|
public MergedHlsDownload(HttpClient client) {
|
||||||
super(client);
|
super(client);
|
||||||
|
@ -85,11 +84,11 @@ public class MergedHlsDownload extends AbstractHlsDownload {
|
||||||
mergeThread = createMergeThread(targetFile, progressListener, false);
|
mergeThread = createMergeThread(targetFile, progressListener, false);
|
||||||
LOG.debug("Merge thread started");
|
LOG.debug("Merge thread started");
|
||||||
mergeThread.start();
|
mergeThread.start();
|
||||||
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();
|
||||||
byte[] key = Config.getInstance().getSettings().key;
|
byte[] key = Config.getInstance().getSettings().key;
|
||||||
if(!Config.getInstance().getContextPath().isEmpty()) {
|
if (!Config.getInstance().getContextPath().isEmpty()) {
|
||||||
path = path.substring(Config.getInstance().getContextPath().length());
|
path = path.substring(Config.getInstance().getContextPath().length());
|
||||||
}
|
}
|
||||||
String hmac = Hmac.calculate(path, key);
|
String hmac = Hmac.calculate(path, key);
|
||||||
|
@ -100,28 +99,29 @@ public class MergedHlsDownload extends AbstractHlsDownload {
|
||||||
LOG.debug("Waiting for merge thread to finish");
|
LOG.debug("Waiting for merge thread to finish");
|
||||||
mergeThread.join();
|
mergeThread.join();
|
||||||
LOG.debug("Merge thread finished");
|
LOG.debug("Merge thread finished");
|
||||||
} catch(ParseException e) {
|
} catch (ParseException e) {
|
||||||
throw new IOException("Couldn't parse stream information", e);
|
throw new IOException("Couldn't parse stream information", e);
|
||||||
} catch(PlaylistException e) {
|
} catch (PlaylistException e) {
|
||||||
throw new IOException("Couldn't parse HLS playlist", e);
|
throw new IOException("Couldn't parse HLS playlist", e);
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
throw new IOException("Couldn't wait for write thread to finish. Recording might be cut off", e);
|
throw new IOException("Couldn't wait for write thread to finish. Recording might be cut off", e);
|
||||||
} catch (InvalidKeyException | NoSuchAlgorithmException | IllegalStateException e) {
|
} catch (InvalidKeyException | NoSuchAlgorithmException | IllegalStateException e) {
|
||||||
throw new IOException("Couldn't add HMAC to playlist url", e);
|
throw new IOException("Couldn't add HMAC to playlist url", e);
|
||||||
} finally {
|
} finally {
|
||||||
try {
|
try {
|
||||||
streamer.stop();
|
streamer.stop();
|
||||||
} catch(Exception e) {
|
} catch (Exception e) {
|
||||||
LOG.error("Couldn't stop streamer", e);
|
LOG.error("Couldn't stop streamer", e);
|
||||||
}
|
}
|
||||||
downloadThreadPool.shutdown();
|
downloadThreadPool.shutdown();
|
||||||
try {
|
try {
|
||||||
LOG.debug("Waiting for last segments for {}", model);
|
LOG.debug("Waiting for last segments for {}", model);
|
||||||
downloadThreadPool.awaitTermination(60, TimeUnit.SECONDS);
|
downloadThreadPool.awaitTermination(60, TimeUnit.SECONDS);
|
||||||
} catch (InterruptedException e) {}
|
} catch (InterruptedException e) {
|
||||||
synchronized (downloadFinished) {
|
Thread.currentThread().interrupt();
|
||||||
downloadFinished.notifyAll();
|
|
||||||
}
|
}
|
||||||
|
downloadFinished = true;
|
||||||
LOG.debug("Download terminated for {}", segmentPlaylistUri);
|
LOG.debug("Download terminated for {}", segmentPlaylistUri);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -129,14 +129,13 @@ public class MergedHlsDownload extends AbstractHlsDownload {
|
||||||
@Override
|
@Override
|
||||||
public void start() throws IOException {
|
public void start() throws IOException {
|
||||||
try {
|
try {
|
||||||
if(!model.isOnline(IGNORE_CACHE)) {
|
if (!model.isOnline(IGNORE_CACHE)) {
|
||||||
throw new IOException(model.getName() +"'s room is not public");
|
throw new IOException(model.getName() + "'s room is not public");
|
||||||
}
|
}
|
||||||
|
|
||||||
running = true;
|
running = true;
|
||||||
super.startTime = Instant.now();
|
super.startTime = Instant.now();
|
||||||
splitRecStartTime = ZonedDateTime.now();
|
splitRecStartTime = ZonedDateTime.now();
|
||||||
super.model = model;
|
|
||||||
|
|
||||||
String segments = getSegmentPlaylistUrl(model);
|
String segments = getSegmentPlaylistUrl(model);
|
||||||
mergeThread = createMergeThread(targetFile, null, true);
|
mergeThread = createMergeThread(targetFile, null, true);
|
||||||
|
@ -149,16 +148,17 @@ public class MergedHlsDownload extends AbstractHlsDownload {
|
||||||
} else {
|
} else {
|
||||||
throw new IOException("Couldn't determine segments uri");
|
throw new IOException("Couldn't determine segments uri");
|
||||||
}
|
}
|
||||||
} catch(ParseException e) {
|
} catch (ParseException e) {
|
||||||
throw new IOException("Couldn't parse stream information", e);
|
throw new IOException("Couldn't parse stream information", e);
|
||||||
} catch(PlaylistException e) {
|
} catch (PlaylistException e) {
|
||||||
throw new IOException("Couldn't parse HLS playlist", e);
|
throw new IOException("Couldn't parse HLS playlist", e);
|
||||||
} catch(EOFException e) {
|
} catch (EOFException e) {
|
||||||
// end of playlist reached
|
// end of playlist reached
|
||||||
LOG.debug("Reached end of playlist for model {}", model);
|
LOG.debug("Reached end of playlist for model {}", model);
|
||||||
} catch(Exception e) {
|
} catch (Exception e) {
|
||||||
throw new IOException("Couldn't download segment", e);
|
throw new IOException("Couldn't download segment", e);
|
||||||
} finally {
|
} finally {
|
||||||
|
|
||||||
if (streamer != null) {
|
if (streamer != null) {
|
||||||
try {
|
try {
|
||||||
streamer.stop();
|
streamer.stop();
|
||||||
|
@ -170,11 +170,10 @@ public class MergedHlsDownload extends AbstractHlsDownload {
|
||||||
try {
|
try {
|
||||||
LOG.debug("Waiting for last segments for {}", model);
|
LOG.debug("Waiting for last segments for {}", model);
|
||||||
downloadThreadPool.awaitTermination(60, TimeUnit.SECONDS);
|
downloadThreadPool.awaitTermination(60, TimeUnit.SECONDS);
|
||||||
} catch (InterruptedException e) {}
|
} catch (InterruptedException e) {
|
||||||
synchronized (downloadFinished) {
|
Thread.currentThread().interrupt();
|
||||||
LOG.debug("Download finished notify {}", model);
|
|
||||||
downloadFinished.notifyAll();
|
|
||||||
}
|
}
|
||||||
|
downloadFinished = true;
|
||||||
LOG.debug("Download for {} terminated", model);
|
LOG.debug("Download for {} terminated", model);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -182,17 +181,17 @@ public class MergedHlsDownload extends AbstractHlsDownload {
|
||||||
private void downloadSegments(String segmentPlaylistUri, boolean livestreamDownload) throws IOException, ParseException, PlaylistException {
|
private void downloadSegments(String segmentPlaylistUri, boolean livestreamDownload) throws IOException, ParseException, PlaylistException {
|
||||||
int lastSegment = 0;
|
int lastSegment = 0;
|
||||||
int nextSegment = 0;
|
int nextSegment = 0;
|
||||||
while(running) {
|
while (running) {
|
||||||
try {
|
try {
|
||||||
SegmentPlaylist lsp = getNextSegments(segmentPlaylistUri);
|
SegmentPlaylist lsp = getNextSegments(segmentPlaylistUri);
|
||||||
emptyPlaylistCheck(lsp);
|
emptyPlaylistCheck(lsp);
|
||||||
if(!livestreamDownload) {
|
if (!livestreamDownload) {
|
||||||
multiSource.setTotalSegments(lsp.segments.size());
|
multiSource.setTotalSegments(lsp.segments.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
// download new segments
|
// download new segments
|
||||||
long downloadStart = System.currentTimeMillis();
|
long downloadStart = System.currentTimeMillis();
|
||||||
if(livestreamDownload) {
|
if (livestreamDownload) {
|
||||||
downloadNewSegments(lsp, nextSegment);
|
downloadNewSegments(lsp, nextSegment);
|
||||||
} else {
|
} else {
|
||||||
downloadRecording(lsp);
|
downloadRecording(lsp);
|
||||||
|
@ -200,12 +199,12 @@ public class MergedHlsDownload extends AbstractHlsDownload {
|
||||||
long downloadTookMillis = System.currentTimeMillis() - downloadStart;
|
long downloadTookMillis = System.currentTimeMillis() - downloadStart;
|
||||||
|
|
||||||
// download segments, which might have been skipped
|
// download segments, which might have been skipped
|
||||||
//downloadMissedSegments(lsp, nextSegment);
|
if (nextSegment > 0 && lsp.seq > nextSegment) {
|
||||||
if(nextSegment > 0 && lsp.seq > nextSegment) {
|
LOG.warn("Missed segments {} < {} in download for {}. Download took {}ms. Playlist is {}sec", nextSegment, lsp.seq, lsp.url,
|
||||||
LOG.warn("Missed segments {} < {} in download for {}. Download took {}ms. Playlist is {}sec", nextSegment, lsp.seq, lsp.url, downloadTookMillis, lsp.totalDuration);
|
downloadTookMillis, lsp.totalDuration);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(livestreamDownload) {
|
if (livestreamDownload) {
|
||||||
// split up the recording, if configured
|
// split up the recording, if configured
|
||||||
boolean split = splitRecording();
|
boolean split = splitRecording();
|
||||||
if (split) {
|
if (split) {
|
||||||
|
@ -220,16 +219,16 @@ public class MergedHlsDownload extends AbstractHlsDownload {
|
||||||
} else {
|
} else {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} catch(HttpException e) {
|
} catch (HttpException e) {
|
||||||
if(e.getResponseCode() == 404) {
|
if (e.getResponseCode() == 404) {
|
||||||
LOG.debug("Playlist not found (404). Model {} probably went offline", model);
|
LOG.debug("Playlist not found (404). Model {} probably went offline", model);
|
||||||
} else if(e.getResponseCode() == 403) {
|
} else if (e.getResponseCode() == 403) {
|
||||||
LOG.debug("Playlist access forbidden (403). Model {} probably went private or offline", model);
|
LOG.debug("Playlist access forbidden (403). Model {} probably went private or offline", model);
|
||||||
} else {
|
} else {
|
||||||
LOG.info("Unexpected error while downloading {}", model, e);
|
LOG.info("Unexpected error while downloading {}", model, e);
|
||||||
}
|
}
|
||||||
running = false;
|
running = false;
|
||||||
} catch(Exception e) {
|
} catch (Exception e) {
|
||||||
LOG.info("Unexpected error while downloading {}", model, e);
|
LOG.info("Unexpected error while downloading {}", model, e);
|
||||||
running = false;
|
running = false;
|
||||||
}
|
}
|
||||||
|
@ -245,19 +244,19 @@ public class MergedHlsDownload extends AbstractHlsDownload {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void downloadNewSegments(SegmentPlaylist lsp, int nextSegment) throws MalformedURLException, MissingSegmentException, ExecutionException, HttpException {
|
private void downloadNewSegments(SegmentPlaylist lsp, int nextSegment) throws MalformedURLException, ExecutionException, HttpException {
|
||||||
int skip = nextSegment - lsp.seq;
|
int skip = nextSegment - lsp.seq;
|
||||||
|
|
||||||
// add segments to download threadpool
|
// add segments to download threadpool
|
||||||
Queue<Future<byte[]>> downloads = new LinkedList<>();
|
Queue<Future<byte[]>> downloads = new LinkedList<>();
|
||||||
if(downloadQueue.remainingCapacity() == 0) {
|
if (downloadQueue.remainingCapacity() == 0) {
|
||||||
LOG.warn("Download to slow for this stream. Download queue is full. Skipping segment");
|
LOG.warn("Download to slow for this stream. Download queue is full. Skipping segment");
|
||||||
} else {
|
} else {
|
||||||
for (String segment : lsp.segments) {
|
for (String segment : lsp.segments) {
|
||||||
if(!running) {
|
if (!running) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if(skip > 0) {
|
if (skip > 0) {
|
||||||
skip--;
|
skip--;
|
||||||
} else {
|
} else {
|
||||||
URL segmentUrl = new URL(segment);
|
URL segmentUrl = new URL(segment);
|
||||||
|
@ -278,27 +277,28 @@ public class MergedHlsDownload extends AbstractHlsDownload {
|
||||||
byte[] segmentData = downloadFuture.get();
|
byte[] segmentData = downloadFuture.get();
|
||||||
writeSegment(segmentData);
|
writeSegment(segmentData);
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
LOG.error("Error while downloading segment", e);
|
LOG.error("Error while downloading segment", e);
|
||||||
} catch (ExecutionException e) {
|
} catch (ExecutionException e) {
|
||||||
Throwable cause = e.getCause();
|
Throwable cause = e.getCause();
|
||||||
if(cause instanceof MissingSegmentException) {
|
if (cause instanceof MissingSegmentException) {
|
||||||
if(model != null && !isModelOnline()) {
|
if (model != null && !isModelOnline()) {
|
||||||
LOG.debug("Error while downloading segment, because model {} is offline. Stopping now", model.getName());
|
LOG.debug("Error while downloading segment, because model {} is offline. Stopping now", model.getName());
|
||||||
running = false;
|
running = false;
|
||||||
} else {
|
} else {
|
||||||
LOG.debug("Segment not available, but model {} still online. Going on", model.getName());
|
LOG.debug("Segment not available, but model {} still online. Going on", Optional.ofNullable(model).map(Model::getName).orElse("n/a"));
|
||||||
}
|
}
|
||||||
} else if(cause instanceof HttpException) {
|
} else if (cause instanceof HttpException) {
|
||||||
HttpException he = (HttpException) cause;
|
HttpException he = (HttpException) cause;
|
||||||
if(model != null && !isModelOnline()) {
|
if (model != null && !isModelOnline()) {
|
||||||
LOG.debug("Error {} while downloading segment, because model {} is offline. Stopping now", he.getResponseCode(), model.getName());
|
LOG.debug("Error {} while downloading segment, because model {} is offline. Stopping now", he.getResponseCode(), model.getName());
|
||||||
running = false;
|
running = false;
|
||||||
} else {
|
} else {
|
||||||
if(he.getResponseCode() == 404) {
|
if (he.getResponseCode() == 404) {
|
||||||
LOG.info("Playlist for {} not found [HTTP 404]. Stopping now", model.getName());
|
LOG.info("Playlist for {} not found [HTTP 404]. Stopping now", Optional.ofNullable(model).map(Model::getName).orElse("n/a"));
|
||||||
running = false;
|
running = false;
|
||||||
} else if(he.getResponseCode() == 403) {
|
} else if (he.getResponseCode() == 403) {
|
||||||
LOG.info("Playlist for {} not accessible [HTTP 403]. Stopping now", model.getName());
|
LOG.info("Playlist for {} not accessible [HTTP 403]. Stopping now", Optional.ofNullable(model).map(Model::getName).orElse("n/a"));
|
||||||
running = false;
|
running = false;
|
||||||
} else {
|
} else {
|
||||||
throw he;
|
throw he;
|
||||||
|
@ -318,33 +318,12 @@ public class MergedHlsDownload extends AbstractHlsDownload {
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
// try {
|
|
||||||
// File lastTargetFile = targetFile;
|
|
||||||
//
|
|
||||||
// // switch to the next file
|
|
||||||
// targetFile = Config.getInstance().getFileForRecording(model, "ts");
|
|
||||||
// LOG.debug("Switching to file {}", targetFile.getAbsolutePath());
|
|
||||||
// fileChannel = FileChannel.open(targetFile.toPath(), CREATE, WRITE);
|
|
||||||
// MTSSink sink = ByteChannelSink.builder().setByteChannel(fileChannel).build();
|
|
||||||
// streamer.switchSink(sink);
|
|
||||||
// super.startTime = Instant.now();
|
|
||||||
// splitRecStartTime = ZonedDateTime.now();
|
|
||||||
//
|
|
||||||
// // post-process current recording
|
|
||||||
// Thread pp = new Thread(() -> postprocess(lastTargetFile));
|
|
||||||
// pp.setName("Post-Processing split recording");
|
|
||||||
// pp.setPriority(Thread.MIN_PRIORITY);
|
|
||||||
// pp.start();
|
|
||||||
// } catch (IOException e) {
|
|
||||||
// LOG.error("Error while splitting recording", e);
|
|
||||||
// running = false;
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
@ -354,8 +333,8 @@ public class MergedHlsDownload extends AbstractHlsDownload {
|
||||||
try {
|
try {
|
||||||
long wait = 0;
|
long wait = 0;
|
||||||
if (lastSegment == lsp.seq) {
|
if (lastSegment == lsp.seq) {
|
||||||
int timeLeftMillis = (int)(lsp.totalDuration * 1000 - downloadTookMillis);
|
int timeLeftMillis = (int) (lsp.totalDuration * 1000 - downloadTookMillis);
|
||||||
if(timeLeftMillis < 3000) { // we have less than 3 seconds to get the new playlist and start downloading it
|
if (timeLeftMillis < 3000) { // we have less than 3 seconds to get the new playlist and start downloading it
|
||||||
wait = 1;
|
wait = 1;
|
||||||
} else {
|
} else {
|
||||||
// wait a second to be nice to the server (don't hammer it with requests)
|
// wait a second to be nice to the server (don't hammer it with requests)
|
||||||
|
@ -365,11 +344,12 @@ public class MergedHlsDownload extends AbstractHlsDownload {
|
||||||
LOG.trace("Playlist didn't change... waiting for {}ms", wait);
|
LOG.trace("Playlist didn't change... waiting for {}ms", wait);
|
||||||
} else {
|
} else {
|
||||||
// playlist did change -> wait for at least last segment duration
|
// playlist did change -> wait for at least last segment duration
|
||||||
wait = 1;// (long) lsp.lastSegDuration * 1000;
|
wait = 1;
|
||||||
LOG.trace("Playlist changed... waiting for {}ms", wait);
|
LOG.trace("Playlist changed... waiting for {}ms", wait);
|
||||||
}
|
}
|
||||||
Thread.sleep(wait);
|
Thread.sleep(wait);
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
if (running) {
|
if (running) {
|
||||||
LOG.error("Couldn't sleep between segment downloads. This might mess up the download!");
|
LOG.error("Couldn't sleep between segment downloads. This might mess up the download!");
|
||||||
}
|
}
|
||||||
|
@ -377,15 +357,20 @@ public class MergedHlsDownload extends AbstractHlsDownload {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public synchronized void stop() {
|
public void stop() {
|
||||||
if (running) {
|
if (running) {
|
||||||
internalStop();
|
|
||||||
try {
|
try {
|
||||||
synchronized (downloadFinished) {
|
internalStop();
|
||||||
LOG.debug("Waiting for finished notify {}", model);
|
int count = 0;
|
||||||
downloadFinished.wait();
|
while (!downloadFinished && count++ < 60) {
|
||||||
|
LOG.debug("Waiting for download to finish {}", model);
|
||||||
|
Thread.sleep(1000);
|
||||||
|
}
|
||||||
|
if(!downloadFinished) {
|
||||||
|
LOG.warn("Download didn't finishe properly for model {}", model);
|
||||||
}
|
}
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
LOG.error("Couldn't wait for download to finish", e);
|
LOG.error("Couldn't wait for download to finish", e);
|
||||||
}
|
}
|
||||||
LOG.debug("Download stopped");
|
LOG.debug("Download stopped");
|
||||||
|
@ -393,7 +378,7 @@ public class MergedHlsDownload extends AbstractHlsDownload {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
void internalStop() {
|
synchronized void internalStop() {
|
||||||
running = false;
|
running = false;
|
||||||
if (streamer != null) {
|
if (streamer != null) {
|
||||||
streamer.stop();
|
streamer.stop();
|
||||||
|
@ -403,31 +388,24 @@ public class MergedHlsDownload extends AbstractHlsDownload {
|
||||||
|
|
||||||
private Thread createMergeThread(File targetFile, ProgressListener listener, boolean liveStream) {
|
private Thread createMergeThread(File targetFile, ProgressListener listener, boolean liveStream) {
|
||||||
Thread t = new Thread(() -> {
|
Thread t = new Thread(() -> {
|
||||||
multiSource = BlockingMultiMTSSource.builder()
|
multiSource = BlockingMultiMTSSource.builder().setFixContinuity(true).setProgressListener(listener).build();
|
||||||
.setFixContinuity(true)
|
|
||||||
.setProgressListener(listener)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Path downloadDir = targetFile.getParentFile().toPath();
|
Path downloadDir = targetFile.getParentFile().toPath();
|
||||||
if (!Files.exists(downloadDir, LinkOption.NOFOLLOW_LINKS)) {
|
if (!downloadDir.toFile().exists()) {
|
||||||
Files.createDirectories(downloadDir);
|
Files.createDirectories(downloadDir);
|
||||||
}
|
}
|
||||||
fileChannel = FileChannel.open(targetFile.toPath(), CREATE, WRITE);
|
fileChannel = FileChannel.open(targetFile.toPath(), CREATE, WRITE);
|
||||||
MTSSink sink = ByteChannelSink.builder().setByteChannel(fileChannel).build();
|
MTSSink sink = ByteChannelSink.builder().setByteChannel(fileChannel).build();
|
||||||
|
|
||||||
streamer = Streamer.builder()
|
streamer = Streamer.builder().setSource(multiSource).setSink(sink).setSleepingEnabled(liveStream).setBufferSize(10)
|
||||||
.setSource(multiSource)
|
.setName(Optional.ofNullable(model).map(Model::getName).orElse("")).build();
|
||||||
.setSink(sink)
|
|
||||||
.setSleepingEnabled(liveStream)
|
|
||||||
.setBufferSize(10)
|
|
||||||
.setName(Optional.ofNullable(model).map(m -> m.getName()).orElse(""))
|
|
||||||
.build();
|
|
||||||
|
|
||||||
// Start streaming
|
// Start streaming
|
||||||
streamer.stream();
|
streamer.stream();
|
||||||
LOG.debug("Streamer finished");
|
LOG.debug("Streamer finished");
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
if (running) {
|
if (running) {
|
||||||
LOG.error("Error while waiting for a download future", e);
|
LOG.error("Error while waiting for a download future", e);
|
||||||
}
|
}
|
||||||
|
@ -478,27 +456,27 @@ public class MergedHlsDownload extends AbstractHlsDownload {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public byte[] call() throws IOException {
|
public byte[] call() throws IOException {
|
||||||
LOG.trace("Downloading segment " + url.getFile());
|
LOG.trace("Downloading segment {}", url.getFile());
|
||||||
int maxTries = 3;
|
int maxTries = 3;
|
||||||
for (int i = 1; i <= maxTries && running; i++) {
|
for (int i = 1; i <= maxTries && running; i++) {
|
||||||
Request request = new Request.Builder().url(url).addHeader("connection", "keep-alive").build();
|
Request request = new Request.Builder().url(url).addHeader("connection", "keep-alive").build();
|
||||||
try (Response response = client.execute(request)) {
|
try (Response response = client.execute(request)) {
|
||||||
if(response.isSuccessful()) {
|
if (response.isSuccessful()) {
|
||||||
byte[] segment = response.body().bytes();
|
byte[] segment = response.body().bytes();
|
||||||
if(lsp.encrypted) {
|
if (lsp.encrypted) {
|
||||||
segment = new Crypto(lsp.encryptionKeyUrl, client).decrypt(segment);
|
segment = new Crypto(lsp.encryptionKeyUrl, client).decrypt(segment);
|
||||||
}
|
}
|
||||||
return segment;
|
return segment;
|
||||||
} else {
|
} else {
|
||||||
throw new HttpException(response.code(), response.message());
|
throw new HttpException(response.code(), response.message());
|
||||||
}
|
}
|
||||||
} catch(Exception e) {
|
} catch (Exception e) {
|
||||||
if (i == maxTries) {
|
if (i == maxTries) {
|
||||||
LOG.error("Error while downloading segment. Segment {} finally failed", url.getFile());
|
LOG.error("Error while downloading segment. Segment {} finally failed", url.getFile());
|
||||||
} else {
|
} else {
|
||||||
LOG.trace("Error while downloading segment {} on try {}", url.getFile(), i, e);
|
LOG.trace("Error while downloading segment {} on try {}", url.getFile(), i, e);
|
||||||
}
|
}
|
||||||
if(model != null && !isModelOnline()) {
|
if (model != null && !isModelOnline()) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -510,7 +488,10 @@ public class MergedHlsDownload extends AbstractHlsDownload {
|
||||||
public boolean isModelOnline() {
|
public boolean isModelOnline() {
|
||||||
try {
|
try {
|
||||||
return model.isOnline(IGNORE_CACHE);
|
return model.isOnline(IGNORE_CACHE);
|
||||||
} catch (IOException | ExecutionException | InterruptedException e) {
|
} catch (IOException | ExecutionException e) {
|
||||||
|
return false;
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -107,13 +107,10 @@ public class Streamer {
|
||||||
boolean resetState = false;
|
boolean resetState = false;
|
||||||
MTSPacket packet = null;
|
MTSPacket packet = null;
|
||||||
long packetCount = 0;
|
long packetCount = 0;
|
||||||
//long pcrPidPacketCount = 0;
|
|
||||||
Long firstPcrValue = null;
|
Long firstPcrValue = null;
|
||||||
Long firstPcrTime = null;
|
Long firstPcrTime = null;
|
||||||
//Long firstPcrPacketCount = null;
|
|
||||||
Long lastPcrValue = null;
|
Long lastPcrValue = null;
|
||||||
Long lastPcrTime = null;
|
Long lastPcrTime = null;
|
||||||
//Long lastPcrPacketCount = null;
|
|
||||||
Long averageSleep = null;
|
Long averageSleep = null;
|
||||||
while (!streamingShouldStop) {
|
while (!streamingShouldStop) {
|
||||||
if (resetState) {
|
if (resetState) {
|
||||||
|
@ -145,6 +142,7 @@ public class Streamer {
|
||||||
log.error("Interrupted while waiting for packet");
|
log.error("Interrupted while waiting for packet");
|
||||||
continue;
|
continue;
|
||||||
} else {
|
} else {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -164,100 +162,78 @@ public class Streamer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pid != 0 && patSection!=null) {
|
if (pid != 0 && patSection!=null && patSection.getPrograms().values().contains(pid) && packet.isPayloadUnitStartIndicator()) {
|
||||||
if (patSection.getPrograms().values().contains(pid)) {
|
ByteBuffer payload = packet.getPayload();
|
||||||
if (packet.isPayloadUnitStartIndicator()) {
|
payload.rewind();
|
||||||
ByteBuffer payload = packet.getPayload();
|
int pointer = payload.get() & 0xff;
|
||||||
payload.rewind();
|
payload.position(payload.position() + pointer);
|
||||||
int pointer = payload.get() & 0xff;
|
pmtSection.put(pid, PMTSection.parse(payload));
|
||||||
payload.position(payload.position() + pointer);
|
|
||||||
pmtSection.put(pid, PMTSection.parse(payload));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check PID matches PCR PID
|
// Check PID matches PCR PID
|
||||||
if (true) {//mtsPacket.pid == pmt.getPcrPid()) {
|
if (averageSleep != null) {
|
||||||
//pcrPidPacketCount++;
|
sleepNanos = averageSleep;
|
||||||
|
|
||||||
if (averageSleep != null) {
|
|
||||||
sleepNanos = averageSleep;
|
|
||||||
} else {
|
|
||||||
// if (pcrPidPacketCount < 2) {
|
|
||||||
// if (pcrPidPacketCount % 10 == 0) {
|
|
||||||
// sleepNanos = 15;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for PCR
|
// Check for PCR
|
||||||
if (packet.getAdaptationField() != null) {
|
if (packet.getAdaptationField() != null && packet.getAdaptationField().getPcr() != null) {
|
||||||
if (packet.getAdaptationField().getPcr() != null) {
|
if (packet.getPid() == getPCRPid()) {
|
||||||
if (packet.getPid() == getPCRPid()) {
|
if (!packet.getAdaptationField().isDiscontinuityIndicator()) {
|
||||||
if (!packet.getAdaptationField().isDiscontinuityIndicator()) {
|
// Get PCR and current nano time
|
||||||
// Get PCR and current nano time
|
long pcrValue = packet.getAdaptationField().getPcr().getValue();
|
||||||
long pcrValue = packet.getAdaptationField().getPcr().getValue();
|
long pcrTime = System.nanoTime();
|
||||||
long pcrTime = System.nanoTime();
|
|
||||||
|
|
||||||
// Compute sleepNanosOrig
|
// Compute sleepNanosOrig
|
||||||
if (firstPcrValue == null || firstPcrTime == null) {
|
if (firstPcrValue == null || firstPcrTime == null) {
|
||||||
firstPcrValue = pcrValue;
|
firstPcrValue = pcrValue;
|
||||||
firstPcrTime = pcrTime;
|
firstPcrTime = pcrTime;
|
||||||
//firstPcrPacketCount = pcrPidPacketCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compute sleepNanosPrevious
|
|
||||||
Long sleepNanosPrevious = null;
|
|
||||||
if (lastPcrValue != null && lastPcrTime != null) {
|
|
||||||
if (pcrValue <= lastPcrValue) {
|
|
||||||
log.trace("PCR discontinuity ! " + packet.getPid());
|
|
||||||
resetState = true;
|
|
||||||
} else {
|
|
||||||
sleepNanosPrevious = ((pcrValue - lastPcrValue) / 27 * 1000) - (pcrTime - lastPcrTime);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// System.out.println("pcrValue=" + pcrValue + ", lastPcrValue=" + lastPcrValue + ", sleepNanosPrevious=" + sleepNanosPrevious + ", sleepNanosOrig=" + sleepNanosOrig);
|
|
||||||
|
|
||||||
// Set sleep time based on PCR if possible
|
|
||||||
if (sleepNanosPrevious != null) {
|
|
||||||
// Safety : We should never have to wait more than 100ms
|
|
||||||
if (sleepNanosPrevious > 100000000) {
|
|
||||||
log.warn("PCR sleep ignored, too high !");
|
|
||||||
resetState = true;
|
|
||||||
} else {
|
|
||||||
sleepNanos = sleepNanosPrevious;
|
|
||||||
// averageSleep = sleepNanosPrevious / (pcrPidPacketCount - lastPcrPacketCount - 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set lastPcrValue/lastPcrTime
|
|
||||||
lastPcrValue = pcrValue;
|
|
||||||
lastPcrTime = pcrTime + sleepNanos;
|
|
||||||
//lastPcrPacketCount = pcrPidPacketCount;
|
|
||||||
} else {
|
|
||||||
log.warn("Skipped PCR - Discontinuity indicator");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Compute sleepNanosPrevious
|
||||||
|
Long sleepNanosPrevious = null;
|
||||||
|
if (lastPcrValue != null && lastPcrTime != null) {
|
||||||
|
if (pcrValue <= lastPcrValue) {
|
||||||
|
log.trace("PCR discontinuity ! {}", packet.getPid());
|
||||||
|
resetState = true;
|
||||||
|
} else {
|
||||||
|
sleepNanosPrevious = ((pcrValue - lastPcrValue) / 27 * 1000) - (pcrTime - lastPcrTime);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set sleep time based on PCR if possible
|
||||||
|
if (sleepNanosPrevious != null) {
|
||||||
|
// Safety : We should never have to wait more than 100ms
|
||||||
|
if (sleepNanosPrevious > 100000000) {
|
||||||
|
log.warn("PCR sleep ignored, too high !");
|
||||||
|
resetState = true;
|
||||||
|
} else {
|
||||||
|
sleepNanos = sleepNanosPrevious;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set lastPcrValue/lastPcrTime
|
||||||
|
lastPcrValue = pcrValue;
|
||||||
|
lastPcrTime = pcrTime + sleepNanos;
|
||||||
} else {
|
} else {
|
||||||
log.debug("Skipped PCR - PID does not match");
|
log.warn("Skipped PCR - Discontinuity indicator");
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
log.debug("Skipped PCR - PID does not match");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sleep if needed
|
// Sleep if needed
|
||||||
if (sleepNanos > 0 && sleepingEnabled) {
|
if (sleepNanos > 0 && sleepingEnabled) {
|
||||||
log.trace("Sleeping " + sleepNanos / 1000000 + " millis, " + sleepNanos % 1000000 + " nanos");
|
log.trace("Sleeping {} millis, {} nanos", sleepNanos / 1000000, sleepNanos % 1000000);
|
||||||
try {
|
try {
|
||||||
Thread.sleep(sleepNanos / 1000000, (int) (sleepNanos % 1000000));
|
Thread.sleep(sleepNanos / 1000000, (int) (sleepNanos % 1000000));
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
log.warn("Streaming sleep interrupted!");
|
log.warn("Streaming sleep interrupted!");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stream packet
|
// Stream packet
|
||||||
// System.out.println("Streaming packet #" + packetCount + ", PID=" + mtsPacket.getPid() + ", pcrCount=" + pcrCount + ", continuityCounter=" + mtsPacket.getContinuityCounter());
|
|
||||||
|
|
||||||
if(!streamingShouldStop && !Thread.interrupted()) {
|
if(!streamingShouldStop && !Thread.interrupted()) {
|
||||||
try {
|
try {
|
||||||
sink.send(packet);
|
sink.send(packet);
|
||||||
|
@ -289,18 +265,12 @@ public class Streamer {
|
||||||
while (!streamingShouldStop && (packet = source.nextPacket()) != null) {
|
while (!streamingShouldStop && (packet = source.nextPacket()) != null) {
|
||||||
boolean put = false;
|
boolean put = false;
|
||||||
while (!put) {
|
while (!put) {
|
||||||
try {
|
put = putPacketToBuffer(packet);
|
||||||
buffer.put(packet);
|
|
||||||
put = true;
|
|
||||||
} catch (InterruptedException ignored) {
|
|
||||||
log.error("Error adding packet to buffer", ignored);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
if(!streamingShouldStop) {
|
Thread.currentThread().interrupt();
|
||||||
log.error("Error reading from source", e);
|
log.error("Error reading from source", e);
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("Error reading from source", e);
|
log.error("Error reading from source", e);
|
||||||
} finally {
|
} finally {
|
||||||
|
@ -313,9 +283,19 @@ public class Streamer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean putPacketToBuffer(MTSPacket packet) {
|
||||||
|
try {
|
||||||
|
buffer.put(packet);
|
||||||
|
return true;
|
||||||
|
} catch (InterruptedException ignored) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
log.error("Error adding packet to buffer", ignored);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private int getPCRPid() {
|
private int getPCRPid() {
|
||||||
if ((!pmtSection.isEmpty())) {
|
if ((!pmtSection.isEmpty())) {
|
||||||
// TODO change this
|
|
||||||
return pmtSection.values().iterator().next().getPcrPid();
|
return pmtSection.values().iterator().next().getPcrPid();
|
||||||
}
|
}
|
||||||
return -1;
|
return -1;
|
||||||
|
|
Loading…
Reference in New Issue