Skip to content

Commit

Permalink
Merge pull request #495 from opentok/e2ee-example
Browse files Browse the repository at this point in the history
Added e2ee android example
  • Loading branch information
devwithzachary authored Jan 8, 2024
2 parents b0028d2 + d3f4811 commit c2574b3
Show file tree
Hide file tree
Showing 27 changed files with 874 additions and 0 deletions.
15 changes: 15 additions & 0 deletions E2EE-Video-Chat-Kotlin/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# intellij
*.iml

.gradle
/local.properties
/.idea/workspace.xml
/.idea/libraries
.DS_Store
/build
/captures
.externalNativeBuild
app/build

.settings/
app/jniLibs/
39 changes: 39 additions & 0 deletions E2EE-Video-Chat-Kotlin/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# E2EE Video Chat

Upon deploying this sample application, you should be able to have two-way End to End Encrypted (E2EE) audio and video communication using OpenTok.

Main features:
* Connect to an E2EE OpenTok session
* Publish an audio-video stream to the session
* Subscribe to another client's audio-video stream

# Configure the app
Open the `OpenTokConfig` file and configure the `API_KEY`, `SESSION_ID`, and `TOKEN` variables. You can obtain these values from your [TokBox account](https://tokbox.com/account/#/).
Set the `SECRET` to your own Encryption Secret. A valid secret is a string between 8 and 256 characters.

# Enabling E2EE
To create an E2EE connection you must first enable this functionality server side.
You enable end-to-end encryption when you create a session using the REST API. Set the e2ee property to true. See [session creation](https://tokbox.com/developer/guides/create-session/).

The following Node.js example creates an end-to-end encryption enabled session:

```javascript
const opentok = new OpenTok(API_KEY, API_SECRET);
const sessionId;
opentok.createSession({
mediaMode: 'routed',
e2ee: true,
}, function(error, session) {
if (error) {
console.log('Error creating session:', error)
} else {
sessionId = session.sessionId;
console.log('Session ID: ' + sessionId);
}
});
```

## Further Reading

* Review [other sample projects](../)
* Read more about [OpenTok Android SDK](https://tokbox.com/developer/sdks/android/)
4 changes: 4 additions & 0 deletions E2EE-Video-Chat-Kotlin/app/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/build
config.gradle
*.jar
*.so
49 changes: 49 additions & 0 deletions E2EE-Video-Chat-Kotlin/app/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
plugins {
id 'com.android.application'
id 'kotlin-android'
}

apply {
from '../../commons.gradle'
}

android {
compileSdkVersion extCompileSdkVersion

defaultConfig {
applicationId "com.tokbox.sample.ee2evideo"
minSdkVersion extMinSdkVersion
targetSdkVersion extTargetSdkVersion
versionCode extVersionCode
versionName extVersionName
}

buildTypes {
release {
minifyEnabled extMinifyEnabled
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}

compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}

kotlinOptions {
jvmTarget = '1.8'
}
}

dependencies {
// Dependency versions are defined in the ../../commons.gradle file
implementation "com.opentok.android:opentok-android-sdk:2.27.0"
implementation "androidx.appcompat:appcompat:${extAppCompatVersion}"
implementation "pub.devrel:easypermissions:${extEasyPermissionsVersion}"
implementation "androidx.constraintlayout:constraintlayout:${extConstraintLyoutVersion}"

implementation "com.squareup.retrofit2:retrofit:${extRetrofitVersion}"
implementation "com.squareup.okhttp3:okhttp:${extOkHttpVersion}"
implementation "com.squareup.retrofit2:converter-moshi:${extRetrofit2ConverterMoshi}"
implementation "com.squareup.okhttp3:logging-interceptor:${extOkHttpLoggingInterceptor}"
}
32 changes: 32 additions & 0 deletions E2EE-Video-Chat-Kotlin/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.tokbox.sample.basicvideochat" >

<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />

<uses-feature android:name="android.hardware.camera" />
<uses-feature android:name="android.hardware.camera.autofocus" />

<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
<activity
android:name=".MainActivity"
android:screenOrientation="portrait"
android:label="@string/app_name"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>

</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
package com.tokbox.sample.ee2evideo

import android.Manifest
import android.opengl.GLSurfaceView
import android.os.Bundle
import android.util.Log
import android.widget.FrameLayout
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import com.opentok.android.BaseVideoRenderer
import com.opentok.android.OpentokError
import com.opentok.android.Publisher
import com.opentok.android.PublisherKit
import com.opentok.android.PublisherKit.PublisherListener
import com.opentok.android.Session
import com.opentok.android.Session.SessionListener
import com.opentok.android.Stream
import com.opentok.android.Subscriber
import com.opentok.android.SubscriberKit
import com.opentok.android.SubscriberKit.SubscriberListener
import com.tokbox.sample.basicvideochat.R
import com.tokbox.sample.ee2evideo.MainActivity
import com.tokbox.sample.ee2evideo.network.APIService
import com.tokbox.sample.ee2evideo.network.GetSessionResponse
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import pub.devrel.easypermissions.AfterPermissionGranted
import pub.devrel.easypermissions.EasyPermissions
import pub.devrel.easypermissions.EasyPermissions.PermissionCallbacks
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory

class MainActivity : AppCompatActivity(), PermissionCallbacks {
private var retrofit: Retrofit? = null
private var apiService: APIService? = null
private var session: Session? = null
private var publisher: Publisher? = null
private var subscriber: Subscriber? = null
private lateinit var publisherViewContainer: FrameLayout
private lateinit var subscriberViewContainer: FrameLayout
private val publisherListener: PublisherListener = object : PublisherListener {
override fun onStreamCreated(publisherKit: PublisherKit, stream: Stream) {
Log.d(TAG, "onStreamCreated: Publisher Stream Created. Own stream ${stream.streamId}")
}

override fun onStreamDestroyed(publisherKit: PublisherKit, stream: Stream) {
Log.d(TAG, "onStreamDestroyed: Publisher Stream Destroyed. Own stream ${stream.streamId}")
}

override fun onError(publisherKit: PublisherKit, opentokError: OpentokError) {
if (opentokError.errorCode == OpentokError.ErrorCode.EncryptionInternalError) {
// The application should communicate that the secret was not set.
}
finishWithMessage("PublisherKit onError: ${opentokError.message}")
}
}
private val sessionListener: SessionListener = object : SessionListener {
override fun onConnected(session: Session) {
Log.d(TAG, "onConnected: Connected to session: ${session.sessionId}")
publisher = Publisher.Builder(this@MainActivity).build()
publisher?.setPublisherListener(publisherListener)
publisher?.renderer?.setStyle(BaseVideoRenderer.STYLE_VIDEO_SCALE, BaseVideoRenderer.STYLE_VIDEO_FILL)
publisherViewContainer.addView(publisher?.view)
if (publisher?.view is GLSurfaceView) {
(publisher?.view as GLSurfaceView).setZOrderOnTop(true)
}
session.publish(publisher)
}

override fun onDisconnected(session: Session) {
Log.d(TAG, "onDisconnected: Disconnected from session: ${session.sessionId}")
}

override fun onStreamReceived(session: Session, stream: Stream) {
Log.d(TAG, "onStreamReceived: New Stream Received ${stream.streamId} in session: ${session.sessionId}")
if (subscriber == null) {
subscriber = Subscriber.Builder(this@MainActivity, stream).build().also {
it.renderer?.setStyle(
BaseVideoRenderer.STYLE_VIDEO_SCALE,
BaseVideoRenderer.STYLE_VIDEO_FILL
)

it.setSubscriberListener(subscriberListener)
}

session.subscribe(subscriber)
subscriberViewContainer.addView(subscriber?.view)
}
}

override fun onStreamDropped(session: Session, stream: Stream) {
Log.d(TAG, "onStreamDropped: Stream Dropped: ${stream.streamId} in session: ${session.sessionId}")
if (subscriber != null) {
subscriber = null
subscriberViewContainer.removeAllViews()
}
}

override fun onError(session: Session, opentokError: OpentokError) {
if (opentokError.errorCode == OpentokError.ErrorCode.EncryptionSecretMissing) {
// Notify the user that they cannot join the session
}
finishWithMessage("Session error: ${opentokError.message}")
}
}
var subscriberListener: SubscriberListener = object : SubscriberListener {
override fun onConnected(subscriberKit: SubscriberKit) {
Log.d(TAG, "onConnected: Subscriber connected. Stream: ${subscriberKit.stream.streamId}")
}

override fun onDisconnected(subscriberKit: SubscriberKit) {
Log.d(TAG, "onDisconnected: Subscriber disconnected. Stream: ${subscriberKit.stream.streamId}")
}

override fun onError(subscriberKit: SubscriberKit, opentokError: OpentokError) {
if (opentokError.errorCode == OpentokError.ErrorCode.EncryptionSecretMismatch) {
// Activate a UI element communicating that there's been an encryption secret mismatch.
}
finishWithMessage("SubscriberKit onError: ${opentokError.message}")
}
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
publisherViewContainer = findViewById(R.id.publisher_container)
subscriberViewContainer = findViewById(R.id.subscriber_container)
requestPermissions()
}

override fun onPause() {
super.onPause()
session?.onPause()
}

override fun onResume() {
super.onResume()
session?.onResume()
}

override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
EasyPermissions.onRequestPermissionsResult(requestCode, permissions, grantResults, this)
}

override fun onPermissionsGranted(requestCode: Int, perms: List<String>) {
Log.d(TAG, "onPermissionsGranted:$requestCode: $perms")
}

override fun onPermissionsDenied(requestCode: Int, perms: List<String>) {
finishWithMessage("onPermissionsDenied: $requestCode: $perms")
}

@AfterPermissionGranted(PERMISSIONS_REQUEST_CODE)
private fun requestPermissions() {
val perms = arrayOf(Manifest.permission.INTERNET, Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO)
if (EasyPermissions.hasPermissions(this, *perms)) {
if (ServerConfig.hasChatServerUrl()) {
// Custom server URL exists - retrieve session config
if (!ServerConfig.isValid) {
finishWithMessage("Invalid chat server url: ${ServerConfig.CHAT_SERVER_URL}")
return
}
initRetrofit()
getSession()
} else {
// Use hardcoded session config
if (!OpenTokConfig.isValid) {
finishWithMessage("Invalid OpenTokConfig. ${OpenTokConfig.description}")
return
}
initializeSession(OpenTokConfig.API_KEY, OpenTokConfig.SESSION_ID, OpenTokConfig.TOKEN, OpenTokConfig.SECRET)
}
} else {
EasyPermissions.requestPermissions(
this,
getString(R.string.rationale_video_app),
PERMISSIONS_REQUEST_CODE,
*perms
)
}
}

/* Make a request for session data */
private fun getSession() {
Log.i(TAG, "getSession")

apiService?.session?.enqueue(object : Callback<GetSessionResponse?> {
override fun onResponse(call: Call<GetSessionResponse?>, response: Response<GetSessionResponse?>) {
response.body()?.also {
initializeSession(it.apiKey, it.sessionId, it.token, it.secret)
}
}

override fun onFailure(call: Call<GetSessionResponse?>, t: Throwable) {
throw RuntimeException(t.message)
}
})
}

private fun initializeSession(apiKey: String, sessionId: String, token: String, secret: String) {
Log.i(TAG, "apiKey: $apiKey")
Log.i(TAG, "sessionId: $sessionId")
Log.i(TAG, "token: $token")

/*
The context used depends on the specific use case, but usually, it is desired for the session to
live outside of the Activity e.g: live between activities. For a production applications,
it's convenient to use Application context instead of Activity context.
*/
session = Session.Builder(this, apiKey, sessionId).build().also {
it.setSessionListener(sessionListener)
//Encrypt the connection
it.setEncryptionSecret(secret)
it.connect(token)
}
}

private fun initRetrofit() {
val logging = HttpLoggingInterceptor()
logging.setLevel(HttpLoggingInterceptor.Level.BODY)
val client: OkHttpClient = OkHttpClient.Builder()
.addInterceptor(logging)
.build()

retrofit = Retrofit.Builder()
.baseUrl(ServerConfig.CHAT_SERVER_URL)
.addConverterFactory(MoshiConverterFactory.create())
.client(client)
.build().also {
apiService = it.create(APIService::class.java)
}
}

private fun finishWithMessage(message: String) {
Log.e(TAG, message)
Toast.makeText(this, message, Toast.LENGTH_LONG).show()
finish()
}

companion object {
private val TAG = MainActivity::class.java.simpleName
private const val PERMISSIONS_REQUEST_CODE = 124
}
}
Loading

0 comments on commit c2574b3

Please sign in to comment.