/* 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 -> 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