From b31f7b1ba72641446dc1d9f90d22d4ed0295bce5 Mon Sep 17 00:00:00 2001 From: Karlatemp Date: Sat, 9 Oct 2021 16:21:58 +0800 Subject: [PATCH] AudioToSilkService (#1591) * AudioToSilkService * Update mirai-core-api/src/commonMain/kotlin/utils/ExternalResource.kt Co-authored-by: Eritque arcus <1930893235@qq.com> * `@since`; `@Experimental` Co-authored-by: Eritque arcus <1930893235@qq.com> --- ...binary-compatibility-validator-android.api | 1 + .../api/binary-compatibility-validator.api | 1 + .../internal/utils/ExternalImageImpls.kt | 11 +++- .../kotlin/spi/AudioToSilkService.kt | 56 +++++++++++++++++++ .../commonMain/kotlin/spi/SPIServiceLoader.kt | 47 ++++++++++++++++ .../kotlin/utils/ExternalResource.kt | 25 ++++++++- .../commonMain/kotlin/contact/FriendImpl.kt | 43 +++++++------- .../commonMain/kotlin/contact/GroupImpl.kt | 29 ++++++---- 8 files changed, 179 insertions(+), 34 deletions(-) create mode 100644 mirai-core-api/src/commonMain/kotlin/spi/AudioToSilkService.kt create mode 100644 mirai-core-api/src/commonMain/kotlin/spi/SPIServiceLoader.kt diff --git a/binary-compatibility-validator/android/api/binary-compatibility-validator-android.api b/binary-compatibility-validator/android/api/binary-compatibility-validator-android.api index 9691f9abc..639b15e8a 100644 --- a/binary-compatibility-validator/android/api/binary-compatibility-validator-android.api +++ b/binary-compatibility-validator/android/api/binary-compatibility-validator-android.api @@ -5709,6 +5709,7 @@ public abstract interface class net/mamoe/mirai/utils/ExternalResource : java/io public abstract fun getClosed ()Lkotlinx/coroutines/Deferred; public abstract fun getFormatName ()Ljava/lang/String; public abstract fun getMd5 ()[B + public fun getOrigin ()Ljava/lang/Object; public fun getSha1 ()[B public abstract fun getSize ()J public abstract fun inputStream ()Ljava/io/InputStream; diff --git a/binary-compatibility-validator/api/binary-compatibility-validator.api b/binary-compatibility-validator/api/binary-compatibility-validator.api index ec677c124..b76047234 100644 --- a/binary-compatibility-validator/api/binary-compatibility-validator.api +++ b/binary-compatibility-validator/api/binary-compatibility-validator.api @@ -5709,6 +5709,7 @@ public abstract interface class net/mamoe/mirai/utils/ExternalResource : java/io public abstract fun getClosed ()Lkotlinx/coroutines/Deferred; public abstract fun getFormatName ()Ljava/lang/String; public abstract fun getMd5 ()[B + public fun getOrigin ()Ljava/lang/Object; public fun getSha1 ()[B public abstract fun getSize ()J public abstract fun inputStream ()Ljava/io/InputStream; diff --git a/mirai-core-api/src/commonMain/kotlin/internal/utils/ExternalImageImpls.kt b/mirai-core-api/src/commonMain/kotlin/internal/utils/ExternalImageImpls.kt index 0c02a3085..edaef541a 100644 --- a/mirai-core-api/src/commonMain/kotlin/internal/utils/ExternalImageImpls.kt +++ b/mirai-core-api/src/commonMain/kotlin/internal/utils/ExternalImageImpls.kt @@ -40,6 +40,10 @@ internal class ExternalResourceImplByFileWithMd5( } } + override var origin: Any? = null + internal set + + override val holder: ResourceHolder = ResourceHolder(file) override val sha1: ByteArray by lazy { inputStream().sha1() } @@ -97,8 +101,11 @@ internal interface ExternalResourceInternal : ExternalResource { internal class ExternalResourceImplByFile( private val file: RandomAccessFile, formatName: String?, - closeOriginalFileOnClose: Boolean = true + closeOriginalFileOnClose: Boolean = true, ) : ExternalResourceInternal { + override var origin: Any? = null + internal set + internal class ResourceHolder( @JvmField internal val closeOriginalFileOnClose: Boolean, @JvmField internal val file: RandomAccessFile, @@ -146,6 +153,8 @@ internal class ExternalResourceImplByByteArray( ?: ExternalResource.DEFAULT_FORMAT_NAME } override val closed: CompletableDeferred = CompletableDeferred() + override val origin: Any + get() = data//.clone() override fun inputStream(): InputStream = data.inputStream() override fun close() { diff --git a/mirai-core-api/src/commonMain/kotlin/spi/AudioToSilkService.kt b/mirai-core-api/src/commonMain/kotlin/spi/AudioToSilkService.kt new file mode 100644 index 000000000..717e7b0f5 --- /dev/null +++ b/mirai-core-api/src/commonMain/kotlin/spi/AudioToSilkService.kt @@ -0,0 +1,56 @@ +/* + * Copyright 2019-2021 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. + * + * https://github.com/mamoe/mirai/blob/dev/LICENSE + */ + +package net.mamoe.mirai.spi + +import net.mamoe.mirai.utils.* +import java.io.IOException + +/** + * 将源音频文件转换为 silk v3 with tencent 格式 + * + * @since 2.8.0 + */ +@MiraiExperimentalApi +public interface AudioToSilkService : BaseService { + /** + * implementation note: + * + * 如果返回值为转换后的资源文件: + * + * 如果 [ExternalResource.isAutoClose], 需要关闭 [source], + * 返回的 [ExternalResource] 的 [ExternalResource.isAutoClose] 必须为 `true` + * + * 特别的, 如果该方法体抛出了一个错误, 如果 [ExternalResource.isAutoClose], 需要关闭 [source] + * + * @see [withAutoClose] + * @see [runAutoClose] + * @see [useAutoClose] + */ + @Throws(IOException::class) + public suspend fun convert(source: ExternalResource): ExternalResource + + @MiraiExperimentalApi + public companion object : AudioToSilkService { + private val loader = SPIServiceLoader(object : AudioToSilkService { + override suspend fun convert(source: ExternalResource): ExternalResource = source + }, AudioToSilkService::class.java) + + @Suppress("BlockingMethodInNonBlockingContext") + @Throws(IOException::class) + override suspend fun convert(source: ExternalResource): ExternalResource { + return loader.service.convert(source) + } + + @JvmStatic + public fun setService(service: AudioToSilkService) { + loader.service = service + } + } +} \ No newline at end of file diff --git a/mirai-core-api/src/commonMain/kotlin/spi/SPIServiceLoader.kt b/mirai-core-api/src/commonMain/kotlin/spi/SPIServiceLoader.kt new file mode 100644 index 000000000..ba0e93580 --- /dev/null +++ b/mirai-core-api/src/commonMain/kotlin/spi/SPIServiceLoader.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2019-2021 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. + * + * https://github.com/mamoe/mirai/blob/dev/LICENSE + */ + +package net.mamoe.mirai.spi + +import net.mamoe.mirai.utils.MiraiExperimentalApi +import net.mamoe.mirai.utils.MiraiLogger +import java.util.* + +/** + * 基本 SPI 接口 + * @since 2.8.0 + */ +@MiraiExperimentalApi +public interface BaseService { + /** 使用优先级, 值越小越先使用 */ + public val priority: Int get() = 5 +} + +internal class SPIServiceLoader( + @JvmField val defaultService: T, + @JvmField val serviceType: Class, +) { + @JvmField + var service: T = defaultService + + fun reload() { + val loader = ServiceLoader.load(serviceType) + service = loader.minByOrNull { it.priority } ?: defaultService + } + + init { + reload() + } + + companion object { + val SPI_SERVICE_LOADER_LOGGER by lazy { + MiraiLogger.Factory.create(SPIServiceLoader::class.java, "spi-service-loader") + } + } +} diff --git a/mirai-core-api/src/commonMain/kotlin/utils/ExternalResource.kt b/mirai-core-api/src/commonMain/kotlin/utils/ExternalResource.kt index e6cf146c1..eb74ae44b 100644 --- a/mirai-core-api/src/commonMain/kotlin/utils/ExternalResource.kt +++ b/mirai-core-api/src/commonMain/kotlin/utils/ExternalResource.kt @@ -133,6 +133,27 @@ public interface ExternalResource : Closeable { return generateImageId(md5, formatName.ifEmpty { DEFAULT_FORMAT_NAME }) } + /** + * 该 [ExternalResource] 的数据来源, 可能有以下的返回 + * + * - [File] 本地文件 + * - [java.nio.file.Path] 某个具体文件路径 + * - [java.nio.ByteBuffer] RAM + * - [java.net.URI] uri + * - [ByteArray] RAM + * - Or more... + * + * implementation note: + * + * - 对于无法二次读取的数据来源 (如 [InputStream]), 返回 `null` + * - 对于一个来自网络的资源, 请返回 [java.net.URI] (not URL, 或者其他库的 URI/URL 类型) + * - 不要返回 [String], 没有约定 [String] 代表什么 + * - 数据源外漏会严重影响 [inputStream] 等的执行的可以返回 `null` (如 [RandomAccessFile]) + * + * @since TODO + */ + public val origin: Any? get() = null + public companion object { /** * 在无法识别文件格式时使用的默认格式名. "mirai". @@ -157,7 +178,9 @@ public interface ExternalResource : Closeable { @JvmName("create") public fun File.toExternalResource(formatName: String? = null): ExternalResource = // although RandomAccessFile constructor throws IOException, actual performance influence is minor so not propagating IOException - RandomAccessFile(this, "r").toExternalResource(formatName) + RandomAccessFile(this, "r").toExternalResource(formatName).also { + it.cast().origin = this@toExternalResource + } /** * 创建 [ExternalResource]. diff --git a/mirai-core/src/commonMain/kotlin/contact/FriendImpl.kt b/mirai-core/src/commonMain/kotlin/contact/FriendImpl.kt index 34d4509aa..b5c757daf 100644 --- a/mirai-core/src/commonMain/kotlin/contact/FriendImpl.kt +++ b/mirai-core/src/commonMain/kotlin/contact/FriendImpl.kt @@ -36,6 +36,7 @@ import net.mamoe.mirai.internal.utils.io.serialization.toByteArray import net.mamoe.mirai.message.MessageReceipt import net.mamoe.mirai.message.data.Message import net.mamoe.mirai.message.data.OfflineAudio +import net.mamoe.mirai.spi.AudioToSilkService import net.mamoe.mirai.utils.* import kotlin.contracts.ExperimentalContracts import kotlin.contracts.contract @@ -83,15 +84,17 @@ internal class FriendImpl( override fun toString(): String = "Friend($id)" - override suspend fun uploadAudio(resource: ExternalResource): OfflineAudio = resource.withAutoClose { + override suspend fun uploadAudio(resource: ExternalResource): OfflineAudio = AudioToSilkService.convert( + resource + ).useAutoClose { res -> var audio: OfflineAudioImpl? = null kotlin.runCatching { val resp = Highway.uploadResourceBdh( bot = bot, - resource = resource, + resource = res, kind = ResourceKind.PRIVATE_AUDIO, commandId = 26, - extendInfo = PttStore.C2C.createC2CPttStoreBDHExt(bot, this@FriendImpl.uin, resource) + extendInfo = PttStore.C2C.createC2CPttStoreBDHExt(bot, this@FriendImpl.uin, res) .toByteArray(Cmd0x346.ReqBody.serializer()) ) // resp._miraiContentToString("UV resp") @@ -100,44 +103,44 @@ internal class FriendImpl( error("Upload failed") } audio = OfflineAudioImpl( - filename = "${resource.md5.toUHexString("")}.amr", - fileMd5 = resource.md5, - fileSize = resource.size, - codec = resource.audioCodec, + filename = "${res.md5.toUHexString("")}.amr", + fileMd5 = res.md5, + fileSize = res.size, + codec = res.audioCodec, originalPtt = ImMsgBody.Ptt( fileType = 4, srcUin = bot.uin, fileUuid = c346resp.msgApplyUploadRsp.uuid, - fileMd5 = resource.md5, - fileName = resource.md5 + ".amr".toByteArray(), - fileSize = resource.size.toInt(), + fileMd5 = res.md5, + fileName = res.md5 + ".amr".toByteArray(), + fileSize = res.size.toInt(), boolValid = true, ) ) }.recoverCatchingSuppressed { - when (val resp = PttStore.GroupPttUp(bot.client, bot.id, id, resource).sendAndExpect(bot)) { + when (val resp = PttStore.GroupPttUp(bot.client, bot.id, id, res).sendAndExpect(bot)) { is PttStore.GroupPttUp.Response.RequireUpload -> { tryServersUpload( bot, resp.uploadIpList.zip(resp.uploadPortList), - resource.size, + res.size, ResourceKind.GROUP_AUDIO, ChannelKind.HTTP ) { ip, port -> - Mirai.Http.postPtt(ip, port, resource, resp.uKey, resp.fileKey) + Mirai.Http.postPtt(ip, port, res, resp.uKey, resp.fileKey) } audio = OfflineAudioImpl( - filename = "${resource.md5.toUHexString("")}.amr", - fileMd5 = resource.md5, - fileSize = resource.size, - codec = resource.audioCodec, + filename = "${res.md5.toUHexString("")}.amr", + fileMd5 = res.md5, + fileSize = res.size, + codec = res.audioCodec, originalPtt = ImMsgBody.Ptt( fileType = 4, srcUin = bot.uin, fileUuid = resp.fileId.toByteArray(), - fileMd5 = resource.md5, - fileName = resource.md5 + ".amr".toByteArray(), - fileSize = resource.size.toInt(), + fileMd5 = res.md5, + fileName = res.md5 + ".amr".toByteArray(), + fileSize = res.size.toInt(), boolValid = true, ) ) diff --git a/mirai-core/src/commonMain/kotlin/contact/GroupImpl.kt b/mirai-core/src/commonMain/kotlin/contact/GroupImpl.kt index a19b0b157..e923d88b1 100644 --- a/mirai-core/src/commonMain/kotlin/contact/GroupImpl.kt +++ b/mirai-core/src/commonMain/kotlin/contact/GroupImpl.kt @@ -47,6 +47,7 @@ import net.mamoe.mirai.internal.utils.io.serialization.toByteArray import net.mamoe.mirai.internal.utils.subLogger import net.mamoe.mirai.message.MessageReceipt import net.mamoe.mirai.message.data.* +import net.mamoe.mirai.spi.AudioToSilkService import net.mamoe.mirai.utils.* import java.util.concurrent.ConcurrentLinkedQueue import kotlin.contracts.contract @@ -239,19 +240,21 @@ internal class GroupImpl constructor( } @Suppress("OverridingDeprecatedMember", "DEPRECATION") - override suspend fun uploadVoice(resource: ExternalResource): Voice = resource.withAutoClose { + override suspend fun uploadVoice(resource: ExternalResource): Voice = AudioToSilkService.convert( + resource + ).useAutoClose { res -> return bot.network.run { - uploadAudioResource(resource) + uploadAudioResource(res) // val body = resp?.loadAs(Cmd0x388.RspBody.serializer()) // ?.msgTryupPttRsp // ?.singleOrNull()?.fileKey ?: error("Group voice highway transfer succeed but failed to find fileKey") Voice( - "${resource.md5.toUHexString("")}.amr", - resource.md5, - resource.size, - resource.voiceCodec, + "${res.md5.toUHexString("")}.amr", + res.md5, + res.size, + res.voiceCodec, "" ) } @@ -284,19 +287,21 @@ internal class GroupImpl constructor( }.getOrThrow() } - override suspend fun uploadAudio(resource: ExternalResource): OfflineAudio = resource.withAutoClose { + override suspend fun uploadAudio(resource: ExternalResource): OfflineAudio = AudioToSilkService.convert( + resource + ).useAutoClose { res -> return bot.network.run { - uploadAudioResource(resource) + uploadAudioResource(res) // val body = resp?.loadAs(Cmd0x388.RspBody.serializer()) // ?.msgTryupPttRsp // ?.singleOrNull()?.fileKey ?: error("Group voice highway transfer succeed but failed to find fileKey") OfflineAudioImpl( - filename = "${resource.md5.toUHexString("")}.amr", - fileMd5 = resource.md5, - fileSize = resource.size, - codec = resource.audioCodec, + filename = "${res.md5.toUHexString("")}.amr", + fileMd5 = res.md5, + fileSize = res.size, + codec = res.audioCodec, originalPtt = null, ) }