Rust Trip 系列
Coding

241003编程日记

以前对Blazor开发的一些抱怨QAQ,勿放心上

Hako Chest
更新于
7324 字
14 min read
241003编程日记

这个也是之前自己一时兴起写的,很多地方都是纯粹情绪发泄和主观臆断的暴论,不能保证正确性

暂时先放在这了,以后有时间再调整里面有误的地方

哈基めで

以后再也不基米你了

感到口渴,但是现在也没水喝了啊

缺水有时是会造成比较失常的状态,不过也无所谓了


虽然今日写代码的时间也只有晚上八点钟到一点钟的这五个小时左右,但是就其内容的丰富度而言,我觉得还是可以写出一篇尚可称之为完整的博客的

动机什么的已经竟然忘却,大概是为了未完成的事情吧

但现在想来,于国庆假期确是不合时宜的——并且是十分不合时宜地令人感到厌烦

拉下了台灯的开关,看见了天青色的键盘旋钮,方感到一丝寒凉

内容之类的简单安排一下就好:大概写到两点钟写完也还算能保证睡眠,写不完以后有心情再补了

  • 相机推流proto的诸方案
    • 原比特流传输-前端解析
    • 后端转base64-前端直接显示
  • 后端于Tonic配置camera.rs
    • ClientNoStream模式
    • 相较于前端简直就是天堂
  • 前端于Blazor WASM项目引入gRPC
    • 下包
    • 于容器注册服务
    • 又一个小坑
  • 相机推流在Blazor的三个大坑
    • Typecript-Interop-Canvas 模式
    • ImageSharp 模式
    • Base64喂饭 模式
  • 结语

相机推流proto的诸方案

仅仅对StartStream这个ServerStream的rpc进行分析,两种方案的区别就在于StreamResponse里面的image_data是bytes还是string了

service Camera {
    rpc StartStream(StartStreamRequest) returns (stream StreamResponse);
}

原比特流传输-前端解析

message StreamResponse {
    int32 status = 1;
    bytes image_data = 2;
}

这里的坑,主要在像素格式上,我手上的这个黑白相机默认是Mono8灰度格式,但是浏览器显示的base64以及blob都是RGBA的,所以,必须要手动给他Mono8转成RGBA之后才能正常显示

这方面的后端写好之后,Vue前端很快就搭起来了,推流十分流畅,帧率也稳定20fps以上,肉眼可见的延迟在100ms以内,写完这个大概也就九点多不到十点

这个时候我还以为今晚十一点左右就能把显微镜推流给推上呢…但是CSharp的生态用来写Web前端,其实体感比Rust的Web前端还要吃屎一百倍,并且吃的不明不白的,塑料纸包了四五层的大泄

这里贴一下最后的Vue实现,简单做了个Mono8->RGBA,就直接显示了

<script setup lang="ts">
import { GrpcWebFetchTransport } from "@protobuf-ts/grpcweb-transport";
import { CameraClient } from "./grpc/camera.client.ts";
import { ref, watch, onUnmounted } from "vue";

const rawImage = ref<Uint8Array | null>(null);
const image = ref<string | null>(null);
const canvasRef = ref<HTMLCanvasElement | null>(null);
const transport = new GrpcWebFetchTransport({ baseUrl: "http://[::1]:50051" });
const cameraClient = new CameraClient(transport);

const handleCamera = async () => {
  const stream = cameraClient.startStream({ cameraId: "1" });
  for await (const imageData of stream.responses) {
    console.log("Camera update:", imageData);
    rawImage.value = imageData.imageData; // 确保是 Uint8Array
  }
};

watch(rawImage, (newRawImage) => {
  if (newRawImage) {
    console.log("Raw Image Data Length:", newRawImage.length);
    convertMono8ToRGBA(newRawImage, 640, 480); // 转换为 RGBA 并渲染到 Canvas
  }
});

const convertMono8ToRGBA = (
  mono8Data: Uint8Array,
  width: number,
  height: number,
) => {
  const canvas = canvasRef.value;
  if (!canvas) return;

  const context = canvas.getContext("2d");
  if (!context) return;

  const imageData = context.createImageData(width, height);
  for (let i = 0; i < mono8Data.length; i++) {
    const value = mono8Data[i]; // 获取 Mono8 的灰度值
    const index = i * 4; // RGBA 每个像素占 4 个字节
    imageData.data[index] = value; // R
    imageData.data[index + 1] = value; // G
    imageData.data[index + 2] = value; // B
    imageData.data[index + 3] = 255; // A (不透明)
  }

  context.putImageData(imageData, 0, 0); // 将图像数据绘制到 Canvas
};

