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": [
[]
]
}
]