In diesem Artikel wird die Funktion zum Spiegeln des Android-Bildschirms auf einen PC erläutert. Fortsetzung von diesem Artikel Erstellen einer Software, die den Android-Bildschirm auf eine PC 2 Real-Time-Touch-Edition spiegelt es gibt.
Erstellen Sie eine Software, die den Android-Bildschirm auf einen PC spiegelt. Ich weiß es nicht mit Gif, aber es funktioniert schleimig bei 50-60FPS. Es funktioniert auch in Windows-, Mac- und Linux-Umgebungen.
Die Bitrate von H.264 beträgt 5000 Kbit / s, die Auflösung 1/2 und die Verzögerung ca. 300 ms, also ziemlich in Echtzeit. Sie kann je nach Einstellung kürzer sein.
Es gibt verschiedene Software, die den Android-Bildschirm spiegeln.
Unter diesen ist Vysor eine Software mit ausgezeichneter Bildqualität und Bildrate. Es bewegt sich normalerweise mit 60 FPS, daher war ich beeindruckt, als ich es zum ersten Mal verwendete. Die Bildqualität ist in der kostenlosen Version begrenzt, wird jedoch beim Aufladen storniert. (Es gibt ein Abonnementsystem und ein Kaufsystem)
Es ist jedoch schwierig für Studenten, Geld sanft zu bezahlen. Ich habe beschlossen, es selbst zu machen.
Dieses Mal erstellen wir jedoch nur eine Spiegelungsfunktion.
Erstellen Sie einen Server auf der Android-Seite und stellen Sie eine Verbindung von einem PC aus her. Es ist über WLAN möglich, kommuniziert jedoch aus Stabilitätsgründen über USB.
Erfassen Sie den Bildschirm mit Media Projection. (Daher sind kompatible Terminals 5.0 oder höher.) Codieren Sie die Aufnahme mit Media Codec und senden Sie sie an den PC.
Stellen Sie eine Verbindung zum Server her, um den Stream zu dekodieren und anzuzeigen. Dieses Mal werde ich jedoch alles auf ffplay werfen und keine Programme auf der PC-Seite erstellen (lacht)
ffplay ist eine Videowiedergabesoftware, die im berühmten Videokonvertierungs-Tool FFmpeg enthalten ist. Sie können verschiedene Dinge spielen, indem Sie die Parameter angeben. Dieses Mal werden wir es verwenden, um den Stream zu dekodieren und in Echtzeit anzuzeigen. Ich habe diesmal zu Beginn über die Betriebssystemumgebung gesprochen, da dieses ffmpeg mit verschiedenen Betriebssystemen kompatibel ist.
Es gibt eine Liste von Codecs, die auf der Android-Seite in Unterstützte Medienformate codiert werden können, aber am Ende scheint sie je nach Terminal geeignet zu sein. Ich habe es auf mehreren realen Maschinen und Emulatoren versucht, aber nur H.264 funktionierte auf allen.
Obwohl VP8 den Encoder selbst generieren kann, scheint etwas mit dem erfassten Puffer nicht zu stimmen und er schlägt mit einem Fehler fehl. VP9 wurde zu [Ungültige Daten bei der Verarbeitung der Eingabe gefunden] und ffplay erkannte sie nicht. H.265 kann mit jedem Terminal verwendet werden, das verwendet werden kann.
In diesem Beispiel kann der Codec angegeben werden. Probieren Sie ihn daher auf dem tatsächlichen Computer aus, um festzustellen, welcher funktioniert. Wenn Sie VP8 oder 9 verwenden können, wäre es einfacher, ohne sich um die Lizenz zu sorgen, aber es ist eine Schande.
In Bezug auf den Fehler werde ich ihn hinzufügen, sobald die Ursache bekannt ist. (Ich wäre Ihnen dankbar, wenn Sie mir mitteilen könnten, welche Informationen Sie haben.)
Einzelheiten zu den Codec-Typen finden Sie unter Unterschiede zu Video-Codec-Typen (H.264, VP9, MPEG, Xvid, DivX, WMV usw.) [Vergleich]. Eingeführt.
Sie können den Bildschirm von der Anwendungsseite auf Android 5.0 oder höher abrufen. Verwenden Sie insbesondere die Medienprojektion. Machen Sie einen Screenshot aus der ANDROID 5.0-App Es wird hier ausführlich erklärt.
Es ist einfach, aber hier erfahren Sie, wie Sie Media Projection verwenden. Vielleicht möchten Sie es sich ansehen, während Sie sich auf den Code im obigen Artikel beziehen.
・ ** MediaProjectionManager ** Zeigen Sie ein Dialogfeld an, in dem der Benutzer um Erlaubnis zum Erfassen des Bildschirms gebeten wird, und rufen Sie die Medienprojektion ab, falls dies zulässig ist.
・ ** Medienprojektion ** Bietet die Funktion zum Erfassen des Bildschirms. Um genau zu sein, wird ein Puffer erstellt, der als virtuelle Anzeige bezeichnet wird, und der Bildschirm dort gespiegelt. Es gibt verschiedene andere Modi als das Spiegeln.
・ ** VirtualDisplay ** Ein von MediaProjection erstellter und geschriebener Puffer. Es hat eine Oberfläche zum Schreiben, und es ist eigentlich ein Puffer. Sie können diese Oberfläche beim Erstellen angeben. Wenn Sie die Oberfläche von ImageReader angeben, können Sie das Bild daher über ImageReader abrufen. Wenn Sie die Oberfläche der Oberflächenansicht angeben, wird sie in Echtzeit in der Ansicht angezeigt.
· ** Oberfläche ** Ein Puffer, der sich im Gegensatz zu normalen Puffern auf das "Behandeln von Bildern" spezialisiert hat Zusätzlich zu VirtualDisplay wird es auch in SurfaceView- und Videowiedergabeplayern verwendet, die beim Erstellen von Spielen verwendet werden.
Eigentlich hat ImageReader einen Mechanismus zum Speichern von Frames, aber es sieht so aus.
Verwenden Sie MediaCodec.
Die folgenden Artikel waren hilfreich. Offizielles Dokument Übersicht über die japanische Übersetzung der MediaCodec-Klasse Komprimieren von Videos ohne FFmpeg mit MediaCodec unter Android (mit Bibliothek) Was wurde im obigen Artikel vorgestellt EncodeAndMuxTest (Obwohl die Methode alt ist, war die Prozedur hilfreich)
Im Folgenden finden Sie einige allgemeine Schritte zur Verwendung von MediaCodec.
・ ** MediaCodec ** Video-Encoder und -Decoder ・ ** MediaFormat ** Speichert Videoinformationen wie Codec, Bitrate und Bildrate. Wird zum Festlegen von MediaCodec verwendet.
Sie können Buffer oder Surface für die Frame-Eingabe und -Ausgabe verwenden. Es ist auch möglich, Surface für die Eingabe und Buffer für die Ausgabe zu verwenden.
Wie oben erwähnt, können Oberfläche und Puffer zum Eingeben und Ausgeben von Mediencodec-Daten verwendet werden. Es gibt jedoch Unterschiede in der Versandart, je nachdem, was Sie verwenden.
Wenn Sie Buffer verwenden, müssen Sie die Daten manuell an MediaCodec übergeben. Die Oberfläche wird automatisch übergeben, wenn der Inhalt aktualisiert wird.
Sie müssen die Daten manuell abrufen, wenn Sie Buffer verwenden. Der Inhalt von Surface wird automatisch aktualisiert.
Diese Software verwendet Surface für die Eingabe und Buffer für die Ausgabe.
** Klicken Sie hier für Layout-XML (https://github.com/SIY1121/ScreenCastSample/blob/master/app/src/main/res/layout/activity_main.xml) **
Der Vorgang beginnt mit dem Klicken auf die Schaltfläche Start.
Ich habe alles in MainActivity.java zusammengestellt, daher habe ich es nicht in großem Umfang implementiert. Bitte beachten Sie auch, dass einige Teile nicht auf Fehler überprüft werden. ** Der gesamte Code ist hier **
Das Folgende ist ein Auszug aus dem Code. Bitte beziehen Sie sich auf den gesamten Code.
Code in den Zeilen 130-155
MainActivity.java
button_start.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
switch (states) {
case Stop:
//Zeigen Sie einen Dialog zur Bestätigung der Erfassung an
manager = (MediaProjectionManager) getSystemService(Context.MEDIA_PROJECTION_SERVICE);
startActivityForResult(manager.createScreenCaptureIntent(), REQUEST_CODE);
break;
case Waiting:
//Standby abbrechen
Disconnect();
break;
case Running:
//Trennen
Disconnect();
break;
}
}
});
Da diese Schaltfläche auch zum Stoppen verwendet wird, wird der Prozess je nach Status verzweigt. Die Verarbeitung beginnt bei Stop. Ich habe ** MediaProjectionManager ** erworben und zeige einen Dialog zur Bestätigung der Erfassung für den Benutzer an.
Dies ist der Code in den Zeilen 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;
}
//Wenn der Benutzer die Bildschirmaufnahme genehmigt
//Holen Sie sich Medienprojektion
mediaProjection = manager.getMediaProjection(resultCode, intent);
//Bestimmen Sie die Größe der virtuellen Anzeige
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//Ich Rahmen ist fest
);
SetupVirtualDisplay(WIDTH, HEIGHT, DENSITY);
StartServer();
} catch (Exception ex) {//Ein Fehler beim Erstellen eines Encoders
ex.printStackTrace();
Toast.makeText(this, ex.getMessage(), Toast.LENGTH_LONG).show();
}
}
Wenn der Benutzer auf den in 1. angezeigten Dialog tippt, wird ** onActivityResult ** generiert. Wenn erlaubt, holen Sie sich ** MediaProjection ** mit ** getMediaProjection **. Holen Sie sich dann die Bildschirmgröße und bereiten Sie den Encoder und die virtuelle Anzeige vor.
Dies ist der Code in den Zeilen 218-274.
MainActivity.java
//Vorbereitung des Encoders
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);
//Legen Sie die Formateigenschaften fest
//Wenn Sie die Mindesteigenschaften nicht festlegen, führt die Konfiguration zu einem Fehler.
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);
//Holen Sie sich den Encoder
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);
//Senden Sie verschlüsselte Daten
Send(array);
//Freier Puffer
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));
}
});
//Encoder einstellen
codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
//Holen Sie sich die Oberfläche, mit der der Rahmen an den Encoder übergeben wird
//Muss zwischen configure und start aufgerufen werden
inputSurface = codec.createInputSurface();
}
Erstellen Sie zunächst das ** Medienformat **. Stellen Sie dann die für die Codierung erforderlichen Parameter ein. Verwenden Sie dann ** createEncoderByType **, um ** MediaCodec ** zu erstellen. Führen Sie dann ** configure ** aus, um ** Media Format ** festzulegen.
Rufen Sie abschließend ** createInputSurface ** auf, um die Oberfläche für die Eingabe abzurufen. Wenn Sie ein Bild auf diese Oberfläche schreiben, wird dessen Inhalt automatisch codiert.
Außerdem setze ich hier einen Rückruf, aber ich verwende Nur ** onOutputBufferAvailable **, das aufgerufen wird, wenn die codierten Daten verfügbar werden. Erfasst die codierten Daten als Byte-Array und sendet sie an die PC-Seite.
Dies ist der Code in den Zeilen 208-216.
MainActivity.java
//Einrichtung der virtuellen Anzeige
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);//Verwenden Sie für die Schreibfläche die vom Encoder erhaltene
}
Ich erstelle eine virtuelle Anzeige. ** Das Wichtigste dabei ist, die vom Encoder erhaltene Eingangsfläche auf die Schreibfläche einzustellen. ** ** ** Auf diese Weise wird der gespiegelte Bildschirm direkt auf die Eingabefläche des Encoders geschrieben. Der Bildschirm wird ohne besondere Aktion codiert. Der Fluss ist unten gezeigt.
Dies ist der Code in den Zeilen 312 bis 322.
MainAcitvity
//Starten Sie den Standby-Modus und senden Sie Threads
private void StartServer() {
senderThread = new HandlerThread("senderThread");
senderThread.start();
senderHandler = new Handler(senderThread.getLooper());
serverThread = new Thread(this);
serverThread.start();
setState(States.Waiting);
}
Wir starten einen Thread zum Senden und einen Thread zum Abhören. Da der Standby-Thread Runnable in Activity implementiert, führt er dort die Verarbeitung in run () durch. Der sendende Thread verwendet HandlerThread, damit er in die Warteschlange gestellt werden kann.
Dies ist der Code in den Zeilen 324 bis 346.
MainActivity.java
//Server-Thread
//Akzeptiert die Verbindung nur einmal
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();//Warten Sie bis die Verbindung hergestellt ist
inputStream = clientSocket.getInputStream();
outputStream = clientSocket.getOutputStream();
//Die Codierung muss gestartet werden, wenn der Client verbunden ist
codec.start();
setState(States.Running);
} catch (Exception e) {
e.printStackTrace();
}
}
Starten Sie den Server-Socket und warten Sie. Dieses Mal ist es nicht erforderlich, auf mehrere PCs zu verteilen, daher akzeptieren wir Verbindungen nur einmal.
Nach dem Anschließen startet der Encoder. Andernfalls könnte es nicht auf der PC-Seite abgespielt werden. Der erste Frame nach dem Start der Codierung ist der I-Frame, der für die zukünftige Decodierung wesentlich ist. Wenn Sie den I-Frame nicht zuerst erhalten, können Sie ihn nicht auf der PC-Seite abspielen. Ich denke, das ist der Grund, warum es nicht gespielt werden kann. (Bitte darauf hinweisen, wenn es anders ist)
Ich rahme? ?? Diejenigen, die sagen Was ist ein Keyframe? Unterschied zwischen I-Frame, P-Frame und B-Frame [GOP] Bitte sehen Sie.
Code in den Zeilen 348-366
MainActivity.java
//Daten senden
//Ändern Sie nicht die Reihenfolge
//Zur Warteschlange hinzufügen
private void Send(final byte[] array) {
senderHandler.post(new Runnable() {
@Override
public void run() {
try {
outputStream.write(array);
} catch (IOException ex) {
//Wenn es nicht gesendet werden kann, wird es als getrennt betrachtet.
ex.printStackTrace();
Disconnect();
}
}
});
}
Es wird im Rückrufsatz im Encoder von 3 aufgerufen. Der Rückruf wird auf dem Hauptthread ausgeführt, ebenso wie diese aufgerufene Methode. Aufgrund der Einschränkung, dass die netzwerkbezogene Verarbeitung nicht im Hauptthread ausgeführt werden soll Ich versuche, den Übertragungsprozess im Übertragungsthread durchzuführen.
Abgesehen davon kann der auf der PC-Seite angezeigte Bildschirm gestört sein, wenn Sie Folgendes schreiben.
MainActivity.java
private void Send(final byte[] array) {
new Thread(new Runnable() {
@Override
public void run() {
try {
outputStream.write(array);
} catch (IOException ex) {
//Wenn es nicht gesendet werden kann, wird es als getrennt betrachtet.
ex.printStackTrace();
Disconnect();
}
}
}).start();
}
Erstens ist es zum Zeitpunkt des ständigen Erstellens von Threads kein guter Code. Dies garantiert nicht die Reihenfolge der zu sendenden Frames. Wie bereits im I-Frame-Kommentar erwähnt, Weil der komprimierte Frame nur den Unterschied zwischen dem vorherigen und dem nächsten Frame darstellt Wenn der Kontext gestört ist, wird er nicht korrekt dekodiert.
Dies ist der Code in den Zeilen 368-387.
MainActivity.java
//Schneidvorgang
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);
}
Die bisher verwendeten Objekte werden gestoppt und freigegeben. Dadurch kehren Sie in den Stoppzustand zurück. Wenn Sie die Taste erneut drücken, beginnt der Vorgang von vorne und Sie können erneut eine Verbindung herstellen.
Insbesondere kann dies durch Verwendung eines ADB-Servers wie eines Proxyservers realisiert werden. Mit adb spielen Ich habe hier darauf hingewiesen.
adb forward tcp:xxxx tcp:yyyy
Geben Sie in xxxx die auf der PC-Seite verwendete Portnummer und in yyyy die auf dem Terminal verwendete Portnummer an. Diesmal
adb forward tcp:8080 tcp:8080
Ich denke es ist okay. Wenn Sie nun eine Verbindung zum 8080-Port von localhost (127.0.0.1) auf der PC-Seite herstellen, wird die Verbindung zum 8080 auf der Terminalseite ** über USB hergestellt.
Abgesehen davon, warum wurde 127.0.0.1 der IP von localhost zugewiesen? Ich war neugierig darauf, und als ich es nachgeschlagen habe, scheint es einen historischen Hintergrund für IPv4 zu geben. Warum ist 127.0.0.1 ein lokaler Host?
Vielen Dank, dass Sie diesen langen Artikel bisher gelesen haben. Zum Schluss möchte ich den Bildschirm auf dem PC anzeigen und fertig stellen. Da das am Anfang des Artikels eingeführte ffplay verwendet wird, laden Sie es bitte herunter, wenn Sie es nicht haben. Download FFmpeg Entpacken Sie nach dem Herunterladen die Datei und Sie finden den Ordner bin, der das Hauptgerät enthält. Wie bei FFmpeg wird ffplay durch Angabe von Parametern über die CUI gestartet.
ffplay -framerate 60 -analyzeduration 100 -i tcp://127.0.0.1:8080
Der Bildschirm auf der Android-Seite wird jetzt auf Ihrem PC angezeigt. (Wenn es nicht angezeigt wird, senken Sie bitte die Statusleiste oder kehren Sie nach Hause zurück, um den Bildschirm zu aktualisieren.)
Sie können mit Esc beenden.
-framerate 60 gibt einfach die Bildrate an. Muss mit den Einstellungen der Cast-App übereinstimmen.
-analyzeduration 100 Begrenzt die Zeitspanne, in der ffplay empfangene Frames analysiert. (Diesmal 100 ms) ffplay analysiert und zeigt an, nachdem sich eine bestimmte Anzahl von Frames angesammelt hat. Wenn diese Option nicht angegeben ist, wird sie mit einer Verzögerung angezeigt.
-i tcp: //127.0.0.1:8080 Die Adresse, an der der Stream empfangen werden soll. Wenn Sie es über WLAN versuchen möchten, geben Sie bitte die IP des Terminals an. Wenn Sie hier den Dateipfad angeben, können Sie das Video auch normal abspielen.
Ich habe ein persönliches Problem. Wenn Sie Informationen haben, lassen Sie es mich bitte wissen. ** Unter Android 8.0 erfolgt das Warten auf die Verbindung des Server-Sockets in einem separaten Thread
MainActivity.java
clientSocket = listener.accept();
Die Benutzeroberfläche ist blockiert mit. Die physischen Tasten funktionieren auch überhaupt nicht mehr. Wenn Sie keine Verbindung herstellen und die Blockierung aufheben, wird die Systembenutzeroberfläche nach einer Weile neu gestartet. Sie können es mit einem Emulator reproduzieren, versuchen Sie es also bitte.
Haben Sie in 8.0 irgendwelche Spezifikationen geändert ...? Es funktioniert gut vor 7.1.
Es reicht immer noch nicht aus, Vysor zu ersetzen, aber ich war überrascht, wie einfach es war, die Spiegelung zu implementieren. Es gibt immer noch nicht genügend Funktionen wie Touch-Verarbeitung in Echtzeit, aber ich würde es gerne in Zukunft machen.
Außerdem möchte ich eine Funktion erstellen, die das Terminal automatisch mit einem Skript bedienen kann. Integrieren Sie in diesem Zusammenhang die Skriptfunktion und den Editor in die C # -App, die unter Windows ausgeführt wird. Versuchen Sie, der C # -App eine Skriptfunktion hinzuzufügen Wir haben auch einen Artikel mit dem Titel veröffentlicht. Schauen Sie also bitte vorbei, wenn Sie interessiert sind.
Dann danke, dass du bis zum Ende zugesehen hast.
Nächster Erstellen einer Software, die den Android-Bildschirm auf eine PC 2 Real-Time-Touch-Edition spiegelt
Recommended Posts