Reputation: 3268
I want to compress videos for sharing in my Android app. These are my preferred settings:
Resolution: 1280x720
Container: MP4
Video codec: H.264
Video bitrate: 3 Mbit/s
Video FPS: 30
Audio codec: AAC
Audio sampling rate: 44100
Audio bitrate: 128 kbit/s
MoveMoov atom
to the start to enable streaming (optional)
I do not want to use FFmpeg as it uses software encoders and is very slow. I am getting about 0.7x encoding speed when converting an H.265, 1080p, 120 fps, 40 Mbit/s video to the above settings. Also I want to avoid the hassle of managing an NDK library.
So I eventually settled on Jetpack Media3 Transformer for my use case. And I have this code working perfectly:
val transformationRequest = TransformationRequest.Builder()
.setVideoMimeType(MimeTypes.VIDEO_H264)
.setAudioMimeType(MimeTypes.AUDIO_AAC)
.setResolution(1280)
.build()
val inputMediaItem = MediaItem.fromUri(uri)
val outputFile = MediaUtils.createTempFile(c,MediaType.VIDEO)
// Create a Transformer
val transformer = Transformer.Builder(c)
.setTransformationRequest(transformationRequest) // Pass in TransformationRequest
.addListener(object: Transformer.Listener {
override fun onTransformationCompleted(inputMediaItem: MediaItem, transformationResult: TransformationResult) {
super.onTransformationCompleted(inputMediaItem, transformationResult)
Log.d("ENCODE", "Encoding Complete")
Log.d("ENCODE", "Output Size: ${MediaUtils.getFileSize(outputFile)}")
}
}) // transformerListener is an implementation of Transformer.Listener
.build()
withContext(Dispatchers.Main) {
// Start the transformation
transformer.startTransformation(inputMediaItem, outputFile.absolutePath)
}
This encode completes at 4-5x speed. But I don’t see a way to set anything except the codecs and resolution. The Transformer Documentation does say this:
video encoding works with default settings, but you can also pass custom video encoder settings or replace the encoder factory to get complete control over how encoders are used
But I cannot find any documentation on how to set the video fps and video/audio bitrate.
Upvotes: 4
Views: 1385
Reputation: 176
In Transformer you can use DefaultEncoderFactory
to set your desired bitrate:
transformerBuilder.setEncoderFactory(
new DefaultEncoderFactory.Builder( /* context= */ this)
.setRequestedVideoEncoderSettings(
new VideoEncoderSettings.Builder()
.setBitrate(BITRATE_VALUE)
.build())
.build());
Upvotes: 1
Reputation: 92
// kotlin
@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 > resizeConfig.limitFrameRate) {
setFrameRate(resizeConfig.limitFrameRate)
}
// 入力のピクセルサイズが大きすぎるなら制限する
if (info.size.w > 0 && info.size.h > 0 &&
info.squarePixels > resizeConfig.limitSquarePixels
) {
val aspect = info.size.w.toFloat() / info.size.h.toFloat()
val newW = sqrt(resizeConfig.limitSquarePixels * aspect)
val newH = sqrt(resizeConfig.limitSquarePixels / aspect)
val scale = max(newW, newH) / max(1, max(info.size.w, info.size.h))
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
}
Upvotes: 0