-
Notifications
You must be signed in to change notification settings - Fork 85
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
Implement Camera2 support as a MediaSource #232
Conversation
* Since we don't support cropping of the sensor or input surface, we will record to a Surface | ||
* that has the same aspect ratio as the camera's sensor. | ||
*/ | ||
private fun getRecordSize(sensorRect: Rect?, sensorOrientation: Int?): Size { |
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.
For now, we will require the source aspect ratio to match the target aspect ratio. For the camera specific cases, we could follow up with a couple of options:
- Have people crop at the sensor
- Implement some filter support for cropping
While the former is arguably simpler, it introduces the limitation that if the application is using a Preview Surface, then it will also be cropped. A crop based filter may be a better / more versatile solution.
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 have used DefaultVideoFrameRenderFilter
for cropping in the past, we could probably experiment with that here.
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.
Sounds good.
I thought for v1, requiring the same aspect ratio of the sensor is a requirement some people could live with. Then separately, we can iterate on it and support things like cropping.
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.
👍
@@ -61,7 +61,7 @@ class AudioRecordMediaSource( | |||
private var isRecording = false | |||
|
|||
@Synchronized | |||
fun start() { | |||
fun startRecording() { |
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 tweaked to be consistent with the new MediaSources, and also thought being more explicit (rather than start/stop) would be better.
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.
👍
private lateinit var mediaTransformer: MediaTransformer | ||
private var targetMedia: TargetMedia = TargetMedia() | ||
|
||
private val cameraManager: CameraManager 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.
This requires lazy
because I am deferring it to when I need to use it, which will be after the context has been attached to the Fragment.
|
||
private const val DEFAULT_CAMERA_FPS = 30 | ||
private const val DEFAULT_TARGET_BITRATE = 5_000_000 // 5Mbps | ||
private const val DEFAULT_RECORD_WIDTH = 1280 |
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 works (and you can keep this "as is" since this is in demo app), but I would use resolution as a parameter since it is a common terminology.
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.
Yeah, makes sense. I'll have a think, and potentially push another update. Maybe, while we have the aspect ratio requirement, we could do something like a "maximum resolution" that we compute our target resolution within. For this example case, it would be like passing in a max resolution of "1280x1280" which is basically the max bounding box?
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.
Commonly, video size is defined as resolution (HD, FHD, 4K, etc.) + aspect ratio (square, letterbox, wide screen, etc.) I usually use that convention.
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.
Again, this is a fragment, not public LiTr API, so we can do whatever here.
private lateinit var mediaTransformer: MediaTransformer | ||
private var targetMedia: TargetMedia = TargetMedia() | ||
|
||
private val cameraManager: CameraManager 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.
you can also use lateinit var
here
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.
Sure, we switch to be consistent 👍
// this. | ||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { | ||
AlertDialog.Builder(requireContext()) | ||
.setMessage("Android Marshmallow or newer required") |
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 will probably get a lint error on hard coding strings.
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.
Removed hard coded strings, and also fixed the same on in the RecordAudioFragment
.setTargetTrack(1) | ||
.setTargetFormat(createAudioMediaFormat(audioTrackFormat)) | ||
.setEncoder(new MediaCodecEncoder()) | ||
.setDecoder(new MediaCodecDecoder()); |
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.
JC: wasn't this supposed to be a PassthroughDecoder
for audio recorder?
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 can't remember exactly why I used the MediaCodecDecoder
but I tried to change as you suggested, but it results in the following error:
2022-12-27 10:29:05.019 17230-17298/com.linkedin.android.litr.demo E/TransformationJob: Transformation job error
java.nio.BufferOverflowException
at java.nio.ByteBuffer.put(ByteBuffer.java:613)
at java.nio.DirectByteBuffer.put(DirectByteBuffer.java:268)
at com.linkedin.android.litr.render.PassthroughAudioProcessor.processFrame(PassthroughAudioProcessor.kt:21)
at com.linkedin.android.litr.render.AudioRenderer.renderFrame(AudioRenderer.kt:103)
at com.linkedin.android.litr.transcoder.AudioTrackTranscoder.queueDecodedInputFrame(AudioTrackTranscoder.java:182)
at com.linkedin.android.litr.transcoder.AudioTrackTranscoder.processNextFrame(AudioTrackTranscoder.java:90)
at com.linkedin.android.litr.TransformationJob.processNextFrame(TransformationJob.java:211)
at com.linkedin.android.litr.TransformationJob.transform(TransformationJob.java:108)
at com.linkedin.android.litr.TransformationJob.run(TransformationJob.java:77)
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:463)
at java.util.concurrent.FutureTask.run(FutureTask.java:264)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1137)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:637)
at java.lang.Thread.run(Thread.java:1012)
That seems to happen even if I increase the inputBufferCapacity
. I'd suggest we keep as is, but can look into this error separately, if that's okay?
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.
That is totally fine. It was more of a question than change suggestion.
} | ||
|
||
companion object { | ||
private val TAG = AutoFitSurfaceView::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 don't think we need companion object
here, this can be a const val
previewSurfaceHolder = holder | ||
} | ||
|
||
override fun surfaceChanged(surfaceHolder: SurfaceHolder, p1: Int, p2: Int, p3: Int) {} |
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.
p1
, etc are not good parameter names, let's make them more descriptive
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.
Nice catch, thanks
* configured Callback will be notified via onDeviceReady. Once this has done, the consumer is | ||
* able to start the Transcode session. | ||
*/ | ||
@SuppressLint("MissingPermission") |
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.
Can we use @RequiresPermission
(https://developer.android.com/reference/androidx/annotation/RequiresPermission) here?
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 think we'd need to use both, right?
- The
RequiresPermission
reports that the caller needs the permission - The
SuppressLint
avoids Android Studio trying to force that we add the permission to the litr manifest (instead requiring people who want/need to use the source to make sure they add it)
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.
Actually, scrap that, you're right (and I made a typo which is why it failed locally)
* Since we don't support cropping of the sensor or input surface, we will record to a Surface | ||
* that has the same aspect ratio as the camera's sensor. | ||
*/ | ||
private fun getRecordSize(sensorRect: Rect?, sensorOrientation: Int?): Size { |
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 have used DefaultVideoFrameRenderFilter
for cropping in the past, we could probably experiment with that here.
* that has the same aspect ratio as the camera's sensor. | ||
*/ | ||
private fun getRecordSize(sensorRect: Rect?, sensorOrientation: Int?): Size { | ||
val aspectRatio = sensorRect?.let { it.height().toFloat() / it.width().toFloat() } ?: (3f / 4f) |
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.
note to self: we should add an enum (or a collection of constants) for typical aspect ratios. Don't have to do this in this PR.
* Callback. An instance of this class is expected to be both the MediaSource, and Decoder for which | ||
* the pipeline is configured. This allows these components to be bypassed. | ||
*/ | ||
open class ExternalMediaSource: MediaSource, Decoder { |
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 is a question, not a change suggestion. Would it make sense to name this CaptureMediaSource
or something similar?
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.
Yeah, I like it. I think "External" made more sense when we expected most consumers would need it directly. However, with the addition of Camera2MediaSource
, most people may use this instead. Will rename
ca4d1e1
to
79caab9
Compare
|
||
private const val DEFAULT_CAMERA_FPS = 30 | ||
private const val DEFAULT_TARGET_BITRATE = 5_000_000 // 5Mbps | ||
private const val DEFAULT_RECORD_WIDTH = 1280 |
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.
Commonly, video size is defined as resolution (HD, FHD, 4K, etc.) + aspect ratio (square, letterbox, wide screen, etc.) I usually use that convention.
|
||
private const val DEFAULT_CAMERA_FPS = 30 | ||
private const val DEFAULT_TARGET_BITRATE = 5_000_000 // 5Mbps | ||
private const val DEFAULT_RECORD_WIDTH = 1280 |
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.
Again, this is a fragment, not public LiTr API, so we can do whatever here.
* Since we don't support cropping of the sensor or input surface, we will record to a Surface | ||
* that has the same aspect ratio as the camera's sensor. | ||
*/ | ||
private fun getRecordSize(sensorRect: Rect?, sensorOrientation: Int?): Size { |
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.
👍
private const val TAG = "CaptureMediaSource" | ||
private const val DECODER_NAME = "CaptureMediaSource.Decoder" | ||
|
||
private const val MIME_TYPE_RAW = "video/raw" |
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 add this to MimeType
class
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.
Will do. I originally resisted because it's not really a "standard" mime type, but happy to move.
79caab9
to
d199abd
Compare
@izzytwosheds - That's me pushed a change to move the MimeType. If you're happy with the current PR, maybe we could land and look at any tweaks in some follow up PRs? |
This PR introduces two new MediaSources:
ExternalMediaSource
: This provides the owner direct access to the Surface that will be used as the input into any configured filters, followed by the encoder. The reason for this is to allow non-file based sources of video.Camera2MediaSource
: This utilises theExternalMediaSource
and uses Android's Camera2 APIs to render from the camera to the input Surface. This is being combined with the previously landedAudioRecordMediaSource
that is adding the audio stream. This demonstrates a Camera to File example.Also, one thing to note is that the ExternalMediaSource does not support cropping. It expects the source aspect ratio to match that of the file/target aspect ratio. We could either keep this as a hard constraint, or we could introduce a CroppingFilter to handle this case.