Wie man Flutter SDK auf Set-Top-Boxen für Android TV Apps portiert. Ausführung und Entwicklung
Vor Kurzem haben wir das Flutter-Framework erfolgreich auf Set-Top-Boxen mit der Open-Source-Softwareplattform RDK portiert. In diesem Artikel werden wir über die aufgetretenen Schwierigkeiten und Nuancen sprechen.
Inhalt
Da das RDK-Softwarepaket oder Referenzdesign-Kit inzwischen intensiv zur Entwicklung von OTT-Anwendungen, Sprachsteuerung für Set-Top-Boxen und anderen fortschrittlichen Funktionen für Video-on-Demand (VoD) eingesetzt wird, wollten wir herausfinden, ob Flutter auf einer Set-Top-Box funktionieren könnte. Es stellte sich heraus, dass dies möglich ist, aber wie so oft gibt es Nuancen. Als Nächstes beschreiben wir Schritt für Schritt den Prozess der Portierung und Ausführung von Flutter auf den Embedded-Linux-Plattformen und sehen, wie sich dieses Open-Source-SDK von Google auf Hardware mit begrenzten Ressourcen und ARM-Prozessoren verhält.
Bevor wir jedoch direkt zu Flutter und seinen Vorteilen kommen, lassen Sie uns ein paar Worte über die ursprüngliche Lösung sagen, die auf unseren Set-Top-Boxen verwendet wurde. Auf der Platine lief die „EFL-Bibliothekssuite” + das “Wayland-Protokoll“, und das Zeichnen von Primitiven wurde von node.js aus auf der Grundlage eines nativen Plugin-Moduls implementiert. Diese Lösung war in Bezug auf die Leistung beim Frame-Rendering recht gut, aber EFL selbst ist nicht das neueste Rendering-Framework. Und zur Laufzeit schienen node.js und seine riesige Ereignisschleife nicht mehr die vielversprechendste Idee zu sein. Flutter könnte es uns inzwischen ermöglichen, ein produktiveres Rendering-Paket zu verwenden.
Für diejenigen, die nicht auf dem Laufenden sind: Google hat die erste Version dieses Open-Source-SDK vor sechs Jahren eingeführt. Zu dieser Zeit war Flutter nur für das Android-Betriebssystem geeignet. Jetzt können Sie Anwendungen für das Web, iOS, Linux und sogar Google Fuchsia schreiben. :-) Eine Arbeitssprache für die Entwicklung von Apps auf Flutter ist Dart, sie wurde als Alternative zu JavaScript eingeführt.
Die Frage, die sich uns stellte, war: Wird der Wechsel zu Flutter zu einer Leistungssteigerung führen? Schließlich wird ein völlig anderer Ansatz verwendet, obwohl es dasselbe Grafik-Subsystem Wayland + OpenGL gibt. Wir haben uns auch gefragt, wie es mit der Unterstützung für Prozessoren mit Neon-Anweisungen aussieht. Es gab auch andere Fragen, wie z. B. die Nuancen der Portierung unserer Benutzeroberfläche auf Dart oder die Tatsache, dass sich die Linux-Unterstützung in der Alpha-Beta-Phase befindet.
Erstellung der Flutter-Engine für ARM-basierte Set-Top-Boxen
Also, fangen wir an. Zunächst muss Futter auf einer fremden Plattform mit Wayland + OpenGL ES ausgeführt werden. Das Rendering von Flutter basiert auf der Skia-Bibliothek, die OpenGL ES perfekt unterstützt, sodass theoretisch alles gut aussah.
Beim Erstellen von Flutter für unsere Zielgeräte (drei Set-Top-Boxen mit RDK) traten zu unserer Überraschung nur bei einem STB-Gerät Probleme auf. Wir beschlossen, uns nicht damit herumzuschlagen, da die alte Intel x86-Architektur nicht unsere Priorität war. Es ist besser, sich auf die beiden verbleibenden ARM-Plattformen zu konzentrieren.
Hier sind die Optionen, die wir zum Erstellen der Flutter Engine verwendet haben:
./flutter/tools/gn \
--embedder-for-target \
--target-os linux \
--linux-cpu arm \
--target-sysroot DEVICE_SYSROOT
--disable-desktop-embeddings \
--arm-float-abi hard
--target-toolchain /usr
--target-triple arm-linux-gnueabihf
--runtime-mode debug
ninja -C out/linux_debug_unopt_arm
Die meisten Optionen sind klar: Für 32-Bit-ARM-Prozessor und Linux erstellen und dabei alles Unnötige mit --embedder-for-target --disable-desktop-embeddings
deaktivieren.
Für diesen Build muss das System über Clang Version 9 oder höher verfügen, d. h. es handelt sich um eine Standard-Flutter-Build-Engine, und das gcc-Cross-Compilation-Toolkit funktioniert nicht. Das Wichtigste ist die Angabe der korrekten target-sysroot des RDK-basierten Geräts.
Ehrlich gesagt waren wir überrascht, dass es während des Erstellungsprozesses überhaupt keine Nuancen gab. Das Ergebnis ist die begehrte flutter_engine.so-Bibliothek und ein Header mit den erforderlichen Funktionen für den Embedder.
Wir können jetzt ein flutter/dart-Zielprojekt mit unserer Bibliothek/Engine erstellen. Das ist ganz einfach:
flutter --local-engine-src-path PATH_TO_BUILDED_ENGINE_src
--local-engine=host_debug_unopt build bundle
Achtung! Das Projekt muss nicht auf dem Gerät mit der integrierten Bibliothek, sondern auf dem Hostgerät, d. h. x86_64, erstellt werden! Führen Sie dazu die GN- und Ninja-Builds erneut nur auf x86_64 aus! Dies ist im host_debug_unopt-Parameter angegeben. PATH_TO_BUILDED_ENGINE_src ist der Pfad, in dem sich engine/src/out befindet.
Embedder ist normalerweise für die Ausführung der Flutter Engine auf dem System verantwortlich. Es konfiguriert Flutter für das Zielsystem und übergibt die wichtigsten Rendering-Kontexte an die Skia-Bibliothek und den Dart-Handler. Vor nicht allzu langer Zeit wurde Flutter um linux-embedder und insbesondere GTK-embedder erweitert, sodass Sie es sofort verwenden können. Auf unserer Plattform war dies zum Zeitpunkt der Portierung keine Option, sodass wir etwas Unabhängiges von GTK benötigten.
Betrachten wir einige Besonderheiten der Implementierung, die bei einem benutzerdefinierten Embedder berücksichtigt werden mussten (alle, die nicht Nuancen, sondern den Quellcode in seiner Gesamtheit auseinandernehmen möchten, können direkt zu unserem Fork-Projekt mit Änderungen auf github.com gehen). Darüber hinaus übertraf unsere Version die GTK-Version leicht, was für unseren Kunden äußerst wichtig war. Außerdem hat sie nicht den ganzen Zoo der GTK-Bibliotheken in Mitleidenschaft gezogen.
Was braucht ein Embedder also überhaupt, um eine Flutter-Anwendung auszuführen? Es genügt, flutter_engine.so aus der Bibliothek aufzurufen
FlutterEngineRun(FLUTTER_ENGINE_VERSION, &config, &args, display /* userdata */, &engine_);
wobei die Parameter die Projekteinstellungen für FlutterProjectArgs args (das Verzeichnis mit dem erstellten Flutter-Bundle) und die Rendering-Argumente FlutterRendererConfig config sind.
Die erste Struktur gibt den Pfad des vom Flutter-Dienstprogramm erstellten Bundle-Pakets an, und die zweite verwendet OpenGL-Kontexte.
// siehe ein Anwendungsbeispiel auf github.com
Es ist ziemlich einfach, aber es reicht aus, um die Anwendung auszuführen.
Probleme und Lösungen
Lassen Sie uns nun über die Nuancen sprechen, die uns während der Portierungsphase begegnet sind. Wie könnte es ohne sie sein? Es geht nicht nur um das Erstellen von Bibliotheken, oder? :-)
1. Absturz des Embedders und Änderung der Funktionsaufrufwarteschlange
Das erste Problem, auf das wir stießen, war ein Absturz des Embedders auf unserer Zielplattform. Die Initialisierung des Gel-Kontexts in anderen Anwendungen verlief normal, FlutterRendererConfig wurde korrekt initialisiert, der Embedder startete jedoch nicht. Offensichtlich stimmt also etwas nicht damit. Es stellte sich heraus, dass eglBindAPI nicht vor eglGetDisplay aufgerufen werden kann, da der Nexus-Anzeigetreiber speziell dort initialisiert wird (unsere Plattform basiert auf einem BCM-Chip). Bei „üblichem“ Linux ist dies kein Problem, aber auf der Zielplattform stellte sich heraus, dass dies anders ist.
Die korrekte Initialisierung des Embedders sieht wie folgt aus:
egl_display_ = eglGetDisplay(display_);
if (egl_display_ == EGL_NO_DISPLAY) {
LogLastEGLError();
FL_ERROR("Could not access EGL display.");
return false;
}
if (eglInitialize(egl_display_, nullptr, nullptr) != EGL_TRUE) {
LogLastEGLError();
FL_ERROR("Could not initialize EGL display.");
return false;
}
if (eglBindAPI(EGL_OPENGL_ES_API) != EGL_TRUE) {
LogLastEGLError();
FL_ERROR("Could not bind the ES API.");
return false;
}
// github.com — das ist unsere korrekte Implementierung, d. h. die geänderte Reihenfolge der Funktionsaufrufe hat geholfen. Jetzt, da die Nuancen des Starts geklärt sind, freuen wir uns, das begehrte Demo-App-Fenster auf dem Bildschirm zu sehen :-)
2. Optimierung der Leistung
Es ist an der Zeit, die Leistung zu überprüfen. Und ehrlich gesagt hat sie uns im Debug-Modus nicht sonderlich gefallen. Etwas war schnell und etwas war langsamer als EFL+Node.js mit Frame-Verzögerungen.
Wir waren ein wenig frustriert und begannen, weiter zu graben. Das Flutter SDK verfügt über AOT, einen speziellen Maschinencode-Kompilierungsmodus, der nicht einmal ein JIT ist, sondern eine Kompilierung in nativen Code mit allen damit verbundenen Optimierungen. Dies ist mit der Flutter-Release-Version gemeint. Wir hatten diese Art von Unterstützung im Embedder nicht, also haben wir sie hinzugefügt
Es wurden bestimmte Anweisungen benötigt, die durch Argumente an FlutterEngineRun gegeben wurden
// tdie vollständige Implementierung finden Sie hier: github.com (elf.cc)
vm_snapshot_instructions_ = dlsym(fd, "_kDartVmSnapshotInstructions");
if (vm_snapshot_instructions_ == NULL) {
error_ = strerror(errno);
break;
}
vm_isolate_snapshot_instructions_ = dlsym(fd, "_kDartIsolateSnapshotInstructions");
if (vm_isolate_snapshot_instructions_ == NULL) {
error_ = strerror(errno);
break;
}
vm_snapshot_data_ = dlsym(fd, "_kDartVmSnapshotData");
if (vm_snapshot_data_ == NULL) {
error_ = strerror(errno);
break;
}
vm_isolate_snapshot_data_ = dlsym(fd, "_kDartIsolateSnapshotData");
if (vm_isolate_snapshot_data_ == NULL) {
error_ = strerror(errno);
break;
}
if (vm_snapshot_data_ == NULL ||
vm_snapshot_instructions_ == NULL ||
vm_isolate_snapshot_data_ == NULL ||
vm_isolate_snapshot_instructions_ == NULL) {
return false;
}
*vm_snapshot_data = reinterpret_cast <
const uint8_t * > (vm_snapshot_data_);
*vm_snapshot_instructions = reinterpret_cast <
const uint8_t * > (vm_snapshot_instructions_);
*vm_isolate_snapshot_data = reinterpret_cast <
const uint8_t * > (vm_isolate_snapshot_data_);
*vm_isolate_snapshot_instructions = reinterpret_cast <
const uint8_t * > (vm_isolate_snapshot_instructions_);
FlutterProjectArgs args;
// we pass on everything necessary to args
args.vm_snapshot_data = vm_snapshot_data;
args.vm_snapshot_instructions = vm_snapshot_instructions;
args.isolate_snapshot_data = vm_isolate_snapshot_data;
args.isolate_snapshot_instructions = vm_isolate_snapshot_instructions;
Jetzt, da alles vorhanden ist, müssen wir die Anwendung auf eine spezielle Weise erstellen, um ein AOT-kompiliertes Modul für die Zielplattform zu erhalten. Dies kann durch Ausführen eines Befehls aus dem Stammverzeichnis des Dart-Projekts erfolgen:
$HOST_ENGINE/dart-sdk/bin/dart \
--disable-dart-dev \
$HOST_ENGINE/gen/frontend_server.dart.snapshot \
--sdk-root $DEVICE_ENGINE}/flutter_patched_sdk/ \
--target=flutter \
-Ddart.developer.causal_async_stacks=false \
-Ddart.vm.profile=release \
-Ddart.vm.product=release \
--bytecode-options=source-positions \
--aot \
--tfa \
--packages .packages \
--output-dill build/tmp/app.dill \
--depfile build/kernel_snapshot.d \
package:lib/main.dart
$DEVICE_ENGINE/gen_snapshot \
--deterministic \
--snapshot_kind=app-aot-elf \
--elf=build/lib/libapp.so \
--no-causal-async-stacks \
--lazy-async-stacks \
build/tmp/app.dill
Lassen Sie sich nicht von der großen Anzahl an Parametern einschüchtern, die meisten davon sind Standard. Insbesondere:
-Ddart.vm.profile=release \
-Ddart.vm.product=release \
geben Sie an, dass wir keinen Profiler im Paket benötigen und dass wir die Produktversion haben.
output-dill wird benötigt, um die native libapp.so-Bibliothek zu erstellen.
Die wichtigsten Pfade für uns sind $DEVICE_ENGINE und $HOST_ENGINE – die beiden erstellten Engines für die Ziel- (ARM) bzw. Hostsysteme (x86_64). Es ist wichtig, nichts zu verwechseln und sicherzustellen, dass libapp.so die 32-Bit-ARM-Version ist:
$ file libapp.so
libapp.so: ELF 32-bit LSB shared object, ARM, EABI5 version 1 (SYSV),
dynamically linked
Wir starten es und ... voila! – Alles funktioniert!
Und es funktioniert viel schneller! Jetzt können wir von einer vergleichbaren Leistung und Rendering-Effizienz wie bei der Originalanwendung sprechen, die auf den EFL-Bibliotheken basiert. Das Rendering funktioniert in einfachen Anwendungen fast nahtlos und nahezu perfekt.
3. Eingabegeräte verbinden
Für die Zwecke dieses Artikels überspringen wir die Geschichte, wie Wayland und Embedder sich mit der Fernbedienung, der Maus und anderen Eingabegeräten angefreundet haben. Sie können sich ihre Implementierung im Quellcode des Embedders ansehen.
4. Die Schnittstelle auf Linux und Android STB und wie man die Leistung um das 2- bis 3-fache steigern kann
Lassen Sie uns noch auf einige andere Leistungsnuancen eingehen, die in der Produkt-UI-Anwendung auftreten. Wir waren sehr zufrieden mit der Authentizität der Benutzeroberfläche auf dem Zielgerät sowie auf Linux und Android. Jetzt kann Flutter eine sehr flexible Portabilität vorweisen.
//...
filter: new ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0),
//…
Allein durch die Kommentierung dieses Effekts konnten wir die Produktivität um das Zwei- bis Dreifache steigern und die Anwendung auf stabile 50–60 fps bringen. Dies ist ein gutes Beispiel dafür, wie ein bestimmter Effekt die Leistung einer gesamten Flutter-Anwendung beeinträchtigen kann, obwohl er nur in einem Panel auftrat, das die meiste Zeit über ausgeblendet war.
Das Ergebnis
Als Ergebnis erhielten wir nicht nur eine funktionierende Produktanwendung, sondern eine funktionierende Anwendung mit einer hochwertigen Bildrate bei Flutter auf unserer Ziel-STB. Die Fork und unsere Version des Embedders für RDK und andere Wayland-basierte Plattformen finden Sie hier: github.com (flutter_wayland)
Wir hoffen, dass die Erfahrung unseres Teams bei der Entwicklung und Portierung von Software für Set-Top-Boxen und Smart-TVs für Ihre Projekte nützlich sein wird und als Ausgangspunkt für die Portierung von Flutter auf andere Geräte dienen kann.
Danksagung. Ich möchte Alexey Kasyanchuk, unserem Software-Ingenieur, für seinen wertvollen Beitrag zu diesem Artikel danken. Ihre Fragen und Kommentare sind willkommen. Wir freuen uns, unsere Erfahrungen bei der Entwicklung maßgeschneiderter Smart-TV-Apps mit Ihnen zu teilen.
Unsere Projekte