2 Commits
7 ... main

Author SHA1 Message Date
Hermes Agent d10759b26b fix: pad кадра до кратности 16 (H.264 макроблоки) — scale+pad=ceil(iw/16)*16, картинка без искажений
Build Media Mirror Worker / Build and publish (release) Successful in 1m20s
Эмуляторный декодер (c2.goldfish) отказывался декодировать 893x480 (не кратно 16). Теперь после scale любой ширины pad доводит кадр до кратного 16 чёрными полями, пропорции не меняются.
2026-08-19 11:50:16 +03:00
Hermes Agent 96a9a0536d fix: устойчивое скачивание из Jellyfin — OkHttp + retry вместо HttpURLConnection
Build Media Mirror Worker / Build and publish (release) Successful in 1m25s
Прод-инцидент (3 джоба failed подряд): HttpURLConnection рвал скачивание
7.2 ГБ на ~5-й минуте ('Can't download video from Jellyfin'), при этом curl
с той же машины качал стабильно (HTTP/1.1 и HTTP/2). Сеть и Jellyfin чисты.

- JellyfinInput: OkHttpClient (readTimeout=0, followRedirects), byteStream→Files.copy,
  retry до 3 попыток с паузой 5с на IOException, warn-лог каждой попытки
- InputService: лог с полным stacktrace (реальная причина обрыва теперь видна)
2026-08-19 06:00:18 +03:00
4 changed files with 45 additions and 17 deletions
+1
View File
@@ -50,6 +50,7 @@ dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.11.0")
implementation(platform("software.amazon.awssdk:bom:2.46.21"))
implementation("software.amazon.awssdk:s3")
implementation("com.squareup.okhttp3:okhttp:4.12.0")
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation(kotlin("test-junit5"))
@@ -82,7 +82,9 @@ class FfmpegService(
}
if (width != null || height != null) {
args += "-vf"
args += "scale=${width ?: "-1"}:${height ?: "-1"}"
// Высота прибита (480), ширина — по пропорциям; pad доводит кадр до кратности 16
// (H.264 макроблоки 16x16): чёрные поля по краям, картинка не искажается.
args += "scale=${width ?: "-1"}:${height ?: "-1"},pad=ceil(iw/16)*16:ceil(ih/16)*16:(ow-iw)/2:(oh-ih)/2"
}
if (!removeAudio) {
if (audioCodec != null) {
@@ -28,6 +28,7 @@ class InputService(
logger.info("Download cancelled: {}", source)
throw e
} catch (e: Throwable) {
logger.warn("Download failed: {}", e.message, e)
throw IllegalStateException("Can't download video from $source", e)
}
logger.info("Downloaded success in {} ms", (System.nanoTime() - startTime) / 1_000_000)
@@ -1,15 +1,18 @@
package pw.binom.mirror.worker.convert
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import okhttp3.Request
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Component
import java.net.HttpURLConnection
import java.net.URL
import java.io.IOException
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.StandardCopyOption
import java.util.concurrent.TimeUnit
@Component
class JellyfinInput(
@@ -17,24 +20,46 @@ class JellyfinInput(
) : InputService.InputImplementation {
private val logger: Logger = LoggerFactory.getLogger(JellyfinInput::class.java)
private val client: OkHttpClient by lazy {
OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(0, TimeUnit.SECONDS)
.followRedirects(true)
.followSslRedirects(true)
.build()
}
override fun isSupport(source: InputSource): Boolean = source is InputSource.Jellyfin
override suspend fun download(source: InputSource): Path = withContext(Dispatchers.IO) {
source as InputSource.Jellyfin
val url = URL(source.streamUrl)
val connection = url.openConnection() as HttpURLConnection
connection.requestMethod = "GET"
connection.instanceFollowRedirects = true
connection.connectTimeout = 30_000
connection.readTimeout = 120_000
try {
if (connection.responseCode != HttpURLConnection.HTTP_OK) {
throw IllegalStateException("Invalid response code ${connection.responseCode}")
var lastError: IOException? = null
for (attempt in 1..3) {
try {
return@withContext downloadOnce(source)
} catch (e: IOException) {
lastError = e
logger.warn("Download attempt {}/3 failed: {}", attempt, e.message)
if (attempt < 3) {
delay(5_000)
}
}
}
throw lastError!!
}
private fun downloadOnce(source: InputSource.Jellyfin): Path {
val request = Request.Builder().url(source.streamUrl).get().build()
val response = client.newCall(request).execute()
response.use {
if (!response.isSuccessful) {
throw IllegalStateException("Invalid response code ${response.code}")
}
val resultFile = localStorageService.genFile()
logger.info("Downloading file in to ${resultFile}...")
try {
connection.inputStream.use { input ->
logger.info("Downloading file in to {}...", resultFile)
val path = try {
val body = response.body ?: throw IllegalStateException("Empty response body")
body.byteStream().use { input ->
Files.copy(input, resultFile, StandardCopyOption.REPLACE_EXISTING)
}
logger.info("File success downloaded!")
@@ -43,8 +68,7 @@ class JellyfinInput(
runCatching { Files.deleteIfExists(resultFile) }
throw e
}
} finally {
connection.disconnect()
return path
}
}
}