Skip to content

Commit

Permalink
MJPEG: Fit image to browser window #218
Browse files Browse the repository at this point in the history
  • Loading branch information
dkrivoruchko committed Aug 9, 2024
1 parent 00413f0 commit e250696
Show file tree
Hide file tree
Showing 32 changed files with 263 additions and 21 deletions.
2 changes: 1 addition & 1 deletion mjpeg/src/main/assets/dev/babel.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"@babel/preset-env",
{
"useBuiltIns": "entry",
"corejs": "3.37.0"
"corejs": "3.37.1"
}
]
]
Expand Down
4 changes: 2 additions & 2 deletions mjpeg/src/main/assets/dev/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"devDependencies": {
"@babel/cli": "^7.24.0",
"@babel/core": "^7.24.0",
"@babel/preset-env": "^7.24.0"
"@babel/core": "^7.25.0",
"@babel/preset-env": "^7.25.0"
}
}
10 changes: 10 additions & 0 deletions mjpeg/src/main/assets/dev/script.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,15 @@ function configureButtons(enable) {
}
}
}
function configureFitWindow(enable) {
if (enable) {
stream.style.width = "100%";
stream.style.objectFit = "contain";
} else {
stream.style.width = null;
stream.style.objectFit = null;
}
}
if (!document.pictureInPictureEnabled) buttonPiP.style.display = "none";

