Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

VideoFrameDecoder creates large .tmp which are not cleaned up if the app is killed #2550

Closed
d4rken opened this issue Oct 15, 2024 · 11 comments
Closed

Comments

@d4rken
Copy link

d4rken commented Oct 15, 2024

Describe the bug
I think what happens is that VideoFrameDecoder creates some large .tmp files to get a thumbnail. This creation can take time, e.g. for a 4GB video from my phone, it may take a few seconds to get that thumbnail. If the app is killed during that time, then coil looses track of these .tmp files.

This can happen if by the user not waiting for thumbnails to finish loading and swipes the app away from the "recents" screen.

Disabling disk cache (diskCachePolicy(CachePolicy.DISABLED)) does not affect this.

Do I need to check for orphaned tmp files and delete them on app launch?

Is there a way to configure the file name or path for these?

To Reproduce

  • Download SD Maid: https://github.com/d4rken-org/sdmaid-se
  • Complete setup for file access
  • Go to storage analyzer
  • Browse some folder with large files (Storage > Main > User Files > /storage/emulated/0/DCIM )
  • While the thumbnail for a video file is being decoded, kill the app, e.g. close it by swiping away from recents

Logs/Screenshots
Screenshot from 2024-10-15 12-01-13

Version
2.7.0 reproduced it on a Pixel 6 running Android 15

@d4rken
Copy link
Author

d4rken commented Oct 15, 2024

Actual file seems to be created by this call:

setDataSource(source.file().toFile().path)

Specifically: source.file()

Which calls this:

// Copy the source to a temp file.
val tempFile = createTempFile()
fileSystem.write(tempFile) {
writeAll(source!!)
}

Path comes from here:

private fun createTempFile(): Path {
val cacheDirectory = cacheDirectoryFactory!!.invoke()
check(cacheDirectory.isDirectory) { "cacheDirectory must be a directory." }
// Replace JVM call with https://github.com/square/okio/issues/1090 once it's available.
return File.createTempFile("tmp", null, cacheDirectory).toOkioPath()
}

Okay so regarding this:

Is there a way to configure the file name or path for these?

I can pass my own path via ImageSource constructor.

@d4rken
Copy link
Author

d4rken commented Oct 15, 2024

I'm wondering if calling File.deleteOnExit() would prevent these tmp files from being orphaned if the app dies. Probably depends on whether the "on-exit" triggers if the app process dies for some reason. 🤔

d4rken added a commit to d4rken-org/sdmaid-se that referenced this issue Oct 15, 2024
…n creating video previews

When SD Maid generates thumbnails for large videos via coil, then large `.tmp` files can be created.
If SD Maid is killed before thumbnail generation has finished, then these `.tmp` can stay around until manually deleted.

This PR changes the path where these files are stored, and deleted any orphaned files `.tmp` files on the next app launch.

Also see coil-kt/coil#2550
d4rken added a commit to d4rken-org/sdmaid-se that referenced this issue Oct 15, 2024
…reviews

When SD Maid generates thumbnails for large videos via coil, then large `.tmp` files can be created.
If SD Maid is killed before thumbnail generation has finished, then these `.tmp` can stay around until manually deleted.

This PR changes the path where these files are stored, and deleted any orphaned files `.tmp` files on the next app launch.

Also see coil-kt/coil#2550
@colinrtwhite
Copy link
Member

Hmm I thought createTempFile would be deleted on exit, but sounds like we need something else. Adding deleteOnExit sounds good to me!

Unfortunately MediaMetadataRetriever needs a seekable data source, which basically means a file since it's generally not possible to keep the complete video file in memory. How are you loading these files? Ideally we fall into one of these earlier cases, which lets us avoid creating a temp file.

@d4rken
Copy link
Author

d4rken commented Oct 16, 2024

Via Java NIO API, DELETE_ON_CLOSE might also be an option:

https://stackoverflow.com/questions/28752006/alternative-to-file-deleteonexit-in-java-nio

