Capture full YUV planes and add YUV420->ARGB converter
- VideoDecoder.Frame and PlaybackEngine.VideoFrame now carry U and V chroma planes plus their strides, in addition to luma. - YuvConverter converts YUVJ420P frames to packed 0xAARRGGBB pixels for the renderer, with unit tests.
This commit is contained in:
@@ -95,16 +95,33 @@ public final class VideoDecoder implements AutoCloseable {
|
||||
|
||||
int width = frame.width();
|
||||
int height = frame.height();
|
||||
BytePointer y = frame.data(0);
|
||||
int stride = frame.linesize(0);
|
||||
byte[] yPlane = new byte[height * stride];
|
||||
y.position(0).get(yPlane);
|
||||
int chromaWidth = (width + 1) / 2;
|
||||
int chromaHeight = (height + 1) / 2;
|
||||
|
||||
return new Frame(width, height, frame.format(), yPlane, stride);
|
||||
BytePointer y = frame.data(0);
|
||||
BytePointer u = frame.data(1);
|
||||
BytePointer v = frame.data(2);
|
||||
|
||||
int yStride = frame.linesize(0);
|
||||
int uStride = frame.linesize(1);
|
||||
int vStride = frame.linesize(2);
|
||||
|
||||
byte[] yPlane = new byte[height * yStride];
|
||||
byte[] uPlane = new byte[chromaHeight * uStride];
|
||||
byte[] vPlane = new byte[chromaHeight * vStride];
|
||||
|
||||
y.position(0).get(yPlane);
|
||||
u.position(0).get(uPlane);
|
||||
v.position(0).get(vPlane);
|
||||
|
||||
return new Frame(width, height, frame.format(), yPlane, yStride, uPlane, uStride, vPlane, vStride);
|
||||
}
|
||||
|
||||
/** A decoded video frame. */
|
||||
public record Frame(int width, int height, int pixelFormat, byte[] yPlane, int yStride) {
|
||||
/** A decoded video frame (planar YUV 4:2:0). */
|
||||
public record Frame(int width, int height, int pixelFormat,
|
||||
byte[] yPlane, int yStride,
|
||||
byte[] uPlane, int uStride,
|
||||
byte[] vPlane, int vStride) {
|
||||
}
|
||||
|
||||
/** Returns the next decoded frame, or {@code null} if none is buffered. */
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
package dev.quoije.umd.media;
|
||||
|
||||
/**
|
||||
* Converts decoded video frames (planar YUV 4:2:0, full range as used by
|
||||
* JPEG/YUVJ420P) into packed 0xAARRGGBB pixels.
|
||||
*/
|
||||
public final class YuvConverter {
|
||||
|
||||
private YuvConverter() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a YUVJ420P frame to ARGB. The returned array has
|
||||
* {@code width * height} packed pixels.
|
||||
*/
|
||||
public static int[] yuvj420ToArgb(int width, int height,
|
||||
byte[] yPlane, int yStride,
|
||||
byte[] uPlane, int uStride,
|
||||
byte[] vPlane, int vStride) {
|
||||
int[] argb = new int[width * height];
|
||||
|
||||
for (int row = 0; row < height; row++) {
|
||||
int yRow = row * yStride;
|
||||
int uvRow = (row >> 1) * uStride;
|
||||
int outRow = row * width;
|
||||
|
||||
for (int col = 0; col < width; col++) {
|
||||
int y = yPlane[yRow + col] & 0xFF;
|
||||
int u = uPlane[uvRow + (col >> 1)] & 0xFF;
|
||||
int v = vPlane[uvRow + (col >> 1)] & 0xFF;
|
||||
|
||||
int du = u - 128;
|
||||
int dv = v - 128;
|
||||
|
||||
int r = y + ((359 * dv) >> 8);
|
||||
int g = y - ((88 * du) >> 8) - ((183 * dv) >> 8);
|
||||
int b = y + ((454 * du) >> 8);
|
||||
|
||||
argb[outRow + col] = 0xFF000000
|
||||
| (clamp(r) << 16)
|
||||
| (clamp(g) << 8)
|
||||
| clamp(b);
|
||||
}
|
||||
}
|
||||
|
||||
return argb;
|
||||
}
|
||||
|
||||
private static int clamp(int value) {
|
||||
if (value < 0) {
|
||||
return 0;
|
||||
}
|
||||
return Math.min(value, 255);
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,10 @@ public final class PlaybackEngine implements AutoCloseable {
|
||||
public static final long AUDIO_FRAME_TICKS = (long) AUDIO_FRAME_SAMPLES * MPEG_TIMESTAMP / AUDIO_SAMPLE_RATE;
|
||||
|
||||
/** A decoded video frame tagged with its presentation timestamp. */
|
||||
public record VideoFrame(long pts, int width, int height, int pixelFormat, byte[] yPlane, int yStride) {
|
||||
public record VideoFrame(long pts, int width, int height, int pixelFormat,
|
||||
byte[] yPlane, int yStride,
|
||||
byte[] uPlane, int uStride,
|
||||
byte[] vPlane, int vStride) {
|
||||
}
|
||||
|
||||
/** A chunk of interleaved S16 PCM tagged with its presentation timestamp. */
|
||||
@@ -136,7 +139,8 @@ public final class PlaybackEngine implements AutoCloseable {
|
||||
while ((frame = videoDecoder.receive()) != null) {
|
||||
videoQueue.add(new VideoFrame(
|
||||
assignVideoPts(), frame.width(), frame.height(),
|
||||
frame.pixelFormat(), frame.yPlane(), frame.yStride()));
|
||||
frame.pixelFormat(), frame.yPlane(), frame.yStride(),
|
||||
frame.uPlane(), frame.uStride(), frame.vPlane(), frame.vStride()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -202,7 +206,8 @@ public final class PlaybackEngine implements AutoCloseable {
|
||||
while ((frame = videoDecoder.receive()) != null) {
|
||||
videoQueue.add(new VideoFrame(
|
||||
assignVideoPts(), frame.width(), frame.height(),
|
||||
frame.pixelFormat(), frame.yPlane(), frame.yStride()));
|
||||
frame.pixelFormat(), frame.yPlane(), frame.yStride(),
|
||||
frame.uPlane(), frame.uStride(), frame.vPlane(), frame.vStride()));
|
||||
}
|
||||
|
||||
if (audioDecoder != null) {
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
package dev.quoije.umd.media;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class YuvConverterTest {
|
||||
|
||||
@Test
|
||||
void convertsKnownColours() {
|
||||
// 2x2 frame; Y=255 everywhere (white), U=V=128 (no chroma) -> white.
|
||||
byte[] y = new byte[4];
|
||||
byte[] u = new byte[]{(byte) 128};
|
||||
byte[] v = new byte[]{(byte) 128};
|
||||
java.util.Arrays.fill(y, (byte) 255);
|
||||
|
||||
int[] argb = YuvConverter.yuvj420ToArgb(2, 2, y, 2, u, 1, v, 1);
|
||||
assertThat(argb).containsExactly(0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF);
|
||||
|
||||
// Y=0, U=V=128 -> black.
|
||||
java.util.Arrays.fill(y, (byte) 0);
|
||||
argb = YuvConverter.yuvj420ToArgb(2, 2, y, 2, u, 1, v, 1);
|
||||
assertThat(argb).containsExactly(0xFF000000, 0xFF000000, 0xFF000000, 0xFF000000);
|
||||
}
|
||||
|
||||
@Test
|
||||
void respectsChroma() {
|
||||
// Y=0, U=128, V=255 -> strong red (V - 128 = 127 => r = 0 + 1.402*127 ~ 178).
|
||||
byte[] y = new byte[]{0, 0, 0, 0};
|
||||
byte[] u = new byte[]{-128}; // 128
|
||||
byte[] v = new byte[]{-1}; // 255
|
||||
|
||||
int[] argb = YuvConverter.yuvj420ToArgb(2, 2, y, 2, u, 1, v, 1);
|
||||
int r = (argb[0] >> 16) & 0xFF;
|
||||
int g = (argb[0] >> 8) & 0xFF;
|
||||
int b = argb[0] & 0xFF;
|
||||
assertThat(r).isGreaterThan(150);
|
||||
assertThat(b).isEqualTo(0);
|
||||
assertThat(g).isLessThanOrEqualTo(0);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user