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.
single HTML file · CDN dependencies onlyinterface may contain Chinese labels
Source
<!--
示例:视频叠加
要点: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.