Download fixes.

This commit is contained in:
Koen 2024-08-29 13:42:18 +00:00
commit bf685a607f
2 changed files with 118 additions and 12 deletions

View file

@ -58,6 +58,7 @@ import kotlinx.serialization.Transient
import java.io.File import java.io.File
import java.io.FileOutputStream import java.io.FileOutputStream
import java.io.IOException import java.io.IOException
import java.lang.Thread.sleep
import java.time.OffsetDateTime import java.time.OffsetDateTime
import java.util.UUID import java.util.UUID
import java.util.concurrent.Executors import java.util.concurrent.Executors
@ -156,6 +157,7 @@ class VideoDownload {
this.targetBitrate = targetBitrate; this.targetBitrate = targetBitrate;
this.hasVideoRequestExecutor = video is JSSource && video.hasRequestExecutor; this.hasVideoRequestExecutor = video is JSSource && video.hasRequestExecutor;
this.requiresLiveVideoSource = false; this.requiresLiveVideoSource = false;
this.requiresLiveAudioSource = false;
this.targetVideoName = videoSource?.name; this.targetVideoName = videoSource?.name;
} }
constructor(video: IPlatformVideoDetails, videoSource: IVideoSource?, audioSource: IAudioSource?, subtitleSource: SubtitleRawSource?) { constructor(video: IPlatformVideoDetails, videoSource: IVideoSource?, audioSource: IAudioSource?, subtitleSource: SubtitleRawSource?) {
@ -667,7 +669,7 @@ class VideoDownload {
if(Settings.instance.downloads.byteRangeDownload && head?.containsKey("accept-ranges") == true && head.containsKey("content-length")) if(Settings.instance.downloads.byteRangeDownload && head?.containsKey("accept-ranges") == true && head.containsKey("content-length"))
{ {
val concurrency = Settings.instance.downloads.getByteRangeThreadCount(); val concurrency = Settings.instance.downloads.getByteRangeThreadCount();
Logger.i(TAG, "Download $name ByteRange Parallel (${concurrency})"); Logger.i(TAG, "Download $name ByteRange Parallel (${concurrency}): " + videoUrl);
sourceLength = head["content-length"]!!.toLong(); sourceLength = head["content-length"]!!.toLong();
onProgress(sourceLength, 0, 0); onProgress(sourceLength, 0, 0);
downloadSource_Ranges(name, client, fileStream, videoUrl, sourceLength, 1024*512, concurrency, onProgress); downloadSource_Ranges(name, client, fileStream, videoUrl, sourceLength, 1024*512, concurrency, onProgress);
@ -751,6 +753,76 @@ class VideoDownload {
onProgress(sourceLength, totalRead, 0); onProgress(sourceLength, totalRead, 0);
return sourceLength; return sourceLength;
} }
/*private fun downloadSource_Sequential(client: ManagedHttpClient, fileStream: FileOutputStream, url: String, onProgress: (Long, Long, Long) -> Unit): Long {
val progressRate: Int = 4096 * 25
var lastProgressCount: Int = 0
val speedRate: Int = 4096 * 25
var readSinceLastSpeedTest: Long = 0
var timeSinceLastSpeedTest: Long = System.currentTimeMillis()
var lastSpeed: Long = 0
var totalRead: Long = 0
var sourceLength: Long
val buffer = ByteArray(4096)
var isPartialDownload = false
var result: ManagedHttpClient.Response? = null
do {
result = client.get(url, if (isPartialDownload) hashMapOf("Range" to "bytes=$totalRead-") else hashMapOf())
if (isPartialDownload) {
if (result.code != 206)
throw IllegalStateException("Failed to download source, byte range fallback failed. Web[${result.code}] Error")
} else {
if (!result.isOk)
throw IllegalStateException("Failed to download source. Web[${result.code}] Error")
}
if (result.body == null)
throw IllegalStateException("Failed to download source. Web[${result.code}] No response")
isPartialDownload = true
sourceLength = result.body!!.contentLength()
val sourceStream = result.body!!.byteStream()
try {
while (true) {
val read = sourceStream.read(buffer)
if (read <= 0) {
break
}
fileStream.write(buffer, 0, read)
totalRead += read
readSinceLastSpeedTest += read
if (totalRead / progressRate > lastProgressCount) {
onProgress(sourceLength, totalRead, lastSpeed)
lastProgressCount++
}
if (readSinceLastSpeedTest > speedRate) {
val lastSpeedTime = timeSinceLastSpeedTest
timeSinceLastSpeedTest = System.currentTimeMillis()
val timeSince = timeSinceLastSpeedTest - lastSpeedTime
if (timeSince > 0)
lastSpeed = (readSinceLastSpeedTest / (timeSince / 1000.0)).toLong()
readSinceLastSpeedTest = 0
}
if (isCancelled)
throw CancellationException("Cancelled")
}
} catch (e: Throwable) {
Logger.w(TAG, "Sequential download was interrupted, trying to fallback to byte ranges", e)
} finally {
sourceStream.close()
result.body?.close()
}
} while (totalRead < sourceLength)
onProgress(sourceLength, totalRead, 0)
return sourceLength
}*/
private fun downloadSource_Ranges(name: String, client: ManagedHttpClient, fileStream: FileOutputStream, url: String, sourceLength: Long, rangeSize: Int, concurrency: Int = 1, onProgress: (Long, Long, Long) -> Unit) { private fun downloadSource_Ranges(name: String, client: ManagedHttpClient, fileStream: FileOutputStream, url: String, sourceLength: Long, rangeSize: Int, concurrency: Int = 1, onProgress: (Long, Long, Long) -> Unit) {
val progressRate: Int = 4096 * 5; val progressRate: Int = 4096 * 5;
var lastProgressCount: Int = 0; var lastProgressCount: Int = 0;
@ -823,10 +895,18 @@ class VideoDownload {
return tasks.map { it.get() }; return tasks.map { it.get() };
} }
private fun requestByteRange(client: ManagedHttpClient, url: String, rangeStart: Long, rangeEnd: Long): Triple<ByteArray, Long, Long> { private fun requestByteRange(client: ManagedHttpClient, url: String, rangeStart: Long, rangeEnd: Long): Triple<ByteArray, Long, Long> {
var retryCount = 0
var lastException: Throwable? = null
while (retryCount <= 3) {
try {
val toRead = rangeEnd - rangeStart; val toRead = rangeEnd - rangeStart;
val req = client.get(url, mutableMapOf(Pair("Range", "bytes=${rangeStart}-${rangeEnd}"))); val req = client.get(url, mutableMapOf(Pair("Range", "bytes=${rangeStart}-${rangeEnd}")));
if(!req.isOk) if (!req.isOk) {
val bodyString = req.body?.string()
req.body?.close()
throw IllegalStateException("Range request failed Code [${req.code}] due to: ${req.message}"); throw IllegalStateException("Range request failed Code [${req.code}] due to: ${req.message}");
}
if (req.body == null) if (req.body == null)
throw IllegalStateException("Range request failed, No body"); throw IllegalStateException("Range request failed, No body");
val read = req.body.contentLength(); val read = req.body.contentLength();
@ -835,6 +915,22 @@ class VideoDownload {
throw IllegalStateException("Byte-Range request attempted to provide less (${read} < ${toRead})"); throw IllegalStateException("Byte-Range request attempted to provide less (${read} < ${toRead})");
return Triple(req.body.bytes(), rangeStart, rangeEnd); return Triple(req.body.bytes(), rangeStart, rangeEnd);
} catch (e: Throwable) {
Logger.w(TAG, "Failed to download range (url=${url} bytes=${rangeStart}-${rangeEnd})", e)
retryCount++
lastException = e
sleep(when (retryCount) {
1 -> 1000 + ((Math.random() * 300.0).toLong() - 150)
2 -> 2000 + ((Math.random() * 300.0).toLong() - 150)
3 -> 4000 + ((Math.random() * 300.0).toLong() - 150)
else -> 1000 + ((Math.random() * 300.0).toLong() - 150)
})
}
}
throw lastException!!
} }
fun validate() { fun validate() {

View file

@ -28,7 +28,12 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import okhttp3.OkHttpClient
import java.net.InetAddress
import java.net.InetSocketAddress
import java.net.Proxy
import java.net.SocketException import java.net.SocketException
import java.time.Duration
import java.time.OffsetDateTime import java.time.OffsetDateTime
class DownloadService : Service() { class DownloadService : Service() {
@ -44,7 +49,12 @@ class DownloadService : Service() {
private var _notificationManager: NotificationManager? = null; private var _notificationManager: NotificationManager? = null;
private var _notificationChannel: NotificationChannel? = null; private var _notificationChannel: NotificationChannel? = null;
private val _client = ManagedHttpClient(); private val _client = ManagedHttpClient(OkHttpClient.Builder()
//.proxy(Proxy(Proxy.Type.HTTP, InetSocketAddress(InetAddress.getByName("192.168.1.175"), 8081)))
.readTimeout(Duration.ofSeconds(30))
.writeTimeout(Duration.ofSeconds(30))
.connectTimeout(Duration.ofSeconds(30))
.callTimeout(Duration.ofMinutes(30)))
private var _started = false; private var _started = false;