Merge branch 'master' of github.com:ZoneMinder/zoneminder

This commit is contained in:
Isaac Connor
2024-06-02 12:51:52 -04:00
10 changed files with 200 additions and 50 deletions

View File

@@ -1033,7 +1033,7 @@ public static function getStatuses() {
' . $blockRatioControl . '
<div class="grid-stack-item-content">
<div id="monitor'. $this->Id() . '" data-id="'.$this->Id().'" class="monitor"
title="shift+click to zoom, click+drag to pan, ctrl+click to zoom out, ctrl+shift+click to zoom out completely"
title="Shift+Click to Zoom, Click+Drag to Pan &#013;Ctrl+Click to Zoom out, Ctrl+Shift+Click to Zoom out completely"
>
<div
id="imageFeed'. $this->Id() .'"

View File

@@ -94,6 +94,7 @@ function MonitorStream(monitorData) {
* height should be auto, 100%, integer +px
* param.resizeImg be boolean (added only for using GridStack & PanZoom on Montage page)
* param.scaleImg scaling 1=100% (added only for using PanZoom on Montage & Watch page)
* param.streamQuality in %, numeric value from -50 to +50)
* */
this.setScale = function(newscale, width, height, param = {}) {
const img = this.getElement();
@@ -173,10 +174,15 @@ function MonitorStream(monitorData) {
$j(img).closest('.monitorStream')[0].style.overflow = 'hidden';
}
}
this.setStreamScale(newscale);
let streamQuality = 0;
if (param.streamQuality) {
streamQuality = param.streamQuality;
newscale += parseInt(newscale/100*streamQuality);
}
this.setStreamScale(newscale, streamQuality);
}; // setScale
this.setStreamScale = function(newscale) {
this.setStreamScale = function(newscale, streamQuality=0) {
const img = this.getElement();
if (!img) {
console.log("No img in setScale");
@@ -187,7 +193,7 @@ function MonitorStream(monitorData) {
newscale = parseInt(100*parseInt(stream_frame.width())/this.width);
}
if (newscale > 100) newscale = 100; // we never request a larger image, as it just wastes bandwidth
if (newscale < 25) newscale = 25; // Arbitrary, lower values look bad
if (newscale < 25 && streamQuality > -1) newscale = 25; // Arbitrary, lower values look bad
if (newscale <= 0) newscale = 100;
this.scale = newscale;
if (this.connKey) {

View File

@@ -15,15 +15,9 @@ var zmPanZoom = {
const _this = this;
$j('.zoompan').each( function() {
_this.action('enable', {obj: this});
var id;
if (this.querySelector("[id^='liveStream']")) {
//Montage & Watch page
id = stringToNumber(this.querySelector("[id^='liveStream']").id);
} else {
//Event page
id = eventData.MonitorId;
}
$j(document).on('keyup keydown', function(e) {
const stream = this.querySelector("[id^='liveStream']");
const id = (stream) ? stringToNumber(stream.id) /* Montage & Watch page */ : eventData.MonitorId; /* Event page */
$j(document).on('keyup.panzoom keydown.panzoom', function(e) {
_this.shifted = e.shiftKey ? e.shiftKey : e.shift;
_this.ctrled = e.ctrlKey;
_this.manageCursor(id);
@@ -69,6 +63,7 @@ var zmPanZoom = {
_this.setTriggerChangedMonitors(id);
});
} else if (action == "disable") { //Disable a specific object
$j(document).off('keyup.panzoom keydown.panzoom');
$j('.btn-zoom-in').addClass('hidden');
$j('.btn-zoom-out').addClass('hidden');
this.panZoom[param['id']].reset();

View File

@@ -44,18 +44,39 @@ $rates = array(
$scales = array(
# We use 0 instead of words because we are saving this in the monitor
# and use this array to populate the default scale option
'0' => translate('Auto'),
'0' => translate('Auto'),
# '400' => '4x',
# '300' => '3x',
# '200' => '2x',
# '150' => '1.5x',
'100' => translate('Actual'),
'100' => translate('Actual'),
# '75' => '3/4x',
# '50' => '1/2x',
# '33' => '1/3x',
# '25' => '1/4x',
# '12.5' => '1/8x',
'fit_to_width' => translate('Fit to width'),
'fit_to_width' => translate('Fit to width'),
'480px' => translate('Max 480px'),
'640px' => translate('Max 640px'),
'800px' => translate('Max 800px'),
'1024px' => translate('Max 1024px'),
'1280px' => translate('Max 1280px'),
'1600px' => translate('Max 1600px'),
);
$streamQuality = array(
# In %
'+50' => '+50%',
'+40' => '+40%',
'+30' => '+30%',
'+20' => '+20%',
'+10' => '+10%',
'0' => translate('Optimal'),
'-10' => '-10%',
'-20' => '-20%',
'-30' => '-30%',
'-40' => '-40%',
'-50' => '-50%',
);
if ( isset($_REQUEST['view']) && ($_REQUEST['view'] == 'montage') ) {

View File

@@ -52,7 +52,7 @@ if ($rate > 1600) {
zm_setcookie('zmEventRate', $rate);
}
// $scaleSelected - временно для адаптации нового алгоритма, т.к. $scale может быть только численное значение !
// $scaleSelected - temporarily to adapt the new algorithm, because $scale can only be a numeric value!
if (isset($_REQUEST['scale'])) {
$scale = validInt($_REQUEST['scale']);
$scaleSelected = $_REQUEST['scale'];
@@ -63,8 +63,18 @@ if (isset($_REQUEST['scale'])) {
$scale = validInt($monitor->DefaultScale());
$scaleSelected = $monitor->DefaultScale();
}
if (!validInt($scale) and $scale != '0') {
$scale = '0';
//if (!validInt($scale) and $scale != '0') {
// $scale = '0';
//}
$scale = '10'; // temporarily to adapt the new algorithm, not used in new calculations!
$streamQualitySelected = '0';
if (isset($_REQUEST['streamQuality'])) {
$streamQualitySelected = $_REQUEST['streamQuality'];
} else if (isset($_COOKIE['zmStreamQuality'])) {
$streamQualitySelected = $_COOKIE['zmStreamQuality'];
} else if (isset($_SESSION['zmStreamQuality']) ) {
$streamQualitySelected = $_SESSION['zmStreamQuality'];
}
$showZones = false;
@@ -227,6 +237,10 @@ if ( $Event->Id() and !file_exists($Event->Path()) )
<label for="scale"><?php echo translate('Scale') ?></label>
<?php echo htmlSelect('scale', $scales, $scaleSelected, array('data-on-change'=>'changeScale','id'=>'scale')); ?>
</div>
<div id="streamQualityControl">
<label for="streamQuality"><?php echo translate('Stream quality') ?></label>
<?php echo htmlSelect('streamQuality', $streamQuality, $streamQualitySelected, array('data-on-change'=>'changeStreamQuality','id'=>'streamQuality')); ?>
</div>
<div id="codecControl">
<label for="codec"><?php echo translate('Codec') ?></label>
<?php echo htmlSelect('codec', $codecs, $codec, array('data-on-change'=>'changeCodec','id'=>'codec')); ?>
@@ -402,9 +416,9 @@ if ( (ZM_WEB_STREAM_METHOD == 'mpeg') && ZM_MPEG_LIVE_FORMAT ) {
<button type="button" id="fastFwdBtn" title="<?php echo translate('FastForward') ?>" class="inactive" data-on-click-true="streamFastFwd">
<i class="material-icons md-18">fast_forward</i>
</button>
<button type="button" id="zoomOutBtn" title="<?php echo translate('ZoomOut') ?>" class="unavail" disabled="disabled" data-on-click="clickZoomOut">
<!--<button type="button" id="zoomOutBtn" title="<?php echo translate('ZoomOut') ?>" class="unavail" disabled="disabled" data-on-click="clickZoomOut">
<i class="material-icons md-18">zoom_out</i>
</button>
</button>-->
<button type="button" id="fullscreenBtn" title="<?php echo translate('Fullscreen') ?>" class="avail" data-on-click="fullscreenClicked">
<i class="material-icons md-18">fullscreen</i>
</button>

View File

@@ -41,6 +41,7 @@ var coordinateMouse = {
};
var leftBtnStatus = {Down: false, UpAfterDown: false};
var updateScale = false; //Scale needs to be updated
var currentScale = 0; // Temporarily, because need to put things in order with the "scale" variable = "select" block
$j(document).on("keydown", "", function(e) {
e = e || window.event;
@@ -276,6 +277,10 @@ function changeCodec() {
location.replace(thisUrl + '?view=event&eid=' + eventData.Id + filterQuery + sortQuery+'&codec='+$j('#codec').val());
}
function deltaScale() {
return parseInt(currentScale/100*$j('#streamQuality').val()); // "-" - Decrease quality, "+" - Increase image quality in %
}
function changeScale() {
const scaleSel = $j('#scale').val();
let newWidth;
@@ -284,6 +289,7 @@ function changeScale() {
const alarmCue = $j('#alarmCues');
const bottomEl = $j('#replayStatus');
const landscape = eventData.width / eventData.height > 1 ? true : false; //Image orientation.
setCookie('zmEventScale'+eventData.MonitorId, scaleSel);
@@ -295,26 +301,41 @@ function changeScale() {
//Actual, 100% of original size
newWidth = eventData.Width;
newHeight = eventData.Height;
scale = 100;
currentScale = 100;
} else if (scaleSel == '0') {
//Auto, Width is calculated based on the occupied height so that the image and control buttons occupy the visible part of the screen.
newSize = scaleToFit(eventData.Width, eventData.Height, eventViewer, bottomEl, $j('#wrapperEventVideo'));
newWidth = newSize.width;
newHeight = newSize.height;
scale = newSize.autoScale;
currentScale = newSize.autoScale;
} else if (scaleSel == 'fit_to_width') {
//Fit to screen width
newSize = scaleToFit(eventData.Width, eventData.Height, eventViewer, false, $j('#wrapperEventVideo'));
newWidth = newSize.width;
newHeight = newSize.height;
//newHeight = 'auto';
scale = newSize.autoScale;
currentScale = newSize.autoScale;
} else if (scaleSel.indexOf("px") > -1) {
newSize = scaleToFit(eventData.Width, eventData.Height, eventViewer, false, $j('#wrapperEventVideo')); // Only for calculating the maximum width!
let w = h = '';
if (landscape) {
w = Math.min(stringToNumber(scaleSel), newSize.width);
h = w / (eventData.Width / eventData.Height);
} else {
h = Math.min(stringToNumber(scaleSel), newSize.height);
w = h * (eventData.Width / eventData.Height);
}
newWidth = parseInt(w);
newHeight = parseInt(h);
currentScale = parseInt(w / eventData.Width * 100);
currentScale = currentScale;
}
//console.log(`Real dimensions: ${eventData.Width} X ${eventData.Height}, Scale: ${currentScale}, deltaScale: ${deltaScale()}, New dimensions: ${newWidth} X ${newHeight}`);
eventViewer.width(newWidth);
eventViewer.height(newHeight);
if (!vid) { // zms needs extra sizing
streamScale(scale);
streamScale(currentScale);
drawProgressBar();
}
if (cueFrames) {
@@ -325,7 +346,7 @@ function changeScale() {
// After a resize, check if we still have room to display the event stats table
onStatsResize(newWidth);
updateScale = true;
//updateScale = true;
/* OLD version
scale = parseFloat($j('#scale').val());
@@ -364,6 +385,12 @@ function changeScale() {
*/
} // end function changeScale
function changeStreamQuality() {
const streamQuality = $j('#streamQuality').val();
setCookie('zmStreamQuality', streamQuality);
streamScale(currentScale);
}
function changeReplayMode() {
var replayMode = $j('#replayMode').val();
@@ -449,15 +476,17 @@ function getCmdResponse(respObj, respText) {
streamPlay( );
}
$j('#progressValue').html(secsToTime(parseInt(streamStatus.progress)));
$j('#zoomValue').html(streamStatus.zoom);
if (streamStatus.zoom == '1.0') {
setButtonState('zoomOutBtn', 'unavail');
} else {
setButtonState('zoomOutBtn', 'inactive');
}
if (scale && (streamStatus.scale !== undefined) && (streamStatus.scale != scale)) {
console.log("Stream not scaled, re-applying", scale, streamStatus.scale);
streamScale(scale);
//$j('#zoomValue').html(streamStatus.zoom);
$j('#zoomValue').html(zmPanZoom.panZoom[eventData.MonitorId].getScale().toFixed(1));
//if (streamStatus.zoom == '1.0') {
// setButtonState('zoomOutBtn', 'unavail');
//} else {
// setButtonState('zoomOutBtn', 'inactive');
//}
if (currentScale && (streamStatus.scale !== undefined) && (streamStatus.scale != currentScale + deltaScale())) {
console.log("Stream not scaled, re-applying", currentScale + deltaScale(), streamStatus.scale);
streamScale(currentScale);
}
updateProgressBar();
@@ -511,8 +540,10 @@ function playClicked( ) {
// The assumption is that the command failed because zms exited, so restart the stream.
const img = document.getElementById('evtStream');
const src = img.src;
const url = new URL(src);
url.searchParams.set('scale', currentScale); // In event.php we dont yet know what scale to substitute. Let it be for now.
img.src = '';
img.src = src;
img.src = url;
zmsBroke = false;
} else {
streamReq({command: CMD_PLAY});
@@ -686,6 +717,7 @@ function tagAndPrev(action) {
streamPrev(action);
}
/* Not used
function vjsPanZoom(action, x, y) { //Pan and zoom with centering where the click occurs
var outer = $j('#videoobj');
var video = outer.children().first();
@@ -758,13 +790,16 @@ function streamZoomOut() {
streamReq({command: CMD_ZOOMOUT});
}
}
*/
function streamScale(scale) {
scale += deltaScale();
if (document.getElementById('evtStream')) {
streamReq({command: CMD_SCALE, scale: (scale>100) ? 100 : scale});
}
}
/*
function streamPan(x, y) {
if (vid) {
vjsPanZoom('pan', x, y);
@@ -772,6 +807,7 @@ function streamPan(x, y) {
streamReq({command: CMD_PAN, x: x, y: y});
}
}
*/
function streamSeek(offset) {
if (vid) {
@@ -808,7 +844,7 @@ function getEventResponse(respObj, respText) {
$j('#modeValue').html('Replay');
$j('#zoomValue').html('1');
$j('#rate').val('100');
vjsPanZoom('zoomOut');
//vjsPanZoom('zoomOut');
} else {
drawProgressBar();
}
@@ -1019,6 +1055,7 @@ function handleClick(event) {
}
} else {
// +++ Old ZoomPan algorithm.
/*
if (vid && (event.target.id != 'videoobj')) {
return; // ignore clicks on control bar
}
@@ -1045,10 +1082,11 @@ function handleClick(event) {
updatePrevCoordinatFrame(x, y); //Fixing current coordinates after scaling or shifting
}
}
// --- Old ZoomPan algorithm.
*/// --- Old ZoomPan algorithm.
}
}
/*
function shiftImgFrame() { //We calculate the coordinates of the image displacement and shift the image
let newPosX = parseInt(PrevCoordinatFrame.x - coordinateMouse.shiftMouse_x);
let newPosY = parseInt(PrevCoordinatFrame.y - coordinateMouse.shiftMouse_y);
@@ -1082,8 +1120,10 @@ function getCoordinateMouse(event) { //We get the current cursor coordinates tak
return {x: parseInt((event.pageX - pos.left) * scaleX), y: parseInt((event.pageY - pos.top) * scaleY)}; //The point of the mouse click relative to the dimensions of the real frame.
}
*/
function handleMove(event) {
/*
if (panZoomEnabled) {
return;
}
@@ -1126,6 +1166,7 @@ function handleMove(event) {
leftBtnStatus.UpAfterDown = false;
}
// --- Old ZoomPan algorithm.
*/
}
// Manage the DELETE CONFIRMATION modal button
@@ -1277,6 +1318,7 @@ function initPage() {
// Load the event stats
getStat();
zmPanZoom.init();
changeStreamQuality();
if (getEvtStatsCookie() != 'on') {
eventStats.toggle(false);
@@ -1341,7 +1383,8 @@ function initPage() {
}
} // end if videojs or mjpeg stream
progressBarNav();
if (scale == '0') changeScale();
//if (scale == '0') changeScale();
changeScale();
nearEventsQuery(eventData.Id);
initialAlarmCues(eventData.Id); //call ajax+renderAlarmCues
document.querySelectorAll('select[name="rate"]').forEach(function(el) {
@@ -1634,9 +1677,10 @@ function initPage() {
if (updateScale) {
const eventViewer = $j(vid ? '#videoobj' : '#evtStream');
const panZoomScale = panZoomEnabled ? zmPanZoom.panZoom[eventData.MonitorId].getScale() : 1;
const newSize = scaleToFit(eventData.Width, eventData.Height, eventViewer, false, $j('#wrapperEventVideo'), panZoomScale);
const newSize = scaleToFit(eventData.Width, eventData.Height, eventViewer, false, $j('#videoFeed'), panZoomScale);
scale = newSize.autoScale > 100 ? 100 : newSize.autoScale;
//streamScale(scale);
currentScale = scale;
streamScale(currentScale);
updateScale = false;
}
}, 500);

View File

@@ -947,6 +947,12 @@ function panZoomOut(el) {
zmPanZoom.zoomOut(el);
}
function changeStreamQuality() {
const streamQuality = $j('#streamQuality').val();
setCookie('zmStreamQuality', streamQuality);
monitorsSetScale();
}
function monitorsSetScale(id=null) {
// This function will probably need to be moved to the main JS file, because now used on Watch & Montage pages
if (id || typeof monitorStream !== 'undefined') {
@@ -960,13 +966,13 @@ function monitorsSetScale(id=null) {
}
const el = document.getElementById('liveStream'+id);
const panZoomScale = panZoomEnabled ? zmPanZoom.panZoom[id].getScale() : 1;
currentMonitor.setScale(0, el.clientWidth * panZoomScale + 'px', el.clientHeight * panZoomScale + 'px', {resizeImg: false});
currentMonitor.setScale(0, el.clientWidth * panZoomScale + 'px', el.clientHeight * panZoomScale + 'px', {resizeImg: false, streamQuality: $j('#streamQuality').val()});
} else {
for ( let i = 0, length = monitors.length; i < length; i++ ) {
const id = monitors[i].id;
const el = document.getElementById('liveStream'+id);
const panZoomScale = panZoomEnabled ? zmPanZoom.panZoom[id].getScale() : 1;
monitors[i].setScale(0, parseInt(el.clientWidth * panZoomScale) + 'px', parseInt(el.clientHeight * panZoomScale) + 'px', {resizeImg: false});
monitors[i].setScale(0, parseInt(el.clientWidth * panZoomScale) + 'px', parseInt(el.clientHeight * panZoomScale) + 'px', {resizeImg: false, streamQuality: $j('#streamQuality').val()});
}
}
}

View File

@@ -163,6 +163,13 @@ function changeScale() {
setScale();
*/
}
function changeStreamQuality() {
const streamQuality = $j('#streamQuality').val();
setCookie('zmStreamQuality', streamQuality);
monitorsSetScale(monitorId);
}
// Implement current scale, as opposed to changing it
function setScale() {
/*
@@ -1321,7 +1328,11 @@ function monitorsSetScale(id=null) {
const scale = $j('#scale').val();
let resize;
let width;
let maxWidth = '';
let height;
let overrideHW = false;
let defScale = 0;
const landscape = curentMonitor.width / curentMonitor.height > 1 ? true : false; //Image orientation.
if (scale == '0') {
//Auto, Width is calculated based on the occupied height so that the image and control buttons occupy the visible part of the screen.
@@ -1338,22 +1349,49 @@ function monitorsSetScale(id=null) {
resize = false;
width = parseInt(window.innerWidth * panZoomScale) + 'px';
height = 'auto';
} else if (scale.indexOf("px") > -1) {
if (landscape) {
maxWidth = scale;
defScale = parseInt(Math.min(stringToNumber(scale), window.innerWidth) / curentMonitor.width * panZoomScale * 100);
height = 'auto';
} else {
defScale = parseInt(Math.min(stringToNumber(scale), window.innerHeight) / curentMonitor.height * panZoomScale * 100);
height = scale;
}
resize = true;
width = 'auto';
overrideHW = true;
}
if (resize) {
document.getElementById('monitor'+id).style.width = 'max-content'; //Required when switching from resize=false to resize=true
}
//curentMonitor.setScale(0, el.clientWidth * panZoomScale + 'px', el.clientHeight * panZoomScale + 'px', {resizeImg:true, scaleImg:panZoomScale});
curentMonitor.setScale(0, width, height, {resizeImg: resize, scaleImg: panZoomScale});
if (!resize) {
if (scale == '0') {
document.getElementById('monitor'+id).style.width = 'max-content'; //Required when switching from resize=false to resize=true
}
document.getElementById('monitor'+id).style.maxWidth = maxWidth;
if (!landscape) { //PORTRAIT
document.getElementById('monitor'+id).style.width = 'max-content';
document.getElementById('liveStream'+id).style.height = height;
}
} else {
document.getElementById('liveStream'+id).style.height = '';
document.getElementById('monitor'+id).style.width = width;
document.getElementById('monitor'+id).style.maxWidth = '';
if (scale == 'fit_to_width') {
document.getElementById('monitor'+id).style.width = '';
} else if (scale == '100') {
document.getElementById('monitor'+id).style.width = 'max-content';
document.getElementById('liveStream'+id).style.width = width;
}
}
//curentMonitor.setScale(0, maxWidth ? maxWidth : width, height, {resizeImg: resize, scaleImg: panZoomScale});
curentMonitor.setScale(defScale, width, height, {resizeImg: resize, scaleImg: panZoomScale, streamQuality: $j('#streamQuality').val()});
if (overrideHW) {
if (!landscape) { //PORTRAIT
document.getElementById('monitor'+id).style.width = 'max-content';
} else {
document.getElementById('liveStream'+id).style.height = 'auto';
document.getElementById('monitor'+id).style.width = 'auto';
}
}
} else {
for ( let i = 0, length = monitors.length; i < length; i++ ) {
const id = monitors[i].id;

View File

@@ -162,6 +162,15 @@ scaleControl is no longer used!
*/
}
$streamQualitySelected = '0';
if (isset($_REQUEST['streamQuality'])) {
$streamQualitySelected = $_REQUEST['streamQuality'];
} else if (isset($_COOKIE['zmStreamQuality'])) {
$streamQualitySelected = $_COOKIE['zmStreamQuality'];
} else if (isset($_SESSION['zmStreamQuality']) ) {
$streamQualitySelected = $_SESSION['zmStreamQuality'];
}
if (!empty($_REQUEST['maxfps']) and validFloat($_REQUEST['maxfps']) and ($_REQUEST['maxfps']>0)) {
$options['maxfps'] = validHtmlStr($_REQUEST['maxfps']);
} else if (isset($_COOKIE['zmMontageRate'])) {
@@ -290,6 +299,10 @@ echo htmlSelect('changeRate', $maxfps_options, $options['maxfps'], array('id'=>'
<label><?php echo translate('Ratio') ?></label>
<?php echo htmlSelect('ratio', [], '', array('id'=>'ratio', 'data-on-change'=>'changeRatioForAll', 'class'=>'chosen')); ?>
</span>
<span id="streamQualityControl">
<label for="streamQuality"><?php echo translate('Stream quality') ?></label>
<?php echo htmlSelect('streamQuality', $streamQuality, $streamQualitySelected, array('data-on-change'=>'changeStreamQuality','id'=>'streamQuality')); ?>
</span>
<span id="widthControl" class="hidden"> <!-- OLD version, requires removal -->
<label><?php echo translate('Width') ?></label>
<?php echo htmlSelect('width', $widths, 'auto'/*$options['width']*/, array('id'=>'width', 'data-on-change'=>'changeWidth', 'class'=>'chosen')); ?>

View File

@@ -165,6 +165,15 @@ if ( !isset($scales[$scale])) {
}
$options['scale'] = 0; //Somewhere something is spoiled because of this...
$streamQualitySelected = '0';
if (isset($_REQUEST['streamQuality'])) {
$streamQualitySelected = $_REQUEST['streamQuality'];
} else if (isset($_COOKIE['zmStreamQuality'])) {
$streamQualitySelected = $_COOKIE['zmStreamQuality'];
} else if (isset($_SESSION['zmStreamQuality']) ) {
$streamQualitySelected = $_SESSION['zmStreamQuality'];
}
if (isset($_REQUEST['width'])) {
$options['width'] = validInt($_REQUEST['width']);
} else if ( isset($_COOKIE['zmWatchWidth']) and $_COOKIE['zmWatchWidth'] ) {
@@ -276,6 +285,10 @@ echo htmlSelect('changeRate', $maxfps_options, $options['maxfps']);
<label><?php echo translate('Scale') ?>:</label>
<?php echo htmlSelect('scale', $scales, $scale, array('id'=>'scale', 'data-on-change-this'=>'changeScale') ); ?>
</span>
<span id="streamQualityControl">
<label for="streamQuality"><?php echo translate('Stream quality') ?></label>
<?php echo htmlSelect('streamQuality', $streamQuality, $streamQualitySelected, array('data-on-change'=>'changeStreamQuality','id'=>'streamQuality')); ?>
</span>
</div><!--sizeControl-->
</div><!--control header-->
</div><!--flip-->