package org.schabi.newpipe.extractor.services.media_ccc.extractors; import static org.schabi.newpipe.extractor.services.media_ccc.extractors.MediaCCCParsingHelper.getThumbnailsFromLiveStreamItem; import static org.schabi.newpipe.extractor.stream.AudioStream.UNKNOWN_BITRATE; import static org.schabi.newpipe.extractor.stream.Stream.ID_UNKNOWN; import com.grack.nanojson.JsonArray; import com.grack.nanojson.JsonObject; import org.schabi.newpipe.extractor.Image; import org.schabi.newpipe.extractor.MediaFormat; import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.downloader.Downloader; import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.extractor.exceptions.ParsingException; import org.schabi.newpipe.extractor.linkhandler.LinkHandler; import org.schabi.newpipe.extractor.stream.AudioStream; import org.schabi.newpipe.extractor.stream.DeliveryMethod; import org.schabi.newpipe.extractor.stream.Description; import org.schabi.newpipe.extractor.stream.Stream; import org.schabi.newpipe.extractor.stream.StreamExtractor; import org.schabi.newpipe.extractor.stream.StreamType; import org.schabi.newpipe.extractor.stream.VideoStream; import java.io.IOException; import java.util.Collections; import java.util.List; import java.util.function.Function; import java.util.stream.Collectors; import javax.annotation.Nonnull; public class MediaCCCLiveStreamExtractor extends StreamExtractor { private static final String STREAMS = "streams"; private static final String URLS = "urls"; private static final String URL = "url"; private JsonObject conference = null; private String group = ""; private JsonObject room = null; public MediaCCCLiveStreamExtractor(final StreamingService service, final LinkHandler linkHandler) { super(service, linkHandler); } @Override public void onFetchPage(@Nonnull final Downloader downloader) throws IOException, ExtractionException { final JsonArray doc = MediaCCCParsingHelper.getLiveStreams(downloader, getExtractorLocalization()); // Find the correct room for (int c = 0; c < doc.size(); c++) { final JsonObject conferenceObject = doc.getObject(c); final JsonArray groups = conferenceObject.getArray("groups"); for (int g = 0; g < groups.size(); g++) { final String groupObject = groups.getObject(g).getString("group"); final JsonArray rooms = groups.getObject(g).getArray("rooms"); for (int r = 0; r < rooms.size(); r++) { final JsonObject roomObject = rooms.getObject(r); if (getId().equals(conferenceObject.getString("slug") + "/" + roomObject.getString("slug"))) { conference = conferenceObject; group = groupObject; room = roomObject; return; } } } } throw new ExtractionException("Could not find room matching id: '" + getId() + "'"); } @Nonnull @Override public String getName() throws ParsingException { return room.getString("display"); } @Nonnull @Override public List getThumbnails() throws ParsingException { return getThumbnailsFromLiveStreamItem(room); } @Nonnull @Override public Description getDescription() throws ParsingException { return new Description(conference.getString("description") + " - " + group, Description.PLAIN_TEXT); } @Override public long getViewCount() { return -1; } @Nonnull @Override public String getUploaderUrl() throws ParsingException { return "https://streaming.media.ccc.de/" + conference.getString("slug"); } @Nonnull @Override public String getUploaderName() throws ParsingException { return conference.getString("conference"); } /** * Get the URL of the first DASH stream found. * *

* There can be several DASH streams, so the URL of the first one found is returned by this * method. *

* *

* You can find the other DASH video streams by using {@link #getVideoStreams()} *

*/ @Nonnull @Override public String getDashMpdUrl() throws ParsingException { return getManifestOfDeliveryMethodWanted("dash"); } /** * Get the URL of the first HLS stream found. * *

* There can be several HLS streams, so the URL of the first one found is returned by this * method. *

* *

* You can find the other HLS video streams by using {@link #getVideoStreams()} *

*/ @Nonnull @Override public String getHlsUrl() { return getManifestOfDeliveryMethodWanted("hls"); } @Nonnull private String getManifestOfDeliveryMethodWanted(@Nonnull final String deliveryMethod) { return room.getArray(STREAMS).stream() .filter(JsonObject.class::isInstance) .map(JsonObject.class::cast) .map(streamObject -> streamObject.getObject(URLS)) .filter(urls -> urls.has(deliveryMethod)) .map(urls -> urls.getObject(deliveryMethod).getString(URL, "")) .findFirst() .orElse(""); } @Override public List getAudioStreams() throws IOException, ExtractionException { return getStreams("audio", dto -> { final AudioStream.Builder builder = new AudioStream.Builder() .setId(dto.urlValue.getString("tech", ID_UNKNOWN)) .setContent(dto.urlValue.getString(URL), true) .setAverageBitrate(UNKNOWN_BITRATE); if ("hls".equals(dto.urlKey)) { // We don't know with the type string what media format will // have HLS streams. // However, the tech string may contain some information // about the media format used. return builder.setDeliveryMethod(DeliveryMethod.HLS) .build(); } return builder.setMediaFormat(MediaFormat.getFromSuffix(dto.urlKey)) .build(); }); } @Override public List getVideoStreams() throws IOException, ExtractionException { return getStreams("video", dto -> { final JsonArray videoSize = dto.streamJsonObj.getArray("videoSize"); final VideoStream.Builder builder = new VideoStream.Builder() .setId(dto.urlValue.getString("tech", ID_UNKNOWN)) .setContent(dto.urlValue.getString(URL), true) .setIsVideoOnly(false) .setResolution(videoSize.getInt(0) + "x" + videoSize.getInt(1)); if ("hls".equals(dto.urlKey)) { // We don't know with the type string what media format will // have HLS streams. // However, the tech string may contain some information // about the media format used. return builder.setDeliveryMethod(DeliveryMethod.HLS) .build(); } return builder.setMediaFormat(MediaFormat.getFromSuffix(dto.urlKey)) .build(); }); } /** * This is just an internal class used in {@link #getStreams(String, Function)} to tie together * the stream json object, its URL key and its URL value. An object of this class would be * temporary and the three values it holds would be converted to a proper {@link Stream} * object based on the wanted stream type. */ private static final class MediaCCCLiveStreamMapperDTO { final JsonObject streamJsonObj; final String urlKey; final JsonObject urlValue; MediaCCCLiveStreamMapperDTO(final JsonObject streamJsonObj, final String urlKey, final JsonObject urlValue) { this.streamJsonObj = streamJsonObj; this.urlKey = urlKey; this.urlValue = urlValue; } } private List getStreams( @Nonnull final String streamType, @Nonnull final Function converter) { return room.getArray(STREAMS).stream() // Ensure that we use only process JsonObjects .filter(JsonObject.class::isInstance) .map(JsonObject.class::cast) // Only process streams of requested type .filter(streamJsonObj -> streamType.equals(streamJsonObj.getString("type"))) // Flatmap Urls and ensure that we use only process JsonObjects .flatMap(streamJsonObj -> streamJsonObj.getObject(URLS).entrySet().stream() .filter(e -> e.getValue() instanceof JsonObject) .map(e -> new MediaCCCLiveStreamMapperDTO( streamJsonObj, e.getKey(), (JsonObject) e.getValue()))) // The DASH manifest will be extracted with getDashMpdUrl .filter(dto -> !"dash".equals(dto.urlKey)) // Convert .map(converter) .collect(Collectors.toList()); } @Override public List getVideoOnlyStreams() { return Collections.emptyList(); } @Override public StreamType getStreamType() throws ParsingException { return StreamType.LIVE_STREAM; } @Nonnull @Override public String getCategory() { return group; } }