[JAVA] Créer un logiciel qui reflète l'écran Android sur un PC 1

introduction

Cet article explique la fonction de mise en miroir de l'écran Android sur un PC. Suite de cet article Création d'un logiciel qui reflète l'écran Android sur un PC 2 Édition tactile en temps réel il y a.

Je vais faire quelque chose comme ça

capture3.gif

Créez un logiciel qui reflète l'écran Android sur un PC. Je ne sais pas avec Gif, mais cela fonctionne de manière gluante à 50-60FPS. Il fonctionne également dans les environnements Windows, Mac et Linux.

pic.jpg Le débit binaire de H.264 est de 5000 Kbps, la résolution est de 1/2 et le délai est d'environ 300 ms, donc c'est assez en temps réel. Il peut être plus court selon le réglage.

Motivation

Il existe plusieurs logiciels qui reflètent l'écran Android.

Parmi eux, Vysor est un logiciel avec une excellente qualité d'image et une excellente fréquence d'images. Il se déplace normalement à 60 FPS, donc j'ai été impressionné lorsque je l'ai utilisé pour la première fois. La qualité d'image est limitée dans la version gratuite, mais elle sera annulée lors de la charge. (Il existe un système d'abonnement et un système d'achat)

Cependant, il est difficile pour les étudiants de payer doucement. J'ai décidé de le faire moi-même.

Cependant, cette fois, nous ne créerons qu'une fonction de mise en miroir.

spécification

fig1.png

Créez un serveur côté Android et connectez-vous à partir d'un PC. C'est possible via Wi-Fi, mais il communique via USB pour plus de stabilité.

Être du côté Android

Capturez l'écran à l'aide de Media Projection. (Par conséquent, les terminaux compatibles seront 5.0 ou version ultérieure.) Encodez la capture avec Media Codec et envoyez-la côté PC.

Être du côté PC

Connectez-vous au serveur pour décoder et afficher le flux. Cependant, cette fois, je vais tout jeter sur ffplay et ne créerai aucun programme côté PC (rires)

ffplay est un logiciel de lecture vidéo inclus dans le célèbre outil de conversion vidéo FFmpeg. Vous pouvez jouer diverses choses en spécifiant les paramètres. Cette fois, nous allons l'utiliser pour décoder le flux et l'afficher en temps réel. J'ai parlé de l'environnement OS au début cette fois car ce ffmpeg est compatible avec différents OS.

À propos du codec à utiliser

Il existe une liste de codecs qui peuvent être encodés côté Android dans Formats média pris en charge, mais au final, cela semble convenir en fonction du terminal. Je l'ai essayé sur plusieurs machines et émulateurs réels, mais seul H.264 fonctionnait sur tous.

En outre, bien que VP8 puisse générer l'encodeur lui-même, il semble qu'il y ait quelque chose qui ne va pas avec le tampon acquis et qu'il échoue avec une erreur. VP9 est devenu [Données invalides trouvées lors du traitement de l'entrée] et ffplay ne l'a pas reconnue. H.265 peut être utilisé avec n'importe quel terminal pouvant être utilisé.

Dans cet exemple, le codec peut être spécifié, veuillez donc l'essayer sur la machine réelle pour voir lequel fonctionne. Si vous pouvez utiliser VP8 ou 9, ce serait plus facile sans vous soucier de la licence, mais c'est dommage.

Concernant l'erreur, je l'ajouterai dès que la cause sera connue. (Je vous serais reconnaissant si vous pouviez me dire quelles informations vous avez.)

Pour plus de détails sur les types de codecs, voir Différents des types de codecs vidéo (H.264, VP9, MPEG, Xvid, DivX, WMV, etc.) [Comparaison] Introduit.

Capturez l'écran d'Android

Vous pouvez obtenir l'écran du côté de l'application sur Android 5.0 ou version ultérieure. Plus précisément, utilisez la projection multimédia. Prendre une capture d'écran de l'application ANDROID 5.0 Il est expliqué en détail ici.

Flux d'utilisation de la projection multimédia

C'est facile, mais voici comment utiliser Media Projection. Vous voudrez peut-être l'examiner en vous référant au code de l'article ci-dessus.

Classe à utiliser

