Fix recording downloads from server to client
This commit is contained in:
parent
95fddfcb79
commit
4eeb101cbb
|
@ -0,0 +1,54 @@
|
|||
package ctbrec.ui;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URL;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import ctbrec.io.HttpClient;
|
||||
import ctbrec.recorder.ProgressListener;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
|
||||
public class FileDownload {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(FileDownload.class);
|
||||
|
||||
private HttpClient httpClient;
|
||||
private ProgressListener downloadListener;
|
||||
|
||||
public FileDownload(HttpClient httpClient, ProgressListener downloadListener) {
|
||||
this.httpClient = httpClient;
|
||||
this.downloadListener = downloadListener;
|
||||
}
|
||||
|
||||
public void start(URL url, File target) throws IOException {
|
||||
LOG.trace("Downloading file {} to {}", url, target);
|
||||
Request request = new Request.Builder().url(url).addHeader("connection", "keep-alive").build();
|
||||
Response response = httpClient.execute(request);
|
||||
long fileSize = Long.parseLong(response.header("Content-Length", String.valueOf(Long.MAX_VALUE)));
|
||||
InputStream in = null;
|
||||
try (FileOutputStream fos = new FileOutputStream(target)) {
|
||||
in = response.body().byteStream();
|
||||
byte[] b = new byte[1024 * 100];
|
||||
long totalBytesRead = 0;
|
||||
int length = -1;
|
||||
while ((length = in.read(b)) >= 0) {
|
||||
fos.write(b, 0, length);
|
||||
totalBytesRead += length;
|
||||
int progress = (int)(totalBytesRead * 100d / fileSize);
|
||||
downloadListener.update(progress);
|
||||
}
|
||||
} finally {
|
||||
if (in != null) {
|
||||
in.close();
|
||||
}
|
||||
response.close();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -4,6 +4,7 @@ import static ctbrec.Recording.State.*;
|
|||
import static javafx.scene.control.ButtonType.*;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.nio.file.NoSuchFileException;
|
||||
|
@ -32,6 +33,7 @@ import ctbrec.Config;
|
|||
import ctbrec.Recording;
|
||||
import ctbrec.Recording.State;
|
||||
import ctbrec.StringUtil;
|
||||
import ctbrec.recorder.ProgressListener;
|
||||
import ctbrec.recorder.Recorder;
|
||||
import ctbrec.recorder.download.hls.MergedHlsDownload;
|
||||
import ctbrec.sites.Site;
|
||||
|
@ -406,21 +408,14 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
|
|||
}
|
||||
|
||||
MenuItem downloadRecording = new MenuItem("Download");
|
||||
downloadRecording.setOnAction(e -> {
|
||||
try {
|
||||
download(first);
|
||||
} catch (IOException e1) {
|
||||
showErrorDialog(ERROR_WHILE_DOWNLOADING_RECORDING, "The recording could not be downloaded", e1);
|
||||
LOG.error(ERROR_WHILE_DOWNLOADING_RECORDING, e1);
|
||||
}
|
||||
});
|
||||
downloadRecording.setOnAction(e -> download(first));
|
||||
if (!Config.getInstance().getSettings().localRecording && first.getStatus() == State.FINISHED) {
|
||||
contextMenu.getItems().add(downloadRecording);
|
||||
}
|
||||
|
||||
MenuItem rerunPostProcessing = new MenuItem("Rerun Post-Processing");
|
||||
rerunPostProcessing.setOnAction(e -> triggerPostProcessing(first));
|
||||
if (first.getStatus() == State.FINISHED || first.getStatus() == State.WAITING) {
|
||||
if ((first.getStatus() == State.FINISHED || first.getStatus() == State.WAITING) && !first.isSegmented()) {
|
||||
contextMenu.getItems().add(rerunPostProcessing);
|
||||
}
|
||||
|
||||
|
@ -451,8 +446,9 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
|
|||
}).start();
|
||||
}
|
||||
|
||||
private void download(Recording recording) throws IOException {
|
||||
String filename = recording.getPath().substring(1).replace("/", "-") + ".ts";
|
||||
private void download(Recording recording) {
|
||||
LOG.debug("Path {}", recording.getPath());
|
||||
String filename = proposeTargetFilename(recording);
|
||||
FileChooser chooser = new FileChooser();
|
||||
chooser.setInitialFileName(filename);
|
||||
if(config.getSettings().lastDownloadDir != null && !config.getSettings().lastDownloadDir.equals("")) {
|
||||
|
@ -465,36 +461,45 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
|
|||
File target = chooser.showSaveDialog(null);
|
||||
if(target != null) {
|
||||
config.getSettings().lastDownloadDir = target.getParent();
|
||||
String hlsBase = config.getServerUrl() + "/hls";
|
||||
URL url = new URL(hlsBase + recording.getPath() + "/playlist.m3u8");
|
||||
LOG.info("Downloading {}", recording.getPath());
|
||||
startDownloadThread(url, target, recording);
|
||||
startDownloadThread(target, recording);
|
||||
recording.setStatus(State.DOWNLOADING);
|
||||
recording.setProgress(0);
|
||||
}
|
||||
}
|
||||
|
||||
private void startDownloadThread(URL url, File target, Recording recording) {
|
||||
private String proposeTargetFilename(Recording recording) {
|
||||
String path = recording.getPath().substring(1);
|
||||
if(recording.isSegmented()) {
|
||||
String filename = path.replace("/", "-");
|
||||
if(!filename.endsWith(".mp4")) {
|
||||
filename += ".mp4";
|
||||
}
|
||||
return filename;
|
||||
} else {
|
||||
return new File(path).getName();
|
||||
}
|
||||
}
|
||||
|
||||
private void startDownloadThread(File target, Recording recording) {
|
||||
Thread t = new Thread(() -> {
|
||||
try {
|
||||
String hlsBase = config.getServerUrl() + "/hls";
|
||||
if (recording.isSegmented()) {
|
||||
URL url = new URL(hlsBase + recording.getPath() + "/playlist.m3u8");
|
||||
MergedHlsDownload download = new MergedHlsDownload(CamrecApplication.httpClient);
|
||||
LOG.info("Downloading {}", url);
|
||||
// download.start(url.toString(), target, progress -> Platform.runLater(() -> {
|
||||
// if (progress == 100) {
|
||||
// recording.setStatus(FINISHED);
|
||||
// recording.setProgress(-1);
|
||||
// LOG.debug("Download finished for recording {}", recording.getPath());
|
||||
// } else {
|
||||
// recording.setStatus(DOWNLOADING);
|
||||
// recording.setProgress(progress);
|
||||
// }
|
||||
// }));
|
||||
// } catch (FileNotFoundException e) {
|
||||
// showErrorDialog(ERROR_WHILE_DOWNLOADING_RECORDING, "The target file couldn't be created", e);
|
||||
// LOG.error(ERROR_WHILE_DOWNLOADING_RECORDING, e);
|
||||
// } catch (IOException e) {
|
||||
// showErrorDialog(ERROR_WHILE_DOWNLOADING_RECORDING, "The recording could not be downloaded", e);
|
||||
// LOG.error(ERROR_WHILE_DOWNLOADING_RECORDING, e);
|
||||
download.downloadFinishedRecording(url.toString(), target, createDownloadListener(recording));
|
||||
} else {
|
||||
URL url = new URL(hlsBase + recording.getPath());
|
||||
FileDownload download = new FileDownload(CamrecApplication.httpClient, createDownloadListener(recording));
|
||||
download.start(url, target);
|
||||
}
|
||||
} catch (FileNotFoundException e) {
|
||||
showErrorDialog(ERROR_WHILE_DOWNLOADING_RECORDING, "The target file couldn't be created", e);
|
||||
LOG.error(ERROR_WHILE_DOWNLOADING_RECORDING, e);
|
||||
} catch (Exception e) {
|
||||
showErrorDialog(ERROR_WHILE_DOWNLOADING_RECORDING, "The recording could not be downloaded", e);
|
||||
LOG.error(ERROR_WHILE_DOWNLOADING_RECORDING, e);
|
||||
} finally {
|
||||
Platform.runLater(() -> {
|
||||
recording.setStatus(FINISHED);
|
||||
|
@ -507,6 +512,19 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
|
|||
t.start();
|
||||
}
|
||||
|
||||
private ProgressListener createDownloadListener(Recording recording) {
|
||||
return progress -> Platform.runLater(() -> {
|
||||
if (progress == 100) {
|
||||
recording.setStatus(FINISHED);
|
||||
recording.setProgress(-1);
|
||||
LOG.debug("Download finished for recording {}", recording.getPath());
|
||||
} else {
|
||||
recording.setStatus(DOWNLOADING);
|
||||
recording.setProgress(progress);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void showErrorDialog(final String title, final String msg, final Exception e) {
|
||||
Platform.runLater(() -> {
|
||||
AutosizeAlert autosizeAlert = new AutosizeAlert(AlertType.ERROR, getTabPane().getScene());
|
||||
|
@ -531,7 +549,7 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
|
|||
}.start();
|
||||
} else {
|
||||
String hlsBase = Config.getInstance().getServerUrl() + "/hls";
|
||||
url = hlsBase + recording.getPath() + (recording.getPath().endsWith(".mp4") ? "" : "/playlist.m3u8");
|
||||
url = hlsBase + recording.getPath() + (recording.isSegmented() ? "/playlist.m3u8" : "");
|
||||
new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
|
|
|
@ -6,6 +6,7 @@ import java.time.Instant;
|
|||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Optional;
|
||||
|
||||
import ctbrec.event.EventBusHolder;
|
||||
import ctbrec.event.RecordingStateChangedEvent;
|
||||
|
@ -216,4 +217,8 @@ public class Recording {
|
|||
public void refresh() {
|
||||
sizeInByte = getSize();
|
||||
}
|
||||
|
||||
public boolean isSegmented() {
|
||||
return !Optional.ofNullable(getPath()).orElse("").endsWith(".mp4");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,17 +1,29 @@
|
|||
package ctbrec.recorder.download.hls;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URL;
|
||||
import java.nio.file.Files;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.iheartradio.m3u8.ParseException;
|
||||
import com.iheartradio.m3u8.PlaylistException;
|
||||
|
||||
import ctbrec.Config;
|
||||
import ctbrec.Hmac;
|
||||
import ctbrec.OS;
|
||||
import ctbrec.io.HttpClient;
|
||||
import ctbrec.io.StreamRedirectThread;
|
||||
import ctbrec.recorder.ProgressListener;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
|
||||
public class MergedHlsDownload extends HlsDownload {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(MergedHlsDownload.class);
|
||||
|
@ -28,8 +40,18 @@ public class MergedHlsDownload extends HlsDownload {
|
|||
if (!playlist.exists()) {
|
||||
super.generatePlaylist(recording);
|
||||
}
|
||||
|
||||
File targetFile = new File(dir, "0merged.mp4");
|
||||
try {
|
||||
postprocess(playlist, targetFile);
|
||||
recording.setPath(recording.getPath() + '/' + "0merged.mp4"); // TODO set the actual name
|
||||
} catch (PostProcessingException e) {
|
||||
LOG.error("An error occurred during post-processing", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void postprocess(File playlist, File target) throws PostProcessingException {
|
||||
try {
|
||||
File dir = playlist.getParentFile();
|
||||
// @formatter:off
|
||||
String[] cmdline = OS.getFFmpegCommand(
|
||||
"-i", playlist.getAbsolutePath(),
|
||||
|
@ -37,7 +59,7 @@ public class MergedHlsDownload extends HlsDownload {
|
|||
"-c:a", "copy",
|
||||
"-movflags", "faststart",
|
||||
"-f", "mp4",
|
||||
new File(dir, "0merged.mp4").getAbsolutePath()
|
||||
target.getAbsolutePath()
|
||||
);
|
||||
// @formatter:on
|
||||
LOG.debug("Command line: {}", Arrays.toString(cmdline));
|
||||
|
@ -46,12 +68,13 @@ public class MergedHlsDownload extends HlsDownload {
|
|||
new Thread(new StreamRedirectThread(ffmpeg.getErrorStream(), System.err)).start(); // NOSONAR
|
||||
int exitCode = ffmpeg.waitFor();
|
||||
if (exitCode == 0) {
|
||||
recording.setPath(recording.getPath() + '/' + "0merged.mp4");
|
||||
Files.delete(playlist.toPath());
|
||||
File[] segments = dir.listFiles((directory, filename) -> filename.endsWith(".ts"));
|
||||
for (File segment : segments) {
|
||||
Files.delete(segment.toPath());
|
||||
}
|
||||
} else {
|
||||
throw new PostProcessingException("FFmpeg exit code was " + exitCode);
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
|
@ -60,4 +83,57 @@ public class MergedHlsDownload extends HlsDownload {
|
|||
LOG.error("Couldn't execute FFMPEG", e);
|
||||
}
|
||||
}
|
||||
|
||||
public void downloadFinishedRecording(String segmentPlaylistUri, File target, ProgressListener progressListener)
|
||||
throws IOException, ParseException, PlaylistException, InvalidKeyException, NoSuchAlgorithmException, PostProcessingException {
|
||||
if (Config.getInstance().getSettings().requireAuthentication) {
|
||||
URL u = new URL(segmentPlaylistUri);
|
||||
String path = u.getPath();
|
||||
byte[] key = Config.getInstance().getSettings().key;
|
||||
if (!Config.getInstance().getContextPath().isEmpty()) {
|
||||
path = path.substring(Config.getInstance().getContextPath().length());
|
||||
}
|
||||
String hmac = Hmac.calculate(path, key);
|
||||
segmentPlaylistUri = segmentPlaylistUri + "?hmac=" + hmac;
|
||||
}
|
||||
|
||||
File tempDir = new File(target.getParentFile(), "ctbrec-download-tmp-" + target.getName());
|
||||
Files.createDirectories(tempDir.toPath());
|
||||
|
||||
downloadFile(segmentPlaylistUri, tempDir);
|
||||
SegmentPlaylist segmentPlaylist = getNextSegments(segmentPlaylistUri);
|
||||
int fileCounter = 0;
|
||||
for (String segmentUrl : segmentPlaylist.segments) {
|
||||
downloadFile(segmentUrl, tempDir);
|
||||
fileCounter++;
|
||||
int total = segmentPlaylist.segments.size();
|
||||
int progress = (int) (fileCounter / (double) total * 100);
|
||||
progressListener.update(progress);
|
||||
}
|
||||
|
||||
File downloadedPlaylist = new File(tempDir, "playlist.m3u8");
|
||||
postprocess(downloadedPlaylist, target);
|
||||
Files.delete(tempDir.toPath());
|
||||
}
|
||||
|
||||
private void downloadFile(String fileUri, File tempDir) throws IOException {
|
||||
LOG.trace("Downloading file {} to {}", fileUri, tempDir);
|
||||
Request request = new Request.Builder().url(fileUri).addHeader("connection", "keep-alive").build();
|
||||
Response response = client.execute(request);
|
||||
InputStream in = null;
|
||||
File file = new File(request.url().encodedPath());
|
||||
try (FileOutputStream fos = new FileOutputStream(new File(tempDir, file.getName()))) {
|
||||
in = response.body().byteStream();
|
||||
byte[] b = new byte[1024 * 100];
|
||||
int length = -1;
|
||||
while ((length = in.read(b)) >= 0) {
|
||||
fos.write(b, 0, length);
|
||||
}
|
||||
} finally {
|
||||
if (in != null) {
|
||||
in.close();
|
||||
}
|
||||
response.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
package ctbrec.recorder.download.hls;
|
||||
|
||||
public class PostProcessingException extends Exception {
|
||||
|
||||
public PostProcessingException(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue