Trouble remote viewing Blue Iris

dmadesign

n3wb
Joined
Aug 11, 2015
Messages
16
Reaction score
0
My wife is currently unable to remotely view our Blue Iris server at her work. When she uses Fire Fox it gets stuck on everything saying busy:
Loading wed content... Busy
Loading vector graphics... Busy
Loading H.264 player... Busy
Checking session status... Busy
Loading server status... Busy
Loading camera list... Busy.
I am currently using the most recent version of Blue Iris 4 4.8.6.3, and as far as I know nothing has changed since she was last able to access the web server. I've restarted my Blue Iris PC several times as well as I've had her delete the browsers cookies and cache but that didn't help. I am able to access it from my work which leads me to believe that it is something wrong with her computer. Any ideas on what I can try next to get this working again?
 

mikeynags

Known around here
Joined
Mar 14, 2017
Messages
1,034
Reaction score
939
Location
CT
Are you able to view remotely as well?


Sent from my iPhone using Tapatalk
 

dmadesign

n3wb
Joined
Aug 11, 2015
Messages
16
Reaction score
0
Are you able to view remotely as well?

Yes. It's kinda weird, I can view it remotely on my phone using a web page or using the Android app as well as a computer at work but she said her IOS app doesn't work correctly and that she can't access it on the computer at work as well. The app keeps saying "signal lost,retrying..." when she tries to view our driveway camera. I've tried rebooting everything at the house as well as deleting and re-adding the cameras with no luck.
 

dmadesign

n3wb
Joined
Aug 11, 2015
Messages
16
Reaction score
0
I forgot to mention that I am running Blue Iris through a reverse proxy (NGINX) but I've had it setup this way for 2 years without any problems until now.
 

bp2008

Staff member
Joined
Mar 10, 2014
Messages
12,666
Reaction score
14,005
Location
USA
@dmadesign So the iOS app doesn't work either? Does it function correctly when it is not connected to wifi at work? Some business firewalls are overly protective.
 

dmadesign

n3wb
Joined
Aug 11, 2015
Messages
16
Reaction score
0
@dmadesign So the iOS app doesn't work either? Does it function correctly when it is not connected to wifi at work? Some business firewalls are overly protective.
It doesn't work correctly. Just when you select the driveway camera or "all camera" you see the message. If you try and click on the screen where the picture should be it actually kicks you out of the app due to too many failed login attempts. She uses LTE when she is at work at they don't have WIFI but it doesn't work correctly an home when it is on WIFI either. I googled a couple of cases where someone had the same issue and deleting/reinstalling the camera seemed to have worked for them but I tried that last night and it still isn't working.
 

bp2008

Staff member
Joined
Mar 10, 2014
Messages
12,666
Reaction score
14,005
Location
USA
@dmadesign The two failed script loads (which you censored the host name in) are the only lines related to UI3 in that console output. Check the Network tab and look for failed requests (ignore any that are for a "ui3-local-overrides" file). See what the response status is for these.

Here's an example of UI3 loading correctly in firefox:

 

dmadesign

n3wb
Joined
Aug 11, 2015
Messages
16
Reaction score
0
She only got 13 requests and the only failed request was 1 "ui3-local-overrides".
 

bp2008

Staff member
Joined
Mar 10, 2014
Messages
12,666
Reaction score
14,005
Location
USA
Based on that it looks like the ui3.js file might not have loaded properly. Even compressed, it is supposed to be closer to 155 KB transferred. Certainly not 28.07 KB. There may be further clues if she tried opening ui3.js in a new browser tab. (right click it and there should be an option to open it in a new tab).

The correct ui3.js file should begin with this:

Code:
/* eslint eqeqeq: 0, no-extra-parens: 0, semi: 0, no-redeclare: 0, no-empty: 0, valid-jsdoc: 0 */
/// <reference path="ui3-local-overrides.js" />
/// <reference path="libs-src/jquery-1.12.4.js" />
/// <reference path="libs-ui3.js" />
/// This web interface is licensed under the GNU LGPL Version 3
and end with this:

Code:
function BindEventsPassive(ele, events, handler)
{
    BindEvents(ele, events, handler, { passive: true });
}

Most other files loaded from cache, so it could be helpful to check the Disable cache box there in the Network tab of dev tools.
 

dmadesign

n3wb
Joined
Aug 11, 2015
Messages
16
Reaction score
0
I think this is the file you asked for. It looks a little different than what you posted though.

Code:
/* eslint eqeqeq: 0, no-extra-parens: 0, semi: 0, no-redeclare: 0, no-empty: 0 */
/// <reference path="ui3-local-overrides.js" />
/// <reference path="libs-src/jquery-1.12.4.js" />
/// <reference path="libs-ui3.js" />
/// This web interface is licensed under the GNU LGPL Version 3
"use strict";
var developerMode = false;

if (navigator.cookieEnabled)
{
    NavRemoveUrlParams("session");
}
///////////////////////////////////////////////////////////////
// Feature Detect /////////////////////////////////////////////
///////////////////////////////////////////////////////////////
var _browser_is_ie = -1;
function BrowserIsIE()
{
    if (_browser_is_ie === -1)
        _browser_is_ie = /MSIE \d|Trident.*rv:/.test(navigator.userAgent) ? 1 : 0;
    return _browser_is_ie == 1;
}
var _browser_is_edge = -1;
function BrowserIsEdge()
{
    if (_browser_is_edge === -1)
        _browser_is_edge = window.navigator.userAgent.indexOf(" Edge/") > -1 ? 1 : 0;
    return _browser_is_edge === 1;
}
function BrowserEdgeVersion()
{
    if (BrowserIsEdge())
    {
        var m = window.navigator.userAgent.match(/ Edge\/([0-9\.,]+)/);
        if (m)
            return m[1];
    }
    return null;
}
var _browser_is_firefox = -1;
function BrowserIsFirefox()
{
    if (_browser_is_firefox === -1)
        _browser_is_firefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1 ? 1 : 0;
    return _browser_is_firefox === 1;
}
var h264_playback_supported = false;
var audio_playback_supported = false;
var web_workers_supported = false;
var export_blob_supported = false;
var exporting_clips_to_avi_supported = false;
var fetch_supported = false;
var readable_stream_supported = false;
var webgl_supported = false;
var web_audio_supported = false;
var web_audio_buffer_source_supported = false;
var web_audio_buffer_copyToChannel_supported = false;
var web_audio_requires_user_input = false;
var fullscreen_supported = false;
var browser_is_ios = false;
var browser_is_android = false;
var pnacl_player_supported = false;
var mse_mp4_h264_supported = false;
var mse_mp4_aac_supported = false;
var vibrate_supported = false;
var web_audio_autoplay_disabled = false;
var cookies_accessible = false;
var fetch_streams_cant_close_bug = false;
function DoUIFeatureDetection()
{
    try
    {
        requestAnimationFramePolyFill();
        if (!isCanvasSupported())
            MissingRequiredFeature("HTML5 Canvas"); // Excludes IE 8
        else
        {
            // All critical tests pass
            // Non-critical tests can run here and store their results in global vars.
            cookies_accessible = testCookieFunctionality();
            browser_is_ios = BrowserIsIOS();
            browser_is_android = BrowserIsAndroid();
            web_workers_supported = typeof Worker !== "undefined";
            export_blob_supported = detectIfCanExportBlob();
            fetch_supported = typeof fetch == "function";
            if (fetch_supported && BrowserIsEdge())
            {
                var edgeVersion = BrowserEdgeVersion();
                if (edgeVersion && parseInt(edgeVersion) >= 17)
                    fetch_streams_cant_close_bug = true;
            }
            readable_stream_supported = typeof ReadableStream === "function";
            webgl_supported = detectWebGLContext();
            detectAudioSupport();
            vibrate_supported = detectVibrateSupport();
            fullscreen_supported = ((document.documentElement.requestFullscreen || document.documentElement.msRequestFullscreen || document.documentElement.mozRequestFullScreen || document.documentElement.webkitRequestFullscreen) && (document.exitFullscreen || document.msExitFullscreen || document.mozCancelFullScreen || document.webkitExitFullscreen)) ? true : false;
            h264_playback_supported = web_workers_supported && fetch_supported && readable_stream_supported && webgl_supported;
            audio_playback_supported = h264_playback_supported && web_audio_supported && web_audio_buffer_source_supported && web_audio_buffer_copyToChannel_supported;
            exporting_clips_to_avi_supported = h264_playback_supported && export_blob_supported;

            if (h264_playback_supported)
            {
                pnacl_player_supported = detectIfPnaclSupported();
                var mse_support = detectMSESupport();
                mse_mp4_h264_supported = (mse_support & 1) > 0;
                mse_mp4_aac_supported = (mse_support & 2) > 0; // Not yet used
            }

            $(function ()
            {
                var ul_root = $('<ul></ul>');
                if (!h264_playback_supported)
                {
                    var ul = $('<ul></ul>');
                    if (!web_workers_supported)
                        ul.append('<li>Web Workers</li>');
                    if (!fetch_supported)
                        ul.append('<li>Fetch API</li>');
                    if (!readable_stream_supported)
                        ul.append('<li>ReadableStream</li>');
                    if (!webgl_supported)
                        ul.append('<li>WebGL</li>');
                    ul_root.append($('<li>The H.264 video player requires these unsupported features:</li>').append(ul));
                }
                if (!audio_playback_supported)
                {
                    var ul = $('<ul></ul>');
                    if (!h264_playback_supported)
                        ul.append('<li>H.264 Video Player</li>');
                    if (!web_audio_supported)
                        ul.append('<li>Web Audio API</li>');
                    if (!web_audio_buffer_source_supported)
                        ul.append('<li>AudioBufferSourceNode</li>');
                    if (!web_audio_buffer_copyToChannel_supported)
                        ul.append('<li>AudioBuffer.copyToChannel</li>');
                    ul_root.append($('<li>The audio player requires these unsupported features:</li>').append(ul));
                }
                if (!isLocalStorageEnabled())
                {
                    ul_root.append('<li>Local Storage is disabled or unavailable in your browser. Settings will not be saved between sessions.</li>');
                }
                if (!navigator.cookieEnabled)
                {
                    ul_root.append('<li>Cookies are disabled in this browser. The browser cache will be less effective, making UI3 load at sub-optimal speed.</li>');
                }
                if (!fullscreen_supported)
                {
                    ul_root.append('<li>Fullscreen mode is not supported.</li>');
                }
                if (browser_is_ios)
                {
                    ul_root.append('<li>Context menus are not supported.</li>');
                }
                if (!isHtml5HistorySupported())
                {
                    ul_root.append('<li>The back button will not close the current clip or camera, like it does on most other platforms.</li>');
                }
                if (!exporting_clips_to_avi_supported)
                {
                    ul_root.append('<li>Exporting clips to AVI is not supported.</li>');
                }
                if (fetch_streams_cant_close_bug)
                {
                    ul_root.append('<li>This browser has a compatibility issue which makes H.264 streams not close properly, leading to stability problems.  H.264 playback is disabled by default, but may be re-enabled in UI Settings -&gt; Video Player.</li>');
                }
                if (ul_root.children().length > 0)
                {
                    var $opt = $('#optionalFeaturesNotSupported');
                    $opt.append(ul_root);
                    $opt.show();
                }
                var $videoPlayers = $("<ul></ul>");
                $videoPlayers.append("<li>Jpeg</li>");
                if (h264_playback_supported)
                    $videoPlayers.append("<li>H.264 via JavaScript</li>");
                if (pnacl_player_supported)
                    $videoPlayers.append("<li>H.264 via NaCl</li>");
                if (mse_mp4_h264_supported)
                    $videoPlayers.append("<li>H.264 via HTML5</li>");
                $('#videoPlayersSupported').append($videoPlayers);
            });
            return;
        }
        // A critical test failed
        location.href = "/jpegpull.htm" + currentServer.GetLocalSessionArg("?");
    }
    catch (ex)
    {
        alert("Unknown error during feature detection. This web browser is likely incompatible.\n" + ex);
        try
        {
            console.log(ex);
        }
        catch (ex2)
        {
        }
    }
}
function MissingRequiredFeature(featureName, description)
{
    alert("This web interface requires a feature that is unavailable or disabled in your web browser.\n\nMissing feature: " + featureName + (description ? ". " + description : "") + "\n\nYou will be redirected to a simpler web interface.");
}
function isCanvasSupported()
{
    var elem = document.createElement('canvas');
    return !!(elem.getContext && elem.getContext('2d'));
}
function testCookieFunctionality()
{
    try
    {
        if (!navigator.cookieEnabled)
            return false;
        var session = $.cookie("session");
        if (session)
            return true;
        $.cookie("session", "test", { path: "/" });
        session = $.cookie("session")
        $.cookie("session", "", { path: "/" });
        return session === "test";
    } catch (e) { }
    return false;
}
function isLocalStorageEnabled()
{
    try // May throw exception if local storage is disabled by browser settings!
    {
        var key = "local_storage_test_item";
        localStorage.setItem(key, key);
        localStorage.removeItem(key);
        return true;
    } catch (e)
    {
        return false;
    }
}
function isHtml5HistorySupported()
{
    try
    {
        if (BrowserIsIOSChrome())
            return false; // Chrome on iOS has too many history bugs.
        if (BrowserIsAndroid())
            return false; // If the back button is overridden on Android, it can't be used to close the browser while UI3 is the first item in history.
        if (window.history && typeof window.history.state == "object" && typeof window.history.pushState == "function" && typeof window.history.replaceState == "function")
            return true;
        return false;
    } catch (e)
    {
        return false;
    }
}
function requestAnimationFramePolyFill()
{
    try
    {
        if (typeof requestAnimationFrame != "function")
            requestAnimationFrame = function (callback) { setTimeout(callback, 33); };
        return true;
    }
    catch (e)
    {
        return false;
    }
}
function detectWebGLContext()
{
    var canvas = document.createElement("canvas");
    var gl = canvas.getContext("webgl")
        || canvas.getContext("experimental-webgl");
    return gl && gl instanceof WebGLRenderingContext;
}
function detectIfCanExportBlob()
{
    try
    {
        return typeof window.URL !== "undefined" && typeof window.URL.revokeObjectURL === "function" && typeof Blob !== "undefined";
    }
    catch (ex)
    {
    }
    return false;
}
function detectIfPnaclSupported()
{
    try
    {
        return navigator.mimeTypes['application/x-pnacl'] !== undefined;
    }
    catch (ex) { }
    return false;
}
function detectMSESupport()
{
    try
    {
        if (window.MediaSource)
        {
            if (MediaSource.isTypeSupported("video/mp4; codecs=\"avc1.640033\""))
                return 1;
        }
    }
    catch (ex) { }
    return 0;
}
function detectAudioSupport()
{
    try
    {
        // Web Audio (camera sound)
        var AudioContext = window.AudioContext || window.webkitAudioContext;
        if (AudioContext)
        {
            var context = new AudioContext();

            if (typeof context.createGain === "function")
            {
                web_audio_supported = true;
                web_audio_autoplay_disabled = context.state === "suspended";

                if (typeof context.createBuffer === "function" && typeof context.createBufferSource === "function")
                {
                    var buffer = context.createBuffer(1, 1, 22050);
                    if (buffer)
                    {
                        web_audio_buffer_source_supported = true;
                        if (typeof buffer.copyFromChannel === "function" && typeof buffer.copyToChannel === "function")
                            web_audio_buffer_copyToChannel_supported = true;
                    }
                }
            }
        }
    }
    catch (ex) { }
}
function detectVibrateSupport()
{
    try
    {
        return typeof window.navigator.vibrate === "function";
    }
    catch (ex) { }
    return false;
}

DoUIFeatureDetection();
///////////////////////////////////////////////////////////////
// Globals (most of them) /////////////////////////////////////
///////////////////////////////////////////////////////////////
var toaster = new Toaster();
var ajaxHistoryManager;
var loadingHelper = new LoadingHelper();
var touchEvents = new TouchEventHelper();
var clipboardHelper;
var uiSizeHelper = null;
var uiSettingsPanel = null;
var pcmPlayer = null;
var diskUsageGUI = null;
var systemConfig = null;
var cameraListDialog = null;
var clipProperties = null;
var clipDownloadDialog = null;
var statusBars = null;
var dropdownBoxes = null;
var leftBarBools = null;
var cornerStatusIcons = null;
var genericQualityHelper = null;
var jpegQualityHelper = null;
var streamingProfileUI = null;
var ptzButtons = null;
var playbackHeader = null;
var exportControls = null;
var seekBar = null;
var playbackControls = null;
var clipTimeline = null;
var hotkeys = null;
var dateFilter = null;
var hlsPlayer = null;
var maximizedModeController = null;
var fullScreenModeController = null;
var canvasContextMenu = null;
var calendarContextMenu = null;
var clipListContextMenu = null;
var togglableContextMenus = null;
var cameraConfig = null;
var videoPlayer = null;
var imageRenderer = null;
var cameraNameLabels = null;
var sessionManager = null;
var statusLoader = null;
var cameraListLoader = null;
var clipLoader = null;
var clipThumbnailVideoPreview = null;
var nerdStats = null;
var sessionTimeout = null;

var currentPrimaryTab = "";

var togglableUIFeatures =
    [
        // The uniqueId is also used in the name of a setting which remembers the enabled state.
        // If you add a new togglabe UI feature here, also add the corresponding default setting value.
        // [selector, uniqueId, displayName, onToggle, extraMenuButtons, shouldDisableToggler, labels]
        ["#volumeBar", "volumeBar", "Volume Controls", function (enabled)
        {
            statusBars.setEnabled("volume", enabled);
            if (enabled)
                $("#volumeBar").removeClass("disabled")
            else
                $("#volumeBar").addClass("disabled")
        }, null, null]
        , ["#profileStatusBox", "profileStatus", "Profile Status Controls", function (enabled) { statusLoader.SetProfileButtonsEnabled(enabled); }, null, null]
        , ["#stoplightBtn", "stopLight", "Stoplight Controls", function (enabled) { statusLoader.SetStoplightButtonEnabled(enabled); }, null, null]
        , ["#globalScheduleBox", "globalSchedule", "Schedule Controls", function (enabled) { dropdownBoxes.setEnabled("schedule", enabled); }, null, null]
        // The PTZ Controls hot area specifically does not include the main button pad because a context menu on that pad would break touchscreen usability
        , [".ptzpreset", "ptzControls", "PTZ Controls", function (enabled) { ptzButtons.setEnabled(enabled); }
            , [{
                getName: function (ele) { return "Goto Preset " + ele.getAttribute("presetnum") + htmlEncode(ptzButtons.GetPresetDescription(ele.getAttribute("presetnum"), true)); }
                , action: function (ele) { ptzButtons.PTZ_goto_preset(ele.presetnum); }
                , shouldDisable: function () { return !ptzButtons.isEnabledNow(); }
            }
                , {
                getName: function (ele) { return "Set Preset " + ele.getAttribute("presetnum"); }
                , action: function (ele) { ptzButtons.PresetSet(ele.getAttribute("presetnum")); }
                , shouldDisable: function () { return !ptzButtons.isEnabledNow(); }
            }]
            , function () { return !videoPlayer.Loading().image.ptz; }
        ]
        , ["#playbackHeader", "clipNameLabel", "Clip Name", function (enabled)
        {
            if (enabled)
                $("#clipNameHeading").show();
            else
                $("#clipNameHeading").hide();
        }, null, null, ["Show", "Hide", "Toggle"]]
    ];

///////////////////////////////////////////////////////////////
// Notes that require BI changes //////////////////////////////
///////////////////////////////////////////////////////////////

// TODO: Around May 11, 2018 with BI 4.7.4.1, Blue Iris began enforcing a default jpeg height of 720px sourced from the Streaming 0 profile's frame size setting and I haven't been able to talk the developer out of it.  UI3 now works around this by appending w=99999 to jpeg requests that are intended to be native resolution.  If this limit goes away, the workarounds should be removed.  The workarounds are tagged with "LOC0" (approximately 9 locations).  Since shortly after, this affects quality too, so a q=85 argument has been added at LOC0 locations too.

///////////////////////////////////////////////////////////////
// High priority notes ////////////////////////////////////////
///////////////////////////////////////////////////////////////

///////////////////////////////////////////////////////////////
// Low priority notes /////////////////////////////////////////
///////////////////////////////////////////////////////////////

// CONSIDER: Android Chrome > Back button can't close the browser if there is no history, so the back button override is disabled on Android.  Also disabled on iOS for similar bugs.
// CONSIDER: Seeking while paused in Chrome, the canvas sometimes shows the image scaled using nearest-neighbor.
// CONSIDER: Add "Remote Control" menu based on that which is available in iOS and Android apps.
// CONSIDER: Stop using ImageToDataUrl for the clip thumbnail mouseover popup, now that clip thumbnails are cacheable.  I'm not sure there is a point though.
// CONSIDER: Sometimes the clip list scrolls down when you're trying to work with it, probably related to automatic refreshing addings items at the top.

