2020-04-06 11:01:17 +00:00
package eu.siacs.conversations.xmpp.jingle ;
import android.content.Context ;
2020-04-14 17:06:39 +00:00
import android.os.Build ;
2020-04-13 10:02:34 +00:00
import android.os.Handler ;
import android.os.Looper ;
2020-04-06 13:45:06 +00:00
import android.util.Log ;
2020-04-06 11:01:17 +00:00
2020-04-14 17:06:39 +00:00
import com.google.common.base.Optional ;
import com.google.common.base.Preconditions ;
2020-04-06 11:01:17 +00:00
import com.google.common.util.concurrent.Futures ;
import com.google.common.util.concurrent.ListenableFuture ;
import com.google.common.util.concurrent.MoreExecutors ;
import com.google.common.util.concurrent.SettableFuture ;
import org.webrtc.AudioSource ;
import org.webrtc.AudioTrack ;
2020-04-07 09:36:28 +00:00
import org.webrtc.Camera1Enumerator ;
2020-04-14 17:06:39 +00:00
import org.webrtc.Camera2Enumerator ;
2020-04-16 07:03:39 +00:00
import org.webrtc.CameraEnumerationAndroid ;
2020-04-14 17:06:39 +00:00
import org.webrtc.CameraEnumerator ;
2020-04-07 09:36:28 +00:00
import org.webrtc.CameraVideoCapturer ;
2020-04-08 11:30:12 +00:00
import org.webrtc.CandidatePairChangeEvent ;
2020-04-06 11:01:17 +00:00
import org.webrtc.DataChannel ;
2020-04-14 17:06:39 +00:00
import org.webrtc.DefaultVideoDecoderFactory ;
import org.webrtc.DefaultVideoEncoderFactory ;
import org.webrtc.EglBase ;
2020-04-06 11:01:17 +00:00
import org.webrtc.IceCandidate ;
import org.webrtc.MediaConstraints ;
import org.webrtc.MediaStream ;
import org.webrtc.PeerConnection ;
import org.webrtc.PeerConnectionFactory ;
import org.webrtc.RtpReceiver ;
import org.webrtc.SdpObserver ;
import org.webrtc.SessionDescription ;
2020-04-14 17:06:39 +00:00
import org.webrtc.SurfaceTextureHelper ;
2020-04-07 09:36:28 +00:00
import org.webrtc.VideoSource ;
import org.webrtc.VideoTrack ;
2020-04-06 11:01:17 +00:00
2020-04-16 07:03:39 +00:00
import java.util.ArrayList ;
import java.util.Collections ;
2020-04-06 11:01:17 +00:00
import java.util.List ;
2020-04-13 10:02:34 +00:00
import java.util.Set ;
2020-04-06 11:01:17 +00:00
import javax.annotation.Nonnull ;
import javax.annotation.Nullable ;
2020-04-06 13:45:06 +00:00
import eu.siacs.conversations.Config ;
2020-04-13 10:02:34 +00:00
import eu.siacs.conversations.services.AppRTCAudioManager ;
2020-04-06 13:45:06 +00:00
2020-04-06 11:01:17 +00:00
public class WebRTCWrapper {
2020-04-16 07:03:39 +00:00
private static final int CAPTURING_RESOLUTION = 1920 ;
private static final int CAPTURING_MAX_FRAME_RATE = 30 ;
2020-04-14 17:06:39 +00:00
private final EventCallback eventCallback ;
private final AppRTCAudioManager . AudioManagerEvents audioManagerEvents = new AppRTCAudioManager . AudioManagerEvents ( ) {
@Override
public void onAudioDeviceChanged ( AppRTCAudioManager . AudioDevice selectedAudioDevice , Set < AppRTCAudioManager . AudioDevice > availableAudioDevices ) {
eventCallback . onAudioDeviceChanged ( selectedAudioDevice , availableAudioDevices ) ;
}
} ;
private final Handler mainHandler = new Handler ( Looper . getMainLooper ( ) ) ;
2020-04-07 09:36:28 +00:00
private VideoTrack localVideoTrack = null ;
private VideoTrack remoteVideoTrack = null ;
2020-04-06 11:01:17 +00:00
private final PeerConnection . Observer peerConnectionObserver = new PeerConnection . Observer ( ) {
@Override
public void onSignalingChange ( PeerConnection . SignalingState signalingState ) {
2020-04-06 13:45:06 +00:00
Log . d ( Config . LOGTAG , " onSignalingChange( " + signalingState + " ) " ) ;
2020-04-13 16:30:12 +00:00
//this is called after removeTrack or addTrack
//and should then trigger a content-add or content-remove or something
//https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/removeTrack
2020-04-06 13:45:06 +00:00
}
2020-04-06 11:01:17 +00:00
2020-04-06 13:45:06 +00:00
@Override
public void onConnectionChange ( PeerConnection . PeerConnectionState newState ) {
2020-04-07 11:15:24 +00:00
eventCallback . onConnectionChange ( newState ) ;
2020-04-06 11:01:17 +00:00
}
@Override
public void onIceConnectionChange ( PeerConnection . IceConnectionState iceConnectionState ) {
}
2020-04-08 11:30:12 +00:00
@Override
public void onSelectedCandidatePairChanged ( CandidatePairChangeEvent event ) {
Log . d ( Config . LOGTAG , " remote candidate selected: " + event . remote ) ;
Log . d ( Config . LOGTAG , " local candidate selected: " + event . local ) ;
}
2020-04-06 11:01:17 +00:00
@Override
public void onIceConnectionReceivingChange ( boolean b ) {
}
@Override
public void onIceGatheringChange ( PeerConnection . IceGatheringState iceGatheringState ) {
}
@Override
public void onIceCandidate ( IceCandidate iceCandidate ) {
eventCallback . onIceCandidate ( iceCandidate ) ;
}
@Override
public void onIceCandidatesRemoved ( IceCandidate [ ] iceCandidates ) {
}
@Override
public void onAddStream ( MediaStream mediaStream ) {
2020-04-06 13:45:06 +00:00
Log . d ( Config . LOGTAG , " onAddStream " ) ;
2020-04-07 09:36:28 +00:00
final List < VideoTrack > videoTracks = mediaStream . videoTracks ;
if ( videoTracks . size ( ) > 0 ) {
Log . d ( Config . LOGTAG , " more than zero remote video tracks found. using first " ) ;
remoteVideoTrack = videoTracks . get ( 0 ) ;
}
2020-04-06 11:01:17 +00:00
}
@Override
public void onRemoveStream ( MediaStream mediaStream ) {
}
@Override
public void onDataChannel ( DataChannel dataChannel ) {
}
@Override
public void onRenegotiationNeeded ( ) {
}
@Override
public void onAddTrack ( RtpReceiver rtpReceiver , MediaStream [ ] mediaStreams ) {
2020-04-06 13:45:06 +00:00
Log . d ( Config . LOGTAG , " onAddTrack() " ) ;
2020-04-06 11:01:17 +00:00
}
} ;
@Nullable
private PeerConnection peerConnection = null ;
2020-04-13 10:53:23 +00:00
private AudioTrack localAudioTrack = null ;
2020-04-13 10:02:34 +00:00
private AppRTCAudioManager appRTCAudioManager = null ;
2020-04-14 17:06:39 +00:00
private Context context = null ;
private EglBase eglBase = null ;
2020-04-17 12:16:39 +00:00
private CapturerChoice capturerChoice ;
2020-04-06 11:01:17 +00:00
public WebRTCWrapper ( final EventCallback eventCallback ) {
this . eventCallback = eventCallback ;
}
2020-04-15 16:47:15 +00:00
public void setup ( final Context context , final AppRTCAudioManager . SpeakerPhonePreference speakerPhonePreference ) {
2020-04-06 11:01:17 +00:00
PeerConnectionFactory . initialize (
PeerConnectionFactory . InitializationOptions . builder ( context ) . createInitializationOptions ( )
) ;
2020-04-14 17:06:39 +00:00
this . eglBase = EglBase . create ( ) ;
this . context = context ;
2020-04-13 10:02:34 +00:00
mainHandler . post ( ( ) - > {
2020-04-15 16:47:15 +00:00
appRTCAudioManager = AppRTCAudioManager . create ( context , speakerPhonePreference ) ;
2020-04-13 12:55:07 +00:00
appRTCAudioManager . start ( audioManagerEvents ) ;
eventCallback . onAudioDeviceChanged ( appRTCAudioManager . getSelectedAudioDevice ( ) , appRTCAudioManager . getAudioDevices ( ) ) ;
2020-04-13 10:02:34 +00:00
} ) ;
2020-04-06 11:01:17 +00:00
}
2020-04-15 10:07:19 +00:00
public void initializePeerConnection ( final Set < Media > media , final List < PeerConnection . IceServer > iceServers ) throws InitializationException {
2020-04-14 17:06:39 +00:00
Preconditions . checkState ( this . eglBase ! = null ) ;
2020-04-15 10:07:19 +00:00
Preconditions . checkNotNull ( media ) ;
Preconditions . checkArgument ( media . size ( ) > 0 , " media can not be empty when initializing peer connection " ) ;
2020-04-14 17:06:39 +00:00
PeerConnectionFactory peerConnectionFactory = PeerConnectionFactory . builder ( )
. setVideoDecoderFactory ( new DefaultVideoDecoderFactory ( eglBase . getEglBaseContext ( ) ) )
. setVideoEncoderFactory ( new DefaultVideoEncoderFactory ( eglBase . getEglBaseContext ( ) , true , true ) )
. createPeerConnectionFactory ( ) ;
2020-04-07 09:36:28 +00:00
2020-04-14 17:06:39 +00:00
final MediaStream stream = peerConnectionFactory . createLocalMediaStream ( " my-media-stream " ) ;
2020-04-07 09:36:28 +00:00
2020-04-17 12:16:39 +00:00
final Optional < CapturerChoice > optionalCapturerChoice = media . contains ( Media . VIDEO ) ? getVideoCapturer ( ) : Optional . absent ( ) ;
2020-04-07 09:36:28 +00:00
2020-04-17 12:16:39 +00:00
if ( optionalCapturerChoice . isPresent ( ) ) {
this . capturerChoice = optionalCapturerChoice . get ( ) ;
final CameraVideoCapturer capturer = this . capturerChoice . cameraVideoCapturer ;
2020-04-14 17:06:39 +00:00
final VideoSource videoSource = peerConnectionFactory . createVideoSource ( false ) ;
SurfaceTextureHelper surfaceTextureHelper = SurfaceTextureHelper . create ( " webrtc " , eglBase . getEglBaseContext ( ) ) ;
capturer . initialize ( surfaceTextureHelper , requireContext ( ) , videoSource . getCapturerObserver ( ) ) ;
2020-04-17 12:16:39 +00:00
Log . d ( Config . LOGTAG , String . format ( " start capturing at %dx%d@%d " , capturerChoice . captureFormat . width , capturerChoice . captureFormat . height , capturerChoice . getFrameRate ( ) ) ) ;
capturer . startCapture ( capturerChoice . captureFormat . width , capturerChoice . captureFormat . height , capturerChoice . getFrameRate ( ) ) ;
2020-04-07 09:36:28 +00:00
2020-04-14 17:06:39 +00:00
this . localVideoTrack = peerConnectionFactory . createVideoTrack ( " my-video-track " , videoSource ) ;
2020-04-07 09:36:28 +00:00
2020-04-14 17:06:39 +00:00
stream . addTrack ( this . localVideoTrack ) ;
2020-04-07 09:36:28 +00:00
}
2020-04-15 10:07:19 +00:00
if ( media . contains ( Media . AUDIO ) ) {
//set up audio track
final AudioSource audioSource = peerConnectionFactory . createAudioSource ( new MediaConstraints ( ) ) ;
this . localAudioTrack = peerConnectionFactory . createAudioTrack ( " my-audio-track " , audioSource ) ;
stream . addTrack ( this . localAudioTrack ) ;
}
2020-04-07 09:36:28 +00:00
2020-04-06 11:01:17 +00:00
final PeerConnection peerConnection = peerConnectionFactory . createPeerConnection ( iceServers , peerConnectionObserver ) ;
if ( peerConnection = = null ) {
2020-04-09 13:22:03 +00:00
throw new InitializationException ( " Unable to create PeerConnection " ) ;
2020-04-06 11:01:17 +00:00
}
peerConnection . addStream ( stream ) ;
2020-04-06 13:45:06 +00:00
peerConnection . setAudioPlayout ( true ) ;
peerConnection . setAudioRecording ( true ) ;
2020-04-06 11:01:17 +00:00
this . peerConnection = peerConnection ;
}
2020-04-13 12:55:07 +00:00
2020-04-08 13:27:17 +00:00
public void close ( ) {
final PeerConnection peerConnection = this . peerConnection ;
2020-04-17 12:16:39 +00:00
final CapturerChoice capturerChoice = this . capturerChoice ;
2020-04-15 10:07:19 +00:00
final AppRTCAudioManager audioManager = this . appRTCAudioManager ;
final EglBase eglBase = this . eglBase ;
2020-04-08 13:27:17 +00:00
if ( peerConnection ! = null ) {
2020-04-14 17:06:39 +00:00
peerConnection . dispose ( ) ;
2020-04-18 16:22:10 +00:00
this . peerConnection = null ;
2020-04-08 13:27:17 +00:00
}
2020-04-13 10:02:34 +00:00
if ( audioManager ! = null ) {
mainHandler . post ( audioManager : : stop ) ;
}
2020-04-14 19:06:26 +00:00
this . localVideoTrack = null ;
this . remoteVideoTrack = null ;
2020-04-17 12:16:39 +00:00
if ( capturerChoice ! = null ) {
2020-04-14 19:06:26 +00:00
try {
2020-04-17 12:16:39 +00:00
capturerChoice . cameraVideoCapturer . stopCapture ( ) ;
2020-04-14 19:06:26 +00:00
} catch ( InterruptedException e ) {
2020-04-15 10:07:19 +00:00
Log . e ( Config . LOGTAG , " unable to stop capturing " ) ;
2020-04-14 19:06:26 +00:00
}
}
2020-04-15 10:07:19 +00:00
if ( eglBase ! = null ) {
eglBase . release ( ) ;
2020-04-18 16:22:10 +00:00
this . eglBase = null ;
}
}
void verifyClosed ( ) {
if ( this . peerConnection ! = null
| | this . eglBase ! = null
| | this . localVideoTrack ! = null
| | this . remoteVideoTrack ! = null ) {
throw new IllegalStateException ( " WebRTCWrapper hasn't been closed properly " ) ;
2020-04-15 10:07:19 +00:00
}
2020-04-08 13:27:17 +00:00
}
2020-04-15 17:16:47 +00:00
boolean isMicrophoneEnabled ( ) {
2020-04-13 10:53:23 +00:00
final AudioTrack audioTrack = this . localAudioTrack ;
if ( audioTrack = = null ) {
throw new IllegalStateException ( " Local audio track does not exist (yet) " ) ;
}
2020-04-14 17:06:39 +00:00
return audioTrack . enabled ( ) ;
2020-04-13 10:53:23 +00:00
}
2020-04-15 17:16:47 +00:00
void setMicrophoneEnabled ( final boolean enabled ) {
2020-04-13 10:53:23 +00:00
final AudioTrack audioTrack = this . localAudioTrack ;
if ( audioTrack = = null ) {
throw new IllegalStateException ( " Local audio track does not exist (yet) " ) ;
}
2020-04-14 17:06:39 +00:00
audioTrack . setEnabled ( enabled ) ;
2020-04-13 10:53:23 +00:00
}
2020-04-15 17:16:47 +00:00
public boolean isVideoEnabled ( ) {
final VideoTrack videoTrack = this . localVideoTrack ;
if ( videoTrack = = null ) {
throw new IllegalStateException ( " Local video track does not exist " ) ;
}
return videoTrack . enabled ( ) ;
}
public void setVideoEnabled ( final boolean enabled ) {
final VideoTrack videoTrack = this . localVideoTrack ;
if ( videoTrack = = null ) {
throw new IllegalStateException ( " Local video track does not exist " ) ;
}
videoTrack . setEnabled ( enabled ) ;
}
2020-04-06 11:01:17 +00:00
public ListenableFuture < SessionDescription > createOffer ( ) {
return Futures . transformAsync ( getPeerConnectionFuture ( ) , peerConnection - > {
final SettableFuture < SessionDescription > future = SettableFuture . create ( ) ;
peerConnection . createOffer ( new CreateSdpObserver ( ) {
@Override
public void onCreateSuccess ( SessionDescription sessionDescription ) {
future . set ( sessionDescription ) ;
}
@Override
public void onCreateFailure ( String s ) {
2020-04-14 17:06:39 +00:00
Log . d ( Config . LOGTAG , " create failure " + s ) ;
2020-04-06 11:01:17 +00:00
future . setException ( new IllegalStateException ( " Unable to create offer: " + s ) ) ;
}
} , new MediaConstraints ( ) ) ;
return future ;
} , MoreExecutors . directExecutor ( ) ) ;
}
public ListenableFuture < SessionDescription > createAnswer ( ) {
return Futures . transformAsync ( getPeerConnectionFuture ( ) , peerConnection - > {
final SettableFuture < SessionDescription > future = SettableFuture . create ( ) ;
peerConnection . createAnswer ( new CreateSdpObserver ( ) {
@Override
public void onCreateSuccess ( SessionDescription sessionDescription ) {
future . set ( sessionDescription ) ;
}
@Override
public void onCreateFailure ( String s ) {
future . setException ( new IllegalStateException ( " Unable to create answer: " + s ) ) ;
}
} , new MediaConstraints ( ) ) ;
return future ;
} , MoreExecutors . directExecutor ( ) ) ;
}
public ListenableFuture < Void > setLocalDescription ( final SessionDescription sessionDescription ) {
return Futures . transformAsync ( getPeerConnectionFuture ( ) , peerConnection - > {
final SettableFuture < Void > future = SettableFuture . create ( ) ;
peerConnection . setLocalDescription ( new SetSdpObserver ( ) {
@Override
public void onSetSuccess ( ) {
future . set ( null ) ;
}
@Override
public void onSetFailure ( String s ) {
2020-04-14 17:06:39 +00:00
Log . d ( Config . LOGTAG , " unable to set local " + s ) ;
2020-04-06 13:45:06 +00:00
future . setException ( new IllegalArgumentException ( " unable to set local session description: " + s ) ) ;
2020-04-06 11:01:17 +00:00
}
} , sessionDescription ) ;
return future ;
} , MoreExecutors . directExecutor ( ) ) ;
}
public ListenableFuture < Void > setRemoteDescription ( final SessionDescription sessionDescription ) {
return Futures . transformAsync ( getPeerConnectionFuture ( ) , peerConnection - > {
final SettableFuture < Void > future = SettableFuture . create ( ) ;
peerConnection . setRemoteDescription ( new SetSdpObserver ( ) {
@Override
public void onSetSuccess ( ) {
future . set ( null ) ;
}
@Override
public void onSetFailure ( String s ) {
2020-04-06 13:45:06 +00:00
future . setException ( new IllegalArgumentException ( " unable to set remote session description: " + s ) ) ;
2020-04-06 11:01:17 +00:00
}
} , sessionDescription ) ;
return future ;
} , MoreExecutors . directExecutor ( ) ) ;
}
@Nonnull
private ListenableFuture < PeerConnection > getPeerConnectionFuture ( ) {
final PeerConnection peerConnection = this . peerConnection ;
if ( peerConnection = = null ) {
return Futures . immediateFailedFuture ( new IllegalStateException ( " initialize PeerConnection first " ) ) ;
} else {
return Futures . immediateFuture ( peerConnection ) ;
}
}
public void addIceCandidate ( IceCandidate iceCandidate ) {
2020-04-07 12:22:12 +00:00
requirePeerConnection ( ) . addIceCandidate ( iceCandidate ) ;
}
2020-04-14 17:06:39 +00:00
private CameraEnumerator getCameraEnumerator ( ) {
if ( Build . VERSION . SDK_INT > = Build . VERSION_CODES . LOLLIPOP ) {
return new Camera2Enumerator ( requireContext ( ) ) ;
} else {
return new Camera1Enumerator ( ) ;
}
}
2020-04-16 07:03:39 +00:00
private Optional < CapturerChoice > getVideoCapturer ( ) {
2020-04-14 17:06:39 +00:00
final CameraEnumerator enumerator = getCameraEnumerator ( ) ;
final String [ ] deviceNames = enumerator . getDeviceNames ( ) ;
2020-04-14 19:06:26 +00:00
for ( final String deviceName : deviceNames ) {
2020-04-14 17:06:39 +00:00
if ( enumerator . isFrontFacing ( deviceName ) ) {
2020-04-16 07:03:39 +00:00
return Optional . fromNullable ( of ( enumerator , deviceName ) ) ;
2020-04-14 17:06:39 +00:00
}
}
if ( deviceNames . length = = 0 ) {
return Optional . absent ( ) ;
} else {
2020-04-16 07:03:39 +00:00
return Optional . fromNullable ( of ( enumerator , deviceNames [ 0 ] ) ) ;
}
}
@Nullable
private static CapturerChoice of ( CameraEnumerator enumerator , final String deviceName ) {
final CameraVideoCapturer capturer = enumerator . createCapturer ( deviceName , null ) ;
if ( capturer = = null ) {
return null ;
}
final ArrayList < CameraEnumerationAndroid . CaptureFormat > choices = new ArrayList < > ( enumerator . getSupportedFormats ( deviceName ) ) ;
Collections . sort ( choices , ( a , b ) - > b . width - a . width ) ;
for ( final CameraEnumerationAndroid . CaptureFormat captureFormat : choices ) {
if ( captureFormat . width < = CAPTURING_RESOLUTION ) {
return new CapturerChoice ( capturer , captureFormat ) ;
}
2020-04-14 17:06:39 +00:00
}
2020-04-16 07:03:39 +00:00
return null ;
2020-04-14 17:06:39 +00:00
}
2020-04-07 12:22:12 +00:00
public PeerConnection . PeerConnectionState getState ( ) {
return requirePeerConnection ( ) . connectionState ( ) ;
}
2020-04-16 07:03:39 +00:00
EglBase . Context getEglBaseContext ( ) {
2020-04-14 17:06:39 +00:00
return this . eglBase . getEglBaseContext ( ) ;
}
public Optional < VideoTrack > getLocalVideoTrack ( ) {
return Optional . fromNullable ( this . localVideoTrack ) ;
}
public Optional < VideoTrack > getRemoteVideoTrack ( ) {
return Optional . fromNullable ( this . remoteVideoTrack ) ;
}
2020-04-07 12:22:12 +00:00
private PeerConnection requirePeerConnection ( ) {
2020-04-06 11:01:17 +00:00
final PeerConnection peerConnection = this . peerConnection ;
if ( peerConnection = = null ) {
throw new IllegalStateException ( " initialize PeerConnection first " ) ;
}
2020-04-07 12:22:12 +00:00
return peerConnection ;
2020-04-06 13:45:06 +00:00
}
2020-04-14 17:06:39 +00:00
private Context requireContext ( ) {
final Context context = this . context ;
if ( context = = null ) {
throw new IllegalStateException ( " call setup first " ) ;
}
return context ;
}
2020-04-13 10:53:23 +00:00
public AppRTCAudioManager getAudioManager ( ) {
return appRTCAudioManager ;
}
2020-04-14 17:06:39 +00:00
public interface EventCallback {
void onIceCandidate ( IceCandidate iceCandidate ) ;
void onConnectionChange ( PeerConnection . PeerConnectionState newState ) ;
void onAudioDeviceChanged ( AppRTCAudioManager . AudioDevice selectedAudioDevice , Set < AppRTCAudioManager . AudioDevice > availableAudioDevices ) ;
}
2020-04-06 11:01:17 +00:00
private static abstract class SetSdpObserver implements SdpObserver {
@Override
public void onCreateSuccess ( org . webrtc . SessionDescription sessionDescription ) {
throw new IllegalStateException ( " Not able to use SetSdpObserver " ) ;
}
@Override
public void onCreateFailure ( String s ) {
throw new IllegalStateException ( " Not able to use SetSdpObserver " ) ;
}
}
private static abstract class CreateSdpObserver implements SdpObserver {
@Override
public void onSetSuccess ( ) {
throw new IllegalStateException ( " Not able to use CreateSdpObserver " ) ;
}
@Override
public void onSetFailure ( String s ) {
throw new IllegalStateException ( " Not able to use CreateSdpObserver " ) ;
}
}
2020-04-09 13:22:03 +00:00
public static class InitializationException extends Exception {
private InitializationException ( String message ) {
super ( message ) ;
}
}
2020-04-16 07:03:39 +00:00
private static class CapturerChoice {
private final CameraVideoCapturer cameraVideoCapturer ;
private final CameraEnumerationAndroid . CaptureFormat captureFormat ;
public CapturerChoice ( CameraVideoCapturer cameraVideoCapturer , CameraEnumerationAndroid . CaptureFormat captureFormat ) {
this . cameraVideoCapturer = cameraVideoCapturer ;
this . captureFormat = captureFormat ;
}
public int getFrameRate ( ) {
return Math . max ( captureFormat . framerate . min , Math . min ( CAPTURING_MAX_FRAME_RATE , captureFormat . framerate . max ) ) ;
}
}
2020-04-06 11:01:17 +00:00
}