Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,13 @@ Versions after 2.17 are `MAJOR.MINOR.PATCH` (2.17 is followed by 2.17.1 or 2.18.
`docs/testing.md`, "Versions and releases"); the headings below keep the numbers as they
were released.

## 2.18.0 — 2026-10-19
## 2.18.1 — 2026-10-20

- Snapshot (`/snapshot.jpg`, the debug page): the single view's picture keeps its aspect ratio
in its cell, as on the screen, so a turned (portrait) slot is no longer stretched to 16:9
(#248).

## 2.18.0 — 2026-10-20

- Mirror in the camera settings, next to Turn: flips the picture left to right in the single
view and the grid, for H.264, H.265 and MJPEG; a mirrored H.264/H.265 slot is drawn with
Expand Down
4 changes: 2 additions & 2 deletions android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ android {
applicationId "com.openipc.decoder"
minSdk 21
targetSdk 36
versionCode 43
versionName "2.18.0"
versionCode 44
versionName "2.18.1"

buildConfigField "String", "GIT_HASH", "\"${getGitHash()}\""
}
Expand Down
4 changes: 4 additions & 0 deletions android/app/src/main/java/com/openipc/decoder/Decoder.java
Original file line number Diff line number Diff line change
Expand Up @@ -1498,6 +1498,10 @@ private List<Snapshots.Cell> snapshotCells(int[] side) {
? statusText.getText().toString() : c.live ? "Playing" : "No new frames";
// Copied with the grid cells, off the UI thread (#76)
if (c.live) c.view = mSurface;
// As on the screen: a turned picture is not stretched to the cell (#248)
if (mVideoWidth > 0 && mVideoHeight > 0) {
c.aspect = (float) mVideoWidth / mVideoHeight;
}
}
c.onScreen = onScreen(i);
cells.add(c);
Expand Down
33 changes: 31 additions & 2 deletions android/app/src/main/java/com/openipc/decoder/Snapshots.java
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,27 @@ static final class Cell {
boolean ok;
/** What the TV shows, in a grid drawn for the single view (#100): framed. */
boolean onScreen;
/**
* Width / height of the picture as the screen shows it, fitted into the cell with
* black around it (the single view, e.g. a turned slot, #248); 0 fills the cell.
*/
float aspect;
}

/**
* The largest rectangle of the given aspect ratio (width / height) centred in the cell,
* as {left, top, right, bottom}; the whole cell for 0 or a ratio within 1 % of its own.
*/
static int[] fit(int left, int top, int right, int bottom, float aspect) {
int w = right - left, h = bottom - top;
if (aspect <= 0 || w <= 0 || h <= 0 || Math.abs(aspect * h / w - 1f) < 0.01f) {
return new int[] {left, top, right, bottom};
}
int fw = w, fh = h;
if (aspect * h > w) fh = Math.max(1, Math.round(w / aspect));
else fw = Math.max(1, Math.round(h * aspect));
int l = left + (w - fw) / 2, t = top + (h - fh) / 2;
return new int[] {l, t, l + fw, t + fh};
}

private static HandlerThread copyThread;
Expand Down Expand Up @@ -100,12 +121,20 @@ static byte[] render(List<Cell> cells, int side, int width, boolean labels) {
rects[i] = new Rect(col * width / side, row * height / side,
(col + 1) * width / side, (row + 1) * height / side);
}
// Where each frame goes: its own aspect ratio inside the cell (#248)
Rect[] dst = new Rect[n];
for (int i = 0; i < n; i++) {
Cell c = cells.get(i);
Rect r = rects[i];
int[] f = fit(r.left, r.top, r.right, r.bottom, c != null ? c.aspect : 0);
dst[i] = new Rect(f[0], f[1], f[2], f[3]);
}
// All copies are requested at once and run side by side, then awaited together
Copy[] copies = new Copy[n];
for (int i = 0; i < n; i++) {
Cell c = cells.get(i);
if (c != null && c.live && c.view != null) {
copies[i] = Copy.start(c.view, rects[i].width(), rects[i].height());
copies[i] = Copy.start(c.view, dst[i].width(), dst[i].height());
}
}
long deadline = SystemClock.elapsedRealtime() + COPY_TIMEOUT_MS;
Expand Down Expand Up @@ -137,7 +166,7 @@ static byte[] render(List<Cell> cells, int side, int width, boolean labels) {
if (copies[i] != null && frame != null) copied++;
boolean shown = c.live && frame != null;
if (shown) {
canvas.drawBitmap(frame, null, r, frames);
canvas.drawBitmap(frame, null, dst[i], frames);
}
if (frame != null) frame.recycle();
if (labels) {
Expand Down
37 changes: 37 additions & 0 deletions android/app/src/test/java/com/openipc/decoder/SnapshotsTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Copyright (c) OpenIPC https://openipc.org Prosperity Public License 3.0.0
*
* SnapshotsTest.java — where a frame goes in its snapshot cell (#248)
*
*/

package com.openipc.decoder;

import static org.junit.Assert.assertArrayEquals;

import org.junit.Test;

public class SnapshotsTest {
@Test
public void fitKeepsTheCellForNoOrTheSameRatio() {
assertArrayEquals(new int[] {0, 0, 640, 360}, Snapshots.fit(0, 0, 640, 360, 0f));
assertArrayEquals(new int[] {0, 0, 640, 360}, Snapshots.fit(0, 0, 640, 360, 16f / 9));
// 1280×720 into a cell of 213×120: within 1 %, no one-pixel bars
assertArrayEquals(new int[] {213, 120, 426, 240}, Snapshots.fit(213, 120, 426, 240, 16f / 9));
}

@Test
public void fitCentresATurnedPictureWithBarsAtTheSides() {
// 1920×2560 (2560×1920 turned 90°) in a 16:9 cell: as tall as the cell, centred
assertArrayEquals(new int[] {185, 0, 455, 360}, Snapshots.fit(0, 0, 640, 360, 1920f / 2560));
// The same in the bottom right cell of a 3×3 grid of 1280×720
assertArrayEquals(new int[] {976, 480, 1156, 720},
Snapshots.fit(853, 480, 1280, 720, 1920f / 2560));
}

@Test
public void fitPutsBarsAboveAndBelowAWiderPicture() {
// 32:9 in a 16:9 cell
assertArrayEquals(new int[] {0, 90, 640, 270}, Snapshots.fit(0, 0, 640, 360, 32f / 9));
}
}
2 changes: 1 addition & 1 deletion docs/debug.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ A JPEG laid out like the screen, at the screen's resolution (at most 1920 wide,

## Stream checks in the single view

In the single view (and the U view) the page and the snapshot still show the whole grid ([#100](https://github.com/OpenIPC/decoder/issues/100)): the camera on screen is framed in blue, and every other configured slot gets a short stream check while the page is open. Its cell says what was found, for example `Stream OK · H.265 · 25 fps · 3.1 Mbit/s · checked 12 s ago`, or the error.
In the single view (and the U view) the page and the snapshot still show the whole grid ([#100](https://github.com/OpenIPC/decoder/issues/100)): the camera on screen is framed in blue, its picture fitted into the cell with its own aspect ratio, as on the screen (a turned slot is not stretched, [#248](https://github.com/OpenIPC/decoder/issues/248)), and every other configured slot gets a short stream check while the page is open. Its cell says what was found, for example `Stream OK · H.265 · 25 fps · 3.1 Mbit/s · checked 12 s ago`, or the error.

- A check connects, receives packets for about 3 s and does not decode.
- Each slot is checked again about every 45 s, at most two checks run at a time, one new check starts every 2 s.
Expand Down
Loading