1
0
mirror of git://f0xx.org/android_cast synced 2026-07-29 04:18:09 +03:00
Signed-off-by: Anton Afanasyeu <a.afanasieff@gmail.com>
This commit is contained in:
Anton Afanasyeu
2026-05-18 22:19:39 +02:00
parent 115e0e1548
commit 0647e0b2a0
15 changed files with 502 additions and 57 deletions

67
Dockerfile Normal file
View File

@@ -0,0 +1,67 @@
# Minimal Android Cast CI image (glibc-based; NDK prebuilts require linux-x86_64 glibc).
#
# Build:
# docker build -t androidcast-ci:latest -f Dockerfile .
#
# Run CI build (mount repo):
# docker run --rm -v "$(pwd)":/workspace -w /workspace androidcast-ci:latest
#
# Interactive:
# docker run --rm -it -v "$(pwd)":/workspace -w /workspace androidcast-ci:latest bash
#
# Note: Alpine/musl breaks NDK clang prebuilts; use Debian slim instead of rust:* for size.
FROM debian:bookworm-slim
ARG DEBIAN_FRONTEND=noninteractive
ARG ANDROID_SDK_ROOT=/opt/android-sdk
ARG ANDROID_NDK_VERSION=27.0.12077973
ARG ANDROID_CMAKE_VERSION=3.22.1
ARG CMDLINE_TOOLS_REV=11076708
ENV ANDROID_SDK_ROOT="${ANDROID_SDK_ROOT}" \
ANDROID_HOME="${ANDROID_SDK_ROOT}" \
ANDROID_NDK_HOME="${ANDROID_SDK_ROOT}/ndk/${ANDROID_NDK_VERSION}" \
ANDROID_NDK_ROOT="${ANDROID_SDK_ROOT}/ndk/${ANDROID_NDK_VERSION}" \
PATH="${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin:${PATH}" \
ANDROIDCAST_CCACHE=1 \
ANDROIDCAST_USE_DISTCC=0 \
GRADLE_USER_HOME=/root/.gradle \
JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64
RUN apt-get update && apt-get install -y --no-install-recommends \
bash ca-certificates curl git openjdk-21-jdk-headless unzip wget \
ninja-build cmake ccache \
&& rm -rf /var/lib/apt/lists/*
# Optional distributed compile (off unless ANDROIDCAST_USE_DISTCC=1 + DISTCC_HOSTS at run time).
RUN apt-get update && apt-get install -y --no-install-recommends distcc \
&& rm -rf /var/lib/apt/lists/* || true
RUN mkdir -p "${ANDROID_SDK_ROOT}/cmdline-tools" \
&& curl -fsSL "https://dl.google.com/android/repository/commandlinetools-linux-${CMDLINE_TOOLS_REV}_latest.zip" \
-o /tmp/cmdline-tools.zip \
&& unzip -q /tmp/cmdline-tools.zip -d "${ANDROID_SDK_ROOT}/cmdline-tools" \
&& mv "${ANDROID_SDK_ROOT}/cmdline-tools/cmdline-tools" "${ANDROID_SDK_ROOT}/cmdline-tools/latest" \
&& rm -f /tmp/cmdline-tools.zip
RUN yes | sdkmanager --licenses >/dev/null \
&& sdkmanager \
"platform-tools" \
"platforms;android-34" \
"build-tools;34.0.0" \
"ndk;${ANDROID_NDK_VERSION}" \
"cmake;${ANDROID_CMAKE_VERSION}"
WORKDIR /workspace
# Pre-create ccache dir (volume can override at runtime).
RUN mkdir -p /workspace/build/.ccache || true
COPY scripts/ci-build.sh scripts/android-ndk.sh scripts/native-build-cache.sh scripts/build-native-codecs.sh \
/workspace/scripts/
RUN chmod +x /workspace/scripts/ci-build.sh /workspace/scripts/build-native-codecs.sh
# Full tree is bind-mounted in CI; default command runs the pipeline.
CMD ["./scripts/ci-build.sh"]

View File

@@ -2,6 +2,24 @@ plugins {
id 'com.android.application'
}
def resolveCcachePath = {
def fromEnv = System.getenv('CCACHE') ?: System.getenv('NDK_CCACHE')
if (fromEnv) {
return fromEnv
}
if (System.getenv('ANDROIDCAST_CCACHE') in ['0', 'false', 'no', 'off']) {
return null
}
try {
def out = providers.exec {
commandLine 'sh', '-c', 'command -v ccache || true'
}.standardOutput.asText.get().trim()
return out ?: null
} catch (Exception ignored) {
return null
}
}()
def gitCommitShort = 'unknown'
def gitCommitAuthor = 'unknown'
def buildTimeEpochMs = System.currentTimeMillis()
@@ -31,7 +49,11 @@ android {
buildConfigField 'String', 'BUILD_TIME_DISPLAY', "\"${buildTimeDisplay.replace('\\\\', '\\\\\\\\').replace('\"', '\\\\\"')}\""
externalNativeBuild {
cmake {
arguments "-DANDROID_STL=c++_shared"
def cmakeArgs = ["-DANDROID_STL=c++_shared"]
if (resolveCcachePath) {
cmakeArgs += "-DANDROID_CCACHE=${resolveCcachePath}"
}
arguments cmakeArgs
cppFlags ""
}
}

View File

@@ -115,14 +115,8 @@ 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).
*/
/** Developer: show priority scores in codec settings spinners (default off). */
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);
}

View File

@@ -180,10 +180,13 @@ public final class CastSettingsBinder {
List<CodecCatalog.VideoCodecChoice> choices = CodecCatalog.videoCodecChoicesForUi(settings);
CastSettings.VideoCodec[] values = new CastSettings.VideoCodec[choices.size()];
String[] labels = new String[choices.size()];
int[] scores = new int[choices.size()];
boolean[] selectable = new boolean[choices.size()];
boolean showScores = AppPreferences.isShowCodecPriorityScores(activity);
for (int i = 0; i < choices.size(); i++) {
values[i] = choices.get(i).codec;
labels[i] = labelVideoCodec(activity, choices.get(i).codec, choices.get(i).priorityScore);
labels[i] = labelVideoCodec(activity, choices.get(i).codec);
scores[i] = codecSpinnerScore(showScores, choices.get(i).codec, choices.get(i).priorityScore);
selectable[i] = choices.get(i).selectable;
}
CastSettings.VideoCodec current = settings.getVideoCodec();
@@ -191,7 +194,7 @@ public final class CastSettingsBinder {
current = CastSettings.VideoCodec.AUTO;
settings.setVideoCodec(current);
}
SettingsEnumSpinner.bind(spinner, values, labels, selectable, current, settings::setVideoCodec);
SettingsEnumSpinner.bind(spinner, values, labels, scores, selectable, current, settings::setVideoCodec);
}
/** Rebind codec spinners after {@code codecs.json} reload or debug score toggle. */
@@ -226,10 +229,13 @@ public final class CastSettingsBinder {
List<CodecCatalog.AudioCodecChoice> choices = CodecCatalog.audioCodecChoicesForUi(settings);
CastSettings.AudioCodec[] values = new CastSettings.AudioCodec[choices.size()];
String[] labels = new String[choices.size()];
int[] scores = new int[choices.size()];
boolean[] selectable = new boolean[choices.size()];
boolean showScores = AppPreferences.isShowCodecPriorityScores(activity);
for (int i = 0; i < choices.size(); i++) {
values[i] = choices.get(i).codec;
labels[i] = labelAudioCodec(activity, choices.get(i).codec, choices.get(i).priorityScore);
labels[i] = labelAudioCodec(activity, choices.get(i).codec);
scores[i] = codecSpinnerScore(showScores, choices.get(i).codec, choices.get(i).priorityScore);
selectable[i] = choices.get(i).selectable;
}
CastSettings.AudioCodec current = settings.getAudioCodec();
@@ -240,7 +246,7 @@ public final class CastSettingsBinder {
break;
}
}
SettingsEnumSpinner.bind(spinner, values, labels, selectable, current, settings::setAudioCodec);
SettingsEnumSpinner.bind(spinner, values, labels, scores, selectable, current, settings::setAudioCodec);
}
private static void readAudioCodec(AppCompatActivity activity, CastSettings settings) {
@@ -560,57 +566,46 @@ public final class CastSettingsBinder {
}
}
private static String labelVideoCodec(AppCompatActivity activity, CastSettings.VideoCodec codec,
int priorityScore) {
String base;
private static String labelVideoCodec(AppCompatActivity activity, CastSettings.VideoCodec codec) {
switch (codec) {
case H264:
base = activity.getString(R.string.codec_h264);
break;
return activity.getString(R.string.codec_h264);
case H265:
base = activity.getString(R.string.codec_h265);
break;
return activity.getString(R.string.codec_h265);
case LIBVPX_VP8:
base = activity.getString(R.string.codec_libvpx_vp8);
break;
return activity.getString(R.string.codec_libvpx_vp8);
case LIBVPX_VP9:
base = activity.getString(R.string.codec_libvpx_vp9);
break;
return activity.getString(R.string.codec_libvpx_vp9);
case PASSTHROUGH:
base = activity.getString(R.string.codec_passthrough);
break;
return activity.getString(R.string.codec_passthrough);
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,
int priorityScore) {
String base;
private static String labelAudioCodec(AppCompatActivity activity, CastSettings.AudioCodec codec) {
switch (codec) {
case AAC:
base = activity.getString(R.string.codec_aac);
break;
return activity.getString(R.string.codec_aac);
case SPEEX:
base = activity.getString(R.string.codec_speex);
break;
return activity.getString(R.string.codec_speex);
case OPUS:
base = activity.getString(R.string.codec_opus);
break;
return activity.getString(R.string.codec_opus);
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;
private static int codecSpinnerScore(boolean showScores, Enum<?> codec, int priorityScore) {
if (!showScores || codec == null) {
return com.foxx.androidcast.ui.SettingsScoredArrayAdapter.NO_SCORE;
}
return base + activity.getString(R.string.codec_priority_score_suffix, priorityScore);
String name = codec.name();
if ("AUTO".equals(name)) {
return com.foxx.androidcast.ui.SettingsScoredArrayAdapter.NO_SCORE;
}
return priorityScore;
}
}

View File

@@ -29,20 +29,42 @@ public class DeveloperSettingsActivity extends AppCompatActivity {
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));
refreshFromPreferences(debugOverlay, codecScores, sessionStats);
reloadCodecs.setOnClickListener(v -> {
String summary = CodecPriorityCatalog.reload(this);
Toast.makeText(this, summary, Toast.LENGTH_LONG).show();
});
}
@Override
protected void onResume() {
super.onResume();
refreshFromPreferences(
findViewById(R.id.check_receiver_debug_overlay),
findViewById(R.id.check_show_codec_priority_scores),
findViewById(R.id.check_grab_session_stats));
}
private void refreshFromPreferences(CheckBox debugOverlay, CheckBox codecScores, CheckBox sessionStats) {
bindDeveloperCheckbox(debugOverlay, AppPreferences.isShowReceiverDebugOverlay(this),
AppPreferences::setShowReceiverDebugOverlay);
bindDeveloperCheckbox(codecScores, AppPreferences.isShowCodecPriorityScores(this),
AppPreferences::setShowCodecPriorityScores);
bindDeveloperCheckbox(sessionStats, AppPreferences.isGrabSessionStats(this),
AppPreferences::setGrabSessionStats);
}
private interface BoolPrefWriter {
void write(Context context, boolean value);
}
private void bindDeveloperCheckbox(CheckBox box, boolean checked, BoolPrefWriter writer) {
if (box == null) {
return;
}
box.setOnCheckedChangeListener(null);
box.setChecked(checked);
box.setOnCheckedChangeListener((buttonView, isChecked) -> writer.write(this, isChecked));
}
}

View File

@@ -21,9 +21,16 @@ public final class SettingsEnumSpinner {
public static <T> void bind(Spinner spinner, T[] values, String[] labels, boolean[] selectable,
T current, java.util.function.Consumer<T> onSelect) {
bind(spinner, values, labels, null, selectable, current, onSelect);
}
public static <T> void bind(Spinner spinner, T[] values, String[] labels, int[] priorityScores,
boolean[] selectable, T current, java.util.function.Consumer<T> onSelect) {
spinner.setTag(R.id.tag_spinner_enum, values);
spinner.setTag(R.id.tag_spinner_selectable, selectable);
DisabledAdapter adapter = new DisabledAdapter(spinner.getContext(), labels, selectable);
SettingsStringArrayAdapter adapter = priorityScores != null
? new ScoredDisabledAdapter(spinner.getContext(), labels, priorityScores, selectable)
: new DisabledAdapter(spinner.getContext(), labels, selectable);
spinner.setBackgroundResource(R.drawable.bg_settings_spinner);
spinner.setAdapter(adapter);
spinner.setPopupBackgroundResource(R.drawable.bg_settings_spinner_popup);
@@ -35,7 +42,7 @@ public final class SettingsEnumSpinner {
}
}
spinner.setSelection(selectedIndex[0]);
adapter.setSelectedPosition(selectedIndex[0]);
setAdapterSelectedPosition(adapter, selectedIndex[0]);
SettingsStringArrayAdapter.wireSelectionRefresh(spinner, adapter);
spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
@@ -48,7 +55,7 @@ public final class SettingsEnumSpinner {
return;
}
selectedIndex[0] = position;
adapter.setSelectedPosition(position);
setAdapterSelectedPosition(adapter, position);
onSelect.accept(values[position]);
}
@@ -57,6 +64,10 @@ public final class SettingsEnumSpinner {
});
}
private static void setAdapterSelectedPosition(SettingsStringArrayAdapter adapter, int position) {
adapter.setSelectedPosition(position);
}
@SuppressWarnings("unchecked")
public static <T extends Enum<T>> T read(Spinner spinner, T fallback) {
Object tag = spinner.getTag(R.id.tag_spinner_enum);
@@ -86,4 +97,18 @@ public final class SettingsEnumSpinner {
return position >= 0 && position < selectable.length && selectable[position];
}
}
private static final class ScoredDisabledAdapter extends SettingsScoredArrayAdapter {
private final boolean[] selectable;
ScoredDisabledAdapter(Context context, String[] labels, int[] scores, boolean[] selectable) {
super(context, labels, scores);
this.selectable = selectable;
}
@Override
public boolean isEnabled(int position) {
return position >= 0 && position < selectable.length && selectable[position];
}
}
}