・ ** MediaProjectionManager ** Afficher une boîte de dialogue demandant à l'utilisateur l'autorisation de capturer l'écran et obtenir la projection multimédia si cela est autorisé.

・ ** Projection multimédia ** Fournit la fonction d'acquisition de l'écran. Pour être précis, il crée un tampon appelé affichage virtuel et y reflète l'écran. Il existe plusieurs modes autres que la mise en miroir.

・ ** Affichage virtuel ** Un tampon créé et écrit par MediaProjection. Il a une surface à écrire à l'intérieur, et c'est en fait un tampon. Vous pouvez spécifier cette surface lorsque vous la créez. Par conséquent, si vous spécifiez la surface d'ImageReader, vous pouvez obtenir l'image via ImageReader, Si vous spécifiez la surface de la vue de surface, elle sera affichée dans la vue en temps réel.

· ** Surface ** Un tampon spécialisé dans la «gestion des images» contrairement aux tampons ordinaires En plus de VirtualDisplay, il est également utilisé dans les lecteurs SurfaceView et de lecture vidéo utilisés lors de la création de jeux.

Image lors de l'utilisation d'ImageReader

En fait, ImageReader a un mécanisme pour stocker les cadres, mais cela ressemble à ceci. fig2.png

Procédure (le code est dans la seconde moitié)

  1. Obtenez ** MeidaProjectionManager ** avec ** getSystemService **
  2. Créez un intent qui demande l'autorisation de capturer l'écran avec ** createScreenCaptureIntent ** de ** Manager ** Lancez l'intent créé dans 3.2 et attrapez-le avec ** onActivityResult ** de ** Activity **
  3. Si l'utilisateur a l'autorisation, obtenez ** MediaProjection ** avec ** getMediaProjection ** de ** Manager **
  4. Créez un écran virtuel en mode miroir avec le ** Media Projection ** ** createVirtualDisplay ** À ce stade, spécifiez ** Surface ** que vous souhaitez écrire Le contenu de l'écran étant écrit en temps réel sur la ** Surface ** spécifiée en 6.5, utilisez-la.

Encoder la vidéo sur Android

Utilisez MediaCodec.

Les articles suivants ont été utiles. Document officiel Présentation de la classe MediaCodec Traduction japonaise Comment compresser une vidéo sans FFmpeg à l'aide de MediaCodec sur Android (avec bibliothèque) Ce qui a été introduit dans l'article ci-dessus EncodeAndMuxTest (Bien que la méthode soit ancienne, la procédure a été utile)

Flux d'utilisation de MediaCodec

Voici quelques étapes générales à suivre lors de l'utilisation de MediaCodec.

Classe à utiliser

・ ** MediaCodec ** Encodeur et décodeur vidéo ・ ** MediaFormat ** Stocke les informations vidéo telles que le codec, le débit binaire et la fréquence d'images. Utilisé pour définir MediaCodec.

Image d'utilisation

Vous pouvez utiliser Buffer ou Surface pour l'entrée et la sortie d'image. Il est également possible d'utiliser Surface pour l'entrée et Buffer pour la sortie.

fig3.png

Procédure (le code est dans la seconde moitié)

  1. Créez un encodeur et un décodeur avec ** createEncoderByType / createDecoderByType **
  2. Créez ** MediaFormat ** et définissez la vidéo à encoder et décoder.
  3. Exécutez ** configure ** de ** Media Codec **. Spécifiez le format de média créé dans 2.
  4. Définissez un rappel lors du traitement asynchrone
  5. Lancez la conversion avec ** start **
  6. Envoyez les données avant l'encodage / décodage à l'entrée
  7. Extraire les données traitées de Output

Précautions lors de la saisie / sortie de données

Comme mentionné ci-dessus, Surface et Buffer peuvent être utilisés pour entrer et sortir des données Media Codec. Cependant, il existe des différences dans la méthode de livraison en fonction de ce que vous utilisez.

Pour l'entrée

Lorsque vous utilisez Buffer, vous devez transmettre manuellement les données à MediaCodec. La Surface sera remise automatiquement lorsque le contenu sera mis à jour.

Pour la sortie

Vous devez obtenir les données manuellement lorsque vous utilisez Buffer. Le contenu de Surface sera mis à jour automatiquement.

