1
0
mirror of git://f0xx.org/android_cast synced 2026-07-29 05:17:39 +03:00

for tests

Signed-off-by: Anton Afanasyeu <a.afanasieff@gmail.com>
This commit is contained in:
Anton Afanasyeu
2026-05-18 19:06:56 +02:00
parent 598eec63b0
commit 115e0e1548
13 changed files with 348 additions and 43 deletions

View File

@@ -20,6 +20,7 @@ public final class AppPreferences {
private static final String KEY_PLAY_INCOMING_AUDIO = "play_incoming_audio";
private static final String KEY_DEV_RECEIVER_DEBUG_OVERLAY = "dev_receiver_debug_overlay";
private static final String KEY_DEV_GRAB_SESSION_STATS = "dev_grab_session_stats";
private static final String KEY_DEV_SHOW_CODEC_PRIORITY_SCORES = "dev_show_codec_priority_scores";
private static final String KEY_RECEIVER_PIP_USER_DEFINED = "receiver_pip_user_defined";
private static final String KEY_RECEIVER_PIP_ENABLED = "receiver_pip_enabled";
@@ -114,6 +115,21 @@ public final class AppPreferences {
prefs(context).edit().putBoolean(KEY_DEV_GRAB_SESSION_STATS, grab).apply();
}
/**
* Show priority score suffix in codec settings spinners (also on when receiver debug overlay is on).
*/
public static boolean isShowCodecPriorityScores(Context context) {
return isShowReceiverDebugOverlay(context) || isDevShowCodecPriorityScoresExplicit(context);
}
public static boolean isDevShowCodecPriorityScoresExplicit(Context context) {
return prefs(context).getBoolean(KEY_DEV_SHOW_CODEC_PRIORITY_SCORES, false);
}
public static void setShowCodecPriorityScores(Context context, boolean show) {
prefs(context).edit().putBoolean(KEY_DEV_SHOW_CODEC_PRIORITY_SCORES, show).apply();
}
/**
* Receiver PiP: user toggle when set; otherwise derived from PiP support + notifications permission.
*/

View File

@@ -183,7 +183,7 @@ public final class CastSettingsBinder {
boolean[] selectable = new boolean[choices.size()];
for (int i = 0; i < choices.size(); i++) {
values[i] = choices.get(i).codec;
labels[i] = labelVideoCodec(activity, choices.get(i).codec);
labels[i] = labelVideoCodec(activity, choices.get(i).codec, choices.get(i).priorityScore);
selectable[i] = choices.get(i).selectable;
}
CastSettings.VideoCodec current = settings.getVideoCodec();
@@ -194,6 +194,14 @@ public final class CastSettingsBinder {
SettingsEnumSpinner.bind(spinner, values, labels, selectable, current, settings::setVideoCodec);
}
/** Rebind codec spinners after {@code codecs.json} reload or debug score toggle. */
public static void refreshCodecSpinners(AppCompatActivity activity, CastSettings senderSettings,
CastSettings receiverSettings) {
bindVideoCodec(activity, senderSettings, R.id.spinner_sender_video_codec);
bindVideoCodec(activity, receiverSettings, R.id.spinner_receiver_video_codec);
bindAudioCodec(activity, senderSettings);
}
private static boolean isVideoCodecSelectable(List<CodecCatalog.VideoCodecChoice> choices,
CastSettings.VideoCodec codec) {
for (CodecCatalog.VideoCodecChoice c : choices) {
@@ -221,7 +229,7 @@ public final class CastSettingsBinder {
boolean[] selectable = new boolean[choices.size()];
for (int i = 0; i < choices.size(); i++) {
values[i] = choices.get(i).codec;
labels[i] = labelAudioCodec(activity, choices.get(i).codec);
labels[i] = labelAudioCodec(activity, choices.get(i).codec, choices.get(i).priorityScore);
selectable[i] = choices.get(i).selectable;
}
CastSettings.AudioCodec current = settings.getAudioCodec();
@@ -552,35 +560,57 @@ public final class CastSettingsBinder {
}
}
private static String labelVideoCodec(AppCompatActivity activity, CastSettings.VideoCodec codec) {
private static String labelVideoCodec(AppCompatActivity activity, CastSettings.VideoCodec codec,
int priorityScore) {
String base;
switch (codec) {
case H264:
return activity.getString(R.string.codec_h264);
base = activity.getString(R.string.codec_h264);
break;
case H265:
return activity.getString(R.string.codec_h265);
base = activity.getString(R.string.codec_h265);
break;
case LIBVPX_VP8:
return activity.getString(R.string.codec_libvpx_vp8);
base = activity.getString(R.string.codec_libvpx_vp8);
break;
case LIBVPX_VP9:
return activity.getString(R.string.codec_libvpx_vp9);
base = activity.getString(R.string.codec_libvpx_vp9);
break;
case PASSTHROUGH:
return activity.getString(R.string.codec_passthrough);
base = activity.getString(R.string.codec_passthrough);
break;
case AUTO:
default:
return activity.getString(R.string.option_auto);
}
return appendPriorityScore(activity, base, codec != CastSettings.VideoCodec.AUTO, priorityScore);
}
private static String labelAudioCodec(AppCompatActivity activity, CastSettings.AudioCodec codec) {
private static String labelAudioCodec(AppCompatActivity activity, CastSettings.AudioCodec codec,
int priorityScore) {
String base;
switch (codec) {
case AAC:
return activity.getString(R.string.codec_aac);
base = activity.getString(R.string.codec_aac);
break;
case SPEEX:
return activity.getString(R.string.codec_speex);
base = activity.getString(R.string.codec_speex);
break;
case OPUS:
return activity.getString(R.string.codec_opus);
base = activity.getString(R.string.codec_opus);
break;
case AUTO:
default:
return activity.getString(R.string.codec_audio_auto);
}
return appendPriorityScore(activity, base, codec != CastSettings.AudioCodec.AUTO, priorityScore);
}
private static String appendPriorityScore(AppCompatActivity activity, String base, boolean showScore,
int priorityScore) {
if (!showScore || !AppPreferences.isShowCodecPriorityScores(activity)) {
return base;
}
return base + activity.getString(R.string.codec_priority_score_suffix, priorityScore);
}
}

View File

@@ -122,7 +122,9 @@ public class CastSettingsFragment extends Fragment {
@Override
public void onResume() {
super.onResume();
CastSettingsBinder.refreshReceiverPipFromPermissions((AppCompatActivity) requireActivity());
AppCompatActivity activity = (AppCompatActivity) requireActivity();
CastSettingsBinder.refreshReceiverPipFromPermissions(activity);
CastSettingsBinder.refreshCodecSpinners(activity, senderSettings, receiverSettings);
}
@Override

View File

@@ -2,10 +2,14 @@ package com.foxx.androidcast;
import android.content.Context;
import android.os.Bundle;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import com.foxx.androidcast.media.CodecPriorityCatalog;
/** Hidden developer settings (opened via version label easter egg). */
public class DeveloperSettingsActivity extends AppCompatActivity {
@Override
@@ -21,14 +25,24 @@ public class DeveloperSettingsActivity extends AppCompatActivity {
setTitle(R.string.developer_settings_title);
CheckBox debugOverlay = findViewById(R.id.check_receiver_debug_overlay);
CheckBox codecScores = findViewById(R.id.check_show_codec_priority_scores);
CheckBox sessionStats = findViewById(R.id.check_grab_session_stats);
Button reloadCodecs = findViewById(R.id.btn_reload_codecs_json);
debugOverlay.setChecked(AppPreferences.isShowReceiverDebugOverlay(this));
codecScores.setChecked(AppPreferences.isDevShowCodecPriorityScoresExplicit(this));
sessionStats.setChecked(AppPreferences.isGrabSessionStats(this));
debugOverlay.setOnCheckedChangeListener((buttonView, isChecked) ->
AppPreferences.setShowReceiverDebugOverlay(this, isChecked));
codecScores.setOnCheckedChangeListener((buttonView, isChecked) ->
AppPreferences.setShowCodecPriorityScores(this, isChecked));
sessionStats.setOnCheckedChangeListener((buttonView, isChecked) ->
AppPreferences.setGrabSessionStats(this, isChecked));
reloadCodecs.setOnClickListener(v -> {
String summary = CodecPriorityCatalog.reload(this);
Toast.makeText(this, summary, Toast.LENGTH_LONG).show();
});
}
}

View File

@@ -112,7 +112,7 @@ public final class CastDiagnosticsLabels {
? "libvpx VP9" : "libvpx VP9 (HW fallback)";
case AUTO:
default:
return "CodecNegotiator (HEVC→AVC→VP9→VP8)";
return "AUTO (lowest priority score wins)";
}
}

View File

@@ -1,9 +1,9 @@
package com.foxx.androidcast.media;
import android.media.MediaFormat;
import android.util.Log;
import com.foxx.androidcast.CastSettings;
import com.foxx.androidcast.sender.CodecCatalog;
import java.util.HashSet;
import java.util.List;
@@ -12,39 +12,61 @@ import java.util.Set;
/** Picks a video MIME type both sides support, honoring user preference when possible. */
public final class CodecNegotiator {
private static final String TAG = "CodecNegotiator";
private CodecNegotiator() {}
public static String negotiate(List<String> senderEncoders, List<String> receiverDecoders,
CastSettings.VideoCodec preference) {
return negotiate(senderEncoders, receiverDecoders, preference, null);
return negotiate(senderEncoders, receiverDecoders, preference, null, "negotiate");
}
public static String negotiate(List<String> senderEncoders, List<String> receiverDecoders,
CastSettings.VideoCodec preference, CastSettings negotiationSettings) {
return negotiate(senderEncoders, receiverDecoders, preference, negotiationSettings, "receiver");
}
public static String negotiate(List<String> senderEncoders, List<String> receiverDecoders,
CastSettings.VideoCodec preference, CastSettings negotiationSettings, String logRole) {
Set<String> enc = new HashSet<>(senderEncoders);
Set<String> dec = new HashSet<>(receiverDecoders);
CastSettings scoreSettings = negotiationSettings != null ? negotiationSettings : new CastSettings();
if (preference == CastSettings.VideoCodec.H264) {
return pick(MediaFormat.MIMETYPE_VIDEO_AVC, enc, dec);
String mime = pick(MediaFormat.MIMETYPE_VIDEO_AVC, enc, dec);
CodecPriorityCatalog.logExplicitNegotiation(logRole, mime,
CastSettings.VideoCodec.H264, scoreSettings);
return mime;
}
if (preference == CastSettings.VideoCodec.H265) {
return pick(MediaFormat.MIMETYPE_VIDEO_HEVC, enc, dec);
String mime = pick(MediaFormat.MIMETYPE_VIDEO_HEVC, enc, dec);
CodecPriorityCatalog.logExplicitNegotiation(logRole, mime,
CastSettings.VideoCodec.H265, scoreSettings);
return mime;
}
if (preference == CastSettings.VideoCodec.LIBVPX_VP8) {
return pick(MediaFormat.MIMETYPE_VIDEO_VP8, enc, dec);
String mime = pick(MediaFormat.MIMETYPE_VIDEO_VP8, enc, dec);
CodecPriorityCatalog.logExplicitNegotiation(logRole, mime,
CastSettings.VideoCodec.LIBVPX_VP8, scoreSettings);
return mime;
}
if (preference == CastSettings.VideoCodec.LIBVPX_VP9) {
return pick(MediaFormat.MIMETYPE_VIDEO_VP9, enc, dec);
String mime = pick(MediaFormat.MIMETYPE_VIDEO_VP9, enc, dec);
CodecPriorityCatalog.logExplicitNegotiation(logRole, mime,
CastSettings.VideoCodec.LIBVPX_VP9, scoreSettings);
return mime;
}
String best = CodecPriorityCatalog.negotiateBestVideoMime(
CodecPriorityCatalog.NegotiationDecision decision = CodecPriorityCatalog.decideBestVideoMime(
senderEncoders, receiverDecoders, scoreSettings);
if (best != null) {
return best;
if (decision != null) {
CodecPriorityCatalog.logNegotiationDecision(logRole, decision);
return decision.mime;
}
if (dec.contains(MediaFormat.MIMETYPE_VIDEO_AVC) && enc.contains(MediaFormat.MIMETYPE_VIDEO_AVC)) {
return MediaFormat.MIMETYPE_VIDEO_AVC;
String mime = MediaFormat.MIMETYPE_VIDEO_AVC;
Log.w(TAG, logRole + " codec fallback H.264 (no scored candidate)");
return mime;
}
throw new IllegalStateException("No common video codec (sender=" + enc + " receiver=" + dec + ")");
}

View File

@@ -19,6 +19,8 @@ import java.util.Comparator;
import java.util.List;
import java.util.Locale;
import androidx.annotation.Nullable;
/**
* Runtime codec priority: tier base (0 / 500 / 1000) + component hooks.
* Lower {@link #priorityScore} = higher preference in AUTO negotiation and settings ordering.
@@ -43,16 +45,34 @@ public final class CodecPriorityCatalog {
rootJson = loadJson(appContext);
}
/** Reload {@code assets/codecs.json} and refresh local codec capability cache. */
public static String reload(Context context) {
Context app = context.getApplicationContext();
appContext = app;
JSONObject loaded = loadJson(app);
rootJson = loaded;
CodecCatalog.invalidateCapabilities();
int overrides = countJsonOverrides(loaded);
String summary = "codecs.json reloaded (" + overrides + " override field(s))";
Log.i(TAG, summary);
return summary;
}
public static int priorityScore(CodecPriorityId id, CodecTier tier, CastSettings settings) {
return breakdown(id, tier, settings).total;
}
public static ScoreBreakdown breakdown(CodecPriorityId id, CodecTier tier, CastSettings settings) {
if (id == null || tier == null) {
return Integer.MAX_VALUE;
return new ScoreBreakdown(Integer.MAX_VALUE, 0, 0, 0, 0, 0, 0);
}
return tier.baseScore
+ component(id, KEY_INTERNAL, defaultInternal(id))
+ component(id, KEY_FEC, defaultFec(id))
+ component(id, KEY_PROTECTION, defaultProtection(id, settings))
+ component(id, KEY_OVERRIDE, 0)
+ component(id, KEY_BITRATE, defaultBitrate(id));
int internal = component(id, KEY_INTERNAL, defaultInternal(id));
int fec = component(id, KEY_FEC, defaultFec(id));
int protection = component(id, KEY_PROTECTION, defaultProtection(id, settings));
int override = component(id, KEY_OVERRIDE, 0);
int bitrate = component(id, KEY_BITRATE, defaultBitrate(id));
int total = tier.baseScore + internal + fec + protection + override + bitrate;
return new ScoreBreakdown(total, tier.baseScore, internal, fec, protection, override, bitrate);
}
public static int priorityScoreForVideoMime(String mime, CodecTier tier, CastSettings settings) {
@@ -82,24 +102,77 @@ public final class CodecPriorityCatalog {
}
/** Best MIME for AUTO: lowest score among codecs both sides can use. */
@Nullable
public static String negotiateBestVideoMime(List<String> senderEncoders, List<String> receiverDecoders,
CastSettings settings) {
NegotiationDecision decision = decideBestVideoMime(senderEncoders, receiverDecoders, settings);
return decision != null ? decision.mime : null;
}
/** AUTO negotiation with ranked candidates for logging and diagnostics. */
@Nullable
public static NegotiationDecision decideBestVideoMime(List<String> senderEncoders,
List<String> receiverDecoders, CastSettings settings) {
List<String> common = commonMimes(senderEncoders, receiverDecoders);
if (common.isEmpty()) {
return null;
}
String best = null;
int bestScore = Integer.MAX_VALUE;
List<NegotiationCandidate> candidates = new ArrayList<>();
for (String mime : common) {
int score = Math.max(
videoMimeScoreOnDevice(mime, false, settings),
assumedRemoteEncoderScore(mime, settings));
if (score < bestScore) {
bestScore = score;
best = mime;
}
candidates.add(buildCandidate(mime, settings));
}
return best;
Collections.sort(candidates, Comparator.comparingInt(c -> c.combinedScore));
NegotiationCandidate best = candidates.get(0);
return new NegotiationDecision(best, candidates);
}
public static void logNegotiationDecision(String role, NegotiationDecision decision) {
if (decision == null) {
return;
}
Log.i(TAG, role + " codec AUTO: selected " + decision.logLine());
for (NegotiationCandidate c : decision.candidates) {
boolean picked = c.mime != null && c.mime.equals(decision.mime);
Log.i(TAG, role + " codec candidate: " + c.logLine(picked));
}
}
public static void logExplicitNegotiation(String role, String mime, CastSettings.VideoCodec preference,
CastSettings settings) {
int score = combinedVideoMimeScore(mime, settings);
Log.i(TAG, role + " codec explicit " + preference.name() + ": "
+ displayMime(mime) + " (" + mime + ") score=" + score
+ " · " + formatVideoMimeBreakdown(mime, settings));
}
public static void logReceivedCodec(String role, String mime, CastSettings settings) {
int score = combinedVideoMimeScore(mime, settings);
Log.i(TAG, role + " codec selected by peer: "
+ displayMime(mime) + " (" + mime + ") score=" + score
+ " · " + formatVideoMimeBreakdown(mime, settings));
}
public static String formatVideoMimeBreakdown(String mime, CastSettings settings) {
CodecPriorityId id = CodecPriorityId.fromVideoMime(mime);
if (id == null) {
return mime + " unknown";
}
CodecTier localDec = CodecCatalog.effectiveTierForVideoMime(mime, false);
ScoreBreakdown dec = breakdown(id, localDec, settings);
int peerEncScore = assumedRemoteEncoderScore(mime, settings);
return String.format(Locale.US,
"combined=%d dec=%d (tier %d +%d+%d+%d+%d+%d) peerEnc=%d",
Math.max(dec.total, peerEncScore),
dec.total, dec.tierBase, dec.internal, dec.fec, dec.protection, dec.override, dec.bitrate,
peerEncScore);
}
private static NegotiationCandidate buildCandidate(String mime, CastSettings settings) {
int localDecoder = videoMimeScoreOnDevice(mime, false, settings);
int peerEncoder = assumedRemoteEncoderScore(mime, settings);
int combined = Math.max(localDecoder, peerEncoder);
return new NegotiationCandidate(mime, combined, localDecoder, peerEncoder,
formatVideoMimeBreakdown(mime, settings));
}
/** Conservative score when sender and receiver may use different tiers for the same MIME. */
@@ -192,6 +265,88 @@ public final class CodecPriorityCatalog {
}
}
public static final class ScoreBreakdown {
public final int total;
public final int tierBase;
public final int internal;
public final int fec;
public final int protection;
public final int override;
public final int bitrate;
ScoreBreakdown(int total, int tierBase, int internal, int fec, int protection, int override,
int bitrate) {
this.total = total;
this.tierBase = tierBase;
this.internal = internal;
this.fec = fec;
this.protection = protection;
this.override = override;
this.bitrate = bitrate;
}
}
public static final class NegotiationCandidate {
public final String mime;
public final int combinedScore;
public final int localDecoderScore;
public final int peerEncoderScore;
public final String detail;
NegotiationCandidate(String mime, int combinedScore, int localDecoderScore, int peerEncoderScore,
String detail) {
this.mime = mime;
this.combinedScore = combinedScore;
this.localDecoderScore = localDecoderScore;
this.peerEncoderScore = peerEncoderScore;
this.detail = detail;
}
String logLine(boolean selected) {
return (selected ? "* " : " ")
+ displayMime(mime) + " score=" + combinedScore
+ " (dec=" + localDecoderScore + " peerEnc=" + peerEncoderScore + ") " + detail;
}
}
public static final class NegotiationDecision {
public final String mime;
public final int score;
public final NegotiationCandidate selected;
public final List<NegotiationCandidate> candidates;
NegotiationDecision(NegotiationCandidate selected, List<NegotiationCandidate> candidates) {
this.selected = selected;
this.candidates = candidates;
this.mime = selected.mime;
this.score = selected.combinedScore;
}
String logLine() {
return displayMime(mime) + " (" + mime + ") score=" + score + " · " + selected.detail;
}
}
private static String displayMime(String mime) {
if (mime == null) {
return "?";
}
if (MediaFormat.MIMETYPE_VIDEO_AVC.equals(mime)) {
return "H.264";
}
if (MediaFormat.MIMETYPE_VIDEO_HEVC.equals(mime)) {
return "H.265";
}
if (MediaFormat.MIMETYPE_VIDEO_VP9.equals(mime)) {
return "VP9";
}
if (MediaFormat.MIMETYPE_VIDEO_VP8.equals(mime)) {
return "VP8";
}
int slash = mime.lastIndexOf('/');
return slash >= 0 ? mime.substring(slash + 1).toUpperCase(Locale.US) : mime;
}
private static List<String> commonMimes(List<String> enc, List<String> dec) {
List<String> out = new ArrayList<>();
if (enc == null || dec == null) {
@@ -336,6 +491,33 @@ public final class CodecPriorityCatalog {
}
}
private static int countJsonOverrides(JSONObject root) {
int n = 0;
n += countSectionOverrides(root.optJSONObject("video"));
n += countSectionOverrides(root.optJSONObject("audio"));
return n;
}
private static int countSectionOverrides(JSONObject section) {
if (section == null) {
return 0;
}
int n = 0;
java.util.Iterator<String> keys = section.keys();
while (keys.hasNext()) {
JSONObject codec = section.optJSONObject(keys.next());
if (codec == null) {
continue;
}
for (String k : new String[] {KEY_INTERNAL, KEY_FEC, KEY_PROTECTION, KEY_OVERRIDE, KEY_BITRATE}) {
if (codec.has(k)) {
n++;
}
}
}
return n;
}
private static JSONObject loadJson(Context context) {
try (BufferedReader r = new BufferedReader(new InputStreamReader(
context.getAssets().open(ASSET), StandardCharsets.UTF_8))) {

View File

@@ -2,6 +2,7 @@ package com.foxx.androidcast.network;
import com.foxx.androidcast.CastSettings;
import com.foxx.androidcast.media.CodecNegotiator;
import com.foxx.androidcast.media.CodecPriorityCatalog;
import com.foxx.androidcast.media.codec.PassthroughCodecPolicy;
import com.foxx.androidcast.sender.CodecCatalog;
@@ -74,6 +75,7 @@ public class CastSession {
throw new IOException("Codec negotiation timeout");
}
settings.setNegotiatedVideoMime(negotiated);
CodecPriorityCatalog.logReceivedCodec("sender", negotiated, settings);
return new HandshakeResult(deviceName, settings, negotiated);
}
@@ -118,7 +120,7 @@ public class CastSession {
List<String> receiverCaps = CodecCatalog.localDecoderMimes();
transport.send(CastProtocol.MSG_CODEC_CAPS, CastProtocol.codecCapsPayload(receiverCaps));
String mime = CodecNegotiator.negotiate(senderCaps, receiverCaps,
PassthroughCodecPolicy.forNegotiation(remote.getVideoCodec()), remote);
PassthroughCodecPolicy.forNegotiation(remote.getVideoCodec()), remote, "receiver");
transport.send(CastProtocol.MSG_CODEC_SELECTED, CastProtocol.codecSelectedPayload(mime));
remote.setNegotiatedVideoMime(mime);
return new HandshakeResult(senderName, remote, mime);

View File

@@ -172,6 +172,11 @@ public final class CodecCatalog {
choices.addAll(rest);
}
/** Clears cached HW/SW probe (call after {@link com.foxx.androidcast.media.CodecPriorityCatalog#reload}). */
public static void invalidateCapabilities() {
capabilities = null;
}
private static void ensureCapabilities() {
if (capabilities != null) {
return;

View File

@@ -101,7 +101,10 @@ public final class MultiCastCoordinator {
CastSession.HandshakeResult hs = session.clientHandshake(deviceName, pin, peerSettings);
applyNegotiatedProtection(session, peerSettings, hs.settings);
peers.add(new ActivePeer(t, session, hs.settings, hs.negotiatedVideoMime));
Log.i(TAG, "Peer ready " + t.name + " " + hs.negotiatedVideoMime);
Log.i(TAG, "Peer ready " + t.name + " " + hs.negotiatedVideoMime
+ " score="
+ com.foxx.androidcast.media.CodecNegotiator.priorityScoreForNegotiatedMime(
hs.negotiatedVideoMime, hs.settings));
} catch (IOException e) {
last = e;
Log.w(TAG, "Peer failed " + t.name + ": " + e.getMessage());

View File

@@ -25,6 +25,13 @@
android:checked="true"
android:text="@string/dev_show_receiver_debug_overlay" />
<CheckBox
android:id="@+id/check_show_codec_priority_scores"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@string/dev_show_codec_priority_scores" />
<CheckBox
android:id="@+id/check_grab_session_stats"
android:layout_width="match_parent"
@@ -32,5 +39,19 @@
android:layout_marginTop="12dp"
android:checked="true"
android:text="@string/dev_grab_session_stats" />
<Button
android:id="@+id/btn_reload_codecs_json"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:text="@string/dev_reload_codecs_json" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/dev_reload_codecs_json_hint"
android:textSize="12sp" />
</LinearLayout>
</ScrollView>

View File

@@ -187,7 +187,11 @@
<string name="licenses_load_error">Не могу найти факл лицензий</string>
<string name="developer_settings_title">Для разработчиков</string>
<string name="dev_show_receiver_debug_overlay">Показывать отладочный оверлей на приёмнике</string>
<string name="dev_show_codec_priority_scores">Показывать приоритет кодеков в настройках</string>
<string name="dev_grab_session_stats">Сохранять анонимную статистику сессий</string>
<string name="dev_reload_codecs_json">Перезагрузить codecs.json</string>
<string name="dev_reload_codecs_json_hint">Подгружает overrides из assets/codecs.json без перезапуска. Откройте настройки снова, чтобы обновить списки.</string>
<string name="codec_priority_score_suffix"> · %1$d</string>
<string name="label_audio_codec">Кодек для звука</string>
<string name="codec_libvpx_vp8">libvpx (VP8)</string>
<string name="codec_libvpx_vp9">libvpx (VP9)</string>

View File

@@ -187,7 +187,11 @@
<string name="licenses_load_error">Could not load license text.</string>
<string name="developer_settings_title">Developer settings</string>
<string name="dev_show_receiver_debug_overlay">Show debug overlay on receiver</string>
<string name="dev_show_codec_priority_scores">Show codec priority scores in settings</string>
<string name="dev_grab_session_stats">Grab anonymous session stats</string>
<string name="dev_reload_codecs_json">Reload codecs.json</string>
<string name="dev_reload_codecs_json_hint">Loads assets/codecs.json overrides without restarting the app. Reopen the settings drawer to refresh spinners.</string>
<string name="codec_priority_score_suffix"> · %1$d</string>
<string name="label_audio_codec">Voice codec</string>
<string name="codec_libvpx_vp8">libvpx (VP8)</string>
<string name="codec_libvpx_vp9">libvpx (VP9)</string>