///////////////////////////////////////////////////////////////
// Settings ///////////////////////////////////////////////////
///////////////////////////////////////////////////////////////
var CameraLabelTextValues = {
    Name: "Name",
    ShortName: "Short Name",
    Both: "Name (Short Name)"
}
var CameraLabelPositionValues = {
    Above: "Above",
    Top: "Top",
    Bottom: "Bottom",
    Below: "Below"
}
var H264PlayerOptions = {
    JavaScript: "JavaScript",
    HTML5: "HTML5",
    NaCl_HWVA_Auto: "NaCl (Auto hw accel)",
    NaCl_HWVA_No: "NaCl (No hw accel)",
    NaCl_HWVA_Yes: "NaCl (Only hw accel)"
}
function GetH264PlayerOptions()
{
    var arr = new Array();
    if (mse_mp4_h264_supported)
        arr.push(H264PlayerOptions.HTML5);
    if (pnacl_player_supported)
    {
        arr.push(H264PlayerOptions.NaCl_HWVA_Auto);
        arr.push(H264PlayerOptions.NaCl_HWVA_No);
        arr.push(H264PlayerOptions.NaCl_HWVA_Yes);
    }
    arr.push(H264PlayerOptions.JavaScript);
    return arr;
}
function GetDefaultH264PlayerOption()
{
    if (BrowserIsEdge())
        return H264PlayerOptions.JavaScript;
    else if (BrowserIsFirefox())
        return H264PlayerOptions.JavaScript;
    return GetH264PlayerOptions()[0];
}
var HTML5DelayCompensationOptions = {
    None: "None",
    Weak: "Weak",
    Normal: "Normal",
    Strong: "Strong"
}
var Zoom1xOptions = {
    Camera: "Camera",
    Stream: "Stream"
}
var settings = null;
var settingsCategoryList = ["General Settings", "Video Player", "Clip / Alert Icons", "Event-Triggered Icons", "Event-Triggered Sounds", "Hotkeys", "Camera Labels", "Digital Zoom", "Extra"]; // Create corresponding "ui3_cps_uiSettings_category_" default when adding a category here.
var defaultSettings =
    [
        {
            key: "ui3_defaultTab"
            , value: "live"
        }
        , {
            key: "ui3_defaultCameraGroupId"
            , value: "index"
        }
        , {
            key: "ui3_audioVolume"
            , value: 0
        }
        , {
            key: "ui3_audioMute"
            , value: "1"
        }
        , {
            key: "ui3_streamingQuality"
            , value: "720p^"
        }
        , {
            key: "ui3_playback_reverse"
            , value: "0"
        }
        , {
            key: "ui3_playback_speed"
            , value: "1"
        }
        , {
            key: "ui3_playback_autoplay"
            , value: "0"
        }
        , {
            key: "ui3_playback_loop"
            , value: "0"
        }
        , {
            key: "ui3_recordings_flagged_only"
            , value: "0"
        }
        , {
            key: "ui3_cliplist_larger_thumbnails"
            , value: "0"
        }
        , {
            key: "ui3_cliplist_mouseover_thumbnails"
            , value: "1"
        }
        , {
            key: "ui3_clip_export_withAudio"
            , value: "1"
        }
        , {
            key: "bi_rememberMe"
            , value: "0"
        }
        , {
            key: "bi_username"
            , value: ""
        }
        , {
            key: "bi_password"
            , value: ""
        }
        , {
            key: "ui3_webcasting_disabled_dontShowAgain"
            , value: "0"
        }
        , {
            key: "ui3_feature_enabled_volumeBar" // ui3_feature_enabled keys are tied to unique IDs in togglableUIFeatures
            , value: "1"
        }
        , {
            key: "ui3_feature_enabled_profileStatus"
            , value: "1"
        }
        , {
            key: "ui3_feature_enabled_stopLight"
            , value: "1"
        }
        , {
            key: "ui3_feature_enabled_globalSchedule"
            , value: "1"
        }
        , {
            key: "ui3_feature_enabled_ptzControls"
            , value: "1"
        }
        , {
            key: "ui3_feature_enabled_clipNameLabel"
            , value: "1"
        }
        , {
            key: "ui3_collapsible_ptz"
            , value: "1"
        }
        , {
            key: "ui3_collapsible_profileStatus"
            , value: "1"
        }
        , {
            key: "ui3_collapsible_schedule"
            , value: "1"
        }
        , {
            key: "ui3_collapsible_currentGroup"
            , value: "1"
        }
        , {
            key: "ui3_collapsible_streamingQuality"
            , value: "1"
        }
        , {
            key: "ui3_collapsible_serverStatus"
            , value: "1"
        }
        , {
            key: "ui3_collapsible_filterRecordings"
            , value: "1"
        }
        , {
            key: "ui3_cps_info_visible"
            , value: "1"
        }
        , {
            key: "ui3_cps_gs_visible"
            , value: "1"
        }
        , {
            key: "ui3_cps_mt_visible"
            , value: "1"
        }
        , {
            key: "ui3_cps_mro_visible"
            , value: "1"
        }
        , {
            key: "ui3_cps_mgmt_visible"
            , value: "1"
        }
        , {
            key: "ui3_cps_uiSettings_category_General_Settings_visible"
            , value: "1"
        }
        , {
            key: "ui3_cps_uiSettings_category_Video_Player_visible"
            , value: "1"
        }
        , {
            key: "ui3_cps_uiSettings_category_Clip___Alert_Icons_visible"
            , value: "1"
        }
        , {
            key: "ui3_cps_uiSettings_category_Event_Triggered_Icons_visible"
            , value: "1"
        }
        , {
            key: "ui3_cps_uiSettings_category_Event_Triggered_Sounds_visible"
            , value: "1"
        }
        , {
            key: "ui3_cps_uiSettings_category_Hotkeys_visible"
            , value: "1"
        }
        , {
            key: "ui3_cps_uiSettings_category_Camera_Labels_visible"
            , value: "1"
        }
        , {
            key: "ui3_cps_uiSettings_category_Digital_Zoom_visible"
            , value: "1"
        }
        , {
            key: "ui3_cps_uiSettings_category_Extra_visible"
            , value: "1"
        }
        , {
            key: "ui3_streamingProfileArray"
            , value: "[]"
            , category: "Streaming Profiles" // This category isn't shown in UI Settings, but has special-case logic in ui3-local-overrides.js export.
        }
        , {
            key: "ui3_clipPreviewEnabled"
            , value: "1"
            , inputType: "checkbox"
            , label: "Clip Preview Animations"
            , hint: "When enabled, mousing over the alert/clip list shows a rapid animated preview.  Video streaming performance may suffer while the animation is active."
            , category: "General Settings"
        }
        , {
            key: "ui3_timeout"
            , value: 10
            , inputType: "number"
            , minValue: 0
            , maxValue: 525600
            , label: "The UI will close itself after this many minutes of inactivity. (0 to disable)"
            , category: "General Settings"
        }
        , {
            key: "ui3_preferred_ui_scale"
            , value: "Auto"
            , inputType: "select"
            , options: ["Auto", "Large", "Medium", "Small", "Smaller"]
            , label: "Preferred UI Scale"
            , onChange: OnChange_ui3_preferred_ui_scale
            , category: "General Settings"
        }
        , {
            key: "ui3_time24hour"
            , value: "0"
            , inputType: "checkbox"
            , label: '24-Hour Time'
            , onChange: OnChange_ui3_time24hour
            , category: "General Settings"
        }
        , {
            key: "ui3_doubleClick_behavior"
            , value: "Recordings"
            , inputType: "select"
            , options: ["None", "Live View", "Recordings", "Both"]
            , label: 'Double-Click to Fullscreen<br><a href="javascript:UIHelp.LearnMore(\'Double-Click to Fullscreen\')">(learn more)</a>'
            , category: "Video Player"
        }
        , {
            key: "ui3_edge_fetch_bug_h264_enable"
            , value: "0"
            , inputType: "checkbox"
            , label: '<span style="color:#FF0000;font-weight:bold">Enable H.264 Player</span><div class="settingDesc">This browser has known compatiblity issues. <a href="javascript:UIHelp.LearnMore(\'Edge Fetch Bug\')">(learn more)</a></div>'
            , onChange: OnChange_ui3_edge_fetch_bug_h264_enable
            , preconditionFunc: Precondition_ui3_edge_fetch_bug_h264_enable
            , category: "Video Player"
        }
        , {
            key: "ui3_h264_choice2"
            , value: GetDefaultH264PlayerOption()
            , inputType: "select"
            , options: GetH264PlayerOptions()
            , label: 'H.264 Player <a href="javascript:UIHelp.LearnMore(\'H.264 Player Options\')">(learn more)</a>'
            , onChange: OnChange_ui3_h264_choice2
            , preconditionFunc: Precondition_ui3_h264_choice2
            , category: "Video Player"
        }
        , {
            key: "ui3_streamingProfileBitRateMax"
            , value: -1
            , inputType: "number"
            , minValue: -1
            , maxValue: 8192
            , label: 'Maximum H.264 Kbps<div class="settingDesc">(10-8192, disabled if less than 10)</div>'
            , hint: "Useful for slow connections. Audio streams are not affected by this setting."
            , onChange: OnChange_ui3_streamingProfileBitRateMax
            , preconditionFunc: Precondition_ui3_streamingProfileBitRateMax
            , category: "Video Player"
        }
        , {
            key: "ui3_html5_delay_compensation"
            , value: HTML5DelayCompensationOptions.Normal
            , inputType: "select"
            , options: [HTML5DelayCompensationOptions.None, HTML5DelayCompensationOptions.Weak, HTML5DelayCompensationOptions.Normal, HTML5DelayCompensationOptions.Strong]
            , label: 'HTML5 Video Delay Compensation <a href="javascript:UIHelp.LearnMore(\'HTML5 Video Delay Compensation\')">(learn more)</a>'
            , preconditionFunc: Precondition_ui3_html5_delay_compensation
            , category: "Video Player"
        }
        , {
            key: "ui3_force_gop_1sec"
            , value: "1"
            , inputType: "checkbox"
            , label: '<span style="color:#FF0000">Firefox Stutter Fix</span> <a href="javascript:UIHelp.LearnMore(\'Firefox Stutter Fix\')">(learn more)</a>'
            , onChange: OnChange_ui3_force_gop_1sec
            , preconditionFunc: Precondition_ui3_force_gop_1sec
            , category: "Video Player"
        }
        , {
            key: "ui3_jpegSupersampling"
            , value: 1
            , minValue: 0.01
            , maxValue: 2
            , step: 0.01
            , inputType: "range"
            , label: 'Jpeg Video Supersampling Factor'
            , changeOnStep: true
            , hint: "(Default: 1)\n\nJpeg video frames loaded by UI3 will have their dimensions scaled by this amount.\n\nLow values save bandwidth, while high values improve quality slightly."
            , category: "Video Player"
        }
        , {
            key: "ui3_web_audio_autoplay_warning"
            , value: "0"
            , inputType: "checkbox"
            , label: 'Warn if audio playback requires user input'
            , hint: 'When set to "Yes", a full-page overlay will appear if camera audio playback requires user input. Otherwise, the audio icon will simply turn red.'
            , category: "Video Player"
        }
        , {
            key: "ui3_clipicon_trigger_motion"
            , value: "0"
            , inputType: "checkbox"
            , label: '<svg class="icon clipicon noflip"><use xlink:href="#svg_mio_run"></use></svg> for motion-triggered alerts'
            , category: "Clip / Alert Icons"
        }
        , {
            key: "ui3_clipicon_trigger_audio"
            , value: "1"
            , inputType: "checkbox"
            , label: '<svg class="icon clipicon noflip"><use xlink:href="#svg_mio_volumeUp"></use></svg> for audio-triggered alerts'
            , category: "Clip / Alert Icons"
        }
        , {
            key: "ui3_clipicon_trigger_external"
            , value: "1"
            , inputType: "checkbox"
            , label: '<svg class="icon clipicon"><use xlink:href="#svg_x5F_Alert2"></use></svg> for externally-triggered alerts'
            , category: "Clip / Alert Icons"
        }
        , {
            key: "ui3_clipicon_trigger_group"
            , value: "1"
            , inputType: "checkbox"
            , label: '<svg class="icon clipicon noflip"><use xlink:href="#svg_mio_quilt"></use></svg> for group-triggered alerts'
            , category: "Clip / Alert Icons"
        }
        , {
            key: "ui3_clipicon_clip_audio"
            , value: "1"
            , inputType: "checkbox"
            , label: '<svg class="icon clipicon noflip"><use xlink:href="#svg_mio_volumeUp"></use></svg> for clips with audio'
            , category: "Clip / Alert Icons"
        }
        , {
            key: "ui3_clipicon_clip_backingup"
            , value: "0"
            , inputType: "checkbox"
            , label: '<svg class="icon clipicon noflip"><use xlink:href="#svg_mio_cloudUploading"></use></svg> for clips that are being backed up'
            , category: "Clip / Alert Icons"
        }
        , {
            key: "ui3_clipicon_clip_backup"
            , value: "0"
            , inputType: "checkbox"
            , label: '<svg class="icon clipicon noflip"><use xlink:href="#svg_mio_cloudUploaded"></use></svg> for clips that have been backed up'
            , category: "Clip / Alert Icons"
        }
        , {
            key: "ui3_clipicon_protect"
            , value: "1"
            , inputType: "checkbox"
            , label: '<svg class="icon clipicon noflip"><use xlink:href="#svg_mio_lock"></use></svg> for protected items'
            , category: "Clip / Alert Icons"
        }
        , {
            key: "ui3_comment_eventTriggeredIcons_Heading"
            , value: ""
            , inputType: "comment"
            , comment: GenerateEventTriggeredIconsComment
            , category: "Event-Triggered Icons"
        }
        , {
            key: "ui3_icon_motion"
            , value: "0"
            , inputType: "checkbox"
            , label: '<svg class="icon clipicon noflip" style="fill: rgba(120,205,255,1)"><use xlink:href="#svg_mio_run"></use></svg> on Motion Detected'
            , category: "Event-Triggered Icons"
        }
        , {
            key: "ui3_icon_trigger"
            , value: "0"
            , inputType: "checkbox"
            , label: '<svg class="icon clipicon" style="fill: rgba(255,64,64,1)"><use xlink:href="#svg_x5F_Alert2"></use></svg> on Camera Triggered'
            , category: "Event-Triggered Icons"
        }
        , {
            key: "ui3_icon_recording"
            , value: "0"
            , inputType: "checkbox"
            , label: '<svg class="icon clipicon" style="fill: rgba(255,0,0,1)"><use xlink:href="#svg_x5F_Stoplight"></use></svg> on Camera Recording'
            , hint: "Does not appear when viewing a group of cameras"
            , category: "Event-Triggered Icons"
        }
        , {
            key: "ui3_icons_extraVisibility"
            , value: "0"
            , inputType: "checkbox"
            , label: 'Extra Visibility For Icons'
            , onChange: OnChange_ui3_icons_extraVisibility
            , category: "Event-Triggered Icons"
        }
        , {
            key: "ui3_comment_eventTriggeredSounds_Heading"
            , value: ""
            , inputType: "comment"
            , comment: GenerateEventTriggeredSoundsComment
            , category: "Event-Triggered Sounds"
        }
        , {
            key: "ui3_sound_motion"
            , value: "None"
            , inputType: "select"
            , options: []
            , getOptions: getBISoundOptions
            , alwaysRefreshOptions: true
            , label: 'Motion Detected'
            , onChange: function () { biSoundPlayer.PlayEvent("motion"); }
            , category: "Event-Triggered Sounds"
        }
        , {
            key: "ui3_sound_trigger"
            , value: "None"
            , inputType: "select"
            , options: []
            , getOptions: getBISoundOptions
            , alwaysRefreshOptions: true
            , label: 'Camera Triggered'
            , onChange: function () { biSoundPlayer.PlayEvent("trigger"); }
            , category: "Event-Triggered Sounds"
        }
        , {
            key: "ui3_eventSoundVolume"
            , value: 100
            , minValue: 0
            , maxValue: 100
            , step: 1
            , unitLabel: "%"
            , inputType: "range"
            , label: 'Sound Effect Volume'
            , onChange: function () { biSoundPlayer.AdjustVolume(); }
            , changeOnStep: true
            , category: "Event-Triggered Sounds"
        }
        , {
            key: "ui3_hotkey_maximizeVideoArea"
            , value: "1|0|0|192" // 192: tilde (~`)
            , hotkey: true
            , label: "Maximize Video Area"
            , hint: "Shows or hides the left and top control bars. This can be triggered on page load via the url parameter \"maximize=1\"."
            , actionDown: BI_Hotkey_MaximizeVideoArea
            , category: "Hotkeys"
        }
        , {
            key: "ui3_hotkey_togglefullscreen"
            , value: "0|0|0|192" // 192: tilde (~`)
            , hotkey: true
            , label: "Full Screen Mode"
            , hint: "Toggles Full Screen Mode and shows or hides the left and top control bars according to UI defaults."
            , actionDown: BI_Hotkey_FullScreen
            , category: "Hotkeys"
        }
        , {
            key: "ui3_hotkey_tab_live"
            , value: "0|0|0|112" // 112: F1
            , hotkey: true
            , label: "Load Tab: Live View"
            , hint: "Opens the Live View tab"
            , actionDown: BI_Hotkey_Load_Tab_Live
            , category: "Hotkeys"
        }
        , {
            key: "ui3_hotkey_tab_alerts"
            , value: "0|0|0|113" // 113: F2
            , hotkey: true
            , label: "Load Tab: Alerts"
            , hint: "Opens the Alerts tab"
            , actionDown: BI_Hotkey_Load_Tab_Alerts
            , category: "Hotkeys"
        }
        , {
            key: "ui3_hotkey_tab_clips"
            , value: "0|0|0|114" // 114: F3
            , hotkey: true
            , label: "Load Tab: Clips"
            , hint: "Opens the Clips tab"
            , actionDown: BI_Hotkey_Load_Tab_Clips
            , category: "Hotkeys"
        }
        , {
            key: "ui3_hotkey_cameraLabels"
            , value: "1|0|0|76" // 76: L
            , hotkey: true
            , label: "Toggle Camera Labels"
            , actionDown: BI_Hotkey_Toggle_Camera_Labels
            , category: "Hotkeys"
        }
        , {
            key: "ui3_hotkey_downloadframe"
            , value: "1|0|0|83" // 83: S
            , hotkey: true
            , label: "Download Frame"
            , actionDown: BI_Hotkey_DownloadFrame
            , category: "Hotkeys"
        }
        , {
            key: "ui3_hotkey_toggleMute"
            , value: "1|0|0|77" // 77: M
            , hotkey: true
            , label: "Toggle Camera Mute"
            , actionDown: BI_Hotkey_ToggleMute
            , category: "Hotkeys"
        }
        , {
            key: "ui3_hotkey_nextCamera"
            , value: "0|0|0|190" // 190: . (period)
            , hotkey: true
            , label: "Next Camera"
            , hint: "Manually cycles to the next camera when a camera is maximized."
            , actionDown: BI_Hotkey_NextCamera
            , category: "Hotkeys"
        }
        , {
            key: "ui3_hotkey_prevCamera"
            , value: "0|0|0|188" // 188: , (comma)
            , hotkey: true
            , label: "Previous Camera"
            , hint: "Manually cycles to the previous camera when a camera is maximized."
            , actionDown: BI_Hotkey_PreviousCamera
            , category: "Hotkeys"
        }
        , {
            key: "ui3_hotkey_nextGroup"
            , value: "1|0|1|190" // 190: CTRL + SHIFT + . (period)
            , hotkey: true
            , label: "Next Group"
            , hint: "Manually loads your next group or cycle stream."
            , actionDown: BI_Hotkey_NextGroup
            , category: "Hotkeys"
        }
        , {
            key: "ui3_hotkey_prevGroup"
            , value: "1|0|1|188" // 188: CTRL + SHIFT + , (comma)
            , hotkey: true
            , label: "Previous Group"
            , hint: "Manually loads your previous group or cycle stream."
            , actionDown: BI_Hotkey_PreviousGroup
            , category: "Hotkeys"
        }
        , {
            key: "ui3_hotkey_playpause"
            , value: "0|0|0|32" // 32: space
            , hotkey: true
            , label: "Play/Pause"
            , hint: "Plays or pauses the current recording."
            , actionDown: BI_Hotkey_PlayPause
            , category: "Hotkeys"
        }
        , {
            key: "ui3_hotkey_toggleReverse"
            , value: "0|0|0|8" // 8: backspace
            , hotkey: true
            , label: "Reverse Playback"
            , hint: "Toggles between Forward and Reverse playback."
            , actionDown: BI_Hotkey_ToggleReverse
            , category: "Hotkeys"
        }
        , {
            key: "ui3_hotkey_newerClip"
            , value: "0|0|0|38" // 38: up arrow
            , hotkey: true
            , label: "Next Clip"
            , hint: "Load the next clip, higher up in the list."
            , actionDown: BI_Hotkey_NextClip
            , category: "Hotkeys"
        }
        , {
            key: "ui3_hotkey_olderClip"
            , value: "0|0|0|40" // 40: down arrow
            , hotkey: true
            , label: "Previous Clip"
            , hint: "Load the previous clip, lower down in the list."
            , actionDown: BI_Hotkey_PreviousClip
            , category: "Hotkeys"
        }
        , {
            key: "ui3_hotkey_skipAhead"
            , value: "0|0|0|39" // 39: right arrow
            , hotkey: true
            , label: "Skip Ahead"
            , hint: "Skips ahead in the current recording by a configurable number of seconds."
            , actionDown: BI_Hotkey_SkipAhead
            , category: "Hotkeys"
        }
        , {
            key: "ui3_hotkey_skipBack"
            , value: "0|0|0|37" // 37: left arrow
            , hotkey: true
            , label: "Skip Back"
            , hint: "Skips back in the current recording by a configurable number of seconds."
            , actionDown: BI_Hotkey_SkipBack
            , category: "Hotkeys"
        }
        , {
            key: "ui3_skipAmount"
            , value: 10
            , inputType: "number"
            , minValue: 0
            , maxValue: 9999
            , label: "Skip Time (seconds)"
            , hint: "[0.01-9999] (default: 10) \r\nNumber of seconds to skip forward and back when using hotkeys to skip."
            , onChange: OnChange_ui3_skipAmount
            , category: "Hotkeys"
        }
        , {
            key: "ui3_hotkey_skipAhead1Frame"
            , value: "0|0|0|190" // 190: . (period)
            , hotkey: true
            , label: "Skip Ahead 1 Frame"
            , hint: "Skips ahead in the current recording by approximately one frame."
            , actionDown: BI_Hotkey_SkipAhead1Frame
            , category: "Hotkeys"
        }
        , {
            key: "ui3_hotkey_skipBack1Frame"
            , value: "0|0|0|188" // 188: , (comma)
            , hotkey: true
            , label: "Skip Back 1 Frame"
            , hint: "Skips back in the current recording by approximately one frame."
            , actionDown: BI_Hotkey_SkipBack1Frame
            , category: "Hotkeys"
        }
        , {
            key: "ui3_hotkey_playback_faster"
            , value: "0|0|0|221" // 221: ]
            , hotkey: true
            , label: "Playback Faster"
            , hint: "Increases clip playback speed"
            , actionDown: BI_Hotkey_PlaybackFaster
            , category: "Hotkeys"
        }
        , {
            key: "ui3_hotkey_playback_slower"
            , value: "0|0|0|219" // 219: [
            , hotkey: true
            , label: "Playback Slower"
            , hint: "Decreases clip playback speed"
            , actionDown: BI_Hotkey_PlaybackSlower
            , category: "Hotkeys"
        }
        , {
            key: "ui3_hotkey_close_clip"
            , value: "0|0|0|27" // 27: escape
            , hotkey: true
            , label: "Close Clip"
            , hint: "Closes the current clip."
            , actionDown: BI_Hotkey_CloseClip
            , category: "Hotkeys"
        }
        , {
            key: "ui3_hotkey_close_camera"
            , value: "0|0|0|27" // 27: escape
            , hotkey: true
            , label: "Close Camera"
            , hint: "Closes the current live camera and returns to the group view."
            , actionDown: BI_Hotkey_CloseCamera
            , category: "Hotkeys"
        }
        , {
            key: "ui3_hotkey_digitalZoomIn"
            , value: "0|0|1|187" // 187: =
            , hotkey: true
            , label: "Digital Zoom In"
            , hint: "This has the same function as rolling a mouse wheel one notch."
            , actionDown: BI_Hotkey_DigitalZoomIn
            , allowRepeatKey: true
            , category: "Hotkeys"
        }
        , {
            key: "ui3_hotkey_digitalZoomOut"
            , value: "0|0|1|189" // : 189: -
            , hotkey: true
            , label: "Digital Zoom Out"
            , hint: "This has the same function as rolling a mouse wheel one notch."
            , actionDown: BI_Hotkey_DigitalZoomOut
            , allowRepeatKey: true
            , category: "Hotkeys"
        }
        , {
            key: "ui3_hotkey_digitalPanUp"
            , value: "0|0|1|38" // 38: up arrow
            , hotkey: true
            , label: "Digital Pan Up"
            , hint: "If zoomed in with digital zoom, pans up."
            , actionDown: BI_Hotkey_DigitalPanUp
            , actionUp: BI_Hotkey_DigitalPanUp_Up
            , category: "Hotkeys"
        }
        , {
            key: "ui3_hotkey_digitalPanDown"
            , value: "0|0|1|40" // 40: down arrow
            , hotkey: true
            , label: "Digital Pan Down"
            , hint: "If zoomed in with digital zoom, pans down."
            , actionDown: BI_Hotkey_DigitalPanDown
            , actionUp: BI_Hotkey_DigitalPanDown_Up
            , category: "Hotkeys"
        }
        , {
            key: "ui3_hotkey_digitalPanLeft"
            , value: "0|0|1|37" // 37: left arrow
            , hotkey: true
            , label: "Digital Pan Left"
            , hint: "If zoomed in with digital zoom, pans left."
            , actionDown: BI_Hotkey_DigitalPanLeft
            , actionUp: BI_Hotkey_DigitalPanLeft_Up
            , category: "Hotkeys"
        }
        , {
            key: "ui3_hotkey_digitalPanRight"
            , value: "0|0|1|39" // 39: right arrow
            , hotkey: true
            , label: "Digital Pan Right"
            , hint: "If zoomed in with digital zoom, pans right."
            , actionDown: BI_Hotkey_DigitalPanRight
            , actionUp: BI_Hotkey_DigitalPanRight_Up
            , category: "Hotkeys"
        }
        , {
            key: "ui3_hotkey_ptzUp"
            , value: "0|0|0|38" // 38: up arrow
            , hotkey: true
            , label: "PTZ Up"
            , hint: "If the current live camera is PTZ, moves the camera up."
            , actionDown: BI_Hotkey_PtzUp
            , actionUp: BI_Hotkey_PtzUp_Up
            , category: "Hotkeys"
        }
        , {
            key: "ui3_hotkey_ptzDown"
            , value: "0|0|0|40" // 40: down arrow
            , hotkey: true
            , label: "PTZ Down"
            , hint: "If the current live camera is PTZ, moves the camera down."
            , actionDown: BI_Hotkey_PtzDown
            , actionUp: BI_Hotkey_PtzDown_Up
            , category: "Hotkeys"
        }
        , {
            key: "ui3_hotkey_ptzLeft"
            , value: "0|0|0|37" // 37: left arrow
            , hotkey: true
            , label: "PTZ Left"
            , hint: "If the current live camera is PTZ, moves the camera left."
            , actionDown: BI_Hotkey_PtzLeft
            , actionUp: BI_Hotkey_PtzLeft_Up
            , category: "Hotkeys"
        }
        , {
            key: "ui3_hotkey_ptzRight"
            , value: "0|0|0|39" // 39: right arrow
            , hotkey: true
            , label: "PTZ Right"
            , hint: "If the current live camera is PTZ, moves the camera right."
            , actionDown: BI_Hotkey_PtzRight
            , actionUp: BI_Hotkey_PtzRight_Up
            , category: "Hotkeys"
        }
        , {
            key: "ui3_hotkey_ptzIn"
            , value: "0|0|0|187" // 187: =
            , hotkey: true
            , label: "PTZ Zoom In"
            , hint: "If the current live camera is PTZ, zooms the camera in."
            , actionDown: BI_Hotkey_PtzIn
            , actionUp: BI_Hotkey_PtzIn_Up
            , category: "Hotkeys"
        }
        , {
            key: "ui3_hotkey_ptzOut"
            , value: "0|0|0|189" // 189: -
            , hotkey: true
            , label: "PTZ Zoom Out"
            , hint: "If the current live camera is PTZ, zooms the camera out."
            , actionDown: BI_Hotkey_PtzOut
            , actionUp: BI_Hotkey_PtzOut_Up
            , category: "Hotkeys"
        }
        , {
            key: "ui3_hotkey_ptzFocusFar"
            , value: "0|0|0|221" // 221: ]
            , hotkey: true
            , label: "PTZ Focus Far"
            , hint: "If the current live camera is PTZ, focuses the camera further away."
            , actionDown: BI_Hotkey_PtzFocusFar
            , actionUp: BI_Hotkey_PtzFocusFar_Up
            , category: "Hotkeys"
        }
        , {
            key: "ui3_hotkey_ptzFocusNear"
            , value: "0|0|0|219" // 219: [
            , hotkey: true
            , label: "PTZ Focus Near"
            , hint: "If the current live camera is PTZ, focuses the camera closer."
            , actionDown: BI_Hotkey_PtzFocusNear
            , actionUp: BI_Hotkey_PtzFocusNear_Up
            , category: "Hotkeys"
        }
        , {
            key: "ui3_hotkey_ptzPreset1"
            , value: "0|0|0|49" // 49: 1
            , hotkey: true
            , label: "Load Preset 1"
            , hint: "If the current live camera is PTZ, loads preset 1."
            , actionDown: function () { BI_Hotkey_PtzPreset(1); }
            , category: "Hotkeys"
        }
        , {
            key: "ui3_hotkey_ptzPreset2"
            , value: "0|0|0|50" // 50: 2
            , hotkey: true
            , label: "Load Preset 2"
            , hint: "If the current live camera is PTZ, loads preset 2."
            , actionDown: function () { BI_Hotkey_PtzPreset(2); }
            , category: "Hotkeys"
        }
        , {
            key: "ui3_hotkey_ptzPreset3"
            , value: "0|0|0|51" // 51: 3
            , hotkey: true
            , label: "Load Preset 3"
            , hint: "If the current live camera is PTZ, loads preset 3."
            , actionDown: function () { BI_Hotkey_PtzPreset(3); }
            , category: "Hotkeys"
        }
        , {
            key: "ui3_hotkey_ptzPreset4"
            , value: "0|0|0|52" // 52: 4
            , hotkey: true
            , label: "Load Preset 4"
            , hint: "If the current live camera is PTZ, loads preset 4."
            , actionDown: function () { BI_Hotkey_PtzPreset(4); }
            , category: "Hotkeys"
        }
        , {
            key: "ui3_hotkey_ptzPreset5"
            , value: "0|0|0|53" // 53: 5
            , hotkey: true
            , label: "Load Preset 5"
            , hint: "If the current live camera is PTZ, loads preset 5."
            , actionDown: function () { BI_Hotkey_PtzPreset(5); }
            , category: "Hotkeys"
        }
        , {
            key: "ui3_hotkey_ptzPreset6"
            , value: "0|0|0|54" // 54: 6
            , hotkey: true
            , label: "Load Preset 6"
            , hint: "If the current live camera is PTZ, loads preset 6."
            , actionDown: function () { BI_Hotkey_PtzPreset(6); }
            , category: "Hotkeys"
        }
        , {
            key: "ui3_hotkey_ptzPreset7"
            , value: "0|0|0|55" // 55: 7
            , hotkey: true
            , label: "Load Preset 7"
            , hint: "If the current live camera is PTZ, loads preset 7."
            , actionDown: function () { BI_Hotkey_PtzPreset(7); }
            , category: "Hotkeys"
        }
        , {
            key: "ui3_hotkey_ptzPreset8"
            , value: "0|0|0|56" // 56: 8
            , hotkey: true
            , label: "Load Preset 8"
            , hint: "If the current live camera is PTZ, loads preset 8."
            , actionDown: function () { BI_Hotkey_PtzPreset(8); }
            , category: "Hotkeys"
        }
        , {
            key: "ui3_hotkey_ptzPreset9"
            , value: "0|0|0|57" // 57: 9
            , hotkey: true
            , label: "Load Preset 9"
            , hint: "If the current live camera is PTZ, loads preset 9."
            , actionDown: function () { BI_Hotkey_PtzPreset(9); }
            , category: "Hotkeys"
        }
        , {
            key: "ui3_hotkey_ptzPreset10"
            , value: "0|0|0|48" // 48: 0
            , hotkey: true
            , label: "Load Preset 10"
            , hint: "If the current live camera is PTZ, loads preset 10."
            , actionDown: function () { BI_Hotkey_PtzPreset(10); }
            , category: "Hotkeys"
        }
        , {
            key: "ui3_hotkey_ptzPreset11"
            , value: "1|0|0|49" // 49: 1
            , hotkey: true
            , label: "Load Preset 11"
            , hint: "If the current live camera is PTZ, loads preset 11."
            , actionDown: function () { BI_Hotkey_PtzPreset(11); }
            , category: "Hotkeys"
        }
        , {
            key: "ui3_hotkey_ptzPreset12"
            , value: "1|0|0|50" // 50: 2
            , hotkey: true
            , label: "Load Preset 12"
            , hint: "If the current live camera is PTZ, loads preset 12."
            , actionDown: function () { BI_Hotkey_PtzPreset(12); }
            , category: "Hotkeys"
        }
        , {
            key: "ui3_hotkey_ptzPreset13"
            , value: "1|0|0|51" // 51: 3
            , hotkey: true
            , label: "Load Preset 13"
            , hint: "If the current live camera is PTZ, loads preset 13."
            , actionDown: function () { BI_Hotkey_PtzPreset(13); }
            , category: "Hotkeys"
        }
        , {
            key: "ui3_hotkey_ptzPreset14"
            , value: "1|0|0|52" // 52: 4
            , hotkey: true
            , label: "Load Preset 14"
            , hint: "If the current live camera is PTZ, loads preset 14."
            , actionDown: function () { BI_Hotkey_PtzPreset(14); }
            , category: "Hotkeys"
        }
        , {
            key: "ui3_hotkey_ptzPreset15"
            , value: "1|0|0|53" // 53: 5
            , hotkey: true
            , label: "Load Preset 15"
            , hint: "If the current live camera is PTZ, loads preset 15."
            , actionDown: function () { BI_Hotkey_PtzPreset(15); }
            , category: "Hotkeys"
        }
        , {
            key: "ui3_hotkey_ptzPreset16"
            , value: "1|0|0|54" // 54: 6
            , hotkey: true
            , label: "Load Preset 16"
            , hint: "If the current live camera is PTZ, loads preset 16."
            , actionDown: function () { BI_Hotkey_PtzPreset(16); }
            , category: "Hotkeys"
        }
        , {
            key: "ui3_hotkey_ptzPreset17"
            , value: "1|0|0|55" // 55: 7
            , hotkey: true
            , label: "Load Preset 17"
            , hint: "If the current live camera is PTZ, loads preset 17."
            , actionDown: function () { BI_Hotkey_PtzPreset(17); }
            , category: "Hotkeys"
        }
        , {
            key: "ui3_hotkey_ptzPreset18"
            , value: "1|0|0|56" // 56: 8
            , hotkey: true
            , label: "Load Preset 18"
            , hint: "If the current live camera is PTZ, loads preset 18."
            , actionDown: function () { BI_Hotkey_PtzPreset(18); }
            , category: "Hotkeys"
        }
        , {
            key: "ui3_hotkey_ptzPreset19"
            , value: "1|0|0|57" // 57: 9
            , hotkey: true
            , label: "Load Preset 19"
            , hint: "If the current live camera is PTZ, loads preset 19."
            , actionDown: function () { BI_Hotkey_PtzPreset(19); }
            , category: "Hotkeys"
        }
        , {
            key: "ui3_hotkey_ptzPreset20"
            , value: "1|0|0|48" // 48: 0
            , hotkey: true
            , label: "Load Preset 20"
            , hint: "If the current live camera is PTZ, loads preset 20."
            , actionDown: function () { BI_Hotkey_PtzPreset(20); }
            , category: "Hotkeys"
        }
        , {
            key: "ui3_cameraLabels_enabled"
            , value: "0"
            , inputType: "checkbox"
            , label: 'Camera Labels Enabled'
            , onChange: onui3_cameraLabelsChanged
            , category: "Camera Labels"
        }
        , {
            key: "ui3_cameraLabels_multiCameras"
            , value: "1"
            , inputType: "checkbox"
            , label: 'Label multi-camera streams'
            , onChange: onui3_cameraLabelsChanged
            , category: "Camera Labels"
        }
        , {
            key: "ui3_cameraLabels_singleCameras"
            , value: "0"
            , inputType: "checkbox"
            , label: 'Label single-camera streams'
            , onChange: onui3_cameraLabelsChanged
            , category: "Camera Labels"
        }
        , {
            key: "ui3_cameraLabels_text"
            , value: CameraLabelTextValues.Name
            , inputType: "select"
            , options: [CameraLabelTextValues.Name, CameraLabelTextValues.ShortName, CameraLabelTextValues.Both]
            , label: "Label Text"
            , onChange: onui3_cameraLabelsChanged
            , category: "Camera Labels"
        }
        , {
            key: "ui3_cameraLabels_position"
            , value: CameraLabelPositionValues.Top
            , inputType: "select"
            , options: [CameraLabelPositionValues.Above, CameraLabelPositionValues.Top, CameraLabelPositionValues.Bottom, CameraLabelPositionValues.Below]
            , label: "Label Position"
            , onChange: onui3_cameraLabelsChanged
            , category: "Camera Labels"
        }
        , {
            key: "ui3_cameraLabels_fontSize"
            , value: 10
            , inputType: "number"
            , minValue: 0
            , maxValue: 128
            , label: "Font Size"
            , onChange: onui3_cameraLabelsChanged
            , category: "Camera Labels"
        }
        , {
            key: "ui3_cameraLabels_minimumFontSize"
            , value: 6
            , inputType: "number"
            , minValue: 0
            , maxValue: 128
            , label: "Min Font Size"
            , hint: "When a group view is rendered smaller than native resolution, font size is scaled down no smaller than this."
            , onChange: onui3_cameraLabelsChanged
            , category: "Camera Labels"
        }
        , {
            key: "ui3_cameraLabels_backgroundColor"
            , value: "#000000"
            , inputType: "color"
            , label: 'Background Color'
            , onChange: onui3_cameraLabelsChanged
            , category: "Camera Labels"
        }
        , {
            key: "ui3_cameraLabels_textColor"
            , value: "#FFFFFF"
            , inputType: "color"
            , label: 'Text Color'
            , onChange: onui3_cameraLabelsChanged
            , category: "Camera Labels"
        }
        , {
            key: "ui3_cameraLabels_cameraColor"
            , value: "1"
            , inputType: "checkbox"
            , label: 'Use Camera Color<div class="settingDesc">(ignore colors set above)</div>'
            , onChange: onui3_cameraLabelsChanged
            , category: "Camera Labels"
        }
        , {
            key: "ui3_cameraLabels_backgroundOpacity"
            , value: 100
            , minValue: 0
            , maxValue: 100
            , step: 1
            , unitLabel: "%"
            , inputType: "range"
            , label: 'Background Opacity'
            , onChange: onui3_cameraLabelsChanged
            , changeOnStep: true
            , category: "Camera Labels"
        }
        , {
            key: "ui3_cameraLabels_textOpacity"
            , value: 100
            , minValue: 0
            , maxValue: 100
            , step: 1
            , unitLabel: "%"
            , inputType: "range"
            , label: 'Text Opacity'
            , onChange: onui3_cameraLabelsChanged
            , changeOnStep: true
            , category: "Camera Labels"
        }
        , {
            key: "ui3_wheelZoomMethod"
            , value: "Adjustable"
            , inputType: "select"
            , options: ["Adjustable", "Legacy"]
            , label: "Digital Zoom Method"
            , onChange: OnChange_ui3_wheelZoomMethod
            , category: "Digital Zoom"
        }
        , {
            key: "ui3_wheelAdjustableSpeed"
            , value: 400
            , minValue: 0
            , maxValue: 2000
            , step: 1
            , inputType: "range"
            , label: 'Digital Zoom Speed<br/>(Requires zoom method "Adjustable")'
            , changeOnStep: true
            , hint: "Default: 400"
            , category: "Digital Zoom"
        }
        , {
            key: "ui3_wheelZoomReverse"
            , value: "0"
            , inputType: "checkbox"
            , label: 'Reverse Mouse Wheel Zoom'
            , hint: "By default, UI3 follows the de-facto standard for mouse wheel zoom, where up zooms in."
            , category: "Digital Zoom"
        }
        , {
            key: "ui3_zoom1x_mode"
            , value: Zoom1xOptions.Camera
            , inputType: "select"
            , options: [Zoom1xOptions.Camera, Zoom1xOptions.Stream]
            , label: 'At 1x zoom, match resolution of: '
            , hint: 'Choose "' + Zoom1xOptions.Stream + '" if clip playback has the wrong aspect ratio.'
            , category: "Digital Zoom"
        }
        , {
            key: "ui3_fullscreen_videoonly"
            , value: "1"
            , inputType: "checkbox"
            , label: 'Maximize Video in Full Screen Mode'
            , hint: 'If "yes", toggling Full Screen mode automatically toggles the video player\'s maximize state.'
            , onChange: OnChange_ui3_fullscreen_videoonly
            , category: "Extra"
        }
        , {
            key: "ui3_show_maximize_button"
            , value: "0"
            , inputType: "checkbox"
            , label: 'Always Show Maximize Button<div class="settingDesc">by Full Screen button</div>'
            , hint: 'If "no", the Maximize button only appears while the video player is maximized, to allow you to un-maximize.'
            , onChange: OnChange_ui3_show_maximize_button
            , category: "Extra"
        }
        , {
            key: "ui3_is_maximized"
            , value: "0"
        }
        , {
            key: "ui3_pc_next_prev_buttons"
            , value: "1"
            , inputType: "checkbox"
            , label: 'Playback Controls: Next/Previous'
            , onChange: OnChange_ui3_pc_next_prev_buttons
            , category: "Extra"
        }
        , {
            key: "ui3_pc_seek_buttons"
            , value: "0"
            , inputType: "checkbox"
            , label: 'Playback Controls: Skip Buttons'
            , onChange: OnChange_ui3_pc_seek_buttons
            , category: "Extra"
        }
        , {
            key: "ui3_pc_seek_1frame_buttons"
            , value: "0"
            , inputType: "checkbox"
            , label: 'Playback Controls: Skip 1 Frame Buttons'
            , onChange: OnChange_ui3_pc_seek_1frame_buttons
            , category: "Extra"
        }
        , {
            key: "ui3_extra_playback_controls_padding"
            , value: "0"
            , inputType: "checkbox"
            , label: 'Playback Controls: Extra Padding'
            , onChange: OnChange_ui3_extra_playback_controls_padding
            , category: "Extra"
        }
        , {
            key: "ui3_extra_playback_controls_timestamp"
            , value: "0"
            , inputType: "checkbox"
            , label: 'Playback Controls: Real Timestamp<br>When Streaming H.264'
            , hint: 'Adds a real-world timestamp to the playback controls, available only when streaming .bvr recordings with an H.264 streaming method.'
            , category: "Extra"
        }
        , {
            key: "ui3_extra_playback_controls_alwaysVisible"
            , value: "0"
            , inputType: "checkbox"
            , label: 'Playback Controls: Always Visible'
            , category: "Extra"
        }
        , {
            key: "ui3_ir_brightness_contrast"
            , value: "0"
            , inputType: "checkbox"
            , label: 'PTZ: IR, Brightness, Contrast<br><a href="javascript:UIHelp.LearnMore(\'IR Brightness Contrast\')">(learn more)</a>'
            , onChange: OnChange_ui3_ir_brightness_contrast
            , category: "Extra"
        }
        , {
            key: "ui3_show_session_success"
            , value: "0"
            , inputType: "checkbox"
            , label: 'Show Session Status at Startup'
            , hint: 'If enabled, session status is shown in the lower-right corner when the UI loads.'
            , category: "Extra"
        }
        , {
            key: "ui3_contextMenus_trigger"
            , value: "Right-Click"
            , options: ["Right-Click", "Long-Press", "Double-Click"]
            , inputType: "select"
            , label: 'Context Menu Trigger<br><a href="javascript:UIHelp.LearnMore(\'Context Menu Trigger\')">(learn more)</a>'
            , onChange: OnChange_ui3_contextMenus_trigger
            , category: "Extra"
        }
        , {
            key: "ui3_openFirstRecording"
            , value: "0"
            , inputType: "checkbox"
            , label: 'Automatically Open First Recording<div class="settingDesc">when loading Alerts or Clips tab</div>'
            , category: "Extra"
        }
        , {
            key: "ui3_system_name_button"
            , value: "About This UI"
            , inputType: "select"
            , options: []
            , getOptions: getSystemNameButtonOptions
            , label: 'System Name Button Action'
            , hint: 'This action occurs when you click the system name in the upper left.'
            , onChange: setSystemNameButtonState
            , category: "Extra"
        }
    ];

