diff --git a/app/src/main/java/com/facebook/encapp/CustomEncoder.java b/app/src/main/java/com/facebook/encapp/CustomEncoder.java index 484bcd36..727717f6 100644 --- a/app/src/main/java/com/facebook/encapp/CustomEncoder.java +++ b/app/src/main/java/com/facebook/encapp/CustomEncoder.java @@ -35,7 +35,6 @@ import java.util.Arrays; import java.util.Vector; - /** * Created by jobl on 2018-02-27. */ @@ -57,7 +56,6 @@ class CustomEncoder extends Encoder { public static native void updateSettings(Parameter[] parameters); public static native void close(); - public CustomEncoder(Test test, String filesDir) { super(test); mStats = new Statistics("raw encoder", mTest); @@ -224,8 +222,7 @@ public String start() { Log.d(TAG, width + "x" + height + ", pixelformat: " + pixelformat + ", bitdepth:" + bitdepth + ", bitrate_mode:" + bitratemode + ", bitrate: " + bitrate + ", iframeinterval: "+ iframeinterval); // Create params for this - - // Caching vital values and see if they can be set runtime. + // Caching vital values Vector params = new Vector(mTest.getConfigure().getParameterList()); // This one needs to be set as a native param. try { @@ -274,7 +271,6 @@ public String start() { byte[] headerArray = new byte[estimatedSize]; int outputBufferSize; - Parameter[] param_buffer = new Parameter[params.size()]; params.toArray(param_buffer); int status = initEncoder(param_buffer, width, height, pixelformat, bitdepth); @@ -294,7 +290,7 @@ public String start() { return ""; } headerArray = getHeader(); - FrameInfo info; + FrameInfo encodeInfo = null; while (!input_done || !output_done) { try { long timeoutUs = VIDEO_CODEC_WAIT_TIME_US; @@ -315,20 +311,21 @@ public String start() { } long pts = computePresentationTimeUs(mPts, mFramesAdded, mRefFrameTime); - info = mStats.startEncodingFrame(pts, mFramesAdded); - // Let us read the setting in native and force key frame if set here. - // If (for some reason a key frame is not produced it will be updated in the native code - //info.isIFrame(true); - outputBufferSize = encode(yuvData, outputBuffer, info); - // Look at nal type as well, not just key frame? - // To ms? + mStats.startEncodingFrame(pts, mFramesAdded); + + // Create a separate FrameInfo for the encode call to avoid modifying the stored one + encodeInfo = new FrameInfo(pts); + outputBufferSize = encode(yuvData, outputBuffer, encodeInfo); + if (outputBufferSize < 0) { return "Encoder not started or error occurred"; } // outputBufferSize == 0 means frame was buffered (B-frame reordering) - // This is normal when B-frames are enabled, we'll get output later + // When output is produced, use the OUTPUT PTS to find the matching input frame if (outputBufferSize > 0) { - mStats.stopEncodingFrame(info.getPts() , info.getSize(), info.isIFrame()); + // encodeInfo.getPts() now contains the OUTPUT pts (set by native code) + // This matches the original input frame that produced this output + mStats.stopEncodingFrame(encodeInfo.getPts(), encodeInfo.getSize(), encodeInfo.isIFrame()); } currentFramePosition += frameSize; mFramesAdded++; @@ -370,7 +367,7 @@ public String start() { ByteBuffer buffer = ByteBuffer.wrap(outputBuffer); bufferInfo.offset = 0; bufferInfo.size = outputBufferSize; - bufferInfo.presentationTimeUs = info.getPts(); + bufferInfo.presentationTimeUs = encodeInfo.getPts(); boolean isKeyFrame = checkIfKeyFrame(outputBuffer); if (isKeyFrame) bufferInfo.flags = MediaCodec.BUFFER_FLAG_KEY_FRAME; @@ -393,19 +390,22 @@ public String start() { int delayedFrames = getDelayedFrames(); Log.d(TAG, "Flushing " + delayedFrames + " delayed frames from encoder"); while (delayedFrames > 0) { - info = new FrameInfo(0); - outputBufferSize = flushEncoder(outputBuffer, info); + FrameInfo flushInfo = new FrameInfo(0); + outputBufferSize = flushEncoder(outputBuffer, flushInfo); if (outputBufferSize <= 0) { break; // No more frames or error } - Log.d(TAG, "Flushed frame: pts=" + info.getPts() + ", size=" + outputBufferSize); + Log.d(TAG, "Flushed frame: pts=" + flushInfo.getPts() + ", size=" + outputBufferSize); + + // Stop the encoding frame using the output PTS to find the matching input frame + mStats.stopEncodingFrame(flushInfo.getPts(), flushInfo.getSize(), flushInfo.isIFrame()); // Write flushed frame to muxer if (mMuxerWrapper != null && muxerStarted) { ByteBuffer buffer = ByteBuffer.wrap(outputBuffer); bufferInfo.offset = 0; bufferInfo.size = outputBufferSize; - bufferInfo.presentationTimeUs = info.getPts(); + bufferInfo.presentationTimeUs = flushInfo.getPts(); boolean isKeyFrame = checkIfKeyFrame(outputBuffer); bufferInfo.flags = isKeyFrame ? MediaCodec.BUFFER_FLAG_KEY_FRAME : 0; diff --git a/app/src/main/java/com/facebook/encapp/utils/FrameInfo.java b/app/src/main/java/com/facebook/encapp/utils/FrameInfo.java index e40fbb16..68f47d81 100644 --- a/app/src/main/java/com/facebook/encapp/utils/FrameInfo.java +++ b/app/src/main/java/com/facebook/encapp/utils/FrameInfo.java @@ -15,6 +15,7 @@ public class FrameInfo { boolean mIsIframe; int mFlags; int mOriginalFrame; + int mOutputOrder = -1; // DTS order - the order this frame came out of the encoder int mUUID = -1; static Integer mIdCounter = 0; Dictionary mInfo; @@ -86,6 +87,9 @@ public long getProcessingTime() { public long getStartTime() { return mStartTime;} public long getStopTime() { return mStopTime;} + public void setOutputOrder(int order) { mOutputOrder = order; } + public int getOutputOrder() { return mOutputOrder; } + public Dictionary getInfo() { return mInfo; } diff --git a/app/src/main/java/com/facebook/encapp/utils/Statistics.java b/app/src/main/java/com/facebook/encapp/utils/Statistics.java index 74834625..9196411b 100644 --- a/app/src/main/java/com/facebook/encapp/utils/Statistics.java +++ b/app/src/main/java/com/facebook/encapp/utils/Statistics.java @@ -36,6 +36,7 @@ public class Statistics { private final ArrayList mNamedTimestamps; int mEncodingProcessingFrames = 0; + int mOutputFrameCount = 0; // Counter for tracking DTS/output order Test mTest; Date mStartDate; SystemLoad mLoad = new SystemLoad(); @@ -207,6 +208,7 @@ public FrameInfo stopEncodingFrame(long pts, long size, boolean isIFrame) { frame.stop(); frame.setSize(size); frame.isIFrame(isIFrame); + frame.setOutputOrder(mOutputFrameCount++); // Track DTS/output order } else { Log.e(TAG, "No matching pts! Error in time handling. Pts = " + pts); } @@ -461,8 +463,10 @@ public void writeJSON(Writer writer) throws IOException { } } ArrayList allFrames = mEncodingFrames; - Comparator compareByPts = (FrameInfo o1, FrameInfo o2) -> Long.valueOf(o1.getPts()).compareTo(Long.valueOf(o2.getPts())); - Collections.sort(allFrames, compareByPts); + // Sort by output order (DTS/decode order) to preserve the order frames came out of encoder + Comparator compareByOutputOrder = (FrameInfo o1, FrameInfo o2) -> + Integer.valueOf(o1.getOutputOrder()).compareTo(Integer.valueOf(o2.getOutputOrder())); + Collections.sort(allFrames, compareByOutputOrder); int counter = 0; JSONArray jsonArray = new JSONArray(); @@ -470,7 +474,9 @@ public void writeJSON(Writer writer) throws IOException { ArrayList frameCopy = (ArrayList) allFrames.clone(); for (FrameInfo info : frameCopy) { obj = new JSONObject(); - obj.put("frame", counter++); + // frame = DTS/decode order (order frames came out of encoder) + obj.put("frame", info.getOutputOrder()); + // original_frame = PTS/presentation order (order frames were input) obj.put("original_frame", info.getOriginalFrame()); obj.put("iframe", (info.isIFrame()) ? 1 : 0); obj.put("size", info.getSize()); @@ -492,11 +498,13 @@ public void writeJSON(Writer writer) throws IOException { } } jsonArray.put(obj); + counter++; } json.put("frames", jsonArray); if (mDecodingFrames.size() > 0) { - + Comparator compareByPts = (FrameInfo o1, FrameInfo o2) -> + Long.valueOf(o1.getPts()).compareTo(Long.valueOf(o2.getPts())); allFrames = new ArrayList<>(mDecodingFrames.values()); Collections.sort(allFrames, compareByPts); counter = 1; diff --git a/native/x264_enc/README.md b/native/x264_enc/README.md index 03c734e3..55793086 100644 --- a/native/x264_enc/README.md +++ b/native/x264_enc/README.md @@ -1,25 +1,86 @@ -# x264 build -Set ndk path and build tools first, e.g. -``` -# for MacOs +# x264 Native Encoder Build + +## Prerequisites +- Android NDK installed (e.g., `~/Library/Android/sdk/ndk/29.0.14206865`) +- x264 library already built for Android at `modules/x264/android/arm64-v8a/` + +## Quick Build (Recommended) + +Set NDK path and host tag, then build: + +```bash +cd native/x264_enc + +# For macOS export HOST_TAG=darwin-x86_64 -export NDK="/System/Volumes/Data/Users/XXX/Library/Android/sdk/ndk-bundle/" +export NDK=~/Library/Android/sdk/ndk/29.0.14206865 +export PATH=$NDK:$PATH + +# Build the library +make all +``` + +This will: +1. Compile `x264_enc.cpp` against the x264 static library +2. Create `libnativeencoder.so` in `libs/arm64-v8a/` +3. Copy the library to `/tmp/libnativeencoder.so` + +## Deploy to Device + +```bash +adb push /tmp/libnativeencoder.so /sdcard/ ``` -Run build.sh +## Full Build (including x264 library) + +If you need to rebuild the x264 library from source: + +```bash +# Set environment +export HOST_TAG=darwin-x86_64 # For macOS (use linux-x86_64 for Linux) +export NDK=~/Library/Android/sdk/ndk/29.0.14206865 + +# Run the full build script (builds x264 + native encoder) +./build.sh +``` # Testing -The build library will be copied to /tmp/ -Shared libraries in the codec name field will have similar behavior -as video files in the input section i.e. copied to device workdir -" - configure { - codec: "/tmp/libnativeencoder.so" - bitrate: "500 kbps" -" +The library will be copied to `/tmp/`. Shared libraries in the codec name field +will behave similar to video files in the input section, i.e., they are copied +to the device workdir automatically. + +Example test configuration: +``` +configure { + codec: "/tmp/libnativeencoder.so" + bitrate: "500 kbps" +} +``` + +To verify, run: +```bash +python3 scripts/encapp.py run tests/bitrate_buffer_x264.pbtxt +``` + +## Available x264 Parameters -To verify run: +Common parameters you can set in the test configuration: + +- `preset`: ultrafast, superfast, veryfast, faster, fast, medium, slow, slower, veryslow, placebo +- `tune`: film, animation, grain, stillimage, psnr, ssim, fastdecode, zerolatency +- `i_threads`: number of encoding threads (1 for single-threaded) +- `i_bframe`: number of B-frames between I and P frames (0-16) +- `bitrate`: target bitrate (will be converted from bps to kbps internally) +- `bitrate_mode`: cq/cqp (constant QP), cbr/crf (constant rate factor), vbr/abr (average bitrate) + +Example with B-frames: ``` ->python3 encapp.py run tests/bitrate_buffer_x264.pbtxt +parameter { + key: "i_bframe" + type: intType + value: "3" +} ``` + +**Note:** Using `tune: "zerolatency"` disables B-frames.