From 3dddd91945a446b7c7e7a43443344df383bb828b Mon Sep 17 00:00:00 2001 From: 0xb00bface <0xboobface@gmail.com> Date: Sat, 8 May 2021 19:12:00 +0200 Subject: [PATCH] Add first implementation of Showup WEBRTC download --- client/src/main/resources/logback.xml | 6 +- .../java/ctbrec/sites/showup/ShowupModel.java | 25 ++- .../sites/showup/ShowupWebrtcDownload.java | 182 ++++++++++++++++++ 3 files changed, 206 insertions(+), 7 deletions(-) create mode 100644 common/src/main/java/ctbrec/sites/showup/ShowupWebrtcDownload.java diff --git a/client/src/main/resources/logback.xml b/client/src/main/resources/logback.xml index 15274572..a9e9fb01 100644 --- a/client/src/main/resources/logback.xml +++ b/client/src/main/resources/logback.xml @@ -41,9 +41,9 @@ - - + + @@ -52,6 +52,8 @@ + + diff --git a/common/src/main/java/ctbrec/sites/showup/ShowupModel.java b/common/src/main/java/ctbrec/sites/showup/ShowupModel.java index 8c92b6e7..c84aaa4d 100644 --- a/common/src/main/java/ctbrec/sites/showup/ShowupModel.java +++ b/common/src/main/java/ctbrec/sites/showup/ShowupModel.java @@ -139,11 +139,7 @@ public class ShowupModel extends AbstractModel { @Override public Download createDownload() { - if (Config.isServerMode() && !Config.getInstance().getSettings().recordSingleFile) { - return new ShowupDownload(getSite().getHttpClient()); - } else { - return new ShowupMergedDownload(getSite().getHttpClient()); - } + return new ShowupWebrtcDownload(getSite().getHttpClient()); } @Override @@ -162,4 +158,23 @@ public class ShowupModel extends AbstractModel { fac.setSegmentHeaders(headers); return fac; } + + public String getWebRtcUrl() throws IOException { + if(streamId == null || streamTranscoderAddr == null) { + List modelList = getShowupSite().getModelList(); + for (Model model : modelList) { + ShowupModel m = (ShowupModel) model; + if (Objects.equal(m.getName(), getName())) { + streamId = m.getStreamId(); + streamTranscoderAddr = m.getStreamTranscoderAddr(); + } + } + } + + int cdnHost = 1 + new Random().nextInt(5); + int cid = 100_000 + new Random().nextInt(900_000); + long pid = 10_000_000_000L + new Random().nextInt(); + String urlTemplate = "https://cdn-e0{0}.showup.tv/h5live/stream/?url=rtmp%3A%2F%2F{1}%3A1935%2Fwebrtc&stream={2}_aac&cid={3,number,#}&pid={4,number,#}"; + return MessageFormat.format(urlTemplate, cdnHost, streamTranscoderAddr, streamId, cid, pid); + } } diff --git a/common/src/main/java/ctbrec/sites/showup/ShowupWebrtcDownload.java b/common/src/main/java/ctbrec/sites/showup/ShowupWebrtcDownload.java new file mode 100644 index 00000000..b4837082 --- /dev/null +++ b/common/src/main/java/ctbrec/sites/showup/ShowupWebrtcDownload.java @@ -0,0 +1,182 @@ +package ctbrec.sites.showup; + +import static ctbrec.io.HttpConstants.*; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.time.Instant; +import java.util.concurrent.ExecutorService; +import java.util.regex.Pattern; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ctbrec.Config; +import ctbrec.Model; +import ctbrec.Recording; +import ctbrec.io.HttpClient; +import ctbrec.recorder.download.AbstractDownload; +import ctbrec.recorder.download.Download; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.WebSocket; +import okhttp3.WebSocketListener; +import okio.ByteString; + +public class ShowupWebrtcDownload extends AbstractDownload { + + private static final Logger LOG = LoggerFactory.getLogger(ShowupWebrtcDownload.class); + + private transient WebSocket ws; + private transient HttpClient httpClient; + private transient FileOutputStream fout; + + private volatile boolean running; + + private File targetFile; + + public ShowupWebrtcDownload(HttpClient httpClient) { + this.httpClient = httpClient; + } + + @Override + public void init(Config config, Model model, Instant startTime, ExecutorService executorService) throws IOException { + this.config = config; + this.model = model; + this.startTime = startTime; + this.downloadExecutor = executorService; + splittingStrategy = initSplittingStrategy(config.getSettings()); + targetFile = config.getFileForRecording(model, "mp4", startTime); + + ShowupModel showupModel = (ShowupModel) model; + Request request = new Request.Builder() + .url(showupModel.getWebRtcUrl()) + .header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent) + .header(ACCEPT, "*/*") + .header(ACCEPT_LANGUAGE, "pl") + .header(ORIGIN, Showup.BASE_URL) + .build(); + + running = true; + LOG.debug("Opening webrtc connection {}", request.url()); + ws = httpClient.newWebSocket(request, new WebSocketListener() { + @Override + public void onOpen(WebSocket webSocket, Response response) { + super.onOpen(webSocket, response); + LOG.debug("onOpen {} {}", webSocket, response); + response.close(); + try { + LOG.debug("Recording video stream to {}", targetFile); + Files.createDirectories(targetFile.getParentFile().toPath()); + fout = new FileOutputStream(targetFile); + } catch (Exception e) { + LOG.error("Couldn't open file {} to save the video stream", targetFile, e); + } + } + + @Override + public void onMessage(WebSocket webSocket, ByteString bytes) { + super.onMessage(webSocket, bytes); + if (bytes == null) { + return; + } + try { + fout.write(bytes.toByteArray()); + } catch (IOException e) { + LOG.error("Couldn't write video stream to file", e); + } + } + + @Override + public void onMessage(WebSocket webSocket, String text) { + super.onMessage(webSocket, text); + LOG.debug("onMessageT {} {}", webSocket, text); + } + + @Override + public void onFailure(WebSocket webSocket, Throwable t, Response response) { + super.onFailure(webSocket, t, response); + LOG.error("onFailure {} {}", webSocket, response, t); + if (response != null) { + response.close(); + } + } + + @Override + public void onClosing(WebSocket webSocket, int code, String reason) { + super.onClosing(webSocket, code, reason); + LOG.debug("onClosing {} {} {}", webSocket, code, reason); + } + + @Override + public void onClosed(WebSocket webSocket, int code, String reason) { + super.onClosed(webSocket, code, reason); + LOG.debug("onClosed {} {} {}", webSocket, code, reason); + } + }); + + } + + @Override + public void stop() { + running = false; + ws.close(1000, ""); + } + + @Override + public void finalizeDownload() { + try { + LOG.debug("Closing recording file {}", targetFile); + fout.close(); + } catch (IOException e) { + LOG.error("Error while closing recording file {}", targetFile, e); + } + } + + @Override + public boolean isRunning() { + return running; + } + + @Override + public void postprocess(Recording recording) { + // nothing to do + } + + @Override + public File getTarget() { + return targetFile; + } + + @Override + public String getPath(Model model) { + String absolutePath = targetFile.getAbsolutePath(); + String recordingsDir = Config.getInstance().getSettings().recordingsDir; + String relativePath = absolutePath.replaceFirst(Pattern.quote(recordingsDir), ""); + return relativePath; + } + + @Override + public boolean isSingleFile() { + return true; + } + + @Override + public long getSizeInByte() { + return getTarget().length(); + } + + @Override + public Download call() throws Exception { + if (splittingStrategy.splitNecessary(this)) { + stop(); + rescheduleTime = Instant.now(); + } else { + rescheduleTime = Instant.now().plusSeconds(5); + } + return this; + } + +}