big-article-66702
05/28/2026, 6:58 PMtall-airline-16756
05/28/2026, 6:59 PMtall-airline-16756
05/28/2026, 6:59 PMbig-article-66702
05/28/2026, 7:00 PMtall-airline-16756
05/28/2026, 7:00 PMLocalVideoTrack outside of the Room/LocalParticipant flow. Because that LiveKit-specific approach is not documented, I recommend using a local camera preview that does not rely on LiveKit: either CameraX (recommended for most apps) or the raw WebRTC org.webrtc capturer/renderer.
Which option do you want? I can provide a minimal, copy‑pasteable example for:
• CameraX PreviewView (simple, AndroidX-native), or
• WebRTC SurfaceViewRenderer + Camera2Capturer (closer to LiveKit rendering pipeline).
Tell me which one and I’ll send the example.
, with interactive elementsbig-article-66702
05/28/2026, 7:01 PMtall-airline-16756
05/28/2026, 7:02 PMimplementation 'org.webrtc:google-webrtc:1.0.32006' // replace with current version you use
AndroidManifest (permissions)
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
Layout (res/layout/activity_main.xml)
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<org.webrtc.SurfaceViewRenderer
android:id="@+id/surface_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>
Activity (Kotlin)
import android.Manifest
import android.content.pm.PackageManager
import android.os.Bundle
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import org.webrtc.*
class MainActivity : AppCompatActivity() {
private lateinit var eglBase: EglBase
private lateinit var peerConnectionFactory: PeerConnectionFactory
private lateinit var surfaceView: SurfaceViewRenderer
private var videoCapturer: Camera2Capturer? = null
private var videoSource: VideoSource? = null
private var localVideoTrack: VideoTrack? = null
private val requestPermissionLauncher =
registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { perms ->
val granted = perms[Manifest.permission.CAMERA] == true
if (granted) startCamera()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
surfaceView = findViewById(R.id.surface_view)
// Request camera permission if needed
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
!= PackageManager.PERMISSION_GRANTED) {
requestPermissionLauncher.launch(arrayOf(Manifest.permission.CAMERA))
} else {
initWebRTCAndStart()
}
}
private fun initWebRTCAndStart() {
eglBase = EglBase.create()
// Initialize PeerConnectionFactory
val options = PeerConnectionFactory.InitializationOptions.builder(this)
.createInitializationOptions()
PeerConnectionFactory.initialize(options)
val encoderFactory = DefaultVideoEncoderFactory(
eglBase.eglBaseContext, /* enableIntelVp8 */true, /* enableH264HighProfile */ true)
val decoderFactory = DefaultVideoDecoderFactory(eglBase.eglBaseContext)
val factoryBuilder = PeerConnectionFactory.Builder()
.setVideoEncoderFactory(encoderFactory)
.setVideoDecoderFactory(decoderFactory)
peerConnectionFactory = factoryBuilder.createPeerConnectionFactory()
// Initialize SurfaceViewRenderer
surfaceView.init(eglBase.eglBaseContext, null)
surfaceView.setMirror(true)
surfaceView.setEnableHardwareScaler(true)
startCamera()
}
private fun startCamera() {
// Create VideoCapturer (Camera2)
val cameraEnumerator = Camera2Enumerator(this)
val cameraName = cameraEnumerator.deviceNames.firstOrNull { cameraEnumerator.isFrontFacing(it) }
?: cameraEnumerator.deviceNames.firstOrNull()
?: return
videoCapturer = Camera2Capturer(this, cameraName, object : CameraVideoCapturer.CameraEventsHandler {
override fun onCameraError(error: String?) { /* log if needed */ }
override fun onCameraDisconnected() {}
override fun onCameraFreezed(error: String?) {}
override fun onCameraOpening(cameraName: String?) {}
override fun onFirstFrameAvailable() {}
override fun onCameraClosed() {}
})
// Create VideoSource and VideoTrack
videoSource = peerConnectionFactory.createVideoSource(false)
videoCapturer?.initialize(
SurfaceTextureHelper.create("CaptureThread", eglBase.eglBaseContext),
this,
videoSource?.capturerObserver
)
// Start capture: width, height, fps
videoCapturer?.startCapture(1280, 720, 30)
localVideoTrack = peerConnectionFactory.createVideoTrack("LOCAL_VIDEO", videoSource)
localVideoTrack?.setEnabled(true)
localVideoTrack?.addSink(surfaceView)
}
private fun stopCamera() {
try {
videoCapturer?.stopCapture()
} catch (e: InterruptedException) {
// ignore
}
videoCapturer?.dispose()
videoCapturer = null
localVideoTrack?.removeSink(surfaceView)
localVideoTrack?.dispose()
localVideoTrack = null
videoSource?.dispose()
videoSource = null
}
override fun onResume() {
super.onResume()
surfaceView.onResume()
}
override fun onPause() {
super.onPause()
surfaceView.onPause()
}
override fun onDestroy() {
super.onDestroy()
stopCamera()
surfaceView.release()
peerConnectionFactory.dispose()
eglBase.release()
}
}
Notes and tips
• This code uses `Camera2Enumerator`/`Camera2Capturer` from org.webrtc. If a device lacks Camera2 support, use Camera1Enumerator / Camera1Capturer.
• Ensure the WebRTC artifact version matches your project; class names/packages can vary between builds.
• Keep UI lifecycle in sync with renderer (onResume, onPause, onDestroy) to avoid texture leaks.
• You can switch cameras by creating a new capturer for the other camera name, or use capturer's switchCamera if available.
• No LiveKit Room or publish steps are used — this is purely local preview.
If you want, I can convert this into a Compose example, add a camera switch button, or provide a CameraX-based preview instead. Which would you like next?
, with interactive elementsbig-article-66702
05/28/2026, 7:25 PMtall-airline-16756
05/28/2026, 7:25 PMVideoEncoding or publish options to specify maxBitrate and maxFramerate (use your SDK’s publish options).
1. Use simulcast / dynacast for adaptability
• Enable simulcast layers so the server/subscribers can pick lower/higher layers based on bandwidth.
• Enable dynacast so unused layers are disabled upstream to save bandwidth while keeping a high-quality layer available when needed.
1. Network and bitrate adaptation
• Ensure network conditions allow higher bitrates; use congestion control and let the encoder adapt. If your sender bitrate is too high for available uplink, quality will be reduced by the encoder/network.
• If you need constant quality, consider constraining frame rate or choosing a target bitrate that fits typical available uplink.
1. Practical checklist to improve perceived quality
• Capture at native aspect ratio and resolution of the camera sensor to avoid scaling artifacts.
• Start capture at desired resolution (1280x720 or 1920x1080) rather than upscaling.
• Set encoder bitrate appropriate to resolution/fps.
• Prefer hardware encoder when available (H.264) to reduce CPU stalls that hurt frame pacing.
• Use I-frame frequency / keyframe intervals appropriate for your content (e.g., 2s) to help recover quality after packet loss.
If you want, I can:
• show the exact code snippet to publish a LocalVideoTrack with specific VideoEncoding settings in the Android SDK, or
• suggest concrete bitrate values for your target resolution/fps based on the LiveKit/WebRTC bitrate guide.
Which would you like next?
Sources: Configuring the Client SDK for Optimal Video Quality | WebRTC Video Bitrate Guide | LiveKit, with interactive elementsbig-article-66702
05/28/2026, 7:27 PMVideoEncoding settings in the Android SDK,tall-airline-16756
05/28/2026, 7:27 PMLocalVideoTrack with specific VideoEncoding settings and publish it via the LiveKit Android SDK (assumes you already have a connected Room and camera permission).
import io.livekit.android.*
import io.livekit.android.room.Room
import io.livekit.android.room.track.LocalVideoTrack
import io.livekit.android.room.track.VideoEncoding
import io.livekit.android.room.track.VideoPublishOptions
import io.livekit.android.room.track.VideoCaptureOptions
suspend fun publishCameraWithEncoding(room: Room) {
// 1) Configure capture options (request desired resolution/fps)
val captureOptions = VideoCaptureOptions(
resolution = VideoResolution(1280, 720), // request 720p capture
frameRate = 30
)
// 2) Create a camera track (this uses the SDK's createCameraTrack helper)
// Note: API name may vary by SDK version; example uses createCameraTrack signature with options.
val localVideoTrack: LocalVideoTrack = LocalVideoTrack.createCameraTrack(
context = room.context,
captureOptions = captureOptions
)
// 3) Define encoding settings you want for publishing
val encoding = VideoEncoding(
maxBitrate = 2_500_000, // 2.5 Mbps target bitrate for 720p@30
maxFramerate = 30 // limit encoder framerate
// codec can be set where supported, e.g., VideoCodec.VP9 or VideoCodec.H264 if API allows
)
// 4) Publish with VideoPublishOptions
val publishOptions = VideoPublishOptions(
encoding = encoding,
// enableSimulcast = true, // enable if you want simulcast layers (SDK option name may vary)
// dynacast = true // enable dynacast if supported by your server/SDK
)
// 5) Publish on the local participant
room.localParticipant.publishVideoTrack(localVideoTrack, publishOptions)
}
Notes:
• Use bitrates appropriate to resolution/fps (2.5–4 Mbps is a common target for 720p–1080p). Adjust maxBitrate to match your uplink capacity.
• Consider enabling simulcast/dynacast so subscribers can receive lower/higher layers depending on bandwidth.
• Choose codec per device needs: VP9 for visual quality, H.264 when hardware-accelerated encoding is preferred.
• Ensure camera and audio permissions are granted before creating the camera track and that you call this after room.connect(...) (the room must be connected).
• API names/constructors can differ between SDK versions; if you see different function signatures, tell me your LiveKit Android SDK version and I’ll adapt the snippet., with interactive elements