1
0
mirror of git://f0xx.org/android_cast synced 2026-07-29 06:58:51 +03:00

mobile sync

This commit is contained in:
Anton Afanasyeu
2026-06-06 20:39:05 +02:00
parent 46bf0fbbb6
commit bfb41f06d9
8 changed files with 221 additions and 39 deletions

View File

@@ -14,7 +14,7 @@ package com.foxx.androidcast.crash;
/** Crash reporter tuning from {@code settings.json} → {@code crash} (overrides app defaults). */ /** Crash reporter tuning from {@code settings.json} → {@code crash} (overrides app defaults). */
public final class CrashSettings { public final class CrashSettings {
public static final String DEFAULT_UPLOAD_URL = 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 boolean enabled;
public final String uploadUrl; public final String uploadUrl;

View File

@@ -24,6 +24,7 @@ public final class DevDiagnosticsController {
HW(R.id.dev_diag_hw, R.string.dev_diag_hw_title), HW(R.id.dev_diag_hw, R.string.dev_diag_hw_title),
CAMERA(R.id.dev_diag_camera, R.string.dev_diag_camera_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(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), NETWORK(R.id.dev_diag_network, R.string.dev_diag_network_title),
CASTING(R.id.dev_diag_casting, R.string.dev_diag_casting_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); DevDiagnosticsProbe.probeCameraCapture(activity), sectionText);
runSection(Section.AUDIO, () -> runSection(Section.AUDIO, () ->
DevDiagnosticsProbe.probeAudioCapture(activity), sectionText); DevDiagnosticsProbe.probeAudioCapture(activity), sectionText);
runSection(Section.AUDIO_CODECS, () ->
DevDiagnosticsProbe.probeAudioCodecs(activity), sectionText);
runSection(Section.NETWORK, () -> runSection(Section.NETWORK, () ->
DevDiagnosticsProbe.probeNetwork(activity, cancel), sectionText); DevDiagnosticsProbe.probeNetwork(activity, cancel), sectionText);
runSection(Section.CASTING, () -> runSection(Section.CASTING, () ->

View File

@@ -21,10 +21,14 @@ import androidx.core.content.ContextCompat;
import com.foxx.androidcast.AppPreferences; import com.foxx.androidcast.AppPreferences;
import com.foxx.androidcast.BuildConfig; import com.foxx.androidcast.BuildConfig;
import com.foxx.androidcast.CastSettings;
import com.foxx.androidcast.PlatformCastSupport; import com.foxx.androidcast.PlatformCastSupport;
import com.foxx.androidcast.crash.CrashSettingsStore; import com.foxx.androidcast.crash.CrashSettingsStore;
import com.foxx.androidcast.display.WiredDisplayMonitor; 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.media.codec.jni.NativeCodecBridge;
import com.foxx.androidcast.network.CastCodecFlags;
import com.foxx.androidcast.sender.CodecCatalog; import com.foxx.androidcast.sender.CodecCatalog;
import java.io.IOException; import java.io.IOException;
@@ -44,16 +48,38 @@ public final class DevDiagnosticsProbe {
public static String probeHardwareAcceleration(Context context) { public static String probeHardwareAcceleration(Context context) {
CodecCatalog.invalidateCapabilities(); CodecCatalog.invalidateCapabilities();
CastSettings settings = AppPreferences.loadSenderDefaults(context);
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
sb.append("API ").append(Build.VERSION.SDK_INT).append('\n'); sb.append("API ").append(Build.VERSION.SDK_INT).append('\n');
sb.append("libvpx JNI: ").append(NativeCodecBridge.isLibvpxAvailable() ? "yes" : "no").append('\n'); sb.append("Native JNI probes:\n");
sb.append('\n').append("Video encoders:\n"); sb.append(" libvpx: ").append(NativeCodecBridge.isLibvpxAvailable() ? "yes" : "no").append('\n');
appendVideoCodecLines(sb, true); sb.append(" Opus: ").append(NativeCodecBridge.isOpusAvailable() ? "yes (native)" : "no (stub→AAC)")
sb.append('\n').append("Video decoders:\n"); .append('\n');
appendVideoCodecLines(sb, false); 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<String> 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('\n').append("MIME summary (catalog):\n");
sb.append(" advertised enc: ").append(join(CodecCatalog.localEncoderMimes())).append('\n'); sb.append(" advertised enc: ").append(join(CodecCatalog.localEncoderMimes())).append('\n');
sb.append(" advertised dec: ").append(join(CodecCatalog.localDecoderMimes())); 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(); return sb.toString().trim();
} }
@@ -101,12 +127,37 @@ public final class DevDiagnosticsProbe {
sb.append(" • playback+mic (when mic available)\n"); sb.append(" • playback+mic (when mic available)\n");
sb.append(" • mic (calibration / no projection)\n"); sb.append(" • mic (calibration / no projection)\n");
} }
sb.append('\n').append("Voice codecs (settings):\n"); return sb.toString().trim();
for (CodecCatalog.AudioCodecChoice c : CodecCatalog.audioCodecChoicesForUi()) { }
sb.append("").append(c.codec.name())
.append(c.selectable ? "" : " (N/A)") public static String probeAudioCodecs(Context context) {
.append('\n'); 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(); return sb.toString().trim();
} }
@@ -150,7 +201,39 @@ public final class DevDiagnosticsProbe {
return sb.toString().trim(); return sb.toString().trim();
} }
private static void appendVideoCodecLines(StringBuilder sb, boolean encoder) { private static void appendVideoCodecLines(StringBuilder sb, boolean encoder, CastSettings settings) {
List<String> 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<String> lines = new ArrayList<>(); List<String> lines = new ArrayList<>();
MediaCodecList list = new MediaCodecList(MediaCodecList.REGULAR_CODECS); MediaCodecList list = new MediaCodecList(MediaCodecList.REGULAR_CODECS);
for (MediaCodecInfo info : list.getCodecInfos()) { 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(); boolean hw = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && info.isHardwareAccelerated();
for (String type : info.getSupportedTypes()) { for (String type : info.getSupportedTypes()) {
if (type == null || !type.startsWith("video/")) { if (type == null || !type.startsWith("audio/")) {
continue; continue;
} }
lines.add(String.format(Locale.US, " %s [%s] %s", type, hw ? "HW" : "SW", info.getName())); lines.add(String.format(Locale.US, " %s [%s] %s", type, hw ? "HW" : "SW", info.getName()));

View File

@@ -255,23 +255,11 @@ public final class DevNetworkSelfTestProbe {
return; return;
} }
HttpResult head = httpHead(url); HttpResult head = httpHead(url);
if (head.code > 0 && head.code < 500) { reportHttp(r, "head", head.code, head.summary);
r.itemPass("head", head.summary);
} else {
r.itemFail("head", head.summary);
}
HttpReadResult read = httpRead(url, 32 * 1024); HttpReadResult read = httpRead(url, 32 * 1024);
if (read.summary.startsWith("fail")) { reportHttpRead(r, "downlink", read.code, read.summary);
r.itemFail("downlink", read.summary);
} else {
r.itemPass("downlink", read.summary);
}
HttpResult uplink = httpPostJson(url, "{\"ping\":\"bandwidth\"}"); HttpResult uplink = httpPostJson(url, "{\"ping\":\"bandwidth\"}");
if (uplink.code > 0) { reportHttp(r, "uplink", uplink.code, uplink.summary);
r.itemPass("uplink", uplink.summary);
} else {
r.itemFail("uplink", uplink.summary);
}
} }
private static void probeNtp(Context context, AtomicBoolean cancel, DevNetworkSelfTestReport r) { 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 tz = new java.text.SimpleDateFormat("Z", Locale.US).format(new java.util.Date());
String body = "{\"heartbeat\":{\"type\":\"ntp\",\"date\":" + now + ",\"tz\":\"" + tz + "\"}}"; String body = "{\"heartbeat\":{\"type\":\"ntp\",\"date\":" + now + ",\"tz\":\"" + tz + "\"}}";
HttpTextResult text = httpPostJsonText(heartbeatUrl, body); HttpTextResult text = httpPostJsonText(heartbeatUrl, body);
if (text.summary.startsWith("HTTP") && !text.summary.startsWith("fail")) { if (httpSuccess(text.code)) {
r.itemPass("be-ntp", text.summary); r.itemPass("be-ntp", text.summary);
if (text.body != null && !text.body.isEmpty()) { if (text.body != null && !text.body.isEmpty()) {
r.itemPass("be-payload", truncate(text.body, 120)); 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 { } else {
r.itemFail("be-ntp", text.summary); r.itemFail("be-ntp", text.summary);
} }
@@ -331,7 +323,9 @@ public final class DevNetworkSelfTestProbe {
if (!backend.isEmpty()) { if (!backend.isEmpty()) {
HttpTextResult viaBackend = httpPostJsonText(toHeartbeatUrl(backend), HttpTextResult viaBackend = httpPostJsonText(toHeartbeatUrl(backend),
"{\"heartbeat\":{\"type\":\"ip\"}}"); "{\"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); r.itemFail("external-ip", viaBackend.summary);
} else { } else {
r.itemPass("external-ip", viaBackend.summary); r.itemPass("external-ip", viaBackend.summary);
@@ -342,7 +336,9 @@ public final class DevNetworkSelfTestProbe {
return; return;
} }
HttpTextResult web = httpGetText("https://api.ipify.org?format=json"); 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); r.itemFail("external-ip", web.summary);
} else { } else {
r.itemPass("external-ip", truncate(web.summary, 100)); r.itemPass("external-ip", truncate(web.summary, 100));
@@ -507,6 +503,31 @@ public final class DevNetworkSelfTestProbe {
return false; 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) { private static HttpResult httpHead(String url) {
long t0 = System.currentTimeMillis(); long t0 = System.currentTimeMillis();
try { try {
@@ -570,11 +591,11 @@ public final class DevNetworkSelfTestProbe {
long ms = Math.max(1L, System.currentTimeMillis() - t0); long ms = Math.max(1L, System.currentTimeMillis() - t0);
double kbps = (read * 8.0) / ms; double kbps = (read * 8.0) / ms;
c.disconnect(); 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)); + String.format(Locale.US, "%.1f Kbit/s", kbps));
} catch (Exception e) { } catch (Exception e) {
long ms = System.currentTimeMillis() - t0; 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); String text = readBody(c);
long ms = System.currentTimeMillis() - t0; long ms = System.currentTimeMillis() - t0;
c.disconnect(); c.disconnect();
return new HttpTextResult("HTTP " + code + " · " + ms + " ms", text); return new HttpTextResult(code, "HTTP " + code + " · " + ms + " ms", text);
} catch (Exception e) { } catch (Exception e) {
long ms = System.currentTimeMillis() - t0; 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); String text = readBody(c);
long ms = System.currentTimeMillis() - t0; long ms = System.currentTimeMillis() - t0;
c.disconnect(); c.disconnect();
return new HttpTextResult("HTTP " + code + " · " + ms + " ms", text); return new HttpTextResult(code, "HTTP " + code + " · " + ms + " ms", text);
} catch (Exception e) { } catch (Exception e) {
long ms = System.currentTimeMillis() - t0; 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 { private static final class HttpReadResult {
final int code;
final String summary; final String summary;
HttpReadResult(String summary) { HttpReadResult(int code, String summary) {
this.code = code;
this.summary = summary; this.summary = summary;
} }
} }
private static final class HttpTextResult { private static final class HttpTextResult {
final int code;
final String summary; final String summary;
final String body; final String body;
HttpTextResult(String summary, String body) { HttpTextResult(int code, String summary, String body) {
this.code = code;
this.summary = summary; this.summary = summary;
this.body = body; this.body = body;
} }

View File

@@ -26,6 +26,7 @@ import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Locale;
import java.util.Set; import java.util.Set;
/** Discovers device video encoder/decoder MIME types and UI codec choices with priority scores. */ /** 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); return audioCodecChoicesForUi(null);
} }
/** Advertised audio encoder lines for diagnostics (MIME + tier + priority). */
public static List<String> localAdvertisedAudioEncoderLines(CastSettings settings) {
CastSettings s = settings != null ? settings : new CastSettings();
List<String> 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<String> 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<String> localNativeSoftwareVideoLines(CastSettings settings) {
CastSettings s = settings != null ? settings : new CastSettings();
List<String> 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<String> localNativeSoftwareVideoDecoderLines(CastSettings settings) {
CastSettings s = settings != null ? settings : new CastSettings();
List<String> 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<CastSettings.VideoCodec> supportedChoices() { public static List<CastSettings.VideoCodec> supportedChoices() {
List<CastSettings.VideoCodec> out = new ArrayList<>(); List<CastSettings.VideoCodec> out = new ArrayList<>();
for (VideoCodecChoice c : videoCodecChoicesForUi()) { for (VideoCodecChoice c : videoCodecChoicesForUi()) {

View File

@@ -45,6 +45,10 @@
android:id="@+id/dev_diag_audio" android:id="@+id/dev_diag_audio"
layout="@layout/view_dev_diag_card" /> layout="@layout/view_dev_diag_card" />
<include
android:id="@+id/dev_diag_audio_codecs"
layout="@layout/view_dev_diag_card" />
<include <include
android:id="@+id/dev_diag_network" android:id="@+id/dev_diag_network"
layout="@layout/view_dev_diag_card" /> layout="@layout/view_dev_diag_card" />

View File

@@ -209,6 +209,7 @@
<string name="dev_diag_hw_title">Аппаратное ускорение (видео)</string> <string name="dev_diag_hw_title">Аппаратное ускорение (видео)</string>
<string name="dev_diag_camera_title">Захват с камеры</string> <string name="dev_diag_camera_title">Захват с камеры</string>
<string name="dev_diag_audio_title">Захват звука</string> <string name="dev_diag_audio_title">Захват звука</string>
<string name="dev_diag_audio_codecs_title">Аудиокодеки (сжатие)</string>
<string name="dev_diag_network_title">Сеть</string> <string name="dev_diag_network_title">Сеть</string>
<string name="dev_diag_casting_title">Другой вывод / трансляция</string> <string name="dev_diag_casting_title">Другой вывод / трансляция</string>
<string name="dev_sender_resolution_ui_section">Разрешение отправителя (тест)</string> <string name="dev_sender_resolution_ui_section">Разрешение отправителя (тест)</string>

View File

@@ -220,6 +220,7 @@
<string name="dev_diag_hw_title">Hardware acceleration (video)</string> <string name="dev_diag_hw_title">Hardware acceleration (video)</string>
<string name="dev_diag_camera_title">Camera capture</string> <string name="dev_diag_camera_title">Camera capture</string>
<string name="dev_diag_audio_title">Sound capture</string> <string name="dev_diag_audio_title">Sound capture</string>
<string name="dev_diag_audio_codecs_title">Audio codecs (compression)</string>
<string name="dev_diag_network_title">Network</string> <string name="dev_diag_network_title">Network</string>
<string name="dev_diag_casting_title">Other casting / display</string> <string name="dev_diag_casting_title">Other casting / display</string>
<string name="dev_sender_resolution_ui_section">Sender resolution UI (test)</string> <string name="dev_sender_resolution_ui_section">Sender resolution UI (test)</string>