Rewrite recording code for remote recording
This commit is contained in:
parent
0f3d0b6337
commit
f11fcf7ca1
|
@ -26,6 +26,10 @@ public class JavaFxRecording extends Recording {
|
||||||
setProgress(recording.getProgress());
|
setProgress(recording.getProgress());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Recording getDelegate() {
|
||||||
|
return delegate;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Model getModel() {
|
public Model getModel() {
|
||||||
return delegate.getModel();
|
return delegate.getModel();
|
||||||
|
|
|
@ -525,7 +525,7 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
|
||||||
}.start();
|
}.start();
|
||||||
} else {
|
} else {
|
||||||
String hlsBase = "http://" + config.getSettings().httpServer + ":" + config.getSettings().httpPort + "/hls";
|
String hlsBase = "http://" + config.getSettings().httpServer + ":" + config.getSettings().httpPort + "/hls";
|
||||||
url = hlsBase + "/" + recording.getPath() + "/playlist.m3u8";
|
url = hlsBase + recording.getPath() + "/playlist.m3u8";
|
||||||
new Thread() {
|
new Thread() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
|
@ -567,7 +567,7 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
recorder.delete(r);
|
recorder.delete(r.getDelegate());
|
||||||
deleted.add(r);
|
deleted.add(r);
|
||||||
} catch (IOException | InvalidKeyException | NoSuchAlgorithmException | IllegalStateException e1) {
|
} catch (IOException | InvalidKeyException | NoSuchAlgorithmException | IllegalStateException e1) {
|
||||||
LOG.error("Error while deleting recording", e1);
|
LOG.error("Error while deleting recording", e1);
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
package ctbrec;
|
package ctbrec;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileInputStream;
|
|
||||||
import java.io.FileNotFoundException;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
|
@ -10,15 +8,8 @@ import java.time.LocalDateTime;
|
||||||
import java.time.ZoneId;
|
import java.time.ZoneId;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
|
|
||||||
import com.iheartradio.m3u8.Encoding;
|
|
||||||
import com.iheartradio.m3u8.Format;
|
|
||||||
import com.iheartradio.m3u8.ParseException;
|
import com.iheartradio.m3u8.ParseException;
|
||||||
import com.iheartradio.m3u8.ParsingMode;
|
|
||||||
import com.iheartradio.m3u8.PlaylistException;
|
import com.iheartradio.m3u8.PlaylistException;
|
||||||
import com.iheartradio.m3u8.PlaylistParser;
|
|
||||||
import com.iheartradio.m3u8.data.MediaPlaylist;
|
|
||||||
import com.iheartradio.m3u8.data.Playlist;
|
|
||||||
import com.iheartradio.m3u8.data.TrackData;
|
|
||||||
|
|
||||||
import ctbrec.event.EventBusHolder;
|
import ctbrec.event.EventBusHolder;
|
||||||
import ctbrec.event.RecordingStateChangedEvent;
|
import ctbrec.event.RecordingStateChangedEvent;
|
||||||
|
@ -61,15 +52,6 @@ public class Recording {
|
||||||
|
|
||||||
public Recording() {}
|
public Recording() {}
|
||||||
|
|
||||||
// public Recording(String path) throws ParseException {
|
|
||||||
// this.path = path;
|
|
||||||
// this.modelName = path.substring(0, path.indexOf("/"));
|
|
||||||
// SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd_HH-mm");
|
|
||||||
// Date date = sdf.parse(path.substring(path.indexOf('/')+1));
|
|
||||||
// startDate = Instant.ofEpochMilli(date.getTime());
|
|
||||||
// }
|
|
||||||
|
|
||||||
|
|
||||||
public Instant getStartDate() {
|
public Instant getStartDate() {
|
||||||
return startDate;
|
return startDate;
|
||||||
}
|
}
|
||||||
|
@ -158,42 +140,10 @@ public class Recording {
|
||||||
}
|
}
|
||||||
|
|
||||||
public Duration getLength() throws IOException, ParseException, PlaylistException {
|
public Duration getLength() throws IOException, ParseException, PlaylistException {
|
||||||
// check, if the recording exists
|
if (getDownload() != null) {
|
||||||
File rec = new File(Config.getInstance().getSettings().recordingsDir, getPath());
|
return getDownload().getLength();
|
||||||
if (!rec.exists()) {
|
|
||||||
return Duration.ofSeconds(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// check, if the recording has data at all
|
|
||||||
long size = getSizeInByte();
|
|
||||||
if (size == 0) {
|
|
||||||
return Duration.ofSeconds(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// determine the length
|
|
||||||
if (getPath().endsWith(".ts")) {
|
|
||||||
return Duration.ofSeconds((long) MpegUtil.getFileDuration(rec));
|
|
||||||
} else if (rec.isDirectory()) {
|
|
||||||
File playlist = new File(rec, "playlist.m3u8");
|
|
||||||
if (playlist.exists()) {
|
|
||||||
return Duration.ofSeconds((long) getPlaylistLength(playlist));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Duration.ofSeconds(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
private double getPlaylistLength(File playlist) throws IOException, ParseException, PlaylistException {
|
|
||||||
if (playlist.exists()) {
|
|
||||||
PlaylistParser playlistParser = new PlaylistParser(new FileInputStream(playlist), Format.EXT_M3U, Encoding.UTF_8, ParsingMode.LENIENT);
|
|
||||||
Playlist m3u = playlistParser.parse();
|
|
||||||
MediaPlaylist mediaPlaylist = m3u.getMediaPlaylist();
|
|
||||||
double length = 0;
|
|
||||||
for (TrackData trackData : mediaPlaylist.getTracks()) {
|
|
||||||
length += trackData.getTrackInfo().duration;
|
|
||||||
}
|
|
||||||
return length;
|
|
||||||
} else {
|
} else {
|
||||||
throw new FileNotFoundException(playlist.getAbsolutePath() + " does not exist");
|
return Duration.ofSeconds(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -368,7 +368,9 @@ public class NextGenLocalRecorder implements Recorder {
|
||||||
recording = false;
|
recording = false;
|
||||||
|
|
||||||
LOG.debug("Stopping all recording processes");
|
LOG.debug("Stopping all recording processes");
|
||||||
for (Recording rec : recordingProcesses.values()) {
|
// make a copy to avoid ConcurrentModificationException
|
||||||
|
List<Recording> toStop = new ArrayList<>(recordingProcesses.values());
|
||||||
|
for (Recording rec : toStop) {
|
||||||
Optional.ofNullable(rec.getDownload()).ifPresent(Download::stop);
|
Optional.ofNullable(rec.getDownload()).ifPresent(Download::stop);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -31,6 +31,8 @@ public class OnlineMonitor extends Thread {
|
||||||
|
|
||||||
private Map<Model, Model.State> states = new HashMap<>();
|
private Map<Model, Model.State> states = new HashMap<>();
|
||||||
|
|
||||||
|
// TODO divide models into buckets by their site in each iteration a model of each bucket can be testes in parallel
|
||||||
|
// this will speed up the testing, but not hammer the sites
|
||||||
public OnlineMonitor(Recorder recorder) {
|
public OnlineMonitor(Recorder recorder) {
|
||||||
this.recorder = recorder;
|
this.recorder = recorder;
|
||||||
setName("OnlineMonitor");
|
setName("OnlineMonitor");
|
||||||
|
|
|
@ -122,6 +122,9 @@ public class RecordingManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void delete(Recording recording) throws IOException {
|
public void delete(Recording recording) throws IOException {
|
||||||
|
int idx = recordings.indexOf(recording);
|
||||||
|
recording = recordings.get(idx);
|
||||||
|
|
||||||
recording.setStatus(State.DELETING);
|
recording.setStatus(State.DELETING);
|
||||||
File recordingsDir = new File(config.getSettings().recordingsDir);
|
File recordingsDir = new File(config.getSettings().recordingsDir);
|
||||||
File path = new File(recordingsDir, recording.getPath());
|
File path = new File(recordingsDir, recording.getPath());
|
||||||
|
|
|
@ -47,6 +47,7 @@ public class RemoteRecorder implements Recorder {
|
||||||
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);
|
||||||
|
private JsonAdapter<RecordingRequest> recordingRequestAdapter = moshi.adapter(RecordingRequest.class);
|
||||||
|
|
||||||
private List<Model> models = Collections.emptyList();
|
private List<Model> models = Collections.emptyList();
|
||||||
private List<Model> onlineModels = Collections.emptyList();
|
private List<Model> onlineModels = Collections.emptyList();
|
||||||
|
@ -335,18 +336,19 @@ public class RemoteRecorder implements Recorder {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void delete(Recording recording) throws IOException, InvalidKeyException, NoSuchAlgorithmException, IllegalStateException {
|
public void delete(Recording recording) throws IOException, InvalidKeyException, NoSuchAlgorithmException, IllegalStateException {
|
||||||
String msg = "{\"action\": \"delete\", \"recording\": \""+recording.getPath()+"\"}";
|
RecordingRequest recReq = new RecordingRequest("delete", recording);
|
||||||
|
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("http://" + config.getSettings().httpServer + ":" + config.getSettings().httpPort + "/rec")
|
.url("http://" + config.getSettings().httpServer + ":" + config.getSettings().httpPort + "/rec")
|
||||||
.post(body);
|
.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);
|
||||||
|
@ -384,6 +386,33 @@ public class RemoteRecorder implements Recorder {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static class RecordingRequest {
|
||||||
|
private String action;
|
||||||
|
private Recording recording;
|
||||||
|
|
||||||
|
public RecordingRequest(String action, Recording recording) {
|
||||||
|
super();
|
||||||
|
this.action = action;
|
||||||
|
this.recording = recording;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAction() {
|
||||||
|
return action;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAction(String action) {
|
||||||
|
this.action = action;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Recording getRecording() {
|
||||||
|
return recording;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRecording(Recording recording) {
|
||||||
|
this.recording = recording;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void switchStreamSource(Model model) throws IOException, InvalidKeyException, NoSuchAlgorithmException, IllegalStateException {
|
public void switchStreamSource(Model model) throws IOException, InvalidKeyException, NoSuchAlgorithmException, IllegalStateException {
|
||||||
sendRequest("switch", model);
|
sendRequest("switch", model);
|
||||||
|
|
|
@ -2,6 +2,7 @@ package ctbrec.recorder.download;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.time.Duration;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
|
|
||||||
import ctbrec.Config;
|
import ctbrec.Config;
|
||||||
|
@ -14,6 +15,7 @@ public interface Download {
|
||||||
public void stop();
|
public void stop();
|
||||||
public Model getModel();
|
public Model getModel();
|
||||||
public Instant getStartTime();
|
public Instant getStartTime();
|
||||||
|
public Duration getLength();
|
||||||
public void postprocess(Recording recording);
|
public void postprocess(Recording recording);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
package ctbrec.recorder.download;
|
package ctbrec.recorder.download;
|
||||||
|
|
||||||
import static ctbrec.Recording.State.*;
|
|
||||||
|
|
||||||
import java.io.EOFException;
|
import java.io.EOFException;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
import java.io.FileNotFoundException;
|
import java.io.FileNotFoundException;
|
||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
@ -15,14 +14,12 @@ 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.text.SimpleDateFormat;
|
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
|
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.Date;
|
|
||||||
import java.util.concurrent.Callable;
|
import java.util.concurrent.Callable;
|
||||||
import java.util.concurrent.ExecutionException;
|
|
||||||
import java.util.concurrent.Future;
|
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;
|
||||||
|
@ -30,15 +27,21 @@ import java.util.regex.Pattern;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import com.iheartradio.m3u8.Encoding;
|
||||||
|
import com.iheartradio.m3u8.Format;
|
||||||
import com.iheartradio.m3u8.ParseException;
|
import com.iheartradio.m3u8.ParseException;
|
||||||
|
import com.iheartradio.m3u8.ParsingMode;
|
||||||
import com.iheartradio.m3u8.PlaylistError;
|
import com.iheartradio.m3u8.PlaylistError;
|
||||||
import com.iheartradio.m3u8.PlaylistException;
|
import com.iheartradio.m3u8.PlaylistException;
|
||||||
|
import com.iheartradio.m3u8.PlaylistParser;
|
||||||
|
import com.iheartradio.m3u8.data.MediaPlaylist;
|
||||||
|
import com.iheartradio.m3u8.data.Playlist;
|
||||||
|
import com.iheartradio.m3u8.data.TrackData;
|
||||||
|
|
||||||
import ctbrec.Config;
|
import ctbrec.Config;
|
||||||
import ctbrec.Model;
|
import ctbrec.Model;
|
||||||
import ctbrec.Recording;
|
import ctbrec.Recording;
|
||||||
import ctbrec.event.EventBusHolder;
|
import ctbrec.Recording.State;
|
||||||
import ctbrec.event.RecordingStateChangedEvent;
|
|
||||||
import ctbrec.io.HttpClient;
|
import ctbrec.io.HttpClient;
|
||||||
import ctbrec.io.HttpException;
|
import ctbrec.io.HttpException;
|
||||||
import ctbrec.recorder.PlaylistGenerator;
|
import ctbrec.recorder.PlaylistGenerator;
|
||||||
|
@ -68,7 +71,7 @@ public class HlsDownload extends AbstractHlsDownload {
|
||||||
super.model = model;
|
super.model = model;
|
||||||
startTime = Instant.now();
|
startTime = Instant.now();
|
||||||
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(Config.RECORDING_DATE_FORMAT);
|
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(Config.RECORDING_DATE_FORMAT);
|
||||||
String startTime = formatter.format(this.startTime);
|
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());
|
||||||
downloadDir = FileSystems.getDefault().getPath(modelDir.toString(), startTime);
|
downloadDir = FileSystems.getDefault().getPath(modelDir.toString(), startTime);
|
||||||
}
|
}
|
||||||
|
@ -83,10 +86,6 @@ public class HlsDownload extends AbstractHlsDownload {
|
||||||
throw new IOException(model.getName() +"'s room is not public");
|
throw new IOException(model.getName() +"'s room is not public");
|
||||||
}
|
}
|
||||||
|
|
||||||
// let the world know, that we are recording now
|
|
||||||
RecordingStateChangedEvent evt = new RecordingStateChangedEvent(getTarget(), RECORDING, model, getStartTime());
|
|
||||||
EventBusHolder.BUS.post(evt);
|
|
||||||
|
|
||||||
String segments = getSegmentPlaylistUrl(model);
|
String segments = getSegmentPlaylistUrl(model);
|
||||||
if(segments != null) {
|
if(segments != null) {
|
||||||
if (!Files.exists(downloadDir, LinkOption.NOFOLLOW_LINKS)) {
|
if (!Files.exists(downloadDir, LinkOption.NOFOLLOW_LINKS)) {
|
||||||
|
@ -118,7 +117,10 @@ public class HlsDownload extends AbstractHlsDownload {
|
||||||
}
|
}
|
||||||
|
|
||||||
// split recordings
|
// split recordings
|
||||||
splitRecording(lastSegmentDownload);
|
boolean split = splitRecording(lastSegmentDownload);
|
||||||
|
if (split) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
long wait = 0;
|
long wait = 0;
|
||||||
if(lastSegmentNumber == playlist.seq) {
|
if(lastSegmentNumber == playlist.seq) {
|
||||||
|
@ -181,7 +183,9 @@ public class HlsDownload extends AbstractHlsDownload {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void postprocess(Recording recording) {
|
public void postprocess(Recording recording) {
|
||||||
|
recording.setStatusWithEvent(State.GENERATING_PLAYLIST, true);
|
||||||
generatePlaylist(recording.getAbsoluteFile());
|
generatePlaylist(recording.getAbsoluteFile());
|
||||||
|
recording.setStatusWithEvent(State.POST_PROCESSING, true);
|
||||||
super.postprocess(recording);
|
super.postprocess(recording);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -215,40 +219,16 @@ public class HlsDownload extends AbstractHlsDownload {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void splitRecording(Future<Boolean> lastSegmentDownload) {
|
private boolean splitRecording(Future<Boolean> lastSegmentDownload) {
|
||||||
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) {
|
||||||
File lastTargetFile = downloadDir.toFile();
|
internalStop();
|
||||||
|
return true;
|
||||||
// switch to the next dir
|
|
||||||
SimpleDateFormat sdf = new SimpleDateFormat(Config.RECORDING_DATE_FORMAT);
|
|
||||||
super.startTime = Instant.now();
|
|
||||||
String startTime = sdf.format(new Date());
|
|
||||||
Path modelDir = FileSystems.getDefault().getPath(config.getSettings().recordingsDir, model.getSanitizedNamed());
|
|
||||||
LOG.debug("Switching to {}", downloadDir);
|
|
||||||
downloadDir = FileSystems.getDefault().getPath(modelDir.toString(), startTime);
|
|
||||||
downloadDir.toFile().mkdirs();
|
|
||||||
splitRecStartTime = ZonedDateTime.now();
|
|
||||||
|
|
||||||
// post-process current recording
|
|
||||||
LOG.debug("Running post-processing for {}", lastTargetFile);
|
|
||||||
Thread pp = new Thread(() -> {
|
|
||||||
if(lastSegmentDownload != null) {
|
|
||||||
// wait for last segment in this directory
|
|
||||||
try {
|
|
||||||
lastSegmentDownload.get();
|
|
||||||
} catch (InterruptedException | ExecutionException e) {
|
|
||||||
LOG.error("Couldn't wait for last segment to arrive in this directory. Playlist might be inclomplete", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
pp.setName("Post-Processing split recording");
|
|
||||||
pp.setPriority(Thread.MIN_PRIORITY);
|
|
||||||
pp.start();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -333,4 +313,32 @@ public class HlsDownload extends AbstractHlsDownload {
|
||||||
String relativePath = absolutePath.replaceFirst(Pattern.quote(recordingsDir), "");
|
String relativePath = absolutePath.replaceFirst(Pattern.quote(recordingsDir), "");
|
||||||
return relativePath;
|
return relativePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Duration getLength() {
|
||||||
|
try {
|
||||||
|
File playlist = new File(getTarget(), "playlist.m3u8");
|
||||||
|
if (playlist.exists()) {
|
||||||
|
return Duration.ofSeconds((long) getPlaylistLength(playlist));
|
||||||
|
}
|
||||||
|
} catch (IOException | ParseException | PlaylistException e) {
|
||||||
|
LOG.error("Couldn't determine recording length", e);
|
||||||
|
}
|
||||||
|
return Duration.ofSeconds(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private double getPlaylistLength(File playlist) throws IOException, ParseException, PlaylistException {
|
||||||
|
if (playlist.exists()) {
|
||||||
|
PlaylistParser playlistParser = new PlaylistParser(new FileInputStream(playlist), Format.EXT_M3U, Encoding.UTF_8, ParsingMode.LENIENT);
|
||||||
|
Playlist m3u = playlistParser.parse();
|
||||||
|
MediaPlaylist mediaPlaylist = m3u.getMediaPlaylist();
|
||||||
|
double length = 0;
|
||||||
|
for (TrackData trackData : mediaPlaylist.getTracks()) {
|
||||||
|
length += trackData.getTrackInfo().duration;
|
||||||
|
}
|
||||||
|
return length;
|
||||||
|
} else {
|
||||||
|
throw new FileNotFoundException(playlist.getAbsolutePath() + " does not exist");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,6 +41,7 @@ import com.iheartradio.m3u8.PlaylistException;
|
||||||
import ctbrec.Config;
|
import ctbrec.Config;
|
||||||
import ctbrec.Hmac;
|
import ctbrec.Hmac;
|
||||||
import ctbrec.Model;
|
import ctbrec.Model;
|
||||||
|
import ctbrec.MpegUtil;
|
||||||
import ctbrec.io.HttpClient;
|
import ctbrec.io.HttpClient;
|
||||||
import ctbrec.io.HttpException;
|
import ctbrec.io.HttpException;
|
||||||
import ctbrec.recorder.ProgressListener;
|
import ctbrec.recorder.ProgressListener;
|
||||||
|
@ -516,4 +517,14 @@ public class MergedHlsDownload extends AbstractHlsDownload {
|
||||||
String relativePath = absolutePath.replaceFirst(Pattern.quote(recordingsDir), "");
|
String relativePath = absolutePath.replaceFirst(Pattern.quote(recordingsDir), "");
|
||||||
return relativePath;
|
return relativePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Duration getLength() {
|
||||||
|
try {
|
||||||
|
return Duration.ofSeconds((long) MpegUtil.getFileDuration(targetFile));
|
||||||
|
} catch (IOException e) {
|
||||||
|
LOG.error("Couldn't determine recording length", e);
|
||||||
|
return Duration.ofSeconds(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,304 +0,0 @@
|
||||||
package ctbrec.sites.jasmin;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileOutputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.net.URLEncoder;
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.time.Instant;
|
|
||||||
import java.util.Random;
|
|
||||||
import java.util.regex.Pattern;
|
|
||||||
|
|
||||||
import org.json.JSONArray;
|
|
||||||
import org.json.JSONObject;
|
|
||||||
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.Download;
|
|
||||||
import okhttp3.Request;
|
|
||||||
import okhttp3.Response;
|
|
||||||
import okhttp3.WebSocket;
|
|
||||||
import okhttp3.WebSocketListener;
|
|
||||||
import okio.ByteString;
|
|
||||||
|
|
||||||
public class LiveJasminChunkedHttpDownload implements Download {
|
|
||||||
|
|
||||||
private static final transient Logger LOG = LoggerFactory.getLogger(LiveJasminChunkedHttpDownload.class);
|
|
||||||
private static final transient String USER_AGENT = "Mozilla/5.0 (iPhone; CPU OS 10_14 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/11.1.1 Mobile/14E304 Safari/605.1.15";
|
|
||||||
|
|
||||||
private HttpClient client;
|
|
||||||
private Model model;
|
|
||||||
private Instant startTime;
|
|
||||||
private File targetFile;
|
|
||||||
|
|
||||||
private String applicationId;
|
|
||||||
private String sessionId;
|
|
||||||
private String jsm2SessionId;
|
|
||||||
private String sb_ip;
|
|
||||||
private String sb_hash;
|
|
||||||
private String relayHost;
|
|
||||||
private String hlsHost;
|
|
||||||
private String clientInstanceId = newClientInstanceId(); // generate a 32 digit random number
|
|
||||||
private String streamPath = "streams/clonedLiveStream";
|
|
||||||
private boolean isAlive = true;
|
|
||||||
|
|
||||||
public LiveJasminChunkedHttpDownload(HttpClient client) {
|
|
||||||
this.client = client;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String newClientInstanceId() {
|
|
||||||
return new java.math.BigInteger(256, new Random()).toString().substring(0, 32);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void init(Config config, Model model) {
|
|
||||||
this.model = model;
|
|
||||||
this.startTime = Instant.now();
|
|
||||||
this.targetFile = config.getFileForRecording(model, "mp4");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void start() throws IOException {
|
|
||||||
getPerformerDetails(model.getName());
|
|
||||||
try {
|
|
||||||
getStreamPath();
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
throw new IOException("Couldn't determine stream path", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
LOG.debug("appid: {}", applicationId);
|
|
||||||
LOG.debug("sessionid: {}", sessionId);
|
|
||||||
LOG.debug("jsm2sessionid: {}", jsm2SessionId);
|
|
||||||
LOG.debug("sb_ip: {}", sb_ip);
|
|
||||||
LOG.debug("sb_hash: {}", sb_hash);
|
|
||||||
LOG.debug("hls host: {}", hlsHost);
|
|
||||||
LOG.debug("clientinstanceid {}", clientInstanceId);
|
|
||||||
LOG.debug("stream path {}", streamPath);
|
|
||||||
|
|
||||||
String rtmpUrl = "rtmp://" + sb_ip + "/" + applicationId + "?sessionId-" + sessionId + "|clientInstanceId-" + clientInstanceId;
|
|
||||||
|
|
||||||
String m3u8 = "https://" + hlsHost + "/h5live/http/playlist.m3u8?url=" + URLEncoder.encode(rtmpUrl, "utf-8");
|
|
||||||
m3u8 = m3u8 += "&stream=" + URLEncoder.encode(streamPath, "utf-8");
|
|
||||||
|
|
||||||
Request req = new Request.Builder()
|
|
||||||
.url(m3u8)
|
|
||||||
.header("User-Agent", USER_AGENT)
|
|
||||||
.header("Accept", "application/json,*/*")
|
|
||||||
.header("Accept-Language", "en")
|
|
||||||
.header("Referer", model.getUrl())
|
|
||||||
.header("X-Requested-With", "XMLHttpRequest")
|
|
||||||
.build();
|
|
||||||
try (Response response = client.execute(req)) {
|
|
||||||
if (response.isSuccessful()) {
|
|
||||||
System.out.println(response.body().string());
|
|
||||||
} else {
|
|
||||||
throw new IOException(response.code() + " - " + response.message());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String url = "https://" + hlsHost + "/h5live/http/stream.mp4?url=" + URLEncoder.encode(rtmpUrl, "utf-8");
|
|
||||||
url = url += "&stream=" + URLEncoder.encode(streamPath, "utf-8");
|
|
||||||
|
|
||||||
LOG.debug("Downloading {}", url);
|
|
||||||
req = new Request.Builder()
|
|
||||||
.url(url)
|
|
||||||
.header("User-Agent", USER_AGENT)
|
|
||||||
.header("Accept", "application/json,*/*")
|
|
||||||
.header("Accept-Language", "en")
|
|
||||||
.header("Referer", model.getUrl())
|
|
||||||
.header("X-Requested-With", "XMLHttpRequest")
|
|
||||||
.build();
|
|
||||||
try (Response response = client.execute(req)) {
|
|
||||||
if (response.isSuccessful()) {
|
|
||||||
FileOutputStream fos = null;
|
|
||||||
try {
|
|
||||||
Files.createDirectories(targetFile.getParentFile().toPath());
|
|
||||||
fos = new FileOutputStream(targetFile);
|
|
||||||
|
|
||||||
InputStream in = response.body().byteStream();
|
|
||||||
byte[] b = new byte[10240];
|
|
||||||
int len = -1;
|
|
||||||
while (isAlive && (len = in.read(b)) >= 0) {
|
|
||||||
fos.write(b, 0, len);
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
LOG.error("Couldn't create video file", e);
|
|
||||||
} finally {
|
|
||||||
isAlive = false;
|
|
||||||
if(fos != null) {
|
|
||||||
fos.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw new IOException(response.code() + " - " + response.message());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void getStreamPath() throws InterruptedException {
|
|
||||||
Object lock = new Object();
|
|
||||||
|
|
||||||
Request request = new Request.Builder()
|
|
||||||
.url("https://" + relayHost + "/?random=" + newClientInstanceId())
|
|
||||||
.header("Origin", LiveJasmin.baseUrl)
|
|
||||||
.header("User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:63.0) Gecko/20100101 Firefox/63.0")
|
|
||||||
.header("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
|
|
||||||
.header("Accept-Language", "de,en-US;q=0.7,en;q=0.3")
|
|
||||||
.build();
|
|
||||||
client.newWebSocket(request, new WebSocketListener() {
|
|
||||||
@Override
|
|
||||||
public void onOpen(WebSocket webSocket, Response response) {
|
|
||||||
LOG.debug("relay open {}", model.getName());
|
|
||||||
webSocket.send("{\"event\":\"register\",\"applicationId\":\"" + applicationId
|
|
||||||
+ "\",\"connectionData\":{\"jasmin2App\":true,\"isMobileClient\":false,\"platform\":\"desktop\",\"chatID\":\"freechat\","
|
|
||||||
+ "\"sessionID\":\"" + sessionId + "\"," + "\"jsm2SessionId\":\"" + jsm2SessionId + "\",\"userType\":\"user\"," + "\"performerId\":\""
|
|
||||||
+ model
|
|
||||||
+ "\",\"clientRevision\":\"\",\"proxyIP\":\"\",\"playerVer\":\"nanoPlayerVersion: 3.10.3 appCodeName: Mozilla appName: Netscape appVersion: 5.0 (X11) platform: Linux x86_64\",\"livejasminTvmember\":false,\"newApplet\":true,\"livefeedtype\":null,\"gravityCookieId\":\"\",\"passparam\":\"\",\"brandID\":\"jasmin\",\"cobrandId\":\"\",\"subbrand\":\"livejasmin\",\"siteName\":\"LiveJasmin\",\"siteUrl\":\""+LiveJasmin.baseUrl+"\","
|
|
||||||
+ "\"clientInstanceId\":\"" + clientInstanceId + "\",\"armaVersion\":\"34.10.0\",\"isPassive\":false}}");
|
|
||||||
response.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onMessage(WebSocket webSocket, String text) {
|
|
||||||
LOG.debug("relay <-- {} T{}", model.getName(), text);
|
|
||||||
JSONObject event = new JSONObject(text);
|
|
||||||
if (event.optString("event").equals("accept")) {
|
|
||||||
webSocket.send("{\"event\":\"connectSharedObject\",\"name\":\"data/chat_so\"}");
|
|
||||||
} else if (event.optString("event").equals("updateSharedObject")) {
|
|
||||||
JSONArray list = event.getJSONArray("list");
|
|
||||||
for (int i = 0; i < list.length(); i++) {
|
|
||||||
JSONObject obj = list.getJSONObject(i);
|
|
||||||
if (obj.optString("name").equals("streamList")) {
|
|
||||||
LOG.debug(obj.toString(2));
|
|
||||||
streamPath = getStreamPath(obj.getJSONObject("newValue"));
|
|
||||||
LOG.debug("Stream Path: {}", streamPath);
|
|
||||||
webSocket.send("{\"event\":\"call\",\"funcName\":\"makeActive\",\"data\":[]}");
|
|
||||||
webSocket.close(1000, "");
|
|
||||||
synchronized (lock) {
|
|
||||||
lock.notify();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}else if(event.optString("event").equals("call")) {
|
|
||||||
String func = event.optString("funcName");
|
|
||||||
if(func.equals("closeConnection")) {
|
|
||||||
stop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getStreamPath(JSONObject obj) {
|
|
||||||
String streamName = "streams/clonedLiveStream";
|
|
||||||
int height = 0;
|
|
||||||
if(obj.has("streams")) {
|
|
||||||
JSONArray streams = obj.getJSONArray("streams");
|
|
||||||
for (int i = 0; i < streams.length(); i++) {
|
|
||||||
JSONObject stream = streams.getJSONObject(i);
|
|
||||||
int h = stream.optInt("height");
|
|
||||||
if(h > height) {
|
|
||||||
height = h;
|
|
||||||
streamName = stream.getString("streamNameWithFolder");
|
|
||||||
streamName = "free/" + stream.getString("name");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return streamName;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onMessage(WebSocket webSocket, ByteString bytes) {
|
|
||||||
LOG.debug("relay <-- {} B{}", model.getName(), bytes.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onClosed(WebSocket webSocket, int code, String reason) {
|
|
||||||
LOG.debug("relay closed {} {} {}", code, reason, model.getName());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onFailure(WebSocket webSocket, Throwable t, Response response) {
|
|
||||||
LOG.debug("relay failure {}", model.getName(), t);
|
|
||||||
if (response != null) {
|
|
||||||
response.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
synchronized (lock) {
|
|
||||||
lock.wait();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void getPerformerDetails(String name) throws IOException {
|
|
||||||
String url = "https://m."+LiveJasmin.baseDomain+"/en/chat-html5/" + name;
|
|
||||||
Request req = new Request.Builder()
|
|
||||||
.url(url)
|
|
||||||
.header("User-Agent", USER_AGENT)
|
|
||||||
.header("Accept", "application/json,*/*")
|
|
||||||
.header("Accept-Language", "en")
|
|
||||||
.header("Referer", LiveJasmin.baseUrl)
|
|
||||||
.header("X-Requested-With", "XMLHttpRequest")
|
|
||||||
.build();
|
|
||||||
try (Response response = client.execute(req)) {
|
|
||||||
if (response.isSuccessful()) {
|
|
||||||
String body = response.body().string();
|
|
||||||
JSONObject json = new JSONObject(body);
|
|
||||||
// System.out.println(json.toString(2));
|
|
||||||
if (json.optBoolean("success")) {
|
|
||||||
JSONObject data = json.getJSONObject("data");
|
|
||||||
JSONObject config = data.getJSONObject("config");
|
|
||||||
JSONObject armageddonConfig = config.getJSONObject("armageddonConfig");
|
|
||||||
JSONObject chatRoom = config.getJSONObject("chatRoom");
|
|
||||||
sessionId = armageddonConfig.getString("sessionid");
|
|
||||||
jsm2SessionId = armageddonConfig.getString("jsm2session");
|
|
||||||
sb_hash = chatRoom.getString("sb_hash");
|
|
||||||
sb_ip = chatRoom.getString("sb_ip");
|
|
||||||
applicationId = "memberChat/jasmin" + name + sb_hash;
|
|
||||||
hlsHost = "dss-hls-" + sb_ip.replace('.', '-') + ".dditscdn.com";
|
|
||||||
relayHost = "dss-relay-" + sb_ip.replace('.', '-') + ".dditscdn.com";
|
|
||||||
} else {
|
|
||||||
throw new IOException("Response was not successful: " + body);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw new IOException(response.code() + " - " + response.message());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void stop() {
|
|
||||||
isAlive = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public File getTarget() {
|
|
||||||
return targetFile;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Model getModel() {
|
|
||||||
return model;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Instant getStartTime() {
|
|
||||||
return startTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void postprocess(Recording recording) {
|
|
||||||
}
|
|
||||||
|
|
||||||
@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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,368 +0,0 @@
|
||||||
package ctbrec.sites.jasmin;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileOutputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.UnsupportedEncodingException;
|
|
||||||
import java.net.URLEncoder;
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.time.Instant;
|
|
||||||
import java.util.regex.Pattern;
|
|
||||||
|
|
||||||
import org.json.JSONArray;
|
|
||||||
import org.json.JSONObject;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import com.google.common.eventbus.Subscribe;
|
|
||||||
|
|
||||||
import ctbrec.Config;
|
|
||||||
import ctbrec.Model;
|
|
||||||
import ctbrec.Recording;
|
|
||||||
import ctbrec.event.Event;
|
|
||||||
import ctbrec.event.EventBusHolder;
|
|
||||||
import ctbrec.event.ModelStateChangedEvent;
|
|
||||||
import ctbrec.io.HttpClient;
|
|
||||||
import ctbrec.recorder.download.Download;
|
|
||||||
import okhttp3.Request;
|
|
||||||
import okhttp3.Response;
|
|
||||||
import okhttp3.WebSocket;
|
|
||||||
import okhttp3.WebSocketListener;
|
|
||||||
import okio.ByteString;
|
|
||||||
|
|
||||||
public class LiveJasminWebSocketDownload implements Download {
|
|
||||||
private static final transient Logger LOG = LoggerFactory.getLogger(LiveJasminWebSocketDownload.class);
|
|
||||||
|
|
||||||
private String applicationId;
|
|
||||||
private String sessionId;
|
|
||||||
private String jsm2SessionId;
|
|
||||||
private String sb_ip;
|
|
||||||
private String sb_hash;
|
|
||||||
private String relayHost;
|
|
||||||
private String streamHost;
|
|
||||||
private String clientInstanceId = "01234567890123456789012345678901"; // TODO where to get or generate a random id?
|
|
||||||
private String streamPath = "streams/clonedLiveStream";
|
|
||||||
private WebSocket relay;
|
|
||||||
private WebSocket stream;
|
|
||||||
|
|
||||||
protected boolean connectionClosed;
|
|
||||||
|
|
||||||
private HttpClient client;
|
|
||||||
private Model model;
|
|
||||||
private Instant startTime;
|
|
||||||
private File targetFile;
|
|
||||||
|
|
||||||
public LiveJasminWebSocketDownload(HttpClient client) {
|
|
||||||
this.client = client;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void init(Config config, Model model) {
|
|
||||||
this.model = model;
|
|
||||||
this.startTime = Instant.now();
|
|
||||||
this.targetFile = config.getFileForRecording(model, "mp4");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void start() throws IOException {
|
|
||||||
getPerformerDetails(model.getName());
|
|
||||||
LOG.debug("appid: {}", applicationId);
|
|
||||||
LOG.debug("sessionid: {}",sessionId);
|
|
||||||
LOG.debug("jsm2sessionid: {}",jsm2SessionId);
|
|
||||||
LOG.debug("sb_ip: {}",sb_ip);
|
|
||||||
LOG.debug("sb_hash: {}",sb_hash);
|
|
||||||
LOG.debug("relay host: {}",relayHost);
|
|
||||||
LOG.debug("stream host: {}",streamHost);
|
|
||||||
LOG.debug("clientinstanceid {}",clientInstanceId);
|
|
||||||
|
|
||||||
EventBusHolder.BUS.register(this);
|
|
||||||
|
|
||||||
Request request = new Request.Builder()
|
|
||||||
.url("https://" + relayHost + "/")
|
|
||||||
.header("Origin", LiveJasmin.baseUrl)
|
|
||||||
.header("User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:63.0) Gecko/20100101 Firefox/63.0")
|
|
||||||
.header("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
|
|
||||||
.header("Accept-Language", "de,en-US;q=0.7,en;q=0.3")
|
|
||||||
.build();
|
|
||||||
relay = client.newWebSocket(request, new WebSocketListener() {
|
|
||||||
boolean streamSocketStarted = false;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onOpen(WebSocket webSocket, Response response) {
|
|
||||||
LOG.trace("relay open {}", model.getName());
|
|
||||||
sendToRelay("{\"event\":\"register\",\"applicationId\":\"" + applicationId
|
|
||||||
+ "\",\"connectionData\":{\"jasmin2App\":true,\"isMobileClient\":false,\"platform\":\"desktop\",\"chatID\":\"freechat\","
|
|
||||||
+ "\"sessionID\":\"" + sessionId + "\"," + "\"jsm2SessionId\":\"" + jsm2SessionId + "\",\"userType\":\"user\"," + "\"performerId\":\""
|
|
||||||
+ model
|
|
||||||
+ "\",\"clientRevision\":\"\",\"proxyIP\":\"\",\"playerVer\":\"nanoPlayerVersion: 3.10.3 appCodeName: Mozilla appName: Netscape appVersion: 5.0 (X11) platform: Linux x86_64\",\"livejasminTvmember\":false,\"newApplet\":true,\"livefeedtype\":null,\"gravityCookieId\":\"\",\"passparam\":\"\",\"brandID\":\"jasmin\",\"cobrandId\":\"\",\"subbrand\":\"livejasmin\",\"siteName\":\"LiveJasmin\",\"siteUrl\":\""+LiveJasmin.baseUrl+"\","
|
|
||||||
+ "\"clientInstanceId\":\"" + clientInstanceId + "\",\"armaVersion\":\"34.10.0\",\"isPassive\":false}}");
|
|
||||||
response.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onMessage(WebSocket webSocket, String text) {
|
|
||||||
LOG.trace("relay <-- {} T{}", model.getName(), text);
|
|
||||||
JSONObject event = new JSONObject(text);
|
|
||||||
if (event.optString("event").equals("accept")) {
|
|
||||||
new Thread(() -> {
|
|
||||||
sendToRelay("{\"event\":\"connectSharedObject\",\"name\":\"data/chat_so\"}");
|
|
||||||
}).start();
|
|
||||||
} else if (event.optString("event").equals("updateSharedObject")) {
|
|
||||||
JSONArray list = event.getJSONArray("list");
|
|
||||||
for (int i = 0; i < list.length(); i++) {
|
|
||||||
JSONObject obj = list.getJSONObject(i);
|
|
||||||
if (obj.optString("name").equals("streamList")) {
|
|
||||||
//LOG.debug(obj.toString(2));
|
|
||||||
streamPath = getStreamPath(obj.getJSONObject("newValue"));
|
|
||||||
} else if(obj.optString("name").equals("isPrivate")
|
|
||||||
|| obj.optString("name").equals("onPrivate")
|
|
||||||
|| obj.optString("name").equals("onPrivateAll")
|
|
||||||
|| obj.optString("name").equals("onPrivateLJ"))
|
|
||||||
{
|
|
||||||
if(obj.optBoolean("newValue")) {
|
|
||||||
// model went private, stop recording
|
|
||||||
LOG.debug("Model {} state changed to private -> stopping download", model.getName());
|
|
||||||
stop();
|
|
||||||
}
|
|
||||||
} else if(obj.optString("name").equals("recommendedBandwidth") || obj.optString("name").equals("realQualityData")) {
|
|
||||||
// stream quality related -> do nothing
|
|
||||||
} else {
|
|
||||||
LOG.debug("{} -{}", model.getName(), obj.toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!streamSocketStarted) {
|
|
||||||
streamSocketStarted = true;
|
|
||||||
sendToRelay("{\"event\":\"call\",\"funcName\":\"makeActive\",\"data\":[]}");
|
|
||||||
new Thread(() -> {
|
|
||||||
try {
|
|
||||||
startStreamSocket();
|
|
||||||
} catch (Exception e) {
|
|
||||||
LOG.error("Couldn't start stream websocket", e);
|
|
||||||
stop();
|
|
||||||
}
|
|
||||||
}).start();
|
|
||||||
}
|
|
||||||
} else if(event.optString("event").equals("call")) {
|
|
||||||
String func = event.optString("funcName");
|
|
||||||
if (func.equals("closeConnection")) {
|
|
||||||
connectionClosed = true;
|
|
||||||
// System.out.println(event.get("data"));
|
|
||||||
stop();
|
|
||||||
} else if (func.equals("addLine")) {
|
|
||||||
// chat message -> ignore
|
|
||||||
} else if (func.equals("receiveInvitation")) {
|
|
||||||
// invitation to private show -> ignore
|
|
||||||
} else {
|
|
||||||
LOG.debug("{} -{}", model.getName(), event.toString());
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if(!event.optString("event").equals("pong"))
|
|
||||||
LOG.debug("{} -{}", model.getName(), event.toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getStreamPath(JSONObject obj) {
|
|
||||||
String streamName = "streams/clonedLiveStream";
|
|
||||||
int height = 0;
|
|
||||||
if(obj.has("streams")) {
|
|
||||||
JSONArray streams = obj.getJSONArray("streams");
|
|
||||||
for (int i = 0; i < streams.length(); i++) {
|
|
||||||
JSONObject stream = streams.getJSONObject(i);
|
|
||||||
int h = stream.optInt("height");
|
|
||||||
if(h > height) {
|
|
||||||
height = h;
|
|
||||||
streamName = stream.getString("streamNameWithFolder");
|
|
||||||
streamName = "free/" + stream.getString("name");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return streamName;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onMessage(WebSocket webSocket, ByteString bytes) {
|
|
||||||
LOG.trace("relay <-- {} B{}", model.getName(), bytes.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onClosed(WebSocket webSocket, int code, String reason) {
|
|
||||||
LOG.trace("relay closed {} {} {}", code, reason, model.getName());
|
|
||||||
stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onFailure(WebSocket webSocket, Throwable t, Response response) {
|
|
||||||
if(!connectionClosed) {
|
|
||||||
LOG.trace("relay failure {}", model.getName(), t);
|
|
||||||
stop();
|
|
||||||
if (response != null) {
|
|
||||||
response.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Subscribe
|
|
||||||
public void handleEvent(Event evt) {
|
|
||||||
if(evt.getType() == Event.Type.MODEL_STATUS_CHANGED) {
|
|
||||||
ModelStateChangedEvent me = (ModelStateChangedEvent) evt;
|
|
||||||
if(me.getModel().equals(model) && me.getOldState() == Model.State.ONLINE) {
|
|
||||||
LOG.debug("Model {} state changed to {} -> stopping download", me.getNewState(), model.getName());
|
|
||||||
stop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void sendToRelay(String msg) {
|
|
||||||
LOG.trace("relay --> {} {}", model.getName(), msg);
|
|
||||||
relay.send(msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void getPerformerDetails(String name) throws IOException {
|
|
||||||
String url = "https://m." + LiveJasmin.baseDomain + "/en/chat-html5/" + name;
|
|
||||||
Request req = new Request.Builder()
|
|
||||||
.url(url)
|
|
||||||
.header("User-Agent", "Mozilla/5.0 (iPhone; CPU OS 10_14 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/11.1.1 Mobile/14E304 Safari/605.1.15")
|
|
||||||
.header("Accept", "application/json,*/*")
|
|
||||||
.header("Accept-Language", "en")
|
|
||||||
.header("Referer", LiveJasmin.baseUrl)
|
|
||||||
.header("X-Requested-With", "XMLHttpRequest")
|
|
||||||
.build();
|
|
||||||
try (Response response = client.execute(req)) {
|
|
||||||
if (response.isSuccessful()) {
|
|
||||||
String body = response.body().string();
|
|
||||||
JSONObject json = new JSONObject(body);
|
|
||||||
// System.out.println(json.toString(2));
|
|
||||||
if (json.optBoolean("success")) {
|
|
||||||
JSONObject data = json.getJSONObject("data");
|
|
||||||
JSONObject config = data.getJSONObject("config");
|
|
||||||
JSONObject armageddonConfig = config.getJSONObject("armageddonConfig");
|
|
||||||
JSONObject chatRoom = config.getJSONObject("chatRoom");
|
|
||||||
sessionId = armageddonConfig.getString("sessionid");
|
|
||||||
jsm2SessionId = armageddonConfig.getString("jsm2session");
|
|
||||||
sb_hash = chatRoom.getString("sb_hash");
|
|
||||||
sb_ip = chatRoom.getString("sb_ip");
|
|
||||||
applicationId = "memberChat/jasmin" + name + sb_hash;
|
|
||||||
relayHost = "dss-relay-" + sb_ip.replace('.', '-') + ".dditscdn.com";
|
|
||||||
streamHost = "dss-live-" + sb_ip.replace('.', '-') + ".dditscdn.com";
|
|
||||||
} else {
|
|
||||||
throw new IOException("Response was not successful: " + body);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw new IOException(response.code() + " - " + response.message());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void startStreamSocket() throws UnsupportedEncodingException {
|
|
||||||
String rtmpUrl = "rtmp://" + sb_ip + "/" + applicationId + "?sessionId-" + sessionId + "|clientInstanceId-" + clientInstanceId;
|
|
||||||
String url = "https://" + streamHost + "/stream/?url=" + URLEncoder.encode(rtmpUrl, "utf-8");
|
|
||||||
url = url += "&stream=" + URLEncoder.encode(streamPath, "utf-8") + "&cid=863621&pid=49247581854";
|
|
||||||
LOG.trace(rtmpUrl);
|
|
||||||
LOG.trace(url);
|
|
||||||
|
|
||||||
Request request = new Request.Builder()
|
|
||||||
.url(url)
|
|
||||||
.header("Origin", LiveJasmin.baseUrl)
|
|
||||||
.header("User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:63.0) Gecko/20100101 Firefox/63.0")
|
|
||||||
.header("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8").header("Accept-Language", "de,en-US;q=0.7,en;q=0.3")
|
|
||||||
.build();
|
|
||||||
stream = client.newWebSocket(request, new WebSocketListener() {
|
|
||||||
FileOutputStream fos;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onOpen(WebSocket webSocket, Response response) {
|
|
||||||
LOG.trace("stream open {}", model.getName());
|
|
||||||
// webSocket.send("{\"event\":\"ping\"}");
|
|
||||||
// webSocket.send("");
|
|
||||||
response.close();
|
|
||||||
try {
|
|
||||||
Files.createDirectories(targetFile.getParentFile().toPath());
|
|
||||||
fos = new FileOutputStream(targetFile);
|
|
||||||
} catch (IOException e) {
|
|
||||||
LOG.error("Couldn't create video file", e);
|
|
||||||
stop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onMessage(WebSocket webSocket, String text) {
|
|
||||||
LOG.trace("stream <-- {} T{}", model.getName(), text);
|
|
||||||
JSONObject event = new JSONObject(text);
|
|
||||||
if(event.optString("eventType").equals("onRandomAccessPoint")) {
|
|
||||||
// send ping
|
|
||||||
sendToRelay("{\"event\":\"ping\"}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onMessage(WebSocket webSocket, ByteString bytes) {
|
|
||||||
//System.out.println("stream <-- B" + bytes.toString());
|
|
||||||
try {
|
|
||||||
fos.write(bytes.toByteArray());
|
|
||||||
} catch (IOException e) {
|
|
||||||
LOG.error("Couldn't write video chunk to file", e);
|
|
||||||
stop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onClosed(WebSocket webSocket, int code, String reason) {
|
|
||||||
LOG.trace("stream closed {} {} {}", code, reason, model.getName());
|
|
||||||
stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onFailure(WebSocket webSocket, Throwable t, Response response) {
|
|
||||||
if(!connectionClosed) {
|
|
||||||
LOG.trace("stream failure {}", model.getName(), t);
|
|
||||||
stop();
|
|
||||||
if (response != null) {
|
|
||||||
response.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void stop() {
|
|
||||||
connectionClosed = true;
|
|
||||||
EventBusHolder.BUS.unregister(this);
|
|
||||||
if (stream != null) {
|
|
||||||
stream.close(1000, "");
|
|
||||||
}
|
|
||||||
if (relay != null) {
|
|
||||||
relay.close(1000, "");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public File getTarget() {
|
|
||||||
return targetFile;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Model getModel() {
|
|
||||||
return model;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Instant getStartTime() {
|
|
||||||
return startTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void postprocess(Recording recording) {
|
|
||||||
}
|
|
||||||
|
|
||||||
@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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -75,8 +75,8 @@ public class HttpServer {
|
||||||
site.init();
|
site.init();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
OnlineMonitor monitor = new OnlineMonitor(recorder);
|
onlineMonitor = new OnlineMonitor(recorder);
|
||||||
monitor.start();
|
onlineMonitor.start();
|
||||||
startHttpServer();
|
startHttpServer();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -120,13 +120,10 @@ public class RecorderServlet extends AbstractCtbrecServlet {
|
||||||
resp.getWriter().write("]}");
|
resp.getWriter().write("]}");
|
||||||
break;
|
break;
|
||||||
case "delete":
|
case "delete":
|
||||||
String path = request.recording;
|
recorder.delete(request.recording);
|
||||||
Recording rec = new Recording();
|
|
||||||
rec.setPath(path);
|
|
||||||
recorder.delete(rec);
|
|
||||||
recAdapter = moshi.adapter(Recording.class);
|
recAdapter = moshi.adapter(Recording.class);
|
||||||
resp.getWriter().write("{\"status\": \"success\", \"msg\": \"List of recordings\", \"recordings\": [");
|
resp.getWriter().write("{\"status\": \"success\", \"msg\": \"List of recordings\", \"recordings\": [");
|
||||||
resp.getWriter().write(recAdapter.toJson(rec));
|
resp.getWriter().write(recAdapter.toJson(request.recording));
|
||||||
resp.getWriter().write("]}");
|
resp.getWriter().write("]}");
|
||||||
break;
|
break;
|
||||||
case "switch":
|
case "switch":
|
||||||
|
@ -178,6 +175,6 @@ public class RecorderServlet extends AbstractCtbrecServlet {
|
||||||
private static class Request {
|
private static class Request {
|
||||||
public String action;
|
public String action;
|
||||||
public Model model;
|
public Model model;
|
||||||
public String recording;
|
public Recording recording;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue