egress模板渲染画面时,使用锐化是否可行?目前遇到了渲染卡顿的问题
# ask-ai
a
egress模板渲染画面时,使用锐化是否可行?目前遇到了渲染卡顿的问题
t
Thanks for reaching out, 徐旭. I'm an AI assistant and I'm here to help. Give me a moment while I research this topic.
收到——你在问“在 Egress 模板渲染时使用锐化(sharpening)是否可行”,并提到当前遇到渲染卡顿。我会先给出简短结论与建议,然后请你提供几个关键信息以便进一步诊断。 简短结论与建议 • 在 Egress 自定义模板中做锐化是技术上可行(通过 Canvas / WebGL 或 CSS/Shader 实现),但会增加浏览器渲染负担,可能加剧卡顿,尤其是在无头 Chrome 捕获整个页面时。 • 当前更稳妥的做法是先避免在模板里做额外的实时后处理;若必须锐化,优先考虑使用 GPU 加速的 WebGL/着色器并做性能剖析与降采样(降低分辨率或频率)以减轻负载。 • 使用 LiveKit CLI 的
egress test-template
在本地复现并调优模板渲染,有助于定位是渲染瓶颈还是后端编码瓶颈。 为更准确定位问题,请提供以下信息: 1. 模板实现技术栈(例如 React + DOM/CSS、Canvas、WebGL、或第三方播放器)。 2. 卡顿出现的位置:是在浏览器渲染(页面播放/预览)阶段,还是在 Egress 编码输出(录制/流)阶段?你能否复现并描述时间点(如页面加载、多人视频播放、画面切换时)? 3. 目标输出分辨率与帧率(如 1080p/30fps、720p/15fps 等)。 4. 模板是否对多个媒体轨道(如多路视频、共享屏幕)同时渲染? 5. 是否已有性能数据或截图(CPU/GPU 利用率、Chrome 无头日志、或
egress test-template
的输出)? 6. 你希望实现的锐化强度和应用方式(实时全帧、只在关键帧、离线后处理等)。 如果你把上面几项信息发给我,我会基于你的回答: • 推荐优先级最低到最高的优化步骤(例如降分辨率、合并图层、延迟锐化到后处理等); • 提供一份可测试的最小化改动建议(若你用 Canvas 或 WebGL,我可以给具体实现要点); • 指出如何用
egress test-template
本地复现来验证改动效果。 想先从哪几项信息开始? Sources: Custom recording templates | LiveKit docs | Universal Egress | Universal Egress
a
卡顿出现在渲染阶段、1080p 30fps 一个人共享屏幕,共享动态画面时卡顿,想要全帧锐化。在egress默认录制模板中可以加入锐化吗?是否可行
t
感谢说明细节 — 你描述的是:渲染阶段卡顿、1080p@30fps、单人共享动态屏幕,想做“全帧锐化”,并问是否能在 egress 默认录制模板中加入锐化以及是否可行。 直接回答 • 可以在默认的 Egress 模板中加入锐化,但这会增加无头 Chrome 的渲染负担并很可能使卡顿更严重。 • 实现方式包括:CSS Filter(性能最差、简单)、Canvas 逐帧处理(CPU 或 GPU)、或 WebGL/着色器(性能最好、实现复杂)。 建议(按优先级) 1. 首选避免在实时模板里做高强度全帧后处理——实时锐化对 1080p@30fps 的动态共享最容易成为瓶颈。 2. 若必须实时锐化,使用 WebGL/着色器实现并确保使用 GPU 加速;同时对输入做降采样(比如在渲染端先缩小到 720p 处理,再输出到 1080p)或降低锐化强度以减少开销。 3. 将锐化作为后处理(录制完成后离线处理)是最稳妥的方案——不会影响渲染或实时录制稳定性。 4. 在模板中逐步验收:用
egress test-template
本地测试改动并监测 CPU/GPU 利用率和帧丢失。 如果你想继续,我可以: • 提供一个基于 WebGL 的最小示例着色器用于锐化;或 • 给出一个 Canvas(2D)或 CSS 的快速示例供你在模板里试验;或 • 帮你制定一个测试步骤清单(如何用
egress test-template
测量并比较性能)。 你希望我先给哪个示例或测试清单?
a
提供一个基于 WebGL 的最小示例着色器用于锐化
t
下面给你一个最小可用的 WebGL 着色器示例,用于对每帧做简单的锐化(基于卷积核)。我会同时给出前端整合要点,方便你把它嵌入 Egress 模板的渲染流程中。 着色器与最小集成(HTML + JS)
Copy code
<!-- 最小化示例:把 <video id="src"> 的帧通过 WebGL 做锐化,渲染到 <canvas id="out"> -->
<video id="src" autoplay playsinline muted></video>
<canvas id="out"></canvas>

