0

I would like to have the webcam thumbnail on the map. I used the standard onEachFeature function with defining the popupContent variable where I placed the attribute. One of the elements in my GeoJSON file looks like this:

  "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://www.shetlandwebcams.com/cliff-cam-3/"  
  }, "geometry": { "type": "Point", "coordinates": [ -1.274802004043399, 59.854703404497755 ] 
 } 
  } },
  { "type": "Feature", "properties": { "id": 2, "Location": "Soteag Cliff", "Provider": 
 "Shetland Webcams", "Stream": 1, "Refresh": null, "AzimuthI": 60, "AzimuthII": 120, 
 "Nightmode": 1, "AllSky": 0, "Available": 1, "Rotation": "except overnight", "Country": 
 "United Kingdom", "Importance": null, "Link" : "https://www.foto-webcam.eu/webcam/hochmuth/" 
 }, "geometry": { "type": "Point", "coordinates": [ -1.27255592832451, 59.856353055941931 ] } 
  }

and the major JS code:

  onEachFeature: function (pointFeature, layer) {
   var popupContent = "<p><h2 class='webcam_location'>" +
   pointFeature.properties.Location + "</h2></p>" + 
   "<h4 class='webcam_provider'>" + pointFeature.properties.Provider + "</h4>" +
   "<iframe src=" + pointFeature.properties.Link + "'&output=embed'height='200' width='300' 
    title='camera thumbnail'></iframe>"
  layer.bindPopup(popupContent);
 }

where I have picked up the hint from the link below:

Overcoming "Display forbidden by X-Frame-Options"

but the effect is still like you can see below:

enter image description here

I think the issue is similar to this one:

Get most recent frame from webcam

Is there any way to make the image webcam thumbnail valid? based on the most-recent webcam activity?

UPDATE:

I found a similar hint here:

Overcoming "Display forbidden by X-Frame-Options"

and consequently added the '&output=embed' to my link in the section, but regrettably, it doesn't work either.

Geographos
  • 827
  • 2
  • 23
  • 57
  • Have you a running demo? – Falke Design Nov 24 '21 at 08:07
  • If you see this test website: http://02.mlearnweb.online/ the demo is there – Geographos Nov 24 '21 at 09:06
  • I think one of the reasons why it is not working, is that you provide a wrong url. [.../cliff-cam-3/'&output=embed'height='200'](https://www.shetlandwebcams.com/cliff-cam-3/'&output=embed'height='200') will not work because it returns 404, it has nothing to do with the iframe. You need to use the url without the parameters: https://www.shetlandwebcams.com/cliff-cam-3/ – Falke Design Nov 24 '21 at 10:32
  • the result is exactly the same even without the &output-embed component – Geographos Nov 24 '21 at 13:41

1 Answers1

2

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: '&copy; <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>
Swain
  • 56
  • 4
  • Good explained for your first post, go ahead with this – Falke Design Nov 25 '21 at 09:56
  • The solution is great for a standalone example, but I need something which will be visible in the iframe and the recent image thumbnail will be visible. Is it possible to do something with it? – Geographos Nov 27 '21 at 22:13
  • I've updated the answer to show a simple example directly integrated with leaflet, loaded from geoJSON and displayed in a popup – Swain Nov 28 '21 at 20:26