Skip to content

Commit 9e97b67

Browse files
committed
Compress Image if it is to large to fit into single OPUS packet
1 parent d2a1c17 commit 9e97b67

File tree

3 files changed

+137
-18
lines changed

3 files changed

+137
-18
lines changed

app/src/main/java/org/schabi/newpipe/streams/OggFromWebMWriter.java

Lines changed: 75 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828
import java.util.List;
2929
import java.util.stream.Collectors;
3030

31+
import us.shandian.giga.postprocessing.ImageUtils;
32+
3133
/**
3234
* <p>
3335
* This class is used to convert a WebM stream containing Opus or Vorbis audio
@@ -50,16 +52,44 @@
5052
* @author tobigr
5153
*/
5254
public class OggFromWebMWriter implements Closeable {
55+
private static final String TAG = OggFromWebMWriter.class.getSimpleName();
56+
57+
/**
58+
* No flags set.
59+
*/
5360
private static final byte FLAG_UNSET = 0x00;
54-
//private static final byte FLAG_CONTINUED = 0x01;
61+
/**
62+
* The packet is continued from previous the previous page.
63+
*/
64+
private static final byte FLAG_CONTINUED = 0x01;
65+
/**
66+
* BOS (beginning of stream).
67+
*/
5568
private static final byte FLAG_FIRST = 0x02;
69+
/**
70+
* EOS (end of stream).
71+
*/
5672
private static final byte FLAG_LAST = 0x04;
5773

5874
private static final byte HEADER_CHECKSUM_OFFSET = 22;
5975
private static final byte HEADER_SIZE = 27;
6076

6177
private static final int TIME_SCALE_NS = 1000000000;
6278

79+
private static final int MAX_SEGMENT_SIZE = 255;
80+
81+
private static final int OPUS_MAX_PACKETS_SIZE = 61_140;
82+
83+
/**
84+
* <p>The maximum size of the compressed thumbnail image in bytes,
85+
* to be included in the Opus metadata.</p>
86+
*
87+
* This is a safe size to avoid creating metadata tags that are too large for the Ogg page,
88+
* since the metadata header and other tags can also take up space in the page.
89+
*/
90+
private static final int MAX_THUMBNAIL_SIZE = OPUS_MAX_PACKETS_SIZE - 4500;
91+
92+
6393
private boolean done = false;
6494
private boolean parsed = false;
6595

@@ -323,12 +353,12 @@ private int makePacketHeader(final long granPos, @NonNull final ByteBuffer buffe
323353
* @ImplNote See <a href="https://datatracker.ietf.org/doc/html/rfc7845.html#section-5.2">
324354
* RFC7845 5.2</a>
325355
*
326-
* @return
356+
* @return The binary metadata header, or null if not implemented for the codec
327357
*/
328358
@Nullable
329359
private byte[] makeMetadata() {
330360
if (DEBUG) {
331-
Log.d("OggFromWebMWriter", "Downloading media with codec ID " + webmTrack.codecId);
361+
Log.d(TAG, "Downloading media with codec ID " + webmTrack.codecId);
332362
}
333363

334364
if ("A_OPUS".equals(webmTrack.codecId)) {
@@ -343,18 +373,21 @@ private byte[] makeMetadata() {
343373
.getLocalDateTime()
344374
.format(DateTimeFormatter.ISO_DATE)));
345375
if (thumbnail != null) {
346-
metadata.add(makeOpusPictureTag(thumbnail));
376+
var pictureTag = makeOpusPictureTag(thumbnail, MAX_THUMBNAIL_SIZE);
377+
if (pictureTag != null) {
378+
metadata.add(pictureTag);
379+
}
347380
}
348381
}
349382

350383
if (DEBUG) {
351-
Log.d("OggFromWebMWriter", "Creating metadata header with this data:");
352-
metadata.forEach(p -> Log.d("OggFromWebMWriter", p.first + "=" + p.second));
384+
Log.d(TAG, "Creating metadata header with this data:");
385+
metadata.forEach(p -> Log.d(TAG, p.first + "=" + p.second));
353386
}
354387

355388
return makeOpusTagsHeader(metadata);
356389
} else if ("A_VORBIS".equals(webmTrack.codecId)) {
357-
/**
390+
/*
358391
* See <a href="https://datatracker.ietf.org/doc/html/rfc7845.html#section-5.2">
359392
* RFC7845 5.2</a>
360393
*/
@@ -399,43 +432,68 @@ private static byte[] makeOpusMetadataTag(final Pair<String, String> pair) {
399432
* </p>
400433
*
401434
* @param bitmap The bitmap to use as cover art
435+
* @param maxSize The maximum size of the compressed image in bytes.
436+
* If the compressed image exceeds this size,
437+
* it will be further compressed until it fits.
438+
* This is necessary to avoid creating metadata tags
439+
* that are too large for the Ogg page.
402440
* @return The key-value pair representing the tag
441+
* or null if the image cannot be compressed to the maxSize
403442
*/
404-
private static Pair<String, String> makeOpusPictureTag(final Bitmap bitmap) {
443+
@Nullable
444+
private static Pair<String, String> makeOpusPictureTag(final Bitmap bitmap, final int maxSize) {
405445
// FLAC picture block format (big-endian):
406446
// uint32 picture_type
407-
// uint32 mime_length, mime_string
408-
// uint32 desc_length, desc_string
447+
// uint32 mime_length,
448+
// mime_string
449+
// uint32 desc_length,
450+
// desc_string
409451
// uint32 width
410452
// uint32 height
411453
// uint32 color_depth
412454
// uint32 colors_indexed
413-
// uint32 data_length, data_bytes
455+
// uint32 data_length,
456+
// data_bytes
414457

415458
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
416-
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, baos);
459+
460+
final var compressedThumbnail = ImageUtils.INSTANCE.compressToSize(bitmap, maxSize);
461+
if (compressedThumbnail == null) {
462+
if (DEBUG) {
463+
Log.d(TAG, "failed to compress thumbnail to target size " + maxSize);
464+
}
465+
return null;
466+
}
467+
compressedThumbnail.getBitmap().compress(
468+
Bitmap.CompressFormat.JPEG, compressedThumbnail.getQuality(), baos);
417469

418470
final byte[] imageData = baos.toByteArray();
419471
final byte[] mimeBytes = "image/jpeg".getBytes(StandardCharsets.UTF_8);
420472
final byte[] descBytes = new byte[0]; // optional description
421473
// fixed ints + mime + desc
422474
final int headerSize = 4 * 8 + mimeBytes.length + descBytes.length;
423475
final ByteBuffer buf = ByteBuffer.allocate(headerSize + imageData.length);
424-
buf.putInt(3); // picture type: 3 = Cover (front)
476+
// See https://id3.org/id3v2.3.0#Attached_picture for a full list of picture types
477+
// TODO: allow specifying other picture types, i.e. cover (front) for music albums;
478+
// but this info needs to be provided by the extractor first.
479+
buf.putInt(3); // picture type: 0 = Other
425480
buf.putInt(mimeBytes.length);
426481
buf.put(mimeBytes);
427482
buf.putInt(descBytes.length);
428-
// no description
429483
if (descBytes.length > 0) {
484+
// currently no description available, might be added later.
430485
buf.put(descBytes);
431486
}
432-
buf.putInt(bitmap.getWidth()); // width (unknown)
433-
buf.putInt(bitmap.getHeight()); // height (unknown)
487+
buf.putInt(compressedThumbnail.getBitmap().getWidth());
488+
buf.putInt(compressedThumbnail.getBitmap().getHeight());
434489
buf.putInt(0); // color depth
435490
buf.putInt(0); // colors indexed
436491
buf.putInt(imageData.length);
437492
buf.put(imageData);
493+
438494
final String b64 = Base64.getEncoder().encodeToString(buf.array());
495+
Log.d(TAG, "Compressed thumbnail size: " + imageData.length
496+
+ " bytes, base64 metadata size: " + b64.length() + " characters");
439497
return Pair.create("METADATA_BLOCK_PICTURE", b64);
440498
}
441499

@@ -457,7 +515,7 @@ private static byte[] makeOpusTagsHeader(final List<Pair<String, String>> keyVal
457515
.stream()
458516
.filter(p -> !p.second.isBlank())
459517
.map(OggFromWebMWriter::makeOpusMetadataTag)
460-
.collect(Collectors.toUnmodifiableList());
518+
.toList();
461519

462520
final var tagsBytes = tags.stream().collect(Collectors.summingInt(arr -> arr.length));
463521

app/src/main/java/us/shandian/giga/get/DownloadMission.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -864,7 +864,7 @@ public void fetchThumbnail(@NonNull final List<Image> images) {
864864
// Alternative approaches are to either downscale a high res image or
865865
// to download the correct size depending on the chosen post-processing algorithm.
866866
final String thumbnailUrl = ImageStrategy.choosePreferredImage(
867-
images, PreferredImageQuality.MEDIUM);
867+
images, PreferredImageQuality.HIGH);
868868
// TODO: get context from somewhere else
869869
thumbnail = CoilHelper.INSTANCE.loadBitmapBlocking(App.getInstance(), thumbnailUrl);
870870
thumbnailFetched = true;
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package us.shandian.giga.postprocessing
2+
3+
import android.graphics.Bitmap
4+
import java.io.ByteArrayOutputStream
5+
import java.util.Base64
6+
import org.schabi.newpipe.ktx.scale
7+
import org.schabi.newpipe.util.image.PreferredImageQuality
8+
9+
object ImageUtils {
10+
fun getImageTypeFromUrl(url: String): String {
11+
val extension = url.substringAfterLast('.', "")
12+
return when (extension.lowercase()) {
13+
"jpg", "jpeg" -> "image/jpeg"
14+
"png" -> "image/png"
15+
"gif" -> "image/gif"
16+
"bmp" -> "image/bmp"
17+
"webp" -> "image/webp"
18+
else -> "application/octet-stream" // Default binary type
19+
}
20+
}
21+
22+
data class CompressedImage(
23+
val bitmap: Bitmap,
24+
val quality: Int,
25+
val width: Int,
26+
val height: Int
27+
)
28+
29+
fun compressToSize(original: Bitmap, maxSizeBytes: Int): CompressedImage? {
30+
var quality = 100
31+
var scale = 1.0f
32+
var width = original.width
33+
var height = original.height
34+
var compressedSize: Int
35+
36+
do {
37+
var bitmap = original.copy(original.config ?: Bitmap.Config.ARGB_8888, false)
38+
if (scale < 1.0f) {
39+
bitmap = bitmap.scale(width = width, height = height)
40+
}
41+
do {
42+
val outputStream = ByteArrayOutputStream()
43+
bitmap.compress(Bitmap.CompressFormat.JPEG, quality, outputStream)
44+
compressedSize = Base64.getEncoder().encodeToString(outputStream.toByteArray()).length
45+
quality -= 5 // Decrease quality by 5% for the next iteration
46+
} while (compressedSize > maxSizeBytes && quality > 70)
47+
if (compressedSize <= maxSizeBytes) {
48+
return CompressedImage(bitmap, quality, width, height)
49+
}
50+
if (scale > 0.5f) {
51+
scale -= 0.1f
52+
} else {
53+
scale *= 0.9f
54+
}
55+
width = (original.width * scale).toInt()
56+
height = (original.height * scale).toInt()
57+
quality = 100 // Reset quality for the next size reduction
58+
} while (width > 50 && height > 50) // Prevent too much downscaling
59+
return null
60+
}
61+
}

0 commit comments

Comments
 (0)