How to get actual rendered DOM state for debugging, not just the source code?

When working with UIs, I often notice a mismatch between what’s in the source code/templates and what actually happens at runtime in the browser.

For example:

  • An element can be present in the code but not visible after rendering.
  • Validation or accessibility rules may only apply at runtime.
  • Computed attributes or styles differ from what’s written in the code.

Looking only at the static code doesn’t help much when debugging these cases.

What I tried so far:

  • document.documentElement.outerHTML → gives me static markup, but misses the runtime state.
  • Manually using element.getAttribute() → too verbose and not scalable.
  • MutationObserver → good for tracking changes, but doesn’t provide a full snapshot.

Question:
What are the common practices, browser APIs, or tools developers usually use to inspect or capture the runtime DOM/UI state instead of just relying on static code?

Arrow keys for month selection and clicking on month to select month in date picker, both not working

Moodboard.jsx
Generated.jsx
calendar.jsx (this is included in both moodboard.jsx and generated.jsx)

Issue: In the date picker in Generated.jsx, when I click on the month to select what month’s content I wanna display, it doesn’t select it and also, arrows key to navigate b/w the months are also not working. The clicks on the arrow key are not even getting registered as the log statements inside the gotopreviousmonth and gotonextmonth never show up in console. The irony is that the exact same date picker is working perfectly in the Moodboard.jsx file. So what could be the reason? Please help.

Variables getting stuck on values in my script

My page at http://mrinitialman.com/ColourFinder/colfind.xht is meant to allow people to use HSL or RGB gauges to play around with colours.

However, if you use the RGB gauges to find a colour that corresponds to a CSS colour name, or use the dropdown list on the far right to choose a name, the rgb property of my current_color object gets stuck to those values for some strange reason. I am at wit’s end. You can see the XHTML code in the page provided; but here is my full javascript file.


const colour_names = {
'000000': {'rgb': [0,0,0], 'hsl': [0,0,0]}, //'Black'
'000080': {'rgb': [0,0,128], 'hsl': [240,100,25]}, //'Navy'
'00008B': {'rgb': [0,0,139], 'hsl': [240,100,27]}, //'DarkBlue'
'0000CD': {'rgb': [0,0,205], 'hsl': [240,100,40]}, //'MediumBlue'
'0000FF': {'rgb': [0,0,255], 'hsl': [240,100,50]}, //'Blue'
'006400': {'rgb': [0,100,0], 'hsl': [120,100,20]}, //'DarkGreen'
'008000': {'rgb': [0,128,0], 'hsl': [120,100,25]}, //'Green'
'008080': {'rgb': [0,128,128], 'hsl': [180,100,25]}, //'Teal'
'008B8B': {'rgb': [0,139,139], 'hsl': [180,100,27]}, //'DarkCyan'
'00BFFF': {'rgb': [0,191,255], 'hsl': [195,100,50]}, //'DeepSkyBlue'
'00CED1': {'rgb': [0,206,209], 'hsl': [181,100,41]}, //'DarkTurquoise'
'00FA9A': {'rgb': [0,250,154], 'hsl': [157,100,49]}, //'MediumspringGreen'
'00FF00': {'rgb': [0,255,0], 'hsl': [120,100,50]}, //'Lime'
'00FF7F': {'rgb': [0,255,127], 'hsl': [150,100,50]}, //'SpringGreen'
'00FFFF': {'rgb': [0,255,255], 'hsl': [180,100,50]}, //'Cyan,Aqua'
'191970': {'rgb': [25,25,112], 'hsl': [240,64,27]}, //'MidnightBlue'
'1E90FF': {'rgb': [30,144,255], 'hsl': [210,100,56]}, //'DodgerBlue'
'20B2AA': {'rgb': [32,178,170], 'hsl': [177,70,41]}, //'LightseaGreen'
'228B22': {'rgb': [34,139,34], 'hsl': [120,61,34]}, //'ForestGreen'
'2E8B57': {'rgb': [46,139,87], 'hsl': [146,50,36]}, //'SeaGreen'
'2F4F4F': {'rgb': [47,79,79], 'hsl': [180,25,25]}, //'DarkSlateGray,DarkSlateGrey'
'32CD32': {'rgb': [50,205,50], 'hsl': [120,61,50]}, //'LimeGreen'
'3CB371': {'rgb': [60,179,113], 'hsl': [147,50,47]}, //'MediumseaGreen'
'40E0D0': {'rgb': [64,224,208], 'hsl': [174,72,56]}, //'Turquoise'
'4169E1': {'rgb': [65,105,225], 'hsl': [225,73,57]}, //'RoyalBlue'
'4682B4': {'rgb': [70,130,180], 'hsl': [207,44,49]}, //'SteelBlue'
'483D8B': {'rgb': [72,61,139], 'hsl': [248,39,39]}, //'DarkSlateBlue'
'48D1CC': {'rgb': [72,209,204], 'hsl': [178,60,55]}, //'Mediumturquoise'
'4B0082': {'rgb': [75,0,130], 'hsl': [275,100,25]}, //'Indigo'
'556B2F': {'rgb': [85,107,47], 'hsl': [82,39,30]}, //'DarkOliveGreen'
'5F9EA0': {'rgb': [95,158,160], 'hsl': [182,25,50]}, //'CadetBlue'
'663399': {'rgb': [102,51,153], 'hsl': [270,50,40]}, //'Rebeccapurple'
'6495ED': {'rgb': [100,149,237], 'hsl': [219,79,66]}, //'CornflowerBlue'
'66CDAA': {'rgb': [102,205,170], 'hsl': [160,51,60]}, //'Mediumaquamarine'
'696969': {'rgb': [105,105,105], 'hsl': [0,0,41]}, //'DimGray,DimGrey'
'6A5ACD': {'rgb': [106,90,205], 'hsl': [248,53,58]}, //'SlateBlue'
'6B8E23': {'rgb': [107,142,35], 'hsl': [80,60,35]}, //'Olivedrab'
'708090': {'rgb': [112,128,144], 'hsl': [210,13,50]}, //'SlateGray,SlateGrey'
'778899': {'rgb': [119,136,153], 'hsl': [210,14,53]}, //'LightSlateGray,LightSlateGrey'
'7B68EE': {'rgb': [123,104,238], 'hsl': [249,80,67]}, //'MediumSlateBlue'
'7CFC00': {'rgb': [124,252,0], 'hsl': [90,100,49]}, //'LawnGreen'
'7FFF00': {'rgb': [127,255,0], 'hsl': [90,100,50]}, //'Chartreuse'
'7FFFD4': {'rgb': [127,255,212], 'hsl': [160,100,75]}, //'Aquamarine'
'800000': {'rgb': [128,0,0], 'hsl': [0,100,25]}, //'Maroon'
'800080': {'rgb': [128,0,128], 'hsl': [300,100,25]}, //'Purple'
'808000': {'rgb': [128,128,0], 'hsl': [60,100,25]}, //'Olive'
'808080': {'rgb': [128,128,128], 'hsl': [0,0,50]}, //'Gray,Grey'
'87CEEB': {'rgb': [135,206,235], 'hsl': [197,71,73]}, //'SkyBlue'
'87CEFA': {'rgb': [135,206,250], 'hsl': [203,92,75]}, //'LightSkyBlue'
'8A2BE2': {'rgb': [138,43,226], 'hsl': [271,76,53]}, //'BlueViolet'
'8B0000': {'rgb': [139,0,0], 'hsl': [0,100,27]}, //'DarkRed'
'8B008B': {'rgb': [139,0,139], 'hsl': [300,100,27]}, //'DarkMagenta'
'8B4513': {'rgb': [139,69,19], 'hsl': [25,76,31]}, //'Saddlebrown'
'8FBC8F': {'rgb': [143,188,143], 'hsl': [120,25,65]}, //'DarkSeaGreen'
'90EE90': {'rgb': [144,238,144], 'hsl': [120,73,75]}, //'LightGreen'
'9370D8': {'rgb': [147,112,216], 'hsl': [260,57,64]}, //'Mediumpurple'
'9400D3': {'rgb': [148,0,211], 'hsl': [282,100,41]}, //'DarkViolet'
'98FB98': {'rgb': [152,251,152], 'hsl': [120,93,79]}, //'PaleGreen'
'9932CC': {'rgb': [153,50,204], 'hsl': [280,61,50]}, //'DarkOrchid'
'9ACD32': {'rgb': [154,205,50], 'hsl': [80,61,50]}, //'YellowGreen'
'A0522D': {'rgb': [160,82,45], 'hsl': [19,56,40]}, //'Sienna'
'A52A2A': {'rgb': [165,42,42], 'hsl': [0,59,41]}, //'Brown'
'A9A9A9': {'rgb': [169,169,169], 'hsl': [0,0,66]}, //'DarkGray,DarkGrey'
'ADD8E6': {'rgb': [173,216,230], 'hsl': [195,53,79]}, //'LightBlue'
'ADFF2F': {'rgb': [173,255,47], 'hsl': [84,100,59]}, //'GreenYellow'
'AFEEEE': {'rgb': [175,238,238], 'hsl': [180,65,81]}, //'Paleturquoise'
'B0C4DE': {'rgb': [176,196,222], 'hsl': [214,41,78]}, //'LightSteelBlue'
'B0E0E6': {'rgb': [176,224,230], 'hsl': [187,52,80]}, //'PowderBlue'
'B22222': {'rgb': [178,34,34], 'hsl': [0,68,42]}, //'Firebrick'
'B8860B': {'rgb': [184,134,11], 'hsl': [43,89,38]}, //'DarkGoldenrod'
'BA55D3': {'rgb': [186,85,211], 'hsl': [288,59,58]}, //'Mediumorchid'
'BC8F8F': {'rgb': [188,143,143], 'hsl': [0,25,65]}, //'Rosybrown'
'BDB76B': {'rgb': [189,183,107], 'hsl': [56,38,58]}, //'DarkKhaki'
'C0C0C0': {'rgb': [192,192,192], 'hsl': [0,0,75]}, //'Silver'
'C71585': {'rgb': [199,21,133], 'hsl': [322,81,43]}, //'MediumvioletRed'
'CD5C5C': {'rgb': [205,92,92], 'hsl': [0,53,58]}, //'IndianRed'
'CD853F': {'rgb': [205,133,63], 'hsl': [30,59,53]}, //'Peru'
'D2691E': {'rgb': [210,105,30], 'hsl': [25,75,47]}, //'Chocolate'
'D2B48C': {'rgb': [210,180,140], 'hsl': [34,44,69]}, //'Tan'
'D3D3D3': {'rgb': [211,211,211], 'hsl': [0,0,83]}, //'LightGray,LightGrey'
'D87093': {'rgb': [216,112,147], 'hsl': [340,57,64]}, //'PalevioletRed'
'D8BFD8': {'rgb': [216,191,216], 'hsl': [300,24,80]}, //'Thistle'
'DA70D6': {'rgb': [218,112,214], 'hsl': [302,59,65]}, //'Orchid'
'DAA520': {'rgb': [218,165,32], 'hsl': [43,74,49]}, //'Goldenrod'
'DC143C': {'rgb': [220,20,60], 'hsl': [348,83,47]}, //'Crimson'
'DCDCDC': {'rgb': [220,220,220], 'hsl': [0,0,86]}, //'Gainsboro'
'DDA0DD': {'rgb': [221,160,221], 'hsl': [300,47,75]}, //'Plum'
'DEB887': {'rgb': [222,184,135], 'hsl': [34,57,70]}, //'Burlywood'
'E0FFFF': {'rgb': [224,255,255], 'hsl': [180,100,94]}, //'LightCyan'
'E6E6FA': {'rgb': [230,230,250], 'hsl': [240,67,94]}, //'Lavender'
'E9967A': {'rgb': [233,150,122], 'hsl': [15,72,70]}, //'DarkSalmon'
'EE82EE': {'rgb': [238,130,238], 'hsl': [300,76,72]}, //'Violet'
'EEE8AA': {'rgb': [238,232,170], 'hsl': [55,67,80]}, //'Palegoldenrod'
'F08080': {'rgb': [240,128,128], 'hsl': [0,79,72]}, //'LightCoral'
'F0E68C': {'rgb': [240,230,140], 'hsl': [54,77,75]}, //'Khaki'
'F0F8FF': {'rgb': [240,248,255], 'hsl': [208,100,97]}, //'AliceBlue'
'F0FFF0': {'rgb': [240,255,240], 'hsl': [120,100,97]}, //'Honeydew'
'F0FFFF': {'rgb': [240,255,255], 'hsl': [180,100,97]}, //'Azure'
'F4A460': {'rgb': [244,164,96], 'hsl': [28,87,67]}, //'Sandybrown'
'F5DEB3': {'rgb': [245,222,179], 'hsl': [39,77,83]}, //'Wheat'
'F5F5F5': {'rgb': [245,245,245], 'hsl': [0,0,96]}, //'Whitesmoke'
'F5F5DC': {'rgb': [245,245,220], 'hsl': [60,56,91]}, //'Beige'
'F5FFFA': {'rgb': [245,255,250], 'hsl': [150,100,98]}, //'Mintcream'
'F8F8FF': {'rgb': [248,248,255], 'hsl': [240,100,99]}, //'GhostWhite'
'FAEBD7': {'rgb': [250,235,215], 'hsl': [34,78,91]}, //'AntiqueWhite'
'FA8072': {'rgb': [250,128,114], 'hsl': [6,93,71]}, //'Salmon'
'FAF0E6': {'rgb': [250,240,230], 'hsl': [30,65,94]}, //'Linen'
'FAFAD2': {'rgb': [250,250,210], 'hsl': [60,80,90]}, //'LightGoldenrodYellow'
'FDF5E6': {'rgb': [253,245,230], 'hsl': [39,85,95]}, //'Oldlace'
'FF0000': {'rgb': [255,0,0], 'hsl': [0,100,50]}, //'Red'
'FF00FF': {'rgb': [255,0,255], 'hsl': [300,100,50]}, //'Fuchsia.Magenta'
'FF1493': {'rgb': [255,20,147], 'hsl': [328,100,54]}, //'DeepPink'
'FF4500': {'rgb': [255,69,0], 'hsl': [16,100,50]}, //'OrangeRed'
'FF6347': {'rgb': [255,99,71], 'hsl': [9,100,64]}, //'Tomato'
'FF69B4': {'rgb': [255,105,180], 'hsl': [330,100,71]}, //'HotPink'
'FF7F50': {'rgb': [255,127,80], 'hsl': [16,100,66]}, //'Coral'
'FF8C00': {'rgb': [255,140,0], 'hsl': [33,100,50]}, //'DarkOrange'
'FFA07A': {'rgb': [255,160,122], 'hsl': [17,100,74]}, //'LightSalmon'
'FFA500': {'rgb': [255,165,0], 'hsl': [39,100,50]}, //'Orange'
'FFB6C1': {'rgb': [255,182,193], 'hsl': [351,100,86]}, //'LightPink'
'FFC0CB': {'rgb': [255,192,203], 'hsl': [350,100,88]}, //'Pink'
'FFD700': {'rgb': [255,215,0], 'hsl': [51,100,50]}, //'Gold'
'FFDAB9': {'rgb': [255,218,185], 'hsl': [28,100,86]}, //'Peachpuff'
'FFDEAD': {'rgb': [255,222,173], 'hsl': [36,100,84]}, //'NavajoWhite'
'FFE4B5': {'rgb': [255,228,181], 'hsl': [38,100,85]}, //'Moccasin'
'FFE4C4': {'rgb': [255,228,196], 'hsl': [33,100,88]}, //'Bisque'
'FFE4E1': {'rgb': [255,228,225], 'hsl': [6,100,94]}, //'Mistyrose'
'FFEBCD': {'rgb': [255,235,205], 'hsl': [36,100,90]}, //'BlanchedAlmond'
'FFEFD5': {'rgb': [255,239,213], 'hsl': [37,100,92]}, //'Papayawhip'
'FFF0F5': {'rgb': [255,240,245], 'hsl': [340,100,97]}, //'LavenderBlush'
'FFF5EE': {'rgb': [255,245,238], 'hsl': [25,100,97]}, //'Seashell'
'FFF8DC': {'rgb': [255,248,220], 'hsl': [48,100,93]}, //'Cornsilk'
'FFFACD': {'rgb': [255,250,205], 'hsl': [54,100,90]}, //'LemonChiffon'
'FFFAF0': {'rgb': [255,250,240], 'hsl': [40,100,97]}, //'FloralWhite'
'FFFAFA': {'rgb': [255,250,250], 'hsl': [0,100,99]}, //'Snow'
'FFFF00': {'rgb': [255,255,0], 'hsl': [60,100,50]}, //'Yellow'
'FFFFE0': {'rgb': [255,255,224], 'hsl': [60,100,94]}, //'LightYellow'
'FFFFF0': {'rgb': [255,255,240], 'hsl': [60,100,97]}, //'Ivory'
'FFFFFF': {'rgb': [255,255,255], 'hsl': [0,0,100]} //'White'     
};

Object.freeze(colour_names);

const cm = ', ';
const pc = '%, ';
const pp = '%)';
const sr = 'rgb(';
const sh = 'hsl(';

const xmlns = {
    'svg':'http://www.w3.org/2000/svg',
    'htm':'http://www.w3.org/1999/xhtml'
}
const current_color = {
    'hsl':[0,100,50],
    'rgb':[255,0,0]
};

const el_NameSelect = document.getElementById('NameSelect');
const el_Swatch = document.getElementById('Swatch');
const els_textboxes = {
    'lp':[['hue','red'],['sat','grn'],['lit','blu']],
    'hex':{
        'red':document.getElementById('TXT_HEX_Red'),
        'grn':document.getElementById('TXT_HEX_Grn'),
        'blu':document.getElementById('TXT_HEX_Blu')
    },
    'rgb':{
        'red':document.getElementById('TXT_RGB_Red'),
        'grn':document.getElementById('TXT_RGB_Grn'),
        'blu':document.getElementById('TXT_RGB_Blu')
    },
    'hsl':{
        'hue':document.getElementById('TXT_HSL_Hue'),
        'sat':document.getElementById('TXT_HSL_Sat'),
        'lit':document.getElementById('TXT_HSL_Lit')
    }
}

const els_gauges = {
    'hsl':{
        'point':{
            'hue':document.getElementById('Hue-Gauge-Pointer'),
            'sat':document.getElementById('Sat-Gauge-Pointer'),
            'lit':document.getElementById('Lit-Gauge-Pointer')
        },
        'text':{
            'hue':document.getElementById('Hue-Display-Text'),
            'sat':document.getElementById('Sat-Display-Text'),
            'lit':document.getElementById('Lit-Display-Text')
        }
    },
    'rgb':{
        'point':{
            'red':document.getElementById('Red-Gauge-Pointer'),
            'grn':document.getElementById('Grn-Gauge-Pointer'),
            'blu':document.getElementById('Blu-Gauge-Pointer')
        },
        'text':{
            'red':document.getElementById('Red-Display-Text'),
            'grn':document.getElementById('Grn-Display-Text'),
            'blu':document.getElementById('Blu-Display-Text')
        }
    }
}




var $test_num = 0;

function rgb_2_hex(farr_rgb){
    return (
        farr_rgb[0].toString(16).padStart(2,'0') + 
        farr_rgb[1].toString(16).padStart(2,'0') + 
        farr_rgb[2].toString(16).padStart(2,'0')
    ).toUpperCase();
}

function rgb_2_hsl(farr_rgb){
    /// RGB from 0 - 255 to 0 - 1
    let r = (farr_rgb)[0] / 255;
    let g = (farr_rgb)[1] / 255;
    let b = (farr_rgb)[2] / 255;
    
    /// Get Maximum and Minimum values
    let cmax = Math.max(r, g, b);
    let cmin = Math.min(r, g, b);
    let h, s;
    let l = (cmax + cmin)/2;
    if (l === 0 || l === 1 || cmax === cmin){
        h = 0;
        s = 0;
    } else {
        //  Get Hue
        let delta = cmax - cmin;
        switch (cmax){
            case r:
                h = ((g - b) / delta) % 6;
                break;
            case g:
                h = ((b - r) / delta) + 2;
                break;
            case b:
                h = ((r - g) / delta) + 4;
                break;
        }
        h = Math.round(h * 60);
        if(h < 0){h+=360;}
        s = Math.round((delta / (1 - Math.abs(2 * l - 1))) * 100);
    }
    l = Math.round(l*100);
    
    return [h,s,l];
}
function hsl_2_rgb(farr_hsl) {
  let h = farr_hsl[0];
  let s = farr_hsl[1] / 100;
  let l = farr_hsl[2] / 100;

  let c = (1 - Math.abs(2 * l - 1)) * s;
  let x = c * (1 - Math.abs((h / 60) % 2 - 1));
  let m = l - c / 2;
  let r = 0, g = 0, b = 0;

  if (0 <= h && h < 60) {
    r = c;
    g = x;
    b = 0;
  } else if (60 <= h && h < 120) {
    r = x;
    g = c;
    b = 0;
  } else if (120 <= h && h < 180) {
    r = 0;
    g = c;
    b = x;
  } else if (180 <= h && h < 240) {
    r = 0;
    g = x;
    b = c;
  } else if (240 <= h && h < 300) {
    r = x;
    g = 0;
    b = c;
  } else if (300 <= h && h < 360) {
    r = c;
    g = 0;
    b = x;
  }
  r = Math.round((r + m) * 255);
  g = Math.round((g + m) * 255);
  b = Math.round((b + m) * 255);

  return [r, g, b];
}

function SetRGBGauges(farr_rgb, display){
    const int_gaugeheight = 520;
    let r = farr_rgb[0]
    let g = farr_rgb[1]
    let b = farr_rgb[2]
    
    
    display.point.red.setAttribute('y',(int_gaugeheight - Math.round(r / 255 * int_gaugeheight)));
    display.text.red.textContent = r.toString();
    
    display.point.grn.setAttribute('y',(int_gaugeheight - Math.round(g / 255 * int_gaugeheight)));
    display.text.grn.textContent = g.toString();
    
    display.point.blu.setAttribute('y',(int_gaugeheight - Math.round(b / 255 * int_gaugeheight)));
    display.text.blu.textContent = b.toString();
}
function SetHSLGauges(farr_hsl, display){
    let h = farr_hsl[0];
    let s = farr_hsl[1];
    let l = farr_hsl[2];
    
    const int_gaugeheight = 210;
    display.point.hue.setAttribute('transform','rotate('+h+')');
    display.text.hue.textContent = h.toString() + 'u00B0';
    
    
    display.point.sat.setAttribute('y',(int_gaugeheight - Math.round(s / 100 * int_gaugeheight)));
    display.text.sat.textContent = s.toString() + '%';
    
    display.point.lit.setAttribute('y',(int_gaugeheight - Math.round(l / 100 * int_gaugeheight)));
    display.text.lit.textContent = l.toString() + '%';
}

function SetTXTControls(farr_hsl, farr_rgb, fels_txtboxes){
    let txt_hex = rgb_2_hex(farr_rgb);
    for(let int_lp = 0; int_lp < els_textboxes.lp.length; int_lp++){
        fels_txtboxes.hex[fels_txtboxes.lp[int_lp][1]].value = txt_hex.substr((int_lp*2),2);
        fels_txtboxes.rgb[fels_txtboxes.lp[int_lp][1]].value = farr_rgb[int_lp].toString();
        fels_txtboxes.hsl[fels_txtboxes.lp[int_lp][0]].value = farr_hsl[int_lp].toString();
    }
}


function FindNameRGB(fob_names, farr_rgb){
    //farr_rgb is an array of red, green, and blue values.  fob_names is my object with names and HSL values
    let hex = rgb_2_hex(farr_rgb);
    
    if(fob_names.hasOwnProperty(hex)){
        return hex
    } else {
        return 'NONAME';
    }
}

function FindNameHSL(fob_names, farr_hsl){
    let key = 'NONAME' // NONAME means 'No Name'
    for(key in fob_names){
        if(
            farr_hsl[0] === txt_nameHSL[0] &&
            farr_hsl[1] === txt_nameHSL[1] &&
            farr_hsl[2] === txt_nameHSL[2]
        ){break;}
    }
    return key;
}

//  HSL Gauges

function HSLChange(data){return function(){
    let arindx = data.arindx;
    let change = data.change;
    let curcol = data.curcol;
    let swatch = data.swatch;
    let txtbxs = data.txtbxs;
    let gauges = data.gauges;
    let cnames = data.cnames;
    let cslect = data.cslect;
    
    curcol.hsl[arindx] += change;
    if (curcol.hsl[0] < 0){curcol.hsl[0]+=360;}
    if (curcol.hsl[0] >= 360){curcol.hsl[0]-=360;}
    
    if (curcol.hsl[1] < 0){curcol.hsl[1]= 0;}
    if (curcol.hsl[1] > 100){curcol.hsl[1] = 100;}
    
    if (curcol.hsl[2] < 0){curcol.hsl[2]= 0;}
    if (curcol.hsl[2] > 100){curcol.hsl[2] = 100;}
    
    
    swatch.style.background = sh + curcol.hsl[0] + cm + curcol.hsl[1] + pc + curcol.hsl[2] + pp;
    let nmcode = FindNameHSL(cnames, curcol.hsl);
    
    if(nmcode == 'NONAME'){
        curcol.rgb = hsl_2_rgb(curcol.hsl);
    } else {
        curcol.rgb = cnames[nmcode].rgb;
    }
    SetTXTControls(curcol.hsl, curcol.rgb, txtbxs);
    cslect.value = nmcode;
    
    SetHSLGauges(curcol.hsl, gauges.hsl);
    SetRGBGauges(curcol.rgb, gauges.rgb);
}}

function HSLSet(data){return function(){
    let arindx = data.arindx;
    let setval = data.setval;
    let curcol = data.curcol;
    let swatch = data.swatch;
    let txtbxs = data.txtbxs;
    let gauges = data.gauges;
    let cnames = data.cnames;
    let cslect = data.cslect;

    
    curcol.hsl[arindx] = setval;
    swatch.style.background = sh + curcol.hsl[0] + cm + curcol.hsl[1] + pc + curcol.hsl[2] + pp;
    
    let nmcode = FindNameHSL(cnames, curcol.hsl);
    if(nmcode == 'NONAME'){
        curcol.rgb = hsl_2_rgb(curcol.hsl);
    } else {
        curcol.rgb = cnames[nmcode].rgb;
    }
    SetTXTControls(curcol.hsl, curcol.rgb, txtbxs);
    cslect.value = nmcode;
    
    SetHSLGauges(curcol.hsl, gauges.hsl);
    SetRGBGauges(curcol.rgb, gauges.rgb);
}}

//  Hue Gauge
const hue_vals = document.getElementById('Hue-Gauge-Swatches').getElementsByTagNameNS(xmlns.svg,'circle');
const hue_vals_total = hue_vals.length;

for(let hue_vals_num = 0; hue_vals_num < hue_vals_total; hue_vals_num++){
    hue_vals[hue_vals_num].onclick = HSLSet({
        arindx: 0,
        setval: Number(hue_vals[hue_vals_num].getAttribute('fill').substr(4,3)), 
        curcol: current_color, 
        swatch: el_Swatch, 
        txtbxs: els_textboxes, 
        gauges: els_gauges, 
        cnames: colour_names,
        cslect: el_NameSelect
    });
}



document.getElementById('Hue-mn-10').onclick=HSLChange({
    arindx:0,
    change:-10,
    curcol:current_color,
    swatch:el_Swatch,
    txtbxs:els_textboxes,
    gauges:els_gauges,
    cnames:colour_names,
    cslect:el_NameSelect
})
//-10);
document.getElementById('Hue-mn-01').onclick=HSLChange({
    arindx:0,
    change:-1,
    curcol:current_color,
    swatch:el_Swatch,
    txtbxs:els_textboxes,
    gauges:els_gauges,
    cnames:colour_names,
    cslect:el_NameSelect
});
document.getElementById('Hue-pl-01').onclick=HSLChange({
    arindx:0,
    change:1,
    curcol:current_color,
    swatch:el_Swatch,
    txtbxs:els_textboxes,
    gauges:els_gauges,
    cnames:colour_names,
    cslect:el_NameSelect
});
document.getElementById('Hue-pl-10').onclick=HSLChange({
    arindx:0,
    change:10,
    curcol:current_color,
    swatch:el_Swatch,
    txtbxs:els_textboxes,
    gauges:els_gauges,
    cnames:colour_names,
    cslect:el_NameSelect
});

//  Saturation Gauge

const sat_vals = document.getElementById('Sat-Gauge-Values').getElementsByTagNameNS(xmlns.svg,'rect');
const sat_vals_total = sat_vals.length;

for(let sat_vals_num = 0; sat_vals_num < sat_vals_total; sat_vals_num++){
    sat_vals[sat_vals_num].onclick = HSLSet({
        arindx: 1,
        setval: Number(sat_vals[sat_vals_num].getAttribute('fill').substr(8,3)), 
        curcol: current_color, 
        swatch: el_Swatch, 
        txtbxs: els_textboxes, 
        gauges: els_gauges, 
        cnames: colour_names,
        cslect: el_NameSelect
    });
}

document.getElementById('Sat-mn-5').onclick=HSLChange({
    arindx:1,
    change:-5,
    curcol:current_color,
    swatch:el_Swatch,
    txtbxs:els_textboxes,
    gauges:els_gauges,
    cnames:colour_names,
    cslect:el_NameSelect
});
document.getElementById('Sat-mn-1').onclick=HSLChange({
    arindx:1,
    change:-1,
    curcol:current_color,
    swatch:el_Swatch,
    txtbxs:els_textboxes,
    gauges:els_gauges,
    cnames:colour_names,
    cslect:el_NameSelect
});
document.getElementById('Sat-pl-1').onclick=HSLChange({
    arindx:1,
    change:1,
    curcol:current_color,
    swatch:el_Swatch,
    txtbxs:els_textboxes,
    gauges:els_gauges,
    cnames:colour_names,
    cslect:el_NameSelect
});
document.getElementById('Sat-pl-5').onclick=HSLChange({
    arindx:1,
    change:5,
    curcol:current_color,
    swatch:el_Swatch,
    txtbxs:els_textboxes,
    gauges:els_gauges,
    cnames:colour_names,
    cslect:el_NameSelect
});


//  Lightness Gauge

const lit_vals = document.getElementById('Lit-Gauge-Values').getElementsByTagNameNS(xmlns.svg,'rect');
const lit_vals_total = lit_vals.length;

for(let lit_vals_num = 0; lit_vals_num < lit_vals_total; lit_vals_num++){
    lit_vals[lit_vals_num].onclick = HSLSet({
        arindx: 2,
        setval: Number(lit_vals[lit_vals_num].getAttribute('fill').substr(9,3)), 
        curcol: current_color, 
        swatch: el_Swatch, 
        txtbxs: els_textboxes, 
        gauges: els_gauges, 
        cnames: colour_names,
        cslect: el_NameSelect
    });
}
document.getElementById('Lit-mn-5').onclick=HSLChange({
    arindx:2,
    change:-5,
    curcol:current_color,
    swatch:el_Swatch,
    txtbxs:els_textboxes,
    gauges:els_gauges,
    cnames:colour_names,
    cslect:el_NameSelect
});
document.getElementById('Lit-mn-1').onclick=HSLChange({
    arindx:2,
    change:-1,
    curcol:current_color,
    swatch:el_Swatch,
    txtbxs:els_textboxes,
    gauges:els_gauges,
    cnames:colour_names,
    cslect:el_NameSelect
});
document.getElementById('Lit-pl-1').onclick=HSLChange({
    arindx:2,
    change:1,
    curcol:current_color,
    swatch:el_Swatch,
    txtbxs:els_textboxes,
    gauges:els_gauges,
    cnames:colour_names,
    cslect:el_NameSelect
});
document.getElementById('Lit-pl-5').onclick=HSLChange({
    arindx:2,
    change:5,
    curcol:current_color,
    swatch:el_Swatch,
    txtbxs:els_textboxes,
    gauges:els_gauges,
    cnames:colour_names,
    cslect:el_NameSelect
});



//  RGB Gauges
function RGBChange(data){return function(){
    let arindx = data.arindx;
    let change = data.change;
    let curcol = data.curcol;
    let swatch = data.swatch;
    let txtbxs = data.txtbxs;
    let gauges = data.gauges;
    let cnames = data.cnames;
    let cslect = data.cslect;

    curcol.rgb[arindx] += change;
    if (curcol.rgb[arindx] < 0){curcol.rgb[arindx]=0;}
    if (curcol.rgb[arindx] > 255){curcol.rgb[arindx]=255;}
    
    swatch.style.background = sr + curcol.rgb[0] + cm + curcol.rgb[1] + cm + curcol.rgb[2] + ')';
    
    let nmcode = FindNameRGB(cnames, curcol.rgb);
    if(nmcode == 'NONAME'){
        curcol.hsl = rgb_2_hsl(curcol.rgb);
    } else {
        curcol.hsl = cnames[nmcode].hsl;
    }
    SetTXTControls(curcol.hsl, curcol.rgb, txtbxs);
    cslect.value = nmcode;
    
    SetHSLGauges(curcol.hsl, gauges.hsl);
    SetRGBGauges(curcol.rgb, gauges.rgb);
}}

function RGBSet(data){return function(){
    let arindx = data.arindx;
    let setval = data.setval;
    let curcol = data.curcol;
    let swatch = data.swatch;
    let txtbxs = data.txtbxs;
    let gauges = data.gauges;
    let cnames = data.cnames;
    let cslect = data.cslect;
    
    curcol.rgb[arindx] = setval;
    
    swatch.style.background = sr + curcol.rgb[0] + cm + curcol.rgb[1] + cm + curcol.rgb[2] + ')';
    
    let nmcode = FindNameRGB(cnames, curcol.rgb);
    if(nmcode == 'NONAME'){
        curcol.hsl = rgb_2_hsl(curcol.rgb);
    } else {
        curcol.hsl = cnames[nmcode].hsl;
    }
    SetTXTControls(curcol.hsl, curcol.rgb, txtbxs);
    cslect.value = nmcode;
    
    SetHSLGauges(curcol.hsl, gauges.hsl);
    SetRGBGauges(curcol.rgb, gauges.rgb);

};}

//  Red Gauge
const red_vals = document.getElementById('Red-Gauge-Values').getElementsByTagNameNS(xmlns.svg,'rect');
const red_vals_total = red_vals.length;

for(let red_vals_num = 0; red_vals_num < red_vals_total; red_vals_num++){
    red_vals[red_vals_num].onclick = RGBSet({
        arindx: 0, 
        setval: Number(red_vals[red_vals_num].getAttribute('fill').match(/d+/g)[0]),
        curcol: current_color, 
        swatch: el_Swatch, 
        txtbxs: els_textboxes, 
        gauges: els_gauges,
        cnames: colour_names,
        cslect: el_NameSelect
    });
}

document.getElementById('Red-mn-5').onclick=RGBChange({
    arindx:0,
    change:-5,
    curcol:current_color,
    swatch:el_Swatch,
    txtbxs:els_textboxes,
    gauges:els_gauges,
    cnames:colour_names,
    cslect:el_NameSelect
});
document.getElementById('Red-mn-1').onclick=RGBChange({
    arindx:0,
    change:-1,
    curcol:current_color,
    swatch:el_Swatch,
    txtbxs:els_textboxes,
    gauges:els_gauges,
    cnames:colour_names,
    cslect:el_NameSelect
});
document.getElementById('Red-pl-1').onclick=RGBChange({
    arindx:0,
    change:1,
    curcol:current_color,
    swatch:el_Swatch,
    txtbxs:els_textboxes,
    gauges:els_gauges,
    cnames:colour_names,
    cslect:el_NameSelect
});
document.getElementById('Red-pl-5').onclick=RGBChange({
    arindx:0,
    change:5,
    curcol:current_color,
    swatch:el_Swatch,
    txtbxs:els_textboxes,
    gauges:els_gauges,
    cnames:colour_names,
    cslect:el_NameSelect
});


//  Green Gauge
const grn_vals = document.getElementById('Grn-Gauge-Values').getElementsByTagNameNS(xmlns.svg,'rect');
const grn_vals_total = grn_vals.length;

for(let grn_vals_num = 0; grn_vals_num < grn_vals_total; grn_vals_num++){
    grn_vals[grn_vals_num].onclick = RGBSet({
        arindx: 1, 
        setval: Number(grn_vals[grn_vals_num].getAttribute('fill').match(/d+/g)[1]),
        curcol: current_color, 
        swatch: el_Swatch, 
        txtbxs: els_textboxes, 
        gauges: els_gauges,
        cnames: colour_names,
        cslect: el_NameSelect
    });
}

document.getElementById('Grn-mn-5').onclick=RGBChange({
    arindx:1,
    change:-5,
    curcol:current_color,
    swatch:el_Swatch,
    txtbxs:els_textboxes,
    gauges:els_gauges,
    cnames:colour_names,
    cslect:el_NameSelect
});
document.getElementById('Grn-mn-1').onclick=RGBChange({
    arindx:1,
    change:-1,
    curcol:current_color,
    swatch:el_Swatch,
    txtbxs:els_textboxes,
    gauges:els_gauges,
    cnames:colour_names,
    cslect:el_NameSelect
});
document.getElementById('Grn-pl-1').onclick=RGBChange({
    arindx:1,
    change:1,
    curcol:current_color,
    swatch:el_Swatch,
    txtbxs:els_textboxes,
    gauges:els_gauges,
    cnames:colour_names,
    cslect:el_NameSelect
});
document.getElementById('Grn-pl-5').onclick=RGBChange({
    arindx:1,
    change:5,
    curcol:current_color,
    swatch:el_Swatch,
    txtbxs:els_textboxes,
    gauges:els_gauges,
    cnames:colour_names,
    cslect:el_NameSelect
});


//  ====  //  Blue Gauge  //  ====  //
const blu_vals = document.getElementById('Blu-Gauge-Values').getElementsByTagNameNS(xmlns.svg,'rect');
const blu_vals_total = blu_vals.length;

for(let blu_vals_num = 0; blu_vals_num < blu_vals_total; blu_vals_num++){
    blu_vals[blu_vals_num].onclick = RGBSet({
        arindx: 2, 
        setval: Number(blu_vals[blu_vals_num].getAttribute('fill').match(/d+/g)[2]),
        curcol: current_color, 
        swatch: el_Swatch, 
        txtbxs: els_textboxes, 
        gauges: els_gauges,
        cnames: colour_names,
        cslect: el_NameSelect
    });
}

document.getElementById('Blu-mn-5').onclick=RGBChange({
    arindx:2,
    change:-5,
    curcol:current_color,
    swatch:el_Swatch,
    txtbxs:els_textboxes,
    gauges:els_gauges,
    cnames:colour_names,
    cslect:el_NameSelect
});
document.getElementById('Blu-mn-1').onclick=RGBChange({
    arindx:2,
    change:-1,
    curcol:current_color,
    swatch:el_Swatch,
    txtbxs:els_textboxes,
    gauges:els_gauges,
    cnames:colour_names,
    cslect:el_NameSelect
});
document.getElementById('Blu-pl-1').onclick=RGBChange({
    arindx:2,
    change:1,
    curcol:current_color,
    swatch:el_Swatch,
    txtbxs:els_textboxes,
    gauges:els_gauges,
    cnames:colour_names,
    cslect:el_NameSelect
});
document.getElementById('Blu-pl-5').onclick=RGBChange({
    arindx:2,
    change:5,
    curcol:current_color,
    swatch:el_Swatch,
    txtbxs:els_textboxes,
    gauges:els_gauges,
    cnames:colour_names,
    cslect:el_NameSelect
});







document.getElementById('CheckCurrColo').onclick = function(){
    alert(
        'hsl(' + current_color.hsl[0] + ', ' + current_color.hsl[1] + '%, ' + current_color.hsl[2] + '%)rn' +
        'rgb(' + current_color.rgb[0] + ', ' + current_color.rgb[1] + ', ' + current_color.rgb[2] + ')'
    );
}
//CheckNameCode

