Fix construction of LJ stream URL. There is still a problem clientInstanceId
This commit is contained in:
parent
25cfaf220f
commit
ea2c46144b
|
@ -1,15 +1,11 @@
|
||||||
package ctbrec.sites.jasmin;
|
package ctbrec.sites.jasmin;
|
||||||
|
|
||||||
import com.iheartradio.m3u8.*;
|
import com.iheartradio.m3u8.ParseException;
|
||||||
import com.iheartradio.m3u8.data.MasterPlaylist;
|
import com.iheartradio.m3u8.PlaylistException;
|
||||||
import com.iheartradio.m3u8.data.Playlist;
|
|
||||||
import com.iheartradio.m3u8.data.PlaylistData;
|
|
||||||
import com.iheartradio.m3u8.data.StreamInfo;
|
|
||||||
import com.squareup.moshi.JsonReader;
|
import com.squareup.moshi.JsonReader;
|
||||||
import com.squareup.moshi.JsonWriter;
|
import com.squareup.moshi.JsonWriter;
|
||||||
import ctbrec.AbstractModel;
|
import ctbrec.AbstractModel;
|
||||||
import ctbrec.Config;
|
import ctbrec.Config;
|
||||||
import ctbrec.StringUtil;
|
|
||||||
import ctbrec.io.HttpException;
|
import ctbrec.io.HttpException;
|
||||||
import ctbrec.recorder.download.Download;
|
import ctbrec.recorder.download.Download;
|
||||||
import ctbrec.recorder.download.StreamSource;
|
import ctbrec.recorder.download.StreamSource;
|
||||||
|
@ -20,7 +16,6 @@ import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
|
|
||||||
|
@ -29,11 +24,14 @@ import static ctbrec.io.HttpConstants.*;
|
||||||
public class LiveJasminModel extends AbstractModel {
|
public class LiveJasminModel extends AbstractModel {
|
||||||
|
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(LiveJasminModel.class);
|
private static final Logger LOG = LoggerFactory.getLogger(LiveJasminModel.class);
|
||||||
private final Random rng = new Random();
|
|
||||||
private String id;
|
private String id;
|
||||||
private boolean online = false;
|
private boolean online = false;
|
||||||
private int[] resolution;
|
private int[] resolution;
|
||||||
|
|
||||||
|
private final Random rng = new Random();
|
||||||
|
|
||||||
|
private transient LiveJasminModelInfo modelInfo;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isOnline(boolean ignoreCache) throws IOException, ExecutionException, InterruptedException {
|
public boolean isOnline(boolean ignoreCache) throws IOException, ExecutionException, InterruptedException {
|
||||||
if (ignoreCache) {
|
if (ignoreCache) {
|
||||||
|
@ -62,6 +60,7 @@ public class LiveJasminModel extends AbstractModel {
|
||||||
JSONObject data = json.getJSONObject("data");
|
JSONObject data = json.getJSONObject("data");
|
||||||
JSONObject config = data.getJSONObject("config");
|
JSONObject config = data.getJSONObject("config");
|
||||||
JSONObject chatRoom = config.getJSONObject("chatRoom");
|
JSONObject chatRoom = config.getJSONObject("chatRoom");
|
||||||
|
JSONObject armageddonConfig = config.getJSONObject("armageddonConfig");
|
||||||
setId(chatRoom.getString("p_id"));
|
setId(chatRoom.getString("p_id"));
|
||||||
setName(chatRoom.getString("performer_id"));
|
setName(chatRoom.getString("performer_id"));
|
||||||
setDisplayName(chatRoom.getString("display_name"));
|
setDisplayName(chatRoom.getString("display_name"));
|
||||||
|
@ -79,6 +78,14 @@ public class LiveJasminModel extends AbstractModel {
|
||||||
resolution = new int[2];
|
resolution = new int[2];
|
||||||
resolution[0] = config.optInt("streamWidth");
|
resolution[0] = config.optInt("streamWidth");
|
||||||
resolution[1] = config.optInt("streamHeight");
|
resolution[1] = config.optInt("streamHeight");
|
||||||
|
modelInfo = new LiveJasminModelInfo.LiveJasminModelInfoBuilder()
|
||||||
|
.sbIp(chatRoom.getString("sb_ip"))
|
||||||
|
.sbHash(chatRoom.getString("sb_hash"))
|
||||||
|
.sessionId(armageddonConfig.getString("sessionid"))
|
||||||
|
.jsm2session(armageddonConfig.getString("jsm2session"))
|
||||||
|
.performerId(getName())
|
||||||
|
.clientInstanceId(randomClientInstanceId())
|
||||||
|
.build();
|
||||||
online = onlineState == State.ONLINE;
|
online = onlineState == State.ONLINE;
|
||||||
LOG.trace("{} - status:{} {} {} {} {}", getName(), online, onlineState, Arrays.toString(resolution), getUrl(), id);
|
LOG.trace("{} - status:{} {} {} {} {}", getName(), online, onlineState, Arrays.toString(resolution), getUrl(), id);
|
||||||
} else {
|
} else {
|
||||||
|
@ -90,6 +97,14 @@ public class LiveJasminModel extends AbstractModel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String randomClientInstanceId() {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (int i = 0; i < 32; i++) {
|
||||||
|
sb.append(rng.nextInt(9) + 1);
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
public static State mapStatus(int status) {
|
public static State mapStatus(int status) {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 0:
|
case 0:
|
||||||
|
@ -112,85 +127,20 @@ public class LiveJasminModel extends AbstractModel {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<StreamSource> getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException {
|
public List<StreamSource> getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException {
|
||||||
String masterUrl = getMasterPlaylistUrl();
|
|
||||||
LOG.debug("Master playlist: {}", masterUrl);
|
|
||||||
List<StreamSource> streamSources = new ArrayList<>();
|
|
||||||
Request req = new Request.Builder().url(masterUrl).build();
|
|
||||||
try (Response response = site.getHttpClient().execute(req)) {
|
|
||||||
if (response.isSuccessful()) {
|
|
||||||
InputStream inputStream = response.body().byteStream();
|
|
||||||
PlaylistParser parser = new PlaylistParser(inputStream, Format.EXT_M3U, Encoding.UTF_8, ParsingMode.LENIENT);
|
|
||||||
Playlist playlist = parser.parse();
|
|
||||||
MasterPlaylist master = playlist.getMasterPlaylist();
|
|
||||||
for (PlaylistData playlistData : master.getPlaylists()) {
|
|
||||||
StreamSource streamsource = new StreamSource();
|
|
||||||
String baseUrl = masterUrl;
|
|
||||||
baseUrl = baseUrl.substring(0, baseUrl.lastIndexOf('/') + 1);
|
|
||||||
streamsource.mediaPlaylistUrl = baseUrl + playlistData.getUri();
|
|
||||||
if (playlistData.hasStreamInfo()) {
|
|
||||||
StreamInfo info = playlistData.getStreamInfo();
|
|
||||||
streamsource.bandwidth = info.getBandwidth();
|
|
||||||
streamsource.width = info.hasResolution() ? info.getResolution().width : 0;
|
|
||||||
streamsource.height = info.hasResolution() ? info.getResolution().height : 0;
|
|
||||||
} else {
|
|
||||||
streamsource.bandwidth = 0;
|
|
||||||
streamsource.width = 0;
|
|
||||||
streamsource.height = 0;
|
|
||||||
}
|
|
||||||
streamSources.add(streamsource);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw new HttpException(response.code(), response.message());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return streamSources;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getMasterPlaylistUrl() throws IOException {
|
|
||||||
loadModelInfo();
|
loadModelInfo();
|
||||||
|
|
||||||
// generate a fake guest session ID
|
String websocketUrlTemplate = "wss://dss-relay-{ipWithDashes}.dditscdn.com/?random={clientInstanceId}";
|
||||||
byte[] sessionIdRandom = new byte[16];
|
String websocketUrl = websocketUrlTemplate
|
||||||
rng.nextBytes(sessionIdRandom);
|
.replace("{ipWithDashes}", modelInfo.getSbIp().replace('.', '-'))
|
||||||
String sessionId = 'g' + StringUtil.toHexString(sessionIdRandom, 32);
|
.replace("{clientInstanceId}", modelInfo.getClientInstanceId());
|
||||||
|
modelInfo.setWebsocketUrl(websocketUrl);
|
||||||
|
|
||||||
String highResUrl = "https://api-gateway.dditsadn.com/v1/stream/performers/" + getName() + "/streams/free/formats/hls?brandId=jasmin&session=" + sessionId + "&streamName=stream_1280_720_3000";
|
LiveJasminStreamRegistration liveJasminStreamRegistration = new LiveJasminStreamRegistration(site, modelInfo);
|
||||||
String lowResUrl = "https://api-gateway.dditsadn.com/v1/stream/performers/" + getName() + "/streams/free/formats/hls?brandId=jasmin&session=" + sessionId + "&streamName=stream_1280_720_1953";
|
List<StreamSource> streamSources = liveJasminStreamRegistration.getStreamSources();
|
||||||
|
streamSources.stream().max(Comparator.naturalOrder()).ifPresent(ss -> {
|
||||||
String body;
|
new LiveJasminStreamStarter().start(site, modelInfo, (LiveJasminStreamSource) ss);
|
||||||
try {
|
});
|
||||||
body = getMasterPlaylistUrl(highResUrl);
|
return streamSources;
|
||||||
} catch (Exception e) {
|
|
||||||
LOG.debug("High resolution URL not available for {}. Falling back to low res.", getName());
|
|
||||||
body = getMasterPlaylistUrl(lowResUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
JSONObject json = new JSONObject(body);
|
|
||||||
if (json.has("data")) {
|
|
||||||
JSONObject data = json.getJSONObject("data");
|
|
||||||
return data.getString("url");
|
|
||||||
} else {
|
|
||||||
throw new IOException("Response was not successful: " + lowResUrl + "\n" + body);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getMasterPlaylistUrl(String fromUrl) throws IOException {
|
|
||||||
LOG.debug("Getting master playlist URL from {}", fromUrl);
|
|
||||||
Request request = new Request.Builder()
|
|
||||||
.url(fromUrl)
|
|
||||||
.addHeader(USER_AGENT, Config.getInstance().getSettings().httpUserAgentMobile)
|
|
||||||
.addHeader(ACCEPT, MIMETYPE_APPLICATION_JSON)
|
|
||||||
.addHeader(ACCEPT_LANGUAGE, Locale.ENGLISH.getLanguage())
|
|
||||||
.addHeader(REFERER, getUrl())
|
|
||||||
.addHeader(X_REQUESTED_WITH, XML_HTTP_REQUEST)
|
|
||||||
.build();
|
|
||||||
try (Response response = site.getHttpClient().execute(request)) {
|
|
||||||
if (response.isSuccessful()) {
|
|
||||||
return response.body().string();
|
|
||||||
} else {
|
|
||||||
throw new HttpException(response.code(), response.message());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
package ctbrec.sites.jasmin;
|
||||||
|
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
public class LiveJasminModelInfo {
|
||||||
|
private String websocketUrl;
|
||||||
|
private String sbIp;
|
||||||
|
private String sbHash;
|
||||||
|
private String sessionId;
|
||||||
|
private String jsm2session;
|
||||||
|
private String performerId;
|
||||||
|
private String clientInstanceId;
|
||||||
|
}
|
|
@ -0,0 +1,218 @@
|
||||||
|
package ctbrec.sites.jasmin;
|
||||||
|
|
||||||
|
import ctbrec.Config;
|
||||||
|
import ctbrec.recorder.download.StreamSource;
|
||||||
|
import ctbrec.sites.Site;
|
||||||
|
import okhttp3.Request;
|
||||||
|
import okhttp3.Response;
|
||||||
|
import okhttp3.WebSocket;
|
||||||
|
import okhttp3.WebSocketListener;
|
||||||
|
import okio.ByteString;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
import org.json.JSONArray;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.net.URLEncoder;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.BrokenBarrierException;
|
||||||
|
import java.util.concurrent.CyclicBarrier;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.TimeoutException;
|
||||||
|
|
||||||
|
import static ctbrec.io.HttpConstants.USER_AGENT;
|
||||||
|
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||||
|
|
||||||
|
public class LiveJasminStreamRegistration {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(LiveJasminStreamRegistration.class);
|
||||||
|
private static final String KEY_EVENT = "event";
|
||||||
|
private static final String KEY_FUNC_NAME = "funcName";
|
||||||
|
|
||||||
|
private final Site site;
|
||||||
|
private final LiveJasminModelInfo modelInfo;
|
||||||
|
private final CyclicBarrier barrier = new CyclicBarrier(2);
|
||||||
|
|
||||||
|
public LiveJasminStreamRegistration(Site site, LiveJasminModelInfo modelInfo) {
|
||||||
|
this.site = site;
|
||||||
|
this.modelInfo = modelInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<StreamSource> getStreamSources() {
|
||||||
|
var streamSources = new LinkedList<StreamSource>();
|
||||||
|
try {
|
||||||
|
Request webSocketRequest = new Request.Builder()
|
||||||
|
.url(modelInfo.getWebsocketUrl())
|
||||||
|
.addHeader(USER_AGENT, Config.getInstance().getSettings().httpUserAgentMobile)
|
||||||
|
.build();
|
||||||
|
LOG.debug("Websocket: {}", modelInfo.getWebsocketUrl());
|
||||||
|
site.getHttpClient().newWebSocket(webSocketRequest, new WebSocketListener() {
|
||||||
|
@Override
|
||||||
|
public void onOpen(@NotNull WebSocket webSocket, @NotNull Response response) {
|
||||||
|
LOG.debug("onOpen");
|
||||||
|
JSONObject register = new JSONObject()
|
||||||
|
.put(KEY_EVENT, "register")
|
||||||
|
.put("applicationId", "memberChat/jasmin" + modelInfo.getPerformerId() + modelInfo.getSbHash())
|
||||||
|
.put("connectionData", new JSONObject()
|
||||||
|
.put("jasmin2App", false)
|
||||||
|
.put("isMobileClient", true)
|
||||||
|
.put("platform", "mobile")
|
||||||
|
.put("chatID", "freechat")
|
||||||
|
.put("sessionID", modelInfo.getSessionId())
|
||||||
|
.put("jsm2SessionId", modelInfo.getJsm2session())
|
||||||
|
.put("userType", "user")
|
||||||
|
.put("performerId", modelInfo.getPerformerId())
|
||||||
|
.put("clientRevision", "")
|
||||||
|
.put("playerVer", "nanoPlayerVersion: 4.12.1 appCodeName: Mozilla appName: Netscape appVersion: 5.0 (iPad; CPU OS 10_14 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/11.1.1 Mobile/15E148 Safari/605.1.15 platform: iPad")
|
||||||
|
.put("livejasminTvmember", false)
|
||||||
|
.put("newApplet", true)
|
||||||
|
.put("livefeedtype", JSONObject.NULL)
|
||||||
|
.put("gravityCookieId", "")
|
||||||
|
.put("passparam", "")
|
||||||
|
.put("clientInstanceId", modelInfo.getClientInstanceId())
|
||||||
|
.put("armaVersion", "39.158.0")
|
||||||
|
.put("isPassive", false)
|
||||||
|
.put("brandID", "jasmin")
|
||||||
|
.put("cobrandId", "")
|
||||||
|
.put("subbrand", "livejasmin")
|
||||||
|
.put("siteName", "LiveJasmin")
|
||||||
|
.put("siteUrl", "https://m." + LiveJasmin.baseDomain)
|
||||||
|
.put("chatHistoryRequired", false)
|
||||||
|
.put("peekPatternJsm2", true)
|
||||||
|
);
|
||||||
|
webSocket.send(register.toString());
|
||||||
|
webSocket.send(new JSONObject().put(KEY_EVENT, "ping").toString());
|
||||||
|
webSocket.send(new JSONObject()
|
||||||
|
.put(KEY_EVENT, "call")
|
||||||
|
.put(KEY_FUNC_NAME, "makeActive")
|
||||||
|
.put("data", new JSONArray())
|
||||||
|
.toString());
|
||||||
|
webSocket.send(new JSONObject()
|
||||||
|
.put(KEY_EVENT, "call")
|
||||||
|
.put(KEY_FUNC_NAME, "setVideo")
|
||||||
|
.put("data", new JSONArray()
|
||||||
|
.put(JSONObject.NULL)
|
||||||
|
.put(false)
|
||||||
|
.put(true)
|
||||||
|
.put(modelInfo.getJsm2session())
|
||||||
|
)
|
||||||
|
.toString());
|
||||||
|
webSocket.send(new JSONObject()
|
||||||
|
.put(KEY_EVENT, "connectSharedObject")
|
||||||
|
.put("name", "data/chat_so")
|
||||||
|
.toString());
|
||||||
|
//webSocket.close(1000, "Good bye");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFailure(@NotNull WebSocket webSocket, @NotNull Throwable t, @Nullable Response response) {
|
||||||
|
LOG.error("onFailure", t);
|
||||||
|
awaitBarrier();
|
||||||
|
webSocket.close(1000, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onMessage(@NotNull WebSocket webSocket, @NotNull String text) {
|
||||||
|
JSONObject message = new JSONObject(text);
|
||||||
|
if (message.opt(KEY_EVENT).equals("pong")) {
|
||||||
|
new Thread(() -> {
|
||||||
|
try {
|
||||||
|
Thread.sleep(message.optInt("nextPing"));
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
}
|
||||||
|
webSocket.send(new JSONObject().put(KEY_EVENT, "ping").toString());
|
||||||
|
}).start();
|
||||||
|
} else if (message.optString(KEY_EVENT).equals("updateSharedObject") && message.optString("name").equals("data/chat_so")) {
|
||||||
|
LOG.trace(message.toString(2));
|
||||||
|
JSONArray list = message.getJSONArray("list");
|
||||||
|
for (int i = 0; i < list.length(); i++) {
|
||||||
|
JSONObject attribute = list.getJSONObject(i);
|
||||||
|
if (attribute.optString("name").equals("streamList")) {
|
||||||
|
JSONObject value = attribute.getJSONObject("newValue");
|
||||||
|
JSONObject patterns = value.getJSONObject("patterns");
|
||||||
|
String freePattern = patterns.getString("free");
|
||||||
|
JSONArray streams = value.getJSONArray("streams");
|
||||||
|
for (int j = 0; j < streams.length(); j++) {
|
||||||
|
JSONObject stream = streams.getJSONObject(j);
|
||||||
|
addStreamSource(streamSources, freePattern, stream);
|
||||||
|
}
|
||||||
|
webSocket.close(1000, "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (!message.optString(KEY_FUNC_NAME).equals("chatHistory")) {
|
||||||
|
LOG.trace("onMessageT {}", new JSONObject(text).toString(2));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onMessage(@NotNull WebSocket webSocket, @NotNull ByteString bytes) {
|
||||||
|
LOG.trace("onMessageB");
|
||||||
|
super.onMessage(webSocket, bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onClosed(@NotNull WebSocket webSocket, int code, @NotNull String reason) {
|
||||||
|
LOG.debug("onClosed {} {}", code, reason);
|
||||||
|
super.onClosed(webSocket, code, reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onClosing(@NotNull WebSocket webSocket, int code, @NotNull String reason) {
|
||||||
|
LOG.trace("onClosing {} {}", code, reason);
|
||||||
|
awaitBarrier();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
LOG.debug("Waiting for websocket to return");
|
||||||
|
awaitBarrier();
|
||||||
|
LOG.debug("Websocket is done. Stream sources {}", streamSources);
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOG.error("Couldn't determine stream sources", e);
|
||||||
|
}
|
||||||
|
return streamSources;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addStreamSource(LinkedList<StreamSource> streamSources, String pattern, JSONObject stream) {
|
||||||
|
int w = stream.getInt("width");
|
||||||
|
int h = stream.getInt("height");
|
||||||
|
int bitrate = stream.getInt("bitrate") * 1024;
|
||||||
|
String name = stream.getString("name");
|
||||||
|
String streamName = pattern.replace("{$streamname}", name);
|
||||||
|
|
||||||
|
String rtmpUrl = "rtmp://{ip}/memberChat/jasmin{modelName}{sb_hash}?sessionId-{sessionId}|clientInstanceId-{clientInstanceId}"
|
||||||
|
.replace("{ip}", modelInfo.getSbIp())
|
||||||
|
.replace("{modelName}", modelInfo.getPerformerId())
|
||||||
|
.replace("{sb_hash}", modelInfo.getSbHash())
|
||||||
|
.replace("{sessionId}", modelInfo.getSessionId())
|
||||||
|
.replace("{clientInstanceId}", modelInfo.getClientInstanceId());
|
||||||
|
|
||||||
|
String hlsUrl = "https://dss-hls-{ipWithDashes}.dditscdn.com/h5live/http/playlist.m3u8?url={rtmpUrl}&stream={streamName}"
|
||||||
|
.replace("{ipWithDashes}", modelInfo.getSbIp().replace('.', '-'))
|
||||||
|
.replace("{rtmpUrl}", URLEncoder.encode(rtmpUrl, UTF_8))
|
||||||
|
.replace("{streamName}", URLEncoder.encode(streamName, UTF_8));
|
||||||
|
|
||||||
|
LiveJasminStreamSource streamSource = new LiveJasminStreamSource();
|
||||||
|
streamSource.mediaPlaylistUrl = hlsUrl;
|
||||||
|
streamSource.width = w;
|
||||||
|
streamSource.height = h;
|
||||||
|
streamSource.bandwidth = bitrate;
|
||||||
|
streamSource.rtmpUrl = rtmpUrl;
|
||||||
|
streamSource.streamName = streamName;
|
||||||
|
streamSources.add(streamSource);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void awaitBarrier() {
|
||||||
|
try {
|
||||||
|
barrier.await(10, TimeUnit.SECONDS);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
LOG.error(e.getLocalizedMessage(), e);
|
||||||
|
} catch (TimeoutException | BrokenBarrierException e) {
|
||||||
|
LOG.error(e.getLocalizedMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
package ctbrec.sites.jasmin;
|
||||||
|
|
||||||
|
import ctbrec.recorder.download.StreamSource;
|
||||||
|
|
||||||
|
public class LiveJasminStreamSource extends StreamSource {
|
||||||
|
public String rtmpUrl;
|
||||||
|
public String streamName;
|
||||||
|
}
|
|
@ -0,0 +1,107 @@
|
||||||
|
package ctbrec.sites.jasmin;
|
||||||
|
|
||||||
|
import ctbrec.Config;
|
||||||
|
import ctbrec.sites.Site;
|
||||||
|
import okhttp3.Request;
|
||||||
|
import okhttp3.Response;
|
||||||
|
import okhttp3.WebSocket;
|
||||||
|
import okhttp3.WebSocketListener;
|
||||||
|
import okio.ByteString;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URLEncoder;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.concurrent.BrokenBarrierException;
|
||||||
|
import java.util.concurrent.CyclicBarrier;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.TimeoutException;
|
||||||
|
|
||||||
|
import static ctbrec.io.HttpConstants.USER_AGENT;
|
||||||
|
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||||
|
|
||||||
|
public class LiveJasminStreamStarter {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(LiveJasminStreamStarter.class);
|
||||||
|
private final CyclicBarrier barrier = new CyclicBarrier(2);
|
||||||
|
|
||||||
|
void start(Site site, LiveJasminModelInfo modelInfo, LiveJasminStreamSource ss) {
|
||||||
|
try {
|
||||||
|
String websocketUrl = "wss://dss-live-{ipWithDashes}.dditscdn.com/h5live/http/playlist.m3u8?url={rtmpUrl}&stream={streamName}"
|
||||||
|
.replace("{ipWithDashes}", modelInfo.getSbIp().replace('.', '-'))
|
||||||
|
.replace("{rtmpUrl}", URLEncoder.encode(ss.rtmpUrl, UTF_8))
|
||||||
|
.replace("{streamName}", URLEncoder.encode(ss.streamName, UTF_8));
|
||||||
|
|
||||||
|
Request webSocketRequest = new Request.Builder()
|
||||||
|
.url(websocketUrl)
|
||||||
|
.addHeader(USER_AGENT, Config.getInstance().getSettings().httpUserAgentMobile)
|
||||||
|
.build();
|
||||||
|
LOG.debug("Websocket: {}", websocketUrl);
|
||||||
|
site.getHttpClient().newWebSocket(webSocketRequest, new WebSocketListener() {
|
||||||
|
@Override
|
||||||
|
public void onOpen(@NotNull WebSocket webSocket, @NotNull Response response) {
|
||||||
|
LOG.debug("onOpen");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFailure(@NotNull WebSocket webSocket, @NotNull Throwable t, @Nullable Response response) {
|
||||||
|
String body = Optional.ofNullable(response).map(Response::body).map(responseBody -> {
|
||||||
|
try {
|
||||||
|
return responseBody.string();
|
||||||
|
} catch (IOException e) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}).orElse("");
|
||||||
|
LOG.error("onFailure Body:[{}]", body, t);
|
||||||
|
awaitBarrier();
|
||||||
|
webSocket.close(1000, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onMessage(@NotNull WebSocket webSocket, @NotNull String text) {
|
||||||
|
LOG.debug("{}", new JSONObject(text).toString(2));
|
||||||
|
webSocket.close(1000, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onMessage(@NotNull WebSocket webSocket, @NotNull ByteString bytes) {
|
||||||
|
LOG.trace("onMessageB");
|
||||||
|
super.onMessage(webSocket, bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onClosed(@NotNull WebSocket webSocket, int code, @NotNull String reason) {
|
||||||
|
LOG.debug("onClosed {} {}", code, reason);
|
||||||
|
super.onClosed(webSocket, code, reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onClosing(@NotNull WebSocket webSocket, int code, @NotNull String reason) {
|
||||||
|
LOG.trace("onClosing {} {}", code, reason);
|
||||||
|
awaitBarrier();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
LOG.debug("Waiting for websocket to return");
|
||||||
|
awaitBarrier();
|
||||||
|
LOG.debug("Websocket is done.");
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOG.error("Couldn't start stream", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void awaitBarrier() {
|
||||||
|
try {
|
||||||
|
barrier.await(10, TimeUnit.SECONDS);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
LOG.error(e.getLocalizedMessage(), e);
|
||||||
|
} catch (TimeoutException | BrokenBarrierException e) {
|
||||||
|
LOG.error(e.getLocalizedMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -156,6 +156,12 @@
|
||||||
<artifactId>jetty-rewrite</artifactId>
|
<artifactId>jetty-rewrite</artifactId>
|
||||||
<version>[9.4.19.v20190610,9.99.99)</version>
|
<version>[9.4.19.v20190610,9.99.99)</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
|
<version>1.18.24</version>
|
||||||
|
<optional>true</optional>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</dependencyManagement>
|
</dependencyManagement>
|
||||||
|
|
||||||
|
@ -175,6 +181,11 @@
|
||||||
<artifactId>mockito-inline</artifactId>
|
<artifactId>mockito-inline</artifactId>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
|
<optional>true</optional>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
</project>
|
</project>
|
||||||
|
|
Loading…
Reference in New Issue