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 width = frame.width();
|
||||||
int height = frame.height();
|
int height = frame.height();
|
||||||
BytePointer y = frame.data(0);
|
int chromaWidth = (width + 1) / 2;
|
||||||
int stride = frame.linesize(0);
|
int chromaHeight = (height + 1) / 2;
|
||||||
byte[] yPlane = new byte[height * stride];
|
|
||||||
y.position(0).get(yPlane);
|
|
||||||
|
|
||||||
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. */
|
/** A decoded video frame (planar YUV 4:2:0). */
|
||||||
public record Frame(int width, int height, int pixelFormat, byte[] yPlane, int yStride) {
|
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. */
|
/** 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;
|
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. */
|
/** 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. */
|
/** 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) {
|
while ((frame = videoDecoder.receive()) != null) {
|
||||||
videoQueue.add(new VideoFrame(
|
videoQueue.add(new VideoFrame(
|
||||||
assignVideoPts(), frame.width(), frame.height(),
|
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) {
|
while ((frame = videoDecoder.receive()) != null) {
|
||||||
videoQueue.add(new VideoFrame(
|
videoQueue.add(new VideoFrame(
|
||||||
assignVideoPts(), frame.width(), frame.height(),
|
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) {
|
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