Ce logiciel utilise Surface pour l'entrée et Buffer pour la sortie.

Maintenant, la mise en œuvre

Disposition

fig5.png ** Cliquez ici pour la mise en page xml (https://github.com/SIY1121/ScreenCastSample/blob/master/app/src/main/res/layout/activity_main.xml) **

Flux de processus

Le processus démarre lorsque le bouton de démarrage est cliqué. fig4-2.png

code

J'ai tout rassemblé dans MainActivity.java, donc je ne l'ai pas implémenté à grande échelle. Notez également que certaines pièces ne sont pas vérifiées pour les erreurs. ** L'ensemble du code est ici **

Ce qui suit est un extrait du code, veuillez donc vous référer au code entier.

1. OnClick Affiche une boîte de dialogue demandant l'autorisation de capturer

Code aux lignes 130-155

MainActivity.java


    button_start.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {

                switch (states) {
                    case Stop:
                        //Afficher une boîte de dialogue pour confirmer la capture
                        manager = (MediaProjectionManager) getSystemService(Context.MEDIA_PROJECTION_SERVICE);
                        startActivityForResult(manager.createScreenCaptureIntent(), REQUEST_CODE);
                        break;
                    case Waiting:
                        //Annuler la veille
                        Disconnect();
                        break;
                    case Running:
                        //Déconnecter
                        Disconnect();
                        break;
                }

            }
        });

Puisque ce bouton est également utilisé pour arrêter, le processus est branché en fonction de l'état. Le traitement commence à l'arrêt. J'ai acquis ** MediaProjectionManager ** et affiche une boîte de dialogue pour confirmer la capture à l'utilisateur.

2. Traitez le résultat de la boîte de dialogue onActivityResult

C'est le code aux lignes 162-206.

MainActivity.java


    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent intent) {
        if (resultCode != RESULT_OK) {
            Toast.makeText(this, "permission denied", Toast.LENGTH_LONG).show();
            return;
        }

        //Si l'utilisateur approuve la capture d'écran
        //Obtenir la projection multimédia
        mediaProjection = manager.getMediaProjection(resultCode, intent);


        //Déterminez la taille de l'écran virtuel
        double SCALE = seekBar_scale.getProgress() * 0.01;

        DisplayMetrics metrics = getResources().getDisplayMetrics();
        final int WIDTH = (int) (metrics.widthPixels * SCALE);
        final int HEIGHT = (int) (metrics.heightPixels * SCALE);
        final int DENSITY = metrics.densityDpi;


        try {

            PrepareEncoder(
                    WIDTH,
                    HEIGHT,
                    codecs[spinner_codec.getSelectedItemPosition()],
                    seekBar_bitrate.getProgress(),
                    seekBar_fps.getProgress(),
                    10//Je cadre est fixe
            );

            SetupVirtualDisplay(WIDTH, HEIGHT, DENSITY);

            StartServer();



        } catch (Exception ex) {//Une erreur lors de la création d'un encodeur
            ex.printStackTrace();
            Toast.makeText(this, ex.getMessage(), Toast.LENGTH_LONG).show();
        }


    }

Lorsque l'utilisateur appuie sur la boîte de dialogue affichée en 1., ** onActivityResult ** est généré. Si cela est autorisé, obtenez ** MediaProjection ** avec ** getMediaProjection **. Ensuite, obtenez la taille de l'écran et préparez l'encodeur et l'affichage virtuel.

3.PrepareEncoder Préparation de l'encodeur

C'est le code aux lignes 218-274.

MainActivity.java


