Skip to content

Commit 15e0e74

Browse files
committed
[PeerTube] Add support for stream frames/storyboards extraction
Implement PeerTubeStreamExtractor.getFrames()
1 parent 3402cdb commit 15e0e74

2 files changed

Lines changed: 112 additions & 1 deletion

File tree

extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/extractors/PeertubeStreamExtractor.java

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,11 @@
2626
import org.schabi.newpipe.extractor.stream.AudioStream;
2727
import org.schabi.newpipe.extractor.stream.DeliveryMethod;
2828
import org.schabi.newpipe.extractor.stream.Description;
29+
import org.schabi.newpipe.extractor.stream.Frameset;
2930
import org.schabi.newpipe.extractor.stream.Stream;
3031
import org.schabi.newpipe.extractor.stream.StreamExtractor;
3132
import org.schabi.newpipe.extractor.stream.StreamInfoItemsCollector;
33+
import org.schabi.newpipe.extractor.stream.StreamSegment;
3234
import org.schabi.newpipe.extractor.stream.StreamType;
3335
import org.schabi.newpipe.extractor.stream.SubtitlesStream;
3436
import org.schabi.newpipe.extractor.stream.VideoStream;
@@ -316,6 +318,66 @@ public String getSupportInfo() {
316318
}
317319
}
318320

321+
@Nonnull
322+
@Override
323+
public List<StreamSegment> getStreamSegments() throws ParsingException {
324+
final List<StreamSegment> segments = new ArrayList<>();
325+
final JsonObject segmentsJson;
326+
try {
327+
segmentsJson = fetchSubApiContent("chapters");
328+
} catch (final IOException | ReCaptchaException e) {
329+
throw new ParsingException("Could not get stream segments", e);
330+
}
331+
if (segmentsJson != null && segmentsJson.has("chapters")) {
332+
final JsonArray segmentsArray = segmentsJson.getArray("chapters");
333+
for (int i = 0; i < segmentsArray.size(); i++) {
334+
final JsonObject segmentObject = segmentsArray.getObject(i);
335+
segments.add(new StreamSegment(
336+
segmentObject.getString("title"),
337+
segmentObject.getInt("timecode")));
338+
}
339+
}
340+
341+
return segments;
342+
}
343+
344+
@Nonnull
345+
@Override
346+
public List<Frameset> getFrames() throws ExtractionException {
347+
final List<Frameset> framesets = new ArrayList<>();
348+
final JsonObject storyboards;
349+
try {
350+
storyboards = fetchSubApiContent("storyboards");
351+
} catch (final IOException | ReCaptchaException e) {
352+
throw new ExtractionException("Could not get frames", e);
353+
}
354+
if (storyboards != null && storyboards.has("storyboards")) {
355+
final JsonArray storyboardsArray = storyboards.getArray("storyboards");
356+
for (final Object storyboard : storyboardsArray) {
357+
if (storyboard instanceof JsonObject) {
358+
final JsonObject storyboardObject = (JsonObject) storyboard;
359+
final String url = storyboardObject.getString("storyboardPath");
360+
final int width = storyboardObject.getInt("spriteWidth");
361+
final int height = storyboardObject.getInt("spriteHeight");
362+
final int totalWidth = storyboardObject.getInt("totalWidth");
363+
final int totalHeight = storyboardObject.getInt("totalHeight");
364+
final int framesPerPageX = totalWidth / width;
365+
final int framesPerPageY = totalHeight / height;
366+
final int count = framesPerPageX * framesPerPageY;
367+
final int durationPerFrame = storyboardObject.getInt("spriteDuration") * 1000;
368+
369+
framesets.add(new Frameset(
370+
// there is only one composite image per video containing all frames
371+
List.of(baseUrl + url),
372+
width, height, count,
373+
durationPerFrame, framesPerPageX, framesPerPageY));
374+
}
375+
}
376+
}
377+
378+
return framesets;
379+
}
380+
319381
@Nonnull
320382
private String getRelatedItemsUrl(@Nonnull final List<String> tags)
321383
throws UnsupportedEncodingException {
@@ -636,6 +698,41 @@ private void addNewVideoStream(@Nonnull final JsonObject streamJsonObject,
636698
}
637699
}
638700

701+
/**
702+
* Fetch content from a sub-API of the video.
703+
* @param subPath the API subpath after the video id,
704+
* e.g. "storyboards" for "/api/v1/videos/{id}/storyboards"
705+
* @return the {@link JsonObject} of the sub-API or null if the API does not exist
706+
* which is the case if the instance has an outdated PeerTube version.
707+
* @throws ParsingException if the API response could not be parsed to a {@link JsonObject}
708+
* @throws IOException if the API response could not be fetched
709+
* @throws ReCaptchaException if the API response is a reCaptcha
710+
*/
711+
@Nullable
712+
private JsonObject fetchSubApiContent(@Nonnull final String subPath)
713+
throws ParsingException, IOException, ReCaptchaException {
714+
final String apiUrl = baseUrl + PeertubeStreamLinkHandlerFactory.VIDEO_API_ENDPOINT
715+
+ getId() + "/" + subPath;
716+
final Response response = getDownloader().get(apiUrl);
717+
if (response == null) {
718+
throw new ParsingException("Could not get segments from API.");
719+
}
720+
if (response.responseCode() == 400) {
721+
// Chapter or segments support was added with PeerTube v6.0.0
722+
// This instance does not support it yet.
723+
return null;
724+
}
725+
if (response.responseCode() != 200) {
726+
throw new ParsingException("Could not get segments from API. Response code: "
727+
+ response.responseCode());
728+
}
729+
try {
730+
return JsonParser.object().from(response.responseBody());
731+
} catch (final JsonParserException e) {
732+
throw new ParsingException("Could not parse json data for segments", e);
733+
}
734+
}
735+
639736
@Nonnull
640737
@Override
641738
public String getName() throws ParsingException {

extractor/src/main/java/org/schabi/newpipe/extractor/stream/Frameset.java

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
import java.io.Serializable;
44
import java.util.List;
55

6+
/**
7+
* Class to handle framesets / storyboards which summarize the stream content.
8+
*/
69
public final class Frameset implements Serializable {
710

811
private final List<String> urls;
@@ -13,6 +16,17 @@ public final class Frameset implements Serializable {
1316
private final int framesPerPageX;
1417
private final int framesPerPageY;
1518

19+
/**
20+
* Creates a new Frameset or set of storyboards.
21+
* @param urls the URLs to the images with frames / storyboards
22+
* @param frameWidth the width of a single frame, in pixels
23+
* @param frameHeight the height of a single frame, in pixels
24+
* @param totalCount the total count of frames
25+
* @param durationPerFrame the duration per frame in milliseconds
26+
* @param framesPerPageX the maximum count of frames per page by x / over the width of the image
27+
* @param framesPerPageY the maximum count of frames per page by y / over the height
28+
* of the image
29+
*/
1630
public Frameset(
1731
final List<String> urls,
1832
final int frameWidth,
@@ -32,7 +46,7 @@ public Frameset(
3246
}
3347

3448
/**
35-
* @return list of urls to images with frames
49+
* @return list of URLs to images with frames
3650
*/
3751
public List<String> getUrls() {
3852
return urls;

0 commit comments

Comments
 (0)