Accessing the iPhone camera in iOS Safari on Node-Red Dashboard 2.0

I’m trying to access the camera of iPhone(iOS 17) in safari with a web app I’m building on Node-Red(v3.1.3). What I want to do is to display camera feed on Dashboard 2.0 and be able to take a picture, record video and store them in the server. I am using the code shared in
this question in a template node.

So far I’m able to implement this code in Node-Red and it works on PC Chrome. Chrome asks me to enable the camera for the dashboard and if I allow I can view the camera output. But when I try to use same dashboard on iPhone Safari, I cannot see the camera display. It doesn’t even ask for my permission to use the camera. What am I doing wrong?

Below is the exported Node-Red flow I’ve built so far.
Thanks.

[
    {
        "id": "c911c322aad92eb4",
        "type": "tab",
        "label": "Flow 1",
        "disabled": false,
        "info": "",
        "env": []
    },
    {
        "id": "cf18f162c725eabc",
        "type": "ui-base",
        "name": "My Dashboard1",
        "path": "/dashboard",
        "includeClientData": true,
        "acceptsClientConfig": [
            "ui-notification",
            "ui-control"
        ],
        "showPathInSidebar": false,
        "navigationStyle": "default"
    },
    {
        "id": "f33b4cc2097266ae",
        "type": "ui-theme",
        "name": "Default Theme",
        "colors": {
            "surface": "#ffffff",
            "primary": "#0094CE",
            "bgPage": "#eeeeee",
            "groupBg": "#ffffff",
            "groupOutline": "#cccccc"
        },
        "sizes": {
            "pagePadding": "12px",
            "groupGap": "12px",
            "groupBorderRadius": "4px",
            "widgetGap": "12px"
        }
    },
    {
        "id": "e2669f7ebdf6c137",
        "type": "ui-page",
        "name": "Page N",
        "ui": "cf18f162c725eabc",
        "path": "/pageN",
        "icon": "home",
        "layout": "grid",
        "theme": "f33b4cc2097266ae",
        "order": -1,
        "className": "",
        "visible": "true",
        "disabled": "false"
    },
    {
        "id": "e6e8fb22f71d7392",
        "type": "ui-group",
        "name": "My Group",
        "page": "e2669f7ebdf6c137",
        "width": 6,
        "height": 1,
        "order": -1,
        "showTitle": true,
        "className": "",
        "visible": true,
        "disabled": false
    },
    {
        "id": "3afec64dc18d92b8",
        "type": "ui-group",
        "name": "Group Name",
        "page": "e2669f7ebdf6c137",
        "width": "6",
        "height": "1",
        "order": -1,
        "showTitle": true,
        "className": "",
        "visible": "true",
        "disabled": "false"
    },
    {
        "id": "167a8a7d56b7f4e7",
        "type": "ui-template",
        "z": "c911c322aad92eb4",
        "group": "e6e8fb22f71d7392",
        "page": "",
        "ui": "",
        "name": "iOS Camera Access",
        "order": 0,
        "width": 0,
        "height": 0,
        "head": "",
        "format": "<template>nn<!DOCTYPE html>n<html lang="en">nn<head>n    <meta charset="UTF-8">n    <title>Get User Media Code Along!</title>n    <link rel="stylesheet" href="style.css">n</head>nn<body>nn    <div class="photobooth">n        <div class="controls">n            <button onClick="takePhoto()">Take Photo</button>n            <!--       <div class="rgb">n        <label for="rmin">Red Min:</label>n        <input type="range" min=0 max=255 name="rmin">n        <label for="rmax">Red Max:</label>n        <input type="range" min=0 max=255 name="rmax">n        <br>n        <label for="gmin">Green Min:</label>n        <input type="range" min=0 max=255 name="gmin">n        <label for="gmax">Green Max:</label>n        <input type="range" min=0 max=255 name="gmax">n        <br>n        <label for="bmin">Blue Min:</label>n        <input type="range" min=0 max=255 name="bmin">n        <label for="bmax">Blue Max:</label>n        <input type="range" min=0 max=255 name="bmax">n      </div> -->n        </div>nn        <canvas class="photo"></canvas>n        <video class="player"></video>n        <div class="strip"></div>n    </div>nn    <audio class="snap" src="./snap.mp3" hidden></audio>nn    <script src="scripts.js"></script>nn</body>nn</html>nn</template>nn<script>n    export default {n        data() {n            // define variables available component-widen            // (in <template> and component functions)n            n        },n        watch: {n            // watch for any changes of "count"n            n        },n        computed: {n            // automatically compute this variablen            // whenever VueJS deems appropriaten            n        },n        methods: {n            // expose a method to our <template> and Vue Applicationn            increase: function () {n                this.count++n            },n            takePhoto: function () {n            // played the soundn            snap.currentTime = 0;n            snap.play();n            n            // take the data out of the canvasn            const data = canvas.toDataURL('image/jpeg');n            const link = document.createElement('a');n            link.href = data;n            link.setAttribute('download', 'handsome');n            link.innerHTML = `<img src="${data}" alt="Handsome Man" />`;n                            strip.insertBefore(link, strip.firstChild);n                        }nn        },n        mounted() {n            // code here when the component is first loadednn            const video = document.querySelector('.player');n            const canvas = document.querySelector('.photo');n            const ctx = canvas.getContext('2d');n            const strip = document.querySelector('.strip');n            const snap = document.querySelector('.snap');nn            // Fix for iOS Safari from https://leemartin.dev/hello-webrtc-on-safari-11-e8bcb5335295n            video.setAttribute('autoplay', '');n            video.setAttribute('muted', '');n            video.setAttribute('playsinline', '')nn            const constraints = {n            audio: false,n                video: {n                    facingMode: 'user'n                }n            }nn            function getVideo() {n                navigator.mediaDevices.getUserMedia(constraints)n                    .then(localMediaStream => {n                    console.log(localMediaStream);n                n            //  DEPRECIATION : n            //       The following has been depreceated by major browsers as of Chrome and Firefox.n            //       video.src = window.URL.createObjectURL(localMediaStream);n            //       Please refer to these:n            //       Deprecated  - https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURLn            //       Newer Syntax - https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/srcObjectn                console.dir(video);n                if ('srcObject' in video) {n                    video.srcObject = localMediaStream;n                } else {n                    video.src = URL.createObjectURL(localMediaStream);n                }n                // video.src = window.URL.createObjectURL(localMediaStream);n                video.play();n                })n                .catch(err => {n                console.error(`OH NO!!!!`, err);n                });n            }nn            function paintToCanvas() {n                const width = video.videoWidth;n                const height = video.videoHeight;n                canvas.width = width;n                canvas.height = height;nn                return setInterval(() => {n                    ctx.drawImage(video, 0, 0, width, height);n                    // take the pixels outn                    // let pixels = ctx.getImageData(0, 0, width, height);n                    // mess with themn                    // pixels = redEffect(pixels);nn                    // pixels = rgbSplit(pixels);n                    // ctx.globalAlpha = 0.8;nn                    // pixels = greenScreen(pixels);n                    // put them backn                    // ctx.putImageData(pixels, 0, 0);n                }, 16);n            }nn            nn            function redEffect(pixels) {n                for (let i = 0; i < pixels.data.length; i+=4) {n                    pixels.data[i + 0] = pixels.data[i + 0] + 200; // REDn                    pixels.data[i + 1] = pixels.data[i + 1] - 50; // GREENn                    pixels.data[i + 2] = pixels.data[i + 2] * 0.5; // Bluen                }n                return pixels;n            }nn            function rgbSplit(pixels) {n                for (let i = 0; i < pixels.data.length; i+=4) {n                    pixels.data[i - 150] = pixels.data[i + 0]; // REDn                    pixels.data[i + 500] = pixels.data[i + 1]; // GREENn                    pixels.data[i - 550] = pixels.data[i + 2]; // Bluen                }n                return pixels;n            }nn            function greenScreen(pixels) {n                const levels = {};nn                document.querySelectorAll('.rgb input').forEach((input) => {n                    levels[input.name] = input.value;n                });nn                for (i = 0; i < pixels.data.length; i = i + 4) {n                    red = pixels.data[i + 0];n                    green = pixels.data[i + 1];n                    blue = pixels.data[i + 2];n                    alpha = pixels.data[i + 3];nn                    if (red >= levels.rminn                    && green >= levels.gminn                    && blue >= levels.bminn                    && red <= levels.rmaxn                    && green <= levels.gmaxn                    && blue <= levels.bmax) {n                    // take it out!n                    pixels.data[i + 3] = 0;n                    }n                }nn                return pixels;n            }nn            getVideo();nn            video.addEventListener('canplay', paintToCanvas);n        },n        unmounted() {n            // code here when the component is removed from the Dashboardn            // i.e. when the user navigates away from the pagen        }n    }n</script>n<style>n    /* define any styles here - supports raw CSS */n    .my-class {n        color: red;n    }n    html {n    box-sizing: border-box;n    }n    n    *, *:before, *:after {n    box-sizing: inherit;n    }n    n    html {n    font-size: 10px;n    background: #ff0000;n    }n    n    .photobooth {n    background: white;n    max-width: 150rem;n    margin: 2rem auto;n    border-radius: 2px;n    }n    n    /*clearfix*/n    .photobooth:after {n    content: '';n    display: block;n    clear: both;n    }n    n    .photo {n    width: 100%;n    float: left;n    }n    n    .player {n    position: absolute;n    top: 20px;n    right: 20px;n    width:200px;n    }n    n    /*n    Strip!n    */n    n    .strip {n    padding: 2rem;n    }n    n    .strip img {n    width: 100px;n    overflow-x: scroll;n    padding: 0.8rem 0.8rem 2.5rem 0.8rem;n    box-shadow: 0 0 3px rgba(0,0,0,0.2);n    background: white;n    }n    n    .strip a:nth-child(5n+1) img { transform: rotate(10deg); }n    .strip a:nth-child(5n+2) img { transform: rotate(-2deg); }n    .strip a:nth-child(5n+3) img { transform: rotate(8deg); }n    .strip a:nth-child(5n+4) img { transform: rotate(-11deg); }n    .strip a:nth-child(5n+5) img { transform: rotate(12deg); }n</style>",
        "storeOutMessages": true,
        "passthru": true,
        "resendOnRefresh": true,
        "templateScope": "local",
        "className": "",
        "x": 740,
        "y": 200,
        "wires": [
            []
        ]
    }
]