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

Implement Camera2 support as a MediaSource #232

Merged
merged 1 commit into from
Dec 28, 2022

Conversation

IanDBird
Copy link
Contributor

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 the ExternalMediaSource and uses Android's Camera2 APIs to render from the camera to the input Surface. This is being combined with the previously landed AudioRecordMediaSource 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.

* 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 {
Copy link
Contributor Author

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.

Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Contributor

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() {
Copy link
Contributor Author

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.

Copy link
Contributor

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 {
Copy link
Contributor Author

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
Copy link
Contributor

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.

Copy link
Contributor Author

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?

Copy link
Contributor

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.

Copy link
Contributor

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 {
Copy link
Contributor

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

Copy link
Contributor Author

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")
Copy link
Contributor

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.

Copy link
Contributor Author

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());
Copy link
Contributor

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?

Copy link
Contributor Author

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?

Copy link
Contributor

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
Copy link
Contributor

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) {}
Copy link
Contributor

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

Copy link
Contributor Author

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")
Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Contributor Author

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)

Copy link
Contributor Author

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 {
Copy link
Contributor

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)
Copy link
Contributor

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 {
Copy link
Contributor

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?

Copy link
Contributor Author

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

@IanDBird IanDBird force-pushed the external-record branch 5 times, most recently from ca4d1e1 to 79caab9 Compare December 27, 2022 10:52

private const val DEFAULT_CAMERA_FPS = 30
private const val DEFAULT_TARGET_BITRATE = 5_000_000 // 5Mbps
private const val DEFAULT_RECORD_WIDTH = 1280
Copy link
Contributor

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
Copy link
Contributor

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 {
Copy link
Contributor

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"
Copy link
Contributor

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

Copy link
Contributor Author

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.

@IanDBird
Copy link
Contributor Author

@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?

@izzytwosheds izzytwosheds merged commit 3304f72 into linkedin:main Dec 28, 2022
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

Successfully merging this pull request may close these issues.

2 participants