Skip to main content

Documentation Index

Fetch the complete documentation index at: https://learn.social.plus/llms.txt

Use this file to discover all available pages before exploring further.

Android Implementation Guide

Complete guide for implementing social.plus Video SDK in Android applications using Kotlin and modern Android development practices.

Overview

This guide covers Android-specific implementation details, native components integration, and platform-specific considerations for video streaming applications.

Prerequisites

  • Android 5.0 (API 21)+ minimum SDK version
  • Android Studio 4.2+ for development
  • Kotlin 1.5+ support
  • Gradle 7.0+ build system

Core Dependencies

// App-level build.gradle
dependencies {
    implementation 'com.github.AmityCo.Amity-Social-Cloud-SDK-Android:amity-video-publisher:1.0.0'
    implementation 'com.github.AmityCo.Amity-Social-Cloud-SDK-Android:amity-video-player:1.0.0'
    implementation 'com.github.AmityCo.Amity-Social-Cloud-SDK-Android:amity-video-core:1.0.0'
    
    // Required Android dependencies
    implementation 'androidx.camera:camera-core:1.3.1'
    implementation 'androidx.camera:camera-camera2:1.3.1'
    implementation 'androidx.camera:camera-lifecycle:1.3.1'
    implementation 'androidx.camera:camera-view:1.3.1'
    implementation 'androidx.media3:media3-exoplayer:1.2.0'
    implementation 'androidx.media3:media3-ui:1.2.0'
    
    // Coroutines for async operations
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3'
    
    // Lifecycle components
    implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0'
    implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.7.0'
}

Installation & Setup

Gradle Configuration

// Project-level build.gradle
allprojects {
    repositories {
        google()
        mavenCentral()
        maven { url 'https://jitpack.io' }
        maven { url 'https://releases.social.plus/android' }
    }
}

// App-level build.gradle
android {
    compileSdk 34
    
    defaultConfig {
        minSdk 21
        targetSdk 34
        
        // Required for video features
        ndk {
            abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
        }
    }
    
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
    
    kotlinOptions {
        jvmTarget = '1.8'
    }
    
    buildFeatures {
        viewBinding true
        dataBinding true
    }
}

Application Setup

// Application class
class VideoApplication : Application() {
    
    override fun onCreate() {
        super.onCreate()
        
        // Initialize social.plus Video SDK
        setupVideoSDK()
    }
    
    private fun setupVideoSDK() {
        val config = AmityCoreClient.Configuration(
            apiKey = "your-api-key",
            region = AmityRegion.GLOBAL
        )
        
        AmityCoreClient.setup(this, config)
        
        // Register video publisher
        AmityStreamBroadcasterClient.setup(AmityCoreClient.getConfiguration())
    }
}

Manifest Configuration

<!-- AndroidManifest.xml -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    
    <!-- Required permissions -->
    <uses-permission android:name="android.permission.CAMERA" />
    <uses-permission android:name="android.permission.RECORD_AUDIO" />
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.WAKE_LOCK" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    
    <!-- Camera hardware features -->
    <uses-feature android:name="android.hardware.camera" android:required="true" />
    <uses-feature android:name="android.hardware.camera.autofocus" android:required="false" />
    <uses-feature android:name="android.hardware.microphone" android:required="true" />
    
    <application
        android:name=".VideoApplication"
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme">
        
        <!-- Your activities -->
        
    </application>
</manifest>

Permissions & Runtime Handling

Permission Manager

class PermissionManager(private val activity: Activity) {
    
    companion object {
        private const val PERMISSION_REQUEST_CODE = 1001
        private val REQUIRED_PERMISSIONS = arrayOf(
            Manifest.permission.CAMERA,
            Manifest.permission.RECORD_AUDIO,
            Manifest.permission.WRITE_EXTERNAL_STORAGE
        )
    }
    
    fun requestPermissions(callback: (Boolean) -> Unit) {
        if (hasAllPermissions()) {
            callback(true)
            return
        }
        
        this.permissionCallback = callback
        
        ActivityCompat.requestPermissions(
            activity,
            REQUIRED_PERMISSIONS,
            PERMISSION_REQUEST_CODE
        )
    }
    
    private fun hasAllPermissions(): Boolean {
        return REQUIRED_PERMISSIONS.all { permission ->
            ContextCompat.checkSelfPermission(activity, permission) == PackageManager.PERMISSION_GRANTED
        }
    }
    
    private var permissionCallback: ((Boolean) -> Unit)? = null
    
    fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<String>,
        grantResults: IntArray
    ) {
        if (requestCode == PERMISSION_REQUEST_CODE) {
            val allGranted = grantResults.all { it == PackageManager.PERMISSION_GRANTED }
            permissionCallback?.invoke(allGranted)
            permissionCallback = null
        }
    }
    
    fun showPermissionRationale(callback: () -> Unit) {
        AlertDialog.Builder(activity)
            .setTitle("Permissions Required")
            .setMessage("This app needs camera and microphone access to broadcast video")
            .setPositiveButton("Grant") { _, _ -> callback() }
            .setNegativeButton("Cancel", null)
            .show()
    }
}

Broadcasting Implementation

Broadcast Activity

class LiveBroadcastActivity : AppCompatActivity() {
    
    private lateinit var binding: ActivityLiveBroadcastBinding
    private lateinit var broadcaster: AmityStreamBroadcaster
    private lateinit var permissionManager: PermissionManager
    
    private var streamId: String? = null
    private var isLive = false
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityLiveBroadcastBinding.inflate(layoutInflater)
        setContentView(binding.root)
        
