From 5b2f137e3dcbaf7151a0a63316eb7d43d7130259 Mon Sep 17 00:00:00 2001 From: Compot Developer Date: Tue, 13 Oct 2026 14:14:13 +0000 Subject: [PATCH 1/2] feat: /stats.json lists the video decoders with instance limits and adaptive playback (#88 probe) --- .../java/com/openipc/decoder/CodecInfo.java | 68 +++++++++++++++++++ .../java/com/openipc/decoder/Decoder.java | 2 + 2 files changed, 70 insertions(+) create mode 100644 android/app/src/main/java/com/openipc/decoder/CodecInfo.java diff --git a/android/app/src/main/java/com/openipc/decoder/CodecInfo.java b/android/app/src/main/java/com/openipc/decoder/CodecInfo.java new file mode 100644 index 00000000..102168ed --- /dev/null +++ b/android/app/src/main/java/com/openipc/decoder/CodecInfo.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) OpenIPC https://openipc.org Prosperity Public License 3.0.0 + * + * CodecInfo.java — the device's video decoders and their abilities, for /stats.json (#88) + * + */ + +package com.openipc.decoder; + +import android.media.MediaCodecInfo; +import android.media.MediaCodecList; +import android.os.Build; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * What a shared key-frame decoder (#88) would rely on, per video decoder: the instance limit, + * adaptive playback (a new size mid-stream without a reconfigure) and the 1080p frame rate it + * declares. Built once; it does not change while the app runs. + */ +final class CodecInfo { + private static volatile List> cached; + + private CodecInfo() {} + + static List> videoDecoders() { + List> c = cached; + if (c != null) return c; + List> out = new ArrayList<>(); + try { + for (MediaCodecInfo ci : new MediaCodecList(MediaCodecList.ALL_CODECS) + .getCodecInfos()) { + if (ci.isEncoder()) continue; + for (String type : ci.getSupportedTypes()) { + if (!type.startsWith("video/")) continue; + Map m = new LinkedHashMap<>(); + m.put("name", ci.getName()); + m.put("type", type); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + m.put("hardware", ci.isHardwareAccelerated()); + } + try { + MediaCodecInfo.CodecCapabilities caps = ci.getCapabilitiesForType(type); + m.put("adaptive", caps.isFeatureSupported( + MediaCodecInfo.CodecCapabilities.FEATURE_AdaptivePlayback)); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + m.put("max_instances", caps.getMaxSupportedInstances()); + } + MediaCodecInfo.VideoCapabilities v = caps.getVideoCapabilities(); + if (v != null && v.isSizeSupported(1920, 1080)) { + m.put("fps_1080p", Math.round( + v.getSupportedFrameRatesFor(1920, 1080).getUpper())); + } + } catch (RuntimeException ignored) { + // Capabilities of a broken entry: the name and type are still worth it + } + out.add(m); + } + } + } catch (RuntimeException ignored) { + } + cached = out; + return out; + } +} diff --git a/android/app/src/main/java/com/openipc/decoder/Decoder.java b/android/app/src/main/java/com/openipc/decoder/Decoder.java index c97230d9..6193ccf4 100755 --- a/android/app/src/main/java/com/openipc/decoder/Decoder.java +++ b/android/app/src/main/java/com/openipc/decoder/Decoder.java @@ -1331,6 +1331,8 @@ private Map debugDevice() { for (String ip : NetInfo.addresses()) urls.add("http://" + ip + ":" + DebugServer.PORT + "/"); m.put("addresses", urls); m.put("server", DebugServer.get().state()); + // The video decoders and what they can do: instances, adaptive playback (#88) + m.put("decoders", CodecInfo.videoDecoders()); return m; } From 60b44fd1e27749216ec5f7f4f4fac878520ff428 Mon Sep 17 00:00:00 2001 From: Compot Developer Date: Tue, 13 Oct 2026 14:17:55 +0000 Subject: [PATCH 2/2] fix: decoder list built on a background thread, one broken codec skipped (Qodo on #137) --- .../java/com/openipc/decoder/CodecInfo.java | 28 ++++++++++++++++--- .../java/com/openipc/decoder/Decoder.java | 5 +++- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/android/app/src/main/java/com/openipc/decoder/CodecInfo.java b/android/app/src/main/java/com/openipc/decoder/CodecInfo.java index 102168ed..c7e5e6f4 100644 --- a/android/app/src/main/java/com/openipc/decoder/CodecInfo.java +++ b/android/app/src/main/java/com/openipc/decoder/CodecInfo.java @@ -26,13 +26,32 @@ final class CodecInfo { private CodecInfo() {} - static List> videoDecoders() { + /** The list, or null while it is still being built (Qodo on #137: never on the UI thread). */ + static List> videoDecodersIfReady() { + return cached; + } + + /** Builds the list on a background thread, once. */ + static void warmUp() { + if (cached != null) return; + Thread t = new Thread(CodecInfo::videoDecoders, "codec-info"); + t.setDaemon(true); + t.start(); + } + + static synchronized List> videoDecoders() { List> c = cached; if (c != null) return c; List> out = new ArrayList<>(); + MediaCodecInfo[] infos; try { - for (MediaCodecInfo ci : new MediaCodecList(MediaCodecList.ALL_CODECS) - .getCodecInfos()) { + infos = new MediaCodecList(MediaCodecList.ALL_CODECS).getCodecInfos(); + } catch (RuntimeException e) { + return out; // not cached: the next request tries again + } + for (MediaCodecInfo ci : infos) { + // One broken entry must not hide the others (Qodo on #137) + try { if (ci.isEncoder()) continue; for (String type : ci.getSupportedTypes()) { if (!type.startsWith("video/")) continue; @@ -59,8 +78,9 @@ static List> videoDecoders() { } out.add(m); } + } catch (RuntimeException ignored) { + // Skip this codec } - } catch (RuntimeException ignored) { } cached = out; return out; diff --git a/android/app/src/main/java/com/openipc/decoder/Decoder.java b/android/app/src/main/java/com/openipc/decoder/Decoder.java index 6193ccf4..caa5836a 100755 --- a/android/app/src/main/java/com/openipc/decoder/Decoder.java +++ b/android/app/src/main/java/com/openipc/decoder/Decoder.java @@ -181,6 +181,8 @@ protected void onCreate(Bundle savedInstanceState) { mUserAgent = "User-Agent: OpenIPC-Decoder/" + mVersion + "\r\n"; mDensity = getResources().getDisplayMetrics().density; credentials = CredentialStore.create(); + // The decoder list for /stats.json, built off the UI thread (#88) + CodecInfo.warmUp(); // --- Create managers --- videoDecoder = new MediaCodecManager(new MediaCodecManager.Listener() { @@ -1332,7 +1334,8 @@ private Map debugDevice() { m.put("addresses", urls); m.put("server", DebugServer.get().state()); // The video decoders and what they can do: instances, adaptive playback (#88) - m.put("decoders", CodecInfo.videoDecoders()); + List> decoders = CodecInfo.videoDecodersIfReady(); + m.put("decoders", decoders != null ? decoders : "loading"); return m; }