<script type="text/javascript">
const vsSource = `#version 300 es
in vec2 a_position;
in vec2 a_texcoord;
out vec2 v_texcoord;
void main() {
  v_texcoord = a_texcoord;
  gl_Position = vec4(a_position, 0.0, 1.0);
}
`;

// 简单 3x3 锐化卷积核(可调节中心权重)
const fsSource = `#version 300 es
precision highp float;
in vec2 v_texcoord;
uniform sampler2D u_texture;
uniform vec2 u_texelSize; // 1.0 / textureSize
uniform float u_strength; // e.g., 1.0
out vec4 outColor;
void main() {
  vec2 off[9];
  off[0] = v_texcoord + vec2(-u_texelSize.x, -u_texelSize.y);
  off[1] = v_texcoord + vec2(0.0, -u_texelSize.y);
  off[2] = v_texcoord + vec2(u_texelSize.x, -u_texelSize.y);
  off[3] = v_texcoord + vec2(-u_texelSize.x, 0.0);
  off[4] = v_texcoord;
  off[5] = v_texcoord + vec2(u_texelSize.x, 0.0);
  off[6] = v_texcoord + vec2(-u_texelSize.x, u_texelSize.y);
  off[7] = v_texcoord + vec2(0.0, u_texelSize.y);
  off[8] = v_texcoord + vec2(u_texelSize.x, u_texelSize.y);

  float kernel[9];
  kernel[0] = 0.0; kernel[1] = -1.0; kernel[2] = 0.0;
  kernel[3] = -1.0; kernel[4] = 5.0;  kernel[5] = -1.0;
  kernel[6] = 0.0;  kernel[7] = -1.0; kernel[8] = 0.0;

  // mix identity kernel with sharpening by u_strength
  float center = kernel[4];
  kernel[4] = mix(1.0, center, u_strength);

  vec3 sum = vec3(0.0);
  for (int i = 0; i < 9; ++i) {
    vec4 c = texture(u_texture, off[i]);
    sum += c.rgb * kernel[i];
  }
  outColor = vec4(clamp(sum, 0.0, 1.0), 1.0);
}
`;

function createShader(gl, type, source) {
  const s = gl.createShader(type);
  gl.shaderSource(s, source);
  gl.compileShader(s);
  if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) {
    console.error(gl.getShaderInfoLog(s));
    gl.deleteShader(s);
    return null;
  }
  return s;
}

function createProgram(gl, vsSrc, fsSrc) {
  const vs = createShader(gl, gl.VERTEX_SHADER, vsSrc);
  const fs = createShader(gl, gl.FRAGMENT_SHADER, fsSrc);
  const p = gl.createProgram();
  gl.attachShader(p, vs);
  gl.attachShader(p, fs);
  gl.linkProgram(p);
  if (!gl.getProgramParameter(p, gl.LINK_STATUS)) {
    console.error(gl.getProgramInfoLog(p));
    gl.deleteProgram(p);
    return null;
  }
  return p;
}

(async function init() {
  const video = document.getElementById('src');
  const canvas = document.getElementById('out');
  // 获取屏幕共享流示例:你可能已经有 rtc track,这里只是示例
  const stream = await navigator.mediaDevices.getDisplayMedia({ video: true });
  video.srcObject = stream;
  await video.play();

  // 设置 canvas 大小与视频一致(或按需降采样)
  canvas.width = video.videoWidth;
  canvas.height = video.videoHeight;

  const gl = canvas.getContext('webgl2');
  const program = createProgram(gl, vsSource, fsSource);
  gl.useProgram(program);

  // 顶点数据(全屏四边形)
  const posBuf = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, posBuf);
  const positions = new Float32Array([
    -1, -1,  0, 0,
     1, -1,  1, 0,
    -1,  1,  0, 1,
     1,  1,  1, 1,
  ]);
  gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW);

  const vao = gl.createVertexArray();
  gl.bindVertexArray(vao);
  const a_position = gl.getAttribLocation(program, 'a_position');
  const a_texcoord = gl.getAttribLocation(program, 'a_texcoord');
  gl.enableVertexAttribArray(a_position);
  gl.vertexAttribPointer(a_position, 2, gl.FLOAT, false, 16, 0);
  gl.enableVertexAttribArray(a_texcoord);
  gl.vertexAttribPointer(a_texcoord, 2, gl.FLOAT, false, 16, 8);

  // 创建视频纹理
  const tex = gl.createTexture();
  gl.bindTexture(gl.TEXTURE_2D, tex);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);

  const u_texture = gl.getUniformLocation(program, 'u_texture');
  const u_texelSize = gl.getUniformLocation(program, 'u_texelSize');
  const u_strength = gl.getUniformLocation(program, 'u_strength');

  function render() {
    if (video.readyState >= 2) {
      gl.bindTexture(gl.TEXTURE_2D, tex);
      gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, video);

      gl.viewport(0, 0, canvas.width, canvas.height);
      gl.clearColor(0, 0, 0, 1);
      gl.clear(gl.COLOR_BUFFER_BIT);

      gl.uniform1i(u_texture, 0);
      gl.uniform2f(u_texelSize, 1.0 / canvas.width, 1.0 / canvas.height);
      gl.uniform1f(u_strength, 1.0); // 0.0 = no sharp, 1.0 = full kernel

      gl.bindVertexArray(vao);
      gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
    }
    requestAnimationFrame(render);
  }
  requestAnimationFrame(render);
})();
</script>
整合与性能要点(简短) • 优先使用 `webgl2`,用着色器在 GPU 做卷积,比用 Canvas 2D 更省 CPU。 • 在 Egress 模板中直接启用全帧 1080p@30 的实时锐化会显著增加 GPU 负载,可能加剧卡顿。建议先在本地用
egress test-template
复现并测量(监控 CPU/GPU 使用率)。 • 若 GPU 成为瓶颈,可考虑:降低处理分辨率(例如先在 720p 做锐化再 upscale)、降低 `u_strength`,或仅对感兴趣区域(ROI)做锐化而非整帧。 • 注意纹理上传开销:调用
texImage2D
每帧会有成本,确保使用合适的像素格式并避免不必要的像素转换。 • 在无头 Chrome 里,确保启用了 GPU 加速(容器/环境需允许 GPU),否则着色器仍会在 CPU 上运行,性能劣化明显。 如果你需要,我可以: • 把这个示例改为在渲染前降采样然后再上采样的版本;或 • 提供一个更高效的边缘增强着色器(比如使用 separable kernel 或不同算子)以减少采样次数。 想要哪种进一步的示例?
a
怎么集成到模板中去
t
好的——下面给出把前面那个 WebGL 锐化示例集成到 LiveKit Egress 自定义模板的简明步骤与示例要点,帮助你把着色器嵌入默认录制模板并进行本地调试。 要点概览 • 把锐化渲染放在模板的渲染路径(即把屏幕/视频帧渲染到一个 canvas,使用 WebGL 着色器处理,然后让模板展示该 canvas)。 • 确保在无头 Chrome 环境中启用 GPU 加速(否则 WebGL 会回退到软件实现,性能很差)。 • 在模板中尽量复用已有 video 元素或 track,并只在必要时对帧做锐化(可按 ROI 或降采样)。 • 本地调试使用
egress test-template
来复现与测量性能。 集成步骤(按序) 1. 准备自定义模板目录 • 在本地建立模板文件夹(比如 `my-egress-template/`),包含 `index.html`、静态资源与脚本。 • 你可以基于默认模板复制并修改,也可以从空白模板开始。 1. 在模板中嵌入 WebGL canvas 与 video 元素 • 把之前的示例 HTML 中的
<video id="src">
和
<canvas id="out">
放入模板的适当位置。 • 如果模板已有用于展示远端/屏幕的 video 元素,可以直接复用该元素的 MediaStream(或把 track attach 到示例 video 上)。 示例片段(index.html 的核心部分)
Copy code
<body>
  <!-- 复用或新建 video,用于接收屏幕共享或远端 track -->
  <video id="src" autoplay playsinline muted style="display:none;"></video>

  <!-- 最终展示/被录制的 canvas -->
  <canvas id="out" width="1920" height="1080"></canvas>

  <script src="./sharpen.js"></script>