        permissionManager = PermissionManager(this)
        
        setupUI()
        requestPermissions()
    }
    
    private fun setupUI() {
        binding.apply {
            startButton.setOnClickListener { startBroadcast() }
            stopButton.setOnClickListener { stopBroadcast() }
            switchCameraButton.setOnClickListener { switchCamera() }
            toggleMuteButton.setOnClickListener { toggleMute() }
            qualityButton.setOnClickListener { showQualitySelector() }
        }
    }
    
    private fun requestPermissions() {
        permissionManager.requestPermissions { granted ->
            if (granted) {
                setupBroadcaster()
            } else {
                showPermissionDeniedDialog()
            }
        }
    }
    
    private fun setupBroadcaster() {
        try {
            // Create broadcaster configuration
            val config = AmityStreamBroadcasterConfiguration.Builder()
                .setOrientation(Configuration.ORIENTATION_PORTRAIT)
                .setResolution(AmityBroadcastResolution.HD_720P)
                .build()
            
            // Create broadcaster
            broadcaster = AmityStreamBroadcaster.Builder(binding.cameraView)
                .setConfiguration(config)
                .build()
            
            // Set up event listeners
            broadcaster.setOnStateChangedListener { state ->
                runOnUiThread { updateUI(state) }
            }
            
            broadcaster.setOnErrorListener { error ->
                runOnUiThread { handleBroadcastError(error) }
            }
            
            updateUI(AmityStreamBroadcasterState.IDLE)
            
        } catch (e: Exception) {
            Log.e("Broadcast", "Failed to setup broadcaster", e)
            showError("Failed to initialize broadcaster: ${e.message}")
        }
    }
    
    private fun startBroadcast() {
        if (!::broadcaster.isInitialized) {
            showError("Broadcaster not initialized")
            return
        }
        
        val title = binding.titleEditText.text.toString().ifEmpty { "Live Stream" }
        val description = binding.descriptionEditText.text.toString().ifEmpty { "Broadcasting live from Android" }
        
        broadcaster.startPublish(title, description)
            .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe({ stream ->
                streamId = stream.streamId
                isLive = true
                updateUI(AmityStreamBroadcasterState.CONNECTED)
                
                // Track analytics
                trackBroadcastStarted(stream.streamId)
                
            }, { error ->
                Log.e("Broadcast", "Failed to start broadcast", error)
                handleBroadcastError(error)
            })
    }
    
    private fun stopBroadcast() {
        if (!::broadcaster.isInitialized || !isLive) return
        
        broadcaster.stopPublish()
            .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe({
                isLive = false
                streamId = null
                updateUI(AmityStreamBroadcasterState.IDLE)
                
                // Track analytics
                trackBroadcastStopped()
                
            }, { error ->
                Log.e("Broadcast", "Failed to stop broadcast", error)
                handleBroadcastError(error)
            })
    }
    
    private fun switchCamera() {
        if (!::broadcaster.isInitialized) return
        
        broadcaster.switchCamera()
            .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe({
                // Camera switched successfully
            }, { error ->
                Log.e("Broadcast", "Failed to switch camera", error)
                showError("Failed to switch camera")
            })
    }
    
    private fun toggleMute() {
        if (!::broadcaster.isInitialized) return
        
        val isMuted = broadcaster.isMuted()
        broadcaster.setMuted(!isMuted)
        
        binding.toggleMuteButton.apply {
            text = if (!isMuted) "Unmute" else "Mute"
            setCompoundDrawablesWithIntrinsicBounds(
                if (!isMuted) R.drawable.ic_mic_off else R.drawable.ic_mic_on,
                0, 0, 0
            )
        }
    }
    
    private fun showQualitySelector() {
        val qualities = arrayOf("480p", "720p", "1080p")
        val resolutions = arrayOf(
            AmityBroadcastResolution.SD_480P,
            AmityBroadcastResolution.HD_720P,
            AmityBroadcastResolution.FHD_1080P
        )
        
        AlertDialog.Builder(this)
            .setTitle("Select Quality")
            .setItems(qualities) { _, which ->
                updateBroadcastQuality(resolutions[which])
            }
            .show()
    }
    
    private fun updateBroadcastQuality(resolution: AmityBroadcastResolution) {
        if (isLive) {
            showError("Cannot change quality while broadcasting")
            return
        }
        
        // Update configuration
        val config = AmityStreamBroadcasterConfiguration.Builder()
            .setOrientation(Configuration.ORIENTATION_PORTRAIT)
            .setResolution(resolution)
            .build()
        
        broadcaster.updateConfiguration(config)
    }
    
    private fun updateUI(state: AmityStreamBroadcasterState) {
        binding.apply {
            when (state) {
                AmityStreamBroadcasterState.IDLE -> {
                    statusText.text = "Ready to broadcast"
                    startButton.isEnabled = true
                    stopButton.isEnabled = false
                    liveIndicator.visibility = View.GONE
                }
                
                AmityStreamBroadcasterState.CONNECTING -> {
                    statusText.text = "Connecting..."
                    startButton.isEnabled = false
                    stopButton.isEnabled = false
                    liveIndicator.visibility = View.VISIBLE
                }
                
                AmityStreamBroadcasterState.CONNECTED -> {
                    statusText.text = "LIVE"
                    startButton.isEnabled = false
                    stopButton.isEnabled = true
                    liveIndicator.visibility = View.VISIBLE
                }
                
                AmityStreamBroadcasterState.DISCONNECTED -> {
                    statusText.text = "Disconnected"
                    startButton.isEnabled = true
                    stopButton.isEnabled = false
                    liveIndicator.visibility = View.GONE
                }
            }
        }
    }
    
    private fun handleBroadcastError(error: Throwable) {
        val message = when (error) {
            is AmityNetworkException -> "Network error. Please check your connection."
            is AmityPermissionException -> "Permission denied. Please grant required permissions."
            is AmityStreamException -> "Stream error: ${error.message}"
            else -> "Broadcast error: ${error.message}"
        }
        
        showError(message)
        
        // Reset UI state
        isLive = false
        updateUI(AmityStreamBroadcasterState.IDLE)
    }
    
    private fun showError(message: String) {
        Toast.makeText(this, message, Toast.LENGTH_LONG).show()
    }
    
    private fun showPermissionDeniedDialog() {
        AlertDialog.Builder(this)
            .setTitle("Permissions Required")
            .setMessage("Camera and microphone permissions are required for broadcasting")
            .setPositiveButton("Grant") { _, _ ->
                permissionManager.requestPermissions { granted ->
                    if (granted) setupBroadcaster()
                    else finish()
                }
            }
            .setNegativeButton("Cancel") { _, _ -> finish() }
            .setCancelable(false)
            .show()
    }
    
    private fun trackBroadcastStarted(streamId: String) {
        // Analytics tracking
        val properties = mapOf(
            "stream_id" to streamId,
            "platform" to "android",
            "quality" to broadcaster.configuration.resolution.name
        )
        
        // Send to your analytics service
        // Analytics.track("broadcast_started", properties)
    }
    
    private fun trackBroadcastStopped() {
        // Analytics tracking
        // Analytics.track("broadcast_stopped")
    }
    
    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<String>,
        grantResults: IntArray
    ) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        permissionManager.onRequestPermissionsResult(requestCode, permissions, grantResults)
    }
    
    override fun onDestroy() {
        super.onDestroy()
        
        // Clean up broadcaster
        if (::broadcaster.isInitialized) {
            if (isLive) {
                broadcaster.stopPublish().subscribe()
            }
            broadcaster.release()
        }
    }
}

