礼物mp4
This commit is contained in:
parent
b4c45e6400
commit
9aa22dcdd8
@ -237,6 +237,7 @@ class GiftMp4VideoPlatformView(
|
|||||||
private data class HalfStats(
|
private data class HalfStats(
|
||||||
val saturation: Double,
|
val saturation: Double,
|
||||||
val grayError: Double,
|
val grayError: Double,
|
||||||
|
val brightness: Double,
|
||||||
)
|
)
|
||||||
|
|
||||||
private enum class SplitOrientation { HORIZONTAL, VERTICAL }
|
private enum class SplitOrientation { HORIZONTAL, VERTICAL }
|
||||||
@ -265,6 +266,11 @@ class GiftMp4VideoPlatformView(
|
|||||||
const val VIEW_TYPE = "gift_mp4_video_view"
|
const val VIEW_TYPE = "gift_mp4_video_view"
|
||||||
const val CHANNEL_NAME = "com.org.yumiparty/gift_mp4_events"
|
const val CHANNEL_NAME = "com.org.yumiparty/gift_mp4_events"
|
||||||
const val CONTROL_CHANNEL_NAME = "com.org.yumiparty/gift_mp4_control"
|
const val CONTROL_CHANNEL_NAME = "com.org.yumiparty/gift_mp4_control"
|
||||||
|
private const val MIN_SPLIT_MATCH_FRAMES = 3
|
||||||
|
private const val MIN_USEFUL_FRAME_BRIGHTNESS = 0.035
|
||||||
|
private const val MIN_SPLIT_SATURATION_GAP = 0.08
|
||||||
|
private const val MAX_SPLIT_MASK_SATURATION = 0.28
|
||||||
|
private const val MAX_SPLIT_MASK_GRAY_ERROR = 0.06
|
||||||
|
|
||||||
private val registry = mutableMapOf<Int, GiftMp4VideoPlatformView>()
|
private val registry = mutableMapOf<Int, GiftMp4VideoPlatformView>()
|
||||||
private val layoutCache = mutableMapOf<String, SourceLayout>()
|
private val layoutCache = mutableMapOf<String, SourceLayout>()
|
||||||
@ -512,10 +518,7 @@ class GiftMp4VideoPlatformView(
|
|||||||
.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)
|
.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)
|
||||||
?.toLongOrNull()
|
?.toLongOrNull()
|
||||||
?: 0L
|
?: 0L
|
||||||
val timesUs =
|
val timesUs = splitSampleTimesUs(durationMs)
|
||||||
listOf(0L, 500_000L, 1_000_000L, 2_000_000L)
|
|
||||||
.filter { durationMs == 0L || it <= durationMs * 1000L }
|
|
||||||
.ifEmpty { listOf(0L) }
|
|
||||||
|
|
||||||
val horizontalScore = SplitScore()
|
val horizontalScore = SplitScore()
|
||||||
val verticalScore = SplitScore()
|
val verticalScore = SplitScore()
|
||||||
@ -523,13 +526,16 @@ class GiftMp4VideoPlatformView(
|
|||||||
val frame =
|
val frame =
|
||||||
retriever.getFrameAtTime(
|
retriever.getFrameAtTime(
|
||||||
timeUs,
|
timeUs,
|
||||||
MediaMetadataRetriever.OPTION_CLOSEST_SYNC,
|
MediaMetadataRetriever.OPTION_CLOSEST,
|
||||||
) ?: continue
|
) ?: continue
|
||||||
val horizontalStats = statsByHorizontalHalves(frame)
|
try {
|
||||||
val verticalStats = statsByVerticalHalves(frame)
|
val horizontalStats = statsByHorizontalHalves(frame)
|
||||||
frame.recycle()
|
val verticalStats = statsByVerticalHalves(frame)
|
||||||
addSplitScore(horizontalScore, horizontalStats.first, horizontalStats.second)
|
addSplitScore(horizontalScore, horizontalStats.first, horizontalStats.second)
|
||||||
addSplitScore(verticalScore, verticalStats.first, verticalStats.second)
|
addSplitScore(verticalScore, verticalStats.first, verticalStats.second)
|
||||||
|
} finally {
|
||||||
|
frame.recycle()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
val horizontalCandidate =
|
val horizontalCandidate =
|
||||||
splitCandidateFromScore(SplitOrientation.HORIZONTAL, horizontalScore)
|
splitCandidateFromScore(SplitOrientation.HORIZONTAL, horizontalScore)
|
||||||
@ -545,6 +551,32 @@ class GiftMp4VideoPlatformView(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun splitSampleTimesUs(durationMs: Long): List<Long> {
|
||||||
|
val durationUs = durationMs * 1000L
|
||||||
|
val desiredTimesUs =
|
||||||
|
listOf(
|
||||||
|
300_000L,
|
||||||
|
600_000L,
|
||||||
|
900_000L,
|
||||||
|
1_200_000L,
|
||||||
|
1_500_000L,
|
||||||
|
1_800_000L,
|
||||||
|
2_100_000L,
|
||||||
|
2_400_000L,
|
||||||
|
2_700_000L,
|
||||||
|
3_000_000L,
|
||||||
|
)
|
||||||
|
return desiredTimesUs
|
||||||
|
.filter { durationMs <= 0L || it <= durationUs }
|
||||||
|
.ifEmpty {
|
||||||
|
if (durationUs > 0L) {
|
||||||
|
listOf(durationUs / 2L)
|
||||||
|
} else {
|
||||||
|
listOf(0L)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun detectPackedAlphaTopRightSourceLayout(
|
private fun detectPackedAlphaTopRightSourceLayout(
|
||||||
source: GiftMp4Source,
|
source: GiftMp4Source,
|
||||||
): SourceLayout? {
|
): SourceLayout? {
|
||||||
@ -732,25 +764,40 @@ class GiftMp4VideoPlatformView(
|
|||||||
first: HalfStats,
|
first: HalfStats,
|
||||||
second: HalfStats,
|
second: HalfStats,
|
||||||
) {
|
) {
|
||||||
|
if (isLowBrightnessFrame(first, second)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
val alphaStats = if (first.saturation < second.saturation) first else second
|
val alphaStats = if (first.saturation < second.saturation) first else second
|
||||||
|
val saturationGap = kotlin.math.abs(second.saturation - first.saturation)
|
||||||
|
if (saturationGap < MIN_SPLIT_SATURATION_GAP ||
|
||||||
|
alphaStats.saturation > MAX_SPLIT_MASK_SATURATION ||
|
||||||
|
alphaStats.grayError > MAX_SPLIT_MASK_GRAY_ERROR
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
score.direction += second.saturation - first.saturation
|
score.direction += second.saturation - first.saturation
|
||||||
score.saturationGap += kotlin.math.abs(second.saturation - first.saturation)
|
score.saturationGap += saturationGap
|
||||||
score.maskSaturation += alphaStats.saturation
|
score.maskSaturation += alphaStats.saturation
|
||||||
score.maskGrayError += alphaStats.grayError
|
score.maskGrayError += alphaStats.grayError
|
||||||
score.frames++
|
score.frames++
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun isLowBrightnessFrame(
|
||||||
|
first: HalfStats,
|
||||||
|
second: HalfStats,
|
||||||
|
): Boolean = maxOf(first.brightness, second.brightness) < MIN_USEFUL_FRAME_BRIGHTNESS
|
||||||
|
|
||||||
private fun splitCandidateFromScore(
|
private fun splitCandidateFromScore(
|
||||||
orientation: SplitOrientation,
|
orientation: SplitOrientation,
|
||||||
score: SplitScore,
|
score: SplitScore,
|
||||||
): Pair<SplitAlphaCandidate, Double>? {
|
): Pair<SplitAlphaCandidate, Double>? {
|
||||||
if (score.frames == 0) return null
|
if (score.frames < MIN_SPLIT_MATCH_FRAMES) return null
|
||||||
val averageSaturationGap = score.saturationGap / score.frames
|
val averageSaturationGap = score.saturationGap / score.frames
|
||||||
val averageMaskSaturation = score.maskSaturation / score.frames
|
val averageMaskSaturation = score.maskSaturation / score.frames
|
||||||
val averageMaskGrayError = score.maskGrayError / score.frames
|
val averageMaskGrayError = score.maskGrayError / score.frames
|
||||||
if (averageSaturationGap < 0.08 ||
|
if (averageSaturationGap < MIN_SPLIT_SATURATION_GAP ||
|
||||||
averageMaskSaturation > 0.28 ||
|
averageMaskSaturation > MAX_SPLIT_MASK_SATURATION ||
|
||||||
averageMaskGrayError > 0.06
|
averageMaskGrayError > MAX_SPLIT_MASK_GRAY_ERROR
|
||||||
) {
|
) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@ -783,6 +830,7 @@ class GiftMp4VideoPlatformView(
|
|||||||
val stepY = maxOf(1, (endY - startY) / 120)
|
val stepY = maxOf(1, (endY - startY) / 120)
|
||||||
var saturationSum = 0.0
|
var saturationSum = 0.0
|
||||||
var grayErrorSum = 0.0
|
var grayErrorSum = 0.0
|
||||||
|
var brightnessSum = 0.0
|
||||||
var count = 0
|
var count = 0
|
||||||
var y = startY
|
var y = startY
|
||||||
while (y < endY) {
|
while (y < endY) {
|
||||||
@ -800,15 +848,19 @@ class GiftMp4VideoPlatformView(
|
|||||||
(kotlin.math.abs(red - green) +
|
(kotlin.math.abs(red - green) +
|
||||||
kotlin.math.abs(green - blue) +
|
kotlin.math.abs(green - blue) +
|
||||||
kotlin.math.abs(blue - red)) / 3.0
|
kotlin.math.abs(blue - red)) / 3.0
|
||||||
|
brightnessSum += red * 0.299 + green * 0.587 + blue * 0.114
|
||||||
count++
|
count++
|
||||||
x += stepX
|
x += stepX
|
||||||
}
|
}
|
||||||
y += stepY
|
y += stepY
|
||||||
}
|
}
|
||||||
if (count == 0) return HalfStats(saturation = 0.0, grayError = 0.0)
|
if (count == 0) {
|
||||||
|
return HalfStats(saturation = 0.0, grayError = 0.0, brightness = 0.0)
|
||||||
|
}
|
||||||
return HalfStats(
|
return HalfStats(
|
||||||
saturation = saturationSum / count,
|
saturation = saturationSum / count,
|
||||||
grayError = grayErrorSum / count,
|
grayError = grayErrorSum / count,
|
||||||
|
brightness = brightnessSum / count,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -482,6 +482,7 @@ private struct GiftMp4SplitAlphaCandidate {
|
|||||||
private struct GiftMp4HalfStats {
|
private struct GiftMp4HalfStats {
|
||||||
let saturation: Double
|
let saturation: Double
|
||||||
let grayError: Double
|
let grayError: Double
|
||||||
|
let brightness: Double
|
||||||
}
|
}
|
||||||
|
|
||||||
private struct GiftMp4SplitScore {
|
private struct GiftMp4SplitScore {
|
||||||
@ -500,6 +501,12 @@ private struct GiftMp4PackedAlphaCandidate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private final class GiftMp4SourceLayoutResolver {
|
private final class GiftMp4SourceLayoutResolver {
|
||||||
|
private static let minSplitMatchFrames = 3
|
||||||
|
private static let minUsefulFrameBrightness = 0.035
|
||||||
|
private static let minSplitSaturationGap = 0.08
|
||||||
|
private static let maxSplitMaskSaturation = 0.28
|
||||||
|
private static let maxSplitMaskGrayError = 0.06
|
||||||
|
|
||||||
private static var layoutCache: [String: GiftMp4SourceLayout] = [:]
|
private static var layoutCache: [String: GiftMp4SourceLayout] = [:]
|
||||||
|
|
||||||
static func resolve(
|
static func resolve(
|
||||||
@ -888,7 +895,7 @@ private final class GiftMp4SourceLayoutResolver {
|
|||||||
generator.requestedTimeToleranceAfter = .positiveInfinity
|
generator.requestedTimeToleranceAfter = .positiveInfinity
|
||||||
|
|
||||||
let durationSeconds = CMTimeGetSeconds(asset.duration)
|
let durationSeconds = CMTimeGetSeconds(asset.duration)
|
||||||
let desiredSeconds = [0.0, 0.5, 1.0, 2.0, 3.0]
|
let desiredSeconds = [0.3, 0.6, 0.9, 1.2, 1.5, 1.8, 2.1, 2.4, 2.7, 3.0]
|
||||||
.filter { durationSeconds.isNaN || durationSeconds <= 0 || $0 <= durationSeconds }
|
.filter { durationSeconds.isNaN || durationSeconds <= 0 || $0 <= durationSeconds }
|
||||||
let times = (desiredSeconds.isEmpty ? [0.0] : desiredSeconds)
|
let times = (desiredSeconds.isEmpty ? [0.0] : desiredSeconds)
|
||||||
.map { NSValue(time: CMTime(seconds: $0, preferredTimescale: 600)) }
|
.map { NSValue(time: CMTime(seconds: $0, preferredTimescale: 600)) }
|
||||||
@ -950,6 +957,7 @@ private final class GiftMp4SourceLayoutResolver {
|
|||||||
let stepY = max(1, height / 32)
|
let stepY = max(1, height / 32)
|
||||||
var saturationSum = 0.0
|
var saturationSum = 0.0
|
||||||
var grayErrorSum = 0.0
|
var grayErrorSum = 0.0
|
||||||
|
var brightnessSum = 0.0
|
||||||
var count = 0.0
|
var count = 0.0
|
||||||
|
|
||||||
var y = startY
|
var y = startY
|
||||||
@ -967,8 +975,14 @@ private final class GiftMp4SourceLayoutResolver {
|
|||||||
abs(Double(pixel.g) - Double(pixel.b)) +
|
abs(Double(pixel.g) - Double(pixel.b)) +
|
||||||
abs(Double(pixel.b) - Double(pixel.r))
|
abs(Double(pixel.b) - Double(pixel.r))
|
||||||
) / (3.0 * 255.0)
|
) / (3.0 * 255.0)
|
||||||
|
let brightness = (
|
||||||
|
Double(pixel.r) * 0.299 +
|
||||||
|
Double(pixel.g) * 0.587 +
|
||||||
|
Double(pixel.b) * 0.114
|
||||||
|
) / 255.0
|
||||||
saturationSum += saturation
|
saturationSum += saturation
|
||||||
grayErrorSum += grayError
|
grayErrorSum += grayError
|
||||||
|
brightnessSum += brightness
|
||||||
count += 1
|
count += 1
|
||||||
x += stepX
|
x += stepX
|
||||||
}
|
}
|
||||||
@ -976,11 +990,12 @@ private final class GiftMp4SourceLayoutResolver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if count == 0 {
|
if count == 0 {
|
||||||
return GiftMp4HalfStats(saturation: 0, grayError: 0)
|
return GiftMp4HalfStats(saturation: 0, grayError: 0, brightness: 0)
|
||||||
}
|
}
|
||||||
return GiftMp4HalfStats(
|
return GiftMp4HalfStats(
|
||||||
saturation: saturationSum / count,
|
saturation: saturationSum / count,
|
||||||
grayError: grayErrorSum / count
|
grayError: grayErrorSum / count,
|
||||||
|
brightness: brightnessSum / count
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -989,19 +1004,37 @@ private final class GiftMp4SourceLayoutResolver {
|
|||||||
first: GiftMp4HalfStats,
|
first: GiftMp4HalfStats,
|
||||||
second: GiftMp4HalfStats
|
second: GiftMp4HalfStats
|
||||||
) {
|
) {
|
||||||
|
if isLowBrightnessFrame(first: first, second: second) {
|
||||||
|
return
|
||||||
|
}
|
||||||
let alphaStats = first.saturation < second.saturation ? first : second
|
let alphaStats = first.saturation < second.saturation ? first : second
|
||||||
|
let saturationGap = abs(second.saturation - first.saturation)
|
||||||
|
guard
|
||||||
|
saturationGap >= minSplitSaturationGap,
|
||||||
|
alphaStats.saturation <= maxSplitMaskSaturation,
|
||||||
|
alphaStats.grayError <= maxSplitMaskGrayError
|
||||||
|
else {
|
||||||
|
return
|
||||||
|
}
|
||||||
score.direction += second.saturation - first.saturation
|
score.direction += second.saturation - first.saturation
|
||||||
score.saturationGap += abs(second.saturation - first.saturation)
|
score.saturationGap += saturationGap
|
||||||
score.maskSaturation += alphaStats.saturation
|
score.maskSaturation += alphaStats.saturation
|
||||||
score.maskGrayError += alphaStats.grayError
|
score.maskGrayError += alphaStats.grayError
|
||||||
score.frames += 1
|
score.frames += 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static func isLowBrightnessFrame(
|
||||||
|
first: GiftMp4HalfStats,
|
||||||
|
second: GiftMp4HalfStats
|
||||||
|
) -> Bool {
|
||||||
|
max(first.brightness, second.brightness) < minUsefulFrameBrightness
|
||||||
|
}
|
||||||
|
|
||||||
private static func splitCandidateFromScore(
|
private static func splitCandidateFromScore(
|
||||||
orientation: GiftMp4SplitOrientation,
|
orientation: GiftMp4SplitOrientation,
|
||||||
score: GiftMp4SplitScore
|
score: GiftMp4SplitScore
|
||||||
) -> (GiftMp4SplitAlphaCandidate, Double)? {
|
) -> (GiftMp4SplitAlphaCandidate, Double)? {
|
||||||
guard score.frames > 0 else {
|
guard score.frames >= minSplitMatchFrames else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
let frames = Double(score.frames)
|
let frames = Double(score.frames)
|
||||||
@ -1010,9 +1043,9 @@ private final class GiftMp4SourceLayoutResolver {
|
|||||||
let averageMaskGrayError = score.maskGrayError / frames
|
let averageMaskGrayError = score.maskGrayError / frames
|
||||||
|
|
||||||
guard
|
guard
|
||||||
averageSaturationGap >= 0.08,
|
averageSaturationGap >= minSplitSaturationGap,
|
||||||
averageMaskSaturation <= 0.28,
|
averageMaskSaturation <= maxSplitMaskSaturation,
|
||||||
averageMaskGrayError <= 0.06
|
averageMaskGrayError <= maxSplitMaskGrayError
|
||||||
else {
|
else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user