你是否曾希望在骑行前预览骑行路线?我们构建了一个自动化系统,为Party Onbici上的每条派对路线生成精美的飞越视频,让骑行者可以电影般地预览他们旅程中将会遇到的风景。

挑战

在组织或参加骑行活动时,了解路线至关重要。静态地图虽然有用,但无法传达实际骑行路线的体验。我们想给用户一种方式,让他们在承诺参加骑行之前能够虚拟地"飞越"路线。

我们的解决方案:无头视频渲染

我们构建了一个Node.js服务,使用Puppeteer运行无头Chrome浏览器,在交互式MapLibre GL地图上渲染路线,并在虚拟摄像机沿路径飞行时捕获帧。以下是其工作原理:

1. 路线插值

一条骑行路线可能有数千个坐标点。要创建15秒30FPS的流畅视频,我们需要精确450帧。我们使用基于距离的插值来均匀采样路线:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// Calculate cumulative distances using Haversine formula
const distances = [0];
for (let i = 1; i < coordinates.length; i++) {
  const dist = haversineDistance(coordinates[i-1], coordinates[i]);
  totalDistance += dist;
  distances.push(totalDistance);
}

// Sample at exactly totalFrames points
for (let frame = 0; frame < totalFrames; frame++) {
  const targetDist = (frame / (totalFrames - 1)) * totalDistance;
  // Interpolate position at this distance...
}

2. 平滑方位的摄像机动画

简单地将摄像机指向行进方向会在弯曲的路线上产生抖动。我们使用12帧的移动平均来平滑摄像机方位,并对360度/0度的环绕进行特殊处理:

1
2
3
4
5
6
7
// Use sin/cos components for circular averaging
for (let j = i - smoothWindow; j <= i + smoothWindow; j++) {
  const rad = rawBearings[j] * Math.PI / 180;
  sinSum += Math.sin(rad);
  cosSum += Math.cos(rad);
}
const avgBearing = Math.atan2(sinSum/count, cosSum/count) * 180 / Math.PI;

3. MapLibre GL渲染

路线在精美的Stadia Maps底图上渲染,包括:

  • 带白色轮廓的高亮路线线条以提高可见性
  • 动画摄像机位置指示器
  • 起点(绿色)和终点(红色)标记
  • 可选的城市区域3D建筑突出

4. 帧捕获和视频编码

Puppeteer将每帧捕获为PNG截图,然后我们使用ffmpeg将它们编码为VP9编解码器的WebM视频:

1
2
3
ffmpeg -framerate 30 -i frame_%05d.png \
  -c:v libvpx-vp9 -crf 20 -b:v 0 \
  route.webm

技术架构

1
Django App → Celery Task → Node.js Renderer → S3 Storage
  1. Django触发视频生成 - 当派对被创建或更新时
  2. Celery将任务排队 - 用于异步处理
  3. Node.js渲染器 - 使用MapLibre GL运行Puppeteer
  4. ffmpeg编码 - 捕获的帧
  5. 视频上传 - 到S3并更新派对记录

性能考虑

分辨率时长渲染时间文件大小
1280x72015秒60-90秒2-4 MB
1920x108015秒90-120秒4-8 MB

在生产环境中,我们使用带软件渲染(SwiftShader)的Docker容器来避免GPU依赖:

1
2
--use-angle=swiftshader
--enable-unsafe-swiftshader

我们添加的功能

  • 可自定义视频尺寸 - 用于社交媒体分享的正方形格式
  • Logo水印 - 用您组织的Logo为视频添加品牌标识
  • 摄像机倾斜和缩放 - 调整视角和高度
  • 路线线条样式 - 自定义颜色和宽度

Django集成

从应用程序代码中,生成视频非常简单:

1
2
party = Party.objects.get(uid="...")
party.generate_video(width=1080, height=1080, duration=10)

渲染完成后视频URL会自动更新:

1
2
3
4
5
{% if party.video_url %}
<video controls>
  <source src="{{ party.video_full_url }}" type="video/webm">
</video>
{% endif %}

下一步计划

我们正在探索几项增强功能:

  • 实时预览 - 用户绘制路线时进行实时渲染
  • 多摄像机角度 - 侧视图、俯视镜头
  • 天气叠加 - 显示沿路线的预报条件
  • 海拔剖面 - 在视频中可视化上坡和下坡

路线飞越视频已成为我们最受欢迎的功能之一,帮助骑行者做出明智的决定,选择参加哪些骑行活动。现代Web技术的结合将原本昂贵的制作任务转变为自动化的按需服务。


想试试吗?创建一个派对,您的路线视频将自动生成!