Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
5e67e01
feat: FLOW-UFO toy drones over UDP (#187)
openipc-ai Oct 15, 2026
5e459b7
test: ImageIO by reflection, unit tests compile against android.jar (…
openipc-ai Oct 15, 2026
8aaf666
fix: FLOW-UFO window and client after review (Qodo on #188)
openipc-ai Oct 15, 2026
4746e45
Merge v2 into feat/flowufo-187
openipc-ai Oct 15, 2026
417c46c
chore: version 2.11 (2.10 went to #190)
openipc-ai Oct 15, 2026
d9d4607
Merge v2 into feat/flowufo-187
compot-dev Oct 15, 2026
b898ca1
feat: FLOW-UFO after turbodrone (both ports, quality bytes, lagging s…
compot-dev Oct 15, 2026
dd56ae7
feat: Jieli HF toy drones over CTP (#187)
compot-dev Oct 15, 2026
4a9245a
fix: lock onto a port only by a frame fragment, new sockets on every …
compot-dev Oct 15, 2026
2eacdfa
fix: keepalive from the last one sent, quiet status in order with the…
compot-dev Oct 15, 2026
d094871
Merge feat/flowufo-187 into feat/jieli-187 (NetInfo: Android 6 bindin…
openipc-ai Oct 15, 2026
950277a
Merge pull request #198 from OpenIPC/feat/jieli-187
compot-dev Oct 15, 2026
3a04551
fix: /log keeps the app's lines and only Info and up from system libr…
openipc-ai Oct 15, 2026
b93fd47
feat: KY UFO toys (rtsp://…:7070/webcam) show MJPEG turned 90° counte…
openipc-ai Oct 15, 2026
05f3093
fix: a reconfigured RTSP session drops the previous camera's heartbea…
openipc-ai Oct 15, 2026
4e4c483
fix: KY UFO toys turned 270° by default; MJPEG shows the newest frame…
compot-dev Oct 16, 2026
3696475
feat: Jieli toys play the JPEG stream on UDP 2224 (YL3, #187)
compot-dev Oct 16, 2026
51b4ab1
test: Jieli status names both video ports (#187)
compot-dev Oct 16, 2026
e4364ee
Merge v2 into feat/flowufo-187
compot-dev Oct 16, 2026
168012d
chore: version 2.15 (2.14 went to #220)
compot-dev Oct 16, 2026
916c88d
docs: align Markdown tables for reading as plain text (toys, transpor…
compot-dev Oct 16, 2026
e37c2d0
feat: jieli:// turns frames 270° by default (?rotate=), why frames ar…
compot-dev Oct 16, 2026
c9179e3
Merge remote-tracking branch 'origin/v2' into feat/flowufo-187
compot-dev Oct 16, 2026
3f4ae3e
fix: open Qodo findings on #188 and #198 — FLOW-UFO sockets closed wh…
compot-dev Oct 16, 2026
100ac9b
fix: YL3 frames put together from each piece's own length (bytes 2-3)…
compot-dev Oct 16, 2026
776d32d
docs: toy drones are in the release (2.15): FLOW-UFO and KY UFO work,…
compot-dev Oct 16, 2026
367b897
fix: Jieli stream frames count each byte once and wait for pieces tha…
compot-dev Oct 16, 2026
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ Every item, the remote, sharing a URL from another app and **Reset**: [Usage](do
| [Testing and releases](docs/testing.md) | branches, CI, unit tests, versions and releases, branch builds, `mapping.txt`, building locally |
| [Security](docs/security.md) | credential store, debug server (LAN only), log redaction, no encryption on the network |
| [Devices](docs/devices.md) | verified and not working devices, hardware decoder notes, device and camera reports |
| [Drones and Wi‑Fi toys](docs/toys.md) | how toys stream, FLOW-UFO (in testing), what to try with an unknown toy, what to send |
| [Drones and Wi‑Fi toys](docs/toys.md) | how toys stream, FLOW-UFO, KY UFO and Jieli drones, what to try with an unknown toy, what to send |

The change history is the [merged pull requests](https://github.com/OpenIPC/decoder/pulls?q=is%3Apr+is%3Amerged+base%3Av2) and the [commits of v2](https://github.com/OpenIPC/decoder/commits/v2).

Expand Down
2 changes: 1 addition & 1 deletion README.ru.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@
| [Testing and releases](docs/testing.md) | ветки, CI, юнит-тесты, версии и релизы, сборки веток, `mapping.txt`, локальная сборка |
| [Security](docs/security.md) | хранилище учётных данных, отладочный сервер (только локальная сеть), скрытие паролей в логах, отсутствие шифрования в сети |
| [Devices](docs/devices.md) | проверенные и неработающие устройства, особенности аппаратных декодеров, отчёты об устройствах и камерах |
| [Drones and Wi‑Fi toys](docs/toys.md) | как передают видео игрушки, FLOW-UFO (в тестировании), что попробовать с неизвестной игрушкой, что прислать |
| [Drones and Wi‑Fi toys](docs/toys.md) | как передают видео игрушки, дроны FLOW-UFO, KY UFO и Jieli, что попробовать с неизвестной игрушкой, что прислать |

История изменений — это [принятые pull request'ы](https://github.com/OpenIPC/decoder/pulls?q=is%3Apr+is%3Amerged+base%3Av2) и [коммиты ветки v2](https://github.com/OpenIPC/decoder/commits/v2).

Expand Down
2 changes: 1 addition & 1 deletion README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@
| [Testing and releases](docs/testing.md) | 分支、CI、单元测试、版本与发布、分支构建、`mapping.txt`、本地构建 |
| [Security](docs/security.md) | 凭据存储、调试服务器(仅限局域网)、日志脱敏、网络传输未加密 |
| [Devices](docs/devices.md) | 已验证和无法使用的设备、硬件解码器说明、设备与摄像机报告 |
| [Drones and Wi‑Fi toys](docs/toys.md) | 玩具如何传输视频、FLOW-UFO(测试中)、遇到未知玩具时可以尝试什么、需要提交哪些信息 |
| [Drones and Wi‑Fi toys](docs/toys.md) | 玩具如何传输视频、FLOW-UFO、KY UFO 和 Jieli 无人机、遇到未知玩具时可以尝试什么、需要提交哪些信息 |

更新历史即[已合并的 pull request](https://github.com/OpenIPC/decoder/pulls?q=is%3Apr+is%3Amerged+base%3Av2) 和 [v2 分支的提交](https://github.com/OpenIPC/decoder/commits/v2)。

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 38
versionName "2.13"
versionCode 40
versionName "2.15"

buildConfigField "String", "GIT_HASH", "\"${getGitHash()}\""
}
Expand Down
2 changes: 2 additions & 0 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<!-- The networks and their subnets, to reach a toy drone on Wi-Fi without internet (#187) -->
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!-- Multicast RTP over Wi-Fi (#68): without a multicast lock the driver drops it -->
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
Expand Down
17 changes: 17 additions & 0 deletions android/app/src/main/java/com/openipc/decoder/DebugServer.java
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ interface Source {
* labels: only the frames, for the page that draws the text over them.
*/
byte[] snapshotJpeg(boolean labels);

/** Raw bytes of the single view's jieli:// slot (#187), or null. */
default byte[] jieliRaw(String which) { return null; }
}

private static DebugServer instance;
Expand Down Expand Up @@ -210,6 +213,20 @@ void serve(OutputStream out, String path, String query) throws IOException {
case "/log":
reply(out, 200, "text/plain; charset=utf-8", log.text());
break;
case "/jieli-frame.jpg":
case "/jieli-dropped.bin":
// The toy's frames as they came (#187), to see what the picture is made from
Source src = source;
String which = path.startsWith("/jieli-frame") ? "frame" : "dropped";
byte[] raw = src != null ? src.jieliRaw(which) : Jieli.Client.lastRaw(which);
if (raw != null) {
reply(out, 200, path.endsWith(".jpg") ? "image/jpeg"
: "application/octet-stream", raw);
} else {
reply(out, 404, "text/plain", "Nothing yet (a jieli:// slot in the single"
+ " view)\n");
}
break;
case "/report.txt":
// Downloaded by the browser, not shown (#207)
Date now = new Date();
Expand Down
49 changes: 48 additions & 1 deletion android/app/src/main/java/com/openipc/decoder/Decoder.java
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,7 @@ public void onCodecChanged(boolean h265, AudioConfig audio) {
menuManager.updateQuality(avg);
menuManager.applyQualityToActive(quadEnabled, mActive);
}
@Override public void onDetail(String text) { showStatusDetail(text); }
});
rtspClient.setAudioManager(audioManager);
menuManager = new MenuManager(this, mDensity);
Expand Down Expand Up @@ -755,6 +756,18 @@ private void showConnectedDetails() {
statusText.setText("Connected • " + transport + videoCodec + aCodec + bps);
}

/**
* What a flowufo:// slot waits for (#187), in place of the plain status; posted after the
* status it belongs to, so it wins. Not shown once the stream plays.
*/
private void showStatusDetail(String text) {
runOnUiThread(() -> {
if (statusText == null || "connected".equals(singleState)) return;
statusText.setText(text);
statusText.setVisibility(View.VISIBLE);
});
}

private void updateStatus(String status) {
singleState = status;
runOnUiThread(() -> {
Expand Down Expand Up @@ -1375,6 +1388,10 @@ private void removeQuadOverlay(int index) {
private final DebugServer.Source debugSource = new DebugServer.Source() {
@Override public Map<String, Object> snapshot() { return debugSnapshot(); }
@Override public byte[] snapshotJpeg(boolean labels) { return debugSnapshotJpeg(labels); }
@Override public byte[] jieliRaw(String which) {
RtspClient c = rtspClient;
return c != null ? c.jieliRaw(which) : Jieli.Client.lastRaw(which);
}
};
/** A cell counts as playing in a snapshot when it showed a frame this recently (#98). */
private static final long SNAPSHOT_LIVE_MS = 2000;
Expand Down Expand Up @@ -1579,6 +1596,16 @@ private Map<String, Object> debugState() {
// Frames shown in the last second and the playback mode of S (#153)
single.put("out_fps", singleOutFps);
single.put("decode", slides ? "key frames" : "full");
// The toy drone's protocol counters (#187)
Map<String, Object> ufo = rtspClient != null ? rtspClient.flowUfoStats() : null;
if (ufo != null) single.put("flowufo", ufo);
Map<String, Object> jl = rtspClient != null ? rtspClient.jieliStats() : null;
if (jl != null) single.put("jieli", jl);
Map<String, Object> hb = rtspClient != null ? rtspClient.heartbeatStats() : null;
if (hb != null) single.put("heartbeat", hb);
// MJPEG turned counter-clockwise, "size" is the turned one (#200)
int rotate = rtspClient != null && rtspClient.isJpeg() ? rtspClient.rotate() : 0;
if (rotate != 0) single.put("rotate", rotate);
m.put("single", single);
}
QuadCell[] cells = quadCells;
Expand Down Expand Up @@ -1617,6 +1644,13 @@ private Map<String, Object> debugState() {
? cell.keyLoad() : cell.fullLoad());
c.put("paused", cell.isPaused());
c.put("audio", i == quadAudio);
Map<String, Object> ufo = cell.flowUfoStats();
if (ufo != null) c.put("flowufo", ufo);
Map<String, Object> jl = cell.jieliStats();
if (jl != null) c.put("jieli", jl);
Map<String, Object> hb = cell.heartbeatStats();
if (hb != null) c.put("heartbeat", hb);
if (cell.rotate() != 0) c.put("rotate", cell.rotate());
}
list.add(c);
}
Expand Down Expand Up @@ -1684,6 +1718,8 @@ private String slotTransport(int i, String url) {
}
if (RtpListener.Url.matches(url)) return "UDP listen";
if (HttpMjpeg.matches(url)) return "HTTP";
if (FlowUfo.Url.matches(url)) return "FLOW-UFO UDP";
if (Jieli.Url.matches(url)) return "Jieli RTP";
Transport.Mode m = StreamProbe.probeMode(mTransports[i]);
return "check over " + (m == Transport.Mode.MULTICAST ? "multicast" : m.name());
}
Expand Down Expand Up @@ -2230,7 +2266,10 @@ private int dp(float dp) {
*/
private boolean drawJpeg(Frame frame, int gen) {
Surface s = mVideoSurface;
int[] size = jpegRenderer.render(frame, s, () -> gen == listenerGen && s == mVideoSurface);
RtspClient client = rtspClient;
int rotate = client != null ? client.rotate() : 0;
int[] size = jpegRenderer.render(frame, s, rotate,
() -> gen == listenerGen && s == mVideoSurface);
if (size != null) jpegShown(size[0], size[1]);
return size != null;
}
Expand Down Expand Up @@ -2293,6 +2332,14 @@ private void startListener() {
try {
Frame buffer = nalQueue.poll(5, TimeUnit.MILLISECONDS);
if (buffer != null && rtspClient.isJpeg()) {
// Every JPEG frame is whole: when drawing falls behind (a turned frame
// is drawn in software, #187), show the newest and drop the older ones
// instead of letting the delay grow
Frame newer;
while ((newer = nalQueue.poll()) != null) {
framePool.recycle(buffer);
buffer = newer;
}
// S: MJPEG has no key frames, one frame per second instead (#153); the
// second counts from a frame really shown (Qodo on #155)
long now = System.nanoTime() / 1_000_000;
Expand Down
Loading
Loading