function OverrideDefaultSetting(key, value, IncludeInOptionsWindow, AlwaysReload, Generation)
{
    /// <summary>
    /// Overrides a default setting. This method is intended to be called by the ui3_local_overrides.js file.
    /// </summary>
    for (var i = 0; i < defaultSettings.length; i++)
        if (defaultSettings[i].key == key)
        {
            defaultSettings[i].value = value;
            defaultSettings[i].AlwaysReload = AlwaysReload;
            defaultSettings[i].Generation = Generation;
            if (!IncludeInOptionsWindow)
                defaultSettings[i].label = null;
            break;
        }
}
function LoadDefaultSettings()
{
    if (settings == null) // This null check allows local overrides to replace the settings object.
        settings = SetupStorageSniffing(GetLocalStorageWrapper());
    for (var i = 0; i < defaultSettings.length; i++)
    {
        if (settings.getItem(defaultSettings[i].key) == null
            || defaultSettings[i].AlwaysReload
            || IsNewGeneration(defaultSettings[i].key, defaultSettings[i].Generation))
            settings.setItem(defaultSettings[i].key, defaultSettings[i].value);
    }
}
function RevertSettingsToDefault()
{
    for (var i = 0; i < defaultSettings.length; i++)
        settings.setItem(defaultSettings[i].key, defaultSettings[i].value);
}
function GetLocalStorage()
{
    /// <summary>
    /// Returns the localStorage object, or a dummy localStorage object if the localStorage object is not available.
    /// This method should be used only when the wrapped localStorage object is not desired (e.g. when using settings that are persisted globally, not specific to a Blue Iris server).
    /// </summary>
    if (isLocalStorageEnabled())
        return localStorage;
    return GetDummyLocalStorage();
}
function IsNewGeneration(key, gen)
{
    if (typeof gen == "undefined" || gen == null)
        return false;

    gen = parseInt(gen);
    var currentGen = settings.getItem("ui3_gen_" + key);
    if (currentGen == null)
        currentGen = 0;
    else
        currentGen = parseInt(currentGen);

    var isNewGen = gen > currentGen;
    if (isNewGen)
        settings.setItem("ui3_gen_" + key, gen);
    return isNewGen;
}
function GetLocalStorageWrapper()
{
    /// <summary>Returns the local storage object or a wrapper suitable for the current Blue Iris server. The result of this should be stored in the settings variable.</summary>
    if (isLocalStorageEnabled())
    {
        if (currentServer.isUsingRemoteServer)
        {
            if (typeof Object.defineProperty == "function")
                return GetRemoteServerLocalStorage();
            else
            {
                toaster.Error("Your browser is not compatible with Object.defineProperty which is necessary to use remote servers.", 10000);
                SetRemoteServer("");
                return GetLocalStorage();
            }
        }
        else
            return GetLocalStorage();
    }
    return GetDummyLocalStorage();
}
function GetRemoteServerLocalStorage()
{
    var serverNamePrefix = currentServer.remoteServerName.toLowerCase().replace(/ /g, '_') + "_";

    var myLocalStorage = GetLocalStorage();
    var wrappedStorage = new Object();
    wrappedStorage.getItem = function (key)
    {
        return myLocalStorage[serverNamePrefix + key];
    };
    wrappedStorage.setItem = function (key, value)
    {
        return (myLocalStorage[serverNamePrefix + key] = value);
    };
    AttachDefaultSettingsProperties(wrappedStorage);
    return wrappedStorage;
}
var localStorageDummy = null;
function GetDummyLocalStorage()
{
    if (localStorageDummy === null)
    {
        var dummy = new Object();
        dummy.getItem = function (key)
        {
            return dummy[key];
        };
        dummy.setItem = function (key, value)
        {
            return (dummy[key] = value);
        };
        localStorageDummy = dummy;
    }
    return localStorageDummy;
}
function SetupStorageSniffing(storageObj)
{
    if (typeof Object.defineProperty === "function")
    {
        var isInvokingChangedEvent = {};
        var storageWrapper = new Object();
        storageWrapper.getItem = function (key)
        {
            return storageObj.getItem(key);
        };
        storageWrapper.setItem = function (key, value)
        {
            if (isInvokingChangedEvent[key])
                storageObj.setItem(key, value);
            else
            {
                var oldValue = storageObj.getItem(key);
                storageObj.setItem(key, value);
                isInvokingChangedEvent[key] = true;
                BI_CustomEvent.Invoke("SettingChanged", { key: key, value: value, oldValue: oldValue });
                isInvokingChangedEvent[key] = false;
            }
        };
        AttachDefaultSettingsProperties(storageWrapper);
        return storageWrapper;
    }
    else
    {
        console.log('The custom event "SettingChanged" requires Object.defineProperty which is not available.');
        return storageObj;
    }
}
function AttachDefaultSettingsProperties(storageWrapper)
{
    if (typeof Object.defineProperty !== "function")
        return;
    for (var i = 0; i < defaultSettings.length; i++)
    {
        var tmp = function (key)
        {
            Object.defineProperty(storageWrapper, key,
                {
                    get: function ()
                    {
                        return storageWrapper.getItem(key);
                    },
                    set: function (value)
                    {
                        return storageWrapper.setItem(key, value);
                    }
                });
        }(defaultSettings[i].key);
    }
}
///////////////////////////////////////////////////////////////
// UI Loading /////////////////////////////////////////////////
///////////////////////////////////////////////////////////////
// Load svg before document.ready, to give it a head-start.
$.ajax({
    url: "ui3/icons.svg?v=" + combined_version + local_bi_session_arg,
    dataType: "html",
    cache: true,
    success: function (data)
    {
        $("#svgContainer").html(data);
        loadingHelper.SetLoadedStatus("svg");

        BI_CustomEvent.Invoke("svgLoaded");
    },
    error: function (jqXHR, textStatus, errorThrown)
    {
        loadingHelper.SetErrorStatus("svg", 'Error trying to load icons.svg<br/>Response: ' + jqXHR.status + ' ' + jqXHR.statusText + '<br>Status: ' + textStatus + '<br>Error: ' + errorThrown);
    }
});
$(function ()
{
    BI_CustomEvent.Invoke("UI_Loading_Start");

    $DialogDefaults.theme = "dark";

    if (location.protocol == "file:")
    {
        var fileSystemErrorMessage = "This interface must be loaded through the Blue Iris web server, and cannot function when loaded directly from your filesystem.";
        alert(fileSystemErrorMessage);
        toaster.Error(fileSystemErrorMessage, 60000);
        return;
    }

    if (!isLocalStorageEnabled())
    {
        toaster.Warning("Local Storage is disabled or unavailable in your browser. Settings will not be saved between sessions.", 10000);
    }

    $("#ui_version_label").text(ui_version);
    $("#bi_version_label").text(bi_version);

    LoadDefaultSettings();

    try
    {
        if (typeof localStorage.ui3_contextMenus_longPress !== "undefined")
        {
            if (localStorage.ui3_contextMenus_longPress === "1" && settings.ui3_contextMenus_trigger === "Right-Click")
                settings.ui3_contextMenus_trigger = "Long-Press"; // one-time transition
            delete localStorage.ui3_contextMenus_longPress;
        }
    }
    catch (e) { }

    if (fetch_streams_cant_close_bug && settings.ui3_edge_fetch_bug_h264_enable !== "1")
        h264_playback_supported = false; // Affects Edge 17.x, 18.x, and possibly newer versions.

    HandlePreLoadUrlParameters();

    biSoundPlayer.TestUserInputRequirement();

    currentPrimaryTab = ValidateTabName(settings.ui3_defaultTab);

    setSystemNameButtonState();

    ptzButtons = new PtzButtons();

    if (!h264_playback_supported)
        loadingHelper.SetLoadedStatus("h264"); // We aren't going to load the player, so clear the loading step.

    $("#layoutleftLiveScrollable").CustomScroll(
        {
            changeMarginRightToScrollBarWidth: false
            , trackClass: 'layoutleft-track'
            , handleClass: 'layoutleft-track-handle'
        });
    $("#clipsbody").CustomScroll(
        {
            changeMarginRightToScrollBarWidth: false
            , trackClass: 'layoutleft-track'
            , handleClass: 'layoutleft-track-handle'
        });
    $(".topbar_tab").click(function ()
    {
        var $ele = $(this);
        $(".topbar_tab").removeClass("selected");
        $ele.addClass("selected");

        currentPrimaryTab = settings.ui3_defaultTab = $ele.attr("name");

        var tabDisplayName;
        if (currentPrimaryTab == "live")
        {
            tabDisplayName = "Live";
            $("#layoutleftLive").show();
            $("#layoutleftRecordings").hide();
            //$("#layoutbottom").hide();
        }
        else
        {
            tabDisplayName = currentPrimaryTab == "clips" ? "Clips" : "Alerts";
            $("#layoutleftLive").hide();
            $("#layoutleftRecordings").show();
            //$("#layoutbottom").show();
            $("#recordingsFilterByHeading").text("Filter " + tabDisplayName + " by:");
        }

        if (settings.ui3_openFirstRecording === "1")
            clipLoader.OpenFirstRecordingAfterNextClipListLoad();

        BI_CustomEvent.Invoke("TabLoaded_" + currentPrimaryTab);

        resized();
    });
    BI_CustomEvent.AddListener("TabLoaded_live", function () { videoPlayer.goLive(); });
    BI_CustomEvent.AddListener("TabLoaded_clips", function () { clipLoader.LoadClips("cliplist"); });
    BI_CustomEvent.AddListener("TabLoaded_alerts", function () { clipLoader.LoadClips("alertlist"); });

    clipboardHelper = new ClipboardHelper();

    uiSizeHelper = new UiSizeHelper();

    uiSettingsPanel = new UISettingsPanel();

    pcmPlayer = new PcmAudioPlayer();

    diskUsageGUI = new DiskUsageGUI();

    systemConfig = new SystemConfig();

    cameraListDialog = new CameraListDialog();

    clipProperties = new ClipProperties();

    clipDownloadDialog = new ClipDownloadDialog();

    statusBars = new StatusBars();
    statusBars.setLabel("volume", $("#pcVolume"));
    statusBars.addDragHandle("volume");
    statusBars.addOnProgressChangedListener("volume", function (newVolume)
    {
        newVolume = Clamp(parseFloat(newVolume), 0, 1);
        if (!pcmPlayer.SuppressAudioVolumeSave())
        {
            settings.ui3_audioMute = "0";
            settings.ui3_audioVolume = newVolume;
        }
        pcmPlayer.SetVolume(newVolume);
    });
    statusBars.addLabelClickHandler("volume", CameraAudioMuteToggle);
    pcmPlayer.SetAudioVolumeFromSettings();

    dropdownBoxes = new DropdownBoxes();

    leftBarBools = new LeftBarBooleans();

    cornerStatusIcons = new CornerStatusIcons();

    genericQualityHelper = new GenericQualityHelper();

    jpegQualityHelper = new JpegQualityHelper();

    streamingProfileUI = new StreamingProfileUI();

    SetupCollapsibleTriggers();

    exportControls = new ExportControls();

    seekBar = new SeekBar();

    playbackHeader = new PlaybackHeader();

    playbackControls = new PlaybackControls();

    clipTimeline = new ClipTimeline();

    hotkeys = new BI_Hotkeys();

    dateFilter = new DateFilter("#dateRangeLabel");

    hlsPlayer = new HLSPlayer();

    maximizedModeController = new MaximizedModeController();

    fullScreenModeController = new FullScreenModeController();

    canvasContextMenu = new CanvasContextMenu();

    calendarContextMenu = new CalendarContextMenu();

    clipListContextMenu = new ClipListContextMenu();

    cameraConfig = new CameraConfig();

    videoPlayer = new VideoPlayerController();
    videoPlayer.PreLoadPlayerModules();

    imageRenderer = new ImageRenderer();

    cameraNameLabels = new CameraNameLabels();

    statusLoader = new StatusLoader();

    sessionManager = new SessionManager();

    cameraListLoader = new CameraListLoader();

    clipLoader = new ClipLoader("#clipsbody");

    clipThumbnailVideoPreview = new ClipThumbnailVideoPreview_BruteForce();

    nerdStats = new UI3NerdStats();

    sessionTimeout = new SessionTimeout();

    togglableContextMenus = new Array();
    for (var i = 0; i < togglableUIFeatures.length; i++)
    {
        var item = togglableUIFeatures[i];
        if (item.length < 4)
            continue;
        if (item.length < 5)
            item.push(null);
        if (item.length < 6)
            item.push(null);
        if (item.length < 7)
            item.push(["Enable", "Disable", "Toggle"]);
        else if (item[6].length != 3)
            item[6] = ["Enable", "Disable", "Toggle"];

        togglableContextMenus.push(new ContextMenu_EnableDisableItem(item[0], item[1], item[2], item[3], item[4], item[5], item[6]));
    }

    OnChange_ui3_time24hour();
    OnChange_ui3_skipAmount();
    OnChange_ui3_pc_next_prev_buttons();
    OnChange_ui3_pc_seek_buttons();
    OnChange_ui3_pc_seek_1frame_buttons();
    OnChange_ui3_extra_playback_controls_padding();
    OnChange_ui3_ir_brightness_contrast();

    // This makes it impossible to text-select or drag certain UI elements.
    makeUnselectable($("#layouttop, #layoutleft, #layoutdivider, #layoutbody"));

    sessionManager.Initialize();

    $(window).resize(resized);
    $('.topbar_tab[name="' + currentPrimaryTab + '"]').click(); // this calls resized()

    BI_CustomEvent.Invoke("UI_Loading_End");
});
function ValidateTabName(tabName)
{
    if (tabName == "live" || tabName == "alerts" || tabName == "clips")
        return tabName;
    return "live";
}
function SetupCollapsibleTriggers()
{
    $(".collapsibleTrigger,.serverStatusLabel").each(function (idx, ele)
    {
        var $ele = $(ele);
        var collapsibleid = $ele.attr('collapsibleid');
        if (collapsibleid && collapsibleid.length > 0 && settings.getItem("ui3_collapsible_" + collapsibleid) != "1")
            $ele.next().hide();
        if ($ele.next().is(":visible"))
            $ele.removeClass("collapsed");
        else
            $ele.addClass("collapsed");
        $ele.click(function (e)
        {
            $ele.next().slideToggle({
                duration: 100
                , complete: function ()
                {
                    var vis = $ele.next().is(":visible");
                    if (vis)
                        $ele.removeClass("collapsed");
                    else
                        $ele.addClass("collapsed");
                    if (collapsibleid && collapsibleid.length > 0)
                        settings.setItem("ui3_collapsible_" + collapsibleid, vis ? "1" : "0");
                    if ($ele.hasClass("serverStatusLabel") || $ele.attr("id") == "recordingsFilterByHeading")
                        resized();
                }
            });
        });
        if (!$ele.hasClass("serverStatusLabel"))
            $ele.prepend('<svg class="icon collapsibleTriggerIcon"><use xlink:href="#svg_x5F_PTZcardinalDown"></use></svg>');
    });
}
///////////////////////////////////////////////////////////////
// Incoming URL Parameters ////////////////////////////////////
///////////////////////////////////////////////////////////////
function HandlePreLoadUrlParameters()
{
    // Parameter "tab"
    var tab = UrlParameters.Get("tab");
    if (tab != '')
        settings.ui3_defaultTab = tab;

    // Parameter "group"
    var group = UrlParameters.Get("group");
    if (group != '')
        settings.ui3_defaultCameraGroupId = group;

    // Parameter "cam"
    var cam = UrlParameters.Get("cam");
    if (cam != '')
    {
        BI_CustomEvent.AddListener("FinishedLoading", function ()
        {
            var camData = cameraListLoader.GetCameraWithId(cam);
            if (camData != null)
                videoPlayer.ImgClick_Camera(camData);
        });
    }
    var maximize = UrlParameters.Get("maximize");
    if (maximize == "1" || maximize.toLowerCase() == "true")
        settings.ui3_is_maximized = "1";
    else if (maximize == "0" || maximize.toLowerCase() == "false")
        settings.ui3_is_maximized = "0";
}
///////////////////////////////////////////////////////////////
// UI Resize //////////////////////////////////////////////////
///////////////////////////////////////////////////////////////
function resized()
{
    var windowW = $(window).width();
    var windowH = $(window).height();

    // Adjust UI style presets based on window size
    uiSizeHelper.SetMostAppropriateSize(windowW, windowH);

    // Learn some sizes
    var layouttop = $("#layouttop");
    var layoutleft = $("#layoutleft");
    var layoutbody = $("#layoutbody");
    var layoutbottom = $("#layoutbottom");
    var statusArea = $("#statusArea");
    var llrControls = $("#layoutLeftRecordingsControls");
    var systemnamewrapper = $("#systemnamewrapper");
    var camimg_loading_anim = $("#camimg_loading_anim,#camimg_false_loading_anim");
    var videoCenter_Icons = $("#camimg_playIcon,#camimg_pauseIcon");
    var videoCenter_Bg = $("#camimg_centerIconBackground");

    var topVis = layouttop.is(":visible");
    var leftVis = layoutleft.is(":visible");
    var botVis = layoutbottom.is(":visible");

    var topH = topVis ? layouttop.height() : 0;
    var botH = botVis ? layoutbottom.height() : 0;
    var leftH = leftVis ? (windowH - topH) : 0;
    var leftW = leftVis ? layoutleft.width() : 0;
    var statusH = statusArea.outerHeight(true);

    // Size layouttop
    // Measure width of objects in top bar
    var systemNameWidth = systemnamewrapper.width();
    var topTabCurrentWidth = -1000;
    var topWidthNoTabs = 5; // Workaround for rounding errors
    layouttop.children().each(function (idx, ele)
    {
        var $ele = $(ele);
        var w = $ele.outerWidth(true);
        if ($ele.hasClass("topbar_tab"))
            topTabCurrentWidth = w;
        else if ($ele.attr("id") != "systemnamewrapper")
            topWidthNoTabs += w;
    });
    // Determine how much space is needed for top bar
    var topTabDesiredWidth = leftW;
    var topTabAllowableWidth = topTabDesiredWidth;
    var systemNameAllowableWidth = topTabDesiredWidth;
    var topBarDesiredWidth = topWidthNoTabs + (4 * topTabDesiredWidth);
    var topTabMinWidth = 42;
    if (topBarDesiredWidth > windowW)
    {
        topTabAllowableWidth = Math.min(topTabDesiredWidth, ((windowW - topWidthNoTabs) - topTabDesiredWidth) / 3);
        if (topTabAllowableWidth < topTabMinWidth)
        {
            topTabAllowableWidth = topTabMinWidth;
            systemNameAllowableWidth = (windowW - topWidthNoTabs) - (topTabAllowableWidth * 3);
        }
    }
    if (topTabCurrentWidth != topTabAllowableWidth)
        layouttop.children(".topbar_tab").css("width", topTabAllowableWidth + "px");
    if (systemNameWidth != systemNameAllowableWidth)
        systemnamewrapper.css("width", systemNameAllowableWidth + "px");

    // Size layoutleft
    layoutleft.css("top", topH);
    layoutleft.css("height", leftH + "px");

    if (currentPrimaryTab == "live")
        $("#layoutleftLiveScrollableWrapper").css("height", leftH - statusH + "px");
    else
    {
        var llrControlsH = llrControls.outerHeight(true);
        $("#clipsbodyWrapper").css("height", leftH - statusH - llrControlsH + "px");
    }

    var statusArea_margins = statusArea.outerWidth(true) - statusArea.width();
    statusArea.css("width", (leftW - statusArea_margins) + "px");

    // Size layoutbody
    layoutbody.css("top", topH + "px");
    layoutbody.css("left", leftW + "px");
    var bodyW = windowW - leftW;
    var bodyH = windowH - topH - botH;
    layoutbody.css("width", bodyW + "px");
    layoutbody.css("height", bodyH + "px");

    // Size camimg_loading_anim
    var camimg_loading_anim_Size = Clamp(Math.min(bodyW, bodyH), 10, 120);
    camimg_loading_anim.css("top", ((bodyH - camimg_loading_anim_Size) / 2) + "px");
    camimg_loading_anim.css("left", ((bodyW - camimg_loading_anim_Size) / 2) + "px");
    camimg_loading_anim.css("width", camimg_loading_anim_Size + "px");
    camimg_loading_anim.css("height", camimg_loading_anim_Size + "px");

    // Size videoCenter_Bg
    var videoCenter_Bg_Size = Clamp(Math.min(bodyW, bodyH), 10, 72);
    videoCenter_Bg.css("top", ((bodyH - videoCenter_Bg_Size) / 2) + "px");
    videoCenter_Bg.css("left", ((bodyW - videoCenter_Bg_Size) / 2) + "px");
    videoCenter_Bg.css("width", videoCenter_Bg_Size + "px");
    videoCenter_Bg.css("height", videoCenter_Bg_Size + "px");

    // Size videoCenter_Icons
    var videoCenter_Icon_Size = Clamp(Math.min(bodyW, bodyH), 10, 40);
    videoCenter_Icons.css("top", ((bodyH - videoCenter_Icon_Size) / 2) + "px");
    videoCenter_Icons.css("left", ((bodyW - videoCenter_Icon_Size) / 2) + "px");
    videoCenter_Icons.css("width", videoCenter_Icon_Size + "px");
    videoCenter_Icons.css("height", videoCenter_Icon_Size + "px");

    playbackControls.resized();

    playbackHeader.resized();

    // Size layoutbottom
    layoutbottom.css("left", leftW + "px");
    layoutbottom.css("width", windowW - leftW + "px");

    clipTimeline.Resized();
    clipTimeline.Draw();

    // Size misc items
    imageRenderer.ImgResized(false);

    dropdownBoxes.Resized();

    // Call other methods to notify that resizing is done
    clipLoader.resizeClipList();
    $.CustomScroll.callMeOnContainerResize();
    BI_CustomEvent.Invoke("afterResize");
}
function UiSizeHelper()
{
    var self = this;
    var largeMinH = 1042; // 1075
    var mediumMinH = 786; // 815
    var largeMinW = 670;// 550 575 1160;
    var mediumMinW = 540;// 450 515 900;
    var smallMinW = 350;//680;
    var currentSize = "large";
    var autoSize = true;

    this.SetMostAppropriateSize = function (availableWidth, availableHeight)
    {
        if (autoSize)
        {
            if (availableWidth < smallMinW)
                SetSize("smaller");
            else if (availableHeight < mediumMinH || availableWidth < mediumMinW)
                SetSize("small");
            else if (availableHeight < largeMinH || availableWidth < largeMinW)
                SetSize("medium");
            else
                SetSize("large");
        }
    }
    var SetSize = function (size)
    {
        if (currentSize == size)
            return;
        currentSize = size;
        var $roots = $('body');
        $roots.removeClass("sizeSmaller sizeSmall sizeMedium sizeLarge");
        if (size == "smaller")
            $roots.addClass("sizeSmall sizeSmaller");
        else if (size == "small")
            $roots.addClass("sizeSmall");
        else if (size == "medium")
            $roots.addClass("sizeMedium");
        else
            $roots.addClass("sizeLarge");
    }
    this.GetCurrentSize = function ()
    {
        return currentSize;
    }
    this.SetUISizeByName = function (size)
    {
        if (size)
            size = size.toLowerCase();
        autoSize = size == "auto";
        if (!autoSize)
            SetSize(size);
        resized();
        //setTimeout(resized);
    }

    setTimeout(function () { self.SetUISizeByName(settings.ui3_preferred_ui_scale); }, 0);
}
///////////////////////////////////////////////////////////////
// Progress bar / Scrub bar / Status bar //////////////////////
///////////////////////////////////////////////////////////////
function StatusBars()
{
    var self = this;
    var statusElements = {};
    $(".statusBar").each(function (idx, ele)
    {
        var $ele = $(ele);
        if ($ele.children().length > 0)
            return;
        var statusTiny = $ele.hasClass("statusTiny");
        if (!statusTiny)
            ele.$label = $('<div class="statusBarLabel">' + $ele.attr('label') + '</div>');
        ele.$pb = $('<div></div>');
        if (!statusTiny)
            ele.$amount = $('<div class="statusBarAmount">' + $ele.attr('defaultAmountText') + '</div>');
        $ele.append(ele.$label);
        $ele.append(ele.$pb);
        $ele.append(ele.$amount);
        ProgressBar.initialize(ele.$pb);
        var name = $ele.attr("name");
        if (!statusElements[name])
            statusElements[name] = [];
        statusElements[name].push(ele);
    });
    this.setProgress = function (name, progressAmount, progressAmountText, progressColor, progressBackgroundColor)
    {
        var statusEles = statusElements[name];
        if (statusEles)
            for (var i = 0; i < statusEles.length; i++)
            {
                ProgressBar.setProgress(statusEles[i].$pb, progressAmount);
                ProgressBar.setColor(statusEles[i].$pb, progressColor, progressBackgroundColor);
                statusEles[i].$amount && statusEles[i].$amount.text(progressAmountText);
            }
    };
    this.setTooltip = function (name, tooltipText)
    {
        var statusEles = statusElements[name];
        if (statusEles)
            for (var i = 0; i < statusEles.length; i++)
                $(statusEles[i]).attr("title", tooltipText);
    };
    this.setColor = function (name, progressColor, progressBackgroundColor)
    {
        var statusEles = statusElements[name];
        if (statusEles)
            for (var i = 0; i < statusEles.length; i++)
                ProgressBar.setColor(statusEles[i].$pb, progressColor, progressBackgroundColor);
    };
    this.setLabel = function (name, label)
    {
        var statusEles = statusElements[name];
        if (statusEles)
            for (var i = 0; i < statusEles.length; i++)
                statusEles[i].$label && statusEles[i].$label.append(label);
    };
    this.getLabelObjs = function (name)
    {
        var statusEles = statusElements[name];
        if (statusEles)
            return $(statusEles);
        return $();
    };
    this.addDragHandle = function (name)
    {
        var statusEles = statusElements[name];
        if (statusEles)
            for (var i = 0; i < statusEles.length; i++)
                ProgressBar.addDragHandle(statusEles[i].$pb, function (newValue) { self.setProgress(name, newValue, parseInt(newValue * 100) + "%") });
    };
    this.getValue = function (name)
    {
        var statusEles = statusElements[name];
        if (statusEles)
            for (var i = 0; i < statusEles.length; i++)
                return statusEles[i].getValue();
        return -1;
    };
    this.addOnProgressChangedListener = function (name, onProgressChanged)
    {
        var statusEles = statusElements[name];
        if (statusEles)
            if (statusEles.length > 0) // Add the listener only to the first element, so in case of multiple elements with the same name, we only create one callback.
                ProgressBar.addOnProgressChangedListener(statusEles[0].$pb, onProgressChanged);
    };
    this.addLabelClickHandler = function (name, onLabelClick)
    {
        var $labels = self.getLabelObjs(name);
        $labels.each(function (idx, ele)
        {
            $(ele).children('.statusBarLabel').on('click', onLabelClick);
        });
    }
    this.setEnabled = function (name, enabled)
    {
        var statusEles = statusElements[name];
        if (statusEles)
            for (var i = 0; i < statusEles.length; i++)
                ProgressBar.setEnabled(statusEles[0].$pb, enabled);
    };
}
var ProgressBar =
{
    initialize: function ($ele)
    {
        if ($ele.children().length == 0)
        {
            var ele = $ele.get(0);
            ele.$progressBarInner = $('<div class="progressBarInner"></div>');
            $ele.append(ele.$progressBarInner);
            $ele.addClass("progressBarOuter");
            ele.defaultColor = ele.$progressBarInner.css("background-color");
            ele.defaultBackgroundColor = $ele.css("background-color");
        }
    }
    , setProgress: function ($ele, progressAmount)
    {
        var ele = $ele.get(0);
        progressAmount = Clamp(progressAmount, 0, 1);
        var changed = typeof ele.pbValue == "undefined" || ele.pbValue != progressAmount;
        ele.pbValue = progressAmount;
        ele.$progressBarInner.css("width", (progressAmount * 100) + "%");
        if (typeof ele.moveDragHandleElements == "function")
            ele.moveDragHandleElements();
        if (changed && typeof ele.onProgressChanged == "function")
            ele.onProgressChanged(progressAmount);
    }
    , addDragHandle: function ($ele, onDrag)
    {
        $ele.addClass("withDragHandle");
        var ele = $ele.get(0);
        ele.$dragHandle = $('<div class="statusBarDragHandle"><div class="statusBarDragHandleInner"></div></div>');
        var dragHandleWidth = ele.$dragHandle.width();
        $ele.prepend(ele.$dragHandle);

        ele.onDragHandleDragged = function (pageX)
        {
            var relX = pageX - $ele.offset().left;
            var progressPercentage = Clamp(relX / $ele.width(), 0, 1);
            onDrag(progressPercentage);
        };
        ele.moveDragHandleElements = function ()
        {
            ele.$dragHandle.css("left", (ele.pbValue * $ele.width()) - (ele.$dragHandle.width() / 2) + "px");
        };
        ele.moveDragHandleElements();
        BI_CustomEvent.AddListener("afterResize", ele.moveDragHandleElements);

        // Set up input events
        $ele.on("mousedown touchstart", function (e)
        {
            mouseCoordFixer.fix(e);
            if (e.which != 3)
            {
                if ($ele.hasClass("disabled"))
                    return;
                ele.isDragging = true;
                ele.onDragHandleDragged(e.pageX);
            }
        });
        $(document).on("mouseup touchend touchcancel", function (e)
        {
            mouseCoordFixer.fix(e);
            ele.isDragging = false;
        });
        $(document).on("mousemove touchmove", function (e)
        {
            mouseCoordFixer.fix(e);
            if (ele.isDragging)
                ele.onDragHandleDragged(e.pageX);
        });
    }
    , getValue: function ($ele)
    {
        return $ele.get(0).pbValue;
    }
    , addOnProgressChangedListener: function ($ele, onProgressChanged)
    {
        $ele.get(0).onProgressChanged = onProgressChanged;
    }
    , setColor: function ($ele, progressColor, progressBackgroundColor)
    {
        if (progressColor)
        {
            if (progressColor == "default")
                progressColor = $ele.get(0).defaultColor;
            $ele.get(0).$progressBarInner.css("background-color", progressColor);
        }
        if (progressBackgroundColor)
        {
            if (progressBackgroundColor == "default")
                progressBackgroundColor = $ele.get(0).Background;
            $ele.css("background-color", progressBackgroundColor);
        }
    }
    , setEnabled: function ($ele, enabled)
    {
        if (enabled)
            $ele.removeClass("disabled");
        else
            $ele.addClass("disabled");
    }
};
///////////////////////////////////////////////////////////////
// Dropdown Boxes /////////////////////////////////////////////
///////////////////////////////////////////////////////////////
function DropdownListDefinition(name, options)
{
    var self = this;
    this.name = name;
    this.timeClosed = 0;
    // Default Options
    this.items = [];
    this.onItemClick = null;
    this.getDefaultLabel = function () { return "..."; };
    // End options
    $.extend(this, options);
}
function DropdownListItem(options)
{
    var self = this;
    // Default Options
    this.text = "List Item";
    this.isHtml = false;
    this.autoSetLabelText = true;
    // End options
    this.GetTooltip = function ()
    {
        if (typeof self.tooltip == "function")
            return self.tooltip(self);
        else if (self.tooltip)
            return self.tooltip;
        else
            return "";
    }
    $.extend(this, options);
}
function DropdownBoxes()
{
    var self = this;
    var handleElements = {};
    var $dropdownBoxes = $(".dropdownBox,#btn_main_menu,.dropdownTrigger");
    var currentlyOpenList = null;
    var preventDDLClose = false;

    this.listDefs = {};
    this.listDefs["schedule"] = new DropdownListDefinition("schedule",
        {
            onItemClick: function (item)
            {
                var scheduleName = item.id;
                if (scheduleName != null)
                {
                    self.setLabelText("schedule", "...");
                    statusLoader.ChangeSchedule(scheduleName);
                }
            }
            , rebuildItems: function ()
            {
                this.items = [];
                if (statusLoader.IsGlobalScheduleEnabled())
                {
                    var schedulesArray = sessionManager.GetSchedulesArray();
                    if (schedulesArray)
                    {
                        if (schedulesArray.length == 0)
                        {
                            console.log("Schedules array is empty. Opening login dialog because login responses provide the schedule list");
                            openLoginDialog();
                            return;
                        }
                        for (var i = 0; i < schedulesArray.length; i++)
                        {
                            var scheduleName = schedulesArray[i];
                            this.items.push(new DropdownListItem(
                                {
                                    text: scheduleName
                                    , id: scheduleName
                                    , selected: scheduleName == statusLoader.GetCurrentlySelectedScheduleName()
                                }));
                        }
                    }
                }
                else
                    this.items.push(new DropdownListItem(
                        {
                            text: "The global schedule must first be configured in Blue Iris."
                            , id: null
                            , selected: false
                            , autoSetLabelText: false
                        }));
            }
        });
    this.listDefs["currentGroup"] = new DropdownListDefinition("currentGroup",
        {
            onItemClick: function (item)
            {
                videoPlayer.SelectCameraGroup(item.id);
            }
            , rebuildItems: function (data)
            {
                this.items = [];
                for (var i = 0; i < data.length; i++)
                {
                    if (cameraListLoader.CameraIsGroupOrCycle(data[i]))
                    {
                        this.items.push(new DropdownListItem(
                            {
                                text: CleanUpGroupName(data[i].optionDisplay)
                                , id: data[i].optionValue
                            }));
                    }
                }
            }
        });
    this.listDefs["streamingQuality"] = new DropdownListDefinition("streamingQuality",
        {
            items: [new DropdownListItem({ text: "Not Loaded!", uniqueId: "Not Loaded!" })]
            , onItemClick: function (item)
            {
                genericQualityHelper.QualityChoiceChanged(item.uniqueId);
            }
        });
    this.listDefs["mainMenu"] = new DropdownListDefinition("mainMenu",
        {
            selectedIndex: -1
            , items:
                [
                    new DropdownListItem({ cmd: "ui_settings", text: "UI Settings", icon: "#svg_x5F_Settings", cssClass: "goldenLarger", tooltip: "User interface settings are stored in this browser and are not shared with other computers." })
                    , new DropdownListItem({ cmd: "about_this_ui", text: "About This UI", icon: "#svg_x5F_About", cssClass: "goldenLarger" })
                    , new DropdownListItem({ cmd: "streaming_profiles", text: "Streaming Profiles", icon: "#svg_mio_VideoFilter", cssClass: "goldenLarger" })
                    , new DropdownListItem({ cmd: "system_log", text: "System Log", icon: "#svg_x5F_SystemLog", cssClass: "blueLarger" })
                    , new DropdownListItem({ cmd: "user_list", text: "User List", icon: "#svg_x5F_User", cssClass: "blueLarger" })
                    , new DropdownListItem({ cmd: "device_list", text: "Device List", icon: "#svg_mio_deviceInfo", cssClass: "blueLarger" })
                    , new DropdownListItem({ cmd: "full_camera_list", text: "Full Camera List", icon: "#svg_x5F_FullCameraList", cssClass: "blueLarger" })
                    , new DropdownListItem({ cmd: "disk_usage", text: "Disk Usage", icon: "#svg_x5F_Information", cssClass: "blueLarger" })
                    , new DropdownListItem({ cmd: "system_configuration", text: "System Configuration", icon: "#svg_x5F_SystemConfiguration", cssClass: "blueLarger", tooltip: "Blue Iris Settings" })
                    , new DropdownListItem({ cmd: "help", text: "Help", icon: "#svg_mio_help", cssClass: "goldenLarger" })
                    , new DropdownListItem({ cmd: "logout", text: "Log Out", icon: "#svg_x5F_Logout", cssClass: "goldenLarger" })
                ]
            , onItemClick: function (item)
            {
                switch (item.cmd)
                {
                    case "ui_settings":
                        uiSettingsPanel.open();
                        break;
                    case "about_this_ui":
                        openAboutDialog();
                        break;
                    case "streaming_profiles":
                        streamingProfileUI.open();
                        break;
                    case "system_log":
                        systemLog.open();
                        break;
                    case "user_list":
                        userList.open();
                        break;
                    case "device_list":
                        deviceList.open();
                        break;
                    case "system_configuration":
                        systemConfig.open();
                        break;
                    case "full_camera_list":
                        cameraListDialog.open();
                        break;
                    case "disk_usage":
                        statusLoader.diskUsageClick();
                        break;
                    case "help":
                        window.open("ui3/help/help.html" + currentServer.GetLocalSessionArg("?") + "#overview");
                        break;
                    case "logout":
                        logout();
                        break;
                }
            }
        });

    this.listDefs["ptzIR"] = new DropdownListDefinition("ptzIR",
        {
            items:
                [
                    new DropdownListItem({ cmd: "off", text: "IR Off" })
                    , new DropdownListItem({ cmd: "on", text: "IR On" })
                    , new DropdownListItem({ cmd: "auto", text: "IR Auto" })
                ]
            , onItemClick: function (item)
            {
                var loading = videoPlayer.Loading().image;
                if (loading.ptz && loading.isLive)
                    switch (item.cmd)
                    {
                        case "off":
                            ptzButtons.PTZ_unsafe_async_guarantee(loading.id, 34);
                            ptzButtons.SetIRButtonState(0);
                            break;
                        case "on":
                            ptzButtons.PTZ_unsafe_async_guarantee(loading.id, 35);
                            ptzButtons.SetIRButtonState(1);
                            break;
                        case "auto":
                            ptzButtons.PTZ_unsafe_async_guarantee(loading.id, 36);
                            ptzButtons.SetIRButtonState(2);
                            break;
                    }
            }
        });
    this.listDefs["ptzBrightness"] = new DropdownListDefinition("ptzBrightness",
        {
            items: GetNumberedDropdownListItems("Brightness", 0, 15)
            , onItemClick: function (item)
            {
                var loading = videoPlayer.Loading().image;
                if (loading.ptz && loading.isLive)
                {
                    var newBrightness = Clamp(parseInt(item.cmd), 0, 15);
                    ptzButtons.PTZ_unsafe_async_guarantee(loading.id, 11 + newBrightness);
                    ptzButtons.SetBrightnessButtonState(newBrightness);
                }
            }
        });
    this.listDefs["ptzContrast"] = new DropdownListDefinition("ptzContrast",
        {
            items: GetNumberedDropdownListItems("Contrast", 0, 6)
            , onItemClick: function (item)
            {
                var loading = videoPlayer.Loading().image;
                if (loading.ptz && loading.isLive)
                {
                    var newContrast = Clamp(parseInt(item.cmd), 0, 6);
                    ptzButtons.PTZ_unsafe_async_guarantee(loading.id, 27 + newContrast);
                    ptzButtons.SetContrastButtonState(newContrast);
                }
            }
        });

    function GetNumberedDropdownListItems(name, min, max)
    {
        var items = [];
        for (var i = min; i <= max; i++)
            items.push(new DropdownListItem({ cmd: i.toString(), text: name + " " + i }));
        return items;
    }

    $dropdownBoxes.each(function (idx, ele)
    {
        var $ele = $(ele);
        var name = $ele.attr("name");
        var listDef = self.listDefs[name];
        if (listDef == null)
        {
            toaster.Warning("Unknown dropdown box name: " + htmlEncode(name));
            return;
        }
        ele.extendLeft = $ele.attr("extendLeft") == "1";
        if ($ele.hasClass('dropdownBox'))
        {
            ele.$label = $('<div class="dropdownLabel"></div>');
            ele.$label.text(listDef.getDefaultLabel());
            ele.$arrow = $('<div class="dropdownArrow"><svg class="icon"><use xlink:href="#svg_x5F_DownArrow"></use></svg></div>');
            $ele.append(ele.$label);
            $ele.append(ele.$arrow);
        }
        else if ($ele.hasClass('dropdownTrigger'))
        {
            ele.$label = $ele.find('div.invisibleLabel');
        }
        else
        {
            ele.$label = $();
            ele.$arrow = $();
        }
        $ele.on('click', function ()
        {
            if ($ele.hasClass("disabled"))
                return;
            LoadDropdownList(name, $ele);
        });
        if (!handleElements[name])
            handleElements[name] = [];
        handleElements[name].push(ele);
    });
    this.setLabelText = function (name, labelText, isHtml)
    {
        var handleEles = handleElements[name];
        if (handleEles)
            for (var i = 0; i < handleEles.length; i++)
            {
                var ele = handleEles[i];
                if (ele)
                {
                    if (isHtml)
                        ele.$label.html(labelText);
                    else
                        ele.$label.text(labelText);
                }
            }
    };
    this.getLabelText = function (name, isHtml)
    {
        var handleEles = handleElements[name];
        if (handleEles)
            for (var i = 0; i < handleEles.length; i++)
            {
                var ele = handleEles[i];
                if (ele)
                {
                    if (isHtml)
                        return ele.$label.html();
                    else
                        return ele.$label.text();
                }
            }
        return "";
    }
    this.setEnabled = function (name, enabled)
    {
        var handleEles = handleElements[name];
        if (handleEles)
            for (var i = 0; i < handleEles.length; i++)
            {
                var ele = handleEles[i];
                if (ele)
                {
                    if (enabled)
                        $(ele).removeClass("disabled");
                    else
                        $(ele).addClass("disabled");
                }
            }
    }
    var getFirstVisibleEle = function (name)
    {
        var handleEles = handleElements[name];
        if (handleEles)
            for (var i = 0; i < handleEles.length; i++)
            {
                var ele = handleEles[i];
                if (ele && $(ele).is(":visible"))
                    return ele;
            }
        return null;
    }
    var LoadDropdownList = function (name, $parent)
    {
        var ele = getFirstVisibleEle(name);
        if (ele == null)
            return;
        var listDef = self.listDefs[name];
        if (listDef == null)
            return;
        if (new Date().getTime() - 33 <= listDef.timeClosed)
            return;
        var $ele = $(ele);
        var offset = $ele.offset();

        var $ddl = listDef.$currentListEle = $('<div class="dropdown_list"></div>');
        $ddl.on("mouseup", function ()
        {
            return false;
        });

        var selectedText = self.getLabelText(name);
        for (var i = 0; i < listDef.items.length; i++)
            AddDropdownListItem($ddl, listDef, i, selectedText);
        if (listDef.items.length == 0)
            $ddl.append("<div>This list is empty!</div>");

        $("body").append($ddl);

        if ($parent.length > 0)
            $ddl.css('min-width', $parent.innerWidth() + "px");

        if (name == "mainMenu")
        {
            $ddl.css("min-width", ($ddl.width() + 1) + "px"); // Workaround for "System Configuration" wrapping bug
            if (BrowserIsIE())
                $ddl.css("height", ($ddl.height() + 3) + "px"); // Workaround for IE bug that adds unnecessary scroll bars
        }

        var windowH = $(window).height();
        var windowW = $(window).width();
        var width = $ddl.outerWidth();
        var height = $ddl.outerHeight();
        var top = (offset.top + $ele.outerHeight());
        var left = offset.left;
        if (ele.extendLeft)
        {
            left = (left + $ele.outerWidth()) - width;
            if ((BrowserIsIE() || BrowserIsEdge()) && height > windowH)
                left -= 20; // Workaround for Edge/IE bug that renders scroll bar offscreen
        }

        // Adjust box position so the box doesn't extend off the bottom, top, right, left, in that order.
        if (top + height > windowH)
            top = windowH - height;
        if (top < 0)
            top = 0;
        if (left + width > windowW)
            left = windowW - width;
        if (left < 0)
            left = 0;

        $ddl.css("top", top + "px");
        $ddl.css("left", left + "px");

        closeDropdownLists();
        currentlyOpenList = listDef;
        preventDDLClose = true;
        setTimeout(allowDDLClose, 0);
        self.Resized();

        var $selectedItem = $ddl.children('.selected');
        if ($selectedItem.length > 0)
        {
            // Determine ideal scroll position
            var eleCenter = $selectedItem.position().top + $selectedItem.outerHeight(true) / 2;
            var visibleHeight = $ddl.innerHeight();
            var idealScrollTop = eleCenter - (visibleHeight / 2);
            if (idealScrollTop > 0)
                $ddl.scrollTop(idealScrollTop);
        }
    }
    var AddDropdownListItem = function ($ddl, listDef, i, selectedText)
    {
        var item = listDef.items[i];
        var $item = $("<div></div>");
        if (item.isHtml)
            $item.html(item.text);
        else
            $item.text(item.text);
        if (selectedText == item.text)
            $item.addClass("selected");
        if (item.cssClass)
            $item.addClass(item.cssClass);
        $item.click(function ()
        {
            if (listDef.items[i].autoSetLabelText)
                self.setLabelText(listDef.name, item.text, item.isHtml);
            listDef.selectedIndex = i;
            listDef.onItemClick && listDef.onItemClick(listDef.items[i]); // run if not null
            closeDropdownLists();
        });
        if (item.icon)
            $item.prepend('<div class="mainMenuIcon"><svg class="icon' + (item.icon.indexOf('_x5F_') == -1 ? " noflip" : "") + '"><use xlink:href="' + item.icon + '"></use></svg></div>');
        var tooltip = item.GetTooltip();
        if (tooltip)
            $item.attr('title', tooltip);
        $ddl.append($item);
    }
    $(document).mouseup(function (e)
    {
        if (!preventDDLClose)
            closeDropdownLists();
    });
    $(document).mouseleave(function (e)
    {
        if (!preventDDLClose)
            closeDropdownLists();
    });
    var closeDropdownLists = function ()
    {
        if (currentlyOpenList != null)
        {
            currentlyOpenList.$currentListEle.remove();
            currentlyOpenList.$currentListEle = null;
            currentlyOpenList.timeClosed = new Date().getTime();
            currentlyOpenList = null;
        }
    }
    var allowDDLClose = function ()
    {
        /// <summary>This exists to prevent a glitch where dropdown lists close immediately in Edge when using a touchscreen, giving the appearance that the dropdown lists never even open.</summary>
        preventDDLClose = false;
    }
    this.Resized = function ()
    {
        var windowH = $(window).height();
        $(".dropdown_list").each(function (idx, ele)
        {
            var $ele = $(ele);
            $ele.css("max-height", (windowH - $ele.offset().top - 2) + "px");
        });
    }
}
function GetTooltipForStreamQuality(index)
{
    var arr = sessionManager.GetStreamsArray();
    if (arr && arr.length > 0 && index > -1 && index < arr.length)
        return arr[index];
    return "";
}
///////////////////////////////////////////////////////////////
// System Name Button /////////////////////////////////////////
///////////////////////////////////////////////////////////////
var systemNameButton;
function getSystemNameButtonOptions()
{
    var mmItems = dropdownBoxes.listDefs["mainMenu"].items;
    var opts = new Array();
    for (var i = 0; i < mmItems.length; i++)
        opts.push(mmItems[i].text);
    opts.push("Do Nothing");
    return opts;
}
function systemNameButtonClick()
{
    var mmItems = dropdownBoxes.listDefs["mainMenu"].items;
    for (var i = 0; i < mmItems.length; i++)
        if (settings.ui3_system_name_button == mmItems[i].text)
        {
            dropdownBoxes.listDefs["mainMenu"].onItemClick(mmItems[i]);
            return;
        }
}
function setSystemNameButtonState()
{
    if (settings.ui3_system_name_button == "Do Nothing")
        $("#systemnamewrapper").removeClass("hot");
    else
        $("#systemnamewrapper").addClass("hot");
}
///////////////////////////////////////////////////////////////
// Left Bar Boolean Options ///////////////////////////////////
///////////////////////////////////////////////////////////////
function LeftBarBooleans()
{
    var $items = $('#layoutleft .leftBarBool');
    $items.each(function (idx, ele)
    {
        var $ele = $(ele);
        var name = $ele.attr("name");
        switch (name)
        {
            case "flaggedOnly":
                {
                    var $cb = $('<input type="checkbox" />');
                    if (settings.ui3_recordings_flagged_only == "1")
                        $cb.prop('checked', 'checked');
                    $cb.on('change', function ()
                    {
                        settings.ui3_recordings_flagged_only = $cb.is(':checked') ? "1" : "0";
                        clipLoader.LoadClips();
                    });
                    var $label = $('<label></label>');
                    $label.append('<div class="smallFlagIcon"><svg class="icon"><use xlink:href="#svg_x5F_Flag"></use></svg></div>');
                    $label.append($cb);
                    $label.append($ele.html());
                    $ele.empty();
                    $ele.append($label);
                }
                break;
        }
    });
}
///////////////////////////////////////////////////////////////
// PTZ Pad Buttons ////////////////////////////////////////////
///////////////////////////////////////////////////////////////
function PtzButtons()
{
    var self = this;

    var ptzControlsEnabled = false;
    var $hoveredEle = null;
    var $activeEle = null;

    var unsafePtzActionNeedsStopped = false;
    var currentPtz = "0";
    var currentPtzCamId = "";
    var unsafePtzActionQueued = null;
    var unsafePtzActionInProgress = false;
    var currentPtzData = null;

    var $ptzGraphicWrapper = $("#ptzGraphicWrapper");
    var $ptzGraphics = $("#ptzGraphicWrapper div.ptzGraphic");
    var $ptzBackgroundGraphics = $("#ptzGraphicWrapper div.ptzBackground");
    var $ptzGraphicContainers = $("#ptzGraphicWrapper .ptzGraphicContainer");
    var $ptzPresets = $("#ptzPresetsContent .ptzpreset");
    var $ptzButtons = $("#ptzButtonsMain");
    var $ptzControlsContainers = $("#ptzPresetsContent,#ptzButtonsMain");
    var $ptzExtraDropdowns = $("#ptzIrBrightnessContrast .dropdownTrigger");
    var $irButtonText = $("#irButtonText");
    var $irButtonLabel = $("#irButtonLabel");
    var $brightnessButtonLabel = $("#brightnessButtonLabel");
    var $contrastButtonLabel = $("#contrastButtonLabel");

    var hitPolys = {};
    hitPolys["PTZzoomIn"] = [[64, 64], [82, 82], [91, 77], [99, 77], [106, 81], [126, 64], [116, 58], [105, 53], [86, 53], [74, 58]];
    hitPolys["PTZzoomOut"] = [[64, 126], [82, 108], [91, 113], [99, 113], [106, 109], [126, 126], [116, 132], [105, 137], [86, 137], [74, 132]];
    hitPolys["PTZfocusNear"] = [[126, 64], [108, 82], [113, 91], [113, 99], [109, 106], [126, 126], [132, 116], [137, 105], [137, 86], [132, 74]];
    hitPolys["PTZfocusFar"] = [[64, 64], [82, 82], [77, 91], [77, 99], [81, 106], [64, 126], [58, 116], [53, 105], [53, 86], [58, 74]];
    hitPolys["PTZstop"] = [[82, 82], [91, 77], [99, 77], [108, 82], [113, 91], [113, 99], [108, 108], [99, 113], [91, 113], [82, 108], [77, 99], [77, 91]];
    hitPolys["PTZcardinalUp"] = [[52, 9], [74, 58], [86, 53], [105, 53], [116, 58], [138, 9], [96, 0]];
    hitPolys["PTZcardinalRight"] = [[181, 52], [132, 74], [138, 86], [138, 105], [132, 116], [181, 138], [190, 96]];
    hitPolys["PTZcardinalDown"] = [[52, 181], [74, 132], [86, 138], [105, 138], [116, 132], [138, 181], [96, 190]];
    hitPolys["PTZcardinalLeft"] = [[9, 52], [58, 74], [53, 86], [53, 105], [58, 116], [9, 138], [0, 96]];
    hitPolys["PTZordinalNE"] = [[138, 9], [116, 58], [124, 63], [127, 66], [132, 74], [181, 52], [171, 19]];
    hitPolys["PTZordinalNW"] = [[52, 9], [74, 58], [66, 63], [63, 66], [58, 74], [9, 52], [19, 19]];
    hitPolys["PTZordinalSW"] = [[52, 181], [74, 132], [66, 127], [63, 124], [58, 116], [9, 138], [19, 171]];
    hitPolys["PTZordinalSE"] = [[138, 181], [116, 132], [124, 127], [127, 124], [132, 116], [181, 138], [171, 171]];

    var ptzCmds = {};
    ptzCmds["PTZzoomIn"] = 5;
    ptzCmds["PTZzoomOut"] = 6;
    ptzCmds["PTZfocusNear"] = -1;
    ptzCmds["PTZfocusFar"] = -2;
    ptzCmds["PTZstop"] = 64;
    ptzCmds["PTZcardinalUp"] = 2;
    ptzCmds["PTZcardinalRight"] = 1;
    ptzCmds["PTZcardinalDown"] = 3;
    ptzCmds["PTZcardinalLeft"] = 0;
    ptzCmds["PTZordinalNE"] = 60;
    ptzCmds["PTZordinalNW"] = 59;
    ptzCmds["PTZordinalSW"] = 61;
    ptzCmds["PTZordinalSE"] = 62;

    var ptzTitles = {};
    ptzTitles["PTZzoomIn"] = "Zoom In";
    ptzTitles["PTZzoomOut"] = "Zoom Out";
    ptzTitles["PTZfocusNear"] = "Focus Near";
    ptzTitles["PTZfocusFar"] = "Focus Far";
    ptzTitles["PTZstop"] = "Stop";
    ptzTitles["PTZcardinalUp"] = "Up";
    ptzTitles["PTZcardinalRight"] = "Right";
    ptzTitles["PTZcardinalDown"] = "Down";
    ptzTitles["PTZcardinalLeft"] = "Left";
    ptzTitles["PTZordinalNE"] = "Up Right";
    ptzTitles["PTZordinalNW"] = "Up Left";
    ptzTitles["PTZordinalSW"] = "Down Left";
    ptzTitles["PTZordinalSE"] = "Down Right";

    $ptzGraphicContainers.each(function (idx, ele)
    {
        ele.graphicObjects = {};
    });

    // Layout PTZ buttons //
    $ptzGraphics.each(function (idx, ele)
    {
        var $ele = $(ele);
        ele.svgid = $ele.attr('svgid');
        ele.ptzcmd = ptzCmds[ele.svgid];
        ele.tooltipText = ptzTitles[ele.svgid];
        var layoutParts = $ele.attr('layoutR').split(' ');
        ele.layout =
            {
                x: parseFloat(layoutParts[0])
                , y: parseFloat(layoutParts[1])
                , w: parseFloat(layoutParts[2])
                , h: parseFloat(layoutParts[3])
            };

        $ele.css("left", ele.layout.x + "px");
        $ele.css("top", ele.layout.y + "px");
        $ele.css("width", ele.layout.w + "px");
        $ele.css("height", ele.layout.h + "px");

        $ele.append('<svg class="icon"><use xlink:href="#svg_x5F_' + ele.svgid + '"></use></svg>');
        ele.defaultColor = $ele.hasClass("ptzBackground") ? "#363B46" : "#15171B";
        $ele.css('color', ele.defaultColor);
        ele.parentNode.graphicObjects[ele.svgid] = ele;
    });

    // PTZ button input events //

    // onHoverEnter called whenever a mouse pointer begins hovering over any button.
    var onHoverEnter = function (btn)
    {
        $ptzButtons.attr("title", btn.tooltipText);
        $hoveredEle = $(btn);
        $hoveredEle.css("color", "#969BA7");
    }
    // onHoverLeave called whenever a mouse pointer leaves any button or a mouse up event is triggered
    var onHoverLeave = function ()
    {
        if ($hoveredEle != null)
        {
            $ptzButtons.removeAttr("title");
            $hoveredEle.css('color', $hoveredEle.get(0).defaultColor);
            $hoveredEle = null;
        }
        if ($activeEle != null)
        {
            self.SendOrQueuePtzCommand(null, null, true);
            $activeEle.css('color', $activeEle.get(0).defaultColor);
            $activeEle = null;
        }
    }
    var onButtonMouseDown = function (btn)
    {
        self.SendOrQueuePtzCommand(videoPlayer.Loading().image.id, btn.ptzcmd, false);
        $activeEle = $(btn);
        $activeEle.css("color", "#FFFFFF");
    }
    var onPointerMove = function (e)
    {
        if (pointInsideElement($ptzGraphicWrapper, e.pageX, e.pageY))
        {
            // Hovering near buttons, maybe over one
            if ($activeEle == null && !touchEvents.isTouchEvent(e))
            {
                var offset = $ptzGraphicWrapper.offset();
                var x = e.pageX - offset.left;
                var y = e.pageY - offset.top;
                var btn = GetHoveredPTZButton(x, y);
                if (btn == null)
                {
                    // Not hovering on any buttons
                    onHoverLeave();
                }
                else if ($hoveredEle == null || $hoveredEle.get(0).svgid != btn.svgid)
                {
                    onHoverLeave();
                    onHoverEnter(btn);
                }
            }
        }
        else
        {
            // Not hovering on any buttons
            onHoverLeave();
        }
    }
    // Hide long-press square that appears when using ptz controls on Windows touchscreen devices.  Unfortunately, it can only be hidden in IE and Edge.
    $ptzGraphicWrapper.get(0).addEventListener("MSHoldVisual", function (e) { e.preventDefault(); }, false);
    $ptzGraphicWrapper.on('mousedown touchstart', function (e)
    {
        if (!ptzControlsEnabled)
            return;
        mouseCoordFixer.fix(e);
        if (touchEvents.Gate(e))
            return;
        if (e.which != 3)
        {
            var offset = $ptzGraphicWrapper.offset();
            var x = e.pageX - offset.left;
            var y = e.pageY - offset.top;
            var btn = GetHoveredPTZButton(x, y);
            if (btn != null)
            {
                onHoverLeave();
                onButtonMouseDown(btn);
                $.hideAllContextMenus();
                return stopDefault(e);
            }
        }
        onPointerMove(e);
    });
    $(document).on('mouseup mouseleave touchend touchcancel', function (e)
    {
        if (!ptzControlsEnabled)
            return;
        mouseCoordFixer.fix(e);
        if (touchEvents.Gate(e))
            return;
        onHoverLeave();
        onPointerMove(e);
    });
    $(document).on('mousemove touchmove', function (e)
    {
        if (!ptzControlsEnabled)
            return;
        mouseCoordFixer.fix(e);
        if (touchEvents.Gate(e))
            return;
        onPointerMove(e);
    });
    var GetHoveredPTZButton = function (x, y)
    {
        var sizeMultiplier = $ptzGraphicWrapper.width() / 190;
        var point = [x / sizeMultiplier, y / sizeMultiplier];
        var buttonId = FindPolyForPoint(point);
        if (buttonId != null)
        {
            var visibleGraphicContainer = GetVisibleGraphicContainer();
            return visibleGraphicContainer ? visibleGraphicContainer.graphicObjects[buttonId] : null;
        }
        return null;
    }
    var FindPolyForPoint = function (point)
    {
        var keys = Object.keys(hitPolys);
        for (var i = 0; i < keys.length; i++)
        {
            if (pointInPolygon(point, hitPolys[keys[i]]))
                return keys[i];
        }
        return null;
    }
    var GetVisibleGraphicContainer = function ()
    {
        return $ptzGraphicContainers.filter(':visible').get(0);
    }

    // PTZ Control display state //
    this.UpdatePtzControlDisplayState = function (loadThumbsOverride)
    {
        var featureEnabled = GetUi3FeatureEnabled("ptzControls");
        LoadPtzPresetThumbs(loadThumbsOverride);
        if (videoPlayer.Loading().image.ptz)
            ptzControlsEnabled = featureEnabled;
        else
        {
            onHoverLeave();
            ptzControlsEnabled = false;
        }
        if (ptzControlsEnabled)
        {
            $ptzControlsContainers.removeAttr("title");
            $ptzPresets.removeClass("disabled");
            $ptzButtons.removeClass("disabled");
            $ptzExtraDropdowns.removeClass("disabled");
            $ptzBackgroundGraphics.css("color", $ptzBackgroundGraphics.get(0).defaultColor);
        }
        else
        {
            $ptzControlsContainers.attr("title", featureEnabled ? "PTZ not available for current camera" : "PTZ disabled by user preference");
            $ptzPresets.addClass("disabled");
            $ptzButtons.addClass("disabled");
            $ptzExtraDropdowns.addClass("disabled");
            $ptzBackgroundGraphics.css("color", "#20242b");
        }
    }
    this.isEnabledNow = function ()
    {
        return ptzControlsEnabled;
    }
    this.setEnabled = function (enabled)
    {
        self.UpdatePtzControlDisplayState(true);
    }
    this.PresetSet = function (presetNumStr)
    {
        if (!ptzControlsEnabled)
            return;
        if (!videoPlayer.Loading().image.ptz)
            return;
        if (!sessionManager.IsAdministratorSession())
        {
            openLoginDialog(function () { self.PresetSet(presetNumStr); });
            return;
        }
        var presetNum = parseInt(presetNumStr);
        var $descInput = $('<input type="text" />');
        $descInput.val(self.GetPresetDescription(presetNum));
        var $question = $('<div style="margin:7px 3px 20px 3px;text-align:center;">Enter a description:<br><br></div>');
        $question.append($descInput);
        AskYesNo($question, function ()
        {
            PTZ_set_preset(presetNum, $descInput.val());
        }, null, toaster.Error, "Set Preset " + presetNum, "Cancel", videoPlayer.Loading().cam.optionDisplay);
    }
    // Enable preset buttons //
    $ptzPresets.each(function (idx, ele)
    {
        var $ele = $(ele);
        ele.presetnum = parseInt($ele.attr("presetnum"));
        $ele.text(ele.presetnum);
        $ele.click(function (e)
        {
            bigThumbHelper.Hide();
            self.PTZ_goto_preset(ele.presetnum);
        });
        if (settings.ui3_contextMenus_trigger !== "Long-Press")
            $ele.longpress(function (e) { self.PresetSet($ele.attr("presetnum")); });
        $ele.on("mouseenter touchstart", function (e)
        {
            if (!ptzControlsEnabled)
                return;

            // Show big preset thumbnail
            var imgData = ptzPresetThumbLoader.GetImgData(videoPlayer.Loading().image.id, ele.presetnum);
            var imgUrl = null;
            var imgW = 0;
            var imgH = 0;
            if (imgData && !imgData.error)
            {
                if (imgData.loaded)
                    imgUrl = imgData.imgEle;
                else
                    imgUrl = imgData.src;
                imgW = imgData.w;
                imgH = imgData.h;
            }
            bigThumbHelper.Show($ele, $ele.parent(), self.GetPresetDescription(ele.presetnum), imgUrl, imgW, imgH);
        });
        $ele.on("mouseleave touchend touchcancel", function (e)
        {
            bigThumbHelper.Hide();
        });
    });
    $(document).on('touchend touchcancel', function (e)
    {
        bigThumbHelper.Hide();
    });
    // Presets //
    var LoadPtzPresetThumbs = function ()
    {
        var loading = videoPlayer.Loading().image;
        if (loading.ptz && GetUi3FeatureEnabled("ptzControls"))
        {
            ptzPresetThumbLoader.NotifyPtzCameraSelected(loading.id);
            LoadPTZPresetDescriptions(loading.id);
        }
        else
        {
            $ptzPresets.each(function (idx, ele)
            {
                $(ele).text(ele.presetnum);
            });
        }
    }
    this.PTZ_goto_preset = function (presetNumber)
    {
        if (!ptzControlsEnabled)
            return;
        if (!videoPlayer.Loading().image.ptz)
        {
            toaster.Error("Current camera is not PTZ");
            return;
        }
        self.PTZ_async_noguarantee(videoPlayer.Loading().image.id, 100 + parseInt(presetNumber));
    }
    var PTZ_set_preset = function (presetNumber, description)
    {
        if (!ptzControlsEnabled)
            return;
        if (!videoPlayer.Loading().image.ptz)
        {
            toaster.Error("Current camera is not PTZ");
            return;
        }
        var cameraId = videoPlayer.Loading().image.id;
        if (description == null || description == "")
            description = "Preset " + presetNumber;
        var args = { cmd: "ptz", camera: cameraId, button: (100 + presetNumber), description: description };
        ExecJSON(args, function (response)
        {
            if (response && typeof response.result != "undefined" && response.result == "success")
            {
                RememberPresetDescription(cameraId, presetNumber, description);
                toaster.Success("Preset " + presetNumber + " set successfully.");
                UpdatePresetImage(cameraId, presetNumber);
            }
        }, function ()
            {
                toaster.Error("Unable to save preset");
            });
    }
    var UpdatePresetImage = function (cameraId, presetNumber)
    {
        if (currentServer.isLoggingOut)
            return;

        // Wait a moment in case Blue Iris needs time to save the updated preset image.
        setTimeout(function ()
        {
            ptzPresetThumbLoader.ReloadPresetImage(cameraId, presetNumber);
        }, 50);
    }
    var LoadPTZPresetDescriptions = function (cameraId)
    {
        if (currentPtzData && currentPtzData.cameraId == cameraId)
            return;
        ExecJSON({ cmd: "ptz", camera: cameraId }, function (response)
        {
            if (videoPlayer.Loading().image.id == cameraId)
            {
                /*
                    brightness:-1
                    contrast:0
                    irmode:0
                    powermode:-1
                    presetnum:15
                    presets:[""]
                    talksamplerate:8000
                */
                currentPtzData = response.data;
                currentPtzData.cameraId = cameraId;
                self.SetIRButtonState();
                self.SetBrightnessButtonState();
                self.SetContrastButtonState();
            }
        }, function ()
            {
                if (videoPlayer.Loading().image.id == cameraId)
                    toaster.Warning("Unable to load PTZ metadata for camera: " + cameraId);
            });
    }
    this.GetPresetDescription = function (presetNum, asAnnotation)
    {
        presetNum = parseInt(presetNum);
        if (presetNum < 0 || presetNum > 20)
            return asAnnotation ? "" : ("Preset " + presetNum);
        var desc = null;
        if (currentPtzData && currentPtzData.cameraId == videoPlayer.Loading().image.id && currentPtzData.presets && currentPtzData.presets.length > presetNum - 1)
            desc = currentPtzData.presets[presetNum - 1];
        if (desc == null || desc == "")
            desc = "Preset " + presetNum;
        if (asAnnotation)
        {
            if (desc.match(/^Preset [0-9]+$/i) == null)
                desc = ' (' + desc + ')';
            else
                desc = '';
        }
        return desc;
    }
    var RememberPresetDescription = function (cameraId, presetNum, description)
    {
        presetNum = parseInt(presetNum);
        if (presetNum < 0 || presetNum > 20)
            return;
        if (currentPtzData && currentPtzData.cameraId == cameraId)
        {
            if (!currentPtzData.presets)
                currentPtzData.presets = [];
            while (currentPtzData.presets.length < presetNum)
                currentPtzData.presets.push('Preset' + (currentPtzData.presets.length + 1));
            currentPtzData.presets[presetNum - 1] = description;
        }
    }
    // PTZ Actions //
    window.onbeforeunload = function ()
    {
        if (unsafePtzActionNeedsStopped)
        {
            unsafePtzActionNeedsStopped = false;
            unsafePtzActionQueued = null;
            if (!unsafePtzActionInProgress)
                self.PTZ_unsafe_sync_guarantee(currentPtzCamId, currentPtz, 1);
        }
        return;
    }
    this.SendOrQueuePtzCommand = function (ptzCamId, ptzCmd, isStopCommand)
    {
        ptzCmd = parseInt(ptzCmd);
        if (isStopCommand)
        {
            if (unsafePtzActionNeedsStopped)
            {
                if (currentPtzCamId != null && currentPtz != null)
                {
                    if (unsafePtzActionInProgress)
                    {
                        unsafePtzActionQueued = function ()
                        {
                            self.PTZ_unsafe_async_guarantee(currentPtzCamId, currentPtz, 1);
                        };
                    }
                    else
                        self.PTZ_unsafe_async_guarantee(currentPtzCamId, currentPtz, 1);
                }
                unsafePtzActionNeedsStopped = false;
            }
        }
        else
        {
            if (!unsafePtzActionNeedsStopped && !unsafePtzActionInProgress && unsafePtzActionQueued == null)
            {
                // All-clear for new start command
                currentPtzCamId = ptzCamId;
                currentPtz = ptzCmd;
                unsafePtzActionNeedsStopped = true;
                self.PTZ_unsafe_async_guarantee(currentPtzCamId, currentPtz, 1);
            }
        }
    }
    this.PTZ_async_noguarantee = function (cameraId, ptzCmd, updown)
    {
        var args = { cmd: "ptz", camera: cameraId, button: parseInt(ptzCmd) };
        if (updown == "1")
            args.updown = 1;
        else if (updown == "2")
            args.button = 64;
        ExecJSON(args, function (response)
        {
        }, function ()
            {
            });
    }
    this.PTZ_unsafe_async_guarantee = function (cameraId, ptzCmd, updown)
    {
        unsafePtzActionInProgress = true;
        var args = { cmd: "ptz", camera: cameraId, button: parseInt(ptzCmd) };
        if (updown == "1")
            args.updown = 1;
        else if (updown == "2")
            args.button = 64;
        ExecJSON(args, function (response)
        {
            unsafePtzActionInProgress = false;
            if (unsafePtzActionQueued != null)
            {
                unsafePtzActionQueued();
                unsafePtzActionQueued = null;
            }
        }, function ()
            {
                setTimeout(function ()
                {
                    self.PTZ_unsafe_async_guarantee(cameraId, ptzCmd, updown);
                }, 100);
            });
    }
    this.PTZ_unsafe_sync_guarantee = function (cameraId, ptzCmd, updown)
    {
        unsafePtzActionInProgress = true;
        var args = { cmd: "ptz", camera: cameraId, button: parseInt(ptzCmd) };
        if (updown == "1")
            args.updown = 1;
        else if (updown == "2")
            args.button = 64;
        ExecJSON(args, function (response)
        {
            unsafePtzActionInProgress = false;
            if (unsafePtzActionQueued != null)
            {
                unsafePtzActionQueued();
                unsafePtzActionQueued = null;
            }
        }, function ()
            {
                self.PTZ_unsafe_sync_guarantee(cameraId, ptzCmd, updown);
            }, true);
    }
    this.Get$PtzPresets = function ()
    {
        return $ptzPresets;
    }
    this.SetIRButtonState = function (irmode)
    {
        if (typeof irmode != "undefined")
            currentPtzData.irmode = irmode;

        if (currentPtzData.irmode == 1)
        {
            $irButtonText.text("*").parent().addClass("yellow");
            $irButtonLabel.text("IR On");
        }
        else if (currentPtzData.irmode == 2)
        {
            $irButtonText.text("A").parent().removeClass("yellow");
            $irButtonLabel.text("IR Auto");
        }
        else // if (currentPtzData.irmode == 0)
        {
            $irButtonText.text("").parent().removeClass("yellow");
            $irButtonLabel.text("IR Off");
        }
    }
    this.SetBrightnessButtonState = function (brightness)
    {
        if (typeof brightness != "undefined")
            currentPtzData.brightness = brightness;
        $brightnessButtonLabel.text("Brightness " + currentPtzData.brightness);
    }
    this.SetContrastButtonState = function (contrast)
    {
        if (typeof contrast != "undefined")
            currentPtzData.contrast = contrast;
        $contrastButtonLabel.text("Contrast " + currentPtzData.contrast);
    }
}
///////////////////////////////////////////////////////////////
// PtzPresetThumbLoader ///////////////////////////////////////
///////////////////////////////////////////////////////////////
var ptzPresetThumbLoader = new (function ()
{
    var self = this;
    // A two-level cache.  The first level is a map of camera names.  The second level is a map of preset numbers to image elements.
    var cache = {};
    var asyncThumbLoader = null;

    var Initialize = function ()
    {
        if (asyncThumbLoader)
            return;
        asyncThumbLoader = new AsyncPresetThumbnailDownloader(thumbLoaded, thumbError);
    }
    this.NotifyPtzCameraSelected = function (cameraId)
    {
        /// <summary>Call this when a PTZ camera is selected so the thumbnails can begin loading (unless they are already cached).</summary>
        if (!CameraIsEligible(cameraId))
            return;

        var camCache = cache[cameraId];
        if (!camCache)
        {
            camCache = cache[cameraId] = {}; // Note: cache and camCache are maps, not arrays.
            for (var i = 1; i <= 20; i++)
            {
                var $img = $('<img src="" alt="' + i + '" class="presetThumb" />');
                $img.hide();
                var img = camCache[i] = $img[0];
                // Unfortunately, we can't allow the browser cache to be used for these, or the cached images become stale when updated and reloading the page doesn't fix it.
                img.imgData = {
                    src: self.UrlForPreset(cameraId, i, true),
                    w: 0,
                    h: 0,
                    imgEle: img
                };
                asyncThumbLoader.Enqueue(img, img.imgData.src);
            }
        }
        ptzButtons.Get$PtzPresets().each(function (idx, ele)
        {
            var $ele = $(ele).empty();
            var img = camCache[ele.presetnum];
            if (img.imgData.w == 0)
                $ele.append('<span>' + ele.presetnum + '</span>')
            $ele.append(img);
        });
    }
    this.ReloadPresetImage = function (cameraId, presetNumber)
    {
        /// <summary>Force-reloads a preset image from the server.</summary>
        if (currentServer.isLoggingOut)
            return false;

        if (presetNumber < 1 || presetNumber > 20)
            return;
        var camCache = cache[cameraId];
        if (camCache)
        {
            var img = camCache[presetNumber];
            img.imgData.src = self.UrlForPreset(cameraId, presetNumber, true);
            asyncThumbLoader.Enqueue(img, img.imgData.src);
        }
        else
            self.NotifyPtzCameraSelected(cameraId); // This case shouldn't happen.
    }
    this.GetImgData = function (cameraId, presetNumber)
    {
        if (presetNumber >= 1 && presetNumber <= 20)
        {
            var camCache = cache[cameraId];
            if (camCache)
                return camCache[presetNumber].imgData;
        }
        return null;
    }
    var thumbLoaded = function (img)
    {
        if (img.complete && typeof img.naturalWidth != "undefined" && img.naturalWidth > 0)
        {
            img.imgData.error = false;
            img.imgData.w = img.naturalWidth
            img.imgData.h = img.naturalHeight;
            img.imgData.loaded = true;
            var $img = $(img);
            $img.prev('span').remove();
            $img.show();
            try
            {
                var remainder = img.getBoundingClientRect().height % 1;
                if (remainder != 0)
                    $thumb.css("padding-bottom", (1 - remainder) + "px");
            }
            catch (ex) { }
        }
        else
            img.imgData.error = true;
    }
    var thumbError = function (img)
    {
        img.imgData.error = true;
    }
    this.UrlForPreset = function (cameraId, presetNumber, overrideCache)
    {
        if (presetNumber < 1 || presetNumber > 20)
            return "";
        var sessionArg = currentServer.GetAPISessionArg("?");
        var cacheArg = overrideCache ? ((sessionArg ? "&" : "?") + "cache=" + Date.now()) : "";
        return currentServer.remoteBaseURL + "image/" + cameraId + "/preset_" + presetNumber + ".jpg" + sessionArg + cacheArg;
    }
    var CameraIsEligible = function (cameraId)
    {
        if (currentServer.isLoggingOut)
            return false;
        var loading = videoPlayer.Loading().image;
        if (cameraId != loading.id)
            return false;
        if (!loading.ptz)
            return false;
        if (!GetUi3FeatureEnabled("ptzControls"))
            return false;
        Initialize();
        return true;
    }
})();
///////////////////////////////////////////////////////////////
// Timeline ///////////////////////////////////////////////////
///////////////////////////////////////////////////////////////
function ClipTimeline()
{
    var self = this;
    var $canvas = $("#canvas_clipTimeline");
    var canvas = $canvas.get(0);
    var dpiScale = BI_GetDevicePixelRatio();
    var isDragging = false;
    var currentSelectedRelativePosition = -1;
    var currentGhostRelativePosition = -1;
    var currentSoftGhostRelativePosition = -1;
    $canvas.on("mousedown touchstart", function (e)
    {
        mouseCoordFixer.fix(e);
        isDragging = true;
        currentGhostRelativePosition = pageXToRelativePosition(e.pageX);
        self.Draw();
    });
    $(document).on("mouseup touchend touchcancel", function (e)
    {
        mouseCoordFixer.fix(e);
        if (isDragging)
        {
            isDragging = false;
            var newPosition = pageXToRelativePosition(e.pageX);
            if (newPosition != -1)
                currentSelectedRelativePosition = newPosition;
            currentGhostRelativePosition = -1;
            self.Draw();
        }
    });
    $(document).on("mousemove touchmove", function (e)
    {
        mouseCoordFixer.fix(e);
        var newSoftGhost = -1;
        if (pointInsideElement($canvas, e.pageX, e.pageY))
            newSoftGhost = pageXToRelativePosition(e.pageX);
        var changedGhost = currentSoftGhostRelativePosition != newSoftGhost;
        currentSoftGhostRelativePosition = newSoftGhost;
        if (isDragging)
        {
            currentGhostRelativePosition = pageXToRelativePosition(e.pageX);
            self.Draw();
        }
        else if (changedGhost)
            self.Draw();
    });
    $canvas.on("mouseleave", function (e)
    {
        mouseCoordFixer.fix(e);
        currentSoftGhostRelativePosition = -1;
        self.Draw();
    });
    this.Resized = function ()
    {
        if (!$canvas.is(":visible"))
            return;
        dpiScale = BI_GetDevicePixelRatio();
        canvas.width = $canvas.width() * dpiScale;
        canvas.height = $canvas.height() * dpiScale;
    };
    this.Draw = function ()
    {
        drawInternal(currentSelectedRelativePosition, currentGhostRelativePosition, currentSoftGhostRelativePosition);
    }
    var drawInternal = function (handlePosition, ghostPosition, softGhostPosition)
    {
        if (!$canvas.is(":visible"))
            return;
        var timelineBorder = getTimelineBorder();

        var timelineHourLabelsY = 30 * dpiScale;
        var timelineTickMarkStartY = 33 * dpiScale;
        var timelineTickMarkEndY = timelineBorder.startY;
        var timelineTickMarksH = 4 * dpiScale;

        var fontSize = 16 * dpiScale;
        if (canvas.width < 1000)
            fontSize = Math.max(8, fontSize * (canvas.width / 1000));

        var ctx = canvas.getContext("2d");
        ctx.clearRect(0, 0, canvas.width, canvas.height);
        ctx.strokeStyle = ctx.fillStyle = "#FFFFFF";
        //ctx.strokeStyle = ctx.fillStyle = "#969BA7";
        ctx.font = fontSize + "px Arial";

        // Draw timeline rectangle
        ctx.strokeRect(timelineBorder.startX, timelineBorder.startY, timelineBorder.width, timelineBorder.height);

        // Draw tick marks
        ctx.beginPath();
        var tickMarkDistance = timelineBorder.width / 24;
        for (var i = 0; i <= 24; i++)
        {
            var x = timelineBorder.startX + (i * tickMarkDistance);
            ctx.moveTo(x, timelineTickMarkStartY);
            ctx.lineTo(x, timelineTickMarkEndY);
        }
        ctx.stroke();

        // Draw hour labels
        for (var i = 0; i <= 24; i++)
        {
            var label = (i + "").padLeft(2, '0');
            var width = ctx.measureText(label).width;
            var x = (timelineBorder.startX + (i * tickMarkDistance)) - (width / 2);
            ctx.fillText(label, x, timelineHourLabelsY);
        }

        // Draw handle
        if (typeof handlePosition != "undefined" && handlePosition != -1)
            drawHandle(ctx, timelineBorder.startX + (handlePosition * timelineBorder.width), "1", "1");

        // Draw ghost handle
        if (typeof ghostPosition != "undefined" && ghostPosition != -1)
            drawHandle(ctx, timelineBorder.startX + (ghostPosition * timelineBorder.width), ".5", "0.1");

        // Draw softer ghost handle
        if (typeof softGhostPosition != "undefined" && softGhostPosition != -1)
            drawHandle(ctx, timelineBorder.startX + (softGhostPosition * timelineBorder.width), ".2", "0");
    };
    var drawHandle = function (ctx, handleX, strokeAlpha, fillAlpha)
    {
        var handleTopY = 33 * dpiScale;
        var handlePointY = 61 * dpiScale;
        var handleRectTopY = 67 * dpiScale;
        var handleRectBotY = 83 * dpiScale;
        var handleWidth = 16 * dpiScale;
        var handleLeft = handleX - (handleWidth / 2);
        var handleRight = handleX + (handleWidth / 2);

        ctx.strokeStyle = "rgba(0,151,240," + strokeAlpha + ")";
        ctx.fillStyle = "rgba(0,151,240," + fillAlpha + ")";
        ctx.beginPath();
        ctx.moveTo(handleX, handlePointY);
        ctx.lineTo(handleLeft, handleRectTopY);
        ctx.lineTo(handleLeft, handleRectBotY);
        ctx.lineTo(handleRight, handleRectBotY);
        ctx.lineTo(handleRight, handleRectTopY);
        ctx.closePath();
        ctx.fill();
        ctx.stroke();
        ctx.beginPath();
        ctx.moveTo(handleX, handlePointY);
        ctx.lineTo(handleX, handleTopY);
        ctx.stroke();
    };
    var pageXToRelativePosition = function (pageX)
    {
        var timelineBorder = getTimelineBorder();
        var position = (pageX - timelineBorder.startX - $canvas.offset().left) / timelineBorder.width;
        if (position < -0.1 || position > 1.1)
            return -1;
        position = Clamp(position, 0, 1);
        return position;
    };
    var getTimelineBorder
 
Top