Skip to content

Instantly share code, notes, and snippets.

@bellbind
Last active June 25, 2021 07:43
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bellbind/8ce6fb9104be14f5272897e7f174e3a3 to your computer and use it in GitHub Desktop.
Save bellbind/8ce6fb9104be14f5272897e7f174e3a3 to your computer and use it in GitHub Desktop.
[browser] Time Place Player for YouTube video
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>Time Place Player for YouTube video</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%2290%22 font-size=%2290%22>🎥</text></svg>"/>
<link
rel="stylesheet" href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css"
integrity="sha512-xodZBNTC5n17Xt2atTPuE1HxjVMSvLVW9ocqUKLsCC5CXdbqCmblAshOMAS6/keqq/sMZMZ19scR4PsZChSR7A=="
crossorigin=""/>
<script
src="https://unpkg.com/leaflet@1.7.1/dist/leaflet.js"
integrity="sha512-XQoYMqMTK8LvdxXYG3nZ448hOEQiglfqkJs1NOQV44cWnUrBc8PkAOcXy20w0vlaXaVUearIOBhiXZ5V3ynxwA=="
crossorigin=""></script>
<script src="https://www.youtube.com/iframe_api"></script>
<script type="module" src="./main.js"></script>
<style>
#map {height: 40vh; width: 90vw;}
</style>
</head>
<body>
<div>
<label>YouTube Video ID:
<input id="url" type="text"value="" />
<button id="load">load</button></label>
</div>
<div id="yt" style="width: 90vw; height: 40vh;"></div>
<div>
<label>Import KML: <input id="kml" type="file" accept=".kml" /></label>
</div>
<div>
<label>Overlay GPX: <input id="gpx" type="file" accept=".gpx" /></label>
</div>
<div id="map"></div>
</body>
</html>
const state = {plots: []};
const map = L.map("map");
const tileLayer = L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {});
tileLayer.addTo(map);
map.locate({setView: true, maxZoom: 16});
const iconSpan = document.createElement("span");
iconSpan.textContent = "🎥";
iconSpan.style.fontSize = "40px";
const icon = L.divIcon({
html: iconSpan,
iconSize: [0, 0],
iconAnchor: [20, 40],
});
const centerMarker = L.marker([0, 0], {icon: icon}).setOpacity(0).addTo(map);
const video = {player: null, timer: null};
const play = () => {
if (video.player.getPlayerState() !== YT.PlayerState.PLAYING) return;
const t = video.player.getCurrentTime();
const nextIndex = state.plots.findIndex(p => p.sec >= t);
if (nextIndex >= 0) {
const next = state.plots[nextIndex];
if (!next.marker.isPopupOpen()) next.marker.openPopup();
map.setView([next.lat, next.lng], map.getZoom(), {animate: false});
centerMarker.setLatLng([next.lat, next.lng]).setOpacity(1);
}
if (nextIndex > 0) {
const prev = state.plots[nextIndex - 1];
const next = state.plots[nextIndex];
const rate = (t - prev.sec) / (next.sec - prev.sec);
const lat = prev.lat + rate * (next.lat - prev.lat);
const lng = prev.lng + rate * (next.lng - prev.lng);
//map.setView([lat, lng], map.getZoom(), {animate: true});
map.setView([lat, lng], map.getZoom(), {animate: false});
centerMarker.setLatLng([lat, lng]).setOpacity(1);
}
};
const stateChanged = ev => {
if (ev.data === YT.PlayerState.PLAYING) {
video.timer = setInterval(play, 1000);
}
if (ev.data === YT.PlayerState.PAUSED || ev.data === YT.PlayerState.ENDED) {
clearInterval(video.timer);
}
};
const ready = ev => {
//const id = document.getElementById("url").value.trim();
//video.player.loadVideoById(id);
};
window.onYouTubeIframeAPIReady = () => {
video.player = new YT.Player("yt", {
width: "100vw", height: "40vh",
events: {onStateChange: stateChanged, onReady: ready},
});
};
document.getElementById("load").addEventListener("click", ev => {
const id = document.getElementById("url").value.trim();
video.player.cueVideoById({videoId: id});
});
const kmlToPlotData = xml => {
const parser = new DOMParser();
const kml = parser.parseFromString(xml, "application/xml");
// use Placemark only
const markers = [...kml.querySelectorAll("Placemark")];
const plots = markers.map(pm => {
const point = pm.querySelector("Point");
if (!point) return null;
const label = pm.querySelector("name").textContent;
const desc = pm.querySelector("description").textContent;
const [lng,lat,alt] = point.querySelector("coordinates").textContent.split(",").map(t => Number(t));
let secText = desc;
try {
secText = new URL(desc).searchParams.get("t");
} catch (ex) {}
const match = secText.match(/^(\d+)s$/);
if (!match) return null;
const sec = Number(match[1]);
return {sec, lat, lng, label};
});
return plots.filter(p => p);
};
const addMarker = ({sec, lat, lng, label = ""} = {}) => {
const marker = L.marker({lat, lng}, {draggable: false});
const plot = {marker, sec, lat, lng, label};
const labelInput = document.createElement("input");
labelInput.value = label;
const jump = document.createElement("button");
jump.textContent = "⏫";
jump.addEventListener("click", ev => {
//console.log(sec);
video.player.seekTo(sec, true);
});
const content = document.createElement("div");
content.append(labelInput, `${sec}s`, jump);
marker.bindPopup(content);
marker.addTo(map).openPopup();
return plot;
};
const kmlToCoords = xml => {
const parser = new DOMParser();
const kml = parser.parseFromString(xml, "application/xml");
const lscoords = kml.querySelector("Placemark LineString coordinates");
if (!lscoords) return [];
const coords = lscoords.textContent.split(/\n/).map(l => l.trim()).filter(l => l.length > 0);
const lnglatalt = coords.map(l => l.split(",").map(Number));
return lnglatalt.map(([lng, lat, alt]) => [lat, lng]);
};
const kmlInput = document.getElementById("kml");
kmlInput.addEventListener("input", ev => {
if (kmlInput.files.length !== 1) return;
const file = kmlInput.files[0];
file.text().then(xml => {
//console.log({xml});
const plots = kmlToPlotData(xml);
//console.log({plots});
plots.forEach(p => {
const plot = addMarker(p);
state.plots.push(plot);
});
const coords = kmlToCoords(xml);
//console.log(coords);
L.polyline(coords, {color: "blue", weight: 5, opacity: 0.5}).addTo(map);
});
});
const gpxToCoords = xml => {
const parser = new DOMParser();
const gpx = parser.parseFromString(xml, "application/xml");
const trkpts = [...gpx.querySelectorAll("trkpt")];
return trkpts.map(trkpt => [
Number(trkpt.getAttribute("lat")), Number(trkpt.getAttribute("lon")),
]);
};
const gpxInput = document.getElementById("gpx");
gpxInput.addEventListener("input", ev => {
if (gpxInput.files.length !== 1) return;
const file = gpxInput.files[0];
file.text().then(xml => {
const coords = gpxToCoords(xml);
L.polyline(coords, {color: "blue", weight: 5, opacity: 0.5}).addTo(map);
});
});
@bellbind
Copy link
Author

This is a player for "Time Place marker Editor" kml for video file.

The Editor makes time-place plots for a video file. The plots can be exported as KML.

This Player loads the video file uploaded to YouTube and the plot KML for it.
The loaded video playing synchronizes to focus the map plot for each seconds.

demo: https://gist.githack.com/bellbind/8ce6fb9104be14f5272897e7f174e3a3/raw/index.html

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment