| 1 | import CoreGraphics |
| 2 | import Foundation |
| 3 | import XCTest |
| 4 | |
| 5 | @testable import CallsKit |
| 6 | |
| 7 | /// 2026-10-05: a tile's placeholder centred in the whole tile, the stage |
| 8 | /// filling the window under the filmstrip, a camera cropped with headroom, |
| 9 | /// and the screen's backdrop. |
| 10 | final class TileLayoutTests: XCTestCase { |
| 11 | // MARK: Placeholder |
| 12 | |
| 13 | func testCentredInTheWholeTileWhenThereIsRoom() { |
| 14 | // A stage 600 tall, a 34 pt label block: centred, not above the label. |
| 15 | let top = PlaceholderPlacement.top(tile: 600, content: 80, reserve: 34) |
| 16 | XCTAssertEqual(top, 260) |
| 17 | XCTAssertEqual(top + 40, 300, "its middle is the tile's middle") |
| 18 | } |
| 19 | |
| 20 | func testRisesOnlyAsFarAsTheLabelNeedsOnAShortTile() { |
| 21 | // The mini window at its smallest, 200 × 112.5, label block 44: a |
| 22 | // 34 pt symbol centred would end at 73.25, past the label's top less |
| 23 | // the gap (112.5 - 48 = 64.5), so it rises by 8.75 and no more. |
| 24 | let top = PlaceholderPlacement.top(tile: 112.5, content: 34, reserve: 44) |
| 25 | XCTAssertEqual(top + 34, 112.5 - 44 - PlaceholderPlacement.gap, accuracy: 0.001) |
| 26 | XCTAssertLessThan(top, (112.5 - 34) / 2) |
| 27 | XCTAssertEqual((112.5 - 34) / 2 - top, 8.75, accuracy: 0.001) |
| 28 | } |
| 29 | |
| 30 | func testNeverAboveTheTop() { |
| 31 | XCTAssertEqual(PlaceholderPlacement.top(tile: 60, content: 50, reserve: 40), 0) |
| 32 | } |
| 33 | |
| 34 | func testNoLabelIsPlainCentring() { |
| 35 | XCTAssertEqual(PlaceholderPlacement.top(tile: 100, content: 20, reserve: 0), 40) |
| 36 | XCTAssertEqual(PlaceholderPlacement.room(tile: 100, reserve: 0), 100) |
| 37 | } |
| 38 | |
| 39 | func testRoomIsWhatChoosesTheWords() { |
| 40 | // At 200 wide the words and symbol (about 66 pt) do not fit above a |
| 41 | // 44 pt label in 112.5: the symbol stands alone. |
| 42 | XCTAssertLessThan(PlaceholderPlacement.room(tile: 112.5, reserve: 44), 66) |
| 43 | // At 320 (180 tall) they do. |
| 44 | XCTAssertGreaterThan(PlaceholderPlacement.room(tile: 180, reserve: 44), 66) |
| 45 | } |
| 46 | |
| 47 | func testEveryMiniSizeKeepsTheSymbolClearOfTheLabelAndCentredWhereItCan() { |
| 48 | for width in [200.0, 320, 480] { |
| 49 | let tile = width * 9 / 16 |
| 50 | let reserve: CGFloat = 44 |
| 51 | let content: CGFloat = PlaceholderPlacement.room(tile: tile, reserve: reserve) >= 66 ? 66 : 34 |
| 52 | let top = PlaceholderPlacement.top(tile: tile, content: content, reserve: reserve) |
| 53 | XCTAssertLessThanOrEqual(top + content, tile - reserve - PlaceholderPlacement.gap + 0.001, "\(width)") |
| 54 | let centred = (tile - content) / 2 |
| 55 | if centred + content <= tile - reserve - PlaceholderPlacement.gap { |
| 56 | XCTAssertEqual(top, centred, accuracy: 0.001, "\(width): centred when it clears") |
| 57 | } |
| 58 | } |
| 59 | } |
| 60 | |
| 61 | // MARK: The stage |
| 62 | |
| 63 | func testStageLabelRidesAboveTheFilmstrip() { |
| 64 | // Controls showing: the filmstrip rises 72 for the bar; band 102. |
| 65 | XCTAssertEqual(StageLabel.lift(bar: 72, band: 102), 174) |
| 66 | // Hidden: just the band. |
| 67 | XCTAssertEqual(StageLabel.lift(bar: 0, band: 102), 102) |
| 68 | // A call of two: no filmstrip, the bar alone. |
| 69 | XCTAssertEqual(StageLabel.lift(bar: 72, band: 0), 72) |
| 70 | } |
| 71 | |
| 72 | func testACameraFillsWithMoreCropFromTheBottom() { |
| 73 | // 16:9 into a 4:3 window: fills, crops sides evenly. |
| 74 | let wide = VideoFrame.place(video: CGSize(width: 1280, height: 720), in: CGSize(width: 800, height: 600), fit: false) |
| 75 | XCTAssertEqual(wide.height, 600, accuracy: 0.001) |
| 76 | XCTAssertEqual(wide.minX, -(wide.width - 800) / 2, accuracy: 0.001) |
| 77 | XCTAssertEqual(wide.minY, 0, accuracy: 0.001) |
| 78 | // 16:9 into a tall window: crops top and bottom, 35% from the top. |
| 79 | let tall = VideoFrame.place(video: CGSize(width: 1280, height: 720), in: CGSize(width: 1600, height: 600), fit: false) |
| 80 | let overflow = tall.height - 600 |
| 81 | XCTAssertGreaterThan(overflow, 0) |
| 82 | XCTAssertEqual(-tall.minY, overflow * VideoFrame.headroom, accuracy: 0.001) |
| 83 | XCTAssertEqual(tall.maxY - 600, overflow * (1 - VideoFrame.headroom), accuracy: 0.001) |
| 84 | } |
| 85 | |
| 86 | func testAScreenIsFittedWholeAndCentred() { |
| 87 | let r = VideoFrame.place(video: CGSize(width: 1920, height: 1200), in: CGSize(width: 1000, height: 1000), fit: true) |
| 88 | XCTAssertEqual(r.width, 1000, accuracy: 0.001) |
| 89 | XCTAssertEqual(r.minY, (1000 - r.height) / 2, accuracy: 0.001) |
| 90 | XCTAssertGreaterThanOrEqual(r.minY, 0) |
| 91 | } |
| 92 | |
| 93 | // MARK: The screen's backdrop |
| 94 | |
| 95 | func testBackdropPictureIsTheFramesColoursBlurred() throws { |
| 96 | // A 64 × 36 I420 frame: left half pure red, right half pure blue. |
| 97 | let (w, h) = (BackdropSampler.width, BackdropSampler.height) |
| 98 | var y = [UInt8](repeating: 0, count: w * h) |
| 99 | var u = [UInt8](repeating: 0, count: (w / 2) * (h / 2)) |
| 100 | var v = [UInt8](repeating: 0, count: (w / 2) * (h / 2)) |
| 101 | for row in 0..<h { |
| 102 | for col in 0..<w { y[row * w + col] = col < w / 2 ? 81 : 41 } |
| 103 | } |
| 104 | for row in 0..<(h / 2) { |
| 105 | for col in 0..<(w / 2) { |
| 106 | u[row * (w / 2) + col] = col < w / 4 ? 90 : 240 |
| 107 | v[row * (w / 2) + col] = col < w / 4 ? 240 : 110 |
| 108 | } |
| 109 | } |
| 110 | var rgba = y.withUnsafeBufferPointer { yp in |
| 111 | u.withUnsafeBufferPointer { up in |
| 112 | v.withUnsafeBufferPointer { vp in |
| 113 | BackdropSampler.rgbaFromI420( |
| 114 | width: w, height: h, y: yp.baseAddress!, strideY: w, u: up.baseAddress!, strideU: w / 2, |
| 115 | v: vp.baseAddress!, strideV: w / 2, outWidth: w, outHeight: h) |
| 116 | } |
| 117 | } |
| 118 | } |
| 119 | XCTAssertGreaterThan(rgba[0], 200, "red on the left") |
| 120 | XCTAssertGreaterThan(rgba[(w - 1) * 4 + 2], 200, "blue on the right") |
| 121 | let edge = (w / 2) * 4 |
| 122 | let before = rgba[edge] |
| 123 | BackdropSampler.boxBlur(&rgba, width: w, height: h, radius: 3, passes: 2) |
| 124 | XCTAssertLessThan(rgba[edge], before == 0 ? 255 : 255, "blurred") |
| 125 | XCTAssertGreaterThan(rgba[edge + 2], 40, "blue has bled across the edge") |
| 126 | XCTAssertGreaterThan(rgba[edge], 40, "and red") |
| 127 | XCTAssertNotNil(BackdropSampler.image(rgba, width: w, height: h)) |
| 128 | _ = try XCTUnwrap(rgba.last) |
| 129 | } |
| 130 | |
| 131 | func testBackdropWorkIsCheap() { |
| 132 | // What happens twice a second for a screen on the stage, after |
| 133 | // WebRTC's own scale to 64 × 36: conversion, blur and the CGImage. |
| 134 | let (w, h) = (BackdropSampler.width, BackdropSampler.height) |
| 135 | let y = [UInt8](repeating: 120, count: w * h) |
| 136 | let c = [UInt8](repeating: 128, count: (w / 2) * (h / 2)) |
| 137 | let runs = 50 |
| 138 | let start = Date() |
| 139 | for _ in 0..<runs { |
| 140 | var rgba = y.withUnsafeBufferPointer { yp in |
| 141 | c.withUnsafeBufferPointer { cp in |
| 142 | BackdropSampler.rgbaFromI420( |
| 143 | width: w, height: h, y: yp.baseAddress!, strideY: w, u: cp.baseAddress!, strideU: w / 2, |
| 144 | v: cp.baseAddress!, strideV: w / 2, outWidth: w, outHeight: h) |
| 145 | } |
| 146 | } |
| 147 | BackdropSampler.boxBlur(&rgba, width: w, height: h, radius: 3, passes: 2) |
| 148 | _ = BackdropSampler.image(rgba, width: w, height: h) |
| 149 | } |
| 150 | let each = Date().timeIntervalSince(start) / Double(runs) * 1000 |
| 151 | print("backdrop: \(String(format: "%.3f", each)) ms a picture, twice a second") |
| 152 | // A debug build; release is several times quicker. Well under a frame. |
| 153 | XCTAssertLessThan(each, 8) |
| 154 | } |
| 155 | } |
| 156 | |