Skip to content
Closed
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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,8 +145,8 @@ A slot also takes an HTTP MJPEG URL, such as Majestic's `http://user:password@<c

While the app runs it serves its debug information over HTTP on port **8085**, for a browser or `curl` on the same network:

- `http://<device>:8085/` — the snapshot fills the page, with each cell's camera, size, fps and losses (in red) drawn over it; the header shows the version, the device time and uptime. **Info On / Off** in the header hides the text over the cells (and the list below them on a narrow screen), leaving only the picture; the choice is remembered in the browser. A red note in the header says when the device's video decoder stopped responding (`decoder_stuck`);
- `http://<device>:8085/snapshot.jpg` (`?labels=0`: frames only) — a picture laid out like the screen, at the screen's resolution (at most 1920×1080), made from the frames the app shows right now; a cell without video is black with its status. Taken only when asked for; the page refreshes it every 3, 5 or 10 s while the tab is visible;
- `http://<device>:8085/` — the snapshot fills the page, with each cell's camera, size, fps and losses (in red) drawn over it; the header shows the version, the device time and uptime. **Info On / Off** in the header hides the text over the cells (and the list below them on a narrow screen), leaving only the picture; it is **Off** until switched on, and the choice is remembered in the browser. A red note in the header says when the device's video decoder stopped responding (`decoder_stuck`);
- `http://<device>:8085/snapshot.jpg` (`?labels=0`: frames only) — a picture laid out like the screen, at the screen's resolution (at most 1920×1080), made from the frames the app shows right now; a cell without video is black with its status. Taken only when asked for; the page refreshes it every 3, 5 (default) or 10 s while the tab is visible;
- `http://<device>:8085/stats.json` — the same as JSON, for scripts;
- In single view the page and the snapshot still show the whole grid (#100): the camera on screen is framed in blue, every other configured slot gets a short stream check (connect, a few seconds of packets, no decoding) about every 45 s while the page is open, and its cell says what was found, e.g. `Stream OK · H.265 · 25 fps · 3.1 Mbit/s · checked 12 s ago`, or the error. A slot of the camera on screen is never checked (some cameras take one client only), nor a `udp://` slot. `stats.json` has these cells under `slots`;
- `http://<device>:8085/log` — the last 500 lines of the app's log.
Expand Down
4 changes: 2 additions & 2 deletions README.ru.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,8 +145,8 @@ outgoing:

Пока приложение работает, оно отдаёт отладочную информацию по HTTP на порту **8085** — для браузера или `curl` из той же сети:

- `http://<приставка>:8085/` — снимок на всю страницу, поверх каждой ячейки камера, размер, fps и потери (красным); в шапке версия, время приставки и аптайм. Переключатель **Info On / Off** в шапке убирает текст поверх ячеек (и список под ними на узком экране), остаётся только картинка; выбор запоминается в браузере. Красная пометка в шапке сообщает, что видеодекодер устройства перестал отвечать (`decoder_stuck`);
- `http://<приставка>:8085/snapshot.jpg` (`?labels=0` — только кадры) — картинка с раскладкой экрана в разрешении экрана (не больше 1920×1080) из кадров, которые приложение показывает сейчас; ячейка без видео — чёрная с её статусом. Снимается только по запросу; страница обновляет её раз в 3, 5 или 10 с, пока вкладка видна;
- `http://<приставка>:8085/` — снимок на всю страницу, поверх каждой ячейки камера, размер, fps и потери (красным); в шапке версия, время приставки и аптайм. Переключатель **Info On / Off** в шапке убирает текст поверх ячеек (и список под ними на узком экране), остаётся только картинка; по умолчанию **Off**, выбор запоминается в браузере. Красная пометка в шапке сообщает, что видеодекодер устройства перестал отвечать (`decoder_stuck`);
- `http://<приставка>:8085/snapshot.jpg` (`?labels=0` — только кадры) — картинка с раскладкой экрана в разрешении экрана (не больше 1920×1080) из кадров, которые приложение показывает сейчас; ячейка без видео — чёрная с её статусом. Снимается только по запросу; страница обновляет её раз в 3, 5 (по умолчанию) или 10 с, пока вкладка видна;
- `http://<приставка>:8085/stats.json` — то же в JSON, для скриптов;
- В одиночном виде страница и снимок всё равно показывают всю сетку (#100): камера на экране обведена синей рамкой, а каждый другой настроенный слот, пока страница открыта, примерно раз в 45 с получает короткую проверку потока (подключение, несколько секунд пакетов, без декодирования); в его ячейке — результат, например `Stream OK · H.265 · 25 fps · 3.1 Mbit/s · checked 12 s ago`, или ошибка. Слот с камерой, которая на экране, не проверяется (некоторые камеры принимают только одного клиента), как и слоты `udp://`. В `stats.json` эти ячейки — в поле `slots`;
- `http://<приставка>:8085/log` — последние 500 строк лога приложения.
Expand Down
4 changes: 2 additions & 2 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,8 +145,8 @@ outgoing:

应用运行时会在 **8085** 端口通过 HTTP 提供调试信息,可在同一网络中用浏览器或 `curl` 访问:

- `http://<设备>:8085/` — 快照铺满页面,每个画面上叠加摄像机、分辨率、帧率和丢帧(红色);页头显示版本、设备时间和运行时长。页头的 **Info On / Off** 开关可隐藏画面上的文字(以及窄屏时下方的列表),只保留画面;选择会保存在浏览器中。设备视频解码器停止响应时,页头会出现红色提示(`decoder_stuck`);
- `http://<设备>:8085/snapshot.jpg`(`?labels=0`:仅画面)— 按屏幕布局拼成的图片,分辨率与屏幕相同(最大 1920×1080),取自应用当前显示的画面;没有视频的画面为黑色并显示其状态。仅在请求时截取;页面在标签页可见时每 3、5 或 10 秒刷新一次;
- `http://<设备>:8085/` — 快照铺满页面,每个画面上叠加摄像机、分辨率、帧率和丢帧(红色);页头显示版本、设备时间和运行时长。页头的 **Info On / Off** 开关可隐藏画面上的文字(以及窄屏时下方的列表),只保留画面;默认为 **Off**,选择会保存在浏览器中。设备视频解码器停止响应时,页头会出现红色提示(`decoder_stuck`);
- `http://<设备>:8085/snapshot.jpg`(`?labels=0`:仅画面)— 按屏幕布局拼成的图片,分辨率与屏幕相同(最大 1920×1080),取自应用当前显示的画面;没有视频的画面为黑色并显示其状态。仅在请求时截取;页面在标签页可见时每 3、5(默认)或 10 秒刷新一次;
- `http://<设备>:8085/stats.json` — 相同内容的 JSON,便于脚本使用;
- 单画面模式下,页面和快照仍显示完整网格(#100):屏幕上的摄像机用蓝框标出;页面打开期间,其他每个已配置的槽位约每 45 秒进行一次简短的流检测(连接、接收几秒数据包、不解码),其画面显示检测结果,例如 `Stream OK · H.265 · 25 fps · 3.1 Mbit/s · checked 12 s ago`,或错误信息。与屏幕上相同的摄像机的槽位不检测(部分摄像机只接受一个客户端),`udp://` 槽位也不检测。`stats.json` 中这些画面位于 `slots` 字段;
- `http://<设备>:8085/log` — 应用日志的最后 500 行。
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 30
versionName "2.05"
versionCode 31
versionName "2.06"

buildConfigField "String", "GIT_HASH", "\"${getGitHash()}\""
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -551,8 +551,9 @@ private static void closeQuietly(Socket s) {
+ "try{localStorage.setItem('every',every)}catch(x){}};mark();\n"
+ "var img=$('snap');img.onload=img.onerror=function(){later(snap,every*1000)};\n"
+ "function snap(){img.src='/snapshot.jpg?labels=0&t='+Date.now()}\n"
// Info on or off (#141), remembered like the interval
+ "var shown=1,iseg=$('info');try{shown=localStorage.getItem('info')==='0'?0:1}catch(x){}\n"
// Info on or off (#141), remembered like the interval; Off until chosen: the page
// opens on the clean picture, every 5 s
+ "var shown=0,iseg=$('info');try{shown=localStorage.getItem('info')==='1'?1:0}catch(x){}\n"
+ "function imark(){[].forEach.call(iseg.children,function(b){b.className=+b.value===sh"
+ "own?'on':''});document.body.classList.toggle('clean',!shown)}\n"
+ "iseg.onclick=function(e){if(!e.target.value)return;shown=+e.target.value;imark();"
Expand Down
53 changes: 48 additions & 5 deletions android/app/src/main/java/com/openipc/decoder/MenuManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import android.os.Build;
import android.text.InputType;
import android.text.SpannableString;
import android.text.style.ForegroundColorSpan;
import android.text.style.RelativeSizeSpan;
import android.text.style.SuperscriptSpan;
import android.util.Log;
Expand Down Expand Up @@ -68,6 +69,8 @@ class MenuManager {
private TextView mSettingsBtn;
/** Settings | WebUI, shown in the single view only (#173). */
private LinearLayout mSettingsRow;
/** Left items of the two-item rows, aligned with the slot row's middle (#173). */
private final java.util.List<TextView> mPairLefts = new java.util.ArrayList<>();
private TextView mWebUiBtn;
TextView[] camButtons;
private Dialog mBrowserDialog;
Expand Down Expand Up @@ -183,6 +186,9 @@ public boolean dispatchKeyEvent(KeyEvent event) {

LinearLayout camRow = new LinearLayout(activity);
camRow.setOrientation(LinearLayout.HORIZONTAL);
// Not inside the layout pass: changing a size there would be lost or loop
camRow.addOnLayoutChangeListener((v, l, t, r, b, ol, ot, or, ob) ->
camRow.post(() -> alignPairs(camRow)));
// 8 slots + U D Q (#13, #93) may not fit a narrow screen or a large font: the row scrolls,
// and follows the focus when moving with a remote
HorizontalScrollView camScroll = new HorizontalScrollView(activity);
Expand Down Expand Up @@ -290,10 +296,17 @@ public boolean dispatchKeyEvent(KeyEvent event) {
if (listener != null) listener.onToggleSlides();
});

String code = "Exit [v" + version + ", " + gitHash + "]";
// "Exit" centred in its half (#176): the small version text follows it, and an
// invisible copy of it in front balances the line, so the word itself is in the middle
String ver = "[v" + version + ", " + gitHash + "]";
String code = ver + " Exit " + ver;
int exitEnd = ver.length() + 5;
SpannableString s = new SpannableString(code);
s.setSpan(new SuperscriptSpan(), 5, s.length(), 0);
s.setSpan(new RelativeSizeSpan(0.5f), 5, s.length(), 0);
s.setSpan(new SuperscriptSpan(), 0, ver.length(), 0);
s.setSpan(new RelativeSizeSpan(0.5f), 0, ver.length(), 0);
s.setSpan(new ForegroundColorSpan(Color.TRANSPARENT), 0, ver.length(), 0);
s.setSpan(new SuperscriptSpan(), exitEnd, s.length(), 0);
s.setSpan(new RelativeSizeSpan(0.5f), exitEnd, s.length(), 0);

View divider = new View(activity);
divider.setBackgroundColor(Color.DKGRAY);
Expand Down Expand Up @@ -405,14 +418,16 @@ static boolean isNavigationKey(int keyCode) {
}

/**
* Two menu items side by side, split by a thin line exactly in the middle (#173): both get
* half the width. Left / Right move between them with a remote.
* Two menu items side by side, split by a thin line (#173), which {@link #alignPairs} puts
* under the gap between slots 6 and 7 of the row above. Left / Right move between them
* with a remote.
*/
private LinearLayout pairRow(TextView left, TextView right) {
LinearLayout row = new LinearLayout(activity);
row.setOrientation(LinearLayout.HORIZONTAL);
row.addView(left, new LinearLayout.LayoutParams(0,
LinearLayout.LayoutParams.WRAP_CONTENT, 1));
mPairLefts.add(left);
View line = new View(activity);
line.setBackgroundColor(Color.DKGRAY);
row.addView(line, new LinearLayout.LayoutParams(dp(1),
Expand All @@ -421,9 +436,37 @@ private LinearLayout pairRow(TextView left, TextView right) {
LinearLayout.LayoutParams.WRAP_CONTENT, 1));
left.setSingleLine(true);
right.setSingleLine(true);
// Labels in the middle of their halves (#176)
left.setGravity(Gravity.CENTER);
right.setGravity(Gravity.CENTER);
return row;
}

/**
* The left item of every two-item row ends where slot 7 begins, so the dividing lines run
* on from the gap between slots 6 and 7: the slot buttons differ in width, half the row
* is not that gap. Until the slot row is laid out, and while it is too wide for the menu,
* both halves stay equal.
*/
private void alignPairs(LinearLayout camRow) {
if (camButtons == null || camButtons.length < 7 || camButtons[6] == null) return;
int gap = (camButtons[5].getRight() + camButtons[6].getLeft()) / 2;
int width = gap - dp(1) / 2;
Comment on lines +451 to +454

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Dividers drift from scrolled slots 📎 Requirement gap ≡ Correctness

alignPairs() sets the pair-row divider positions from slot coordinates inside camRow without
accounting for the slot row’s horizontal scroll offset. When focus scrolls the slot row on a narrow
screen, the gap between slots 6 and 7 moves across the popup while both dividers remain fixed in the
separate pair rows.
Agent Prompt
## Issue description
The pair-row dividers use positions calculated in slot-row coordinates, but the slot row scrolls independently of the pair rows. After scrolling, the dividers no longer align with the gap between slots 6 and 7.

## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/MenuManager.java[188-200]
- android/app/src/main/java/com/openipc/decoder/MenuManager.java[439-450]

## Recommended Fix
Calculate the visible gap position using the horizontal scroll offset and update both divider positions whenever that offset changes, or keep the slot row and pair rows in a shared coordinate system. Keep both pair rows usable when the gap moves outside the viewport.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

// A slot row wider than the menu scrolls, and its gap moves with it: then plain halves,
// which keep both items whole (Qodo on #175)
View scroll = (View) camRow.getParent();
boolean fits = scroll != null && camRow.getWidth() <= scroll.getWidth();
for (TextView left : mPairLefts) {
LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) left.getLayoutParams();
int w = fits && width > 0 ? width : 0;
float weight = fits && width > 0 ? 0 : 1;
if (lp.width == w && lp.weight == weight) continue;
lp.width = w;
lp.weight = weight;
left.setLayoutParams(lp);
}
}

private TextView createItem(String title) {
TextView text = new TextView(activity);
text.setText(title);
Expand Down
Loading