-
Notifications
You must be signed in to change notification settings - Fork 253
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
[Sample] [Media] - Implementation of "UltraHDR to HDR Video" #83
Conversation
...media/ultrahdr/src/main/java/com/example/platform/media/ultrahdr/video/UltraHDRToHDRVideo.kt
Outdated
Show resolved
Hide resolved
...media/ultrahdr/src/main/java/com/example/platform/media/ultrahdr/video/UltraHDRToHDRVideo.kt
Outdated
Show resolved
Hide resolved
...media/ultrahdr/src/main/java/com/example/platform/media/ultrahdr/video/UltraHDRToHDRVideo.kt
Outdated
Show resolved
Hide resolved
...media/ultrahdr/src/main/java/com/example/platform/media/ultrahdr/video/UltraHDRToHDRVideo.kt
Outdated
Show resolved
Hide resolved
...media/ultrahdr/src/main/java/com/example/platform/media/ultrahdr/video/UltraHDRToHDRVideo.kt
Outdated
Show resolved
Hide resolved
* [ImageWriter.setOnImageReleasedListener] callbacks. | ||
*/ | ||
private lateinit var imageWriterHandler: Handler | ||
private val imageWriterThread: HandlerThread by lazy { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it possible to use coroutines instead? This sample mixes coroutines with threads and it's a bit confusing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unfortunately, ImageWriter
and MediaCodec.Callback
need their own handlers to process callback calls on their own thread.
Not sure if or when they will switch to coroutine scopes if ever.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not familiar with this APIs but I think if you pass null to the handler it will use the thread handler
https://developer.android.com/reference/android/media/ImageWriter#setOnImageReleasedListener(android.media.ImageWriter.OnImageReleasedListener,%20android.os.Handler)
If you then call this method within a coroutine using a Dispatcher.IO shouldn't that work?
In anycase shouldn't you stop the thread on stop/destroy?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can create a Dispatcher
from a Handler
using Handler.asCoroutineDispatcher()
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah going through again the above suggestion won't change anything as you have to pass a Handler
to the API.
tags = ["UltraHDR"], | ||
) | ||
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) | ||
class UltraHDRToHDRVideo : Fragment() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please consider moving the concurrency and processing bits into a separate state holder class. As it stands, the UI and its state are really strongly coupled.
CoroutineScope(Dispatchers.IO).launch { | ||
val threadName = Thread.currentThread().name | ||
Log.i(TAG, "Starting Conversion to Video on $threadName") | ||
doImageToVideoConversion() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This coroutine is launched on the IO
Dispatcher
, and subsequently used withContext
to also place it on the same dispatcher. The latter alone should be enough.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I recommend the following:
- Use Compose for UI
- Increase separation of concerns by abstracting the video processing to a separate class.
tags = ["UltraHDR"], | ||
) | ||
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) | ||
class UltraHDRToHDRVideo : Fragment() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Have you considered using Compose? It is required that all new samples are written in Compose.
* | ||
* Using [AtomicBoolean] since it can be accessed by multiple threads. | ||
*/ | ||
private var isFrameBufferProcessed = AtomicBoolean(true) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The video processing code is mixed with presentation code. I highly recommend to abstract video processing to a separate class.
* The [MediaCodec.Callback], used to asynchronously process frame produced by the | ||
* [ImageWriter]. | ||
*/ | ||
private fun setUpMediaCodecCallback() = object : MediaCodec.Callback() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It might be beneficial to move this class to its own file. By doing so, we can inject it as a dependency into the relevant video processor. This would align better with the Single Responsibility Principle, ensuring each class has a clear, distinct function. This change would likely enhance code readability and maintainability. What do you think?
// Create path to cache directory. Delete any previously generated videos. | ||
val dir = File(folderPath) | ||
if (dir.exists() && dir.isDirectory) dir.listFiles()?.let { | ||
for (file in it) file.delete() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it be possible to use File::deleteRecursively
here?
|
||
bitmaps.forEach { bitmap -> | ||
var frameCount = 1 | ||
while (frameCount <= FORMAT_FRAME_RATE) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could repeat(FORMAT_FRAME_RATE)
be sufficient?
/** | ||
* Folder path name where .mp4 files will be encoded to | ||
*/ | ||
private val FOLDER_PATH_NAME = UltraHDRToHDRVideo::class.java.simpleName |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd be cautious here. If the code obfuscation is enabled the name of the class will be changed to something random. I think it's best to simply hardcode a string.
Will add a second PR what uses the same logic that will split video processing from sample @mmoczkowski @marcelpinto :) |
* Testing Implementation for UltraHDR to HDR Video * Complete HW aceel implementation * Add temp muxer to write file * working implementation of output * Clean up implementation of UltraHDR to HDR Video * Slight fixes to implementation, better coroutines * pr nits
Description
This CL contains sample code for converting a series of UltraHDR images into an HDR video using GPU hardware acceleration. When rendering an UltraHDR image into a 10-bit frame, the gain map is considered.
This implementation is very detailed as there is many moving parts of it, do excessive notes have been added to guide developer through the various steps needed to make this a reality.
How Has This Been Tested?
Test Configuration #1 - Pixel 7 Pro
Test Configuration #2 - Pixel 7
Checklist:
Dem
ultrahdr_to_hdrvideo_screen_recording.mp4
Resulting File
This file have been taken from device and depending on the device you view it on, you will see it in true 10-bit HDR HLG.
https://github.com/android/platform-samples/assets/5649571/934421b8-6059-4828-8686-b3bec05a26d8