Broadcast Layout

<!-- activity_live_broadcast.xml -->
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res/constraints"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/black">
    
    <!-- Camera preview -->
    <com.amity.socialcloud.sdk.video.AmityCameraView
        android:id="@+id/cameraView"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toTopOf="@+id/controlsContainer"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintDimensionRatio="H,9:16" />
    
    <!-- Live indicator -->
    <TextView
        android:id="@+id/liveIndicator"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="LIVE"
        android:textColor="@color/white"
        android:textSize="16sp"
        android:textStyle="bold"
        android:background="@drawable/live_indicator_bg"
        android:padding="8dp"
        android:visibility="gone"
        app:layout_constraintTop_toTopOf="@+id/cameraView"
        app:layout_constraintStart_toStartOf="@+id/cameraView"
        android:layout_margin="16dp" />
    
    <!-- Top controls -->
    <LinearLayout
        android:id="@+id/topControls"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:padding="16dp"
        app:layout_constraintTop_toTopOf="@+id/cameraView"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent">
        
        <Button
            android:id="@+id/switchCameraButton"
            android:layout_width="48dp"
            android:layout_height="48dp"
            android:background="@drawable/circle_button_bg"
            android:drawableTop="@drawable/ic_switch_camera"
            android:layout_marginEnd="16dp" />
        
        <View
            android:layout_width="0dp"
            android:layout_height="1dp"
            android:layout_weight="1" />
        
        <Button
            android:id="@+id/qualityButton"
            android:layout_width="wrap_content"
            android:layout_height="48dp"
            android:text="720p"
            android:textColor="@color/white"
            android:background="@drawable/rounded_button_bg"
            android:padding="12dp" />
    </LinearLayout>
    
    <!-- Controls container -->
    <LinearLayout
        android:id="@+id/controlsContainer"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:padding="24dp"
        android:background="@drawable/controls_bg"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent">
        
        <!-- Stream info inputs -->
        <EditText
            android:id="@+id/titleEditText"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:hint="Stream title"
            android:textColor="@color/white"
            android:textColorHint="@color/white_70"
            android:background="@drawable/edit_text_bg"
            android:padding="12dp"
            android:layout_marginBottom="8dp" />
        
        <EditText
            android:id="@+id/descriptionEditText"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:hint="Description (optional)"
            android:textColor="@color/white"
            android:textColorHint="@color/white_70"
            android:background="@drawable/edit_text_bg"
            android:padding="12dp"
            android:layout_marginBottom="16dp" />
        
        <!-- Status text -->
        <TextView
            android:id="@+id/statusText"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Ready to broadcast"
            android:textColor="@color/white"
            android:textSize="16sp"
            android:layout_gravity="center_horizontal"
            android:layout_marginBottom="16dp" />
        
        <!-- Control buttons -->
        <LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:orientation="horizontal"
            android:layout_gravity="center_horizontal">
            
            <Button
                android:id="@+id/toggleMuteButton"
                android:layout_width="56dp"
                android:layout_height="56dp"
                android:background="@drawable/circle_button_bg"
                android:drawableTop="@drawable/ic_mic_on"
                android:layout_marginEnd="24dp" />
            
            <Button
                android:id="@+id/startButton"
                android:layout_width="80dp"
                android:layout_height="80dp"
                android:background="@drawable/broadcast_button_bg"
                android:text="GO\nLIVE"
                android:textColor="@color/white"
                android:textSize="12sp"
                android:textStyle="bold"
                android:layout_marginEnd="24dp" />
            
            <Button
                android:id="@+id/stopButton"
                android:layout_width="56dp"
                android:layout_height="56dp"
                android:background="@drawable/stop_button_bg"
                android:enabled="false" />
        </LinearLayout>
    </LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

Video Playback Implementation

Player Activity

class VideoPlayerActivity : AppCompatActivity() {
    
    private lateinit var binding: ActivityVideoPlayerBinding
    private lateinit var player: AmityVideoPlayer
    
    private var streamId: String? = null
    private var isLiveStream = false
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityVideoPlayerBinding.inflate(layoutInflater)
        setContentView(binding.root)
        
        // Get stream info from intent
        streamId = intent.getStringExtra("stream_id")
        isLiveStream = intent.getBooleanExtra("is_live", false)
        
        setupPlayer()
        setupUI()
        loadStream()
    }
    
    private fun setupPlayer() {
        player = AmityVideoPlayer.Builder(this)
            .setPlayerView(binding.playerView)
            .build()
        
        // Set up event listeners
        player.setOnStateChangedListener { state ->
            runOnUiThread { updateUI(state) }
        }
        
        player.setOnErrorListener { error ->
            runOnUiThread { handlePlayerError(error) }
        }
        
        player.setOnProgressListener { position, duration ->
            runOnUiThread { updateProgress(position, duration) }
        }
    }
    
    private fun setupUI() {
        binding.apply {
            playButton.setOnClickListener { player.play() }
            pauseButton.setOnClickListener { player.pause() }
            fullscreenButton.setOnClickListener { toggleFullscreen() }
            
            // Seek bar for VOD content
            if (!isLiveStream) {
                seekBar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
                    override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
                        if (fromUser) {
                            player.seekTo(progress.toLong())
                        }
                    }
                    
                    override fun onStartTrackingTouch(seekBar: SeekBar?) {}
                    override fun onStopTrackingTouch(seekBar: SeekBar?) {}
                })
            } else {
                seekBar.visibility = View.GONE
                liveIndicator.visibility = View.VISIBLE
            }
        }
    }
    
    private fun loadStream() {
        streamId?.let { id ->
            player.loadStream(id)
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe({ stream ->
                    updateStreamInfo(stream)
                    
                }, { error ->
                    Log.e("Player", "Failed to load stream", error)
                    handlePlayerError(error)
                })
        }
    }
    
    private fun updateStreamInfo(stream: AmityStream) {
        binding.apply {
            titleText.text = stream.title
            descriptionText.text = stream.description
            
            if (isLiveStream) {
                viewerCountText.text = "${stream.viewerCount} viewers"
                viewerCountText.visibility = View.VISIBLE
            }
        }
    }
    
    private fun updateUI(state: AmityVideoPlayerState) {
        binding.apply {
            when (state) {
                AmityVideoPlayerState.IDLE -> {
                    playButton.visibility = View.VISIBLE
                    pauseButton.visibility = View.GONE
                    loadingIndicator.visibility = View.GONE
                }
                
                AmityVideoPlayerState.LOADING -> {
                    playButton.visibility = View.GONE
                    pauseButton.visibility = View.GONE
                    loadingIndicator.visibility = View.VISIBLE
                }
                
                AmityVideoPlayerState.PLAYING -> {
                    playButton.visibility = View.GONE
                    pauseButton.visibility = View.VISIBLE
                    loadingIndicator.visibility = View.GONE
                }
                
                AmityVideoPlayerState.PAUSED -> {
                    playButton.visibility = View.VISIBLE
                    pauseButton.visibility = View.GONE
                    loadingIndicator.visibility = View.GONE
                }
                
                AmityVideoPlayerState.ENDED -> {
                    playButton.visibility = View.VISIBLE
                    pauseButton.visibility = View.GONE
                    loadingIndicator.visibility = View.GONE
                    
                    if (!isLiveStream) {
                        binding.seekBar.progress = 0
                    }
                }
            }
        }
    }
    
    private fun updateProgress(position: Long, duration: Long) {
        if (!isLiveStream && duration > 0) {
            binding.apply {
                seekBar.max = duration.toInt()
                seekBar.progress = position.toInt()
                
                currentTimeText.text = formatTime(position)
                totalTimeText.text = formatTime(duration)
            }
        }
    }
    
    private fun formatTime(timeMs: Long): String {
        val seconds = (timeMs / 1000) % 60
        val minutes = (timeMs / (1000 * 60)) % 60
        val hours = (timeMs / (1000 * 60 * 60))
        
        return if (hours > 0) {
            String.format("%02d:%02d:%02d", hours, minutes, seconds)
        } else {
            String.format("%02d:%02d", minutes, seconds)
        }
    }
    
    private fun toggleFullscreen() {
        if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) {
            requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
        } else {
            requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
        }
    }
    
    private fun handlePlayerError(error: Throwable) {
        val message = when (error) {
            is AmityNetworkException -> "Network error. Please check your connection."
            is AmityStreamNotFoundException -> "Stream not found."
            is AmityStreamUnavailableException -> "Stream is not available."
            else -> "Playback error: ${error.message}"
        }
        
        Toast.makeText(this, message, Toast.LENGTH_LONG).show()
    }
    
    override fun onDestroy() {
        super.onDestroy()
        player.release()
    }
    
    override fun onPause() {
        super.onPause()
        player.pause()
    }
    
    override fun onResume() {
        super.onResume()
        // Auto-resume for live streams
        if (isLiveStream) {
            player.play()
        }
    }
}

Push Notifications Integration

Firebase Messaging Service

class VideoFirebaseMessagingService : FirebaseMessagingService() {
    
