|
26 | 26 | import org.schabi.newpipe.extractor.stream.AudioStream; |
27 | 27 | import org.schabi.newpipe.extractor.stream.DeliveryMethod; |
28 | 28 | import org.schabi.newpipe.extractor.stream.Description; |
| 29 | +import org.schabi.newpipe.extractor.stream.Frameset; |
29 | 30 | import org.schabi.newpipe.extractor.stream.Stream; |
30 | 31 | import org.schabi.newpipe.extractor.stream.StreamExtractor; |
31 | 32 | import org.schabi.newpipe.extractor.stream.StreamInfoItemsCollector; |
| 33 | +import org.schabi.newpipe.extractor.stream.StreamSegment; |
32 | 34 | import org.schabi.newpipe.extractor.stream.StreamType; |
33 | 35 | import org.schabi.newpipe.extractor.stream.SubtitlesStream; |
34 | 36 | import org.schabi.newpipe.extractor.stream.VideoStream; |
@@ -316,6 +318,66 @@ public String getSupportInfo() { |
316 | 318 | } |
317 | 319 | } |
318 | 320 |
|
| 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 | + |
319 | 381 | @Nonnull |
320 | 382 | private String getRelatedItemsUrl(@Nonnull final List<String> tags) |
321 | 383 | throws UnsupportedEncodingException { |
@@ -636,6 +698,41 @@ private void addNewVideoStream(@Nonnull final JsonObject streamJsonObject, |
636 | 698 | } |
637 | 699 | } |
638 | 700 |
|
| 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 | + |
639 | 736 | @Nonnull |
640 | 737 | @Override |
641 | 738 | public String getName() throws ParsingException { |
|
0 commit comments