//  Text Inputs
el_NameSelect.onchange = function(){
    let txt_hexcode = el_NameSelect.value;
    el_Swatch.style.background='#'+txt_hexcode;
    current_color.hsl = colour_names[txt_hexcode].hsl;
    current_color.rgb = colour_names[txt_hexcode].rgb;
    SetTXTControls(colour_names[txt_hexcode].hsl, colour_names[txt_hexcode].rgb, els_textboxes)
  SetRGBGauges(colour_names[txt_hexcode].rgb, els_gauges.rgb);
  SetHSLGauges(colour_names[txt_hexcode].hsl, els_gauges.hsl);
}


Chrome throwing an error for a line that has been commented out already

The following line is commented out, but still throwing an error. Why would console still throw an error for a line that is already commented out? I have commented it out using // as well as `/…../

I have tried with both literal strings

// asdf = `<text>...${var}` 

inside the comment block as well as concatenated strings.

I have checked in Firefox and Chrome.

The specific error thrown in the console is Error: <textPath> attribute startOffset: Expected length, "'+offset+'%".

The following are various minimal reproducible examples of what I mean:

test.svg

<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="500px" height="500px" viewBox="0 0 500 500" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<script>
//  asdf += '<text font-family="sans-serif" font-size="14" fill="#1a202c"><textPath startOffset="'+offset+'%" xlink:href="#weeks-path">'+i+'</textPath></text>';
/*  asdf += '<text font-family="sans-serif" font-size="14" fill="#1a202c"><textPath startOffset="'+offset+'%" xlink:href="#weeks-path">'+i+'</textPath></text>';*/
</script>
</svg>

test.html

<!DOCTYPE html>
<html>
<svg width="500px" height="500px" viewBox="0 0 500 500" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<script>
//  asdf += '<text font-family="sans-serif" font-size="14" fill="#1a202c"><textPath startOffset="'+offset+'%" xlink:href="#weeks-path">'+i+'</textPath></text>';
/*  asdf += '<text font-family="sans-serif" font-size="14" fill="#1a202c"><textPath startOffset="'+offset+'%" xlink:href="#weeks-path">'+i+'</textPath></text>';*/
</script>
</svg>
</html>

Do ALL HTTPS requests in the browser derive from XMLHttpRequest and fetch API?

I’m trying to make a “nearly universal interceptor” package that can intercept all outgoing requests (coming soon…).

I added interceptors for fetch API and XMLHttpRequest, and then got ready to search for more types of HTTPS requests. However, I only found packages like Axios and Alova.

If I intercept every fetch request and XMLHttpRequest request, then can I say that I am intercepting ALL outgoing requests from the browser? If not, what is the closest way to reach a “universal interceptor”?

The trigger disappears due to a conflict between two ScrollTriggers

I’m currently creating a horizontal scroll for my website. At the very end of this horizontal scroll, I also set up a ScrollTrigger for stacking effects. However, strangely, the trigger for the stacking effects, #facts, disappears from the viewport once the user enters the onLeave area. This is very odd.

function createScrollTriggerMD() {
  ScrollTrigger.create({
    trigger: "#scroll-container",
    start: "top top",
    end: () => `+=${getScrollAmount() * -1}`,
    pin: "#scroll-container",
    animation: tween,
    scrub: 1,
    onUpdate: (e) => {
      let scale;
      let y;
      if (e.progress <= 0.5) {
        scale = gsap.utils.mapRange(0, 0.5, 1, 0.6, e.progress);
        y = gsap.utils.mapRange(0, 0.5, 0, -150, e.progress);
      } else {
        scale = gsap.utils.mapRange(0.5, 1, 0.6, 1, e.progress);
        y = gsap.utils.mapRange(0.5, 1, -150, -150, e.progress);
      }

      gsap.to("#red-bubble", {
        y,
        scale,
        x: `-${e.progress * 40}%`,
        overwrite: "auto",
        duration: 0.1,
      });
    },
    onLeave: () => {
      const cards = document.querySelectorAll(".protection-card");
      const header = document.querySelector("#facts");
      const animation = gsap.timeline();
      let cardHeight;

      function initCards() {
        animation.clear();
        cardHeight = cards[0].offsetHeight;
        console.log("initCards()", cardHeight);
        cards.forEach((card, index) => {
          if (index > 0) {
            //increment y value of each card by cardHeight
            gsap.set(card, { y: index * cardHeight });
            //animate each card back to 0 (for stacking)
            animation.to(
              card,
              { y: index * 10, duration: index * 0.5, ease: "none" },
              0
            );
          }
        });
      }

      initCards();

      ScrollTrigger.create({
        trigger: "#facts",
        start: "top top",
        pin: true,
        end: () => `+=${cards.length * cardHeight + header.offsetHeight}`,
        scrub: true,
        animation: animation,
        markers: true,
        invalidateOnRefresh: true,
      });

      ScrollTrigger.addEventListener("refreshInit", initCards);
    },
    invalidateOnRefresh: true,
  });
}

My goal is simple: how can I combine these two ScrollTriggers so that the stacking effects ScrollTrigger only runs after the horizontal scroll ScrollTrigger has finished?

Why are Socket.io or WebSockets used in React applications?

Socket.io or WebSockets are used in React applications for **real-time
communication Normally, React fetches data using HTTP requests (GET, POST), which work on a request-response cycle.

But for features like:
Chat applications
Live notifications
Real-time dashboards
Multiplayer games

HTTP requests become inefficient because the client has to keep asking the server for updates.

Socket.io/WebSockets solve this by creating a persistent connection between the server and client:

The server can push data to the client instantly.
The client can receive updates without sending repeated requests.
Latency is reduced, and data stays real-time synced

Example:

// Client-side React
import { useEffect } from 'react';
import io from 'socket.io-client';

const socket = io('http://localhost:3000');

function Chat() {
  useEffect(() => {
    socket.on('message', (msg) => {
      console.log('New message:', msg);
    });

    return () => socket.disconnect();
  }, []);

  return <div>Chat component running...</div>;
}






1. What you tried:

Describe the exact steps you took, including code changes, commands, or tools used.
Be precise and concise so someone else can replicate it.

Example:

> “I tried updating the React component to use Socket.io for real-time messaging. I imported `io` from `socket.io-client` and established a connection in a `useEffect` hook, then listened for the `message` event.”

2. What you expected:

 Explain the expected outcome if everything worked correctly.
 Be specific about the behavior, output, or UI change.


valueOf() changes after Object.create() on changed prototype. Why?

I observe strange behavior of valueOf() after Object.create() on changed prototype.

  1. reassign prototype.
  2. create two similar objects.
  3. one of them is used as prototype with Object.create()
  4. it changes its valueOf()!

I use VS Code.

function Animal(name) {
    this.name = name;
}

function Rabbit(name) {
    this.name = name;
}

let animal = new Animal("Bob");

Rabbit.prototype = animal;

// if we reassign constructor here, then odd_rabbit keeps its - valueof() - as Rabbit.
// but we comment it to see odd behavior later...
// Rabbit.prototype.constructor = Rabbit; // <-- this line fixes problem

let rabbit1 = new Rabbit("Tom");
let odd_rabbit = new Rabbit("Odd rabbit");

// rabbit1.valueOf() - Rabbit - first object is Ok
// odd_rabbit.valueOf() - Rabbit - second is still fine here

// After this line everything changes...
let rabbit2 = Object.create(odd_rabbit);

// rabbit1.valueOf() - Rabbit
// odd_rabbit.valueOf() - Animal - Why???

Why did odd_rabbit has changed its valueOf() after being used as a prototype of Object.create(odd_rabbit)? Or did I make a silly mistake?

Why does my div element display and then disappear after clicking the submit button?

I add an addEventListener so that when the user clicks on the “post” button, a new div with the user input should appear above it. But it disappears as soon as it appears. I have attached a live example to better illustrate the problem.

const form = document.getElementById('form');
form.style.visibility = 'hidden';

function comment() {
  const postLink = document.getElementById('postLink');
  postLink.style.visibility = 'hidden';

  if (form.style.visibility === 'hidden') {
    form.style.visibility = 'visible';
  } else {
    form.style.visibility = 'hidden';
  }
}

document.getElementById('button').addEventListener('click', function createPost() {
  userInput();
});

function userInput() {
  const textarea = document.getElementById('textarea').value;
  if (textarea.length > 0) {
    const cards = document.getElementById('cards');
    const div = document.createElement('div');
    cards.appendChild(div);
    div.textContent = textarea;
  }
}
.cards {
  display: grid;
  grid-template-columns: repeat(1, minmax(100px, 1fr));
  gap: 40px;
  justify-content: center;
}

.cards>div {
  border-top-style: solid;
  border-width: 5px 1px 0px 0px;
  box-shadow: 5px 5px 20px #e0dfdf;
  justify-self: center;
  padding: 10px;
}
<div class="cards" id="cards">
  <div>Type anything you want here and it will keep expanding.</div>
</div>
<p id="postLink"><a onclick='comment()' href="#">post</a></p>
<form id="form">
  <label for="textarea"></label>
  <textarea name="post" id="textarea" rows="8" cols="50" placeholder="start typing your essay..."></textarea>
  <div><button type="submit" id="button">post</button></div>
</form>
</div>

</div>
<script src="script.js"></script>

Audio preloading and sprite flicker issues in my HTML5 canvas space shooter—how to fix?

I made a browser-based space shooter game text in HTML5 and JavaScript. Here’s a minimal code example where I preload audio assets:

const hitSound = new Audio(‘hit.mp3’);
hitSound.preload = ‘auto’;

Yet when enemies die, the first hit is silent or delayed. Also, in full-screen mode, the spaceship sprite flickers badly. Here’s my drawing loop…

I’ve tried using requestAnimationFrame, double-buffering, and forcing audio.play() multiple times, but the problems persist. What am I missing?

THREE.js depth-map parallax looks “off” / swimming—how do I correctly align depth with the texture?

I’m building a layered parallax effect in React + THREE.js where each PNG texture has a matching grayscale depth map. The scene renders and the mouse parallax works, but the motion looks unnatural—almost like the depth map is slightly misregistered with the color, causing features to “swim” when I move the mouse.

What I’m doing

  • React + THREE WebGLRenderer with an OrthographicCamera so layers keep their size; parallax comes from sampling the depth map in the fragment shader.
  • Each layer has a color texture and a depth map; both are PNGs with the same aspect and (as far as I can tell) the same pixel dimensions and alignment.
  • I draw one full-screen plane per layer (plane size = viewport width × height) and sort by textureZ.
  • Displacement is computed from the depth map and the mouse position; I allow per-layer tuning with textureZ and depthZ.

Minimal shader (fragment):

uniform sampler2D u_image;
uniform sampler2D u_depth;
uniform vec2 u_mouse;     // normalized [0..1]
uniform float u_strength; // 0.15
uniform float u_texZ;     // per-layer
uniform float u_depthZ;   // per-layer
varying vec2 vUv;

void main() {
  float d = texture2D(u_depth, vUv).r;   // grayscale depth
  float zFactor = (u_texZ - u_depthZ) * 0.057; // tuning constant
  vec2 center = u_mouse;
  vec2 disp = (vUv - center) * u_strength * (1.0 - d) * zFactor;
  vec2 uv = vUv + disp;
  gl_FragColor = texture2D(u_image, uv);
}

Full component (React + THREE) is included below for reference.

What “off” looks like

  • Edges and high-contrast details appear to slide relative to their color as I move the mouse.
  • Flat objects (e.g., a picture frame) bend or shear subtly rather than staying rigid.
  • The parallax direction is mostly right, but the magnitude seems inconsistent across the image (some areas over-displace, others under-displace).

Environment

  • React 18, THREE r1xx (via three NPM).
  • Textures and depth maps are PNG, linear filtering.
  • Renderer: antialias: true, alpha: true, sRGBEncoding not explicitly set.
  • Reproduces in Chrome and Firefox on desktop (Windows + macOS).

Things I’ve already checked / tried

  1. Same resolution & aspect for texture and depth map: verified in an image editor.
  2. UVs: using a single full-screen plane; vUv is standard [0..1].
  3. Filtering: LinearFilter for min/mag; still swims.
  4. Wrap mode: not explicitly set; defaults apply.
  5. Depth polarity: tried d vs 1.0 - d; only changes the direction, not the “swim.”
  6. Displacement scale: reduced u_strength and zFactor; effect is smaller but still looks misaligned.
  7. Z sorting: layers are sorted far→near; visually correct stacking, but the artifact persists.
  8. Mouse math: normalized to the canvas rect; seems correct.

Suspicions

  • Color space mismatch: the color texture might be sampled in sRGB, depth in linear, leading to nonlinear depth response. I haven’t set renderer.outputEncoding or texture.encoding.
  • Texture wrap/clamp: lack of ClampToEdgeWrapping could cause edge bleeding when uv goes out of [0..1].
  • Depth map generation: the depth may not be metrically consistent (e.g., not linearized), causing local over/under-displacement.
  • Incorrect center: using u_mouse as the parallax center may produce perspective that feels wrong for an ortho camera without a focal length model.
  • Plane scaling vs. image pixels: viewport-sized plane might stretch textures slightly compared to their native pixel grid, revealing minor registration errors.
  • Per-layer Z math: (u_texZ - u_depthZ) * 0.057 might double-count or mis-scale perceived depth.

