投稿時の動画変換のコードを整理
This commit is contained in:
parent
3c90cfa6c8
commit
cd8168b62a
|
@ -967,19 +967,14 @@ class SavedAccount(
|
|||
?: if (ti.instanceType == InstanceType.Pixelfed) 15 else 8
|
||||
)
|
||||
|
||||
fun getMovieResizeConfig(): MovieResizeConfig = MovieResizeConfig(
|
||||
mode = when (movieTranscodeMode) {
|
||||
MovieResizeConfig.MODE_NO,
|
||||
MovieResizeConfig.MODE_AUTO,
|
||||
MovieResizeConfig.NODE_ALWAYS,
|
||||
-> movieTranscodeMode
|
||||
else -> MovieResizeConfig.MODE_AUTO
|
||||
},
|
||||
limitBitrate = movieTranscodeBitrate.toLongOrNull()
|
||||
?.takeIf { it >= 100_000L } ?: 2_000_000L,
|
||||
limitFrameRate = movieTranscodeFramerate.toIntOrNull()
|
||||
?.takeIf { it >= 1 } ?: 30,
|
||||
limitPixelMatrix = movieTranscodeSquarePixels.toIntOrNull()
|
||||
?.takeIf { it > 0 } ?: 2304000,
|
||||
)
|
||||
fun getMovieResizeConfig() =
|
||||
MovieResizeConfig(
|
||||
mode = MovideResizeMode.fromInt(movieTranscodeMode),
|
||||
limitBitrate = movieTranscodeBitrate.toLongOrNull()
|
||||
?.takeIf { it >= 100_000L } ?: 2_000_000L,
|
||||
limitFrameRate = movieTranscodeFramerate.toIntOrNull()
|
||||
?.takeIf { it >= 1 } ?: 30,
|
||||
limitSquarePixels = movieTranscodeSquarePixels.toIntOrNull()
|
||||
?.takeIf { it > 0 } ?: 2304000,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -15,13 +15,13 @@ import jp.juggler.subwaytooter.api.entity.*
|
|||
import jp.juggler.subwaytooter.api.runApiTask
|
||||
import jp.juggler.subwaytooter.table.SavedAccount
|
||||
import jp.juggler.util.*
|
||||
import jp.juggler.util.VideoInfo.Companion.videoInfo
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.channels.ClosedReceiveChannelException
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.MediaType
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.MultipartBody
|
||||
import okhttp3.RequestBody
|
||||
|
@ -30,7 +30,6 @@ import java.io.*
|
|||
import java.util.*
|
||||
import java.util.concurrent.CancellationException
|
||||
import kotlin.coroutines.coroutineContext
|
||||
import kotlin.math.min
|
||||
|
||||
class AttachmentRequest(
|
||||
val account: SavedAccount,
|
||||
|
@ -302,27 +301,13 @@ class AttachmentUploader(
|
|||
?.let { ResizeConfig(ResizeType.SquarePixel, it) }
|
||||
?: account.getResizeConfig()
|
||||
|
||||
val movieResizeConfig = account.getMovieResizeConfig()
|
||||
|
||||
mediaConfig?.int("video_frame_rate_limit")
|
||||
?.takeIf { it >= 1f }
|
||||
?.let {
|
||||
movieResizeConfig.limitFrameRate = min(movieResizeConfig.limitFrameRate, it)
|
||||
}
|
||||
|
||||
mediaConfig?.int("video_matrix_limit")
|
||||
?.takeIf { it > 1 }
|
||||
?.let {
|
||||
movieResizeConfig.limitPixelMatrix = min(movieResizeConfig.limitPixelMatrix, it)
|
||||
}
|
||||
|
||||
// 入力データの変換など
|
||||
val opener = createOpener(
|
||||
account,
|
||||
uri,
|
||||
mimeType,
|
||||
mediaConfig = mediaConfig,
|
||||
imageResizeConfig = imageResizeConfig,
|
||||
movieResizeConfig = movieResizeConfig,
|
||||
postAttachment = pa,
|
||||
)
|
||||
|
||||
|
@ -337,8 +322,7 @@ class AttachmentUploader(
|
|||
?: account.getImageMaxBytes(ti)
|
||||
}
|
||||
|
||||
val contentLength = getStreamSize(true, opener.open())
|
||||
if (contentLength > mediaSizeMax) {
|
||||
if (opener.contentLength > mediaSizeMax) {
|
||||
return TootApiResult(
|
||||
context.getString(R.string.file_size_too_big, mediaSizeMax / 1000000)
|
||||
)
|
||||
|
@ -362,17 +346,12 @@ class AttachmentUploader(
|
|||
|
||||
val fileName = fixDocumentName(getDocumentName(context.contentResolver, uri))
|
||||
pa.progress = context.getString(R.string.attachment_handling_uploading, 0)
|
||||
var nWrite = 0
|
||||
fun writeProgress(delta: Int) {
|
||||
nWrite += delta
|
||||
if (contentLength > 0) {
|
||||
val percent = (100f * nWrite.toFloat() / contentLength.toFloat()).toInt()
|
||||
if (percent < 100) {
|
||||
pa.progress =
|
||||
context.getString(R.string.attachment_handling_uploading, percent)
|
||||
} else {
|
||||
pa.progress = context.getString(R.string.attachment_handling_waiting)
|
||||
}
|
||||
fun writeProgress(percent: Int) {
|
||||
if (percent < 100) {
|
||||
pa.progress =
|
||||
context.getString(R.string.attachment_handling_uploading, percent)
|
||||
} else {
|
||||
pa.progress = context.getString(R.string.attachment_handling_waiting)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -388,29 +367,7 @@ class AttachmentUploader(
|
|||
multipartBuilder.addFormDataPart(
|
||||
"file",
|
||||
fileName,
|
||||
object : RequestBody() {
|
||||
override fun contentType(): MediaType {
|
||||
return opener.mimeType.toMediaType()
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun contentLength(): Long {
|
||||
return contentLength
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun writeTo(sink: BufferedSink) {
|
||||
opener.open().use { inData ->
|
||||
val tmp = ByteArray(4096)
|
||||
while (true) {
|
||||
val r = inData.read(tmp, 0, tmp.size)
|
||||
if (r <= 0) break
|
||||
writeProgress(r)
|
||||
sink.write(tmp, 0, r)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
opener.toRequestBody { writeProgress(it) },
|
||||
)
|
||||
|
||||
val result = client.request(
|
||||
|
@ -437,29 +394,7 @@ class AttachmentUploader(
|
|||
.addFormDataPart(
|
||||
"file",
|
||||
fileName,
|
||||
object : RequestBody() {
|
||||
override fun contentType(): MediaType {
|
||||
return opener.mimeType.toMediaType()
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun contentLength(): Long {
|
||||
return contentLength
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun writeTo(sink: BufferedSink) {
|
||||
opener.open().use { inData ->
|
||||
val tmp = ByteArray(4096)
|
||||
while (true) {
|
||||
val r = inData.read(tmp, 0, tmp.size)
|
||||
if (r <= 0) break
|
||||
writeProgress(r)
|
||||
sink.write(tmp, 0, r)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
opener.toRequestBody { writeProgress(it) },
|
||||
)
|
||||
.build().toPost()
|
||||
)
|
||||
|
@ -481,18 +416,18 @@ class AttachmentUploader(
|
|||
// 202 accepted 以外はポーリングしない
|
||||
code != 202 -> return result
|
||||
}
|
||||
pa.progress = context.getString(R.string.attachment_handling_waiting2)
|
||||
|
||||
// ポーリングして処理完了を待つ
|
||||
val id =
|
||||
parseItem(::TootAttachment, ServiceType.MASTODON, result?.jsonObject)
|
||||
?.id
|
||||
?: return TootApiResult("/api/v2/media did not return the media ID.")
|
||||
pa.progress = context.getString(R.string.attachment_handling_waiting_async)
|
||||
val id = parseItem(::TootAttachment, ServiceType.MASTODON, result?.jsonObject)
|
||||
?.id
|
||||
?: return TootApiResult("/api/v2/media did not return the media ID.")
|
||||
|
||||
var lastResponse = SystemClock.elapsedRealtime()
|
||||
loop@ while (true) {
|
||||
|
||||
delay(1000L)
|
||||
|
||||
val r2 = client.request("/api/v1/media/$id")
|
||||
?: return null // cancelled
|
||||
|
||||
|
@ -504,7 +439,7 @@ class AttachmentUploader(
|
|||
// continue to wait
|
||||
206 -> lastResponse = now
|
||||
|
||||
// too many temporary error without 206 response.
|
||||
// temporary errors, check timeout without 206 response.
|
||||
else -> if (now - lastResponse >= 120000L) {
|
||||
return TootApiResult("timeout.")
|
||||
}
|
||||
|
@ -561,25 +496,78 @@ class AttachmentUploader(
|
|||
pa.callback?.onPostAttachmentComplete(pa)
|
||||
}
|
||||
|
||||
internal interface InputStreamOpener {
|
||||
val mimeType: String
|
||||
// contentLengthの測定などで複数回オープンする必要がある
|
||||
private abstract class InputStreamOpener {
|
||||
abstract val mimeType: String
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun open(): InputStream
|
||||
abstract fun open(): InputStream
|
||||
|
||||
fun deleteTempFile()
|
||||
abstract fun deleteTempFile()
|
||||
|
||||
val contentLength by lazy { getStreamSize(true, open()) }
|
||||
|
||||
// okhttpのRequestBodyにする
|
||||
fun toRequestBody(onWrote: (percent: Int) -> Unit = {}) =
|
||||
object : RequestBody() {
|
||||
override fun contentType() = mimeType.toMediaType()
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun contentLength(): Long = contentLength
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun writeTo(sink: BufferedSink) {
|
||||
val length = contentLength.toFloat()
|
||||
open().use { inStream ->
|
||||
val tmp = ByteArray(4096)
|
||||
var nWrite = 0L
|
||||
while (true) {
|
||||
val delta = inStream.read(tmp, 0, tmp.size)
|
||||
if (delta <= 0) break
|
||||
sink.write(tmp, 0, delta)
|
||||
nWrite += delta
|
||||
val percent = (100f * nWrite.toFloat() / length).toInt()
|
||||
onWrote(percent)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun contentUriOpener(contentResolver: ContentResolver, uri: Uri, mimeType: String) =
|
||||
object : InputStreamOpener() {
|
||||
override val mimeType = mimeType
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun open(): InputStream {
|
||||
return contentResolver.openInputStream(uri)
|
||||
?: error("openInputStream returns null")
|
||||
}
|
||||
|
||||
override fun deleteTempFile() = Unit
|
||||
}
|
||||
|
||||
private fun tempFileOpener(mimeType: String, file: File) =
|
||||
object : InputStreamOpener() {
|
||||
override val mimeType = mimeType
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun open() = FileInputStream(file)
|
||||
override fun deleteTempFile() {
|
||||
file.delete()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun createOpener(
|
||||
account: SavedAccount,
|
||||
uri: Uri,
|
||||
mimeType: String,
|
||||
mediaConfig: JsonObject? = null,
|
||||
imageResizeConfig: ResizeConfig,
|
||||
movieResizeConfig: MovieResizeConfig? = null,
|
||||
postAttachment: PostAttachment? = null,
|
||||
): InputStreamOpener {
|
||||
if (mimeType == MIME_TYPE_JPEG || mimeType == MIME_TYPE_PNG) {
|
||||
// 静止画(リサイズできなくてもOK)
|
||||
// 静止画(失敗したらオリジナルデータにフォールバックする)
|
||||
try {
|
||||
return createResizedImageOpener(
|
||||
uri,
|
||||
|
@ -600,33 +588,22 @@ class AttachmentUploader(
|
|||
postAttachment,
|
||||
forcePng = true
|
||||
)
|
||||
} else {
|
||||
// 動画画(リサイズできなくてもOK)
|
||||
} else if (mimeType.startsWith("video/")) {
|
||||
// 動画のトランスコード(失敗したらオリジナルデータにフォールバックする)
|
||||
try {
|
||||
return createResizedMovieOpener(
|
||||
account,
|
||||
uri,
|
||||
mimeType,
|
||||
mediaConfig = mediaConfig,
|
||||
movieResizeConfig,
|
||||
postAttachment,
|
||||
postAttachment = postAttachment,
|
||||
)
|
||||
} catch (ex: Throwable) {
|
||||
log.w(ex, "createResizedMovieOpener failed. fall back to original movie.")
|
||||
}
|
||||
}
|
||||
|
||||
return object : InputStreamOpener {
|
||||
override val mimeType = mimeType
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun open(): InputStream {
|
||||
return context.contentResolver.openInputStream(uri)
|
||||
?: error("openInputStream returns null")
|
||||
}
|
||||
|
||||
override fun deleteTempFile() {
|
||||
}
|
||||
}
|
||||
return contentUriOpener(context.contentResolver, uri, mimeType)
|
||||
}
|
||||
|
||||
private fun createResizedImageOpener(
|
||||
|
@ -662,7 +639,7 @@ class AttachmentUploader(
|
|||
bitmap.compress(Bitmap.CompressFormat.JPEG, 95, os)
|
||||
}
|
||||
}
|
||||
return createTempFileOpener(outputMimeType, tempFile)
|
||||
return tempFileOpener(outputMimeType, tempFile)
|
||||
} finally {
|
||||
bitmap.recycle()
|
||||
}
|
||||
|
@ -670,14 +647,12 @@ class AttachmentUploader(
|
|||
|
||||
|
||||
private suspend fun createResizedMovieOpener(
|
||||
account: SavedAccount,
|
||||
uri: Uri,
|
||||
mimeType: String,
|
||||
mediaConfig: JsonObject?,
|
||||
movieResizeConfig: MovieResizeConfig?,
|
||||
postAttachment: PostAttachment?,
|
||||
): InputStreamOpener {
|
||||
movieResizeConfig ?: error("missing movieResizeConfig.")
|
||||
|
||||
val cacheDir = context.externalCacheDir
|
||||
?.apply { mkdirs() }
|
||||
?: error("getExternalCacheDir returns null.")
|
||||
|
@ -693,20 +668,41 @@ class AttachmentUploader(
|
|||
}
|
||||
|
||||
try {
|
||||
val limitFileSize = mediaConfig?.long("video_size_limit")?.takeIf { it > 0L }
|
||||
// 動画のメタデータを調べる
|
||||
val info = tempFile.videoInfo
|
||||
|
||||
// サーバに指定されたファイルサイズ上限と入力動画の時間長があれば、ビットレート上限を制限する
|
||||
val duration = info.duration?.takeIf { it >= 0.1f }
|
||||
val limitFileSize = mediaConfig?.float("video_size_limit")?.takeIf { it >= 1f }
|
||||
val limitBitrate = when {
|
||||
duration != null && limitFileSize != null ->
|
||||
(limitFileSize / duration).toLong()
|
||||
else -> null
|
||||
}
|
||||
|
||||
// アカウント別の動画トランスコード設定
|
||||
// ビットレート、フレームレート、平方ピクセル数をサーバ指定により制限する
|
||||
val movieResizeConfig = account.getMovieResizeConfig()
|
||||
.restrict(
|
||||
limitBitrate = limitBitrate,
|
||||
limitFrameRate = mediaConfig?.int("video_frame_rate_limit")
|
||||
?.takeIf { it >= 1f },
|
||||
limitSquarePixels = mediaConfig?.int("video_matrix_limit")
|
||||
?.takeIf { it > 1 },
|
||||
)
|
||||
|
||||
val result = transcodeVideo(
|
||||
info,
|
||||
tempFile,
|
||||
outFile,
|
||||
movieResizeConfig,
|
||||
limitFileSize
|
||||
) {
|
||||
val percent = (it * 100f).toInt()
|
||||
postAttachment?.progress =
|
||||
context.getString(R.string.attachment_handling_compress_ratio, percent)
|
||||
}
|
||||
resultFile = result
|
||||
return createTempFileOpener(
|
||||
return tempFileOpener(
|
||||
when (result) {
|
||||
tempFile -> mimeType
|
||||
else -> "video/mp4"
|
||||
|
@ -719,16 +715,6 @@ class AttachmentUploader(
|
|||
}
|
||||
}
|
||||
|
||||
private fun createTempFileOpener(mimeType: String, file: File) =
|
||||
object : InputStreamOpener {
|
||||
override val mimeType = mimeType
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun open() = FileInputStream(file)
|
||||
override fun deleteTempFile() {
|
||||
file.delete()
|
||||
}
|
||||
}
|
||||
|
||||
fun getMimeType(uri: Uri, mimeTypeArg: String?): String? {
|
||||
// image/j()pg だの image/j(e)pg だの、mime type を誤記するアプリがあまりに多い
|
||||
|
@ -829,7 +815,7 @@ class AttachmentUploader(
|
|||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////
|
||||
|
||||
// 添付データのカスタムサムネイル
|
||||
suspend fun uploadCustomThumbnail(
|
||||
account: SavedAccount,
|
||||
src: GetContentResultEntry,
|
||||
|
@ -844,14 +830,15 @@ class AttachmentUploader(
|
|||
val (ti, ri) = TootInstance.get(client)
|
||||
ti ?: return@runApiTask ri
|
||||
|
||||
val resizeConfig = ResizeConfig(ResizeType.SquarePixel, 400)
|
||||
|
||||
val opener = createOpener(src.uri, mimeType, imageResizeConfig = resizeConfig)
|
||||
val opener = createOpener(
|
||||
account,
|
||||
src.uri,
|
||||
mimeType,
|
||||
imageResizeConfig = ResizeConfig(ResizeType.SquarePixel, 400)
|
||||
)
|
||||
|
||||
val mediaSizeMax = 1000000
|
||||
|
||||
val contentLength = getStreamSize(true, opener.open())
|
||||
if (contentLength > mediaSizeMax) {
|
||||
if (opener.contentLength > mediaSizeMax) {
|
||||
return@runApiTask TootApiResult(
|
||||
getString(R.string.file_size_too_big, mediaSizeMax / 1000000)
|
||||
)
|
||||
|
@ -886,28 +873,7 @@ class AttachmentUploader(
|
|||
.addFormDataPart(
|
||||
"thumbnail",
|
||||
fileName,
|
||||
object : RequestBody() {
|
||||
override fun contentType(): MediaType {
|
||||
return opener.mimeType.toMediaType()
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun contentLength(): Long {
|
||||
return contentLength
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun writeTo(sink: BufferedSink) {
|
||||
opener.open().use { inData ->
|
||||
val tmp = ByteArray(4096)
|
||||
while (true) {
|
||||
val r = inData.read(tmp, 0, tmp.size)
|
||||
if (r <= 0) break
|
||||
sink.write(tmp, 0, r)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
opener.toRequestBody(),
|
||||
)
|
||||
.build().toPut()
|
||||
)
|
||||
|
|
|
@ -3,81 +3,92 @@ package jp.juggler.util
|
|||
import com.otaliastudios.transcoder.Transcoder
|
||||
import com.otaliastudios.transcoder.TranscoderListener
|
||||
import com.otaliastudios.transcoder.common.Size
|
||||
import com.otaliastudios.transcoder.resize.Resizer
|
||||
import com.otaliastudios.transcoder.strategy.DefaultAudioStrategy
|
||||
import com.otaliastudios.transcoder.strategy.DefaultVideoStrategy
|
||||
import jp.juggler.util.VideoInfo.Companion.videoInfo
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.channels.ClosedReceiveChannelException
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import kotlin.coroutines.resumeWithException
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
import kotlin.math.sqrt
|
||||
|
||||
private val log = LogCategory("MovieUtils")
|
||||
|
||||
data class MovieResizeConfig(
|
||||
var mode: Int = 0,
|
||||
var limitFrameRate: Int = 30,
|
||||
var limitBitrate: Long = 2_000_000L,
|
||||
var limitPixelMatrix: Int = 2304000,
|
||||
) {
|
||||
enum class MovideResizeMode(val int: Int) {
|
||||
Auto(0),
|
||||
No(1),
|
||||
Always(2),
|
||||
;
|
||||
|
||||
companion object {
|
||||
const val MODE_AUTO = 0
|
||||
const val MODE_NO = 1
|
||||
const val NODE_ALWAYS = 2
|
||||
fun fromInt(i: Int) = values().find { it.int == i } ?: Auto
|
||||
}
|
||||
}
|
||||
|
||||
class AtMostSquarePixelResizer(private val limit: Int) : Resizer {
|
||||
override fun getOutputSize(inputSize: Size): Size {
|
||||
val inSquarePixel = abs(inputSize.major) * abs(inputSize.minor)
|
||||
if (inSquarePixel <= limit || inputSize.major <= 0 || inputSize.minor <= 0) {
|
||||
return inputSize
|
||||
}
|
||||
val aspect = inputSize.major.toFloat() / inputSize.minor.toFloat()
|
||||
return Size(
|
||||
max(1, (sqrt(limit.toFloat() * aspect) + 0.5f).toInt()),
|
||||
max(1, (sqrt(limit.toFloat() / aspect) + 0.5f).toInt()),
|
||||
)
|
||||
data class MovieResizeConfig(
|
||||
val mode: MovideResizeMode,
|
||||
val limitFrameRate: Int,
|
||||
val limitBitrate: Long,
|
||||
val limitSquarePixels: Int,
|
||||
) {
|
||||
// 値を狭めた新しいオブジェクトを返す
|
||||
fun restrict(
|
||||
limitFrameRate: Int? = null,
|
||||
limitBitrate: Long? = null,
|
||||
limitSquarePixels: Int? = null,
|
||||
) = MovieResizeConfig(
|
||||
mode = this.mode,
|
||||
limitFrameRate = min(
|
||||
limitFrameRate ?: this.limitFrameRate,
|
||||
this.limitFrameRate
|
||||
),
|
||||
limitBitrate = min(
|
||||
limitBitrate ?: this.limitBitrate,
|
||||
this.limitBitrate
|
||||
),
|
||||
limitSquarePixels = min(
|
||||
limitSquarePixels ?: this.limitSquarePixels,
|
||||
this.limitSquarePixels
|
||||
),
|
||||
)
|
||||
|
||||
// トランスコードをスキップする判定
|
||||
fun isTranscodeRequired(info: VideoInfo) = when (mode) {
|
||||
MovideResizeMode.No -> false
|
||||
MovideResizeMode.Always -> true
|
||||
MovideResizeMode.Auto ->
|
||||
info.squarePixels > limitSquarePixels ||
|
||||
(info.actualBps ?: 0).toFloat() > limitBitrate.toFloat() * 1.5f ||
|
||||
(info.frameRatio?.toInt() ?: 0) > limitFrameRate
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
suspend fun transcodeVideo(
|
||||
info: VideoInfo,
|
||||
inFile: File,
|
||||
outFile: File,
|
||||
resizeConfig: MovieResizeConfig,
|
||||
limitFileSize: Long?,
|
||||
onProgress: (Float) -> Unit,
|
||||
): File = try {
|
||||
withContext(Dispatchers.IO) {
|
||||
val info = inFile.videoInfo
|
||||
|
||||
// サーバに指定された上限ファイルサイズと
|
||||
// 入力動画の時間帳があれば
|
||||
// ビットレート制限を更新できる
|
||||
limitFileSize?.takeIf { it > 0L }?.let { size ->
|
||||
info.duration?.takeIf { it > 0f }?.let { duration ->
|
||||
(size.toFloat() / duration).toLong()
|
||||
}
|
||||
}?.let { limitBps ->
|
||||
resizeConfig.limitBitrate = min(resizeConfig.limitBitrate, limitBps)
|
||||
if (!resizeConfig.isTranscodeRequired(info)) {
|
||||
log.i("transcodeVideo: isTranscodeRequired returns false.")
|
||||
return@withContext inFile
|
||||
}
|
||||
|
||||
when (resizeConfig.mode) {
|
||||
MovieResizeConfig.MODE_NO ->
|
||||
return@withContext inFile
|
||||
MovieResizeConfig.MODE_AUTO -> {
|
||||
if (info.size.w * info.size.h <= resizeConfig.limitPixelMatrix &&
|
||||
MovideResizeMode.No -> return@withContext inFile
|
||||
MovideResizeMode.Always -> Unit
|
||||
MovideResizeMode.Auto -> {
|
||||
if (info.squarePixels <= resizeConfig.limitSquarePixels &&
|
||||
(info.actualBps ?: 0).toFloat() <= resizeConfig.limitBitrate * 1.5f &&
|
||||
(info.frameRatio?.toInt() ?: 0) <= resizeConfig.limitFrameRate
|
||||
) {
|
||||
log.i("transcodeVideo skip.")
|
||||
log.i("transcodeVideo: no need to transcode.")
|
||||
return@withContext inFile
|
||||
}
|
||||
}
|
||||
|
@ -107,16 +118,26 @@ suspend fun transcodeVideo(
|
|||
// ワークアラウンドとしてファイルではなくfdを渡す
|
||||
val future = Transcoder.into(outFile.canonicalPath)
|
||||
.addDataSource(inStream.fd)
|
||||
.setVideoTrackStrategy(
|
||||
DefaultVideoStrategy.Builder()
|
||||
.addResizer(
|
||||
AtMostSquarePixelResizer(resizeConfig.limitPixelMatrix)
|
||||
)
|
||||
.frameRate(resizeConfig.limitFrameRate)
|
||||
.keyFrameInterval(10f)
|
||||
.bitRate(resizeConfig.limitBitrate)
|
||||
.build()
|
||||
)
|
||||
.setVideoTrackStrategy(DefaultVideoStrategy.Builder()
|
||||
.addResizer { inSize ->
|
||||
val squarePixels = inSize.major * inSize.minor
|
||||
val limit = resizeConfig.limitSquarePixels
|
||||
if (squarePixels <= limit || inSize.major <= 0 || inSize.minor <= 0) {
|
||||
// 入力サイズが0以下の場合もアスペクト計算に支障がでるのでリサイズできない
|
||||
inSize
|
||||
} else {
|
||||
// アスペクト比を維持しつつ平方ピクセルが指定に収まるようにする
|
||||
val aspect = inSize.major.toFloat() / inSize.minor.toFloat()
|
||||
Size(
|
||||
max(1, (sqrt(limit.toFloat() * aspect) + 0.5f).toInt()),
|
||||
max(1, (sqrt(limit.toFloat() / aspect) + 0.5f).toInt()),
|
||||
)
|
||||
}
|
||||
}
|
||||
.frameRate(resizeConfig.limitFrameRate)
|
||||
.keyFrameInterval(10f)
|
||||
.bitRate(resizeConfig.limitBitrate)
|
||||
.build())
|
||||
.setAudioTrackStrategy(
|
||||
DefaultAudioStrategy.Builder()
|
||||
.channels(2)
|
||||
|
|
|
@ -5,6 +5,7 @@ import android.media.MediaFormat
|
|||
import android.media.MediaMetadataRetriever
|
||||
import android.os.Build
|
||||
import java.io.File
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
/**
|
||||
|
@ -126,6 +127,8 @@ class VideoInfo(
|
|||
mmr.int(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT) ?: 0,
|
||||
)
|
||||
|
||||
val squarePixels: Int get() = max(1, size.w) * max(1, size.h)
|
||||
|
||||
val bitrate = mmr.int(MediaMetadataRetriever.METADATA_KEY_BITRATE)
|
||||
|
||||
val duration = mmr.long(MediaMetadataRetriever.METADATA_KEY_DURATION)
|
||||
|
|
|
@ -23,10 +23,10 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingStart="12dp"
|
||||
android:paddingTop="12dp"
|
||||
android:paddingBottom="320dp"
|
||||
android:paddingEnd="12dp"
|
||||
android:paddingBottom="320dp">
|
||||
android:paddingStart="12dp"
|
||||
android:paddingTop="12dp">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/llReply"
|
||||
|
@ -101,15 +101,15 @@
|
|||
android:layout_weight="1"
|
||||
android:background="@drawable/btn_bg_transparent_round6dp"
|
||||
android:gravity="center_vertical"
|
||||
android:paddingStart="8dp"
|
||||
android:paddingEnd="8dp"
|
||||
android:paddingStart="8dp"
|
||||
android:textAllCaps="false" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/llAttachment"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="48dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:baselineAligned="false"
|
||||
android:gravity="top|start"
|
||||
|
@ -121,7 +121,8 @@
|
|||
android:layout_height="48dp"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:background="@drawable/btn_bg_transparent_round6dp"
|
||||
android:scaleType="fitCenter" />
|
||||
android:scaleType="fitCenter"
|
||||
tools:src="@drawable/ic_videocam" />
|
||||
|
||||
<jp.juggler.subwaytooter.view.MyNetworkImageView
|
||||
android:id="@+id/ivMedia2"
|
||||
|
@ -129,7 +130,8 @@
|
|||
android:layout_height="48dp"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:background="@drawable/btn_bg_transparent_round6dp"
|
||||
android:scaleType="fitCenter" />
|
||||
android:scaleType="fitCenter"
|
||||
tools:src="@drawable/ic_videocam" />
|
||||
|
||||
<jp.juggler.subwaytooter.view.MyNetworkImageView
|
||||
android:id="@+id/ivMedia3"
|
||||
|
@ -137,7 +139,8 @@
|
|||
android:layout_height="48dp"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:background="@drawable/btn_bg_transparent_round6dp"
|
||||
android:scaleType="fitCenter" />
|
||||
android:scaleType="fitCenter"
|
||||
tools:src="@drawable/ic_videocam" />
|
||||
|
||||
<jp.juggler.subwaytooter.view.MyNetworkImageView
|
||||
android:id="@+id/ivMedia4"
|
||||
|
@ -145,17 +148,18 @@
|
|||
android:layout_height="48dp"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:background="@drawable/btn_bg_transparent_round6dp"
|
||||
android:scaleType="fitCenter" />
|
||||
android:scaleType="fitCenter"
|
||||
tools:src="@drawable/ic_videocam" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/tvAttachmentProgress"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:textSize="11sp"
|
||||
tools:text="アップロード中です"
|
||||
/>
|
||||
android:visibility="gone"
|
||||
tools:text="アップロード中です\nアップロード中です\nアップロード中です\nアップロード中です\nアップロード中です"
|
||||
tools:visibility="visible" />
|
||||
</LinearLayout>
|
||||
|
||||
<com.google.android.flexbox.FlexboxLayout
|
||||
|
@ -433,8 +437,8 @@
|
|||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:layout_marginStart="4dp"
|
||||
android:text="@string/plus" />
|
||||
|
||||
<EditText
|
||||
|
@ -454,8 +458,8 @@
|
|||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:layout_marginStart="4dp"
|
||||
android:text="@string/plus" />
|
||||
|
||||
<EditText
|
||||
|
@ -503,8 +507,8 @@
|
|||
android:layout_marginStart="4dp"
|
||||
android:background="@drawable/btn_bg_transparent_round6dp"
|
||||
android:contentDescription="@string/visibility"
|
||||
android:minWidth="48dp"
|
||||
android:minHeight="48dp"
|
||||
android:minWidth="48dp"
|
||||
app:tint="?attr/colorVectorDrawable"
|
||||
tools:src="@drawable/ic_public" />
|
||||
|
||||
|
@ -526,9 +530,7 @@
|
|||
android:background="@drawable/btn_bg_transparent_round6dp"
|
||||
android:contentDescription="@string/more"
|
||||
android:src="@drawable/ic_more"
|
||||
app:tint="?attr/colorVectorDrawable"
|
||||
|
||||
/>
|
||||
app:tint="?attr/colorVectorDrawable" />
|
||||
|
||||
<View
|
||||
android:layout_width="0dp"
|
||||
|
@ -555,6 +557,6 @@
|
|||
android:src="@drawable/ic_send"
|
||||
app:tint="?attr/colorVectorDrawable"
|
||||
|
||||
/>
|
||||
tools:ignore="DuplicateSpeakableTextCheck" />
|
||||
</LinearLayout>
|
||||
</jp.juggler.subwaytooter.actpost.ActPostRootLinearLayout>
|
||||
|
|
|
@ -1113,7 +1113,7 @@
|
|||
<string name="attachment_handling_compress_ratio">圧縮中 %1$d%%…</string>
|
||||
<string name="attachment_handling_uploading">アップロード中 %1$d%%…</string>
|
||||
<string name="attachment_handling_waiting">応答待ち…</string>
|
||||
<string name="attachment_handling_waiting2">応答待ち(非同期)…</string>
|
||||
<string name="attachment_handling_waiting_async">応答待ち(非同期)…</string>
|
||||
<string name="option_deprecated_mastodon342">(Mastodon 3.4.2以降では指定した値ではなく、サーバから提供される情報が使われます)</string>
|
||||
<string name="movie_transcode">動画の再圧縮</string>
|
||||
<string name="movie_transcode_mode">モード</string>
|
||||
|
|
|
@ -1124,7 +1124,7 @@
|
|||
<string name="attachment_handling_compress_ratio">Compressing %1$d%%…</string>
|
||||
<string name="attachment_handling_uploading">Uploading %1$d%%…</string>
|
||||
<string name="attachment_handling_waiting">Waiting response…</string>
|
||||
<string name="attachment_handling_waiting2">Waiting response(asynchronized)…</string>
|
||||
<string name="attachment_handling_waiting_async">Waiting response(asynchronized)…</string>
|
||||
<string name="option_deprecated_mastodon342">(This option is deprecated for Mastodon 3.4.2+. App uses information from the server.)</string>
|
||||
<string name="movie_transcode">Movie transcoding</string>
|
||||
<string name="movie_transcode_mode">Mode</string>
|
||||
|
|
Loading…
Reference in New Issue