I currently can't test it, because I can't get coil to build on my local machine.

How are you loading these files? Ideally we fall into one of these earlier cases, which lets us avoid creating a temp file.

I've got a custom IO API that handles access via different permissions, including root and adb, that exposes a Source to Coil and a custom fetcher:

https://github.com/d4rken-org/sdmaid-se/blob/db0be9ec1262d2571d3fa933d5159507f74b395a/app/src/main/java/eu/darken/sdmse/common/coil/BitmapFetcher.kt#L35-L44

@d4rken
Copy link
Author

d4rken commented Oct 16, 2024

deleteOnExit does not work with Android, not triggered when app is swiped away. ☹️

@d4rken
Copy link
Author

d4rken commented Oct 17, 2024

Could I provide Coil with an Okio.FileHandle (which is a seekable source), so that creating the tmp file is not necessary?

@d4rken
Copy link
Author

d4rken commented Oct 17, 2024

I found the experimental API for MediaSourceMetadata from #1790 🔥

I've changed my API to return a FileHandle and wrap that using MediaSourceMetaData, afaict it prevents the temporary file from being created.

@OptIn(ExperimentalCoilApi::class)
internal fun FileHandle.toImageSource(
    options: Options,
): ImageSource {
    val handle = this
    val sourceBuffer = this.source().buffer()
    val mediaDataSource = object : MediaDataSource() {
        override fun readAt(position: Long, buffer: ByteArray, offset: Int, size: Int): Int {
            return handle.read(position, buffer, offset, size)
        }

        override fun getSize(): Long {
            return handle.size()
        }

        override fun close() {
            sourceBuffer.close()
            handle.close()
        }
    }
    return ImageSource(
        source = sourceBuffer,
        context = options.context,
        metadata = MediaSourceMetadata(mediaDataSource),
    )
}

d4rken added a commit to d4rken-org/sdmaid-se that referenced this issue Oct 17, 2024
Replace the previous sepperate read/write functions.
Allows `Coil` to efficiently load video thumbnails.
Improves SAF performance.

See #1434
See coil-kt/coil#2550
d4rken added a commit to d4rken-org/sdmaid-se that referenced this issue Oct 18, 2024
Replace the previous sepperate read/write functions.
Allows `Coil` to efficiently load video thumbnails.
Improves SAF performance.

See #1434
See coil-kt/coil#2550
d4rken added a commit to d4rken-org/sdmaid-se that referenced this issue Oct 18, 2024
Replace the previous sepperate read/write functions.
Allows `Coil` to efficiently load video thumbnails.
Improves SAF performance.

See #1434
See coil-kt/coil#2550
@colinrtwhite
Copy link
Member

Nice! For cleaning up temp files, unfortunately I don't think there's a great solution. We already delete the temp file in a finally block, however that isn't run when the app is force closed.

From my research it sounds like temp files won't be cleaned up when the app is closed, however they're likely to be cleared when the device needs space or the device is rebooted. Overall, they shouldn't last forever and will be cleaned up eventually.

@colinrtwhite
Copy link
Member

Also as an aside I used some of your code here to fix using VideoFrameDecoder when used with custom FileSystem implementations. Thanks!

@d4rken
Copy link
Author

d4rken commented Oct 19, 2024

So as I've found a solution for my issue and the "app-death" handling of tmp files is kinda out of scope (coil would have to keep track of the tmp files in some kind of DB), I think we can close this.

What exactly did #2570 fix?

@d4rken d4rken closed this as completed Oct 19, 2024
@colinrtwhite
Copy link
Member

colinrtwhite commented Oct 19, 2024

@d4rken Using custom Okio FileSystems like FakeFileSystem for tests would fail because VideoFrameDecoder was always assuming FileSystem.SYSTEM was used. There are other cases (for instance with Compose Multiplatform Resources) where the non-default FileSystem will be used.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants