[LINUX] Erstellen einer Software, die den Android-Bildschirm auf eine PC 2 Real-Time Touch Edition spiegelt

Einführung

Dieser Artikel ist eine Fortsetzung von Erstellen von Software, die Android-Bildschirme auf einen PC 1 spiegelt. Dort wird erklärt, wie Sie eine Funktion zum Spiegeln des Android-Bildschirms erstellen.

Ich werde so etwas machen

capture5.gif

Ermöglicht die Bedienung Ihres Android-Geräts mit der Maus.

Spezifikation

fig7.png Ich hätte gerne nur eine Verbindung zwischen Android und PC, aber ich habe Kompromisse geschlossen, weil das Programm kompliziert zu sein schien.

Client-Software sein

Zeigt die von FFmpeg fließenden Daten an. Außerdem wird die Mausbedienung in die Berührungsbedienung des Bildschirms umgewandelt und auf die Terminalseite geworfen.

Auf der Android-Seite sein

Der Bildschirminhalt wird nach wie vor verschlüsselt und an die PC-Seite gesendet. Es empfängt auch Berührungsereignisse von der PC-Seite und greift in das System ein.

Bedienen Sie ein Android-Gerät von einem PC aus

In diesem Abschnitt wird beschrieben, wie Android-Geräte mit Berührungen und Tastenanschlägen umgehen. Sie können Echtzeit-Touch implementieren, ohne es zu wissen. Wenn Sie nicht interessiert sind, fahren Sie mit "Implementierung" fort.

Wie kann ich Android von einem PC aus bedienen? Beginnen wir mit einem einfachen Beispiel.

Geben Sie die Terminal-Shell ein und drücken Sie den Befehl

Android ist ein Linux-basiertes Betriebssystem. Obwohl es sich um eine limitierte Edition handelt, können Sie das Linux-Terminal verwenden.

adb shell

Sie können den Dialog aufrufen, indem Sie dies tun. Dann gibt es einen Befehl namens ** Eingabe **, der Berührungsoperationen und Tastenoperationen sendet. Verwenden Sie diesen Befehl. Ein Beispiel ist unten angegeben.

input touchscreen tap x y
input touchscreen swipe x1 y1 x2 y2
input keyevent Key
input text Text

Durch Ausführen dieser können Sie einfach ein Ereignis an das Terminal senden. Aber wenn ich es starte, dauert es ** Zeit, bis das Ereignis ausgelöst wird **. Dies ist nicht für den Echtzeitbetrieb geeignet. Was ist auch, wenn Sie andere komplizierte Vorgänge als Tippen und Wischen ausführen möchten?

getEvent Verwenden Sie sendEvent

Der Befehl ** getevent ** ist ein Befehl, der die vom Touchpanel oder der physischen Taste des Terminals gesendeten Daten ausgibt. Versuchen Sie, das Touchpanel zu bedienen, nachdem Sie den folgenden Befehl ausgeführt haben.

getevent

Anschließend werden die numerischen Werte in einer Reihe angezeigt (siehe Abbildung unten). image.png Dies sind die vom Touchpanel gesendeten Daten. Das Betriebssystem interpretiert und reflektiert diese Daten. Wie es tatsächlich interpretiert wird, wird in [Android] Berühren des Terminals aus dem Programm [ADB] erklärt. Guck dir das mal bitte an.

** sendevent ** ist ein Befehl, der beliebige Daten senden kann, als ob sie von einem Touchpanel gesendet würden. Mit anderen Worten, die Berührungsoperation kann reproduziert werden, indem die von ** getevent ** erhaltenen Daten erneut an ** sendevent ** gesendet werden. Dieser Befehl ist jedoch auch langsam auszuführen und kann nicht vollständig reproduziert werden.

Bearbeiten Sie Gerätedateien direkt

Da Android auf Linux basiert, werden Gerätedateien verwendet. Es ist möglicherweise nicht bekannt für diejenigen, die Nicht-Unix-Betriebssysteme wie Windows verwenden. Eine Gerätedatei ist eine spezielle Datei, die zur Interaktion mit verschiedenen verbundenen Geräten verwendet wird. Wenn Sie beispielsweise die Gerätedatei des Touchpanels öffnen, können Sie die von getevent erhaltenen Daten zum Betrieb des Touchpanels lesen. Wenn Sie dagegen in eine Gerätedatei schreiben, werden die geschriebenen Daten so behandelt, als stammten sie von diesem Gerät, ähnlich wie bei sendevent. Gerätedatei [Gerätespezialdatei](https://uc2.h2np.net/index.php/%E3%83%87%E3%83%90%E3%82%A4%E3%82%B9%E3%82%B9 % E3% 83% 9A% E3% 82% B7% E3% 83% A3% E3% 83% AB% E3% 83% 95% E3% 82% A1% E3% 82% A4% E3% 83% AB) Ich denke, dieser Bereich wird hilfreich sein.

Lassen Sie uns die Gerätedatei tatsächlich öffnen. Ich denke, Sie können den Pfad / dev / input / event4 im Bild oben sehen. Dies ist der Speicherort der Gerätedatei. Im Verzeichnis / dev / input befinden sich mehrere Gerätedateien wie event0, event1 ...., die Touchpanels, physischen Tasten, Sensoren usw. entsprechen. ** Bitte beachten Sie, dass die entsprechende Nummer je nach Terminal unterschiedlich ist. ** ** **

cat /dev/input/event●

Wenn Sie diesen Befehl ausführen, fließen Daten von diesem Gerät. Es wird jedoch so.

image.png Die aus der Gerätedatei erhaltenen Rohdaten sind Binärdaten, die nicht in Zeichen angezeigt werden. Das zu Beginn eingeführte getEvent, sendEvent und der Eingabebefehl konvertieren zwischen Binär und Zeichen (numerische Werte).

Apropos,

cat /dev/input/event● > /sdcard/event.bin

Auf diese Weise können Sie die Rohdaten in einer Datei speichern.

cat /sdcard/event.bin > /dev/input/event●

Dann können Sie die Berührungsdaten reproduzieren, aber auch hier gibt es eine Falle. Es läuft zu schnell (bitteres Lächeln). Ereignisdaten, die in wenigen Sekunden aufgezeichnet wurden, fließen sofort ein. Ich denke, es ist zu früh, um ordnungsgemäß zu funktionieren. Um die Operation zu reproduzieren, muss der Ruhezustand eingefügt werden, damit die Daten zu einem geeigneten Zeitpunkt gesendet werden können.

Was ist doch zu tun?

Der Punkt ist, dass das von der PC-Seite gesendete Berührungsereignis an die Gerätedatei gesendet werden kann. Sie können es möglicherweise mit einem Shell-Skript schreiben, aber da wir mit Android Studio entwickeln, Es ist einfach, in Java / Kotlin zu schreiben. Früher habe ich auf Gerätedateien zugegriffen, wie ich möchte, aber es ist eine Technik, die nur mit Shell-Berechtigungen erreicht werden kann. Mit anderen Worten, normale Apps haben keine Berechtigung, mit Systemdateien herumzuspielen. (Es sei denn, es ist verwurzelt) Ich wollte gerade aufgeben, aber ich fand diese Frage. How does vysor create touch events on a non rooted device? Wie Vysor ein Echtzeit-Touch-Ereignis auf einem nicht gerooteten Gerät ist Rennst du? Ist die Frage. Und die Antwort ist

What he does is, he then starts his Main class as a separate process using this shell user. Now, the Java code inside that Main class has the same privileges as the shell user (because duh, it's linux).

Er verwendet die Berechtigungen des Shell-Benutzers, um die Hauptklasse in einem separaten Prozess zu starten. Jetzt hat der Java-Code in der Hauptklasse die gleichen Berechtigungen wie der Shell-Benutzer (da Android Linux ist).

Mit anderen Worten, wenn Sie die im apk-Paket enthaltene Klasse über die Shell ausführen, kann das gestartete Programm auch das Shell-Privileg verwenden. Kein Wunder, aber ich habe nicht daran gedacht. Dieses Mal werde ich mit dieser Methode Echtzeit-Touch implementieren.

Nun zur Implementierung

Wie oben erwähnt, ist die Leistung der Shell erforderlich, um von der Anwendung aus in den Systembereich einzugreifen. Gehen Sie zunächst wie folgt vor, um die im apk-Paket enthaltene Klasse mit Shell-Berechtigungen zu starten.

sh -c "CLASSPATH=[Pfad zur APK-Datei] /system/bin/app_process /system/bin [Paketnamen].[Der Name der Klasse, die die Hauptmethode enthält]"

Der Pfad zur apk-Datei lautet

pm path [Paketnamen]

Sie können es mit bekommen, aber wenn Sie normalerweise mit Android Studio hier debuggen, werden mehrere Pfade angezeigt. Dies ist ein Phänomen, das bei Verwendung von Instant Run auftritt, einer Funktion, die die Erstellungszeit verkürzt und Programmänderungen in Echtzeit widerspiegelt. Durch die Aufteilung von apk in mehrere Teile scheint es, dass zum Zeitpunkt der Änderung nur der Unterschied erstellt und installiert wird, um Zeit zu sparen. Wenn es jedoch aufgeteilt wird, funktioniert der obige Befehl nicht ordnungsgemäß, sodass Instant Run deaktiviert werden muss. Der Ort der Einstellung ist wie folgt. image.png

Noch wichtiger ist, dass die vom Benutzer selbst gestartete App und das mit Shell-Berechtigungen gestartete Programm zum selben Paket gehören. Da die Prozesse jedoch unterschiedlich sind, kann die statische Freigabe überhaupt nicht durchgeführt werden. Wenn Sie also kommunizieren möchten, müssen Sie dies über eine Steckdose usw. tun.

Eingreifen von Ereignissen aus dem Programm in das System

Schauen Sie sich zuerst den folgenden Code an.

InputService.java


public class InputService {
    InputManager im;
    Method injectInputEventMethod;


    public InputService() throws Exception {

        //Holen Sie sich eine Instanz von InputManager
        im = (InputManager) InputManager.class.getDeclaredMethod("getInstance").invoke(null, new Object[0]);

        //Ermöglicht das Aufrufen statischer Methoden, die MotionEvent generieren
        MotionEvent.class.getDeclaredMethod(
                "obtain",
                long.class, long.class, int.class, int.class,
                MotionEvent.PointerProperties[].class, MotionEvent.PointerCoords[].class,
                int.class, int.class, float.class, float.class, int.class, int.class, int.class, int.class
        ).setAccessible(true);

        //Holen Sie sich eine Methode, um ein Ereignis in das System einzugreifen
        injectInputEventMethod = InputManager.class.getDeclaredMethod("injectInputEvent", new Class[]{InputEvent.class, int.class});

    }

    //Touch-Ereignis generieren
    public void injectMotionEvent(int inputSource, int action, float x, float y) throws InvocationTargetException, IllegalAccessException {

        MotionEvent.PointerProperties[] pointerProperties = new MotionEvent.PointerProperties[1];
        pointerProperties[0] = new MotionEvent.PointerProperties();
        pointerProperties[0].id = 0;


        MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[1];
        pointerCoords[0] = new MotionEvent.PointerCoords();
        pointerCoords[0].pressure = 1;
        pointerCoords[0].size = 1;
        pointerCoords[0].touchMajor = 1;
        pointerCoords[0].touchMinor = 1;
        pointerCoords[0].x = x;
        pointerCoords[0].y = y;

        MotionEvent event = MotionEvent.obtain(SystemClock.uptimeMillis(), SystemClock.uptimeMillis(), action, 1, pointerProperties, pointerCoords, 0, 0, 1, 1, 0, 0, inputSource, 0);
        injectInputEventMethod.invoke(im, new Object[]{event, 0});
    }

    //Schlüsselereignis generieren
    public void injectKeyEvent(KeyEvent event)throws InvocationTargetException, IllegalAccessException{
        injectInputEventMethod.invoke(im, new Object[]{event, 0});
    }
}

Dieser Code basiert auf hier und verwendet die neue API. Ich habe es geändert, um es zu verwenden.

Wir verwenden Shell-Berechtigungen, um auf Systemmethoden und Instanzen zuzugreifen, auf die normalerweise durch Reflektion nicht zugegriffen werden kann. Dies wird als "injizierenInputEvent" bezeichnet. Wenn Sie Ereignisdaten an diese Methode übergeben, wird das Ereignis ausgeführt. Ich habe in der AOSP-Quelle nachgesehen, wo sich die Methode tatsächlich befindet, und festgestellt, dass hier Es war in Zeile 914 von /hardware/input/InputManager.java). Auf die Annotation "Ausblenden" kann normalerweise nicht zugegriffen werden. Betrachtung? Für diejenigen, die sagen, kann dieser Artikel hilfreich sein.

Implementierung von Input Host Server

Führen Sie das vom Personal Computer gesendete Ereignis mit der obigen InputService-Klasse aus.

InputHost.java


public class InputHost {
    static InputService inputService;

    static ServerSocket listener;//Server-Socket

    static Socket clientSocket;//Buchse zur Client-Seite
    static InputStream inputStream;//Zum Empfangen von Nachrichten von Clients
    static OutputStream outputStream;//Stream zum Senden von Daten an den Client


    static boolean runnning = false;


    public static void main(String args[]) {
        try {
            inputService = new InputService();
        } catch (Exception ex) {
            ex.printStackTrace();
        }

        try {
            listener = new ServerSocket();
            listener.setReuseAddress(true);
            listener.bind(new InetSocketAddress(8081));
            System.out.println("Server listening on port 8081...");

            clientSocket = listener.accept();//Warten Sie bis die Verbindung hergestellt ist

            System.out.println("Connected");

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

            runnning = true;

            BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));

            while (runnning) {
                String msg = reader.readLine();
                String[] data = msg.split(" ");

                if (data.length > 0) {
                    if (data[0].equals("screen")) {//Für Touch-Daten
                        inputService.injectMotionEvent(InputDeviceCompat.SOURCE_TOUCHSCREEN, Integer.valueOf(data[1]), Integer.valueOf(data[2]), Integer.valueOf(data[3]));
                    } else if (data[0].equals("key")) {//Für Schlüssel
                        inputService.injectKeyEvent(new KeyEvent(Integer.valueOf(data[1]), Integer.valueOf(data[2])));
                    } else if (data[0].equals("exit")) {//Anruf beenden
                        Disconnect();
                    }
                }
            }

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

    }

    //Schneidvorgang
    private static void Disconnect() {

        runnning = false;

        try {

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

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

        System.out.println("Disconnected");

    }
}

Es ist ein einfaches Serverprogramm. Ich dachte auch darüber nach, den Datenaustausch mit dem PC mithilfe von json zu modernisieren. Es ist keine große Sache, deshalb habe ich beschlossen, es in einem Format wie CSV zu senden, das durch Leerzeichen getrennt ist.

Dies ist die einzige Implementierung auf der Android-Seite. ** Der gesamte Code ist hier **

Implementieren Sie als Nächstes die PC-Seite.

Client erstellen

Ich möchte es mit C # & WPF erstellen. Warum hast du dich nicht für WinForms entschieden? Dies liegt daran, dass es schwierig war, das Bild mit 60 FPS anzuzeigen. Als ich es implementierte, waren es ungefähr 30 FPS. Wenn DoubleBuffered deaktiviert war, wurden es 60 FPS, aber das Flimmern war so groß, dass es nicht praktikabel war.

WPF fühlt sich ebenfalls subtil an, ist aber besser als WinForms, da es GPU zum Zeichnen verwendet. Wenn Sie es ernsthaft tun, verwenden Sie eine Grafik-API wie OpenGL (OpenTK für C #) ... Ich hätte nie gedacht, dass die empfangende Seite eher der Engpass als die sendende Seite ist ^^;

** Klicken Sie hier für den gesamten Client-Code (https://github.com/SIY1121/ScreenCastClient) **

UI image.png

MainWindow.xaml


<Window x:Class="ScreenCastClient.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:ScreenCastClient"
        mc:Ignorable="d"
        Title="ScreenCastClient" Height="819.649" Width="420.611" Loaded="Window_Loaded" Closing="Window_Closing">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition/>
            <RowDefinition Height="60"/>
        </Grid.RowDefinitions>
        <Image x:Name="image" MouseDown="image_MouseDown" MouseUp="image_MouseUp" MouseMove="image_MouseMove"/>

        <Grid Grid.Row="1" Background="#FF008BFF">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="204*"/>
                <ColumnDefinition Width="193*"/>
            </Grid.ColumnDefinitions>
            <Polygon Points="0,15 25,0 25,30" Fill="White" Margin="30,17,0,0" HorizontalAlignment="Left" Width="36" MouseDown="Polygon_MouseDown" MouseUp="Polygon_MouseUp" />
            <Ellipse Fill="White" Margin="186,18,181,12" Width="30" HorizontalAlignment="Center" MouseDown="Ellipse_MouseDown" MouseUp="Ellipse_MouseUp" Grid.ColumnSpan="2"/>
            <Rectangle Fill="White" Margin="0,17,30,10" HorizontalAlignment="Right" Width="30" MouseDown="Rectangle_MouseDown" MouseUp="Rectangle_MouseUp" Grid.Column="1"/>
        </Grid>
    </Grid>
</Window>

Methode für die Zukunft

Hier finden Sie eine Zusammenfassung der Funktionen, die in Zukunft häufig verwendet werden. Reguläre Ausdrücke sind sehr nützlich, wenn Sie nur die erforderlichen Zahlen aus einigen Daten extrahieren möchten. Darüber hinaus zur Überprüfung Online-Regex-Tester und Debugger: PHP, PCRE, Python, Golang und JavaScript Diese Seiten sind sehr nützlich.

C#:MainWindow.xaml.cs


        //Führen Sie einfach den Befehl aus und geben Sie die Standardausgabe zurück
        private string Exec(string str)
        {
            Process process = new Process
            {
                StartInfo =
                 {
                    FileName =  "cmd",
                    Arguments = @"/c " + str,
                    UseShellExecute = false,
                    CreateNoWindow = true,
                    RedirectStandardInput = true,
                    RedirectStandardError = true,
                    RedirectStandardOutput = true
                 },
                EnableRaisingEvents = true
            };
            process.Start();
            string results = process.StandardOutput.ReadToEnd();
            process.WaitForExit();
            process.Close();
            return results;
        }

        //Gibt ein Datenarray zurück, das mit einem regulären Ausdruck übereinstimmt
        private string[] GetRegexResult(string src, string pattern)
        {
            Regex regex = new Regex(pattern);
            Match match = regex.Match(src);
            string[] res = new string[match.Groups.Count - 1];
            for (int i = 1; i < match.Groups.Count; i++)
                res[i - 1] = match.Groups[i].Value;
            return res;
        }

Videos in Zusammenarbeit mit FFmpeg dekodieren und anzeigen

Anwendungen jedes Betriebssystems verfügen über Standard-Eingabe- und Ausgabefunktionen. fig8-2.png

In der allgemeinen Videokonvertierungssoftware werden Eingabe und Ausgabe von Dateien ausgeführt. Da FFmpeg jedoch die Standardeingabe und -ausgabe verwenden kann, können Sie die aus dem Programm decodierten Daten lesen, indem Sie das Ausgabeziel auf stdout setzen. Im Fall von FFmpeg wird außerdem angegeben, dass das Protokoll von stderr ausgegeben wird. Das folgende Programm startet FFmpeg und stellt eine Verbindung mit stdout und stderr her.

C#:MainWindow.xaml.cs


        private void StartFFmpeg()
        {
            //Porteinstellungen
            Exec("adb forward tcp:8080 tcp:8080");

            var inputArgs = "-framerate 60  -analyzeduration 100 -i tcp://127.0.0.1:8080";
            var outputArgs = "-f rawvideo -pix_fmt bgr24 -r 60 -flags +global_header - ";
            Process process = new Process
            {
                StartInfo =
                 {
                    FileName = "ffmpeg.exe",
                    Arguments = $"{inputArgs} {outputArgs}",
                    UseShellExecute = false,
                    CreateNoWindow = true,
                    RedirectStandardInput = true,
                    RedirectStandardError = true,//Stderr lesbar machen
                    RedirectStandardOutput=true//Machen Sie stdout lesbar
                 },
                EnableRaisingEvents = true
            };
            process.ErrorDataReceived += Process_ErrorDataReceived;//Die Protokolle werden von stderr übertragen. Verarbeiten Sie sie daher separat.
            process.Start();
            rawStream = process.StandardOutput.BaseStream;//Daten fließen von stdout, also holen Sie sich den Stream
            process.BeginErrorReadLine();
            running = true;
            Task.Run(() =>
            {
                //Beginnen Sie in einem anderen Thread zu lesen
                ReadRawData();
            });
        }

Stellen Sie die erforderlichen Argumente ein und starten Sie FFmpeg. Es gibt zwei Möglichkeiten, Daten aus der Ausgabe abzurufen: Die erste besteht darin, sich für ein Ereignis zu registrieren, und die zweite darin, einen Stream abzurufen und selbst zu lesen. Ersteres kann leicht erhalten werden, kann jedoch nicht zum Austausch von Binärdaten verwendet werden, da es in Zeichendaten konvertiert wird. Letzteres verarbeitet Streams, ist also etwas umständlich, aber eine Feinsteuerung ist möglich. Dieses Mal, da das Protokoll von stderr fließt, wird das erstere gelesen, und da die Binärdaten des Bildes von stdout fließen, werden die Daten durch das letztere Verfahren gelesen.

Von FFmpeg gesendete Prozessprotokolle

C#:MainWindow.xaml.cs


        //Lesen Sie die Standardfehlerausgabe von FFmpeg
        private void Process_ErrorDataReceived(object sender, DataReceivedEventArgs e)
        {
            if (e.Data == null) return;

            Console.WriteLine(e.Data);

            if (imageWidth == 0 && imageHeight == 0)//Wenn die zu sendende Größe noch nicht bestätigt ist
            {
                //Grobe Arbeit beim Extrahieren der Größe aus der FFmpeg-Ausgabe
                string[] res = GetRegexResult(e.Data, @"([0-9]*?)x([0-9]*?), [0-9]*? fps");
                if (res.Length == 2)
                {
                    imageWidth = int.Parse(res[0]);
                    imageHeight = int.Parse(res[1]);
                    bytePerframe = imageWidth * imageHeight * 3;

                    if(imageWidth>imageHeight)//Für Querformat
                    {
                        //Tauschen Sie die Maximal- und Minimalwerte der Berührungskoordinaten aus
                        int tmp = displayWidth;
                        displayWidth = displayHeight;
                        displayHeight = tmp;
                    }

                    Dispatcher.Invoke(() => {//Wenn Sie im UI-Thread keine Bitmap erstellen, kann diese nicht in der UI wiedergegeben werden
                        writeableBitmap = new WriteableBitmap(imageWidth, imageHeight, 96, 96, PixelFormats.Bgr24, null);
                        image.Source = writeableBitmap;
                    });
                }
            }

        }

Die Größe des Bildes ist wichtig, um die in Zukunft gesendeten Rohdaten wiederherzustellen. Wenn FFmpeg mit der Konvertierung beginnt, gibt es die Informationen des Streams aus, der in das Protokoll ausgegeben werden soll, sodass die Größe des Bilds grob extrahiert werden kann. Eine Variable namens bytePerFrame ist die Anzahl der Bytes, die zum Generieren eines Einzelbilds erforderlich sind. Sie kann anhand der Anzahl der Bytes berechnet werden, die für vertikal x horizontal x 1 Pixel des Bildes verwendet werden. Dieses Mal ist FFmpeg so eingestellt, dass es mit rgb24 ausgegeben wird (8 Bit für jedes RGB sind 24 Bit), sodass die Anzahl der für 1 Pixel verwendeten Bytes 3 beträgt. Wenn Sie sich Sorgen über den Mechanismus von Bildern machen, wissen Sie nicht? Grundkenntnisse in Bild und Dateistruktur. .

Geben Sie die von FFmpeg gesendeten Daten an das Bild zurück

C#:MainWindow.xaml.cs


        //Lesen Sie rawStream von FFmpeg und schreiben Sie in Bitmap
        private void ReadRawData()
        {
            MemoryStream ms = new MemoryStream();

            byte[] buf = new byte[10240];
            while (running)
            {
                int resSize = rawStream.Read(buf, 0, buf.Length);

                if (ms.Length + resSize >= bytePerframe)//Wenn die gelesenen Daten dieses Mal die Daten für einen Frame erreichen oder überschreiten
                {
                    int needSize = bytePerframe - (int)ms.Length;//Die Größe der verbleibenden Daten, die für einen Frame erforderlich sind
                    int remainSize = (int)ms.Length + resSize - bytePerframe;//Größe der überschüssigen Daten

                    ms.Write(buf, 0, bytePerframe - (int)ms.Length);//Lesen Sie den Rest der benötigten Daten in einem Frame

                    Dispatcher.Invoke(() =>
                    {
                        if (writeableBitmap != null)//Daten schreiben
                            writeableBitmap.WritePixels(new Int32Rect(0, 0, imageWidth, imageHeight), ms.ToArray(), 3 * imageWidth, 0);
                    });

                    ms.Close();
                    ms = new MemoryStream();
                    ms.Write(buf, needSize + 1, remainSize);//Schreiben Sie überschüssige Daten
                }
                else
                {
                    ms.Write(buf, 0, resSize);//Daten akkumulieren
                }
            }
        }

Daten werden aus dem Stream erfasst und in MemoryStream gesammelt. Wenn ein Datenrahmen gesichert ist, werden die Daten in MemoryStream in einem Image wiederhergestellt. WritableBitmap verfügt über eine Methode, die aus einem Array wiederhergestellt wird. Verwenden Sie diese Methode. Der Zugriff auf WritableBitmap muss auch im UI-Thread erfolgen.

Starten Sie InputHost und verbinden Sie sich

C#:MainWindows.xaml.cs


        //Starten Sie InputHost und verbinden Sie sich
        private void StartInputHost()
        {
            string inputInfo = Exec("adb shell getevent -i");//Erhalten Sie Daten zur Eingabe des Android-Terminals
            //Extrahieren Sie den Maximalwert der Berührungskoordinaten von innen
            string[] tmp = GetRegexResult(inputInfo, @"ABS[\s\S]*?35.*?max (.*?),[\s\S]*?max (.*?),");
            displayWidth = int.Parse(tmp[0]);
            displayHeight = int.Parse(tmp[1]);

            //Porteinstellungen
            Exec("adb forward tcp:8081 tcp:8081");
            //Holen Sie sich den App-Pfad
            //Entfernen Sie zusätzliche Zeichen und Zeilenvorschubcode
            string pathToPackage = Exec("adb shell pm path space.siy.screencastsample").Replace("package:", "").Replace("\r\n", "");

            Process process = new Process
            {
                StartInfo =
                 {
                    FileName = "adb",
                    Arguments = $"shell",
                    UseShellExecute = false,
                    CreateNoWindow = true,
                    RedirectStandardInput = true,
                    RedirectStandardError = true,
                    RedirectStandardOutput = true
                 },
                EnableRaisingEvents = true
            };
            process.Start();
            process.OutputDataReceived += (s, e) =>
            {
                Console.WriteLine(e.Data);//Ich kann in Zukunft etwas tun
            };
            process.BeginOutputReadLine();
            //Starten Sie InputHost mit Shell-Berechtigungen
            process.StandardInput.WriteLine($"sh -c \"CLASSPATH={pathToPackage} /system/bin/app_process /system/bin space.siy.screencastsample.InputHost\"");
            System.Threading.Thread.Sleep(1000);//Warten Sie, bis es beginnt
            TcpClient tcp = new TcpClient("127.0.0.1", 8081);//Stellen Sie eine Verbindung zu InputHost her
            streamToInputHost = tcp.GetStream();
        }

Zuerst starte ich adb shell getevent -i und lese die Ausgabe, Dieser Befehl zeigt Daten zum Android-Eingabegerät an. Hier wird der Maximalwert der Koordinaten erfasst, die das Touchpanel annehmen kann. Und wir starten InputHost. Bitte beachten Sie, dass wir schwierige Dinge tun, z. B. 1 Sekunde warten, bis es beginnt.

Daten an InputHost senden

Nachdem die Vorbereitungen für die Verbindung abgeschlossen sind, müssen wir nur noch die Daten an den InputHost senden.

C#:MainWindow.xaml.cs


        private void image_MouseDown(object sender, MouseButtonEventArgs e)
        {
            Point p = GetDisplayPosition(e.GetPosition(image));
            byte[] sendByte = Encoding.UTF8.GetBytes($"screen 0 {p.X} {p.Y}\n");
            streamToInputHost.Write(sendByte, 0, sendByte.Length);
            mouseDown = true;
        }

        private void image_MouseMove(object sender, MouseEventArgs e)
        {
            if (mouseDown)
            {
                Point p = GetDisplayPosition(e.GetPosition(image));
                byte[] sendByte = Encoding.UTF8.GetBytes($"screen 2 {p.X} {p.Y}\n");
                streamToInputHost.Write(sendByte, 0, sendByte.Length);
            }
        }

        private void image_MouseUp(object sender, MouseButtonEventArgs e)
        {
            Point p = GetDisplayPosition(e.GetPosition(image));
            byte[] sendByte = Encoding.UTF8.GetBytes($"screen 1 {p.X} {p.Y}\n");
            streamToInputHost.Write(sendByte, 0, sendByte.Length);
            mouseDown = false;
        }

        //Konvertieren Sie die Mausposition in Terminal-Touch-Koordinaten
        private Point GetDisplayPosition(Point p)
        {
            int x = (int)(p.X / image.ActualWidth * displayWidth);
            int y = (int)(p.Y / image.ActualHeight * displayHeight);
            return new Point(x, y);
        }

Ich sende Daten mit einem Ereignis über die Maus im Bild. Die zweite Zahl 0,1,2 im Leerzeichen bedeutet 0 für Donw, 1 für Up und 2 für Move. Darüber hinaus lautet das Schlüsselereignis wie folgt.

C#:MainWindow.xaml.cs


        private void Polygon_MouseDown(object sender, MouseButtonEventArgs e)
        {
            byte[] sendByte = Encoding.UTF8.GetBytes($"key 0 4\n");
            streamToInputHost.Write(sendByte, 0, sendByte.Length);
        }

        private void Polygon_MouseUp(object sender, MouseButtonEventArgs e)
        {
            byte[] sendByte = Encoding.UTF8.GetBytes($"key 1 4\n");
            streamToInputHost.Write(sendByte, 0, sendByte.Length);
        }

        private void Ellipse_MouseDown(object sender, MouseButtonEventArgs e)
        {
            byte[] sendByte = Encoding.UTF8.GetBytes($"key 0 3\n");
            streamToInputHost.Write(sendByte, 0, sendByte.Length);
        }

        private void Ellipse_MouseUp(object sender, MouseButtonEventArgs e)
        {
            byte[] sendByte = Encoding.UTF8.GetBytes($"key 1 3\n");
            streamToInputHost.Write(sendByte, 0, sendByte.Length);
        }

        private void Rectangle_MouseDown(object sender, MouseButtonEventArgs e)
        {
            byte[] sendByte = Encoding.UTF8.GetBytes($"key 0 187\n");
            streamToInputHost.Write(sendByte, 0, sendByte.Length);
        }

        private void Rectangle_MouseUp(object sender, MouseButtonEventArgs e)
        {
            byte[] sendByte = Encoding.UTF8.GetBytes($"key 1 187\n");
            streamToInputHost.Write(sendByte, 0, sendByte.Length);
        }

Das zweite Leerzeichen hat die gleiche Bedeutung wie oben. Die dritte ist die eindeutige Nummer des Schlüssels, die unter KeyEvent bestätigt werden kann. Sie können auch Schlüssel senden, die nicht auf Ihrem Gerät implementiert sind. Beispielsweise wird 120 eine PrintScreen-Taste zugewiesen. Der Screenshot wird normalerweise durch gleichzeitiges Drücken der Ein- / Aus-Taste und der Lautstärke erstellt. Sie können einen Screenshot machen, indem Sie diesen Schlüssel senden. Wenn Sie es sofort versuchen möchten

adb shell input keyevent 120

Kann mit reproduziert werden.

Ich werde es tatsächlich bewegen

Portweiterleitung, Starten von InputHost über die Shell usw. Alle lästigen Dinge sind in der Client-Software implementiert, so dass es einfach ist.

Verfahren

    1. Starten Sie die App auf der Android-Seite und drücken Sie Start
  1. Starten Sie die Client-Software

Nur das. Der Bildschirm wird jetzt projiziert und Sie können den Bildschirm mit der Maus bedienen.

Impressionen

Ich war zuerst verwirrt, weil das Ausführen der Klasse in apk mit Shell keine normale Anwendungsentwicklung ist, aber ich konnte sie implementieren. Da adb für diese Funktion unverzichtbar geworden ist, muss adb installiert werden, wenn es an normale Benutzer verteilt wird. Vor allem, da jetzt nur noch adb heruntergeladen werden kann, wurde der Schwellenwert für die Einführung gesenkt. (Ist es enthalten?)

Jetzt bin ich Vysor ziemlich nahe. Ich konnte jedoch noch keine Zeichen eingeben oder Dateien übertragen, daher möchte ich dies als Nächstes implementieren. Dann danke, dass du bis zum Ende zugesehen hast.

Recommended Posts

Erstellen einer Software, die den Android-Bildschirm auf eine PC 2 Real-Time Touch Edition spiegelt
Erstellen einer Software zur Visualisierung der Datenstruktur ~ Heap ~
[Python / C] Ich habe versucht, ein Gerät zu erstellen, das den Bildschirm eines PCs drahtlos aus der Ferne scrollt.
Zeigen Sie einen Bildschirm an, für den Sie sich bei Digital Signage anmelden müssen
Übergang zum Update-Bildschirm mit dem Django-Tag
Erstellen eines Python-Skripts, das die e-Stat-API unterstützt (Version 2)
Der erste Schritt beim Erstellen einer serverlosen Anwendung mit Zappa
Lassen Sie uns einen Roboter bauen, der den Zauberwürfel löst! 3 Software
Ich habe versucht, eine SATA-Software-RAID-Konfiguration zu erstellen, die das Betriebssystem unter Ubuntu Server startet