mirror of
git://f0xx.org/android_cast
synced 2026-07-29 05:17:39 +03:00
libvpx integration
This commit is contained in:
@@ -82,4 +82,9 @@ public final class CodecNegotiator {
|
|||||||
public static boolean isHevc(String mime) {
|
public static boolean isHevc(String mime) {
|
||||||
return MediaFormat.MIMETYPE_VIDEO_HEVC.equals(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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.foxx.androidcast.media;
|
package com.foxx.androidcast.media;
|
||||||
|
|
||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
|
import android.media.Image;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ARGB to YUV420 planar (I420) or semi-planar (NV12) for MediaCodec buffer input.
|
* 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;
|
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) {
|
private static byte[] toI420(int[] argb, int width, int height) {
|
||||||
int frameSize = width * height;
|
int frameSize = width * height;
|
||||||
int chromaW = (width + 1) / 2;
|
int chromaW = (width + 1) / 2;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,22 +1,31 @@
|
|||||||
package com.foxx.androidcast.media.codec;
|
package com.foxx.androidcast.media.codec;
|
||||||
|
|
||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
|
import android.graphics.PixelFormat;
|
||||||
|
import android.media.Image;
|
||||||
|
import android.media.ImageReader;
|
||||||
import android.media.MediaFormat;
|
import android.media.MediaFormat;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.HandlerThread;
|
||||||
|
import android.util.Log;
|
||||||
import android.view.Surface;
|
import android.view.Surface;
|
||||||
|
|
||||||
import com.foxx.androidcast.CastSettings;
|
import com.foxx.androidcast.CastSettings;
|
||||||
import com.foxx.androidcast.CastTuningEngine;
|
import com.foxx.androidcast.CastTuningEngine;
|
||||||
import com.foxx.androidcast.media.RgbToYuv420;
|
import com.foxx.androidcast.media.RgbToYuv420;
|
||||||
import com.foxx.androidcast.media.codec.jni.NativeCodecBridge;
|
import com.foxx.androidcast.media.codec.jni.NativeCodecBridge;
|
||||||
|
import com.foxx.androidcast.media.codec.jni.VpxEncodedFrame;
|
||||||
import com.foxx.androidcast.sender.VideoEncoder;
|
import com.foxx.androidcast.sender.VideoEncoder;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Software VP8/VP9 via libvpx when {@link NativeCodecBridge#isLibvpxAvailable()}.
|
* 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 {
|
public final class LibvpxVideoEncoderSink implements VideoEncoderSink {
|
||||||
|
private static final String TAG = "LibvpxVideoEncoder";
|
||||||
|
|
||||||
private final CastSettings.VideoCodec preference;
|
private final CastSettings.VideoCodec preference;
|
||||||
private final VideoEncoder hwFallback = new VideoEncoder();
|
private final VideoEncoder hwFallback = new VideoEncoder();
|
||||||
|
|
||||||
@@ -29,12 +38,19 @@ public final class LibvpxVideoEncoderSink implements VideoEncoderSink {
|
|||||||
private byte[] yuvScratch;
|
private byte[] yuvScratch;
|
||||||
private boolean forceKeyframe;
|
private boolean forceKeyframe;
|
||||||
|
|
||||||
|
private HandlerThread captureThread;
|
||||||
|
private Handler captureHandler;
|
||||||
|
private ImageReader imageReader;
|
||||||
|
|
||||||
public LibvpxVideoEncoderSink(CastSettings.VideoCodec preference) {
|
public LibvpxVideoEncoderSink(CastSettings.VideoCodec preference) {
|
||||||
this.preference = preference;
|
this.preference = preference;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Surface getInputSurface() {
|
public Surface getInputSurface() {
|
||||||
|
if (nativeActive && imageReader != null) {
|
||||||
|
return imageReader.getSurface();
|
||||||
|
}
|
||||||
return hwFallback.getInputSurface();
|
return hwFallback.getInputSurface();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,14 +67,37 @@ public final class LibvpxVideoEncoderSink implements VideoEncoderSink {
|
|||||||
@Override
|
@Override
|
||||||
public Surface prepare(int width, int height, String mime, CastTuningEngine.EffectiveParams params,
|
public Surface prepare(int width, int height, String mime, CastTuningEngine.EffectiveParams params,
|
||||||
Callback callback) throws IOException {
|
Callback callback) throws IOException {
|
||||||
nativeActive = false;
|
releaseCapture();
|
||||||
CodecSessionRegistry.setVideoBackend(VideoCodecBackend.MEDIA_CODEC_HW, mime);
|
this.callback = callback;
|
||||||
return hwFallback.prepare(width, height, mime, params, 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
|
@Override
|
||||||
public void prepareBufferInput(int width, int height, String mime, CastTuningEngine.EffectiveParams params,
|
public void prepareBufferInput(int width, int height, String mime, CastTuningEngine.EffectiveParams params,
|
||||||
Callback callback) throws IOException {
|
Callback callback) throws IOException {
|
||||||
|
releaseCapture();
|
||||||
this.width = width;
|
this.width = width;
|
||||||
this.height = height;
|
this.height = height;
|
||||||
this.mime = mime;
|
this.mime = mime;
|
||||||
@@ -76,6 +115,33 @@ public final class LibvpxVideoEncoderSink implements VideoEncoderSink {
|
|||||||
hwFallback.prepareBufferInput(width, height, mime, params, callback);
|
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
|
@Override
|
||||||
public void queueBitmapFrame(Bitmap bitmap, long ptsUs) {
|
public void queueBitmapFrame(Bitmap bitmap, long ptsUs) {
|
||||||
if (!nativeActive) {
|
if (!nativeActive) {
|
||||||
@@ -83,7 +149,7 @@ public final class LibvpxVideoEncoderSink implements VideoEncoderSink {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
byte[] yuv = RgbToYuv420.fromBitmap(bitmap, RgbToYuv420.Layout.I420);
|
byte[] yuv = RgbToYuv420.fromBitmap(bitmap, RgbToYuv420.Layout.I420);
|
||||||
queueYuvFrame(yuv, ptsUs);
|
emitEncodedFrame(yuv, ptsUs);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -92,12 +158,7 @@ public final class LibvpxVideoEncoderSink implements VideoEncoderSink {
|
|||||||
hwFallback.queueYuvFrame(yuv, ptsUs);
|
hwFallback.queueYuvFrame(yuv, ptsUs);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
byte[] frame = NativeCodecBridge.vpxEncodeYuv(nativeHandle, yuv, ptsUs, forceKeyframe);
|
emitEncodedFrame(yuv, ptsUs);
|
||||||
forceKeyframe = false;
|
|
||||||
if (frame != null && callback != null) {
|
|
||||||
boolean key = frame.length > 0 && (frame[0] & 0x01) == 0;
|
|
||||||
callback.onEncodedFrame(ptsUs, key, frame);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -107,9 +168,22 @@ public final class LibvpxVideoEncoderSink implements VideoEncoderSink {
|
|||||||
nativeHandle = 0L;
|
nativeHandle = 0L;
|
||||||
nativeActive = false;
|
nativeActive = false;
|
||||||
}
|
}
|
||||||
|
releaseCapture();
|
||||||
hwFallback.stop();
|
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) {
|
static String mimeFor(CastSettings.VideoCodec preference) {
|
||||||
if (preference == CastSettings.VideoCodec.LIBVPX_VP9) {
|
if (preference == CastSettings.VideoCodec.LIBVPX_VP9) {
|
||||||
return MediaFormat.MIMETYPE_VIDEO_VP9;
|
return MediaFormat.MIMETYPE_VIDEO_VP9;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import android.media.MediaFormat;
|
|||||||
|
|
||||||
import com.foxx.androidcast.CastSettings;
|
import com.foxx.androidcast.CastSettings;
|
||||||
import com.foxx.androidcast.media.CodecNegotiator;
|
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.
|
* 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) {
|
public static VideoCodecBackend videoBackendFor(CastSettings.VideoCodec preference) {
|
||||||
if (preference == CastSettings.VideoCodec.LIBVPX_VP8
|
if (preference == CastSettings.VideoCodec.LIBVPX_VP8
|
||||||
|| preference == CastSettings.VideoCodec.LIBVPX_VP9) {
|
|| preference == CastSettings.VideoCodec.LIBVPX_VP9) {
|
||||||
return VideoCodecBackend.LIBVPX_STUB;
|
return NativeCodecBridge.isLibvpxAvailable()
|
||||||
|
? VideoCodecBackend.LIBVPX_NATIVE : VideoCodecBackend.LIBVPX_STUB;
|
||||||
}
|
}
|
||||||
if (preference == CastSettings.VideoCodec.PASSTHROUGH) {
|
if (preference == CastSettings.VideoCodec.PASSTHROUGH) {
|
||||||
return VideoCodecBackend.PASSTHROUGH;
|
return VideoCodecBackend.PASSTHROUGH;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.foxx.androidcast.media.codec;
|
package com.foxx.androidcast.media.codec;
|
||||||
|
|
||||||
|
import com.foxx.androidcast.media.codec.jni.NativeCodecBridge;
|
||||||
import com.foxx.androidcast.receiver.VideoDecoder;
|
import com.foxx.androidcast.receiver.VideoDecoder;
|
||||||
|
|
||||||
/** Creates video decoder sinks. */
|
/** Creates video decoder sinks. */
|
||||||
@@ -7,6 +8,9 @@ public final class VideoDecoderFactory {
|
|||||||
private VideoDecoderFactory() {}
|
private VideoDecoderFactory() {}
|
||||||
|
|
||||||
public static VideoDecoderSink createDecoder() {
|
public static VideoDecoderSink createDecoder() {
|
||||||
|
if (NativeCodecBridge.isLibvpxAvailable()) {
|
||||||
|
return new LibvpxCapableVideoDecoder();
|
||||||
|
}
|
||||||
return new VideoDecoder();
|
return new VideoDecoder();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.foxx.androidcast.media.codec.jni;
|
package com.foxx.androidcast.media.codec.jni;
|
||||||
|
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
import android.view.Surface;
|
||||||
|
|
||||||
/** JNI probes for third-party codecs built from {@code ndk/} + {@code third-party/}. */
|
/** JNI probes for third-party codecs built from {@code ndk/} + {@code third-party/}. */
|
||||||
public final class NativeCodecBridge {
|
public final class NativeCodecBridge {
|
||||||
@@ -47,10 +48,36 @@ public final class NativeCodecBridge {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static byte[] vpxEncodeYuv(long handle, byte[] i420, long ptsUs, boolean forceKeyframe) {
|
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) {
|
if (handle == 0L || i420 == null) {
|
||||||
return 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) {
|
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 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 nativeVpxEncoderRelease(long handle);
|
||||||
|
|
||||||
private static native void nativeVpxRequestKeyframe(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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import android.media.MediaFormat;
|
|||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
|
|
||||||
import com.foxx.androidcast.CastSettings;
|
import com.foxx.androidcast.CastSettings;
|
||||||
|
import com.foxx.androidcast.media.codec.jni.NativeCodecBridge;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.LinkedHashSet;
|
import java.util.LinkedHashSet;
|
||||||
@@ -121,6 +122,10 @@ public final class CodecCatalog {
|
|||||||
if (out.isEmpty()) {
|
if (out.isEmpty()) {
|
||||||
out.add(MediaFormat.MIMETYPE_VIDEO_AVC);
|
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);
|
return new ArrayList<>(out);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -475,8 +475,6 @@ public class ScreenCastService extends Service implements
|
|||||||
}
|
}
|
||||||
|
|
||||||
videoEncoder = VideoCodecFactory.createEncoder(settings.getVideoCodec());
|
videoEncoder = VideoCodecFactory.createEncoder(settings.getVideoCodec());
|
||||||
CodecSessionRegistry.setVideoBackend(
|
|
||||||
PassthroughCodecPolicy.videoBackendFor(settings.getVideoCodec()), mime);
|
|
||||||
CodecSessionRegistry.setProtectionSummary(PassthroughCodecPolicy.protectionDescription(
|
CodecSessionRegistry.setProtectionSummary(PassthroughCodecPolicy.protectionDescription(
|
||||||
settings.getStreamProtection(), settings.getTransport(), settings.getAudioCodec()));
|
settings.getStreamProtection(), settings.getTransport(), settings.getAudioCodec()));
|
||||||
VideoEncoderSink.Callback videoCb = buildVideoEncoderCallback(mime);
|
VideoEncoderSink.Callback videoCb = buildVideoEncoderCallback(mime);
|
||||||
|
|||||||
@@ -22,10 +22,11 @@ Enable in **Settings → Stream protection (UDP)** on both sender and receiver,
|
|||||||
| Item | Status |
|
| Item | Status |
|
||||||
|------|--------|
|
|------|--------|
|
||||||
| `libandroidcast_codecs.so` + JNI probes | Done |
|
| `libandroidcast_codecs.so` + JNI probes | Done |
|
||||||
| Cross-compile `libvpx.a` (`scripts/build-native-codecs.sh`) | Script ready (needs submodule + NDK build) |
|
| Cross-compile `libvpx.a` (`scripts/build-native-codecs.sh`) | Done |
|
||||||
| JNI encode in `libvpx_bridge.c` | Done (when `ANDROIDCAST_LIBVPX_LIB` set) |
|
| JNI encode/decode in `libvpx_bridge.c` | Done (when `libvpx.a` linked per ABI) |
|
||||||
| `LibvpxVideoEncoderSink` (buffer / calibration path) | Done |
|
| `LibvpxVideoEncoderSink` (surface + buffer) | Done |
|
||||||
| libvpx decoder on receiver | TODO |
|
| libvpx decoder on receiver (`LibvpxCapableVideoDecoder`) | Done |
|
||||||
|
| Surface → libvpx without MediaCodec passthrough on cast | Done (ImageReader) |
|
||||||
| Opus / Speex build + sinks | TODO |
|
| Opus / Speex build + sinks | TODO |
|
||||||
|
|
||||||
See [ndk/README.md](../ndk/README.md).
|
See [ndk/README.md](../ndk/README.md).
|
||||||
|
|||||||
@@ -46,4 +46,5 @@ else()
|
|||||||
endif()
|
endif()
|
||||||
|
|
||||||
find_library(log-lib log)
|
find_library(log-lib log)
|
||||||
target_link_libraries(androidcast_codecs ${log-lib})
|
find_library(android-lib android)
|
||||||
|
target_link_libraries(androidcast_codecs ${log-lib} ${android-lib})
|
||||||
|
|||||||
@@ -8,13 +8,16 @@ The app loads `libandroidcast_codecs.so` for availability probes and future soft
|
|||||||
|-----------|------|--------|-------|
|
|-----------|------|--------|-------|
|
||||||
| FEC 3/4 + RS | Yes | — | v2 **per-shard** on wire (`FecShardWire`); v1 monolithic decode still accepted |
|
| FEC 3/4 + RS | Yes | — | v2 **per-shard** on wire (`FecShardWire`); v1 monolithic decode still accepted |
|
||||||
| NACK + retransmit cache | Yes | — | UDP `MSG_NACK`, combined with FEC when enabled |
|
| NACK + retransmit cache | Yes | — | UDP `MSG_NACK`, combined with FEC when enabled |
|
||||||
| libvpx VP8/VP9 | Stub | Probe only | HW passthrough until `ANDROIDCAST_LIBVPX_LIB` is linked |
|
| libvpx VP8/VP9 | Native | Encode + decode | Screen cast via ImageReader; receiver libvpx or MediaCodec fallback |
|
||||||
| Opus / Speex | Stub | Probe only | Submodules under `third-party/` |
|
| Opus / Speex | Stub | Probe only | Submodules under `third-party/` |
|
||||||
|
|
||||||
## Enable libvpx (developer)
|
## Enable libvpx (developer)
|
||||||
|
|
||||||
1. `git submodule update --init third-party/libvpx`
|
1. `git submodule update --init third-party/libvpx`
|
||||||
2. Build `libvpx.a` for your ABI: `./scripts/build-native-codecs.sh arm64-v8a`
|
2. Build `libvpx.a` per ABI (NDK is auto-detected; see `scripts/android-ndk.sh`):
|
||||||
|
|
||||||
|
`./scripts/build-native-codecs.sh arm64-v8a`
|
||||||
|
|
||||||
(If the repo path has **spaces**, the script uses `/tmp/androidcast-libvpx-$USER` automatically.)
|
(If the repo path has **spaces**, the script uses `/tmp/androidcast-libvpx-$USER` automatically.)
|
||||||
3. Build **each ABI** you ship (Gradle links `build/native/<abi>/libvpx.a` automatically):
|
3. Build **each ABI** you ship (Gradle links `build/native/<abi>/libvpx.a` automatically):
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
#include <jni.h>
|
#include <jni.h>
|
||||||
|
#include <android/native_window.h>
|
||||||
|
#include <android/native_window_jni.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
@@ -14,6 +16,7 @@
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
#define VPX_ENC_MAGIC 0x56505845L /* VPXE */
|
#define VPX_ENC_MAGIC 0x56505845L /* VPXE */
|
||||||
|
#define VPX_DEC_MAGIC 0x56505844L /* VPXD */
|
||||||
|
|
||||||
#if ANDROIDCAST_HAVE_LIBVPX
|
#if ANDROIDCAST_HAVE_LIBVPX
|
||||||
|
|
||||||
@@ -34,6 +37,13 @@ static vpx_codec_iface_t *pick_encoder_iface(const char *mime) {
|
|||||||
return vpx_codec_vp8_cx();
|
return vpx_codec_vp8_cx();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static vpx_codec_iface_t *pick_decoder_iface(const char *mime) {
|
||||||
|
if (mime != NULL && strstr(mime, "VP9") != NULL) {
|
||||||
|
return vpx_codec_vp9_dx();
|
||||||
|
}
|
||||||
|
return vpx_codec_vp8_dx();
|
||||||
|
}
|
||||||
|
|
||||||
static vpx_img_fmt_t img_fmt(void) {
|
static vpx_img_fmt_t img_fmt(void) {
|
||||||
return VPX_IMG_FMT_I420;
|
return VPX_IMG_FMT_I420;
|
||||||
}
|
}
|
||||||
@@ -63,6 +73,75 @@ static VpxEncoderCtx *enc_from_handle(jlong handle) {
|
|||||||
return ctx;
|
return ctx;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
unsigned long magic;
|
||||||
|
vpx_codec_ctx_t codec;
|
||||||
|
} VpxDecoderCtx;
|
||||||
|
|
||||||
|
static VpxDecoderCtx *dec_from_handle(jlong handle) {
|
||||||
|
VpxDecoderCtx *ctx = (VpxDecoderCtx *) (intptr_t) handle;
|
||||||
|
if (ctx == NULL || ctx->magic != VPX_DEC_MAGIC) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
return ctx;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void render_i420_to_window(ANativeWindow *window, const vpx_image_t *img) {
|
||||||
|
if (window == NULL || img == NULL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const int width = (int) img->d_w;
|
||||||
|
const int height = (int) img->d_h;
|
||||||
|
if (width <= 0 || height <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ANativeWindow_setBuffersGeometry(window, width, height, WINDOW_FORMAT_RGBA_8888);
|
||||||
|
ANativeWindow_Buffer buffer;
|
||||||
|
if (ANativeWindow_lock(window, &buffer, NULL) != 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
uint8_t *dst = (uint8_t *) buffer.bits;
|
||||||
|
const int dst_stride = buffer.stride * 4;
|
||||||
|
const uint8_t *y_plane = img->planes[VPX_PLANE_Y];
|
||||||
|
const uint8_t *u_plane = img->planes[VPX_PLANE_U];
|
||||||
|
const uint8_t *v_plane = img->planes[VPX_PLANE_V];
|
||||||
|
const int y_stride = img->stride[VPX_PLANE_Y];
|
||||||
|
const int u_stride = img->stride[VPX_PLANE_U];
|
||||||
|
const int v_stride = img->stride[VPX_PLANE_V];
|
||||||
|
|
||||||
|
for (int row = 0; row < height; row++) {
|
||||||
|
uint8_t *row_dst = dst + row * dst_stride;
|
||||||
|
for (int col = 0; col < width; col++) {
|
||||||
|
int y = y_plane[row * y_stride + col];
|
||||||
|
int u = u_plane[(row / 2) * u_stride + (col / 2)] - 128;
|
||||||
|
int v = v_plane[(row / 2) * v_stride + (col / 2)] - 128;
|
||||||
|
int r = y + ((351 * v) >> 8);
|
||||||
|
int g = y - ((179 * v + 86 * u) >> 8);
|
||||||
|
int b = y + ((443 * u) >> 8);
|
||||||
|
if (r < 0) {
|
||||||
|
r = 0;
|
||||||
|
} else if (r > 255) {
|
||||||
|
r = 255;
|
||||||
|
}
|
||||||
|
if (g < 0) {
|
||||||
|
g = 0;
|
||||||
|
} else if (g > 255) {
|
||||||
|
g = 255;
|
||||||
|
}
|
||||||
|
if (b < 0) {
|
||||||
|
b = 0;
|
||||||
|
} else if (b > 255) {
|
||||||
|
b = 255;
|
||||||
|
}
|
||||||
|
row_dst[col * 4] = (uint8_t) r;
|
||||||
|
row_dst[col * 4 + 1] = (uint8_t) g;
|
||||||
|
row_dst[col * 4 + 2] = (uint8_t) b;
|
||||||
|
row_dst[col * 4 + 3] = 255;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ANativeWindow_unlockAndPost(window);
|
||||||
|
}
|
||||||
|
|
||||||
#endif /* ANDROIDCAST_HAVE_LIBVPX */
|
#endif /* ANDROIDCAST_HAVE_LIBVPX */
|
||||||
|
|
||||||
JNIEXPORT jboolean JNICALL
|
JNIEXPORT jboolean JNICALL
|
||||||
@@ -132,8 +211,8 @@ Java_com_foxx_androidcast_media_codec_jni_NativeCodecBridge_nativeVpxEncoderCrea
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
JNIEXPORT jbyteArray JNICALL
|
JNIEXPORT jobject JNICALL
|
||||||
Java_com_foxx_androidcast_media_codec_jni_NativeCodecBridge_nativeVpxEncodeYuv(
|
Java_com_foxx_androidcast_media_codec_jni_NativeCodecBridge_nativeVpxEncodeYuvFrame(
|
||||||
JNIEnv *env, jclass clazz, jlong handle, jbyteArray yuv, jlong ptsUs, jboolean forceKeyframe) {
|
JNIEnv *env, jclass clazz, jlong handle, jbyteArray yuv, jlong ptsUs, jboolean forceKeyframe) {
|
||||||
(void) clazz;
|
(void) clazz;
|
||||||
#if !ANDROIDCAST_HAVE_LIBVPX
|
#if !ANDROIDCAST_HAVE_LIBVPX
|
||||||
@@ -167,7 +246,7 @@ Java_com_foxx_androidcast_media_codec_jni_NativeCodecBridge_nativeVpxEncodeYuv(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (vpx_codec_encode(&ctx->codec, &ctx->img, (vpx_codec_pts_t) ptsUs, 1, flags,
|
if (vpx_codec_encode(&ctx->codec, &ctx->img, (vpx_codec_pts_t) ptsUs, 1, flags,
|
||||||
VPX_DL_REALTIME) != VPX_CODEC_OK) {
|
VPX_DL_REALTIME) != VPX_CODEC_OK) {
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,18 +255,47 @@ Java_com_foxx_androidcast_media_codec_jni_NativeCodecBridge_nativeVpxEncodeYuv(
|
|||||||
while ((pkt = vpx_codec_get_cx_data(&ctx->codec, &iter)) != NULL) {
|
while ((pkt = vpx_codec_get_cx_data(&ctx->codec, &iter)) != NULL) {
|
||||||
if (pkt->kind == VPX_CODEC_CX_FRAME_PKT) {
|
if (pkt->kind == VPX_CODEC_CX_FRAME_PKT) {
|
||||||
jbyteArray out = (*env)->NewByteArray(env, (jsize) pkt->data.frame.sz);
|
jbyteArray out = (*env)->NewByteArray(env, (jsize) pkt->data.frame.sz);
|
||||||
if (out != NULL) {
|
if (out == NULL) {
|
||||||
(*env)->SetByteArrayRegion(env, out, 0, (jsize) pkt->data.frame.sz,
|
return NULL;
|
||||||
(const jbyte *) pkt->data.frame.buf);
|
|
||||||
}
|
}
|
||||||
|
(*env)->SetByteArrayRegion(env, out, 0, (jsize) pkt->data.frame.sz,
|
||||||
|
(const jbyte *) pkt->data.frame.buf);
|
||||||
|
jboolean key = (pkt->data.frame.flags & VPX_FRAME_IS_KEY) ? JNI_TRUE : JNI_FALSE;
|
||||||
|
jclass frame_cls = (*env)->FindClass(
|
||||||
|
env, "com/foxx/androidcast/media/codec/jni/VpxEncodedFrame");
|
||||||
|
jmethodID ctor = (*env)->GetMethodID(env, frame_cls, "<init>", "([BZ)V");
|
||||||
|
jobject frame = (*env)->NewObject(env, frame_cls, ctor, out, key);
|
||||||
ctx->frame_count++;
|
ctx->frame_count++;
|
||||||
return out;
|
return frame;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return NULL;
|
return NULL;
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
JNIEXPORT jbyteArray JNICALL
|
||||||
|
Java_com_foxx_androidcast_media_codec_jni_NativeCodecBridge_nativeVpxEncodeYuv(
|
||||||
|
JNIEnv *env, jclass clazz, jlong handle, jbyteArray yuv, jlong ptsUs, jboolean forceKeyframe) {
|
||||||
|
#if !ANDROIDCAST_HAVE_LIBVPX
|
||||||
|
(void) env;
|
||||||
|
(void) clazz;
|
||||||
|
(void) handle;
|
||||||
|
(void) yuv;
|
||||||
|
(void) ptsUs;
|
||||||
|
(void) forceKeyframe;
|
||||||
|
return NULL;
|
||||||
|
#else
|
||||||
|
jobject frame = Java_com_foxx_androidcast_media_codec_jni_NativeCodecBridge_nativeVpxEncodeYuvFrame(
|
||||||
|
env, clazz, handle, yuv, ptsUs, forceKeyframe);
|
||||||
|
if (frame == NULL) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
jclass frame_cls = (*env)->GetObjectClass(env, frame);
|
||||||
|
jfieldID data_f = (*env)->GetFieldID(env, frame_cls, "data", "[B");
|
||||||
|
return (jbyteArray) (*env)->GetObjectField(env, frame, data_f);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
JNIEXPORT void JNICALL
|
JNIEXPORT void JNICALL
|
||||||
Java_com_foxx_androidcast_media_codec_jni_NativeCodecBridge_nativeVpxEncoderRelease(
|
Java_com_foxx_androidcast_media_codec_jni_NativeCodecBridge_nativeVpxEncoderRelease(
|
||||||
JNIEnv *env, jclass clazz, jlong handle) {
|
JNIEnv *env, jclass clazz, jlong handle) {
|
||||||
@@ -217,3 +325,95 @@ Java_com_foxx_androidcast_media_codec_jni_NativeCodecBridge_nativeVpxRequestKeyf
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
JNIEXPORT jlong JNICALL
|
||||||
|
Java_com_foxx_androidcast_media_codec_jni_NativeCodecBridge_nativeVpxDecoderCreate(
|
||||||
|
JNIEnv *env, jclass clazz, jstring mime) {
|
||||||
|
(void) clazz;
|
||||||
|
#if !ANDROIDCAST_HAVE_LIBVPX
|
||||||
|
(void) env;
|
||||||
|
(void) mime;
|
||||||
|
return 0L;
|
||||||
|
#else
|
||||||
|
const char *mime_utf = (*env)->GetStringUTFChars(env, mime, NULL);
|
||||||
|
VpxDecoderCtx *ctx = (VpxDecoderCtx *) calloc(1, sizeof(VpxDecoderCtx));
|
||||||
|
if (ctx == NULL) {
|
||||||
|
(*env)->ReleaseStringUTFChars(env, mime, mime_utf);
|
||||||
|
return 0L;
|
||||||
|
}
|
||||||
|
ctx->magic = VPX_DEC_MAGIC;
|
||||||
|
vpx_codec_dec_cfg_t cfg = {0};
|
||||||
|
cfg.threads = 1;
|
||||||
|
if (vpx_codec_dec_init(&ctx->codec, pick_decoder_iface(mime_utf), &cfg, 0) != VPX_CODEC_OK) {
|
||||||
|
free(ctx);
|
||||||
|
(*env)->ReleaseStringUTFChars(env, mime, mime_utf);
|
||||||
|
return 0L;
|
||||||
|
}
|
||||||
|
(*env)->ReleaseStringUTFChars(env, mime, mime_utf);
|
||||||
|
return (jlong) (intptr_t) ctx;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT jboolean JNICALL
|
||||||
|
Java_com_foxx_androidcast_media_codec_jni_NativeCodecBridge_nativeVpxDecodeFrame(
|
||||||
|
JNIEnv *env, jclass clazz, jlong handle, jbyteArray data, jlong ptsUs, jobject surface) {
|
||||||
|
(void) clazz;
|
||||||
|
(void) ptsUs;
|
||||||
|
#if !ANDROIDCAST_HAVE_LIBVPX
|
||||||
|
(void) env;
|
||||||
|
(void) handle;
|
||||||
|
(void) data;
|
||||||
|
(void) surface;
|
||||||
|
return JNI_FALSE;
|
||||||
|
#else
|
||||||
|
VpxDecoderCtx *ctx = dec_from_handle(handle);
|
||||||
|
if (ctx == NULL || data == NULL || surface == NULL) {
|
||||||
|
return JNI_FALSE;
|
||||||
|
}
|
||||||
|
jsize len = (*env)->GetArrayLength(env, data);
|
||||||
|
if (len <= 0) {
|
||||||
|
return JNI_FALSE;
|
||||||
|
}
|
||||||
|
jbyte *bytes = (*env)->GetByteArrayElements(env, data, NULL);
|
||||||
|
if (bytes == NULL) {
|
||||||
|
return JNI_FALSE;
|
||||||
|
}
|
||||||
|
vpx_codec_err_t err = vpx_codec_decode(&ctx->codec, (const uint8_t *) bytes, (unsigned int) len,
|
||||||
|
NULL, 0);
|
||||||
|
(*env)->ReleaseByteArrayElements(env, data, bytes, JNI_ABORT);
|
||||||
|
if (err != VPX_CODEC_OK) {
|
||||||
|
return JNI_FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
ANativeWindow *window = ANativeWindow_fromSurface(env, surface);
|
||||||
|
if (window == NULL) {
|
||||||
|
return JNI_FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
vpx_codec_iter_t iter = NULL;
|
||||||
|
vpx_image_t *img;
|
||||||
|
jboolean rendered = JNI_FALSE;
|
||||||
|
while ((img = vpx_codec_get_frame(&ctx->codec, &iter)) != NULL) {
|
||||||
|
render_i420_to_window(window, img);
|
||||||
|
rendered = JNI_TRUE;
|
||||||
|
}
|
||||||
|
ANativeWindow_release(window);
|
||||||
|
return rendered;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT void JNICALL
|
||||||
|
Java_com_foxx_androidcast_media_codec_jni_NativeCodecBridge_nativeVpxDecoderRelease(
|
||||||
|
JNIEnv *env, jclass clazz, jlong handle) {
|
||||||
|
(void) env;
|
||||||
|
(void) clazz;
|
||||||
|
#if ANDROIDCAST_HAVE_LIBVPX
|
||||||
|
VpxDecoderCtx *ctx = dec_from_handle(handle);
|
||||||
|
if (ctx == NULL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ctx->magic = 0;
|
||||||
|
vpx_codec_destroy(&ctx->codec);
|
||||||
|
free(ctx);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|||||||
@@ -107,9 +107,9 @@ reconnect_known_devices() {
|
|||||||
adb devices -l
|
adb devices -l
|
||||||
}
|
}
|
||||||
|
|
||||||
# shellcheck source=android_find_latest_ndk.sh
|
# shellcheck source=scripts/android-ndk.sh
|
||||||
source "$ROOT/android_find_latest_ndk.sh"
|
source "$ROOT/scripts/android-ndk.sh"
|
||||||
export ANDROID_NDK_HOME="$(android_find_ndk_root "$ROOT")"
|
export ANDROID_NDK_HOME="$(androidcast_find_ndk_root "$ROOT")"
|
||||||
export ANDROID_NDK_ROOT="$ANDROID_NDK_HOME"
|
export ANDROID_NDK_ROOT="$ANDROID_NDK_HOME"
|
||||||
echo "==> Using NDK: $ANDROID_NDK_HOME"
|
echo "==> Using NDK: $ANDROID_NDK_HOME"
|
||||||
|
|
||||||
|
|||||||
91
scripts/android-ndk.sh
Executable file
91
scripts/android-ndk.sh
Executable file
@@ -0,0 +1,91 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Android NDK discovery for AndroidCast build scripts.
|
||||||
|
# Source this file: source "$(dirname "$0")/android-ndk.sh"
|
||||||
|
#
|
||||||
|
# Optional env:
|
||||||
|
# ANDROIDCAST_NDK_VERSION — pin a version under $HOME/Android/Sdk/ndk (e.g. 29.0.14206865)
|
||||||
|
# ANDROID_NDK_HOME / ANDROID_NDK_ROOT — used when valid
|
||||||
|
#
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
_androidcast_sdk_base() {
|
||||||
|
printf '%s\n' "${ANDROIDCAST_SDK_BASE:-${ANDROID_HOME:-${ANDROID_SDK_ROOT:-$HOME/Android/Sdk}}}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Print absolute NDK root (directory containing ndk-build). Errors on stderr.
|
||||||
|
androidcast_find_ndk_root() {
|
||||||
|
local project_root="${1:-}"
|
||||||
|
local ndk="" ndk_root ndk_ver parent pinned
|
||||||
|
|
||||||
|
if [[ -n "$project_root" && -f "$project_root/local.properties" ]]; then
|
||||||
|
ndk="$(sed -n 's/^[[:space:]]*ndk\.dir=//p' "$project_root/local.properties" | tr -d '\r' | head -1)"
|
||||||
|
ndk="${ndk%/}"
|
||||||
|
while [[ -n "$ndk" ]]; do
|
||||||
|
if [[ -x "$ndk/ndk-build" ]]; then
|
||||||
|
realpath "$ndk"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
parent="$(dirname "$ndk")"
|
||||||
|
[[ "$parent" == "$ndk" ]] && break
|
||||||
|
ndk="$parent"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
for ndk in "${ANDROID_NDK_HOME:-}" "${ANDROID_NDK_ROOT:-}"; do
|
||||||
|
ndk="${ndk%/}"
|
||||||
|
if [[ -n "$ndk" && -x "$ndk/ndk-build" ]]; then
|
||||||
|
realpath "$ndk"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
ndk_root="$(_androidcast_sdk_base)/ndk"
|
||||||
|
if [[ ! -d "$ndk_root" ]]; then
|
||||||
|
echo "ERROR: No Android NDK found. Install via SDK Manager ($ndk_root) or set ANDROID_NDK_HOME." >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
pinned="${ANDROIDCAST_NDK_VERSION:-}"
|
||||||
|
if [[ -n "$pinned" ]]; then
|
||||||
|
if [[ -d "$ndk_root/$pinned" ]]; then
|
||||||
|
ndk_ver="$pinned"
|
||||||
|
else
|
||||||
|
echo "ERROR: ANDROIDCAST_NDK_VERSION=$pinned not found under $ndk_root" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
ndk_ver="$(ls -1 "$ndk_root" 2>/dev/null | sort -V | tail -n 1 || true)"
|
||||||
|
if [[ -z "$ndk_ver" ]]; then
|
||||||
|
echo "ERROR: No NDK versions under $ndk_root" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
ndk="$ndk_root/$ndk_ver"
|
||||||
|
if [[ ! -x "$ndk/ndk-build" ]]; then
|
||||||
|
echo "ERROR: ndk-build missing under $ndk" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
realpath "$ndk"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Print llvm prebuilt bin directory for the current host.
|
||||||
|
androidcast_ndk_host_bin() {
|
||||||
|
local ndk_root="${1:?NDK root required}"
|
||||||
|
local os arch host_tag
|
||||||
|
case "$(uname -s)" in
|
||||||
|
Linux) os=linux ;;
|
||||||
|
Darwin) os=darwin ;;
|
||||||
|
*) os="$(uname -s | tr '[:upper:]' '[:lower:]')" ;;
|
||||||
|
esac
|
||||||
|
arch="$(uname -m)"
|
||||||
|
case "$arch" in
|
||||||
|
x86_64 | amd64) arch=x86_64 ;;
|
||||||
|
aarch64 | arm64) arch=aarch64 ;;
|
||||||
|
esac
|
||||||
|
host_tag="${os}-${arch}"
|
||||||
|
if [[ ! -d "$ndk_root/toolchains/llvm/prebuilt/${host_tag}/bin" ]]; then
|
||||||
|
host_tag="linux-x86_64"
|
||||||
|
fi
|
||||||
|
realpath "$ndk_root/toolchains/llvm/prebuilt/${host_tag}/bin"
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
#
|
#
|
||||||
# Prerequisites:
|
# Prerequisites:
|
||||||
# git submodule update --init third-party/libvpx
|
# git submodule update --init third-party/libvpx
|
||||||
# Android NDK (ndk.dir in local.properties or ANDROID_NDK_HOME)
|
# Android NDK (auto-detected via scripts/android-ndk.sh)
|
||||||
#
|
#
|
||||||
# Usage:
|
# Usage:
|
||||||
# ./scripts/build-native-codecs.sh arm64-v8a
|
# ./scripts/build-native-codecs.sh arm64-v8a
|
||||||
@@ -27,12 +27,12 @@ if [[ ! -d "$VPX_SRC_REAL" ]]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# shellcheck source=android_find_latest_ndk.sh
|
# shellcheck source=android-ndk.sh
|
||||||
source "$ROOT/android_find_latest_ndk.sh"
|
source "$ROOT/scripts/android-ndk.sh"
|
||||||
if ! NDK="$(android_find_ndk_root "$ROOT")"; then
|
if ! NDK="$(androidcast_find_ndk_root "$ROOT")"; then
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
NDK_HOST_BIN="$(android_ndk_host_prebuilt_bin "$NDK")"
|
NDK_HOST_BIN="$(androidcast_ndk_host_bin "$NDK")"
|
||||||
if [[ ! -d "$NDK_HOST_BIN" ]]; then
|
if [[ ! -d "$NDK_HOST_BIN" ]]; then
|
||||||
echo "ERROR: NDK host toolchain bin not found under $NDK"
|
echo "ERROR: NDK host toolchain bin not found under $NDK"
|
||||||
exit 1
|
exit 1
|
||||||
|
|||||||
Reference in New Issue
Block a user