View File

@@ -0,0 +1,70 @@
package com.foxx.androidcast.ui;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import com.foxx.androidcast.R;
/** Settings spinner row with label left and optional priority score right-aligned. */
public class SettingsScoredArrayAdapter extends SettingsStringArrayAdapter {
/** Hide score column when negative. */
public static final int NO_SCORE = -1;
private final int[] scores;
public SettingsScoredArrayAdapter(Context context, String[] labels, int[] scores) {
super(context, labels, R.layout.spinner_dropdown_item_scored);
this.scores = scores != null ? scores : new int[labels.length];
}
@Override
@NonNull
public View getView(int position, View convertView, @NonNull ViewGroup parent) {
View row = convertView;
if (!isScoredRow(row)) {
row = LayoutInflater.from(getContext()).inflate(R.layout.spinner_item_scored, parent, false);
}
bindRow(row, position);
row.setAlpha(isEnabled(position) ? 1f : 0.45f);
return row;
}
@Override
public View getDropDownView(int position, View convertView, @NonNull ViewGroup parent) {
View row = convertView;
if (!isScoredRow(row)) {
row = LayoutInflater.from(getContext())
.inflate(R.layout.spinner_dropdown_item_scored, parent, false);
}
bindRow(row, position);
SettingsSpinnerDropdownStyle.apply(row, position == selectedPosition, isEnabled(position));
return row;
}
private void bindRow(View row, int position) {
TextView label = row.findViewById(R.id.spinner_item_label);
TextView score = row.findViewById(R.id.spinner_item_score);
if (label != null) {
label.setText(getItem(position));
}
if (score != null) {
int value = position >= 0 && position < scores.length ? scores[position] : NO_SCORE;
if (value >= 0) {
score.setVisibility(View.VISIBLE);
score.setText(String.valueOf(value));
} else {
score.setVisibility(View.GONE);
}
}
}
private static boolean isScoredRow(View row) {
return row != null && row.findViewById(R.id.spinner_item_label) != null;
}
}

