From 55a2af20ad10588f4a319a0358cc05517c15aa72 Mon Sep 17 00:00:00 2001 From: TobiGr Date: Sat, 19 Aug 2023 19:53:32 +0200 Subject: [PATCH 1/2] [media.ccc.de] Fix wrong ListLinkHandlerFactories for kiosks Regression introduced in #1082 --- .../services/media_ccc/MediaCCCService.java | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/MediaCCCService.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/MediaCCCService.java index 3e3a4726e..43cb434e2 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/MediaCCCService.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/MediaCCCService.java @@ -29,6 +29,8 @@ import org.schabi.newpipe.extractor.services.media_ccc.extractors.MediaCCCSearch import org.schabi.newpipe.extractor.services.media_ccc.extractors.MediaCCCStreamExtractor; import org.schabi.newpipe.extractor.services.media_ccc.linkHandler.MediaCCCConferenceLinkHandlerFactory; import org.schabi.newpipe.extractor.services.media_ccc.linkHandler.MediaCCCConferencesListLinkHandlerFactory; +import org.schabi.newpipe.extractor.services.media_ccc.linkHandler.MediaCCCLiveListLinkHandlerFactory; +import org.schabi.newpipe.extractor.services.media_ccc.linkHandler.MediaCCCRecentListLinkHandlerFactory; import org.schabi.newpipe.extractor.services.media_ccc.linkHandler.MediaCCCSearchQueryHandlerFactory; import org.schabi.newpipe.extractor.services.media_ccc.linkHandler.MediaCCCStreamLinkHandlerFactory; import org.schabi.newpipe.extractor.stream.StreamExtractor; @@ -112,37 +114,41 @@ public class MediaCCCService extends StreamingService { @Override public KioskList getKioskList() throws ExtractionException { final KioskList list = new KioskList(this); - final ListLinkHandlerFactory h = MediaCCCConferencesListLinkHandlerFactory.getInstance(); + final ListLinkHandlerFactory conferenceLLHF = MediaCCCConferencesListLinkHandlerFactory + .getInstance(); + final ListLinkHandlerFactory recentLLHF = MediaCCCRecentListLinkHandlerFactory + .getInstance(); + final ListLinkHandlerFactory liveLLHF = MediaCCCLiveListLinkHandlerFactory.getInstance(); // add kiosks here e.g.: try { list.addKioskEntry( (streamingService, url, kioskId) -> new MediaCCCConferenceKiosk( MediaCCCService.this, - h.fromUrl(url), + conferenceLLHF.fromUrl(url), kioskId ), - h, + conferenceLLHF, MediaCCCConferenceKiosk.KIOSK_ID ); list.addKioskEntry( (streamingService, url, kioskId) -> new MediaCCCRecentKiosk( MediaCCCService.this, - h.fromUrl(url), + recentLLHF.fromUrl(url), kioskId ), - h, + recentLLHF, MediaCCCRecentKiosk.KIOSK_ID ); list.addKioskEntry( (streamingService, url, kioskId) -> new MediaCCCLiveStreamKiosk( MediaCCCService.this, - h.fromUrl(url), + liveLLHF.fromUrl(url), kioskId ), - h, + liveLLHF, MediaCCCLiveStreamKiosk.KIOSK_ID ); From a97c0580902f4cc105a477c0e49826321a58bbfe Mon Sep 17 00:00:00 2001 From: TobiGr Date: Sat, 19 Aug 2023 20:01:45 +0200 Subject: [PATCH 2/2] [media.ccc.de] Live stream kiosk: detect break "talks" segements Add and improve tests for MediaCCCLiveStreamKioskExtractor: - test stream items if a live stream is running - use mock tests to check live talk extraction and testing conferences --- .../MediaCCCLiveStreamKioskExtractor.java | 25 ++++- .../extractors/MediaCCCParsingHelper.java | 9 ++ .../MediaCCCLiveStreamListExtractorTest.java | 97 +++++++++++++++++-- .../services/media_ccc/MediaCCCTestUtils.java | 15 +++ .../live/preparation/generated_mock_0.json | 47 +++++++++ .../kiosk/live/running/generated_mock_0.json | 47 +++++++++ 6 files changed, 228 insertions(+), 12 deletions(-) create mode 100644 extractor/src/test/java/org/schabi/newpipe/extractor/services/media_ccc/MediaCCCTestUtils.java create mode 100644 extractor/src/test/resources/org/schabi/newpipe/extractor/services/media.ccc.de/kiosk/live/preparation/generated_mock_0.json create mode 100644 extractor/src/test/resources/org/schabi/newpipe/extractor/services/media.ccc.de/kiosk/live/running/generated_mock_0.json diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCLiveStreamKioskExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCLiveStreamKioskExtractor.java index f6c5ac862..c7f6e44e8 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCLiveStreamKioskExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCLiveStreamKioskExtractor.java @@ -9,6 +9,7 @@ import org.schabi.newpipe.extractor.stream.StreamType; import javax.annotation.Nonnull; import javax.annotation.Nullable; +import java.time.OffsetDateTime; import java.util.List; import static org.schabi.newpipe.extractor.services.media_ccc.extractors.MediaCCCParsingHelper.getThumbnailsFromLiveStreamItem; @@ -19,17 +20,25 @@ public class MediaCCCLiveStreamKioskExtractor implements StreamInfoItemExtractor private final String group; private final JsonObject roomInfo; + @Nonnull + private final JsonObject currentTalk; + public MediaCCCLiveStreamKioskExtractor(final JsonObject conferenceInfo, final String group, final JsonObject roomInfo) { this.conferenceInfo = conferenceInfo; this.group = group; this.roomInfo = roomInfo; + this.currentTalk = roomInfo.getObject("talks").getObject("current"); } @Override public String getName() throws ParsingException { - return roomInfo.getObject("talks").getObject("current").getString("title"); + if (isBreak()) { + return roomInfo.getString("display") + " - Pause"; + } else { + return currentTalk.getString("title"); + } } @Override @@ -95,6 +104,18 @@ public class MediaCCCLiveStreamKioskExtractor implements StreamInfoItemExtractor @Nullable @Override public DateWrapper getUploadDate() throws ParsingException { - return null; + if (isBreak()) { + return new DateWrapper(OffsetDateTime.parse(currentTalk.getString("fstart"))); + } else { + return new DateWrapper(OffsetDateTime.parse(conferenceInfo.getString("startsAt"))); + } + } + + /** + * Whether the current "talk" is a talk or a pause. + */ + private boolean isBreak() { + return OffsetDateTime.parse(currentTalk.getString("fstart")).isBefore(OffsetDateTime.now()) + || "gap".equals(currentTalk.getString("special")); } } diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCParsingHelper.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCParsingHelper.java index 2a4ea6a9f..b7ca38866 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCParsingHelper.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCParsingHelper.java @@ -82,6 +82,15 @@ public final class MediaCCCParsingHelper { return liveStreams; } + /** + *

