mirror of
git://f0xx.org/android_cast
synced 2026-07-29 03:38:52 +03:00
snap
This commit is contained in:
34
app/src/main/assets/codecs.json
Normal file
34
app/src/main/assets/codecs.json
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,8 @@ package com.foxx.androidcast;
|
||||
import android.app.Application;
|
||||
import android.content.Context;
|
||||
|
||||
import com.foxx.androidcast.media.CodecPriorityCatalog;
|
||||
|
||||
/** Applies saved theme and locale at process start. */
|
||||
public class AndroidCastApplication extends Application {
|
||||
@Override
|
||||
@@ -13,6 +15,7 @@ public class AndroidCastApplication extends Application {
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
CodecPriorityCatalog.init(this);
|
||||
CastLocaleHelper.applyStoredLocale(this);
|
||||
CastThemeHelper.applyNightMode(this);
|
||||
CastTrayNotifier.syncOnAppStart(this);
|
||||
|
||||
@@ -177,7 +177,7 @@ public final class CastSettingsBinder {
|
||||
|
||||
private static void bindVideoCodec(AppCompatActivity activity, CastSettings settings, int 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()];
|
||||
String[] labels = new String[choices.size()];
|
||||
boolean[] selectable = new boolean[choices.size()];
|
||||
@@ -215,7 +215,7 @@ public final class CastSettingsBinder {
|
||||
if (spinner == null) {
|
||||
return;
|
||||
}
|
||||
List<CodecCatalog.AudioCodecChoice> choices = CodecCatalog.audioCodecChoicesForUi();
|
||||
List<CodecCatalog.AudioCodecChoice> choices = CodecCatalog.audioCodecChoicesForUi(settings);
|
||||
CastSettings.AudioCodec[] values = new CastSettings.AudioCodec[choices.size()];
|
||||
String[] labels = new String[choices.size()];
|
||||
boolean[] selectable = new boolean[choices.size()];
|
||||
|
||||
@@ -3,8 +3,8 @@ package com.foxx.androidcast.media;
|
||||
import android.media.MediaFormat;
|
||||
|
||||
import com.foxx.androidcast.CastSettings;
|
||||
import com.foxx.androidcast.sender.CodecCatalog;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
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. */
|
||||
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() {}
|
||||
|
||||
public static String negotiate(List<String> senderEncoders, List<String> receiverDecoders,
|
||||
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> dec = new HashSet<>(receiverDecoders);
|
||||
CastSettings scoreSettings = negotiationSettings != null ? negotiationSettings : new CastSettings();
|
||||
|
||||
if (preference == CastSettings.VideoCodec.H264) {
|
||||
return pick(MediaFormat.MIMETYPE_VIDEO_AVC, enc, dec);
|
||||
}
|
||||
@@ -37,10 +37,11 @@ public final class CodecNegotiator {
|
||||
if (preference == CastSettings.VideoCodec.LIBVPX_VP9) {
|
||||
return pick(MediaFormat.MIMETYPE_VIDEO_VP9, enc, dec);
|
||||
}
|
||||
for (String mime : PREFERENCE_ORDER) {
|
||||
if (enc.contains(mime) && dec.contains(mime)) {
|
||||
return mime;
|
||||
}
|
||||
|
||||
String best = CodecPriorityCatalog.negotiateBestVideoMime(
|
||||
senderEncoders, receiverDecoders, scoreSettings);
|
||||
if (best != null) {
|
||||
return best;
|
||||
}
|
||||
if (dec.contains(MediaFormat.MIMETYPE_VIDEO_AVC) && enc.contains(MediaFormat.MIMETYPE_VIDEO_AVC)) {
|
||||
return MediaFormat.MIMETYPE_VIDEO_AVC;
|
||||
@@ -87,4 +88,9 @@ public final class CodecNegotiator {
|
||||
return MediaFormat.MIMETYPE_VIDEO_VP8.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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
14
app/src/main/java/com/foxx/androidcast/media/CodecTier.java
Normal file
14
app/src/main/java/com/foxx/androidcast/media/CodecTier.java
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -118,7 +118,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()));
|
||||
PassthroughCodecPolicy.forNegotiation(remote.getVideoCodec()), remote);
|
||||
transport.send(CastProtocol.MSG_CODEC_SELECTED, CastProtocol.codecSelectedPayload(mime));
|
||||
remote.setNegotiatedVideoMime(mime);
|
||||
return new HandshakeResult(senderName, remote, mime);
|
||||
|
||||
@@ -6,61 +6,87 @@ import android.media.MediaFormat;
|
||||
import android.os.Build;
|
||||
|
||||
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 java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
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 {
|
||||
private static volatile CapabilitySnapshot capabilities;
|
||||
|
||||
private CodecCatalog() {}
|
||||
|
||||
public static final class VideoCodecChoice {
|
||||
public final CastSettings.VideoCodec codec;
|
||||
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.selectable = selectable;
|
||||
this.priorityScore = priorityScore;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class AudioCodecChoice {
|
||||
public final CastSettings.AudioCodec codec;
|
||||
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.selectable = selectable;
|
||||
this.priorityScore = priorityScore;
|
||||
}
|
||||
}
|
||||
|
||||
/** Selectable + disabled future codecs for settings spinners. */
|
||||
public static List<VideoCodecChoice> videoCodecChoicesForUi() {
|
||||
/** Selectable codecs for settings spinners (AUTO first, then by ascending priority score). */
|
||||
public static List<VideoCodecChoice> videoCodecChoicesForUi(CastSettings settings) {
|
||||
ensureCapabilities();
|
||||
List<VideoCodecChoice> out = new ArrayList<>();
|
||||
out.add(new VideoCodecChoice(CastSettings.VideoCodec.AUTO, true));
|
||||
Set<String> enc = new LinkedHashSet<>(localEncoderMimes());
|
||||
out.add(new VideoCodecChoice(CastSettings.VideoCodec.AUTO, true, 0));
|
||||
Set<String> enc = capabilities.hardwareEncoderMimes;
|
||||
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)) {
|
||||
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));
|
||||
out.add(new VideoCodecChoice(CastSettings.VideoCodec.LIBVPX_VP9, true));
|
||||
out.add(new VideoCodecChoice(CastSettings.VideoCodec.PASSTHROUGH, true));
|
||||
if (capabilities.softwareEncoderMimes.contains(MediaFormat.MIMETYPE_VIDEO_VP8)) {
|
||||
out.add(scoredVideo(CastSettings.VideoCodec.LIBVPX_VP8, true, settings));
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
public static List<AudioCodecChoice> audioCodecChoicesForUi() {
|
||||
List<AudioCodecChoice> out = new ArrayList<>();
|
||||
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;
|
||||
return audioCodecChoicesForUi(null);
|
||||
}
|
||||
|
||||
public static List<CastSettings.VideoCodec> supportedChoices() {
|
||||
@@ -74,14 +100,115 @@ public final class CodecCatalog {
|
||||
}
|
||||
|
||||
public static List<String> localEncoderMimes() {
|
||||
return listCodecMimes(true);
|
||||
ensureCapabilities();
|
||||
return new ArrayList<>(capabilities.advertisedEncoderMimes);
|
||||
}
|
||||
|
||||
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<>();
|
||||
String[] want = {
|
||||
MediaFormat.MIMETYPE_VIDEO_AVC,
|
||||
@@ -119,13 +246,38 @@ public final class CodecCatalog {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (out.isEmpty()) {
|
||||
out.add(MediaFormat.MIMETYPE_VIDEO_AVC);
|
||||
return out;
|
||||
}
|
||||
|
||||
private static boolean containsIgnoreCase(Set<String> set, String mime) {
|
||||
if (mime == null) {
|
||||
return false;
|
||||
}
|
||||
if (NativeCodecBridge.isLibvpxAvailable()) {
|
||||
out.add(MediaFormat.MIMETYPE_VIDEO_VP8);
|
||||
out.add(MediaFormat.MIMETYPE_VIDEO_VP9);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user