View File

@@ -16,7 +16,7 @@ import com.foxx.androidcast.R;
/** Settings spinner adapter: plain row when closed, styled card rows in the dropdown. */
public class SettingsStringArrayAdapter extends ArrayAdapter<String> {
private final int dropdownLayout;
private int selectedPosition;
protected int selectedPosition;
public SettingsStringArrayAdapter(Context context, String[] labels) {
this(context, labels, R.layout.spinner_dropdown_item_settings);

View File

@@ -22,7 +22,6 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:checked="true"
android:text="@string/dev_show_receiver_debug_overlay" />
<CheckBox
@@ -37,7 +36,6 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:checked="true"
android:text="@string/dev_grab_session_stats" />
<Button

View File

@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/spinner_dropdown_item_root"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="6dp"
android:layout_marginTop="4dp"
android:layout_marginEnd="6dp"
android:layout_marginBottom="4dp"
android:background="@drawable/bg_settings_spinner_item"
android:clipToOutline="true"
android:elevation="4dp"
android:outlineProvider="background">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingStart="12dp"
android:paddingTop="10dp"
android:paddingEnd="12dp"
android:paddingBottom="12dp">
<TextView
android:id="@+id/spinner_item_label"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:ellipsize="end"
android:maxLines="1"
android:textColor="@color/settings_spinner_text"
android:textSize="16sp" />
<TextView
android:id="@+id/spinner_item_score"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:fontFamily="monospace"
android:gravity="end"
android:minWidth="40dp"
android:textColor="@color/settings_spinner_text"
android:textSize="14sp"
android:alpha="0.72" />
</LinearLayout>
</FrameLayout>

View File

@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingStart="8dp"
android:paddingTop="8dp"
android:paddingEnd="8dp"
android:paddingBottom="8dp">
<TextView
android:id="@+id/spinner_item_label"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:ellipsize="end"
android:maxLines="1"
android:textColor="@color/settings_spinner_text"
android:textSize="16sp" />
<TextView
android:id="@+id/spinner_item_score"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:fontFamily="monospace"
android:gravity="end"
android:minWidth="36dp"
android:textColor="@color/settings_spinner_text"
android:textSize="14sp"
android:alpha="0.72" />
</LinearLayout>

View File

@@ -109,9 +109,17 @@ reconnect_known_devices() {
# shellcheck source=scripts/android-ndk.sh
source "$ROOT/scripts/android-ndk.sh"
# shellcheck source=scripts/native-build-cache.sh
source "$ROOT/scripts/native-build-cache.sh"
export ANDROIDCAST_ROOT="$ROOT"
export ANDROID_NDK_HOME="$(androidcast_find_ndk_root "$ROOT")"
export ANDROID_NDK_ROOT="$ANDROID_NDK_HOME"
echo "==> Using NDK: $ANDROID_NDK_HOME"
if androidcast_ccache_wanted 2>/dev/null; then
echo "==> Native ccache: enabled (ANDROIDCAST_CCACHE=${ANDROIDCAST_CCACHE:-auto})"
else
echo "==> Native ccache: disabled"
fi
# Match app/build.gradle defaultConfig.ndk.abiFilters
ARCH_ABIS="armeabi-v7a arm64-v8a x86_64"

View File

@@ -74,6 +74,10 @@ export ANDROID_NDK_HOME="$NDK"
export ANDROID_NDK_ROOT="$NDK"
export PATH="$NDK_HOST_BIN:$PATH"
# shellcheck source=native-build-cache.sh
source "$ROOT/scripts/native-build-cache.sh"
androidcast_apply_native_build_accelerators "$ROOT" "$ABI" "$NDK" "$NDK_HOST_BIN"
VPX_CONFIGURE_EXTRA=()
APP_CFLAGS_EXTRA=""
case "$ABI" in
@@ -88,6 +92,9 @@ case "$ABI" in
;;
esac
## common native optimization options
APP_CFLAGS_EXTRA+=" -O2 -fdata-sections -ffunction-sections -Wl,--gc-sections -ftree-vectorize -O2 -s -funroll-loops -fomit-frame-pointer "
echo "Building libvpx for $ABI (target=$VPX_TARGET) ..."
echo " source: $VPX_SRC"
echo " build: $BUILD_DIR"
@@ -125,6 +132,7 @@ elif [[ "$ABI" == x86 || "$ABI" == x86_64 ]] && [[ -f "$BUILD_DIR/vpx_config.h"
fi
if $need_configure; then
# CC/CXX may be ccache-wrapped NDK clang (see native-build-cache.sh).
"$VPX_SRC/configure" \
--target="$VPX_TARGET" \
--disable-examples \

32
scripts/ci-build.sh Executable file
View File

@@ -0,0 +1,32 @@
#!/usr/bin/env bash
# CI entry: native codecs (all ABIs) + Gradle debug APK + unit tests.
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT"
export ANDROIDCAST_ROOT="$ROOT"
export ANDROIDCAST_CCACHE="${ANDROIDCAST_CCACHE:-1}"
export ANDROIDCAST_USE_DISTCC="${ANDROIDCAST_USE_DISTCC:-0}"
# shellcheck source=android-ndk.sh
source "$ROOT/scripts/android-ndk.sh"
export ANDROID_NDK_HOME="$(androidcast_find_ndk_root "$ROOT")"
export ANDROID_NDK_ROOT="$ANDROID_NDK_HOME"
if [[ -f .gitmodules ]]; then
git submodule update --init --recursive
fi
ABIS="${ANDROIDCAST_CI_ABIS:-armeabi-v7a arm64-v8a x86_64}"
echo "==> Building libvpx for: $ABIS"
rm -rf "$ROOT/build/native"
mkdir -p "$ROOT/build/native"
for abi in $ABIS; do
./scripts/build-native-codecs.sh "$abi"
done
echo "==> Gradle assembleDebug + unit tests"
./gradlew --no-daemon clean assembleDebug testDebugUnitTest
echo "==> Done: $ROOT/app/build/outputs/apk/debug/app-debug.apk"

View File

@@ -0,0 +1,124 @@
#!/usr/bin/env bash
# Host build accelerators for native codecs (libvpx / NDK). Source, do not execute.
#
# ccache (default: auto-detect):
# ANDROIDCAST_CCACHE=auto|1|0
# CCACHE_DIR — default: <repo>/build/.ccache
#
# distcc (off by default; experimental for libvpx configure/make only, not ndk-build):
# ANDROIDCAST_USE_DISTCC=1
# DISTCC_HOSTS=localhost/8 192.168.1.10/16
#
set -euo pipefail
[[ -n "${ANDROIDCAST_NATIVE_CACHE_SOURCED:-}" ]] && return 0
ANDROIDCAST_NATIVE_CACHE_SOURCED=1
androidcast_project_root() {
if [[ -n "${ANDROIDCAST_ROOT:-}" ]]; then
printf '%s\n' "$ANDROIDCAST_ROOT"
return 0
fi
local here
here="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
printf '%s\n' "$here"
}
androidcast_ccache_wanted() {
case "${ANDROIDCAST_CCACHE:-auto}" in
0 | false | FALSE | no | NO | off | OFF) return 1 ;;
1 | true | TRUE | yes | YES | on | ON) return 0 ;;
auto | AUTO | "")
command -v ccache >/dev/null 2>&1
;;
*) return 0 ;;
esac
}
androidcast_ndk_clang_basename() {
case "${1:-}" in
arm64-v8a) echo aarch64-linux-android29-clang ;;
armeabi-v7a) echo armv7a-linux-androideabi29-clang ;;
x86_64) echo x86_64-linux-android29-clang ;;
x86) echo i686-linux-android29-clang ;;
*) echo "" ;;
esac
}
androidcast_ndk_clangxx_basename() {
case "${1:-}" in
arm64-v8a) echo aarch64-linux-android29-clang++ ;;
armeabi-v7a) echo armv7a-linux-androideabi29-clang++ ;;
x86_64) echo x86_64-linux-android29-clang++ ;;
x86) echo i686-linux-android29-clang++ ;;
*) echo "" ;;
esac
}
# Configure ccache + optional distcc; set CC/CXX/NDK_CCACHE for libvpx + ndk-build.
# Args: project_root abi ndk_root ndk_host_bin
androidcast_apply_native_build_accelerators() {
local root="${1:?root}"
local abi="${2:?abi}"
local ndk_bin="${4:?ndk_host_bin}"
local clang clangxx
clang="$(androidcast_ndk_clang_basename "$abi")"
clangxx="$(androidcast_ndk_clangxx_basename "$abi")"
if [[ -z "$clang" || ! -x "$ndk_bin/$clang" ]]; then
echo "WARN: NDK clang not found for $abi under $ndk_bin" >&2
return 0
fi
local prefix=""
local ccache_on=false
local distcc_on=false
if androidcast_ccache_wanted && command -v ccache >/dev/null 2>&1; then
ccache_on=true
export CCACHE_DIR="${CCACHE_DIR:-$root/build/.ccache}"
mkdir -p "$CCACHE_DIR"
export CCACHE_BASEDIR="$root"
export CCACHE_COMPRESS="${CCACHE_COMPRESS:-1}"
export CCACHE_COMPILERCHECK="${CCACHE_COMPILERCHECK:-content}"
export CCACHE_MAXSIZE="${CCACHE_MAXSIZE:-5G}"
export NDK_CCACHE="${NDK_CCACHE:-ccache}"
prefix="ccache "
echo "ccache: ON (dir=$CCACHE_DIR, NDK_CCACHE=$NDK_CCACHE)"
ccache -s 2>/dev/null | sed 's/^/ /' || true
elif androidcast_ccache_wanted; then
echo "ccache: requested but not installed (install ccache or ANDROIDCAST_CCACHE=0)"
else
echo "ccache: off (ANDROIDCAST_CCACHE=${ANDROIDCAST_CCACHE:-auto})"
fi
case "${ANDROIDCAST_USE_DISTCC:-0}" in
1 | true | TRUE | yes | YES | on | ON)
if command -v distcc >/dev/null 2>&1 && [[ -n "${DISTCC_HOSTS:-}" ]]; then
distcc_on=true
export DISTCC_VERBOSE="${DISTCC_VERBOSE:-0}"
prefix="distcc ${prefix}"
echo "distcc: ON (experimental, libvpx make/configure only; hosts=$DISTCC_HOSTS)"
elif command -v distcc >/dev/null 2>&1; then
echo "distcc: ANDROIDCAST_USE_DISTCC=1 but DISTCC_HOSTS empty — skipped" >&2
else
echo "distcc: requested but distcc not installed — skipped" >&2
fi
;;
*)
echo "distcc: off (set ANDROIDCAST_USE_DISTCC=1 and DISTCC_HOSTS to try)"
;;
esac
export CC="${prefix}${ndk_bin}/${clang}"
if [[ -n "$clangxx" && -x "$ndk_bin/$clangxx" ]]; then
export CXX="${prefix}${ndk_bin}/${clangxx}"
else
export CXX="${prefix}${ndk_bin}/${clang}"
fi
export AR="${ndk_bin}/llvm-ar"
export RANLIB="${ndk_bin}/llvm-ranlib"
# libvpx configure reads these when cross-compiling.
export AS="$CC"
export LD="${ndk_bin}/ld.lld"
}