    override fun onNewToken(token: String) {
        super.onNewToken(token)
        Log.d("FCM", "New token: $token")
        
        // Register with social.plus Video SDK
        VideoNotificationManager.getInstance().registerDevice(token) { result ->
            when (result) {
                is AmityResult.Success -> {
                    Log.d("FCM", "Device registered successfully")
                }
                is AmityResult.Error -> {
                    Log.e("FCM", "Failed to register device", result.exception)
                }
            }
        }
    }
    
    override fun onMessageReceived(remoteMessage: RemoteMessage) {
        super.onMessageReceived(remoteMessage)
        
        Log.d("FCM", "Message received from: ${remoteMessage.from}")
        
        val data = remoteMessage.data
        val eventType = data["event_type"]
        val streamId = data["stream_id"]
        
        when (eventType) {
            "stream.started" -> handleStreamStarted(data)
            "viewer.milestone" -> handleViewerMilestone(data)
            "recording.ready" -> handleRecordingReady(data)
            else -> handleGenericNotification(remoteMessage)
        }
    }
    
    private fun handleStreamStarted(data: Map<String, String>) {
        val streamId = data["stream_id"] ?: return
        val broadcasterName = data["broadcaster_name"] ?: "Someone"
        val streamTitle = data["stream_title"] ?: "Untitled Stream"
        
        val notification = NotificationCompat.Builder(this, STREAM_CHANNEL_ID)
            .setContentTitle("🔴 $broadcasterName is LIVE")
            .setContentText(streamTitle)
            .setSmallIcon(R.drawable.ic_live_stream)
            .setAutoCancel(true)
            .setContentIntent(createStreamPendingIntent(streamId, isLive = true))
            .addAction(
                R.drawable.ic_play,
                "Watch",
                createStreamPendingIntent(streamId, isLive = true)
            )
            .addAction(
                R.drawable.ic_share,
                "Share",
                createSharePendingIntent(streamId)
            )
            .setPriority(NotificationCompat.PRIORITY_HIGH)
            .build()
        
        NotificationManagerCompat.from(this).notify(streamId.hashCode(), notification)
    }
    
    private fun handleViewerMilestone(data: Map<String, String>) {
        val streamId = data["stream_id"] ?: return
        val viewerCount = data["viewer_count"] ?: return
        val broadcasterName = data["broadcaster_name"] ?: "Your stream"
        
        val notification = NotificationCompat.Builder(this, MILESTONE_CHANNEL_ID)
            .setContentTitle("🎉 Milestone Reached!")
            .setContentText("$viewerCount people are watching $broadcasterName")
            .setSmallIcon(R.drawable.ic_celebration)
            .setAutoCancel(true)
            .setContentIntent(createStreamPendingIntent(streamId, isLive = true))
            .build()
        
        NotificationManagerCompat.from(this).notify(
            "milestone_$streamId".hashCode(),
            notification
        )
    }
    
    private fun handleRecordingReady(data: Map<String, String>) {
        val streamId = data["stream_id"] ?: return
        val recordingUrl = data["recording_url"] ?: return
        
        val notification = NotificationCompat.Builder(this, RECORDING_CHANNEL_ID)
            .setContentTitle("Recording Ready")
            .setContentText("Your stream recording is now available")
            .setSmallIcon(R.drawable.ic_recording)
            .setAutoCancel(true)
            .setContentIntent(createRecordingPendingIntent(recordingUrl))
            .addAction(
                R.drawable.ic_play,
                "Watch",
                createRecordingPendingIntent(recordingUrl)
            )
            .build()
        
        NotificationManagerCompat.from(this).notify(
            "recording_$streamId".hashCode(),
            notification
        )
    }
    
    private fun createStreamPendingIntent(streamId: String, isLive: Boolean): PendingIntent {
        val intent = Intent(this, VideoPlayerActivity::class.java).apply {
            putExtra("stream_id", streamId)
            putExtra("is_live", isLive)
            flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
        }
        
        return PendingIntent.getActivity(
            this,
            streamId.hashCode(),
            intent,
            PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
        )
    }
    
    private fun createSharePendingIntent(streamId: String): PendingIntent {
        val shareIntent = Intent(Intent.ACTION_SEND).apply {
            type = "text/plain"
            putExtra(Intent.EXTRA_TEXT, "Watch this live stream: https://your-app.com/stream/$streamId")
        }
        
        val intent = Intent.createChooser(shareIntent, "Share Stream")
        
        return PendingIntent.getActivity(
            this,
            "share_$streamId".hashCode(),
            intent,
            PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
        )
    }
    
    private fun createRecordingPendingIntent(recordingUrl: String): PendingIntent {
        val intent = Intent(this, VideoPlayerActivity::class.java).apply {
            putExtra("recording_url", recordingUrl)
            putExtra("is_live", false)
            flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
        }
        
        return PendingIntent.getActivity(
            this,
            recordingUrl.hashCode(),
            intent,
            PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
        )
    }
    
    companion object {
        private const val STREAM_CHANNEL_ID = "stream_notifications"
        private const val MILESTONE_CHANNEL_ID = "milestone_notifications"
        private const val RECORDING_CHANNEL_ID = "recording_notifications"
    }
}

Notification Channels Setup

class NotificationChannelManager(private val context: Context) {
    
    fun createNotificationChannels() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
            
