Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 19 additions & 3 deletions coil-video/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,31 @@ val imageLoader = ImageLoader.Builder(context)
.build()
```

To specify the time code of the frame to extract from a video, use `videoFrameMillis` or `videoFrameMicros`:
To specify the time of the frame to extract from a video, use `videoFrameMillis` or `videoFrameMicros`:

```kotlin
imageView.load("/path/to/video.mp4") {
videoFrameMillis(1000)
videoFrameMillis(1000) // extracts the frame at 1 second of the video
}
```

If a frame time isn't specified, the first frame of the video is decoded.
For specifying the exact frame number, use `videoFrameIndex`:

```kotlin
imageView.load("/path/to/video.mp4") {
videoFrameIndex(1234) // extracts the 1234th frame of the video
}
```

To select a video frame based on a percentage of the video's total duration, use `videoFramePercent`:

```kotlin
imageView.load("/path/to/video.mp4") {
videoFramePercent(0.5) // extracts the frame in the middle of the video's duration
}
```

If no frame position is specified, the first frame of the video will be decoded.

The `ImageLoader` will automatically detect any videos and extract their frames if the request's filename/URI ends with a [valid video extension](https://developer.android.com/guide/topics/media/media-formats#video-formats). If it does not, you can set the `Decoder` explicitly for the request:

Expand Down
4 changes: 4 additions & 0 deletions coil-video/api/coil-video.api
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
public final class coil3/video/ImageRequestsKt {
public static final fun getVideoFrameIndex (Lcoil3/Extras$Key$Companion;)Lcoil3/Extras$Key;
public static final fun getVideoFrameIndex (Lcoil3/request/ImageRequest;)I
public static final fun getVideoFrameIndex (Lcoil3/request/Options;)I
public static final fun getVideoFrameMicros (Lcoil3/Extras$Key$Companion;)Lcoil3/Extras$Key;
public static final fun getVideoFrameMicros (Lcoil3/request/ImageRequest;)J
public static final fun getVideoFrameMicros (Lcoil3/request/Options;)J
Expand All @@ -8,6 +11,7 @@ public final class coil3/video/ImageRequestsKt {
public static final fun getVideoFramePercent (Lcoil3/Extras$Key$Companion;)Lcoil3/Extras$Key;
public static final fun getVideoFramePercent (Lcoil3/request/ImageRequest;)D
public static final fun getVideoFramePercent (Lcoil3/request/Options;)D
public static final fun videoFrameIndex (Lcoil3/request/ImageRequest$Builder;I)Lcoil3/request/ImageRequest$Builder;
public static final fun videoFrameMicros (Lcoil3/request/ImageRequest$Builder;J)Lcoil3/request/ImageRequest$Builder;
public static final fun videoFrameMillis (Lcoil3/request/ImageRequest$Builder;J)Lcoil3/request/ImageRequest$Builder;
public static final fun videoFrameOption (Lcoil3/request/ImageRequest$Builder;I)Lcoil3/request/ImageRequest$Builder;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,32 @@ class VideoFrameDecoderTest {
actual.assertIsSimilarTo(expected)
}

@Test
fun specificFrameIndex() = runTest(timeout = 1.minutes) {
// MediaMetadataRetriever#getFrameAtIndex does not work on the emulator pre-API 28.
assumeTrue(SDK_INT >= 28)

val result = VideoFrameDecoder(
source = ImageSource(
source = context.assets.open("video.mp4").source().buffer(),
fileSystem = FileSystem.SYSTEM,
),
options = Options(
context = context,
extras = Extras.Builder()
.set(Extras.Key.videoFrameIndex, 807)
.build(),
)
).decode()

val actual = result.image.bitmap
assertNotNull(actual)
assertFalse(result.isSampled)

val expected = context.decodeBitmapAsset("video_frame_2.jpg")
actual.assertIsSimilarTo(expected)
}

@Test
fun rotation() = runTest(timeout = 1.minutes) {
// MediaMetadataRetriever does not work on the emulator pre-API 23.
Expand Down
11 changes: 10 additions & 1 deletion coil-video/src/main/java/coil3/video/VideoFrameDecoder.kt
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import coil3.toAndroidUri
import coil3.util.heightPx
import coil3.util.widthPx
import coil3.video.MediaDataSourceFetcher.MediaSourceMetadata
import coil3.video.internal.getFrameAtIndex
import coil3.video.internal.getFrameAtTime
import coil3.video.internal.getScaledFrameAtTime
import coil3.video.internal.use
Expand Down Expand Up @@ -89,7 +90,15 @@ class VideoFrameDecoder(

val frameMicros = computeFrameMicros(retriever)
val (dstWidth, dstHeight) = dstSize
val rawBitmap: Bitmap? = if (SDK_INT >= 27 && dstWidth is Pixels && dstHeight is Pixels) {
val rawBitmap: Bitmap? = if (SDK_INT >= 28 && options.videoFrameIndex >= 0) {
retriever.getFrameAtIndex(
frameIndex = options.videoFrameIndex,
config = options.bitmapConfig,
)?.also {
srcWidth = it.width
srcHeight = it.height
}
} else if (SDK_INT >= 27 && dstWidth is Pixels && dstHeight is Pixels) {
retriever.getScaledFrameAtTime(
timeUs = frameMicros,
option = options.videoFrameOption,
Expand Down
26 changes: 26 additions & 0 deletions coil-video/src/main/java/coil3/video/imageRequests.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,32 @@ import coil3.getExtra
import coil3.request.ImageRequest
import coil3.request.Options

// region videoFrameIndex

/**
* Set the the frame index to extract from a video.
*
* When both [videoFrameIndex] and other videoFrame-prefixed properties are set,
* [videoFrameIndex] will take precedence.
*/
fun ImageRequest.Builder.videoFrameIndex(frameIndex: Int) = apply {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's annotate these methods with @RequiresApi(28) since they need API 28 to work.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure thing. I've added it to the README too.

require(frameIndex >= 0) { "frameIndex must be >= 0." }
memoryCacheKeyExtra("coil#videoFrameIndex", frameIndex.toString())
extras[videoFrameIndexKey] = frameIndex
}

val ImageRequest.videoFrameIndex: Int
get() = getExtra(videoFrameIndexKey)

val Options.videoFrameIndex: Int
get() = getExtra(videoFrameIndexKey)

val Extras.Key.Companion.videoFrameIndex: Extras.Key<Int>
get() = videoFrameIndexKey

private val videoFrameIndexKey = Extras.Key(default = -1)

// endregion
// region videoFrameMicros

/**
Expand Down
6 changes: 6 additions & 0 deletions coil-video/src/main/java/coil3/video/internal/utils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,9 @@ internal fun MediaMetadataRetriever.getScaledFrameAtTime(
} else {
getScaledFrameAtTime(timeUs, option, dstWidth, dstHeight)
}

@RequiresApi(28)
internal fun MediaMetadataRetriever.getFrameAtIndex(
frameIndex: Int,
config: Bitmap.Config,
): Bitmap? = getFrameAtIndex(frameIndex, BitmapParams().apply { preferredConfig = config })