Atlaslog
Atlaslog / Examples / Leaflet / Video Overlay
Leaflet · Runnable Example

Video Overlay

Runnable Leaflet example: Video Overlay. Open the live demo, read the complete source, and copy the file into your project. No build step or API key.

Open standalone → Chinese notes All Leaflet examples
/demos/leaflet-video-overlay.htmlLIVE
single HTML file · CDN dependencies onlyinterface may contain Chinese labels

Source

demos/leaflet-video-overlay.html
<!--
  示例:视频叠加
  要点:canvas.captureStream 造视频流、HTMLVideoElement 传给 L.videoOverlay、静音自动播放与暂停控制
  依赖:Leaflet 1.9.4 · jsDelivr CDN
-->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta name="robots" content="noindex,follow">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>视频叠加</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/leaflet@1.9.4/dist/leaflet.css">
<style>
  html,body{height:100%;margin:0}
  body{display:flex;flex-direction:column;overflow:hidden;
       font:13px/1.6 -apple-system,"Segoe UI","PingFang SC",sans-serif}
  .bar{flex:0 0 auto;padding:8px 12px;background:#f6f7f9;border-bottom:1px solid #e3e6ea;
       display:flex;gap:10px;align-items:center;font-size:12px;color:#58606e}
  .bar button{font:inherit;font-size:12px;padding:4px 10px;border-radius:4px;cursor:pointer;
       border:1px solid #ccd1d9;background:#fff;color:#16181d}
  .bar button:hover{background:#eef0f3}
  .bar input[type=range]{width:110px;vertical-align:middle}
  .bar b{color:#16181d;font-weight:600}
  #map{flex:1 1 auto;min-height:0}
</style>
</head>
<body>
<div class="bar">
  <button id="toggle">暂停</button>
  <label>透明度 <input id="opacity" type="range" min="0" max="1" step="0.05" value="0.7"></label>
  <span id="val">0.70</span>
  <b id="mode">canvas 视频流 · 25 fps</b>
</div>
<div id="map"></div>
<script src="https://cdn.jsdelivr.net/npm/leaflet@1.9.4/dist/leaflet.js"></script>
<script>
(function () {
  'use strict';

  var map = L.map('map', { center: [39.92, 116.40], zoom: 11 });

  L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Street_Map/MapServer/tile/{z}/{y}/{x}', {
    maxZoom: 19,
    attribution: 'Tiles © Esri'
  }).addTo(map);

  /* 视频覆盖范围:经度 0.32°、纬度 0.16°,与 640×320 画布同为 2:1 */
  var bounds = [[39.84, 116.24], [40.00, 116.56]];

  var W = 640;
  var H = 320;
  var canvas = document.createElement('canvas');
  canvas.width = W;
  canvas.height = H;
  var ctx = canvas.getContext('2d');

  var mode = document.getElementById('mode');

  /* 画一张随帧移动的「流场」示意图 */
  function draw(t) {
    ctx.clearRect(0, 0, W, H);
    ctx.fillStyle = 'rgba(15, 23, 42, 0.08)';
    ctx.fillRect(0, 0, W, H);

    for (var y = 0; y < H; y += 20) {
      for (var x = 0; x < W; x += 20) {
        var a = Math.sin((x + t * 0.05) * 0.02) + Math.cos((y - t * 0.04) * 0.02);
        var len = 9 + a * 5;
        ctx.strokeStyle = a > 0.6 ? 'rgba(194, 65, 12, 0.85)'
                        : a > 0 ? 'rgba(31, 95, 208, 0.75)'
                        : 'rgba(23, 105, 74, 0.6)';
        ctx.lineWidth = 1.4;
        ctx.beginPath();
        ctx.moveTo(x, y);
        ctx.lineTo(x + Math.cos(a) * len, y + Math.sin(a) * len);
        ctx.stroke();
      }
    }

    ctx.fillStyle = 'rgba(22, 24, 29, 0.55)';
    ctx.font = '12px sans-serif';
    ctx.fillText('模拟流场视频 · ' + Math.round(t / 50) + ' s', 10, H - 12);
  }

  var raf = null;
  var start = performance.now();

  function loop(now) {
    draw(now - start);
    raf = requestAnimationFrame(loop);
  }

  function startLive() {
    loop(performance.now());
    raf = requestAnimationFrame(loop);
  }

  var video = document.createElement('video');
  video.muted = true;
  video.loop = true;
  video.playsInline = true;
  video.setAttribute('playsinline', '');
  video.width = W;
  video.height = H;

  var overlay;

  if (typeof canvas.captureStream === 'function') {
    startLive();
    video.srcObject = canvas.captureStream(25);
    video.play().catch(function () {
      mode.textContent = '自动播放被拦截,点击地图后重试';
    });

    overlay = L.videoOverlay(video, bounds, {
      opacity: 0.7,
      autoplay: true,
      loop: true
    }).addTo(map);
  } else {
    /* 老浏览器:退化成静态图片叠加,功能仍可用 */
    draw(6000);
    overlay = L.imageOverlay(canvas.toDataURL('image/png'), bounds, { opacity: 0.7 }).addTo(map);
    mode.textContent = '浏览器不支持 captureStream,已降级为静态图片';
    document.getElementById('toggle').disabled = true;
  }

  document.getElementById('opacity').addEventListener('input', function () {
    overlay.setOpacity(Number(this.value));
    document.getElementById('val').textContent = Number(this.value).toFixed(2);
  });

  var playing = true;
  document.getElementById('toggle').addEventListener('click', function () {
    playing = !playing;
    if (playing) {
      video.play();
      if (!raf) startLive();
      this.textContent = '暂停';
    } else {
      video.pause();
      if (raf) cancelAnimationFrame(raf);
      raf = null;
      this.textContent = '播放';
    }
  });

  map.on('click', function (e) {
    console.log('点击坐标', e.latlng.lat.toFixed(5), e.latlng.lng.toFixed(5));
  });
})();
</script>
</body>
</html>
Language: the code and this page are in English; the full tutorial and API notes for this example are in Chinese. The demo itself is the authoritative version — it runs exactly as shown.

More Leaflet examples

Leaflet · EXAMPLEWMS Leaflet · EXAMPLEZoom Slider Leaflet · EXAMPLEBasemap Switch