# Agora RTE Extension v2

## 简介

Agora RTE Extension v2 是新一代插件 SDK，为 Agora Web SDK 提供 `useExtension` 模式的视频/音频插件开发能力，完全替代 v1 的 `pipe` 模式。

与 v1 不同，新框架由 SDK 统一管理媒体数据的获取、转换和输出，插件只需实现核心处理逻辑。视频插件在共享的 WebGL2 context 上直接操作 GPU texture，避免不必要的 CPU↔GPU 拷贝；音频插件通过 AudioWorklet 在实时音频线程中处理数据，需要更大帧长或 WASM 资源的算法由 worklet 侧插件自行缓冲和管理。

## 安装

```bash
npm install agora-rte-extension-ng@^1.0.0
```

## 架构概览

### 视频插件

```text
摄像头 Track → texImage2D → inputTexture (GPU)
  → ext1.applyEffect({input:A, output:B}) → swap A↔B
  → ext2.applyEffect({input:B, output:A}) → swap A↔B
  → blitFramebuffer → canvas → 输出 Track
```

框架创建共享的 WebGL2 context，将视频帧上传为 GPU texture，然后按 useExtension 顺序依次调用每个 enabled 插件的 `applyEffect`，通过 ping-pong 交换 input/output texture。最终结果 blit 到 canvas 输出。

### 音频插件

```text
麦克风 Track → AudioWorkletNode (框架)
  → 128 采样 quantum → AudioWorklet 侧插件 processAudioFrame(input)
  → 输出 Track
```

框架拥有 AudioWorklet 管线。主线程上的 `AudioExtension` 只承载控制面状态和生命周期；实时处理类在 AudioWorklet 模块中通过 `registerAudioWorkletExtension()` 注册，框架按 `registrationName` 实例化后，在 `process()` 回调中同步调用其 `processAudioFrame()`。

## 快速开始

### 视频插件

```typescript
import { VideoExtension } from "agora-rte-extension-ng";
import type { VideoExtensionContext, Frame } from "agora-rte-extension-ng";

class MyVideoExtension extends VideoExtension {
  public name = "MyVideoExtension";

  public init(context: VideoExtensionContext): void {
    super.init(context);
    // context.gl — 共享的 WebGL2RenderingContext
    // context.canvas — 共享的 HTMLCanvasElement
    // 在此初始化 WASM、shader 等资源
  }

  public destroy(): void {
    // 释放资源
    super.destroy();
  }

  public async applyEffect(frame: Frame): Promise<void> {
    if (frame.kind === "cpu") {
      // CPU 模式：frame.videoFrame 是 VideoFrame
      return;
    }
    // GPU 模式：
    // frame.input  — 当前帧输入纹理（只读）
    // frame.output — 必须将处理结果写入此纹理
    // frame.width / height — 帧尺寸

    // 示例：直接拷贝（直通）
    this.blitTexture(frame.input, frame.output, frame.width, frame.height);
  }
}
```

使用：

```typescript
const ext = localVideoTrack.useExtension(MyVideoExtension);
await ext.disable();  // 框架跳过该插件处理，开销很低
await ext.enable();   // 重新启用
ext.destroy();        // 移除并销毁
```

### 音频插件

```typescript
// my-audio-extension.ts（主线程控制面）
import { AudioExtension } from "agora-rte-extension-ng";
import type { AudioExtensionContext } from "agora-rte-extension-ng";

export class MyAudioExtension extends AudioExtension {
  public static extensionDefinition = {
    moduleURL: new URL("./my-audio-worklet.js", import.meta.url).href,
    registrationName: "my-audio-extension",
  };

  public name = "MyAudioExtension";

  public init(context: AudioExtensionContext): void {
    super.init(context);
    // context.audioContext — AudioContext
    // context.sampleRate — 采样率
  }
}
```

```typescript
// my-audio-worklet.ts（AudioWorklet 侧实时处理）
import { registerAudioWorkletExtension } from "agora-rte-extension-ng";
import type {
  AudioWorkletExtensionContext,
  IAudioWorkletExtension,
} from "agora-rte-extension-ng";

class MyAudioWorkletExtension implements IAudioWorkletExtension {
  public init(context: AudioWorkletExtensionContext): void {
    // context.sampleRate — 采样率
    // context.extensionOptions — useExtension() 传入的初始配置
  }

  /**
   * 框架在 AudioWorklet 的 process() 中调用此方法。
   * 传入 128 采样的 Float32Array，必须同步返回处理后的 128 采样。
   * 如果算法帧长大于 128，在 worklet 侧维护缓冲，并同步返回本 quantum 的输出。
   */
  public processAudioFrame(input: Float32Array): Float32Array {
    // 示例：直通
    return input;
  }
}

registerAudioWorkletExtension("my-audio-extension", MyAudioWorkletExtension);
```

## API 参考

### VideoExtension（视频插件基类）

| 属性/方法 | 说明 |
|-----------|------|
| `name: string` | 插件名称（必须实现） |
| `enabled: boolean` | 当前启用状态（只读） |
| `mode: VideoExtensionMode` | 处理模式，默认 `TEXTURE` |
| `supportsCPUMode: boolean` | 是否支持 CPU 回退模式，默认 `false` |
| `init(context)` | 初始化，子类必须调用 `super.init(context)` |
| `destroy(options?)` | 销毁，子类必须调用 `super.destroy(options)`。框架在回退/恢复过程中可能传入 `{ reason: "context-loss" }`。 |
| `enable()` | 启用插件 |
| `disable()` | 禁用插件（框架跳过该插件处理，开销很低） |
| `applyEffect(frame)` | TEXTURE 模式处理方法（子类覆写）。接收 `Frame`，`kind: "gpu"` 或 `kind: "cpu"` |
| `processVideoFrame(frame)` | VIDEO_FRAME 模式处理方法（子类覆写） |
| `setOptions(options)` | 声明式配置入口（唯一入站通道）。子类覆写以应用配置；框架在 `init()` 后用初始 `extensionOptions` 自动调用一次。`options` 中的图片 / ArrayBuffer 会自动跨 worker 边界 transfer。 |
| `on(event, handler)` / `off(event, handler)` | 监听插件通过 `emit(event, data)` 推送出的数据（唯一出站通道），例如逐帧 face landmark。 |

