1
0
mirror of git://f0xx.org/android_cast synced 2026-07-29 05:17:39 +03:00

libvpx integration

This commit is contained in:
Anton Afanasyeu
2026-05-18 17:05:47 +02:00
parent 1dbfc996c2
commit bbf64d30b9
18 changed files with 636 additions and 37 deletions

View File

@@ -82,4 +82,9 @@ public final class CodecNegotiator {
public static boolean isHevc(String mime) {
return MediaFormat.MIMETYPE_VIDEO_HEVC.equals(mime);
}
public static boolean isVpx(String mime) {
return MediaFormat.MIMETYPE_VIDEO_VP8.equals(mime)
|| MediaFormat.MIMETYPE_VIDEO_VP9.equals(mime);
}
}

View File

@@ -1,6 +1,7 @@
package com.foxx.androidcast.media;
import android.graphics.Bitmap;
import android.media.Image;
/**
* ARGB to YUV420 planar (I420) or semi-planar (NV12) for MediaCodec buffer input.
@@ -32,6 +33,35 @@ public final class RgbToYuv420 {
return width * height + (width * height) / 2;
}
/** Fills {@code scratch} with I420 from an RGBA {@link Image} (reuses scratch when large enough). */
public static byte[] fromRgbaImage(Image image, byte[] scratch) {
if (image == null) {
return null;
}
int width = image.getWidth();
int height = image.getHeight();
int need = bufferSize(width, height);
byte[] out = scratch != null && scratch.length >= need ? scratch : new byte[need];
Image.Plane plane = image.getPlanes()[0];
java.nio.ByteBuffer buffer = plane.getBuffer();
int rowStride = plane.getRowStride();
int pixelStride = plane.getPixelStride();
int[] argb = new int[width * height];
int offset = 0;
for (int row = 0; row < height; row++) {
int rowStart = row * rowStride;
for (int col = 0; col < width; col++) {
int pixel = buffer.getInt(rowStart + col * pixelStride);
argb[offset++] = pixel;
}
}
byte[] i420 = toI420(argb, width, height);
if (out != i420) {
System.arraycopy(i420, 0, out, 0, i420.length);
}
return out;
}
private static byte[] toI420(int[] argb, int width, int height) {
int frameSize = width * height;
int chromaW = (width + 1) / 2;

View File

@@ -0,0 +1,52 @@
package com.foxx.androidcast.media.codec;
import android.util.Log;
import android.view.Surface;
import com.foxx.androidcast.media.CodecNegotiator;
import com.foxx.androidcast.receiver.LibvpxVideoDecoder;
import com.foxx.androidcast.receiver.VideoDecoder;
import java.io.IOException;
/**
* Uses libvpx for VP8/VP9 when linked; otherwise or on failure falls back to {@link VideoDecoder}.
*/
public final class LibvpxCapableVideoDecoder implements VideoDecoderSink {
private static final String TAG = "LibvpxCapableVideoDecoder";
private final VideoDecoder hwDecoder = new VideoDecoder();
private final LibvpxVideoDecoder libvpxDecoder = new LibvpxVideoDecoder();
private VideoDecoderSink active;
@Override
public void configure(String mime, int width, int height, byte[] csd0, byte[] csd1, Surface surface,
Listener listener) throws IOException {
if (CodecNegotiator.isVpx(mime)) {
try {
libvpxDecoder.configure(mime, width, height, csd0, csd1, surface, listener);
active = libvpxDecoder;
Log.i(TAG, "Using libvpx for " + mime);
return;
} catch (IOException e) {
Log.w(TAG, "libvpx decode unavailable, trying MediaCodec: " + e.getMessage());
}
}
hwDecoder.configure(mime, width, height, csd0, csd1, surface, listener);
active = hwDecoder;
}
@Override
public void queueFrame(long ptsUs, boolean keyFrame, byte[] data) {
if (active != null) {
active.queueFrame(ptsUs, keyFrame, data);
}
}
@Override
public void release() {
hwDecoder.release();
libvpxDecoder.release();
active = null;
}
}

View File

@@ -1,22 +1,31 @@
package com.foxx.androidcast.media.codec;
import android.graphics.Bitmap;
import android.graphics.PixelFormat;
import android.media.Image;
import android.media.ImageReader;
import android.media.MediaFormat;
import android.os.Handler;
import android.os.HandlerThread;
import android.util.Log;
import android.view.Surface;
import com.foxx.androidcast.CastSettings;
import com.foxx.androidcast.CastTuningEngine;
import com.foxx.androidcast.media.RgbToYuv420;
import com.foxx.androidcast.media.codec.jni.NativeCodecBridge;
import com.foxx.androidcast.media.codec.jni.VpxEncodedFrame;
import com.foxx.androidcast.sender.VideoEncoder;
import java.io.IOException;
/**
* Software VP8/VP9 via libvpx when {@link NativeCodecBridge#isLibvpxAvailable()}.
* Surface capture still uses hardware {@link VideoEncoder} (libvpx is buffer-input only today).
* Screen capture uses an {@link ImageReader} surface; calibration may use buffer or GLES paths.
*/
public final class LibvpxVideoEncoderSink implements VideoEncoderSink {
private static final String TAG = "LibvpxVideoEncoder";
private final CastSettings.VideoCodec preference;
private final VideoEncoder hwFallback = new VideoEncoder();
@@ -29,12 +38,19 @@ public final class LibvpxVideoEncoderSink implements VideoEncoderSink {
private byte[] yuvScratch;
private boolean forceKeyframe;
private HandlerThread captureThread;
private Handler captureHandler;
private ImageReader imageReader;
public LibvpxVideoEncoderSink(CastSettings.VideoCodec preference) {
this.preference = preference;
}
@Override
public Surface getInputSurface() {
if (nativeActive && imageReader != null) {
return imageReader.getSurface();
}
return hwFallback.getInputSurface();
}
@@ -51,14 +67,37 @@ public final class LibvpxVideoEncoderSink implements VideoEncoderSink {
@Override
public Surface prepare(int width, int height, String mime, CastTuningEngine.EffectiveParams params,
Callback callback) throws IOException {
nativeActive = false;
CodecSessionRegistry.setVideoBackend(VideoCodecBackend.MEDIA_CODEC_HW, mime);
return hwFallback.prepare(width, height, mime, params, callback);
releaseCapture();
this.callback = callback;
this.width = width;
this.height = height;
this.mime = mime;
yuvScratch = new byte[RgbToYuv420.bufferSize(width, height)];
int bitrateKbps = Math.max(500, params.videoBitrate / 1000);
nativeHandle = NativeCodecBridge.vpxEncoderCreate(mime, width, height, bitrateKbps);
nativeActive = nativeHandle != 0L;
if (!nativeActive) {
Log.w(TAG, "libvpx encoder init failed; using MediaCodec for surface capture");
CodecSessionRegistry.setVideoBackend(VideoCodecBackend.MEDIA_CODEC_HW, mime);
return hwFallback.prepare(width, height, mime, params, callback);
}
CodecSessionRegistry.setVideoBackend(VideoCodecBackend.LIBVPX_NATIVE, mime);
captureThread = new HandlerThread("libvpx-capture");
captureThread.start();
captureHandler = new Handler(captureThread.getLooper());
imageReader = ImageReader.newInstance(width, height, PixelFormat.RGBA_8888, 2);
imageReader.setOnImageAvailableListener(this::onImageAvailable, captureHandler);
callback.onConfig(width, height, null, null);
Log.i(TAG, "libvpx surface encoder " + width + "x" + height + " mime=" + mime);
return imageReader.getSurface();
}
@Override
public void prepareBufferInput(int width, int height, String mime, CastTuningEngine.EffectiveParams params,
Callback callback) throws IOException {
releaseCapture();
this.width = width;
this.height = height;
this.mime = mime;
@@ -76,6 +115,33 @@ public final class LibvpxVideoEncoderSink implements VideoEncoderSink {
hwFallback.prepareBufferInput(width, height, mime, params, callback);
}
private void onImageAvailable(ImageReader reader) {
if (!nativeActive || callback == null) {
return;
}
try (Image image = reader.acquireLatestImage()) {
if (image == null) {
return;
}
byte[] yuv = RgbToYuv420.fromRgbaImage(image, yuvScratch);
if (yuv == null) {
return;
}
long ptsUs = image.getTimestamp() / 1000L;
emitEncodedFrame(yuv, ptsUs);
} catch (Exception e) {
Log.w(TAG, "Capture frame skipped: " + e.getMessage());
}
}
private void emitEncodedFrame(byte[] yuv, long ptsUs) {
VpxEncodedFrame frame = NativeCodecBridge.vpxEncodeYuvFrame(nativeHandle, yuv, ptsUs, forceKeyframe);
forceKeyframe = false;
if (frame != null && frame.data != null && callback != null) {
callback.onEncodedFrame(ptsUs, frame.keyFrame, frame.data);
}
}
@Override
public void queueBitmapFrame(Bitmap bitmap, long ptsUs) {
if (!nativeActive) {
@@ -83,7 +149,7 @@ public final class LibvpxVideoEncoderSink implements VideoEncoderSink {
return;
}
byte[] yuv = RgbToYuv420.fromBitmap(bitmap, RgbToYuv420.Layout.I420);
queueYuvFrame(yuv, ptsUs);
emitEncodedFrame(yuv, ptsUs);
}
@Override
@@ -92,12 +158,7 @@ public final class LibvpxVideoEncoderSink implements VideoEncoderSink {
hwFallback.queueYuvFrame(yuv, ptsUs);
return;
}
byte[] frame = NativeCodecBridge.vpxEncodeYuv(nativeHandle, yuv, ptsUs, forceKeyframe);
forceKeyframe = false;
if (frame != null && callback != null) {
boolean key = frame.length > 0 && (frame[0] & 0x01) == 0;
callback.onEncodedFrame(ptsUs, key, frame);
}
emitEncodedFrame(yuv, ptsUs);
}
@Override
@@ -107,9 +168,22 @@ public final class LibvpxVideoEncoderSink implements VideoEncoderSink {
nativeHandle = 0L;
nativeActive = false;
}
releaseCapture();
hwFallback.stop();
}
private void releaseCapture() {
if (imageReader != null) {
imageReader.close();
imageReader = null;
}
if (captureThread != null) {
captureThread.quitSafely();
captureThread = null;
captureHandler = null;
}
}
static String mimeFor(CastSettings.VideoCodec preference) {
if (preference == CastSettings.VideoCodec.LIBVPX_VP9) {
return MediaFormat.MIMETYPE_VIDEO_VP9;

View File

@@ -4,6 +4,7 @@ import android.media.MediaFormat;
import com.foxx.androidcast.CastSettings;
import com.foxx.androidcast.media.CodecNegotiator;
import com.foxx.androidcast.media.codec.jni.NativeCodecBridge;
/**
* Maps UI / future software codec choices to wire negotiation and HW backends.
@@ -30,7 +31,8 @@ public final class PassthroughCodecPolicy {
public static VideoCodecBackend videoBackendFor(CastSettings.VideoCodec preference) {
if (preference == CastSettings.VideoCodec.LIBVPX_VP8
|| preference == CastSettings.VideoCodec.LIBVPX_VP9) {
return VideoCodecBackend.LIBVPX_STUB;
return NativeCodecBridge.isLibvpxAvailable()
? VideoCodecBackend.LIBVPX_NATIVE : VideoCodecBackend.LIBVPX_STUB;
}
if (preference == CastSettings.VideoCodec.PASSTHROUGH) {
return VideoCodecBackend.PASSTHROUGH;

View File

@@ -1,5 +1,6 @@
package com.foxx.androidcast.media.codec;
import com.foxx.androidcast.media.codec.jni.NativeCodecBridge;
import com.foxx.androidcast.receiver.VideoDecoder;
/** Creates video decoder sinks. */
@@ -7,6 +8,9 @@ public final class VideoDecoderFactory {
private VideoDecoderFactory() {}
public static VideoDecoderSink createDecoder() {
if (NativeCodecBridge.isLibvpxAvailable()) {
return new LibvpxCapableVideoDecoder();
}
return new VideoDecoder();
}
}

View File

@@ -1,6 +1,7 @@
package com.foxx.androidcast.media.codec.jni;
import android.util.Log;
import android.view.Surface;
/** JNI probes for third-party codecs built from {@code ndk/} + {@code third-party/}. */
public final class NativeCodecBridge {
@@ -47,10 +48,36 @@ public final class NativeCodecBridge {
}
public static byte[] vpxEncodeYuv(long handle, byte[] i420, long ptsUs, boolean forceKeyframe) {
VpxEncodedFrame frame = vpxEncodeYuvFrame(handle, i420, ptsUs, forceKeyframe);
return frame != null ? frame.data : null;
}
public static VpxEncodedFrame vpxEncodeYuvFrame(long handle, byte[] i420, long ptsUs,
boolean forceKeyframe) {
if (handle == 0L || i420 == null) {
return null;
}
return nativeVpxEncodeYuv(handle, i420, ptsUs, forceKeyframe);
return nativeVpxEncodeYuvFrame(handle, i420, ptsUs, forceKeyframe);
}
public static long vpxDecoderCreate(String mime) {
if (!isLibvpxAvailable()) {
return 0L;
}
return nativeVpxDecoderCreate(mime);
}
public static boolean vpxDecodeFrame(long handle, byte[] data, long ptsUs, Surface surface) {
if (handle == 0L || data == null || surface == null) {
return false;
}
return nativeVpxDecodeFrame(handle, data, ptsUs, surface);
}
public static void vpxDecoderRelease(long handle) {
if (handle != 0L) {
nativeVpxDecoderRelease(handle);
}
}
public static void vpxEncoderRelease(long handle) {
@@ -75,7 +102,16 @@ public final class NativeCodecBridge {
private static native byte[] nativeVpxEncodeYuv(long handle, byte[] yuv, long ptsUs, boolean forceKeyframe);
private static native VpxEncodedFrame nativeVpxEncodeYuvFrame(long handle, byte[] yuv, long ptsUs,
boolean forceKeyframe);
private static native void nativeVpxEncoderRelease(long handle);
private static native void nativeVpxRequestKeyframe(long handle);
private static native long nativeVpxDecoderCreate(String mime);
private static native boolean nativeVpxDecodeFrame(long handle, byte[] data, long ptsUs, Surface surface);
private static native void nativeVpxDecoderRelease(long handle);
}

View File

@@ -0,0 +1,12 @@
package com.foxx.androidcast.media.codec.jni;
/** One compressed VP8/VP9 frame from libvpx. */
public final class VpxEncodedFrame {
public final byte[] data;
public final boolean keyFrame;
public VpxEncodedFrame(byte[] data, boolean keyFrame) {
this.data = data;
this.keyFrame = keyFrame;
}
}

View File

@@ -0,0 +1,85 @@
package com.foxx.androidcast.receiver;
import android.util.Log;
import android.view.Surface;
import com.foxx.androidcast.media.codec.VideoDecoderSink;
import com.foxx.androidcast.media.codec.jni.NativeCodecBridge;
import java.io.IOException;
/** Software VP8/VP9 decode via libvpx, rendering to a {@link Surface}. */
public final class LibvpxVideoDecoder implements VideoDecoderSink {
private static final String TAG = "LibvpxVideoDecoder";
private final Object lock = new Object();
private long nativeHandle;
private Surface outputSurface;
private Listener listener;
private boolean released;
private boolean firstFrameNotified;
@Override
public void configure(String mime, int width, int height, byte[] csd0, byte[] csd1, Surface surface,
Listener listener) throws IOException {
synchronized (lock) {
releaseLocked();
if (!NativeCodecBridge.isLibvpxAvailable()) {
throw new IOException("libvpx not available");
}
nativeHandle = NativeCodecBridge.vpxDecoderCreate(mime);
if (nativeHandle == 0L) {
throw new IOException("libvpx decoder init failed for " + mime);
}
this.outputSurface = surface;
this.listener = listener;
this.released = false;
this.firstFrameNotified = false;
Log.i(TAG, "Configured libvpx decoder " + mime + " " + width + "x" + height);
}
}
@Override
public void queueFrame(long ptsUs, boolean keyFrame, byte[] data) {
synchronized (lock) {
if (released || nativeHandle == 0L || outputSurface == null || data == null || data.length == 0) {
return;
}
if (NativeCodecBridge.vpxDecodeFrame(nativeHandle, data, ptsUs, outputSurface)) {
notifyRenderedLocked();
}
}
}
@Override
public void release() {
synchronized (lock) {
releaseLocked();
}
}
private void releaseLocked() {
released = true;
listener = null;
outputSurface = null;
firstFrameNotified = false;
if (nativeHandle != 0L) {
NativeCodecBridge.vpxDecoderRelease(nativeHandle);
nativeHandle = 0L;
}
}
private void notifyRenderedLocked() {
Listener cb = listener;
if (cb == null) {
return;
}
if (!firstFrameNotified) {
firstFrameNotified = true;
Log.i(TAG, "First frame rendered (libvpx)");
cb.onFirstFrameRendered();
} else {
cb.onFrameRendered();
}
}
}

View File

@@ -6,6 +6,7 @@ import android.media.MediaFormat;
import android.os.Build;
import com.foxx.androidcast.CastSettings;
import com.foxx.androidcast.media.codec.jni.NativeCodecBridge;
import java.util.ArrayList;
import java.util.LinkedHashSet;
@@ -121,6 +122,10 @@ public final class CodecCatalog {
if (out.isEmpty()) {
out.add(MediaFormat.MIMETYPE_VIDEO_AVC);
}
if (NativeCodecBridge.isLibvpxAvailable()) {
out.add(MediaFormat.MIMETYPE_VIDEO_VP8);
out.add(MediaFormat.MIMETYPE_VIDEO_VP9);
}
return new ArrayList<>(out);
}
}

View File

@@ -475,8 +475,6 @@ public class ScreenCastService extends Service implements
}
videoEncoder = VideoCodecFactory.createEncoder(settings.getVideoCodec());
CodecSessionRegistry.setVideoBackend(
PassthroughCodecPolicy.videoBackendFor(settings.getVideoCodec()), mime);
CodecSessionRegistry.setProtectionSummary(PassthroughCodecPolicy.protectionDescription(
settings.getStreamProtection(), settings.getTransport(), settings.getAudioCodec()));
VideoEncoderSink.Callback videoCb = buildVideoEncoderCallback(mime);