~rom1v/blog { un blog libre }

Compiler un exécutable pour Android

Je vais présenter dans ce billet comment compiler un exécutable ARM pour Android, l’intégrer à un APK et l’utiliser dans une application.

À titre d’exemple, nous allons intégrer un programme natif, udpxy, dans une application minimale de lecture vidéo.

Contexte

Le framework multimédia d’Android ne supporte pas nativement la lecture de flux UDP multicast (1, 2).

Il est possible, pour y parvenir, d’utiliser des lecteurs alternatifs, par exemple basés sur ffmpeg/libav (l’un est un fork de l’autre) ou libvlc.

Il existe par ailleurs un outil natif, sous licence GPLv3, relayant du trafic UDP multicast vers du HTTP : udpxy. N’importe quel client supportant HTTP (comme le lecteur natif d’Android) peut alors s’y connecter. C’est cet outil que nous allons utiliser ici.

udpxy

Compilation classique

Avant de l’intégrer, comprenons son utilisation en le faisant tourner sur un ordinateur classique (Debian Wheezy 64 bits pour moi).

Il faut d’abord le télécharger les sources, les extraire et compiler :

wget http://www.udpxy.com/download/1_23/udpxy.1.0.23-9-prod.tar.gz
tar xf udpxy.1.0.23-9-prod.tar.gz
cd udpxy-1.0.23-9/
make

Si tout se passe bien, nous obtenons (entre autres) un binaire udpxy.

Test de diffusion

Pour tester, nous avons besoin d’une source UDP multicast. Ça tombe bien, VLC peut la fournir. Pour obtenir le résultat attendu par udpxy, nous devons diffuser vers une adresse multicast (ici 239.0.0.1). Par exemple, à partir d’un fichier MKV :

cvlc video.mkv ':sout=#udp{dst=239.0.0.1:1234}'

En parallèle, démarrons une instance d’udpxy, que nous venons de compiler :

./udpxy -p 8379

Cette commande va démarrer un proxy relayant de l’UDP multicast vers de l’HTTP, écoutant sur le port 8379.

Dans un autre terminal, nous pouvons faire pointer VLC sur le flux ainsi proxifié :

vlc http://localhost:8379/udp/239.0.0.1:1234/

Normalement, le flux doit être lu correctement.

Remarquez qu’udpxy pourrait très bien être démarré sur une autre machine (il suffirait alors de remplacer localhost par son IP). Mais pour la suite, nous souhaiterons justement exécuter udpxy localement sur Android.

Bien sûr, avec VLC, nous n’aurions pas besoin d’udpxy. Le flux est lisible directement avec la commande :

vlc udp://@239.0.0.1:1234/

Android

Notez que certains devices Android ne supportent pas le multicast, la réception de flux multicast ne fonctionnera donc pas.

Maintenant que nous avons vu comment fonctionne udpxy, portons-le sur Android.

Notre but est de le contrôler à partir d’une application et le faire utiliser par le lecteur vidéo natif.

Pour cela, plusieurs étapes sont nécessaires :

  1. obtenir un binaire ARM exécutable pour Android ;
  2. le packager avec une application ;
  3. l’extraire ;
  4. l’exécuter.

Exécutable ARM

Pré-compilé

Pour obtenir un binaire ARM exécutable, le plus simple, c’est évidemment de le récupérer déjà compilé, s’il est disponible (c’est le cas pour udpxy). Dans ce cas, il n’y a rien à faire.

Pour le tester, transférons-le sur le téléphone et exécutons-le :

adb push udpxy /data/local/tmp
adb shell /data/local/tmp/udpxy -p 8379

Si tout se passe bien, cette commande ne produit en apparence rien : elle attend qu’un client se connecte. Pour valider le fonctionnement, si le téléphone est sur le même réseau que votre ordinateur, vous pouvez utiliser cette instance (ARM) d’udpxy comme proxy entre la source multicast et un lecteur VLC local :

vlc http://xx.xx.xx.xx:8379/udp/239.0.0.1:1234/

Replacer xx.xx.xx.xx par l’ip du device, qu’il est possible d’obtenir ainsi :

adb shell netcfg | grep UP

Compilation ponctuelle

S’il n’est pas disponible, il va falloir le compiler soi-même à partir des sources, ce qui nécessite le NDK Android, fournissant des chaînes de compilation pré-compilées.

Il suffit alors d’initialiser la variable d’environnement CC pour pointer sur la bonne chaîne de compilation (adaptez les chemins et l’architecture selon votre configuration) :

export NDK=~/android/ndk
export SYSROOT="$NDK/platforms/android-19/arch-arm"
export CC="$NDK/toolchains/arm-linux-androideabi-4.8/prebuilt/linux-x86_64/bin/arm-linux-androideabi-gcc --sysroot=$SYSROOT"
make

Bravo, vous venez de générer un binaire udpxy pour l’architecture ARM.

Compilation intégrée

La compilation telle que réalisée ci-dessus est bien adaptée à la génération d’un exécutable une fois de temps en temps, mais s’intègre mal dans un système de build automatisé. En particulier, un utilisateur avec une architecture différente devra adapter les commandes à exécuter.

Heureusement, le NDK permet une compilation plus générique.

Pour cela, il faut créer un répertoire jni dans un projet Android (ou n’importe où d’ailleurs, mais en pratique c’est là qu’il est censé être), y mettre les sources et écrire des Makefiles.

