davidliu
Committed by GitHub

Fixes for video processing and examples (#495)

* Add utility class NoDropVideoProcessor to force video processing while not connected

* Fix local video tracks not rendering processed frames
  1 +---
  2 +"client-sdk-android": patch
  3 +---
  4 +
  5 +Fix local video tracks not rendering processed frames
  1 +---
  2 +"client-sdk-android": patch
  3 +---
  4 +
  5 +Add utility class NoDropVideoProcessor to force video processing while not connected
@@ -31,6 +31,7 @@ import com.google.mlkit.vision.common.InputImage @@ -31,6 +31,7 @@ import com.google.mlkit.vision.common.InputImage
31 import com.google.mlkit.vision.segmentation.Segmentation 31 import com.google.mlkit.vision.segmentation.Segmentation
32 import com.google.mlkit.vision.segmentation.Segmenter 32 import com.google.mlkit.vision.segmentation.Segmenter
33 import com.google.mlkit.vision.segmentation.selfie.SelfieSegmenterOptions 33 import com.google.mlkit.vision.segmentation.selfie.SelfieSegmenterOptions
  34 +import io.livekit.android.room.track.video.NoDropVideoProcessor
34 import kotlinx.coroutines.CoroutineDispatcher 35 import kotlinx.coroutines.CoroutineDispatcher
35 import kotlinx.coroutines.CoroutineScope 36 import kotlinx.coroutines.CoroutineScope
36 import kotlinx.coroutines.cancel 37 import kotlinx.coroutines.cancel
@@ -41,13 +42,12 @@ import kotlinx.coroutines.sync.Mutex @@ -41,13 +42,12 @@ import kotlinx.coroutines.sync.Mutex
41 import livekit.org.webrtc.EglBase 42 import livekit.org.webrtc.EglBase
42 import livekit.org.webrtc.SurfaceTextureHelper 43 import livekit.org.webrtc.SurfaceTextureHelper
43 import livekit.org.webrtc.VideoFrame 44 import livekit.org.webrtc.VideoFrame
44 -import livekit.org.webrtc.VideoProcessor  
45 import livekit.org.webrtc.VideoSink 45 import livekit.org.webrtc.VideoSink
46 import livekit.org.webrtc.YuvHelper 46 import livekit.org.webrtc.YuvHelper
47 import java.io.ByteArrayOutputStream 47 import java.io.ByteArrayOutputStream
48 import java.nio.ByteBuffer 48 import java.nio.ByteBuffer
49 49
50 -class SelfieBitmapVideoProcessor(eglBase: EglBase, dispatcher: CoroutineDispatcher) : VideoProcessor { 50 +class SelfieBitmapVideoProcessor(eglBase: EglBase, dispatcher: CoroutineDispatcher) : NoDropVideoProcessor() {
51 51
52 private var targetSink: VideoSink? = null 52 private var targetSink: VideoSink? = null
53 private val segmenter: Segmenter 53 private val segmenter: Segmenter
@@ -138,6 +138,7 @@ class SelfieBitmapVideoProcessor(eglBase: EglBase, dispatcher: CoroutineDispatch @@ -138,6 +138,7 @@ class SelfieBitmapVideoProcessor(eglBase: EglBase, dispatcher: CoroutineDispatch
138 frameBuffer.release() 138 frameBuffer.release()
139 frame.release() 139 frame.release()
140 140
  141 + // Ready for segementation processing.
141 val inputImage = InputImage.fromBitmap(bitmap, 0) 142 val inputImage = InputImage.fromBitmap(bitmap, 0)
142 val task = segmenter.process(inputImage) 143 val task = segmenter.process(inputImage)
143 144
@@ -156,6 +157,7 @@ class SelfieBitmapVideoProcessor(eglBase: EglBase, dispatcher: CoroutineDispatch @@ -156,6 +157,7 @@ class SelfieBitmapVideoProcessor(eglBase: EglBase, dispatcher: CoroutineDispatch
156 } 157 }
157 } 158 }
158 159
  160 + // Prepare for creating the processed video frame.
159 if (lastRotation != rotationDegrees) { 161 if (lastRotation != rotationDegrees) {
160 surfaceTextureHelper?.setFrameRotation(rotationDegrees) 162 surfaceTextureHelper?.setFrameRotation(rotationDegrees)
161 lastRotation = rotationDegrees 163 lastRotation = rotationDegrees
@@ -175,6 +177,7 @@ class SelfieBitmapVideoProcessor(eglBase: EglBase, dispatcher: CoroutineDispatch @@ -175,6 +177,7 @@ class SelfieBitmapVideoProcessor(eglBase: EglBase, dispatcher: CoroutineDispatch
175 } 177 }
176 178
177 if (canvas != null) { 179 if (canvas != null) {
  180 + // Create the video frame.
178 canvas.drawBitmap(bitmap, Matrix(), Paint()) 181 canvas.drawBitmap(bitmap, Matrix(), Paint())
179 surface.unlockCanvasAndPost(canvas) 182 surface.unlockCanvasAndPost(canvas)
180 } 183 }
@@ -21,17 +21,17 @@ import com.google.mlkit.vision.common.InputImage @@ -21,17 +21,17 @@ import com.google.mlkit.vision.common.InputImage
21 import com.google.mlkit.vision.segmentation.Segmentation 21 import com.google.mlkit.vision.segmentation.Segmentation
22 import com.google.mlkit.vision.segmentation.Segmenter 22 import com.google.mlkit.vision.segmentation.Segmenter
23 import com.google.mlkit.vision.segmentation.selfie.SelfieSegmenterOptions 23 import com.google.mlkit.vision.segmentation.selfie.SelfieSegmenterOptions
  24 +import io.livekit.android.room.track.video.NoDropVideoProcessor
24 import kotlinx.coroutines.CoroutineDispatcher 25 import kotlinx.coroutines.CoroutineDispatcher
25 import kotlinx.coroutines.CoroutineScope 26 import kotlinx.coroutines.CoroutineScope
26 import kotlinx.coroutines.channels.BufferOverflow 27 import kotlinx.coroutines.channels.BufferOverflow
27 import kotlinx.coroutines.flow.MutableSharedFlow 28 import kotlinx.coroutines.flow.MutableSharedFlow
28 import kotlinx.coroutines.launch 29 import kotlinx.coroutines.launch
29 import livekit.org.webrtc.VideoFrame 30 import livekit.org.webrtc.VideoFrame
30 -import livekit.org.webrtc.VideoProcessor  
31 import livekit.org.webrtc.VideoSink 31 import livekit.org.webrtc.VideoSink
32 import java.nio.ByteBuffer 32 import java.nio.ByteBuffer
33 33
34 -class SelfieVideoProcessor(dispatcher: CoroutineDispatcher) : VideoProcessor { 34 +class SelfieVideoProcessor(dispatcher: CoroutineDispatcher) : NoDropVideoProcessor() {
35 35
36 private var targetSink: VideoSink? = null 36 private var targetSink: VideoSink? = null
37 private val segmenter: Segmenter 37 private val segmenter: Segmenter
  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.annotations
  18 +
  19 +/**
  20 + * The target marked with this annotation is sensitive to the internal
  21 + * code of WebRTC, and should be directly retested whenever WebRTC version
  22 + * is upgraded.
  23 + */
  24 +@Retention(AnnotationRetention.SOURCE)
  25 +annotation class WebRTCSensitive
@@ -476,13 +476,20 @@ constructor( @@ -476,13 +476,20 @@ constructor(
476 source.setVideoProcessor(videoProcessor) 476 source.setVideoProcessor(videoProcessor)
477 477
478 val surfaceTextureHelper = SurfaceTextureHelper.create("VideoCaptureThread", rootEglBase.eglBaseContext) 478 val surfaceTextureHelper = SurfaceTextureHelper.create("VideoCaptureThread", rootEglBase.eglBaseContext)
479 - val dispatchObserver = CaptureDispatchObserver()  
480 - dispatchObserver.registerObserver(source.capturerObserver) 479 +
  480 + // Dispatch raw frames to local renderer only if not using a VideoProcessor.
  481 + val dispatchObserver = if (videoProcessor == null) {
  482 + CaptureDispatchObserver().apply {
  483 + registerObserver(source.capturerObserver)
  484 + }
  485 + } else {
  486 + null
  487 + }
481 488
482 capturer.initialize( 489 capturer.initialize(
483 surfaceTextureHelper, 490 surfaceTextureHelper,
484 context, 491 context,
485 - dispatchObserver, 492 + dispatchObserver ?: source.capturerObserver,
486 ) 493 )
487 val rtcTrack = peerConnectionFactory.createVideoTrack(UUID.randomUUID().toString(), source) 494 val rtcTrack = peerConnectionFactory.createVideoTrack(UUID.randomUUID().toString(), source)
488 495
  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 io.livekit.android.annotations.WebRTCSensitive
  20 +import livekit.org.webrtc.VideoFrame
  21 +import livekit.org.webrtc.VideoProcessor
  22 +
  23 +/**
  24 + * When not connected to a room, the base [VideoProcessor] implementation will refuse
  25 + * to process frames as they will all be dropped (i.e. not sent).
  26 + *
  27 + * This implementation by default forces all frames to be processed regardless of publish status.
  28 + *
  29 + * Change [allowDropping] to true if you want to allow dropping of frames.
  30 + */
  31 +abstract class NoDropVideoProcessor : VideoProcessor {
  32 + /**
  33 + * If set to false, forces all frames to be processed regardless of publish status.
  34 + * If set to true, frames will only be processed when the associated video track is published.
  35 + *
  36 + * By default, set to false.
  37 + */
  38 + @Suppress("MemberVisibilityCanBePrivate")
  39 + var allowDropping = false
  40 +
  41 + @WebRTCSensitive
  42 + override fun onFrameCaptured(frame: VideoFrame, parameters: VideoProcessor.FrameAdaptationParameters) {
  43 + if (allowDropping) {
  44 + super.onFrameCaptured(frame, parameters)
  45 + } else {
  46 + // Altered from VideoProcessor
  47 + val adaptedFrame = VideoProcessor.applyFrameAdaptationParameters(frame, parameters)
  48 + if (adaptedFrame != null) {
  49 + this.onFrameCaptured(adaptedFrame)
  50 + adaptedFrame.release()
  51 + } else {
  52 + this.onFrameCaptured(frame)
  53 + }
  54 + }
  55 + }
  56 +}