Concrete questions

  1. What’s the correct way to ensure the depth map aligns perfectly with the color texture in this setup?

  2. Should I explicitly set encodings and wraps like:

    renderer.outputColorSpace = THREE.SRGBColorSpace;
    tex.colorSpace = THREE.SRGBColorSpace;
    depth.colorSpace = THREE.LinearSRGBColorSpace; // or leave linear?
    tex.wrapS = tex.wrapT = THREE.ClampToEdgeWrapping;
    depth.wrapS = depth.wrapT = THREE.ClampToEdgeWrapping;
    depth.minFilter = depth.magFilter = THREE.LinearFilter;
    
  3. Is there a known pattern for screen-space parallax that avoids UVs leaving [0..1] (e.g., uv = clamp(uv, 0.0, 1.0);) without introducing visible edge pinning?

  4. If a depth map is not metrically linear, is there a common remapping (gamma, curve, or per-layer scale) you use to stabilize the motion?

  5. Would switching to a perspective camera and computing parallax from a camera basis (instead of mouse delta in UV space) make the motion feel more natural?

Debugging steps I’m planning (open to better ideas)

  • Visualize the depth directly: gl_FragColor = vec4(vec3(d), 1.0) to confirm alignment.
  • Draw the Sobel edges of the depth and overlay the color to check registration.
  • Clamp UVs and compare: uv = clamp(uv, 0.0, 1.0) to test edge bleeding hypothesis.
  • Toggle color/depth encodings and verify with a test gradient depth map.
  • Add a single global scale slider and separate per-layer scales for displacement to see if “swim” is just nonlinearity.

Any advice on the correct encoding/wrap settings, depth remapping, or parallax math to eliminate the “swimming” would be hugely appreciated! If there’s a canonical sample that combines sRGB color + linear depth correctly, a pointer would be gold.

Type Error while trying to import page props in a next js form

Type error: Type '{ params: { type: string; }; }' does not satisfy the constraint 'PageProps'.
  Types of property 'params' are incompatible.
    Type '{ type: string; }' is missing the following properties from type 'Promise<any>': then, catch, finally, [Symbol.toStringTag]

this is the error i am getting while trying to import some props.

The goal of this below code is to import any from dynamically according to the url or the button pressed and display it accordingly. this is done to reduce the number of separate pages and make a uniform url format.

import {
    Breadcrumb,
    BreadcrumbItem,
    BreadcrumbLink,
    BreadcrumbList,
    BreadcrumbPage,
    BreadcrumbSeparator,
} from "@/components/ui/breadcrumb"
import * as form from "@/components/forms";

const formMap: Record<string, React.ComponentType> = {
    client: form.ClientForm,
    invoice: form.InvoiceForm,
};

export default async function DynamicFormPage({ params }: { params: { type: string } }) {
    const FormComponent =await formMap[params.type];

    return (
        <main className="mx-70 flex flex-col gap-y-15">
            <section>
                <Breadcrumb>
                <BreadcrumbList>
                    <BreadcrumbItem>
                        <BreadcrumbLink href="/">Home</BreadcrumbLink>
                    </BreadcrumbItem>
                    <BreadcrumbSeparator/>
                    <BreadcrumbItem>
                        <BreadcrumbLink>New</BreadcrumbLink>
                    </BreadcrumbItem>
                    <BreadcrumbSeparator/>
                    <BreadcrumbItem>
                        <BreadcrumbPage>{params.type}</BreadcrumbPage>
                    </BreadcrumbItem>
                </BreadcrumbList>
            </Breadcrumb>
            </section>
            <section>
                <FormComponent/>
            </section>
        </main>
    );
}

while the example form i am using is

"use client";

//frontend imports
import {
    Accordion,
    AccordionContent,
    AccordionItem,
    AccordionTrigger,
} from "@/components/ui/accordion";
import * as field from "@/components/forms/fields";
import RadioField from "@/components/forms/fields/RadioField";
import { Checkbox } from "@/components/ui/checkbox";
import { Label } from "@/components/ui/label";

//backend imports
import { useForm } from "react-hook-form";
import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";

const clientSchema = z.object({
    businessName: z.string().min(4, "Company name too short!!"),
})

export default function ClientForm() {

    const {
        register,
        handleSubmit,
        formState: { errors },
    } = useForm({
        resolver: zodResolver(clientSchema),
        mode: "onBlur",
    });

    const onSubmit = (data: any) => {
        console.log("Valid data:", data);
    };

    return (
        <form onSubmit={handleSubmit(onSubmit)}>
            <Accordion
                type="single"
                collapsible
                defaultValue="item-1"
            >
                <AccordionItem value="item-1">
                    <AccordionTrigger className="text-xl !no-underline">Basic Information</AccordionTrigger>
                    <AccordionContent className="grid grid-cols-2 gap-4 text-balance">
                        <field.TextField
                            label="Business Name"
                            {...register("businessName")}
                            error={errors.businessName?.message}
                            required
                        />
                        <field.DropdownField
                            label="Business Type"
                            options={[
                                { value: "accounting", label: "Accounting" },
                                { value: "advertising", label: "Advertising" },
                                { value: "aerospace", label: "Aerospace & Defense" },
                                { value: "agriculture", label: "Agriculture" },
                                { value: "apparel", label: "Apparel & Fashion" },
                                { value: "architecture", label: "Architecture & Design" },
                                { value: "artificial_intelligence", label: "Artificial Intelligence" },
                                { value: "arts", label: "Arts & Culture" },
                                { value: "automotive", label: "Automotive" },
                                { value: "aviation", label: "Aviation" },
                                { value: "banking", label: "Banking" },
                                { value: "biotechnology", label: "Biotechnology" },
                                { value: "blockchain", label: "Blockchain" },
                                { value: "chemicals", label: "Chemicals" },
                                { value: "cloud_computing", label: "Cloud Computing" },
                                { value: "construction", label: "Construction" },
                                { value: "consulting", label: "Consulting" },
                                { value: "cybersecurity", label: "Cybersecurity" },
                                { value: "defense", label: "Defense" },
                                { value: "ecommerce", label: "E-commerce" },
                                { value: "education", label: "Education" },
                                { value: "edtech", label: "EdTech" },
                                { value: "electronics", label: "Electronics" },
                                { value: "energy", label: "Energy" },
                                { value: "environmental", label: "Environmental Services" },
                                { value: "farming", label: "Farming" },
                                { value: "financial_services", label: "Financial Services" },
                                { value: "fintech", label: "FinTech" },
                                { value: "fishing", label: "Fishing" },
                                { value: "film", label: "Film & TV" },
                                { value: "food_and_beverage", label: "Food & Beverage" },
                                { value: "forestry", label: "Forestry" },
                                { value: "gaming", label: "Gaming" },
                                { value: "government", label: "Government" },
                                { value: "healthcare", label: "Healthcare" },
                                { value: "hospitals", label: "Hospitals" },
                                { value: "hospitality", label: "Hospitality" },
                                { value: "human_resources", label: "Human Resources" },
                                { value: "insurance", label: "Insurance" },
                                { value: "investment", label: "Investment & Asset Management" },
                                { value: "it_services", label: "IT Services" },
                                { value: "jewelry", label: "Jewelry" },
                                { value: "legal", label: "Legal Services" },
                                { value: "logistics", label: "Logistics" },
                                { value: "luxury_goods", label: "Luxury Goods" },
                                { value: "manufacturing", label: "Manufacturing" },
                                { value: "marketing", label: "Marketing & PR" },
                                { value: "maritime", label: "Maritime" },
                                { value: "media", label: "Media & Entertainment" },
                                { value: "medical_devices", label: "Medical Devices" },
                                { value: "metaverse", label: "Metaverse" },
                                { value: "mining", label: "Mining" },
                                { value: "music", label: "Music" },
                                { value: "nonprofit", label: "Non-Profit" },
                                { value: "nuclear_energy", label: "Nuclear Energy" },
                                { value: "oil_and_gas", label: "Oil & Gas" },
                                { value: "pet_industry", label: "Pet Industry" },
                                { value: "pharmaceuticals", label: "Pharmaceuticals" },
                                { value: "printing", label: "Printing & Packaging" },
                                { value: "private_equity", label: "Private Equity" },
                                { value: "publishing", label: "Publishing" },
                                { value: "railways", label: "Railways" },
                                { value: "real_estate", label: "Real Estate" },
                                { value: "religious", label: "Religious Institutions" },
                                { value: "renewable_energy", label: "Renewable Energy" },
                                { value: "research", label: "Research & Development" },
                                { value: "restaurants", label: "Restaurants" },
                                { value: "retail", label: "Retail" },
                                { value: "semiconductors", label: "Semiconductors" },
                                { value: "shipping", label: "Shipping" },
                                { value: "software", label: "Software Development" },
                                { value: "space", label: "Space & Astronomy" },
                                { value: "sports", label: "Sports" },
                                { value: "telecommunications", label: "Telecommunications" },
                                { value: "textiles", label: "Textiles" },
                                { value: "toys", label: "Toys & Games" },
                                { value: "tourism", label: "Tourism" },
                                { value: "transportation", label: "Transportation" },
                                { value: "utilities", label: "Utilities" },
                                { value: "venture_capital", label: "Venture Capital" },
                                { value: "waste_management", label: "Waste Management" },
                                { value: "water_supply", label: "Water Supply" },
                                { value: "wholesale", label: "Wholesale" },
                            ]}
                        />
                        <field.DropdownField
                            label="Country"
                            required
                            options={[
                                { value: "argentina", label: "Argentina" },
                                { value: "australia", label: "Australia" },
                                { value: "brazil", label: "Brazil" },
                                { value: "canada", label: "Canada" },
                                { value: "china", label: "China" },
                                { value: "france", label: "France" },
                                { value: "germany", label: "Germany" },
                                { value: "india", label: "India" },
                                { value: "indonesia", label: "Indonesia" },
                                { value: "italy", label: "Italy" },
                                { value: "japan", label: "Japan" },
                                { value: "mexico", label: "Mexico" },
                                { value: "russia", label: "Russia" },
                                { value: "saudi_arabia", label: "Saudi Arabia" },
                                { value: "south_africa", label: "South Africa" },
                                { value: "south_korea", label: "South Korea" },
                                { value: "turkey", label: "Turkey" },
                                { value: "united_arab_emirates", label: "United Arab Emirates" },
                                { value: "united_kingdom", label: "United Kingdom" },
                                { value: "united_states", label: "United States" },
                            ]}
                        />
                        <field.TextField
                            label="City/Town"
                            name="cityTown"
                            required
                        />
                    </AccordionContent>
                </AccordionItem>
                <AccordionItem value="item-2">
                    <AccordionTrigger className="text-xl !no-underline">Tax Information (Optional)</AccordionTrigger>
                    <AccordionContent className="grid grid-cols-2 gap-4 text-balance">
                        <field.TextField
                            label="Business GSTIN"
                            name="gstin"
                        />
                        <field.TextField
                            label="Business PAN"
                            name="pan"
                        />
                        <RadioField
                            label="Payment Method"
                            name="payment"
                            tooltip="Important for filing GST reports"
                            orientation="horizontal"
                            className="col-span-2"
                            options={[
                                { value: "individual", label: "Individual" },
                                { value: "company", label: "Company" },
                            ]}
                        />
                        <field.DropdownField
                            label="Country"
                            required
                            className="col-span-2"
                            options={[
                                { value: "it", label: "IT Services" },
                                { value: "finance", label: "Finance" },
                                { value: "retail", label: "Retail" },
                            ]}
                        />
                    </AccordionContent>
                </AccordionItem>
                <AccordionItem value="item-3">
                    <AccordionTrigger className="text-xl !no-underline">Address (Optional)</AccordionTrigger>
                    <AccordionContent className="grid grid-cols-2 gap-4 text-balance">
                        <field.DropdownField
                            label="Country"
                            required
                            className="col-span-2"
                            options={[
                                { value: "it", label: "IT Services" },
                                { value: "finance", label: "Finance" },
                                { value: "retail", label: "Retail" },
                            ]}
                        />
                        <field.DropdownField
                            label="State/Province"
                            required
                            className="col-span-2"
                            options={[
                                { value: "it", label: "IT Services" },
                                { value: "finance", label: "Finance" },
                                { value: "retail", label: "Retail" },
                            ]}
                        />
                        <field.TextField
                            label="City/Town"
                            name="cityTown"
                        />
                        <field.TextField
                            label="Postal Code"
                            name="pincode"
                        />
                        <field.TextField
                            label="Street Address"
                            name="address"
                            className="col-span-2"
                        />
                    </AccordionContent>
                </AccordionItem>
                <AccordionItem value="item-4">
                    <AccordionTrigger className="text-xl !no-underline">Shipping Details (Optional)</AccordionTrigger>
                    <AccordionContent className="grid grid-cols-2 gap-4 text-balance">
                        <field.DropdownField
                            label="Country"
                            required
                            className="col-span-2"
                            options={[
                                { value: "it", label: "IT Services" },
                                { value: "finance", label: "Finance" },
                                { value: "retail", label: "Retail" },
                            ]}
                        />
                        <field.DropdownField
                            label="State/Province"
                            required
                            className="col-span-2"
                            options={[
                                { value: "it", label: "IT Services" },
                                { value: "finance", label: "Finance" },
                                { value: "retail", label: "Retail" },
                            ]}
                        />
                        <field.TextField
                            label="City/Town"
                            name="cityTown"
                        />
                        <field.TextField
                            label="Postal Code"
                            name="pincode"
                        />
                        <field.TextField
                            label="Street Address"
                            name="address"
                            className="col-span-2"
                        />
                    </AccordionContent>
                </AccordionItem>
                <AccordionItem value="item-5">
                    <AccordionTrigger className="text-xl !no-underline">Additional Details (Optional)</AccordionTrigger>
                    <AccordionContent className="grid grid-cols-2 gap-4 text-balance">
                        <field.TextField
                            label="Business Alias"
                            name="businessAlias"
                        />
                        <field.TextField
                            label="Unique Key"
                            name="uniqueKey"
                        />
                        <div className="flex items-center space-x-2">
                            <field.TextField
                                label="Email"
                                name="address"
                            />
                            <Checkbox
                                //checked={formData.showEmail}
                                //onCheckedChange={(val) => handleChange("showEmail", val === true)}
                            />
                            <Label>Show Email in Invoice</Label>
                        </div>
                        <field.TextField
                            label="Phone No."
                            name="phoneNumber"
                        />
                    </AccordionContent>
                </AccordionItem>
                <AccordionItem value="item-6">
                    <AccordionTrigger className="text-xl !no-underline">Account Details (Optional)</AccordionTrigger>
                    <AccordionContent className="grid grid-cols-2 gap-4 text-balance">
                        <field.TextField
                            label="Bank Name"
                            name="BankName"
                        />
                        <field.TextField
                            label="IFSC Code"
                            name="ifscCode"
                        />
                        <field.TextField
                            label="Account Number"
                            name="accountNumber"
                            className="col-span-2"
                        />
                    </AccordionContent>
                </AccordionItem>
            </Accordion>
        </form>
    );
}

(recreation guide: install breadcrumb and accordion component from shadcn and place the page code in a directory such as app/new/[type]/page.tsx and the form code in any directory with the correct path.)

the project is on nextjs 15+ and typescript.

i tried async await tho it should not be awaited. also asked a few AIs but none of them were able to solve the issue. you are also free to recommend other changes in the code apart from the issue as i am a new developer so I don’t know much about optimization and security.

Mobile chat widget panel shrinks and header/composer misaligns when keyboard opens (iOS Safari & Android Chrome)

I’m building a custom chat widget in JavaScript (similar to an in-page messenger). The widget attaches as a fixed panel with a sticky header, a scrollable message stream, and a composer at the bottom.

The problem happens on mobile when the keyboard opens:

On Android Chrome:

The panel shrinks when the keyboard opens.

Sometimes the underlying page becomes visible behind the panel.

If the address bar animates (top/bottom), the panel height jumps.

On iOS Safari:

The header (sticky) sometimes disappears when typing.

The panel height jiggles when the URL bar and keyboard animate.

Sometimes the composer is overshooting or undershooting the keyboard.

What I’ve tried

100vh, 100dvh, 100svh → panel still shrinks on keyboard open.

100lvh (Large Viewport Height) with a JS fallback:

const vv = window.visualViewport;
const large = vv.height + (vv.offsetTop || 0);
panel.style.setProperty('--lvh', (large/100) + 'px');

Locking page scroll while widget is open.

Raising the composer with a –kb CSS variable computed from visualViewport deltas.

This works better, but I still see edge cases:

On Android Chrome, panel briefly shrinks when the address bar animates.

On iOS Safari, header sometimes stops being sticky, and the panel still jiggles.

Goal

Panel should always cover the full viewport (no background page visible), even when the keyboard is open.

Composer should rise above the keyboard without overshooting.

Should work on both Android Chrome (address bar top/bottom) and iOS Safari (new & older versions).

Question

What’s the most robust pattern (CSS + JS) to keep a fixed chat widget panel:

Full height independent of the keyboard,

With a sticky header and bottom composer that adjust correctly,

On both Android Chrome and iOS Safari,

Without relying on UA-sniffing hacks?

here is my code

<div id="panel" class="panel">
  <div class="head">OAT Clinic</div>
  <div id="stream" class="stream">
    <!-- messages -->
  </div>
  <div id="composer" class="composer">
    <textarea id="msg" placeholder="How can we help you today?"></textarea>
  </div>
</div>
css
Copy code
:root {
  /* Large viewport height (JS sets this var) */
  --lvh: 1vh;
  /* Keyboard overlap (JS sets this var) */
  --kb: 0px;
}

/* Fullscreen panel that should NOT shrink when keyboard opens */
.panel {
  position: fixed;
  inset: 0;
  height: calc(var(--lvh) * 100);   /* prefer LVH when available */
  display: flex;
  flex-direction: column;
  background: #f7f8fb;
  overflow: hidden;
}

.head {
  position: sticky;
  top: 0;
  z-index: 1;
  padding: 9px 13px;
  background: white;
  border-bottom: 1px solid #e5e7eb;
}

.stream {
  flex: 1 1 auto;
  min-height: 0;                  /* critical for mobile scrolling */
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
}

.composer {
  position: sticky;               /* stays at bottom of panel */
  bottom: 0;
  border-top: 1px solid #e5e7eb;
  background: #f7f8fb;
  /* Lift above keyboard (JS sets --kb) */
  transform: translateY(calc(-1 * var(--kb)));
}

textarea {
  width: 100%;
  min-height: 54px;
  max-height: 120px;
  border: 1px solid #E1E1E1;
  border-radius: 12px;
  margin: 8px;
  padding: 8px 10px;
  resize: none;
}
js
Copy code
// Robust LVH + keyboard offset calculation
(function () {
  const panel = document.getElementById('panel');
  const composer = document.getElementById('composer');
  const msg = document.getElementById('msg');

  // Use Large Viewport Height when supported; JS fallback otherwise.
  function setLVH() {
    const vv = window.visualViewport;
    const large = vv ? (vv.height + (vv.offsetTop || 0)) : window.innerHeight;

    // Extra fallback using physical screen size for iOS quirks
    const alt = (window.screen && window.screen.height && window.devicePixelRatio)
      ? (window.screen.height / window.devicePixelRatio)
      : large;

    const lvh = Math.max(large, alt) / 100; // 1% unit
    panel.style.setProperty('--lvh', lvh + 'px');
  }

  let baseVVH = null; // visible height baseline (without keyboard)
  function setBaseVVH() {
    const vv = window.visualViewport;
    baseVVH = vv ? vv.height : window.innerHeight;
  }

  function computeKeyboardOffset() {
    const vv = window.visualViewport;
    let kb = 0;

    if (vv) {
      // How much of the window is occluded by the keyboard / chrome
      const occluded = Math.max(0, Math.round(window.innerHeight - vv.height - (vv.offsetTop || 0)));
      // Is the composer currently below the visible bottom?
      const visBottom = vv.height + (vv.offsetTop || 0);
      const compBottom = composer.getBoundingClientRect().bottom;
      const overlap = Math.max(0, Math.round(compBottom - visBottom));
      // Visible height delta vs baseline (helps with iOS “jiggle”)
      const delta = Math.max(0, Math.round((baseVVH || vv.height) - vv.height));

      kb = Math.max(occluded, overlap, delta);
    }

    // Thresholds to ignore small URL bar animations
    const IS_IOS = /iPad|iPhone|iPod/.test(navigator.userAgent);
    const THRESH = IS_IOS ? 120 : 80;
    if (kb < THRESH) kb = 0;

    // Cap to prevent overshoot
    const maxKb = Math.max(0, Math.round(panel.getBoundingClientRect().height - 56));
    kb = Math.min(kb, maxKb);

    panel.style.setProperty('--kb', kb + 'px');
  }

  function relayout() {
    setLVH();
    computeKeyboardOffset();
  }

  // Init
  setBaseVVH();
  setLVH();

  // Recalculate on viewport changes (keyboard / URL bar animation)
  if (window.visualViewport) {
    let timer = null;
    const onVV = () => {
      clearTimeout(timer);
      // Debounce to avoid rapid jiggle
      timer = setTimeout(() => {
        relayout();
      }, 50);
    };
    visualViewport.addEventListener('resize', onVV);
    visualViewport.addEventListener('scroll', onVV);
  }

  window.addEventListener('resize', relayout);
  window.addEventListener('orientationchange', () => setTimeout(relayout, 250));

  // Update baseline around focus/blur to track keyboard state changes
  msg.addEventListener('focus', () => { setBaseVVH(); computeKeyboardOffset(); });
  msg.addEventListener('blur', () => {
    panel.style.setProperty('--kb', '0px');
    setBaseVVH();
  });
})();

Whatsapp user initiated calling not working on live

I’ve implemented WhatsApp user initiated calling using WebRTC, nodeJS, ReactJS. Its working properly on local but not working on live, the i’m getting on both ends seems similar and normal, But on live the call the is getting auto hungup on after 20 seconds. On local its working Fine. Following is the code to handle user initiated calls from WhatsApp.

I’m following this documentation

https://developers.facebook.com/docs/whatsapp/cloud-api/calling/user-initiated-calls

const handleUserInitiatedCallRequest = async ({ req, res }) => {
  let db;
  try {
    // Extract and validate webhook data
    const change = req.body.entry?.[0]?.changes?.[0] || {};
    const call = change?.value?.calls?.[0] || {};
    const callId = call?.id;
    const receivedSdp = call.session?.sdp;
    const WHATSAPP_BUSINESS_ACCOUNT_ID =
      change.value?.metadata?.phone_number_id;

    let tenantIdRes = await getTenantIdFromUserMetaData(
      `whatsapp_id`,
      WHATSAPP_BUSINESS_ACCOUNT_ID
    );

    tenant_id = tenantIdRes?.tenant_id;

    const io = getIO();

    db = await getUserDBfromRequest({ tenant_id });

    let contactDetails =
      (await getChatUserDetailsById(db, call?.from, {
        platform: "whatsapp",
      })) || {};

    if (!callId || !receivedSdp || !WHATSAPP_BUSINESS_ACCOUNT_ID) {
      console.error("Missing required call data:", {
        callId,
        receivedSdp,
        WHATSAPP_BUSINESS_ACCOUNT_ID,
        chat_user: contactDetails,
      });
      return;
    }

    const callerName = call?.from || "Unknown";
    const callerNumber = call?.from || "Unknown";

    console.log(`Incoming WhatsApp call from ${callerName} (${callerNumber})`);
    io.emit(`whatsapp_calling_incoming_request_${tenant_id}`, {
      callId,
      callerName,
      callerNumber,
      chat_user: contactDetails,
    });

    setActiveCalls({
      callId,
      value: {
        whatsappOfferSdp: receivedSdp,
        phone_number_id: WHATSAPP_BUSINESS_ACCOUNT_ID,
      },
    });

    await initiateWebRTCBridge({ callId });
  } catch (error) {
    if (db) db.destroy();
    console.error("Error handling call request:", getAxiosError(error));
  }
};

async function initiateWebRTCBridge({ callId }) {
  try {
    const { getIO } = require("../../../../../sockets");
    let io = getIO();
    let browserOfferSdp = activeCalls[callId]?.browserOfferSdp;
    let whatsappOfferSdp = activeCalls[callId]?.whatsappOfferSdp;
    let browserSocket = io;
    let whatsappPc = activeCalls[callId]?.whatsappPc;
    if (!browserOfferSdp || !whatsappOfferSdp || !browserSocket) return;

    // --- Setup browser peer connection ---
    activeCalls[callId].browserPc = new RTCPeerConnection({
      iceServers: STUN_SERVERS,
    });
    activeCalls[callId].browserStream = new MediaStream();

    activeCalls[callId].browserPc.ontrack = (event) => {
      console.log("Audio track received from browser.");
      event.streams[0]
        .getTracks()
        .forEach((track) => activeCalls[callId].browserStream.addTrack(track));
    };

    activeCalls[callId].browserPc.onicecandidate = (event) => {
      if (event.candidate) {
        browserSocket.emit("whatsapp-calling-browser-candidate", {
          candidate: event.candidate,
          callId,
        });
      }
    };

    try {
      sdpTransform.parse(sanitizeSdp(browserOfferSdp)); // pinpoints the bad line
    } catch (e) {
      console.error("Bad SDP from browser:", e);
    }

    const cleanSdp = sanitizeSdp(browserOfferSdp);

    await activeCalls[callId].browserPc.setRemoteDescription(
      new RTCSessionDescription(cleanSdp, "offer")
    );
    console.log("Browser offer SDP set as remote description.");

    // --- Setup WhatsApp peer connection ---
    whatsappPc = new RTCPeerConnection({ iceServers: STUN_SERVERS });

    const waTrackPromise = new Promise((resolve, reject) => {
      const timeout = setTimeout(
        () => reject("Timed out waiting for WhatsApp track"),
        10000
      );
      whatsappPc.ontrack = (event) => {
        clearTimeout(timeout);
        console.log("Audio track received from WhatsApp.");
        whatsappStream = event.streams[0];
        resolve();
      };

      whatsappPc.onicecandidate = (e) => {
        console.log(
          "[WA-PC] cand:",
          e.candidate?.candidate || "end-of-candidates"
        );
      };
      whatsappPc.oniceconnectionstatechange = () => {
        console.log("[WA-PC] ice:", whatsappPc.iceConnectionState);
      };
    });

    await whatsappPc.setRemoteDescription(
      new RTCSessionDescription(whatsappOfferSdp, "offer")
    );
    console.log("WhatsApp offer SDP set as remote description.");

    // Forward browser mic to WhatsApp
    activeCalls[callId].browserStream?.getAudioTracks().forEach((track) => {
      console.log(track, "<<<<<<<<<<<<<<<< sending track to whatsapp");
      whatsappPc.addTrack(track, activeCalls[callId].browserStream);
    });
    console.log("Forwarded browser audio to WhatsApp.");

    // Wait for WhatsApp to send audio
    await waTrackPromise;

    // Forward WhatsApp audio to browser
    whatsappStream?.getAudioTracks().forEach((track) => {
      console.log(track, "<<<<<<<<<<<<<<<< sending track to browser");
      activeCalls[callId].browserPc.addTrack(track, whatsappStream);
    });

    // --- Create SDP answers for both peers ---
    const browserAnswer = await activeCalls[callId].browserPc.createAnswer();
    await activeCalls[callId].browserPc.setLocalDescription(browserAnswer);
    browserSocket.emit("whatsapp-calling-browser-answer", {
      sdp: browserAnswer.sdp,
      callId,
    });
    console.log("Browser answer SDP created and sent.");

    const waAnswer = await whatsappPc.createAnswer();
    await whatsappPc.setLocalDescription(waAnswer);
    const finalWaSdp = waAnswer.sdp.replace(
      "a=setup:actpass",
      "a=setup:active"
    );
    console.log("WhatsApp answer SDP prepared.");

    // Send pre-accept, and only proceed with accept if successful
    const preAcceptSuccess = await answerCallToWhatsApp(
      callId,
      finalWaSdp,
      "pre_accept",
      activeCalls?.[callId]?.phone_number_id
    );

    if (preAcceptSuccess) {
      await Promise.race([
        new Promise((r) => {
          const h = () => {
            if (
              ["connected", "completed"].includes(whatsappPc.iceConnectionState)
            ) {
              whatsappPc.removeEventListener("iceconnectionstatechange", h);
              r(true);
            }
          };
          whatsappPc.addEventListener("iceconnectionstatechange", h);
        }),
        waitForOutboundAudio(whatsappPc, 12000),
        new Promise((r) => setTimeout(r, 12000)),
      ]);

      setTimeout(async () => {
        const acceptSuccess = await answerCallToWhatsApp(
          callId,
          finalWaSdp,
          "accept",
          activeCalls?.[callId]?.phone_number_id
        );
        if (acceptSuccess && browserSocket) {
          browserSocket.emit("start-browser-timer");
        }
      }, 10);
    } else {
      console.error("Pre-accept failed. Aborting accept step.");
    }

    // Reset session state
    browserOfferSdp = null;
    whatsappOfferSdp = null;
  } catch (error) {
    console.error("Error initiating WebRTC bridge:", getAxiosError(error));
  }
}