Créons donc un répertoire jni contenant les sources. Vu que nous les avons déjà extraites, copions-les à la racine de jni/ :

cp -rp udpxy-1.0.23-9/ jni/
cd jni/

Créons un Makefile nommé Android.mk :

LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

LOCAL_MODULE    := udpxy
LOCAL_SRC_FILES := udpxy.c sloop.c rparse.c util.c prbuf.c ifaddr.c ctx.c \
                   mkpg.c rtp.c uopt.c dpkt.c netop.c extrn.c main.c

include $(BUILD_EXECUTABLE)

Puis compilons :

ndk-build

ndk-build se trouve à la racine du NDK.

Le binaire sera généré dans libs/armeabi/udpxy.

Afin d’organiser les projets plus proprement, il vaut mieux mettre les sources d’udpxy et son Android.mk dans un sous-répertoire spécifique au projet (dans jni/udpxy/). Dans ce cas, il faut rajouter un fichier jni/Android.mk contenant :

include $(call all-subdir-makefiles)

Packager avec l’application

Je suppose ici que vous savez déjà créer une application Android.

Nous devons maintenant intégrer le binaire dans l’APK. Pour cela, il y a principalement deux solutions :

  • l’intégrer aux ressources (dans res/raw/) ;
  • l’intégrer aux assets (dans assets/).

Vu que les projets library ne gèrent pas les assets, nous allons utiliser une ressource raw.

Il faut donc copier le binaire dans res/raw/, à chaque fois qu’il est généré (à automatiser donc).

Extraire l’exécutable

L’exécutable est bien packagé avec l’application, et comme toutes les ressources, nous pouvons facilement obtenir un InputStream (le fonctionnement est similaire pour les assets).

Mais pour l’exécuter en natif, le binaire doit être présent sur le système de fichiers. Il faut donc le copier et lui donner les droits d’exécution. Sans la gestion des exceptions, cela donne :

// "/data/data/<package>/files/udpxy"
File target = new File(getFilesDir(), "udpxy")
InputStream in = getResources().openRawResource(R.raw.udpxy);
OutputStream out = new FileOutputStream(target);
// copy from R.raw.udpxy to /data/data/<package>/files/udpxy
FileUtils.copy(in, out);
// make the file executable
FileUtils.chmod(target, 0755);

Et les parties intéressantes de FileUtils :

public static void copy(InputStream in, OutputStream os) throws IOException {
    byte[] buf = new byte[4096];
    int read;
    while ((read = (in.read(buf))) != -1) {
        os.write(buf, 0, read);
    }
}

public static boolean chmod(File file, int mode) throws IOException {
    String sMode = String.format("%03o", mode); // to string octal value
    String path = file.getAbsolutePath();
    String[] argv = { "chmod", sMode, path };
    try {
        return Runtime.getRuntime().exec(argv).waitFor() == 0;
    } catch (InterruptedException e) {
        throw new IOException(e);
    }
}

Exécuter le programme natif

Maintenant que le binaire est disponible sur le système de fichiers, il suffit de l’exécuter :

String[] command = { udpxyBin.getAbsolutePath(), "-p", "8379" };
udpxyProcess = Runtime.getRuntime().exec(command);

Le lecteur vidéo pourra alors utiliser l’URI proxifié comme source de données :

String src = UdpxyService.proxify("239.0.0.1:1234");

Projets

andudpxy

Je mets à disposition sous licence GPLv3 le projet library andudpxy, qui met en œuvre ce que j’ai expliqué ici : andudpxy.

Pour l’utiliser dans votre application, n’oubliez pas de référencer la library et de déclarer le service UdpxyService dans votre AndroidManifest.xml :

<service android:name="com.rom1v.andudpxy.UdpxyService" />

Pour démarrer le démon :

UdpxyService.startUdpxy(context);

et pour l’arrêter :

UdpxyService.stopUdpxy(context);

andudpxy-sample

J’ai également écrit une application minimale de lecture vidéo qui utilise cette library : andudpxy-sample.

C’est toujours utile d’avoir une application d’exemple censée fonctionner ;-)

L’adresse du flux UDP multicast à lire est écrite en dur dans MainActivity (et le flux doit fonctionner lors du démarrage de l’activité) :

private static final String ADDR = "239.0.0.1:1234";

Compilation

Après avoir cloné les 2 projets dans un même répertoire parent, renommez les local.properties.sample en local.properties, éditez-les pour indiquer le chemin du SDK et du NDK.

Ensuite, allez dans le répertoire andudpxy-sample, puis exécutez :

ant clean debug

Vous devriez obtenir bin/andudpxy-sample-debug.apk.

Bien sûr, vous pouvez aussi les importer dans Eclipse (ou un autre IDE) et les compiler selon vos habitudes.

Conclusion

Nous avons réussi à compiler et exécuter un binaire ARM sur Android, packagé dans une application.

Ceci peut être utile pour exécuter du code déjà implémenté nativement pour d’autres plates-formes, pour faire tourner un démon natif… Par exemple, le projet Serval (sur lequel j’ai un peu travaillé) utilise un démon servald, qui tourne également sur d’autres architectures.

Ce n’est cependant pas la seule manière d’exécuter du code natif dans une application : la plus courante est d’appeler des fonctions natives (et non un exécutable) directement à partir de Java, en utilisant JNI. L’une et l’autre répondent à des besoins différents.