onUnmounted(() => {
  image.value = null;
});
</script>

<template>
   
  <div id="app">
        <button @click="handleCamera">Start Camera</button>    
    <canvas ref="canvasRef" width="640" height="480"></canvas>
    <!-- 使用 Canvas 渲染图像 -->
     
  </div>
</template>

非常顺滑,用Canvas逐个像素绘制出来,HTML5原生高性能

但是这个方案在Blazor方面碰壁了。受限于Blazor的低能生态

后端转base64-前端直接显示

message StreamResponse {
    int32 status = 1;
    string image_data = 2;
}

这个方案是仅仅是用来配合一些小脑不健全的前端项目而做的,例如Blazor

和方案一的区别也就是,把Mono8-RGBA甚至RGBA-base64的过程都在Rust后端完成,之后直接发Base64数据给前端,前端拿到直接显示即可

这种方案的弊端是很明显的,Mono8-RGBA之后,数据大小会膨胀四倍所有,随后经过base64加密,一张图的数据量相较于比特流原图传输就膨胀了五六倍了,在4K相机上我想都不敢想

另一个小一点的弊端是转码放到后端可能会阻塞推流之类的,但是实际上,Rust的ArcMutex、Iter以及base64-simd加速三者的性能都经受住了考验,图传仍然稳健

但是不知道是不是Blazor显示不了这么大的base64,最后还是没能显示出来。

我服了

这里就简单附一下Rust的处理代码了


// Mono8 -> RGBA
let mut rgba_data = Vec::with_capacity(width * height * 4);

for &gray in data.iter() {
	rgba_data.push(gray); // R
	rgba_data.push(gray); // G
	rgba_data.push(gray); // B
	rgba_data.push(255);   // A
}

let mut image_data = IMAGE_DATA.lock().unwrap();
*image_data = rgba_data;

// Base64 encoding
let base64 = base64_simd::STANDARD;
let image_base64 = base64.encode_to_string(&image_data);

后端于Tonic配置camera.rs

快到两点钟了,这方面的内容也不是今天编程用到的,就不写了,有时间补吧

但是,Rust写Grpc后端的体感非常的好,并且性能也非常的给力,起码今晚调试的时间大部分也都是后端在等前端启动

ClientNoStream模式

有时间补吧

相较于前端简直就是天堂

有时间补吧

前端于Blazor WASM项目引入gRPC

NuGet的包管理下包,在Rider里面用起来还算舒服,很硬

下包

要用到的也就Grpc.Net.ClientGrpc.Net.Client.WebGoogle.Protobuf

去NuGet搜一下直接下就行了

于容器注册服务

这里的坑在于是否报错取决于你有没有在App层注册服务,而具体逻辑取决于你在Client层的注册

也是整上声明和实现了,但是我寻思这俩Program.cs里面写的是一个东西啊

给你挂上了,这样注册就行,App和Client两边都放

using GrpcCamera;
using Grpc.Net.Client;
using Grpc.Net.Client.Web;

builder.Services.AddSingleton(services =>
{
    var httpClient = new HttpClient(new GrpcWebHandler(GrpcWebMode.GrpcWeb, new HttpClientHandler()));
    var baseUri = "http://[::1]:50051/";
// 这里只能用http://localhost:50051/
    // 不能用https,也不能用ipv6
    var channel = GrpcChannel.ForAddress(baseUri, new GrpcChannelOptions { HttpClient = httpClient });
    return new Camera.CameraClient(channel); });

又一个小坑

如上面的注释所示,只能用http://localhost:50051/

毕竟Blazor原生不支持gRPC,只能走gRPC-web,但是确实也是诡异,我写成IPV6就直接给我CORS了

相机推流在Blazor的三个大坑

Typescript-Interop-Canvas 模式

要用到的包是Microsoft.Typescript.MSBuild

在Client里面新建一个Types文件夹放ts文件,然后yarn add typescript,再新建一个tsconfig.json,后配置一个tsc的scripts,然后直接yarn build,生成的文件放到App的wwwroot/js里面,也就相当于是public了,直接在Pages的具体页面里面用script引入即可使用

TS代码就贴在这了,实际上这个export是可以去掉的

export function drawPixels(
  id: number,
  x: number,
  y: number,
  xw: number,
  yw: number,
  pixels: Uint8Array,
) {
  const canvas = document.getElementById("canvasRef") as HTMLCanvasElement;
  if (!canvas) return;

  const context = canvas.getContext("2d");
  if (!context) return;

  const imageData = context.createImageData(xw, yw);

  for (let i = 0; i < pixels.length; i++) {
    imageData.data[i] = pixels[i];
  }
  context.putImageData(imageData, x, y);
}

在Razor里面这样用就行

await JSRuntime.InvokeAsync<string>("drawPixels", "canvasRef", 0, 0, 640, 480, rgbaData);
// 结论是:性能极差,延迟卡顿丢包,无解,和Vue前端没有可比性

但是非常令人失望。结论是:性能极差,延迟卡顿丢包,无解,和Vue前端没有可比性

采取这个方案的原因是,Blazor生态里面的的Canvas库,无论是Blazor.Extensions.Canvas还是Exubo.Blazor.Canvas,都没有实现像素绘图功能,也就是putImageData,关于image都只实现了一个drawImageAsync,这个函数b用没有,只能挂一个HTML元素到Canvas画布上。。。

导致只能通过TS手动封装一个也就是putImageData之后用CSharp执行,但是很显然,WASM可能并不太适合这种类型的数据转来转去,推流一开直接慢爆了。

.NET里面的死库太多了

有被Support for putImageData这个issue的想法激发到,但是他这个实现有点唐氏,你都用TS了怎么还要用TS去调一个CSharp封装的Canvas,原生Canvas接口放着不用,还要去改他内库的protected变量,也是神人了

ImageSharp 模式

也就是换了个库,因为GPT说的那个BitMap之类的方法根本没找到

简单贴一下吧,这个方法实际跑出来,比TS的还要卡,我觉得就是他优化不行,转png花时间太久了,但是后面还是起码能看到,图片转出来是对的

var image = new Image<Rgba32>(width, height);

for (int i = 0; i < mono8Data.Length; i++)
{
    var value = mono8Data[i];
    image[i % width, i / width] = new Rgba32(value, value, value, 255); // RGBA
}

using (var ms = new MemoryStream())
{
    await image.SaveAsPngAsync(ms);
    var base64String = Convert.ToBase64String(ms.ToArray());
    return $"data:image/png;base64,{base64String}";
}

Base64喂饭 模式

这个前面应该说过了,我就只简单贴一下Blazor这边的代码了

这个就直接图都没有了!但是我也没时间继续调试了!投降了哥!

while (await call.ResponseStream.MoveNext(cts.Token))
{
    var imageData = call.ResponseStream.Current.ImageData;
    imageSrc = $"data:image/png;base64,{imageData}";
    // 这种就更是需要在后端就转成RGBA然后再转base64了,我草啊
    // 并且转出来也不对,休息了,简单写一个博客吧
    await Task.Delay(100, cts.Token);
}

结语

很难想象世界上居然很有一群人在用Blazor做前端开发

以及很难理解某些人对C#的莫名的执念,就我来说写前端的体感并不太好,可能是习惯了Web的那套模式了,不过Blazor的生态确实也是烂,避雷了

等写完这个项目,就些许能休息了

晚安

后记

后面想了一下,WASM的性能也不至于差到这个田地

但是印象中依稀记得WASM并不适合介入过多的DOM操作

所以就按照惰性求值的思路,只去初始化一个CanvasElement,然后后续进行操作

但是发现还是卡

之后,认为是For的问题,加了异步任务之后就好多了,可以达到类似于Vue的推流效果

但是推流一段时间后有时回莫名变卡或者卡住,需要重新点击推流,然后就流畅如初了

以后再解决吧

迁移到了Tauri V2,API变了,然后拖拽的抽象实现也导致了更抽象的效果

这个也以后再解决吧

就酱

#Rust #Blazor #gRPC #Vue #WebRTC #Camera