window.onmousemove = () => {
Expand Down Expand Up @@ -167,6 +176,7 @@ function connect() {
if (message.type === "SETTINGS") {
document.body.style.backgroundColor = message.data.backColor;
configureButtons(message.data.enableButtons);
configureFitWindow(message.data.fitWindow);
return;
}

Expand Down
18 changes: 14 additions & 4 deletions mjpeg/src/main/assets/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@
</div>
</div>

<div id="streamDiv"><img id="stream" /></div>
<div id="streamDiv"><img id="stream" FIT_WINDOW /></div>

<div id="buttonsDiv">
<input type="image" id="fullscreen" style="width:24px;height:24px;margin:4px;" src="data:image/svg+xml;charset=UTF-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23FFF' d='M5,5H10V7H7V10H5V5M14,5H19V10H17V7H14V5M17,14H19V19H14V17H17V14M10,17V19H5V14H7V17H10Z' /%3E%3C/svg%3E" onclick="toggleFullscreen()" />
Expand Down Expand Up @@ -273,6 +273,15 @@
}
}
}
function configureFitWindow(enable) {
if (enable) {
stream.style.width = "100%";
stream.style.objectFit = "contain";
} else {
stream.style.width = null;
stream.style.objectFit = null;
}
}
if (!document.pictureInPictureEnabled) buttonPiP.style.display = "none";
window.onmousemove = function () {
if (!enableButtons) return;
Expand Down Expand Up @@ -391,6 +400,7 @@
if (message.type === "SETTINGS") {
document.body.style.backgroundColor = message.data.backColor;
configureButtons(message.data.enableButtons);
configureFitWindow(message.data.fitWindow);
return;
}
window.DD_LOGS && DD_LOGS.logger.error("websocket.onmessage. Unknown data:", {
Expand Down Expand Up @@ -451,15 +461,15 @@
if (document.pictureInPictureElement) {
document.exitPictureInPicture();
} else {
var drawMJPEGStream = function drawMJPEGStream() {
var _drawMJPEGStream = function drawMJPEGStream() {
var naturalWidth = stream.naturalWidth,
naturalHeight = stream.naturalHeight;
if (canvas.width != naturalWidth || canvas.height != naturalHeight) {
canvas.width = naturalWidth;
canvas.height = naturalHeight;
}
context.drawImage(stream, 0, 0);
drawTimeoutId = setTimeout(drawMJPEGStream, 32);
drawTimeoutId = setTimeout(_drawMJPEGStream, 32);
};
var canvas = document.createElement("canvas");
canvas.style.display = "none";
Expand Down Expand Up @@ -493,7 +503,7 @@
});
pipStreamDiv.appendChild(videoElement);
var context = canvas.getContext("2d");
drawMJPEGStream();
_drawMJPEGStream();
}
}
if (document.readyState === "loading") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,16 +124,23 @@ internal class HttpServer(
val coroutineScope = CoroutineScope(Job() + Dispatchers.Default)

mjpegSettings.data
.map { it.htmlBackColor }
.map { Pair(it.htmlBackColor, it.htmlFitWindow) }
.distinctUntilChanged()
.onEach { htmlBackColor -> indexHtml.set(baseIndexHtml.replace("BACKGROUND_COLOR", htmlBackColor.toColorHexString())) }
.onEach { (backColor, fitWindow) ->
indexHtml.set(
baseIndexHtml
.replace("BACKGROUND_COLOR", backColor.toColorHexString())
.replace("FIT_WINDOW", if (fitWindow) """style='width: 100%; object-fit: contain;'""" else "")
)
}
.launchIn(coroutineScope)

mjpegSettings.data
.map { Pair(it.htmlEnableButtons && serverData.enablePin.not(), it.htmlBackColor.toColorHexString()) }
.map { Triple(it.htmlEnableButtons && serverData.enablePin.not(), it.htmlBackColor.toColorHexString(), it.htmlFitWindow) }
.distinctUntilChanged()
.onEach { (enableButtons, backColor) ->
serverData.notifyClients("SETTINGS", JSONObject().put("enableButtons", enableButtons).put("backColor", backColor))
.onEach { (enableButtons, backColor, fitWindow) ->
val data = JSONObject(mapOf("enableButtons" to enableButtons, "backColor" to backColor, "fitWindow" to fitWindow))
serverData.notifyClients("SETTINGS", data)
}
.launchIn(coroutineScope)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import android.graphics.Color
import android.graphics.LinearGradient
import android.graphics.Paint
import android.graphics.Rect
import android.graphics.RectF
import android.graphics.Shader
import android.media.projection.MediaProjection
import android.media.projection.MediaProjectionManager
Expand All @@ -25,6 +26,7 @@ import android.os.PowerManager
import android.widget.Toast
import androidx.annotation.AnyThread
import androidx.annotation.MainThread
import androidx.window.layout.WindowMetricsCalculator
import com.elvishew.xlog.XLog
import info.dvkr.screenstream.common.getLog
import info.dvkr.screenstream.mjpeg.MjpegKoinScope
Expand All @@ -49,6 +51,8 @@ import kotlinx.coroutines.withTimeoutOrNull
import org.koin.core.annotation.InjectedParam
import org.koin.core.annotation.Scope
import org.koin.core.annotation.Scoped
import kotlin.math.max
import kotlin.math.min
import kotlin.random.Random

@Scope(MjpegKoinScope::class)
Expand Down Expand Up @@ -548,20 +552,40 @@ internal class MjpegStreamingService(
private fun getStartBitmap(): Bitmap {
startBitmap?.let { return it }

val bitmap = Bitmap.createBitmap(600, 400, Bitmap.Config.ARGB_8888)
val screenSize = WindowMetricsCalculator.getOrCreate().computeMaximumWindowMetrics(service).bounds
val bitmap = Bitmap.createBitmap(max(screenSize.width(), 600), max(screenSize.height(), 800), Bitmap.Config.ARGB_8888)

var width = min(bitmap.width.toFloat(), 1536F)
val height = min(bitmap.height.toFloat(), width * 0.75F)
width = height / 0.75F

val left = max((bitmap.width - width) / 2F, 0F)
val top = max((bitmap.height - height) / 2F, 0F)
val right = bitmap.width - left
val bottom = (bitmap.height + height) / 2
val backRect = RectF(left, top, right, bottom)
val canvas = Canvas(bitmap).apply {
drawColor(mjpegSettings.data.value.htmlBackColor)
val shader = LinearGradient(0F, 0F, 0F, 400F, Color.parseColor("#144A74"), Color.parseColor("#001D34"), Shader.TileMode.CLAMP)
drawRoundRect(0F, 0F, 600F, 400F, 32F, 32F, Paint().apply { setShader(shader) })
val shader = LinearGradient(
backRect.left, backRect.top, backRect.left, backRect.bottom,
Color.parseColor("#144A74"), Color.parseColor("#001D34"), Shader.TileMode.CLAMP
)
drawRoundRect(backRect, 32F, 32F, Paint().apply { setShader(shader) })
}
val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply { textSize = 24f; color = Color.WHITE }

val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply { textSize = 26F / 600 * backRect.width(); color = Color.WHITE }
val logoSize = (min(backRect.width(), backRect.height()) * 0.7).toInt()
val logo = service.getFileFromAssets("logo.png")
.run { BitmapFactory.decodeByteArray(this, 0, size) }
.let { Bitmap.createScaledBitmap(it, 256, 256, true) }
canvas.drawBitmap(logo, 172f, 16f, paint)
.let { Bitmap.createScaledBitmap(it, logoSize, logoSize, true) }
canvas.drawBitmap(logo, backRect.left + (backRect.width() - logo.width) / 2, backRect.top, paint)

val message = service.getString(R.string.mjpeg_start_image_text)
val bounds = Rect().apply { paint.getTextBounds(message, 0, message.length, this) }
canvas.drawText(message, (bitmap.width - bounds.width()) / 2f, 324f, paint)
val textX = backRect.left + (backRect.width() - bounds.width()) / 2
val textY = backRect.top + logo.height + (backRect.height() - logo.height) / 2 - bounds.height() / 2
canvas.drawText(message, textX, textY, paint)

startBitmap = bitmap
return bitmap
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ public interface MjpegSettings {
public val HTML_ENABLE_BUTTONS: Preferences.Key<Boolean> = booleanPreferencesKey("HTML_ENABLE_BUTTONS")
public val HTML_SHOW_PRESS_START: Preferences.Key<Boolean> = booleanPreferencesKey("HTML_SHOW_PRESS_START")
public val HTML_BACK_COLOR: Preferences.Key<Int> = intPreferencesKey("HTML_BACK_COLOR")
public val HTML_FIT_WINDOW: Preferences.Key<Boolean> = booleanPreferencesKey("HTML_FIT_WINDOW")

public val VR_MODE: Preferences.Key<Int> = intPreferencesKey("VR_MODE")
public val IMAGE_CROP: Preferences.Key<Boolean> = booleanPreferencesKey("IMAGE_CROP")
Expand Down Expand Up @@ -52,9 +53,10 @@ public interface MjpegSettings {
public const val STOP_ON_CONFIGURATION_CHANGE: Boolean = false
public const val NOTIFY_SLOW_CONNECTIONS: Boolean = false

public const val HTML_ENABLE_BUTTONS: Boolean = true
public const val HTML_ENABLE_BUTTONS: Boolean = false
public const val HTML_SHOW_PRESS_START: Boolean = true
public const val HTML_BACK_COLOR: Int = -15723496// "FF101418".toLong(radix = 16).toInt()
public const val HTML_FIT_WINDOW: Boolean = true

public const val VR_MODE_DISABLE: Int = 0
public const val VR_MODE_LEFT: Int = 1
Expand Down Expand Up @@ -102,6 +104,7 @@ public interface MjpegSettings {
public val htmlEnableButtons: Boolean = Default.HTML_ENABLE_BUTTONS,
public val htmlShowPressStart: Boolean = Default.HTML_SHOW_PRESS_START,
public val htmlBackColor: Int = Default.HTML_BACK_COLOR,
public val htmlFitWindow: Boolean = Default.HTML_FIT_WINDOW,

public val vrMode: Int = Default.VR_MODE_DISABLE,
public val imageCrop: Boolean = Default.IMAGE_CROP,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ internal class MjpegSettingsImpl(
if (newSettings.htmlBackColor != MjpegSettings.Default.HTML_BACK_COLOR)
set(MjpegSettings.Key.HTML_BACK_COLOR, newSettings.htmlBackColor)

if (newSettings.htmlFitWindow != MjpegSettings.Default.HTML_FIT_WINDOW)
set(MjpegSettings.Key.HTML_FIT_WINDOW, newSettings.htmlFitWindow)


if (newSettings.vrMode != MjpegSettings.Default.VR_MODE_DISABLE)
set(MjpegSettings.Key.VR_MODE, newSettings.vrMode)
Expand Down Expand Up @@ -153,6 +156,7 @@ internal class MjpegSettingsImpl(
htmlEnableButtons = this[MjpegSettings.Key.HTML_ENABLE_BUTTONS] ?: MjpegSettings.Default.HTML_ENABLE_BUTTONS,
htmlShowPressStart = this[MjpegSettings.Key.HTML_SHOW_PRESS_START] ?: MjpegSettings.Default.HTML_SHOW_PRESS_START,
htmlBackColor = this[MjpegSettings.Key.HTML_BACK_COLOR] ?: MjpegSettings.Default.HTML_BACK_COLOR,
htmlFitWindow = this[MjpegSettings.Key.HTML_FIT_WINDOW] ?: MjpegSettings.Default.HTML_FIT_WINDOW,

vrMode = this[MjpegSettings.Key.VR_MODE] ?: MjpegSettings.Default.VR_MODE_DISABLE,
imageCrop = this[MjpegSettings.Key.IMAGE_CROP] ?: MjpegSettings.Default.IMAGE_CROP,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import info.dvkr.screenstream.common.ModuleSettings
import info.dvkr.screenstream.mjpeg.R
import info.dvkr.screenstream.mjpeg.ui.settings.general.HtmlBackColor
import info.dvkr.screenstream.mjpeg.ui.settings.general.HtmlEnableButtons
import info.dvkr.screenstream.mjpeg.ui.settings.general.HtmlFitWindow
import info.dvkr.screenstream.mjpeg.ui.settings.general.HtmlShowPressStart
import info.dvkr.screenstream.mjpeg.ui.settings.general.KeepAwake
import info.dvkr.screenstream.mjpeg.ui.settings.general.NotifySlowConnections
Expand All @@ -19,7 +20,16 @@ public object GeneralGroup : ModuleSettings.Group {
override val id: String = "GENERAL"
override val position: Int = 0
override val items: List<ModuleSettings.Item> =
listOf(KeepAwake, StopOnSleep, StopOnConfigurationChange, NotifySlowConnections, HtmlEnableButtons, HtmlShowPressStart, HtmlBackColor)
listOf(
KeepAwake,
StopOnSleep,
StopOnConfigurationChange,
NotifySlowConnections,
HtmlEnableButtons,
HtmlShowPressStart,
HtmlBackColor,
HtmlFitWindow
)
.filter { it.available }.sortedBy { it.position }

@Composable
Expand Down
Loading

0 comments on commit e250696

Please sign in to comment.