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>
This commit is contained in:
Karlatemp 2021-10-09 16:21:58 +08:00 committed by GitHub
parent 8bd3d94537
commit b31f7b1ba7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 179 additions and 34 deletions

View File

@ -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;

View File

@ -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;

View File

@ -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<Unit> = CompletableDeferred()
override val origin: Any
get() = data//.clone()
override fun inputStream(): InputStream = data.inputStream()
override fun close() {

View File

@ -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
}
}
}

View File

@ -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<T : BaseService>(
@JvmField val defaultService: T,
@JvmField val serviceType: Class<T>,
) {
@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")
}
}
}

View File

@ -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<ExternalResourceImplByFile>().origin = this@toExternalResource
}
/**
* 创建 [ExternalResource].

View File

@ -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,
)
)

View File

@ -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,
)
}