I was able to come up with a solution, albeit quite an inelegant one.
The first thing to note is that we can't simply use an iframe to show only the webcam stream. First, because the site we want to embed it doesn't have native support for external embedding and Second, because even if we could, we would not be allowed programmatic access to the actual <video>
element in the embedded iframe for security reasons.
What's the best case output for our case:
The format that we want out of this is probably going to be a Data URL/URI in base64 format, that way it's pretty much impossible for leaflet's popout to get upset if we insert an <img>
element with a src equal to a base64 encoded representation of the image we want it to display.
To do this we need to get the actual webcam video into html5 <video>
element, and then draw the current video frame to a canvas, then we can convert the canvas content to a base64 data uri. So, the first order of business is we need to actually put the video stream into an html5 <video>
element.
So we need to grab some handle on the source of the webcam stream, in this case, a playlist.m3u8
file that will allow us to get the current stream, at it's current time point.
To get a link to the playlist.m3u8
, we need to go to the page which is hosting the webcam stream and open devtools, go to network, reload the page, then click play on the stream.
I'm using Shetland Webcams Burrafirth for this example, but all of the Shetland Webcams streams work in the same way.
Then we will filter the network requests down with the keyword playlist.m3u8
.
Your devtools should reveal the request made for playlist.m3u8
We are going to need the entire URL string, in the case of Burrafirth, it looks like:
https://uk-lon-edge-01.zetcast.net/dk-studio-4/studio4abr/playlist.m3u8
Example Chrome Devtools Output
The URL for the playlist.m3u8 file is different for each webcam stream, so for each one you wish to capture, you'll need to manually collect it.
Now that we know what we want out of the video, and where the video is, we need to actually put it into an html5 <video>
element, but our playlist.m3u8
file will eventually resolve to an MPEG-2 Transport Stream
. Which isn't playable with a standard html5 video player. We will need video-js to play this format.
So, here is the solution.
I've hard coded two streams into this example, but I trust that you'll be able to see how you can integrate this with your geoJSON features array.
<html>
<head>
<!-- Pulling in the video-js libraries from unpkg CDN -->
<link href="https://unpkg.com/video.js/dist/video-js.min.css" rel="stylesheet">
<script src="https://unpkg.com/video.js/dist/video.min.js"></script>
<script>
// You'll be using the geoJSON as the storage for the video sources, but I'm using an array, since thats all we need to act as a standin
var videoSources = [
"https://uk-lon-edge-01.zetcast.net/TownHallEast_abr/TownHallEastABR/playlist.m3u8",
"https://uk-lon-edge-01.zetcast.net/dk-studio-4/studio4abr/playlist.m3u8"
];
function createVideoPlayer(sourceURL){
disposeVideoPlayer();
var newVidPlayer = document.createElement("video");
newVidPlayer.id = "tmp-video-player";
newVidPlayer.classList.add("video-js")
newVidPlayer.setAttribute("data-setup", "{}");
newVidPlayer.muted = true;
newVidPlayer.controls = true;
var newVideoSource = document.createElement("source")
newVideoSource.src = sourceURL;
newVideoSource.type = "application/x-mpegURL";
newVidPlayer.appendChild(newVideoSource)
var tmpVideoContainer = document.getElementById("hidden-video-container");
tmpVideoContainer.appendChild(newVidPlayer)
// Initialize the videojs player for the new video player
videojs("tmp-video-player", {}, function(){
var myPlayer = videojs("tmp-video-player");
// This doesn't work on Internet Explorer, even IE 11
// but total IE usage is less than 0.9% of browsers, so it wont work for them...
var playPromise = myPlayer.play();
playPromise
.then((res)=>{
var dataURL = performFrameCapture();
// You can access the URI encoded data here, run whatever popout create you need, etc...
})
.catch((err)=>{
console.log(err);
})
});
}
function disposeVideoPlayer(){
// Does what it says on the tin
var existingVidPlayer = document.getElementById("tmp-video-player");
if(existingVidPlayer != null){
videojs(existingVidPlayer).dispose();
}
}
function performFrameCapture(){
var video = document.getElementById("tmp-video-player_html5_api");
var canvas = document.getElementById("frame-capture");
var canvasContext = canvas.getContext('2d');
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
// Copy the currrent frame onto the canvas
canvasContext.drawImage(video, 0, 0, canvas.width, canvas.height);
let base64ImageData = canvas.toDataURL();
disposeVideoPlayer();
// Returns the image in base64 Data URI format
return base64ImageData;
}
</script>
</head>
<body>
<!-- This div would be hidden in production, its only visible here to allow us to see better whats actually happening -->
<div id="hidden-video-container"></div>
<button onclick="createVideoPlayer(videoSources[0])">Attempt Capture Frame From Stream Index 0</button>
<button onclick="createVideoPlayer(videoSources[1])">Attempt Capture Frame From Stream Index 1</button>
<!-- This div would be hidden in production, its only visible here to allow us to see better whats actually happening -->
<div id="hidden-still-frame-container">
<canvas id="frame-capture"></canvas>
</div>
</body>
</html>
This runs as a standalone page, just in case you were wondering.
This is my first post on Stack Overflow, so if I'm leaving something out please let me know.
Edit: As requested, here is an example of how to tie the standalone solution into a leaflet popup.
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<!-- Leaflet JS Dependenices -->
<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>
<!-- Video JS Dependenices -->
<link href="https://unpkg.com/video.js/dist/video-js.min.css" rel="stylesheet">
<script src="https://unpkg.com/video.js/dist/video.min.js"></script>
<script>
// A Example of a single geoJSON Feature
var geoJSON = {
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"properties": {
"id": 1,
"Location": "Sumburg Head",
"Provider": "Shetland Webcams",
"Stream": 1,
"Refresh": null,
"AzimuthI": 300,
"AzimuthII": 360,
"Nightmode": 1,
"AllSky": 0,
"Available": 1,
"Rotation": "except overnight",
"Country": "United Kingdom",
"Importance": null,
"Link": "https://uk-lon-edge-01.zetcast.net/CliffCam3_abr/CliffCam3ABR/playlist.m3u8"
},
"geometry": {
"type": "Point",
"coordinates": [
-1.274802004043399,
59.854703404497755
]
}
}
]
};
var map = null;
function initMap(){
// Lets attach our leatflet map
map = L.map("map").setView([60.333333, -1.333333], 8);
L.tileLayer( 'http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
subdomains: ['a','b','c']
}).addTo( map );
// This part is super important, we NEED to actually build the popout when the user clicks on the layer, and we cannot do it any earlier than that without some serious deep magic.
L.geoJSON(geoJSON)
.on('click', buildLeafletOnClickDynamic)
.addTo(map);
}
function buildLeafletOnClickDynamic(e){
console.log(e);
// Give the user something to let them know we're working on something to show them
e.target.bindPopup("Loading Live Preview... Please Stand By").openPopup();
// Now start the process of building the actual popout
createVideoPlayer(e.layer.feature.properties.Link)
.then((response)=>{
// We destroy the old popout
e.target.closePopup();
e.target.unbindPopup();
// Then bind a new one, and open it
e.target.bindPopup(`<img src="${response.data}">`,{ minWidth: (response.width + 20)}).openPopup();
})
.catch((err)=>{
e.target.bindPopup(`An Error Has Occured While Loading The Live Preview`).openPopup();
})
}
function createVideoPlayer(sourceURL){
return new Promise((resolve, reject)=>{
disposeVideoPlayer();
var newVidPlayer = document.createElement("video");
newVidPlayer.id = "tmp-video-player";
newVidPlayer.classList.add("video-js")
newVidPlayer.setAttribute("data-setup", "{}");
newVidPlayer.muted = true;
newVidPlayer.controls = true;
var newVideoSource = document.createElement("source")
newVideoSource.src = sourceURL;
newVideoSource.type = "application/x-mpegURL";
newVidPlayer.appendChild(newVideoSource)
var tmpVideoContainer = document.getElementById("hidden-video-container");
tmpVideoContainer.appendChild(newVidPlayer)
// Initialize the videojs player for the new video player
videojs("tmp-video-player", {}, function(){
var myPlayer = videojs("tmp-video-player");
// This doesn't work on Internet Explorer, even IE 11
// but total IE usage is less than 0.9% of browsers, so it wont work for them...
var playPromise = myPlayer.play();
playPromise
.then((res)=>{
var dataURL = performFrameCapture();
// You can access the URI encoded data here, run whatever popout create you need, etc...
resolve(dataURL);
})
.catch((err)=>{
console.log(err);
reject(err);
})
});
})
}
function disposeVideoPlayer(){
// Does what it says on the tin
var existingVidPlayer = document.getElementById("tmp-video-player");
if(existingVidPlayer != null){
videojs(existingVidPlayer).dispose();
}
}
function performFrameCapture(){
var video = document.getElementById("tmp-video-player_html5_api");
var canvas = document.getElementById("frame-capture");
var canvasContext = canvas.getContext('2d');
var width = video.videoWidth;
var height = video.videoHeight;
canvas.width = width
canvas.height = height;
// Copy the currrent frame onto the canvas
canvasContext.drawImage(video, 0, 0, canvas.width, canvas.height);
let base64ImageData = canvas.toDataURL();
disposeVideoPlayer();
// Returns the image in base64 Data URI format
return {
data: base64ImageData,
height: height,
width: width
};
}
</script>
<style>
#hidden-video-container{
display:none;
}
#hidden-still-frame-container{
display:none;
}
#map {
height: 500px;
width: 500px;
}
</style>
</head>
<body onload="initMap()">
<div id="map"></div>
<div id="hidden-video-container"></div>
<div id="hidden-still-frame-container">
<canvas id="frame-capture"></canvas>
</div>
</body>
<script defer>
</script>
</html>