package de.duffy.screencast import android.media.MediaCodec import android.media.MediaCodecInfo import android.media.MediaFormat import android.os.Build import android.util.Log import android.view.Surface import java.nio.ByteBuffer /** * Hardware-H.264-Encoder. Bekommt das Bildschirmbild ueber eine Surface vom * VirtualDisplay und liefert fertige Frames an den [Listener]. * * Die Einstellungen sind auf niedrige Latenz getrimmt: keine B-Frames, * konstante Bitrate, kurze Keyframe-Abstaende. */ class ScreenEncoder( private val width: Int, private val height: Int, private val bitrate: Int, private val frameRate: Int, private val listener: Listener ) { interface Listener { /** Einmalig, sobald der Encoder SPS/PPS ausgegeben hat. */ fun onConfig(sps: ByteArray, pps: ByteArray, width: Int, height: Int) fun onFrame(nalUnits: List, ptsUs: Long, keyframe: Boolean) } private var codec: MediaCodec? = null private var thread: Thread? = null @Volatile private var running = false private var firstPtsUs = -1L private var lastPtsUs = -1L lateinit var surface: Surface private set fun start() { val encoder = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC) try { encoder.configure(buildFormat(true), null, null, MediaCodec.CONFIGURE_FLAG_ENCODE) } catch (e: Exception) { // Manche Chips lehnen ein fest vorgegebenes Profil ab - dann ohne. Log.w(TAG, "Encoder mag die Profilvorgabe nicht, zweiter Versuch", e) encoder.reset() encoder.configure(buildFormat(false), null, null, MediaCodec.CONFIGURE_FLAG_ENCODE) } surface = encoder.createInputSurface() encoder.start() codec = encoder running = true thread = Thread({ drain(encoder) }, "screen-encoder").also { it.start() } } private fun buildFormat(withProfile: Boolean): MediaFormat { return MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, width, height).apply { setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface) setInteger(MediaFormat.KEY_BIT_RATE, bitrate) setInteger(MediaFormat.KEY_FRAME_RATE, frameRate) setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 2) setInteger(MediaFormat.KEY_BITRATE_MODE, MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CBR) if (withProfile) { // Baseline ohne B-Frames: das versteht jeder Fernseher-Browser. setInteger(MediaFormat.KEY_PROFILE, MediaCodecInfo.CodecProfileLevel.AVCProfileBaseline) setInteger(MediaFormat.KEY_LEVEL, MediaCodecInfo.CodecProfileLevel.AVCLevel4) } // Standbild trotzdem weitersenden, sonst laeuft der Puffer im Player leer. setLong(MediaFormat.KEY_REPEAT_PREVIOUS_FRAME_AFTER, 250_000L) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { setInteger(MediaFormat.KEY_PRIORITY, 0) // Echtzeit } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { setInteger(MediaFormat.KEY_LATENCY, 1) } } } /** Neuen Keyframe anfordern - noetig, wenn ein Client frisch verbindet. */ fun requestKeyFrame() { try { codec?.setParameters(android.os.Bundle().apply { putInt(MediaCodec.PARAMETER_KEY_REQUEST_SYNC_FRAME, 0) }) } catch (e: Exception) { Log.w(TAG, "Keyframe-Anforderung fehlgeschlagen", e) } } fun stop() { running = false thread?.join(1500) thread = null try { codec?.stop() } catch (_: Exception) {} try { codec?.release() } catch (_: Exception) {} codec = null try { if (::surface.isInitialized) surface.release() } catch (_: Exception) {} } private fun drain(encoder: MediaCodec) { val info = MediaCodec.BufferInfo() var sps: ByteArray? = null var pps: ByteArray? = null while (running) { val index = try { encoder.dequeueOutputBuffer(info, 100_000L) } catch (e: IllegalStateException) { Log.w(TAG, "Encoder beendet", e); break } if (index < 0) continue val buffer: ByteBuffer? = encoder.getOutputBuffer(index) if (buffer == null || info.size <= 0) { encoder.releaseOutputBuffer(index, false); continue } val data = ByteArray(info.size) buffer.position(info.offset) buffer.get(data, 0, info.size) encoder.releaseOutputBuffer(index, false) val nals = Fmp4Muxer.splitNalUnits(data, 0, data.size) if (info.flags and MediaCodec.BUFFER_FLAG_CODEC_CONFIG != 0) { for (nal in nals) { when (nal[0].toInt() and 0x1F) { 7 -> sps = nal 8 -> pps = nal } } if (sps != null && pps != null) listener.onConfig(sps!!, pps!!, width, height) continue } // Manche Encoder liefern SPS/PPS zusammen mit dem ersten Keyframe. if (sps == null || pps == null) { for (nal in nals) { when (nal[0].toInt() and 0x1F) { 7 -> sps = nal 8 -> pps = nal } } if (sps != null && pps != null) listener.onConfig(sps!!, pps!!, width, height) } if (firstPtsUs < 0) firstPtsUs = info.presentationTimeUs var pts = info.presentationTimeUs - firstPtsUs if (pts <= lastPtsUs) pts = lastPtsUs + 1000 // monoton halten lastPtsUs = pts val keyframe = info.flags and MediaCodec.BUFFER_FLAG_KEY_FRAME != 0 if (nals.isNotEmpty()) listener.onFrame(nals, pts, keyframe) } } companion object { private const val TAG = "ScreenEncoder" } }