</body>
1. 在脚本中获取 LiveKit 的媒体并绑定到 video • 如果模板通过 LiveKit SDK 注入已有 track(例如 Egress 模板常见做法是把 track attach 到 DOM),直接把对应的 MediaStream 或 MediaStreamTrack 绑定到 `video.srcObject`。 • 常见方式: ◦ 如果你拿到的是 `MediaStream`:`video.srcObject = stream;` ◦ 如果你拿到的是 `MediaStreamTrack`:先创建
const s = new MediaStream([track]); video.srcObject = s;
示例(在
sharpen.js
中) • 在页面初始化时等待 LiveKit 的 track attach 或查找已有 video,然后调用着色器初始化函数并启动 render loop。 • 如果模板用的是某个 front-end 框架(React/Vue),把初始化放到组件挂载后。 1. 把前面提供的 WebGL 着色器逻辑放到
sharpen.js
• 将 VS/FS、着色器编译、纹理上传与
requestAnimationFrame
渲染循环代码放到 `sharpen.js`。 • 在每帧上传视频帧到纹理时,注意捕获异常并在 video 尺寸变化时调整 canvas 大小。 1. 性能优化建议(集成时必须考虑) • 启用 GPU:确保无头 Chrome 运行环境允许 GPU 渲染(容器/CI 环境需映射 GPU 或使用支持 GPU 的运行时)。 • 降采样策略:如果 1080p 全帧锐化太慢,可先把 canvas 设置为 1280x720、在 shader 中处理后再 upscale 到 1080p(或让 Egress 输出降为 720p)。 • 局部锐化:只对活跃区域(ROI)进行卷积,避免全帧计算。 • 减少纹理上传开销:如果可能,使用 `texSubImage2D`(在支持情况下)或避免格式转换。 • 控制调用频率:如果场景允许,尝试以 20–25fps 做锐化而不是强制 30fps。 1. 在本地用 egress test-template 验证 • 使用 LiveKit CLI 的 `egress test-template`(或已有命令)在本地启动无头 Chrome 并加载你的模板,观察是否复现卡顿。 • 同时监控 CPU/GPU 使用率与无头 Chrome 日志,调整 `u_strength`、分辨率或是否对 ROI 处理。 简短示例:把 video track 绑定到示例 video(伪代码)
Copy code
// 假设 liveTrack 是你从 LiveKit 模板环境获取到的 MediaStreamTrack
const video = document.getElementById('src');
const ms = new MediaStream([liveTrack]);
video.srcObject = ms;
await video.play();
// 然后调用你的 WebGL 初始化逻辑,canvas 会渲染并显示结果
测试与回归 • 先在本地用屏幕共享或录制视频文件测试;确认无头 Chrome 在你的环境下启用了 GPU。 • 逐步增加锐化强度与分辨率,观察帧率与系统资源,找到可接受的平衡点。 • 若无法启用 GPU,在无头环境中做复杂 shader 会非常慢,应改为后处理或降采样策略。 如果你希望,我可以: • 把完整的
index.html + sharpen.js
替你整理为一个压缩包结构,方便直接替换模板;或 • 给出一个降采样后再锐化并上采样的改进版 shader 示例以降低采样次数。 你想先要哪种文件示例?我可以直接输出完整文件内容以便你拷贝。
a
egress在渲染的时候加入锐化可以提升画质,是不是可以说明画质丢失出现在渲染环节
t
你问:在 Egress 渲染里加入锐化能提升画质,是否能说明画质丢失发生在渲染环节? 简短回答 • 不一定。锐化能改善视觉清晰度,但不能证明画质“主要”丢失在渲染环节。 为什么如此 • 渲染确实会带来模糊(缩放、合成、浏览器采样等),在这环节做锐化常能补偿这类可见模糊。 • 但编码(GStreamer/H.264 等)的压缩比特率、编码预设与转码策略对最终画质影响更大,通常是主要画质损失来源。 • 另外,上游输入质量(源分辨率、采集质量)和网络适配(丢包、动态比特率)也会导致画质下降。 因此的实操建议(短) • 把锐化当作视觉补偿手段,但不要仅凭锐化效果断定问题来源。 • 优先检查/调整:输出比特率与编码设置、保证源分辨率、网络稳定性。 • 同时在渲染端用局部/降采样/GPU 加速的锐化做试验,配合对比测试(有/无锐化、不同比特率),以定位主要瓶颈。 • 如需,我可以帮你列出一个对比测试清单(渲染 vs 编码 vs 源)或给出如何在模板里做有/无锐化的对比脚本。 要不要我先给出那个对比测试清单? Sources: Universal Egress | Bringing Zoom&amp;#39;s end-to-end optimizations to WebRTC | Egress API | LiveKit docs
a
模板渲染使用的是canvas还是 WebGL
t
好——针对你问“模板渲染使用的是 canvas 还是 WebGL”的问题,简短明确回答并附上关键点: 直接回答 • 都可以:Egress 自定义模板在无头 Chrome 中渲染网页,因此可以使用 HTML Canvas(2D)或 WebGL,具体由你在模板里如何实现决定。 关键说明(要点) • 模板本质是一个网页应用,支持任意浏览器图形技术(Canvas、WebGL、CSS、DOM 合成等)。 • 无论采用 Canvas 还是 WebGL,浏览器渲染的整个页面会被 headless Chrome 捕获并由 GStreamer 进行录制。 • 对于高性能、全帧锐化或复杂特效,推荐使用 WebGL(GPU 加速);简单 2D 合成或轻量滤镜可用 Canvas 2D。 • 在无头环境应确保 GPU 加速可用,否则 WebGL 可能回退到软件实现,性能会受影响。 • 若你要做锐化:优先在本地用
egress test-template
复现并对比 Canvas vs WebGL 性能(CPU/GPU 占用、帧率、延迟)。 如果你愿意,我可以: • 帮你把之前的 WebGL 锐化示例改成与默认模板更容易集成的版本;或 • 给出一个同时支持 Canvas 2D 和 WebGL 的切换示例,方便在无头环境下做对比测试。 你想先要哪个示例? Sources: Composite and web recordings | LiveKit docs | Custom recording templates | LiveKit docs | Universal Egress
a
egress默认模板使用的是哪个技术
t
默认模板使用的是 HTML5 Canvas(2D context),不是 WebGL。 简要说明: • 默认录制模板把远端/共享的 MediaStream 关联到 `<video>`,每帧通过 Canvas 2D 把视频绘制到画布上进行合成与布局。 • 浏览器渲染的整个页面由 headless Chrome 捕获,随后由 GStreamer 进行录制/转码。 • 因为默认模板是 Canvas 2D 实现,想做高性能的全帧锐化建议自建自定义模板并在其中使用 WebGL 着色器(GPU 加速),否则在 Canvas 2D 上做逐帧像素处理会显著增加 CPU 负担并可能恶化卡顿。 如果你想,我可以: • 把之前给你的 WebGL 锐化示例改成一个直接替换默认模板的完整
index.html
+
sharpen.js
文件,便于把模板上传并用
egress test-template
验证;或 • 给出在 Canvas 2D 上做轻量锐化的示例并说明性能差异。 你想先要哪一个? Sources: client-sdk-unity-web | livekit/livekit_composite | DeepWiki
a
给出在 Canvas 2D 上做轻量锐化的示例
t
下面给你一个在 Canvas 2D 上做轻量锐化(edge-enhance)的最小可直接集成示例,且已尽量复用缓冲以减少每帧分配开销。注意性能警告与可选优化点也一并列出。 注意事项(简短) • Canvas 2D 每帧像素处理较耗 CPU,1080p@30fps 全帧锐化可能显著增加渲染负担并加剧卡顿。先在本地用
egress test-template
验证并监控 CPU。 • 若性能不足,优先采用降采样、仅对 ROI 处理或改用 WebGL(GPU 加速)。 • 尽量复用缓冲区并可把计算移到 WebWorker(仍然会用到 CPU)。 示例代码(可直接嵌入模板的 index.html)
Copy code
<video id="src" autoplay playsinline muted style="display:none;"></video>
<canvas id="out" width="1920" height="1080"></canvas>