//Préparation du codeur
    private void PrepareEncoder(int WIDTH, int HEIGHT, String MIME_TYPE, int BIT_RATE, int FPS, int IFRAME_INTERVAL) throws Exception {

        MediaFormat format = MediaFormat.createVideoFormat(MIME_TYPE, WIDTH, HEIGHT);
        //Définir les propriétés de format
        //Si vous ne définissez pas les propriétés minimales, configure entraînera une erreur.
        format.setInteger(MediaFormat.KEY_COLOR_FORMAT,
                MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
        format.setInteger(MediaFormat.KEY_BIT_RATE, BIT_RATE);
        format.setInteger(MediaFormat.KEY_FRAME_RATE, FPS);
        format.setInteger(MediaFormat.KEY_CAPTURE_RATE, FPS);
        format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, IFRAME_INTERVAL);


        //Obtenez l'encodeur
        codec = MediaCodec.createEncoderByType(MIME_TYPE);

        codec.setCallback(new MediaCodec.Callback() {
            @Override
            public void onInputBufferAvailable(@NonNull MediaCodec codec, int index) {
                Log.d("MediaCodec", "onInputBufferAvailable : " + codec.getCodecInfo());

            }

            @Override
            public void onOutputBufferAvailable(@NonNull final MediaCodec codec, final int index, @NonNull MediaCodec.BufferInfo info) {
                Log.d("MediaCodec", "onOutputBufferAvailable : " + info.toString());
                ByteBuffer buffer = codec.getOutputBuffer(index);
                byte[] array = new byte[buffer.limit()];
                buffer.get(array);

                //Envoyer des données encodées
                Send(array);

                //Tampon gratuit
                codec.releaseOutputBuffer(index, false);
            }

            @Override
            public void onError(@NonNull MediaCodec codec, @NonNull MediaCodec.CodecException e) {
                Log.d("MediaCodec", "onError : " + e.getMessage());
            }

            @Override
            public void onOutputFormatChanged(@NonNull MediaCodec codec, @NonNull MediaFormat format) {
                Log.d("MediaCodec", "onOutputFormatChanged : " + format.getString(MediaFormat.KEY_MIME));
            }
        });

        //Définir l'encodeur
        codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);

        //Obtenir la surface utilisée pour transmettre le cadre à l'encodeur
        //Doit être appelé entre configure et start
        inputSurface = codec.createInputSurface();

    }

Commencez par créer ** Media Format **. Ensuite, définissez les paramètres requis pour le codage. Utilisez ensuite ** createEncoderByType ** pour créer ** MediaCodec **. Exécutez ensuite ** configure ** pour définir ** Media Format **.

Enfin, appelez ** createInputSurface ** pour obtenir la surface en entrée. Lorsque vous écrivez une image sur cette surface, son contenu est automatiquement codé.

Aussi, je mets un rappel ici, mais j'utilise Uniquement ** onOutputBufferAvailable ** qui sera appelé lorsque les données encodées seront disponibles. Acquiert les données codées sous forme de tableau d'octets et les envoie au côté PC.

4.SetupVirtualDisplay Création d'un affichage virtuel

C'est le code des lignes 208-216.

MainActivity.java


//Configuration de l'affichage virtuel
    private void SetupVirtualDisplay(int WIDTH, int HEIGHT, int DENSITY) {

        virtualDisplay = mediaProjection
                .createVirtualDisplay("Capturing Display",
                        WIDTH, HEIGHT, DENSITY,
                        DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
                        inputSurface, null, null);//Utilisez celui obtenu de l'encodeur pour la surface d'écriture
    }

Je crée un affichage virtuel. ** La chose la plus importante ici est de définir la surface d'entrée obtenue de l'encodeur sur la surface d'écriture. ** ** En faisant cela, l'écran en miroir sera écrit directement sur la surface d'entrée de l'encodeur, L'écran est encodé sans aucune action particulière. Le flux est illustré ci-dessous.

fig6.png

5. StartServer Démarrer le thread du serveur

C'est le code des lignes 312 à 322.

MainAcitvity


//Démarrer la veille et envoyer des threads
    private void StartServer() {
        senderThread = new HandlerThread("senderThread");
        senderThread.start();
        senderHandler = new Handler(senderThread.getLooper());

        serverThread = new Thread(this);
        serverThread.start();

        setState(States.Waiting);
    }

Nous commençons un fil pour l'envoi et un fil pour l'écoute. Étant donné que le thread de secours implémente Runnable dans Activity, il y effectue le traitement dans run (). Le thread d'envoi utilise HandlerThread afin qu'il puisse être mis en file d'attente.

6. Traitement du serveur En attente d'une connexion depuis un PC

C'est le code des lignes 324 à 346.

