mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 17:25:20 +00:00
Wi-Fi bulk: raise image-prep budget so real photos clear the 64 KiB AWDL threshold
Image prep downscaled every photo to 448 px and force-compressed to a 45 KB byte budget, so a typical camera photo landed ~40 KB — below TransportConfig.wifiBulkMinPayloadBytes (64 KiB). WifiBulkPolicy.shouldOffer requires payloadBytes > 64 KiB, so the Wi-Fi bulk (AWDL) data plane never triggered in production even when it worked on-device: real photos always fell back to BLE fragmentation. Raise the prep budget so genuinely detailed photos land well above 64 KiB while staying under the 512 KiB FileTransferLimits.maxImageBytes hard cap: - defaultMaxDimension 448 -> 1024 px - compressionQuality 0.82 -> 0.85 - targetImageBytes 45 KB -> 200 KB (a ceiling, not a target to hit) Measured on a representative photo-like image: ~40 KB before -> ~190 KB after, crossing the 64 KiB offer threshold while remaining ~2.6x under the hard cap. Because the raised dimension makes near-incompressible inputs (e.g. full- frame noise) able to exceed maxImageBytes at the quality floor, prep now downscales-and-retries until the payload fits the hard cap, so a send can never fail with imageTooLarge on pathological input (it couldn't at 448 px). Also de-dupes the previously copy-pasted per-platform encodeJPEG into one shared helper. Residual limitation: prep is not recipient-capability-aware. Images are prepared at this fidelity regardless of whether the recipient supports Wi-Fi bulk, so BLE-only peers and public broadcasts now carry ~190 KB images too (still within the existing 512 KiB image cap). Making prep choose fidelity per-recipient would need capability plumbing into the prep pipeline and doesn't help public broadcasts, so it's deferred. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,9 @@
|
||||
import BitFoundation
|
||||
import CoreGraphics
|
||||
import Foundation
|
||||
import ImageIO
|
||||
import Testing
|
||||
import UniformTypeIdentifiers
|
||||
#if os(iOS)
|
||||
import UIKit
|
||||
#else
|
||||
@@ -58,6 +61,25 @@ struct ChatMediaPreparationTests {
|
||||
#expect(prepared.packet.fileSize == UInt64(prepared.packet.content.count))
|
||||
#expect(prepared.packet.encode() != nil)
|
||||
}
|
||||
|
||||
/// A genuinely detailed photo must prepare to more than
|
||||
/// `TransportConfig.wifiBulkMinPayloadBytes` (64 KiB); otherwise the Wi-Fi
|
||||
/// bulk (AWDL) data plane is never offered in production because
|
||||
/// `WifiBulkPolicy.shouldOffer` requires `payloadBytes > 64 KiB`.
|
||||
/// Regression guard for the ~40 KB over-compression gap (PR #1385).
|
||||
@Test
|
||||
func prepareImagePacket_detailedImageExceedsWifiBulkThreshold() throws {
|
||||
let sourceURL = try makeDetailedImageURL(dimension: 1200)
|
||||
defer { try? FileManager.default.removeItem(at: sourceURL) }
|
||||
|
||||
let prepared = try ChatMediaPreparation.prepareImagePacket(from: sourceURL)
|
||||
defer { try? FileManager.default.removeItem(at: prepared.outputURL) }
|
||||
|
||||
// Comfortably above the 64 KiB Wi-Fi bulk offer threshold...
|
||||
#expect(prepared.packet.content.count > TransportConfig.wifiBulkMinPayloadBytes)
|
||||
// ...and still within the hard image cap for the BLE path.
|
||||
#expect(prepared.packet.content.count <= FileTransferLimits.maxImageBytes)
|
||||
}
|
||||
}
|
||||
|
||||
private func makeTemporaryImageURL() throws -> URL {
|
||||
@@ -87,6 +109,50 @@ private func makeTemporaryImageURL() throws -> URL {
|
||||
return url
|
||||
}
|
||||
|
||||
/// Builds a random-noise PNG. Noise is incompressible, so the JPEG the prep
|
||||
/// pipeline produces stays large — a faithful stand-in for a detailed photo,
|
||||
/// unlike a flat solid-color image which would compress to a few KB regardless
|
||||
/// of dimension.
|
||||
private func makeDetailedImageURL(dimension: Int) throws -> URL {
|
||||
let width = dimension
|
||||
let height = dimension
|
||||
let bytesPerPixel = 4
|
||||
let bytesPerRow = width * bytesPerPixel
|
||||
|
||||
var pixels = [UInt8](repeating: 0, count: bytesPerRow * height)
|
||||
for index in pixels.indices {
|
||||
pixels[index] = UInt8.random(in: 0...255)
|
||||
}
|
||||
|
||||
let colorSpace = CGColorSpace(name: CGColorSpace.sRGB) ?? CGColorSpaceCreateDeviceRGB()
|
||||
guard let context = CGContext(
|
||||
data: &pixels,
|
||||
width: width,
|
||||
height: height,
|
||||
bitsPerComponent: 8,
|
||||
bytesPerRow: bytesPerRow,
|
||||
space: colorSpace,
|
||||
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue
|
||||
), let cgImage = context.makeImage() else {
|
||||
throw ChatMediaPreparationTestError.imageEncodingFailed
|
||||
}
|
||||
|
||||
let url = FileManager.default.temporaryDirectory.appendingPathComponent("detailed-\(UUID().uuidString).png")
|
||||
guard let destination = CGImageDestinationCreateWithURL(
|
||||
url as CFURL,
|
||||
UTType.png.identifier as CFString,
|
||||
1,
|
||||
nil
|
||||
) else {
|
||||
throw ChatMediaPreparationTestError.imageEncodingFailed
|
||||
}
|
||||
CGImageDestinationAddImage(destination, cgImage, nil)
|
||||
guard CGImageDestinationFinalize(destination) else {
|
||||
throw ChatMediaPreparationTestError.imageEncodingFailed
|
||||
}
|
||||
return url
|
||||
}
|
||||
|
||||
private enum ChatMediaPreparationTestError: Error {
|
||||
case imageEncodingFailed
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user