package ctbrec.sites.dreamcam; import com.iheartradio.m3u8.*; import com.iheartradio.m3u8.data.MasterPlaylist; import com.iheartradio.m3u8.data.Playlist; import com.iheartradio.m3u8.data.PlaylistData; import ctbrec.AbstractModel; import ctbrec.Config; import ctbrec.StringUtil; import ctbrec.io.HttpClient; import ctbrec.io.HttpException; import ctbrec.recorder.InvalidPlaylistException; import ctbrec.recorder.download.Download; import ctbrec.recorder.download.StreamSource; import ctbrec.recorder.download.hls.MergedFfmpegHlsDownload; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.text.MessageFormat; import java.time.Duration; import java.time.Instant; import java.util.ArrayList; import java.util.List; import java.util.Locale; import java.util.Objects; import java.util.Optional; import java.util.concurrent.ExecutionException; import javax.xml.bind.JAXBException; import okhttp3.FormBody; import okhttp3.Request; import okhttp3.Response; import org.json.JSONArray; import org.json.JSONObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import static ctbrec.Model.State.*; import static ctbrec.io.HttpConstants.*; import static java.nio.charset.StandardCharsets.UTF_8; public class DreamcamModel extends AbstractModel { private static final Logger LOG = LoggerFactory.getLogger(DreamcamModel.class); private static final String API_URL = "https://bss.dreamcamtrue.com"; private int[] resolution = new int[2]; private JSONObject modelInfo; private boolean VRMode = false; private transient Instant lastInfoRequest = Instant.EPOCH; @Override public boolean isOnline(boolean ignoreCache) throws IOException, ExecutionException, InterruptedException { if (ignoreCache) { try { JSONObject json = getModelInfo(); mapOnlineState(json.optString("broadcastStatus")); } catch (Exception e) { setOnlineState(OFFLINE); } } return onlineState == ONLINE; } private void mapOnlineState(String status) { switch (status) { case "public" -> setOnlineState(ONLINE); case "private" -> setOnlineState(PRIVATE); case "offline" -> setOnlineState(OFFLINE); default -> setOnlineState(OFFLINE); } } @Override public State getOnlineState(boolean failFast) throws IOException, ExecutionException { if (failFast && onlineState != UNKNOWN) { return onlineState; } else { try { JSONObject json = getModelInfo(); mapOnlineState(json.optString("broadcastStatus")); } catch (Exception ex) { setOnlineState(OFFLINE); } return onlineState; } } @Override public List getStreamSources() throws InvalidPlaylistException { List sources = new ArrayList<>(); try { StreamSource src = new StreamSource(); src.mediaPlaylistUrl = getPlaylistUrl(); sources.add(src); } catch (Exception e) { LOG.error("Can not get stream sources for {}: {}", getName(), e.getMessage()); throw new InvalidPlaylistException(e.getMessage()); } return sources; } private String getPlaylistUrl() throws IOException, InvalidPlaylistException { JSONObject json = getModelInfo(); String mediaUrl = ""; if (json.has("streams")) { JSONArray streams = json.getJSONArray("streams"); for (int i=0; i < streams.length(); i++) { JSONObject s = streams.getJSONObject(i); if (s.has("streamType") && s.has("url")) { String streamType = s.getString("streamType"); if (streamType.equals("video2D")) { mediaUrl = s.optString("url"); LOG.trace("PlaylistUrl for {}: {}", getName(), mediaUrl); } } } } if (StringUtil.isBlank(mediaUrl)) { throw new InvalidPlaylistException("Playlist has no media"); } return mediaUrl; } public String getWsUrl() throws IOException { JSONObject json = getModelInfo(); return json.optString("streamUrl").replace("fmp4s://", "wss://"); } public String getChatId() throws IOException { JSONObject json = getModelInfo(); return json.optString("roomChatId"); } @Override public int[] getStreamResolution(boolean failFast) throws ExecutionException { return new int[2]; } private JSONObject getModelInfo() throws IOException { if (Duration.between(lastInfoRequest, Instant.now()).getSeconds() < 5) { modelInfo = Optional.ofNullable(modelInfo).orElse(loadModelInfo()); } else { modelInfo = loadModelInfo(); } return modelInfo; } private JSONObject loadModelInfo() throws IOException { lastInfoRequest = Instant.now(); String url = MessageFormat.format(API_URL + "/api/clients/v1/broadcasts/models/{0}?partnerId=dreamcam_oauth2&show-hidden=true&stream-types=video2D,video3D", getName()); Request req = new Request.Builder() .url(url) .header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent) .header(ACCEPT, MIMETYPE_APPLICATION_JSON) .header(ACCEPT_LANGUAGE, Locale.ENGLISH.getLanguage()) .header(X_REQUESTED_WITH, XML_HTTP_REQUEST) .header(REFERER, getUrl()) .header(ORIGIN, getSite().getBaseUrl()) .build(); try (Response response = getSite().getHttpClient().execute(req)) { if (response.isSuccessful()) { JSONObject json = new JSONObject(response.body().string()); return json; } else { throw new HttpException(response.code(), response.message()); } } } public String getPreviewURL() throws IOException { JSONObject json = getModelInfo(); return json.optString("modelProfilePhotoUrl"); } @Override public boolean follow() throws IOException { return false; } @Override public boolean unfollow() throws IOException { return false; } @Override public void receiveTip(Double tokens) throws IOException { // not implemented } @Override public void invalidateCacheEntries() { resolution = new int[]{0, 0}; lastInfoRequest = Instant.EPOCH; modelInfo = null; } @Override public Download createDownload() { if (Config.getInstance().getSettings().dreamcamVR) { return new DreamcamDownload(getSite().getHttpClient()); } else { return new MergedFfmpegHlsDownload(getSite().getHttpClient()); } } }