1
0
mirror of git://f0xx.org/android_cast synced 2026-07-29 05:17:39 +03:00
This commit is contained in:
Anton Afanasyeu
2026-05-18 18:59:14 +02:00
parent 6d02a9ee82
commit 598eec63b0
9 changed files with 700 additions and 42 deletions

View File

@@ -0,0 +1,34 @@
{
"_comment": "Optional overrides for codec priority (lower total score = higher priority). Tier bases: HW=0, SW=500, passthrough/raw=1000.",
"video": {
"h264": {
"priority_override_score": 0
},
"h265": {
"priority_override_score": 0
},
"vp8": {
"priority_override_score": 0
},
"vp9": {
"priority_override_score": 0
},
"passthrough": {
"priority_override_score": 0
}
},
"audio": {
"aac": {
"priority_override_score": 0
},
"opus": {
"priority_override_score": 0
},
"speex": {
"priority_override_score": 0
},
"pcm": {
"priority_override_score": 0
}
}
}

View File

@@ -3,6 +3,8 @@ package com.foxx.androidcast;
import android.app.Application; import android.app.Application;
import android.content.Context; import android.content.Context;
import com.foxx.androidcast.media.CodecPriorityCatalog;
/** Applies saved theme and locale at process start. */ /** Applies saved theme and locale at process start. */
public class AndroidCastApplication extends Application { public class AndroidCastApplication extends Application {
@Override @Override
@@ -13,6 +15,7 @@ public class AndroidCastApplication extends Application {
@Override @Override
public void onCreate() { public void onCreate() {
super.onCreate(); super.onCreate();
CodecPriorityCatalog.init(this);
CastLocaleHelper.applyStoredLocale(this); CastLocaleHelper.applyStoredLocale(this);
CastThemeHelper.applyNightMode(this); CastThemeHelper.applyNightMode(this);
CastTrayNotifier.syncOnAppStart(this); CastTrayNotifier.syncOnAppStart(this);

View File

@@ -177,7 +177,7 @@ public final class CastSettingsBinder {
private static void bindVideoCodec(AppCompatActivity activity, CastSettings settings, int spinnerId) { private static void bindVideoCodec(AppCompatActivity activity, CastSettings settings, int spinnerId) {
Spinner spinner = activity.findViewById(spinnerId); Spinner spinner = activity.findViewById(spinnerId);
List<CodecCatalog.VideoCodecChoice> choices = CodecCatalog.videoCodecChoicesForUi(); List<CodecCatalog.VideoCodecChoice> choices = CodecCatalog.videoCodecChoicesForUi(settings);
CastSettings.VideoCodec[] values = new CastSettings.VideoCodec[choices.size()]; CastSettings.VideoCodec[] values = new CastSettings.VideoCodec[choices.size()];
String[] labels = new String[choices.size()]; String[] labels = new String[choices.size()];
boolean[] selectable = new boolean[choices.size()]; boolean[] selectable = new boolean[choices.size()];
@@ -215,7 +215,7 @@ public final class CastSettingsBinder {
if (spinner == null) { if (spinner == null) {
return; return;
} }
List<CodecCatalog.AudioCodecChoice> choices = CodecCatalog.audioCodecChoicesForUi(); List<CodecCatalog.AudioCodecChoice> choices = CodecCatalog.audioCodecChoicesForUi(settings);
CastSettings.AudioCodec[] values = new CastSettings.AudioCodec[choices.size()]; CastSettings.AudioCodec[] values = new CastSettings.AudioCodec[choices.size()];
String[] labels = new String[choices.size()]; String[] labels = new String[choices.size()];
boolean[] selectable = new boolean[choices.size()]; boolean[] selectable = new boolean[choices.size()];

View File

@@ -3,8 +3,8 @@ package com.foxx.androidcast.media;
import android.media.MediaFormat; import android.media.MediaFormat;
import com.foxx.androidcast.CastSettings; import com.foxx.androidcast.CastSettings;
import com.foxx.androidcast.sender.CodecCatalog;
import java.util.ArrayList;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
@@ -12,19 +12,19 @@ import java.util.Set;
/** Picks a video MIME type both sides support, honoring user preference when possible. */ /** Picks a video MIME type both sides support, honoring user preference when possible. */
public final class CodecNegotiator { public final class CodecNegotiator {
private static final String[] PREFERENCE_ORDER = {
MediaFormat.MIMETYPE_VIDEO_HEVC,
MediaFormat.MIMETYPE_VIDEO_AVC,
MediaFormat.MIMETYPE_VIDEO_VP9,
MediaFormat.MIMETYPE_VIDEO_VP8,
};
private CodecNegotiator() {} private CodecNegotiator() {}
public static String negotiate(List<String> senderEncoders, List<String> receiverDecoders, public static String negotiate(List<String> senderEncoders, List<String> receiverDecoders,
CastSettings.VideoCodec preference) { CastSettings.VideoCodec preference) {
return negotiate(senderEncoders, receiverDecoders, preference, null);
}
public static String negotiate(List<String> senderEncoders, List<String> receiverDecoders,
CastSettings.VideoCodec preference, CastSettings negotiationSettings) {
Set<String> enc = new HashSet<>(senderEncoders); Set<String> enc = new HashSet<>(senderEncoders);
Set<String> dec = new HashSet<>(receiverDecoders); Set<String> dec = new HashSet<>(receiverDecoders);
CastSettings scoreSettings = negotiationSettings != null ? negotiationSettings : new CastSettings();
if (preference == CastSettings.VideoCodec.H264) { if (preference == CastSettings.VideoCodec.H264) {
return pick(MediaFormat.MIMETYPE_VIDEO_AVC, enc, dec); return pick(MediaFormat.MIMETYPE_VIDEO_AVC, enc, dec);
} }
@@ -37,10 +37,11 @@ public final class CodecNegotiator {
if (preference == CastSettings.VideoCodec.LIBVPX_VP9) { if (preference == CastSettings.VideoCodec.LIBVPX_VP9) {
return pick(MediaFormat.MIMETYPE_VIDEO_VP9, enc, dec); return pick(MediaFormat.MIMETYPE_VIDEO_VP9, enc, dec);
} }
for (String mime : PREFERENCE_ORDER) {
if (enc.contains(mime) && dec.contains(mime)) { String best = CodecPriorityCatalog.negotiateBestVideoMime(
return mime; senderEncoders, receiverDecoders, scoreSettings);
} if (best != null) {
return best;
} }
if (dec.contains(MediaFormat.MIMETYPE_VIDEO_AVC) && enc.contains(MediaFormat.MIMETYPE_VIDEO_AVC)) { if (dec.contains(MediaFormat.MIMETYPE_VIDEO_AVC) && enc.contains(MediaFormat.MIMETYPE_VIDEO_AVC)) {
return MediaFormat.MIMETYPE_VIDEO_AVC; return MediaFormat.MIMETYPE_VIDEO_AVC;
@@ -87,4 +88,9 @@ public final class CodecNegotiator {
return MediaFormat.MIMETYPE_VIDEO_VP8.equals(mime) return MediaFormat.MIMETYPE_VIDEO_VP8.equals(mime)
|| MediaFormat.MIMETYPE_VIDEO_VP9.equals(mime); || MediaFormat.MIMETYPE_VIDEO_VP9.equals(mime);
} }
/** Score for diagnostics (lower = preferred). */
public static int priorityScoreForNegotiatedMime(String mime, CastSettings settings) {
return CodecPriorityCatalog.combinedVideoMimeScore(mime, settings);
}
} }

View File

@@ -0,0 +1,353 @@
package com.foxx.androidcast.media;
import android.content.Context;
import android.media.MediaFormat;
import android.util.Log;
import com.foxx.androidcast.CastSettings;
import com.foxx.androidcast.media.codec.jni.NativeCodecBridge;
import com.foxx.androidcast.sender.CodecCatalog;
import org.json.JSONObject;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
/**
* Runtime codec priority: tier base (0 / 500 / 1000) + component hooks.
* Lower {@link #priorityScore} = higher preference in AUTO negotiation and settings ordering.
*/
public final class CodecPriorityCatalog {
private static final String TAG = "CodecPriority";
private static final String ASSET = "codecs.json";
private static final String KEY_INTERNAL = "internal_codec_score";
private static final String KEY_FEC = "no_inband_fec_score";
private static final String KEY_PROTECTION = "no_inband_protection_score";
private static final String KEY_OVERRIDE = "priority_override_score";
private static final String KEY_BITRATE = "bitrate_score";
private static volatile JSONObject rootJson = new JSONObject();
private static volatile Context appContext;
private CodecPriorityCatalog() {}
public static void init(Context context) {
appContext = context.getApplicationContext();
rootJson = loadJson(appContext);
}
public static int priorityScore(CodecPriorityId id, CodecTier tier, CastSettings settings) {
if (id == null || tier == null) {
return Integer.MAX_VALUE;
}
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));
}
public static int priorityScoreForVideoMime(String mime, CodecTier tier, CastSettings settings) {
CodecPriorityId id = CodecPriorityId.fromVideoMime(mime);
if (id == null) {
return CodecTier.PASSTHROUGH.baseScore + 100;
}
return priorityScore(id, tier, settings);
}
public static int priorityScore(CastSettings.VideoCodec codec, CastSettings settings) {
CodecPriorityId id = CodecPriorityId.fromVideoPreference(codec);
if (id == null) {
return 0;
}
CodecTier tier = tierForVideoPreference(codec, id);
return priorityScore(id, tier, settings);
}
public static int priorityScore(CastSettings.AudioCodec codec, CastSettings settings) {
CodecPriorityId id = CodecPriorityId.fromAudioPreference(codec);
if (id == null) {
return 0;
}
CodecTier tier = tierForAudioPreference(codec);
return priorityScore(id, tier, settings);
}
/** Best MIME for AUTO: lowest score among codecs both sides can use. */
public static String negotiateBestVideoMime(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;
for (String mime : common) {
int score = Math.max(
videoMimeScoreOnDevice(mime, false, settings),
assumedRemoteEncoderScore(mime, settings));
if (score < bestScore) {
bestScore = score;
best = mime;
}
}
return best;
}
/** Conservative score when sender and receiver may use different tiers for the same MIME. */
public static int combinedVideoMimeScore(String mime, CastSettings settings) {
int localEncoder = videoMimeScoreOnDevice(mime, true, settings);
int localDecoder = videoMimeScoreOnDevice(mime, false, settings);
int assumedPeerEncoder = assumedRemoteEncoderScore(mime, settings);
return Math.max(Math.max(localEncoder, localDecoder), assumedPeerEncoder);
}
/**
* Estimates encoder-side score for the remote peer during handshake (only MIME caps known).
*/
public static int assumedRemoteEncoderScore(String mime, CastSettings remoteSettings) {
CodecPriorityId id = CodecPriorityId.fromVideoMime(mime);
if (id == null) {
return CodecTier.PASSTHROUGH.baseScore + 100;
}
CodecTier tier = assumedEncoderTier(id, remoteSettings);
return priorityScore(id, tier, remoteSettings);
}
private static CodecTier assumedEncoderTier(CodecPriorityId id, CastSettings remote) {
CastSettings.VideoCodec pref = remote != null ? remote.getVideoCodec() : CastSettings.VideoCodec.AUTO;
if (pref == CastSettings.VideoCodec.PASSTHROUGH) {
return CodecTier.PASSTHROUGH;
}
if (pref == CastSettings.VideoCodec.LIBVPX_VP8 && id == CodecPriorityId.VP8) {
return CodecTier.SOFTWARE;
}
if (pref == CastSettings.VideoCodec.LIBVPX_VP9 && id == CodecPriorityId.VP9) {
return CodecTier.SOFTWARE;
}
if (id == CodecPriorityId.VP8 || id == CodecPriorityId.VP9) {
return CodecTier.HARDWARE;
}
return CodecTier.HARDWARE;
}
public static int videoMimeScoreOnDevice(String mime, boolean encoder, CastSettings settings) {
CodecPriorityId id = CodecPriorityId.fromVideoMime(mime);
if (id == null) {
return CodecTier.PASSTHROUGH.baseScore + 100;
}
CodecTier tier = CodecCatalog.effectiveTierForVideoMime(mime, encoder);
return priorityScore(id, tier, settings);
}
public static CodecTier tierForVideoPreference(CastSettings.VideoCodec preference, CodecPriorityId id) {
if (preference == CastSettings.VideoCodec.PASSTHROUGH) {
return CodecTier.PASSTHROUGH;
}
if (preference == CastSettings.VideoCodec.LIBVPX_VP8
|| preference == CastSettings.VideoCodec.LIBVPX_VP9) {
return CodecTier.SOFTWARE;
}
if (id == CodecPriorityId.VP8 || id == CodecPriorityId.VP9) {
return CodecTier.SOFTWARE;
}
return CodecTier.HARDWARE;
}
public static CodecTier tierForAudioPreference(CastSettings.AudioCodec preference) {
if (preference == CastSettings.AudioCodec.SPEEX) {
return CodecTier.PASSTHROUGH;
}
if (preference == CastSettings.AudioCodec.OPUS) {
return CodecTier.SOFTWARE;
}
return CodecTier.HARDWARE;
}
public static <T> List<T> sortByPriority(List<ScoredItem<T>> items) {
List<ScoredItem<T>> copy = new ArrayList<>(items);
Collections.sort(copy, Comparator.comparingInt(a -> a.priorityScore));
List<T> out = new ArrayList<>(copy.size());
for (ScoredItem<T> item : copy) {
out.add(item.value);
}
return out;
}
public static final class ScoredItem<T> {
public final T value;
public final int priorityScore;
public ScoredItem(T value, int priorityScore) {
this.value = value;
this.priorityScore = priorityScore;
}
}
private static List<String> commonMimes(List<String> enc, List<String> dec) {
List<String> out = new ArrayList<>();
if (enc == null || dec == null) {
return out;
}
for (String mime : enc) {
if (mime != null && containsIgnoreCase(dec, mime)) {
out.add(mime);
}
}
return out;
}
private static boolean containsIgnoreCase(List<String> list, String mime) {
for (String s : list) {
if (s != null && s.equalsIgnoreCase(mime)) {
return true;
}
}
return false;
}
private static int component(CodecPriorityId id, String key, int compileDefault) {
try {
JSONObject section = sectionFor(id);
if (section == null) {
return compileDefault;
}
JSONObject codec = section.optJSONObject(id.jsonKey);
if (codec != null && codec.has(key)) {
return codec.optInt(key, compileDefault);
}
} catch (Exception ignored) {
}
return compileDefault;
}
private static JSONObject sectionFor(CodecPriorityId id) {
if (rootJson == null) {
return null;
}
switch (id) {
case H264:
case H265:
case VP8:
case VP9:
case PASSTHROUGH:
return rootJson.optJSONObject("video");
case AAC:
case OPUS:
case SPEEX:
case PCM:
default:
return rootJson.optJSONObject("audio");
}
}
private static int defaultInternal(CodecPriorityId id) {
switch (id) {
case H264:
case VP9:
return 0;
case H265:
case OPUS:
return 1;
case VP8:
case AAC:
return 2;
case SPEEX:
return 5;
case PCM:
case PASSTHROUGH:
default:
return 10;
}
}
private static int defaultFec(CodecPriorityId id) {
switch (id) {
case H264:
case OPUS:
return 0;
case H265:
case VP8:
case AAC:
return 1;
case VP9:
return 2;
case SPEEX:
return 5;
default:
return 1;
}
}
private static int defaultProtection(CodecPriorityId id, CastSettings settings) {
switch (id) {
case VP8:
case VP9:
return vpxInbandProtectionScore(settings);
case H264:
case H265:
return 1;
case OPUS:
return 0;
case SPEEX:
return 5;
default:
return 1;
}
}
/** 0 when libvpx in-band protection is available and enabled; 2 otherwise. */
private static int vpxInbandProtectionScore(CastSettings settings) {
if (!NativeCodecBridge.isLibvpxAvailable()) {
return 2;
}
if (settings == null) {
return 0;
}
CastSettings.StreamProtection mode = settings.getStreamProtection();
if (mode == null || mode == CastSettings.StreamProtection.AUTO
|| mode == CastSettings.StreamProtection.NONE) {
return 2;
}
return 0;
}
private static int defaultBitrate(CodecPriorityId id) {
switch (id) {
case H265:
case VP9:
case OPUS:
return 0;
case H264:
case AAC:
return 1;
case VP8:
return 2;
default:
return 10;
}
}
private static JSONObject loadJson(Context context) {
try (BufferedReader r = new BufferedReader(new InputStreamReader(
context.getAssets().open(ASSET), StandardCharsets.UTF_8))) {
StringBuilder sb = new StringBuilder();
String line;
while ((line = r.readLine()) != null) {
sb.append(line);
}
return new JSONObject(sb.toString());
} catch (Exception e) {
Log.w(TAG, "Using compile-time codec scores only (" + e.getMessage() + ")");
return new JSONObject();
}
}
}

View File

@@ -0,0 +1,96 @@
package com.foxx.androidcast.media;
import android.media.MediaFormat;
import com.foxx.androidcast.CastSettings;
/** Canonical codec id for scoring tables and {@code codecs.json} keys. */
public enum CodecPriorityId {
H264("h264"),
H265("h265"),
VP8("vp8"),
VP9("vp9"),
PASSTHROUGH("passthrough"),
AAC("aac"),
OPUS("opus"),
SPEEX("speex"),
PCM("pcm");
public final String jsonKey;
CodecPriorityId(String jsonKey) {
this.jsonKey = jsonKey;
}
public static CodecPriorityId fromVideoMime(String mime) {
if (mime == null) {
return null;
}
if (MediaFormat.MIMETYPE_VIDEO_AVC.equalsIgnoreCase(mime)) {
return H264;
}
if (MediaFormat.MIMETYPE_VIDEO_HEVC.equalsIgnoreCase(mime)) {
return H265;
}
if (MediaFormat.MIMETYPE_VIDEO_VP8.equalsIgnoreCase(mime)) {
return VP8;
}
if (MediaFormat.MIMETYPE_VIDEO_VP9.equalsIgnoreCase(mime)) {
return VP9;
}
return null;
}
public static CodecPriorityId fromVideoPreference(CastSettings.VideoCodec codec) {
if (codec == null) {
return null;
}
switch (codec) {
case H264:
return H264;
case H265:
return H265;
case LIBVPX_VP8:
return VP8;
case LIBVPX_VP9:
return VP9;
case PASSTHROUGH:
return PASSTHROUGH;
case AUTO:
default:
return null;
}
}
public static CodecPriorityId fromAudioPreference(CastSettings.AudioCodec codec) {
if (codec == null) {
return null;
}
switch (codec) {
case AAC:
return AAC;
case OPUS:
return OPUS;
case SPEEX:
return SPEEX;
case AUTO:
default:
return null;
}
}
public String toMime() {
switch (this) {
case H264:
return MediaFormat.MIMETYPE_VIDEO_AVC;
case H265:
return MediaFormat.MIMETYPE_VIDEO_HEVC;
case VP8:
return MediaFormat.MIMETYPE_VIDEO_VP8;
case VP9:
return MediaFormat.MIMETYPE_VIDEO_VP9;
default:
return null;
}
}
}

View File

@@ -0,0 +1,14 @@
package com.foxx.androidcast.media;
/** Base tier added before per-codec component scores (lower total = higher priority). */
public enum CodecTier {
HARDWARE(0),
SOFTWARE(500),
PASSTHROUGH(1000);
public final int baseScore;
CodecTier(int baseScore) {
this.baseScore = baseScore;
}
}

View File

@@ -118,7 +118,7 @@ public class CastSession {
List<String> receiverCaps = CodecCatalog.localDecoderMimes(); List<String> receiverCaps = CodecCatalog.localDecoderMimes();
transport.send(CastProtocol.MSG_CODEC_CAPS, CastProtocol.codecCapsPayload(receiverCaps)); transport.send(CastProtocol.MSG_CODEC_CAPS, CastProtocol.codecCapsPayload(receiverCaps));
String mime = CodecNegotiator.negotiate(senderCaps, receiverCaps, String mime = CodecNegotiator.negotiate(senderCaps, receiverCaps,
PassthroughCodecPolicy.forNegotiation(remote.getVideoCodec())); PassthroughCodecPolicy.forNegotiation(remote.getVideoCodec()), remote);
transport.send(CastProtocol.MSG_CODEC_SELECTED, CastProtocol.codecSelectedPayload(mime)); transport.send(CastProtocol.MSG_CODEC_SELECTED, CastProtocol.codecSelectedPayload(mime));
remote.setNegotiatedVideoMime(mime); remote.setNegotiatedVideoMime(mime);
return new HandshakeResult(senderName, remote, mime); return new HandshakeResult(senderName, remote, mime);

View File

@@ -6,61 +6,87 @@ import android.media.MediaFormat;
import android.os.Build; import android.os.Build;
import com.foxx.androidcast.CastSettings; import com.foxx.androidcast.CastSettings;
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 java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
/** Discovers device video encoder/decoder MIME types. */ /** Discovers device video encoder/decoder MIME types and UI codec choices with priority scores. */
public final class CodecCatalog { public final class CodecCatalog {
private static volatile CapabilitySnapshot capabilities;
private CodecCatalog() {} private CodecCatalog() {}
public static final class VideoCodecChoice { public static final class VideoCodecChoice {
public final CastSettings.VideoCodec codec; public final CastSettings.VideoCodec codec;
public final boolean selectable; public final boolean selectable;
/** Lower = higher priority in UI and AUTO negotiation hints. */
public final int priorityScore;
public VideoCodecChoice(CastSettings.VideoCodec codec, boolean selectable) { public VideoCodecChoice(CastSettings.VideoCodec codec, boolean selectable, int priorityScore) {
this.codec = codec; this.codec = codec;
this.selectable = selectable; this.selectable = selectable;
this.priorityScore = priorityScore;
} }
} }
public static final class AudioCodecChoice { public static final class AudioCodecChoice {
public final CastSettings.AudioCodec codec; public final CastSettings.AudioCodec codec;
public final boolean selectable; public final boolean selectable;
public final int priorityScore;
public AudioCodecChoice(CastSettings.AudioCodec codec, boolean selectable) { public AudioCodecChoice(CastSettings.AudioCodec codec, boolean selectable, int priorityScore) {
this.codec = codec; this.codec = codec;
this.selectable = selectable; this.selectable = selectable;
this.priorityScore = priorityScore;
} }
} }
/** Selectable + disabled future codecs for settings spinners. */ /** Selectable codecs for settings spinners (AUTO first, then by ascending priority score). */
public static List<VideoCodecChoice> videoCodecChoicesForUi() { public static List<VideoCodecChoice> videoCodecChoicesForUi(CastSettings settings) {
ensureCapabilities();
List<VideoCodecChoice> out = new ArrayList<>(); List<VideoCodecChoice> out = new ArrayList<>();
out.add(new VideoCodecChoice(CastSettings.VideoCodec.AUTO, true)); out.add(new VideoCodecChoice(CastSettings.VideoCodec.AUTO, true, 0));
Set<String> enc = new LinkedHashSet<>(localEncoderMimes()); Set<String> enc = capabilities.hardwareEncoderMimes;
if (enc.contains(MediaFormat.MIMETYPE_VIDEO_AVC)) { if (enc.contains(MediaFormat.MIMETYPE_VIDEO_AVC)) {
out.add(new VideoCodecChoice(CastSettings.VideoCodec.H264, true)); out.add(scoredVideo(CastSettings.VideoCodec.H264, true, settings));
} }
if (enc.contains(MediaFormat.MIMETYPE_VIDEO_HEVC)) { if (enc.contains(MediaFormat.MIMETYPE_VIDEO_HEVC)) {
out.add(new VideoCodecChoice(CastSettings.VideoCodec.H265, true)); out.add(scoredVideo(CastSettings.VideoCodec.H265, true, settings));
} }
out.add(new VideoCodecChoice(CastSettings.VideoCodec.LIBVPX_VP8, true)); if (capabilities.softwareEncoderMimes.contains(MediaFormat.MIMETYPE_VIDEO_VP8)) {
out.add(new VideoCodecChoice(CastSettings.VideoCodec.LIBVPX_VP9, true)); out.add(scoredVideo(CastSettings.VideoCodec.LIBVPX_VP8, true, settings));
out.add(new VideoCodecChoice(CastSettings.VideoCodec.PASSTHROUGH, true)); }
if (capabilities.softwareEncoderMimes.contains(MediaFormat.MIMETYPE_VIDEO_VP9)) {
out.add(scoredVideo(CastSettings.VideoCodec.LIBVPX_VP9, true, settings));
}
out.add(scoredVideo(CastSettings.VideoCodec.PASSTHROUGH, true, settings));
sortVideoChoices(out);
return out;
}
public static List<VideoCodecChoice> videoCodecChoicesForUi() {
return videoCodecChoicesForUi(null);
}
public static List<AudioCodecChoice> audioCodecChoicesForUi(CastSettings settings) {
List<AudioCodecChoice> out = new ArrayList<>();
out.add(new AudioCodecChoice(CastSettings.AudioCodec.AUTO, true, 0));
out.add(scoredAudio(CastSettings.AudioCodec.AAC, true, settings));
out.add(scoredAudio(CastSettings.AudioCodec.OPUS, true, settings));
out.add(scoredAudio(CastSettings.AudioCodec.SPEEX, true, settings));
sortAudioChoices(out);
return out; return out;
} }
public static List<AudioCodecChoice> audioCodecChoicesForUi() { public static List<AudioCodecChoice> audioCodecChoicesForUi() {
List<AudioCodecChoice> out = new ArrayList<>(); return audioCodecChoicesForUi(null);
out.add(new AudioCodecChoice(CastSettings.AudioCodec.AUTO, true));
out.add(new AudioCodecChoice(CastSettings.AudioCodec.AAC, true));
out.add(new AudioCodecChoice(CastSettings.AudioCodec.SPEEX, true));
out.add(new AudioCodecChoice(CastSettings.AudioCodec.OPUS, true));
return out;
} }
public static List<CastSettings.VideoCodec> supportedChoices() { public static List<CastSettings.VideoCodec> supportedChoices() {
@@ -74,14 +100,115 @@ public final class CodecCatalog {
} }
public static List<String> localEncoderMimes() { public static List<String> localEncoderMimes() {
return listCodecMimes(true); ensureCapabilities();
return new ArrayList<>(capabilities.advertisedEncoderMimes);
} }
public static List<String> localDecoderMimes() { public static List<String> localDecoderMimes() {
return listCodecMimes(false); ensureCapabilities();
return new ArrayList<>(capabilities.advertisedDecoderMimes);
} }
private static List<String> listCodecMimes(boolean encoder) { public static boolean hasHardwareCodec(String mime, boolean encoder) {
ensureCapabilities();
Set<String> set = encoder ? capabilities.hardwareEncoderMimes : capabilities.hardwareDecoderMimes;
return containsIgnoreCase(set, mime);
}
public static boolean hasSoftwareCodec(String mime, boolean encoder) {
ensureCapabilities();
Set<String> set = encoder ? capabilities.softwareEncoderMimes : capabilities.softwareDecoderMimes;
return containsIgnoreCase(set, mime);
}
/** Tier used for scoring on this device for a MIME (HW preferred over SW when both exist). */
public static CodecTier effectiveTierForVideoMime(String mime, boolean encoder) {
if (hasHardwareCodec(mime, encoder)) {
return CodecTier.HARDWARE;
}
if (hasSoftwareCodec(mime, encoder)) {
return CodecTier.SOFTWARE;
}
return CodecTier.PASSTHROUGH;
}
public static boolean canUseVideoMimeOnDevice(String mime, boolean encoder) {
return hasHardwareCodec(mime, encoder) || hasSoftwareCodec(mime, encoder);
}
private static VideoCodecChoice scoredVideo(CastSettings.VideoCodec codec, boolean selectable,
CastSettings settings) {
int score = CodecPriorityCatalog.priorityScore(codec, settings);
return new VideoCodecChoice(codec, selectable, score);
}
private static AudioCodecChoice scoredAudio(CastSettings.AudioCodec codec, boolean selectable,
CastSettings settings) {
int score = CodecPriorityCatalog.priorityScore(codec, settings);
return new AudioCodecChoice(codec, selectable, score);
}
private static void sortVideoChoices(List<VideoCodecChoice> choices) {
if (choices.size() <= 2) {
return;
}
VideoCodecChoice auto = choices.get(0);
List<VideoCodecChoice> rest = new ArrayList<>(choices.subList(1, choices.size()));
Collections.sort(rest, Comparator.comparingInt(c -> c.priorityScore));
choices.clear();
choices.add(auto);
choices.addAll(rest);
}
private static void sortAudioChoices(List<AudioCodecChoice> choices) {
if (choices.size() <= 2) {
return;
}
AudioCodecChoice auto = choices.get(0);
List<AudioCodecChoice> rest = new ArrayList<>(choices.subList(1, choices.size()));
Collections.sort(rest, Comparator.comparingInt(c -> c.priorityScore));
choices.clear();
choices.add(auto);
choices.addAll(rest);
}
private static void ensureCapabilities() {
if (capabilities != null) {
return;
}
synchronized (CodecCatalog.class) {
if (capabilities != null) {
return;
}
capabilities = probeCapabilities();
}
}
private static CapabilitySnapshot probeCapabilities() {
Set<String> hwEnc = probeHardwareMimes(true);
Set<String> hwDec = probeHardwareMimes(false);
Set<String> swEnc = new LinkedHashSet<>();
Set<String> swDec = new LinkedHashSet<>();
if (NativeCodecBridge.isLibvpxAvailable()) {
swEnc.add(MediaFormat.MIMETYPE_VIDEO_VP8);
swEnc.add(MediaFormat.MIMETYPE_VIDEO_VP9);
swDec.add(MediaFormat.MIMETYPE_VIDEO_VP8);
swDec.add(MediaFormat.MIMETYPE_VIDEO_VP9);
}
Set<String> advertisedEnc = new LinkedHashSet<>(hwEnc);
advertisedEnc.addAll(swEnc);
if (advertisedEnc.isEmpty()) {
advertisedEnc.add(MediaFormat.MIMETYPE_VIDEO_AVC);
}
Set<String> advertisedDec = new LinkedHashSet<>(hwDec);
advertisedDec.addAll(swDec);
if (advertisedDec.isEmpty()) {
advertisedDec.add(MediaFormat.MIMETYPE_VIDEO_AVC);
}
return new CapabilitySnapshot(hwEnc, hwDec, swEnc, swDec, advertisedEnc, advertisedDec);
}
private static Set<String> probeHardwareMimes(boolean encoder) {
Set<String> out = new LinkedHashSet<>(); Set<String> out = new LinkedHashSet<>();
String[] want = { String[] want = {
MediaFormat.MIMETYPE_VIDEO_AVC, MediaFormat.MIMETYPE_VIDEO_AVC,
@@ -119,13 +246,38 @@ public final class CodecCatalog {
} }
} }
} }
if (out.isEmpty()) { return out;
out.add(MediaFormat.MIMETYPE_VIDEO_AVC);
} }
if (NativeCodecBridge.isLibvpxAvailable()) {
out.add(MediaFormat.MIMETYPE_VIDEO_VP8); private static boolean containsIgnoreCase(Set<String> set, String mime) {
out.add(MediaFormat.MIMETYPE_VIDEO_VP9); if (mime == null) {
return false;
}
for (String s : set) {
if (s != null && s.equalsIgnoreCase(mime)) {
return true;
}
}
return false;
}
private static final class CapabilitySnapshot {
final Set<String> hardwareEncoderMimes;
final Set<String> hardwareDecoderMimes;
final Set<String> softwareEncoderMimes;
final Set<String> softwareDecoderMimes;
final Set<String> advertisedEncoderMimes;
final Set<String> advertisedDecoderMimes;
CapabilitySnapshot(Set<String> hardwareEncoderMimes, Set<String> hardwareDecoderMimes,
Set<String> softwareEncoderMimes, Set<String> softwareDecoderMimes,
Set<String> advertisedEncoderMimes, Set<String> advertisedDecoderMimes) {
this.hardwareEncoderMimes = hardwareEncoderMimes;
this.hardwareDecoderMimes = hardwareDecoderMimes;
this.softwareEncoderMimes = softwareEncoderMimes;
this.softwareDecoderMimes = softwareDecoderMimes;
this.advertisedEncoderMimes = advertisedEncoderMimes;
this.advertisedDecoderMimes = advertisedDecoderMimes;
} }
return new ArrayList<>(out);
} }
} }