diff --git a/app/src/main/java/com/foxx/androidcast/crash/CrashSettings.java b/app/src/main/java/com/foxx/androidcast/crash/CrashSettings.java index 06bbe13..982a2be 100644 --- a/app/src/main/java/com/foxx/androidcast/crash/CrashSettings.java +++ b/app/src/main/java/com/foxx/androidcast/crash/CrashSettings.java @@ -14,7 +14,7 @@ package com.foxx.androidcast.crash; /** Crash reporter tuning from {@code settings.json} → {@code crash} (overrides app defaults). */ public final class CrashSettings { public static final String DEFAULT_UPLOAD_URL = - "https://f0xx.org/app/androidcast_project/crashes/api/upload.php"; + "https://apps.f0xx.org/app/androidcast_project/crashes/api/upload.php"; public final boolean enabled; public final String uploadUrl; diff --git a/app/src/main/java/com/foxx/androidcast/dev/DevDiagnosticsController.java b/app/src/main/java/com/foxx/androidcast/dev/DevDiagnosticsController.java index f47bc33..6c6ac67 100644 --- a/app/src/main/java/com/foxx/androidcast/dev/DevDiagnosticsController.java +++ b/app/src/main/java/com/foxx/androidcast/dev/DevDiagnosticsController.java @@ -24,6 +24,7 @@ public final class DevDiagnosticsController { HW(R.id.dev_diag_hw, R.string.dev_diag_hw_title), CAMERA(R.id.dev_diag_camera, R.string.dev_diag_camera_title), AUDIO(R.id.dev_diag_audio, R.string.dev_diag_audio_title), + AUDIO_CODECS(R.id.dev_diag_audio_codecs, R.string.dev_diag_audio_codecs_title), NETWORK(R.id.dev_diag_network, R.string.dev_diag_network_title), CASTING(R.id.dev_diag_casting, R.string.dev_diag_casting_title); @@ -100,6 +101,8 @@ public final class DevDiagnosticsController { DevDiagnosticsProbe.probeCameraCapture(activity), sectionText); runSection(Section.AUDIO, () -> DevDiagnosticsProbe.probeAudioCapture(activity), sectionText); + runSection(Section.AUDIO_CODECS, () -> + DevDiagnosticsProbe.probeAudioCodecs(activity), sectionText); runSection(Section.NETWORK, () -> DevDiagnosticsProbe.probeNetwork(activity, cancel), sectionText); runSection(Section.CASTING, () -> diff --git a/app/src/main/java/com/foxx/androidcast/dev/DevDiagnosticsProbe.java b/app/src/main/java/com/foxx/androidcast/dev/DevDiagnosticsProbe.java index 11b486c..f6ad704 100644 --- a/app/src/main/java/com/foxx/androidcast/dev/DevDiagnosticsProbe.java +++ b/app/src/main/java/com/foxx/androidcast/dev/DevDiagnosticsProbe.java @@ -21,10 +21,14 @@ import androidx.core.content.ContextCompat; import com.foxx.androidcast.AppPreferences; import com.foxx.androidcast.BuildConfig; +import com.foxx.androidcast.CastSettings; import com.foxx.androidcast.PlatformCastSupport; import com.foxx.androidcast.crash.CrashSettingsStore; import com.foxx.androidcast.display.WiredDisplayMonitor; +import com.foxx.androidcast.media.CodecPriorityCatalog; +import com.foxx.androidcast.media.CodecTier; import com.foxx.androidcast.media.codec.jni.NativeCodecBridge; +import com.foxx.androidcast.network.CastCodecFlags; import com.foxx.androidcast.sender.CodecCatalog; import java.io.IOException; @@ -44,16 +48,38 @@ public final class DevDiagnosticsProbe { public static String probeHardwareAcceleration(Context context) { CodecCatalog.invalidateCapabilities(); + CastSettings settings = AppPreferences.loadSenderDefaults(context); StringBuilder sb = new StringBuilder(); sb.append("API ").append(Build.VERSION.SDK_INT).append('\n'); - sb.append("libvpx JNI: ").append(NativeCodecBridge.isLibvpxAvailable() ? "yes" : "no").append('\n'); - sb.append('\n').append("Video encoders:\n"); - appendVideoCodecLines(sb, true); - sb.append('\n').append("Video decoders:\n"); - appendVideoCodecLines(sb, false); + sb.append("Native JNI probes:\n"); + sb.append(" libvpx: ").append(NativeCodecBridge.isLibvpxAvailable() ? "yes" : "no").append('\n'); + sb.append(" Opus: ").append(NativeCodecBridge.isOpusAvailable() ? "yes (native)" : "no (stub→AAC)") + .append('\n'); + sb.append(" Speex: ").append(NativeCodecBridge.isSpeexAvailable() ? "yes (native)" : "no (stub→AAC)") + .append('\n'); + sb.append('\n').append("Video encoders (MediaCodec):\n"); + appendVideoCodecLines(sb, true, settings); + List swEnc = CodecCatalog.localNativeSoftwareVideoLines(settings); + if (!swEnc.isEmpty()) { + sb.append('\n').append("Video encoders (bundled SW, not in MediaCodec list):\n"); + for (String line : swEnc) { + sb.append(line).append('\n'); + } + } + sb.append('\n').append("Video decoders (MediaCodec):\n"); + appendVideoCodecLines(sb, false, settings); + if (!swEnc.isEmpty()) { + sb.append('\n').append("Video decoders (bundled SW):\n"); + for (String line : CodecCatalog.localNativeSoftwareVideoDecoderLines(settings)) { + sb.append(line).append('\n'); + } + } sb.append('\n').append("MIME summary (catalog):\n"); sb.append(" advertised enc: ").append(join(CodecCatalog.localEncoderMimes())).append('\n'); sb.append(" advertised dec: ").append(join(CodecCatalog.localDecoderMimes())); + if (NativeCodecBridge.isLibvpxAvailable()) { + sb.append('\n').append(" native SW: video/vp8, video/vp9 (libvpx JNI)"); + } return sb.toString().trim(); } @@ -101,12 +127,37 @@ public final class DevDiagnosticsProbe { sb.append(" • playback+mic (when mic available)\n"); sb.append(" • mic (calibration / no projection)\n"); } - sb.append('\n').append("Voice codecs (settings):\n"); - for (CodecCatalog.AudioCodecChoice c : CodecCatalog.audioCodecChoicesForUi()) { - sb.append(" • ").append(c.codec.name()) - .append(c.selectable ? "" : " (N/A)") - .append('\n'); + return sb.toString().trim(); + } + + public static String probeAudioCodecs(Context context) { + CastSettings settings = AppPreferences.loadSenderDefaults(context); + StringBuilder sb = new StringBuilder(); + sb.append("User preference (sender): ").append(settings.getAudioCodec().name()).append('\n'); + sb.append("Wire mask: 0x").append(Integer.toHexString(CastCodecFlags.localAudioMask())).append('\n'); + sb.append('\n').append("Settings choices (priority order after AUTO):\n"); + for (CodecCatalog.AudioCodecChoice c : CodecCatalog.audioCodecChoicesForUi(settings)) { + if (c.codec == CastSettings.AudioCodec.AUTO) { + sb.append(" • AUTO\n"); + continue; + } + String marker = c.codec == settings.getAudioCodec() ? " ← user" : ""; + sb.append(String.format(Locale.US, " • %s pri=%d%s\n", + c.codec.name(), c.priorityScore, marker)); } + sb.append('\n').append("MIME summary (catalog):\n"); + sb.append(" advertised enc:\n"); + for (String line : CodecCatalog.localAdvertisedAudioEncoderLines(settings)) { + sb.append(line).append('\n'); + } + sb.append(" advertised dec:\n"); + for (String line : CodecCatalog.localAdvertisedAudioDecoderLines(settings)) { + sb.append(line).append('\n'); + } + sb.append('\n').append("Audio encoders (MediaCodec):\n"); + appendAudioCodecLines(sb, true); + sb.append('\n').append("Audio decoders (MediaCodec):\n"); + appendAudioCodecLines(sb, false); return sb.toString().trim(); } @@ -150,7 +201,39 @@ public final class DevDiagnosticsProbe { return sb.toString().trim(); } - private static void appendVideoCodecLines(StringBuilder sb, boolean encoder) { + private static void appendVideoCodecLines(StringBuilder sb, boolean encoder, CastSettings settings) { + List lines = new ArrayList<>(); + MediaCodecList list = new MediaCodecList(MediaCodecList.REGULAR_CODECS); + for (MediaCodecInfo info : list.getCodecInfos()) { + if (info.isEncoder() != encoder) { + continue; + } + boolean hw = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && info.isHardwareAccelerated(); + CodecTier tier = hw ? CodecTier.HARDWARE : CodecTier.SOFTWARE; + String tierTag = hw ? "HW" : "SW"; + for (String type : info.getSupportedTypes()) { + if (type == null || !type.startsWith("video/")) { + continue; + } + int score = CodecPriorityCatalog.priorityScoreForVideoMime(type, tier, settings); + lines.add(formatCodecDiagLine(tierTag, score, type, info.getName())); + } + } + if (lines.isEmpty()) { + sb.append(" (none)\n"); + return; + } + for (String line : lines) { + sb.append(line).append('\n'); + } + } + + /** `[HW:0042] MIME: 'video/avc', c2.unisoc.avc.encoder` — tier tag + catalog priority score. */ + private static String formatCodecDiagLine(String tierTag, int priorityScore, String mime, String codecName) { + return String.format(Locale.US, " [%s:%04d] MIME: '%s', %s", tierTag, priorityScore, mime, codecName); + } + + private static void appendAudioCodecLines(StringBuilder sb, boolean encoder) { List lines = new ArrayList<>(); MediaCodecList list = new MediaCodecList(MediaCodecList.REGULAR_CODECS); for (MediaCodecInfo info : list.getCodecInfos()) { @@ -159,7 +242,7 @@ public final class DevDiagnosticsProbe { } boolean hw = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && info.isHardwareAccelerated(); for (String type : info.getSupportedTypes()) { - if (type == null || !type.startsWith("video/")) { + if (type == null || !type.startsWith("audio/")) { continue; } lines.add(String.format(Locale.US, " %s [%s] %s", type, hw ? "HW" : "SW", info.getName())); diff --git a/app/src/main/java/com/foxx/androidcast/dev/DevNetworkSelfTestProbe.java b/app/src/main/java/com/foxx/androidcast/dev/DevNetworkSelfTestProbe.java index bf04120..04c6c17 100644 --- a/app/src/main/java/com/foxx/androidcast/dev/DevNetworkSelfTestProbe.java +++ b/app/src/main/java/com/foxx/androidcast/dev/DevNetworkSelfTestProbe.java @@ -255,23 +255,11 @@ public final class DevNetworkSelfTestProbe { return; } HttpResult head = httpHead(url); - if (head.code > 0 && head.code < 500) { - r.itemPass("head", head.summary); - } else { - r.itemFail("head", head.summary); - } + reportHttp(r, "head", head.code, head.summary); HttpReadResult read = httpRead(url, 32 * 1024); - if (read.summary.startsWith("fail")) { - r.itemFail("downlink", read.summary); - } else { - r.itemPass("downlink", read.summary); - } + reportHttpRead(r, "downlink", read.code, read.summary); HttpResult uplink = httpPostJson(url, "{\"ping\":\"bandwidth\"}"); - if (uplink.code > 0) { - r.itemPass("uplink", uplink.summary); - } else { - r.itemFail("uplink", uplink.summary); - } + reportHttp(r, "uplink", uplink.code, uplink.summary); } private static void probeNtp(Context context, AtomicBoolean cancel, DevNetworkSelfTestReport r) { @@ -299,11 +287,15 @@ public final class DevNetworkSelfTestProbe { String tz = new java.text.SimpleDateFormat("Z", Locale.US).format(new java.util.Date()); String body = "{\"heartbeat\":{\"type\":\"ntp\",\"date\":" + now + ",\"tz\":\"" + tz + "\"}}"; HttpTextResult text = httpPostJsonText(heartbeatUrl, body); - if (text.summary.startsWith("HTTP") && !text.summary.startsWith("fail")) { + if (httpSuccess(text.code)) { r.itemPass("be-ntp", text.summary); if (text.body != null && !text.body.isEmpty()) { r.itemPass("be-payload", truncate(text.body, 120)); } + } else if (text.code == 404) { + r.itemFail("be-ntp", text.summary + " · heartbeat.php missing on host"); + } else if (text.code > 0) { + r.itemFail("be-ntp", text.summary); } else { r.itemFail("be-ntp", text.summary); } @@ -331,7 +323,9 @@ public final class DevNetworkSelfTestProbe { if (!backend.isEmpty()) { HttpTextResult viaBackend = httpPostJsonText(toHeartbeatUrl(backend), "{\"heartbeat\":{\"type\":\"ip\"}}"); - if (viaBackend.summary.startsWith("fail")) { + if (viaBackend.code < 0) { + r.itemFail("external-ip", viaBackend.summary); + } else if (!httpSuccess(viaBackend.code)) { r.itemFail("external-ip", viaBackend.summary); } else { r.itemPass("external-ip", viaBackend.summary); @@ -342,7 +336,9 @@ public final class DevNetworkSelfTestProbe { return; } HttpTextResult web = httpGetText("https://api.ipify.org?format=json"); - if (web.summary.startsWith("fail")) { + if (web.code < 0) { + r.itemFail("external-ip", web.summary); + } else if (!httpSuccess(web.code)) { r.itemFail("external-ip", web.summary); } else { r.itemPass("external-ip", truncate(web.summary, 100)); @@ -507,6 +503,31 @@ public final class DevNetworkSelfTestProbe { return false; } + private static boolean httpSuccess(int code) { + return code >= 200 && code < 300; + } + + /** 2xx pass; 404 fail; other 4xx warn (host answered, wrong path/method/body). */ + private static void reportHttp(DevNetworkSelfTestReport r, String key, int code, String summary) { + if (httpSuccess(code)) { + r.itemPass(key, summary); + } else if (code == 404) { + r.itemFail(key, summary + " · not found"); + } else if (code > 0 && code < 500) { + r.itemWarn(key, summary + " · reachable, unexpected status"); + } else { + r.itemFail(key, summary); + } + } + + private static void reportHttpRead(DevNetworkSelfTestReport r, String key, int code, String summary) { + if (summary.startsWith("fail")) { + r.itemFail(key, summary); + } else { + reportHttp(r, key, code, summary); + } + } + private static HttpResult httpHead(String url) { long t0 = System.currentTimeMillis(); try { @@ -570,11 +591,11 @@ public final class DevNetworkSelfTestProbe { long ms = Math.max(1L, System.currentTimeMillis() - t0); double kbps = (read * 8.0) / ms; c.disconnect(); - return new HttpReadResult("HTTP " + code + " · " + read + " B · " + return new HttpReadResult(code, "HTTP " + code + " · " + read + " B · " + String.format(Locale.US, "%.1f Kbit/s", kbps)); } catch (Exception e) { long ms = System.currentTimeMillis() - t0; - return new HttpReadResult("fail · " + e.getClass().getSimpleName() + " · " + ms + " ms"); + return new HttpReadResult(-1, "fail · " + e.getClass().getSimpleName() + " · " + ms + " ms"); } } @@ -594,10 +615,10 @@ public final class DevNetworkSelfTestProbe { String text = readBody(c); long ms = System.currentTimeMillis() - t0; c.disconnect(); - return new HttpTextResult("HTTP " + code + " · " + ms + " ms", text); + return new HttpTextResult(code, "HTTP " + code + " · " + ms + " ms", text); } catch (Exception e) { long ms = System.currentTimeMillis() - t0; - return new HttpTextResult("fail · " + e.getClass().getSimpleName() + " · " + ms + " ms", ""); + return new HttpTextResult(-1, "fail · " + e.getClass().getSimpleName() + " · " + ms + " ms", ""); } } @@ -612,10 +633,10 @@ public final class DevNetworkSelfTestProbe { String text = readBody(c); long ms = System.currentTimeMillis() - t0; c.disconnect(); - return new HttpTextResult("HTTP " + code + " · " + ms + " ms", text); + return new HttpTextResult(code, "HTTP " + code + " · " + ms + " ms", text); } catch (Exception e) { long ms = System.currentTimeMillis() - t0; - return new HttpTextResult("fail · " + e.getClass().getSimpleName() + " · " + ms + " ms", ""); + return new HttpTextResult(-1, "fail · " + e.getClass().getSimpleName() + " · " + ms + " ms", ""); } } @@ -651,18 +672,22 @@ public final class DevNetworkSelfTestProbe { } private static final class HttpReadResult { + final int code; final String summary; - HttpReadResult(String summary) { + HttpReadResult(int code, String summary) { + this.code = code; this.summary = summary; } } private static final class HttpTextResult { + final int code; final String summary; final String body; - HttpTextResult(String summary, String body) { + HttpTextResult(int code, String summary, String body) { + this.code = code; this.summary = summary; this.body = body; } diff --git a/app/src/main/java/com/foxx/androidcast/sender/CodecCatalog.java b/app/src/main/java/com/foxx/androidcast/sender/CodecCatalog.java index 293291f..83ffc7d 100644 --- a/app/src/main/java/com/foxx/androidcast/sender/CodecCatalog.java +++ b/app/src/main/java/com/foxx/androidcast/sender/CodecCatalog.java @@ -26,6 +26,7 @@ import java.util.Collections; import java.util.Comparator; import java.util.LinkedHashSet; import java.util.List; +import java.util.Locale; import java.util.Set; /** Discovers device video encoder/decoder MIME types and UI codec choices with priority scores. */ @@ -102,6 +103,70 @@ public final class CodecCatalog { return audioCodecChoicesForUi(null); } + /** Advertised audio encoder lines for diagnostics (MIME + tier + priority). */ + public static List localAdvertisedAudioEncoderLines(CastSettings settings) { + CastSettings s = settings != null ? settings : new CastSettings(); + List out = new ArrayList<>(); + out.add(audioAdvertisedLine(CastSettings.AudioCodec.AAC, MediaFormat.MIMETYPE_AUDIO_AAC, s)); + out.add(audioAdvertisedLine(CastSettings.AudioCodec.OPUS, "audio/opus", s)); + out.add(audioAdvertisedLine(CastSettings.AudioCodec.SPEEX, "audio/speex", s)); + out.add(audioAdvertisedLine(CastSettings.AudioCodec.PASSTHROUGH_DEBUG, "audio/raw", s)); + return out; + } + + public static List localAdvertisedAudioDecoderLines(CastSettings settings) { + return localAdvertisedAudioEncoderLines(settings); + } + + private static String audioAdvertisedLine(CastSettings.AudioCodec codec, String mime, CastSettings settings) { + CodecTier tier = com.foxx.androidcast.media.CodecPriorityCatalog.tierForAudioPreference(codec); + int score = com.foxx.androidcast.media.CodecPriorityCatalog.priorityScore(codec, settings); + String backend = audioBackendLabel(codec); + return String.format(Locale.US, " %s [%s] pri=%d · %s", mime, tier.name(), score, backend); + } + + private static String audioBackendLabel(CastSettings.AudioCodec codec) { + switch (codec) { + case OPUS: + return NativeCodecBridge.isOpusAvailable() ? "Opus native enc" : "Opus stub→AAC enc"; + case SPEEX: + return NativeCodecBridge.isSpeexAvailable() ? "Speex native enc" : "Speex stub→AAC enc"; + case PASSTHROUGH_DEBUG: + return "PCM passthrough (debug)"; + case AAC: + default: + return "MediaCodec AAC enc"; + } + } + + public static List localNativeSoftwareVideoLines(CastSettings settings) { + CastSettings s = settings != null ? settings : new CastSettings(); + List out = new ArrayList<>(); + if (!NativeCodecBridge.isLibvpxAvailable()) { + return out; + } + out.add(nativeVideoLine(MediaFormat.MIMETYPE_VIDEO_VP8, CastSettings.VideoCodec.LIBVPX_VP8, s, "enc")); + out.add(nativeVideoLine(MediaFormat.MIMETYPE_VIDEO_VP9, CastSettings.VideoCodec.LIBVPX_VP9, s, "enc")); + return out; + } + + public static List localNativeSoftwareVideoDecoderLines(CastSettings settings) { + CastSettings s = settings != null ? settings : new CastSettings(); + List out = new ArrayList<>(); + if (!NativeCodecBridge.isLibvpxAvailable()) { + return out; + } + out.add(nativeVideoLine(MediaFormat.MIMETYPE_VIDEO_VP8, CastSettings.VideoCodec.LIBVPX_VP8, s, "dec")); + out.add(nativeVideoLine(MediaFormat.MIMETYPE_VIDEO_VP9, CastSettings.VideoCodec.LIBVPX_VP9, s, "dec")); + return out; + } + + private static String nativeVideoLine(String mime, CastSettings.VideoCodec codec, CastSettings settings, + String role) { + int score = com.foxx.androidcast.media.CodecPriorityCatalog.priorityScore(codec, settings); + return String.format(Locale.US, " [SW:%04d] MIME: '%s', libvpx JNI %s", score, mime, role); + } + public static List supportedChoices() { List out = new ArrayList<>(); for (VideoCodecChoice c : videoCodecChoicesForUi()) { diff --git a/app/src/main/res/layout/activity_dev_diagnostics_panel.xml b/app/src/main/res/layout/activity_dev_diagnostics_panel.xml index f58165a..06d80b4 100644 --- a/app/src/main/res/layout/activity_dev_diagnostics_panel.xml +++ b/app/src/main/res/layout/activity_dev_diagnostics_panel.xml @@ -45,6 +45,10 @@ android:id="@+id/dev_diag_audio" layout="@layout/view_dev_diag_card" /> + + diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index b3d4266..3a46b52 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -209,6 +209,7 @@ Аппаратное ускорение (видео) Захват с камеры Захват звука + Аудиокодеки (сжатие) Сеть Другой вывод / трансляция Разрешение отправителя (тест) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b69d689..5a85b39 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -220,6 +220,7 @@ Hardware acceleration (video) Camera capture Sound capture + Audio codecs (compression) Network Other casting / display Sender resolution UI (test)