Blazor WASM QRCode Reader

Eigentlich war mein Ziel einen Prototyp zu schreiben. Einen QR Code Decoder im Browser als Web Assembly per Blazor. Das Ergebnis gleich am Anfang. Ich bin gescheitert. Die verwendete .net Library (und wohl auch alle anderen) nutzen aus Performance Gründen GDI+, das wiederum über System.Drawing abstrahiert wird. Auf der Plattform Browser scheint in der verwendeten Mono Runtime nicht zu funktionieren. Es würde mich wundern ob so was generell möglich ist.

Die Entscheidung für WASM ist gefallen, da ein Bild auf das vorhanden sein eines QR Codes untersucht werden muss. Am besten mehrmals pro Sekunde. Die Bilder sind relativ groß (min 1MB) und das wäre heavy load für den Server und die Leitung.

Also bleibt nur einen Blazor Client App bzw. WASM.

JavaScript API

Wer Blazor macht, macht auch JavaScript. Browser lassen sich nur über eine API ansprechen. Deswegen steht zuallererst die Logik in JS, die ich hier camhelper.js genannt habe und unterhalb der wwwroot Struktur ablegen muss.

Es werden Aufrufe aus .NET Code für Kamera Initialisierung und Foto speichern per Export verfügbar gemacht.

Auch der Rückruf von JS nach C# per dotnet Objekt (als Parameter) wird genutzt.

Das Kamerabild wird auf ein dynamisch erstelltes Canvas gezeichnet und dann in ein Base64 encodierten Image String exportiert. Alles Sachen die man in Blazor nicht hin bekommt.

   1:  const constraints = { audio: false, video: true };
   2:  var video;
   3:  export async function initialize(v, dotnet) {
   4:      video = v;
   5:          let stream = 
await navigator.mediaDevices.getUserMedia(constraints);
   6:          video.srcObject = stream;
   7:          video.play();
   8:  }
   9:  export function getBase64Img(video) {
  10:      let canvas = document.createElement("canvas");
  11:      let context = canvas.getContext('2d');
  12:      canvas.setAttribute('width', video.videoWidth);
  13:      canvas.setAttribute('height', video.videoHeight);
  14:      context.drawImage(video, 0, 0, video.videoWidth, video.videoHeight);
  15:      let data = canvas.toDataURL('image/png');
  16:      canvas.remove();
  17:      return data;
  18:  }
  19:   
  20:   
  21:   
  22:  function errorMsg(msg, error, dotnet) {
  23:      dotnet.invokeMethodAsync("OnWebCamLiveError", msg);
  24:  }

 

Das Blazor UI

Dann lege ich eine Datei video.razor im Pages Verzeichnis an mit rudimentären HTML UI. Um später auf JavaScript Module zugreifen zu können wird das Objekt IJsRuntime von der DI Container Liste injiziert. Per @ref kann einem HTML Objekt ein Blazor Code Objekt zugewiesen werden.

   1:  @page "/video"
   2:  @inject IJSRuntime JsRuntime
   3:  @implements IAsyncDisposable
   4:  <h3>Video</h3>
   5:  <video @ref="VideoHTMLElement">
   6:      kein Video
   7:  </video>
   8:   
   9:  <button @onclick="makefoto" class="btn">foto</button>
  10:  <img src="@bild" />

 

Die Blazor Code Logik

Zunächst werden die bindbaren Variablen im Code Block deklariert. Das relative neue IJSObjetReference Objekt erlaubt es JS Module die per Export definiert sind zu laden. Zeile 2 finalisiert den Ansatz der HTML Element Referenz zu einem Objekt.

   1:  Task<IJSObjectReference> moduleTask;
   2:  public ElementReference VideoHTMLElement { get; set; }
   3:  static string bild;

Nachdem Blazor den HTML Code gerendert hat, lässt sich die CamHelper JavaScript Logik importieren und die Initialisierung  per JavaScript Logik ausführen. Um den Rückruf aus JS nach .NET leichter zu machen, wird einen Dotnet Referenz mit übergeben.

   1:   protected override async Task OnAfterRenderAsync(bool firstRender)
   2:      {
   3:          if (firstRender)
   4:          {
   5:              moduleTask = JsRuntime.InvokeAsync<IJSObjectReference>("import",
   6:          "./js/camhelper.js").AsTask();
   7:              var module = await moduleTask;
   8:              await module.InvokeVoidAsync("initialize",
VideoHTMLElement, DotNetObjectReference.Create(this));
   9:          }
  10:      }

Ohne dem dotnet Objekt müssten die C# Rückruf Funktionen Statisch ausgeführt werden. Macht es grad nicht leichter. In jedem Fall müssen die Funktionen per Annotation extern für einen JavaScript Aufruf verfügbar gemacht werden.

   1:   
   2 [JSInvokable]
   3: public void OnWebCameraError(string error)
   4:      {

In diesem Blazor Webcam Beispiel fehlt nur noch der Auslöser. Das Foto auch machen. Also Referenz auf das JavaScript Modul besorgen und die dortige Methode aufrufen. Zu guter Letzt Blazor noch darauf aufmerksam machen, dass sich das UI verändert hat.

   1:   public async void makefoto()
   2:      {
   3:          var module = await moduleTask;
   4:          bild = await module.InvokeAsync<string>
("getBase64Img", VideoHTMLElement);
   5:          StateHasChanged();
   6:      }

Lass uns Frames sehen

Ich habe schon vielfach mit Windows Kameras oder auch eine Kinect programmiert. Das geht einigermaßen einfach und recht performant. So eine Kamera liefert schnell mal 60fps die man durch eine QR Code Lib wie ZXine durchjagen muss.

Das gleiche im Browser ist ein Abenteuer. Zunächst ist die Funktion ein Frame Event gesteuert zu erhalten wohl recht neu mit requestVideoFrameCallback. Das bedeutet meine JS Datei bekommt mehr Code

   1:  const updateCanvas = (now, metadata) => {
   2:      DotNet.invokeMethodAsync('WebCamLabWASM',
'OnFrameArrived', getBase64Img(video));
   3:      video.requestVideoFrameCallback(updateCanvas);
   4:  };

und in init muss man einmalig das ganze anstossen

video.requestVideoFrameCallback(updateCanvas);

Auf der Blazor c’ Code Logik Seite beginnt man ungefähr so. Statisch weil man kein dotnet Objekt im Parameter hat. Dazu kommt das dann auch die internen Variablen statisch ausgelegt werden müssen.

   1:  [JSInvokable]
   2:      public static async Task OnFrameArrived(string _bild)
   3:      {
   4:          FrameCount++;
   5:          bild = _bild; //QRCode todo

 

Zu allem Überfluss benötigt man dann auch noch ein Dispatching zwischen der statischen Methode und dem UI, beginnen mit einer Action (Event)

  private static Action RefreshUI;

Die Belegung des Action Objekts am besten in afterRender Methode per Lambda Expression

 RefreshUI += () => StateHasChanged();

Nun kann man das Ganze auch in der static Method OnFrameArrived aufrufen

   RefreshUI?.Invoke();

In dieser minimal Konfig komme ich mit Chrome und Edge auf rund 2fps die man in Blazor erhalten kann. Dabei läuft der Browser recht Ressourcen intensiv.

Fortsetzung folgt.

Kommentare sind geschlossen