Reset cached live stream data.

+ * This is a temporary method which can be used to reset the cached live stream data until a + * caching policy for {@link #getLiveStreams(Downloader, Localization)} is implemented. + */ + public static void resetCachedLiveStreamInfo() { + liveStreams = null; + } + /** * Get an {@link Image} list from a given image logo URL. * diff --git a/extractor/src/test/java/org/schabi/newpipe/extractor/services/media_ccc/MediaCCCLiveStreamListExtractorTest.java b/extractor/src/test/java/org/schabi/newpipe/extractor/services/media_ccc/MediaCCCLiveStreamListExtractorTest.java index 67e05fbe2..a9a3be42a 100644 --- a/extractor/src/test/java/org/schabi/newpipe/extractor/services/media_ccc/MediaCCCLiveStreamListExtractorTest.java +++ b/extractor/src/test/java/org/schabi/newpipe/extractor/services/media_ccc/MediaCCCLiveStreamListExtractorTest.java @@ -2,29 +2,106 @@ package org.schabi.newpipe.extractor.services.media_ccc; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import org.schabi.newpipe.downloader.DownloaderFactory; import org.schabi.newpipe.downloader.DownloaderTestImpl; +import org.schabi.newpipe.downloader.MockOnly; import org.schabi.newpipe.extractor.InfoItem; +import org.schabi.newpipe.extractor.ListExtractor; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.kiosk.KioskExtractor; +import org.schabi.newpipe.extractor.services.media_ccc.extractors.MediaCCCLiveStreamKiosk; import java.util.List; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.schabi.newpipe.extractor.ServiceList.MediaCCC; +import static org.schabi.newpipe.extractor.services.DefaultTests.defaultTestListOfItems; public class MediaCCCLiveStreamListExtractorTest { - private static KioskExtractor extractor; - @BeforeAll - public static void setUpClass() throws Exception { - NewPipe.init(DownloaderTestImpl.getInstance()); - extractor = MediaCCC.getKioskList().getExtractorById("live", null); - extractor.fetchPage(); + private static final String RESOURCE_PATH = DownloaderFactory.RESOURCE_PATH + + "services/media.ccc.de/kiosk/live/"; + private static final String LIVE_KIOSK_ID = MediaCCCLiveStreamKiosk.KIOSK_ID; + + /** + * Test against the media.ccc.de livestream API endpoint + * and ensure that no exceptions are thrown. + */ + public static class LiveDataTest { + private static KioskExtractor extractor; + + @BeforeAll + public static void setUpClass() throws Exception { + MediaCCCTestUtils.ensureStateless(); + NewPipe.init(DownloaderTestImpl.getInstance()); + extractor = MediaCCC.getKioskList().getExtractorById(LIVE_KIOSK_ID, null); + extractor.fetchPage(); + } + + @Test + void getConferencesListTest() throws Exception { + final ListExtractor.InfoItemsPage liveStreamPage = extractor.getInitialPage(); + final List items = liveStreamPage.getItems(); + if (items.isEmpty()) { + // defaultTestListOfItems() fails, if items is empty. + // This can happen if there are no current live streams. + // In this case, we just check if an exception was thrown + assertTrue(liveStreamPage.getErrors().isEmpty()); + } else { + defaultTestListOfItems(MediaCCC, items, liveStreamPage.getErrors()); + } + } } - @Test - public void getConferencesListTest() throws Exception { - final List items = extractor.getInitialPage().getItems(); - // just test if there is an exception thrown + /** + * Test conferences which are available via the API for C3voc internal testing, + * but not intended to be shown to users. + */ + @MockOnly("The live stream API returns different data depending on if and what conferences" + + " are running. The PreparationTest tests a conference which is used " + + "for internal testing.") + public static class PreparationTest { + private static KioskExtractor extractor; + + @BeforeAll + public static void setUpClass() throws Exception { + MediaCCCTestUtils.ensureStateless(); + NewPipe.init(DownloaderFactory.getDownloader(RESOURCE_PATH + "preparation")); + extractor = MediaCCC.getKioskList().getExtractorById(LIVE_KIOSK_ID, null); + extractor.fetchPage(); + } + + @Test + void getConferencesListTest() throws Exception { + // Testing conferences and the corresponding talks should not be extracted. + assertTrue(extractor.getInitialPage().getItems().isEmpty()); + } } + /** + * Test a running conference. + */ + @MockOnly("The live stream API returns different data depending on if and what conferences" + + " are running. Using mocks to ensure that there are conferences & talks to extract.") + public static class LiveConferenceTest { + private static KioskExtractor extractor; + + @BeforeAll + public static void setUpClass() throws Exception { + MediaCCCTestUtils.ensureStateless(); + NewPipe.init(DownloaderFactory.getDownloader(RESOURCE_PATH + "running")); + extractor = MediaCCC.getKioskList().getExtractorById(LIVE_KIOSK_ID, null); + extractor.fetchPage(); + } + + @Test + void getConferencesListTest() throws Exception { + final ListExtractor.InfoItemsPage liveStreamPage = extractor.getInitialPage(); + final List items = liveStreamPage.getItems(); + assertEquals(6, items.size()); + defaultTestListOfItems(MediaCCC, items, liveStreamPage.getErrors()); + + } + } } diff --git a/extractor/src/test/java/org/schabi/newpipe/extractor/services/media_ccc/MediaCCCTestUtils.java b/extractor/src/test/java/org/schabi/newpipe/extractor/services/media_ccc/MediaCCCTestUtils.java new file mode 100644 index 000000000..e25f203b1 --- /dev/null +++ b/extractor/src/test/java/org/schabi/newpipe/extractor/services/media_ccc/MediaCCCTestUtils.java @@ -0,0 +1,15 @@ +package org.schabi.newpipe.extractor.services.media_ccc; + +import org.schabi.newpipe.extractor.services.media_ccc.extractors.MediaCCCParsingHelper; + +public final class MediaCCCTestUtils { + + /** + * Clears static media.ccc.de states. + *

This method needs to be called in every class before running and recording mock tests.

+ */ + public static void ensureStateless() { + MediaCCCParsingHelper.resetCachedLiveStreamInfo(); + } + +} diff --git a/extractor/src/test/resources/org/schabi/newpipe/extractor/services/media.ccc.de/kiosk/live/preparation/generated_mock_0.json b/extractor/src/test/resources/org/schabi/newpipe/extractor/services/media.ccc.de/kiosk/live/preparation/generated_mock_0.json new file mode 100644 index 000000000..c1316262d --- /dev/null +++ b/extractor/src/test/resources/org/schabi/newpipe/extractor/services/media.ccc.de/kiosk/live/preparation/generated_mock_0.json @@ -0,0 +1,47 @@ +{ + "request": { + "httpMethod": "GET", + "url": "https://streaming.media.ccc.de/streams/v2.json", + "headers": { + "Accept-Language": [ + "en-GB, en;q\u003d0.9" + ] + }, + "localization": { + "languageCode": "en", + "countryCode": "GB" + } + }, + "response": { + "responseCode": 200, + "responseMessage": "OK", + "responseHeaders": { + "access-control-allow-origin": [ + "*" + ], + "content-type": [ + "application/json" + ], + "date": [ + "Sat, 05 Aug 2023 10:59:09 GMT" + ], + "server": [ + "nginx" + ], + "strict-transport-security": [ + "max-age\u003d31536000" + ], + "transfer-encoding": [ + "chunked" + ], + "vary": [ + "Accept-Encoding" + ], + "x-cache": [ + "HIT origin" + ] + }, + "responseBody": "[\n {\n \"conference\": \"BornHack 2023\",\n \"slug\": \"bornhack2023\",\n \"author\": \"BornHack ApS\",\n \"description\": \"BornHack is a 7 day outdoor tent camp where hackers, makers and people with an interest in technology or security come together to celebrate technology, socialise, learn and have fun.\",\n \"keywords\": \"\",\n \"schedule\": null,\n \"startsAt\": \"2023-08-02T13:00:00+0000\",\n \"endsAt\": \"2023-08-09T18:00:00+0000\",\n \"isCurrentlyStreaming\": false,\n \"groups\": [\n {\n \"group\": \"Lecture Rooms\",\n \"rooms\": [\n {\n \"slug\": \"bornhack1\",\n \"schedulename\": \"Speakers Tent\",\n \"thumb\": \"https://cdn.c3voc.de/thumbnail/bornhack23s1/thumb.jpeg\",\n \"poster\": \"https://cdn.c3voc.de/thumbnail/bornhack23s1/poster.jpeg\",\n \"link\": \"https://streaming.media.ccc.de/bornhack2023/bornhack1\",\n \"display\": \"Speakers Tent\",\n \"stream\": \"bornhack23s1\",\n \"talks\": {\n \"current\": null,\n \"next\": null\n },\n \"streams\": [\n {\n \"slug\": \"hls-native\",\n \"display\": \"Speakers Tent \",\n \"type\": \"hls\",\n \"isTranslated\": false,\n \"videoSize\": null,\n \"urls\": {}\n },\n {\n \"slug\": \"hls-translated\",\n \"display\": \"Speakers Tent (Translated)\",\n \"type\": \"hls\",\n \"isTranslated\": true,\n \"videoSize\": null,\n \"urls\": {}\n },\n {\n \"slug\": \"hd-native\",\n \"display\": \"Speakers Tent FullHD Video\",\n \"type\": \"video\",\n \"isTranslated\": false,\n \"videoSize\": [\n 1920,\n 1080\n ],\n \"urls\": {\n \"hls\": {\n \"display\": \"HLS\",\n \"tech\": \"1920x1080, h264+AAC im MPEG-TS-Container via HTTP, 3 MBit/s\",\n \"url\": \"https://cdn.c3voc.de/hls/bornhack23s1/native_hd.m3u8\"\n }\n }\n },\n {\n \"slug\": \"hd-translated\",\n \"display\": \"Speakers Tent FullHD Video (Translated)\",\n \"type\": \"video\",\n \"isTranslated\": true,\n \"videoSize\": [\n 1920,\n 1080\n ],\n \"urls\": {\n \"hls\": {\n \"display\": \"HLS\",\n \"tech\": \"1920x1080, h264+AAC im MPEG-TS-Container via HTTP, 3 MBit/s\",\n \"url\": \"https://cdn.c3voc.de/hls/bornhack23s1/translated_hd.m3u8\"\n }\n }\n }\n ]\n },\n {\n \"slug\": \"bornhack2\",\n \"schedulename\": \"Bar Area\",\n \"thumb\": \"https://cdn.c3voc.de/thumbnail/bornhack23s2/thumb.jpeg\",\n \"poster\": \"https://cdn.c3voc.de/thumbnail/bornhack23s2/poster.jpeg\",\n \"link\": \"https://streaming.media.ccc.de/bornhack2023/bornhack2\",\n \"display\": \"Bar Area\",\n \"stream\": \"bornhack23s2\",\n \"talks\": {\n \"current\": null,\n \"next\": null\n },\n \"streams\": [\n {\n \"slug\": \"hls-native\",\n \"display\": \"Bar Area \",\n \"type\": \"hls\",\n \"isTranslated\": false,\n \"videoSize\": null,\n \"urls\": {}\n },\n {\n \"slug\": \"hls-translated\",\n \"display\": \"Bar Area (Translated)\",\n \"type\": \"hls\",\n \"isTranslated\": true,\n \"videoSize\": null,\n \"urls\": {}\n },\n {\n \"slug\": \"hd-native\",\n \"display\": \"Bar Area FullHD Video\",\n \"type\": \"video\",\n \"isTranslated\": false,\n \"videoSize\": [\n 1920,\n 1080\n ],\n \"urls\": {\n \"hls\": {\n \"display\": \"HLS\",\n \"tech\": \"1920x1080, h264+AAC im MPEG-TS-Container via HTTP, 3 MBit/s\",\n \"url\": \"https://cdn.c3voc.de/hls/bornhack23s2/native_hd.m3u8\"\n }\n }\n },\n {\n \"slug\": \"hd-translated\",\n \"display\": \"Bar Area FullHD Video (Translated)\",\n \"type\": \"video\",\n \"isTranslated\": true,\n \"videoSize\": [\n 1920,\n 1080\n ],\n \"urls\": {\n \"hls\": {\n \"display\": \"HLS\",\n \"tech\": \"1920x1080, h264+AAC im MPEG-TS-Container via HTTP, 3 MBit/s\",\n \"url\": \"https://cdn.c3voc.de/hls/bornhack23s2/translated_hd.m3u8\"\n }\n }\n }\n ]\n },\n {\n \"slug\": \"bornhack3\",\n \"schedulename\": \"Bar Meetup Area\",\n \"thumb\": \"https://cdn.c3voc.de/thumbnail/bornhack23s3/thumb.jpeg\",\n \"poster\": \"https://cdn.c3voc.de/thumbnail/bornhack23s3/poster.jpeg\",\n \"link\": \"https://streaming.media.ccc.de/bornhack2023/bornhack3\",\n \"display\": \"Bar Meetup Area\",\n \"stream\": \"bornhack23s3\",\n \"talks\": {\n \"current\": null,\n \"next\": null\n },\n \"streams\": [\n {\n \"slug\": \"hls-native\",\n \"display\": \"Bar Meetup Area \",\n \"type\": \"hls\",\n \"isTranslated\": false,\n \"videoSize\": null,\n \"urls\": {}\n },\n {\n \"slug\": \"hls-translated\",\n \"display\": \"Bar Meetup Area (Translated)\",\n \"type\": \"hls\",\n \"isTranslated\": true,\n \"videoSize\": null,\n \"urls\": {}\n },\n {\n \"slug\": \"hd-native\",\n \"display\": \"Bar Meetup Area FullHD Video\",\n \"type\": \"video\",\n \"isTranslated\": false,\n \"videoSize\": [\n 1920,\n 1080\n ],\n \"urls\": {\n \"hls\": {\n \"display\": \"HLS\",\n \"tech\": \"1920x1080, h264+AAC im MPEG-TS-Container via HTTP, 3 MBit/s\",\n \"url\": \"https://cdn.c3voc.de/hls/bornhack23s3/native_hd.m3u8\"\n }\n }\n },\n {\n \"slug\": \"hd-translated\",\n \"display\": \"Bar Meetup Area FullHD Video (Translated)\",\n \"type\": \"video\",\n \"isTranslated\": true,\n \"videoSize\": [\n 1920,\n 1080\n ],\n \"urls\": {\n \"hls\": {\n \"display\": \"HLS\",\n \"tech\": \"1920x1080, h264+AAC im MPEG-TS-Container via HTTP, 3 MBit/s\",\n \"url\": \"https://cdn.c3voc.de/hls/bornhack23s3/translated_hd.m3u8\"\n }\n }\n }\n ]\n }\n ]\n }\n ]\n }\n]", + "latestUrl": "https://streaming.media.ccc.de/streams/v2.json" + } +} \ No newline at end of file diff --git a/extractor/src/test/resources/org/schabi/newpipe/extractor/services/media.ccc.de/kiosk/live/running/generated_mock_0.json b/extractor/src/test/resources/org/schabi/newpipe/extractor/services/media.ccc.de/kiosk/live/running/generated_mock_0.json new file mode 100644 index 000000000..072dc8409 --- /dev/null +++ b/extractor/src/test/resources/org/schabi/newpipe/extractor/services/media.ccc.de/kiosk/live/running/generated_mock_0.json @@ -0,0 +1,47 @@ +{ + "request": { + "httpMethod": "GET", + "url": "https://streaming.media.ccc.de/streams/v2.json", + "headers": { + "Accept-Language": [ + "en-GB, en;q\u003d0.9" + ] + }, + "localization": { + "languageCode": "en", + "countryCode": "GB" + } + }, + "response": { + "responseCode": 200, + "responseMessage": "OK", + "responseHeaders": { + "access-control-allow-origin": [ + "*" + ], + "content-type": [ + "application/json" + ], + "date": [ + "Sat, 05 Aug 2023 11:25:01 GMT" + ], + "server": [ + "nginx" + ], + "strict-transport-security": [ + "max-age\u003d31536000" + ], + "transfer-encoding": [ + "chunked" + ], + "vary": [ + "Accept-Encoding" + ], + "x-cache": [ + "EXPIRED origin" + ] + }, + "responseBody": "[\n {\n \"conference\": \"FrOSCon 2023\",\n \"slug\": \"froscon2023\",\n \"author\": \"FrOSCon 2023\",\n \"description\": \"\",\n \"keywords\": \"\",\n \"schedule\": \"https://bats.science/froscon2023/schedule.xml\",\n \"startsAt\": \"2023-08-05T07:00:00+0000\",\n \"endsAt\": \"2023-08-06T18:00:00+0000\",\n \"isCurrentlyStreaming\": true,\n \"groups\": [\n {\n \"group\": \"Lecture Rooms\",\n \"rooms\": [\n {\n \"slug\": \"S1\",\n \"schedulename\": \"HS1\",\n \"thumb\": \"https://cdn.c3voc.de/thumbnail/s1/thumb.jpeg\",\n \"poster\": \"https://cdn.c3voc.de/thumbnail/s1/poster.jpeg\",\n \"link\": \"https://streaming.media.ccc.de/froscon2023/S1\",\n \"display\": \"HS 1/2\",\n \"stream\": \"s1\",\n \"talks\": {\n \"current\": {\n \"fstart\": \"2023-08-05T12:45:00+02:00\",\n \"fend\": \"2023-08-05T13:45:00+02:00\",\n \"tstart\": \"12:45\",\n \"tend\": \"13:45\",\n \"start\": 1691232300,\n \"end\": 1691235900,\n \"offset\": 7200,\n \"duration\": 3600,\n \"guid\": \"ae171c71-c145-453e-8248-c044b9c4987e\",\n \"title\": \"Ohne Open Source? Wie hätte das denn gehen sollen?\",\n \"speaker\": \"Bianca Kastl\",\n \"room_known\": true,\n \"optout\": false,\n \"url\": \"https://programm.froscon.org/2023/events/2977.html\"\n },\n \"next\": {\n \"fstart\": \"2023-08-05T14:00:00+02:00\",\n \"fend\": \"2023-08-05T15:00:00+02:00\",\n \"tstart\": \"14:00\",\n \"tend\": \"15:00\",\n \"start\": 1691236800,\n \"end\": 1691240400,\n \"offset\": 7200,\n \"duration\": 3600,\n \"guid\": \"2d6d797b-b334-4308-b828-11bede347e2d\",\n \"title\": \"Der eigene digitale (offline) Sprachassistent - das Ziel ist (fast) erreicht\",\n \"speaker\": \"Jürgen Pabel\",\n \"room_known\": true,\n \"optout\": false,\n \"url\": \"https://programm.froscon.org/2023/events/2928.html\"\n }\n },\n \"streams\": [\n {\n \"slug\": \"hls-native\",\n \"display\": \"HS 1/2 \",\n \"type\": \"hls\",\n \"isTranslated\": false,\n \"videoSize\": null,\n \"urls\": {}\n },\n {\n \"slug\": \"hls-translated\",\n \"display\": \"HS 1/2 (Translated)\",\n \"type\": \"hls\",\n \"isTranslated\": true,\n \"videoSize\": null,\n \"urls\": {}\n },\n {\n \"slug\": \"hd-native\",\n \"display\": \"HS 1/2 FullHD Video\",\n \"type\": \"video\",\n \"isTranslated\": false,\n \"videoSize\": [\n 1920,\n 1080\n ],\n \"urls\": {\n \"hls\": {\n \"display\": \"HLS\",\n \"tech\": \"1920x1080, h264+AAC im MPEG-TS-Container via HTTP, 3 MBit/s\",\n \"url\": \"https://cdn.c3voc.de/hls/s1/native_hd.m3u8\"\n }\n }\n },\n {\n \"slug\": \"hd-translated\",\n \"display\": \"HS 1/2 FullHD Video (Translated)\",\n \"type\": \"video\",\n \"isTranslated\": true,\n \"videoSize\": [\n 1920,\n 1080\n ],\n \"urls\": {\n \"hls\": {\n \"display\": \"HLS\",\n \"tech\": \"1920x1080, h264+AAC im MPEG-TS-Container via HTTP, 3 MBit/s\",\n \"url\": \"https://cdn.c3voc.de/hls/s1/translated_hd.m3u8\"\n }\n }\n }\n ]\n },\n {\n \"slug\": \"S2\",\n \"schedulename\": \"HS7\",\n \"thumb\": \"https://cdn.c3voc.de/thumbnail/s2/thumb.jpeg\",\n \"poster\": \"https://cdn.c3voc.de/thumbnail/s2/poster.jpeg\",\n \"link\": \"https://streaming.media.ccc.de/froscon2023/S2\",\n \"display\": \"HS 7\",\n \"stream\": \"s2\",\n \"talks\": {\n \"current\": {\n \"fstart\": \"2023-08-05T12:15:00+02:00\",\n \"fend\": \"2023-08-05T14:00:00+02:00\",\n \"tstart\": \"12:15\",\n \"tend\": \"14:00\",\n \"start\": 1691230500,\n \"end\": 1691236800,\n \"offset\": 7200,\n \"duration\": 6300,\n \"special\": \"pause\",\n \"title\": \"105 minutes pause\",\n \"room_known\": true\n },\n \"next\": {\n \"fstart\": \"2023-08-05T14:00:00+02:00\",\n \"fend\": \"2023-08-05T15:00:00+02:00\",\n \"tstart\": \"14:00\",\n \"tend\": \"15:00\",\n \"start\": 1691236800,\n \"end\": 1691240400,\n \"offset\": 7200,\n \"duration\": 3600,\n \"guid\": \"cced13df-4057-429e-8136-05794a738833\",\n \"title\": \"Collabora Online -Update-\",\n \"speaker\": \"StefanU\",\n \"room_known\": true,\n \"optout\": false,\n \"url\": \"https://programm.froscon.org/2023/events/2945.html\"\n }\n },\n \"streams\": [\n {\n \"slug\": \"hls-native\",\n \"display\": \"HS 7 \",\n \"type\": \"hls\",\n \"isTranslated\": false,\n \"videoSize\": null,\n \"urls\": {}\n },\n {\n \"slug\": \"hls-translated\",\n \"display\": \"HS 7 (Translated)\",\n \"type\": \"hls\",\n \"isTranslated\": true,\n \"videoSize\": null,\n \"urls\": {}\n },\n {\n \"slug\": \"hd-native\",\n \"display\": \"HS 7 FullHD Video\",\n \"type\": \"video\",\n \"isTranslated\": false,\n \"videoSize\": [\n 1920,\n 1080\n ],\n \"urls\": {\n \"hls\": {\n \"display\": \"HLS\",\n \"tech\": \"1920x1080, h264+AAC im MPEG-TS-Container via HTTP, 3 MBit/s\",\n \"url\": \"https://cdn.c3voc.de/hls/s2/native_hd.m3u8\"\n }\n }\n },\n {\n \"slug\": \"hd-translated\",\n \"display\": \"HS 7 FullHD Video (Translated)\",\n \"type\": \"video\",\n \"isTranslated\": true,\n \"videoSize\": [\n 1920,\n 1080\n ],\n \"urls\": {\n \"hls\": {\n \"display\": \"HLS\",\n \"tech\": \"1920x1080, h264+AAC im MPEG-TS-Container via HTTP, 3 MBit/s\",\n \"url\": \"https://cdn.c3voc.de/hls/s2/translated_hd.m3u8\"\n }\n }\n }\n ]\n },\n {\n \"slug\": \"S3\",\n \"schedulename\": \"HS3\",\n \"thumb\": \"https://cdn.c3voc.de/thumbnail/s3/thumb.jpeg\",\n \"poster\": \"https://cdn.c3voc.de/thumbnail/s3/poster.jpeg\",\n \"link\": \"https://streaming.media.ccc.de/froscon2023/S3\",\n \"display\": \"HS 3\",\n \"stream\": \"s3\",\n \"talks\": {\n \"current\": {\n \"fstart\": \"2023-08-05T12:15:00+02:00\",\n \"fend\": \"2023-08-05T14:00:00+02:00\",\n \"tstart\": \"12:15\",\n \"tend\": \"14:00\",\n \"start\": 1691230500,\n \"end\": 1691236800,\n \"offset\": 7200,\n \"duration\": 6300,\n \"special\": \"pause\",\n \"title\": \"105 minutes pause\",\n \"room_known\": true\n },\n \"next\": {\n \"fstart\": \"2023-08-05T14:00:00+02:00\",\n \"fend\": \"2023-08-05T15:00:00+02:00\",\n \"tstart\": \"14:00\",\n \"tend\": \"15:00\",\n \"start\": 1691236800,\n \"end\": 1691240400,\n \"offset\": 7200,\n \"duration\": 3600,\n \"guid\": \"7c0bc3e1-1bea-475a-b215-e11ac0a45cde\",\n \"title\": \"Effortless RPM packaging with CPack\",\n \"speaker\": \"Sergei Golubchik\",\n \"room_known\": true,\n \"optout\": false,\n \"url\": \"https://programm.froscon.org/2023/events/2880.html\"\n }\n },\n \"streams\": [\n {\n \"slug\": \"hls-native\",\n \"display\": \"HS 3 \",\n \"type\": \"hls\",\n \"isTranslated\": false,\n \"videoSize\": null,\n \"urls\": {}\n },\n {\n \"slug\": \"hls-translated\",\n \"display\": \"HS 3 (Translated)\",\n \"type\": \"hls\",\n \"isTranslated\": true,\n \"videoSize\": null,\n \"urls\": {}\n },\n {\n \"slug\": \"hd-native\",\n \"display\": \"HS 3 FullHD Video\",\n \"type\": \"video\",\n \"isTranslated\": false,\n \"videoSize\": [\n 1920,\n 1080\n ],\n \"urls\": {\n \"hls\": {\n \"display\": \"HLS\",\n \"tech\": \"1920x1080, h264+AAC im MPEG-TS-Container via HTTP, 3 MBit/s\",\n \"url\": \"https://cdn.c3voc.de/hls/s3/native_hd.m3u8\"\n }\n }\n },\n {\n \"slug\": \"hd-translated\",\n \"display\": \"HS 3 FullHD Video (Translated)\",\n \"type\": \"video\",\n \"isTranslated\": true,\n \"videoSize\": [\n 1920,\n 1080\n ],\n \"urls\": {\n \"hls\": {\n \"display\": \"HLS\",\n \"tech\": \"1920x1080, h264+AAC im MPEG-TS-Container via HTTP, 3 MBit/s\",\n \"url\": \"https://cdn.c3voc.de/hls/s3/translated_hd.m3u8\"\n }\n }\n }\n ]\n },\n {\n \"slug\": \"S4\",\n \"schedulename\": \"HS4\",\n \"thumb\": \"https://cdn.c3voc.de/thumbnail/s4/thumb.jpeg\",\n \"poster\": \"https://cdn.c3voc.de/thumbnail/s4/poster.jpeg\",\n \"link\": \"https://streaming.media.ccc.de/froscon2023/S4\",\n \"display\": \"HS 4\",\n \"stream\": \"s4\",\n \"talks\": {\n \"current\": {\n \"fstart\": \"2023-08-05T12:15:00+02:00\",\n \"fend\": \"2023-08-05T14:00:00+02:00\",\n \"tstart\": \"12:15\",\n \"tend\": \"14:00\",\n \"start\": 1691230500,\n \"end\": 1691236800,\n \"offset\": 7200,\n \"duration\": 6300,\n \"special\": \"pause\",\n \"title\": \"105 minutes pause\",\n \"room_known\": true\n },\n \"next\": {\n \"fstart\": \"2023-08-05T14:00:00+02:00\",\n \"fend\": \"2023-08-05T15:00:00+02:00\",\n \"tstart\": \"14:00\",\n \"tend\": \"15:00\",\n \"start\": 1691236800,\n \"end\": 1691240400,\n \"offset\": 7200,\n \"duration\": 3600,\n \"guid\": \"41284771-2f34-4f84-bfea-5d936b4dfffa\",\n \"title\": \"From 0 to Kubernetes.\",\n \"speaker\": \"Heiko Borchers, Cedric Kienzler\",\n \"room_known\": true,\n \"optout\": false,\n \"url\": \"https://programm.froscon.org/2023/events/2918.html\"\n }\n },\n \"streams\": [\n {\n \"slug\": \"hls-native\",\n \"display\": \"HS 4 \",\n \"type\": \"hls\",\n \"isTranslated\": false,\n \"videoSize\": null,\n \"urls\": {}\n },\n {\n \"slug\": \"hls-translated\",\n \"display\": \"HS 4 (Translated)\",\n \"type\": \"hls\",\n \"isTranslated\": true,\n \"videoSize\": null,\n \"urls\": {}\n },\n {\n \"slug\": \"hd-native\",\n \"display\": \"HS 4 FullHD Video\",\n \"type\": \"video\",\n \"isTranslated\": false,\n \"videoSize\": [\n 1920,\n 1080\n ],\n \"urls\": {\n \"hls\": {\n \"display\": \"HLS\",\n \"tech\": \"1920x1080, h264+AAC im MPEG-TS-Container via HTTP, 3 MBit/s\",\n \"url\": \"https://cdn.c3voc.de/hls/s4/native_hd.m3u8\"\n }\n }\n },\n {\n \"slug\": \"hd-translated\",\n \"display\": \"HS 4 FullHD Video (Translated)\",\n \"type\": \"video\",\n \"isTranslated\": true,\n \"videoSize\": [\n 1920,\n 1080\n ],\n \"urls\": {\n \"hls\": {\n \"display\": \"HLS\",\n \"tech\": \"1920x1080, h264+AAC im MPEG-TS-Container via HTTP, 3 MBit/s\",\n \"url\": \"https://cdn.c3voc.de/hls/s4/translated_hd.m3u8\"\n }\n }\n }\n ]\n },\n {\n \"slug\": \"S5\",\n \"schedulename\": \"HS8\",\n \"thumb\": \"https://cdn.c3voc.de/thumbnail/s5/thumb.jpeg\",\n \"poster\": \"https://cdn.c3voc.de/thumbnail/s5/poster.jpeg\",\n \"link\": \"https://streaming.media.ccc.de/froscon2023/S5\",\n \"display\": \"HS 8\",\n \"stream\": \"s5\",\n \"talks\": {\n \"current\": {\n \"fstart\": \"2023-08-05T12:15:00+02:00\",\n \"fend\": \"2023-08-05T14:00:00+02:00\",\n \"tstart\": \"12:15\",\n \"tend\": \"14:00\",\n \"start\": 1691230500,\n \"end\": 1691236800,\n \"offset\": 7200,\n \"duration\": 6300,\n \"special\": \"pause\",\n \"title\": \"105 minutes pause\",\n \"room_known\": true\n },\n \"next\": {\n \"fstart\": \"2023-08-05T14:00:00+02:00\",\n \"fend\": \"2023-08-05T15:00:00+02:00\",\n \"tstart\": \"14:00\",\n \"tend\": \"15:00\",\n \"start\": 1691236800,\n \"end\": 1691240400,\n \"offset\": 7200,\n \"duration\": 3600,\n \"guid\": \"88602229-b3a9-4af6-9846-8c361e9fb51a\",\n \"title\": \"Wrapping entire Kubernetes clusters into a confidential-computing envelope with Constellation\",\n \"speaker\": \"Malte Poll, Paul Meyer\",\n \"room_known\": true,\n \"optout\": false,\n \"url\": \"https://programm.froscon.org/2023/events/2885.html\"\n }\n },\n \"streams\": [\n {\n \"slug\": \"hls-native\",\n \"display\": \"HS 8 \",\n \"type\": \"hls\",\n \"isTranslated\": false,\n \"videoSize\": null,\n \"urls\": {}\n },\n {\n \"slug\": \"hls-translated\",\n \"display\": \"HS 8 (Translated)\",\n \"type\": \"hls\",\n \"isTranslated\": true,\n \"videoSize\": null,\n \"urls\": {}\n },\n {\n \"slug\": \"hd-native\",\n \"display\": \"HS 8 FullHD Video\",\n \"type\": \"video\",\n \"isTranslated\": false,\n \"videoSize\": [\n 1920,\n 1080\n ],\n \"urls\": {\n \"hls\": {\n \"display\": \"HLS\",\n \"tech\": \"1920x1080, h264+AAC im MPEG-TS-Container via HTTP, 3 MBit/s\",\n \"url\": \"https://cdn.c3voc.de/hls/s5/native_hd.m3u8\"\n }\n }\n },\n {\n \"slug\": \"hd-translated\",\n \"display\": \"HS 8 FullHD Video (Translated)\",\n \"type\": \"video\",\n \"isTranslated\": true,\n \"videoSize\": [\n 1920,\n 1080\n ],\n \"urls\": {\n \"hls\": {\n \"display\": \"HLS\",\n \"tech\": \"1920x1080, h264+AAC im MPEG-TS-Container via HTTP, 3 MBit/s\",\n \"url\": \"https://cdn.c3voc.de/hls/s5/translated_hd.m3u8\"\n }\n }\n }\n ]\n },\n {\n \"slug\": \"S6\",\n \"schedulename\": \"HS6\",\n \"thumb\": \"https://cdn.c3voc.de/thumbnail/s6/thumb.jpeg\",\n \"poster\": \"https://cdn.c3voc.de/thumbnail/s6/poster.jpeg\",\n \"link\": \"https://streaming.media.ccc.de/froscon2023/S6\",\n \"display\": \"HS 6\",\n \"stream\": \"s6\",\n \"talks\": {\n \"current\": {\n \"fstart\": \"2023-08-05T12:15:00+02:00\",\n \"fend\": \"2023-08-05T14:00:00+02:00\",\n \"tstart\": \"12:15\",\n \"tend\": \"14:00\",\n \"start\": 1691230500,\n \"end\": 1691236800,\n \"offset\": 7200,\n \"duration\": 6300,\n \"special\": \"pause\",\n \"title\": \"105 minutes pause\",\n \"room_known\": true\n },\n \"next\": {\n \"fstart\": \"2023-08-05T14:00:00+02:00\",\n \"fend\": \"2023-08-05T15:00:00+02:00\",\n \"tstart\": \"14:00\",\n \"tend\": \"15:00\",\n \"start\": 1691236800,\n \"end\": 1691240400,\n \"offset\": 7200,\n \"duration\": 3600,\n \"guid\": \"a456e12b-6d66-4f07-bd3f-c507f09bd886\",\n \"title\": \"Eine verstehbare, digitale Welt – gemeinsam, für alle!\",\n \"speaker\": \"Dominik George, Darius Auding\",\n \"room_known\": true,\n \"optout\": false,\n \"url\": \"https://programm.froscon.org/2023/events/2898.html\"\n }\n },\n \"streams\": [\n {\n \"slug\": \"hls-native\",\n \"display\": \"HS 6 \",\n \"type\": \"hls\",\n \"isTranslated\": false,\n \"videoSize\": null,\n \"urls\": {}\n },\n {\n \"slug\": \"hls-translated\",\n \"display\": \"HS 6 (Translated)\",\n \"type\": \"hls\",\n \"isTranslated\": true,\n \"videoSize\": null,\n \"urls\": {}\n },\n {\n \"slug\": \"hd-native\",\n \"display\": \"HS 6 FullHD Video\",\n \"type\": \"video\",\n \"isTranslated\": false,\n \"videoSize\": [\n 1920,\n 1080\n ],\n \"urls\": {\n \"hls\": {\n \"display\": \"HLS\",\n \"tech\": \"1920x1080, h264+AAC im MPEG-TS-Container via HTTP, 3 MBit/s\",\n \"url\": \"https://cdn.c3voc.de/hls/s6/native_hd.m3u8\"\n }\n }\n },\n {\n \"slug\": \"hd-translated\",\n \"display\": \"HS 6 FullHD Video (Translated)\",\n \"type\": \"video\",\n \"isTranslated\": true,\n \"videoSize\": [\n 1920,\n 1080\n ],\n \"urls\": {\n \"hls\": {\n \"display\": \"HLS\",\n \"tech\": \"1920x1080, h264+AAC im MPEG-TS-Container via HTTP, 3 MBit/s\",\n \"url\": \"https://cdn.c3voc.de/hls/s6/translated_hd.m3u8\"\n }\n }\n }\n ]\n }\n ]\n }\n ]\n }\n]", + "latestUrl": "https://streaming.media.ccc.de/streams/v2.json" + } +} \ No newline at end of file