commit 05ebeba4d308bd6ec05174c2efeed6aa1f40151a
parent f8898118ee978948c038cb44eef0e734d7c36409
Author: rhunk <101876869+rhunk@users.noreply.github.com>
Date:   Fri, 19 May 2023 12:37:19 +0200

fix: media downloader

Diffstat:
Mapp/src/main/kotlin/me/rhunk/snapenhance/features/impl/downloader/MediaDownloader.kt | 107+++++++++----------------------------------------------------------------------
Mapp/src/main/kotlin/me/rhunk/snapenhance/features/impl/extras/Notifications.kt | 17++++++++---------
Aapp/src/main/kotlin/me/rhunk/snapenhance/util/MediaDownloaderHelper.kt | 92+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 111 insertions(+), 105 deletions(-)

diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/downloader/MediaDownloader.kt b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/downloader/MediaDownloader.kt @@ -24,8 +24,9 @@ import me.rhunk.snapenhance.hook.HookAdapter import me.rhunk.snapenhance.hook.HookStage import me.rhunk.snapenhance.hook.Hooker import me.rhunk.snapenhance.util.EncryptionUtils +import me.rhunk.snapenhance.util.MediaDownloaderHelper +import me.rhunk.snapenhance.util.MediaType import me.rhunk.snapenhance.util.PreviewUtils -import me.rhunk.snapenhance.util.download.CdnDownloader import me.rhunk.snapenhance.util.getObjectField import me.rhunk.snapenhance.util.protobuf.ProtoReader import java.io.* @@ -34,14 +35,10 @@ import java.net.URL import java.nio.file.Paths import java.util.* import java.util.concurrent.atomic.AtomicReference -import java.util.zip.ZipInputStream import javax.crypto.Cipher import javax.crypto.CipherInputStream import kotlin.io.path.inputStream -enum class MediaType { - ORIGINAL, OVERLAY -} class MediaDownloader : Feature("MediaDownloader", loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) { private var lastSeenMediaInfoMap: MutableMap<MediaType, MediaInfo>? = null @@ -98,50 +95,6 @@ class MediaDownloader : Feature("MediaDownloader", loadParams = FeatureLoadParam } return true } - - - private fun mergeOverlay(original: ByteArray, overlay: ByteArray, isPreviewMode: Boolean): ByteArray? { - context.longToast("Merging current media with overlay. This may take a while.") - val originalFileType = FileType.fromByteArray(original) - val overlayFileType = FileType.fromByteArray(overlay) - //merge files - val mergedFile = File.createTempFile("merged", "." + originalFileType.fileExtension) - val tempVideoFile = File.createTempFile("original", "." + originalFileType.fileExtension).also { - with(FileOutputStream(it)) { - write(original) - close() - } - } - val tempOverlayFile = File.createTempFile("overlay", "." + overlayFileType.fileExtension).also { - with(FileOutputStream(it)) { - write(overlay) - close() - } - } - - //TODO: improve ffmpeg speed - val fFmpegSession = FFmpegKit.execute( - "-y -i " + - tempVideoFile.absolutePath + - " -i " + - tempOverlayFile.absolutePath + - " -filter_complex \"[0]scale2ref[img][vid];[img]setsar=1[img];[vid]nullsink; [img][1]overlay=(W-w)/2:(H-h)/2,scale=2*trunc(iw*sar/2):2*trunc(ih/2)\" -c:v libx264 -q:v 13 -c:a copy " + - " -threads 6 ${(if (isPreviewMode) "-frames:v 1" else "")} " + - mergedFile.absolutePath - ) - tempVideoFile.delete() - tempOverlayFile.delete() - if (fFmpegSession.returnCode.value != 0) { - mergedFile.delete() - context.longToast("Failed to merge video and overlay. See logs for more details.") - xposedLog(fFmpegSession.output) - return null - } - val mergedFileData: ByteArray = FileInputStream(mergedFile).readBytes() - mergedFile.delete() - return mergedFileData - } - private fun queryMediaData(mediaInfo: MediaInfo): ByteArray { val mediaUri = Uri.parse(mediaInfo.uri) val mediaInputStream = AtomicReference<InputStream>() @@ -206,7 +159,7 @@ class MediaDownloader : Feature("MediaDownloader", loadParams = FeatureLoadParam } val overlayMediaInfo = mediaInfoMap[MediaType.OVERLAY]!! val overlayContent: ByteArray = queryMediaData(overlayMediaInfo) - mediaContent = mergeOverlay(mediaContent, overlayContent, false) + mediaContent = MediaDownloaderHelper.mergeOverlay(mediaContent, overlayContent, false) } val fileType = FileType.fromByteArray(mediaContent!!) downloadMediaContent(mediaContent, hash, author, fileType) @@ -369,53 +322,15 @@ class MediaDownloader : Feature("MediaDownloader", loadParams = FeatureLoadParam //download the message content try { - var inputStream: InputStream = CdnDownloader.downloadWithDefaultEndpoints(urlKey) ?: throw FileNotFoundException("Unable to get $urlKey from cdn list. Check the logs for more info") - inputStream = EncryptionUtils.decryptInputStreamFromArroyo( - inputStream, - contentType, - messageReader - ) + context.longToast("Querying $urlKey. It might take a while...") + val downloadedMedia = MediaDownloaderHelper.downloadMediaFromKey(urlKey, canMergeOverlay(), isPreviewMode) { + EncryptionUtils.decryptInputStreamFromArroyo(it, contentType, messageReader) + }[MediaType.ORIGINAL] ?: throw Exception("Failed to download media for key $urlKey") + val fileType = FileType.fromByteArray(downloadedMedia) - var mediaData: ByteArray = inputStream.readBytes() - var fileType = FileType.fromByteArray(mediaData) - val isZipFile = fileType == FileType.ZIP - - //videos with overlay are packed in a zip file - //there are 2 files in the zip file, the video (webm) and the overlay (png) - if (isZipFile) { - var videoData: ByteArray? = null - var overlayData: ByteArray? = null - val zipInputStream = ZipInputStream(ByteArrayInputStream(mediaData)) - while (zipInputStream.nextEntry != null) { - val zipEntryData: ByteArray = zipInputStream.readBytes() - val entryFileType = FileType.fromByteArray(zipEntryData) - if (entryFileType.isVideo) { - videoData = zipEntryData - } else if (entryFileType.isImage) { - overlayData = zipEntryData - } - } - if (videoData == null || overlayData == null) { - xposedLog("Invalid data in zip file") - return - } - val mergedVideo = mergeOverlay(videoData, overlayData, isPreviewMode) - val videoFileType = FileType.fromByteArray(videoData) - if (!isPreviewMode) { - downloadMediaContent( - mergedVideo!!, - Arrays.hashCode(videoData), - messageAuthor, - videoFileType - ) - return - } - mediaData = mergedVideo!! - fileType = videoFileType - } if (isPreviewMode) { runCatching { - val bitmap: Bitmap? = PreviewUtils.createPreview(mediaData, fileType.isVideo) + val bitmap: Bitmap? = PreviewUtils.createPreview(downloadedMedia, fileType.isVideo) if (bitmap == null) { context.shortToast("Failed to create preview") return @@ -435,9 +350,9 @@ class MediaDownloader : Feature("MediaDownloader", loadParams = FeatureLoadParam } return } - downloadMediaContent(mediaData, mediaData.contentHashCode(), messageAuthor, fileType) + downloadMediaContent(downloadedMedia, downloadedMedia.contentHashCode(), messageAuthor, fileType) } catch (e: Throwable) { - context.shortToast("Failed to download " + e.message) + context.longToast("Failed to download " + e.message) xposedLog(e) } } diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/extras/Notifications.kt b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/extras/Notifications.kt @@ -22,8 +22,9 @@ import me.rhunk.snapenhance.hook.HookStage import me.rhunk.snapenhance.hook.Hooker import me.rhunk.snapenhance.util.CallbackBuilder import me.rhunk.snapenhance.util.EncryptionUtils +import me.rhunk.snapenhance.util.MediaDownloaderHelper +import me.rhunk.snapenhance.util.MediaType import me.rhunk.snapenhance.util.PreviewUtils -import me.rhunk.snapenhance.util.download.CdnDownloader import me.rhunk.snapenhance.util.protobuf.ProtoReader class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.INIT_SYNC) { @@ -114,7 +115,6 @@ class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.IN val urlKey = ProtoReader(mediaContent).getString(2, 2) ?: return@forEach runCatching { //download the media - var mediaInputStream = CdnDownloader.downloadWithDefaultEndpoints(urlKey)!! val mediaInfo = ProtoReader(contentData).let { if (contentType == ContentType.EXTERNAL_MEDIA) return@let it.readPath(*Constants.MESSAGE_EXTERNAL_MEDIA_ENCRYPTION_PROTO_PATH) @@ -122,14 +122,13 @@ class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.IN return@let it.readPath(*Constants.MESSAGE_SNAP_ENCRYPTION_PROTO_PATH) }?: return@runCatching - //decrypt if necessary - if (mediaInfo.exists(Constants.ARROYO_ENCRYPTION_PROTO_INDEX)) { - mediaInputStream = EncryptionUtils.decryptInputStream(mediaInputStream, false, mediaInfo, Constants.ARROYO_ENCRYPTION_PROTO_INDEX) - } - - val mediaByteArray = mediaInputStream.readBytes() - val bitmapPreview = PreviewUtils.createPreview(mediaByteArray, mediaType == MediaReferenceType.VIDEO)!! + val downloadedMedia = MediaDownloaderHelper.downloadMediaFromKey(urlKey, mergeOverlay = false, isPreviewMode = false) { + if (mediaInfo.exists(Constants.ARROYO_ENCRYPTION_PROTO_INDEX)) + EncryptionUtils.decryptInputStream(it, false, mediaInfo, Constants.ARROYO_ENCRYPTION_PROTO_INDEX) + else it + }[MediaType.ORIGINAL] ?: throw Throwable("Failed to download media from key $urlKey") + val bitmapPreview = PreviewUtils.createPreview(downloadedMedia, mediaType == MediaReferenceType.VIDEO)!! val notificationBuilder = XposedHelpers.newInstance( Notification.Builder::class.java, context.androidContext, diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/util/MediaDownloaderHelper.kt b/app/src/main/kotlin/me/rhunk/snapenhance/util/MediaDownloaderHelper.kt @@ -0,0 +1,91 @@ +package me.rhunk.snapenhance.util + +import com.arthenica.ffmpegkit.FFmpegKit +import me.rhunk.snapenhance.Logger +import me.rhunk.snapenhance.data.FileType +import me.rhunk.snapenhance.util.download.CdnDownloader +import java.io.ByteArrayInputStream +import java.io.File +import java.io.FileInputStream +import java.io.FileNotFoundException +import java.io.FileOutputStream +import java.io.InputStream +import java.util.zip.ZipInputStream + +enum class MediaType { + ORIGINAL, OVERLAY +} +object MediaDownloaderHelper { + fun downloadMediaFromKey(key: String, mergeOverlay: Boolean, isPreviewMode: Boolean, decryptionCallback: (InputStream) -> InputStream): Map<MediaType, ByteArray> { + val inputStream: InputStream = CdnDownloader.downloadWithDefaultEndpoints(key) ?: throw FileNotFoundException("Unable to get $key from cdn list. Check the logs for more info") + val content = decryptionCallback(inputStream).readBytes().also { inputStream.close() } + val fileType = FileType.fromByteArray(content) + val isZipFile = fileType == FileType.ZIP + + //videos with overlay are packed in a zip file + //there are 2 files in the zip file, the video (webm) and the overlay (png) + if (isZipFile) { + var videoData: ByteArray? = null + var overlayData: ByteArray? = null + val zipInputStream = ZipInputStream(ByteArrayInputStream(content)) + while (zipInputStream.nextEntry != null) { + val zipEntryData: ByteArray = zipInputStream.readBytes() + val entryFileType = FileType.fromByteArray(zipEntryData) + if (entryFileType.isVideo) { + videoData = zipEntryData + } else if (entryFileType.isImage) { + overlayData = zipEntryData + } + } + videoData ?: throw FileNotFoundException("Unable to find video file in zip file") + overlayData ?: throw FileNotFoundException("Unable to find overlay file in zip file") + if (mergeOverlay) { + val mergedVideo = mergeOverlay(videoData, overlayData, isPreviewMode) + return mapOf(MediaType.ORIGINAL to mergedVideo) + } + return mapOf(MediaType.ORIGINAL to videoData, MediaType.OVERLAY to overlayData) + } + + return mapOf(MediaType.ORIGINAL to content) + } + + fun mergeOverlay(original: ByteArray, overlay: ByteArray, isPreviewMode: Boolean): ByteArray { + val originalFileType = FileType.fromByteArray(original) + val overlayFileType = FileType.fromByteArray(overlay) + //merge files + val mergedFile = File.createTempFile("merged", "." + originalFileType.fileExtension) + val tempVideoFile = File.createTempFile("original", "." + originalFileType.fileExtension).also { + with(FileOutputStream(it)) { + write(original) + close() + } + } + val tempOverlayFile = File.createTempFile("overlay", "." + overlayFileType.fileExtension).also { + with(FileOutputStream(it)) { + write(overlay) + close() + } + } + + //TODO: improve ffmpeg speed + val fFmpegSession = FFmpegKit.execute( + "-y -i " + + tempVideoFile.absolutePath + + " -i " + + tempOverlayFile.absolutePath + + " -filter_complex \"[0]scale2ref[img][vid];[img]setsar=1[img];[vid]nullsink; [img][1]overlay=(W-w)/2:(H-h)/2,scale=2*trunc(iw*sar/2):2*trunc(ih/2)\" -c:v libx264 -q:v 13 -c:a copy " + + " -threads 6 ${(if (isPreviewMode) "-frames:v 1" else "")} " + + mergedFile.absolutePath + ) + tempVideoFile.delete() + tempOverlayFile.delete() + if (fFmpegSession.returnCode.value != 0) { + mergedFile.delete() + Logger.xposedLog(fFmpegSession.output) + throw IllegalStateException("Failed to merge video and overlay. See logs for more details.") + } + val mergedFileData: ByteArray = FileInputStream(mergedFile).readBytes() + mergedFile.delete() + return mergedFileData + } +}+ \ No newline at end of file