            // Stream notifications channel
            val streamChannel = NotificationChannel(
                STREAM_CHANNEL_ID,
                "Stream Notifications",
                NotificationManager.IMPORTANCE_HIGH
            ).apply {
                description = "Notifications for live stream events"
                enableLights(true)
                lightColor = Color.RED
                enableVibration(true)
                setSound(
                    Uri.parse("android.resource://${context.packageName}/raw/stream_notification"),
                    AudioAttributes.Builder()
                        .setUsage(AudioAttributes.USAGE_NOTIFICATION)
                        .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
                        .build()
                )
            }
            
            // Milestone notifications channel
            val milestoneChannel = NotificationChannel(
                MILESTONE_CHANNEL_ID,
                "Milestone Notifications",
                NotificationManager.IMPORTANCE_DEFAULT
            ).apply {
                description = "Notifications for viewer milestones"
                enableLights(true)
                lightColor = Color.BLUE
            }
            
            // Recording notifications channel
            val recordingChannel = NotificationChannel(
                RECORDING_CHANNEL_ID,
                "Recording Notifications",
                NotificationManager.IMPORTANCE_DEFAULT
            ).apply {
                description = "Notifications for recording events"
            }
            
            notificationManager.createNotificationChannels(
                listOf(streamChannel, milestoneChannel, recordingChannel)
            )
        }
    }
    
    companion object {
        private const val STREAM_CHANNEL_ID = "stream_notifications"
        private const val MILESTONE_CHANNEL_ID = "milestone_notifications"
        private const val RECORDING_CHANNEL_ID = "recording_notifications"
    }
}

Jetpack Compose Integration

Compose Broadcasting Screen

@Composable
fun LiveBroadcastScreen(
    viewModel: BroadcastViewModel = hiltViewModel()
) {
    val state by viewModel.state.collectAsState()
    val context = LocalContext.current
    
    LaunchedEffect(Unit) {
        viewModel.requestPermissions(context as Activity)
    }
    
    Box(modifier = Modifier.fillMaxSize()) {
        // Camera preview
        AndroidView(
            factory = { context ->
                AmityCameraView(context).apply {
                    viewModel.setupBroadcaster(this)
                }
            },
            modifier = Modifier.fillMaxSize()
        )
        
        // Overlay UI
        LiveBroadcastOverlay(
            state = state,
            onStartBroadcast = viewModel::startBroadcast,
            onStopBroadcast = viewModel::stopBroadcast,
            onSwitchCamera = viewModel::switchCamera,
            onToggleMute = viewModel::toggleMute,
            onQualityChange = viewModel::changeQuality
        )
    }
}

@Composable
fun LiveBroadcastOverlay(
    state: BroadcastState,
    onStartBroadcast: (String, String) -> Unit,
    onStopBroadcast: () -> Unit,
    onSwitchCamera: () -> Unit,
    onToggleMute: () -> Unit,
    onQualityChange: (AmityBroadcastResolution) -> Unit
) {
    var showControls by remember { mutableStateOf(true) }
    var title by remember { mutableStateOf("") }
    var description by remember { mutableStateOf("") }
    
    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp)
    ) {
        // Top controls
        Row(
            modifier = Modifier.fillMaxWidth(),
            horizontalArrangement = Arrangement.SpaceBetween
        ) {
            IconButton(onClick = onSwitchCamera) {
                Icon(
                    imageVector = Icons.Default.Cameraswitch,
                    contentDescription = "Switch Camera",
                    tint = Color.White
                )
            }
            
            if (state.isLive) {
                Card(
                    colors = CardDefaults.cardColors(containerColor = Color.Red),
                    modifier = Modifier.padding(8.dp)
                ) {
                    Text(
                        text = "LIVE",
                        color = Color.White,
                        fontWeight = FontWeight.Bold,
                        modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp)
                    )
                }
            }
            
            QualitySelector(
                currentQuality = state.quality,
                onQualityChange = onQualityChange
            )
        }
        
        Spacer(modifier = Modifier.weight(1f))
        
        // Bottom controls
        if (showControls) {
            Card(
                modifier = Modifier.fillMaxWidth(),
                colors = CardDefaults.cardColors(
                    containerColor = Color.Black.copy(alpha = 0.7f)
                )
            ) {
                Column(
                    modifier = Modifier.padding(16.dp)
                ) {
                    if (!state.isLive) {
                        OutlinedTextField(
                            value = title,
                            onValueChange = { title = it },
                            label = { Text("Stream Title") },
                            colors = TextFieldDefaults.outlinedTextFieldColors(
                                textColor = Color.White,
                                focusedLabelColor = Color.White,
                                unfocusedLabelColor = Color.Gray
                            ),
                            modifier = Modifier.fillMaxWidth()
                        )
                        
                        Spacer(modifier = Modifier.height(8.dp))
                        
                        OutlinedTextField(
                            value = description,
                            onValueChange = { description = it },
                            label = { Text("Description (optional)") },
                            colors = TextFieldDefaults.outlinedTextFieldColors(
                                textColor = Color.White,
                                focusedLabelColor = Color.White,
                                unfocusedLabelColor = Color.Gray
                            ),
                            modifier = Modifier.fillMaxWidth()
                        )
                        
                        Spacer(modifier = Modifier.height(16.dp))
                    }
                    
                    Text(
                        text = state.statusText,
                        color = Color.White,
                        fontSize = 16.sp,
                        modifier = Modifier.align(Alignment.CenterHorizontally)
                    )
                    
                    Spacer(modifier = Modifier.height(16.dp))
                    
                    Row(
                        modifier = Modifier.fillMaxWidth(),
                        horizontalArrangement = Arrangement.SpaceEvenly
                    ) {
                        IconButton(onClick = onToggleMute) {
                            Icon(
                                imageVector = if (state.isMuted) Icons.Default.MicOff else Icons.Default.Mic,
                                contentDescription = if (state.isMuted) "Unmute" else "Mute",
                                tint = Color.White
                            )
                        }
                        
                        FloatingActionButton(
                            onClick = {
                                if (state.isLive) {
                                    onStopBroadcast()
                                } else {
                                    onStartBroadcast(
                                        title.ifEmpty { "Live Stream" },
                                        description
                                    )
                                }
                            },
                            containerColor = if (state.isLive) Color.Red else Color.Green,
                            modifier = Modifier.size(80.dp)
                        ) {
                            if (state.isConnecting) {
                                CircularProgressIndicator(
                                    color = Color.White,
                                    modifier = Modifier.size(24.dp)
                                )
                            } else {
                                Icon(
                                    imageVector = if (state.isLive) Icons.Default.Stop else Icons.Default.PlayArrow,
                                    contentDescription = if (state.isLive) "Stop" else "Start",
                                    tint = Color.White
                                )
                            }
                        }
                        
                        IconButton(onClick = { /* Settings */ }) {
                            Icon(
                                imageVector = Icons.Default.Settings,
                                contentDescription = "Settings",
                                tint = Color.White
                            )
                        }
                    }
                }
            }
        }
    }
}

