From fbe8e7aa56047ed487513c5c7c4dfb4ab0cd085e Mon Sep 17 00:00:00 2001 From: Aryan Date: Sat, 21 Mar 2026 11:49:27 +0530 Subject: [PATCH] General fixes (#102) * Improved PDF embedded annotation hit detection logic * Updated EPUB and PDF reader logic to improve position tracking and TTS stability. Key changes: - Refined CFI and locator handling in `EpubReaderScreen` with improved initialization. - Updated `BookPaginator` to more accurately calculate text offsets in chunks and improve CFI matching logic. - Modified `TtsPlaybackManager` to insert media items at the correct index, ensuring proper playback order. - Simplified `EpubReaderTts` page scrolling by removing conditional checks for backward jumps. - Disabled OCR fallback in `PdfViewerScreen` during text extraction for TTS. * Updated extractTextWithCfiFromTop to use getBoundingClientRect for identifying the starting block --- app/src/main/assets/epub_reader.js | 54 ++++------- .../aryan/reader/epubreader/ChapterWebView.kt | 4 +- .../reader/epubreader/EpubReaderScreen.kt | 19 ++-- .../aryan/reader/epubreader/EpubReaderTts.kt | 7 +- .../reader/paginatedreader/BookPaginator.kt | 18 +++- .../aryan/reader/paginatedreader/Locator.kt | 31 +------ .../com/aryan/reader/pdf/PdfPageComposable.kt | 45 ++++++--- .../com/aryan/reader/pdf/PdfViewerScreen.kt | 91 ++----------------- .../aryan/reader/tts/TtsPlaybackManager.kt | 10 +- 9 files changed, 95 insertions(+), 184 deletions(-) diff --git a/app/src/main/assets/epub_reader.js b/app/src/main/assets/epub_reader.js index 4121089..1d04359 100644 --- a/app/src/main/assets/epub_reader.js +++ b/app/src/main/assets/epub_reader.js @@ -1097,62 +1097,44 @@ window.extractTextWithCfiFromTop = function () { try { - // 1. Find the element at the top of the viewport. - const viewportX = window.innerWidth / 2; - const viewportY = window.VIEWPORT_PADDING_TOP + 20; // A bit down from the very top edge - let topElement = document.elementFromPoint(viewportX, viewportY); - - if (!topElement) { - // Fallback if nothing is found (e.g., blank space between elements) - topElement = document.body.querySelector("p, h1, h2, h3, h4, img, svg, table, li"); - if (!topElement) return "[]"; // Chapter seems empty - } - - // 2. Find its containing block-level element that we use for TTS. const ttsNodeSelector = "p, h1, h2, h3, h4, h5, h6, li, blockquote"; - let startBlock = topElement.closest(ttsNodeSelector); - - if (!startBlock) { - // If the element itself isn't in a TTS block, fall back to the start of the chapter. - console.log("Could not find a starting TTS block. Falling back to full chapter."); - return window.extractTextWithCfi(); - } - - // 3. Get all potential TTS nodes. const allContentNodes = Array.from(document.body.querySelectorAll(ttsNodeSelector)); - // 4. Find the index of our starting block. - const startIndex = allContentNodes.findIndex((node) => node === startBlock); + let startBlock = null; + let startIndex = -1; - if (startIndex === -1) { - // Should be rare if startBlock was found, but as a safeguard: - console.log("Could not find the start block in the node list. Falling back to full chapter."); + for (let i = 0; i < allContentNodes.size || i < allContentNodes.length; i++) { + const node = allContentNodes[i]; + const rect = node.getBoundingClientRect(); + + if (rect.bottom > (window.VIEWPORT_PADDING_TOP + 10)) { + startBlock = node; + startIndex = i; + break; + } + } + + if (!startBlock) { return window.extractTextWithCfi(); } - // 5. Slice the array and process it. const nodesToProcess = allContentNodes.slice(startIndex); const results = []; nodesToProcess.forEach((node) => { const text = node.innerText ? node.innerText.trim() : ""; - if (text.length > 0 && node.offsetParent !== null) { try { - const cfi = getCfiPathForElement(node, 0); - - if (cfi) { - results.push({ cfi: cfi, text: text }); + const cfiObj = getCfiPathForElement(node, 0); + if (cfiObj && cfiObj.cfi) { + results.push({ cfi: cfiObj, text: text }); } - } catch (e) { - // ignore CFI generation errors for a single node - } + } catch (e) {} } }); return JSON.stringify(results); } catch (e) { - // On any error, fall back to extracting everything to not break TTS completely. return window.extractTextWithCfi(); } }; diff --git a/app/src/main/java/com/aryan/reader/epubreader/ChapterWebView.kt b/app/src/main/java/com/aryan/reader/epubreader/ChapterWebView.kt index 7267acf..135afb4 100644 --- a/app/src/main/java/com/aryan/reader/epubreader/ChapterWebView.kt +++ b/app/src/main/java/com/aryan/reader/epubreader/ChapterWebView.kt @@ -720,9 +720,7 @@ fun ChapterWebView( if (!initialCfi.isNullOrBlank()) { val cfiJsCommand = "javascript:window.scrollToCfi('$initialCfi');" - Timber.d( - "WebView onPageFinished: Executing initial scroll to CFI: $initialCfi" - ) + Timber.tag("POS_DIAG").d("WebView onPageFinished: Triggering scroll to initialCfi: $initialCfi") view?.evaluateJavascript(cfiJsCommand) { onChapterInitiallyScrolled() scrollActionTaken = true diff --git a/app/src/main/java/com/aryan/reader/epubreader/EpubReaderScreen.kt b/app/src/main/java/com/aryan/reader/epubreader/EpubReaderScreen.kt index d7775dc..674d09b 100644 --- a/app/src/main/java/com/aryan/reader/epubreader/EpubReaderScreen.kt +++ b/app/src/main/java/com/aryan/reader/epubreader/EpubReaderScreen.kt @@ -884,11 +884,15 @@ fun EpubReaderHost( var chapterHead by remember(currentChapterIndex) { mutableStateOf("") } var isChapterParsing by remember(currentChapterIndex) { mutableStateOf(true) } - var cfiToLoad by remember { mutableStateOf(null) } + var cfiToLoad by remember { mutableStateOf(initialCfi) } var fragmentToLoad by remember { mutableStateOf(null) } var isInitialCfiLoad by remember(initialLocator) { mutableStateOf(initialLocator != null) } var bookmarkPageMap by remember { mutableStateOf>(emptyMap()) } + LaunchedEffect(Unit) { + Timber.tag("POS_DIAG").d("Reader Opening: initialLocator=$initialLocator, initialCfi=$initialCfi") + } + var initialScrollTargetForChapter by rememberSaveable(epubBook.title) { mutableStateOf(if (initialLocator != null) null else ChapterScrollPosition.START) } @@ -1175,8 +1179,8 @@ fun EpubReaderHost( var foundIdx = -1 for (i in chunks.indices) { val c = chunks[i] - val cPath = c.sourceCfi.substringBefore(":") - val bPath = baseCfi.substringBefore(":") + val cPath = com.aryan.reader.paginatedreader.CfiUtils.getPath(c.sourceCfi) + val bPath = com.aryan.reader.paginatedreader.CfiUtils.getPath(baseCfi) if (cPath == bPath && startOffset >= c.startOffsetInSource && startOffset < c.startOffsetInSource + c.text.length) { foundIdx = i break @@ -2550,10 +2554,9 @@ fun EpubReaderHost( showDictionaryUpsellDialog = true }, onCfiGenerated = { cfi -> - Timber.tag("PosSaveDiag").d("EpubReaderScreen: onCfiGenerated callback triggered with CFI: '$cfi'") + Timber.tag("POS_DIAG").d("JS generated CFI: '$cfi'") if (cfi.isBlank() || !cfi.startsWith('/')) { - Timber.tag("PosSaveDiag").w("EpubReaderScreen: onCfiGenerated received an invalid CFI, aborting save: '$cfi'") if (isSavingAndExiting) { isSavingAndExiting = false onNavigateBack() @@ -2562,7 +2565,6 @@ fun EpubReaderHost( } scope.launch { - Timber.tag("PosSaveDiag").d("EpubReaderScreen: Requesting locator conversion for chapter $latestChapterIndex") val locator = locatorConverter.getLocatorFromCfi( epubBook, @@ -2570,8 +2572,6 @@ fun EpubReaderHost( cfi ) - Timber.tag("PosSaveDiag").d("EpubReaderScreen: Locator conversion returned: $locator") - if (locator != null) { lastKnownLocator = locator @@ -2639,8 +2639,7 @@ fun EpubReaderHost( } else { 0f } - Timber.d("CFI received for saving: $cfi. Progress: $progress%" - ) + Timber.tag("POS_DIAG").i("Saving Position: Chapter=$latestChapterIndex, CFI=$cfi, Progress=$progress%") onSavePosition(locator, cfi, progress) } else { Timber.w("Failed to convert CFI to Locator: $cfi." diff --git a/app/src/main/java/com/aryan/reader/epubreader/EpubReaderTts.kt b/app/src/main/java/com/aryan/reader/epubreader/EpubReaderTts.kt index b9e8a00..16f6199 100644 --- a/app/src/main/java/com/aryan/reader/epubreader/EpubReaderTts.kt +++ b/app/src/main/java/com/aryan/reader/epubreader/EpubReaderTts.kt @@ -215,11 +215,8 @@ fun TtsHighlightHandler( val targetPage = pag.findPageForCfiAndOffset(chapterIdx, cfi, offset) if (targetPage != null && targetPage != pagerState.currentPage) { - // Prevent backward jumps during reading (unless significant) to avoid jitter - if (targetPage >= pagerState.currentPage) { - scope.launch { - pagerState.animateScrollToPage(targetPage) - } + scope.launch { + pagerState.animateScrollToPage(targetPage) } } } diff --git a/app/src/main/java/com/aryan/reader/paginatedreader/BookPaginator.kt b/app/src/main/java/com/aryan/reader/paginatedreader/BookPaginator.kt index 0bc0bfe..13a8467 100644 --- a/app/src/main/java/com/aryan/reader/paginatedreader/BookPaginator.kt +++ b/app/src/main/java/com/aryan/reader/paginatedreader/BookPaginator.kt @@ -352,15 +352,23 @@ class BookPaginator( if (blockText.isNotBlank()) { val textChunksInBlock = splitTextIntoChunks(blockText) - var currentOffsetInBlock = 0 + var currentSearchIndex = 0 textChunksInBlock.forEach { chunkText -> + val firstWord = chunkText.trim().substringBefore(' ') + val relativeOffset = if (firstWord.isNotEmpty()) { + val idx = blockText.indexOf(firstWord, currentSearchIndex) + if (idx != -1) idx else currentSearchIndex + } else { + currentSearchIndex + } + val chunk = TtsChunk( text = chunkText, sourceCfi = block.cfi!!, - startOffsetInSource = block.startCharOffsetInSource + currentOffsetInBlock + startOffsetInSource = block.startCharOffsetInSource + relativeOffset ) allTtsChunks.add(chunk) - currentOffsetInBlock += chunkText.length + currentSearchIndex = relativeOffset + chunkText.length } } else { Timber.d("PAGINATOR: Skipping blank text block. CFI: ${block.cfi}, startOffset: ${block.startCharOffsetInSource}") @@ -1036,8 +1044,10 @@ class BookPaginator( return null } + val targetPath = CfiUtils.getPath(cfi) val foundRange = index.find { range -> - val cfiMatches = cfi.startsWith(range.cfi) + val rangePath = CfiUtils.getPath(range.cfi) + val cfiMatches = targetPath == rangePath || targetPath.startsWith(rangePath) || rangePath.startsWith(targetPath) val offsetMatches = charOffset >= range.startOffset && charOffset < range.endOffset cfiMatches && offsetMatches } diff --git a/app/src/main/java/com/aryan/reader/paginatedreader/Locator.kt b/app/src/main/java/com/aryan/reader/paginatedreader/Locator.kt index f02aeff..755b602 100644 --- a/app/src/main/java/com/aryan/reader/paginatedreader/Locator.kt +++ b/app/src/main/java/com/aryan/reader/paginatedreader/Locator.kt @@ -51,39 +51,25 @@ class LocatorConverter( private val context: Context ) { private suspend fun processAndCacheChapter(book: EpubBook, chapterIndex: Int): List? = withContext(Dispatchers.IO) { - Timber.tag("PosSaveDiag").d("processAndCacheChapter: STARTED for book='${book.title}', chapterIndex=$chapterIndex") + Timber.tag("POS_DIAG").d("processAndCacheChapter: Processing for bookId='${book.title}' index=$chapterIndex") try { - val chapter = book.chapters.getOrNull(chapterIndex) - if (chapter == null) { - Timber.tag("PosSaveDiag").e("processAndCacheChapter: FAILED. Chapter is null for index $chapterIndex") - return@withContext null - } + val chapter = book.chapters.getOrNull(chapterIndex) ?: return@withContext null - Timber.tag("PosSaveDiag").d("processAndCacheChapter: Checking HTML content. RAM content length: ${chapter.htmlContent.length}") - - val htmlToParse = if (chapter.htmlContent.isNotBlank()) { - Timber.tag("PosSaveDiag").d("processAndCacheChapter: Using HTML from RAM.") - chapter.htmlContent - } else { - Timber.tag("PosSaveDiag").d("processAndCacheChapter: RAM HTML is blank. Falling back to disk. Path: ${book.extractionBasePath} / ${chapter.htmlFilePath}") + val htmlToParse = chapter.htmlContent.ifBlank { try { val file = File(book.extractionBasePath, chapter.htmlFilePath) if (file.exists()) { val content = file.readText() - Timber.tag("PosSaveDiag").d("processAndCacheChapter: Read file from disk SUCCESS. Content length: ${content.length}") content } else { - Timber.tag("PosSaveDiag").e("processAndCacheChapter: File DOES NOT EXIST at ${file.absolutePath}") "" } - } catch (e: Exception) { - Timber.tag("PosSaveDiag").e(e, "processAndCacheChapter: Exception reading chapter file from disk") + } catch (_: Exception) { "" } } if (htmlToParse.isBlank()) { - Timber.tag("PosSaveDiag").w("processAndCacheChapter: Final HTML to parse is blank. Aborting semantic block generation.") return@withContext null } @@ -129,7 +115,6 @@ class LocatorConverter( otherComplex = mergedOtherComplex ) - Timber.tag("PosSaveDiag").d("processAndCacheChapter: Calling htmlToSemanticBlocks...") val semanticBlocks = htmlToSemanticBlocks( html = htmlToParse, cssRules = parsingCssRules, @@ -140,10 +125,8 @@ class LocatorConverter( fontFamilyMap = emptyMap(), constraints = constraints ) - Timber.tag("PosSaveDiag").d("processAndCacheChapter: htmlToSemanticBlocks returned ${semanticBlocks.size} blocks.") val protoBytes = proto.encodeToByteArray(semanticBlocks) - Timber.tag("PosSaveDiag").d("processAndCacheChapter: Encoded blocks to protoBytes (size: ${protoBytes.size} bytes).") val newCacheEntry = ProcessedChapter( bookId = book.title, @@ -152,10 +135,8 @@ class LocatorConverter( estimatedPageCount = 0 ) bookCacheDao.insertProcessedChapters(listOf(newCacheEntry)) - Timber.tag("PosSaveDiag").i("processAndCacheChapter: On-demand processing and DB caching SUCCESS for chapter $chapterIndex.") semanticBlocks - } catch (e: Exception) { - Timber.tag("PosSaveDiag").e(e, "processAndCacheChapter: FAILED for chapter $chapterIndex") + } catch (_: Exception) { null } } @@ -172,12 +153,10 @@ class LocatorConverter( } if (allBlocks.isNullOrEmpty()) { - Timber.tag("PosSaveDiag").w("getLocatorFromCfi: Cache missing or empty for chapter $chapterIndex. Triggering on-demand processing.") allBlocks = processAndCacheChapter(book, chapterIndex) } if (allBlocks.isNullOrEmpty()) { - Timber.tag("PosSaveDiag").e("getLocatorFromCfi: FAILED. Could not get or process semantic blocks.") return@withContext null } diff --git a/app/src/main/java/com/aryan/reader/pdf/PdfPageComposable.kt b/app/src/main/java/com/aryan/reader/pdf/PdfPageComposable.kt index 34c6017..8c47d16 100644 --- a/app/src/main/java/com/aryan/reader/pdf/PdfPageComposable.kt +++ b/app/src/main/java/com/aryan/reader/pdf/PdfPageComposable.kt @@ -138,6 +138,7 @@ import kotlin.math.PI import kotlin.math.abs import kotlin.math.atan2 import kotlin.math.cos +import kotlin.math.max import kotlin.math.min import kotlin.math.pow import kotlin.math.roundToInt @@ -919,20 +920,27 @@ internal fun PdfPageComposable( if (count > 0) { val allAnnots = (0 until count).mapNotNull { i -> val subtype = NativePdfiumBridge.getAnnotSubtype(pagePtr, i) - if (subtype == annotLink) return@mapNotNull null // skip links here + if (subtype == annotLink) return@mapNotNull null + + var contents = NativePdfiumBridge.getAnnotString(pagePtr, i, "Contents") + if (contents.isNullOrBlank()) { + contents = NativePdfiumBridge.getAnnotString(pagePtr, i, "RC") + } - val contents = NativePdfiumBridge.getAnnotString(pagePtr, i, "Contents") val name = NativePdfiumBridge.getAnnotString(pagePtr, i, "NM") val irt = NativePdfiumBridge.getAnnotString(pagePtr, i, "IRT") val author = NativePdfiumBridge.getAnnotString(pagePtr, i, "T") val pdfRectArray = NativePdfiumBridge.getAnnotRect(pagePtr, i) val pdfRectF = if (pdfRectArray != null) { - android.graphics.RectF(pdfRectArray[0], pdfRectArray[3], pdfRectArray[2], pdfRectArray[1]) + android.graphics.RectF( + min(pdfRectArray[0], pdfRectArray[2]), + max(pdfRectArray[1], pdfRectArray[3]), + max(pdfRectArray[0], pdfRectArray[2]), + min(pdfRectArray[1], pdfRectArray[3]) + ) } else android.graphics.RectF() - Timber.tag("PdfCommentDebug").v("Extracted Annot[$i]: Name=$name, IRT=$irt, Subtype=$subtype, Text=${contents?.take(10)}...") - EmbeddedAnnotation(i, subtype, pdfRectF, contents, author, name, irt) } @@ -2405,6 +2413,8 @@ internal fun PdfPageComposable( detectTapGestures(onTap = { tapOffset -> val tapInContentCoords = screenToContentCoordinates(tapOffset) + val tapXInBitmap = tapInContentCoords.x + val tapYInBitmap = tapInContentCoords.y coroutineScope.launch { val wasHandled = withContext(Dispatchers.IO) { @@ -2439,9 +2449,6 @@ internal fun PdfPageComposable( } } - val tapXInBitmap = tapInContentCoords.x - val tapYInBitmap = tapInContentCoords.y - val annotHitTolerance = with(density) { 24.dp.toPx() } / inputScale val hitTolerance = with(density) { 16.dp.toPx() } / inputScale @@ -2466,14 +2473,24 @@ internal fun PdfPageComposable( } else false } - val standardHit = standardAnnotScreenRects.findLast { (_, screenRect) -> + val standardHit = standardAnnotScreenRects.findLast { (annot, screenRect) -> + if (annot.subtype == 2) return@findLast false + + val left = min(screenRect.left, screenRect.right) + val right = max(screenRect.left, screenRect.right) + val top = min(screenRect.top, screenRect.bottom) + val bottom = max(screenRect.top, screenRect.bottom) + val inflatedHitBox = Rect( - (screenRect.left - annotHitTolerance).toInt(), - (screenRect.top - annotHitTolerance).toInt(), - (screenRect.right + annotHitTolerance).toInt(), - (screenRect.bottom + annotHitTolerance).toInt() + (left - annotHitTolerance).toInt(), + (top - annotHitTolerance).toInt(), + (right + annotHitTolerance).toInt(), + (bottom + annotHitTolerance).toInt() ) - inflatedHitBox.contains(tapInContentCoords.x.toInt(), tapInContentCoords.y.toInt()) + + val isHit = inflatedHitBox.contains(tapInContentCoords.x.toInt(), tapInContentCoords.y.toInt()) + + isHit } if (standardHit != null) { diff --git a/app/src/main/java/com/aryan/reader/pdf/PdfViewerScreen.kt b/app/src/main/java/com/aryan/reader/pdf/PdfViewerScreen.kt index 4f1da78..c4da76b 100644 --- a/app/src/main/java/com/aryan/reader/pdf/PdfViewerScreen.kt +++ b/app/src/main/java/com/aryan/reader/pdf/PdfViewerScreen.kt @@ -2792,93 +2792,14 @@ fun PdfViewerScreen( withContext(Dispatchers.IO) { tempTextPage?.close() } } + ocrUsedForCurrentPageTts = false + withContext(Dispatchers.IO) { + tempPage?.close() + } if (rawPageText.isNullOrBlank()) { - Timber.i( - "TTS: Pdfium text is blank or extraction failed. Attempting OCR for page $pageToRead." - ) - ocrAttempted = true - ocrUsedForCurrentPageTts = true - var ocrBitmap: Bitmap? = null - try { - if (tempPage == null) { - withContext(Dispatchers.IO) { - tempPage?.close() - Timber.d("TTS/OCR: Re-opening page $pageToRead for bitmap rendering.") - tempPage = pdfDocument!!.openPage(pageToRead) - } - } - - val pageForOcr = tempPage ?: throw IllegalStateException( - "TTS/OCR: PDF page couldn't be opened for OCR." - ) - - val ocrBitmapWidth = 1080 - val ocrBitmapHeight: Int - - withContext(Dispatchers.IO) { - val originalWidthPoints = pageForOcr.getPageWidthPoint() - val originalHeightPoints = pageForOcr.getPageHeightPoint() - - if (originalWidthPoints <= 0 || originalHeightPoints <= 0) { - throw IllegalStateException( - "TTS/OCR: Invalid page dimensions (points) from Pdfium for page $pageToRead." - ) - } - val aspectRatio = - originalWidthPoints.toFloat() / originalHeightPoints.toFloat() - ocrBitmapHeight = (ocrBitmapWidth / aspectRatio).toInt() - - if (ocrBitmapHeight <= 0) { - throw IllegalStateException( - "TTS/OCR: Calculated invalid bitmap dimensions for OCR ($ocrBitmapWidth x $ocrBitmapHeight) for page $pageToRead." - ) - } - - Timber.d( - "TTS/OCR: Rendering page $pageToRead to bitmap of size ${ocrBitmapWidth}x$ocrBitmapHeight." - ) - ocrBitmap = createBitmap(ocrBitmapWidth, ocrBitmapHeight) - pageForOcr.renderPageBitmap( - bitmap = ocrBitmap, - startX = 0, - startY = 0, - drawSizeX = ocrBitmapWidth, - drawSizeY = ocrBitmapHeight, - renderAnnot = false - ) - } - Timber.d("TTS/OCR: Bitmap rendered for page $pageToRead. Attempting OCR.") - - rawPageText = OcrHelper.extractTextFromBitmap(ocrBitmap!!) { - isOcrModelDownloading = true - }?.text - - if (!rawPageText.isNullOrBlank()) { - Timber.i( - "TTS: Text extracted via OCR for page $pageToRead (length: ${rawPageText?.length})." - ) - } else { - Timber.w( - "TTS: OCR process completed for page $pageToRead but returned no text or blank text." - ) - } - } catch (e: Exception) { - Timber.e(e, "TTS: Error during OCR process for page $pageToRead") - } finally { - ocrBitmap?.recycle() - withContext(Dispatchers.IO) { - tempPage?.close() - Timber.d("TTS/OCR: Closed page $pageToRead after OCR attempt.") - } - } + Timber.i("TTS: Pdfium text is blank or extraction failed. OCR fallback is temporarily disabled.") } else { - ocrUsedForCurrentPageTts = false - withContext(Dispatchers.IO) { - tempPage?.close() - Timber.d( - "TTS: Closed page $pageToRead after successful Pdfium text extraction." - ) - } + Timber.d("TTS: Closed page $pageToRead after successful Pdfium text extraction.") } if (rawPageText != null && rawPageText!!.isNotBlank()) { diff --git a/app/src/main/java/com/aryan/reader/tts/TtsPlaybackManager.kt b/app/src/main/java/com/aryan/reader/tts/TtsPlaybackManager.kt index 6cc3a44..38ae8ae 100644 --- a/app/src/main/java/com/aryan/reader/tts/TtsPlaybackManager.kt +++ b/app/src/main/java/com/aryan/reader/tts/TtsPlaybackManager.kt @@ -473,7 +473,15 @@ class TtsPlaybackManager( } if (!exists) { - player.addMediaItem(nextMediaItem) + var insertPosition = player.mediaItemCount + for (k in 0 until player.mediaItemCount) { + val id = player.getMediaItemAt(k).mediaId.toIntOrNull() ?: -1 + if (id > targetIndex) { + insertPosition = k + break + } + } + player.addMediaItem(insertPosition, nextMediaItem) } if (player.playbackState == Player.STATE_ENDED && player.playWhenReady && targetIndex == player.currentMediaItemIndex + 1) {