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
Original file line number Diff line number Diff line change
Expand Up @@ -85,11 +85,12 @@ class VideoConfig(
/**
* Video encoder I-frame interval in seconds.
* This is a best effort as few camera can not generate a fixed framerate.
* For live streaming, I-frame interval should be really low. For recording, I-frame interval should be higher.
* For live streaming, a longer interval reduces bandwidth at the cost of resilience to packet loss.
* For recording, I-frame interval should be higher.
* A value of 0 means that each frame is an I-frame.
* On device with API < 25, this value will be rounded to an integer. So don't expect a precise value and any value < 0.5 will be considered as 0.
*/
val gopDuration: Float = 1f // 1s between I frames
val gopDuration: Float = 3f // 3s between I frames for better compression efficiency
) : Config(mimeType, startBitrate, profile) {
init {
require(mimeType.isVideo) { "MimeType must be video" }
Expand Down Expand Up @@ -220,10 +221,10 @@ class VideoConfig(
fun getBestBitrate(resolution: Size): Int {
val numOfPixels = resolution.width * resolution.height
return when {
numOfPixels <= 320 * 240 -> 800000
numOfPixels <= 640 * 480 -> 1000000
numOfPixels <= 1280 * 720 -> 2000000
numOfPixels <= 1920 * 1080 -> 3500000
numOfPixels <= 320 * 240 -> 500000 // Lower for efficiency
numOfPixels <= 640 * 480 -> 800000 // Reduced for 640x480 (our default)
numOfPixels <= 1280 * 720 -> 1500000 // Slightly reduced
numOfPixels <= 1920 * 1080 -> 3000000
else -> 4000000
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,14 @@
package io.github.thibaultbee.streampack.internal.encoders

import android.media.MediaCodec
import android.media.MediaCodecInfo
import android.media.MediaFormat
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.HandlerThread
import io.github.thibaultbee.streampack.data.Config
import io.github.thibaultbee.streampack.data.VideoConfig
import io.github.thibaultbee.streampack.error.StreamPackError
import io.github.thibaultbee.streampack.internal.data.Frame
import io.github.thibaultbee.streampack.internal.events.EventHandler
Expand Down Expand Up @@ -156,7 +159,25 @@ abstract class MediaCodecEncoder<T : Config>(
open fun createMediaFormat(config: Config, withProfileLevel: Boolean) =
config.getFormat(withProfileLevel)

open fun extendMediaFormat(config: Config, format: MediaFormat) {}
open fun extendMediaFormat(config: Config, format: MediaFormat) {
// Quality-focused parameters
if (config is VideoConfig) {
try {
// Bitrate mode: prefer quality over constant bitrate
if (config.mimeType == MediaFormat.MIMETYPE_VIDEO_AVC ||
config.mimeType == MediaFormat.MIMETYPE_VIDEO_HEVC) {
// Use VBR mode for better quality/size ratio for H.264/H.265
format.setInteger(MediaFormat.KEY_BITRATE_MODE,
MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_VBR)
}

// Prioritize quality over speed when using AVC/HEVC for streaming
format.setInteger("quality", 1)
} catch (e: Exception) {
Logger.d(TAG, "Could not set quality parameters: ${e.message}")
}
}
}

private fun createCodec(config: Config, withProfileLevel: Boolean): MediaCodec {
val format = createMediaFormat(config, withProfileLevel)
Expand All @@ -180,6 +201,18 @@ abstract class MediaCodecEncoder<T : Config>(
codec.setCallback(encoderCallback)
}

// Power-efficient encoding parameters
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
try {
// Set operating rate to normal (not low-latency) - more power efficient
val params = Bundle()
params.putInt(MediaCodec.PARAMETER_KEY_VIDEO_BITRATE, _bitrate)
codec.setParameters(params)
} catch (e: Exception) {
Logger.d(TAG, "Could not set encoder parameters: ${e.message}")
}
}

try {
codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
} catch (e: Exception) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import io.github.thibaultbee.streampack.internal.orientation.ISourceOrientationL
import io.github.thibaultbee.streampack.internal.orientation.ISourceOrientationProvider
import io.github.thibaultbee.streampack.internal.utils.av.video.DynamicRangeProfile
import io.github.thibaultbee.streampack.listeners.OnErrorListener
import io.github.thibaultbee.streampack.logger.Logger
import java.util.concurrent.Executors

/**
Expand Down Expand Up @@ -130,10 +131,20 @@ class VideoMediaCodecEncoder(
private var eglSurface: EglWindowSurface? = null
private var fullFrameRect: FullFrameRect? = null
private var textureId = -1
private val executor = Executors.newSingleThreadExecutor()
// Single thread with minimal priority executor for power savings
private val executor = Executors.newSingleThreadExecutor { r ->
Thread(r).apply {
priority = Thread.MIN_PRIORITY
name = "encoder-power-save-thread"
}
}
private var isRunning = false
private var surfaceTexture: SurfaceTexture? = null
private val stMatrix = FloatArray(16)

// Power optimization: batch frame processing to reduce wake-ups - strict 24fps cap
private var lastFrameTimeMs = 0L
private val minFrameIntervalMs = 41L // ~24fps max to match video encoding settings

private var _inputSurface: Surface? = null
val inputSurface: Surface?
Expand Down Expand Up @@ -244,23 +255,85 @@ class VideoMediaCodecEncoder(
}
}

// Track how many frames we're dropping
private var totalFramesReceived = 0L
private var totalFramesProcessed = 0L
private var lastLogTime = 0L

// System time when the first frame was received
private var streamStartTimeMs = 0L

override fun onFrameAvailable(surfaceTexture: SurfaceTexture) {
if (!isRunning) {
return
}


// Initialize stream start time if needed
if (streamStartTimeMs == 0L) {
streamStartTimeMs = System.currentTimeMillis()
}

// Count incoming frames for statistics
totalFramesReceived++

// Get system time for frame rate control
val currentTimeMs = System.currentTimeMillis()
val timeSinceLastFrame = currentTimeMs - lastFrameTimeMs

// CRITICAL: Skip enqueueing to executor if we're falling behind
// This prevents executor queue buildup which is a major cause of latency
if (timeSinceLastFrame < minFrameIntervalMs && lastFrameTimeMs > 0) {
// Skip this frame entirely - don't even queue it
return
}

// Queue for processing only if we're not backed up
executor.execute {
synchronized(this) {
// Check running state again after potential queue delay
if (!isRunning) return@synchronized

eglSurface?.let {
it.makeCurrent()
surfaceTexture.updateTexImage()

// Critical: Aggressively flush ALL pending frames to get to latest
// This ensures we stay current even with a burst of frames
var frameCount = 0
var lastTimestamp: Long
do {
lastTimestamp = surfaceTexture.timestamp
surfaceTexture.updateTexImage()
frameCount++
} while (frameCount < 20 && // Limit loop iterations for safety
surfaceTexture.timestamp != 0L &&
surfaceTexture.timestamp != lastTimestamp) // Stop when no new frames

// Get latest transform matrix
surfaceTexture.getTransformMatrix(stMatrix)

// Use the identity matrix for MVP so our 2x2 FULL_RECTANGLE covers the viewport.
// Draw and send the frame
fullFrameRect?.drawFrame(textureId, stMatrix)
it.setPresentationTime(surfaceTexture.timestamp)
it.swapBuffers()
lastFrameTimeMs = currentTimeMs
totalFramesProcessed++

// Release texture image
surfaceTexture.releaseTexImage()

// Log statistics every 5 seconds
if (currentTimeMs - lastLogTime > 5000) {
val streamTimeSeconds = (currentTimeMs - streamStartTimeMs) / 1000.0
val droppedFrames = totalFramesReceived - totalFramesProcessed
val droppedPercent = if (totalFramesReceived > 0)
(droppedFrames * 100.0 / totalFramesReceived) else 0.0

Logger.d(TAG, "Stream stats: Received=${totalFramesReceived}, " +
"Processed=${totalFramesProcessed}, " +
"Dropped=${droppedFrames} (${droppedPercent.toInt()}%), " +
"Avg FPS=${totalFramesProcessed / streamTimeSeconds}")
lastLogTime = currentTimeMs
}
}
}
}
Expand Down Expand Up @@ -300,5 +373,9 @@ class VideoMediaCodecEncoder(
surfaceTexture?.release()
surfaceTexture = null
}

companion object {
private const val TAG = "CodecSurface"
}
}
}
}
Loading