Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 13 additions & 5 deletions client/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,24 @@ android {
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
compileOptions {
targetCompatibility 1.8
sourceCompatibility 1.8
}
}

tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
kotlinOptions.jvmTarget = 1.8
}

dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
compile 'org.webrtc:google-webrtc:1.0.19742'
compile 'com.squareup.okhttp3:okhttp:3.7.0'
implementation 'org.webrtc:google-webrtc:1.0.27306'
implementation 'com.squareup.okhttp3:okhttp:3.10.0'
implementation 'com.android.support:appcompat-v7:26.1.0'
implementation 'com.android.support.constraint:constraint-layout:1.0.2'
implementation 'com.android.support.constraint:constraint-layout:1.1.3'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.1'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import org.webrtc.RendererCommon
import org.webrtc.SurfaceViewRenderer
import ventures.webrtc.webrtcroulette.videocall.VideoCallSession
import ventures.webrtc.webrtcroulette.videocall.VideoCallStatus
import ventures.webrtc.webrtcroulette.videocall.VideoRenderers
import ventures.webrtc.webrtcroulette.videocall.VideoSinks

class VideoCallActivity : AppCompatActivity() {

Expand Down Expand Up @@ -88,7 +88,7 @@ class VideoCallActivity : AppCompatActivity() {
}

private fun startVideoSession() {
videoSession = VideoCallSession.connect(this, BACKEND_URL, VideoRenderers(localVideoView, remoteVideoView), this::onStatusChanged)
videoSession = VideoCallSession.connect(applicationContext, BACKEND_URL, VideoSinks(localVideoView, remoteVideoView), this::onStatusChanged)

localVideoView?.init(videoSession?.renderContext, null)
localVideoView?.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FIT)
Expand All @@ -115,8 +115,8 @@ class VideoCallActivity : AppCompatActivity() {
}

companion object {
private val CAMERA_AUDIO_PERMISSION_REQUEST = 1
private val TAG = "VideoCallActivity"
private val BACKEND_URL = "ws://HOST:8000/" // Change HOST to your server's IP if you want to test
const val CAMERA_AUDIO_PERMISSION_REQUEST = 1
const val TAG = "VideoCallActivity"
const val BACKEND_URL = "ws://HOST:8000/" // Change HOST to your server's IP if you want to test
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ enum class MessageType(val value: String) {

open class ClientMessage(val type: MessageType)

data class SDPMessage(val sdp: String) : ClientMessage(MessageType.SDPMessage)
data class SDPMessage(val sdpType: Int, val sdp: String) : ClientMessage(MessageType.SDPMessage)
data class ICEMessage(val label: Int, val id: String, val candidate: String) : ClientMessage(MessageType.ICEMessage)
data class MatchMessage(val match: String, val offer: Boolean) : ClientMessage(MessageType.MatchMessage)
class PeerLeft : ClientMessage(MessageType.PeerLeft)
Expand All @@ -44,7 +44,7 @@ class SignalingWebSocket : WebSocketListener() {
val clientMessage =
when(type) {
"sdp" ->
SDPMessage(json.getString("sdp"))
SDPMessage(json.getInt("sdpType"), json.getString("sdp"))
"ice" ->
ICEMessage(json.getInt("label"), json.getString("id"), json.getString("candidate"))
"matched" ->
Expand Down Expand Up @@ -74,6 +74,7 @@ class SignalingWebSocket : WebSocketListener() {
json.put("type", clientMessage.type)
when(clientMessage) {
is SDPMessage -> {
json.put("sdpType", clientMessage.sdpType)
json.put("sdp", clientMessage.sdp)
}
is ICEMessage -> {
Expand All @@ -94,8 +95,8 @@ class SignalingWebSocket : WebSocketListener() {
webSocket?.close(1000, null)
}

fun sendSDP(sdp: String) {
send(SDPMessage(sdp))
fun sendSDP(type: Int, sdp: String) {
send(SDPMessage(type, sdp))
}

fun sendCandidate(label: Int, id: String, candidate: String) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,23 +22,16 @@ enum class VideoCallStatus(val label: Int, val color: Int) {
FINISHED(R.string.status_finished, R.color.colorConnected);
}

data class VideoRenderers(private val localView: SurfaceViewRenderer?, private val remoteView: SurfaceViewRenderer?) {
val localRenderer: (VideoRenderer.I420Frame) -> Unit =
if(localView == null) this::sink else { f -> localView.renderFrame(f) }
val remoteRenderer: (VideoRenderer.I420Frame) -> Unit =
if(remoteView == null) this::sink else { f -> remoteView.renderFrame(f) }

private fun sink(frame: VideoRenderer.I420Frame) {
Log.w("VideoRenderer", "Missing surface view, dropping frame")
VideoRenderer.renderFrameDone(frame)
}
}
data class VideoSinks(
val localView: SurfaceViewRenderer?,
val remoteView: SurfaceViewRenderer?
)

class VideoCallSession(
private val context: Context,
private val onStatusChangedListener: (VideoCallStatus) -> Unit,
private val signaler: SignalingWebSocket,
private val videoRenderers: VideoRenderers) {
private val videoSinks: VideoSinks) {

private var peerConnection : PeerConnection? = null
private var factory : PeerConnectionFactory? = null
Expand All @@ -48,6 +41,12 @@ class VideoCallSession(
private val eglBase = EglBase.create()
private var videoCapturer: VideoCapturer? = null

private val mediaConstraints = MediaConstraints().apply {
mandatory.add(MediaConstraints.KeyValuePair("OfferToReceiveAudio", "true"))
mandatory.add(MediaConstraints.KeyValuePair("OfferToReceiveVideo", "true"))
optional.add(MediaConstraints.KeyValuePair("RtpDataChannels", "true"))
}

val renderContext: EglBase.Context
get() = eglBase.eglBaseContext

Expand Down Expand Up @@ -92,24 +91,26 @@ class VideoCallSession(
}

private fun init() {
PeerConnectionFactory.initializeAndroidGlobals(context, true)
val opts = PeerConnectionFactory.Options()
opts.networkIgnoreMask = 0
val createInitializationOptions = PeerConnectionFactory.InitializationOptions.builder(context).createInitializationOptions()
PeerConnectionFactory.initialize(createInitializationOptions)

factory = PeerConnectionFactory(opts)
factory?.setVideoHwAccelerationOptions(eglBase.eglBaseContext, eglBase.eglBaseContext)
factory = PeerConnectionFactory.builder()
.setVideoDecoderFactory(DefaultVideoDecoderFactory(eglBase.eglBaseContext))
.setVideoEncoderFactory(DefaultVideoEncoderFactory(eglBase.eglBaseContext, true, true))
.setOptions(PeerConnectionFactory.Options())
.createPeerConnectionFactory()

val iceServers = arrayListOf(
PeerConnection.IceServer.builder("stun:stun.l.google.com:19302").createIceServer()
PeerConnection.IceServer
.builder("stun:stun.l.google.com:19302")
.createIceServer()
)

val constraints = MediaConstraints()
constraints.mandatory.add(MediaConstraints.KeyValuePair("OfferToReceiveAudio", "true"))
constraints.mandatory.add(MediaConstraints.KeyValuePair("OfferToReceiveVideo", "true"))
val rtcCfg = PeerConnection.RTCConfiguration(iceServers)
rtcCfg.continualGatheringPolicy = PeerConnection.ContinualGatheringPolicy.GATHER_CONTINUALLY
val rtcConfig = PeerConnection.RTCConfiguration(iceServers)
rtcConfig.sdpSemantics = PeerConnection.SdpSemantics.UNIFIED_PLAN
rtcConfig.continualGatheringPolicy = PeerConnection.ContinualGatheringPolicy.GATHER_CONTINUALLY
val rtcEvents = SimpleRTCEventHandler(this::handleLocalIceCandidate, this::addRemoteStream, this::removeRemoteStream)
peerConnection = factory?.createPeerConnection(rtcCfg, constraints, rtcEvents)
peerConnection = factory?.createPeerConnection(rtcConfig, rtcEvents)
setupMediaDevices()
}

Expand All @@ -119,7 +120,7 @@ class VideoCallSession(

private fun maybeCreateOffer() {
if(isOfferingPeer) {
peerConnection?.createOffer(SDPCreateCallback(this::createDescriptorCallback), MediaConstraints())
peerConnection?.createOffer(SDPCreateCallback(this::createDescriptorCallback), this.mediaConstraints)
}
}

Expand All @@ -135,7 +136,7 @@ class VideoCallSession(
if(stream.videoTracks.isNotEmpty()) {
val remoteVideoTrack = stream.videoTracks.first()
remoteVideoTrack.setEnabled(true)
remoteVideoTrack.addRenderer(VideoRenderer(videoRenderers.remoteRenderer))
remoteVideoTrack.addSink(videoSinks.remoteView)
}
}
}
Expand All @@ -155,74 +156,50 @@ class VideoCallSession(
}

private fun setupMediaDevices() {

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
val camera2 = Camera2Enumerator(context)
if(camera2.deviceNames.isNotEmpty()) {
val selectedDevice = camera2.deviceNames.firstOrNull(camera2::isFrontFacing) ?: camera2.deviceNames.first()
videoCapturer = camera2.createCapturer(selectedDevice, null)
}
}
if(videoCapturer == null) {
if (videoCapturer == null) {
val camera1 = Camera1Enumerator(true)
val selectedDevice = camera1.deviceNames.firstOrNull(camera1::isFrontFacing) ?: camera1.deviceNames.first()
videoCapturer = camera1.createCapturer(selectedDevice, null)
}


videoSource = factory?.createVideoSource(videoCapturer)

videoCapturer?.startCapture(640, 480, 24)

val stream = factory?.createLocalMediaStream(STREAM_LABEL)
val surfaceTextureHelper = SurfaceTextureHelper.create("CaptureThread", renderContext)
videoSource = factory?.createVideoSource(videoCapturer?.isScreencast ?: false)
videoSource?.capturerObserver?.let { videoCapturer?.initialize(surfaceTextureHelper, context, it) }
val videoTrack = factory?.createVideoTrack(VIDEO_TRACK_LABEL, videoSource)
videoTrack?.addSink(videoSinks.localView)

val videoRenderer = VideoRenderer(videoRenderers.localRenderer)
videoTrack?.addRenderer(videoRenderer)
stream?.addTrack(videoTrack)
videoCapturer?.startCapture(640, 480, 24)
peerConnection?.addTrack(videoTrack, listOf("video0"))

audioSource = factory?.createAudioSource(createAudioConstraints())
audioSource = factory?.createAudioSource(this.mediaConstraints)
val audioTrack = factory?.createAudioTrack(AUDIO_TRACK_LABEL, audioSource)

stream?.addTrack(audioTrack)

peerConnection?.addStream(stream)
}

private fun createAudioConstraints(): MediaConstraints {
val audioConstraints = MediaConstraints()
audioConstraints.mandatory.add(MediaConstraints.KeyValuePair("googEchoCancellation", "false"))
audioConstraints.mandatory.add(MediaConstraints.KeyValuePair("googAutoGainControl", "false"))
audioConstraints.mandatory.add(MediaConstraints.KeyValuePair("googHighpassFilter", "false"))
audioConstraints.mandatory.add(MediaConstraints.KeyValuePair("googNoiseSuppression", "false"))
return audioConstraints
peerConnection?.addTrack(audioTrack)
}

private fun handleRemoteDescriptor(sdp: String) {
if(isOfferingPeer) {
peerConnection?.setRemoteDescription(SDPSetCallback({ setError ->
if(setError != null) {
Log.e(TAG, "setRemoteDescription failed: $setError")
}
}), SessionDescription(SessionDescription.Type.ANSWER, sdp))
} else {
peerConnection?.setRemoteDescription(SDPSetCallback({ setError ->
if(setError != null) {
Log.e(TAG, "setRemoteDescription failed: $setError")
} else {
peerConnection?.createAnswer(SDPCreateCallback(this::createDescriptorCallback), MediaConstraints())
}
}), SessionDescription(SessionDescription.Type.OFFER, sdp))
}
private fun handleRemoteDescriptor(sdp: SessionDescription) {
peerConnection?.setRemoteDescription(SDPSetCallback { setError ->
if (setError != null) {
Log.e(TAG, "setRemoteDescription failed: $setError")
} else if (!isOfferingPeer) {
peerConnection?.createAnswer(SDPCreateCallback(this::createDescriptorCallback), this.mediaConstraints)
}
}, sdp)
}

private fun createDescriptorCallback(result: SDPCreateResult) {
when(result) {
is SDPCreateSuccess -> {
peerConnection?.setLocalDescription(SDPSetCallback({ setResult ->
peerConnection?.setLocalDescription(SDPSetCallback { setResult ->
Log.i(TAG, "SetLocalDescription: $setResult")
}), result.descriptor)
signaler.sendSDP(result.descriptor.description)
signaler.sendSDP(result.descriptor.type.ordinal, result.descriptor.description)
}, result.descriptor)
}
is SDPCreateFailure -> Log.e(TAG, "Error creating offer: ${result.reason}")
}
Expand All @@ -236,7 +213,8 @@ class VideoCallSession(
start()
}
is SDPMessage -> {
handleRemoteDescriptor(message.sdp)
val map = SessionDescription.Type.values().associateBy(SessionDescription.Type::ordinal)
handleRemoteDescriptor(SessionDescription(map[message.sdpType], message.sdp))
}
is ICEMessage -> {
handleRemoteCandidate(message.label, message.id, message.candidate)
Expand All @@ -247,7 +225,6 @@ class VideoCallSession(
}
}


fun terminate() {
signaler.close()
try {
Expand All @@ -258,19 +235,16 @@ class VideoCallSession(
videoSource?.dispose()

audioSource?.dispose()

peerConnection?.dispose()

factory?.dispose()

eglBase.release()
}

companion object {

fun connect(context: Context, url: String, videoRenderers: VideoRenderers, callback: (VideoCallStatus) -> Unit) : VideoCallSession {
fun connect(context: Context, url: String, sinks: VideoSinks, callback: (VideoCallStatus) -> Unit) : VideoCallSession {
val websocketHandler = SignalingWebSocket()
val session = VideoCallSession(context, callback, websocketHandler, videoRenderers)
val session = VideoCallSession(context, callback, websocketHandler, sinks)
val client = OkHttpClient()
val request = Request.Builder().url(url).build()
Log.i(TAG, "Connecting to $url")
Expand All @@ -279,10 +253,9 @@ class VideoCallSession(
return session
}

private val STREAM_LABEL = "remoteStream"
private val VIDEO_TRACK_LABEL = "remoteVideoTrack"
private val AUDIO_TRACK_LABEL = "remoteAudioTrack"
private val TAG = "VideoCallSession"
const val VIDEO_TRACK_LABEL = "remoteVideoTrack"
const val AUDIO_TRACK_LABEL = "remoteAudioTrack"
const val TAG = "VideoCallSession"
private val executor = Executors.newSingleThreadExecutor()
}
}
}