#### 受保护的 GL 工具方法

| 方法 | 说明 |
|------|------|
| `blitTexture(src, dst, w, h)` | 通过 blitFramebuffer 拷贝 texture |
| `drawTextureToFbo(tex, fbo, w, h)` | 使用直通 shader 渲染到 FBO |
| `createTexture(w, h)` | 创建 RGBA texture |
| `createFbo(texture)` | 创建绑定到 texture 的 FBO |
| `compileProgram(vert, frag)` | 编译链接 shader program |
| `pixelToGL(px, py, w, h)` | 像素坐标转 GL NDC 坐标（框架 Y 翻转约定） |

### AudioExtension（音频插件基类）

| 属性/方法 | 说明 |
|-----------|------|
| `name: string` | 插件名称（必须实现） |
| `enabled: boolean` | 当前启用状态（只读） |
| `init(context)` | 初始化主线程控制面，子类必须调用 `super.init(context)` |
| `destroy(options?)` | 销毁主线程控制面资源，子类必须调用 `super.destroy(options)`。 |
| `enable()` | 启用插件 |
| `disable()` | 禁用插件 |

### AudioWorklet 侧插件

| API | 说明 |
|-----|------|
| `AudioExtension.extensionDefinition`（静态字段） | 声明 `moduleURL` / `registrationName`，供框架加载 worklet 模块 |
| `registerAudioWorkletExtension(name, ctor)` | 在 AudioWorklet 模块中注册实时处理类 |
| `IAudioWorkletExtension.init(context)` | 初始化 worklet 侧插件，可读取 `sampleRate` 与初始 `extensionOptions` |
| `IAudioWorkletExtension.processAudioFrame(input, referenceInputs?)` | 实时音频处理方法；每次接收 128 采样，必须同步、低分配地返回 128 采样 |

### VideoExtensionMode（枚举）

| 值 | 说明 |
|----|------|
| `TEXTURE` | GPU Texture 模式（默认，推荐） |
| `VIDEO_FRAME` | VideoFrame 模式（旧插件兼容） |

### Frame（类型）

```typescript
type Frame =
  | { kind: "gpu"; input: WebGLTexture; output: WebGLTexture; width: number; height: number }
  | { kind: "cpu"; width: number; height: number; videoFrame: VideoFrame };
```

### VideoExtensionContext / AudioExtensionContext（类型）

```typescript
type VideoExtensionContext = {
  backend: "gpu" | "cpu";
  threadMode: "worker" | "main";
  canvas?: HTMLCanvasElement | OffscreenCanvas;
  gl?: WebGL2RenderingContext;
};

type AudioExtensionContext = {
  audioContext: AudioContext;
  sampleRate: number;
  extensionOptions?: Record<string, unknown>;
};

type AudioWorkletExtensionContext = {
  sampleRate: number;
  extensionOptions?: Record<string, unknown>;
};
```

## 视频路径说明

### 帧尺寸偶数对齐

框架会自动将帧尺寸向下对齐到偶数（如 641→640, 481→480）。这是因为视频编解码器使用的 YUV 色度子采样（NV12/I420）要求宽高为偶数，奇数尺寸会导致编解码链路中出现绿屏。插件收到的 `frame.width` / `frame.height` 始终是偶数对齐后的值，与 texture 尺寸一致。

### WebGL (GPU) 主路径

直接读 input texture，通过 shader 或 WASM 处理，写 output texture。常规 GPU 插件推荐使用这一路径。

适用：超分辨率、水印叠加、色彩调整等以 GPU 为主的处理。

### CPU 数据兼容路径

如插件确实需要 CPU 侧像素数据，可使用 VIDEO_FRAME 模式（声明 `mode = VideoExtensionMode.VIDEO_FRAME`），框架会自动做 texture ↔ VideoFrame 转换。或者在 `applyEffect` 中处理 `frame.kind === "cpu"` 分支。

新插件应优先使用 WebGL (GPU) 主路径，仅在确有需要时使用 VIDEO_FRAME 模式。

## 与 v1 pipe 模式的区别

| | v1 pipe 模式 | v2 useExtension 模式 |
|---|---|---|
| 数据流 | Processor 链，每个 Processor 自行管理 MediaStreamTrack | 框架统一管理 GPU texture，插件只操作 texture |
| GL context | 每个 Processor 自建 | 框架创建共享 context |
| CPU↔GPU 拷贝 | 每个 Processor 至少一次 | WebGL (GPU) 主路径通常无额外 CPU 拷贝；CPU 数据兼容路径会增加一次 readPixels |
| 生命周期 | registerExtension → createProcessor → pipe → unpipe | useExtension → enable/disable → destroy |
| 互斥 | 不能与 useExtension 同时使用 | 不能与 pipe 同时使用 |

## 构建

```bash
npm run build
```

输出到 `dist/` 目录：
- `AgoraRTC_N-extension.js` — UMD
- `AgoraRTC_N-extension.esm.mjs` — ESM
- `extension.d.ts` — TypeScript 类型声明

## 测试

```bash
npm test
```

运行当前提交的单元测试，覆盖 SDK 基类、管理器/运行时行为以及 WebGL context loss/restore 处理。