<script>
// 轻量 3x3 锐化卷积(中心权重可调)
const K = [
  0, -1,  0,
 -1,  5, -1,
  0, -1,  0
];

const video = document.getElementById('src');
const canvas = document.getElementById('out');
const ctx = canvas.getContext('2d');

let tmpOut = null; // 复用输出缓冲

async function initWithStream(stream) {
  video.srcObject = stream;
  await video.play();
  canvas.width = video.videoWidth || canvas.width;
  canvas.height = video.videoHeight || canvas.height;
  tmpOut = new Uint8ClampedArray(canvas.width * canvas.height * 4);
  requestAnimationFrame(render);
}

// 每帧卷积(在主线程,适合轻量或降采样后处理)
function render() {
  if (video.readyState >= 2) {
    ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
    const img = ctx.getImageData(0, 0, canvas.width, canvas.height);
    const data = img.data;
    const w = canvas.width, h = canvas.height;

    // step = 1 为全帧;改为 2 可降低开销(每隔像素处理)
    const step = 1;
    if (step === 1) {
      const out = tmpOut;
      out.fill(0);
      let kIdx;
      for (let y = 0; y < h; y++) {
        for (let x = 0; x < w; x++) {
          const idx = (y * w + x) * 4;
          let r = 0, g = 0, b = 0;
          kIdx = 0;
          for (let ky = -1; ky <= 1; ky++) {
            const yy = Math.min(h - 1, Math.max(0, y + ky));
            for (let kx = -1; kx <= 1; kx++) {
              const xx = Math.min(w - 1, Math.max(0, x + kx));
              const kpix = (yy * w + xx) * 4;
              const kval = K[kIdx++];
              if (kval === 0) continue;
              r += data[kpix] * kval;
              g += data[kpix + 1] * kval;
              b += data[kpix + 2] * kval;
            }
          }
          out[idx]     = Math.min(255, Math.max(0, r));
          out[idx + 1] = Math.min(255, Math.max(0, g));
          out[idx + 2] = Math.min(255, Math.max(0, b));
          out[idx + 3] = data[idx + 3];
        }
      }
      img.data.set(out);
      ctx.putImageData(img, 0, 0);
    } else {
      // 降采样处理思路(更快):
      // 1) 在小尺寸临时 canvas 上绘制并获取像素
      // 2) 对小图做卷积
      // 3) 将小图放大回主 canvas(ctx.drawImage)
      // 具体实现可按需提供。
    }
  }
  requestAnimationFrame(render);
}

// 模板里把 LiveKit 提供的 MediaStream 或 MediaStreamTrack 传入 initWithStream
// 例如: const ms = new MediaStream([mediaTrack]); initWithStream(ms);

// 本地调试可用:屏幕共享
// navigator.mediaDevices.getDisplayMedia({video:true}).then(initWithStream);
</script>
可选优化建议(简短) • 降采样处理:先把视频画到小 canvas(如 720p),处理后再放大回主 canvas。 • ROI 处理:只对感兴趣区域做卷积而非整帧。 • WebWorker:把像素卷积移到 Worker,主线程只做 drawImage/putImageData(仍有数据复制开销)。 • 若需要持续高质量且稳定的全帧锐化,优先改用 WebGL 着色器(GPU 加速)。 要我把示例改为哪种版本: • 完整的降采样→锐化→上采样实现;或 • 把卷积移到 WebWorker 的示例;或 • 直接给出可替换默认模板的完整文件(index.html + sharpen.js)?