- add transcodeVideoMedia3Transformer that use JetPack Media3 Transformer
- use transcodeVideoMedia3Transformer (does not support frame ratio conversion) instead of transcodeVideo(depends on LiTr) - createResizedVideoOpener add arbument VideoInfo.
This commit is contained in:
parent
a67b177c9d
commit
5229215661
|
@ -7,7 +7,7 @@
|
|||
<option name="testRunner" value="GRADLE" />
|
||||
<option name="distributionType" value="DEFAULT_WRAPPED" />
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="gradleJvm" value="Android Studio java home" />
|
||||
<option name="gradleJvm" value="#JAVA_HOME" />
|
||||
<option name="modules">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
|
|
|
@ -18,9 +18,10 @@ import jp.juggler.util.data.notEmpty
|
|||
import jp.juggler.util.log.LogCategory
|
||||
import jp.juggler.util.log.errorEx
|
||||
import jp.juggler.util.media.ResizeConfig
|
||||
import jp.juggler.util.media.VideoInfo
|
||||
import jp.juggler.util.media.VideoInfo.Companion.videoInfo
|
||||
import jp.juggler.util.media.createResizedBitmap
|
||||
import jp.juggler.util.media.transcodeVideo
|
||||
import jp.juggler.util.media.transcodeVideoMedia3Transformer
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.util.concurrent.CancellationException
|
||||
|
@ -134,7 +135,7 @@ class AttachmentRequest(
|
|||
when (isVideo) {
|
||||
true -> try {
|
||||
// 動画のトランスコード(失敗したらオリジナルデータにフォールバックする)
|
||||
return createResizedVideoOpener()
|
||||
return createResizedVideoOpener(vi)
|
||||
} catch (ex: Throwable) {
|
||||
log.w(
|
||||
ex,
|
||||
|
@ -304,7 +305,7 @@ class AttachmentRequest(
|
|||
return false
|
||||
}
|
||||
|
||||
private suspend fun createResizedVideoOpener(): InputStreamOpener {
|
||||
private suspend fun createResizedVideoOpener(srcInfo: VideoInfo): InputStreamOpener {
|
||||
|
||||
val cacheDir = context.externalCacheDir
|
||||
?.apply { mkdirs() }
|
||||
|
@ -324,10 +325,9 @@ class AttachmentRequest(
|
|||
val mediaConfig = mediaConfig()
|
||||
|
||||
// 動画のメタデータを調べる
|
||||
val info = tempFile.videoInfo
|
||||
|
||||
// サーバに指定されたファイルサイズ上限と入力動画の時間長があれば、ビットレート上限を制限する
|
||||
val duration = info.duration?.takeIf { it >= 0.1f }
|
||||
val duration = srcInfo.duration?.takeIf { it >= 0.1f }
|
||||
val limitFileSize = mediaConfig?.float("video_size_limit")?.takeIf { it >= 1f }
|
||||
val limitBitrate = when {
|
||||
duration != null && limitFileSize != null ->
|
||||
|
@ -346,12 +346,22 @@ class AttachmentRequest(
|
|||
limitSquarePixels = mediaConfig?.int("video_matrix_limit")
|
||||
?.takeIf { it > 1 },
|
||||
)
|
||||
|
||||
val result = transcodeVideo(
|
||||
info,
|
||||
tempFile,
|
||||
outFile,
|
||||
movieResizeConfig,
|
||||
// val result = transcodeVideo(
|
||||
// srcInfo,
|
||||
// tempFile,
|
||||
// outFile,
|
||||
// movieResizeConfig,
|
||||
// ) {
|
||||
// val percent = (it * 100f).toInt()
|
||||
// pa.progress =
|
||||
// context.getString(R.string.attachment_handling_compress_ratio, percent)
|
||||
// }
|
||||
val result = transcodeVideoMedia3Transformer(
|
||||
context = context,
|
||||
info = srcInfo,
|
||||
inFile = tempFile,
|
||||
outFile = outFile,
|
||||
resizeConfig = movieResizeConfig,
|
||||
) {
|
||||
val percent = (it * 100f).toInt()
|
||||
pa.progress =
|
||||
|
|
|
@ -1,18 +1,38 @@
|
|||
package jp.juggler.util.media
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.MimeTypes
|
||||
import androidx.media3.effect.ScaleAndRotateTransformation
|
||||
import androidx.media3.transformer.Composition
|
||||
import androidx.media3.transformer.DefaultEncoderFactory
|
||||
import androidx.media3.transformer.EditedMediaItem
|
||||
import androidx.media3.transformer.Effects
|
||||
import androidx.media3.transformer.ExportException
|
||||
import androidx.media3.transformer.ExportResult
|
||||
import androidx.media3.transformer.ProgressHolder
|
||||
import androidx.media3.transformer.TransformationRequest
|
||||
import androidx.media3.transformer.Transformer
|
||||
import androidx.media3.transformer.VideoEncoderSettings
|
||||
import com.otaliastudios.transcoder.Transcoder
|
||||
import com.otaliastudios.transcoder.TranscoderListener
|
||||
import com.otaliastudios.transcoder.common.Size
|
||||
import com.otaliastudios.transcoder.strategy.DefaultAudioStrategy
|
||||
import com.otaliastudios.transcoder.strategy.DefaultVideoStrategy
|
||||
import jp.juggler.util.coroutine.AppDispatchers
|
||||
import jp.juggler.util.data.clip
|
||||
import jp.juggler.util.data.notZero
|
||||
import jp.juggler.util.log.LogCategory
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.channels.ClosedReceiveChannelException
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
import kotlin.coroutines.resumeWithException
|
||||
import kotlin.math.ceil
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
import kotlin.math.sqrt
|
||||
|
@ -64,7 +84,7 @@ data class MovieResizeConfig(
|
|||
MovideResizeMode.Auto ->
|
||||
info.squarePixels > limitSquarePixels ||
|
||||
(info.actualBps ?: 0).toFloat() > limitBitrate.toFloat() * 1.5f ||
|
||||
(info.frameRatio?.toInt() ?: 0) > limitFrameRate
|
||||
(info.frameRatio==null || info.frameRatio<1f || info.frameRatio > limitFrameRate)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -98,6 +118,138 @@ private fun createScaledSize(inSize: Size, limitSquarePixels: Int): Size {
|
|||
)
|
||||
}
|
||||
|
||||
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
|
||||
suspend fun transcodeVideoMedia3Transformer(
|
||||
context: Context,
|
||||
info: VideoInfo,
|
||||
inFile: File,
|
||||
outFile: File,
|
||||
resizeConfig: MovieResizeConfig,
|
||||
onProgress: (Float) -> Unit,
|
||||
): File = try {
|
||||
withContext(AppDispatchers.MainImmediate) {
|
||||
|
||||
when (resizeConfig.mode) {
|
||||
MovideResizeMode.No -> return@withContext inFile
|
||||
MovideResizeMode.Always -> Unit
|
||||
MovideResizeMode.Auto -> {
|
||||
if (!resizeConfig.isTranscodeRequired(info)) {
|
||||
log.i("transcodeVideoMedia3Transformer: transcode not required.")
|
||||
return@withContext inFile
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val srcMediaItem = MediaItem.fromUri(Uri.fromFile(inFile))
|
||||
val editedMediaItem = EditedMediaItem.Builder(srcMediaItem).apply {
|
||||
|
||||
// 入力のフレームレートが高すぎるなら制限する
|
||||
if (info.frameRatio==null || info.frameRatio<1f ||
|
||||
info.frameRatio > resizeConfig.limitFrameRate
|
||||
) {
|
||||
// This should be set for inputs that don't have an implicit frame rate (e.g. images).
|
||||
// It will be ignored for inputs that do have an implicit frame rate (e.g. video).
|
||||
setFrameRate(resizeConfig.limitFrameRate)
|
||||
}
|
||||
|
||||
// 入力のピクセルサイズが大きすぎるなら制限する
|
||||
if (info.size.w > 0 && info.size.h > 0 &&
|
||||
info.squarePixels > resizeConfig.limitSquarePixels
|
||||
) {
|
||||
// 端数やodd補正などによる問題が出なさそうなscale値を計算する
|
||||
fun calcScale(
|
||||
srcLongerSide:Int,
|
||||
aspect:Float,
|
||||
limitSqPixel:Int,
|
||||
):Float {
|
||||
var sqPixel = limitSqPixel
|
||||
while (true) {
|
||||
val newW = ceil(sqrt(sqPixel * aspect)).toInt().fixOdd()
|
||||
val newH = ceil(sqrt(sqPixel / aspect)).toInt().fixOdd()
|
||||
if (newW * newH <= resizeConfig.limitSquarePixels) {
|
||||
return max(newW, newH).toFloat().div(srcLongerSide)
|
||||
}
|
||||
sqPixel -= srcLongerSide
|
||||
}
|
||||
}
|
||||
val scale = calcScale(
|
||||
srcLongerSide = max(info.size.w, info.size.h),
|
||||
aspect = info.size.w.toFloat() / info.size.h.toFloat(),
|
||||
limitSqPixel = resizeConfig.limitSquarePixels
|
||||
)
|
||||
val effects = Effects(
|
||||
/* audioProcessors */ emptyList(),
|
||||
/* videoEffects */ listOf(
|
||||
ScaleAndRotateTransformation.Builder().apply {
|
||||
setScale(scale, scale)
|
||||
}.build()
|
||||
)
|
||||
)
|
||||
setEffects(effects)
|
||||
}
|
||||
}.build()
|
||||
|
||||
val request = TransformationRequest.Builder().apply {
|
||||
setVideoMimeType(MimeTypes.VIDEO_H264)
|
||||
setAudioMimeType(MimeTypes.AUDIO_AAC)
|
||||
// ビットレートがないな…
|
||||
}.build()
|
||||
|
||||
// 完了検知
|
||||
val completed = AtomicBoolean(false)
|
||||
val error = AtomicReference<Throwable>(null)
|
||||
val listener = object : Transformer.Listener {
|
||||
override fun onCompleted(composition: Composition, exportResult: ExportResult) {
|
||||
super.onCompleted(composition, exportResult)
|
||||
completed.compareAndSet(false, true)
|
||||
}
|
||||
|
||||
override fun onError(
|
||||
composition: Composition,
|
||||
exportResult: ExportResult,
|
||||
exportException: ExportException,
|
||||
) {
|
||||
super.onError(composition, exportResult, exportException)
|
||||
error.compareAndSet(null, exportException)
|
||||
}
|
||||
}
|
||||
|
||||
val videoEncoderSettings = VideoEncoderSettings.Builder().apply {
|
||||
setBitrate(resizeConfig.limitBitrate.clip(100_000L, Int.MAX_VALUE.toLong()).toInt())
|
||||
}.build()
|
||||
|
||||
val encoderFactory = DefaultEncoderFactory.Builder(context).apply {
|
||||
setRequestedVideoEncoderSettings(videoEncoderSettings)
|
||||
// missing setRequestedAudioEncoderSettings
|
||||
}.build()
|
||||
|
||||
// 開始
|
||||
val transformer = Transformer.Builder(context).apply {
|
||||
setEncoderFactory(encoderFactory)
|
||||
setTransformationRequest(request)
|
||||
addListener(listener)
|
||||
}.build()
|
||||
transformer.start(editedMediaItem, outFile.canonicalPath)
|
||||
|
||||
// 完了まで待機しつつ、定期的に進捗コールバックを呼ぶ
|
||||
val progressHolder = ProgressHolder()
|
||||
while (!completed.get()) {
|
||||
error.get()?.let { throw it }
|
||||
val progress = when (transformer.getProgress(progressHolder)) {
|
||||
Transformer.PROGRESS_STATE_NOT_STARTED -> 0f
|
||||
else -> progressHolder.progress.toFloat() / 100f
|
||||
}
|
||||
onProgress(progress)
|
||||
delay(1000L)
|
||||
}
|
||||
outFile
|
||||
}
|
||||
} catch (ex: Throwable) {
|
||||
log.w("delete outFile due to error.")
|
||||
outFile.delete()
|
||||
throw ex
|
||||
}
|
||||
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
suspend fun transcodeVideo(
|
||||
info: VideoInfo,
|
||||
|
|
Loading…
Reference in New Issue