Keep video rotation metadata when transcoding (#5008)

This commit is contained in:
Jorge Martin Espinosa 2025-07-10 17:02:42 +02:00 committed by GitHub
parent 7fdbd4c24b
commit fb318ad6fe
2 changed files with 39 additions and 16 deletions

View file

@ -90,6 +90,7 @@ class VideoCompressor @Inject constructor(
val height = it.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)?.toIntOrNull() ?: -1 val height = it.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)?.toIntOrNull() ?: -1
val bitrate = it.extractMetadata(MediaMetadataRetriever.METADATA_KEY_BITRATE)?.toLongOrNull() ?: -1 val bitrate = it.extractMetadata(MediaMetadataRetriever.METADATA_KEY_BITRATE)?.toLongOrNull() ?: -1
val framerate = it.extractMetadata(MediaMetadataRetriever.METADATA_KEY_CAPTURE_FRAMERATE)?.toIntOrNull() ?: -1 val framerate = it.extractMetadata(MediaMetadataRetriever.METADATA_KEY_CAPTURE_FRAMERATE)?.toIntOrNull() ?: -1
val rotation = it.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION)?.toIntOrNull() ?: 0
val (actualWidth, actualHeight) = if (width == -1 || height == -1) { val (actualWidth, actualHeight) = if (width == -1 || height == -1) {
// Try getting the first frame instead // Try getting the first frame instead
@ -103,7 +104,8 @@ class VideoCompressor @Inject constructor(
width = actualWidth, width = actualWidth,
height = actualHeight, height = actualHeight,
bitrate = bitrate, bitrate = bitrate,
frameRate = framerate frameRate = framerate,
rotation = rotation,
) )
} }
}.onFailure { }.onFailure {
@ -113,10 +115,11 @@ class VideoCompressor @Inject constructor(
} }
internal data class VideoFileMetadata( internal data class VideoFileMetadata(
val width: Int?, val width: Int,
val height: Int?, val height: Int,
val bitrate: Long?, val bitrate: Long,
val frameRate: Int?, val frameRate: Int,
val rotation: Int,
) )
sealed interface VideoTranscodingEvent { sealed interface VideoTranscodingEvent {
@ -136,10 +139,11 @@ internal object VideoStrategyFactory {
metadata: VideoFileMetadata?, metadata: VideoFileMetadata?,
shouldBeCompressed: Boolean, shouldBeCompressed: Boolean,
): TrackStrategy { ): TrackStrategy {
val width = metadata?.width ?: Int.MAX_VALUE val width = metadata?.width?.takeIf { it >= 0 } ?: Int.MAX_VALUE
val height = metadata?.height ?: Int.MAX_VALUE val height = metadata?.height?.takeIf { it >= 0 } ?: Int.MAX_VALUE
val bitrate = metadata?.bitrate val bitrate = metadata?.bitrate?.takeIf { it >= 0 }
val frameRate = metadata?.frameRate val frameRate = metadata?.frameRate?.takeIf { it >= 0 }
val rotation = metadata?.rotation?.takeIf { it >= 0 }
// We only create a resizer if needed // We only create a resizer if needed
val resizer = when { val resizer = when {
@ -148,8 +152,9 @@ internal object VideoStrategyFactory {
else -> null else -> null
} }
return if (resizer == null && expectedExtension == MP4_EXTENSION) { return if (resizer == null && rotation == 0 && expectedExtension == MP4_EXTENSION) {
// If there's no transcoding or resizing needed for the video file, just create a new file with the same contents but no metadata // If there's no transcoding or resizing needed for the video file, just create a new file with the same contents but no metadata
// Rotation is not kept by the PassThroughTrackStrategy, so we need to ensure the video is not rotated
PassThroughTrackStrategy() PassThroughTrackStrategy()
} else { } else {
DefaultVideoStrategy.Builder() DefaultVideoStrategy.Builder()

View file

@ -39,7 +39,7 @@ class VideoStrategyFactoryTest {
fun `if the video should be compressed and is larger than 720p it will be transcoded`() { fun `if the video should be compressed and is larger than 720p it will be transcoded`() {
// Given // Given
val expectedExtension = "mp4" val expectedExtension = "mp4"
val metadata = VideoFileMetadata(width = 1920, height = 1080, bitrate = 1_000_000, frameRate = 50) val metadata = VideoFileMetadata(width = 1920, height = 1080, bitrate = 1_000_000, frameRate = 50, rotation = 0)
val shouldBeCompressed = true val shouldBeCompressed = true
// When // When
@ -57,7 +57,7 @@ class VideoStrategyFactoryTest {
fun `if the video should be compressed, has the right format and is smaller or equal to 720p it will not be transcoded`() { fun `if the video should be compressed, has the right format and is smaller or equal to 720p it will not be transcoded`() {
// Given // Given
val expectedExtension = "mp4" val expectedExtension = "mp4"
val metadata = VideoFileMetadata(width = 1280, height = 720, bitrate = 1_000_000, frameRate = 50) val metadata = VideoFileMetadata(width = 1280, height = 720, bitrate = 1_000_000, frameRate = 50, rotation = 0)
val shouldBeCompressed = true val shouldBeCompressed = true
// When // When
@ -75,7 +75,7 @@ class VideoStrategyFactoryTest {
fun `if the video should not be compressed and is larger than 1080p it will be transcoded`() { fun `if the video should not be compressed and is larger than 1080p it will be transcoded`() {
// Given // Given
val expectedExtension = "mp4" val expectedExtension = "mp4"
val metadata = VideoFileMetadata(width = 2560, height = 1440, bitrate = 1_000_000, frameRate = 50) val metadata = VideoFileMetadata(width = 2560, height = 1440, bitrate = 1_000_000, frameRate = 50, rotation = 0)
val shouldBeCompressed = false val shouldBeCompressed = false
// When // When
@ -93,7 +93,7 @@ class VideoStrategyFactoryTest {
fun `if the video should not be compressed, has the right format and is smaller or equal than 1080p it will not be transcoded`() { fun `if the video should not be compressed, has the right format and is smaller or equal than 1080p it will not be transcoded`() {
// Given // Given
val expectedExtension = "mp4" val expectedExtension = "mp4"
val metadata = VideoFileMetadata(width = 1920, height = 1080, bitrate = 1_000_000, frameRate = 50) val metadata = VideoFileMetadata(width = 1920, height = 1080, bitrate = 1_000_000, frameRate = 50, rotation = 0)
val shouldBeCompressed = false val shouldBeCompressed = false
// When // When
@ -111,7 +111,7 @@ class VideoStrategyFactoryTest {
fun `if the video should not be compressed but has a wrong format it will be transcoded`() { fun `if the video should not be compressed but has a wrong format it will be transcoded`() {
// Given // Given
val expectedExtension = "mkv" val expectedExtension = "mkv"
val metadata = VideoFileMetadata(width = 320, height = 240, bitrate = 1_000_000, frameRate = 50) val metadata = VideoFileMetadata(width = 320, height = 240, bitrate = 1_000_000, frameRate = 50, rotation = 0)
val shouldBeCompressed = false val shouldBeCompressed = false
// When // When
@ -129,7 +129,7 @@ class VideoStrategyFactoryTest {
fun `if the video should be compressed and has a wrong format it will be transcoded`() { fun `if the video should be compressed and has a wrong format it will be transcoded`() {
// Given // Given
val expectedExtension = "mkv" val expectedExtension = "mkv"
val metadata = VideoFileMetadata(width = 320, height = 240, bitrate = 1_000_000, frameRate = 50) val metadata = VideoFileMetadata(width = 320, height = 240, bitrate = 1_000_000, frameRate = 50, rotation = 0)
val shouldBeCompressed = true val shouldBeCompressed = true
// When // When
@ -143,6 +143,24 @@ class VideoStrategyFactoryTest {
assertIsTranscoded(videoStrategy) assertIsTranscoded(videoStrategy)
} }
@Test
fun `if the video should not be compressed but has a rotation not zero it will be transcoded`() {
// Given
val expectedExtension = "mp4"
val metadata = VideoFileMetadata(width = 320, height = 240, bitrate = 1_000_000, frameRate = 50, rotation = 90)
val shouldBeCompressed = false
// When
val videoStrategy = VideoStrategyFactory.create(
expectedExtension = expectedExtension,
metadata = metadata,
shouldBeCompressed = shouldBeCompressed
)
// Then
assertIsTranscoded(videoStrategy)
}
private inline fun assertIsTranscoded(videoStrategy: TrackStrategy) { private inline fun assertIsTranscoded(videoStrategy: TrackStrategy) {
assert(videoStrategy is DefaultVideoStrategy) assert(videoStrategy is DefaultVideoStrategy)
} }