@Composable
fun QualitySelector(
    currentQuality: AmityBroadcastResolution,
    onQualityChange: (AmityBroadcastResolution) -> Unit
) {
    var expanded by remember { mutableStateOf(false) }
    
    Box {
        TextButton(
            onClick = { expanded = true },
            colors = ButtonDefaults.textButtonColors(contentColor = Color.White)
        ) {
            Text(
                text = when (currentQuality) {
                    AmityBroadcastResolution.SD_480P -> "480p"
                    AmityBroadcastResolution.HD_720P -> "720p"
                    AmityBroadcastResolution.FHD_1080P -> "1080p"
                }
            )
        }
        
        DropdownMenu(
            expanded = expanded,
            onDismissRequest = { expanded = false }
        ) {
            DropdownMenuItem(
                text = { Text("480p") },
                onClick = {
                    onQualityChange(AmityBroadcastResolution.SD_480P)
                    expanded = false
                }
            )
            DropdownMenuItem(
                text = { Text("720p") },
                onClick = {
                    onQualityChange(AmityBroadcastResolution.HD_720P)
                    expanded = false
                }
            )
            DropdownMenuItem(
                text = { Text("1080p") },
                onClick = {
                    onQualityChange(AmityBroadcastResolution.FHD_1080P)
                    expanded = false
                }
            )
        }
    }
}

Broadcast ViewModel

@HiltViewModel
class BroadcastViewModel @Inject constructor(
    private val permissionManager: PermissionManager,
    private val analyticsService: AnalyticsService
) : ViewModel() {
    
    private val _state = MutableStateFlow(BroadcastState())
    val state: StateFlow<BroadcastState> = _state.asStateFlow()
    
    private var broadcaster: AmityStreamBroadcaster? = null
    private var streamId: String? = null
    
    fun requestPermissions(activity: Activity) {
        permissionManager.requestPermissions(activity) { granted ->
            if (granted) {
                _state.value = _state.value.copy(permissionsGranted = true)
            } else {
                _state.value = _state.value.copy(error = "Permissions required for broadcasting")
            }
        }
    }
    
    fun setupBroadcaster(cameraView: AmityCameraView) {
        try {
            val config = AmityStreamBroadcasterConfiguration.Builder()
                .setOrientation(Configuration.ORIENTATION_PORTRAIT)
                .setResolution(AmityBroadcastResolution.HD_720P)
                .build()
            
            broadcaster = AmityStreamBroadcaster.Builder(cameraView)
                .setConfiguration(config)
                .build()
            
            broadcaster?.setOnStateChangedListener { state ->
                updateBroadcastState(state)
            }
            
            broadcaster?.setOnErrorListener { error ->
                handleError(error)
            }
            
            _state.value = _state.value.copy(
                isInitialized = true,
                quality = AmityBroadcastResolution.HD_720P
            )
            
        } catch (e: Exception) {
            _state.value = _state.value.copy(error = "Failed to initialize broadcaster: ${e.message}")
        }
    }
    
    fun startBroadcast(title: String, description: String) {
        broadcaster?.startPublish(title, description)
            ?.subscribeOn(Schedulers.io())
            ?.observeOn(AndroidSchedulers.mainThread())
            ?.subscribe({ stream ->
                streamId = stream.streamId
                analyticsService.trackBroadcastStarted(stream.streamId)
                
            }, { error ->
                handleError(error)
            })
    }
    
    fun stopBroadcast() {
        broadcaster?.stopPublish()
            ?.subscribeOn(Schedulers.io())
            ?.observeOn(AndroidSchedulers.mainThread())
            ?.subscribe({
                streamId?.let { id ->
                    analyticsService.trackBroadcastStopped(id)
                }
                streamId = null
                
            }, { error ->
                handleError(error)
            })
    }
    
    fun switchCamera() {
        broadcaster?.switchCamera()
            ?.subscribeOn(Schedulers.io())
            ?.observeOn(AndroidSchedulers.mainThread())
            ?.subscribe({
                // Camera switched successfully
            }, { error ->
                handleError(error)
            })
    }
    
    fun toggleMute() {
        broadcaster?.let { broadcaster ->
            val isMuted = broadcaster.isMuted()
            broadcaster.setMuted(!isMuted)
            _state.value = _state.value.copy(isMuted = !isMuted)
        }
    }
    
    fun changeQuality(quality: AmityBroadcastResolution) {
        if (_state.value.isLive) {
            _state.value = _state.value.copy(error = "Cannot change quality while broadcasting")
            return
        }
        
        val config = AmityStreamBroadcasterConfiguration.Builder()
            .setOrientation(Configuration.ORIENTATION_PORTRAIT)
            .setResolution(quality)
            .build()
        
        broadcaster?.updateConfiguration(config)
        _state.value = _state.value.copy(quality = quality)
    }
    
    private fun updateBroadcastState(broadcasterState: AmityStreamBroadcasterState) {
        val newState = when (broadcasterState) {
            AmityStreamBroadcasterState.IDLE -> _state.value.copy(
                isLive = false,
                isConnecting = false,
                statusText = "Ready to broadcast"
            )
            AmityStreamBroadcasterState.CONNECTING -> _state.value.copy(
                isConnecting = true,
                statusText = "Connecting..."
            )
            AmityStreamBroadcasterState.CONNECTED -> _state.value.copy(
                isLive = true,
                isConnecting = false,
                statusText = "LIVE"
            )
            AmityStreamBroadcasterState.DISCONNECTED -> _state.value.copy(
                isLive = false,
                isConnecting = false,
                statusText = "Disconnected"
            )
        }
        _state.value = newState
    }
    
    private fun handleError(error: Throwable) {
        val message = when (error) {
            is AmityNetworkException -> "Network error. Please check your connection."
            is AmityPermissionException -> "Permission denied. Please grant required permissions."
            else -> "Error: ${error.message}"
        }
        
        _state.value = _state.value.copy(
            error = message,
            isLive = false,
            isConnecting = false
        )
    }
    
    override fun onCleared() {
        super.onCleared()
        broadcaster?.release()
    }
}

