davidliu
Committed by GitHub

Render video directly from capturer for local video tracks (#419)

* Render video directly from capturer for local video tracks

* spotless
@@ -121,7 +121,7 @@ internal constructor( @@ -121,7 +121,7 @@ internal constructor(
121 options: LocalVideoTrackOptions = videoTrackCaptureDefaults.copy(), 121 options: LocalVideoTrackOptions = videoTrackCaptureDefaults.copy(),
122 videoProcessor: VideoProcessor? = null, 122 videoProcessor: VideoProcessor? = null,
123 ): LocalVideoTrack { 123 ): LocalVideoTrack {
124 - return LocalVideoTrack.createTrack( 124 + return LocalVideoTrack.createCameraTrack(
125 peerConnectionFactory, 125 peerConnectionFactory,
126 context, 126 context,
127 name, 127 name,
@@ -31,6 +31,7 @@ import io.livekit.android.room.track.video.CameraCapturerUtils.createCameraEnume @@ -31,6 +31,7 @@ import io.livekit.android.room.track.video.CameraCapturerUtils.createCameraEnume
31 import io.livekit.android.room.track.video.CameraCapturerUtils.findCamera 31 import io.livekit.android.room.track.video.CameraCapturerUtils.findCamera
32 import io.livekit.android.room.track.video.CameraCapturerUtils.getCameraPosition 32 import io.livekit.android.room.track.video.CameraCapturerUtils.getCameraPosition
33 import io.livekit.android.room.track.video.CameraCapturerWithSize 33 import io.livekit.android.room.track.video.CameraCapturerWithSize
  34 +import io.livekit.android.room.track.video.CaptureDispatchObserver
34 import io.livekit.android.room.track.video.VideoCapturerWithSize 35 import io.livekit.android.room.track.video.VideoCapturerWithSize
35 import io.livekit.android.room.util.EncodingUtils 36 import io.livekit.android.room.util.EncodingUtils
36 import io.livekit.android.util.FlowObservable 37 import io.livekit.android.util.FlowObservable
@@ -50,6 +51,7 @@ import livekit.org.webrtc.RtpTransceiver @@ -50,6 +51,7 @@ import livekit.org.webrtc.RtpTransceiver
50 import livekit.org.webrtc.SurfaceTextureHelper 51 import livekit.org.webrtc.SurfaceTextureHelper
51 import livekit.org.webrtc.VideoCapturer 52 import livekit.org.webrtc.VideoCapturer
52 import livekit.org.webrtc.VideoProcessor 53 import livekit.org.webrtc.VideoProcessor
  54 +import livekit.org.webrtc.VideoSink
53 import livekit.org.webrtc.VideoSource 55 import livekit.org.webrtc.VideoSource
54 import java.util.UUID 56 import java.util.UUID
55 import livekit.LivekitModels.VideoQuality as ProtoVideoQuality 57 import livekit.LivekitModels.VideoQuality as ProtoVideoQuality
@@ -72,6 +74,11 @@ constructor( @@ -72,6 +74,11 @@ constructor(
72 private val eglBase: EglBase, 74 private val eglBase: EglBase,
73 private val defaultsManager: DefaultsManager, 75 private val defaultsManager: DefaultsManager,
74 private val trackFactory: Factory, 76 private val trackFactory: Factory,
  77 + /**
  78 + * If this is assigned, you must ensure that this observer is associated with the [capturer],
  79 + * as this will be used to receive frames in [addRenderer].
  80 + **/
  81 + @Assisted private var dispatchObserver: CaptureDispatchObserver? = null,
75 ) : VideoTrack(name, rtcTrack) { 82 ) : VideoTrack(name, rtcTrack) {
76 83
77 override var rtcTrack: livekit.org.webrtc.VideoTrack = rtcTrack 84 override var rtcTrack: livekit.org.webrtc.VideoTrack = rtcTrack
@@ -126,6 +133,22 @@ constructor( @@ -126,6 +133,22 @@ constructor(
126 closeableManager.close() 133 closeableManager.close()
127 } 134 }
128 135
  136 + override fun addRenderer(renderer: VideoSink) {
  137 + if (dispatchObserver != null) {
  138 + dispatchObserver?.registerSink(renderer)
  139 + } else {
  140 + super.addRenderer(renderer)
  141 + }
  142 + }
  143 +
  144 + override fun removeRenderer(renderer: VideoSink) {
  145 + if (dispatchObserver != null) {
  146 + dispatchObserver?.unregisterSink(renderer)
  147 + } else {
  148 + super.removeRenderer(renderer)
  149 + }
  150 + }
  151 +
129 fun setDeviceId(deviceId: String) { 152 fun setDeviceId(deviceId: String) {
130 restartTrack(options.copy(deviceId = deviceId)) 153 restartTrack(options.copy(deviceId = deviceId))
131 } 154 }
@@ -239,7 +262,7 @@ constructor( @@ -239,7 +262,7 @@ constructor(
239 val oldCloseable = closeableManager.unregisterResource(oldRtcTrack) 262 val oldCloseable = closeableManager.unregisterResource(oldRtcTrack)
240 oldCloseable?.close() 263 oldCloseable?.close()
241 264
242 - val newTrack = createTrack( 265 + val newTrack = createCameraTrack(
243 peerConnectionFactory, 266 peerConnectionFactory,
244 context, 267 context,
245 name, 268 name,
@@ -386,85 +409,78 @@ constructor( @@ -386,85 +409,78 @@ constructor(
386 name: String, 409 name: String,
387 options: LocalVideoTrackOptions, 410 options: LocalVideoTrackOptions,
388 rtcTrack: livekit.org.webrtc.VideoTrack, 411 rtcTrack: livekit.org.webrtc.VideoTrack,
  412 + dispatchObserver: CaptureDispatchObserver?,
389 ): LocalVideoTrack 413 ): LocalVideoTrack
390 } 414 }
391 415
392 companion object { 416 companion object {
393 417
394 - internal fun createTrack( 418 + internal fun createCameraTrack(
395 peerConnectionFactory: PeerConnectionFactory, 419 peerConnectionFactory: PeerConnectionFactory,
396 context: Context, 420 context: Context,
397 name: String, 421 name: String,
398 - capturer: VideoCapturer,  
399 - options: LocalVideoTrackOptions = LocalVideoTrackOptions(), 422 + options: LocalVideoTrackOptions,
400 rootEglBase: EglBase, 423 rootEglBase: EglBase,
401 trackFactory: Factory, 424 trackFactory: Factory,
402 videoProcessor: VideoProcessor? = null, 425 videoProcessor: VideoProcessor? = null,
403 ): LocalVideoTrack { 426 ): LocalVideoTrack {
404 - val source = peerConnectionFactory.createVideoSource(false)  
405 - source.setVideoProcessor(videoProcessor)  
406 - val surfaceTextureHelper = SurfaceTextureHelper.create("VideoCaptureThread", rootEglBase.eglBaseContext)  
407 - capturer.initialize(  
408 - surfaceTextureHelper,  
409 - context,  
410 - source.capturerObserver,  
411 - )  
412 - val rtcTrack = peerConnectionFactory.createVideoTrack(UUID.randomUUID().toString(), source) 427 + if (ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) !=
  428 + PackageManager.PERMISSION_GRANTED
  429 + ) {
  430 + throw SecurityException("Camera permissions are required to create a camera video track.")
  431 + }
413 432
414 - val track = trackFactory.create(  
415 - capturer = capturer,  
416 - source = source,  
417 - options = options,  
418 - name = name,  
419 - rtcTrack = rtcTrack,  
420 - ) 433 + val (capturer, newOptions) = CameraCapturerUtils.createCameraCapturer(context, options) ?: TODO()
421 434
422 - track.closeableManager.registerResource(  
423 - rtcTrack,  
424 - SurfaceTextureHelperCloser(surfaceTextureHelper), 435 + return createTrack(
  436 + peerConnectionFactory = peerConnectionFactory,
  437 + context = context,
  438 + name = name,
  439 + capturer = capturer,
  440 + options = newOptions,
  441 + rootEglBase = rootEglBase,
  442 + trackFactory = trackFactory,
  443 + videoProcessor = videoProcessor,
425 ) 444 )
426 - return track  
427 } 445 }
428 446
429 internal fun createTrack( 447 internal fun createTrack(
430 peerConnectionFactory: PeerConnectionFactory, 448 peerConnectionFactory: PeerConnectionFactory,
431 context: Context, 449 context: Context,
432 name: String, 450 name: String,
433 - options: LocalVideoTrackOptions, 451 + capturer: VideoCapturer,
  452 + options: LocalVideoTrackOptions = LocalVideoTrackOptions(),
434 rootEglBase: EglBase, 453 rootEglBase: EglBase,
435 trackFactory: Factory, 454 trackFactory: Factory,
436 videoProcessor: VideoProcessor? = null, 455 videoProcessor: VideoProcessor? = null,
437 ): LocalVideoTrack { 456 ): LocalVideoTrack {
438 - if (ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) !=  
439 - PackageManager.PERMISSION_GRANTED  
440 - ) {  
441 - throw SecurityException("Camera permissions are required to create a camera video track.")  
442 - }  
443 -  
444 val source = peerConnectionFactory.createVideoSource(options.isScreencast) 457 val source = peerConnectionFactory.createVideoSource(options.isScreencast)
445 source.setVideoProcessor(videoProcessor) 458 source.setVideoProcessor(videoProcessor)
446 - val (capturer, newOptions) = CameraCapturerUtils.createCameraCapturer(context, options) ?: TODO() 459 +
447 val surfaceTextureHelper = SurfaceTextureHelper.create("VideoCaptureThread", rootEglBase.eglBaseContext) 460 val surfaceTextureHelper = SurfaceTextureHelper.create("VideoCaptureThread", rootEglBase.eglBaseContext)
  461 + val dispatchObserver = CaptureDispatchObserver()
  462 + dispatchObserver.registerObserver(source.capturerObserver)
  463 +
448 capturer.initialize( 464 capturer.initialize(
449 surfaceTextureHelper, 465 surfaceTextureHelper,
450 context, 466 context,
451 - source.capturerObserver, 467 + dispatchObserver,
452 ) 468 )
453 val rtcTrack = peerConnectionFactory.createVideoTrack(UUID.randomUUID().toString(), source) 469 val rtcTrack = peerConnectionFactory.createVideoTrack(UUID.randomUUID().toString(), source)
454 470
455 val track = trackFactory.create( 471 val track = trackFactory.create(
456 capturer = capturer, 472 capturer = capturer,
457 source = source, 473 source = source,
458 - options = newOptions, 474 + options = options,
459 name = name, 475 name = name,
460 rtcTrack = rtcTrack, 476 rtcTrack = rtcTrack,
  477 + dispatchObserver = dispatchObserver,
461 ) 478 )
462 479
463 track.closeableManager.registerResource( 480 track.closeableManager.registerResource(
464 rtcTrack, 481 rtcTrack,
465 SurfaceTextureHelperCloser(surfaceTextureHelper), 482 SurfaceTextureHelperCloser(surfaceTextureHelper),
466 ) 483 )
467 -  
468 return track 484 return track
469 } 485 }
470 } 486 }
  1 +/*
  2 + * Copyright 2024 LiveKit, Inc.
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +package io.livekit.android.room.track.video
  18 +
  19 +import livekit.org.webrtc.CapturerObserver
  20 +import livekit.org.webrtc.VideoFrame
  21 +import livekit.org.webrtc.VideoSink
  22 +
  23 +class CaptureDispatchObserver : CapturerObserver {
  24 + private val observers = linkedSetOf<CapturerObserver>()
  25 + private val sinks = linkedSetOf<VideoSink>()
  26 +
  27 + @Synchronized
  28 + fun registerObserver(observer: CapturerObserver) {
  29 + observers.add(observer)
  30 + }
  31 +
  32 + @Synchronized
  33 + fun unregisterObserver(observer: CapturerObserver) {
  34 + observers.remove(observer)
  35 + }
  36 +
  37 + @Synchronized
  38 + fun registerSink(sink: VideoSink) {
  39 + sinks.add(sink)
  40 + }
  41 +
  42 + @Synchronized
  43 + fun unregisterSink(sink: VideoSink) {
  44 + sinks.remove(sink)
  45 + }
  46 +
  47 + @Synchronized
  48 + override fun onCapturerStarted(success: Boolean) {
  49 + for (observer in observers) {
  50 + observer.onCapturerStarted(success)
  51 + }
  52 + }
  53 +
  54 + @Synchronized
  55 + override fun onCapturerStopped() {
  56 + for (observer in observers) {
  57 + observer.onCapturerStopped()
  58 + }
  59 + }
  60 +
  61 + @Synchronized
  62 + override fun onFrameCaptured(frame: VideoFrame) {
  63 + for (observer in observers) {
  64 + observer.onFrameCaptured(frame)
  65 + }
  66 +
  67 + for (sink in sinks) {
  68 + sink.onFrame(frame)
  69 + }
  70 + }
  71 +}