async function waitForOutboundAudio(pc, ms = 12000) {
  const start = Date.now();
  while (Date.now() - start < ms) {
    const stats = await pc.getStats();
    for (const r of stats.values()) {
      if (
        r.type === "outbound-rtp" &&
        r.kind === "audio" &&
        (r.packetsSent || r.bytesSent)
      )
        return true;
    }
    await new Promise((r) => setTimeout(r, 300));
  }
  return false;
}

Following are the logs from live server

Incoming WhatsApp call from 9180975xxxxx (9180975xxxxx)
Received SDP offer from browser.
Audio track received from browser.
Browser offer SDP set as remote description.
Audio track received from WhatsApp.
WhatsApp offer SDP set as remote description.
MediaStreamTrack {
 _events: [Object: null prototype] {},
 _eventsCount: 0,
 _maxListeners: undefined,
 addEventListener: [Function: value],
 removeEventListener: [Function: value],
 uuid: 'c156af26-535c-492d-a2b1-75bcf2859547',
 streamId: '50ae6e2f-314a-45f4-9fd1-2b8ce773f552',
 remote: true,
 label: 'remote audio',
 kind: 'audio',
 id: 'c0e95fa9-f787-48a3-a31e-8e5f1a26beea',
 ssrc: 77909064,
 rid: undefined,
 header: undefined,
 codec: RTCRtpCodecParameters {
  payloadType: 111,
  mimeType: 'audio/opus',
  clockRate: 48000,
  channels: 2,
  rtcpFeedback: [ [RTCRtcpFeedback] ],
  parameters: 'minptime=10;useinbandfec=1',
  direction: 'all'
 },
 enabled: true,
 onReceiveRtp: Event {
  event: { stack: [Array], promiseStack: [], eventId: 1 },
  ended: false,
  onended: undefined,
  onerror: [Function: value],
  execute: [Function: value],
  complete: [Function: value],
  error: [Function: value],
  allUnsubscribe: [Function: value],
  subscribe: [Function: value],
  queuingSubscribe: [Function: value],
  once: [Function: value],
  watch: [Function: value],
  asPromise: [Function: value]
 },
 onReceiveRtcp: Event {
  event: { stack: [], promiseStack: [], eventId: 0 },
  ended: false,
  onended: undefined,
  onerror: [Function: value],
  execute: [Function: value],
  complete: [Function: value],
  error: [Function: value],
  allUnsubscribe: [Function: value],
  subscribe: [Function: value],
  queuingSubscribe: [Function: value],
  once: [Function: value],
  watch: [Function: value],
  asPromise: [Function: value]
 },
 onSourceChanged: Event {
  event: { stack: [], promiseStack: [], eventId: 0 },
  ended: false,
  onended: undefined,
  onerror: [Function: value],
  execute: [Function: value],
  complete: [Function: value],
  error: [Function: value],
  allUnsubscribe: [Function: value],
  subscribe: [Function: value],
  queuingSubscribe: [Function: value],
  once: [Function: value],
  watch: [Function: value],
  asPromise: [Function: value]
 },
 stopped: false,
 muted: true,
 stop: [Function: value],
 writeRtp: [Function: value],
 Symbol(shapeMode): false,
 Symbol(kCapture): false
} <<<<<<<<<<<<<<<< sending track to whatsapp
Forwarded browser audio to WhatsApp.
MediaStreamTrack {
 _events: [Object: null prototype] {},
 _eventsCount: 0,
 _maxListeners: undefined,
 addEventListener: [Function: value],
 removeEventListener: [Function: value],
 uuid: '58d00710-efb0-46d8-826c-2d6bdc94e0e2',
 streamId: undefined,
 remote: true,
 label: 'remote audio',
 kind: 'audio',
 id: '23ce1f0e-c369-41c0-8086-bdc092f1daa6',
 ssrc: 2795528785,
 rid: undefined,
 header: undefined,
 codec: RTCRtpCodecParameters {
  payloadType: 111,
  mimeType: 'audio/opus',
  clockRate: 48000,
  channels: 2,
  rtcpFeedback: [ [RTCRtcpFeedback] ],
  parameters: 'maxaveragebitrate=20000;maxplaybackrate=16000;minptime=20;sprop-maxcapturerate=16000;useinbandfec=1',
  direction: 'all'
 },
 enabled: true,
 onReceiveRtp: Event {
  event: { stack: [Array], promiseStack: [], eventId: 1 },
  ended: false,
  onended: undefined,
  onerror: [Function: value],
  execute: [Function: value],
  complete: [Function: value],
  error: [Function: value],
  allUnsubscribe: [Function: value],
  subscribe: [Function: value],
  queuingSubscribe: [Function: value],
  once: [Function: value],
  watch: [Function: value],
  asPromise: [Function: value]
 },
 onReceiveRtcp: Event {
  event: { stack: [], promiseStack: [], eventId: 0 },
  ended: false,
  onended: undefined,
  onerror: [Function: value],
  execute: [Function: value],
  complete: [Function: value],
  error: [Function: value],
  allUnsubscribe: [Function: value],
  subscribe: [Function: value],
  queuingSubscribe: [Function: value],
  once: [Function: value],
  watch: [Function: value],
  asPromise: [Function: value]
 },
 onSourceChanged: Event {
  event: { stack: [], promiseStack: [], eventId: 0 },
  ended: false,
  onended: undefined,
  onerror: [Function: value],
  execute: [Function: value],
  complete: [Function: value],
  error: [Function: value],
  allUnsubscribe: [Function: value],
  subscribe: [Function: value],
  queuingSubscribe: [Function: value],
  once: [Function: value],
  watch: [Function: value],
  asPromise: [Function: value]
 },
 stopped: false,
 muted: true,
 stop: [Function: value],
 writeRtp: [Function: value],
 Symbol(shapeMode): false,
 Symbol(kCapture): false
} <<<<<<<<<<<<<<<< sending track to browser
Received browser-candidate from browser. {
 candidate: 'candidate:1995127518 1 udp 2122260223 192.168.1.17 62065 typ host generation 0 ufrag PKLr network-id 1 network-cost 10',
 sdpMid: '0',
 sdpMLineIndex: 0,
 usernameFragment: 'PKLr'
} wacid.HBgMOTE4MDk3NTM0Njg0FQIAEhggOUU2RUM3QzM0NkQzQ0ZGRjZBNzk1NEE5NDY4OUVCOUYcGAw5MTkxNjk5NTk5NTkVAgAVAgA=
Received browser-candidate from browser. {
 candidate: 'candidate:884647390 1 udp 41885695 172.237.33.131 41118 typ relay raddr 116.72.105.227 rport 62065 generation 0 ufrag PKLr network-id 1 network-cost 10',
 sdpMid: '0',
 sdpMLineIndex: 0,
 usernameFragment: 'PKLr'
} wacid.HBgMOTE4MDk3NTM0Njg0FQIAEhggOUU2RUM3QzM0NkQzQ0ZGRjZBNzk1NEE5NDY4OUVCOUYcGAw5MTkxNjk5NTk5NTkVAgAVAgA=
Received browser-candidate from browser. {
 candidate: 'candidate:136632390 1 tcp 1518280447 192.168.1.17 9 typ host tcptype active generation 0 ufrag PKLr network-id 1 network-cost 10',
 sdpMid: '0',
 sdpMLineIndex: 0,
 usernameFragment: 'PKLr'
} wacid.HBgMOTE4MDk3NTM0Njg0FQIAEhggOUU2RUM3QzM0NkQzQ0ZGRjZBNzk1NEE5NDY4OUVCOUYcGAw5MTkxNjk5NTk5NTkVAgAVAgA=
Browser answer SDP created and sent.
Received browser-candidate from browser. {
 candidate: 'candidate:907102327 1 udp 1686052607 116.72.105.227 62065 typ srflx raddr 192.168.1.17 rport 62065 generation 0 ufrag PKLr network-id 1 network-cost 10',
 sdpMid: '0',
 sdpMLineIndex: 0,
 usernameFragment: 'PKLr'
} wacid.HBgMOTE4MDk3NTM0Njg0FQIAEhggOUU2RUM3QzM0NkQzQ0ZGRjZBNzk1NEE5NDY4OUVCOUYcGAw5MTkxNjk5NTk5NTkVAgAVAgA=
WhatsApp answer SDP prepared.
WhatsApp API Pre-accept Request Payload: {
 url: 'https://graph.facebook.com/v21.0/115017851530658/calls',
 method: 'POST',
 data: {
  messaging_product: 'whatsapp',
  call_id: 'wacid.HBgMOTE4MDk3NTM0Njg0FQIAEhggOUU2RUM3QzM0NkQzQ0ZGRjZBNzk1NEE5NDY4OUVCOUYcGAw5MTkxNjk5NTk5NTkVAgAVAgA=',
  action: 'pre_accept',
  session: {
   sdp_type: 'answer',
   sdp: 'v=0rn' +
    'o=- 59754882 0 IN IP4 0.0.0.0rn' +
    's=-rn' +
    't=0 0rn' +
    'a=group:BUNDLE audiorn' +
    'a=extmap-allow-mixedrn' +
    'a=msid-semantic:WMS *rn' +
    'm=audio 9 UDP/TLS/RTP/SAVPF 111rn' +
    'c=IN IP4 0.0.0.0rn' +
    'a=ice-ufrag:1ba9rn' +
    'a=ice-pwd:1b204f1058a8ccc5068917rn' +
    'a=ice-options:tricklern' +
    'a=fingerprint:sha-256 18:21:C7:E5:37:E6:AE:D3:DE:C5:DE:08:55:9D:13:CE:F8:46:AE:0F:54:18:84:00:A6:5C:60:FE:DE:C1:8D:A6rn' +
    'a=setup:activern' +
    'a=sendrecvrn' +
    'a=mid:audiorn' +
    'a=msid:7e5d7d63-467d-4bd2-b41c-8d08d08b1220 23ce1f0e-c369-41c0-8086-bdc092f1daa6rn' +
    'a=rtcp:9 IN IP4 0.0.0.0rn' +
    'a=rtcp-muxrn' +
    'a=ssrc:4129580503 cname:c6837d9e-0285-4fb2-81ac-f684d95751a9rn' +
    'a=rtpmap:111 opus/48000/2rn' +
    'a=rtcp-fb:111 transport-ccrn' +
    'a=fmtp:111 maxaveragebitrate=20000;maxplaybackrate=16000;minptime=20;sprop-maxcapturerate=16000;useinbandfec=1rn'
  }
 }
}
Pre-accept API Response: { messaging_product: 'whatsapp', success: true }
WhatsApp API Pre-accept Request Payload: {
 url: 'https://graph.facebook.com/v21.0/115017851530658/calls',
 method: 'POST',
 data: {
  messaging_product: 'whatsapp',
  call_id: 'wacid.HBgMOTE4MDk3NTM0Njg0FQIAEhggOUU2RUM3QzM0NkQzQ0ZGRjZBNzk1NEE5NDY4OUVCOUYcGAw5MTkxNjk5NTk5NTkVAgAVAgA=',
  action: 'accept',
  session: {
   sdp_type: 'answer',
   sdp: 'v=0rn' +
    'o=- 59754882 0 IN IP4 0.0.0.0rn' +
    's=-rn' +
    't=0 0rn' +
    'a=group:BUNDLE audiorn' +
    'a=extmap-allow-mixedrn' +
    'a=msid-semantic:WMS *rn' +
    'm=audio 9 UDP/TLS/RTP/SAVPF 111rn' +
    'c=IN IP4 0.0.0.0rn' +
    'a=ice-ufrag:1ba9rn' +
    'a=ice-pwd:1b204f1058a8ccc5068917rn' +
    'a=ice-options:tricklern' +
    'a=fingerprint:sha-256 18:21:C7:E5:37:E6:AE:D3:DE:C5:DE:08:55:9D:13:CE:F8:46:AE:0F:54:18:84:00:A6:5C:60:FE:DE:C1:8D:A6rn' +
    'a=setup:activern' +
    'a=sendrecvrn' +
    'a=mid:audiorn' +
    'a=msid:7e5d7d63-467d-4bd2-b41c-8d08d08b1220 23ce1f0e-c369-41c0-8086-bdc092f1daa6rn' +
    'a=rtcp:9 IN IP4 0.0.0.0rn' +
    'a=rtcp-muxrn' +
    'a=ssrc:4129580503 cname:c6837d9e-0285-4fb2-81ac-f684d95751a9rn' +
    'a=rtpmap:111 opus/48000/2rn' +
    'a=rtcp-fb:111 transport-ccrn' +
    'a=fmtp:111 maxaveragebitrate=20000;maxplaybackrate=16000;minptime=20;sprop-maxcapturerate=16000;useinbandfec=1rn'
  }
 }
}
Pre-accept API Response: { messaging_product: 'whatsapp', success: true }

Following are the logs i’m getting on local

Incoming WhatsApp call from 91809xxxxxxx (91809xxxxxxx)
Received SDP offer from browser.
Audio track received from browser.
Browser offer SDP set as remote description.
Audio track received from WhatsApp.
WhatsApp offer SDP set as remote description.
MediaStreamTrack {
 _events: [Object: null prototype] {},
 _eventsCount: 0,
 _maxListeners: undefined,
 addEventListener: [Function: value],
 removeEventListener: [Function: value],
 uuid: 'c156af26-535c-492d-a2b1-75bcf2859547',
 streamId: '50ae6e2f-314a-45f4-9fd1-2b8ce773f552',
 remote: true,
 label: 'remote audio',
 kind: 'audio',
 id: 'c0e95fa9-f787-48a3-a31e-8e5f1a26beea',
 ssrc: 77909064,
 rid: undefined,
 header: undefined,
 codec: RTCRtpCodecParameters {
  payloadType: 111,
  mimeType: 'audio/opus',
  clockRate: 48000,
  channels: 2,
  rtcpFeedback: [ [RTCRtcpFeedback] ],
  parameters: 'minptime=10;useinbandfec=1',
  direction: 'all'
 },
 enabled: true,
 onReceiveRtp: Event {
  event: { stack: [Array], promiseStack: [], eventId: 1 },
  ended: false,
  onended: undefined,
  onerror: [Function: value],
  execute: [Function: value],
  complete: [Function: value],
  error: [Function: value],
  allUnsubscribe: [Function: value],
  subscribe: [Function: value],
  queuingSubscribe: [Function: value],
  once: [Function: value],
  watch: [Function: value],
  asPromise: [Function: value]
 },
 onReceiveRtcp: Event {
  event: { stack: [], promiseStack: [], eventId: 0 },
  ended: false,
  onended: undefined,
  onerror: [Function: value],
  execute: [Function: value],
  complete: [Function: value],
  error: [Function: value],
  allUnsubscribe: [Function: value],
  subscribe: [Function: value],
  queuingSubscribe: [Function: value],
  once: [Function: value],
  watch: [Function: value],
  asPromise: [Function: value]
 },
 onSourceChanged: Event {
  event: { stack: [], promiseStack: [], eventId: 0 },
  ended: false,
  onended: undefined,
  onerror: [Function: value],
  execute: [Function: value],
  complete: [Function: value],
  error: [Function: value],
  allUnsubscribe: [Function: value],
  subscribe: [Function: value],
  queuingSubscribe: [Function: value],
  once: [Function: value],
  watch: [Function: value],
  asPromise: [Function: value]
 },
 stopped: false,
 muted: true,
 stop: [Function: value],
 writeRtp: [Function: value],
 Symbol(shapeMode): false,
 Symbol(kCapture): false
} <<<<<<<<<<<<<<<< sending track to whatsapp
Forwarded browser audio to WhatsApp.
MediaStreamTrack {
 _events: [Object: null prototype] {},
 _eventsCount: 0,
 _maxListeners: undefined,
 addEventListener: [Function: value],
 removeEventListener: [Function: value],
 uuid: '58d00710-efb0-46d8-826c-2d6bdc94e0e2',
 streamId: undefined,
 remote: true,
 label: 'remote audio',
 kind: 'audio',
 id: '23ce1f0e-c369-41c0-8086-bdc092f1daa6',
 ssrc: 2795528785,
 rid: undefined,
 header: undefined,
 codec: RTCRtpCodecParameters {
  payloadType: 111,
  mimeType: 'audio/opus',
  clockRate: 48000,
  channels: 2,
  rtcpFeedback: [ [RTCRtcpFeedback] ],
  parameters: 'maxaveragebitrate=20000;maxplaybackrate=16000;minptime=20;sprop-maxcapturerate=16000;useinbandfec=1',
  direction: 'all'
 },
 enabled: true,
 onReceiveRtp: Event {
  event: { stack: [Array], promiseStack: [], eventId: 1 },
  ended: false,
  onended: undefined,
  onerror: [Function: value],
  execute: [Function: value],
  complete: [Function: value],
  error: [Function: value],
  allUnsubscribe: [Function: value],
  subscribe: [Function: value],
  queuingSubscribe: [Function: value],
  once: [Function: value],
  watch: [Function: value],
  asPromise: [Function: value]
 },
 onReceiveRtcp: Event {
  event: { stack: [], promiseStack: [], eventId: 0 },
  ended: false,
  onended: undefined,
  onerror: [Function: value],
  execute: [Function: value],
  complete: [Function: value],
  error: [Function: value],
  allUnsubscribe: [Function: value],
  subscribe: [Function: value],
  queuingSubscribe: [Function: value],
  once: [Function: value],
  watch: [Function: value],
  asPromise: [Function: value]
 },
 onSourceChanged: Event {
  event: { stack: [], promiseStack: [], eventId: 0 },
  ended: false,
  onended: undefined,
  onerror: [Function: value],
  execute: [Function: value],
  complete: [Function: value],
  error: [Function: value],
  allUnsubscribe: [Function: value],
  subscribe: [Function: value],
  queuingSubscribe: [Function: value],
  once: [Function: value],
  watch: [Function: value],
  asPromise: [Function: value]
 },
 stopped: false,
 muted: true,
 stop: [Function: value],
 writeRtp: [Function: value],
 Symbol(shapeMode): false,
 Symbol(kCapture): false
} <<<<<<<<<<<<<<<< sending track to browser
Received browser-candidate from browser. {
 candidate: 'candidate:1995127518 1 udp 2122260223 192.168.1.17 62065 typ host generation 0 ufrag PKLr network-id 1 network-cost 10',
 sdpMid: '0',
 sdpMLineIndex: 0,
 usernameFragment: 'PKLr'
} wacid.HBgMOTE4MDk3NTM0Njg0FQIAEhggOUU2RUM3QzM0NkQzQ0ZGRjZBNzk1NEE5NDY4OUVCOUYcGAw5MTkxNjk5NTk5NTkVAgAVAgA=
Received browser-candidate from browser. {
 candidate: 'candidate:884647390 1 udp 41885695 172.237.33.131 41118 typ relay raddr 116.72.105.227 rport 62065 generation 0 ufrag PKLr network-id 1 network-cost 10',
 sdpMid: '0',
 sdpMLineIndex: 0,
 usernameFragment: 'PKLr'
} wacid.HBgMOTE4MDk3NTM0Njg0FQIAEhggOUU2RUM3QzM0NkQzQ0ZGRjZBNzk1NEE5NDY4OUVCOUYcGAw5MTkxNjk5NTk5NTkVAgAVAgA=
Received browser-candidate from browser. {
 candidate: 'candidate:136632390 1 tcp 1518280447 192.168.1.17 9 typ host tcptype active generation 0 ufrag PKLr network-id 1 network-cost 10',
 sdpMid: '0',
 sdpMLineIndex: 0,
 usernameFragment: 'PKLr'
} wacid.HBgMOTE4MDk3NTM0Njg0FQIAEhggOUU2RUM3QzM0NkQzQ0ZGRjZBNzk1NEE5NDY4OUVCOUYcGAw5MTkxNjk5NTk5NTkVAgAVAgA=
Browser answer SDP created and sent.
Received browser-candidate from browser. {
 candidate: 'candidate:907102327 1 udp 1686052607 116.72.105.227 62065 typ srflx raddr 192.168.1.17 rport 62065 generation 0 ufrag PKLr network-id 1 network-cost 10',
 sdpMid: '0',
 sdpMLineIndex: 0,
 usernameFragment: 'PKLr'
} wacid.HBgMOTE4MDk3NTM0Njg0FQIAEhggOUU2RUM3QzM0NkQzQ0ZGRjZBNzk1NEE5NDY4OUVCOUYcGAw5MTkxNjk5NTk5NTkVAgAVAgA=
WhatsApp answer SDP prepared.
WhatsApp API Pre-accept Request Payload: {
 url: 'https://graph.facebook.com/v21.0/115017851530658/calls',
 method: 'POST',
 data: {
  messaging_product: 'whatsapp',
  call_id: 'wacid.HBgMOTE4MDk3NTM0Njg0FQIAEhggOUU2RUM3QzM0NkQzQ0ZGRjZBNzk1NEE5NDY4OUVCOUYcGAw5MTkxNjk5NTk5NTkVAgAVAgA=',
  action: 'pre_accept',
  session: {
   sdp_type: 'answer',
   sdp: 'v=0rn' +
    'o=- 59754882 0 IN IP4 0.0.0.0rn' +
    's=-rn' +
    't=0 0rn' +
    'a=group:BUNDLE audiorn' +
    'a=extmap-allow-mixedrn' +
    'a=msid-semantic:WMS *rn' +
    'm=audio 9 UDP/TLS/RTP/SAVPF 111rn' +
    'c=IN IP4 0.0.0.0rn' +
    'a=ice-ufrag:1ba9rn' +
    'a=ice-pwd:1b204f1058a8ccc5068917rn' +
    'a=ice-options:tricklern' +
    'a=fingerprint:sha-256 18:21:C7:E5:37:E6:AE:D3:DE:C5:DE:08:55:9D:13:CE:F8:46:AE:0F:54:18:84:00:A6:5C:60:FE:DE:C1:8D:A6rn' +
    'a=setup:activern' +
    'a=sendrecvrn' +
    'a=mid:audiorn' +
    'a=msid:7e5d7d63-467d-4bd2-b41c-8d08d08b1220 23ce1f0e-c369-41c0-8086-bdc092f1daa6rn' +
    'a=rtcp:9 IN IP4 0.0.0.0rn' +
    'a=rtcp-muxrn' +
    'a=ssrc:4129580503 cname:c6837d9e-0285-4fb2-81ac-f684d95751a9rn' +
    'a=rtpmap:111 opus/48000/2rn' +
    'a=rtcp-fb:111 transport-ccrn' +
    'a=fmtp:111 maxaveragebitrate=20000;maxplaybackrate=16000;minptime=20;sprop-maxcapturerate=16000;useinbandfec=1rn'
  }
 }
}
Pre-accept API Response: { messaging_product: 'whatsapp', success: true }
WhatsApp API Pre-accept Request Payload: {
 url: 'https://graph.facebook.com/v21.0/115017851530658/calls',
 method: 'POST',
 data: {
  messaging_product: 'whatsapp',
  call_id: 'wacid.HBgMOTE4MDk3NTM0Njg0FQIAEhggOUU2RUM3QzM0NkQzQ0ZGRjZBNzk1NEE5NDY4OUVCOUYcGAw5MTkxNjk5NTk5NTkVAgAVAgA=',
  action: 'accept',
  session: {
   sdp_type: 'answer',
   sdp: 'v=0rn' +
    'o=- 59754882 0 IN IP4 0.0.0.0rn' +
    's=-rn' +
    't=0 0rn' +
    'a=group:BUNDLE audiorn' +
    'a=extmap-allow-mixedrn' +
    'a=msid-semantic:WMS *rn' +
    'm=audio 9 UDP/TLS/RTP/SAVPF 111rn' +
    'c=IN IP4 0.0.0.0rn' +
    'a=ice-ufrag:1ba9rn' +
    'a=ice-pwd:1b204f1058a8ccc5068917rn' +
    'a=ice-options:tricklern' +
    'a=fingerprint:sha-256 18:21:C7:E5:37:E6:AE:D3:DE:C5:DE:08:55:9D:13:CE:F8:46:AE:0F:54:18:84:00:A6:5C:60:FE:DE:C1:8D:A6rn' +
    'a=setup:activern' +
    'a=sendrecvrn' +
    'a=mid:audiorn' +
    'a=msid:7e5d7d63-467d-4bd2-b41c-8d08d08b1220 23ce1f0e-c369-41c0-8086-bdc092f1daa6rn' +
    'a=rtcp:9 IN IP4 0.0.0.0rn' +
    'a=rtcp-muxrn' +
    'a=ssrc:4129580503 cname:c6837d9e-0285-4fb2-81ac-f684d95751a9rn' +
    'a=rtpmap:111 opus/48000/2rn' +
    'a=rtcp-fb:111 transport-ccrn' +
    'a=fmtp:111 maxaveragebitrate=20000;maxplaybackrate=16000;minptime=20;sprop-maxcapturerate=16000;useinbandfec=1rn'
  }
 }
}
Pre-accept API Response: { messaging_product: 'whatsapp', success: true }