SubwayTooter-Android-App/base/src/main/java/jp/juggler/util/media/VideoInfo.kt

201 lines
7.5 KiB
Kotlin

package jp.juggler.util.media
import android.content.Context
import android.media.MediaCodecList
import android.media.MediaFormat
import android.media.MediaMetadataRetriever
import android.net.Uri
import android.os.Build
import jp.juggler.util.log.LogCategory
import java.io.File
import kotlin.math.max
import kotlin.math.min
/**
* 動画の情報
*/
@Suppress("MemberVisibilityCanBePrivate")
class VideoInfo(
// val file: File,
mmr: MediaMetadataRetriever,
val bytesLength: Long,
val uri: Uri,
) {
companion object {
private val log = LogCategory("VideoInfo")
val File.videoInfo: VideoInfo
get() = MediaMetadataRetriever().use { mmr ->
mmr.setDataSource(canonicalPath)
VideoInfo(mmr, length(), Uri.fromFile(canonicalFile))
}
fun Uri.videoInfo(context: Context, length: Long): VideoInfo =
MediaMetadataRetriever().use { mmr ->
mmr.setDataSource(context, this)
VideoInfo(mmr, length, this)
}
private fun MediaMetadataRetriever.string(key: Int) =
extractMetadata(key)
private fun MediaMetadataRetriever.int(key: Int) =
string(key)?.toIntOrNull()
private fun MediaMetadataRetriever.long(key: Int) =
string(key)?.toLongOrNull()
/**
* 調査のためコーデックを列挙して情報をログに出す
*/
fun dumpCodec() {
val mcl = MediaCodecList(MediaCodecList.REGULAR_CODECS)
for (info in mcl.codecInfos) {
try {
if (!info.isEncoder) continue
val caps = try {
info.getCapabilitiesForType(MediaFormat.MIMETYPE_VIDEO_AVC) ?: continue
} catch (ex: Throwable) {
log.w(ex, "getCapabilitiesForType failed.")
continue
}
for (colorFormat in caps.colorFormats) {
log.i("${info.name} color 0x${colorFormat.toString(16)}")
// OMX.qcom.video.encoder.avc color 7fa30c04 不明
// OMX.qcom.video.encoder.avc color 7f000789 COLOR_FormatSurface
// OMX.qcom.video.encoder.avc color 7f420888 COLOR_FormatYUV420Flexible
// OMX.qcom.video.encoder.avc color 15 COLOR_Format32bitBGRA8888
}
caps.videoCapabilities.bitrateRange?.let { range ->
log.i("bitrateRange $range")
}
caps.videoCapabilities.supportedFrameRates?.let { range ->
log.i("supportedFrameRates $range")
}
if (Build.VERSION.SDK_INT >= 28) {
caps.encoderCapabilities.qualityRange?.let { range ->
log.i("qualityRange $range")
}
}
} catch (ex: Throwable) {
log.w(ex, "dumpCodec failed.")
// type is not supported
}
}
}
}
data class Size(var w: Int, var h: Int) {
override fun toString() = "[$w,$h]"
private val aspect: Float get() = w.toFloat() / h.toFloat()
/**
* アスペクト比を維持しつつ上限に合わせた解像度を提案する
* - 拡大はしない
*/
fun scaleTo(limitLonger: Int, limitShorter: Int): Size {
val inSize = this
// ゼロ除算対策
if (inSize.w < 1 || inSize.h < 1) {
return Size(limitLonger, limitShorter)
}
val inAspect = inSize.aspect
// 入力の縦横に合わせて上限を決める
val outSize = if (inAspect >= 1f) {
Size(limitLonger, limitShorter)
} else {
Size(limitShorter, limitLonger)
}
// 縦横比を比較する
return if (inAspect >= outSize.aspect) {
// 入力のほうが横長なら横幅基準でスケーリングする
// 拡大はしない
val scale = outSize.w.toFloat() / inSize.w.toFloat()
if (scale >= 1f) inSize else outSize.apply {
h = min(h, (scale * inSize.h + 0.5f).toInt())
}
} else {
// 入力のほうが縦長なら縦幅基準でスケーリングする
// 拡大はしない
val scale = outSize.h.toFloat() / inSize.h.toFloat()
if (scale >= 1f) inSize else outSize.apply {
w = min(w, (scale * inSize.w + 0.5f).toInt())
}
}
}
}
val mimeType = mmr.string(MediaMetadataRetriever.METADATA_KEY_MIMETYPE)
val rotation = mmr.int(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION) ?: 0
val size = Size(
mmr.int(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH) ?: 0,
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)
?.toFloat()?.div(1000)?.takeIf { it > 0.1f }
val frameCount = if (Build.VERSION.SDK_INT >= 28) {
mmr.int(MediaMetadataRetriever.METADATA_KEY_VIDEO_FRAME_COUNT)?.takeIf { it > 0 }
} else {
null
}
val frameRatio = if (frameCount != null && duration != null) {
frameCount.toFloat().div(duration)
} else {
null
}
val audioSampleRate = if (Build.VERSION.SDK_INT >= 31) {
mmr.int(MediaMetadataRetriever.METADATA_KEY_SAMPLERATE)?.takeIf { it > 0 }
} else {
null
}
val hasAudio = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_HAS_AUDIO)
?.let { it == "yes" }
val hasVideo = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_HAS_VIDEO)
?.let { it == "yes" }
val actualBps by lazy {
// ファイルサイズを取得できないならエラー
if (bytesLength <= 0L) return@lazy null
// 時間帳が短すぎるなら算出できない
if (duration == null || duration < 0.1f) return@lazy null
// bpsを計算
bytesLength.toFloat().div(duration).times(8).toInt()
}
/**
* 動画のファイルサイズが十分に小さいなら真
*/
fun isSmallEnough(limitBps: Int): Boolean {
// ファイルサイズを取得できないならエラー
if (bytesLength <= 0L) error("too small file. $uri")
// ファイルサイズが500KB以内ならビットレートを気にしない
if (bytesLength < 500_000) return true
// ファイルサイズからビットレートを計算できなかったなら再エンコード必要
val actualBps = this.actualBps ?: return false
// bpsを計算
log.i("isSmallEnough duration=$duration, bps=$actualBps/$limitBps")
return actualBps <= limitBps
}
override fun toString() =
"rotation=$rotation, size=$size, frameRatio=$frameRatio, bitrate=${actualBps ?: bitrate}, audioSampleRate=$audioSampleRate, mimeType=$mimeType, uri=$uri"
}