MainActivity.java


    //Fil de serveur
    //Accepte la connexion une seule fois
    public void run() {
        try {
            listener = new ServerSocket();
            listener.setReuseAddress(true);
            listener.bind(new InetSocketAddress(8080));
            System.out.println("Server listening on port 8080...");

            clientSocket = listener.accept();//Attendez la connexion

            inputStream = clientSocket.getInputStream();
            outputStream = clientSocket.getOutputStream();

            //Il est nécessaire de démarrer l'encodage lorsque le client est connecté
            codec.start();

            setState(States.Running);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

Lancez le socket serveur et attendez. Cette fois, il n'est pas nécessaire de distribuer à plusieurs PC, nous n'acceptons donc les connexions qu'une seule fois.

Après la connexion, l'encodeur démarre. Sinon, il ne pourrait pas être lu côté PC. La première image après le début du codage est la trame I, qui est essentielle pour le décodage futur. Si vous ne recevez pas l'image I en premier, vous ne pourrez pas la lire côté PC. Je pense que c'est la raison pour laquelle il ne peut pas être joué. (Veuillez préciser si c'est différent)

Je cadre? ?? Ceux qui disent Qu'est-ce qu'une image clé? Différence entre les images I, P et B [GOP] S'il te plait regarde.

7. Transmission de données

Code aux lignes 348-366

MainActivity.java


    //Envoyer des données
    //Ne changez pas l'ordre
    //Ajouter à la liste
    private void Send(final byte[] array) {
        senderHandler.post(new Runnable() {
            @Override
            public void run() {

                try {
                    outputStream.write(array);
                } catch (IOException ex) {
                    //S'il ne peut pas être envoyé, il est considéré comme déconnecté.
                    ex.printStackTrace();
                    Disconnect();
                }

            }
        });
    }

Il est appelé dans le jeu de rappel dans l'encodeur de 3. Le rappel s'exécute sur le thread principal, tout comme cette méthode appelée. En raison de la restriction selon laquelle le traitement lié au réseau ne doit pas être effectué dans le thread principal J'essaye d'effectuer le processus de transmission dans le fil de transmission.

De plus, en passant, si vous écrivez ce qui suit, l'écran affiché côté PC peut être perturbé.

MainActivity.java


    private void Send(final byte[] array) {

        new Thread(new Runnable() {
            @Override
            public void run() {

                try {
                    outputStream.write(array);
                } catch (IOException ex) {
                    //S'il ne peut pas être envoyé, il est considéré comme déconnecté.
                    ex.printStackTrace();
                    Disconnect();
                }

            }
        }).start();
    }

En premier lieu, ce n'est pas du bon code au moment de créer des threads tout le temps, Cela ne garantit pas l'ordre des trames à envoyer. Comme mentionné précédemment dans l'article de commentaire I-frame, Parce que l'image compressée ne représente que la différence entre les images précédentes et suivantes Si le contexte est perturbé, il ne sera pas décodé correctement.

8. Découpe et post-traitement

C'est le code aux lignes 368-387.

MainActivity.java


//Processus de coupe
    private void Disconnect() {

        try {
            codec.stop();
            codec.release();
            virtualDisplay.release();
            mediaProjection.stop();


            listener.close();
            if (clientSocket != null)
                clientSocket.close();

        } catch (IOException ex) {
            ex.printStackTrace();
        }

        setState(States.Stop);
    }

Les objets utilisés jusqu'à présent sont arrêtés et libérés. Cela vous ramènera à l'état d'arrêt, donc si vous appuyez à nouveau sur le bouton, le processus recommencera depuis le début et vous pourrez vous reconnecter.

Communiquez entre le PC et l'appareil Android via USB

Plus précisément, il peut être réalisé en utilisant adb-server comme un serveur proxy. Jouez avec adb Je l'ai mentionné ici.

Facile avec une seule commande

adb forward tcp:xxxx tcp:yyyy

Spécifiez le numéro de port utilisé côté PC en xxxx et le numéro de port utilisé sur le terminal en yyyy. Cette fois

adb forward tcp:8080 tcp:8080

Je pense que ça va. Désormais, lorsque vous vous connectez au port 8080 de localhost (127.0.0.1) côté PC, vous serez connecté au 8080 côté terminal ** via USB.

En passant, pourquoi 127.0.0.1 a-t-il été attribué à l'adresse IP de localhost? J'étais curieux à ce sujet, alors quand je l'ai recherché, il semble qu'il y ait un arrière-plan historique d'IPv4. Pourquoi 127.0.0.1 est-il un hôte local?

Affichez l'écran sur le PC

Merci d'avoir lu ce long article jusqu'à présent. Enfin, j'aimerais afficher l'écran sur le PC et terminer. Puisque ffplay introduit au début de l'article est utilisé, veuillez le télécharger si vous ne l'avez pas. Download FFmpeg Après le téléchargement, décompressez le fichier et vous trouverez le dossier bin, qui contient l'unité principale. Comme FFmpeg, ffplay est démarré en spécifiant les paramètres de CUI.

procédure

    1. Connectez votre appareil Android à votre PC afin qu'il soit reconnu par adb.
  1. Exécuter adb forward tcp: 8080 tcp: 8080
    1. Lancez l'application sur votre appareil Android et appuyez sur Démarrer
  2. Après avoir démarré l'invite de commande ou PowerShell, accédez au répertoire où se trouve ffplay, puis exécutez ce qui suit
ffplay -framerate 60 -analyzeduration 100 -i tcp://127.0.0.1:8080

L'écran du côté Android sera maintenant affiché sur votre PC. (S'il n'apparaît pas, abaissez la barre d'état ou retournez à la maison pour actualiser l'écran.)

Vous pouvez quitter avec Echap.

Signification des paramètres

-framerate 60 spécifie simplement la fréquence d'images. Doit être identique aux paramètres de l'application Cast.

-analyzeduration 100 Limite la durée pendant laquelle ffplay analyse les trames reçues. (100 ms cette fois) ffplay analyse et affiche après qu'un certain nombre d'images se sont accumulées, donc si cette option n'est pas spécifiée, elle sera affichée avec un délai.

-i tcp: //127.0.0.1:8080 L'adresse de réception du flux. Si vous souhaitez essayer via Wi-Fi, veuillez spécifier l'adresse IP du terminal. De plus, si vous spécifiez le chemin du fichier ici, vous pouvez lire la vidéo normalement.

J'ai des problèmes

J'ai un problème personnel. Si vous avez des informations, merci de me le faire savoir. ** Sur Android 8.0, même si le processus d'attente de la connexion du socket serveur se fait dans un thread séparé **

MainActivity.java


clientSocket = listener.accept();

L'interface utilisateur est bloquée avec. Les boutons physiques cessent également de fonctionner du tout, et si vous ne vous connectez pas et ne débloquez pas, l'interface utilisateur du système redémarrera après un certain temps. Vous pouvez le reproduire avec un émulateur, alors essayez-le.

Avez-vous modifié des spécifications dans la version 8.0 ...? Cela fonctionne bien avant la version 7.1.

Impressions

Ce n'est toujours pas suffisant pour remplacer Vysor, mais j'ai été surpris de voir à quel point il était facile de mettre en œuvre la mise en miroir. Il n'y a toujours pas assez de fonctions telles que le traitement tactile en temps réel, mais j'aimerais le faire à l'avenir.

Aussi, je voudrais créer une fonction qui peut faire fonctionner automatiquement le terminal avec un script. À cet égard, intégrez la fonction de script et l'éditeur dans l'application C # qui s'exécute sous Windows. Essayez d'ajouter une fonction de script à l'application C # Nous avons également publié un article intitulé, alors jetez un œil si vous êtes intéressé.

Ensuite, merci d'avoir regardé jusqu'au bout.

Suivant Création d'un logiciel qui reflète l'écran Android sur un PC 2 Édition tactile en temps réel

Recommended Posts

Créer un logiciel qui reflète l'écran Android sur un PC 1
Comment créer un écran de démarrage
[Android] Comment créer un fragment de dialogue
[Android] J'ai créé un écran de liste de matériaux avec ListView + Bottom Sheet
Comment créer un plug-in natif Unity (version Android)
Comment créer un lecteur de musique Android imposant
[Android Studio] [Java] Comment réparer l'écran verticalement
Un débutant en développement d'applications a essayé de créer une application de calculatrice Android
[Introduction au développement d'applications Android] Faisons un compteur