- 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:
tateisu 2023-08-06 10:46:33 +09:00
parent a67b177c9d
commit 5229215661
3 changed files with 175 additions and 13 deletions

View File

@ -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$" />

View File

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

View File

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