Skip to content
Merged
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
40 changes: 20 additions & 20 deletions app/src/main/java/com/facebook/encapp/CustomEncoder.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@
import java.util.Arrays;
import java.util.Vector;


/**
* Created by jobl on 2018-02-27.
*/
Expand All @@ -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);
Expand Down Expand Up @@ -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<Parameter> params = new Vector(mTest.getConfigure().getParameterList());
// This one needs to be set as a native param.
try {
Expand Down Expand Up @@ -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);
Expand All @@ -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;
Expand All @@ -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++;
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand Down
4 changes: 4 additions & 0 deletions app/src/main/java/com/facebook/encapp/utils/FrameInfo.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, Object> mInfo;
Expand Down Expand Up @@ -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;
}
Expand Down
16 changes: 12 additions & 4 deletions app/src/main/java/com/facebook/encapp/utils/Statistics.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ public class Statistics {
private final ArrayList<Pair> mNamedTimestamps;

int mEncodingProcessingFrames = 0;
int mOutputFrameCount = 0; // Counter for tracking DTS/output order
Test mTest;
Date mStartDate;
SystemLoad mLoad = new SystemLoad();
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -461,16 +463,20 @@ public void writeJSON(Writer writer) throws IOException {
}
}
ArrayList<FrameInfo> allFrames = mEncodingFrames;
Comparator<FrameInfo> 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<FrameInfo> 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();

JSONObject obj = null;
ArrayList<FrameInfo> frameCopy = (ArrayList<FrameInfo>) 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());
Expand All @@ -492,11 +498,13 @@ public void writeJSON(Writer writer) throws IOException {
}
}
jsonArray.put(obj);
counter++;
}
json.put("frames", jsonArray);

if (mDecodingFrames.size() > 0) {

Comparator<FrameInfo> 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;
Expand Down
93 changes: 77 additions & 16 deletions native/x264_enc/README.md
Original file line number Diff line number Diff line change
@@ -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.