Skip to content

Commit 9111efe

Browse files
committed
encapp: x264 bugfix, multithread.
New test added: bitrate_buffer_x264_multithread.pbtxt TODO: fix stats for b-frames
1 parent 49e0445 commit 9111efe

4 files changed

Lines changed: 225 additions & 63 deletions

File tree

app/src/main/java/com/facebook/encapp/CustomEncoder.java

Lines changed: 51 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@ class CustomEncoder extends Encoder {
4747
public static native byte[] getHeader();
4848
// TODO: can the size, color and bitdepth change runtime?
4949
public static native int encode(byte[] input, byte[] output, FrameInfo info);
50+
// Flush buffered frames. Call until returns 0.
51+
public static native int flushEncoder(byte[] output, FrameInfo info);
52+
// Get number of frames currently buffered in encoder
53+
public static native int getDelayedFrames();
5054

5155
public static native StringParameter[] getAllEncoderSettings();
5256

@@ -90,7 +94,7 @@ public CustomEncoder(Test test, String filesDir) {
9094
Log.e(TAG, "Failed to load library, " + name + ", " + targetPath + ": " + e.getMessage());
9195
}
9296
}
93-
97+
9498

9599
public static byte[] readYUVFromFile(String filePath, int size, int framePosition) throws IOException {
96100
byte[] inputBuffer = new byte[size];
@@ -172,7 +176,7 @@ public void setRuntimeParameters(int frame) {
172176
}
173177

174178
for (Long sync : mRuntimeParams.getRequestSyncList()) {
175-
if (sync == frame) {
179+
if (sync == frame) {
176180
addEncoderParameters(params, DataValueType.longType.name(), "request-sync", "");
177181
break;
178182
}
@@ -318,11 +322,13 @@ public String start() {
318322
outputBufferSize = encode(yuvData, outputBuffer, info);
319323
// Look at nal type as well, not just key frame?
320324
// To ms?
321-
mStats.stopEncodingFrame(info.getPts() , info.getSize(), info.isIFrame());
322-
if (outputBufferSize == 0) {
323-
return "Failed to encode frame";
324-
} else if (outputBufferSize == -1) {
325-
return "Encoder not started";
325+
if (outputBufferSize < 0) {
326+
return "Encoder not started or error occurred";
327+
}
328+
// outputBufferSize == 0 means frame was buffered (B-frame reordering)
329+
// This is normal when B-frames are enabled, we'll get output later
330+
if (outputBufferSize > 0) {
331+
mStats.stopEncodingFrame(info.getPts() , info.getSize(), info.isIFrame());
326332
}
327333
currentFramePosition += frameSize;
328334
mFramesAdded++;
@@ -359,16 +365,17 @@ public String start() {
359365
muxerStarted = true;
360366
}
361367

362-
ByteBuffer buffer = ByteBuffer.wrap(outputBuffer);
363-
bufferInfo.offset = 0;
364-
bufferInfo.size = outputBufferSize;
365-
bufferInfo.presentationTimeUs = info.getPts();
368+
// Only write to muxer if we have actual output
369+
if (outputBufferSize > 0 && mMuxerWrapper != null) {
370+
ByteBuffer buffer = ByteBuffer.wrap(outputBuffer);
371+
bufferInfo.offset = 0;
372+
bufferInfo.size = outputBufferSize;
373+
bufferInfo.presentationTimeUs = info.getPts();
366374

367-
//TODO: we get this from FrameInfo instead
368-
boolean isKeyFrame = checkIfKeyFrame(outputBuffer);
369-
if (isKeyFrame) bufferInfo.flags = MediaCodec.BUFFER_FLAG_KEY_FRAME;
375+
boolean isKeyFrame = checkIfKeyFrame(outputBuffer);
376+
if (isKeyFrame) bufferInfo.flags = MediaCodec.BUFFER_FLAG_KEY_FRAME;
377+
else bufferInfo.flags = 0;
370378

371-
if(mMuxerWrapper != null) {
372379
buffer.position(bufferInfo.offset);
373380
buffer.limit(bufferInfo.offset + bufferInfo.size);
374381

@@ -382,6 +389,35 @@ public String start() {
382389
}
383390
mStats.stop();
384391

392+
// Flush any remaining buffered frames (important for B-frames/multi-threading)
393+
int delayedFrames = getDelayedFrames();
394+
Log.d(TAG, "Flushing " + delayedFrames + " delayed frames from encoder");
395+
while (delayedFrames > 0) {
396+
info = new FrameInfo(0);
397+
outputBufferSize = flushEncoder(outputBuffer, info);
398+
if (outputBufferSize <= 0) {
399+
break; // No more frames or error
400+
}
401+
Log.d(TAG, "Flushed frame: pts=" + info.getPts() + ", size=" + outputBufferSize);
402+
403+
// Write flushed frame to muxer
404+
if (mMuxerWrapper != null && muxerStarted) {
405+
ByteBuffer buffer = ByteBuffer.wrap(outputBuffer);
406+
bufferInfo.offset = 0;
407+
bufferInfo.size = outputBufferSize;
408+
bufferInfo.presentationTimeUs = info.getPts();
409+
410+
boolean isKeyFrame = checkIfKeyFrame(outputBuffer);
411+
bufferInfo.flags = isKeyFrame ? MediaCodec.BUFFER_FLAG_KEY_FRAME : 0;
412+
413+
buffer.position(bufferInfo.offset);
414+
buffer.limit(bufferInfo.offset + bufferInfo.size);
415+
mMuxerWrapper.writeSampleData(videoTrackIndex, buffer, bufferInfo);
416+
}
417+
delayedFrames = getDelayedFrames();
418+
}
419+
Log.d(TAG, "Encoder flush complete");
420+
385421
Log.d(TAG, "Close encoder and streams");
386422
close();
387423

native/x264_enc/jni/src/x264_enc.cpp

Lines changed: 139 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,6 @@ using namespace std;
3232
extern "C" {
3333

3434
x264_t *encoder = NULL;
35-
x264_nal_t *nal;
36-
int nnal;
3735
int _width = -1;
3836
int _height = -1;
3937
int _colorformat = -1;
@@ -190,14 +188,16 @@ jint init_encoder(JNIEnv *env, jobject thiz, jobjectArray params, jint width,
190188

191189
x264Params.i_width = _width;
192190
x264Params.i_height = _height;
193-
// TODO: remove this, currenlty the encoder is broken without it
194-
x264Params.i_threads = 1;
195191
x264Params.i_csp = _colorformat;
196192
x264Params.i_bitdepth = _bitdepth;
197193
x264Params.i_fps_num = 30;
198194
x264Params.i_fps_den = 1;
199195
x264Params.i_timebase_num = 1;
200-
x264Params.i_timebase_den = 1000000; // Nanosecs
196+
x264Params.i_timebase_den = 1000000; // Microsecs
197+
// Output NALs in Annex B format (start codes) - MediaMuxer handles conversion
198+
x264Params.b_annexb = 1;
199+
// Disable repeat headers - we handle SPS/PPS separately
200+
x264Params.b_repeat_headers = 0;
201201
LOGD("Open x264 encoder");
202202
encoder = x264_encoder_open(&x264Params);
203203
if (!encoder) {
@@ -219,6 +219,8 @@ jbyteArray get_header(JNIEnv *env, jobject thiz, jbyteArray headerArray) {
219219
return NULL;
220220
}
221221

222+
x264_nal_t *nal;
223+
int nnal;
222224
int size_of_headers = x264_encoder_headers(encoder, &nal, &nnal);
223225
jbyte *buf = new jbyte[size_of_headers];
224226
memset(buf, 0, size_of_headers);
@@ -246,6 +248,45 @@ jbyteArray get_header(JNIEnv *env, jobject thiz, jbyteArray headerArray) {
246248
return ret;
247249
}
248250

251+
static int copy_nal_to_output(x264_nal_t *nal, int nnal, jbyte *output_data,
252+
int output_size) {
253+
// Start with 2-byte offset - required for MediaMuxer compatibility
254+
int offset = 2;
255+
output_data[0] = 0;
256+
output_data[1] = 0;
257+
258+
for (int i = 0; i < nnal; i++) {
259+
// Skip header NALs - they're handled separately via get_header()
260+
if (nal[i].i_type == NAL_SPS || nal[i].i_type == NAL_PPS ||
261+
nal[i].i_type == NAL_SEI || nal[i].i_type == NAL_AUD ||
262+
nal[i].i_type == NAL_FILLER) {
263+
continue;
264+
}
265+
if (offset + nal[i].i_payload <= output_size) {
266+
memcpy(output_data + offset, nal[i].p_payload, nal[i].i_payload);
267+
offset += nal[i].i_payload;
268+
} else {
269+
LOGE("Output buffer too small for NAL unit");
270+
}
271+
}
272+
return offset;
273+
}
274+
275+
static void update_frame_info(JNIEnv *env, jobject frameInfo,
276+
x264_picture_t *pic_out, int frame_size) {
277+
jclass infoClass = env->FindClass("com/facebook/encapp/utils/FrameInfo");
278+
jfieldID isIframeId = env->GetFieldID(infoClass, "mIsIframe", "Z");
279+
jfieldID ptsId = env->GetFieldID(infoClass, "mPts", "J");
280+
jfieldID dtsId = env->GetFieldID(infoClass, "mDts", "J");
281+
jfieldID sizeId = env->GetFieldID(infoClass, "mSize", "J");
282+
283+
env->SetLongField(frameInfo, sizeId, frame_size);
284+
env->SetLongField(frameInfo, ptsId, pic_out->i_pts);
285+
env->SetLongField(frameInfo, dtsId, pic_out->i_dts);
286+
env->SetBooleanField(frameInfo, isIframeId, pic_out->b_keyframe);
287+
}
288+
289+
// Returns frame size, 0 if buffered (B-frames), -1 on error
249290
jint encode(JNIEnv *env, jobject thiz, jbyteArray input, jbyteArray output,
250291
jobject frameInfo) {
251292
LOGD("Encoding frame");
@@ -255,32 +296,33 @@ jint encode(JNIEnv *env, jobject thiz, jbyteArray input, jbyteArray output,
255296
}
256297

257298
jclass infoClass = env->FindClass("com/facebook/encapp/utils/FrameInfo");
258-
jfieldID isIframeId = env->GetFieldID(infoClass, "mIsIframe", "Z");
259299
jfieldID ptsId = env->GetFieldID(infoClass, "mPts", "J");
260-
jfieldID dtsId = env->GetFieldID(infoClass, "mDts", "J");
261-
jfieldID sizeId = env->GetFieldID(infoClass, "mSize", "J");
262300

263-
// All interaction must be done before locking java...
301+
x264_nal_t *nal;
302+
int nnal;
303+
264304
x264_picture_t pic_in = {0};
265305
x264_picture_t pic_out = {0};
266306

267-
// TODO: We are assuming yuv420p, add check...
268-
int ySize = _width * _height; // Stride?
307+
int ySize = _width * _height;
269308
int uvSize = (int)(ySize / 4.0f);
309+
int inputSize = ySize + uvSize * 2;
270310

271311
x264_picture_init(&pic_in);
272312
pic_in.img.i_csp = _colorformat;
273-
// TODO: hard code, really?
274313
pic_in.img.i_plane = 3;
275314

276315
long pts = env->GetLongField(frameInfo, ptsId);
277316
LOGD("Set pts: %ld", pts);
278-
pic_in.i_pts = pts; // Convert to milliseconds
317+
pic_in.i_pts = pts;
279318

280-
// Now we are locking java
281-
jbyte *input_data = (jbyte *)env->GetPrimitiveArrayCritical(input, 0);
282-
jbyte *output_data = (jbyte *)env->GetPrimitiveArrayCritical(output, 0);
319+
jsize input_array_size = env->GetArrayLength(input);
320+
jsize output_array_size = env->GetArrayLength(output);
283321

322+
// Use local buffers instead of GetPrimitiveArrayCritical to allow GC
323+
jbyte *input_data = new jbyte[inputSize];
324+
jbyte *output_data = new jbyte[output_array_size];
325+
env->GetByteArrayRegion(input, 0, inputSize, input_data);
284326
pic_in.img.plane[0] = (uint8_t *)input_data;
285327
pic_in.img.plane[1] = (uint8_t *)(input_data + ySize);
286328
pic_in.img.plane[2] = (uint8_t *)(input_data + ySize + uvSize);
@@ -290,41 +332,68 @@ jint encode(JNIEnv *env, jobject thiz, jbyteArray input, jbyteArray output,
290332
pic_in.img.i_stride[2] = _width / 2;
291333

292334
int frame_size = x264_encoder_encode(encoder, &nal, &nnal, &pic_in, &pic_out);
293-
if (frame_size >= 0) {
294-
// TODO: Added total_size = 2 for debugging purpose
295-
int total_size = 2;
296-
for (int i = 0; i < nnal; i++) {
297-
total_size += nal[i].i_payload;
298-
}
299-
int offset = 2;
300-
for (int i = 0; i < nnal; i++) {
301-
if (nal[i].i_type == NAL_SPS || nal[i].i_type == NAL_PPS ||
302-
nal[i].i_type == NAL_SEI || nal[i].i_type == NAL_AUD ||
303-
nal[i].i_type == NAL_FILLER) {
304-
continue;
305-
}
306-
memcpy(output_data + offset, nal[i].p_payload, nal[i].i_payload);
307-
offset += nal[i].i_payload;
308-
}
309-
frame_size = total_size;
335+
336+
int total_size = 0;
337+
if (frame_size > 0) {
338+
total_size = copy_nal_to_output(nal, nnal, output_data, output_array_size);
339+
env->SetByteArrayRegion(output, 0, total_size, output_data);
340+
update_frame_info(env, frameInfo, &pic_out, total_size);
341+
} else if (frame_size == 0) {
342+
// Frame buffered (B-frame reordering)
343+
LOGD("Frame buffered, no output yet (encoder delay)");
344+
update_frame_info(env, frameInfo, &pic_out, 0);
345+
} else {
346+
LOGE("x264_encoder_encode failed with error: %d", frame_size);
310347
}
348+
delete[] input_data;
349+
delete[] output_data;
311350

312-
env->ReleasePrimitiveArrayCritical(input, input_data, 0);
313-
env->ReleasePrimitiveArrayCritical(output, output_data, 0);
351+
return total_size;
352+
}
314353

315-
// Set data from the encoding process
316-
env->SetLongField(frameInfo, sizeId, frame_size);
317-
env->SetLongField(frameInfo, ptsId, pic_out.i_pts);
318-
env->SetLongField(frameInfo, dtsId, pic_out.i_dts);
319-
env->SetBooleanField(frameInfo, isIframeId, pic_out.b_keyframe);
320-
// Do we need additional info?
321-
// LOGD("Not saved: Pic type: %d", pic_out.i_type);
322-
// TODO: we also have a complete list of params. Maybe with a debug flag we
323-
// could push this to java?
324-
325-
// x264_image_properties_t holds psnr and ssim as well (potentially, if
326-
// enabled)
327-
return frame_size;
354+
// Flush buffered frames. Call until returns 0.
355+
jint flush_encoder(JNIEnv *env, jobject thiz, jbyteArray output,
356+
jobject frameInfo) {
357+
LOGD("Flushing encoder");
358+
if (!encoder) {
359+
LOGI("Encoder is not initialized for flushing");
360+
return -1;
361+
}
362+
363+
x264_nal_t *nal;
364+
int nnal;
365+
x264_picture_t pic_out = {0};
366+
367+
jsize output_array_size = env->GetArrayLength(output);
368+
jbyte *output_data = new jbyte[output_array_size];
369+
370+
// NULL input flushes buffered frames
371+
int frame_size =
372+
x264_encoder_encode(encoder, &nal, &nnal, NULL, &pic_out);
373+
374+
int total_size = 0;
375+
if (frame_size > 0) {
376+
total_size = copy_nal_to_output(nal, nnal, output_data, output_array_size);
377+
env->SetByteArrayRegion(output, 0, total_size, output_data);
378+
update_frame_info(env, frameInfo, &pic_out, total_size);
379+
LOGD("Flushed frame: pts=%ld, dts=%ld, size=%d", (long)pic_out.i_pts,
380+
(long)pic_out.i_dts, total_size);
381+
} else if (frame_size == 0) {
382+
LOGD("Encoder flush complete, no more buffered frames");
383+
update_frame_info(env, frameInfo, &pic_out, 0);
384+
} else {
385+
LOGE("x264_encoder_encode (flush) failed with error: %d", frame_size);
386+
}
387+
388+
delete[] output_data;
389+
return total_size;
390+
}
391+
392+
jint get_delayed_frames(JNIEnv *env, jobject thiz) {
393+
if (!encoder) {
394+
return 0;
395+
}
396+
return x264_encoder_delayed_frames(encoder);
328397
}
329398

330399
void update_settings(JNIEnv *env, jobject thiz, jobjectArray params) {
@@ -470,6 +539,25 @@ jobjectArray get_all_settings(JNIEnv *env, jobject thiz) {
470539
parameterClass, paramConstructor, env->NewStringUTF("i_timebase_den"),
471540
env->NewStringUTF("intType"), env->NewStringUTF(buffer)));
472541

542+
snprintf(buffer, len, "%d", info.i_threads);
543+
params.push_back(env->NewObject(
544+
parameterClass, paramConstructor, env->NewStringUTF("i_threads"),
545+
env->NewStringUTF("intType"), env->NewStringUTF(buffer)));
546+
snprintf(buffer, len, "%d", info.i_lookahead_threads);
547+
params.push_back(env->NewObject(
548+
parameterClass, paramConstructor,
549+
env->NewStringUTF("i_lookahead_threads"), env->NewStringUTF("intType"),
550+
env->NewStringUTF(buffer)));
551+
snprintf(buffer, len, "%d", info.b_sliced_threads);
552+
params.push_back(env->NewObject(
553+
parameterClass, paramConstructor, env->NewStringUTF("b_sliced_threads"),
554+
env->NewStringUTF("intType"), env->NewStringUTF(buffer)));
555+
556+
snprintf(buffer, len, "%d", info.i_bframe);
557+
params.push_back(env->NewObject(
558+
parameterClass, paramConstructor, env->NewStringUTF("i_bframe"),
559+
env->NewStringUTF("intType"), env->NewStringUTF(buffer)));
560+
473561
jobjectArray ret = env->NewObjectArray(params.size(), parameterClass, NULL);
474562
int index = 0;
475563
for (auto element : params) {
@@ -492,6 +580,9 @@ static JNINativeMethod methods[] = {
492580
(void *)&init_encoder},
493581
{"getHeader", "()[B", (void *)&get_header},
494582
{"encode", "([B[BLcom/facebook/encapp/utils/FrameInfo;)I", (void *)&encode},
583+
{"flushEncoder", "([BLcom/facebook/encapp/utils/FrameInfo;)I",
584+
(void *)&flush_encoder},
585+
{"getDelayedFrames", "()I", (void *)&get_delayed_frames},
495586
{"close", "()V", (void *)&close},
496587
{"getAllEncoderSettings", "()[Lcom/facebook/encapp/utils/StringParameter;",
497588
(void *)&get_all_settings},

tests/bitrate_buffer_x264.pbtxt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ test {
1111
bitrate: "500 kbps"
1212
bitrate_mode: cbr
1313
i_frame_interval: 10
14+
parameter {
15+
key: "i_threads"
16+
type: intType
17+
value: "1"
18+
}
1419
parameter {
1520
key: "tune"
1621
type: stringType

0 commit comments

Comments
 (0)