data class BroadcastState(
    val isInitialized: Boolean = false,
    val permissionsGranted: Boolean = false,
    val isLive: Boolean = false,
    val isConnecting: Boolean = false,
    val isMuted: Boolean = false,
    val quality: AmityBroadcastResolution = AmityBroadcastResolution.HD_720P,
    val statusText: String = "Initializing...",
    val error: String? = null
)

Performance Optimization

Memory Management

class VideoMemoryManager {
    
    companion object {
        fun optimizeForBroadcasting(context: Context) {
            // Reduce memory usage for other components
            val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
            val memoryInfo = ActivityManager.MemoryInfo()
            activityManager.getMemoryInfo(memoryInfo)
            
            if (memoryInfo.lowMemory) {
                // Implement memory optimization strategies
                System.gc()
                
                // Reduce video quality if needed
                // broadcastManager.reduceQuality()
            }
        }
        
        fun cleanupVideoResources(broadcaster: AmityStreamBroadcaster?) {
            broadcaster?.release()
        }
        
        fun cleanupPlayerResources(player: AmityVideoPlayer?) {
            player?.release()
        }
    }
}

Background Handling

class VideoLifecycleObserver : LifecycleObserver {
    
    private var broadcaster: AmityStreamBroadcaster? = null
    private var player: AmityVideoPlayer? = null
    
    @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
    fun onPause() {
        // Pause video playback
        player?.pause()
        
        // Optionally pause broadcasting (based on requirements)
        // broadcaster?.pause()
    }
    
    @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
    fun onResume() {
        // Resume video playback for live streams
        player?.play()
        
        // Resume broadcasting if it was paused
        // broadcaster?.resume()
    }
    
    @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
    fun onDestroy() {
        // Clean up resources
        broadcaster?.release()
        player?.release()
    }
    
    fun setBroadcaster(broadcaster: AmityStreamBroadcaster) {
        this.broadcaster = broadcaster
    }
    
    fun setPlayer(player: AmityVideoPlayer) {
        this.player = player
    }
}

Troubleshooting

Common Android Issues

  1. Camera Access Denied
    private fun handleCameraPermissionDenied() {
        if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.CAMERA)) {
            // Show rationale and request again
            showPermissionRationale()
        } else {
            // User denied with "Don't ask again" - guide to settings
            showGoToSettingsDialog()
        }
    }
    
  2. Background Limitations
    private fun handleBackgroundRestrictions() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            val powerManager = getSystemService(Context.POWER_SERVICE) as PowerManager
            if (!powerManager.isIgnoringBatteryOptimizations(packageName)) {
                // Request to ignore battery optimizations for streaming
                requestIgnoreBatteryOptimizations()
            }
        }
    }
    
  3. Memory Issues
    override fun onTrimMemory(level: Int) {
        super.onTrimMemory(level)
        
        when (level) {
            ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL -> {
                // Reduce video quality or stop non-essential streams
                VideoMemoryManager.optimizeForBroadcasting(this)
            }
            ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN -> {
                // App is in background, pause non-essential video operations
                pauseNonEssentialVideoOperations()
            }
        }
    }
    

Best Practices

  1. Permission Handling - Always check and request permissions appropriately
  2. Lifecycle Management - Properly handle Android lifecycle events
  3. Memory Management - Release video resources when not needed
  4. Background Limitations - Handle Android’s background processing limits
  5. Network Optimization - Implement adaptive streaming for varying network conditions

Next Steps

  1. iOS Implementation - iOS-specific implementation details
  2. Web Implementation - Web-specific implementation details
  3. Cross-Platform Comparison - Compare platform differences
Android Permissions: Ensure your app handles runtime permissions correctly and provides clear explanations for why permissions are needed. Target SDK 31+ requires careful handling of camera and microphone permissions.